From fea59ed965c08ef1127225ac9c5cb1fd949bc637 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 22 Jan 2025 13:46:32 +0100 Subject: [PATCH 001/277] Update requirements --- requirements.txt | 8 ++++---- simulaqron/toolbox/stabilizer_states.py | 2 +- test_requirements.txt | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 74dd0efc..7358ab3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy>=1.14.0,<2.0.0 scipy>=1.1.0,<2.0.0 -twisted>=20.3.0,<21.0.0 -networkx>=2.2,<3.0 -click>=7.0,<8.0 +twisted>=24.0.0 +networkx>=3.0,<4.0 +click>=8.0,<9.0 daemons>=1.3.0,<2.0.0 -netqasm>=0.8.2,<0.9.0 +netqasm>=1.0.0 diff --git a/simulaqron/toolbox/stabilizer_states.py b/simulaqron/toolbox/stabilizer_states.py index 83e3233d..0ab5d881 100644 --- a/simulaqron/toolbox/stabilizer_states.py +++ b/simulaqron/toolbox/stabilizer_states.py @@ -844,7 +844,7 @@ def find_SQC_equiv_graph_state(self, return_operations=False): # Spp is now in the form of (I,Gamma) where Gamma is the adj mat of the Graph # SQC equivalent to the stabilizer state. adj_mat = Spp.to_array()[:, n : 2 * n] - G = nx.from_numpy_matrix(adj_mat) + G = nx.from_numpy_array(adj_mat) if return_operations: return G, operations diff --git a/test_requirements.txt b/test_requirements.txt index 822eca32..a022224d 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1 +1,2 @@ flake8>=3.6.0,<4.0.0 +pytest >=7.1, <8.0 From ed7a9c345a6aed9f2150fefaaadcc63f03a76911 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 22 Jan 2025 13:46:49 +0100 Subject: [PATCH 002/277] Update github workflows definition to use python 3.10 --- .github/workflows/actions.yaml | 7 ++++--- .github/workflows/netqasm.yaml | 2 +- requirements.txt | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/actions.yaml b/.github/workflows/actions.yaml index 78e913c7..d1d2ac45 100644 --- a/.github/workflows/actions.yaml +++ b/.github/workflows/actions.yaml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@master - uses: actions/setup-python@master with: - python-version: 3.8 + python-version: "3.12" - run: | make install make lint @@ -22,9 +22,10 @@ jobs: - uses: actions/checkout@master - uses: actions/setup-python@master with: - python-version: 3.8 + python-version: "3.12" - run: | make install + make test-deps make tests examples: @@ -34,7 +35,7 @@ jobs: - uses: actions/checkout@master - uses: actions/setup-python@master with: - python-version: 3.8 + python-version: "3.12" - run: | make install make examples diff --git a/.github/workflows/netqasm.yaml b/.github/workflows/netqasm.yaml index 07c2b0fc..1862b77a 100644 --- a/.github/workflows/netqasm.yaml +++ b/.github/workflows/netqasm.yaml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@master - uses: actions/setup-python@master with: - python-version: 3.8 + python-version: "3.12" - name: Clone and install netqasm run: | git clone https://github.com/QuTech-Delft/netqasm.git diff --git a/requirements.txt b/requirements.txt index 7358ab3a..e0dd2f78 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ twisted>=24.0.0 networkx>=3.0,<4.0 click>=8.0,<9.0 daemons>=1.3.0,<2.0.0 -netqasm>=1.0.0 +netqasm>=1.0.0,<2.0.0 From 2c2e81763b402ee5f3561bde74ebc698b270739d Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 15 Jul 2025 16:39:14 +0200 Subject: [PATCH 003/277] Correctly install pybind and projectq when running examples using netqasm backend --- .github/workflows/netqasm.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/netqasm.yaml b/.github/workflows/netqasm.yaml index 1862b77a..a480ac41 100644 --- a/.github/workflows/netqasm.yaml +++ b/.github/workflows/netqasm.yaml @@ -19,8 +19,10 @@ jobs: cd .. - name: Install simulaqron run: make install + - name: Install pybind + run: pip install pybind11 - name: Install projectq - run: pip install -Iv projectq==0.5.1 + run: pip install -Iv projectq==0.8.0 - name: Run examples env: NETQASM_SIMULATOR: simulaqron From 118d438658dab655c807c89ac7c464fb75ef9c82 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 15 Jul 2025 16:46:43 +0200 Subject: [PATCH 004/277] Update flake8 version to make linter work again --- test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requirements.txt b/test_requirements.txt index a022224d..9bbf12b5 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,2 +1,2 @@ -flake8>=3.6.0,<4.0.0 +flake8>=5.0.0,<6.0.0 pytest >=7.1, <8.0 From 448780bece4634ddd17b5285b99a010dbaa21ca7 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 15 Jul 2025 17:49:58 +0200 Subject: [PATCH 005/277] Update to use TOML-based setup instead of old-fashioned setup.py --- .bumpversion.cfg | 8 ----- Makefile | 6 ++-- optional-requirements.txt | 2 -- pyproject.toml | 68 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 7 ---- setup.py | 53 ------------------------------ simulaqron/__init__.py | 2 -- simulaqron/simulaqron.py | 3 +- test_requirements.txt | 2 -- 9 files changed, 73 insertions(+), 78 deletions(-) delete mode 100644 .bumpversion.cfg delete mode 100644 optional-requirements.txt create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py delete mode 100644 test_requirements.txt diff --git a/.bumpversion.cfg b/.bumpversion.cfg deleted file mode 100644 index f8834d0d..00000000 --- a/.bumpversion.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[bumpversion] -current_version = 4.0.0 -commit = True -tag = False - -[bumpversion:file:simulaqron/__init__.py] - -[bumpversion:file:README.md] diff --git a/Makefile b/Makefile index 88d9f2d1..a2f4cae5 100644 --- a/Makefile +++ b/Makefile @@ -17,13 +17,13 @@ lint: @${PYTHON} -m flake8 ${SIMULAQRON_DIR} ${EXAMPLES_DIR} ${TEST_DIR} test-deps: - @${PYTHON} -m pip install -r test_requirements.txt + @${PYTHON} -m pip install .\[test\] requirements python-deps: - @cat requirements.txt | xargs -n 1 -L 1 $(PIP) install + @${PYTHON} -m pip install . install-optional: install - @cat optional-requirements.txt | xargs -n 1 -L 1 $(PIP) install + @${PYTHON} -m pip install .\[opt\] _reset: @${PYTHON} ${RESET_FILE} diff --git a/optional-requirements.txt b/optional-requirements.txt deleted file mode 100644 index 7166df91..00000000 --- a/optional-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -projectq -qutip diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..a4359d31 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,68 @@ +[build-system] +requires = ["setuptools >= 77.0.3", "wheel", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +local_scheme = "no-local-version" # exclude "+" from version name so we can upload to Test PyPI + +[project] +name = "simulaqron" +dynamic = ["version"] +dependencies = [ + "numpy>=1.14.0,<2.0.0", + "scipy>=1.1.0,<2.0.0", + "twisted>=24.0.0", + "networkx>=3.0,<4.0", + "click>=8.0,<9.0", + "daemons>=1.3.0,<2.0.0", + "netqasm>=1.0.0,<2.0.0" +] +requires-python = ">=3.8,<3.13" +authors = [ + {name = "Axel Dahlberg", email = "e.a.dahlberg@tudelft.nl"} +] +maintainers = [ + {name = "Diego Rivera", email = "d.a.g.a.rivera@tudelft.nl"} +] +description = "A simulator for developing Quantum Internet software" +readme = "README.md" +license = "MIT" +license-files = ["LICENCE"] +keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] +classifiers = [ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: Unix", + "Operating System :: MacOS" +] + +[project.optional-dependencies] +test = [ + "flake8>=5.0.0,<6.0.0", + "pytest >=7.1, <8.0" +] +opt = [ + "projectq<=0.8.0", + "qutip" +] +dev = [ + "sphinx>=7.2.6, <8.0", + "sphinx-rtd-theme", + "build", + "twine", + "pylint", + "pyink", + "pytest", + "mypy", +] + +[tool.setuptools.packages.find] +exclude = ["tests", "docs", "examples"] + +[project.scripts] +simulaqron = "simulaqron.simulaqron:cli" + +[project.urls] +Homepage = "https://github.com/SoftwareQuTech/SimulaQron" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e0dd2f78..00000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -numpy>=1.14.0,<2.0.0 -scipy>=1.1.0,<2.0.0 -twisted>=24.0.0 -networkx>=3.0,<4.0 -click>=8.0,<9.0 -daemons>=1.3.0,<2.0.0 -netqasm>=1.0.0,<2.0.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 6958b1b3..00000000 --- a/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -import setuptools -import os - -path_to_here = os.path.dirname(os.path.abspath(__file__)) -simulaqron_init = os.path.join(path_to_here, "simulaqron", "__init__.py") - -with open(simulaqron_init, 'r') as f: - for line in f: - line = line.strip() - if line.startswith("__version__"): - version = line.split("__version__ = ")[1] - version = version.split(' ')[0] - version = eval(version) - break - else: - raise RuntimeError("Could not find the version!") - -with open("README.md", 'r') as f: - long_description = f.read() - -with open("requirements.txt", 'r') as f: - install_requires = [line.strip() for line in f.readlines()] - -setuptools.setup( - name="simulaqron", - version=version, - author="Axel Dahlberg", - author_email="e.a.dahlberg@tudelft.nl", - description="A simulator for developing Quantum Internet software", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/SoftwareQuTech/SimulaQron", - include_package_data=True, - packages=setuptools.find_packages(exclude=('tests', 'docs', 'examples')), - package_data={ - 'simulaqron': ['config/.keep', '.simulaqron_pids/.keep'] - }, - install_requires=install_requires, - python_requires='>=3.5', - classifiers=[ - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "License :: OSI Approved :: MIT License", - "Operating System :: Unix", - "Operating System :: MacOS" - ], - entry_points=''' - [console_scripts] - simulaqron=simulaqron.simulaqron:cli - ''' -) diff --git a/simulaqron/__init__.py b/simulaqron/__init__.py index ff67685a..3d2ca820 100644 --- a/simulaqron/__init__.py +++ b/simulaqron/__init__.py @@ -1,6 +1,4 @@ from simulaqron.toolbox.reset import check_config_files -__version__ = '4.0.0' - # Create default config files if there are none check_config_files() diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index eab26efc..45846e12 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -71,13 +71,14 @@ def cli(): ########### # version # ########### +import importlib.metadata as metadata @cli.command() def version(): """ Prints the version of simulqron. """ - print(simulaqron.__version__) + print(metadata.version('simulaqron')) ################# diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index 9bbf12b5..00000000 --- a/test_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -flake8>=5.0.0,<6.0.0 -pytest >=7.1, <8.0 From 609d06adc53882f85958d6332cd2b1e8e6f2e719 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 15 Jul 2025 17:50:23 +0200 Subject: [PATCH 006/277] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 539c9bb0..fa7371a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ For more details refer to the [documentation](https://softwarequtech.github.io/S Upcoming -------- +2025-07-15 (v4.0.1) +------------------- +- Migrated project specification to use modern TOML approach. +- Tested working with Python versions 3.10, 3.11 and 3.12. 2021-11-18 (v4.0.0) ------------------- From 15b8b04e0335e0c01e69761db20852784332b07c Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 16 Jul 2025 10:26:31 +0200 Subject: [PATCH 007/277] Linter changes --- simulaqron/simulaqron.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 45846e12..59c46c07 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -5,8 +5,8 @@ import logging from daemons.prefab import run from daemons.interfaces import exit +import importlib.metadata as metadata -import simulaqron from simulaqron.network import Network from simulaqron.settings import simulaqron_settings, SimBackend from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor @@ -71,7 +71,6 @@ def cli(): ########### # version # ########### -import importlib.metadata as metadata @cli.command() def version(): From f4dc29a1ffa8059c4d94a473cb055121c1506c53 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 16 Jul 2025 14:20:00 +0200 Subject: [PATCH 008/277] Make logging compatible with modern standards --- .gitignore | 1 + examples/nativeMode/corrRNG/aliceTest.py | 6 +- examples/nativeMode/corrRNG/bobTest.py | 20 +---- examples/nativeMode/extendGHZ/aliceTest.py | 8 +- examples/nativeMode/extendGHZ/bobTest.py | 6 +- examples/nativeMode/extendGHZ/charlieTest.py | 6 +- examples/nativeMode/graphState/aliceTest.py | 6 +- examples/nativeMode/graphState/bobTest.py | 6 +- examples/nativeMode/graphState/charlieTest.py | 6 +- examples/nativeMode/graphState/davidTest.py | 6 +- examples/nativeMode/teleport/aliceTest.py | 6 +- examples/nativeMode/teleport/bobTest.py | 8 +- examples/nativeMode/template/nodeTest.py | 6 +- pyproject.toml | 4 +- simulaqron/local/setup.py | 4 +- simulaqron/netqasm_backend/executioner.py | 44 +++++----- simulaqron/netqasm_backend/factory.py | 9 +- simulaqron/netqasm_backend/qnodeos.py | 2 +- simulaqron/network.py | 6 +- simulaqron/run/run.py | 2 +- simulaqron/sdk/connection.py | 27 +++--- simulaqron/sdk/socket.py | 18 ++-- simulaqron/simulaqron.py | 10 +-- simulaqron/start/start_qnodeos.py | 7 +- simulaqron/virtual_node/quantum.py | 8 +- simulaqron/virtual_node/virtual.py | 83 ++++++++++--------- 26 files changed, 154 insertions(+), 161 deletions(-) diff --git a/.gitignore b/.gitignore index 22e570f8..eaac5440 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.pyc .*.sw? *.DS_Store +*.whl /docs/build/* /build/* /dist/* diff --git a/examples/nativeMode/corrRNG/aliceTest.py b/examples/nativeMode/corrRNG/aliceTest.py index c4005868..76be397c 100644 --- a/examples/nativeMode/corrRNG/aliceTest.py +++ b/examples/nativeMode/corrRNG/aliceTest.py @@ -30,7 +30,7 @@ import logging from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks from twisted.internet import reactor @@ -125,8 +125,8 @@ def main(): classicalFile = "classicalNet.cfg" # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/corrRNG/bobTest.py b/examples/nativeMode/corrRNG/bobTest.py index 2f3e8f57..42d27f7a 100644 --- a/examples/nativeMode/corrRNG/bobTest.py +++ b/examples/nativeMode/corrRNG/bobTest.py @@ -31,13 +31,11 @@ import logging from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks from twisted.spread import pb -from qutip import Qobj - ##################################################################################################### # @@ -105,18 +103,6 @@ def remote_process_qubit(self, virtualNum): print("BOB: My Random Number is ", x, "\n") - def assemble_qubit(self, realM, imagM): - """ - Reconstitute the qubit as a qutip object from its real and imaginary components given as a list. - We need this since Twisted PB does not support sending complex valued object natively. - """ - M = realM - for s in range(len(M)): - for t in range(len(M)): - M[s][t] = realM[s][t] + 1j * imagM[s][t] - - return Qobj(M) - ##################################################################################################### # @@ -134,8 +120,8 @@ def main(): classicalFile = "classicalNet.cfg" # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/extendGHZ/aliceTest.py b/examples/nativeMode/extendGHZ/aliceTest.py index f9c96441..71d23628 100644 --- a/examples/nativeMode/extendGHZ/aliceTest.py +++ b/examples/nativeMode/extendGHZ/aliceTest.py @@ -31,7 +31,7 @@ import logging from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -71,7 +71,7 @@ def runClientNode(qReg, virtRoot, myName, classicalNet): # Send qubit B to Bob # Instruct the virtual node to transfer the qubit remoteNum = yield virtRoot.callRemote("send_qubit", qB, "Bob") - logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) + logging.debug("LOCAL {}: Remote qubit is %d.", myName, remoteNum) # Tell Bob the number of the virtual qubit so the can use it locally # and extend it to a GHZ state with Charlie @@ -127,8 +127,8 @@ def main(): classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/extendGHZ/bobTest.py b/examples/nativeMode/extendGHZ/bobTest.py index 969ebece..98e176e5 100644 --- a/examples/nativeMode/extendGHZ/bobTest.py +++ b/examples/nativeMode/extendGHZ/bobTest.py @@ -31,7 +31,7 @@ import logging from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -134,8 +134,8 @@ def main(): classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/extendGHZ/charlieTest.py b/examples/nativeMode/extendGHZ/charlieTest.py index ab15d5a5..c6a8b679 100644 --- a/examples/nativeMode/extendGHZ/charlieTest.py +++ b/examples/nativeMode/extendGHZ/charlieTest.py @@ -31,7 +31,7 @@ import logging from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -120,8 +120,8 @@ def main(): classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/graphState/aliceTest.py b/examples/nativeMode/graphState/aliceTest.py index 27b47a5c..946a8ea4 100644 --- a/examples/nativeMode/graphState/aliceTest.py +++ b/examples/nativeMode/graphState/aliceTest.py @@ -32,7 +32,7 @@ import logging from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -144,8 +144,8 @@ def main(): classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/graphState/bobTest.py b/examples/nativeMode/graphState/bobTest.py index ab82ca70..74291d3a 100644 --- a/examples/nativeMode/graphState/bobTest.py +++ b/examples/nativeMode/graphState/bobTest.py @@ -33,7 +33,7 @@ import numpy as np from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -143,8 +143,8 @@ def main(): classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/graphState/charlieTest.py b/examples/nativeMode/graphState/charlieTest.py index c7a02292..a7c98794 100644 --- a/examples/nativeMode/graphState/charlieTest.py +++ b/examples/nativeMode/graphState/charlieTest.py @@ -33,7 +33,7 @@ import numpy as np from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -174,8 +174,8 @@ def main(): classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/graphState/davidTest.py b/examples/nativeMode/graphState/davidTest.py index 345f1e89..8496266e 100644 --- a/examples/nativeMode/graphState/davidTest.py +++ b/examples/nativeMode/graphState/davidTest.py @@ -33,7 +33,7 @@ import numpy as np from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -149,8 +149,8 @@ def main(): classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/teleport/aliceTest.py b/examples/nativeMode/teleport/aliceTest.py index ca12f34a..0f9b2ae0 100644 --- a/examples/nativeMode/teleport/aliceTest.py +++ b/examples/nativeMode/teleport/aliceTest.py @@ -31,7 +31,7 @@ import numpy as np from simulaqron.local.setup import setup_local, assemble_qubit -from simulaqron.general.host_config import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.settings import simulaqron_settings from twisted.internet.defer import inlineCallbacks @@ -154,8 +154,8 @@ def main(): classicalFile = "classicalNet.cfg" # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index 3ed10d4f..9dfef805 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -32,9 +32,9 @@ import numpy as np from simulaqron.local.setup import setup_local, assemble_qubit -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings -from simulaqron.toolbox.stabilizerStates import StabilizerState +from simulaqron.toolbox.stabilizer_states import StabilizerState from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -143,8 +143,8 @@ def main(): classicalFile = "classicalNet.cfg" # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/template/nodeTest.py b/examples/nativeMode/template/nodeTest.py index ca4d115e..45e91352 100644 --- a/examples/nativeMode/template/nodeTest.py +++ b/examples/nativeMode/template/nodeTest.py @@ -31,7 +31,7 @@ import logging from simulaqron.local.setup import setup_local -from simulaqron.general.hostConfig import socketsConfig +from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings from twisted.spread import pb from twisted.internet import reactor @@ -137,8 +137,8 @@ def main(): classicalFile = "./classicalNet.cfg" # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = socketsConfig(network_file) - classicalNet = socketsConfig(classicalFile) + virtualNet = SocketsConfig(network_file) + classicalNet = SocketsConfig(classicalFile) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/pyproject.toml b/pyproject.toml index a4359d31..5c95cb86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ classifiers = [ [project.optional-dependencies] test = [ "flake8>=5.0.0,<6.0.0", - "pytest >=7.1, <8.0" + "pytest>=7.1, <8.0" ] opt = [ "projectq<=0.8.0", @@ -54,7 +54,7 @@ dev = [ "twine", "pylint", "pyink", - "pytest", + "pytest>=7.1, <8.0", "mypy", ] diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index 9443ddb8..0ca757a1 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -78,7 +78,7 @@ def setup_local(myName, virtualNet, classicalNet, lNode, func, *args, **kwargs): nb.factory = pb.PBServerFactory(nb.root) reactor.listenTCP(nb.port, nb.factory) except Exception as e: - logging.error("LOCAL {}: Cannot start classical communication servers: {}".format(myName, e)) + logging.error("LOCAL %s: Cannot start classical communication servers: %s", myName, e) return # Give the server some time to start up @@ -167,7 +167,7 @@ def localError(reason): """ Error handling for the connection. """ - logging.error("Critical error: ", reason) + logging.error("Critical error: %s", reason) try: reactor.stop() except ReactorNotRunning: diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 8ecf77db..0f037fdc 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -114,7 +114,7 @@ def add_factory(self, factory): self._factory = factory def _handle_command_exception(self, exc, prog_counter, traceback_str): - self._logger.error(f"At line {prog_counter}: {exc}\n{traceback_str}") + self._logger.error("At line %d: %s\n%s", prog_counter, exc, traceback_str) self._return_msg(msg=ErrorMessage(err_code=ErrorCode.GENERAL)) def _return_msg(self, msg): @@ -143,7 +143,7 @@ def cmd_new(self, physical_address): q_id = physical_address q = VirtualQubitRef(q_id, int(time.time()), virt) self.factory.qubitList[q_id] = q - self._logger.info(f"Requested new physical qubit {q_id})") + self._logger.info("Requested new physical qubit %d)", q_id) finally: self.factory._lock.release() @@ -181,7 +181,7 @@ def apply_rotation(self, axis, angle, qubit_id): Apply a rotation of the qubit specified in cmd with an angle specified in xtra around the axis """ - self._logger.debug(f"Applying a rotation around {axis} to physical qubit id {qubit_id}") + self._logger.debug("Applying a rotation around %s to physical qubit id %d", axis, qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) yield call_method(virt_qubit, "apply_rotation", axis, angle) @@ -196,7 +196,7 @@ def _do_two_qubit_instr(self, instr, subroutine_id, address1, address2): @inlineCallbacks def apply_two_qubit_gate(self, gate, qubit_id1, qubit_id2): - self._logger.debug(f"Applying {gate} to physical qubit id {qubit_id1} target {qubit_id2}") + self._logger.debug("Applying %s to physical qubit id %d target %d", gate, qubit_id1, qubit_id2) control = self.get_virt_qubit(qubit_id=qubit_id1) target = self.get_virt_qubit(qubit_id=qubit_id2) if control == target: @@ -249,12 +249,12 @@ def cmd_measure(self, qubit_id, inplace=True): """ Measure """ - self._logger.debug(f"Measuring physical qubit id {qubit_id}") + self._logger.debug("Measuring physical qubit id %d", qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) outcome = yield call_method(virt_qubit, "measure", inplace) if outcome is None: raise RuntimeError("Measurement failed") - self._logger.debug(f"Measured outcome {outcome}") + self._logger.debug("Measured outcome %s", outcome) return outcome @inlineCallbacks @@ -262,7 +262,7 @@ def cmd_reset(self, qubit_id, correct=True): r""" Reset Qubit to \|0\> """ - self._logger.debug(f"Reset physical qubit id {qubit_id}") + self._logger.debug("Reset physical qubit id %d", qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) outcome = yield call_method(virt_qubit, "measure", inplace=True) @@ -277,13 +277,13 @@ def _do_wait(self, delay=0.1): def _update_shared_memory(self, app_id, entry, value): if isinstance(entry, operand.Register): - self._logger.debug(f"Updating host about register {entry} with value {value}") + self._logger.debug("Updating host about register %s with value %s", entry, value) self._return_msg(msg=ReturnRegMessage( register=entry.cstruct, value=value, )) elif isinstance(entry, operand.Address): - self._logger.debug(f"Updating host about array {entry} with value {value}") + self._logger.debug("Updating host about array %s with value %s", entry, value) address = entry.address self._return_msg(msg=ReturnArrayMessage( address=address, @@ -395,7 +395,7 @@ def cmd_epr( else: raise ValueError(f"Unknown node with ID {remote_node_id}") - self._logger.debug(f"Creating EPR with {remote_node_name} on socket {epr_socket_id}") + self._logger.debug("Creating EPR with %s on socket %s", remote_node_name, epr_socket_id) # Check so that it is not the same node if self.name == remote_node_name: @@ -661,7 +661,7 @@ def send_epr_half( remote_ent_info, ) - self._logger.debug(f"Sent half a EPR pair as qubit id {qubit_id} to {remote_node_name}") + self._logger.debug("Sent half a EPR pair as qubit id %d to %s", qubit_id, remote_node_name) # Remove from active mapped qubits self.remove_qubit_id(qubit_id=qubit_id) @@ -706,7 +706,7 @@ def send_epr_outcome_half( remote_ent_info, ) - self._logger.debug(f"Sent half a measure direclty EPR pair to {remote_node_name}") + self._logger.debug("Sent half a measure direclty EPR pair to %s", remote_node_name) @staticmethod def _unpack_ent_info(raw_ent_info): @@ -734,7 +734,7 @@ def cmd_epr_recv(self, epr_socket_id, qubit_id=None): """ Receive half of epr from another node. Block until qubit is received. """ - self._logger.debug(f"Asking to receive for EPR socket ID {epr_socket_id}") + self._logger.debug("Asking to receive for EPR socket ID %d", epr_socket_id) # This will block until a qubit is received. no_gen = True @@ -758,8 +758,9 @@ def cmd_epr_recv(self, epr_socket_id, qubit_id=None): if isinstance(ent_info, LinkLayerOKTypeK): self._logger.debug( - f"Qubit received for EPR socket ID {epr_socket_id}, " - f"will use {qubit_id} as physical qubit ID" + "Qubit received for EPR socket ID %d, " + "will use %d as physical qubit ID", + epr_socket_id, qubit_id ) # Once we have the qubit, add it to the local list and send a reply we received it. Note that we will @@ -776,13 +777,16 @@ def cmd_epr_recv(self, epr_socket_id, qubit_id=None): self.factory._lock.release() elif isinstance(ent_info, LinkLayerOKTypeM): self._logger.debug( - f"Measure directly EPR request received for EPR socket ID {epr_socket_id}." + "Measure directly EPR request received for EPR socket ID %d.", + epr_socket_id ) self._handle_epr_response(response=ent_info) def remove_qubit_id(self, qubit_id): - self._logger.debug(f"Removing physical qubit with ID {qubit_id} from handles to simulated qubits") + self._logger.debug("Removing physical qubit with ID %d from handles to simulated qubits", + qubit_id + ) self.factory.qubitList.pop(qubit_id) def _get_purpose_id(self, remote_node_id, epr_socket_id): @@ -797,14 +801,16 @@ def _wait_to_handle_epr_responses(self): def _print_error(self, scope, failure): traceback_str = ''.join(traceback.format_tb(failure.__traceback__)) - self._logger.error(f"{scope} failed with error failure {failure}\n traceback: {traceback_str}") + self._logger.error("%s failed with error failure %s\n traceback: %s", + scope, failure, traceback_str + ) def _reserve_physical_qubit(self, physical_address): # NOTE does not do anything, done by cmd_new instead pass def _clear_phys_qubit_in_memory(self, physical_address): - self._logger.debug(f"clearing phys qubit {physical_address}") + self._logger.debug("clearing phys qubit %s", physical_address) yield self.cmd_measure(qubit_id=physical_address, inplace=False) self.remove_qubit_id(qubit_id=physical_address) diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index fdb87dea..75cbfb62 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -108,11 +108,11 @@ def dataReceived(self, data): d.addErrback(self.log_error) def log_handled_message(self, result): - self._logger.info(f"Finished handling message with result = {result}") + self._logger.info("Finished handling message with result = %s", result) @inlineCallbacks def log_error(self, failure): - self._logger.error(f"Handling message failed with failure = {failure}") + self._logger.error("Handling message failed with failure = %s", failure) sys.stderr.write(str(failure)) self._return_msg(msg=ErrorMessage(err_code=ErrorCode.GENERAL)) yield deferLater(reactor, 0.1, self.stop) @@ -136,8 +136,9 @@ def _handle_init_new_app(self, msg): app_id = msg.app_id self._add_app(app_id=app_id) max_qubits = msg.max_qubits - self._logger.debug(f"Allocating a new " - f"unit module of size {max_qubits} for application with app ID {app_id}.\n") + self._logger.debug("Allocating a new unit module" + "of size %d for application with app ID %d.\n", + max_qubits, app_id) self._executioner.init_new_application( app_id=app_id, max_qubits=max_qubits, diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index add74293..08f80e82 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -46,5 +46,5 @@ def stop(self): def _return_msg(self, msg): """Return a message to the host""" assert self._protocol is not None, "Seems protocol of handler has not yet been set" - self._logger.debug(f"sending message {msg} to host") + self._logger.debug("sending message %s to host", msg) self.protocol._return_msg(msg=bytes(msg)) diff --git a/simulaqron/network.py b/simulaqron/network.py index 279fd6fa..fbb414b3 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -141,7 +141,7 @@ def running(self): self._logger.exception("Got unexpected exception when trying to connect: {}".format(err)) raise err else: - self._logger.debug(f"Network {self.name} is now running") + self._logger.debug("Network %s is now running", self.name) self._running = True return self._running @@ -170,10 +170,10 @@ def start(self, wait_until_running=False): blog until the all processes are running and are connected or not. :param wait_until_running: bool """ - self._logger.info("Starting network with name {}".format(self.name)) + self._logger.info("Starting network with name %s", self.name) for p in self.processes: if not p.is_alive(): - self._logger.debug("Starting process {}".format(p.name)) + self._logger.debug("Starting process %s", p.name) p.deamon = True p.start() diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 22e39622..0bd5272b 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -65,7 +65,7 @@ def check_sim_backend(sim_backend): def run_sim_backend(node_names, sim_backend): - logger.debug(f"Starting simulaqron sim_backend process with nodes {node_names}") + logger.debug("Starting simulaqron sim_backend process with nodes %s", node_names) check_sim_backend(sim_backend=sim_backend) simulaqron_settings.sim_backend = sim_backend.value network = Network(name="default", nodes=node_names, force=True, new=True) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index eabf1ea8..869554de 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -148,7 +148,7 @@ def _setup_socket(name, addr, retry_time=0.1): while True: try: logger.debug( - f"App {name} : Trying to connect to NetQASM server (at {addr[-1]})" + "App %s : Trying to connect to NetQASM server (at %d)", name, addr[-1] ) qnodeos_socket = socket.socket(addr[0], addr[1], addr[2]) @@ -158,24 +158,21 @@ def _setup_socket(name, addr, retry_time=0.1): if retry_time is None or retry_time == 0: raise err logger.debug( - "App {} : Could not connect to NetQASM server, trying again...".format( - name - ) + "App %s : Could not connect to NetQASM server, trying again...", + name ) time.sleep(retry_time) qnodeos_socket.close() except Exception as err: logger.exception( - "App {} : Critical error when connection to NetQASM server: {}".format( - name, err - ) + "App %s : Critical error when connection to NetQASM server: %s", + name, err ) qnodeos_socket.close() raise err logger.debug( - "App {} : Could not connect to NetQASM server, trying again...".format( - name - ) + "App %s : Could not connect to NetQASM server, trying again...", + name ) return qnodeos_socket @@ -202,7 +199,7 @@ def _wait_for_done(self, msg_id=None): if msg_id is None: self._logger.debug("Waiting for any msg to be done") else: - self._logger.debug(f"Waiting for msg ID {msg_id}") + self._logger.debug("Waiting for msg ID %d", msg_id) while True: done_msg_id = self._handle_reply() if msg_id is None: @@ -215,7 +212,7 @@ def _wait_for_done(self, msg_id=None): # Other message done, not the one we're waiting for # Wait for another don continue - self._logger.debug(f"Received done for msg ID {done_msg_id}") + self._logger.debug("Received done for msg ID %d", done_msg_id) def _read_more_data(self): """Reads in some more data on the socket to qnodeos""" @@ -224,7 +221,7 @@ def _read_more_data(self): self.buf += data else: self.buf = data - self._logger.debug(f"Got new data {data} on socket to qnodeos") + self._logger.debug("Got new data %s on socket to qnodeos", data) def _handle_reply(self): """Handle all next replies until a done message and return the msg ID for the done""" @@ -241,7 +238,7 @@ def _handle_reply(self): # Remove the data of this message from the buffer self.buf = self.buf[len(ret_msg):] - self._logger.debug(f"Got message {ret_msg}") + self._logger.debug("Got message %s", ret_msg) if isinstance(ret_msg, MsgDoneMessage): self._waiting_msg_ids.remove(ret_msg.msg_id) self._done_msg_ids.add(ret_msg.msg_id) @@ -266,7 +263,7 @@ def _handle_reply(self): def block(self): while len(self._waiting_msg_ids) > 0: self._logger.debug( - f"Blocking and waiting for msg IDs {self._waiting_msg_ids}" + "Blocking and waiting for msg IDs %s", self._waiting_msg_ids ) # Wait for any msg to be done self._wait_for_done() diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index f133c529..01a13e55 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -41,12 +41,12 @@ def __del__(self): def send(self, msg): """Sends a message to the remote node.""" - self._logger.debug(f"Sending msg '{msg}'") + self._logger.debug("Sending msg '%s'", msg) raw_msg = self._serialize_msg(msg=msg) self._app_socket.send(raw_msg) def send_structured(self, msg): - self._logger.debug(f"Sending structured msg '{msg}'") + self._logger.debug("Sending structured msg '%s'", msg) raw_msg = self._serialize_structured_msg(msg=msg) self._app_socket.send(raw_msg) @@ -61,7 +61,7 @@ def recv(self, block=True, maxsize=1024): if not block and not raw_msg: raise RuntimeError("No message to receive (not blocking)") msg = self._deserialize_msg(raw_msg=raw_msg) - self._logger.debug(f"Msg '{msg}' received") + self._logger.debug("Msg '%s' received", msg) return msg def recv_structured(self, block=True, maxsize=1024): @@ -71,7 +71,7 @@ def recv_structured(self, block=True, maxsize=1024): if not block and not raw_msg: raise RuntimeError("No message to receive (not blocking)") msg = self._deserialize_structured_msg(raw_msg=raw_msg) - self._logger.debug(f"Msg '{msg}' received") + self._logger.debug("Msg '%s' received", msg) return msg def recv_silent(self): @@ -113,8 +113,10 @@ def _connect(self): try: app_socket.bind(addr[4]) except OSError as err: - self._logger.debug(f"Could not bind socket since: {err}\n" - f"Trying again in {self.RETRY_TIME}s...") + self._logger.debug( + "Could not bind socket since: %s\nTrying again in %ds...", + err, self.RETRY_TIME + ) time.sleep(self.RETRY_TIME) else: break @@ -128,8 +130,8 @@ def _connect(self): app_socket.connect(addr[4]) except ConnectionRefusedError: self._logger.debug( - f"Could not open application socket, " - f"trying again in {self.RETRY_TIME}s..." + "Could not open application socket, trying again in %d s...", + self.RETRY_TIME ) time.sleep(self.RETRY_TIME) else: diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 59c46c07..122e3298 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -133,10 +133,10 @@ def start(name, nrnodes, nodes, topology, force, keep): new = not keep if name is None: name = "default" - pidfile = os.path.join(PID_FOLDER, "simulaqron_network_{}.pid".format(name)) + pidfile = os.path.join(PID_FOLDER, f"simulaqron_network_{name}.pid") if os.path.exists(pidfile): - logging.warning("Network with name {} is already running".format(name)) - logging.warning("The pidfile for this network is located at {}".format(pidfile)) + logging.warning("Network with name %s is already running", name) + logging.warning("The pidfile for this network is located at %s", pidfile) return if new: if not force: @@ -172,9 +172,9 @@ def stop(name): """Stops a network.""" if name is None: name = "default" - pidfile = os.path.join(PID_FOLDER, "simulaqron_network_{}.pid".format(name)) + pidfile = os.path.join(PID_FOLDER, f"simulaqron_network_{name}.pid") if not os.path.exists(pidfile): - logging.warning("Network with name {} is not running".format(name)) + logging.warning("Network with name %s is not running", name) return d = SimulaQronDaemon(pidfile=pidfile) d.stop() diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index ef326a4e..76e0db3a 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -89,14 +89,13 @@ def setup_netqasm_server(myName, netqasm_factory): break except CannotListenError: logger.error( - "LOCAL {}: NetQASM server address ({}) is already in use, trying again.".format( - myName, myHost.port - ) + "LOCAL %s: NetQASM server address (%d) is already in use, trying again.", + myName, myHost.port ) time.sleep(_RETRY_TIME) except Exception as e: logger.error( - "LOCAL {}: Critical error when starting NetQASM server: {}".format(myName, e) + "LOCAL %s: Critical error when starting NetQASM server: %s", myName, e ) reactor.stop() else: diff --git a/simulaqron/virtual_node/quantum.py b/simulaqron/virtual_node/quantum.py index e6c9b680..465ba7e2 100644 --- a/simulaqron/virtual_node/quantum.py +++ b/simulaqron/virtual_node/quantum.py @@ -87,11 +87,11 @@ def __init__(self, node, register, simNum, num=0): @inlineCallbacks def lock(self): - self._logger.debug(f"locking sim qubit in register with num {self.register.num}") + self._logger.debug("locking sim qubit in register with num %d", self.register.num) while self.isLocked(): yield deferLater(reactor, self._delay, lambda: None) yield self._lock.acquire() - self._logger.debug(f"got lock for sim qubit in register with num {self.register.num}") + self._logger.debug("got lock for sim qubit in register with num %d", self.register.num) @inlineCallbacks def remote_lock(self): @@ -99,10 +99,10 @@ def remote_lock(self): def unlock(self): try: - self._logger.debug(f"unlocking sim qubit in register with num {self.register.num}") + self._logger.debug("unlocking sim qubit in register with num %d", self.register.num) self._lock.release() except AssertionError as exc: - self._logger.error(f"AssertionError {exc}") + self._logger.error("AssertionError %s", exc) def remote_unlock(self): self.unlock() diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 66a61873..0b2d677e 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -106,10 +106,10 @@ def __init__(self, name, virtualFile, network_name="default"): self.config = SocketsConfig(virtualFile, network_name=network_name, config_type="vnode") self.myID = self.config.hostDict[name] except KeyError as e: - self._logger.error(f"No such name in the configuration file {virtualFile}: {e}") + self._logger.error("No such name in the configuration file %s: %s", virtualFile, e) raise e except Exception as e: - self._logger.error(f"Error reading the configuration file {virtualFile}: {e}") + self._logger.error("Error reading the configuration file %s: %s", virtualFile, e) raise e def start(self, maxQubits=simulaqron_settings.max_qubits, maxRegisters=simulaqron_settings.max_registers): @@ -121,17 +121,17 @@ def start(self, maxQubits=simulaqron_settings.max_qubits, maxRegisters=simulaqro """ try: - self._logger.debug(f"Starting on port {self.myID.port}") + self._logger.debug("Starting on port %d", self.myID.port) node = virtualNode(self.myID, self.config, maxQubits=maxQubits, maxRegisters=maxRegisters) reactor.listenTCP(self.myID.port, pb.PBServerFactory(node)) self._logger.debug("Running reactor") reactor.run() except CannotListenError: - self._logger.error(f"NetQASM server address ({self.myID.port}) is already in use.") + self._logger.error("NetQASM server address (%d) is already in use.", self.myID.port) return except Exception as e: - self._logger.error(f"Critical error when starting local virtual node server: {e}") + self._logger.error("Critical error when starting local virtual node server: %s", e) return @@ -229,7 +229,7 @@ def get_connection(self, name): if name in self.conn: return self.conn[name] else: - self._logger.debug(f"Connection to {name} not up yet, need to wait...") + self._logger.debug("Connection to %s not up yet, need to wait...", name) conn_to_return = yield deferLater( reactor, simulaqron_settings.conn_retry_time, @@ -242,7 +242,7 @@ def connect_to_node(self, node): """ Connects to other node. If node not up yet, waits for CONF_WAIT_TIME seconds. """ - self._logger.debug(f"Trying to connect to node {node.name}.") + self._logger.debug("Trying to connect to node %s.", node.name) node.factory = pb.PBClientFactory() reactor.connectTCP(node.hostname, node.port, node.factory) defer = node.factory.getRootObject() @@ -253,7 +253,7 @@ def handle_connection(self, obj, node): """ Callback obtaining twisted root object when connection to the node given by the node details 'node'. """ - self._logger.debug(f"New connection to {node.name}.") + self._logger.debug("New connection to %s.", node.name) # Retrieve the root object: virtualNode on the remote node.root = obj @@ -271,7 +271,7 @@ def handle_connection_error(self, reason, node): try: reason.raiseException() except ConnectionRefusedError: - self._logger.debug(f"Could not connect to {node.name}, trying again...") + self._logger.debug("Could not connect to %s, trying again...", node.name) reactor.callLater(simulaqron_settings.conn_retry_time, self.connect_to_node, node) except Exception as e: self._logger.error(e) @@ -515,7 +515,7 @@ def remote_netqasm_send_qubit(self, num, targetName, app_id, remote_app_id): app_id application asking to have this qubit delivered remote_app_id application ID to deliver the qubit to """ - self._logger.debug(f"request to send qubit {num} to {targetName}") + self._logger.debug("request to send qubit %d to %s", num, targetName) virtQubit = self.remote_get_virtual_ref(num) @@ -558,14 +558,14 @@ def remote_netqasm_add_recv_list(self, fromName, from_epr_socket_id, to_epr_sock new_virt_num, ) ) - self._logger.debug(f"Added a qubit on EPR socket ID {to_epr_socket_id} to recv list") + self._logger.debug("Added a qubit on EPR socket ID %d to recv list", to_epr_socket_id) def remote_netqasm_get_recv(self, to_epr_socket_id): """ Retrieve the next qubit with the given app ID form the received list. """ - self._logger.debug(f"Trying to retrieve qubit on EPR socket ID {to_epr_socket_id} from recv list") + self._logger.debug("Trying to retrieve qubit on EPR socket ID %d from recv list", to_epr_socket_id) # Get the list corresponding to the specified application ID if not (to_epr_socket_id in self.qubit_recv): return None @@ -579,7 +579,7 @@ def remote_netqasm_get_recv(self, to_epr_socket_id): if not qc: return None - self._logger.debug(f"Returning qubit on EPR socket ID {to_epr_socket_id} from recv list") + self._logger.debug("Returning qubit on EPR socket ID %d from recv list", to_epr_socket_id) return self.remote_get_virtual_ref(qc.virt_num) @inlineCallbacks @@ -629,7 +629,7 @@ def remote_netqasm_add_epr_list(self, fromName, from_epr_socket_id, to_epr_socke """ if not (to_epr_socket_id in self.qubit_recv_epr): - self._logger.debug(f"Creating epr list for EPR socket ID {to_epr_socket_id}") + self._logger.debug("Creating epr list for EPR socket ID %d", to_epr_socket_id) self.qubit_recv_epr[to_epr_socket_id] = deque([]) self.qubit_recv_epr[to_epr_socket_id].append( @@ -642,30 +642,30 @@ def remote_netqasm_add_epr_list(self, fromName, from_epr_socket_id, to_epr_socke rawEntInfo=rawEntInfo, ) ) - self._logger.debug(f"Added a qubit on EPR socket ID {to_epr_socket_id} to epr list") + self._logger.debug("Added a qubit on EPR socket ID %d to epr list", to_epr_socket_id) def remote_netqasm_get_epr_recv(self, to_epr_socket_id): """ Retrieve the next qubit (half of an EPR-pair) with the given app ID from the received list. """ - self._logger.debug(f"Trying to retrieve qubit on EPR socket ID {to_epr_socket_id} from epr list") + self._logger.debug("Trying to retrieve qubit on EPR socket ID %d from epr list", to_epr_socket_id) # Get the list corresponding to the specified application ID if not (to_epr_socket_id in self.qubit_recv_epr): - self._logger.debug(f"No epr list for EPR socket ID {to_epr_socket_id}") + self._logger.debug("No epr list for EPR socket ID %d", to_epr_socket_id) return None qQueue = self.qubit_recv_epr[to_epr_socket_id] if not qQueue: - self._logger.debug(f"Nothing in epr list for EPR socket ID {to_epr_socket_id}") + self._logger.debug("Nothing in epr list for EPR socket ID %d", to_epr_socket_id) return None # Retrieve the first element on that list (first in, first out) qc = qQueue.popleft() if not qc: - self._logger.debug(f"First element in epr list is empty for EPR socket ID {to_epr_socket_id}") + self._logger.debug("First element in epr list is empty for EPR socket ID %d", to_epr_socket_id) return None - self._logger.debug(f"Returning qubit on EPR socket ID {to_epr_socket_id} from epr list") + self._logger.debug("Returning qubit on EPR socket ID %d from epr list", to_epr_socket_id) return self.remote_get_virtual_ref(qc.virt_num), qc.rawEntInfo @inlineCallbacks @@ -678,7 +678,7 @@ def remote_send_qubit(self, qubit, targetName): qubit virtual qubit to be sent targetName target ndoe to place qubit at (host object) """ - self._logger.debug(f"Request to send qubit sim Num {qubit.num} to {targetName}.") + self._logger.debug("Request to send qubit sim Num %d to %s.", qubit.num, targetName) if qubit.active != 1: self._logger.debug("Attempt to manipulate qubit no longer at this node.") return @@ -686,7 +686,7 @@ def remote_send_qubit(self, qubit, targetName): # Lookup host id of node if not (targetName in self.config.hostDict): raise virtNetError( - f"Trying to get conncetion to virtual node {targetName}, but this is not in configuration file" + f"Trying to get connection to virtual node {targetName}, but this is not in configuration file" ) remoteNode = yield self.get_connection(targetName) @@ -704,7 +704,7 @@ def remote_send_qubit(self, qubit, targetName): except RemoteError as remote_err: self.reraise_remote_error(remote_err) else: - self._logger.debug(f"Sending qubit simulated remotely at {qubit.simNode.name}") + self._logger.debug("Sending qubit simulated remotely at %s", qubit.simNode.name) # Also lock the virtual node of the simulating node unless it is the remoteNode or this node locked_node = yield self._lock_simulating_node(exclude=[self.virtNode, remoteNode]) try: @@ -745,7 +745,7 @@ def remote_transfer_qubit(self, simQubitNum, targetName): simQubitNum simulated qubit number to be sent targetName target node to place qubit at (host object) """ - self._logger.debug(f"Request to transfer qubit to {targetName}.") + self._logger.debug("Request to transfer qubit to %s.", targetName) # Convert the number into the right local object simQubit = self._q_num_to_obj(simQubitNum) @@ -778,7 +778,7 @@ def remote_add_qubit(self, name, simQubit): simQubit simulated qubit reference in the backend we're adding """ - self._logger.debug(f"Request to add qubit from {name}.") + self._logger.debug("Request to add qubit from %s.", name) # Get the details of the remote node if not (name in self.config.hostDict): @@ -877,7 +877,7 @@ def _remove_sim_qubit(self, delQubit): q.num = q.num - 1 # Remove the qubit form the list of simulated qubits - self._logger.debug(f"removing qubit {delQubit.simNum} from {self.simQubits}") + self._logger.debug("removing qubit %d from %d", delQubit.simNum, self.simQubits) self.simQubits.remove(delQubit) finally: @@ -915,7 +915,7 @@ def local_merge_regs(self, qubit1, qubit2): qubit2 qubit2 in reg2 """ self._logger.debug( - f"Request to merge local register for qubits simNum {qubit1.simNum} and simNum {qubit2.simNum}." + "Request to merge local register for qubits simNum %d and simNum %d.", qubit1.simNum, qubit2.simNum ) # This should only be called if locks are acquired @@ -947,7 +947,7 @@ def local_merge_regs(self, qubit1, qubit2): # Update the simulated qubit numbering and register for i, q in enumerate(self.simQubits): if q.register == reg2: - self._logger.debug(f"Updating register {q.num} to {q.num + offset}.") + self._logger.debug("Updating register %d to %d.", q.num, q.num + offset) q.register = reg1 q.num = q.num + offset @@ -964,17 +964,17 @@ def remote_merge_from(self, simNodeName, simQubitNum, localReg): localReg local register to merge with """ - self._logger.debug(f"Merging from {simNodeName}") + self._logger.debug("Merging from %s", simNodeName) # This should only be called if lock is acquired assert self._lock.locked, f"No global lock present for node {self.myID.name}" - self._logger.debug(f"Merging from {simNodeName} LOCKS PRESENT") + self._logger.debug("Merging from %s LOCKS PRESENT", simNodeName) # Lookup the local connection for this simulating node if not (simNodeName in self.config.hostDict): raise virtNetError( - f"Trying to get conncetion to virtual node {simNodeName}, but this is not in configuration file" + f"Trying to get connection to virtual node {simNodeName}, but this is not in configuration file" ) simNode = yield self.get_connection(simNodeName) @@ -1071,7 +1071,8 @@ def remote_update_virtual_merge(self, newSimNodeName, oldSimNodeName, oldRegNum, # Check if this qubit needs updating if q.simNode == oldSimNode and givenReg == oldRegNum: self._logger.debug( - f"Updating virtual qubit {q.num}, previously {oldSimNode.name} now {newSimNode.name}" + "Updating virtual qubit %d, previously %s now %s", + q.num, oldSimNode.name, newSimNode.name ) q.simNode = newSimNode q.simQubit = newD[givenNum] @@ -1117,7 +1118,7 @@ def remote_get_register_del(self, qubitNum): # If nothing is found, return if gotQ is None: - self._logger.debug(f"No simulated qubit with ID {qubitNum}.") + self._logger.debug("No simulated qubit with ID %d.", qubitNum) return ([], [], 0, 0, 0) (realM, imagM) = gotQ.register.get_register_RI() @@ -1128,7 +1129,7 @@ def remote_get_register_del(self, qubitNum): # Remove all simulated qubits and the register # Need to iterate of simQubits in reverse, otherwise wrong elements are removed - self._logger.debug(f"removing all sim qubits in reg {oldRegNum}") + self._logger.debug("removing all sim qubits in reg %d", oldRegNum) for q in reversed(self.simQubits): if q.register.num == oldRegNum: self.simQubits.remove(q) @@ -1167,7 +1168,7 @@ def remote_get_multiple_qubits(self, qList): nums = [] for q in qList: nums.append(q.simQubit.simNum) - self._logger.debug(f"Looking for simulated qubits. {nums}") + self._logger.debug("Looking for simulated qubits. %s", nums) (R, I) = self.remote_get_state(nums) else: # Qubits are located elsewhere. @@ -1273,7 +1274,7 @@ def _single_gate(self, name, *args): name name of the method corresponding to the name. For example: name = apply_X param parameters for gates such as rotations (axis,angle) """ - self._logger.debug(f"applying gate {name} to virtual qubit {self.num}") + self._logger.debug("applying gate %s to virtual qubit %d", name, self.num) if self.active != 1: self._logger.error("Attempt to manipulate qubits no longer at this node.") return False @@ -1353,7 +1354,7 @@ def remote_measure(self, inplace=False): self._logger.error("Attempt to manipulate qubits no longer at this node.") return - self._logger.debug("measuring virtual qubit {self.num}") + self._logger.debug("measuring virtual qubit %d", self.num) locked_node = yield self._lock_simulating_node() yield call_method(self.simQubit, "lock") @@ -1404,7 +1405,7 @@ def _lock_nodes(self, target): # Get deferreds for locking all the relevant nodes once (which might overlap) for node in set([local_node, control_sim_node, target_sim_node]): ds[node] = call_method(node.root, "get_global_lock") - self._logger.debug(f"For merging gonna lock the nodes {list(ds.keys())}") + self._logger.debug("For merging gonna lock the nodes %s", list(ds.keys())) # Deferred for all of the locks d_lock = DeferredList(list(ds.values()), fireOnOneCallback=False, consumeErrors=True) # Since deadlock might occur also schedule a random timeout @@ -1502,7 +1503,7 @@ def _two_qubit_gate(self, target, name): return localName = "".join(["remote_", name]) - self._logger.debug(f"Doing 2 qubit gate name {name} and local call {localName}") + self._logger.debug("Doing 2 qubit gate name %s and local call %s", name, localName) # First lock the relevant nodes locked_nodes = yield self._lock_nodes(target=target) @@ -1564,7 +1565,7 @@ def _two_qubit_gate(self, target, name): # Execute the 2 qubit gate yield call_method(self.simQubit, name, targetNum) - self._logger.debug(f"Remote 2qubit command to {target.simNode.name}.") + self._logger.debug("Remote 2qubit command to %s.", target.simNode.name) else: # They are simulated at two different nodes if self.simNode == self.virtNode: @@ -1749,7 +1750,7 @@ def _lock_simulating_node(self, exclude=None): locked_node = yield self._lock_simulating_node() return locked_node else: - self._logger.debug("got lock of simulating node {curr_sim_node}") + self._logger.debug("got lock of simulating node %s", curr_sim_node) return curr_sim_node From 93d0c70cea86e2c2d2681ac59c2709440c07caf7 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 16 Jul 2025 17:06:48 +0200 Subject: [PATCH 009/277] Make documentation match the current package distribution --- docs/conf.py | 20 +++-------- docs/requirements.txt | 4 +-- docs/simulaqron.cqc_backend.rst | 29 --------------- docs/simulaqron.general.rst | 6 ++-- docs/simulaqron.rst | 12 ++----- docs/simulaqron.run.rst | 14 ++------ docs/simulaqron.toolbox.rst | 6 ++-- docs/simulaqron.virtNode.rst | 62 -------------------------------- docs/simulaqron.virtual_node.rst | 62 ++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 10 files changed, 81 insertions(+), 136 deletions(-) delete mode 100644 docs/simulaqron.cqc_backend.rst delete mode 100644 docs/simulaqron.virtNode.rst create mode 100644 docs/simulaqron.virtual_node.rst diff --git a/docs/conf.py b/docs/conf.py index 1d6779fc..ba7d6761 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,6 +18,7 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # import os +import importlib.metadata as metadata # import sys @@ -57,21 +58,10 @@ copyright = "2017, Stephanie Wehner and Axel Dahlberg" author = "Stephanie Wehner and Axel Dahlberg" -# Get the version from simulaqron __init__ -path_to_here = os.path.dirname(os.path.abspath(__file__)) -simulaqron_init = os.path.join(path_to_here, "../simulaqron", "__init__.py") - -with open(simulaqron_init, 'r') as f: - for line in f: - line = line.strip() - if line.startswith("__version__"): - _version = line.split("__version__ = ")[1] - _version = _version.split(' ')[0] - _version = eval(_version) - _short_version = '.'.join(_version.split('.')[:-1]) - break - else: - raise RuntimeError("Could not find the version!") +# Get the version from simulaqron +_base_version_line = metadata.version('simulaqron') +_version = _base_version_line +_short_version = '.'.join(_base_version_line.split('.')[:-1]) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/requirements.txt b/docs/requirements.txt index a1e4c338..ada2a3ba 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ -sphinx>=2.2.0,<3.0.0 -sphinx-rtd-theme>=0.4.3,<1.0.0 +sphinx==6.1.3 +sphinx-rtd-theme==1.2.0 Cython qutip projectq diff --git a/docs/simulaqron.cqc_backend.rst b/docs/simulaqron.cqc_backend.rst deleted file mode 100644 index a3fbbf5d..00000000 --- a/docs/simulaqron.cqc_backend.rst +++ /dev/null @@ -1,29 +0,0 @@ -simulaqron.cqc_backend package -============================== - -Submodules ----------- - -simulaqron.cqc_backend.cqcFactory module ----------------------------------------- - -.. automodule:: simulaqron.cqc_backend.cqcFactory - :members: - :undoc-members: - :show-inheritance: - -simulaqron.cqc_backend.cqcMessageHandler module ------------------------------------------------ - -.. automodule:: simulaqron.cqc_backend.cqcMessageHandler - :members: - :undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: simulaqron.toolbox - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/simulaqron.general.rst b/docs/simulaqron.general.rst index 8f2c6422..b17cf9c7 100644 --- a/docs/simulaqron.general.rst +++ b/docs/simulaqron.general.rst @@ -4,10 +4,10 @@ simulaqron.general package Submodules ---------- -simulaqron.general.hostConfig module ------------------------------------- +simulaqron.general.host_config module +------------------------------------- -.. automodule:: simulaqron.general.hostConfig +.. automodule:: simulaqron.general.host_config :members: :undoc-members: :show-inheritance: diff --git a/docs/simulaqron.rst b/docs/simulaqron.rst index 51252598..ef468349 100644 --- a/docs/simulaqron.rst +++ b/docs/simulaqron.rst @@ -6,12 +6,11 @@ Subpackages .. toctree:: - simulaqron.cqc_backend simulaqron.general simulaqron.local simulaqron.run simulaqron.toolbox - simulaqron.virtNode + simulaqron.virtual_node Submodules ---------- @@ -19,7 +18,7 @@ Submodules simulaqron.SimulaQron module ---------------------------- -.. automodule:: simulaqron.SimulaQron +.. automodule:: simulaqron.simulaqron :members: :undoc-members: :show-inheritance: @@ -40,13 +39,6 @@ simulaqron.settings module :undoc-members: :show-inheritance: -simulaqron.tests_run module ---------------------------- - -.. automodule:: simulaqron.tests_run - :members: - :undoc-members: - :show-inheritance: Module contents --------------- diff --git a/docs/simulaqron.run.rst b/docs/simulaqron.run.rst index a1402609..faa2d256 100644 --- a/docs/simulaqron.run.rst +++ b/docs/simulaqron.run.rst @@ -4,18 +4,10 @@ simulaqron.run package Submodules ---------- -simulaqron.run.startCQC module ------------------------------- +simulaqron.run.run module +------------------------- -.. automodule:: simulaqron.run.startCQC - :members: - :undoc-members: - :show-inheritance: - -simulaqron.run.startNode module -------------------------------- - -.. automodule:: simulaqron.run.startNode +.. automodule:: simulaqron.run.run :members: :undoc-members: :show-inheritance: diff --git a/docs/simulaqron.toolbox.rst b/docs/simulaqron.toolbox.rst index 534bbd83..56745e39 100644 --- a/docs/simulaqron.toolbox.rst +++ b/docs/simulaqron.toolbox.rst @@ -36,10 +36,10 @@ simulaqron.toolbox.reset module :undoc-members: :show-inheritance: -simulaqron.toolbox.stabilizerStates module ------------------------------------------- +simulaqron.toolbox.stabilizer_states module +------------------------------------------- -.. automodule:: simulaqron.toolbox.stabilizerStates +.. automodule:: simulaqron.toolbox.stabilizer_states :members: :undoc-members: :show-inheritance: diff --git a/docs/simulaqron.virtNode.rst b/docs/simulaqron.virtNode.rst deleted file mode 100644 index 367827bd..00000000 --- a/docs/simulaqron.virtNode.rst +++ /dev/null @@ -1,62 +0,0 @@ -simulaqron.virtNode package -=========================== - -Submodules ----------- - -simulaqron.virtNode.basics module ---------------------------------- - -.. automodule:: simulaqron.virtNode.basics - :members: - :undoc-members: - :show-inheritance: - -simulaqron.virtNode.projectQSimulator module --------------------------------------------- - -.. automodule:: simulaqron.virtNode.projectQSimulator - :members: - :undoc-members: - :show-inheritance: - -simulaqron.virtNode.quantum module ----------------------------------- - -.. automodule:: simulaqron.virtNode.quantum - :members: - :undoc-members: - :show-inheritance: - -simulaqron.virtNode.qutipSimulator module ------------------------------------------ - -.. automodule:: simulaqron.virtNode.qutipSimulator - :members: - :undoc-members: - :show-inheritance: - -simulaqron.virtNode.stabilizerSimulator module ----------------------------------------------- - -.. automodule:: simulaqron.virtNode.stabilizerSimulator - :members: - :undoc-members: - :show-inheritance: - -simulaqron.virtNode.virtual module ----------------------------------- - -.. automodule:: simulaqron.virtNode.virtual - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: simulaqron.virtNode - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/simulaqron.virtual_node.rst b/docs/simulaqron.virtual_node.rst new file mode 100644 index 00000000..aa4239d3 --- /dev/null +++ b/docs/simulaqron.virtual_node.rst @@ -0,0 +1,62 @@ +simulaqron.virt_node package +============================ + +Submodules +---------- + +simulaqron.virtual_node.basics module +------------------------------------- + +.. automodule:: simulaqron.virtual_node.basics + :members: + :undoc-members: + :show-inheritance: + +simulaqron.virtual_node.project_q_simulator module +-------------------------------------------------- + +.. automodule:: simulaqron.virtual_node.project_q_simulator + :members: + :undoc-members: + :show-inheritance: + +simulaqron.virtual_node.quantum module +-------------------------------------- + +.. automodule:: simulaqron.virtual_node.quantum + :members: + :undoc-members: + :show-inheritance: + +simulaqron.virtual_node.qutip_simulator module +---------------------------------------------- + +.. automodule:: simulaqron.virtual_node.qutip_simulator + :members: + :undoc-members: + :show-inheritance: + +simulaqron.virtual_node.stabilizerSimulator module +-------------------------------------------------- + +.. automodule:: simulaqron.virtual_node.stabilizer_simulator + :members: + :undoc-members: + :show-inheritance: + +simulaqron.virtual_node.virtual module +-------------------------------------- + +.. automodule:: simulaqron.virtual_node.virtual + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: simulaqron.virtual_node + :members: + :undoc-members: + :show-inheritance: diff --git a/pyproject.toml b/pyproject.toml index 5c95cb86..8e7c248c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dynamic = ["version"] dependencies = [ "numpy>=1.14.0,<2.0.0", "scipy>=1.1.0,<2.0.0", - "twisted>=24.0.0", + "twisted>=24.0.0, <26.0.0", "networkx>=3.0,<4.0", "click>=8.0,<9.0", "daemons>=1.3.0,<2.0.0", From 7b98d1e74b39c02b463edb4ea3d8f512fe393c3f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 18 Jul 2025 15:23:15 +0200 Subject: [PATCH 010/277] Enable single qubit netqasm tests again --- simulaqron/netqasm_backend/executioner.py | 7 +- simulaqron/sdk/connection.py | 7 +- simulaqron/virtual_node/virtual.py | 6 +- tests/slow/sdk/test_single_qubit.py | 150 +++++++++++----------- 4 files changed, 87 insertions(+), 83 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 0f037fdc..e289d5e8 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -15,6 +15,8 @@ LinkLayerOKTypeK, LinkLayerOKTypeM, LinkLayerOKTypeR, RandomBasis, RequestType, ReturnType) + +from simulaqron.general import SimUnsupportedError from simulaqron.general.host_config import get_node_id_from_net_config from simulaqron.settings import simulaqron_settings from simulaqron.virtual_node.virtual import call_method @@ -115,7 +117,10 @@ def add_factory(self, factory): def _handle_command_exception(self, exc, prog_counter, traceback_str): self._logger.error("At line %d: %s\n%s", prog_counter, exc, traceback_str) - self._return_msg(msg=ErrorMessage(err_code=ErrorCode.GENERAL)) + if isinstance(exc, SimUnsupportedError): + self._return_msg(msg=ErrorMessage(err_code=ErrorCode.UNSUPP)) + else: + self._return_msg(msg=ErrorMessage(err_code=ErrorCode.GENERAL)) def _return_msg(self, msg): if self._return_msg_func is None: diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 869554de..781d0344 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -4,7 +4,7 @@ from netqasm.backend.messages import (ErrorMessage, MessageHeader, MsgDoneMessage, ReturnArrayMessage, - ReturnRegMessage, deserialize_return_msg) + ReturnRegMessage, deserialize_return_msg, ErrorCode) from netqasm.lang.ir import GenericInstr from netqasm.lang.operand import Address, Register from netqasm.logging.glob import get_netqasm_logger @@ -254,7 +254,10 @@ def _handle_reply(self): value=ret_msg.values, ) elif isinstance(ret_msg, ErrorMessage): - raise RuntimeError(f"Received error message from backend: {ret_msg}") + if ret_msg.err_code == ErrorCode.UNSUPP.value: + raise SimUnsupportedError("Operation not supported") + else: + raise RuntimeError(f"Received error message from backend: {ret_msg}") else: raise NotImplementedError(f"Unknown return message of type {type(ret_msg)}") # Continue handling replies until a done diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 0b2d677e..20bbfd9b 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -28,7 +28,6 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import random -# import traceback from collections import deque @@ -44,6 +43,9 @@ from simulaqron.virtual_node.basics import quantumError, noQubitError, virtNetError from simulaqron.virtual_node.quantum import simulatedQubit from simulaqron.general.host_config import SocketsConfig +# We need this import (despite unused) to reraise local errors +# We then supress the flake8 errors associated with that +from simulaqron.general.errors import * # noqa: F401, F403 from simulaqron.settings import simulaqron_settings, SimBackend if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: @@ -56,7 +58,7 @@ raise quantumError(f"Unknown backend {simulaqron_settings.sim_backend}") -def reraise_remote_error(self, remote_err): +def reraise_remote_error(remote_err): """ This is a function re-raises the error thrown remotely :param remote_err: :obj:`twisted.spread.pb.RemoteError` diff --git a/tests/slow/sdk/test_single_qubit.py b/tests/slow/sdk/test_single_qubit.py index 4f0a291c..4d01d3a5 100644 --- a/tests/slow/sdk/test_single_qubit.py +++ b/tests/slow/sdk/test_single_qubit.py @@ -26,16 +26,16 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import unittest import numpy as np +import pytest from scipy.linalg import expm -import sys from netqasm.sdk.qubit import Qubit from simulaqron.settings import simulaqron_settings, SimBackend from simulaqron.network import Network from simulaqron.sdk.connection import SimulaQronConnection +from simulaqron.run.run import reset from simulaqron.general import SimUnsupportedError @@ -194,169 +194,163 @@ def prep_I_state(): return q -@unittest.skip("We can test these things better when we have implemented a get_qubit_state function for simulaqron") -class SingleQubitGateTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.iterations = 1 - sys.stdout.write("Testing single qubit gates gates with {} iterations \r\n".format(cls.iterations)) +class TestSingleQubitGate: + iterations: int = 1 + + @pytest.fixture + def network(self): + print(f"Testing single qubit gates gates with {self.iterations} iterations \n") simulaqron_settings.default_settings() - cls.network = Network(nodes=["Alice"], force=True) - cls.network.start() + network = Network(nodes=["Alice"], force=True) + network.start() + yield network - @classmethod - def tearDownClass(cls): - cls.network.stop() + network.stop() simulaqron_settings.default_settings() + reset() - def testXGate(self): + def test_X_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test X - sys.stdout.write("Testing X gate:") + print("Testing X gate:") exp_values = calc_exp_values(prep_X_state()) ans = conn.test_preparation(prep_X, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testYGate(self): + def test_Y_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test Y - sys.stdout.write("Testing Y gate:") + print("Testing Y gate:") exp_values = calc_exp_values(prep_Y_state()) ans = conn.test_preparation(prep_Y, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testZGate(self): + def test_Z_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test Z - sys.stdout.write("Testing Z gate:") + print("Testing Z gate:") exp_values = calc_exp_values(prep_Z_state()) ans = conn.test_preparation(prep_Z, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testHGate(self): + def test_H_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test H - sys.stdout.write("Testing H gate:") + print("Testing H gate:") exp_values = calc_exp_values(prep_H_state()) ans = conn.test_preparation(prep_H, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testTGate(self): + def test_T_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test T - sys.stdout.write("Testing T gate:") + print("Testing T gate:") exp_values = calc_exp_values(prep_T_state()) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_T, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_T, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testKGate(self): + def test_K_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test K - sys.stdout.write("Testing K gate:") + print("Testing K gate:") exp_values = calc_exp_values(prep_K_state()) ans = conn.test_preparation(prep_K, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testXpi8Rot(self): + def test_X_pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_X pi/8 - sys.stdout.write("Testing rotation (X,pi/8) gate:") + print("Testing rotation (X,pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([1, 0, 0], np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotx1, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_rotx1, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testX5pi8Rot(self): + def test_X_5pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_X 5*pi/8 - sys.stdout.write("Testing rotation (X,5*pi/8) gate:") + print("Testing rotation (X,5*pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([1, 0, 0], 5 * np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotx2, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_rotx2, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testYpi8Rot(self): + def test_Y_pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Y pi/8 - sys.stdout.write("Testing rotation (Y,pi/8) gate:") + print("Testing rotation (Y,pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([0, 1, 0], np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_roty1, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_roty1, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testY5pi8Rot(self): + def test_Y_5pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Y 5*pi/8 - sys.stdout.write("Testing rotation (Y,5*pi/8) gate:") + print("Testing rotation (Y,5*pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([0, 1, 0], 5 * np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_roty2, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_roty2, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testZpi8Rot(self): + def test_Z_pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Z pi/8 - sys.stdout.write("Testing rotation (Z,pi/8) gate:") + print("Testing rotation (Z,pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([0, 0, 1], np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotz1, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_rotz1, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testZ5pi8Rot(self): + def test_Z_5pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Z 5*pi/8 - sys.stdout.write("Testing rotation (Z,5*pi/8) gate:") + print("Testing rotation (Z,5*pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([0, 0, 1], 5 * np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotz2, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_rotz2, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + print("\n") + assert ans - def testReset(self): + def test_Reset(self, network): with SimulaQronConnection("Alice") as conn: # Test RESET - sys.stdout.write("Testing RESET:") + print("Testing RESET:") exp_values = calc_exp_values(prep_I_state()) ans = conn.test_preparation(prep_reset, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) - - -################################################################################################## - -if __name__ == "__main__": - unittest.main() + print("\n") + assert ans From e8602c0a3522c8b9067634b199a12198a7767da4 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 21 Jul 2025 17:04:36 +0200 Subject: [PATCH 011/277] Small changes on the logging format --- examples/nativeMode/WARNINIG | 5 +++ simulaqron/general/host_config.py | 6 ++-- simulaqron/netqasm_backend/executioner.py | 2 +- simulaqron/netqasm_backend/factory.py | 4 +-- simulaqron/network.py | 28 +++++++-------- simulaqron/sdk/connection.py | 4 +-- simulaqron/settings.py | 2 +- simulaqron/simulaqron.py | 30 ++++++++-------- simulaqron/toolbox/manage_nodes.py | 10 +++--- simulaqron/toolbox/stabilizer_states.py | 36 +++++++++---------- .../virtual_node/project_q_simulator.py | 2 +- simulaqron/virtual_node/quantum.py | 2 +- simulaqron/virtual_node/virtual.py | 2 +- tests/slow/merges/test_merges.py | 8 ++--- 14 files changed, 74 insertions(+), 67 deletions(-) create mode 100644 examples/nativeMode/WARNINIG diff --git a/examples/nativeMode/WARNINIG b/examples/nativeMode/WARNINIG new file mode 100644 index 00000000..91745d8b --- /dev/null +++ b/examples/nativeMode/WARNINIG @@ -0,0 +1,5 @@ +Writing SimulaQron applications using the "native" interface is deprecated. + +The examples in this folder are here just for completeness. +If you want to create a SimulaQron application, please refer to the example +in the "netqasm" folder. diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index e24bc016..ea4bb04b 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -126,8 +126,8 @@ def read_config(self, filename, network_name="default", config_type="vnode"): network_config = NetworksConfigConstructor(file_path=filename).networks[network_name] nodes = network_config.nodes for node_name, node_config in nodes.items(): - hostname = getattr(node_config, "{}_hostname".format(config_type)) - port = getattr(node_config, "{}_port".format(config_type)) + hostname = getattr(node_config, f"{config_type}_hostname") + port = getattr(node_config, f"{config_type}_port") self.hostDict[node_name] = Host(node_name, hostname, port) elif filename.endswith(".cfg"): @@ -140,7 +140,7 @@ def read_config(self, filename, network_name="default", config_type="vnode"): newHost = Host(words[0].strip(), words[1].strip(), words[2].strip()) self.hostDict[words[0]] = newHost else: - raise ValueError("Unknown file type {}".format(filename.split(".")[-1])) + raise ValueError(f"Unknown file type {filename.split(".")[-1]}") def print_details(self, name): """ diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index e289d5e8..ac99532c 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -603,7 +603,7 @@ def _sample_basis_choice(self, random_basis_set, probability_dist_spec): weights = self._get_probability_weights(probability_dist_spec, num_choices=2) basis = random.choices([Basis.ZPLUSX, Basis.ZMINUSX], weights)[0] else: - raise ValueError("Unsupported random basis choice {}".format(random_basis_set)) + raise ValueError(f"Unsupported random basis choice {random_basis_set}") return basis diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 75cbfb62..1d2e962a 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -238,7 +238,7 @@ def is_adjacent(self, remote_host_name): return False else: self._logger.warning( - "Node {} is not in the specified topology and is therefore " - "assumed to have no neighbors".format(self.name) + f"Node {self.name} is not in the specified topology and is therefore " + "assumed to have no neighbors" ) return False diff --git a/simulaqron/network.py b/simulaqron/network.py index fbb414b3..ea2104f7 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -92,9 +92,9 @@ def __init__(self, name=None, nodes=None, topology=None, network_config_file=Non self.nodes = nodes self.topology = construct_topology_config(topology, self.nodes) if not force: - answer = input("Do you want to add/replace the network {} in the file {}" - "with a network constisting of the nodes {}? (yes/no)" - .format(self.name, self._network_config_file, self.nodes)) + answer = input(f"Do you want to add/replace the network {self.name} in the " + f"file {self._network_config_file} with a network consisting " + f"of the nodes {self.nodes}? (yes/no)") if answer not in ["yes", "y"]: raise RuntimeError("User did not want to replace network in file") networks_config.add_network(node_names=self.nodes, network_name=self.name, topology=self.topology) @@ -106,18 +106,17 @@ def __init__(self, name=None, nodes=None, topology=None, network_config_file=Non node_names = networks_config.get_node_names(self.name) self.topology = networks_config.networks[self.name].topology else: - raise ValueError("Network {} is not in the file {}\n" - "If you wish to add this network to the file, use the" - "--new flag.".format(self.name, self._network_config_file)) + raise ValueError(f"Network {self.name} is not in the file {self._network_config_file}\n" + f"If you wish to add this network to the file, use the --new flag.") if nodes is None: self.nodes = node_names else: self.nodes = nodes for node_name in self.nodes: if node_name not in node_names: - raise ValueError("Node {} is not in the current network {} in the file {}\n" - "If you wish to overwrite the current network in the file, use the" - "--new flag.".format(node_name, self.name, self._network_config_file)) + raise ValueError(f"Node {node_name} is not in the current network {self.name} " + f"in the file {self._network_config_file}\nIf you wish to overwrite " + f"the current network in the file, use the --new flag.") self._setup_processes() @@ -138,7 +137,7 @@ def running(self): self._running = False break except Exception as err: - self._logger.exception("Got unexpected exception when trying to connect: {}".format(err)) + self._logger.exception("Got unexpected exception when trying to connect: %s", err) raise err else: self._logger.debug("Network %s is now running", self.name) @@ -156,10 +155,10 @@ def _setup_processes(self): mp.set_start_method("spawn", force=True) for node in self.nodes: process_virtual = mp.Process( - target=start_vnode, args=(node, self.name, get_log_level()), name="VirtNode {}".format(node) + target=start_vnode, args=(node, self.name, get_log_level()), name=f"VirtNode {node}" ) process_qnodeos = mp.Process( - target=start_qnodeos, args=(node, self.name, get_log_level()), name="QnodeOSNode {}".format(node) + target=start_qnodeos, args=(node, self.name, get_log_level()), name=f"QnodeOSNode {node}" ) self.processes += [process_virtual, process_qnodeos] @@ -191,14 +190,15 @@ def stop(self): Stops the network. """ self._running = False - self._logger.info("Stopping network with name {}".format(self.name)) + self._logger.info("Stopping network with name %s", self.name) for p in self.processes: while p.is_alive(): time.sleep(0.1) try: p.terminate() except Exception as err: - self._logger("Could not terminate one of the processes in the network due to error: {}".format(err)) + self._logger.warning("Could not terminate one of the processes in the" + "network due to error: %s", err) def construct_topology_config(topology, nodes): diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 781d0344..77336056 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -125,7 +125,7 @@ def _setup_network_data( myHost = qnodeos_net.hostDict[name] else: raise ValueError( - "Host name '{}' is not in the qnodeos network".format(name) + f"Host name '{name}' is not in the qnodeos network" ) # Get IP and port number @@ -148,7 +148,7 @@ def _setup_socket(name, addr, retry_time=0.1): while True: try: logger.debug( - "App %s : Trying to connect to NetQASM server (at %d)", name, addr[-1] + "App %s : Trying to connect to NetQASM server (at %s)", name, addr[-1] ) qnodeos_socket = socket.socket(addr[0], addr[1], addr[2]) diff --git a/simulaqron/settings.py b/simulaqron/settings.py index d251c238..d64e19b5 100644 --- a/simulaqron/settings.py +++ b/simulaqron/settings.py @@ -119,7 +119,7 @@ def _get_setting(self, setting): try: value = self._config[setting] except KeyError: - raise KeyError("Cannot find the setting {} in the file {}".format(setting, self._internal_settings_file)) + raise KeyError(f"Cannot find the setting {setting} in the file {self._internal_settings_file}") return value def _set_setting(self, setting, value): diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 122e3298..108c20ba 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -42,7 +42,7 @@ def run(self): nodes = [] if self.nrnodes and (self.nrnodes > len(nodes)): - nodes += ["Node{}".format(i) for i in range(self.nrnodes - len(nodes))] + nodes += [f"Node{i}" for i in range(self.nrnodes - len(nodes))] else: nodes = self.nodes @@ -140,9 +140,9 @@ def start(name, nrnodes, nodes, topology, force, keep): return if new: if not force: - answer = input("Do you want to add/replace the network '{}' in the file {} " - "with a new network? (yes/no)" - .format(name, simulaqron_settings.network_config_file)) + answer = input(f"Do you want to add/replace the network '{name}' in the file " + f"{simulaqron_settings.network_config_file} with a new network? " + f"(yes/no)") if not _is_positive_answer(answer): print("Aborted!") return @@ -152,8 +152,8 @@ def start(name, nrnodes, nodes, topology, force, keep): except SystemExit as e: if e.code == exit.PIDFILE_INACCESSIBLE or\ e.code == exit.DAEMONIZE_FAILED: - logging.debug("Failed to launch Simulaqron Daemon. " - "Exit code reported by daemons: {}".format(e.code)) + logging.debug(f"Failed to launch Simulaqron Daemon. " + f"Exit code reported by daemons: {e.code}") print("Failed to launch SimulaQron Daemon. Aborted!") ############### @@ -428,8 +428,9 @@ def add(name, network_name=None, hostname=None, app_port=None, qnodeos_port=None HOSTNAME: The host name of the node, e.g. localhost or 192.168.0.1 """ if not force: - answer = input("Do you want to add the node {} to the network {} in the file {}? (yes/no)." - .format(name, network_name, simulaqron_settings.network_config_file)) + answer = input(f"Do you want to add the node {name} to the " + f"network {network_name} in the file " + f"{simulaqron_settings.network_config_file}? (yes/no).") if not _is_positive_answer(answer): print("Aborting!") return @@ -461,8 +462,9 @@ def remove(name, network_name=None, force=False): NAME: The name of the node, e.g. Alice """ if not force: - answer = input("Do you want to remove the node {} to the network {} in the file {}? (yes/no)." - .format(name, network_name, simulaqron_settings.network_config_file)) + answer = input(f"Do you want to remove the node {name} to the network " + f"{network_name} in the file " + f"{simulaqron_settings.network_config_file}? (yes/no).") if not _is_positive_answer(answer): print("Aborting!") return @@ -488,9 +490,9 @@ def default(network_name=None, force=False): Alice, Bob, Charlie, David, Eve """ if not force: - answer = input("Do you want to set the network {} in the file {} to default," - "i.e. with nodes Alice, Bob, Charlie, David and Eve? (yes/no)." - .format(network_name, simulaqron_settings.network_config_file)) + answer = input(f"Do you want to set the network {network_name} in the file " + f"{simulaqron_settings.network_config_file} to default, i.e. " + f"with nodes Alice, Bob, Charlie, David and Eve? (yes/no).") if not _is_positive_answer(answer): print("Aborting!") return @@ -509,7 +511,7 @@ def get(network_name=None): try: nodes = networks_config.get_node_names(network_name=network_name) except ValueError: - print("No network {}".format(network_name)) + print(f"No network {network_name}") else: print(("{} " * len(nodes))[:-1].format(*nodes)) diff --git a/simulaqron/toolbox/manage_nodes.py b/simulaqron/toolbox/manage_nodes.py index 236815bb..f6ea39e3 100644 --- a/simulaqron/toolbox/manage_nodes.py +++ b/simulaqron/toolbox/manage_nodes.py @@ -63,8 +63,8 @@ def add_node(self, node_name, network_name="default", app_hostname=None, qnodeos else: free = self._check_port_available(hostname, port) if not free: - raise ValueError("Cannot add node {}, since socket address ({}, {}) is already in use." - .format(node_name, hostname, port)) + raise ValueError(f"Cannot add node {node_name}, since socket address " + f"({hostname}, {port}) is already in use.") socket_address = (hostname, port) self.used_sockets.append(socket_address) socket_addresses[i] = socket_address @@ -164,7 +164,7 @@ def get_nodes(self, network_name="default"): nodes = self.networks[network_name].nodes return list(nodes.values()) else: - raise ValueError("{} is not a network in this config".format(network_name)) + raise ValueError(f"{network_name} is not a network in this config") def get_node_names(self, network_name="default"): """ @@ -180,7 +180,7 @@ def get_node_names(self, network_name="default"): nodes = self.networks[network_name].nodes return list(nodes.keys()) else: - raise ValueError("{} is not a network in this config".format(network_name)) + raise ValueError(f"{network_name} is not a network in this config") def to_dict(self): """ @@ -221,7 +221,7 @@ def read_from_file(self, file_path=None): with open(file_path, 'r') as f: dict = json.load(f) else: - raise ValueError("No such file {}".format(file_path)) + raise ValueError(f"No such file {file_path}") for network_name, network_dict in dict.items(): nodes_dict = network_dict["nodes"] diff --git a/simulaqron/toolbox/stabilizer_states.py b/simulaqron/toolbox/stabilizer_states.py index 0ab5d881..49d4aa19 100644 --- a/simulaqron/toolbox/stabilizer_states.py +++ b/simulaqron/toolbox/stabilizer_states.py @@ -131,7 +131,7 @@ def __init__(self, data=None, check_symplectic=True): self._group = np.array(data, dtype=bool) except Exception as err: raise ValueError( - "Could not create an array of the 'data' due to the following error: {}".format(err) + f"Could not create an array of the 'data' due to the following error: {err}" ) if len(self._group.shape) != 2: @@ -217,9 +217,9 @@ def __repr__(self): return "StabilizerState(np." + self._group.__repr__() + ")" def __str__(self): - to_return = "Stabilizer state on {} with the following stabilizer generators:\n".format(self.num_qubits) + to_return = f"Stabilizer state on {self.num_qubits} with the following stabilizer generators:\n" for row_str in self.to_string().split('\n'): - to_return += "\t{}\n".format(row_str) + to_return += f"\t{row_str}\n" return to_return[:-1] def __len__(self): @@ -229,7 +229,7 @@ def __len__(self): def _row_to_string(row): assert (len(row) - 1) % 2 == 0 n = int((len(row) - 1) / 2) - to_return = "{} ".format(StabilizerState.bool2phase[row[-1]]) + to_return = f"{StabilizerState.bool2phase[row[-1]]} " for i in range(n): to_return += StabilizerState.bool2Pauli[(row[i], row[i + n])] return to_return @@ -272,7 +272,7 @@ def boolean_gaussian_elimination(matrix, return_pivot_columns=False): try: new_matrix = np.array(matrix, dtype=bool) except Exception as err: - raise ValueError("Could not create an array of the 'data' due to the following error: {}".format(err)) + raise ValueError(f"Could not create an array of the 'data' due to the following error: {err}") if len(new_matrix.shape) != 2: raise ValueError("'data' needs to be an array of rank 2") @@ -420,7 +420,7 @@ def _contains(matrix, stabilizer): if isinstance(stabilizer, str): stab = StabilizerState._str_to_operator(stabilizer) if stab is None: - raise ValueError("Cannot parse {} as a stabilizer.".format(stabilizer)) + raise ValueError(f"Cannot parse {stabilizer} as a stabilizer.") else: stab = list(stabilizer) num_cols = matrix.shape[1] @@ -453,7 +453,7 @@ def _assert_valid_stabilizer(stabilizer, num_cols): if not isinstance(entry, bool): raise ValueError("All entries in a stabilizer should be of type `bool`.") if len(stabilizer) != num_cols: - raise ValueError("Stabilizer must be of length {}, not {}".format(num_cols, len(stabilizer))) + raise ValueError(f"Stabilizer must be of length {num_cols}, not {len(stabilizer)}") def add_qubit(self): r""" @@ -537,7 +537,7 @@ def apply_X(self, position): """ n = self.num_qubits if not (position >= 0 and position < n): - raise ValueError("position= {} if not a valid qubit position (i.e. in [0, {}]".format(position, n)) + raise ValueError(f"position= {position} if not a valid qubit position (i.e. in [0, {n}]") yz_rows = self._group[:, position + n] # Flip phases for Y and Z rows @@ -552,7 +552,7 @@ def apply_Y(self, position): """ n = self.num_qubits if not (position >= 0 and position < n): - raise ValueError("position= {} if not a valid qubit position (i.e. in [0, {}]".format(position, n)) + raise ValueError(f"position= {position} if not a valid qubit position (i.e. in [0, {n}]") xz_rows = np.logical_xor(self._group[:, position], self._group[:, position + n]) # Flip phases for X and Z rows @@ -567,7 +567,7 @@ def apply_Z(self, position): """ n = self.num_qubits if not (position >= 0 and position < n): - raise ValueError("position= {} if not a valid qubit position (i.e. in [0, {}]".format(position, n)) + raise ValueError(f"position= {position} if not a valid qubit position (i.e. in [0, {n}]") xy_rows = self._group[:, position] # Flip phases for X and Y rows @@ -582,7 +582,7 @@ def apply_H(self, position): """ n = self.num_qubits if not (position >= 0 and position < n): - raise ValueError("position= {} if not a valid qubit position (i.e. in [0, {}]".format(position, n)) + raise ValueError(f"position= {position} if not a valid qubit position (i.e. in [0, {n}]") # Swap the Z and X columns self._group[:, [position, position + n]] = self._group[:, [position + n, position]] @@ -599,7 +599,7 @@ def apply_K(self, position): """ n = self.num_qubits if not (position >= 0 and position < n): - raise ValueError("position= {} if not a valid qubit position (i.e. in [0, {}]".format(position, n)) + raise ValueError(f"position= {position} if not a valid qubit position (i.e. in [0, {n}]") # Perform effective CNOT from Z column to X column yz_rows = self._group[:, position + n] self._group[yz_rows, position] = np.logical_not(self._group[yz_rows, position]) @@ -617,7 +617,7 @@ def apply_S(self, position): """ n = self.num_qubits if not (position >= 0 and position < n): - raise ValueError("position= {} if not a valid qubit position (i.e. in [0, {}]".format(position, n)) + raise ValueError(f"position= {position} if not a valid qubit position (i.e. in [0, {n}]") # Perform effective CNOT from X column to Z column xy_rows = self._group[:, position] self._group[xy_rows, position + n] = np.logical_not(self._group[xy_rows, position + n]) @@ -644,9 +644,9 @@ def apply_CNOT(self, control, target): """ n = self.num_qubits if not (control >= 0 and control < n): - raise ValueError("control= {} if not a valid qubit position (i.e. in [0, {}]".format(control, n)) + raise ValueError(f"control= {control} if not a valid qubit position (i.e. in [0, {n}]") if not (target >= 0 and target < n): - raise ValueError("target= {} if not a valid qubit position (i.e. in [0, {}]".format(target, n)) + raise ValueError(f"target= {target} if not a valid qubit position (i.e. in [0, {n}]") if control == target: raise ValueError("Control and target qubits cannot be the same") @@ -680,9 +680,9 @@ def apply_CZ(self, control, target): """ n = self.num_qubits if not (control >= 0 and control < n): - raise ValueError("control= {} if not a valid qubit position (i.e. in [0, {}]".format(control, n)) + raise ValueError(f"control= {control} if not a valid qubit position (i.e. in [0, {n}]") if not (target >= 0 and target < n): - raise ValueError("target= {} if not a valid qubit position (i.e. in [0, {}]".format(target, n)) + raise ValueError(f"target= {target} if not a valid qubit position (i.e. in [0, {n}]") if control == target: raise ValueError("Control and target qubits cannot be the same") @@ -714,7 +714,7 @@ def measure(self, position, inplace=False): """ n = self.num_qubits if not (position >= 0 and position < n): - raise ValueError("position = {} if not a valid qubit position (not in [0, {}))".format(position, n)) + raise ValueError(f"position = {position} if not a valid qubit position (not in [0, {n}))") tmp_matrix = self._group # Create a new matrix where the X and Z columns of the corresponding qubit are the first. diff --git a/simulaqron/virtual_node/project_q_simulator.py b/simulaqron/virtual_node/project_q_simulator.py index c67fa48a..056cfe11 100644 --- a/simulaqron/virtual_node/project_q_simulator.py +++ b/simulaqron/virtual_node/project_q_simulator.py @@ -93,7 +93,7 @@ def add_qubit(self, newQubit): norm = np.dot(np.array(newQubit), np.array(newQubit).conj()) if not norm <= 1: - raise quantumError("State {} is not normalized.".format(newQubit)) + raise quantumError(f"State {newQubit} is not normalized.") # Create a fresh qubit num = self.add_fresh_qubit() diff --git a/simulaqron/virtual_node/quantum.py b/simulaqron/virtual_node/quantum.py index 465ba7e2..a9a38634 100644 --- a/simulaqron/virtual_node/quantum.py +++ b/simulaqron/virtual_node/quantum.py @@ -274,7 +274,7 @@ def remote_get_qubit(self): """ backend = settings.simulaqron_settings.sim_backend if backend != settings.SimBackend.QUTIP.value: - raise RuntimeError("Cannot get reduced qubit state using backend {}".format(backend)) + raise RuntimeError(f"Cannot get reduced qubit state using backend {backend}") self._logger.debug("VIRTUAL NODE %s: Returning qubit %d", self.node.name, self.num) return self.register.get_qubits_RI([self.num]) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 20bbfd9b..a9c4f01f 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -879,7 +879,7 @@ def _remove_sim_qubit(self, delQubit): q.num = q.num - 1 # Remove the qubit form the list of simulated qubits - self._logger.debug("removing qubit %d from %d", delQubit.simNum, self.simQubits) + self._logger.debug("removing qubit %d from %s", delQubit.simNum, self.simQubits) self.simQubits.remove(delQubit) finally: diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index bf2f730e..ff7a2680 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -84,7 +84,7 @@ def got_both(self): expectedState = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) correct = state == expectedState else: - ValueError("Unknown backend {}".format(simulaqron_settings.sim_backend)) + ValueError(f"Unknown backend {simulaqron_settings.sim_backend}") return bool(correct) @@ -132,7 +132,7 @@ def remote_receive_one_qubit(self, virtualNum, cnot_direction=0): expectedState = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) correct = state == expectedState else: - ValueError("Unknown backend {}".format(simulaqron_settings.sim_backend)) + ValueError(f"Unknown backend {simulaqron_settings.sim_backend}") return bool(correct) @@ -260,7 +260,7 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): expectedState = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) correct = state == expectedState else: - ValueError("Unknown backend {}".format(simulaqron_settings.sim_backend)) + ValueError(f"Unknown backend {simulaqron_settings.sim_backend}") send_end.send(correct) @@ -313,7 +313,7 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): expectedState = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) correct = state == expectedState else: - ValueError("Unknown backend {}".format(simulaqron_settings.sim_backend)) + ValueError(f"Unknown backend {simulaqron_settings.sim_backend}") send_end.send(correct) From d94cd52988a756da377e4fa8a793605ecf24b132 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 23 Jul 2025 13:26:40 +0200 Subject: [PATCH 012/277] Port the two qubit tests using netqasm backend --- simulaqron/start/start_qnodeos.py | 5 +- tests/slow/sdk/test_single_qubit.py | 30 +--- tests/slow/sdk/test_two_qubit.py | 217 ++++++++++++++-------------- 3 files changed, 114 insertions(+), 138 deletions(-) diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 76e0db3a..71788618 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -80,7 +80,8 @@ def setup_netqasm_server(myName, netqasm_factory): while timer() - t_start < _TIMEOUT: try: logger.debug( - "LOCAL %s: Starting local classical communication server.", myName + "LOCAL %s: Starting local classical communication server, port %d.", + myName, netqasm_factory.host.port ) myHost = netqasm_factory.host myHost.root = netqasm_factory @@ -123,7 +124,7 @@ def main(myName, network_name="default", log_level="WARNING"): # Check if we are in the host-dictionary if myName in qnodeos_network.hostDict: myHost = qnodeos_network.hostDict[myName] - logger.debug(f"Setting up QNodeOS protocol factory for {myName}") + logger.debug("Setting up QNodeOS protocol factory for %s (%s)", myName, myHost.addr) netqasm_factory = NetQASMFactory( myHost, myName, diff --git a/tests/slow/sdk/test_single_qubit.py b/tests/slow/sdk/test_single_qubit.py index 4d01d3a5..c419b83b 100644 --- a/tests/slow/sdk/test_single_qubit.py +++ b/tests/slow/sdk/test_single_qubit.py @@ -194,13 +194,13 @@ def prep_I_state(): return q +# TODO - We can test these things better when we have implemented a get_qubit_state function for simulaqron +# for now, we will permon tests based on the tomography function. class TestSingleQubitGate: iterations: int = 1 @pytest.fixture def network(self): - print(f"Testing single qubit gates gates with {self.iterations} iterations \n") - simulaqron_settings.default_settings() network = Network(nodes=["Alice"], force=True) network.start() @@ -213,144 +213,118 @@ def network(self): def test_X_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test X - print("Testing X gate:") exp_values = calc_exp_values(prep_X_state()) ans = conn.test_preparation(prep_X, exp_values, iterations=self.iterations) - print("\n") assert ans def test_Y_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test Y - print("Testing Y gate:") exp_values = calc_exp_values(prep_Y_state()) ans = conn.test_preparation(prep_Y, exp_values, iterations=self.iterations) - print("\n") assert ans def test_Z_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test Z - print("Testing Z gate:") exp_values = calc_exp_values(prep_Z_state()) ans = conn.test_preparation(prep_Z, exp_values, iterations=self.iterations) - print("\n") assert ans def test_H_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test H - print("Testing H gate:") exp_values = calc_exp_values(prep_H_state()) ans = conn.test_preparation(prep_H, exp_values, iterations=self.iterations) - print("\n") assert ans def test_T_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test T - print("Testing T gate:") exp_values = calc_exp_values(prep_T_state()) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_T, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_T, exp_values, iterations=self.iterations) - print("\n") assert ans def test_K_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test K - print("Testing K gate:") exp_values = calc_exp_values(prep_K_state()) ans = conn.test_preparation(prep_K, exp_values, iterations=self.iterations) - print("\n") assert ans def test_X_pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_X pi/8 - print("Testing rotation (X,pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([1, 0, 0], np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotx1, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_rotx1, exp_values, iterations=self.iterations) - print("\n") assert ans def test_X_5pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_X 5*pi/8 - print("Testing rotation (X,5*pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([1, 0, 0], 5 * np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotx2, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_rotx2, exp_values, iterations=self.iterations) - print("\n") assert ans def test_Y_pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Y pi/8 - print("Testing rotation (Y,pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([0, 1, 0], np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_roty1, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_roty1, exp_values, iterations=self.iterations) - print("\n") assert ans def test_Y_5pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Y 5*pi/8 - print("Testing rotation (Y,5*pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([0, 1, 0], 5 * np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_roty2, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_roty2, exp_values, iterations=self.iterations) - print("\n") assert ans def test_Z_pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Z pi/8 - print("Testing rotation (Z,pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([0, 0, 1], np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotz1, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_rotz1, exp_values, iterations=self.iterations) - print("\n") assert ans def test_Z_5pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Z 5*pi/8 - print("Testing rotation (Z,5*pi/8) gate:") exp_values = calc_exp_values(prep_rot_state([0, 0, 1], 5 * np.pi / 8)) if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotz2, exp_values, iterations=self.iterations, progress=False) else: ans = conn.test_preparation(prep_rotz2, exp_values, iterations=self.iterations) - print("\n") assert ans def test_Reset(self, network): with SimulaQronConnection("Alice") as conn: # Test RESET - print("Testing RESET:") exp_values = calc_exp_values(prep_I_state()) ans = conn.test_preparation(prep_reset, exp_values, iterations=self.iterations) - print("\n") assert ans diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index 3d26d8ac..671e2bbc 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -26,14 +26,17 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import sys -import unittest import numpy as np +import pytest +from netqasm.runtime.application import default_app_instance from simulaqron.sdk.connection import SimulaQronConnection -from netqasm.sdk import Qubit +from netqasm.sdk import Qubit, EPRSocket +from simulaqron.sdk.socket import Socket +from simulaqron.run.run import run_applications from simulaqron.network import Network from simulaqron.settings import simulaqron_settings +from simulaqron.run.run import reset def calc_exp_values(q): @@ -100,44 +103,64 @@ def prep_CPHASE_target(conn): return q2 -def prep_EPR1(conn): - with SimulaQronConnection("Alice", appID=1) as Alice: - qA = Alice.createEPR("Bob") - qB = conn.recvEPR() - qA.measure() - return qB +def EPR_Alice(): + epr_socket = EPRSocket("Bob") + with SimulaQronConnection("Alice", epr_sockets=[epr_socket]): + qA = epr_socket.create_keep()[0] + m = qA.measure() + # "flush" is not necessary, since it is triggered when exiting the context. + return m -def prep_EPR2(conn): - with SimulaQronConnection("Alice", appID=1) as Alice: - qB = conn.createEPR("Alice", remote_appID=1) - qA = Alice.recvEPR() - qA.measure() - return qB +def EPR_Bob(): + epr_socket = EPRSocket("Alice") + with SimulaQronConnection("Bob", epr_sockets=[epr_socket]): + qB = epr_socket.recv_keep()[0] + m = qB.measure() + # "flush" is not necessary, since it is triggered when exiting the context. + return m -def prep_send(conn): - with SimulaQronConnection("Alice", appID=1) as Alice: - qA = Qubit(conn) - qB = Qubit(conn) - qA.H() - qA.cnot(qB) - conn.sendQubit(qA, "Alice", remote_appID=1) - qA = Alice.recvQubit() - m = qA.measure() - if m == 1: - qB.X() - qB.H() - return qB +def teleport_alice(): + socket = Socket("Alice", "Bob") + epr_socket = EPRSocket("Bob") + with SimulaQronConnection("Alice", epr_sockets=[epr_socket]) as alice: + # Create a qubit + q = Qubit(alice) + q.H() + + # Create entanglement + epr = epr_socket.create_keep()[0] + + # Teleport + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + + # Send the correction information + msg = str((int(m1), int(m2))) + socket.send(msg) + return m1, m2 + +def teleport_bob(): + socket = Socket("Bob", "Alice") + epr_socket = EPRSocket("Alice") + with SimulaQronConnection("Bob", epr_sockets=[epr_socket]) as bob: + epr = epr_socket.recv_keep()[0] + bob.flush() -def prep_recv(conn): - with SimulaQronConnection("Alice", appID=1) as Alice: - qA = Qubit(Alice) - qA.H() - Alice.sendQubit(qA, "Bob") - qB = conn.recvQubit() - return qB + # Get the corrections + msg = socket.recv() + + m1, m2 = eval(msg) + if m2 == 1: + epr.X() + if m1 == 1: + epr.Z() + meas = epr.measure() + return meas def prep_mixed_state(): @@ -152,96 +175,74 @@ def prep_H_state(): return np.dot(q2, np.transpose(np.conj(q2))) -@unittest.skip("We can test these things better when we have implemented a get_qubit_state function for simulaqron") -class TwoQubitGateTest(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.iterations = 100 - sys.stdout.write("Testing two qubit gates gates with {} iterations \r\n".format(cls.iterations)) +# TODO - We can test these things better when we have implemented a get_qubit_state function for simulaqron +# for now, we will perform tests based on the tomography function. +class TestTwoQubitGates: + iterations = 100 + + @pytest.fixture + def network(self): + print(f"Testing two qubit gates with {self.iterations} iterations\n") simulaqron_settings.default_settings() - cls.network = Network(nodes=["Alice", "Bob"], force=True) - cls.network.start() + network = Network(nodes=["Alice", "Bob"], force=True) + network.start(wait_until_running=True) + yield network - @classmethod - def tearDownClass(cls): - cls.network.stop() + network.stop() simulaqron_settings.default_settings() + reset() - def testCNOTControl(self): - with SimulaQronConnection("Bob", appID=0) as conn: + def test_CNOT_control(self, network): + with SimulaQronConnection("Bob") as conn: # Test CNOT control - sys.stdout.write("Testing CNOT control:") exp_values = calc_exp_values(prep_mixed_state()) ans = conn.test_preparation(prep_CNOT_control, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + assert ans - def testCNOTTarget(self): - with SimulaQronConnection("Bob", appID=0) as conn: + def test_CNOT_target(self, network): + with SimulaQronConnection("Bob") as conn: # Test CNOT target - sys.stdout.write("Testing CNOT target:") exp_values = calc_exp_values(prep_mixed_state()) ans = conn.test_preparation(prep_CNOT_target, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + assert ans - def testCPHASEControl(self): - with SimulaQronConnection("Bob", appID=0) as conn: + def test_CPHASE_control(self, network): + with SimulaQronConnection("Bob") as conn: # Test CPHASE control - sys.stdout.write("Testing CPHASE control:") exp_values = calc_exp_values(prep_mixed_state()) ans = conn.test_preparation(prep_CPHASE_control, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) + assert ans - def testCPHASETarget(self): - with SimulaQronConnection("Bob", appID=0) as conn: + def test_CPHASE_target(self, network): + with SimulaQronConnection("Bob") as conn: # Test CPHASE target - sys.stdout.write("Testing CPHASE target:") exp_values = calc_exp_values(prep_mixed_state()) ans = conn.test_preparation(prep_CPHASE_target, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) - - def testEPR1(self): - with SimulaQronConnection("Bob", appID=0) as conn: - # Test EPR1 - sys.stdout.write("Testing EPR1:") - exp_values = calc_exp_values(prep_mixed_state()) - ans = conn.test_preparation(prep_EPR1, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) - - def testEPR2(self): - with SimulaQronConnection("Bob", appID=0) as conn: - # Test EPR2 - sys.stdout.write("Testing EPR2:") - exp_values = calc_exp_values(prep_mixed_state()) - ans = conn.test_preparation(prep_EPR2, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) - - def testSendControl(self): - with SimulaQronConnection("Bob", appID=0) as conn: - # Test send control - sys.stdout.write("Testing send:") - exp_values = calc_exp_values(prep_H_state()) - ans = conn.test_preparation(prep_send, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) - - def testRevTarget(self): - with SimulaQronConnection("Bob", appID=0) as conn: - # Test recv target - sys.stdout.write("Testing recv:") - exp_values = calc_exp_values(prep_H_state()) - ans = conn.test_preparation(prep_recv, exp_values, iterations=self.iterations) - sys.stdout.write("\r") - self.assertTrue(ans) - - -################################################################################################## - -if __name__ == "__main__": - unittest.main() + assert ans + + # Tests using multiple nodes + + def test_EPRS(self, network): + apps = default_app_instance( + [ + ("Alice", EPR_Alice), + ("Bob", EPR_Bob) + ] + ) + results = run_applications(apps, use_app_config=False, enable_logging=False, num_rounds=self.iterations) + # both sides MUST measure the same state + assert int(results[0]["app_Alice"]) == int(results[0]["app_Bob"]) + + def test_teleport(self, network): + # To avoid stalling the simulation, the applications *need* to run + # in parallel. For this reason, we use the "run_applications" method + # which spawns a process for each node + apps = default_app_instance( + [ + ("Alice", teleport_alice), + ("Bob", teleport_bob) + ] + ) + results = run_applications(apps, use_app_config=False, enable_logging=False, num_rounds=self.iterations) + #print(results) From b319b7dc706cf44ed2cbc47c4db488bdb4b39513 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 24 Jul 2025 13:02:29 +0200 Subject: [PATCH 013/277] Add small example for using the netqasm SDK to create SimulaQron applications --- examples/netqasm/teleport/teleport.py | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 examples/netqasm/teleport/teleport.py diff --git a/examples/netqasm/teleport/teleport.py b/examples/netqasm/teleport/teleport.py new file mode 100644 index 00000000..0f485bee --- /dev/null +++ b/examples/netqasm/teleport/teleport.py @@ -0,0 +1,53 @@ +from netqasm.runtime.settings import set_simulator + +set_simulator("simulaqron") + +from netqasm.runtime.application import default_app_instance +from netqasm.sdk.external import NetQASMConnection +from netqasm.sdk import Qubit, EPRSocket + +from simulaqron.run.run import run_applications + +def run_alice(): + epr_socket: EPRSocket = EPRSocket("Bob") + with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: + # Create a qubit + q = Qubit(alice) + q.H() + + # Create entanglement + epr = epr_socket.create_keep()[0] + + # Teleport + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + return m1, m2 + + +def run_bob(): + epr_socket: EPRSocket = EPRSocket("Alice") + with NetQASMConnection("Bob", epr_sockets=[epr_socket]): + entangled_qubit = epr_socket.recv_keep()[0] + meas = entangled_qubit.measure() + return meas + +if __name__ == "__main__": + apps = default_app_instance( + [ + ("Alice", run_alice), + ("Bob", run_bob) + ] + ) + raw_results = run_applications(apps, use_app_config=False, enable_logging=False) + + results = {} + + for name, raw_result in raw_results[0].items(): + if isinstance(raw_result, tuple): + results[name] = tuple(int(result) for result in raw_result) + else: + results[name] = int(raw_result) + + print(results) From 2c28c3092057d9c0b018a814a0f6dc8929e14a0b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 24 Jul 2025 13:07:24 +0200 Subject: [PATCH 014/277] Linter changes --- examples/netqasm/teleport/teleport.py | 10 ++++++---- simulaqron/toolbox/stabilizer_states.py | 2 +- tests/slow/sdk/test_two_qubit.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/netqasm/teleport/teleport.py b/examples/netqasm/teleport/teleport.py index 0f485bee..f41b93e0 100644 --- a/examples/netqasm/teleport/teleport.py +++ b/examples/netqasm/teleport/teleport.py @@ -2,11 +2,12 @@ set_simulator("simulaqron") -from netqasm.runtime.application import default_app_instance -from netqasm.sdk.external import NetQASMConnection -from netqasm.sdk import Qubit, EPRSocket +from netqasm.runtime.application import default_app_instance # noqa: E402 +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import Qubit, EPRSocket # noqa: E402 + +from simulaqron.run.run import run_applications # noqa: E402 -from simulaqron.run.run import run_applications def run_alice(): epr_socket: EPRSocket = EPRSocket("Bob") @@ -33,6 +34,7 @@ def run_bob(): meas = entangled_qubit.measure() return meas + if __name__ == "__main__": apps = default_app_instance( [ diff --git a/simulaqron/toolbox/stabilizer_states.py b/simulaqron/toolbox/stabilizer_states.py index 49d4aa19..b2828adf 100644 --- a/simulaqron/toolbox/stabilizer_states.py +++ b/simulaqron/toolbox/stabilizer_states.py @@ -217,7 +217,7 @@ def __repr__(self): return "StabilizerState(np." + self._group.__repr__() + ")" def __str__(self): - to_return = f"Stabilizer state on {self.num_qubits} with the following stabilizer generators:\n" + to_return = f"Stabilizer state on {self.num_qubits} with the following stabilizer generators: \n" for row_str in self.to_string().split('\n'): to_return += f"\t{row_str}\n" return to_return[:-1] diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index 671e2bbc..a1a6d730 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -244,5 +244,5 @@ def test_teleport(self, network): ("Bob", teleport_bob) ] ) - results = run_applications(apps, use_app_config=False, enable_logging=False, num_rounds=self.iterations) - #print(results) + _ = run_applications(apps, use_app_config=False, enable_logging=False, num_rounds=self.iterations) + #print(_) From 481619a7dbba41a01f87260bb4a651f05382eb89 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 4 Aug 2025 16:40:20 +0200 Subject: [PATCH 015/277] Fix dependencies for qutip --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8e7c248c..97a81dd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ test = [ ] opt = [ "projectq<=0.8.0", - "qutip" + "qutip<5.0.0" ] dev = [ "sphinx>=7.2.6, <8.0", From 705c377a94aa7345bdb71e118e2acaf00cc701fb Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 4 Aug 2025 16:41:47 +0200 Subject: [PATCH 016/277] Add support for using "run_applications" to execute multiple times the same instance --- simulaqron/run/run.py | 156 +++++++++++++++++++------------ tests/slow/sdk/test_two_qubit.py | 6 +- 2 files changed, 97 insertions(+), 65 deletions(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 0bd5272b..3004cb69 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -3,6 +3,7 @@ from concurrent.futures import ProcessPoolExecutor as Pool from importlib import reload from time import sleep +from typing import Callable, Optional, Any, Dict, List from netqasm.logging.glob import get_netqasm_logger from netqasm.logging.output import (reset_struct_loggers, @@ -59,44 +60,70 @@ def reset(save_loggers=False): reload(logging) -def check_sim_backend(sim_backend): +def check_sim_backend(sim_backend: SimBackend): if sim_backend in [SimBackend.PROJECTQ, SimBackend.QUTIP]: assert has_module.main(sim_backend.value), f"To use {sim_backend} as backend you need to install the package" -def run_sim_backend(node_names, sim_backend): +def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_config_file: str): logger.debug("Starting simulaqron sim_backend process with nodes %s", node_names) - check_sim_backend(sim_backend=sim_backend) + check_sim_backend(sim_backend) simulaqron_settings.sim_backend = sim_backend.value - network = Network(name="default", nodes=node_names, force=True, new=True) + network = Network(name="default", nodes=node_names, network_config_file=network_config_file, force=True, new=True) network.start() return network def run_applications( app_instance: ApplicationInstance, - num_rounds=1, - network_cfg=None, - log_cfg=None, - results_file=None, - formalism=Formalism.KET, - post_function=None, - flavour=None, - enable_logging=True, - hardware=None, - use_app_config=True, # whether to give app_config as argument to app's main() -): + num_rounds: int = 1, + network_cfg: str = None, # WARNING - The type of this argument *cannot* be harmonized + nv_cfg: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" + log_cfg: LogConfig = None, + formalism: Formalism = Formalism.KET, + use_app_config: bool = True, + post_function: Optional[Callable] = None, + enable_logging: bool = True, + hardware: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" +) -> List[Dict[str, Any]]: """Executes functions containing application scripts, Parameters ---------- - applications : dict + app_instance : ApplicationInstance Keys should be names of nodes Values should be the functions + num_rounds : int + Number executions for this simulation + network_cfg: + Path of the network configuration file. + nv_cfg: Any + Unused argument. Any parameter given here will be ignored. + log_cfg: LogConfig + Configuration for the logging. + formalism: Formalism + Qubit formalism to use for the simulation. On this value depends + The SimulaQron backend to use. + use_app_config: bool + Whether to give app_config as argument to app's main() + post_function: Optional[Callable] + Function to execute after all rounds have been executed. + enable_logging: bool + Whether to enable logging. + hardware: Any + Unused argument. Any parameter given here will be ignored. + + Returns + ------- + List[Dict[str, Any]] + List of dictionaries describing the application names and the simulation results. + The i-th entry of the list will correspond to the i-th execution round of the + simulation. """ # app_names = [app_cfg.app_name for app_cfg in app_cfgs] - app_names = [program.party for program in app_instance.app.programs] - sim_backend = _SIMULAQRON_BACKENDS[formalism] + app_names: List[str] = [program.party for program in app_instance.app.programs] + sim_backend: SimBackend = _SIMULAQRON_BACKENDS[formalism] + timed_log_dir: str = "" if enable_logging: log_cfg = LogConfig() if log_cfg is None else log_cfg @@ -112,54 +139,59 @@ def run_applications( app_instance.logging_cfg.log_subroutines_dir = timed_log_dir app_instance.logging_cfg.comm_log_dir = timed_log_dir - with Pool(len(app_names)) as executor: - # Start the backend process - network = run_sim_backend(app_names, sim_backend=sim_backend) - - # Start the application processes - app_futures = [] - - programs = app_instance.app.programs - for program in programs: - inputs = app_instance.program_inputs[program.party] - if use_app_config: - app_cfg = AppConfig( - app_name=program.party, - node_name=program.party, # node name should be same as app name - main_func=program.entry, - log_config=app_instance.logging_cfg, - inputs=inputs, - ) - inputs["app_config"] = app_cfg - future = executor.submit(program.entry, **inputs) - app_futures.append(future) - - # for app_cfg in app_cfgs: - # inputs = app_cfg.inputs - # if use_app_config: - # inputs['app_config'] = app_cfg - # future = executor.submit(app_cfg.main_func, **inputs) - # app_futures.append(future) - - # Join the application processes and the backend - names = [f'app_{app_name}' for app_name in app_names] - results = {} - for future, name in as_completed(app_futures, names=names): - results[name] = future.result() - # if results_file is not None: - # save_results(results=results, results_file=results_file) - if enable_logging: - assert timed_log_dir is not None - path = os.path.join(timed_log_dir, "results.yaml") - dump_yaml(data=results, file_path=path) - - network.stop() + results: List[Dict[str, Any]] = [] + + for _ in range(num_rounds): + with Pool(len(app_names)) as executor: + # Start the backend process + network = run_sim_backend(app_names, sim_backend, network_cfg) + + # Start the application processes + app_futures = [] + + programs = app_instance.app.programs + for program in programs: + inputs = app_instance.program_inputs[program.party] + if use_app_config: + app_cfg = AppConfig( + app_name=program.party, + node_name=program.party, # node name should be same as app name + main_func=program.entry, + log_config=app_instance.logging_cfg, + inputs=inputs, + ) + inputs["app_config"] = app_cfg + future = executor.submit(program.entry, **inputs) + app_futures.append(future) + + # for app_cfg in app_cfgs: + # inputs = app_cfg.inputs + # if use_app_config: + # inputs['app_config'] = app_cfg + # future = executor.submit(app_cfg.main_func, **inputs) + # app_futures.append(future) + + # Join the application processes and the backend + names = [f'app_{app_name}' for app_name in app_names] + result = {} + for future, name in as_completed(app_futures, names=names): + result[name] = future.result() + # if results_file is not None: + # save_results(results=results, results_file=results_file) + if enable_logging: + assert timed_log_dir is not None + path = os.path.join(timed_log_dir, "results.yaml") + dump_yaml(data=result, file_path=path) + + results.append(result) + network.stop() + + reset(save_loggers=True) if enable_logging: process_logs.make_last_log(log_dir=timed_log_dir) - reset(save_loggers=True) - return [results] + return results def save_results(results, results_file): diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index a1a6d730..660f6ecb 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -178,7 +178,7 @@ def prep_H_state(): # TODO - We can test these things better when we have implemented a get_qubit_state function for simulaqron # for now, we will perform tests based on the tomography function. class TestTwoQubitGates: - iterations = 100 + iterations = 1 @pytest.fixture def network(self): @@ -223,7 +223,7 @@ def test_CPHASE_target(self, network): # Tests using multiple nodes - def test_EPRS(self, network): + def test_EPRS(self): apps = default_app_instance( [ ("Alice", EPR_Alice), @@ -234,7 +234,7 @@ def test_EPRS(self, network): # both sides MUST measure the same state assert int(results[0]["app_Alice"]) == int(results[0]["app_Bob"]) - def test_teleport(self, network): + def test_teleport(self): # To avoid stalling the simulation, the applications *need* to run # in parallel. For this reason, we use the "run_applications" method # which spawns a process for each node From ee7067ff7e7e0d0fb2f5b93a1f37e6dc2bcd157a Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 4 Aug 2025 17:04:18 +0200 Subject: [PATCH 017/277] Fix error when trying to use a network_config argument with the run_applications method that is not a string. --- simulaqron/run/run.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 3004cb69..00875054 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -2,6 +2,8 @@ import os from concurrent.futures import ProcessPoolExecutor as Pool from importlib import reload +from os import PathLike +from pathlib import Path from time import sleep from typing import Callable, Optional, Any, Dict, List @@ -140,11 +142,17 @@ def run_applications( app_instance.logging_cfg.comm_log_dir = timed_log_dir results: List[Dict[str, Any]] = [] + if isinstance(network_cfg, str) or isinstance(network_cfg, PathLike): + net_cfg = str(network_cfg) + elif isinstance(network_cfg, Path): + net_cfg = str(network_cfg.resolve()) + else: + net_cfg = None for _ in range(num_rounds): with Pool(len(app_names)) as executor: # Start the backend process - network = run_sim_backend(app_names, sim_backend, network_cfg) + network = run_sim_backend(app_names, sim_backend, net_cfg) # Start the application processes app_futures = [] From cedbebd577e61c234b8b5d5bd9638f3d1ff2bba8 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 5 Aug 2025 14:01:03 +0200 Subject: [PATCH 018/277] Add example for distributed teleport --- .../distributed/teleport/network-alice.json | 35 +++++++++++++ .../distributed/teleport/network-bob.json | 35 +++++++++++++ .../distributed/teleport/teleport-alice.py | 51 +++++++++++++++++++ examples/distributed/teleport/teleport-bob.py | 41 +++++++++++++++ simulaqron/run/run.py | 4 +- 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 examples/distributed/teleport/network-alice.json create mode 100644 examples/distributed/teleport/network-bob.json create mode 100644 examples/distributed/teleport/teleport-alice.py create mode 100644 examples/distributed/teleport/teleport-bob.py diff --git a/examples/distributed/teleport/network-alice.json b/examples/distributed/teleport/network-alice.json new file mode 100644 index 00000000..1156091d --- /dev/null +++ b/examples/distributed/teleport/network-alice.json @@ -0,0 +1,35 @@ +{ + "default": { + "nodes": { + "Alice": { + "app_socket": [ + "192.168.0.160", + 8001 + ], + "qnodeos_socket": [ + "192.168.0.160", + 8005 + ], + "vnode_socket": [ + "192.168.0.160", + 8020 + ] + }, + "Bob": { + "app_socket": [ + "192.168.20.249", + 8032 + ], + "qnodeos_socket": [ + "192.168.20.249", + 8037 + ], + "vnode_socket": [ + "192.168.20.249", + 8050 + ] + } + }, + "topology": null + } +} \ No newline at end of file diff --git a/examples/distributed/teleport/network-bob.json b/examples/distributed/teleport/network-bob.json new file mode 100644 index 00000000..16b99a21 --- /dev/null +++ b/examples/distributed/teleport/network-bob.json @@ -0,0 +1,35 @@ +{ + "default": { + "nodes": { + "Alice": { + "app_socket": [ + "localhost", + 8001 + ], + "qnodeos_socket": [ + "localhost", + 8005 + ], + "vnode_socket": [ + "localhost", + 8020 + ] + }, + "Bob": { + "app_socket": [ + "localhost", + 8032 + ], + "qnodeos_socket": [ + "localhost", + 8037 + ], + "vnode_socket": [ + "localhost", + 8050 + ] + } + }, + "topology": null + } +} \ No newline at end of file diff --git a/examples/distributed/teleport/teleport-alice.py b/examples/distributed/teleport/teleport-alice.py new file mode 100644 index 00000000..14cb251d --- /dev/null +++ b/examples/distributed/teleport/teleport-alice.py @@ -0,0 +1,51 @@ +from pathlib import Path + +from netqasm.runtime.settings import set_simulator +from netqasm.runtime.application import default_app_instance + +from simulaqron.run.run import run_applications + +set_simulator("simulaqron") + +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import Qubit, EPRSocket # noqa: E402 + + +def run_alice(): + epr_socket: EPRSocket = EPRSocket("Bob") + with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: + # Create a qubit + q = Qubit(alice) + q.H() + + # Create entanglement + epr = epr_socket.create_keep()[0] + + # Teleport + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + return m1, m2 + + +if __name__ == "__main__": + apps = default_app_instance( + [ + ("Alice", run_alice) + ] + ) + network_cfg_path = Path() / "network-alice.json" + raw_results = run_applications( + apps, use_app_config=False, enable_logging=False, network_cfg=network_cfg_path.resolve() + ) + + results = {} + + for name, raw_result in raw_results[0].items(): + if isinstance(raw_result, tuple): + results[name] = tuple(int(result) for result in raw_result) + else: + results[name] = int(raw_result) + + print(results) diff --git a/examples/distributed/teleport/teleport-bob.py b/examples/distributed/teleport/teleport-bob.py new file mode 100644 index 00000000..70e9a087 --- /dev/null +++ b/examples/distributed/teleport/teleport-bob.py @@ -0,0 +1,41 @@ +from pathlib import Path + +from netqasm.runtime.settings import set_simulator + +from netqasm.runtime.application import default_app_instance + +from simulaqron.run.run import run_applications + +set_simulator("simulaqron") +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +def run_bob(): + epr_socket: EPRSocket = EPRSocket("Alice") + with NetQASMConnection("Bob", epr_sockets=[epr_socket]): + entangled_qubit = epr_socket.recv_keep()[0] + meas = entangled_qubit.measure() + return meas + + +if __name__ == "__main__": + apps = default_app_instance( + [ + ("Bob", run_bob) + ] + ) + network_cfg_path = Path() / "network-alice.json" + raw_results = run_applications( + apps, use_app_config=False, enable_logging=False, network_cfg=network_cfg_path.resolve() + ) + + results = {} + + for name, raw_result in raw_results[0].items(): + if isinstance(raw_result, tuple): + results[name] = tuple(int(result) for result in raw_result) + else: + results[name] = int(raw_result) + + print(results) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 00875054..57f3c2c9 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -5,7 +5,7 @@ from os import PathLike from pathlib import Path from time import sleep -from typing import Callable, Optional, Any, Dict, List +from typing import Callable, Optional, Any, Dict, List, Union from netqasm.logging.glob import get_netqasm_logger from netqasm.logging.output import (reset_struct_loggers, @@ -79,7 +79,7 @@ def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_conf def run_applications( app_instance: ApplicationInstance, num_rounds: int = 1, - network_cfg: str = None, # WARNING - The type of this argument *cannot* be harmonized + network_cfg: Union[str, PathLike, Path] = None, # WARNING - The type of this argument *cannot* be harmonized nv_cfg: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" log_cfg: LogConfig = None, formalism: Formalism = Formalism.KET, From 6853e966aec86f6bf8faa2e49d9ab28d1e3002b7 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 5 Aug 2025 14:34:24 +0200 Subject: [PATCH 019/277] Fixed bugs when running applications with specific network config --- examples/distributed/teleport/teleport-alice.py | 2 +- examples/distributed/teleport/teleport-bob.py | 2 +- simulaqron/run/run.py | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/distributed/teleport/teleport-alice.py b/examples/distributed/teleport/teleport-alice.py index 14cb251d..6166e392 100644 --- a/examples/distributed/teleport/teleport-alice.py +++ b/examples/distributed/teleport/teleport-alice.py @@ -35,7 +35,7 @@ def run_alice(): ("Alice", run_alice) ] ) - network_cfg_path = Path() / "network-alice.json" + network_cfg_path = Path(__file__).parent / "network-alice.json" raw_results = run_applications( apps, use_app_config=False, enable_logging=False, network_cfg=network_cfg_path.resolve() ) diff --git a/examples/distributed/teleport/teleport-bob.py b/examples/distributed/teleport/teleport-bob.py index 70e9a087..08b17d14 100644 --- a/examples/distributed/teleport/teleport-bob.py +++ b/examples/distributed/teleport/teleport-bob.py @@ -25,7 +25,7 @@ def run_bob(): ("Bob", run_bob) ] ) - network_cfg_path = Path() / "network-alice.json" + network_cfg_path = Path(__file__).parent / "network-alice.json" raw_results = run_applications( apps, use_app_config=False, enable_logging=False, network_cfg=network_cfg_path.resolve() ) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 57f3c2c9..1a655fe2 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -67,11 +67,18 @@ def check_sim_backend(sim_backend: SimBackend): assert has_module.main(sim_backend.value), f"To use {sim_backend} as backend you need to install the package" -def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_config_file: str): +def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_config_file: Optional[str]): logger.debug("Starting simulaqron sim_backend process with nodes %s", node_names) check_sim_backend(sim_backend) simulaqron_settings.sim_backend = sim_backend.value - network = Network(name="default", nodes=node_names, network_config_file=network_config_file, force=True, new=True) + new_network = False if network_config_file is None else True + network = Network( + name="default", + nodes=node_names, + network_config_file=network_config_file, + force=True, + new=new_network + ) network.start() return network From e7f639e2a87d42f2ee2a965fe1d4eb8aec03e857 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 5 Aug 2025 14:50:39 +0200 Subject: [PATCH 020/277] Fixed bug to correctly create a new network when network config is not given --- simulaqron/run/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 1a655fe2..7629f5aa 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -71,7 +71,7 @@ def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_conf logger.debug("Starting simulaqron sim_backend process with nodes %s", node_names) check_sim_backend(sim_backend) simulaqron_settings.sim_backend = sim_backend.value - new_network = False if network_config_file is None else True + new_network = True if network_config_file is None else False network = Network( name="default", nodes=node_names, From bbd5b0ab8c84ca56a42ab4fd5d1942be3bce16e0 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 5 Aug 2025 15:37:50 +0200 Subject: [PATCH 021/277] Correctly update the global static network config path if given --- simulaqron/run/run.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 7629f5aa..e90b2ffb 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -151,8 +151,12 @@ def run_applications( results: List[Dict[str, Any]] = [] if isinstance(network_cfg, str) or isinstance(network_cfg, PathLike): net_cfg = str(network_cfg) + # If given a network config path, we need to update the global static config property (really bad design choice) + simulaqron_settings.network_config_file = net_cfg elif isinstance(network_cfg, Path): net_cfg = str(network_cfg.resolve()) + # If given a network config path, we need to update the global static config property (really bad design choice) + simulaqron_settings.network_config_file = net_cfg else: net_cfg = None From 1188c1932e3c95b68b63e5f854c044b767072b7a Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 22 Aug 2025 13:11:29 +0200 Subject: [PATCH 022/277] Update executor to use multiprocess context (allows decorating more functions to be used as an application) --- pyproject.toml | 3 ++- simulaqron/run/run.py | 31 ++++++++++--------------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 97a81dd3..424c819d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ dependencies = [ "networkx>=3.0,<4.0", "click>=8.0,<9.0", "daemons>=1.3.0,<2.0.0", - "netqasm>=1.0.0,<2.0.0" + "netqasm>=1.0.0,<2.0.0", + "multiprocess>=0.70.18,<1.0" ] requires-python = ">=3.8,<3.13" authors = [ diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index e90b2ffb..1bf9e100 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -1,11 +1,10 @@ import logging import os -from concurrent.futures import ProcessPoolExecutor as Pool +from multiprocess.pool import Pool, ApplyResult from importlib import reload from os import PathLike from pathlib import Path -from time import sleep -from typing import Callable, Optional, Any, Dict, List, Union +from typing import Callable, Optional, Any, Dict, List, Union, Generator, Tuple from netqasm.logging.glob import get_netqasm_logger from netqasm.logging.output import (reset_struct_loggers, @@ -34,21 +33,11 @@ } -def as_completed(futures, names=None, sleep_time=0): - futures = list(futures) - if names is not None: - names = list(names) - while len(futures) > 0: - for i, future in enumerate(futures): - if future.done(): - futures.pop(i) - if names is None: - yield future - else: - name = names.pop(i) - yield future, name - if sleep_time > 0: - sleep(sleep_time) +def as_completed(futures: List[ApplyResult], names: List[str]) -> Generator[Tuple[ApplyResult, str], None, None]: + if len(futures) is not len(names): + raise RuntimeError("Not all registered applications have an associated name") + for future, name in zip(futures, names): + yield future, name def reset(save_loggers=False): @@ -180,7 +169,7 @@ def run_applications( inputs=inputs, ) inputs["app_config"] = app_cfg - future = executor.submit(program.entry, **inputs) + future: ApplyResult = executor.apply_async(program.entry, kwds=inputs) app_futures.append(future) # for app_cfg in app_cfgs: @@ -193,8 +182,8 @@ def run_applications( # Join the application processes and the backend names = [f'app_{app_name}' for app_name in app_names] result = {} - for future, name in as_completed(app_futures, names=names): - result[name] = future.result() + for future, name in as_completed(app_futures, names): + result[name] = future.get() # if results_file is not None: # save_results(results=results, results_file=results_file) if enable_logging: From 341bbff426592d87ef8597c8b4e0345c56ae43fa Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 25 Aug 2025 12:55:43 +0200 Subject: [PATCH 023/277] Add type hints for the classical socket --- simulaqron/general/host_config.py | 4 +- simulaqron/sdk/socket.py | 68 +++++++++++++++++++------------ 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index ea4bb04b..eb027f81 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -29,6 +29,8 @@ import socket import struct +from typing import Dict + from twisted.spread import pb from ipaddress import IPv4Address @@ -106,7 +108,7 @@ def __init__(self, filename, network_name="default", config_type="vnode"): Therefore one needs to also specify the network_name and config_type ('vnode', 'app') """ # Dictionary where we will keep host details, indexed by node name (e.g. Alice) - self.hostDict = {} + self.hostDict: Dict[str, Host] = {} # Read config file self.read_config(filename, network_name=network_name, config_type=config_type) diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index 01a13e55..89f91b00 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -1,10 +1,12 @@ -import pickle +import dill import socket import time +from typing import Optional from netqasm.logging.glob import get_netqasm_logger +from netqasm.sdk.classical_communication.message import StructuredMessage from netqasm.sdk.classical_communication.socket import Socket as _Socket -from simulaqron.general.host_config import SocketsConfig +from simulaqron.general.host_config import SocketsConfig, Host from simulaqron.settings import simulaqron_settings @@ -17,7 +19,7 @@ def __init__( app_name, remote_app_name, socket_id=0, - timeout=None, + timeout: Optional[int] = None, use_callbacks=False, network_name="default", log_config=None, @@ -31,29 +33,32 @@ def __init__( self._network_name = network_name self._logger = get_netqasm_logger(f"{self.__class__.__name__}({app_name} <-> {remote_app_name})") - self._app_socket = None - - self._connect() + self._app_socket: socket.socket = self._connect(timeout=timeout) def __del__(self): if self._app_socket: self._app_socket.close() - def send(self, msg): + def send(self, msg: str): """Sends a message to the remote node.""" self._logger.debug("Sending msg '%s'", msg) raw_msg = self._serialize_msg(msg=msg) self._app_socket.send(raw_msg) - def send_structured(self, msg): + def send_structured(self, msg: StructuredMessage): self._logger.debug("Sending structured msg '%s'", msg) raw_msg = self._serialize_structured_msg(msg=msg) self._app_socket.send(raw_msg) - def send_silent(self, msg): + def send_silent(self, msg: str): self.send(msg) - def recv(self, block=True, maxsize=1024): + def recv( + self, + block: bool = True, + timeout: Optional[float] = None, + maxsize: Optional[int] = 1024 + ) -> str: """Receive a message from the remote node.""" self._logger.debug("Receiving msg") self._app_socket.setblocking(block) @@ -64,7 +69,12 @@ def recv(self, block=True, maxsize=1024): self._logger.debug("Msg '%s' received", msg) return msg - def recv_structured(self, block=True, maxsize=1024): + def recv_structured( + self, + block: bool = True, + timeout: Optional[float] = None, + maxsize: Optional[int] = 1024, + ) -> StructuredMessage: self._logger.debug("Receiving structured msg") self._app_socket.setblocking(block) raw_msg = self._app_socket.recv(maxsize) @@ -74,31 +84,36 @@ def recv_structured(self, block=True, maxsize=1024): self._logger.debug("Msg '%s' received", msg) return msg - def recv_silent(self): + def recv_silent( + self, + block: bool = True, + timeout: Optional[float] = None, + maxsize: Optional[int] = None, + ) -> str: return self.recv() @staticmethod - def _serialize_msg(msg): + def _serialize_msg(msg: str): return msg.encode('utf-8') @staticmethod - def _deserialize_msg(raw_msg): + def _deserialize_msg(raw_msg: bytes): return raw_msg.decode('utf-8') @staticmethod - def _serialize_structured_msg(msg): - return pickle.dumps(msg) + def _serialize_structured_msg(msg: StructuredMessage): + return dill.dumps(msg) - @ staticmethod - def _deserialize_structured_msg(raw_msg): - return pickle.loads(raw_msg) + @staticmethod + def _deserialize_structured_msg(raw_msg: bytes) -> StructuredMessage: + return dill.loads(raw_msg) - @ property - def is_server(self): + @property + def is_server(self) -> bool: # Server will always be the "first" return self._node_name < self._remote_node_name - def _connect(self): + def _connect(self, timeout: Optional[int] = None) -> socket.socket: if self.is_server: server_name = self._node_name else: @@ -122,7 +137,7 @@ def _connect(self): break app_socket.listen(1) conn, _ = app_socket.accept() - self._app_socket = conn + connected_socket = conn else: self._logger.debug("Trying to open application socket as client") while True: @@ -136,18 +151,19 @@ def _connect(self): time.sleep(self.RETRY_TIME) else: break - self._app_socket = app_socket + connected_socket = app_socket self._logger.debug("Application socket opened") + return connected_socket def _get_addr_info(self, name): app_net = self._get_app_net_config() - remote_host = app_net.hostDict.get(name) + remote_host: Host = app_net.hostDict.get(name) if remote_host is None: raise ValueError(f"Host name '{name}' is not in the app network") return remote_host.addr - def _get_app_net_config(self): + def _get_app_net_config(self) -> SocketsConfig: network_config_file = simulaqron_settings.network_config_file app_net = SocketsConfig(network_config_file, network_name=self._network_name, config_type="app") return app_net From d062d0b7656e1a859b89ae50aecd4860070d1295 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 25 Aug 2025 14:05:18 +0200 Subject: [PATCH 024/277] Implement timeouts in the classical sockets --- pyproject.toml | 3 ++- simulaqron/sdk/socket.py | 30 ++++++++++++++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 424c819d..07e019a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,8 +10,9 @@ name = "simulaqron" dynamic = ["version"] dependencies = [ "numpy>=1.14.0,<2.0.0", + "dill>=0.4.0,<1.0.0", "scipy>=1.1.0,<2.0.0", - "twisted>=24.0.0, <26.0.0", + "twisted>=24.0.0,<26.0.0", "networkx>=3.0,<4.0", "click>=8.0,<9.0", "daemons>=1.3.0,<2.0.0", diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index 89f91b00..be2b367a 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -33,7 +33,8 @@ def __init__( self._network_name = network_name self._logger = get_netqasm_logger(f"{self.__class__.__name__}({app_name} <-> {remote_app_name})") - self._app_socket: socket.socket = self._connect(timeout=timeout) + self._timeout = timeout + self._app_socket: socket.socket = self._connect() def __del__(self): if self._app_socket: @@ -53,6 +54,19 @@ def send_structured(self, msg: StructuredMessage): def send_silent(self, msg: str): self.send(msg) + def _base_recv(self, block: bool, timeout: float, maxsize: int) -> bytes: + if block: + self._app_socket.setblocking(block) + + old_timeout = self._app_socket.gettimeout() + self._app_socket.settimeout(timeout) + raw_msg = self._app_socket.recv(maxsize) + self._app_socket.settimeout(old_timeout) + + if not block and not raw_msg: + raise RuntimeError("No message to receive (not blocking)") + return raw_msg + def recv( self, block: bool = True, @@ -61,10 +75,7 @@ def recv( ) -> str: """Receive a message from the remote node.""" self._logger.debug("Receiving msg") - self._app_socket.setblocking(block) - raw_msg = self._app_socket.recv(maxsize) - if not block and not raw_msg: - raise RuntimeError("No message to receive (not blocking)") + raw_msg = self._base_recv(block, timeout, maxsize) msg = self._deserialize_msg(raw_msg=raw_msg) self._logger.debug("Msg '%s' received", msg) return msg @@ -76,10 +87,7 @@ def recv_structured( maxsize: Optional[int] = 1024, ) -> StructuredMessage: self._logger.debug("Receiving structured msg") - self._app_socket.setblocking(block) - raw_msg = self._app_socket.recv(maxsize) - if not block and not raw_msg: - raise RuntimeError("No message to receive (not blocking)") + raw_msg = self._base_recv(block, timeout, maxsize) msg = self._deserialize_structured_msg(raw_msg=raw_msg) self._logger.debug("Msg '%s' received", msg) return msg @@ -113,7 +121,7 @@ def is_server(self) -> bool: # Server will always be the "first" return self._node_name < self._remote_node_name - def _connect(self, timeout: Optional[int] = None) -> socket.socket: + def _connect(self) -> socket.socket: if self.is_server: server_name = self._node_name else: @@ -136,12 +144,14 @@ def _connect(self, timeout: Optional[int] = None) -> socket.socket: else: break app_socket.listen(1) + app_socket.settimeout(self._timeout) conn, _ = app_socket.accept() connected_socket = conn else: self._logger.debug("Trying to open application socket as client") while True: try: + app_socket.settimeout(self._timeout) app_socket.connect(addr[4]) except ConnectionRefusedError: self._logger.debug( From 179ce465579af5bbec1538310fb6fb3d046eb8c9 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 28 Aug 2025 15:57:02 +0200 Subject: [PATCH 025/277] Add some type hints in classes. Rename sone classes to match the pythonic naming conventions --- examples/netqasm/teleport/teleport.py | 1 + simulaqron/netqasm_backend/factory.py | 14 +- simulaqron/network.py | 4 +- simulaqron/sdk/connection.py | 144 ++++++++++-------- simulaqron/virtual_node/basics.py | 16 +- .../virtual_node/project_q_simulator.py | 28 ++-- simulaqron/virtual_node/quantum.py | 2 +- simulaqron/virtual_node/qutip_simulator.py | 18 +-- .../virtual_node/stabilizer_simulator.py | 18 +-- simulaqron/virtual_node/virtual.py | 82 +++++----- tests/quick/engine/test_project_q_engine.py | 46 +++--- tests/quick/engine/test_qutip_engine.py | 10 +- tests/quick/engine/test_stabilizer_engine.py | 42 ++--- 13 files changed, 230 insertions(+), 195 deletions(-) diff --git a/examples/netqasm/teleport/teleport.py b/examples/netqasm/teleport/teleport.py index f41b93e0..68d206ff 100644 --- a/examples/netqasm/teleport/teleport.py +++ b/examples/netqasm/teleport/teleport.py @@ -14,6 +14,7 @@ def run_alice(): with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: # Create a qubit q = Qubit(alice) + alice.flush() q.H() # Create entanglement diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 1d2e962a..6e263396 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -27,6 +27,7 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import sys +from typing import Type from twisted.internet import reactor from twisted.internet.defer import DeferredLock, inlineCallbacks @@ -36,6 +37,8 @@ from netqasm.logging.glob import get_netqasm_logger from netqasm.backend.messages import MessageHeader, ErrorMessage, ErrorCode, deserialize_host_msg +from simulaqron.general.host_config import SocketsConfig, Host +from simulaqron.netqasm_backend.qnodeos import SubroutineHandler from simulaqron.settings import simulaqron_settings from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor @@ -52,7 +55,7 @@ class NetQASMProtocol(Protocol): # (host_app_id,remote_node,remote_app_id) _next_ent_id = {} - def __init__(self, factory): + def __init__(self, factory: "NetQASMFactory"): # NetQASM Factory, including our connection to the SimulaQron backend self.factory = factory @@ -160,7 +163,14 @@ def _return_msg(self, msg): class NetQASMFactory(Factory): - def __init__(self, host, name, qnodeos_net, backend, network_name="default"): + def __init__( + self, + host: Host, + name: str, + qnodeos_net: SocketsConfig, + backend: Type[SubroutineHandler], + network_name: str = "default" + ): """ Initialize NetQASM Factory. diff --git a/simulaqron/network.py b/simulaqron/network.py index ea2104f7..dfa3653a 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -30,6 +30,8 @@ import time import random import multiprocessing as mp +from typing import List + import networkx as nx from timeit import default_timer as timer @@ -72,7 +74,7 @@ def __init__(self, name=None, nodes=None, topology=None, network_config_file=Non else: self.name = name - self.processes = [] + self.processes: List[mp.Process] = [] self._logger = get_netqasm_logger(f"{self.__class__.__name__}({self.name})") if network_config_file is None: diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 77336056..67a9d1c2 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -1,6 +1,7 @@ import socket import time -from typing import Type +from threading import Thread +from typing import Type, Optional, Callable, List, Tuple from netqasm.backend.messages import (ErrorMessage, MessageHeader, MsgDoneMessage, ReturnArrayMessage, @@ -8,13 +9,17 @@ from netqasm.lang.ir import GenericInstr from netqasm.lang.operand import Address, Register from netqasm.logging.glob import get_netqasm_logger +from netqasm.sdk import EPRSocket +from netqasm.sdk.config import LogConfig from netqasm.sdk.connection import BaseNetQASMConnection from netqasm.sdk.network import NetworkInfo from netqasm.sdk.shared_memory import SharedMemoryManager +from netqasm.sdk.transpile import SubroutineTranspiler + from simulaqron.general import SimUnsupportedError from simulaqron.general.host_config import (SocketsConfig, get_node_id_from_net_config) -from simulaqron.settings import SimBackend, simulaqron_settings +from simulaqron.settings import simulaqron_settings logger = get_netqasm_logger("SimulaQronConnection") @@ -25,15 +30,15 @@ class SimulaQronConnection(BaseNetQASMConnection): def __init__( self, - app_name, - app_id=None, - max_qubits=5, - log_config=None, - epr_sockets=None, - compiler=None, + app_name: str, + app_id: Optional[int] = None, + max_qubits: int = 5, + log_config: Optional[LogConfig] = None, + epr_sockets: Optional[List[EPRSocket]] = None, + compiler: Optional[Type[SubroutineTranspiler]] = None, socket_address=None, - conn_retry_time=0.1, - network_name=None, + conn_retry_time: float = 0.1, + network_name: Optional[str] = None, ): super().__init__( app_name=app_name, @@ -75,9 +80,9 @@ def __init__( @staticmethod def try_connection( - name, - socket_address=None, - network_name=None, + name: str, + socket_address: Optional[Tuple[str, int]] = None, + network_name: str = None, ): # NOTE using retry_time=None causes an error to be raised of the connection cannot # be established, which can be used to check if the connection is available @@ -91,12 +96,12 @@ def try_connection( @staticmethod def _create_socket( - name, - socket_address=None, - network_name=None, - retry_time=0.1, - ): - # Get network configuraton and addresses + name: str, + socket_address: Optional[Tuple[str, int]] = None, + network_name: str = None, + retry_time: Optional[float] = 0.1, + ) -> Tuple[SocketsConfig, socket.socket]: + # Get network configuration and addresses addr, qnodeos_net = SimulaQronConnection._setup_network_data( name=name, socket_address=socket_address, @@ -111,12 +116,11 @@ def _create_socket( @staticmethod def _setup_network_data( - name, - socket_address, - network_name, - ): - addr = None - qnodeos_net = None + name: str, + socket_address: Tuple[str, int], + network_name: str, + ) -> Tuple[tuple[socket.AddressFamily, socket.SocketKind, int, str, tuple[str, int]], Optional[SocketsConfig]]: + qnodeos_net: Optional[SocketsConfig] = None if socket_address is None: qnodeos_net = _get_qnodeos_net_config(network_name=network_name) @@ -131,7 +135,7 @@ def _setup_network_data( # Get IP and port number addr = myHost.addr - if socket_address is not None: + else: hostname, port = socket_address assert isinstance(hostname, str), "hostname should be a string" assert isinstance(port, int), "port should be an int" @@ -143,7 +147,11 @@ def _setup_network_data( return addr, qnodeos_net @staticmethod - def _setup_socket(name, addr, retry_time=0.1): + def _setup_socket( + name: str, + addr: tuple[socket.AddressFamily, socket.SocketKind, int, str, Tuple[str, int]], + retry_time: float = 0.1 + ) -> socket.socket: qnodeos_socket = None while True: try: @@ -158,7 +166,7 @@ def _setup_socket(name, addr, retry_time=0.1): if retry_time is None or retry_time == 0: raise err logger.debug( - "App %s : Could not connect to NetQASM server, trying again...", + "App %s : Could not connect to NetQASM server, trying again...", name ) time.sleep(retry_time) @@ -171,7 +179,7 @@ def _setup_socket(name, addr, retry_time=0.1): qnodeos_socket.close() raise err logger.debug( - "App %s : Could not connect to NetQASM server, trying again...", + "App %s : Could not connect to NetQASM server, trying again...", name ) return qnodeos_socket @@ -179,19 +187,32 @@ def _setup_socket(name, addr, retry_time=0.1): def _get_network_info(self) -> Type[NetworkInfo]: return SimulaQronNetworkInfo - def _commit_serialized_message(self, raw_msg, block=True, callback=None): + def _commit_serialized_message( + self, raw_msg: bytes, block: bool = True, callback: Optional[Callable] = None + ): """Commit a message to the backend/qnodeos""" msg_id = self._get_new_msg_id() self._waiting_msg_ids.add(msg_id) length = MessageHeader.len() + len(raw_msg) msg_hdr = MessageHeader(id=msg_id, length=length) self._socket.send(bytes(msg_hdr) + raw_msg) - if callback is not None: - raise NotImplementedError("Callback not yet implemented") + # if callback is not None: + # raise NotImplementedError("Callback not yet implemented") if block: self._wait_for_done(msg_id=msg_id) + else: + # Execute callback in a new thread after the subroutine is finished + thread = Thread( + target=self._wait_for_done, + kwargs = { + "msg_id": msg_id, + "callback": callback, + } + ) + thread.daemon = True + thread.start() - def _wait_for_done(self, msg_id=None): + def _wait_for_done(self, msg_id: Optional[int] = None, callback: Optional[Callable] = None): """Waits for a message to be declared done by qnodeos. If `msg_id` is None (default), then we wait once for any message to be done. The ID of this message is then returned. @@ -207,6 +228,9 @@ def _wait_for_done(self, msg_id=None): break elif msg_id == done_msg_id: # Finished waiting for specified message + if callback is not None: + self._logger.debug("Executing callback for message %d", done_msg_id) + callback() break else: # Other message done, not the one we're waiting for @@ -284,30 +308,30 @@ def _update_shared_memory(self, entry, value): f"Cannot update shared memory with entry specified as {entry}" ) - def add_single_qubit_commands(self, instr, qubit_id): - # NOTE override to check that formalism supports operation - if instr in self.NON_STABILIZER_INSTR: - if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - raise SimUnsupportedError( - f"Cannot perform instr {instr} when using stabilizer formalism" - ) - super().add_single_qubit_commands(instr=instr, qubit_id=qubit_id) - - def add_single_qubit_rotation_commands( - self, instruction, virtual_qubit_id, n=0, d=0, angle=None - ): - # NOTE override to check that formalism supports operation - if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - raise SimUnsupportedError( - "Cannot perform rotations when using stabilizer formalism" - ) - super().add_single_qubit_rotation_commands( - instruction=instruction, - virtual_qubit_id=virtual_qubit_id, - n=n, - d=d, - angle=angle, - ) + # def add_single_qubit_commands(self, instr, qubit_id): + # # NOTE override to check that formalism supports operation + # if instr in self.NON_STABILIZER_INSTR: + # if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + # raise SimUnsupportedError( + # f"Cannot perform instr {instr} when using stabilizer formalism" + # ) + # super().add_single_qubit_commands(instr=instr, qubit_id=qubit_id) + # + # def add_single_qubit_rotation_commands( + # self, instruction, virtual_qubit_id, n=0, d=0, angle=None + # ): + # # NOTE override to check that formalism supports operation + # if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + # raise SimUnsupportedError( + # "Cannot perform rotations when using stabilizer formalism" + # ) + # super().add_single_qubit_rotation_commands( + # instruction=instruction, + # virtual_qubit_id=virtual_qubit_id, + # n=n, + # d=d, + # angle=angle, + # ) def _is_done(self, msg_id): return msg_id in self._done_msg_ids @@ -318,14 +342,12 @@ def _get_new_msg_id(self): return msg_id -def _get_qnodeos_net_config(network_name): +def _get_qnodeos_net_config(network_name: str) -> SocketsConfig: network_config_file = simulaqron_settings.network_config_file - qnodeos_net = SocketsConfig( + return SocketsConfig( network_config_file, network_name=network_name, config_type="qnodeos" ) - return qnodeos_net - class SimulaQronNetworkInfo(NetworkInfo): @classmethod diff --git a/simulaqron/virtual_node/basics.py b/simulaqron/virtual_node/basics.py index ff57299f..85206fa3 100644 --- a/simulaqron/virtual_node/basics.py +++ b/simulaqron/virtual_node/basics.py @@ -31,7 +31,7 @@ from twisted.spread import pb -class quantumError(pb.Error): +class QuantumError(pb.Error): def __init__(self, value): self.value = value @@ -39,11 +39,11 @@ def __str__(self): return repr(self.value) -class noQubitError(quantumError): +class NoQubitError(QuantumError): pass -class virtNetError(Exception): +class VirtNetError(Exception): def __init__(self, value): self.value = value @@ -51,7 +51,7 @@ def __str__(self): return repr(self.value) -class quantumEngine(pb.Referenceable): +class QuantumEngine(pb.Referenceable, abc.ABC): """ Basic quantum engine. Abstract class meant to be subclassed to implement different simulation backends. @@ -62,7 +62,7 @@ class quantumEngine(pb.Referenceable): maxQubits maximum number of qubits this register supports """ - def __init__(self, node, num, maxQubits=10): + def __init__(self, node: str, num: int, maxQubits: int = 10): """ Initialize the simple engine. If no number is given for maxQubits, the assumption will be 10. """ @@ -77,7 +77,7 @@ def __init__(self, node, num, maxQubits=10): self.simNode = node @abc.abstractmethod - def add_fresh_qubit(self): + def add_fresh_qubit(self) -> int: """ Add a new qubit initialized in the \|0\> state. :return: The qubit number @@ -86,7 +86,7 @@ def add_fresh_qubit(self): pass @abc.abstractmethod - def add_qubit(self, newQubit): + def add_qubit(self, newQubit) -> int: """ Add new qubit in the state described by the density matrix newQubit :return: The qubit number @@ -95,7 +95,7 @@ def add_qubit(self, newQubit): pass @abc.abstractmethod - def remove_qubit(self, qubitNum): + def remove_qubit(self, qubitNum: int) -> None: """ Removes the qubit with the desired number qubitNum :rtype: None diff --git a/simulaqron/virtual_node/project_q_simulator.py b/simulaqron/virtual_node/project_q_simulator.py index 056cfe11..00fe03a7 100644 --- a/simulaqron/virtual_node/project_q_simulator.py +++ b/simulaqron/virtual_node/project_q_simulator.py @@ -33,10 +33,10 @@ raise RuntimeError("If you want to use the projectq backend you need to install the python package 'projectq'") import numpy as np -from simulaqron.virtual_node.basics import quantumEngine, quantumError, noQubitError +from simulaqron.virtual_node.basics import QuantumEngine, QuantumError, NoQubitError -class projectQEngine(quantumEngine): +class ProjectQEngine(QuantumEngine): """ Basic quantum engine which uses ProjectQ. @@ -44,7 +44,7 @@ class projectQEngine(quantumEngine): maxQubits: maximum number of qubits this engine will support. """ - def __init__(self, node, num, maxQubits=10): + def __init__(self, node: str, num: int, maxQubits: int = 10): """ Initialize the simple engine. If no number is given for maxQubits, the assumption will be 10. """ @@ -67,13 +67,13 @@ def __del__(self): for _ in range(self.activeQubits): self.measure_qubit(0) - def add_fresh_qubit(self): + def add_fresh_qubit(self) -> int: """ Add a new qubit initialized in the \|0\> state. """ # Check if we are still allowed to add qubits if self.activeQubits >= self.maxQubits: - raise noQubitError("No more qubits available in register.") + raise NoQubitError("No more qubits available in register.") # Prepare a clean qubit state in |0> qubit = self.eng.allocate_qubit()[0] @@ -93,7 +93,7 @@ def add_qubit(self, newQubit): norm = np.dot(np.array(newQubit), np.array(newQubit).conj()) if not norm <= 1: - raise quantumError(f"State {newQubit} is not normalized.") + raise QuantumError(f"State {newQubit} is not normalized.") # Create a fresh qubit num = self.add_fresh_qubit() @@ -108,7 +108,7 @@ def remove_qubit(self, qubitNum): Removes the qubit with the desired number qubitNum """ if (qubitNum + 1) > self.activeQubits: - raise quantumError("No such qubit to remove") + raise QuantumError("No such qubit to remove") self.measure_qubit(qubitNum) @@ -220,7 +220,7 @@ def apply_onequbit_gate(self, gate, qubitNum): """ if (qubitNum + 1) > self.activeQubits: - raise quantumError("No such qubit to apply a single qubit gate to") + raise QuantumError("No such qubit to apply a single qubit gate to") gate | self.qubitReg[qubitNum] @@ -234,13 +234,13 @@ def apply_twoqubit_gate(self, gate, qubit1, qubit2): qubit2 the second qubit """ if (qubit1 + 1) > self.activeQubits: - raise quantumError("No such qubit to act as a control qubit") + raise QuantumError("No such qubit to act as a control qubit") if (qubit2 + 1) > self.activeQubits: - raise quantumError("No such qubit to act as a target qubit") + raise QuantumError("No such qubit to act as a target qubit") if qubit1 == qubit2: - raise quantumError("Control and target are equal") + raise QuantumError("Control and target are equal") gate | (self.qubitReg[qubit1], self.qubitReg[qubit2]) @@ -255,7 +255,7 @@ def measure_qubit_inplace(self, qubitNum): # Check we have such a qubit... if (qubitNum + 1) > self.activeQubits: - raise quantumError("No such qubit to be measured.") + raise QuantumError("No such qubit to be measured.") pQ.ops.Measure | self.qubitReg[qubitNum] @@ -295,7 +295,7 @@ def absorb(self, other): # Check whether there is space newNum = self.activeQubits + other.activeQubits if newNum > self.maxQubits: - raise quantumError("Cannot merge: qubits exceed the maximum available.\n") + raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") # Check whether there are in fact qubits to tensor up.... if self.activeQubits == 0: @@ -318,7 +318,7 @@ def absorb_parts(self, R, I, activeQ): # Check whether there is space newNum = self.activeQubits + activeQ if newNum > self.maxQubits: - raise quantumError("Cannot merge: qubits exceed the maximum available.\n") + raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") if activeQ > 0: # Unpack the ordering of qubits and the real and imaginary part diff --git a/simulaqron/virtual_node/quantum.py b/simulaqron/virtual_node/quantum.py index a9a38634..a4600689 100644 --- a/simulaqron/virtual_node/quantum.py +++ b/simulaqron/virtual_node/quantum.py @@ -41,7 +41,7 @@ from simulaqron import settings -class simulatedQubit(pb.Referenceable): +class SimulatedQubit(pb.Referenceable): """ Simulated qubit object in the specified local simulation engine. diff --git a/simulaqron/virtual_node/qutip_simulator.py b/simulaqron/virtual_node/qutip_simulator.py index 4308f7f7..a245b7f2 100644 --- a/simulaqron/virtual_node/qutip_simulator.py +++ b/simulaqron/virtual_node/qutip_simulator.py @@ -37,10 +37,10 @@ except ImportError: raise RuntimeError("If you want to use the qutip backend you need to install the python package 'qutip'") -from simulaqron.virtual_node.basics import quantumEngine, quantumError, noQubitError +from simulaqron.virtual_node.basics import QuantumEngine, QuantumError, NoQubitError -class qutipEngine(quantumEngine): +class QutipEngine(QuantumEngine): """ Basic quantum engine which uses QuTip. Works with density matrices and in principle allows full quantum dynamics via QuTip. Subsequently, this is quite slow. @@ -49,7 +49,7 @@ class qutipEngine(quantumEngine): maxQubits: maximum number of qubits this engine will support. """ - def __init__(self, node, num, maxQubits=10): + def __init__(self, node: str, num: int, maxQubits: int = 10): """ Initialize the simple engine. If no number is given for maxQubits, the assumption will be 10. """ @@ -59,7 +59,7 @@ def __init__(self, node, num, maxQubits=10): self.activeQubits = 0 self.qubitReg = qp.Qobj() - def add_fresh_qubit(self): + def add_fresh_qubit(self) -> int: """ Add a new qubit initialized in the \|0\> state. """ @@ -78,7 +78,7 @@ def add_qubit(self, newQubit): # Check if we are still allowed to add qubits if self.activeQubits >= self.maxQubits: - raise noQubitError("No more qubits available in register.") + raise NoQubitError("No more qubits available in register.") # Append to the existing state at the end if self.activeQubits > 0: @@ -99,7 +99,7 @@ def remove_qubit(self, qubitNum): Removes the qubit with the desired number qubitNum """ if (qubitNum + 1) > self.activeQubits: - raise quantumError("No such qubit to remove") + raise QuantumError("No such qubit to remove") # Check if this the only qubit if self.activeQubits == 1: @@ -316,7 +316,7 @@ def measure_qubit_inplace(self, qubitNum): # Check we have such a qubit... if (qubitNum + 1) > self.activeQubits: - raise quantumError("No such qubit to be measured.") + raise QuantumError("No such qubit to be measured.") # Construct the two measurement operators, and put them at the right position v0 = qp.basis(2, 0) @@ -382,7 +382,7 @@ def absorb(self, other): # Check whether there is space newNum = self.activeQubits + other.activeQubits if newNum > self.maxQubits: - raise quantumError("Cannot merge: qubits exceed the maximum available.\n") + raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") # Check whether there are in fact qubits to tensor up.... if self.activeQubits == 0: @@ -413,7 +413,7 @@ def absorb_parts(self, R, I, activeQ): # Check whether there is space newNum = self.activeQubits + activeQ if newNum > self.maxQubits: - raise quantumError("Cannot merge: qubits exceed the maximum available.\n") + raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") # Check whether there are in fact qubits to tensor up.... if self.activeQubits == 0: diff --git a/simulaqron/virtual_node/stabilizer_simulator.py b/simulaqron/virtual_node/stabilizer_simulator.py index aa533107..2ea4b10b 100644 --- a/simulaqron/virtual_node/stabilizer_simulator.py +++ b/simulaqron/virtual_node/stabilizer_simulator.py @@ -26,12 +26,12 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from simulaqron.virtual_node.basics import quantumEngine, quantumError, noQubitError +from simulaqron.virtual_node.basics import QuantumEngine, QuantumError, NoQubitError from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.general import SimUnsupportedError -class stabilizerEngine(quantumEngine): +class StabilizerEngine(QuantumEngine): """ Basic quantum engine which uses stabilizer formalism. Thus only Clifford operations can be performed @@ -39,7 +39,7 @@ class stabilizerEngine(quantumEngine): maxQubits: maximum number of qubits this engine will support. """ - def __init__(self, node, num, maxQubits=10): + def __init__(self, node: str, num: int, maxQubits: int = 10): """ Initialize the simple engine. If no number is given for maxQubits, the assumption will be 10. """ @@ -52,13 +52,13 @@ def __init__(self, node, num, maxQubits=10): def activeQubits(self): return self.qubitReg.num_qubits - def add_fresh_qubit(self): + def add_fresh_qubit(self) -> int: """ Add a new qubit initialized in the \|0\> state. """ # Check if we are still allowed to add qubits if self.activeQubits >= self.maxQubits: - raise noQubitError("No more qubits available in register.") + raise NoQubitError("No more qubits available in register.") num = self.activeQubits @@ -90,7 +90,7 @@ def remove_qubit(self, qubitNum): Removes the qubit with the desired number qubitNum """ if (qubitNum + 1) > self.activeQubits: - raise quantumError("No such qubit to remove") + raise QuantumError("No such qubit to remove") self.measure_qubit(qubitNum) @@ -206,7 +206,7 @@ def measure_qubit_inplace(self, qubitNum): # Check we have such a qubit... if (qubitNum + 1) > self.activeQubits: - raise quantumError("No such qubit to be measured.") + raise QuantumError("No such qubit to be measured.") outcome = self.qubitReg.measure(qubitNum, inplace=True) @@ -238,7 +238,7 @@ def absorb(self, other): # Check whether there is space newNum = self.activeQubits + other.activeQubits if newNum > self.maxQubits: - raise quantumError("Cannot merge: qubits exceed the maximum available.\n") + raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") self.qubitReg = self.qubitReg.tensor_product(other.qubitReg) @@ -254,6 +254,6 @@ def absorb_parts(self, R, I, activeQ): # Check whether there is space newNum = self.activeQubits + activeQ if newNum > self.maxQubits: - raise quantumError("Cannot merge: qubits exceed the maximum available.\n") + raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") self.qubitReg = self.qubitReg.tensor_product(StabilizerState(R)) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index a9c4f01f..8b0184e3 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -40,8 +40,8 @@ from netqasm.logging.glob import get_netqasm_logger -from simulaqron.virtual_node.basics import quantumError, noQubitError, virtNetError -from simulaqron.virtual_node.quantum import simulatedQubit +from simulaqron.virtual_node.basics import QuantumError, NoQubitError, VirtNetError +from simulaqron.virtual_node.quantum import SimulatedQubit from simulaqron.general.host_config import SocketsConfig # We need this import (despite unused) to reraise local errors # We then supress the flake8 errors associated with that @@ -49,13 +49,13 @@ from simulaqron.settings import simulaqron_settings, SimBackend if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: - from simulaqron.virtual_node.qutip_simulator import qutipEngine + from simulaqron.virtual_node.qutip_simulator import QutipEngine elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: - from simulaqron.virtual_node.project_q_simulator import projectQEngine + from simulaqron.virtual_node.project_q_simulator import ProjectQEngine elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - from simulaqron.virtual_node.stabilizer_simulator import stabilizerEngine + from simulaqron.virtual_node.stabilizer_simulator import StabilizerEngine else: - raise quantumError(f"Unknown backend {simulaqron_settings.sim_backend}") + raise QuantumError(f"Unknown backend {simulaqron_settings.sim_backend}") def reraise_remote_error(remote_err): @@ -124,7 +124,7 @@ def start(self, maxQubits=simulaqron_settings.max_qubits, maxRegisters=simulaqro try: self._logger.debug("Starting on port %d", self.myID.port) - node = virtualNode(self.myID, self.config, maxQubits=maxQubits, maxRegisters=maxRegisters) + node = VirtualNode(self.myID, self.config, maxQubits=maxQubits, maxRegisters=maxRegisters) reactor.listenTCP(self.myID.port, pb.PBServerFactory(node)) self._logger.debug("Running reactor") @@ -139,12 +139,12 @@ def start(self, maxQubits=simulaqron_settings.max_qubits, maxRegisters=simulaqro ####### # -# virtualNode - this is the virtual quantum node. It keeps track of registers simulated here, qubits +# VirtualNode - this is the virtual quantum node. It keeps track of registers simulated here, qubits # virtually available at this node, etc # -class virtualNode(pb.Root): +class VirtualNode(pb.Root): def __init__(self, ID, config, maxQubits=simulaqron_settings.max_qubits, maxRegisters=simulaqron_settings.max_registers): """ @@ -403,18 +403,18 @@ def remote_new_register(self, maxQubits=10): # Make sure that reg numbers are assigned correctly if self.numRegs >= self.maxRegs: self._logger.error("Maximum number of registers reached.") - raise quantumError("Maximum number of registers reached.") + raise QuantumError("Maximum number of registers reached.") self.numRegs = self.numRegs + 1 regNum = self.get_new_reg_num() if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: - newReg = qutipEngine(self.myID, regNum, maxQubits) + newReg = QutipEngine(self.myID, regNum, maxQubits) elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: - newReg = projectQEngine(self.myID, regNum, maxQubits) + newReg = ProjectQEngine(self.myID, regNum, maxQubits) elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - newReg = stabilizerEngine(self.myID, regNum, maxQubits) + newReg = StabilizerEngine(self.myID, regNum, maxQubits) else: - raise quantumError(f"Unknown backend {simulaqron_settings.sim_backend}") + raise QuantumError(f"Unknown backend {simulaqron_settings.sim_backend}") self.registers[regNum] = newReg @@ -451,7 +451,7 @@ def remote_new_qubit(self, ignore_max_qubits=False): try: if (len(self.virtQubits) >= self.maxQubits) and (not ignore_max_qubits): self._logger.error("Maximum number of virtual qubits reached.") - raise noQubitError("Max virtual qubits reached") + raise NoQubitError("Max virtual qubits reached") else: # Qubit in the simulation backend, initialized to |0> simNum = self.get_sim_id() @@ -459,14 +459,14 @@ def remote_new_qubit(self, ignore_max_qubits=False): # Create a new register newReg = self.remote_add_register() - simQubit = simulatedQubit(self.myID, newReg, simNum) + simQubit = SimulatedQubit(self.myID, newReg, simNum) simQubit.make_fresh() self.simQubits.append(simQubit) # Virtual qubit newNum = self.get_virtual_id() - newQubit = virtualQubit(self.myID, self.myID, simQubit, newNum) + newQubit = VirtualQubit(self.myID, self.myID, simQubit, newNum) self.virtQubits.append(newQubit) finally: self._release_global_lock() @@ -481,7 +481,7 @@ def remote_new_qubit_inreg(self, reg): # Only allow if the register is local if reg.simNode != self.myID: - raise quantumError("Can only create qubits registers simulated locally by this node.") + raise QuantumError("Can only create qubits registers simulated locally by this node.") # Get a lock to assure IDs are assigned correctly and maxQubits is consitently checked yield self._get_global_lock() @@ -489,17 +489,17 @@ def remote_new_qubit_inreg(self, reg): try: if len(self.virtQubits) >= self.maxQubits: self._logger.error("Maximum number of virtual qubits reached.") - raise noQubitError("Max virtual qubits reached") + raise NoQubitError("Max virtual qubits reached") else: # Qubit in the local simulation backend, initialized to |0> simNum = self.get_sim_id() - simQubit = simulatedQubit(self.myID, reg, simNum) + simQubit = SimulatedQubit(self.myID, reg, simNum) simQubit.make_fresh() self.simQubits.append(simQubit) # Virtual qubit newNum = self.get_virtual_id() - newQubit = virtualQubit(self.myID, self.myID, simQubit, newNum) + newQubit = VirtualQubit(self.myID, self.myID, simQubit, newNum) self.virtQubits.append(newQubit) finally: self._release_global_lock() @@ -525,7 +525,7 @@ def remote_netqasm_send_qubit(self, num, targetName, app_id, remote_app_id): # Lookup host ID of node if not (targetName in self.config.hostDict): - raise virtNetError( + raise VirtNetError( f"Trying to get conncetion to virtual node {targetName}, but this is not in configuration file" ) remoteNode = yield self.get_connection(targetName) @@ -606,7 +606,7 @@ def remote_netqasm_send_epr_half(self, num, targetName, app_id, remote_app_id, r # Lookup host ID of node if not (targetName in self.config.hostDict): - raise virtNetError( + raise VirtNetError( f"Trying to get conncetion to virtual node {targetName}, but this is not in configuration file" ) remoteNode = yield self.get_connection(targetName) @@ -687,7 +687,7 @@ def remote_send_qubit(self, qubit, targetName): # Lookup host id of node if not (targetName in self.config.hostDict): - raise virtNetError( + raise VirtNetError( f"Trying to get connection to virtual node {targetName}, but this is not in configuration file" ) remoteNode = yield self.get_connection(targetName) @@ -754,7 +754,7 @@ def remote_transfer_qubit(self, simQubitNum, targetName): # Lookup host id of node if not (targetName in self.config.hostDict): - raise virtNetError( + raise VirtNetError( f"Trying to get conncetion to virtual node {targetName}, but this is not in configuration file" ) remoteNode = yield self.get_connection(targetName) @@ -784,7 +784,7 @@ def remote_add_qubit(self, name, simQubit): # Get the details of the remote node if not (name in self.config.hostDict): - raise virtNetError( + raise VirtNetError( f"Trying to get conncetion to virtual node {name}, but this is not in configuration file" ) nb = yield self.get_connection(name) @@ -794,11 +794,11 @@ def remote_add_qubit(self, name, simQubit): try: if len(self.virtQubits) >= self.maxQubits: - raise noQubitError("Max virtual qubits reached") + raise NoQubitError("Max virtual qubits reached") # Generate a new virtual qubit object for the qubit now at this node newNum = self.get_virtual_id() - newQubit = virtualQubit(self.myID, nb, simQubit, newNum) + newQubit = VirtualQubit(self.myID, nb, simQubit, newNum) # Add to local list self.virtQubits.append(newQubit) @@ -844,7 +844,7 @@ def _remove_sim_qubit(self, delQubit): # Caution: Only qubits simulated at this node can be removed if delQubit not in self.simQubits: self._logger.error("Attempt to delete qubit not simulated at this node.") - raise quantumError("Cannot delete qubits we don't simulate.") + raise QuantumError("Cannot delete qubits we don't simulate.") # delNum = delQubit.num @@ -975,7 +975,7 @@ def remote_merge_from(self, simNodeName, simQubitNum, localReg): # Lookup the local connection for this simulating node if not (simNodeName in self.config.hostDict): - raise virtNetError( + raise VirtNetError( f"Trying to get connection to virtual node {simNodeName}, but this is not in configuration file" ) simNode = yield self.get_connection(simNodeName) @@ -999,7 +999,7 @@ def remote_merge_from(self, simNodeName, simQubitNum, localReg): # Make new qubit objects for k in range(activeQ): simNum = self.get_sim_id() - newQubit = simulatedQubit(self.myID, localReg, simNum, offset + k) + newQubit = SimulatedQubit(self.myID, localReg, simNum, offset + k) # Lock the qubit directly until merge is finished yield newQubit.lock() self.simQubits.append(newQubit) @@ -1046,11 +1046,11 @@ def remote_update_virtual_merge(self, newSimNodeName, oldSimNodeName, oldRegNum, # Lookup the local connections for the given node names if not (newSimNodeName in self.config.hostDict): - raise virtNetError( + raise VirtNetError( f"Trying to get conncetion to virtual node {newSimNodeName}, but this is not in configuration file" ) if not (oldSimNodeName in self.config.hostDict): - raise virtNetError( + raise VirtNetError( f"Trying to get conncetion to virtual node {oldSimNodeName}, but this is not in configuration file" ) newSimNode = yield self.get_connection(newSimNodeName) @@ -1085,7 +1085,7 @@ def remote_get_register_RI(self, qubit): Return the real and imaginary part of the (possibly remote) simulated register which contains this virtual qubit. """ - if isinstance(qubit, virtualQubit): + if isinstance(qubit, VirtualQubit): realM, imagM = yield qubit.remote_get_register_RI() else: realM, imagM = yield call_method(qubit, "get_register_RI") @@ -1226,7 +1226,7 @@ def remote_sim_qubit_num_in_same_reg(self, sim_qubit_num1, sim_qubit_num2): ####### # -# virtualQubit - a qubit that is virtually carried at this node. It may be simulated elsewhere +# VirtualQubit - a qubit that is virtually carried at this node. It may be simulated elsewhere # but in the simulation it is located at this particular virtualNode. # # This is given out as a reference object to users who ask for a "local" qubit @@ -1234,7 +1234,7 @@ def remote_sim_qubit_num_in_same_reg(self, sim_qubit_num1, sim_qubit_num2): # -class virtualQubit(pb.Referenceable): +class VirtualQubit(pb.Referenceable): def __init__(self, virtNode, simNode, simQubit, num): """ Creates a virtual qubit object simulated in the specified simulation register backend @@ -1556,7 +1556,7 @@ def _two_qubit_gate(self, target, name): # Sanity check: we really have the right simulating node if fNode != self.simNode.name or tNode != target.simNode.name: self._logger.error("Inconsistent simulation. Cannot merge.") - raise quantumError("Inconsistent simulation") + raise QuantumError("Inconsistent simulation") # Merge the remote register according to the simulation IDs of the qubits self._logger.debug("merging remote same node") @@ -1581,7 +1581,7 @@ def _two_qubit_gate(self, target, name): (fNum, fNode) = yield call_method(target.simQubit, "get_details") if fNode != target.simNode.name: self._logger.error("Inconsistent simulation. Cannot merge.") - raise quantumError("Inconsistent simulation.") + raise QuantumError("Inconsistent simulation.") target.simQubit = yield self.simNode.root.remote_merge_from( target.simNode.name, fNum, self.simQubit.register ) @@ -1601,7 +1601,7 @@ def _two_qubit_gate(self, target, name): (fNum, fNode) = yield call_method(self.simQubit, "get_details") if fNode != self.simNode.name: self._logger.error("Inconsistent simulation. Cannot merge.") - raise quantumError("Inconsistent simulation.") + raise QuantumError("Inconsistent simulation.") self.simQubit = yield target.simNode.root.remote_merge_from( self.simNode.name, fNum, target.simQubit.register ) @@ -1628,11 +1628,11 @@ def _two_qubit_gate(self, target, name): (fNum, fNode) = yield call_method(self.simQubit, "get_details") if fNode != self.simNode.name: self._logger.error("Inconsistent simulation. Cannot merge.") - raise quantumError("Inconsistent simulation.") + raise QuantumError("Inconsistent simulation.") (tNum, tNode) = yield call_method(target.simQubit, "get_details") if tNode != target.simNode.name: self._logger.error("Inconsistent simulation. Cannot merge.") - raise quantumError("Inconsistent simulation.") + raise QuantumError("Inconsistent simulation.") # Pull the remote registers to this node self.simQubit = yield self.virtNode.root.remote_merge_from( diff --git a/tests/quick/engine/test_project_q_engine.py b/tests/quick/engine/test_project_q_engine.py index 90742a5e..3a831cb0 100644 --- a/tests/quick/engine/test_project_q_engine.py +++ b/tests/quick/engine/test_project_q_engine.py @@ -6,8 +6,8 @@ if has_module.main(SimBackend.PROJECTQ.value): - from simulaqron.virtual_node.project_q_simulator import projectQEngine - from simulaqron.virtual_node.basics import noQubitError, quantumError + from simulaqron.virtual_node.project_q_simulator import ProjectQEngine + from simulaqron.virtual_node.basics import NoQubitError, QuantumError from projectq.types._qubit import Qubit @@ -26,15 +26,15 @@ def new_test(self): return new_test -class TestProjectQEngine_init(unittest.TestCase): +class TestProjectQEnginInit(unittest.TestCase): @if_has_module def test_init(self): - eng = projectQEngine("Alice", 0) + eng = ProjectQEngine("Alice", 0) self.assertEqual(eng.maxQubits, 10) self.assertEqual(eng.activeQubits, 0) self.assertEqual(len(eng.qubitReg), 0) - eng = projectQEngine("Alice", 0, 5) + eng = ProjectQEngine("Alice", 0, 5) self.assertEqual(eng.maxQubits, 5) self.assertEqual(eng.activeQubits, 0) self.assertEqual(len(eng.qubitReg), 0) @@ -43,7 +43,7 @@ def test_init(self): class TestProjectQEngine(unittest.TestCase): @if_has_module def setUp(self): - self.eng = projectQEngine("Alice", 0) + self.eng = ProjectQEngine("Alice", 0) @staticmethod def abs_inner_product(state, ref): @@ -63,7 +63,7 @@ def test_add_fresh_qubit(self): def test_add_to_many_fresh_qubits(self): for _ in range(10): self.eng.add_fresh_qubit() - with self.assertRaises(noQubitError): + with self.assertRaises(NoQubitError): self.eng.add_fresh_qubit() @if_has_module @@ -89,7 +89,7 @@ def test_add_qubit_H(self): @if_has_module def test_add_unphysical_qubit(self): new_state = [1, 1] - with self.assertRaises(quantumError): + with self.assertRaises(QuantumError): self.eng.add_qubit(new_state) @if_has_module @@ -98,7 +98,7 @@ def test_remove_qubit(self): self.eng.remove_qubit(num) self.assertEqual(self.eng.activeQubits, 0) self.assertEqual(len(self.eng.qubitReg), 0) - with self.assertRaises(quantumError): + with self.assertRaises(QuantumError): self.eng.remove_qubit(num) @if_has_module @@ -224,7 +224,7 @@ def test_measure_inplace(self): @if_has_module def test_absorb_both_empty(self): - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) self.eng.absorb(eng2) self.assertEqual(self.eng.activeQubits, 0) self.assertEqual(len(self.eng.qubitReg), 0) @@ -233,7 +233,7 @@ def test_absorb_both_empty(self): def test_absorb_other_empty(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) self.eng.absorb(eng2) self.assertEqual(self.eng.activeQubits, 1) self.assertEqual(len(self.eng.qubitReg), 1) @@ -243,7 +243,7 @@ def test_absorb_other_empty(self): @if_has_module def test_absorb_this_empty_H(self): - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) num = eng2.add_fresh_qubit() eng2.apply_H(num) self.eng.absorb(eng2) @@ -255,7 +255,7 @@ def test_absorb_this_empty_H(self): @if_has_module def test_absorb_this_empty_CNOT(self): - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) num1 = eng2.add_fresh_qubit() num2 = eng2.add_fresh_qubit() eng2.apply_H(num1) @@ -270,7 +270,7 @@ def test_absorb_this_empty_CNOT(self): @if_has_module def test_absorb_this_empty_GHZ(self): n = 5 - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) qubits = [eng2.add_fresh_qubit() for _ in range(n)] eng2.apply_H(qubits[0]) for i in range(1, n): @@ -285,7 +285,7 @@ def test_absorb_this_empty_GHZ(self): @if_has_module def test_absorb_2GHZ(self): n = 5 - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) for eng in [self.eng, eng2]: qubits = [eng.add_fresh_qubit() for _ in range(n)] eng.apply_H(qubits[0]) @@ -297,24 +297,24 @@ def test_absorb_2GHZ(self): @if_has_module def test_absorb_to_big_this_empty(self): - eng2 = projectQEngine("Alice", 0, 11) + eng2 = ProjectQEngine("Alice", 0, 11) for _ in range(11): eng2.add_fresh_qubit() - with self.assertRaises(quantumError): + with self.assertRaises(QuantumError): self.eng.absorb(eng2) @if_has_module def test_absorb_to_big(self): self.eng.add_fresh_qubit() - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) for _ in range(10): eng2.add_fresh_qubit() - with self.assertRaises(quantumError): + with self.assertRaises(QuantumError): self.eng.absorb(eng2) @if_has_module def test_absorb_parts_both_empty(self): - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) self.assertEqual(self.eng.activeQubits, 0) self.assertEqual(len(self.eng.qubitReg), 0) @@ -322,7 +322,7 @@ def test_absorb_parts_both_empty(self): @if_has_module def test_absorb_parts(self): self.eng.add_fresh_qubit() - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) eng2.add_fresh_qubit() self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) self.assertEqual(self.eng.activeQubits, 2) @@ -333,7 +333,7 @@ def test_absorb_parts(self): @if_has_module def test_absorb_parts_EPR(self): - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) num1 = eng2.add_fresh_qubit() num2 = eng2.add_fresh_qubit() eng2.apply_H(num1) @@ -349,7 +349,7 @@ def test_absorb_parts_EPR(self): def test_absorb_parts_other_empty(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) - eng2 = projectQEngine("Alice", 0) + eng2 = ProjectQEngine("Alice", 0) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) self.assertEqual(self.eng.activeQubits, 1) self.assertEqual(len(self.eng.qubitReg), 1) diff --git a/tests/quick/engine/test_qutip_engine.py b/tests/quick/engine/test_qutip_engine.py index 685e8565..c7e44f98 100644 --- a/tests/quick/engine/test_qutip_engine.py +++ b/tests/quick/engine/test_qutip_engine.py @@ -34,7 +34,7 @@ if has_module.main(SimBackend.QUTIP.value): - from simulaqron.virtual_node.qutip_simulator import qutipEngine + from simulaqron.virtual_node.qutip_simulator import QutipEngine _has_module = True @@ -54,8 +54,8 @@ def new_test(self): class TestQutipEngine(unittest.TestCase): @if_has_module def test_tracing(self): - se = qutipEngine("alice", 0, 10) - se2 = qutipEngine("Alice", 0, 10) + se = QutipEngine("alice", 0, 10) + se2 = QutipEngine("Alice", 0, 10) se.add_fresh_qubit() se.add_fresh_qubit() @@ -77,7 +77,7 @@ def test_tracing(self): @if_has_module def test_gates(self): - se = qutipEngine("alice", 0, 10) + se = QutipEngine("alice", 0, 10) se.add_fresh_qubit() savedQubit = se.qubitReg @@ -90,7 +90,7 @@ def test_gates(self): @if_has_module def test_measure(self): - se = qutipEngine("alice", 0) + se = QutipEngine("alice", 0) se.add_fresh_qubit() outcome = se.measure_qubit(0) diff --git a/tests/quick/engine/test_stabilizer_engine.py b/tests/quick/engine/test_stabilizer_engine.py index 5b1f4490..d32b4c85 100644 --- a/tests/quick/engine/test_stabilizer_engine.py +++ b/tests/quick/engine/test_stabilizer_engine.py @@ -1,20 +1,20 @@ import unittest import numpy as np -from simulaqron.virtual_node.stabilizer_simulator import stabilizerEngine -from simulaqron.virtual_node.basics import noQubitError, quantumError +from simulaqron.virtual_node.stabilizer_simulator import StabilizerEngine +from simulaqron.virtual_node.basics import NoQubitError, QuantumError from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.general import SimUnsupportedError class TestStabilizerEngine_init(unittest.TestCase): def test_init(self): - eng = stabilizerEngine("Alice", 0) + eng = StabilizerEngine("Alice", 0) self.assertEqual(eng.maxQubits, 10) self.assertEqual(eng.activeQubits, 0) self.assertEqual(len(eng.qubitReg), 0) - eng = stabilizerEngine("Alice", 0, 5) + eng = StabilizerEngine("Alice", 0, 5) self.assertEqual(eng.maxQubits, 5) self.assertEqual(eng.activeQubits, 0) self.assertEqual(len(eng.qubitReg), 0) @@ -22,7 +22,7 @@ def test_init(self): class TestStabilizerEngine(unittest.TestCase): def setUp(self): - self.eng = stabilizerEngine("Alice", 0) + self.eng = StabilizerEngine("Alice", 0) def test_add_fresh_qubit(self): num = self.eng.add_fresh_qubit() @@ -33,7 +33,7 @@ def test_add_fresh_qubit(self): def test_add_to_many_fresh_qubits(self): for _ in range(10): self.eng.add_fresh_qubit() - with self.assertRaises(noQubitError): + with self.assertRaises(NoQubitError): self.eng.add_fresh_qubit() def test_add_qubit(self): @@ -59,7 +59,7 @@ def test_remove_qubit(self): self.eng.remove_qubit(num) self.assertEqual(self.eng.activeQubits, 0) self.assertEqual(len(self.eng.qubitReg), 0) - with self.assertRaises(quantumError): + with self.assertRaises(QuantumError): self.eng.remove_qubit(num) def test_get_register_RI(self): @@ -152,7 +152,7 @@ def test_measure_inplace(self): self.assertEqual(self.eng.activeQubits, 1) def test_absorb_both_empty(self): - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) self.eng.absorb(eng2) self.assertEqual(self.eng.activeQubits, 0) self.assertEqual(len(self.eng.qubitReg), 0) @@ -160,7 +160,7 @@ def test_absorb_both_empty(self): def test_absorb_other_empty(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) self.eng.absorb(eng2) self.assertEqual(self.eng.activeQubits, 1) self.assertEqual(len(self.eng.qubitReg), 1) @@ -168,7 +168,7 @@ def test_absorb_other_empty(self): self.assertTrue(StabilizerState(state) == StabilizerState([[1, 0]])) def test_absorb_this_empty_H(self): - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) num = eng2.add_fresh_qubit() eng2.apply_H(num) self.eng.absorb(eng2) @@ -178,7 +178,7 @@ def test_absorb_this_empty_H(self): self.assertTrue(StabilizerState(state) == StabilizerState([[1, 0]])) def test_absorb_this_empty_CNOT(self): - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) num1 = eng2.add_fresh_qubit() num2 = eng2.add_fresh_qubit() eng2.apply_H(num1) @@ -191,7 +191,7 @@ def test_absorb_this_empty_CNOT(self): def test_absorb_this_empty_GHZ(self): n = 5 - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) qubits = [eng2.add_fresh_qubit() for _ in range(n)] eng2.apply_H(qubits[0]) for i in range(1, n): @@ -208,7 +208,7 @@ def test_absorb_this_empty_GHZ(self): def test_absorb_2GHZ(self): n = 5 - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) for eng in [self.eng, eng2]: qubits = [eng.add_fresh_qubit() for _ in range(n)] eng.apply_H(qubits[0]) @@ -219,29 +219,29 @@ def test_absorb_2GHZ(self): self.assertEqual(len(self.eng.qubitReg), 2 * n) def test_absorb_to_big_this_empty(self): - eng2 = stabilizerEngine("Alice", 0, 11) + eng2 = StabilizerEngine("Alice", 0, 11) for _ in range(11): eng2.add_fresh_qubit() - with self.assertRaises(quantumError): + with self.assertRaises(QuantumError): self.eng.absorb(eng2) def test_absorb_to_big(self): self.eng.add_fresh_qubit() - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) for _ in range(10): eng2.add_fresh_qubit() - with self.assertRaises(quantumError): + with self.assertRaises(QuantumError): self.eng.absorb(eng2) def test_absorb_parts_both_empty(self): - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) self.assertEqual(self.eng.activeQubits, 0) self.assertEqual(len(self.eng.qubitReg), 0) def test_absorb_parts(self): self.eng.add_fresh_qubit() - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) eng2.add_fresh_qubit() self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) self.assertEqual(self.eng.activeQubits, 2) @@ -250,7 +250,7 @@ def test_absorb_parts(self): self.assertTrue(StabilizerState(state) == StabilizerState([[0, 0, 1, 0], [0, 0, 0, 1]])) def test_absorb_parts_EPR(self): - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) num1 = eng2.add_fresh_qubit() num2 = eng2.add_fresh_qubit() eng2.apply_H(num1) @@ -264,7 +264,7 @@ def test_absorb_parts_EPR(self): def test_absorb_parts_other_empty(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) - eng2 = stabilizerEngine("Alice", 0) + eng2 = StabilizerEngine("Alice", 0) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) self.assertEqual(self.eng.activeQubits, 1) self.assertEqual(len(self.eng.qubitReg), 1) From 9ec129669fed3d592721db385e06c8d17df418e3 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Sep 2025 13:46:21 +0200 Subject: [PATCH 026/277] More type hints in key modules --- simulaqron/general/host_config.py | 80 ++++++++++++++------------- simulaqron/netqasm_backend/qnodeos.py | 20 ++++--- simulaqron/sdk/connection.py | 23 ++++---- simulaqron/settings.py | 51 ++++++++--------- simulaqron/start/start_qnodeos.py | 75 +++++++++++++------------ simulaqron/start/start_vnode.py | 10 ++-- simulaqron/virtual_node/virtual.py | 33 +++++------ 7 files changed, 150 insertions(+), 142 deletions(-) diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index eb027f81..4fa23d85 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -29,7 +29,7 @@ import socket import struct -from typing import Dict +from typing import Dict, List from twisted.spread import pb from ipaddress import IPv4Address @@ -37,42 +37,8 @@ from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor -def node_id(fam, ip): - if fam == socket.AF_INET: - return struct.unpack("!L", IPv4Address(ip).packed)[0] - else: - raise ValueError("No IPv6 yet :(") - - -def node_id_from_addrinfo(addr): - fam = addr[0] - sockaddr = addr[4] - ip = sockaddr[0] - return node_id(fam, ip) - - -def get_node_id_from_net_config(net_config, node_name): - """ - NOTE node ID is the index of the node name of a sorted list of all the node names in the network. - """ - if node_name not in net_config.hostDict: - raise ValueError(f"node name {node_name} not in host_dict ({net_config.hostDict.keys()})") - return list(sorted(net_config.hostDict.keys())).index(node_name) - - -def load_node_names(config_file): - """ - Load list of nodes from Nodes.cfg file - - :param config_file: str - pointing to Nodes.cfg file - """ - with open(config_file, 'r') as f: - return [line.strip() for line in f.readlines()] - - class Host(pb.Referenceable): - def __init__(self, name, hostname, port): + def __init__(self, name: str, hostname: str, port: str | int): """ Initialize the details of the host. For now, we just keep the following: @@ -100,7 +66,7 @@ def __init__(self, name, hostname, port): class SocketsConfig(pb.Referenceable): - def __init__(self, filename, network_name="default", config_type="vnode"): + def __init__(self, filename: str, network_name: str = "default", config_type: str = "vnode"): """ Initialize by reading in the configuration file. @@ -113,7 +79,7 @@ def __init__(self, filename, network_name="default", config_type="vnode"): # Read config file self.read_config(filename, network_name=network_name, config_type=config_type) - def read_config(self, filename, network_name="default", config_type="vnode"): + def read_config(self, filename: str, network_name: str = "default", config_type: str = "vnode"): """ Reads the configuration file in which each line has the form: node name, hostname, port number. For example: @@ -144,9 +110,45 @@ def read_config(self, filename, network_name="default", config_type="vnode"): else: raise ValueError(f"Unknown file type {filename.split(".")[-1]}") - def print_details(self, name): + def print_details(self, name: str): """ Prints the details of the specified node with name. """ host = self.hostDict[name] print("Host details of ", name, ": ", host.hostname, ":", host.port) + + +def node_id(fam: socket.AddressFamily, ip: str) -> int: + if fam == socket.AF_INET: + return struct.unpack("!L", IPv4Address(ip).packed)[0] + else: + raise ValueError("No IPv6 yet :(") + + +def node_id_from_addrinfo( + addr: tuple[socket.AddressFamily, socket.SocketKind, int, str, tuple[str, int]] +) -> int: + fam = addr[0] + sockaddr = addr[4] + ip = sockaddr[0] + return node_id(fam, ip) + + +def get_node_id_from_net_config(net_config: SocketsConfig, node_name: str) -> int: + """ + NOTE node ID is the index of the node name of a sorted list of all the node names in the network. + """ + if node_name not in net_config.hostDict: + raise ValueError(f"node name {node_name} not in host_dict ({net_config.hostDict.keys()})") + return list(sorted(net_config.hostDict.keys())).index(node_name) + + +def load_node_names(config_file: str) -> List[str]: + """ + Load list of nodes from Nodes.cfg file + + :param config_file: str + pointing to Nodes.cfg file + """ + with open(config_file, 'r') as f: + return [line.strip() for line in f.readlines()] diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 08f80e82..3a77103d 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -1,12 +1,16 @@ +from typing import Optional + +from netqasm.lang.instr import Flavour from twisted.internet.defer import inlineCallbacks -from netqasm.backend.messages import MsgDoneMessage +from netqasm.backend.messages import MsgDoneMessage, Message from netqasm.backend.qnodeos import QNodeController +from twisted.internet.protocol import Protocol from simulaqron.netqasm_backend.executioner import VanillaSimulaQronExecutioner class SubroutineHandler(QNodeController): - def __init__(self, factory, instr_log_dir=None, flavour=None): + def __init__(self, factory, instr_log_dir: Optional[str] = None, flavour: Optional[Flavour] = None): super().__init__(factory.name, instr_log_dir=instr_log_dir, flavour=flavour) self.factory = factory @@ -18,32 +22,32 @@ def __init__(self, factory, instr_log_dir=None, flavour=None): self._executor.add_factory(self.factory) @property - def protocol(self): + def protocol(self) -> Protocol: return self._protocol @protocol.setter - def protocol(self, protocol): + def protocol(self, protocol: Protocol): self._protocol = protocol @inlineCallbacks - def handle_netqasm_message(self, msg_id, msg): + def handle_netqasm_message(self, msg_id: int, msg: Message): yield from super().handle_netqasm_message( msg_id=msg_id, msg=msg, ) @classmethod - def _get_executor_class(cls, flavour=None): + def _get_executor_class(cls, flavour: Optional[Flavour] = None): return VanillaSimulaQronExecutioner - def _mark_message_finished(self, msg_id, msg): + def _mark_message_finished(self, msg_id: int, msg: Message): ret_msg = MsgDoneMessage(msg_id=msg_id) self._return_msg(msg=ret_msg) def stop(self): self.factory.stop() - def _return_msg(self, msg): + def _return_msg(self, msg: Message): """Return a message to the host""" assert self._protocol is not None, "Seems protocol of handler has not yet been set" self._logger.debug("sending message %s to host", msg) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 67a9d1c2..700f6db4 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -156,7 +156,7 @@ def _setup_socket( while True: try: logger.debug( - "App %s : Trying to connect to NetQASM server (at %s)", name, addr[-1] + "App %s : Trying to connect to NetQASM server (at %s)", name, addr ) qnodeos_socket = socket.socket(addr[0], addr[1], addr[2]) @@ -179,8 +179,9 @@ def _setup_socket( qnodeos_socket.close() raise err logger.debug( - "App %s : Could not connect to NetQASM server, trying again...", - name + "App %s : Connected to NetQASM server at %s", + name, + addr ) return qnodeos_socket @@ -204,7 +205,7 @@ def _commit_serialized_message( # Execute callback in a new thread after the subroutine is finished thread = Thread( target=self._wait_for_done, - kwargs = { + kwargs={ "msg_id": msg_id, "callback": callback, } @@ -247,7 +248,7 @@ def _read_more_data(self): self.buf = data self._logger.debug("Got new data %s on socket to qnodeos", data) - def _handle_reply(self): + def _handle_reply(self) -> int: """Handle all next replies until a done message and return the msg ID for the done""" # Try to read next message from the buffer otherwise read some more and try again try: @@ -333,10 +334,10 @@ def _update_shared_memory(self, entry, value): # angle=angle, # ) - def _is_done(self, msg_id): + def _is_done(self, msg_id) -> bool: return msg_id in self._done_msg_ids - def _get_new_msg_id(self): + def _get_new_msg_id(self) -> int: msg_id = self._next_msg_id self._next_msg_id += 1 return msg_id @@ -351,14 +352,14 @@ def _get_qnodeos_net_config(network_name: str) -> SocketsConfig: class SimulaQronNetworkInfo(NetworkInfo): @classmethod - def _get_node_id(cls, node_name): + def _get_node_id(cls, node_name: str) -> int: """Returns the node id for the node with the given name""" # TODO always use network name "default"? _qnodeos_net = _get_qnodeos_net_config(network_name="default") return get_node_id_from_net_config(_qnodeos_net, node_name) @classmethod - def _get_node_name(cls, node_id): + def _get_node_name(cls, node_id: int) -> str: """Returns the node name for the node with the given ID""" # TODO always use network name "default"? _qnodeos_net = _get_qnodeos_net_config(network_name="default") @@ -368,13 +369,13 @@ def _get_node_name(cls, node_id): raise KeyError("Unknown node ID {node_id}") @classmethod - def get_node_id_for_app(cls, app_name): + def get_node_id_for_app(cls, app_name: str) -> int: """Returns the node id for the app with the given name""" # NOTE app_name and node_name are for now the same in simulaqron return cls._get_node_id(node_name=app_name) @classmethod - def get_node_name_for_app(cls, app_name): + def get_node_name_for_app(cls, app_name: str) -> str: """Returns the node name for the app with the given name""" # NOTE app_name and node_name are for now the same in simulaqron return app_name diff --git a/simulaqron/settings.py b/simulaqron/settings.py index d64e19b5..c66a3689 100644 --- a/simulaqron/settings.py +++ b/simulaqron/settings.py @@ -34,6 +34,7 @@ import json import logging from enum import Enum +from typing import Dict, Any from simulaqron.toolbox import get_simulaqron_path @@ -55,7 +56,7 @@ class Config: _user_settings_file = os.path.join(os.path.expanduser("~"), ".simulaqron.json") # Dictionary for settings - _config = {} + _config: Dict[str, Any] = {} _default_config = { "_read_user": True, @@ -87,7 +88,7 @@ def updated_func(self, value): def __init__(self): self.update_settings() - def update_settings(self, default=False): + def update_settings(self, default: bool = False): # Update with default settings self._config.update(self._default_config) @@ -115,14 +116,14 @@ def _write(self): with open(self._internal_settings_file, 'w') as f: json.dump(self._config, f, indent=4) - def _get_setting(self, setting): + def _get_setting(self, setting: str) -> Any: try: value = self._config[setting] except KeyError: raise KeyError(f"Cannot find the setting {setting} in the file {self._internal_settings_file}") return value - def _set_setting(self, setting, value): + def _set_setting(self, setting: str, value: Any): self._config[setting] = value self._write() @@ -132,17 +133,17 @@ def _set_setting(self, setting, value): @property @Decorator.get_setting - def _read_user(self): + def _read_user(self) -> bool: pass @_read_user.setter @Decorator.set_setting - def _read_user(self, _read_user): + def _read_user(self, _read_user: bool): pass @property @Decorator.get_setting - def sim_backend(self): + def sim_backend(self) -> str: pass @sim_backend.setter @@ -152,92 +153,92 @@ def sim_backend(self, sim_backend): @property @Decorator.get_setting - def max_qubits(self): + def max_qubits(self) -> int: pass @max_qubits.setter @Decorator.set_setting - def max_qubits(self, max_qubits): + def max_qubits(self, max_qubits: int): pass @property @Decorator.get_setting - def max_registers(self): + def max_registers(self) -> int: pass @max_registers.setter @Decorator.set_setting - def max_registers(self, max_registers): + def max_registers(self, max_registers: int): pass @property @Decorator.get_setting - def conn_retry_time(self): + def conn_retry_time(self: float): pass @conn_retry_time.setter @Decorator.set_setting - def conn_retry_time(self, conn_retry_time): + def conn_retry_time(self, conn_retry_time: float): pass @property @Decorator.get_setting - def recv_timeout(self): + def recv_timeout(self) -> int: pass @recv_timeout.setter @Decorator.set_setting - def recv_timeout(self, recv_timeout): + def recv_timeout(self, recv_timeout: int): pass @property @Decorator.get_setting - def recv_retry_time(self): + def recv_retry_time(self) -> float: pass @recv_retry_time.setter @Decorator.set_setting - def recv_retry_time(self, recv_retry_time): + def recv_retry_time(self, recv_retry_time: float): pass @property @Decorator.get_setting - def log_level(self): + def log_level(self) -> int: pass @log_level.setter @Decorator.set_setting - def log_level(self, log_level): + def log_level(self, log_level: int): pass @property @Decorator.get_setting - def network_config_file(self): + def network_config_file(self) -> str: pass @network_config_file.setter @Decorator.set_setting - def network_config_file(self, app_file): + def network_config_file(self, app_file: str): pass @property @Decorator.get_setting - def noisy_qubits(self): + def noisy_qubits(self) -> bool: pass @noisy_qubits.setter @Decorator.set_setting - def noisy_qubits(self, noisy_qubits): + def noisy_qubits(self, noisy_qubits_: bool): pass @property @Decorator.get_setting - def t1(self): + def t1(self) -> float: pass @t1.setter @Decorator.set_setting - def t1(self, t1): + def t1(self, t1: float): pass diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 71788618..6224a8a6 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -21,34 +21,37 @@ _TIMEOUT = 10 -def init_register(virtRoot, myName, node): +def init_register(virt_root, my_name: str, node: NetQASMFactory): """Retrieves the relevant root objects to talk to such remote connections""" - logger.debug("LOCAL %s: All connections set up.", myName) + logger.debug("LOCAL %s: Connection to local virtual node successful", my_name) # Set the virtual node - node.set_virtual_node(virtRoot) + node.set_virtual_node(virt_root) # Start listening to NetQASM messages - setup_netqasm_server(myName, node) + setup_netqasm_server(my_name, node) -def connect_to_virtNode(myName, netqasm_factory, virtual_network): - """Trys to connect to local virtual node. +def connect_to_virt_node(my_name: str , netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig): + """Tries to connect to local virtual node. If connection is refused, we try again after a set amount of time (specified in handle_connection_error) """ - logger.debug("LOCAL %s: Trying to connect to local virtual node.", myName) - virtual_node = virtual_network.hostDict[myName] + virtual_node = virtual_network.hostDict[my_name] + logger.debug( + "LOCAL %s: Trying to connect to local virtual node at %s, %d.", + my_name, virtual_node.hostname, virtual_node.port + ) factory = pb.PBClientFactory() # Connect reactor.connectTCP(virtual_node.hostname, virtual_node.port, factory) - deferVirtual = factory.getRootObject() + defer_virtual_node = factory.getRootObject() # If connection succeeds do: - deferVirtual.addCallback(init_register, myName, netqasm_factory) + defer_virtual_node.addCallback(init_register, my_name, netqasm_factory) # If connection fails do: - deferVirtual.addErrback(handle_connection_error, myName, netqasm_factory, virtual_network) + defer_virtual_node.addErrback(handle_connection_error, my_name, netqasm_factory, virtual_network) -def handle_connection_error(reason, myName, netqasm_factory, virtual_network): +def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig): """ Handles errors from trying to connect to local virtual node. If a ConnectionRefusedError is raised another try will be made after @@ -57,46 +60,46 @@ def handle_connection_error(reason, myName, netqasm_factory, virtual_network): try: reason.raiseException() except ConnectionRefusedError: - logger.debug("LOCAL %s: Could not connect, trying again...", myName) + logger.debug("LOCAL %s: Could not connect, trying again...", my_name) reactor.callLater( simulaqron_settings.conn_retry_time, - connect_to_virtNode, - myName, + connect_to_virt_node, + my_name, netqasm_factory, virtual_network, ) except Exception as e: logger.error( "LOCAL %s: Critical error when connection to local virtual node: %s", - myName, + my_name, e, ) reactor.stop() -def setup_netqasm_server(myName, netqasm_factory): +def setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): """Setup NetQASM server to handle remote on the classical communication network.""" t_start = timer() while timer() - t_start < _TIMEOUT: try: logger.debug( - "LOCAL %s: Starting local classical communication server, port %d.", - myName, netqasm_factory.host.port + "LOCAL %s: Starting local QNodeOS server, port %d.", + my_name, netqasm_factory.host.port ) - myHost = netqasm_factory.host - myHost.root = netqasm_factory - myHost.factory = netqasm_factory - reactor.listenTCP(myHost.port, myHost.factory) + my_host = netqasm_factory.host + my_host.root = netqasm_factory + my_host.factory = netqasm_factory + reactor.listenTCP(my_host.port, my_host.factory) break except CannotListenError: logger.error( "LOCAL %s: NetQASM server address (%d) is already in use, trying again.", - myName, myHost.port + my_name, my_host.port ) time.sleep(_RETRY_TIME) except Exception as e: logger.error( - "LOCAL %s: Critical error when starting NetQASM server: %s", myName, e + "LOCAL %s: Critical error when starting NetQASM server: %s", my_name, e ) reactor.stop() else: @@ -107,10 +110,10 @@ def sigterm_handler(_signo, _stack_frame): reactor.stop() -def main(myName, network_name="default", log_level="WARNING"): +def main(node_name: str, network_name="default", log_level="WARNING"): """Start the indicated backend NetQASM Server""" set_log_level(log_level) - logger.debug(f"Starting QNodeOS at {myName}") + logger.debug("Starting QNodeOS at %s", node_name) signal.signal(signal.SIGTERM, sigterm_handler) signal.signal(signal.SIGINT, sigterm_handler) @@ -122,27 +125,27 @@ def main(myName, network_name="default", log_level="WARNING"): qnodeos_network = SocketsConfig(network_config_file, network_name=network_name, config_type="qnodeos") # Check if we are in the host-dictionary - if myName in qnodeos_network.hostDict: - myHost = qnodeos_network.hostDict[myName] - logger.debug("Setting up QNodeOS protocol factory for %s (%s)", myName, myHost.addr) + if node_name in qnodeos_network.hostDict: + node_host_info = qnodeos_network.hostDict[node_name] + logger.debug("Setting up QNodeOS protocol factory for %s", node_name) netqasm_factory = NetQASMFactory( - myHost, - myName, + node_host_info, + node_name, qnodeos_network, SubroutineHandler, network_name=network_name, ) else: - logger.error("LOCAL %s: Cannot start classical communication servers.", myName) + logger.error("LOCAL %s: Cannot start classical communication servers.", node_name) return # Connect to the local virtual node simulating the "local" qubits - logger.debug(f"Connect to virtual node {myName}") - connect_to_virtNode(myName, netqasm_factory, virtual_network) + logger.debug(f"Connect to virtual node {node_name}") + connect_to_virt_node(node_name, netqasm_factory, virtual_network) # Run reactor reactor.run() - logger.debug(f"Ending QNodeOS at {myName}") + logger.debug(f"Ending QNodeOS at {node_name}") if __name__ == '__main__': diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 9c588be9..12a5fd3b 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -45,18 +45,18 @@ def sigterm_handler(name, _signo, _stack_frame): reactor.stop() -def main(name, network_name="default", log_level="WARNING"): +def main(name: str, network_name: str = "default", log_level: str = "WARNING"): set_log_level(log_level) signal.signal(signal.SIGTERM, partial(sigterm_handler, name)) signal.signal(signal.SIGINT, partial(sigterm_handler, name)) logger.debug("Starting VIRTUAL NODE %s", name) if simulaqron_settings.network_config_file is not None: - virtualFile = simulaqron_settings.network_config_file + virtual_file = simulaqron_settings.network_config_file else: - virtualFile = simulaqron_settings.vnode_file - be = Backend(name, virtualFile, network_name=network_name) - be.start(maxQubits=simulaqron_settings.max_qubits, maxRegisters=simulaqron_settings.max_registers) + virtual_file = simulaqron_settings.vnode_file + be = Backend(name, virtual_file, network_name=network_name) + be.start(max_qubits=simulaqron_settings.max_qubits, max_registers=simulaqron_settings.max_registers) logger.debug("Ending VIRTUAL NODE %s", name) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 8b0184e3..a76e9468 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -49,11 +49,11 @@ from simulaqron.settings import simulaqron_settings, SimBackend if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: - from simulaqron.virtual_node.qutip_simulator import QutipEngine + from simulaqron.virtual_node.qutip_simulator import QutipEngine as QEngine elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: - from simulaqron.virtual_node.project_q_simulator import ProjectQEngine + from simulaqron.virtual_node.project_q_simulator import ProjectQEngine as QEngine elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - from simulaqron.virtual_node.stabilizer_simulator import StabilizerEngine + from simulaqron.virtual_node.stabilizer_simulator import StabilizerEngine as QEngine else: raise QuantumError(f"Unknown backend {simulaqron_settings.sim_backend}") @@ -95,8 +95,8 @@ def call_method(obj, method_name, *args, **kwargs): # Backend - starts the local virtual node and connects to the other virtual nodes # forming the quantum network # -class Backend(object): - def __init__(self, name, virtualFile, network_name="default"): +class Backend: + def __init__(self, name, virtual_file: str, network_name: str = "default"): """ Initialize. This will read the configuration file and populate the name,hostname,port information with the information found in the configuration file for the given name. @@ -105,16 +105,20 @@ def __init__(self, name, virtualFile, network_name="default"): # Read the configuration file try: - self.config = SocketsConfig(virtualFile, network_name=network_name, config_type="vnode") + self.config = SocketsConfig(virtual_file, network_name=network_name, config_type="vnode") self.myID = self.config.hostDict[name] except KeyError as e: - self._logger.error("No such name in the configuration file %s: %s", virtualFile, e) + self._logger.error("No such name in the configuration file %s: %s", virtual_file, e) raise e except Exception as e: - self._logger.error("Error reading the configuration file %s: %s", virtualFile, e) + self._logger.error("Error reading the configuration file %s: %s", virtual_file, e) raise e - def start(self, maxQubits=simulaqron_settings.max_qubits, maxRegisters=simulaqron_settings.max_registers): + def start( + self, + max_qubits: int = simulaqron_settings.max_qubits, + max_registers: int = simulaqron_settings.max_registers + ): """ Start listening to requests from other nodes. @@ -124,7 +128,7 @@ def start(self, maxQubits=simulaqron_settings.max_qubits, maxRegisters=simulaqro try: self._logger.debug("Starting on port %d", self.myID.port) - node = VirtualNode(self.myID, self.config, maxQubits=maxQubits, maxRegisters=maxRegisters) + node = VirtualNode(self.myID, self.config, maxQubits=max_qubits, maxRegisters=max_registers) reactor.listenTCP(self.myID.port, pb.PBServerFactory(node)) self._logger.debug("Running reactor") @@ -407,14 +411,7 @@ def remote_new_register(self, maxQubits=10): self.numRegs = self.numRegs + 1 regNum = self.get_new_reg_num() - if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: - newReg = QutipEngine(self.myID, regNum, maxQubits) - elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: - newReg = ProjectQEngine(self.myID, regNum, maxQubits) - elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - newReg = StabilizerEngine(self.myID, regNum, maxQubits) - else: - raise QuantumError(f"Unknown backend {simulaqron_settings.sim_backend}") + newReg = QEngine(self.myID, regNum, maxQubits) self.registers[regNum] = newReg From ac7444903ec77f799967145ce142a7f1451c7ad6 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Sep 2025 13:48:00 +0200 Subject: [PATCH 027/277] Add test case for get_quibit_state feature --- tests/quick/general/test_get_qubit_state.py | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/quick/general/test_get_qubit_state.py diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/general/test_get_qubit_state.py new file mode 100644 index 00000000..1d25149d --- /dev/null +++ b/tests/quick/general/test_get_qubit_state.py @@ -0,0 +1,63 @@ +from netqasm.runtime.settings import set_simulator + +set_simulator("simulaqron") + +from netqasm.runtime.application import default_app_instance # noqa: E402 +from netqasm.sdk.external import NetQASMConnection, get_qubit_state # noqa: E402 +from netqasm.sdk import Qubit, EPRSocket # noqa: E402 + +from simulaqron.run.run import run_applications # noqa: E402 + +class TestGetQubit: + @staticmethod + def peek_local_qubit(): + with NetQASMConnection("Alice") as alice: + q = Qubit(alice) + q.H() + + alice.flush() + return get_qubit_state(q) + + @staticmethod + def alice_teleport(): + epr_socket: EPRSocket = EPRSocket("Bob") + with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: + # Create a qubit + q = Qubit(alice) + q.H() + + # Create entanglement + epr = epr_socket.create_keep()[0] + + # Teleport + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + return m1, m2 + + @staticmethod + def bob_teleport(): + epr_socket: EPRSocket = EPRSocket("Alice") + with NetQASMConnection("Bob", epr_sockets=[epr_socket]): + entangled_qubit = epr_socket.recv_keep()[0] + meas = entangled_qubit.measure() + return meas + + def test_get_qubit_state_local(self): + apps = default_app_instance( + [ + ("Alice", TestGetQubit.peek_local_qubit) + ] + ) + raw_results = run_applications(apps, use_app_config=False, enable_logging=False) + + def test_get_qubit_state_teleport(self): + apps = default_app_instance( + [ + ("Alice", TestGetQubit.alice_teleport), + ("Bob", TestGetQubit.bob_teleport) + ] + ) + raw_results = run_applications(apps, use_app_config=False, enable_logging=False) + From 3a82d0e4865eae4601633ae64fbe0c171593b546 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Sep 2025 14:15:33 +0200 Subject: [PATCH 028/277] First dummy implementation for the get_qubit_state primitive at the connection level --- simulaqron/sdk/connection.py | 54 ++++++++++++++++++++- simulaqron/sim_util.py | 26 +++++++++- tests/quick/general/test_get_qubit_state.py | 4 +- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 700f6db4..c55d355e 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -1,11 +1,15 @@ +import ctypes import socket import time +from enum import Enum from threading import Thread from typing import Type, Optional, Callable, List, Tuple +import numpy as np +import netqasm.backend.messages as nmsg from netqasm.backend.messages import (ErrorMessage, MessageHeader, MsgDoneMessage, ReturnArrayMessage, - ReturnRegMessage, deserialize_return_msg, ErrorCode) + ReturnRegMessage, deserialize_return_msg, ErrorCode, Message, APP_ID) from netqasm.lang.ir import GenericInstr from netqasm.lang.operand import Address, Register from netqasm.logging.glob import get_netqasm_logger @@ -196,6 +200,7 @@ def _commit_serialized_message( self._waiting_msg_ids.add(msg_id) length = MessageHeader.len() + len(raw_msg) msg_hdr = MessageHeader(id=msg_id, length=length) + print(f"message = '{bytes(msg_hdr) + raw_msg}'") self._socket.send(bytes(msg_hdr) + raw_msg) # if callback is not None: # raise NotImplementedError("Callback not yet implemented") @@ -342,6 +347,53 @@ def _get_new_msg_id(self) -> int: self._next_msg_id += 1 return msg_id + def get_qubit_state(self, app_id: int, qubit_id: int) -> np.array: + # Here we craft the special message that signals QNodeOS to + # retrieve the state of a qubit. + msg = GetQubitStateMessage(app_id=app_id, qubit_id=qubit_id) + #print(f"new message = '{bytes(msg)}'") + self._commit_message(msg) + return np.array([]) + + +# Definitions for the new message types +QUBIT_REGISTRY_NUM = ctypes.c_uint8 + + +# "Extend" (by redefining the enum) the Message Type +class MewMessageType(Enum): + INIT_NEW_APP = 0x00 + OPEN_EPR_SOCKET = 0x01 + SUBROUTINE = 0x02 + STOP_APP = 0x03 + SIGNAL = 0x04 + GET_QUBIT_STATE = 0xCA + + +# New class for the get qubit state message +class GetQubitStateMessage(Message): + _fields_ = [ + ("app_id", APP_ID), # type: ignore + ("qubit_id", QUBIT_REGISTRY_NUM), + ] + TYPE = MewMessageType.GET_QUBIT_STATE + + def __init__(self, app_id: int = 0, qubit_id: int = 0): + super().__init__(self.TYPE.value) + self.app_id = app_id + self.qubit_id = qubit_id + + +# Really dark magic to *replace* the definitions from the netqasm library +nmsg.MessageType = MewMessageType +nmsg.MESSAGE_CLASSES = { + MewMessageType.INIT_NEW_APP: nmsg.InitNewAppMessage, + MewMessageType.OPEN_EPR_SOCKET: nmsg.OpenEPRSocketMessage, + MewMessageType.SUBROUTINE: nmsg.SubroutineMessage, + MewMessageType.STOP_APP: nmsg.StopAppMessage, + MewMessageType.SIGNAL: nmsg.SignalMessage, + MewMessageType.GET_QUBIT_STATE: GetQubitStateMessage +} def _get_qnodeos_net_config(network_name: str) -> SocketsConfig: network_config_file = simulaqron_settings.network_config_file diff --git a/simulaqron/sim_util.py b/simulaqron/sim_util.py index 208b9dcb..30d73d4e 100644 --- a/simulaqron/sim_util.py +++ b/simulaqron/sim_util.py @@ -1,9 +1,31 @@ from netqasm.logging.glob import get_netqasm_logger +from netqasm.sdk import Qubit + +from simulaqron.sdk import SimulaQronConnection logger = get_netqasm_logger("sim_util") -def get_qubit_state(qubit, reduced_dm=True): - """Currenlty we cannot get the qubit in SimulaQron, just return None""" +def get_qubit_state(qubit: Qubit, reduced_dm: bool = True): + """Currently we cannot get the qubit in SimulaQron, just return None""" logger.warning("Cannot get the qubit state in SimulaQron") + # Since the qubit state data is maintained by the virtual node, we need to + # find a way to "bypass" the QNodeOS layer and retrieve the qubit state from + # the VirtualNode layer + + # Idea for implementing this primitive: + # - We get the connection associated with the given qubit + # - We invoke a method on the connection, which sends a particular message to + # the SimulaQron QNodeOS layer. + # - The SimulaQron QNodeOS layer needs to recognize the message, and invoke a + # proper method on the VirtualNode layer + # - The VirtualNode layer handles the request, and (somehow, depending on the + # underlying qubit simulation engine) retrieves the state of the qubit + # - The VirtualNode layers sends the response back to the QNodeOS layer, which + # simply forwards it to the connection, and finally, here. + assert isinstance(qubit.connection, SimulaQronConnection) + connection: SimulaQronConnection = qubit.connection + # TODO - Retrieve the app_id and the qubit_id to pass in the message + connection.get_qubit_state(0, 0) + print("here") return None diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/general/test_get_qubit_state.py index 1d25149d..cb224410 100644 --- a/tests/quick/general/test_get_qubit_state.py +++ b/tests/quick/general/test_get_qubit_state.py @@ -8,6 +8,7 @@ from simulaqron.run.run import run_applications # noqa: E402 + class TestGetQubit: @staticmethod def peek_local_qubit(): @@ -51,6 +52,7 @@ def test_get_qubit_state_local(self): ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) + print(raw_results) def test_get_qubit_state_teleport(self): apps = default_app_instance( @@ -60,4 +62,4 @@ def test_get_qubit_state_teleport(self): ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) - + print(raw_results) From d2544a92fac56511b9b651d1098d2799824bdd5d Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Sep 2025 14:11:33 +0200 Subject: [PATCH 029/277] Propagate get_qubit_stat message all the way to the virtual node object to actually retrieve the qubit state --- simulaqron/netqasm_backend/executioner.py | 18 +++++++++-- simulaqron/netqasm_backend/factory.py | 2 +- simulaqron/netqasm_backend/qnodeos.py | 21 +++++++++++-- simulaqron/sdk/connection.py | 37 ++++++++++++----------- simulaqron/virtual_node/virtual.py | 13 ++++---- 5 files changed, 61 insertions(+), 30 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index ac99532c..0ef49836 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -4,6 +4,7 @@ from collections import defaultdict from enum import Enum from functools import partial +from typing import Any, Generator from netqasm.backend.executor import EprCmdData, Executor from netqasm.backend.messages import (ErrorCode, ErrorMessage, @@ -21,7 +22,7 @@ from simulaqron.settings import simulaqron_settings from simulaqron.virtual_node.virtual import call_method from twisted.internet import reactor, task -from twisted.internet.defer import inlineCallbacks +from twisted.internet.defer import inlineCallbacks, Deferred class UnknownQubitError(RuntimeError): @@ -87,7 +88,7 @@ def __init__(self, *args, **kwargs): self._network_stack = NetworkStack(self) @property - def factory(self): + def factory(self) -> "NetQASMFactory": return self._factory @property @@ -112,7 +113,7 @@ def get_error_class(remote_err): def add_return_msg_func(self, func): self._return_msg_func = func - def add_factory(self, factory): + def add_factory(self, factory: "NetQASMFactory"): self._factory = factory def _handle_command_exception(self, exc, prog_counter, traceback_str): @@ -819,6 +820,17 @@ def _clear_phys_qubit_in_memory(self, physical_address): yield self.cmd_measure(qubit_id=physical_address, inplace=False) self.remove_qubit_id(qubit_id=physical_address) + @inlineCallbacks + def get_qubit_state(self, qubit_id: int) -> Generator[Deferred | Any, Any, Any]: + self._logger.debug("Retriving the state of qubit id %d", qubit_id) + virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) + qubit = call_method(virt_qubit, "get_qubit") + # TODO - Check what's the difference between invoking "get_qubit" on the virtual qubit + # and invoking "get_state" on the virtual node + # Next remote method should be invoked on the virtual node + #qubit = call_method(virt_qubit, "get_state") + yield qubit + class VirtualQubitRef: def __init__(self, qubit_id=0, timestamp=0, virt=0): diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 6e263396..931c7a43 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -185,7 +185,7 @@ def __init__( self.backend = backend(self) self.network_name = network_name - # Dictionary that keeps qubit dictorionaries for each application + # Dictionary that keeps qubit dictionaries for each application self.qubitList = {} # Lock governing access to the qubitList diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 3a77103d..37b5ad1e 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -1,12 +1,13 @@ -from typing import Optional +from typing import Optional, Dict, Callable, Generator, Any from netqasm.lang.instr import Flavour from twisted.internet.defer import inlineCallbacks -from netqasm.backend.messages import MsgDoneMessage, Message +from netqasm.backend.messages import MsgDoneMessage, Message, MessageType from netqasm.backend.qnodeos import QNodeController from twisted.internet.protocol import Protocol from simulaqron.netqasm_backend.executioner import VanillaSimulaQronExecutioner +from simulaqron.sdk.connection import MewMessageType, GetQubitStateMessage class SubroutineHandler(QNodeController): @@ -36,6 +37,22 @@ def handle_netqasm_message(self, msg_id: int, msg: Message): msg=msg, ) + def _handle_get_qubit_state(self, get_quibit_state_msg: GetQubitStateMessage) -> Generator[Any, None, None]: + assert isinstance(self._executor, VanillaSimulaQronExecutioner) + casted_executor: VanillaSimulaQronExecutioner = self._executor + yield from casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) + + # We override the _get_message_handlers method so we can also handle the "get qubit state" message + def _get_message_handlers(self) -> Dict[MewMessageType | MessageType, Callable]: + return { + MessageType.SIGNAL: self._handle_signal, + MessageType.SUBROUTINE: self._handle_subroutine, + MessageType.INIT_NEW_APP: self._handle_init_new_app, + MessageType.STOP_APP: self._handle_stop_app, + MessageType.OPEN_EPR_SOCKET: self._handle_open_epr_socket, + MewMessageType.GET_QUBIT_STATE: self._handle_get_qubit_state + } + @classmethod def _get_executor_class(cls, flavour: Optional[Flavour] = None): return VanillaSimulaQronExecutioner diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index c55d355e..711f07db 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -3,13 +3,12 @@ import time from enum import Enum from threading import Thread -from typing import Type, Optional, Callable, List, Tuple +from typing import Type, Optional, Callable, List, Tuple, Set -import numpy as np -import netqasm.backend.messages as nmsg from netqasm.backend.messages import (ErrorMessage, MessageHeader, MsgDoneMessage, ReturnArrayMessage, - ReturnRegMessage, deserialize_return_msg, ErrorCode, Message, APP_ID) + ReturnRegMessage, ReturnMessage, deserialize_return_msg, + ErrorCode, Message, APP_ID) from netqasm.lang.ir import GenericInstr from netqasm.lang.operand import Address, Register from netqasm.logging.glob import get_netqasm_logger @@ -17,7 +16,7 @@ from netqasm.sdk.config import LogConfig from netqasm.sdk.connection import BaseNetQASMConnection from netqasm.sdk.network import NetworkInfo -from netqasm.sdk.shared_memory import SharedMemoryManager +from netqasm.sdk.shared_memory import SharedMemoryManager, SharedMemory from netqasm.sdk.transpile import SubroutineTranspiler from simulaqron.general import SimUnsupportedError @@ -65,18 +64,18 @@ def __init__( ) # Next message ID - self._next_msg_id = 0 + self._next_msg_id: int = 0 # Messages IDs we're waiting to be done - self._waiting_msg_ids = set() + self._waiting_msg_ids: Set[int] = set() # Keep track of finished msg IDs - self._done_msg_ids = set() + self._done_msg_ids: Set[int] = set() # Buffer for returned messages self.buf = b"" - self._shared_memory = SharedMemoryManager.create_shared_memory(app_name) + self._shared_memory: SharedMemory = SharedMemoryManager.create_shared_memory(app_name) self._init_new_app(max_qubits=max_qubits) @@ -200,7 +199,6 @@ def _commit_serialized_message( self._waiting_msg_ids.add(msg_id) length = MessageHeader.len() + len(raw_msg) msg_hdr = MessageHeader(id=msg_id, length=length) - print(f"message = '{bytes(msg_hdr) + raw_msg}'") self._socket.send(bytes(msg_hdr) + raw_msg) # if callback is not None: # raise NotImplementedError("Callback not yet implemented") @@ -283,6 +281,7 @@ def _handle_reply(self) -> int: entry=Address(address=ret_msg.address), value=ret_msg.values, ) + # TODO - Handle the qubit state return message here, as a new case elif isinstance(ret_msg, ErrorMessage): if ret_msg.err_code == ErrorCode.UNSUPP.value: raise SimUnsupportedError("Operation not supported") @@ -302,7 +301,7 @@ def block(self): self._wait_for_done() self._logger.debug("All messages done, finished blocking") - def _update_shared_memory(self, entry, value): + def _update_shared_memory(self, entry: Register | Address, value: int | Optional[List[Optional[int]]]): shared_memory = self.shared_memory if isinstance(entry, Register): shared_memory.set_register(entry, value) @@ -347,17 +346,18 @@ def _get_new_msg_id(self) -> int: self._next_msg_id += 1 return msg_id - def get_qubit_state(self, app_id: int, qubit_id: int) -> np.array: + def get_qubit_state(self, app_id: int, qubit_id: int): # Here we craft the special message that signals QNodeOS to # retrieve the state of a qubit. msg = GetQubitStateMessage(app_id=app_id, qubit_id=qubit_id) #print(f"new message = '{bytes(msg)}'") self._commit_message(msg) - return np.array([]) + #self.block() # Definitions for the new message types QUBIT_REGISTRY_NUM = ctypes.c_uint8 +MAX_QUBIT_STATE_LEN = 50 * len(bytes(ctypes.c_uint8())) # "Extend" (by redefining the enum) the Message Type @@ -385,13 +385,14 @@ def __init__(self, app_id: int = 0, qubit_id: int = 0): # Really dark magic to *replace* the definitions from the netqasm library +import netqasm.backend.messages as nmsg nmsg.MessageType = MewMessageType nmsg.MESSAGE_CLASSES = { - MewMessageType.INIT_NEW_APP: nmsg.InitNewAppMessage, - MewMessageType.OPEN_EPR_SOCKET: nmsg.OpenEPRSocketMessage, - MewMessageType.SUBROUTINE: nmsg.SubroutineMessage, - MewMessageType.STOP_APP: nmsg.StopAppMessage, - MewMessageType.SIGNAL: nmsg.SignalMessage, + nmsg.MessageType.INIT_NEW_APP: nmsg.InitNewAppMessage, + nmsg.MessageType.OPEN_EPR_SOCKET: nmsg.OpenEPRSocketMessage, + nmsg.MessageType.SUBROUTINE: nmsg.SubroutineMessage, + nmsg.MessageType.STOP_APP: nmsg.StopAppMessage, + nmsg.MessageType.SIGNAL: nmsg.SignalMessage, MewMessageType.GET_QUBIT_STATE: GetQubitStateMessage } diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index a76e9468..b587551f 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -42,7 +42,7 @@ from simulaqron.virtual_node.basics import QuantumError, NoQubitError, VirtNetError from simulaqron.virtual_node.quantum import SimulatedQubit -from simulaqron.general.host_config import SocketsConfig +from simulaqron.general.host_config import SocketsConfig, Host # We need this import (despite unused) to reraise local errors # We then supress the flake8 errors associated with that from simulaqron.general.errors import * # noqa: F401, F403 @@ -96,7 +96,7 @@ def call_method(obj, method_name, *args, **kwargs): # forming the quantum network # class Backend: - def __init__(self, name, virtual_file: str, network_name: str = "default"): + def __init__(self, name: str, virtual_file: str, network_name: str = "default"): """ Initialize. This will read the configuration file and populate the name,hostname,port information with the information found in the configuration file for the given name. @@ -106,7 +106,7 @@ def __init__(self, name, virtual_file: str, network_name: str = "default"): # Read the configuration file try: self.config = SocketsConfig(virtual_file, network_name=network_name, config_type="vnode") - self.myID = self.config.hostDict[name] + self.myID: Host = self.config.hostDict[name] except KeyError as e: self._logger.error("No such name in the configuration file %s: %s", virtual_file, e) raise e @@ -149,8 +149,9 @@ def start( class VirtualNode(pb.Root): - def __init__(self, ID, config, maxQubits=simulaqron_settings.max_qubits, - maxRegisters=simulaqron_settings.max_registers): + def __init__(self, ID: Host, config: SocketsConfig, + maxQubits: int = simulaqron_settings.max_qubits, + maxRegisters: int = simulaqron_settings.max_registers): """ Initialize storing also our own name, hostname and port. @@ -1232,7 +1233,7 @@ def remote_sim_qubit_num_in_same_reg(self, sim_qubit_num1, sim_qubit_num2): class VirtualQubit(pb.Referenceable): - def __init__(self, virtNode, simNode, simQubit, num): + def __init__(self, virtNode: Host, simNode: Host, simQubit: SimulatedQubit, num: int): """ Creates a virtual qubit object simulated in the specified simulation register backend From f4f949391a0c04446f5177d56ee57d6b821823ed Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 8 Sep 2025 14:00:14 +0200 Subject: [PATCH 030/277] Retrieve quantum register real and imaginary parts, and propagate them to the virtual node layer --- examples/nativeMode/teleport/aliceTest.py | 6 ++- examples/nativeMode/teleport/bobTest.py | 6 ++- simulaqron/netqasm_backend/executioner.py | 56 +++++++++++---------- simulaqron/netqasm_backend/qnodeos.py | 20 +++++++- simulaqron/sdk/connection.py | 51 ++++++++++++++++++- simulaqron/sim_util.py | 4 +- tests/quick/general/test_get_qubit_state.py | 10 ++-- 7 files changed, 113 insertions(+), 40 deletions(-) diff --git a/examples/nativeMode/teleport/aliceTest.py b/examples/nativeMode/teleport/aliceTest.py index 0f9b2ae0..e68bbacd 100644 --- a/examples/nativeMode/teleport/aliceTest.py +++ b/examples/nativeMode/teleport/aliceTest.py @@ -171,5 +171,7 @@ def main(): ################################################################################################## -logging.basicConfig(format="%(asctime)s:%(levelname)s:%(message)s", level=logging.DEBUG) -main() +if __name__ == "__main__": + #simulaqron_settings.sim_backend = "qutip" + logging.basicConfig(format="%(asctime)s:%(levelname)s:%(message)s", level=logging.DEBUG) + main() diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index 9dfef805..f594cf02 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -160,5 +160,7 @@ def main(): ################################################################################################## -logging.basicConfig(format="%(asctime)s:%(levelname)s:%(message)s", level=logging.DEBUG) -main() +if __name__ == "__main__": + #simulaqron_settings.sim_backend = "qutip" + logging.basicConfig(format="%(asctime)s:%(levelname)s:%(message)s", level=logging.DEBUG) + main() diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 0ef49836..e408ff5d 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -4,25 +4,26 @@ from collections import defaultdict from enum import Enum from functools import partial -from typing import Any, Generator +from typing import Any, Generator, List +import netqasm.lang.instr.core as core_instructions +import netqasm.lang.instr.vanilla as vanilla_instructions from netqasm.backend.executor import EprCmdData, Executor from netqasm.backend.messages import (ErrorCode, ErrorMessage, ReturnArrayMessage, ReturnRegMessage) from netqasm.backend.network_stack import BaseNetworkStack -from netqasm.lang import instr as instructions from netqasm.lang import operand from netqasm.qlink_compat import (Basis, BellState, LinkLayerErr, LinkLayerOKTypeK, LinkLayerOKTypeM, LinkLayerOKTypeR, RandomBasis, RequestType, ReturnType) +from twisted.internet import reactor, task +from twisted.internet.defer import inlineCallbacks, Deferred from simulaqron.general import SimUnsupportedError from simulaqron.general.host_config import get_node_id_from_net_config from simulaqron.settings import simulaqron_settings from simulaqron.virtual_node.virtual import call_method -from twisted.internet import reactor, task -from twisted.internet.defer import inlineCallbacks, Deferred class UnknownQubitError(RuntimeError): @@ -54,21 +55,21 @@ def get_purpose_id(self, remote_node_id: int, epr_socket_id: int) -> int: class VanillaSimulaQronExecutioner(Executor): SIMULAQRON_OPS = { - instructions.vanilla.GateXInstruction: "apply_X", - instructions.vanilla.GateYInstruction: "apply_Y", - instructions.vanilla.GateZInstruction: "apply_Z", - instructions.vanilla.GateHInstruction: "apply_H", - instructions.vanilla.GateSInstruction: "apply_S", - instructions.vanilla.GateKInstruction: "apply_K", - instructions.vanilla.GateTInstruction: "apply_T", - instructions.vanilla.CnotInstruction: "cnot_onto", - instructions.vanilla.CphaseInstruction: "cphase_onto", + vanilla_instructions.GateXInstruction: "apply_X", + vanilla_instructions.GateYInstruction: "apply_Y", + vanilla_instructions.GateZInstruction: "apply_Z", + vanilla_instructions.GateHInstruction: "apply_H", + vanilla_instructions.GateSInstruction: "apply_S", + vanilla_instructions.GateKInstruction: "apply_K", + vanilla_instructions.GateTInstruction: "apply_T", + vanilla_instructions.CnotInstruction: "cnot_onto", + vanilla_instructions.CphaseInstruction: "cphase_onto", } ROTATION_AXIS = { - instructions.vanilla.RotXInstruction: (1, 0, 0), - instructions.vanilla.RotYInstruction: (0, 1, 0), - instructions.vanilla.RotZInstruction: (0, 0, 1), + vanilla_instructions.RotXInstruction: (1, 0, 0), + vanilla_instructions.RotYInstruction: (0, 1, 0), + vanilla_instructions.RotZInstruction: (0, 0, 1), } # Dictionary storing the next unique entanglement id for each used (host_app_id,remote_node,remote_app_id) @@ -88,7 +89,7 @@ def __init__(self, *args, **kwargs): self._network_stack = NetworkStack(self) @property - def factory(self) -> "NetQASMFactory": + def factory(self) -> "NetQASMFactory": # noqa: F821 return self._factory @property @@ -113,7 +114,7 @@ def get_error_class(remote_err): def add_return_msg_func(self, func): self._return_msg_func = func - def add_factory(self, factory: "NetQASMFactory"): + def add_factory(self, factory: "NetQASMFactory"): # noqa: F821 self._factory = factory def _handle_command_exception(self, exc, prog_counter, traceback_str): @@ -128,7 +129,7 @@ def _return_msg(self, msg): raise RuntimeError("Cannot return msg since no function is set") self._return_msg_func(msg=msg) - def _instr_qalloc(self, subroutine_id, instr: instructions.core.QAllocInstruction): + def _instr_qalloc(self, subroutine_id, instr: core_instructions.QAllocInstruction): physical_address = yield from super()._instr_qalloc( subroutine_id=subroutine_id, instr=instr, @@ -156,7 +157,7 @@ def cmd_new(self, physical_address): def _do_single_qubit_instr(self, instr, subroutine_id, address): position = self._get_position(subroutine_id=subroutine_id, address=address) - if isinstance(instr, instructions.core.InitInstruction): + if isinstance(instr, core_instructions.InitInstruction): yield self.cmd_reset(qubit_id=position) else: simulaqron_gate = self._get_simulaqron_gate(instr=instr) @@ -281,7 +282,7 @@ def _do_wait(self, delay=0.1): self._logger.debug("waiting a bit") yield d - def _update_shared_memory(self, app_id, entry, value): + def _update_shared_memory(self, app_id: int, entry: operand.Register | operand.Address, value: int | List[int]): if isinstance(entry, operand.Register): self._logger.debug("Updating host about register %s with value %s", entry, value) self._return_msg(msg=ReturnRegMessage( @@ -421,12 +422,12 @@ def cmd_epr( ) # Produce EPR-pair - h_gate = self._get_simulaqron_gate(instr=instructions.vanilla.GateHInstruction()) + h_gate = self._get_simulaqron_gate(instr=vanilla_instructions.GateHInstruction()) yield self.apply_single_qubit_gate( gate=h_gate, qubit_id=qubit_id, ) - cnot_gate = self._get_simulaqron_gate(instr=instructions.vanilla.CnotInstruction()) + cnot_gate = self._get_simulaqron_gate(instr=vanilla_instructions.CnotInstruction()) yield self.apply_two_qubit_gate( gate=cnot_gate, qubit_id1=qubit_id, @@ -530,13 +531,13 @@ def _measure_epr_qubit(self, qubit_id, request, remote: bool): if basis == Basis.Z: pass elif basis == Basis.X: - h_gate = self._get_simulaqron_gate(instr=instructions.vanilla.GateHInstruction()) + h_gate = self._get_simulaqron_gate(instr=vanilla_instructions.GateHInstruction()) yield self.apply_single_qubit_gate( gate=h_gate, qubit_id=qubit_id, ) elif basis == Basis.Y: - k_gate = self._get_simulaqron_gate(instr=instructions.vanilla.GateKInstruction()) + k_gate = self._get_simulaqron_gate(instr=vanilla_instructions.GateKInstruction()) yield self.apply_single_qubit_gate( gate=k_gate, qubit_id=qubit_id, @@ -824,12 +825,13 @@ def _clear_phys_qubit_in_memory(self, physical_address): def get_qubit_state(self, qubit_id: int) -> Generator[Deferred | Any, Any, Any]: self._logger.debug("Retriving the state of qubit id %d", qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) - qubit = call_method(virt_qubit, "get_qubit") # TODO - Check what's the difference between invoking "get_qubit" on the virtual qubit # and invoking "get_state" on the virtual node + #qubit = call_method(virt_qubit, "get_qubit") # Next remote method should be invoked on the virtual node #qubit = call_method(virt_qubit, "get_state") - yield qubit + qubit = yield call_method(virt_qubit, "get_register_RI") + return qubit class VirtualQubitRef: diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 37b5ad1e..049c198a 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -6,8 +6,10 @@ from netqasm.backend.qnodeos import QNodeController from twisted.internet.protocol import Protocol +import simulaqron.settings as settings from simulaqron.netqasm_backend.executioner import VanillaSimulaQronExecutioner -from simulaqron.sdk.connection import MewMessageType, GetQubitStateMessage +from simulaqron.sdk.connection import (MewMessageType, GetQubitStateMessage, + ReturnQubitStateMessage) class SubroutineHandler(QNodeController): @@ -40,7 +42,21 @@ def handle_netqasm_message(self, msg_id: int, msg: Message): def _handle_get_qubit_state(self, get_quibit_state_msg: GetQubitStateMessage) -> Generator[Any, None, None]: assert isinstance(self._executor, VanillaSimulaQronExecutioner) casted_executor: VanillaSimulaQronExecutioner = self._executor - yield from casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) + # The ProjectQ backend also returns an unused mapping; we need to fix that + if settings.simulaqron_settings.sim_backend == settings.SimBackend.PROJECTQ.value: + _, [realvec, imagvec] = yield casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) + else: + realvec, imagvec = yield casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) + # TODO - Should we reconstruct the complex numbers here? they can't be serialized using `bytes()` + # (nor floats) so we might need to find a new way to serialize these values + qubit_state = [r + (1j * j) for r, j in zip(realvec, imagvec)] + # Return a message to the connection object + self._return_qubit_state(get_quibit_state_msg.qubit_id, qubit_state) + yield qubit_state + + def _return_qubit_state(self, qubit_id: int, qubit_state): + qubit_state_message = ReturnQubitStateMessage(qubit_id, qubit_state) + self._return_msg(msg=qubit_state_message) # We override the _get_message_handlers method so we can also handle the "get qubit state" message def _get_message_handlers(self) -> Dict[MewMessageType | MessageType, Callable]: diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 711f07db..e257423f 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -281,7 +281,9 @@ def _handle_reply(self) -> int: entry=Address(address=ret_msg.address), value=ret_msg.values, ) - # TODO - Handle the qubit state return message here, as a new case + elif isinstance(ret_msg, ReturnQubitStateMessage): + # TODO - Properly handle the qubit state return message here + print("received") elif isinstance(ret_msg, ErrorMessage): if ret_msg.err_code == ErrorCode.UNSUPP.value: raise SimUnsupportedError("Operation not supported") @@ -384,8 +386,32 @@ def __init__(self, app_id: int = 0, qubit_id: int = 0): self.qubit_id = qubit_id +class NewReturnMessageType(Enum): + DONE = 0x00 + ERR = 0x01 + RET_ARR = 0x02 + RET_REG = 0x03 + RET_QUBIT_STATE = 0xFE + + +# New class for the return of the get qubit state message +class ReturnQubitStateMessage(ReturnMessage): + _fields_ = [ + ("len", ctypes.c_uint32), + ("qubit_id", QUBIT_REGISTRY_NUM), + ("state", MAX_QUBIT_STATE_LEN * ctypes.c_uint8), # type: ignore + ] + TYPE = NewReturnMessageType.RET_QUBIT_STATE + + def __init__(self, qubit_id: int, state): + super().__init__(self.TYPE.value) + self.qubit_id = qubit_id + self.state = bytes(state) + self.len = len(bytes(ctypes.c_uint32())) + len(bytes(self.qubit_id)) + len(self.state) + + # Really dark magic to *replace* the definitions from the netqasm library -import netqasm.backend.messages as nmsg +import netqasm.backend.messages as nmsg # noqa: E402 nmsg.MessageType = MewMessageType nmsg.MESSAGE_CLASSES = { nmsg.MessageType.INIT_NEW_APP: nmsg.InitNewAppMessage, @@ -395,6 +421,27 @@ def __init__(self, app_id: int = 0, qubit_id: int = 0): nmsg.MessageType.SIGNAL: nmsg.SignalMessage, MewMessageType.GET_QUBIT_STATE: GetQubitStateMessage } +nmsg.RETURN_MESSAGE_CLASSES = { + nmsg.ReturnMessageType.DONE: MsgDoneMessage, + nmsg.ReturnMessageType.ERR: ErrorMessage, + nmsg.ReturnMessageType.RET_REG: ReturnRegMessage, + nmsg.ReturnMessageType.RET_ARR: ReturnArrayMessage, + NewReturnMessageType.RET_QUBIT_STATE: ReturnQubitStateMessage, +} + + +def deserialize_host_msg(raw: bytes) -> Message: + """Convert a serialized message into a `Message` object + + :param raw: serialized message (string of bytes) + :return: deserialized message object + """ + message_type = MewMessageType( + nmsg.MESSAGE_TYPE.from_buffer_copy(raw[:nmsg.MESSAGE_TYPE_BYTES]).value + ) + message_class = nmsg.MESSAGE_CLASSES[message_type] + return message_class.deserialize_from(raw) # type: ignore + def _get_qnodeos_net_config(network_name: str) -> SocketsConfig: network_config_file = simulaqron_settings.network_config_file diff --git a/simulaqron/sim_util.py b/simulaqron/sim_util.py index 30d73d4e..4aa5a907 100644 --- a/simulaqron/sim_util.py +++ b/simulaqron/sim_util.py @@ -26,6 +26,6 @@ def get_qubit_state(qubit: Qubit, reduced_dm: bool = True): assert isinstance(qubit.connection, SimulaQronConnection) connection: SimulaQronConnection = qubit.connection # TODO - Retrieve the app_id and the qubit_id to pass in the message - connection.get_qubit_state(0, 0) - print("here") + # Maybe the app_id is not necessary? + connection.get_qubit_state(0, qubit.qubit_id) return None diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/general/test_get_qubit_state.py index cb224410..91cdf6bc 100644 --- a/tests/quick/general/test_get_qubit_state.py +++ b/tests/quick/general/test_get_qubit_state.py @@ -13,11 +13,15 @@ class TestGetQubit: @staticmethod def peek_local_qubit(): with NetQASMConnection("Alice") as alice: - q = Qubit(alice) - q.H() + qA = Qubit(alice) + qB = Qubit(alice) + qA.H() + qB.X() alice.flush() - return get_qubit_state(q) + stateA = get_qubit_state(qA) + stateB = get_qubit_state(qB) + return stateB @staticmethod def alice_teleport(): From 4f625f2ae63d6297aee278fa478c7fb4897e5ab6 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 8 Sep 2025 14:47:31 +0200 Subject: [PATCH 031/277] Correctly serialize the floats associated to the qubit state to transfer them back to the connection layer over a socket --- simulaqron/netqasm_backend/qnodeos.py | 16 ++++++------ simulaqron/sdk/connection.py | 37 +++++++++++---------------- 2 files changed, 23 insertions(+), 30 deletions(-) diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 049c198a..ff37ec88 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, Callable, Generator, Any +from typing import Optional, Dict, Callable, Generator, Any, List from netqasm.lang.instr import Flavour from twisted.internet.defer import inlineCallbacks @@ -8,7 +8,7 @@ import simulaqron.settings as settings from simulaqron.netqasm_backend.executioner import VanillaSimulaQronExecutioner -from simulaqron.sdk.connection import (MewMessageType, GetQubitStateMessage, +from simulaqron.sdk.connection import (NewMessageType, GetQubitStateMessage, ReturnQubitStateMessage) @@ -51,22 +51,22 @@ def _handle_get_qubit_state(self, get_quibit_state_msg: GetQubitStateMessage) -> # (nor floats) so we might need to find a new way to serialize these values qubit_state = [r + (1j * j) for r, j in zip(realvec, imagvec)] # Return a message to the connection object - self._return_qubit_state(get_quibit_state_msg.qubit_id, qubit_state) - yield qubit_state + self._return_qubit_state(get_quibit_state_msg.qubit_id, realvec, imagvec) + # yield qubit_state - def _return_qubit_state(self, qubit_id: int, qubit_state): - qubit_state_message = ReturnQubitStateMessage(qubit_id, qubit_state) + def _return_qubit_state(self, qubit_id: int, real_part: List[float], imag_part: List[float]): + qubit_state_message = ReturnQubitStateMessage(qubit_id, real_part, imag_part) self._return_msg(msg=qubit_state_message) # We override the _get_message_handlers method so we can also handle the "get qubit state" message - def _get_message_handlers(self) -> Dict[MewMessageType | MessageType, Callable]: + def _get_message_handlers(self) -> Dict[NewMessageType | MessageType, Callable]: return { MessageType.SIGNAL: self._handle_signal, MessageType.SUBROUTINE: self._handle_subroutine, MessageType.INIT_NEW_APP: self._handle_init_new_app, MessageType.STOP_APP: self._handle_stop_app, MessageType.OPEN_EPR_SOCKET: self._handle_open_epr_socket, - MewMessageType.GET_QUBIT_STATE: self._handle_get_qubit_state + NewMessageType.GET_QUBIT_STATE: self._handle_get_qubit_state } @classmethod diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index e257423f..f8f642cc 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -359,11 +359,11 @@ def get_qubit_state(self, app_id: int, qubit_id: int): # Definitions for the new message types QUBIT_REGISTRY_NUM = ctypes.c_uint8 -MAX_QUBIT_STATE_LEN = 50 * len(bytes(ctypes.c_uint8())) +MAX_QUBIT_STATE_LEN = 5 * len(bytes(ctypes.c_float())) # "Extend" (by redefining the enum) the Message Type -class MewMessageType(Enum): +class NewMessageType(Enum): INIT_NEW_APP = 0x00 OPEN_EPR_SOCKET = 0x01 SUBROUTINE = 0x02 @@ -378,7 +378,7 @@ class GetQubitStateMessage(Message): ("app_id", APP_ID), # type: ignore ("qubit_id", QUBIT_REGISTRY_NUM), ] - TYPE = MewMessageType.GET_QUBIT_STATE + TYPE = NewMessageType.GET_QUBIT_STATE def __init__(self, app_id: int = 0, qubit_id: int = 0): super().__init__(self.TYPE.value) @@ -399,27 +399,33 @@ class ReturnQubitStateMessage(ReturnMessage): _fields_ = [ ("len", ctypes.c_uint32), ("qubit_id", QUBIT_REGISTRY_NUM), - ("state", MAX_QUBIT_STATE_LEN * ctypes.c_uint8), # type: ignore + ("real_part", MAX_QUBIT_STATE_LEN * ctypes.c_float), # type: ignore + ("imag_part", MAX_QUBIT_STATE_LEN * ctypes.c_float), # type: ignore ] TYPE = NewReturnMessageType.RET_QUBIT_STATE - def __init__(self, qubit_id: int, state): + def __init__(self, qubit_id: int, real_part: List[float], imag_part: List[float]): super().__init__(self.TYPE.value) self.qubit_id = qubit_id - self.state = bytes(state) - self.len = len(bytes(ctypes.c_uint32())) + len(bytes(self.qubit_id)) + len(self.state) + for i, v in enumerate(real_part): + self.real_part[i] = v + for i, v in enumerate(imag_part): + self.imag_part[i] = v + self.len = (len(bytes(ctypes.c_uint32())) + len(bytes(self.qubit_id)) + + len(self.real_part) + len(self.imag_part)) # Really dark magic to *replace* the definitions from the netqasm library import netqasm.backend.messages as nmsg # noqa: E402 -nmsg.MessageType = MewMessageType +nmsg.MessageType = NewMessageType +nmsg.ReturnMessageType = NewReturnMessageType nmsg.MESSAGE_CLASSES = { nmsg.MessageType.INIT_NEW_APP: nmsg.InitNewAppMessage, nmsg.MessageType.OPEN_EPR_SOCKET: nmsg.OpenEPRSocketMessage, nmsg.MessageType.SUBROUTINE: nmsg.SubroutineMessage, nmsg.MessageType.STOP_APP: nmsg.StopAppMessage, nmsg.MessageType.SIGNAL: nmsg.SignalMessage, - MewMessageType.GET_QUBIT_STATE: GetQubitStateMessage + NewMessageType.GET_QUBIT_STATE: GetQubitStateMessage } nmsg.RETURN_MESSAGE_CLASSES = { nmsg.ReturnMessageType.DONE: MsgDoneMessage, @@ -430,19 +436,6 @@ def __init__(self, qubit_id: int, state): } -def deserialize_host_msg(raw: bytes) -> Message: - """Convert a serialized message into a `Message` object - - :param raw: serialized message (string of bytes) - :return: deserialized message object - """ - message_type = MewMessageType( - nmsg.MESSAGE_TYPE.from_buffer_copy(raw[:nmsg.MESSAGE_TYPE_BYTES]).value - ) - message_class = nmsg.MESSAGE_CLASSES[message_type] - return message_class.deserialize_from(raw) # type: ignore - - def _get_qnodeos_net_config(network_name: str) -> SocketsConfig: network_config_file = simulaqron_settings.network_config_file return SocketsConfig( From 3bab954cbe747d819f27cdea1fac6f14ff8281be Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 8 Sep 2025 15:57:46 +0200 Subject: [PATCH 032/277] Add logic to deserialize real and imaginary parts of the requested qubit state --- simulaqron/netqasm_backend/qnodeos.py | 4 --- simulaqron/sdk/connection.py | 52 +++++++++++---------------- simulaqron/sim_util.py | 6 ++-- 3 files changed, 23 insertions(+), 39 deletions(-) diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index ff37ec88..25484885 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -47,12 +47,8 @@ def _handle_get_qubit_state(self, get_quibit_state_msg: GetQubitStateMessage) -> _, [realvec, imagvec] = yield casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) else: realvec, imagvec = yield casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) - # TODO - Should we reconstruct the complex numbers here? they can't be serialized using `bytes()` - # (nor floats) so we might need to find a new way to serialize these values - qubit_state = [r + (1j * j) for r, j in zip(realvec, imagvec)] # Return a message to the connection object self._return_qubit_state(get_quibit_state_msg.qubit_id, realvec, imagvec) - # yield qubit_state def _return_qubit_state(self, qubit_id: int, real_part: List[float], imag_part: List[float]): qubit_state_message = ReturnQubitStateMessage(qubit_id, real_part, imag_part) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index f8f642cc..0b894450 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -283,7 +283,7 @@ def _handle_reply(self) -> int: ) elif isinstance(ret_msg, ReturnQubitStateMessage): # TODO - Properly handle the qubit state return message here - print("received") + print(f"received: {ret_msg}") elif isinstance(ret_msg, ErrorMessage): if ret_msg.err_code == ErrorCode.UNSUPP.value: raise SimUnsupportedError("Operation not supported") @@ -315,31 +315,6 @@ def _update_shared_memory(self, entry: Register | Address, value: int | Optional f"Cannot update shared memory with entry specified as {entry}" ) - # def add_single_qubit_commands(self, instr, qubit_id): - # # NOTE override to check that formalism supports operation - # if instr in self.NON_STABILIZER_INSTR: - # if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - # raise SimUnsupportedError( - # f"Cannot perform instr {instr} when using stabilizer formalism" - # ) - # super().add_single_qubit_commands(instr=instr, qubit_id=qubit_id) - # - # def add_single_qubit_rotation_commands( - # self, instruction, virtual_qubit_id, n=0, d=0, angle=None - # ): - # # NOTE override to check that formalism supports operation - # if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: - # raise SimUnsupportedError( - # "Cannot perform rotations when using stabilizer formalism" - # ) - # super().add_single_qubit_rotation_commands( - # instruction=instruction, - # virtual_qubit_id=virtual_qubit_id, - # n=n, - # d=d, - # angle=angle, - # ) - def _is_done(self, msg_id) -> bool: return msg_id in self._done_msg_ids @@ -352,14 +327,12 @@ def get_qubit_state(self, app_id: int, qubit_id: int): # Here we craft the special message that signals QNodeOS to # retrieve the state of a qubit. msg = GetQubitStateMessage(app_id=app_id, qubit_id=qubit_id) - #print(f"new message = '{bytes(msg)}'") self._commit_message(msg) - #self.block() # Definitions for the new message types QUBIT_REGISTRY_NUM = ctypes.c_uint8 -MAX_QUBIT_STATE_LEN = 5 * len(bytes(ctypes.c_float())) +MAX_QUBIT_STATE_LEN = 5 # "Extend" (by redefining the enum) the Message Type @@ -397,8 +370,9 @@ class NewReturnMessageType(Enum): # New class for the return of the get qubit state message class ReturnQubitStateMessage(ReturnMessage): _fields_ = [ - ("len", ctypes.c_uint32), ("qubit_id", QUBIT_REGISTRY_NUM), + ("len_real", ctypes.c_uint32), + ("len_imag", ctypes.c_uint32), ("real_part", MAX_QUBIT_STATE_LEN * ctypes.c_float), # type: ignore ("imag_part", MAX_QUBIT_STATE_LEN * ctypes.c_float), # type: ignore ] @@ -407,12 +381,26 @@ class ReturnQubitStateMessage(ReturnMessage): def __init__(self, qubit_id: int, real_part: List[float], imag_part: List[float]): super().__init__(self.TYPE.value) self.qubit_id = qubit_id + self.len_real = len(real_part) + self.len_imag = len(imag_part) + if self.len_real > MAX_QUBIT_STATE_LEN or self.len_imag > MAX_QUBIT_STATE_LEN: + logger.warning("Return qubit state message too long") for i, v in enumerate(real_part): self.real_part[i] = v for i, v in enumerate(imag_part): self.imag_part[i] = v - self.len = (len(bytes(ctypes.c_uint32())) + len(bytes(self.qubit_id)) - + len(self.real_part) + len(self.imag_part)) + + def get_real_part(self) -> List[float]: + real_part: List[float] = [] + for i in range(self.len_real): + real_part.append(float(self.real_part[i])) + return real_part + + def get_imag_part(self) -> List[float]: + imag_part: List[float] = [] + for i in range(self.len_imag): + imag_part.append(float(self.imag_part[i])) + return imag_part # Really dark magic to *replace* the definitions from the netqasm library diff --git a/simulaqron/sim_util.py b/simulaqron/sim_util.py index 4aa5a907..5e1845bd 100644 --- a/simulaqron/sim_util.py +++ b/simulaqron/sim_util.py @@ -25,7 +25,7 @@ def get_qubit_state(qubit: Qubit, reduced_dm: bool = True): # simply forwards it to the connection, and finally, here. assert isinstance(qubit.connection, SimulaQronConnection) connection: SimulaQronConnection = qubit.connection - # TODO - Retrieve the app_id and the qubit_id to pass in the message - # Maybe the app_id is not necessary? - connection.get_qubit_state(0, qubit.qubit_id) + # Retrieve the app_id and the qubit_id to pass in the message + # Maybe the app_id is not necessary? + connection.get_qubit_state(connection.app_id, qubit.qubit_id) return None From 691d991005672368948404d78cda205f7d472cac Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 8 Sep 2025 17:13:33 +0200 Subject: [PATCH 033/277] Correctly return the qubit value to the application layer --- simulaqron/sdk/connection.py | 79 ++++++++++++++++++++++++------------ simulaqron/sim_util.py | 26 +++++++++--- 2 files changed, 73 insertions(+), 32 deletions(-) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 0b894450..6f119959 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -3,7 +3,7 @@ import time from enum import Enum from threading import Thread -from typing import Type, Optional, Callable, List, Tuple, Set +from typing import Type, Optional, Callable, List, Tuple, Set, Dict from netqasm.backend.messages import (ErrorMessage, MessageHeader, MsgDoneMessage, ReturnArrayMessage, @@ -28,20 +28,19 @@ class SimulaQronConnection(BaseNetQASMConnection): - NON_STABILIZER_INSTR = [GenericInstr.T] def __init__( - self, - app_name: str, - app_id: Optional[int] = None, - max_qubits: int = 5, - log_config: Optional[LogConfig] = None, - epr_sockets: Optional[List[EPRSocket]] = None, - compiler: Optional[Type[SubroutineTranspiler]] = None, - socket_address=None, - conn_retry_time: float = 0.1, - network_name: Optional[str] = None, + self, + app_name: str, + app_id: Optional[int] = None, + max_qubits: int = 5, + log_config: Optional[LogConfig] = None, + epr_sockets: Optional[List[EPRSocket]] = None, + compiler: Optional[Type[SubroutineTranspiler]] = None, + socket_address=None, + conn_retry_time: float = 0.1, + network_name: Optional[str] = None, ): super().__init__( app_name=app_name, @@ -75,6 +74,9 @@ def __init__( # Buffer for returned messages self.buf = b"" + # Buffer for retrieved qubit states + self._qubit_states: Dict[int, List[complex]] = {} + self._shared_memory: SharedMemory = SharedMemoryManager.create_shared_memory(app_name) self._init_new_app(max_qubits=max_qubits) @@ -83,9 +85,9 @@ def __init__( @staticmethod def try_connection( - name: str, - socket_address: Optional[Tuple[str, int]] = None, - network_name: str = None, + name: str, + socket_address: Optional[Tuple[str, int]] = None, + network_name: str = None, ): # NOTE using retry_time=None causes an error to be raised of the connection cannot # be established, which can be used to check if the connection is available @@ -99,10 +101,10 @@ def try_connection( @staticmethod def _create_socket( - name: str, - socket_address: Optional[Tuple[str, int]] = None, - network_name: str = None, - retry_time: Optional[float] = 0.1, + name: str, + socket_address: Optional[Tuple[str, int]] = None, + network_name: str = None, + retry_time: Optional[float] = 0.1, ) -> Tuple[SocketsConfig, socket.socket]: # Get network configuration and addresses addr, qnodeos_net = SimulaQronConnection._setup_network_data( @@ -119,9 +121,9 @@ def _create_socket( @staticmethod def _setup_network_data( - name: str, - socket_address: Tuple[str, int], - network_name: str, + name: str, + socket_address: Tuple[str, int], + network_name: str, ) -> Tuple[tuple[socket.AddressFamily, socket.SocketKind, int, str, tuple[str, int]], Optional[SocketsConfig]]: qnodeos_net: Optional[SocketsConfig] = None if socket_address is None: @@ -282,8 +284,14 @@ def _handle_reply(self) -> int: value=ret_msg.values, ) elif isinstance(ret_msg, ReturnQubitStateMessage): - # TODO - Properly handle the qubit state return message here - print(f"received: {ret_msg}") + # We locally store the state info to return it later. We have to + # do this since _handle_reply cannot return values others than the + # message id when handling the reply of the original message + self._store_qubit_state( + ret_msg.qubit_id, + ret_msg.get_real_part(), + ret_msg.get_imag_part() + ) elif isinstance(ret_msg, ErrorMessage): if ret_msg.err_code == ErrorCode.UNSUPP.value: raise SimUnsupportedError("Operation not supported") @@ -315,6 +323,21 @@ def _update_shared_memory(self, entry: Register | Address, value: int | Optional f"Cannot update shared memory with entry specified as {entry}" ) + def _store_qubit_state(self, qubit_id: int, real_part: List[float], imag_part: List[float]): + self._logger.debug("Storing qubit state for qubit_id %d: real=%s, imag=%s", + qubit_id, str(real_part), str(imag_part) + ) + # Reconstruct the complex numbers + self._qubit_states[qubit_id] = [r + (1j * j) for r, j in zip(real_part, imag_part)] + + def _retrieve_qubit_state(self, qubit_id: int) -> List[complex]: + if qubit_id not in self._qubit_states: + logger.error("State for the qubit with id '%d' cannot be found in the conneciton buffer", qubit_id) + return [] + state = self._qubit_states[qubit_id] + del self._qubit_states[qubit_id] + return state + def _is_done(self, msg_id) -> bool: return msg_id in self._done_msg_ids @@ -323,11 +346,14 @@ def _get_new_msg_id(self) -> int: self._next_msg_id += 1 return msg_id - def get_qubit_state(self, app_id: int, qubit_id: int): + def get_qubit_state(self, app_id: int, qubit_id: int) -> List[complex]: # Here we craft the special message that signals QNodeOS to # retrieve the state of a qubit. msg = GetQubitStateMessage(app_id=app_id, qubit_id=qubit_id) - self._commit_message(msg) + # We commit the message, and block until receiving a response + self._commit_message(msg, block=True) + # Retrieve and return the qubit state + return self._retrieve_qubit_state(qubit_id) # Definitions for the new message types @@ -405,6 +431,7 @@ def get_imag_part(self) -> List[float]: # Really dark magic to *replace* the definitions from the netqasm library import netqasm.backend.messages as nmsg # noqa: E402 + nmsg.MessageType = NewMessageType nmsg.ReturnMessageType = NewReturnMessageType nmsg.MESSAGE_CLASSES = { diff --git a/simulaqron/sim_util.py b/simulaqron/sim_util.py index 5e1845bd..269426d7 100644 --- a/simulaqron/sim_util.py +++ b/simulaqron/sim_util.py @@ -1,3 +1,4 @@ +import numpy as np from netqasm.logging.glob import get_netqasm_logger from netqasm.sdk import Qubit @@ -6,9 +7,24 @@ logger = get_netqasm_logger("sim_util") -def get_qubit_state(qubit: Qubit, reduced_dm: bool = True): - """Currently we cannot get the qubit in SimulaQron, just return None""" - logger.warning("Cannot get the qubit state in SimulaQron") +def get_qubit_state(qubit: Qubit, reduced_dm: bool = True) -> np.ndarray: + """Get the state of the qubit, only possible in simulation and can be used for debugging. + + .. note:: The function gets the *current* state of the qubit(s). So make sure the subroutine is flushed + before calling the method. + + Parameters + ---------- + qubit : :class:`~netqasm.sdk.Qubit` + The qubit to get the state of . + reduced_dm : bool + Unused; declared to keep compatibility with other simulation engines + + Returns + ------- + np.array + The state as a density matrix. + """ # Since the qubit state data is maintained by the virtual node, we need to # find a way to "bypass" the QNodeOS layer and retrieve the qubit state from # the VirtualNode layer @@ -26,6 +42,4 @@ def get_qubit_state(qubit: Qubit, reduced_dm: bool = True): assert isinstance(qubit.connection, SimulaQronConnection) connection: SimulaQronConnection = qubit.connection # Retrieve the app_id and the qubit_id to pass in the message - # Maybe the app_id is not necessary? - connection.get_qubit_state(connection.app_id, qubit.qubit_id) - return None + return np.array(connection.get_qubit_state(connection.app_id, qubit.qubit_id)) From 7f128cdcd163ed6fc20b806a181de3856eee523a Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 8 Sep 2025 17:14:10 +0200 Subject: [PATCH 034/277] Add test cases for peeking basic and unflushed qubits --- tests/quick/general/test_get_qubit_state.py | 50 ++++++++++++++++++--- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/general/test_get_qubit_state.py index 91cdf6bc..03ad083a 100644 --- a/tests/quick/general/test_get_qubit_state.py +++ b/tests/quick/general/test_get_qubit_state.py @@ -1,3 +1,4 @@ +import numpy as np from netqasm.runtime.settings import set_simulator set_simulator("simulaqron") @@ -10,18 +11,33 @@ class TestGetQubit: + @staticmethod + def peek_unflushed_qubit(): + with NetQASMConnection("Alice") as alice: + q_a = Qubit(alice) + state_a = get_qubit_state(q_a) + return state_a + + @staticmethod + def peek_init_qubit(): + with NetQASMConnection("Alice") as alice: + q_a = Qubit(alice) + alice.flush() + state_a = get_qubit_state(q_a) + return state_a + @staticmethod def peek_local_qubit(): with NetQASMConnection("Alice") as alice: - qA = Qubit(alice) - qB = Qubit(alice) - qA.H() - qB.X() + q_a = Qubit(alice) + q_b = Qubit(alice) + q_a.H() + q_b.X() alice.flush() - stateA = get_qubit_state(qA) - stateB = get_qubit_state(qB) - return stateB + state_a = get_qubit_state(q_a) + state_b = get_qubit_state(q_b) + return state_a, state_b @staticmethod def alice_teleport(): @@ -49,6 +65,26 @@ def bob_teleport(): meas = entangled_qubit.measure() return meas + def test_peek_unflushed_qubit(self): + # TODO - This method should raise an Exception; not freeze the simulation + # Maybe we need to re-throw exceptions from lower levers? + apps = default_app_instance( + [ + ("Alice", TestGetQubit.peek_init_qubit) + ] + ) + _ = run_applications(apps, use_app_config=False, enable_logging=False) + assert False + + def test_get_basic_state_local(self): + apps = default_app_instance( + [ + ("Alice", TestGetQubit.peek_init_qubit) + ] + ) + raw_results = run_applications(apps, use_app_config=False, enable_logging=False) + assert np.array_equal(raw_results[0]["app_Alice"], np.array([1.0+0j, 0+0j])) + def test_get_qubit_state_local(self): apps = default_app_instance( [ From 027b50389e3dfa3fcb5e75420c12c0bb8db032d8 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 9 Sep 2025 14:53:39 +0200 Subject: [PATCH 035/277] Add propagation of error messages from inner layers --- simulaqron/netqasm_backend/executioner.py | 79 +++++++++++------------ simulaqron/netqasm_backend/factory.py | 21 +++--- simulaqron/netqasm_backend/qnodeos.py | 7 +- simulaqron/sdk/connection.py | 41 ++++++++++-- 4 files changed, 90 insertions(+), 58 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index e408ff5d..514067c4 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -9,8 +9,7 @@ import netqasm.lang.instr.core as core_instructions import netqasm.lang.instr.vanilla as vanilla_instructions from netqasm.backend.executor import EprCmdData, Executor -from netqasm.backend.messages import (ErrorCode, ErrorMessage, - ReturnArrayMessage, ReturnRegMessage) +from netqasm.backend.messages import (ErrorCode, ReturnArrayMessage, ReturnRegMessage) from netqasm.backend.network_stack import BaseNetworkStack from netqasm.lang import operand from netqasm.qlink_compat import (Basis, BellState, LinkLayerErr, @@ -22,6 +21,7 @@ from simulaqron.general import SimUnsupportedError from simulaqron.general.host_config import get_node_id_from_net_config +from simulaqron.sdk.connection import RichErrorMessage from simulaqron.settings import simulaqron_settings from simulaqron.virtual_node.virtual import call_method @@ -53,7 +53,6 @@ def get_purpose_id(self, remote_node_id: int, epr_socket_id: int) -> int: class VanillaSimulaQronExecutioner(Executor): - SIMULAQRON_OPS = { vanilla_instructions.GateXInstruction: "apply_X", vanilla_instructions.GateYInstruction: "apply_Y", @@ -120,9 +119,9 @@ def add_factory(self, factory: "NetQASMFactory"): # noqa: F821 def _handle_command_exception(self, exc, prog_counter, traceback_str): self._logger.error("At line %d: %s\n%s", prog_counter, exc, traceback_str) if isinstance(exc, SimUnsupportedError): - self._return_msg(msg=ErrorMessage(err_code=ErrorCode.UNSUPP)) + self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.UNSUPP, err_msg="Unsupported simulation engine")) else: - self._return_msg(msg=ErrorMessage(err_code=ErrorCode.GENERAL)) + self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg="General error")) def _return_msg(self, msg): if self._return_msg_func is None: @@ -300,13 +299,13 @@ def _update_shared_memory(self, app_id: int, entry: operand.Register | operand.A raise TypeError(f"Cannot update shared memory with entry specified as {entry}") def _do_create_epr( - self, - subroutine_id, - remote_node_id, - epr_socket_id, - q_array_address, - arg_array_address, - ent_results_array_address, + self, + subroutine_id, + remote_node_id, + epr_socket_id, + q_array_address, + arg_array_address, + ent_results_array_address, ): create_request = self._get_create_request( subroutine_id=subroutine_id, @@ -344,12 +343,12 @@ def _do_create_epr( ) def _do_recv_epr( - self, - subroutine_id, - remote_node_id, - epr_socket_id, - q_array_address, - ent_results_array_address + self, + subroutine_id, + remote_node_id, + epr_socket_id, + q_array_address, + ent_results_array_address ): app_id = self._get_app_id(subroutine_id=subroutine_id) num_pairs = self._get_num_pairs_from_array( @@ -382,13 +381,13 @@ def _get_remote_epr_socket_id(self, epr_socket_id): @inlineCallbacks def cmd_epr( - self, - create_id, - remote_node_id, - epr_socket_id, - remote_epr_socket_id, - qubit_id, - create_request, + self, + create_id, + remote_node_id, + epr_socket_id, + remote_epr_socket_id, + qubit_id, + create_request, ): """ Create EPR pair with another node. @@ -628,12 +627,12 @@ def _get_new_create_id(cls, remote_node_id): @inlineCallbacks def send_epr_half( - self, - qubit_id, - epr_socket_id, - remote_node_name, - remote_epr_socket_id, - ent_info + self, + qubit_id, + epr_socket_id, + remote_node_name, + remote_epr_socket_id, + ent_info ): """ Send qubit to another node. @@ -674,13 +673,13 @@ def send_epr_half( @inlineCallbacks def send_epr_outcome_half( - self, - epr_socket_id, - remote_node_name, - remote_epr_socket_id, - ent_info, - remote_outcome, - remote_basis + self, + epr_socket_id, + remote_node_name, + remote_epr_socket_id, + ent_info, + remote_outcome, + remote_basis ): """ Send outcome from measure directly to another node. @@ -827,9 +826,9 @@ def get_qubit_state(self, qubit_id: int) -> Generator[Deferred | Any, Any, Any]: virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) # TODO - Check what's the difference between invoking "get_qubit" on the virtual qubit # and invoking "get_state" on the virtual node - #qubit = call_method(virt_qubit, "get_qubit") + # qubit = call_method(virt_qubit, "get_qubit") # Next remote method should be invoked on the virtual node - #qubit = call_method(virt_qubit, "get_state") + # qubit = call_method(virt_qubit, "get_state") qubit = yield call_method(virt_qubit, "get_register_RI") return qubit diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 931c7a43..732bccba 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -26,19 +26,19 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import sys from typing import Type +from netqasm.backend.messages import MessageHeader, ErrorCode, deserialize_host_msg, Message, \ + InitNewAppMessage +from netqasm.logging.glob import get_netqasm_logger from twisted.internet import reactor from twisted.internet.defer import DeferredLock, inlineCallbacks from twisted.internet.protocol import Factory, Protocol, connectionDone from twisted.internet.task import deferLater -from netqasm.logging.glob import get_netqasm_logger -from netqasm.backend.messages import MessageHeader, ErrorMessage, ErrorCode, deserialize_host_msg - from simulaqron.general.host_config import SocketsConfig, Host from simulaqron.netqasm_backend.qnodeos import SubroutineHandler +from simulaqron.sdk.connection import RichErrorMessage from simulaqron.settings import simulaqron_settings from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor @@ -116,8 +116,8 @@ def log_handled_message(self, result): @inlineCallbacks def log_error(self, failure): self._logger.error("Handling message failed with failure = %s", failure) - sys.stderr.write(str(failure)) - self._return_msg(msg=ErrorMessage(err_code=ErrorCode.GENERAL)) + # sys.stderr.write(str(failure)) + self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(failure.value))) yield deferLater(reactor, 0.1, self.stop) def stop(self): @@ -135,7 +135,7 @@ def _parse_message(self): return msg_hdr.id, msg - def _handle_init_new_app(self, msg): + def _handle_init_new_app(self, msg: InitNewAppMessage): app_id = msg.app_id self._add_app(app_id=app_id) max_qubits = msg.max_qubits @@ -147,11 +147,14 @@ def _handle_init_new_app(self, msg): max_qubits=max_qubits, ) - def _return_msg(self, msg): + def _return_msg(self, msg: Message | bytes): """ Return a msg to the host. """ - self.transport.write(msg) + if isinstance(msg, bytes): + self.transport.write(msg) + else: + self.transport.write(bytes(msg)) ############################################################################### diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 25484885..c4f8fdae 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -1,9 +1,9 @@ from typing import Optional, Dict, Callable, Generator, Any, List -from netqasm.lang.instr import Flavour -from twisted.internet.defer import inlineCallbacks from netqasm.backend.messages import MsgDoneMessage, Message, MessageType from netqasm.backend.qnodeos import QNodeController +from netqasm.lang.instr import Flavour +from twisted.internet.defer import inlineCallbacks from twisted.internet.protocol import Protocol import simulaqron.settings as settings @@ -13,7 +13,8 @@ class SubroutineHandler(QNodeController): - def __init__(self, factory, instr_log_dir: Optional[str] = None, flavour: Optional[Flavour] = None): + def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = None, + flavour: Optional[Flavour] = None): super().__init__(factory.name, instr_log_dir=instr_log_dir, flavour=flavour) self.factory = factory diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 6f119959..4394aef1 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -5,7 +5,7 @@ from threading import Thread from typing import Type, Optional, Callable, List, Tuple, Set, Dict -from netqasm.backend.messages import (ErrorMessage, MessageHeader, +from netqasm.backend.messages import (MessageHeader, MsgDoneMessage, ReturnArrayMessage, ReturnRegMessage, ReturnMessage, deserialize_return_msg, ErrorCode, Message, APP_ID) @@ -202,8 +202,6 @@ def _commit_serialized_message( length = MessageHeader.len() + len(raw_msg) msg_hdr = MessageHeader(id=msg_id, length=length) self._socket.send(bytes(msg_hdr) + raw_msg) - # if callback is not None: - # raise NotImplementedError("Callback not yet implemented") if block: self._wait_for_done(msg_id=msg_id) else: @@ -292,11 +290,11 @@ def _handle_reply(self) -> int: ret_msg.get_real_part(), ret_msg.get_imag_part() ) - elif isinstance(ret_msg, ErrorMessage): + elif isinstance(ret_msg, RichErrorMessage): if ret_msg.err_code == ErrorCode.UNSUPP.value: raise SimUnsupportedError("Operation not supported") else: - raise RuntimeError(f"Received error message from backend: {ret_msg}") + raise RuntimeError(f"Received error message from backend: {ret_msg.get_err_msg()}") else: raise NotImplementedError(f"Unknown return message of type {type(ret_msg)}") # Continue handling replies until a done @@ -359,6 +357,7 @@ def get_qubit_state(self, app_id: int, qubit_id: int) -> List[complex]: # Definitions for the new message types QUBIT_REGISTRY_NUM = ctypes.c_uint8 MAX_QUBIT_STATE_LEN = 5 +MAX_ERR_MSG_LEN = 100 # "Extend" (by redefining the enum) the Message Type @@ -393,6 +392,36 @@ class NewReturnMessageType(Enum): RET_QUBIT_STATE = 0xFE +class RichErrorMessage(ReturnMessage): + """Enriched message to the Host that an error occurred at the quantum node controller.""" + + _fields_ = [ + ("err_code", ctypes.c_uint8), + ("err_msg_len", ctypes.c_uint32), + ("err_msg", MAX_ERR_MSG_LEN * ctypes.c_uint8), + ] + + # This works because the enum types are mapped to the very same value + TYPE = NewReturnMessageType.ERR + + def __init__(self, err_code: ErrorCode, err_msg: str): + super().__init__(self.TYPE.value) + err_bytes = err_msg.encode("utf-8") + if len(err_bytes) > MAX_ERR_MSG_LEN: + logger.warning("Reported error message too long") + self.err_code = err_code.value + self.err_msg_len = len(err_bytes) + for i, v in enumerate(err_bytes): + self.err_msg[i] = v + print("here") + + def get_err_msg(self) -> str: + bytes_vals: List[int] = [] + for i in range(self.err_msg_len): + bytes_vals.append(self.err_msg[i]) + return bytes(bytes_vals).decode("utf-8") + + # New class for the return of the get qubit state message class ReturnQubitStateMessage(ReturnMessage): _fields_ = [ @@ -444,7 +473,7 @@ def get_imag_part(self) -> List[float]: } nmsg.RETURN_MESSAGE_CLASSES = { nmsg.ReturnMessageType.DONE: MsgDoneMessage, - nmsg.ReturnMessageType.ERR: ErrorMessage, + nmsg.ReturnMessageType.ERR: RichErrorMessage, nmsg.ReturnMessageType.RET_REG: ReturnRegMessage, nmsg.ReturnMessageType.RET_ARR: ReturnArrayMessage, NewReturnMessageType.RET_QUBIT_STATE: ReturnQubitStateMessage, From 8c95a71951aa30ea70cc46857f12be30bdc7942c Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 9 Sep 2025 14:54:12 +0200 Subject: [PATCH 036/277] Add test case for error message checking --- simulaqron/netqasm_backend/factory.py | 3 +-- simulaqron/sdk/connection.py | 7 +++++-- tests/quick/general/test_get_qubit_state.py | 16 ++++++++++------ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 732bccba..a37ffdf1 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -115,8 +115,7 @@ def log_handled_message(self, result): @inlineCallbacks def log_error(self, failure): - self._logger.error("Handling message failed with failure = %s", failure) - # sys.stderr.write(str(failure)) + self._logger.error("Handling message failed with failure = %s", failure.value) self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(failure.value))) yield deferLater(reactor, 0.1, self.stop) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 4394aef1..65867859 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -206,6 +206,9 @@ def _commit_serialized_message( self._wait_for_done(msg_id=msg_id) else: # Execute callback in a new thread after the subroutine is finished + # TODO - This is not well designed; we need to avoid blocking, and execute the callback + # once the ack msg *for the message id* arrives!!!! + # Moreover, we need to stop the thread and join it right after that! thread = Thread( target=self._wait_for_done, kwargs={ @@ -249,7 +252,8 @@ def _read_more_data(self): self.buf += data else: self.buf = data - self._logger.debug("Got new data %s on socket to qnodeos", data) + self._logger.debug("Got new data '%s' on socket to qnodeos", data) + print(f"Got new data '{data}' on socket to qnodeos") def _handle_reply(self) -> int: """Handle all next replies until a done message and return the msg ID for the done""" @@ -413,7 +417,6 @@ def __init__(self, err_code: ErrorCode, err_msg: str): self.err_msg_len = len(err_bytes) for i, v in enumerate(err_bytes): self.err_msg[i] = v - print("here") def get_err_msg(self) -> str: bytes_vals: List[int] = [] diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/general/test_get_qubit_state.py index 03ad083a..2a159789 100644 --- a/tests/quick/general/test_get_qubit_state.py +++ b/tests/quick/general/test_get_qubit_state.py @@ -1,4 +1,5 @@ import numpy as np +import pytest from netqasm.runtime.settings import set_simulator set_simulator("simulaqron") @@ -12,7 +13,7 @@ class TestGetQubit: @staticmethod - def peek_unflushed_qubit(): + def peek_new_unflushed_qubit(): with NetQASMConnection("Alice") as alice: q_a = Qubit(alice) state_a = get_qubit_state(q_a) @@ -65,16 +66,17 @@ def bob_teleport(): meas = entangled_qubit.measure() return meas - def test_peek_unflushed_qubit(self): + def test_peek_new_unflushed_qubit(self): # TODO - This method should raise an Exception; not freeze the simulation # Maybe we need to re-throw exceptions from lower levers? apps = default_app_instance( [ - ("Alice", TestGetQubit.peek_init_qubit) + ("Alice", TestGetQubit.peek_new_unflushed_qubit) ] ) - _ = run_applications(apps, use_app_config=False, enable_logging=False) - assert False + with pytest.raises(RuntimeError) as exc: + _ = run_applications(apps, use_app_config=False, enable_logging=False) + assert "Alice: Qubit 0 not found" in str(exc.value) def test_get_basic_state_local(self): apps = default_app_instance( @@ -83,8 +85,9 @@ def test_get_basic_state_local(self): ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) - assert np.array_equal(raw_results[0]["app_Alice"], np.array([1.0+0j, 0+0j])) + assert np.array_equal(raw_results[0]["app_Alice"], np.array([1.0 + 0j, 0 + 0j])) + @pytest.mark.skip(reason="todo - fix this test") def test_get_qubit_state_local(self): apps = default_app_instance( [ @@ -94,6 +97,7 @@ def test_get_qubit_state_local(self): raw_results = run_applications(apps, use_app_config=False, enable_logging=False) print(raw_results) + @pytest.mark.skip(reason="todo - fix this test") def test_get_qubit_state_teleport(self): apps = default_app_instance( [ From 4dd9cbc29f9f874dad43949b70f7d0fc72064b0b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 10 Sep 2025 15:31:30 +0200 Subject: [PATCH 037/277] Fix delayed execution of callbacks when handling a response message --- simulaqron/netqasm_backend/factory.py | 2 +- simulaqron/network.py | 11 ++- simulaqron/run/run.py | 27 +++--- simulaqron/sdk/connection.py | 128 ++++++++++++++------------ 4 files changed, 90 insertions(+), 78 deletions(-) diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index a37ffdf1..637c71d7 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -117,7 +117,7 @@ def log_handled_message(self, result): def log_error(self, failure): self._logger.error("Handling message failed with failure = %s", failure.value) self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(failure.value))) - yield deferLater(reactor, 0.1, self.stop) + #yield deferLater(reactor, 0.1, self.stop) def stop(self): self.factory.stop() diff --git a/simulaqron/network.py b/simulaqron/network.py index dfa3653a..9dc11896 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -29,7 +29,7 @@ import time import random -import multiprocessing as mp +from multiprocess.context import Process from typing import List import networkx as nx @@ -74,7 +74,7 @@ def __init__(self, name=None, nodes=None, topology=None, network_config_file=Non else: self.name = name - self.processes: List[mp.Process] = [] + self.processes: List[Process] = [] self._logger = get_netqasm_logger(f"{self.__class__.__name__}({self.name})") if network_config_file is None: @@ -154,12 +154,12 @@ def _setup_processes(self): """ Setup the processes forming the network, however they are not started yet. """ - mp.set_start_method("spawn", force=True) + #mp.set_start_method("spawn", force=True) for node in self.nodes: - process_virtual = mp.Process( + process_virtual = Process( target=start_vnode, args=(node, self.name, get_log_level()), name=f"VirtNode {node}" ) - process_qnodeos = mp.Process( + process_qnodeos = Process( target=start_qnodeos, args=(node, self.name, get_log_level()), name=f"QnodeOSNode {node}" ) self.processes += [process_virtual, process_qnodeos] @@ -198,6 +198,7 @@ def stop(self): time.sleep(0.1) try: p.terminate() + p.join() except Exception as err: self._logger.warning("Could not terminate one of the processes in the" "network due to error: %s", err) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 1bf9e100..02f71f3a 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -18,6 +18,7 @@ from netqasm.sdk.shared_memory import SharedMemoryManager from netqasm.util.yaml import dump_yaml from simulaqron.network import Network +from simulaqron.sdk import SimulaQronConnection from simulaqron.settings import SimBackend, simulaqron_settings from simulaqron.toolbox import has_module @@ -150,7 +151,8 @@ def run_applications( net_cfg = None for _ in range(num_rounds): - with Pool(len(app_names)) as executor: + with Pool(len(app_names) + 3) as executor: + SimulaQronConnection.PROCESS_POOL = executor # Start the backend process network = run_sim_backend(app_names, sim_backend, net_cfg) @@ -182,17 +184,18 @@ def run_applications( # Join the application processes and the backend names = [f'app_{app_name}' for app_name in app_names] result = {} - for future, name in as_completed(app_futures, names): - result[name] = future.get() - # if results_file is not None: - # save_results(results=results, results_file=results_file) - if enable_logging: - assert timed_log_dir is not None - path = os.path.join(timed_log_dir, "results.yaml") - dump_yaml(data=result, file_path=path) - - results.append(result) - network.stop() + try: + for future, name in as_completed(app_futures, names): + result[name] = future.get() + # if results_file is not None: + # save_results(results=results, results_file=results_file) + if enable_logging: + assert timed_log_dir is not None + path = os.path.join(timed_log_dir, "results.yaml") + dump_yaml(data=result, file_path=path) + finally: + results.append(result) + network.stop() reset(save_loggers=True) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 65867859..e7b6b184 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -2,7 +2,7 @@ import socket import time from enum import Enum -from threading import Thread +from multiprocess.pool import Pool from typing import Type, Optional, Callable, List, Tuple, Set, Dict from netqasm.backend.messages import (MessageHeader, @@ -30,6 +30,12 @@ class SimulaQronConnection(BaseNetQASMConnection): NON_STABILIZER_INSTR = [GenericInstr.T] + # Process poool will be set externally when launching the applications + # This is due to the fact that the code creating the connections will run + # *inside a pool worker*, so it cannot create a new process pool because the + # worker itself is a daemon process. + PROCESS_POOL: Optional[Pool] = None + def __init__( self, app_name: str, @@ -65,6 +71,9 @@ def __init__( # Next message ID self._next_msg_id: int = 0 + # Messages ID's with deferred callbacks + self._messages_callbacks: Dict[int, Callable] = {} + # Messages IDs we're waiting to be done self._waiting_msg_ids: Set[int] = set() @@ -203,21 +212,12 @@ def _commit_serialized_message( msg_hdr = MessageHeader(id=msg_id, length=length) self._socket.send(bytes(msg_hdr) + raw_msg) if block: - self._wait_for_done(msg_id=msg_id) + self._wait_for_done(msg_id=msg_id, callback=callback) else: - # Execute callback in a new thread after the subroutine is finished - # TODO - This is not well designed; we need to avoid blocking, and execute the callback - # once the ack msg *for the message id* arrives!!!! - # Moreover, we need to stop the thread and join it right after that! - thread = Thread( - target=self._wait_for_done, - kwargs={ - "msg_id": msg_id, - "callback": callback, - } - ) - thread.daemon = True - thread.start() + # Register the callback so it will be called once the message + # is acknowledged + self._messages_callbacks[msg_id] = callback + def _wait_for_done(self, msg_id: Optional[int] = None, callback: Optional[Callable] = None): """Waits for a message to be declared done by qnodeos. @@ -253,56 +253,64 @@ def _read_more_data(self): else: self.buf = data self._logger.debug("Got new data '%s' on socket to qnodeos", data) - print(f"Got new data '{data}' on socket to qnodeos") def _handle_reply(self) -> int: """Handle all next replies until a done message and return the msg ID for the done""" # Try to read next message from the buffer otherwise read some more and try again - try: - ret_msg = deserialize_return_msg(self.buf) - except ValueError: - # Incomplete message - self._logger.debug("Incomplete message") - time.sleep(0.1) - self._read_more_data() - return self._handle_reply() - - # Remove the data of this message from the buffer - self.buf = self.buf[len(ret_msg):] - - self._logger.debug("Got message %s", ret_msg) - if isinstance(ret_msg, MsgDoneMessage): - self._waiting_msg_ids.remove(ret_msg.msg_id) - self._done_msg_ids.add(ret_msg.msg_id) - return ret_msg.msg_id - elif isinstance(ret_msg, ReturnRegMessage): - self._update_shared_memory( - entry=Register.from_raw(raw=ret_msg.register), - value=ret_msg.value, - ) - elif isinstance(ret_msg, ReturnArrayMessage): - self._update_shared_memory( - entry=Address(address=ret_msg.address), - value=ret_msg.values, - ) - elif isinstance(ret_msg, ReturnQubitStateMessage): - # We locally store the state info to return it later. We have to - # do this since _handle_reply cannot return values others than the - # message id when handling the reply of the original message - self._store_qubit_state( - ret_msg.qubit_id, - ret_msg.get_real_part(), - ret_msg.get_imag_part() - ) - elif isinstance(ret_msg, RichErrorMessage): - if ret_msg.err_code == ErrorCode.UNSUPP.value: - raise SimUnsupportedError("Operation not supported") + # TODO - Change the while to use a condition that can be controlled externally + while True: + try: + ret_msg = deserialize_return_msg(self.buf) + except ValueError: + # Incomplete message + self._logger.debug("Incomplete message") + time.sleep(0.1) + self._read_more_data() + continue + + # Remove the data of this message from the buffer + self.buf = self.buf[len(ret_msg):] + + self._logger.debug("Got message %s", ret_msg) + if isinstance(ret_msg, MsgDoneMessage): + self._waiting_msg_ids.remove(ret_msg.msg_id) + self._done_msg_ids.add(ret_msg.msg_id) + # Call the registered callback, if any + if ret_msg.msg_id in self._messages_callbacks: + if SimulaQronConnection.PROCESS_POOL is None: + raise RuntimeError("Callback process pool was not set correctly") + if self._messages_callbacks[ret_msg.msg_id] is not None: + SimulaQronConnection.PROCESS_POOL.apply_async( + self._messages_callbacks[ret_msg.msg_id] + ) + del self._messages_callbacks[ret_msg.msg_id] + return ret_msg.msg_id + elif isinstance(ret_msg, ReturnRegMessage): + self._update_shared_memory( + entry=Register.from_raw(raw=ret_msg.register), + value=ret_msg.value, + ) + elif isinstance(ret_msg, ReturnArrayMessage): + self._update_shared_memory( + entry=Address(address=ret_msg.address), + value=ret_msg.values, + ) + elif isinstance(ret_msg, ReturnQubitStateMessage): + # We locally store the state info to return it later. We have to + # do this since _handle_reply cannot return values others than the + # message id when handling the reply of the original message + self._store_qubit_state( + ret_msg.qubit_id, + ret_msg.get_real_part(), + ret_msg.get_imag_part() + ) + elif isinstance(ret_msg, RichErrorMessage): + if ret_msg.err_code == ErrorCode.UNSUPP.value: + raise SimUnsupportedError("Operation not supported") + else: + raise RuntimeError(f"Received error message from backend: {ret_msg.get_err_msg()}") else: - raise RuntimeError(f"Received error message from backend: {ret_msg.get_err_msg()}") - else: - raise NotImplementedError(f"Unknown return message of type {type(ret_msg)}") - # Continue handling replies until a done - return self._handle_reply() + raise NotImplementedError(f"Unknown return message of type {type(ret_msg)}") def block(self): while len(self._waiting_msg_ids) > 0: From c1bcf2f097aa262997d523b06b89a2617465b6d2 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 12 Sep 2025 13:57:34 +0200 Subject: [PATCH 038/277] Use twisted asyncio reactor to avoid deadlocks --- simulaqron/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simulaqron/__init__.py b/simulaqron/__init__.py index 3d2ca820..c8398ff8 100644 --- a/simulaqron/__init__.py +++ b/simulaqron/__init__.py @@ -1,3 +1,6 @@ +from twisted.internet import asyncioreactor +asyncioreactor.install() + from simulaqron.toolbox.reset import check_config_files # Create default config files if there are none From 076372bbb5b33e924632c4ac48bb1cdb7a053043 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 12 Sep 2025 14:32:32 +0200 Subject: [PATCH 039/277] Use multiprocess "SpawnProcess" class to use the correct strategy for creating vnode and qnodeos sub-processes --- simulaqron/netqasm_backend/executioner.py | 4 ++-- simulaqron/netqasm_backend/factory.py | 2 +- simulaqron/netqasm_backend/qnodeos.py | 4 ++-- simulaqron/network.py | 2 +- simulaqron/run/run.py | 14 +++++--------- simulaqron/sdk/connection.py | 13 ++++++++++--- simulaqron/virtual_node/virtual.py | 2 +- 7 files changed, 22 insertions(+), 19 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 514067c4..231afe31 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -822,14 +822,14 @@ def _clear_phys_qubit_in_memory(self, physical_address): @inlineCallbacks def get_qubit_state(self, qubit_id: int) -> Generator[Deferred | Any, Any, Any]: - self._logger.debug("Retriving the state of qubit id %d", qubit_id) + self._logger.debug("Retrieving the state of qubit id %d", qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) # TODO - Check what's the difference between invoking "get_qubit" on the virtual qubit # and invoking "get_state" on the virtual node # qubit = call_method(virt_qubit, "get_qubit") # Next remote method should be invoked on the virtual node # qubit = call_method(virt_qubit, "get_state") - qubit = yield call_method(virt_qubit, "get_register_RI") + qubit = yield from call_method(virt_qubit, "get_register_RI") return qubit diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 637c71d7..f975bbb9 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -34,7 +34,7 @@ from twisted.internet import reactor from twisted.internet.defer import DeferredLock, inlineCallbacks from twisted.internet.protocol import Factory, Protocol, connectionDone -from twisted.internet.task import deferLater +#from twisted.internet.task import deferLater from simulaqron.general.host_config import SocketsConfig, Host from simulaqron.netqasm_backend.qnodeos import SubroutineHandler diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index c4f8fdae..36b666d6 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -45,9 +45,9 @@ def _handle_get_qubit_state(self, get_quibit_state_msg: GetQubitStateMessage) -> casted_executor: VanillaSimulaQronExecutioner = self._executor # The ProjectQ backend also returns an unused mapping; we need to fix that if settings.simulaqron_settings.sim_backend == settings.SimBackend.PROJECTQ.value: - _, [realvec, imagvec] = yield casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) + _, [realvec, imagvec] = yield from casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) else: - realvec, imagvec = yield casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) + realvec, imagvec = yield from casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) # Return a message to the connection object self._return_qubit_state(get_quibit_state_msg.qubit_id, realvec, imagvec) diff --git a/simulaqron/network.py b/simulaqron/network.py index 9dc11896..cb064d6b 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -29,7 +29,7 @@ import time import random -from multiprocess.context import Process +from multiprocess.context import SpawnProcess as Process from typing import List import networkx as nx diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 02f71f3a..acfda7c0 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -52,25 +52,20 @@ def reset(save_loggers=False): reload(logging) -def check_sim_backend(sim_backend: SimBackend): - if sim_backend in [SimBackend.PROJECTQ, SimBackend.QUTIP]: - assert has_module.main(sim_backend.value), f"To use {sim_backend} as backend you need to install the package" - - def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_config_file: Optional[str]): logger.debug("Starting simulaqron sim_backend process with nodes %s", node_names) - check_sim_backend(sim_backend) + if sim_backend in [SimBackend.PROJECTQ, SimBackend.QUTIP]: + assert has_module.main(sim_backend.value),\ + f"To use {sim_backend} as backend you need to install the package" simulaqron_settings.sim_backend = sim_backend.value new_network = True if network_config_file is None else False - network = Network( + return Network( name="default", nodes=node_names, network_config_file=network_config_file, force=True, new=new_network ) - network.start() - return network def run_applications( @@ -155,6 +150,7 @@ def run_applications( SimulaQronConnection.PROCESS_POOL = executor # Start the backend process network = run_sim_backend(app_names, sim_backend, net_cfg) + network.start() # Start the application processes app_futures = [] diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index e7b6b184..21f2b4a0 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -2,6 +2,7 @@ import socket import time from enum import Enum + from multiprocess.pool import Pool from typing import Type, Optional, Callable, List, Tuple, Set, Dict @@ -210,7 +211,8 @@ def _commit_serialized_message( self._waiting_msg_ids.add(msg_id) length = MessageHeader.len() + len(raw_msg) msg_hdr = MessageHeader(id=msg_id, length=length) - self._socket.send(bytes(msg_hdr) + raw_msg) + written = self._socket.send(bytes(msg_hdr) + raw_msg) + self._logger.debug("Written %d bytes to NetQASM server", written) if block: self._wait_for_done(msg_id=msg_id, callback=callback) else: @@ -218,7 +220,6 @@ def _commit_serialized_message( # is acknowledged self._messages_callbacks[msg_id] = callback - def _wait_for_done(self, msg_id: Optional[int] = None, callback: Optional[Callable] = None): """Waits for a message to be declared done by qnodeos. If `msg_id` is None (default), then we wait once for any message to be done. @@ -247,7 +248,10 @@ def _wait_for_done(self, msg_id: Optional[int] = None, callback: Optional[Callab def _read_more_data(self): """Reads in some more data on the socket to qnodeos""" - data = self._socket.recv(1024) + try: + data = self._socket.recv(1024) + except Exception as err: + self._logger.exception("Error in recv from NetQASM server", err) if self.buf: self.buf += data else: @@ -267,6 +271,9 @@ def _handle_reply(self) -> int: time.sleep(0.1) self._read_more_data() continue + except Exception as exc: + self._logger.exception("Unexpected exception:", exc) + continue # Remove the data of this message from the buffer self.buf = self.buf[len(ret_msg):] diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index b587551f..f40668af 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -137,7 +137,7 @@ def start( self._logger.error("NetQASM server address (%d) is already in use.", self.myID.port) return except Exception as e: - self._logger.error("Critical error when starting local virtual node server: %s", e) + self._logger.exception("Critical error when starting local virtual node server: %s", e) return From d77a93470dc5c2178c89c971fffad5eacb70a6fa Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 12 Sep 2025 14:33:09 +0200 Subject: [PATCH 040/277] Update test for get_qubit_state --- tests/quick/general/test_get_qubit_state.py | 23 ++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/general/test_get_qubit_state.py index 2a159789..57d16d4f 100644 --- a/tests/quick/general/test_get_qubit_state.py +++ b/tests/quick/general/test_get_qubit_state.py @@ -2,16 +2,27 @@ import pytest from netqasm.runtime.settings import set_simulator +from simulaqron.settings import simulaqron_settings, SimBackend + set_simulator("simulaqron") from netqasm.runtime.application import default_app_instance # noqa: E402 from netqasm.sdk.external import NetQASMConnection, get_qubit_state # noqa: E402 from netqasm.sdk import Qubit, EPRSocket # noqa: E402 -from simulaqron.run.run import run_applications # noqa: E402 +from simulaqron.run.run import run_applications, reset # noqa: E402 class TestGetQubit: + @pytest.fixture(autouse=True) + def network(self): + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + yield + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + reset() + @staticmethod def peek_new_unflushed_qubit(): with NetQASMConnection("Alice") as alice: @@ -66,9 +77,7 @@ def bob_teleport(): meas = entangled_qubit.measure() return meas - def test_peek_new_unflushed_qubit(self): - # TODO - This method should raise an Exception; not freeze the simulation - # Maybe we need to re-throw exceptions from lower levers? + def test_peek_new_unflushed_qubit(self, network): apps = default_app_instance( [ ("Alice", TestGetQubit.peek_new_unflushed_qubit) @@ -78,7 +87,7 @@ def test_peek_new_unflushed_qubit(self): _ = run_applications(apps, use_app_config=False, enable_logging=False) assert "Alice: Qubit 0 not found" in str(exc.value) - def test_get_basic_state_local(self): + def test_get_basic_state_local(self, network): apps = default_app_instance( [ ("Alice", TestGetQubit.peek_init_qubit) @@ -88,7 +97,7 @@ def test_get_basic_state_local(self): assert np.array_equal(raw_results[0]["app_Alice"], np.array([1.0 + 0j, 0 + 0j])) @pytest.mark.skip(reason="todo - fix this test") - def test_get_qubit_state_local(self): + def test_get_qubit_state_local(self, network): apps = default_app_instance( [ ("Alice", TestGetQubit.peek_local_qubit) @@ -98,7 +107,7 @@ def test_get_qubit_state_local(self): print(raw_results) @pytest.mark.skip(reason="todo - fix this test") - def test_get_qubit_state_teleport(self): + def test_get_qubit_state_teleport(self, network): apps = default_app_instance( [ ("Alice", TestGetQubit.alice_teleport), From 18e2a6d60e597de04823594078a27dc9027a9d80 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 12 Sep 2025 14:36:13 +0200 Subject: [PATCH 041/277] Linter changes --- simulaqron/__init__.py | 2 +- simulaqron/netqasm_backend/qnodeos.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/simulaqron/__init__.py b/simulaqron/__init__.py index c8398ff8..335f11be 100644 --- a/simulaqron/__init__.py +++ b/simulaqron/__init__.py @@ -1,7 +1,7 @@ from twisted.internet import asyncioreactor asyncioreactor.install() -from simulaqron.toolbox.reset import check_config_files +from simulaqron.toolbox.reset import check_config_files # noqa: E402 # Create default config files if there are none check_config_files() diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 36b666d6..0e75f54a 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -13,7 +13,7 @@ class SubroutineHandler(QNodeController): - def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = None, + def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = None, # noqa: F821 flavour: Optional[Flavour] = None): super().__init__(factory.name, instr_log_dir=instr_log_dir, flavour=flavour) From 8bc36be60382a646bbb547fd6c08c60166bddf43 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 12 Sep 2025 16:54:10 +0200 Subject: [PATCH 042/277] Centralize the way to access the twisted reactor to avoid installing it more than once --- pyproject.toml | 3 ++- simulaqron/__init__.py | 3 --- simulaqron/local/setup.py | 3 ++- simulaqron/netqasm_backend/executioner.py | 3 ++- simulaqron/netqasm_backend/factory.py | 2 +- simulaqron/reactor.py | 8 ++++++++ simulaqron/start/start_qnodeos.py | 2 +- simulaqron/start/start_vnode.py | 3 ++- simulaqron/virtual_node/quantum.py | 4 ++-- simulaqron/virtual_node/virtual.py | 2 +- tests/quick/general/resources/test.json | 18 +++++++++--------- tests/slow/merges/test_merges.py | 2 +- 12 files changed, 31 insertions(+), 22 deletions(-) create mode 100644 simulaqron/reactor.py diff --git a/pyproject.toml b/pyproject.toml index 07e019a3..613dc7cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,8 @@ dev = [ ] [tool.setuptools.packages.find] -exclude = ["tests", "docs", "examples"] +include = ["simulaqron"] +exclude = ["simulaqron/config"] [project.scripts] simulaqron = "simulaqron.simulaqron:cli" diff --git a/simulaqron/__init__.py b/simulaqron/__init__.py index 335f11be..07900990 100644 --- a/simulaqron/__init__.py +++ b/simulaqron/__init__.py @@ -1,6 +1,3 @@ -from twisted.internet import asyncioreactor -asyncioreactor.install() - from simulaqron.toolbox.reset import check_config_files # noqa: E402 # Create default config files if there are none diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index 0ca757a1..b32f1db0 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -30,11 +30,12 @@ import logging import time from twisted.spread import pb -from twisted.internet import reactor, error +from twisted.internet import error from twisted.internet.defer import DeferredList from twisted.internet.error import ReactorNotRunning from simulaqron.settings import simulaqron_settings +from simulaqron.reactor import reactor ##################################################################################################### diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 231afe31..6d607f0a 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -16,9 +16,10 @@ LinkLayerOKTypeK, LinkLayerOKTypeM, LinkLayerOKTypeR, RandomBasis, RequestType, ReturnType) -from twisted.internet import reactor, task +from twisted.internet import task from twisted.internet.defer import inlineCallbacks, Deferred +from simulaqron.reactor import reactor from simulaqron.general import SimUnsupportedError from simulaqron.general.host_config import get_node_id_from_net_config from simulaqron.sdk.connection import RichErrorMessage diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index f975bbb9..58284ad8 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -31,11 +31,11 @@ from netqasm.backend.messages import MessageHeader, ErrorCode, deserialize_host_msg, Message, \ InitNewAppMessage from netqasm.logging.glob import get_netqasm_logger -from twisted.internet import reactor from twisted.internet.defer import DeferredLock, inlineCallbacks from twisted.internet.protocol import Factory, Protocol, connectionDone #from twisted.internet.task import deferLater +from simulaqron.reactor import reactor from simulaqron.general.host_config import SocketsConfig, Host from simulaqron.netqasm_backend.qnodeos import SubroutineHandler from simulaqron.sdk.connection import RichErrorMessage diff --git a/simulaqron/reactor.py b/simulaqron/reactor.py new file mode 100644 index 00000000..c9633ac4 --- /dev/null +++ b/simulaqron/reactor.py @@ -0,0 +1,8 @@ +from twisted.internet import asyncioreactor +asyncioreactor.install() + +from twisted.internet import reactor # noqa: E402 + +__all__ = [ + "reactor" +] diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 6224a8a6..4811f5a3 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -4,12 +4,12 @@ import signal from timeit import default_timer as timer -from twisted.internet import reactor from twisted.internet.error import ConnectionRefusedError, CannotListenError from twisted.spread import pb from netqasm.logging.glob import get_netqasm_logger, set_log_level +from simulaqron.reactor import reactor from simulaqron.netqasm_backend.factory import NetQASMFactory from simulaqron.netqasm_backend.qnodeos import SubroutineHandler from simulaqron.general.host_config import SocketsConfig diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 12a5fd3b..6444f61a 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -31,11 +31,12 @@ import sys import signal from functools import partial -from twisted.internet import reactor from netqasm.logging.glob import get_netqasm_logger, set_log_level + from simulaqron.virtual_node.virtual import Backend from simulaqron.settings import simulaqron_settings +from simulaqron.reactor import reactor logger = get_netqasm_logger("start_vnode") diff --git a/simulaqron/virtual_node/quantum.py b/simulaqron/virtual_node/quantum.py index a4600689..b5f942a0 100644 --- a/simulaqron/virtual_node/quantum.py +++ b/simulaqron/virtual_node/quantum.py @@ -32,13 +32,13 @@ import numpy as np from twisted.spread import pb from twisted.internet.defer import DeferredLock -from twisted.internet import reactor from twisted.internet.task import deferLater from twisted.internet.defer import inlineCallbacks from netqasm.logging.glob import get_netqasm_logger -from simulaqron import settings +from simulaqron.reactor import reactor +import simulaqron.settings as settings class SimulatedQubit(pb.Referenceable): diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index f40668af..351a0e62 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -32,7 +32,6 @@ from collections import deque from twisted.spread import pb -from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks, DeferredLock, Deferred, DeferredList from twisted.internet.task import deferLater from twisted.internet.error import ConnectionRefusedError, CannotListenError @@ -47,6 +46,7 @@ # We then supress the flake8 errors associated with that from simulaqron.general.errors import * # noqa: F401, F403 from simulaqron.settings import simulaqron_settings, SimBackend +from simulaqron.reactor import reactor if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: from simulaqron.virtual_node.qutip_simulator import QutipEngine as QEngine diff --git a/tests/quick/general/resources/test.json b/tests/quick/general/resources/test.json index ea36bb01..09ab62aa 100644 --- a/tests/quick/general/resources/test.json +++ b/tests/quick/general/resources/test.json @@ -4,29 +4,29 @@ "Alice": { "app_socket": [ "localhost", - 8000 + 8004 ], "qnodeos_socket": [ "localhost", - 8001 + 8028 ], "vnode_socket": [ "localhost", - 8002 + 8029 ] }, "Bob": { "app_socket": [ "localhost", - 8003 + 8037 ], "qnodeos_socket": [ "localhost", - 8004 + 8040 ], "vnode_socket": [ "localhost", - 8005 + 8041 ] } }, @@ -37,15 +37,15 @@ "Charlie": { "app_socket": [ "localhost", - 8006 + 8042 ], "qnodeos_socket": [ "localhost", - 8007 + 8043 ], "vnode_socket": [ "localhost", - 8008 + 8044 ] } }, diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index ff7a2680..98728f11 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -5,7 +5,6 @@ import multiprocessing as mp from twisted.spread import pb -from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks from simulaqron.general.host_config import SocketsConfig @@ -13,6 +12,7 @@ from simulaqron.network import Network from simulaqron.settings import simulaqron_settings, SimBackend from simulaqron.toolbox.stabilizer_states import StabilizerState +from simulaqron.reactor import reactor class localNode(pb.Root): From 85016bbd5b7c70cdd146d44e6b5ad2bd1656f8ab Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 15 Sep 2025 14:40:43 +0200 Subject: [PATCH 043/277] Fix test for qubit teleportation --- tests/quick/general/test_get_qubit_state.py | 39 +++++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/general/test_get_qubit_state.py index 57d16d4f..4cf9cfe7 100644 --- a/tests/quick/general/test_get_qubit_state.py +++ b/tests/quick/general/test_get_qubit_state.py @@ -1,14 +1,17 @@ +import math + import numpy as np import pytest from netqasm.runtime.settings import set_simulator +from netqasm.sdk.classical_communication.message import StructuredMessage from simulaqron.settings import simulaqron_settings, SimBackend set_simulator("simulaqron") from netqasm.runtime.application import default_app_instance # noqa: E402 -from netqasm.sdk.external import NetQASMConnection, get_qubit_state # noqa: E402 -from netqasm.sdk import Qubit, EPRSocket # noqa: E402 +from netqasm.sdk.external import NetQASMConnection, Socket, get_qubit_state # noqa: E402 +from netqasm.sdk import Qubit, EPRSocket, set_qubit_state # noqa: E402 from simulaqron.run.run import run_applications, reset # noqa: E402 @@ -53,29 +56,44 @@ def peek_local_qubit(): @staticmethod def alice_teleport(): + classical_socket = Socket("Alice", "Bob") epr_socket: EPRSocket = EPRSocket("Bob") with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: # Create a qubit q = Qubit(alice) - q.H() - - # Create entanglement + set_qubit_state(q, math.pi / 2.0, math.pi / 4.0) epr = epr_socket.create_keep()[0] + alice.flush() + alice_state = get_qubit_state(q) # Teleport q.cnot(epr) q.H() + m1 = q.measure() m2 = epr.measure() - return m1, m2 + alice.flush() + + classical_socket.send_structured(StructuredMessage("Corrections", f"{int(m1)},{int(m2)}")) + return {"m1": int(m1), "m2": int(m2), "alice_state": alice_state} @staticmethod def bob_teleport(): + classical_socket = Socket("Bob", "Alice") epr_socket: EPRSocket = EPRSocket("Alice") - with NetQASMConnection("Bob", epr_sockets=[epr_socket]): + with NetQASMConnection("Bob", epr_sockets=[epr_socket]) as bob: entangled_qubit = epr_socket.recv_keep()[0] - meas = entangled_qubit.measure() - return meas + bob.flush() + + msg = classical_socket.recv_structured() + m1, m2 = msg.payload.split(",") + if int(m2) == 1: + entangled_qubit.X() + if int(m1) == 1: + entangled_qubit.Z() + bob.flush() + bob_state = get_qubit_state(entangled_qubit) + return {"bob_state": bob_state} def test_peek_new_unflushed_qubit(self, network): apps = default_app_instance( @@ -106,7 +124,6 @@ def test_get_qubit_state_local(self, network): raw_results = run_applications(apps, use_app_config=False, enable_logging=False) print(raw_results) - @pytest.mark.skip(reason="todo - fix this test") def test_get_qubit_state_teleport(self, network): apps = default_app_instance( [ @@ -115,4 +132,4 @@ def test_get_qubit_state_teleport(self, network): ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) - print(raw_results) + assert np.array_equal(raw_results[0]["app_Alice"]["alice_state"], raw_results[0]["app_Bob"]["bob_state"]) From d5315ef57a4642398f58fc5ac6cebd955c11f503 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 15 Sep 2025 15:50:15 +0200 Subject: [PATCH 044/277] Add test for comparing the state of local qubits --- tests/quick/general/test_get_qubit_state.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/general/test_get_qubit_state.py index 4cf9cfe7..f150c9df 100644 --- a/tests/quick/general/test_get_qubit_state.py +++ b/tests/quick/general/test_get_qubit_state.py @@ -52,7 +52,7 @@ def peek_local_qubit(): alice.flush() state_a = get_qubit_state(q_a) state_b = get_qubit_state(q_b) - return state_a, state_b + return {"state_a": state_a, "state_b": state_b} @staticmethod def alice_teleport(): @@ -112,9 +112,8 @@ def test_get_basic_state_local(self, network): ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) - assert np.array_equal(raw_results[0]["app_Alice"], np.array([1.0 + 0j, 0 + 0j])) + assert np.array_equal(raw_results[0]["app_Alice"], np.array([1.0 + 0.0j, 0 + 0.0j])) - @pytest.mark.skip(reason="todo - fix this test") def test_get_qubit_state_local(self, network): apps = default_app_instance( [ @@ -123,6 +122,12 @@ def test_get_qubit_state_local(self, network): ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) print(raw_results) + #qubit A: H(|0>) = 1/sqrt(2) |0> + 1/sqrt(2) |1> = 1/sqrt(2) [1 0] + 1/sqrt(2) [0 1] + expected = np.array([1.0 / math.sqrt(2.0) + 0.0j, 1.0 / math.sqrt(2.0) + 0.0j]) + # Note: Due to loss in serialization, we allow a tolerance of 1e-5 when comparing all the members + assert np.isclose(raw_results[0]["app_Alice"]["state_a"], expected, rtol=1e-5).all() + # qubit B: X(|0>) = |1> = [0 1] + assert np.array_equal(raw_results[0]["app_Alice"]["state_b"], np.array([0.0 + 0.0j, 1 + 0.0j])) def test_get_qubit_state_teleport(self, network): apps = default_app_instance( From a3b18cca9a51d8f6ab30554944bef20780598e84 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 15 Sep 2025 16:06:54 +0200 Subject: [PATCH 045/277] Add test for trying to get the state of an unflushed qubit --- simulaqron/sdk/connection.py | 3 ++ tests/quick/general/test_get_qubit_state.py | 33 +++++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 21f2b4a0..12dd4d9e 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -364,6 +364,9 @@ def _get_new_msg_id(self) -> int: return msg_id def get_qubit_state(self, app_id: int, qubit_id: int) -> List[complex]: + # Check if there are some pending (unflushed) operations on the qubit + if len(self.builder._pending_commands) > 0: + raise RuntimeError(f"Qubit {qubit_id} has unflushed operations") # Here we craft the special message that signals QNodeOS to # retrieve the state of a qubit. msg = GetQubitStateMessage(app_id=app_id, qubit_id=qubit_id) diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/general/test_get_qubit_state.py index f150c9df..00ebcdbf 100644 --- a/tests/quick/general/test_get_qubit_state.py +++ b/tests/quick/general/test_get_qubit_state.py @@ -26,6 +26,8 @@ def network(self): simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value reset() + # Here we define the quantum programs used in the tests + @staticmethod def peek_new_unflushed_qubit(): with NetQASMConnection("Alice") as alice: @@ -33,6 +35,17 @@ def peek_new_unflushed_qubit(): state_a = get_qubit_state(q_a) return state_a + @staticmethod + def peek_unflushed_qubit(): + with NetQASMConnection("Alice") as alice: + q = Qubit(alice) + q.H() + alice.flush() + + q.X() + get_qubit_state(q) + return + @staticmethod def peek_init_qubit(): with NetQASMConnection("Alice") as alice: @@ -74,7 +87,9 @@ def alice_teleport(): m2 = epr.measure() alice.flush() - classical_socket.send_structured(StructuredMessage("Corrections", f"{int(m1)},{int(m2)}")) + classical_socket.send_structured( + StructuredMessage("Corrections", f"{int(m1)}/{int(m2)}") + ) return {"m1": int(m1), "m2": int(m2), "alice_state": alice_state} @staticmethod @@ -86,7 +101,7 @@ def bob_teleport(): bob.flush() msg = classical_socket.recv_structured() - m1, m2 = msg.payload.split(",") + m1, m2 = msg.payload.split("/") if int(m2) == 1: entangled_qubit.X() if int(m1) == 1: @@ -95,6 +110,8 @@ def bob_teleport(): bob_state = get_qubit_state(entangled_qubit) return {"bob_state": bob_state} + # From here down, the actual tests + def test_peek_new_unflushed_qubit(self, network): apps = default_app_instance( [ @@ -103,7 +120,17 @@ def test_peek_new_unflushed_qubit(self, network): ) with pytest.raises(RuntimeError) as exc: _ = run_applications(apps, use_app_config=False, enable_logging=False) - assert "Alice: Qubit 0 not found" in str(exc.value) + assert "Qubit 0 has unflushed operations" in str(exc.value) + + def test_peek_unflushed_qubit(self, network): + apps = default_app_instance( + [ + ("Alice", TestGetQubit.peek_unflushed_qubit) + ] + ) + with pytest.raises(RuntimeError) as exc: + _ = run_applications(apps, use_app_config=False, enable_logging=False) + assert "Qubit 0 has unflushed operations" in str(exc.value) def test_get_basic_state_local(self, network): apps = default_app_instance( From c4d9e86e511d1007c103f9e7cc5f081a3f154adb Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 16 Sep 2025 11:43:53 +0200 Subject: [PATCH 046/277] Move get_qubit_state test to the netqasm folder --- tests/quick/{general => netqasm}/test_get_qubit_state.py | 1 + 1 file changed, 1 insertion(+) rename tests/quick/{general => netqasm}/test_get_qubit_state.py (98%) diff --git a/tests/quick/general/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py similarity index 98% rename from tests/quick/general/test_get_qubit_state.py rename to tests/quick/netqasm/test_get_qubit_state.py index 00ebcdbf..77ab27eb 100644 --- a/tests/quick/general/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -139,6 +139,7 @@ def test_get_basic_state_local(self, network): ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) + # We expect the qubit to be initialized in the |0> state = [1 0] assert np.array_equal(raw_results[0]["app_Alice"], np.array([1.0 + 0.0j, 0 + 0.0j])) def test_get_qubit_state_local(self, network): From 33f341f517342f950dfc7d5190b08e46fa2c645b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 22 Sep 2025 13:23:50 +0200 Subject: [PATCH 047/277] Add tests for qfree operations --- tests/quick/netqasm/test_free_qubit.py | 146 +++++++++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 tests/quick/netqasm/test_free_qubit.py diff --git a/tests/quick/netqasm/test_free_qubit.py b/tests/quick/netqasm/test_free_qubit.py new file mode 100644 index 00000000..6377ba42 --- /dev/null +++ b/tests/quick/netqasm/test_free_qubit.py @@ -0,0 +1,146 @@ +import pytest + +from netqasm.runtime.settings import set_simulator + +from simulaqron.run.run import reset, run_applications +from simulaqron.settings import simulaqron_settings, SimBackend + +set_simulator("simulaqron") + +from netqasm.runtime.application import default_app_instance # noqa: E402 +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import Qubit # noqa: E402 + + +class TestFreeQubit: + @pytest.fixture(autouse=True) + def network(self): + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + yield + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + reset() + + @staticmethod + def too_many_qubits(): + with NetQASMConnection("Alice", max_qubits=2) as alice: + q_a = Qubit(alice) # noqa: F841 + q_b = Qubit(alice) # noqa: F841 + q_c = Qubit(alice) # noqa: F841 + alice.flush() + + @staticmethod + def release_qubit(): + with NetQASMConnection("Alice", max_qubits=2) as alice: + q_a = Qubit(alice) # noqa: F841 + q_b = Qubit(alice) + q_b.free() + alice.flush() + return len(alice.active_qubits) + + @staticmethod + def release_qubit_b(): + with NetQASMConnection("Alice", max_qubits=2) as alice: + q_a = Qubit(alice) # noqa: F841 + q_b = Qubit(alice) + alice.flush() + q_b.free() + alice.flush() + return len(alice.active_qubits) + + # This program compiles to the following NetQASM subroutine: + # set Q0 0 + # qalloc Q0 + # init Q0 + # set Q0 1 + # qalloc Q0 + # init Q0 + # set Q0 1 + # qfree Q0 + # set Q0 2 + # qalloc Q0 + # init Q0 + # Note that this subroutine uses 3 virtual qubit addresses, *but* only uses + # 2 physical qubits, since virtual address 1 is freed *before* allocating + # virtual address 2. Executing that test case in SimulaQron and SquidASM + # (configuring a QPU with 2 physical qubits) have different behaviors: + # In SquidASM, this works correctly, since the executor class (which handles + # the execution of the subroutine) assigns dynamically the physical qubit + # addresses, meaning that the executor *realizes* that virtual qubit 1 is + # freed, so it has physical availability for allocating virtual qubit 2. + # In SimulaQron, this doesn't work correctly. SimulaQron uses the NetQASM + # built-in memory manager (netqasm.sdk.memmgr module), which assigns the + # physical qubit ids. This implementation is flawed, since *it assumes + # the virtual id will map to the same physical id*. In this case, a QPU + # with 2 qubits, whill have physical qubit 0, and 1. When trying to allocate + # a physical qubit ID for virtual qubit 2, the memory manager will fail, since + # it will try to allocate it to physical qubit 2, which is outside the + # allowed physical qubit ids + @staticmethod + def release_and_reuse_qubit(): + with NetQASMConnection("Alice", max_qubits=2) as alice: + q_a = Qubit(alice) # noqa: F841 + q_b = Qubit(alice) + q_b.free() + q_c = Qubit(alice) # noqa: F841 + alice.flush() + return len(alice.active_qubits) + + @staticmethod + def release_and_reuse_qubit_b(): + with NetQASMConnection("Alice", max_qubits=2) as alice: + q_a = Qubit(alice) # noqa: F841 + q_b = Qubit(alice) + q_b.free() + alice.flush() + q_c = Qubit(alice) # noqa: F841 + alice.flush() + return len(alice.active_qubits) + + # Here we define the quantum programs used in the tests + def test_too_many_qubits(self, network): + apps = default_app_instance( + [ + ("Alice", TestFreeQubit.too_many_qubits) + ] + ) + with pytest.raises(RuntimeError) as exc: + _ = run_applications(apps, use_app_config=False, enable_logging=False) + assert "Virtual address 2 is outside the unit module (app ID 0) which has length 2" in str(exc.value) + + def test_release_qubit(self, network): + apps = default_app_instance( + [ + ("Alice", TestFreeQubit.release_qubit) + ] + ) + result = run_applications(apps, use_app_config=False, enable_logging=False) + assert result[0]["app_Alice"] == 1 + + def test_release_qubit_b(self, network): + apps = default_app_instance( + [ + ("Alice", TestFreeQubit.release_qubit_b) + ] + ) + result = run_applications(apps, use_app_config=False, enable_logging=False) + assert result[0]["app_Alice"] == 1 + + def test_release_and_reuse_qubit(self, network): + apps = default_app_instance( + [ + ("Alice", TestFreeQubit.release_and_reuse_qubit) + ] + ) + result = run_applications(apps, use_app_config=False, enable_logging=False) + assert result[0]["app_Alice"] == 2 + + def test_release_and_reuse_qubit_b(self, network): + apps = default_app_instance( + [ + ("Alice", TestFreeQubit.release_and_reuse_qubit_b) + ] + ) + result = run_applications(apps, use_app_config=False, enable_logging=False) + assert result[0]["app_Alice"] == 2 From a8c6df82efc40ff76b56dd0fba005ad7ac0627a3 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 22 Sep 2025 13:24:41 +0200 Subject: [PATCH 048/277] Fix to forward ValueError exceptions from the SimulaQron executioner al the way to the connection --- simulaqron/netqasm_backend/executioner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 6d607f0a..b9d01762 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -121,6 +121,8 @@ def _handle_command_exception(self, exc, prog_counter, traceback_str): self._logger.error("At line %d: %s\n%s", prog_counter, exc, traceback_str) if isinstance(exc, SimUnsupportedError): self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.UNSUPP, err_msg="Unsupported simulation engine")) + elif isinstance(exc, ValueError): + self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(exc))) else: self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg="General error")) From b0da2ec498758342bcc3674eb1cb6c9af01fc1fe Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 22 Sep 2025 16:51:42 +0200 Subject: [PATCH 049/277] Update netqasm dependency to use version including qfree patch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 613dc7cf..879eccd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "networkx>=3.0,<4.0", "click>=8.0,<9.0", "daemons>=1.3.0,<2.0.0", - "netqasm>=1.0.0,<2.0.0", + "netqasm>=2.0.2,<3.0.0", "multiprocess>=0.70.18,<1.0" ] requires-python = ">=3.8,<3.13" From 292c82bcb1a932f6de20e6d7369256c69483a6e8 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 23 Sep 2025 13:36:31 +0200 Subject: [PATCH 050/277] Fix destructor of classical socket class. Add test for classical communication --- simulaqron/sdk/socket.py | 9 ++-- tests/quick/general/test_classical_sockets.py | 54 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 tests/quick/general/test_classical_sockets.py diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index be2b367a..23179c5b 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -16,9 +16,9 @@ class Socket(_Socket): def __init__( self, - app_name, - remote_app_name, - socket_id=0, + app_name: str, + remote_app_name: str, + socket_id: int = 0, timeout: Optional[int] = None, use_callbacks=False, network_name="default", @@ -34,6 +34,9 @@ def __init__( self._logger = get_netqasm_logger(f"{self.__class__.__name__}({app_name} <-> {remote_app_name})") self._timeout = timeout + # We define _app_socket as None as a default value, so the __del__ method + # does not fail when the socket could not be connected correctly. + self._app_socket = None self._app_socket: socket.socket = self._connect() def __del__(self): diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py new file mode 100644 index 00000000..d8408532 --- /dev/null +++ b/tests/quick/general/test_classical_sockets.py @@ -0,0 +1,54 @@ +import pytest +from netqasm.runtime.application import default_app_instance + +from simulaqron.run import run_applications +from simulaqron.sdk.socket import Socket + + +class TestClassicalSocket: + @staticmethod + def alice_program_sender(): + classical_socket: Socket = Socket("Alice", "Bob") + classical_socket.send("ping") + msg = classical_socket.recv() + assert msg == "pong" + + @staticmethod + def bob_program_receiver(): + classical_socket: Socket = Socket("Bob", "Alice") + msg = classical_socket.recv() + assert msg == "ping" + classical_socket.send("pong") + + def test_classical_communication(self): + apps = default_app_instance( + [ + ("Alice", TestClassicalSocket.alice_program_sender), + ("Bob", TestClassicalSocket.bob_program_receiver), + ] + ) + _ = run_applications(apps, use_app_config=False, enable_logging=False) + + def test_unknown_local(self): + with pytest.raises(ValueError) as ex: + # SimulaQron automatically decides that the node with a name + # alphabetically before acts as the server. + # In this case, we declare a local name not known by the (default) + # network configuration, and try to connect to a known remote + Socket("Alice-unknown", "Bob") + assert ex.value.args[0] == "Host name 'Alice-unknown' is not in the app network" + + def test_unknown_remote(self): + with pytest.raises(ValueError) as ex: + # SimulaQron automatically decides that the node with a name + # alphabetically before acts as the server. + # In this case, we force to be in the "client" mode, by declaring + # a local name *alphabetically after* the remote name, so the local + # will try to connect to the unknown remote. + Socket("Unknown", "Pedro") + assert ex.value.args[0] == "Host name 'Pedro' is not in the app network" + + def test_connection_timeout(self): + with pytest.raises(TimeoutError) as ex: + Socket("Alice", "Bob", timeout=1) + assert ex.value.args[0] == "timed out" \ No newline at end of file From 3327894c86804997817701a4b8c4a5c33c5abb36 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 25 Sep 2025 15:18:13 +0200 Subject: [PATCH 051/277] Forward error messages by default instead of reporting generic errors --- simulaqron/netqasm_backend/executioner.py | 4 +--- tests/quick/general/test_classical_sockets.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index b9d01762..4a9263de 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -121,10 +121,8 @@ def _handle_command_exception(self, exc, prog_counter, traceback_str): self._logger.error("At line %d: %s\n%s", prog_counter, exc, traceback_str) if isinstance(exc, SimUnsupportedError): self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.UNSUPP, err_msg="Unsupported simulation engine")) - elif isinstance(exc, ValueError): - self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(exc))) else: - self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg="General error")) + self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(exc))) def _return_msg(self, msg): if self._return_msg_func is None: diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py index d8408532..d11d6b92 100644 --- a/tests/quick/general/test_classical_sockets.py +++ b/tests/quick/general/test_classical_sockets.py @@ -51,4 +51,4 @@ def test_unknown_remote(self): def test_connection_timeout(self): with pytest.raises(TimeoutError) as ex: Socket("Alice", "Bob", timeout=1) - assert ex.value.args[0] == "timed out" \ No newline at end of file + assert ex.value.args[0] == "timed out" From f48461dcc4229f85d1616d5d7b3ea199cab5fa98 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 26 Sep 2025 17:04:54 +0200 Subject: [PATCH 052/277] Adapt internals to return the density matrox as the qubit state --- simulaqron/netqasm_backend/executioner.py | 15 ++++++--------- simulaqron/netqasm_backend/factory.py | 5 +++-- simulaqron/netqasm_backend/qnodeos.py | 5 +---- .../virtual_node/project_q_simulator.py | 17 +++++++++++++++-- simulaqron/virtual_node/qutip_simulator.py | 11 +++++++++-- .../virtual_node/stabilizer_simulator.py | 6 ++++++ simulaqron/virtual_node/virtual.py | 19 +++++++++++++++++-- 7 files changed, 57 insertions(+), 21 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 4a9263de..2ca3c4ab 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -4,7 +4,7 @@ from collections import defaultdict from enum import Enum from functools import partial -from typing import Any, Generator, List +from typing import Any, Generator, List, Tuple import netqasm.lang.instr.core as core_instructions import netqasm.lang.instr.vanilla as vanilla_instructions @@ -822,16 +822,13 @@ def _clear_phys_qubit_in_memory(self, physical_address): self.remove_qubit_id(qubit_id=physical_address) @inlineCallbacks - def get_qubit_state(self, qubit_id: int) -> Generator[Deferred | Any, Any, Any]: + def get_qubit_state( + self, qubit_id: int + ) -> Generator[Deferred, Tuple[List[float], List[float]], Tuple[List[float], List[float]]]: self._logger.debug("Retrieving the state of qubit id %d", qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) - # TODO - Check what's the difference between invoking "get_qubit" on the virtual qubit - # and invoking "get_state" on the virtual node - # qubit = call_method(virt_qubit, "get_qubit") - # Next remote method should be invoked on the virtual node - # qubit = call_method(virt_qubit, "get_state") - qubit = yield from call_method(virt_qubit, "get_register_RI") - return qubit + real_part, im_part = yield call_method(virt_qubit, "get_density_matrix_RI") + return real_part, im_part class VirtualQubitRef: diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 58284ad8..4e16b86b 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -26,7 +26,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from typing import Type +from typing import Type, Dict from netqasm.backend.messages import MessageHeader, ErrorCode, deserialize_host_msg, Message, \ InitNewAppMessage @@ -117,6 +117,7 @@ def log_handled_message(self, result): def log_error(self, failure): self._logger.error("Handling message failed with failure = %s", failure.value) self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(failure.value))) + yield None #yield deferLater(reactor, 0.1, self.stop) def stop(self): @@ -188,7 +189,7 @@ def __init__( self.network_name = network_name # Dictionary that keeps qubit dictionaries for each application - self.qubitList = {} + self.qubitList: Dict[int, "VirtualQubitRef"] = {} # noqa: F821 # Lock governing access to the qubitList self._lock = DeferredLock() diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 0e75f54a..46829619 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -44,10 +44,7 @@ def _handle_get_qubit_state(self, get_quibit_state_msg: GetQubitStateMessage) -> assert isinstance(self._executor, VanillaSimulaQronExecutioner) casted_executor: VanillaSimulaQronExecutioner = self._executor # The ProjectQ backend also returns an unused mapping; we need to fix that - if settings.simulaqron_settings.sim_backend == settings.SimBackend.PROJECTQ.value: - _, [realvec, imagvec] = yield from casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) - else: - realvec, imagvec = yield from casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) + realvec, imagvec = yield from casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) # Return a message to the connection object self._return_qubit_state(get_quibit_state_msg.qubit_id, realvec, imagvec) diff --git a/simulaqron/virtual_node/project_q_simulator.py b/simulaqron/virtual_node/project_q_simulator.py index 00fe03a7..8b52aa42 100644 --- a/simulaqron/virtual_node/project_q_simulator.py +++ b/simulaqron/virtual_node/project_q_simulator.py @@ -26,6 +26,7 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from typing import Tuple, Dict, List try: import projectq as pQ @@ -112,7 +113,7 @@ def remove_qubit(self, qubitNum): self.measure_qubit(qubitNum) - def get_register_RI(self): + def _get_internal_qubit_state(self) -> Tuple[Dict[int, int], List[complex]]: """ Retrieves the entire register in real and imaginary parts and returns the result as a list. Twisted only likes to send real valued lists, not complex ones. @@ -121,10 +122,15 @@ def get_register_RI(self): order, state = self.eng.backend.cheat() # Update the order based on the positions in the qubitReg # and not of the qubit IDs - q_reg_order = {} + q_reg_order: Dict[int, int] = {} for i, q in enumerate(self.qubitReg): q_reg_order[i] = order[q.id] + return q_reg_order, state + + def get_register_RI(self) -> Tuple[Dict[int, int], Tuple[Tuple[float, ...], Tuple[float, ...]]]: + q_reg_order, state = self._get_internal_qubit_state() + # Note previously the format of real and imaginary numbers were # expected, use the same even though Re will be the qubit mapping # and Im the state @@ -133,6 +139,13 @@ def get_register_RI(self): return q_reg_order, (Re, Im) + def get_density_matrix_RI(self) -> Tuple[List[float], List[float]]: + # Get the internal state of the qubit, and compute the outer product |q> Tuple[List[float], List[float]]: """ Retrieves the entire register in real and imaginary parts and returns the result as a list. Twisted only likes to send real valued lists, not complex ones. @@ -142,7 +143,13 @@ def get_register_RI(self): Re = self.qubitReg.full().real.tolist() Im = self.qubitReg.full().imag.tolist() - return (Re, Im) + return Re, Im + + def get_density_matrix_RI(self) -> Tuple[Tuple[float], Tuple[float]]: + # Qutip uses density matrices as the internal representation, so we don't need + # to compute the outer product to get the result + real_part, im_part = self.get_register_RI() + return tuple(*real_part), tuple(*im_part) def apply_H(self, qubitNum): """ diff --git a/simulaqron/virtual_node/stabilizer_simulator.py b/simulaqron/virtual_node/stabilizer_simulator.py index 2ea4b10b..e2cfc80b 100644 --- a/simulaqron/virtual_node/stabilizer_simulator.py +++ b/simulaqron/virtual_node/stabilizer_simulator.py @@ -26,6 +26,8 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from typing import Tuple + from simulaqron.virtual_node.basics import QuantumEngine, QuantumError, NoQubitError from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.general import SimUnsupportedError @@ -107,6 +109,10 @@ def get_register_RI(self): return Re, Im + def get_density_matrix_RI(self) -> Tuple[Tuple[float], Tuple[float]]: + # TODO - Implement this + raise NotImplementedError("get_density_matrix_RI is not implemented for stabilizer engine.") + def apply_H(self, qubitNum): """ Applies a Hadamard gate to the qubits with number qubitNum. diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 351a0e62..61fff349 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -30,6 +30,7 @@ import random from collections import deque +from typing import Tuple, List from twisted.spread import pb from twisted.internet.defer import inlineCallbacks, DeferredLock, Deferred, DeferredList @@ -38,6 +39,7 @@ from twisted.spread.pb import RemoteError, RemoteReference from netqasm.logging.glob import get_netqasm_logger +from typing_extensions import Generator from simulaqron.virtual_node.basics import QuantumError, NoQubitError, VirtNetError from simulaqron.virtual_node.quantum import SimulatedQubit @@ -1091,7 +1093,7 @@ def remote_get_register_RI(self, qubit): def remote_get_register(self, qubit): """ - Return the value of of a locally simulated register which contains this virtual qubit. + Return the value of a locally simulated register which contains this virtual qubit. """ (realM, imagM) = qubit.simQubit.register.get_register_RI() @@ -1103,7 +1105,7 @@ def remote_get_register(self, qubit): def remote_get_register_del(self, qubitNum): """ - Return the value of of a locally simulated register, and remove the simulated qubits from this node. + Return the value of a locally simulated register, and remove the simulated qubits from this node. Caution: virtual qubits not updated. """ @@ -1716,6 +1718,19 @@ def remote_get_qubit(self): return (R, I) + @inlineCallbacks + def remote_get_density_matrix_RI( + self + ) -> Generator[Deferred, Tuple[List[float], List[float]], Tuple[List[float], List[float]]]: + # This function calls itself recursively *on the remote* if the simulated node + # is not the current node + # Otherwise, it calls the corresponding function of the local simulated qubit + if self.simNode == self.virtNode: + real_part, im_part = self.simQubit.register.get_density_matrix_RI() + else: + real_part, im_part = yield call_method(self.simQubit, "get_density_matrix_RI") + return real_part, im_part + @inlineCallbacks def remote_get_register_RI(self): if self.simNode == self.virtNode: From 17fb48c93a5c5bac19df12dfaef0dc8f1bf2378c Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 26 Sep 2025 18:00:12 +0200 Subject: [PATCH 053/277] Adapt transport message class to support square matrices --- simulaqron/netqasm_backend/qnodeos.py | 2 +- simulaqron/sdk/connection.py | 83 +++++++++++++------ .../virtual_node/project_q_simulator.py | 2 +- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 46829619..2aee754d 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -48,7 +48,7 @@ def _handle_get_qubit_state(self, get_quibit_state_msg: GetQubitStateMessage) -> # Return a message to the connection object self._return_qubit_state(get_quibit_state_msg.qubit_id, realvec, imagvec) - def _return_qubit_state(self, qubit_id: int, real_part: List[float], imag_part: List[float]): + def _return_qubit_state(self, qubit_id: int, real_part: List[List[float]], imag_part: List[List[float]]): qubit_state_message = ReturnQubitStateMessage(qubit_id, real_part, imag_part) self._return_msg(msg=qubit_state_message) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 12dd4d9e..e0ed9efe 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -85,7 +85,7 @@ def __init__( self.buf = b"" # Buffer for retrieved qubit states - self._qubit_states: Dict[int, List[complex]] = {} + self._qubit_states: Dict[int, List[List[complex]]] = {} self._shared_memory: SharedMemory = SharedMemoryManager.create_shared_memory(app_name) @@ -308,6 +308,7 @@ def _handle_reply(self) -> int: # message id when handling the reply of the original message self._store_qubit_state( ret_msg.qubit_id, + ret_msg.dimension, ret_msg.get_real_part(), ret_msg.get_imag_part() ) @@ -340,12 +341,23 @@ def _update_shared_memory(self, entry: Register | Address, value: int | Optional f"Cannot update shared memory with entry specified as {entry}" ) - def _store_qubit_state(self, qubit_id: int, real_part: List[float], imag_part: List[float]): + def _store_qubit_state( + self, + qubit_id: int, + dimension: int, + real_part: List[List[float]], + imag_part: List[List[float]] + ): self._logger.debug("Storing qubit state for qubit_id %d: real=%s, imag=%s", qubit_id, str(real_part), str(imag_part) ) - # Reconstruct the complex numbers - self._qubit_states[qubit_id] = [r + (1j * j) for r, j in zip(real_part, imag_part)] + density_matrix: List[List[complex]] = [] + for i in range(dimension): + row: List[complex] = [] + for j in range(dimension): + row.append(real_part[i][j] + (1j * imag_part[i][j])) + density_matrix.append(row) + self._qubit_states[qubit_id] = density_matrix def _retrieve_qubit_state(self, qubit_id: int) -> List[complex]: if qubit_id not in self._qubit_states: @@ -445,37 +457,58 @@ def get_err_msg(self) -> str: # New class for the return of the get qubit state message class ReturnQubitStateMessage(ReturnMessage): + # TODO - Adapt this class to accept square, 2-dim arrays _fields_ = [ ("qubit_id", QUBIT_REGISTRY_NUM), - ("len_real", ctypes.c_uint32), - ("len_imag", ctypes.c_uint32), - ("real_part", MAX_QUBIT_STATE_LEN * ctypes.c_float), # type: ignore - ("imag_part", MAX_QUBIT_STATE_LEN * ctypes.c_float), # type: ignore + ("dim", ctypes.c_uint32), + ("real_part", MAX_QUBIT_STATE_LEN * (MAX_QUBIT_STATE_LEN * ctypes.c_float)), # type: ignore + ("imag_part", MAX_QUBIT_STATE_LEN * (MAX_QUBIT_STATE_LEN * ctypes.c_float)), # type: ignore ] TYPE = NewReturnMessageType.RET_QUBIT_STATE - def __init__(self, qubit_id: int, real_part: List[float], imag_part: List[float]): + def __init__(self, qubit_id: int, real_part: List[List[float]], imag_part: List[List[float]]): super().__init__(self.TYPE.value) + + # Sanity checks - given matrices are square + assert len(real_part) > 0 + assert len(imag_part) > 0 + assert len(real_part) == len(imag_part) and True + for row in real_part: + assert len(row) > 0 + assert len(row) == len(real_part) + for row in imag_part: + assert len(row) > 0 + assert len(row) == len(imag_part) + self.qubit_id = qubit_id - self.len_real = len(real_part) - self.len_imag = len(imag_part) - if self.len_real > MAX_QUBIT_STATE_LEN or self.len_imag > MAX_QUBIT_STATE_LEN: + self.dim = len(real_part) + if self.dim > MAX_QUBIT_STATE_LEN : logger.warning("Return qubit state message too long") - for i, v in enumerate(real_part): - self.real_part[i] = v - for i, v in enumerate(imag_part): - self.imag_part[i] = v - - def get_real_part(self) -> List[float]: - real_part: List[float] = [] - for i in range(self.len_real): - real_part.append(float(self.real_part[i])) + for i in range(self.dim): + for j in range(self.dim): + self.real_part[i][j] = real_part[i][j] + self.imag_part[i][j] = imag_part[i][j] + + @property + def dimension(self) -> int : + return self.dim + + def get_real_part(self) -> List[List[float]]: + real_part: List[List[float]] = [] + for i in range(self.dim): + row: List[float] = [] + for j in range(self.dim): + row.append(float(self.real_part[i][j])) + real_part.append(row) return real_part - def get_imag_part(self) -> List[float]: - imag_part: List[float] = [] - for i in range(self.len_imag): - imag_part.append(float(self.imag_part[i])) + def get_imag_part(self) -> List[List[float]]: + imag_part: List[List[float]] = [] + for i in range(self.dim): + row: List[float] = [] + for j in range(self.dim): + row.append(float(self.imag_part[i][j])) + imag_part.append(row) return imag_part diff --git a/simulaqron/virtual_node/project_q_simulator.py b/simulaqron/virtual_node/project_q_simulator.py index 8b52aa42..8b4fe8ed 100644 --- a/simulaqron/virtual_node/project_q_simulator.py +++ b/simulaqron/virtual_node/project_q_simulator.py @@ -144,7 +144,7 @@ def get_density_matrix_RI(self) -> Tuple[List[float], List[float]]: _, raw_qubit_state = self._get_internal_qubit_state() qubit_state = np.array(raw_qubit_state) density_matrix = np.outer(qubit_state, qubit_state) - return density_matrix.real.tolist(), density_matrix.real.tolist() + return density_matrix.real.tolist(), density_matrix.imag.tolist() def apply_H(self, qubitNum): """ From 8ae47c39ae4abd11a8e2a9af209b3c7b13354b6a Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 29 Sep 2025 11:29:55 +0200 Subject: [PATCH 054/277] Improve reraise of remote exceptions --- simulaqron/virtual_node/virtual.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 61fff349..2848af18 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -26,7 +26,7 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - +import importlib import random from collections import deque @@ -66,11 +66,13 @@ def reraise_remote_error(remote_err): :param remote_err: :obj:`twisted.spread.pb.RemoteError` :return: class """ - # Get name of remote error - error_name = remote_err.remoteType.split(b".")[-1].decode() + # Get names of remote package and remote error class + error_pkg_name = [pkg.decode() for pkg in remote_err.remoteType.split(b".")[:-1]] + error_class_name = remote_err.remoteType.split(b".")[-1].decode() - # Get class of remote error - error_class = eval(error_name) + # Dynamically import the module and the class within the module + error_pkg = importlib.import_module(".".join(error_pkg_name)) + error_class = getattr(error_pkg, error_class_name) raise error_class(str(remote_err)) From b87b49797589cecd0672b7048299ce1e426e8ac4 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 29 Sep 2025 13:02:53 +0200 Subject: [PATCH 055/277] Add declaration of the get_density_matrix function in the virtual qubit class --- simulaqron/virtual_node/basics.py | 10 ++++++++++ simulaqron/virtual_node/quantum.py | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/simulaqron/virtual_node/basics.py b/simulaqron/virtual_node/basics.py index 85206fa3..02f0a2d5 100644 --- a/simulaqron/virtual_node/basics.py +++ b/simulaqron/virtual_node/basics.py @@ -112,6 +112,16 @@ def get_register_RI(self): """ pass + @abc.abstractmethod + def get_density_matrix_RI(self): + """ + Retrieves the entire register in real and imaginary parts and returns the result of + the outer product. In other words, if the qubit is in state |q>, this function + returns the density matrix |q> Date: Mon, 29 Sep 2025 13:05:02 +0200 Subject: [PATCH 056/277] Fix some test cases to assert the density matrix of a qubit --- tests/quick/netqasm/test_get_qubit_state.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 77ab27eb..6f8b1c33 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -140,7 +140,7 @@ def test_get_basic_state_local(self, network): ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) # We expect the qubit to be initialized in the |0> state = [1 0] - assert np.array_equal(raw_results[0]["app_Alice"], np.array([1.0 + 0.0j, 0 + 0.0j])) + assert np.array_equal(raw_results[0]["app_Alice"], np.array([[1.0 + 0.0j, 0 + 0.0j], [0.0 + 0.0j, 0 + 0.0j]])) def test_get_qubit_state_local(self, network): apps = default_app_instance( @@ -149,13 +149,13 @@ def test_get_qubit_state_local(self, network): ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) - print(raw_results) #qubit A: H(|0>) = 1/sqrt(2) |0> + 1/sqrt(2) |1> = 1/sqrt(2) [1 0] + 1/sqrt(2) [0 1] - expected = np.array([1.0 / math.sqrt(2.0) + 0.0j, 1.0 / math.sqrt(2.0) + 0.0j]) + expected_h = np.array([1.0 / math.sqrt(2.0) + 0.0j, 1.0 / math.sqrt(2.0) + 0.0j]) # Note: Due to loss in serialization, we allow a tolerance of 1e-5 when comparing all the members - assert np.isclose(raw_results[0]["app_Alice"]["state_a"], expected, rtol=1e-5).all() + assert np.isclose(raw_results[0]["app_Alice"]["state_a"], np.outer(expected_h, expected_h), rtol=1e-5).all() # qubit B: X(|0>) = |1> = [0 1] - assert np.array_equal(raw_results[0]["app_Alice"]["state_b"], np.array([0.0 + 0.0j, 1 + 0.0j])) + expected_x = np.array([0.0 + 0.0j, 1 + 0.0j]) + assert np.array_equal(raw_results[0]["app_Alice"]["state_b"], np.outer(expected_x, expected_x)) def test_get_qubit_state_teleport(self, network): apps = default_app_instance( From a580be53106d9d485d8434db0cdc50d54ed4c7bb Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 29 Sep 2025 13:54:26 +0200 Subject: [PATCH 057/277] Fix the test assertion when teleporting a qubit --- tests/quick/netqasm/test_get_qubit_state.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 6f8b1c33..9002d68a 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -85,11 +85,10 @@ def alice_teleport(): m1 = q.measure() m2 = epr.measure() - alice.flush() - classical_socket.send_structured( - StructuredMessage("Corrections", f"{int(m1)}/{int(m2)}") - ) + classical_socket.send_structured( + StructuredMessage("Corrections", f"{int(m1)}/{int(m2)}") + ) return {"m1": int(m1), "m2": int(m2), "alice_state": alice_state} @staticmethod @@ -152,7 +151,7 @@ def test_get_qubit_state_local(self, network): #qubit A: H(|0>) = 1/sqrt(2) |0> + 1/sqrt(2) |1> = 1/sqrt(2) [1 0] + 1/sqrt(2) [0 1] expected_h = np.array([1.0 / math.sqrt(2.0) + 0.0j, 1.0 / math.sqrt(2.0) + 0.0j]) # Note: Due to loss in serialization, we allow a tolerance of 1e-5 when comparing all the members - assert np.isclose(raw_results[0]["app_Alice"]["state_a"], np.outer(expected_h, expected_h), rtol=1e-5).all() + assert np.isclose(raw_results[0]["app_Alice"]["state_a"], np.outer(expected_h, expected_h), rtol=1e-10).all() # qubit B: X(|0>) = |1> = [0 1] expected_x = np.array([0.0 + 0.0j, 1 + 0.0j]) assert np.array_equal(raw_results[0]["app_Alice"]["state_b"], np.outer(expected_x, expected_x)) @@ -165,4 +164,4 @@ def test_get_qubit_state_teleport(self, network): ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) - assert np.array_equal(raw_results[0]["app_Alice"]["alice_state"], raw_results[0]["app_Bob"]["bob_state"]) + assert np.isclose(raw_results[0]["app_Alice"]["alice_state"], raw_results[0]["app_Bob"]["bob_state"], rtol=1e-10).all() From 5e675b57c9015af201043f74f6c1462d0a1ae0f1 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 29 Sep 2025 13:56:14 +0200 Subject: [PATCH 058/277] Linter changes --- simulaqron/netqasm_backend/executioner.py | 2 +- simulaqron/netqasm_backend/qnodeos.py | 1 - simulaqron/virtual_node/qutip_simulator.py | 7 ++++--- tests/quick/netqasm/test_get_qubit_state.py | 5 ++++- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 2ca3c4ab..543cfe5c 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -4,7 +4,7 @@ from collections import defaultdict from enum import Enum from functools import partial -from typing import Any, Generator, List, Tuple +from typing import Generator, List, Tuple import netqasm.lang.instr.core as core_instructions import netqasm.lang.instr.vanilla as vanilla_instructions diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 2aee754d..62d82c2b 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -6,7 +6,6 @@ from twisted.internet.defer import inlineCallbacks from twisted.internet.protocol import Protocol -import simulaqron.settings as settings from simulaqron.netqasm_backend.executioner import VanillaSimulaQronExecutioner from simulaqron.sdk.connection import (NewMessageType, GetQubitStateMessage, ReturnQubitStateMessage) diff --git a/simulaqron/virtual_node/qutip_simulator.py b/simulaqron/virtual_node/qutip_simulator.py index 80bcf840..2023badd 100644 --- a/simulaqron/virtual_node/qutip_simulator.py +++ b/simulaqron/virtual_node/qutip_simulator.py @@ -35,6 +35,7 @@ try: import qutip as qp + import qutip.qip.operations.gates as gate_ops except ImportError: raise RuntimeError("If you want to use the qutip backend you need to install the python package 'qutip'") @@ -270,8 +271,8 @@ def apply_onequbit_gate(self, gateU, qubitNum): qubitNum the number of the qubit this gate is applied to """ + overallU = gate_ops.gate_expand_1toN(gateU, self.activeQubits, qubitNum) # Compute the overall unitary, identity everywhere with gateU at position qubitNum - overallU = qp.gate_expand_1toN(gateU, self.activeQubits, qubitNum) # Qutip distinguishes between system dimensionality and matrix dimensionality # so we need to make sure it knows we are talking about multiple qubits @@ -328,11 +329,11 @@ def measure_qubit_inplace(self, qubitNum): # Construct the two measurement operators, and put them at the right position v0 = qp.basis(2, 0) P0 = v0 * v0.dag() - M0 = qp.gate_expand_1toN(P0, self.activeQubits, qubitNum) + M0 = gate_ops.gate_expand_1toN(P0, self.activeQubits, qubitNum) v1 = qp.basis(2, 1) P1 = v1 * v1.dag() - M1 = qp.gate_expand_1toN(P1, self.activeQubits, qubitNum) + M1 = gate_ops.gate_expand_1toN(P1, self.activeQubits, qubitNum) # Compute the success probabilities obj = M0 * self.qubitReg diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 9002d68a..45919b4e 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -164,4 +164,7 @@ def test_get_qubit_state_teleport(self, network): ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) - assert np.isclose(raw_results[0]["app_Alice"]["alice_state"], raw_results[0]["app_Bob"]["bob_state"], rtol=1e-10).all() + assert np.isclose( + raw_results[0]["app_Alice"]["alice_state"], + raw_results[0]["app_Bob"]["bob_state"], + rtol=1e-10).all() From 97cd766aa0238ba513442ba1d0269b942617d8a7 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 3 Oct 2025 17:28:24 +0200 Subject: [PATCH 059/277] Use SpawnContext class to use the correct method to create new child processes --- simulaqron/run/run.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index acfda7c0..66385cbc 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -1,5 +1,7 @@ import logging import os + +from multiprocess.context import SpawnContext from multiprocess.pool import Pool, ApplyResult from importlib import reload from os import PathLike @@ -67,6 +69,9 @@ def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_conf new=new_network ) +def err_callback_example(exc: Exception): + print(exc) + def run_applications( app_instance: ApplicationInstance, @@ -79,6 +84,7 @@ def run_applications( post_function: Optional[Callable] = None, enable_logging: bool = True, hardware: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" + init_func: Callable = None, ) -> List[Dict[str, Any]]: """Executes functions containing application scripts, @@ -106,6 +112,12 @@ def run_applications( Whether to enable logging. hardware: Any Unused argument. Any parameter given here will be ignored. + init_func: Callable + Function to execute to initialize the state of the child processes. The implemented + executor uses the *spawn* method for creating new processes. In this sense, the + child processes *do not receive* a copy of the full memory, but only what is needed. + In particular, all modules will be reimported in the child processes, hence any + state of the classes *will not transfer* to the child processes. Returns ------- @@ -146,7 +158,7 @@ def run_applications( net_cfg = None for _ in range(num_rounds): - with Pool(len(app_names) + 3) as executor: + with SpawnContext().Pool(processes=len(app_names) + 3, initializer=init_func) as executor: SimulaQronConnection.PROCESS_POOL = executor # Start the backend process network = run_sim_backend(app_names, sim_backend, net_cfg) @@ -167,7 +179,8 @@ def run_applications( inputs=inputs, ) inputs["app_config"] = app_cfg - future: ApplyResult = executor.apply_async(program.entry, kwds=inputs) + # executor.apply() + future: ApplyResult = executor.apply_async(program.entry, kwds=inputs, error_callback=err_callback_example) app_futures.append(future) # for app_cfg in app_cfgs: From 4521335c0b3701277264664839fbdb5d125ccd78 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 8 Oct 2025 11:15:55 +0200 Subject: [PATCH 060/277] Change to use epoll-based reactor in twisted --- simulaqron/reactor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simulaqron/reactor.py b/simulaqron/reactor.py index c9633ac4..6d8a6cfa 100644 --- a/simulaqron/reactor.py +++ b/simulaqron/reactor.py @@ -1,5 +1,5 @@ -from twisted.internet import asyncioreactor -asyncioreactor.install() +from twisted.internet import epollreactor +epollreactor.install() from twisted.internet import reactor # noqa: E402 From 9106eab83f37265168d423f4941d8b897a1d6c4c Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 8 Oct 2025 12:40:38 +0200 Subject: [PATCH 061/277] Fix reference to reraise_remote_error --- simulaqron/virtual_node/virtual.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 2848af18..bc86b685 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -543,7 +543,7 @@ def remote_netqasm_send_qubit(self, num, targetName, app_id, remote_app_id): newVirtNum, ) except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) def remote_netqasm_add_recv_list(self, fromName, from_epr_socket_id, to_epr_socket_id, new_virt_num=None): """ @@ -625,7 +625,7 @@ def remote_netqasm_send_epr_half(self, num, targetName, app_id, remote_app_id, r rawEntInfo, ) except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) def remote_netqasm_add_epr_list(self, fromName, from_epr_socket_id, to_epr_socket_id, new_virt_num, rawEntInfo): """ @@ -706,7 +706,7 @@ def remote_send_qubit(self, qubit, targetName): try: newNum = yield call_method(remoteNode.root, "add_qubit", self.myID.name, qubit.simQubit) except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) else: self._logger.debug("Sending qubit simulated remotely at %s", qubit.simNode.name) # Also lock the virtual node of the simulating node unless it is the remoteNode or this node @@ -718,11 +718,11 @@ def remote_send_qubit(self, qubit, targetName): try: simQubitNum = yield call_method(qubit.simQubit, "get_sim_number") except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) try: newNum = yield call_method(qubit.simNode.root, "transfer_qubit", simQubitNum, targetName) except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) finally: if locked_node is not None: yield call_method(locked_node.root, "release_global_lock") @@ -768,7 +768,7 @@ def remote_transfer_qubit(self, simQubitNum, targetName): try: newNum = yield call_method(remoteNode.root, "add_qubit", self.myID.name, simQubit) except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) return newNum @@ -986,7 +986,7 @@ def remote_merge_from(self, simNodeName, simQubitNum, localReg): try: (R, I, activeQ, oldRegNum, oldQubitNum) = yield call_method(simNode.root, "get_register_del", simQubitNum) except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) # Get numbering offset from previous register: append at end offset = localReg.activeQubits @@ -1015,7 +1015,7 @@ def remote_merge_from(self, simNodeName, simQubitNum, localReg): try: yield call_method(nb.root, "update_virtual_merge", self.myID.name, simNodeName, oldRegNum, newD) except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) # Locally, we might also already have virtual qubits which were in the remote simulated # register. Update them as well @@ -1070,7 +1070,7 @@ def remote_update_virtual_merge(self, newSimNodeName, oldSimNodeName, oldRegNum, try: (givenNum, givenReg) = yield call_method(q.simQubit, "get_numbers") except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) # Check if this qubit needs updating if q.simNode == oldSimNode and givenReg == oldRegNum: @@ -1181,12 +1181,12 @@ def remote_get_multiple_qubits(self, qList): try: (num, name) = yield call_method(q.simQubit, "get_details") except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) nums.append(num) try: (R, I) = yield call_method(qList[0].simNode.root, "get_state", nums) except RemoteError as remote_err: - self.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) return (R, I) @@ -1453,7 +1453,7 @@ def _lock_inreg(self, qubit): simNum = yield call_method(qubit.simQubit, "get_sim_number") yield call_method(qubit.simNode.root, "lock_reg_qubits", simNum) except RemoteError as remote_err: - self.virtNode.root.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) @inlineCallbacks def _unlock_inreg(self, qubit): @@ -1468,7 +1468,7 @@ def _unlock_inreg(self, qubit): simNum = yield call_method(qubit.simQubit, "get_sim_number") yield call_method(qubit.simNode.root, "unlock_reg_qubits", simNum) except RemoteError as remote_err: - self.virtNode.root.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) @inlineCallbacks def remote_cnot_onto(self, target): @@ -1651,7 +1651,7 @@ def _two_qubit_gate(self, target, name): # Finally, execute the two qubit gate getattr(self.simQubit, localName)(targetNum) except RemoteError as remote_err: - self.virtNode.root.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) finally: # Release the locks in the register of the control (which now contains also the others) yield self._unlock_inreg(self) @@ -1714,7 +1714,7 @@ def remote_get_qubit(self): try: (R, I) = yield call_method(self.simQubit, "get_qubit") except RemoteError as remote_err: - self.virtNode.root.reraise_remote_error(remote_err) + reraise_remote_error(remote_err) except ConnectionError: self._logger.error("cannot get qubit number.") From 9938e9b71614c90339b3d4b6b95a2859016bba7b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 8 Oct 2025 12:44:30 +0200 Subject: [PATCH 062/277] Fallback to forking new processes to avoid dataraces --- simulaqron/network.py | 2 +- simulaqron/run/run.py | 35 ++++++++++++++++++++++-------- simulaqron/virtual_node/virtual.py | 2 +- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/simulaqron/network.py b/simulaqron/network.py index cb064d6b..07115cfb 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -29,7 +29,7 @@ import time import random -from multiprocess.context import SpawnProcess as Process +from multiprocess.context import ForkProcess as Process from typing import List import networkx as nx diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 66385cbc..911a527f 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -1,8 +1,9 @@ import logging import os +import signal -from multiprocess.context import SpawnContext -from multiprocess.pool import Pool, ApplyResult +from multiprocess.context import ForkContext +from multiprocess.pool import ApplyResult from importlib import reload from os import PathLike from pathlib import Path @@ -69,8 +70,17 @@ def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_conf new=new_network ) -def err_callback_example(exc: Exception): - print(exc) + +def send_sigterm_to_process(parent_pid: int): + def wrapper(exc): + os.kill(parent_pid, signal.SIGTERM) + return wrapper + + +def stop_network(network: Network): + def wrapper(signum: int, frame): + network.stop() + return wrapper def run_applications( @@ -102,8 +112,8 @@ def run_applications( log_cfg: LogConfig Configuration for the logging. formalism: Formalism - Qubit formalism to use for the simulation. On this value depends - The SimulaQron backend to use. + Qubit formalism to use for the simulation. The SimulaQron + backend to use depends on this value. use_app_config: bool Whether to give app_config as argument to app's main() post_function: Optional[Callable] @@ -158,10 +168,12 @@ def run_applications( net_cfg = None for _ in range(num_rounds): - with SpawnContext().Pool(processes=len(app_names) + 3, initializer=init_func) as executor: + with ForkContext().Pool(processes=len(app_names) + 3, initializer=init_func) as executor: SimulaQronConnection.PROCESS_POOL = executor # Start the backend process network = run_sim_backend(app_names, sim_backend, net_cfg) + signal.signal(signal.SIGINT, stop_network(network)) + signal.signal(signal.SIGTERM, stop_network(network)) network.start() # Start the application processes @@ -179,8 +191,13 @@ def run_applications( inputs=inputs, ) inputs["app_config"] = app_cfg - # executor.apply() - future: ApplyResult = executor.apply_async(program.entry, kwds=inputs, error_callback=err_callback_example) + future: ApplyResult = executor.apply_async( + program.entry, + kwds=inputs, + # The error callback with get invoked in the child process, so + # we tell the parent (current pid) to sigal *all* the children + error_callback=send_sigterm_to_process(os.getpid()) + ) app_futures.append(future) # for app_cfg in app_cfgs: diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index bc86b685..80d9ee22 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -60,7 +60,7 @@ raise QuantumError(f"Unknown backend {simulaqron_settings.sim_backend}") -def reraise_remote_error(remote_err): +def reraise_remote_error(remote_err: RemoteError): """ This is a function re-raises the error thrown remotely :param remote_err: :obj:`twisted.spread.pb.RemoteError` From 52e2e557bb68cb186548042553f76f7726adc5fd Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 9 Oct 2025 15:52:02 +0200 Subject: [PATCH 063/277] Implement a more robust way to terminate all the spawned processes when an exception arises --- simulaqron/netqasm_backend/executioner.py | 4 +- simulaqron/netqasm_backend/qnodeos.py | 5 +- simulaqron/network.py | 3 +- simulaqron/reactor.py | 4 +- simulaqron/run/run.py | 79 +++++++++++++++++------ simulaqron/sdk/connection.py | 2 +- 6 files changed, 68 insertions(+), 29 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 543cfe5c..383e2678 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -129,8 +129,8 @@ def _return_msg(self, msg): raise RuntimeError("Cannot return msg since no function is set") self._return_msg_func(msg=msg) - def _instr_qalloc(self, subroutine_id, instr: core_instructions.QAllocInstruction): - physical_address = yield from super()._instr_qalloc( + def _instr_qalloc(self, subroutine_id: int, instr: core_instructions.QAllocInstruction): + physical_address = yield super()._instr_qalloc( subroutine_id=subroutine_id, instr=instr, ) diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 62d82c2b..f1a6bb62 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -1,5 +1,6 @@ -from typing import Optional, Dict, Callable, Generator, Any, List +from typing import Optional, Dict, Callable, Generator, Any, List, Type +from netqasm.backend.executor import Executor from netqasm.backend.messages import MsgDoneMessage, Message, MessageType from netqasm.backend.qnodeos import QNodeController from netqasm.lang.instr import Flavour @@ -63,7 +64,7 @@ def _get_message_handlers(self) -> Dict[NewMessageType | MessageType, Callable]: } @classmethod - def _get_executor_class(cls, flavour: Optional[Flavour] = None): + def _get_executor_class(cls, flavour: Optional[Flavour] = None) -> Type[Executor]: return VanillaSimulaQronExecutioner def _mark_message_finished(self, msg_id: int, msg: Message): diff --git a/simulaqron/network.py b/simulaqron/network.py index 07115cfb..603aacf1 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -29,7 +29,7 @@ import time import random -from multiprocess.context import ForkProcess as Process +from multiprocess.context import SpawnProcess as Process from typing import List import networkx as nx @@ -154,7 +154,6 @@ def _setup_processes(self): """ Setup the processes forming the network, however they are not started yet. """ - #mp.set_start_method("spawn", force=True) for node in self.nodes: process_virtual = Process( target=start_vnode, args=(node, self.name, get_log_level()), name=f"VirtNode {node}" diff --git a/simulaqron/reactor.py b/simulaqron/reactor.py index 6d8a6cfa..c9633ac4 100644 --- a/simulaqron/reactor.py +++ b/simulaqron/reactor.py @@ -1,5 +1,5 @@ -from twisted.internet import epollreactor -epollreactor.install() +from twisted.internet import asyncioreactor +asyncioreactor.install() from twisted.internet import reactor # noqa: E402 diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 911a527f..cfb23e01 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -2,13 +2,14 @@ import os import signal -from multiprocess.context import ForkContext +from multiprocess.context import SpawnContext as ProcessContext from multiprocess.pool import ApplyResult from importlib import reload from os import PathLike from pathlib import Path from typing import Callable, Optional, Any, Dict, List, Union, Generator, Tuple +from multiprocess.sharedctypes import SynchronizedArray from netqasm.logging.glob import get_netqasm_logger from netqasm.logging.output import (reset_struct_loggers, save_all_struct_loggers) @@ -55,12 +56,14 @@ def reset(save_loggers=False): reload(logging) -def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_config_file: Optional[str]): - logger.debug("Starting simulaqron sim_backend process with nodes %s", node_names) +def setup_sim_backend(sim_backend: SimBackend): if sim_backend in [SimBackend.PROJECTQ, SimBackend.QUTIP]: assert has_module.main(sim_backend.value),\ f"To use {sim_backend} as backend you need to install the package" simulaqron_settings.sim_backend = sim_backend.value + + +def configure_network(node_names: List[str], network_config_file: Optional[str]): new_network = True if network_config_file is None else False return Network( name="default", @@ -71,16 +74,40 @@ def run_sim_backend(node_names: List[str], sim_backend: SimBackend, network_conf ) -def send_sigterm_to_process(parent_pid: int): - def wrapper(exc): - os.kill(parent_pid, signal.SIGTERM) - return wrapper +# Global array helper to store PIDs of the children processes running the applications +# Note; this array *will not* store the pids of the QNodeOS and/or Vnode processes +apps_pids: Optional[SynchronizedArray] = None + +def _worker_initializer(synced_array: SynchronizedArray): + # We simply store the reference of the synced object for this process + global apps_pids + apps_pids = synced_array + + +def _app_wrapper(**kwargs): + global apps_pids + assert apps_pids is not None + assert "__instance_num" in kwargs and isinstance(kwargs["__instance_num"], int) + assert "__entry_function" in kwargs and isinstance(kwargs["__entry_function"], Callable) + # Save the pid for this worker + apps_pids[kwargs["__instance_num"]] = os.getpid() + entry_function = kwargs["__entry_function"] + del kwargs["__entry_function"] + del kwargs["__instance_num"] -def stop_network(network: Network): - def wrapper(signum: int, frame): - network.stop() - return wrapper + # TODO - Signal handler for the SIGINT signal? + + # Call the app main function + return entry_function(**kwargs) + + +def _signal_other_apps(exc: BaseException): + global apps_pids + assert apps_pids is not None + for pid in apps_pids: + print(f"Sending SIGINT to pid {pid}") + os.kill(pid, signal.SIGINT) def run_applications( @@ -168,19 +195,29 @@ def run_applications( net_cfg = None for _ in range(num_rounds): - with ForkContext().Pool(processes=len(app_names) + 3, initializer=init_func) as executor: + process_ctx = ProcessContext() + synced_array = process_ctx.Array('i', len(app_instance.app.programs)) + executor = process_ctx.Pool( + processes=len(app_names) + 3, + initializer=_worker_initializer, + initargs=[synced_array] + ) + with executor: SimulaQronConnection.PROCESS_POOL = executor - # Start the backend process - network = run_sim_backend(app_names, sim_backend, net_cfg) - signal.signal(signal.SIGINT, stop_network(network)) - signal.signal(signal.SIGTERM, stop_network(network)) + global apps_pids + apps_pids = synced_array + logger.debug("Starting simulaqron sim_backend process with nodes %s", app_names) + setup_sim_backend(sim_backend) + network = configure_network(app_names, net_cfg) + + # Start the processes that support the simulator: QNodeOS + VirtualNode network.start() # Start the application processes app_futures = [] programs = app_instance.app.programs - for program in programs: + for i, program in enumerate(programs): inputs = app_instance.program_inputs[program.party] if use_app_config: app_cfg = AppConfig( @@ -191,12 +228,14 @@ def run_applications( inputs=inputs, ) inputs["app_config"] = app_cfg + inputs["__instance_num"] = i + inputs["__entry_function"] = program.entry future: ApplyResult = executor.apply_async( - program.entry, + _app_wrapper, kwds=inputs, # The error callback with get invoked in the child process, so - # we tell the parent (current pid) to sigal *all* the children - error_callback=send_sigterm_to_process(os.getpid()) + # we tell other applications that they need to stop + error_callback=_signal_other_apps ) app_futures.append(future) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index e0ed9efe..7d24e156 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -31,7 +31,7 @@ class SimulaQronConnection(BaseNetQASMConnection): NON_STABILIZER_INSTR = [GenericInstr.T] - # Process poool will be set externally when launching the applications + # Process pool will be set externally when launching the applications # This is due to the fact that the code creating the connections will run # *inside a pool worker*, so it cannot create a new process pool because the # worker itself is a daemon process. From 437d0d6e9d03d8f14e5097bdf3fce036da30ff5d Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 13 Oct 2025 13:38:40 +0200 Subject: [PATCH 064/277] Add functionality to gracefully stop vnodes when receiving a message from the QNodeOS server --- simulaqron/netqasm_backend/executioner.py | 3 +- simulaqron/netqasm_backend/factory.py | 9 +- simulaqron/network.py | 5 +- simulaqron/run/run.py | 103 +++++++++--------- simulaqron/sdk/connection.py | 5 +- simulaqron/start/start_vnode.py | 2 +- simulaqron/toolbox/has_module.py | 20 ---- simulaqron/virtual_node/virtual.py | 6 + tests/quick/engine/test_project_q_engine.py | 8 +- tests/quick/engine/test_qutip_engine.py | 8 +- tests/quick/general/test_classical_sockets.py | 11 ++ 11 files changed, 91 insertions(+), 89 deletions(-) delete mode 100755 simulaqron/toolbox/has_module.py diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 383e2678..c0e3dba4 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -18,6 +18,7 @@ ReturnType) from twisted.internet import task from twisted.internet.defer import inlineCallbacks, Deferred +from twisted.spread import pb from simulaqron.reactor import reactor from simulaqron.general import SimUnsupportedError @@ -832,7 +833,7 @@ def get_qubit_state( class VirtualQubitRef: - def __init__(self, qubit_id=0, timestamp=0, virt=0): + def __init__(self, qubit_id: int, timestamp: int , virt: pb.Referenceable): self.qubit_id = qubit_id self.timestamp = timestamp self.virt = virt diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 4e16b86b..676c9172 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -33,7 +33,7 @@ from netqasm.logging.glob import get_netqasm_logger from twisted.internet.defer import DeferredLock, inlineCallbacks from twisted.internet.protocol import Factory, Protocol, connectionDone -#from twisted.internet.task import deferLater +from twisted.internet.task import deferLater from simulaqron.reactor import reactor from simulaqron.general.host_config import SocketsConfig, Host @@ -41,6 +41,7 @@ from simulaqron.sdk.connection import RichErrorMessage from simulaqron.settings import simulaqron_settings from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor +from simulaqron.virtual_node.virtual import call_method class IncompleteMessageError(ValueError): @@ -117,8 +118,9 @@ def log_handled_message(self, result): def log_error(self, failure): self._logger.error("Handling message failed with failure = %s", failure.value) self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(failure.value))) - yield None - #yield deferLater(reactor, 0.1, self.stop) + #self.transport.abortConnection() + #yield None + yield deferLater(reactor, 0.1, self.stop) def stop(self): self.factory.stop() @@ -204,6 +206,7 @@ def __init__( self.topology = networks_config.networks[network_name].topology def stop(self): + yield call_method(self.virtRoot, "stop_vnode") reactor.stop() def buildProtocol(self, addr): diff --git a/simulaqron/network.py b/simulaqron/network.py index 603aacf1..89fd7e50 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -29,7 +29,7 @@ import time import random -from multiprocess.context import SpawnProcess as Process +from multiprocess.context import ForkProcess as Process from typing import List import networkx as nx @@ -202,6 +202,9 @@ def stop(self): self._logger.warning("Could not terminate one of the processes in the" "network due to error: %s", err) + def __str__(self): + return f"Network '{self.name}', procs: {self.processes}" + def construct_topology_config(topology, nodes): """ diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index cfb23e01..1f944c53 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -5,6 +5,7 @@ from multiprocess.context import SpawnContext as ProcessContext from multiprocess.pool import ApplyResult from importlib import reload +from importlib.util import find_spec from os import PathLike from pathlib import Path from typing import Callable, Optional, Any, Dict, List, Union, Generator, Tuple @@ -58,7 +59,7 @@ def reset(save_loggers=False): def setup_sim_backend(sim_backend: SimBackend): if sim_backend in [SimBackend.PROJECTQ, SimBackend.QUTIP]: - assert has_module.main(sim_backend.value),\ + assert find_spec(sim_backend.value) is not None,\ f"To use {sim_backend} as backend you need to install the package" simulaqron_settings.sim_backend = sim_backend.value @@ -195,6 +196,12 @@ def run_applications( net_cfg = None for _ in range(num_rounds): + network = configure_network(app_names, net_cfg) + + # Start the processes that support the simulator: QNodeOS + VirtualNode + network.start() + + # Create the executor pool process_ctx = ProcessContext() synced_array = process_ctx.Array('i', len(app_instance.app.programs)) executor = process_ctx.Pool( @@ -202,54 +209,51 @@ def run_applications( initializer=_worker_initializer, initargs=[synced_array] ) - with executor: - SimulaQronConnection.PROCESS_POOL = executor - global apps_pids - apps_pids = synced_array - logger.debug("Starting simulaqron sim_backend process with nodes %s", app_names) - setup_sim_backend(sim_backend) - network = configure_network(app_names, net_cfg) - - # Start the processes that support the simulator: QNodeOS + VirtualNode - network.start() - - # Start the application processes - app_futures = [] - - programs = app_instance.app.programs - for i, program in enumerate(programs): - inputs = app_instance.program_inputs[program.party] - if use_app_config: - app_cfg = AppConfig( - app_name=program.party, - node_name=program.party, # node name should be same as app name - main_func=program.entry, - log_config=app_instance.logging_cfg, - inputs=inputs, + + try: + with executor: + SimulaQronConnection.PROCESS_POOL = executor + global apps_pids + apps_pids = synced_array + logger.debug("Starting simulaqron sim_backend process with nodes %s", app_names) + setup_sim_backend(sim_backend) + + # Start the application processes + app_futures = [] + + programs = app_instance.app.programs + for i, program in enumerate(programs): + inputs = app_instance.program_inputs[program.party] + if use_app_config: + app_cfg = AppConfig( + app_name=program.party, + node_name=program.party, # node name should be same as app name + main_func=program.entry, + log_config=app_instance.logging_cfg, + inputs=inputs, + ) + inputs["app_config"] = app_cfg + inputs["__instance_num"] = i + inputs["__entry_function"] = program.entry + future: ApplyResult = executor.apply_async( + _app_wrapper, + kwds=inputs, + # The error callback with get invoked in the child process, so + # we tell other applications that they need to stop + error_callback=_signal_other_apps ) - inputs["app_config"] = app_cfg - inputs["__instance_num"] = i - inputs["__entry_function"] = program.entry - future: ApplyResult = executor.apply_async( - _app_wrapper, - kwds=inputs, - # The error callback with get invoked in the child process, so - # we tell other applications that they need to stop - error_callback=_signal_other_apps - ) - app_futures.append(future) - - # for app_cfg in app_cfgs: - # inputs = app_cfg.inputs - # if use_app_config: - # inputs['app_config'] = app_cfg - # future = executor.submit(app_cfg.main_func, **inputs) - # app_futures.append(future) - - # Join the application processes and the backend - names = [f'app_{app_name}' for app_name in app_names] - result = {} - try: + app_futures.append(future) + + # for app_cfg in app_cfgs: + # inputs = app_cfg.inputs + # if use_app_config: + # inputs['app_config'] = app_cfg + # future = executor.submit(app_cfg.main_func, **inputs) + # app_futures.append(future) + + # Join the application processes and the backend + names = [f'app_{app_name}' for app_name in app_names] + result = {} for future, name in as_completed(app_futures, names): result[name] = future.get() # if results_file is not None: @@ -258,10 +262,11 @@ def run_applications( assert timed_log_dir is not None path = os.path.join(timed_log_dir, "results.yaml") dump_yaml(data=result, file_path=path) - finally: results.append(result) network.stop() + finally: + network.stop() reset(save_loggers=True) if enable_logging: diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 7d24e156..cd50baa5 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -251,7 +251,8 @@ def _read_more_data(self): try: data = self._socket.recv(1024) except Exception as err: - self._logger.exception("Error in recv from NetQASM server", err) + self._logger.exception("Error in recv from NetQASM server") + raise err if self.buf: self.buf += data else: @@ -391,7 +392,7 @@ def get_qubit_state(self, app_id: int, qubit_id: int) -> List[complex]: # Definitions for the new message types QUBIT_REGISTRY_NUM = ctypes.c_uint8 MAX_QUBIT_STATE_LEN = 5 -MAX_ERR_MSG_LEN = 100 +MAX_ERR_MSG_LEN = 500 # "Extend" (by redefining the enum) the Message Type diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 6444f61a..032ad41e 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -42,7 +42,7 @@ def sigterm_handler(name, _signo, _stack_frame): - logger.info("Shutting down Node") + print("Shutting down Node from signal %d." % _signo, flush=True) reactor.stop() diff --git a/simulaqron/toolbox/has_module.py b/simulaqron/toolbox/has_module.py deleted file mode 100755 index 4504702c..00000000 --- a/simulaqron/toolbox/has_module.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 - -import sys - - -def main(module_name): - try: - __import__(module_name) - return True - except ImportError: - return False - - -if __name__ == '__main__': - module_name = sys.argv[1] - exists = main(module_name) - if exists: - print('Y') - else: - print("N") diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 80d9ee22..d25539d0 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -1225,6 +1225,12 @@ def remote_sim_qubit_num_in_same_reg(self, sim_qubit_num1, sim_qubit_num2): assert sim_qubit2 is not None, "Sim num {sim_qubit_num2} not in this node" return sim_qubit1.register == sim_qubit2.register + @inlineCallbacks + def remote_stop_vnode(self): + # sys.stdout.write(f"Stopping virtual node pid {os.getpid()}") + print(f"Stopping virtual node pid {os.getpid()}", flush=True) + reactor.stop() + ####### # diff --git a/tests/quick/engine/test_project_q_engine.py b/tests/quick/engine/test_project_q_engine.py index 3a831cb0..2936395b 100644 --- a/tests/quick/engine/test_project_q_engine.py +++ b/tests/quick/engine/test_project_q_engine.py @@ -1,20 +1,16 @@ import unittest import numpy as np -from simulaqron.toolbox import has_module -from simulaqron.settings import SimBackend - -if has_module.main(SimBackend.PROJECTQ.value): +from importlib.util import find_spec +if find_spec("projectq") is not None: from simulaqron.virtual_node.project_q_simulator import ProjectQEngine from simulaqron.virtual_node.basics import NoQubitError, QuantumError from projectq.types._qubit import Qubit _has_module = True - else: - _has_module = False diff --git a/tests/quick/engine/test_qutip_engine.py b/tests/quick/engine/test_qutip_engine.py index c7e44f98..f4cc338a 100644 --- a/tests/quick/engine/test_qutip_engine.py +++ b/tests/quick/engine/test_qutip_engine.py @@ -29,15 +29,11 @@ import unittest -from simulaqron.toolbox import has_module -from simulaqron.settings import SimBackend - -if has_module.main(SimBackend.QUTIP.value): +from importlib.util import find_spec +if find_spec("qutip") is not None: from simulaqron.virtual_node.qutip_simulator import QutipEngine - _has_module = True - else: _has_module = False diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py index d11d6b92..18fefcb2 100644 --- a/tests/quick/general/test_classical_sockets.py +++ b/tests/quick/general/test_classical_sockets.py @@ -2,10 +2,21 @@ from netqasm.runtime.application import default_app_instance from simulaqron.run import run_applications +from simulaqron.run.run import reset from simulaqron.sdk.socket import Socket +from simulaqron.settings import simulaqron_settings, SimBackend class TestClassicalSocket: + @pytest.fixture(autouse=True) + def network(self): + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + yield + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + reset() + @staticmethod def alice_program_sender(): classical_socket: Socket = Socket("Alice", "Bob") From 2ff493d1962eebd0acbfc672c645f22bf433c031 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 13 Oct 2025 17:37:27 +0200 Subject: [PATCH 065/277] Factorized settings to avoid saving static state through successive executions of the simulator --- examples/nativeMode/teleport/aliceTest.py | 6 +- examples/nativeMode/teleport/bobTest.py | 10 +- simulaqron/_default_config/__init__.py | 0 .../_default_config/default_network.json | 77 +++++++++++++ .../_default_config/default_settings.json | 13 +++ simulaqron/netqasm_backend/factory.py | 2 +- simulaqron/run/run.py | 6 +- simulaqron/settings.py | 108 ++++++++---------- simulaqron/simulaqron.py | 3 +- simulaqron/toolbox/get_simulaqron_path.py | 15 --- simulaqron/toolbox/manage_nodes.py | 2 +- simulaqron/virtual_node/quantum.py | 2 +- simulaqron/virtual_node/virtual.py | 8 +- tests/quick/netqasm/test_get_qubit_state.py | 4 +- tests/quick/network/test_network.py | 8 +- tests/quick/test_settings.py | 106 ++++++++++++----- tests/slow/merges/test_merges.py | 24 ++-- tests/slow/sdk/test_single_qubit.py | 16 ++- tests/slow/sdk/test_two_qubit.py | 3 - 19 files changed, 257 insertions(+), 156 deletions(-) create mode 100644 simulaqron/_default_config/__init__.py create mode 100644 simulaqron/_default_config/default_network.json create mode 100644 simulaqron/_default_config/default_settings.json delete mode 100755 simulaqron/toolbox/get_simulaqron_path.py diff --git a/examples/nativeMode/teleport/aliceTest.py b/examples/nativeMode/teleport/aliceTest.py index e68bbacd..0d7b709e 100644 --- a/examples/nativeMode/teleport/aliceTest.py +++ b/examples/nativeMode/teleport/aliceTest.py @@ -69,13 +69,13 @@ def runClientNode(qReg, virtRoot, myName, classicalNet): yield q1.callRemote("apply_H") # For information purposes, let's print the state of that qubit - if simulaqron_settings.sim_backend == "qutip": + if simulaqron_settings.sim_backend.value == "qutip": realRho, imagRho = yield q1.callRemote("get_qubit") state = np.array(assemble_qubit(realRho, imagRho), dtype=complex) - elif simulaqron_settings.sim_backend == "projectq": + elif simulaqron_settings.sim_backend.value == "projectq": realvec, imagvec = yield virtRoot.callRemote("get_register_RI", q1) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] - elif simulaqron_settings.sim_backend == "stabilizer": + elif simulaqron_settings.sim_backend.value == "stabilizer": array, _ = yield virtRoot.callRemote("get_register_RI", q1) state = StabilizerState(array) else: diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index f594cf02..6dd03d39 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -111,20 +111,20 @@ def remote_recover_teleport(self, a, b, virtualNum): yield eprB.callRemote("apply_Z") # Just print the qubit we received - if simulaqron_settings.sim_backend == "qutip": + if simulaqron_settings.sim_backend.value == "qutip": print("here") (realRho, imagRho) = yield eprB.callRemote("get_qubit") state = np.array(assemble_qubit(realRho, imagRho), dtype=complex) - elif simulaqron_settings.sim_backend == "projectq": + elif simulaqron_settings.sim_backend.value == "projectq": realvec, imagvec = yield self.virtRoot.callRemote("get_register_RI", eprB) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] - elif simulaqron_settings.sim_backend == "stabilizer": + elif simulaqron_settings.sim_backend.value == "stabilizer": array, _, = yield self.virtRoot.callRemote("get_register_RI", eprB) state = StabilizerState(array) else: - ValueError("Unknown backend {}".format(simulaqron_settings.sim_backend)) + ValueError(f"Unknown backend {simulaqron_settings.sim_backend}") - print("Qubit is:\n{}".format(state)) + print(f"Qubit is:\n{state}") ##################################################################################################### diff --git a/simulaqron/_default_config/__init__.py b/simulaqron/_default_config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/simulaqron/_default_config/default_network.json b/simulaqron/_default_config/default_network.json new file mode 100644 index 00000000..f533509c --- /dev/null +++ b/simulaqron/_default_config/default_network.json @@ -0,0 +1,77 @@ +{ + "default": { + "nodes": { + "Alice": { + "app_socket": [ + "localhost", + 8001 + ], + "qnodeos_socket": [ + "localhost", + 8016 + ], + "vnode_socket": [ + "localhost", + 8024 + ] + }, + "Bob": { + "app_socket": [ + "localhost", + 8026 + ], + "qnodeos_socket": [ + "localhost", + 8032 + ], + "vnode_socket": [ + "localhost", + 8033 + ] + }, + "Charlie": { + "app_socket": [ + "localhost", + 8046 + ], + "qnodeos_socket": [ + "localhost", + 8047 + ], + "vnode_socket": [ + "localhost", + 8048 + ] + }, + "David": { + "app_socket": [ + "localhost", + 8049 + ], + "qnodeos_socket": [ + "localhost", + 8050 + ], + "vnode_socket": [ + "localhost", + 8051 + ] + }, + "Eve": { + "app_socket": [ + "localhost", + 8052 + ], + "qnodeos_socket": [ + "localhost", + 8053 + ], + "vnode_socket": [ + "localhost", + 8054 + ] + } + }, + "topology": null + } +} \ No newline at end of file diff --git a/simulaqron/_default_config/default_settings.json b/simulaqron/_default_config/default_settings.json new file mode 100644 index 00000000..4a1e9760 --- /dev/null +++ b/simulaqron/_default_config/default_settings.json @@ -0,0 +1,13 @@ +{ + "_read_user": true, + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "log_level": 30, + "sim_backend": "stabilizer", + "network_config_file": "default_network.json", + "noisy_qubits": false, + "t1": 1.0 +} \ No newline at end of file diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 676c9172..67edde51 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -202,7 +202,7 @@ def __init__( # topology self.topology = None if simulaqron_settings.network_config_file is not None: - networks_config = NetworksConfigConstructor(file_path=simulaqron_settings.network_config_file) + networks_config = NetworksConfigConstructor(simulaqron_settings.network_config_file) self.topology = networks_config.networks[network_name].topology def stop(self): diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 1f944c53..f0bea4c8 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -25,7 +25,6 @@ from simulaqron.network import Network from simulaqron.sdk import SimulaQronConnection from simulaqron.settings import SimBackend, simulaqron_settings -from simulaqron.toolbox import has_module logger = get_netqasm_logger() @@ -61,7 +60,7 @@ def setup_sim_backend(sim_backend: SimBackend): if sim_backend in [SimBackend.PROJECTQ, SimBackend.QUTIP]: assert find_spec(sim_backend.value) is not None,\ f"To use {sim_backend} as backend you need to install the package" - simulaqron_settings.sim_backend = sim_backend.value + simulaqron_settings.sim_backend = sim_backend def configure_network(node_names: List[str], network_config_file: Optional[str]): @@ -107,7 +106,6 @@ def _signal_other_apps(exc: BaseException): global apps_pids assert apps_pids is not None for pid in apps_pids: - print(f"Sending SIGINT to pid {pid}") os.kill(pid, signal.SIGINT) @@ -186,11 +184,9 @@ def run_applications( results: List[Dict[str, Any]] = [] if isinstance(network_cfg, str) or isinstance(network_cfg, PathLike): net_cfg = str(network_cfg) - # If given a network config path, we need to update the global static config property (really bad design choice) simulaqron_settings.network_config_file = net_cfg elif isinstance(network_cfg, Path): net_cfg = str(network_cfg.resolve()) - # If given a network config path, we need to update the global static config property (really bad design choice) simulaqron_settings.network_config_file = net_cfg else: net_cfg = None diff --git a/simulaqron/settings.py b/simulaqron/settings.py index c66a3689..c9c8f00f 100644 --- a/simulaqron/settings.py +++ b/simulaqron/settings.py @@ -30,48 +30,30 @@ ######################### # SETTINGS FOR SIMULAQRON ######################### -import os import json -import logging from enum import Enum +from importlib import resources +from os import PathLike +from pathlib import Path from typing import Dict, Any -from simulaqron.toolbox import get_simulaqron_path - -simulaqron_path = get_simulaqron_path.main() -config_folder = os.path.join(simulaqron_path, "config") - class SimBackend(Enum): STABILIZER = "stabilizer" PROJECTQ = "projectq" QUTIP = "qutip" + def __str__(self): + return self.value -class Config: - simulaqron_path = get_simulaqron_path.main() - config_folder = os.path.join(simulaqron_path, "config") + def __repr__(self): + return str(self) - _internal_settings_file = os.path.join(simulaqron_path, "config", "settings.json") - _user_settings_file = os.path.join(os.path.expanduser("~"), ".simulaqron.json") +class Config: # Dictionary for settings _config: Dict[str, Any] = {} - _default_config = { - "_read_user": True, - "max_qubits": 20, - "max_registers": 1000, - "conn_retry_time": 0.5, - "recv_timeout": 100, # (x 100 ms) - "recv_retry_time": 0.1, # (seconds) - "log_level": logging.WARNING, - "sim_backend": SimBackend.STABILIZER.value, - "network_config_file": os.path.join(config_folder, "network.json"), - "noisy_qubits": False, - "t1": 1.0 - } - class Decorator: @classmethod def get_setting(cls, method): @@ -86,50 +68,58 @@ def updated_func(self, value): return updated_func def __init__(self): - self.update_settings() - - def update_settings(self, default: bool = False): - # Update with default settings - self._config.update(self._default_config) - - # Update with internal settings (if exists and default is False) - if not default: - if os.path.exists(self._internal_settings_file): - with open(self._internal_settings_file, 'r') as f: - internal_config = json.load(f) - self._config.update(internal_config) + self._loaded_file = "" # Will be correctly setup when loading the default config + # We populate the object with the default configuration + self.default_settings() + + def update_settings(self, config: Dict[str, Any]): + # Update the config with the given data + if "network_config_file" in config: + # We need to resolve the path of the network config file + given_network_config = Path(config["network_config_file"]).resolve() + if given_network_config.exists() and given_network_config.is_file(): + config["network_config_file"] = str(given_network_config) else: - self._write() - - # Update with internal settings (if exists and _read_user is True) - if self._read_user: - if os.path.exists(self._user_settings_file): - with open(self._user_settings_file, 'r') as f: - user_config = json.load(f) - self._config.update(user_config) + # If it doesn't exist, we load the default + resource = resources.path( + "simulaqron._default_config", + "default_network.json" + ) + with resource as network_path: + network_specs_path = network_path.resolve() + assert network_specs_path.exists() and network_path.is_file() + config["network_config_file"] = str(network_specs_path) + config["sim_backend"] = SimBackend[config["sim_backend"].upper()] + self._config.update(config) def default_settings(self): - self.update_settings(default=True) - self._write() - - def _write(self): - with open(self._internal_settings_file, 'w') as f: - json.dump(self._config, f, indent=4) + default_settings = resources.path("simulaqron._default_config", "default_settings.json") + with default_settings as default_settings_path: + self._loaded_file = str(default_settings_path) + self.load_from_file(default_settings_path) + + def load_from_file(self, path: PathLike): + file_path = Path(str(path)).resolve() + if file_path.exists() and file_path.is_file(): + self._loaded_file = str(file_path) + with open(file_path, 'r') as file: + config = json.load(file) + self.update_settings(config) + else: + raise FileNotFoundError(f"File {file_path} does not exist or is not a file") def _get_setting(self, setting: str) -> Any: try: value = self._config[setting] except KeyError: - raise KeyError(f"Cannot find the setting {setting} in the file {self._internal_settings_file}") + raise KeyError(f"Cannot find the setting {setting} in the file {self._loaded_file}") return value def _set_setting(self, setting: str, value: Any): self._config[setting] = value - self._write() - # Below are the settings, note that _get_setting and _set_setting are automaticaly - # called when a setting is set or get. When a value is set the values is saved to the - # settings (json) file using the name of the property as key. + # Below are the settings, note that _get_setting and _set_setting are automatically + # called when a setting is set or get. @property @Decorator.get_setting @@ -143,12 +133,12 @@ def _read_user(self, _read_user: bool): @property @Decorator.get_setting - def sim_backend(self) -> str: + def sim_backend(self) -> SimBackend: pass @sim_backend.setter @Decorator.set_setting - def sim_backend(self, sim_backend): + def sim_backend(self, sim_backend: SimBackend): pass @property diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 108c20ba..d754a5e5 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -16,7 +16,8 @@ PID_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".simulaqron_pids") # Check that the default network_config_file exists -default_network_config_file = simulaqron_settings._default_config["network_config_file"] +simulaqron_settings.default_settings() +default_network_config_file = simulaqron_settings.network_config_file if not os.path.exists(default_network_config_file): networks_config = NetworksConfigConstructor() networks_config.reset() diff --git a/simulaqron/toolbox/get_simulaqron_path.py b/simulaqron/toolbox/get_simulaqron_path.py deleted file mode 100755 index 6a8cbb4e..00000000 --- a/simulaqron/toolbox/get_simulaqron_path.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 - -import os - - -def main(): - path_to_this_file = os.path.abspath(__file__) - path_to_this_folder = os.path.dirname(path_to_this_file) - simulaqron_path = os.path.split(path_to_this_folder)[0] - return simulaqron_path - - -if __name__ == '__main__': - simulaqron_path = main() - print(simulaqron_path) diff --git a/simulaqron/toolbox/manage_nodes.py b/simulaqron/toolbox/manage_nodes.py index f6ea39e3..8a0a19c2 100644 --- a/simulaqron/toolbox/manage_nodes.py +++ b/simulaqron/toolbox/manage_nodes.py @@ -12,7 +12,7 @@ def __init__(self, file_path=None): be written to a file by calling the method 'write_to_file'. :param file_path: None or str - Path to the network config_file. If None an empty networkconfig constructor is initalized. + Path to the network config_file. If None an empty networkconfig constructor is initialized. Otherwise the content of the file is loaded. """ self.networks = {} diff --git a/simulaqron/virtual_node/quantum.py b/simulaqron/virtual_node/quantum.py index 39062eda..59b775c2 100644 --- a/simulaqron/virtual_node/quantum.py +++ b/simulaqron/virtual_node/quantum.py @@ -279,7 +279,7 @@ def remote_get_qubit(self): Returns the state of the qubits in the list qList by tracing out the rest. """ backend = settings.simulaqron_settings.sim_backend - if backend != settings.SimBackend.QUTIP.value: + if backend != settings.SimBackend.QUTIP: raise RuntimeError(f"Cannot get reduced qubit state using backend {backend}") self._logger.debug("VIRTUAL NODE %s: Returning qubit %d", self.node.name, self.num) return self.register.get_qubits_RI([self.num]) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index d25539d0..38df9b51 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -50,11 +50,11 @@ from simulaqron.settings import simulaqron_settings, SimBackend from simulaqron.reactor import reactor -if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: +if simulaqron_settings.sim_backend == SimBackend.QUTIP: from simulaqron.virtual_node.qutip_simulator import QutipEngine as QEngine -elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: +elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: from simulaqron.virtual_node.project_q_simulator import ProjectQEngine as QEngine -elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: +elif simulaqron_settings.sim_backend == SimBackend.STABILIZER: from simulaqron.virtual_node.stabilizer_simulator import StabilizerEngine as QEngine else: raise QuantumError(f"Unknown backend {simulaqron_settings.sim_backend}") @@ -1228,7 +1228,7 @@ def remote_sim_qubit_num_in_same_reg(self, sim_qubit_num1, sim_qubit_num2): @inlineCallbacks def remote_stop_vnode(self): # sys.stdout.write(f"Stopping virtual node pid {os.getpid()}") - print(f"Stopping virtual node pid {os.getpid()}", flush=True) + # print(f"Stopping virtual node pid {os.getpid()}", flush=True) reactor.stop() diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 45919b4e..644dd89d 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -20,10 +20,10 @@ class TestGetQubit: @pytest.fixture(autouse=True) def network(self): simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + simulaqron_settings.sim_backend = SimBackend.PROJECTQ yield simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + simulaqron_settings.sim_backend = SimBackend.PROJECTQ reset() # Here we define the quantum programs used in the tests diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index b609f82b..b31eab0c 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -1,10 +1,8 @@ -import os import json import time import unittest from timeit import default_timer as timer -from simulaqron.toolbox import get_simulaqron_path from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor from simulaqron.settings import simulaqron_settings from simulaqron.network import Network @@ -21,7 +19,8 @@ def tearDown(self): @classmethod def tearDownClass(cls): - default_network_config_file = simulaqron_settings._default_config["network_config_file"] + simulaqron_settings.default_settings() + default_network_config_file = simulaqron_settings.network_config_file network_config = NetworksConfigConstructor(default_network_config_file) network_config.reset() network_config.write_to_file() @@ -41,8 +40,7 @@ def assert_topology(self, topology1, topology2): self.assert_nodes(neigh1, neigh2) def check_nodes_and_topology_in_file(self, network): - simulaqron_path = get_simulaqron_path.main() - network_config_file = os.path.join(simulaqron_path, "config", "network.json") + network_config_file = simulaqron_settings.network_config_file with open(network_config_file, 'r') as f: network_config = json.load(f) nodes_in_file = list(network_config[network.name]["nodes"].keys()) diff --git a/tests/quick/test_settings.py b/tests/quick/test_settings.py index 08b1929f..3235f07b 100644 --- a/tests/quick/test_settings.py +++ b/tests/quick/test_settings.py @@ -1,40 +1,86 @@ -import unittest +import tempfile import json -import random +import pytest +from importlib import resources +from pathlib import Path -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, SimBackend -##################### -# TODO Add more tests -##################### - -class TestSettings(unittest.TestCase): - @classmethod - def tearDownClass(cls): - simulaqron_settings.default_settings() - +class TestSettings: def test_default_settings(self): - simulaqron_settings.default_settings() - for key, value in simulaqron_settings._default_config.items(): - self.assertEqual(getattr(simulaqron_settings, key), value) - - def test_set_settings(self): - new_settings = {} - for key in simulaqron_settings._default_config: - value = random.randint(0, 100) - new_settings[key] = value - setattr(simulaqron_settings, key, value) + __expected_default_settings = """ + { + "_read_user": true, + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "log_level": 30, + "sim_backend": "stabilizer", + "network_config_file": "CHANGE_ME", + "noisy_qubits": false, + "t1": 1.0 + } + """ + expected_settings = json.loads(__expected_default_settings) + # For testing purposes, we need to "adjust" some of teh expected values: + expected_settings["sim_backend"] =SimBackend[expected_settings["sim_backend"].upper()] + with resources.path("simulaqron._default_config", "default_network.json") as path: + expected_settings["network_config_file"] = str(path) - with open(simulaqron_settings._internal_settings_file, 'r') as f: - file_settings = json.load(f) + simulaqron_settings.default_settings() + for key, value in expected_settings.items(): + assert getattr(simulaqron_settings, key) == value - for key, value in new_settings.items(): - self.assertEqual(getattr(simulaqron_settings, key), value) - self.assertEqual(value, file_settings[key]) + def test_non_existent_network_config(self): + _original_settings = """ + { + "_read_user": false, + "max_qubits": 10, + "max_registers": 500, + "conn_retry_time": 0.25, + "recv_timeout": 10, + "recv_retry_time": 0.05, + "log_level": 30, + "sim_backend": "projectq", + "network_config_file": "/not/existing/network.json", + "noisy_qubits": false, + "t1": 2.0 + } + """ + _expected_settings = """ + { + "_read_user": false, + "max_qubits": 10, + "max_registers": 500, + "conn_retry_time": 0.25, + "recv_timeout": 10, + "recv_retry_time": 0.05, + "log_level": 30, + "sim_backend": "projectq", + "network_config_file": "CHANGE_ME", + "noisy_qubits": false, + "t1": 2.0 + } + """ + expected_settings = json.loads(_expected_settings) + # For testing purposes, we need to "adjust" some of teh expected values: + expected_settings["sim_backend"] =SimBackend[expected_settings["sim_backend"].upper()] + with resources.path("simulaqron._default_config", "default_network.json") as path: + expected_settings["network_config_file"] = str(path) - self.test_default_settings() + _original_settings = json.loads(_original_settings) + with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", delete_on_close=False) as file: + json.dump(_original_settings, file) + file.close() + simulaqron_settings.load_from_file(Path(file.name)) + for key, value in expected_settings.items(): + assert getattr(simulaqron_settings, key) == value -if __name__ == '__main__': - unittest.main() + def test_load_non_existent_config_file(self): + with pytest.raises(FileNotFoundError) as error: + simulaqron_settings.load_from_file("/non/existent/file") + assert "File /non/existent/file does not exist" in str(error.value) diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 98728f11..51703341 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -67,18 +67,18 @@ def got_both(self): yield self.q1.callRemote("apply_H") yield self.q1.callRemote("cnot_onto", self.q2) - if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: + if simulaqron_settings.sim_backend == SimBackend.QUTIP: # Output state (realRho, imagRho) = yield self.virtRoot.callRemote("get_multiple_qubits", [self.q1, self.q2]) rho = assemble_qubit(realRho, imagRho) expectedRho = [[0.5, 0, 0, 0.5], [0, 0, 0, 0], [0, 0, 0, 0], [0.5, 0, 0, 0.5]] correct = np.all(np.isclose(rho, expectedRho)) - elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: + elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: (realvec, imagvec) = yield self.virtRoot.callRemote("get_register_RI", self.q1) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] expectedState = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] correct = np.all(np.isclose(state, expectedState)) - elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + elif simulaqron_settings.sim_backend == SimBackend.STABILIZER: (array, _) = yield self.virtRoot.callRemote("get_register_RI", self.q1) state = StabilizerState(array) expectedState = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) @@ -115,18 +115,18 @@ def remote_receive_one_qubit(self, virtualNum, cnot_direction=0): yield q.callRemote("apply_H") yield q.callRemote("cnot_onto", qA) - if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: + if simulaqron_settings.sim_backend == SimBackend.QUTIP: # Output state (realRho, imagRho) = yield self.virtRoot.callRemote("get_multiple_qubits", [qA, q]) rho = assemble_qubit(realRho, imagRho) expectedRho = [[0.5, 0, 0, 0.5], [0, 0, 0, 0], [0, 0, 0, 0], [0.5, 0, 0, 0.5]] correct = np.all(np.isclose(rho, expectedRho)) - elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: + elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: (realvec, imagvec) = yield self.virtRoot.callRemote("get_register_RI", qA) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] expectedState = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] correct = np.all(np.isclose(state, expectedState)) - elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + elif simulaqron_settings.sim_backend == SimBackend.STABILIZER: (array, _) = yield self.virtRoot.callRemote("get_register_RI", qA) state = StabilizerState(array) expectedState = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) @@ -243,18 +243,18 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): yield qA.callRemote("apply_H") yield qA.callRemote("cnot_onto", qB) - if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: + if simulaqron_settings.sim_backend == SimBackend.QUTIP: # Output state (realRho, imagRho) = yield virtRoot.callRemote("get_multiple_qubits", [qA, qB]) rho = assemble_qubit(realRho, imagRho) expectedRho = [[0.5, 0, 0, 0.5], [0, 0, 0, 0], [0, 0, 0, 0], [0.5, 0, 0, 0.5]] correct = np.all(np.isclose(rho, expectedRho)) - elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: + elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: (realvec, imagvec, _, _, _) = yield virtRoot.callRemote("get_register", qA) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] expectedState = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] correct = np.all(np.isclose(state, expectedState)) - elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + elif simulaqron_settings.sim_backend == SimBackend.STABILIZER: (array, _, _, _, _) = yield virtRoot.callRemote("get_register", qA) state = StabilizerState(array) expectedState = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) @@ -296,18 +296,18 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): yield qA.callRemote("apply_H") yield qA.callRemote("cnot_onto", qB) - if simulaqron_settings.sim_backend == SimBackend.QUTIP.value: + if simulaqron_settings.sim_backend == SimBackend.QUTIP: # Output state (realRho, imagRho) = yield virtRoot.callRemote("get_multiple_qubits", [qA, qB]) rho = assemble_qubit(realRho, imagRho) expectedRho = [[0.5, 0, 0, 0.5], [0, 0, 0, 0], [0, 0, 0, 0], [0.5, 0, 0, 0.5]] correct = np.all(np.isclose(rho, expectedRho)) - elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ.value: + elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: (realvec, imagvec, _, _, _) = yield virtRoot.callRemote("get_register", qA) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] expectedState = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] correct = np.all(np.isclose(state, expectedState)) - elif simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + elif simulaqron_settings.sim_backend == SimBackend.STABILIZER: (array, _, _, _, _) = yield virtRoot.callRemote("get_register", qA) state = StabilizerState(array) expectedState = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) diff --git a/tests/slow/sdk/test_single_qubit.py b/tests/slow/sdk/test_single_qubit.py index c419b83b..c8bff650 100644 --- a/tests/slow/sdk/test_single_qubit.py +++ b/tests/slow/sdk/test_single_qubit.py @@ -201,13 +201,11 @@ class TestSingleQubitGate: @pytest.fixture def network(self): - simulaqron_settings.default_settings() network = Network(nodes=["Alice"], force=True) network.start() yield network network.stop() - simulaqron_settings.default_settings() reset() def test_X_Gate(self, network): @@ -242,7 +240,7 @@ def test_T_Gate(self, network): with SimulaQronConnection("Alice") as conn: # Test T exp_values = calc_exp_values(prep_T_state()) - if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + if simulaqron_settings.sim_backend == SimBackend.STABILIZER: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_T, exp_values, iterations=self.iterations, progress=False) else: @@ -260,7 +258,7 @@ def test_X_pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_X pi/8 exp_values = calc_exp_values(prep_rot_state([1, 0, 0], np.pi / 8)) - if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + if simulaqron_settings.sim_backend == SimBackend.STABILIZER: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotx1, exp_values, iterations=self.iterations, progress=False) else: @@ -271,7 +269,7 @@ def test_X_5pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_X 5*pi/8 exp_values = calc_exp_values(prep_rot_state([1, 0, 0], 5 * np.pi / 8)) - if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + if simulaqron_settings.sim_backend == SimBackend.STABILIZER: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotx2, exp_values, iterations=self.iterations, progress=False) else: @@ -282,7 +280,7 @@ def test_Y_pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Y pi/8 exp_values = calc_exp_values(prep_rot_state([0, 1, 0], np.pi / 8)) - if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + if simulaqron_settings.sim_backend == SimBackend.STABILIZER: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_roty1, exp_values, iterations=self.iterations, progress=False) else: @@ -293,7 +291,7 @@ def test_Y_5pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Y 5*pi/8 exp_values = calc_exp_values(prep_rot_state([0, 1, 0], 5 * np.pi / 8)) - if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + if simulaqron_settings.sim_backend == SimBackend.STABILIZER: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_roty2, exp_values, iterations=self.iterations, progress=False) else: @@ -304,7 +302,7 @@ def test_Z_pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Z pi/8 exp_values = calc_exp_values(prep_rot_state([0, 0, 1], np.pi / 8)) - if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + if simulaqron_settings.sim_backend == SimBackend.STABILIZER: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotz1, exp_values, iterations=self.iterations, progress=False) else: @@ -315,7 +313,7 @@ def test_Z_5pi8Rot(self, network): with SimulaQronConnection("Alice") as conn: # Test ROT_Z 5*pi/8 exp_values = calc_exp_values(prep_rot_state([0, 0, 1], 5 * np.pi / 8)) - if simulaqron_settings.sim_backend == SimBackend.STABILIZER.value: + if simulaqron_settings.sim_backend == SimBackend.STABILIZER: with pytest.raises(SimUnsupportedError): conn.test_preparation(prep_rotz2, exp_values, iterations=self.iterations, progress=False) else: diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index 660f6ecb..e116782d 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -35,7 +35,6 @@ from simulaqron.sdk.socket import Socket from simulaqron.run.run import run_applications from simulaqron.network import Network -from simulaqron.settings import simulaqron_settings from simulaqron.run.run import reset @@ -184,13 +183,11 @@ class TestTwoQubitGates: def network(self): print(f"Testing two qubit gates with {self.iterations} iterations\n") - simulaqron_settings.default_settings() network = Network(nodes=["Alice", "Bob"], force=True) network.start(wait_until_running=True) yield network network.stop() - simulaqron_settings.default_settings() reset() def test_CNOT_control(self, network): From f8513c8362c1fbc4054c31080e247a7c88294ec9 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 14 Oct 2025 11:04:57 +0200 Subject: [PATCH 066/277] Allow having a local SimulaQron config file in the CWD --- .../_default_config/default_network.json | 28 +++++++++---------- simulaqron/settings.py | 17 +++++++++-- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/simulaqron/_default_config/default_network.json b/simulaqron/_default_config/default_network.json index f533509c..823c574b 100644 --- a/simulaqron/_default_config/default_network.json +++ b/simulaqron/_default_config/default_network.json @@ -8,67 +8,67 @@ ], "qnodeos_socket": [ "localhost", - 8016 + 8002 ], "vnode_socket": [ "localhost", - 8024 + 8003 ] }, "Bob": { "app_socket": [ "localhost", - 8026 + 8010 ], "qnodeos_socket": [ "localhost", - 8032 + 8011 ], "vnode_socket": [ "localhost", - 8033 + 8012 ] }, "Charlie": { "app_socket": [ "localhost", - 8046 + 8020 ], "qnodeos_socket": [ "localhost", - 8047 + 8021 ], "vnode_socket": [ "localhost", - 8048 + 8022 ] }, "David": { "app_socket": [ "localhost", - 8049 + 8030 ], "qnodeos_socket": [ "localhost", - 8050 + 8031 ], "vnode_socket": [ "localhost", - 8051 + 8032 ] }, "Eve": { "app_socket": [ "localhost", - 8052 + 8033 ], "qnodeos_socket": [ "localhost", - 8053 + 8034 ], "vnode_socket": [ "localhost", - 8054 + 8035 ] } }, diff --git a/simulaqron/settings.py b/simulaqron/settings.py index c9c8f00f..7f441a52 100644 --- a/simulaqron/settings.py +++ b/simulaqron/settings.py @@ -37,6 +37,11 @@ from pathlib import Path from typing import Dict, Any +# This is the name of the "local" simulaqron settings. +# If a file named like this is found in the CWD, it will be +# automatically loaded when creating the config file +SIMULAQRON_SETTINGS_FILENAME = "simulaqron_settings.json" + class SimBackend(Enum): STABILIZER = "stabilizer" @@ -68,9 +73,15 @@ def updated_func(self, value): return updated_func def __init__(self): - self._loaded_file = "" # Will be correctly setup when loading the default config - # We populate the object with the default configuration - self.default_settings() + self._loaded_file = "" # Will be correctly setup when loading the config + + # We populate the object with the configuration + local_settings_file = (Path.cwd() / SIMULAQRON_SETTINGS_FILENAME).resolve() + if local_settings_file.exists() and local_settings_file.is_file(): + self._loaded_file = str(local_settings_file) + self.load_from_file(local_settings_file) + else: + self.default_settings() def update_settings(self, config: Dict[str, Any]): # Update the config with the given data From 37ee3a1187f811d668bdc36d09afa0d72a4b0b5f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 14 Oct 2025 11:06:48 +0200 Subject: [PATCH 067/277] Store PID files in a hidden folder in the home (~) rather than in the same repository/installation path --- simulaqron/__init__.py | 4 -- simulaqron/simulaqron.py | 38 ++++++++--------- simulaqron/toolbox/reset.py | 24 ----------- tests/quick/general/resources/test.json | 54 ------------------------- 4 files changed, 17 insertions(+), 103 deletions(-) delete mode 100644 simulaqron/toolbox/reset.py delete mode 100644 tests/quick/general/resources/test.json diff --git a/simulaqron/__init__.py b/simulaqron/__init__.py index 07900990..e69de29b 100644 --- a/simulaqron/__init__.py +++ b/simulaqron/__init__.py @@ -1,4 +0,0 @@ -from simulaqron.toolbox.reset import check_config_files # noqa: E402 - -# Create default config files if there are none -check_config_files() diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index d754a5e5..9e7e5f73 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -1,27 +1,23 @@ #!/usr/bin/env python3 -import os import time import click import logging from daemons.prefab import run from daemons.interfaces import exit +from pathlib import Path import importlib.metadata as metadata from simulaqron.network import Network from simulaqron.settings import simulaqron_settings, SimBackend from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor -from simulaqron.toolbox.reset import main as reset_simulaqron CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) -PID_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".simulaqron_pids") +# PID folder should be "LOCAL" +PID_FOLDER = Path.home() / ".simulaqron_pids" -# Check that the default network_config_file exists -simulaqron_settings.default_settings() -default_network_config_file = simulaqron_settings.network_config_file -if not os.path.exists(default_network_config_file): - networks_config = NetworksConfigConstructor() - networks_config.reset() - networks_config.write_to_file(default_network_config_file) +# If the pid folder does not exist, create it +if not PID_FOLDER.exists(): + Path.mkdir(PID_FOLDER) class SimulaQronDaemon(run.RunDaemon): @@ -129,13 +125,14 @@ def version(): "If you want to supress this question, use the --force/-f flag.", is_flag=True, ) + def start(name, nrnodes, nodes, topology, force, keep): """Starts a network with the given parameters or from config files.""" new = not keep if name is None: name = "default" - pidfile = os.path.join(PID_FOLDER, f"simulaqron_network_{name}.pid") - if os.path.exists(pidfile): + pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" + if pidfile.exists(): logging.warning("Network with name %s is already running", name) logging.warning("The pidfile for this network is located at %s", pidfile) return @@ -173,8 +170,8 @@ def stop(name): """Stops a network.""" if name is None: name = "default" - pidfile = os.path.join(PID_FOLDER, f"simulaqron_network_{name}.pid") - if not os.path.exists(pidfile): + pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" + if pidfile.exists(): logging.warning("Network with name %s is not running", name) return d = SimulaQronDaemon(pidfile=pidfile) @@ -203,14 +200,13 @@ def reset(force): else: answer = "yes" if _is_positive_answer(answer): - for entry in os.listdir(PID_FOLDER): - if entry.endswith(".pid"): - pidfile = os.path.join(PID_FOLDER, entry) - d = SimulaQronDaemon(pidfile=pidfile) + for entry in PID_FOLDER.iterdir(): + if entry.suffix == ".pid": + d = SimulaQronDaemon(pidfile=entry) d.stop() - if os.path.exists(pidfile): - os.remove(pidfile) - reset_simulaqron() + if entry.exists(): + entry.unlink() + simulaqron_settings.default_settings() else: print("Aborting!") diff --git a/simulaqron/toolbox/reset.py b/simulaqron/toolbox/reset.py deleted file mode 100644 index 6efda1d2..00000000 --- a/simulaqron/toolbox/reset.py +++ /dev/null @@ -1,24 +0,0 @@ -import os - -from simulaqron.settings import simulaqron_settings -from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor - - -def check_config_files(): - if not os.path.exists(simulaqron_settings.network_config_file): - _create_default_network_config() - - -def _create_default_network_config(): - networks_config = NetworksConfigConstructor() - networks_config.reset() - networks_config.write_to_file(simulaqron_settings.network_config_file) - - -def main(): - simulaqron_settings.default_settings() - _create_default_network_config() - - -if __name__ == '__main__': - main() diff --git a/tests/quick/general/resources/test.json b/tests/quick/general/resources/test.json deleted file mode 100644 index 09ab62aa..00000000 --- a/tests/quick/general/resources/test.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "default": { - "nodes": { - "Alice": { - "app_socket": [ - "localhost", - 8004 - ], - "qnodeos_socket": [ - "localhost", - 8028 - ], - "vnode_socket": [ - "localhost", - 8029 - ] - }, - "Bob": { - "app_socket": [ - "localhost", - 8037 - ], - "qnodeos_socket": [ - "localhost", - 8040 - ], - "vnode_socket": [ - "localhost", - 8041 - ] - } - }, - "topology": null - }, - "test": { - "nodes": { - "Charlie": { - "app_socket": [ - "localhost", - 8042 - ], - "qnodeos_socket": [ - "localhost", - 8043 - ], - "vnode_socket": [ - "localhost", - 8044 - ] - } - }, - "topology": null - } -} \ No newline at end of file From e20328ce679e6baa9223d21e43fe71b390bc9d0f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 14 Oct 2025 12:33:36 +0200 Subject: [PATCH 068/277] Change unittest package in favor of more modern pytest --- tests/quick/engine/test_project_q_engine.py | 242 ++++++------ tests/quick/engine/test_qutip_engine.py | 40 +- tests/quick/engine/test_stabilizer_engine.py | 147 ++++--- tests/quick/general/test_host_config.py | 44 +-- tests/quick/network/test_network.py | 96 ++--- .../test_restricted_topology.py | 17 +- .../test_stabilizer_states.py | 374 +++++++++--------- 7 files changed, 447 insertions(+), 513 deletions(-) diff --git a/tests/quick/engine/test_project_q_engine.py b/tests/quick/engine/test_project_q_engine.py index 2936395b..b60a73d7 100644 --- a/tests/quick/engine/test_project_q_engine.py +++ b/tests/quick/engine/test_project_q_engine.py @@ -1,5 +1,5 @@ -import unittest import numpy as np +import pytest from importlib.util import find_spec @@ -9,173 +9,168 @@ from projectq.types._qubit import Qubit - _has_module = True -else: - _has_module = False +enable_if_projectq = pytest.mark.skipif( + find_spec("projectq") is None, + reason="ProjectQ tests require the 'projectq' module" +) -def if_has_module(test): - def new_test(self): - if _has_module: - test(self) - - return new_test - - -class TestProjectQEnginInit(unittest.TestCase): - @if_has_module +class TestProjectQEnginInit: + @enable_if_projectq def test_init(self): eng = ProjectQEngine("Alice", 0) - self.assertEqual(eng.maxQubits, 10) - self.assertEqual(eng.activeQubits, 0) - self.assertEqual(len(eng.qubitReg), 0) + assert eng.maxQubits == 10 + assert eng.activeQubits== 0 + assert len(eng.qubitReg) == 0 eng = ProjectQEngine("Alice", 0, 5) - self.assertEqual(eng.maxQubits, 5) - self.assertEqual(eng.activeQubits, 0) - self.assertEqual(len(eng.qubitReg), 0) + assert eng.maxQubits == 5 + assert eng.activeQubits == 0 + assert len(eng.qubitReg) == 0 -class TestProjectQEngine(unittest.TestCase): - @if_has_module - def setUp(self): +class TestProjectQEngine: + @pytest.fixture(autouse=True) + def engine(self): self.eng = ProjectQEngine("Alice", 0) + yield + del self.eng @staticmethod - def abs_inner_product(state, ref): + def _abs_inner_product(state, ref): comb_state = np.array(state[0]) + 1j * np.array(state[1]) inner = np.dot(comb_state, np.array(ref).conj()) return np.abs(inner) - @if_has_module + @enable_if_projectq def test_add_fresh_qubit(self): num = self.eng.add_fresh_qubit() - self.assertEqual(num, 0) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) - self.assertTrue(isinstance(self.eng.qubitReg[num], Qubit)) + assert num == 0 + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 + assert isinstance(self.eng.qubitReg[num], Qubit) is True - @if_has_module + @enable_if_projectq def test_add_to_many_fresh_qubits(self): for _ in range(10): self.eng.add_fresh_qubit() - with self.assertRaises(NoQubitError): + with pytest.raises(NoQubitError): self.eng.add_fresh_qubit() - @if_has_module + @enable_if_projectq def test_add_qubit(self): new_state = [1, 0] num = self.eng.add_qubit(new_state) - self.assertEqual(num, 0) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert num == 0 + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state = self.eng.get_register_RI()[1] - self.assertAlmostEqual(self.abs_inner_product(state, new_state), 1) + assert self._abs_inner_product(state, new_state) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_add_qubit_H(self): new_state = [1 / np.sqrt(2), 1 / np.sqrt(2)] num = self.eng.add_qubit(new_state) - self.assertEqual(num, 0) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert num == 0 + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state = self.eng.get_register_RI()[1] - self.assertAlmostEqual(self.abs_inner_product(state, new_state), 1) + assert self._abs_inner_product(state, new_state) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_add_unphysical_qubit(self): new_state = [1, 1] - with self.assertRaises(QuantumError): + with pytest.raises(QuantumError): self.eng.add_qubit(new_state) - @if_has_module + @enable_if_projectq def test_remove_qubit(self): num = self.eng.add_fresh_qubit() self.eng.remove_qubit(num) - self.assertEqual(self.eng.activeQubits, 0) - self.assertEqual(len(self.eng.qubitReg), 0) - with self.assertRaises(QuantumError): + assert self.eng.activeQubits == 0 + assert len(self.eng.qubitReg) == 0 + with pytest.raises(QuantumError): self.eng.remove_qubit(num) - @if_has_module + @enable_if_projectq def test_get_register_RI(self): self.eng.add_fresh_qubit() self.eng.add_fresh_qubit() state = self.eng.get_register_RI()[1] - self.assertAlmostEqual(self.abs_inner_product(state, [1, 0, 0, 0]), 1) + assert self._abs_inner_product(state, [1, 0, 0, 0]) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_H(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) state = self.eng.get_register_RI()[1] - self.assertAlmostEqual(self.abs_inner_product(state, [1 / np.sqrt(2), 1 / np.sqrt(2)]), 1) + assert self._abs_inner_product(state, [1 / np.sqrt(2), 1 / np.sqrt(2)]) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_K(self): num = self.eng.add_fresh_qubit() self.eng.apply_K(num) state = self.eng.get_register_RI()[1] - self.assertAlmostEqual(self.abs_inner_product(state, [1 / np.sqrt(2), 1j / np.sqrt(2)]), 1) + assert self._abs_inner_product(state, [1 / np.sqrt(2), 1j / np.sqrt(2)]) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_X(self): num = self.eng.add_fresh_qubit() self.eng.apply_X(num) state = self.eng.get_register_RI()[1] - self.assertAlmostEqual(self.abs_inner_product(state, [0, 1]), 1) + assert self._abs_inner_product(state, [0, 1]) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_Y(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) self.eng.apply_Y(num) state = self.eng.get_register_RI()[1] ref = [-1j / np.sqrt(2), 1j / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_Z(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) self.eng.apply_Z(num) state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), -1 / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_Rx(self): num = self.eng.add_fresh_qubit() self.eng.apply_rotation(num, (1, 0, 0), np.pi / 2) state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), -1j / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_Ry(self): num = self.eng.add_fresh_qubit() self.eng.apply_rotation(num, (0, 1, 0), np.pi / 2) state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), 1 / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_Rz(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) self.eng.apply_rotation(num, (0, 0, 1), np.pi / 2) state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), 1j / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_faulty_rot(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) - with self.assertRaises(NotImplementedError): + with pytest.raises(NotImplementedError): self.eng.apply_rotation(num, (1, 0, 1), np.pi / 2) - @if_has_module + @enable_if_projectq def test_cnot(self): num1 = self.eng.add_fresh_qubit() num2 = self.eng.add_fresh_qubit() @@ -183,9 +178,9 @@ def test_cnot(self): self.eng.apply_CNOT(num1, num2) state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_cz(self): num1 = self.eng.add_fresh_qubit() num2 = self.eng.add_fresh_qubit() @@ -194,62 +189,62 @@ def test_cz(self): self.eng.apply_CPHASE(num1, num2) state = self.eng.get_register_RI()[1] ref = [1 / 2, 1 / 2, 1 / 2, -1 / 2] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_measure0(self): num = self.eng.add_fresh_qubit() m = self.eng.measure_qubit(num) - self.assertEqual(m, 0) - self.assertEqual(self.eng.activeQubits, 0) + assert m == 0 + assert self.eng.activeQubits == 0 - @if_has_module + @enable_if_projectq def test_measure1(self): num = self.eng.add_fresh_qubit() self.eng.apply_X(num) m = self.eng.measure_qubit(num) - self.assertEqual(m, 1) - self.assertEqual(self.eng.activeQubits, 0) + assert m == 1 + assert self.eng.activeQubits == 0 - @if_has_module + @enable_if_projectq def test_measure_inplace(self): num = self.eng.add_fresh_qubit() m = self.eng.measure_qubit_inplace(num) - self.assertEqual(m, 0) - self.assertEqual(self.eng.activeQubits, 1) + assert m == 0 + assert self.eng.activeQubits == 1 - @if_has_module + @enable_if_projectq def test_absorb_both_empty(self): eng2 = ProjectQEngine("Alice", 0) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 0) - self.assertEqual(len(self.eng.qubitReg), 0) + assert self.eng.activeQubits == 0 + assert len(self.eng.qubitReg) == 0 - @if_has_module + @enable_if_projectq def test_absorb_other_empty(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) eng2 = ProjectQEngine("Alice", 0) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), 1 / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_absorb_this_empty_H(self): eng2 = ProjectQEngine("Alice", 0) num = eng2.add_fresh_qubit() eng2.apply_H(num) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), 1 / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_absorb_this_empty_CNOT(self): eng2 = ProjectQEngine("Alice", 0) num1 = eng2.add_fresh_qubit() @@ -257,13 +252,13 @@ def test_absorb_this_empty_CNOT(self): eng2.apply_H(num1) eng2.apply_CNOT(num1, num2) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 2) - self.assertEqual(len(self.eng.qubitReg), 2) + assert self.eng.activeQubits == 2 + assert len(self.eng.qubitReg) == 2 state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_absorb_this_empty_GHZ(self): n = 5 eng2 = ProjectQEngine("Alice", 0) @@ -272,13 +267,13 @@ def test_absorb_this_empty_GHZ(self): for i in range(1, n): eng2.apply_CNOT(qubits[0], qubits[i]) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, n) - self.assertEqual(len(self.eng.qubitReg), n) + assert self.eng.activeQubits == n + assert len(self.eng.qubitReg) == n state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2)] + [0] * (2 ** n - 2) + [1 / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_absorb_2GHZ(self): n = 5 eng2 = ProjectQEngine("Alice", 0) @@ -288,46 +283,46 @@ def test_absorb_2GHZ(self): for i in range(1, n): eng.apply_CNOT(qubits[0], qubits[i]) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 2 * n) - self.assertEqual(len(self.eng.qubitReg), 2 * n) + assert self.eng.activeQubits == 2 * n + assert len(self.eng.qubitReg) == 2 * n - @if_has_module + @enable_if_projectq def test_absorb_to_big_this_empty(self): eng2 = ProjectQEngine("Alice", 0, 11) for _ in range(11): eng2.add_fresh_qubit() - with self.assertRaises(QuantumError): + with pytest.raises(QuantumError): self.eng.absorb(eng2) - @if_has_module + @enable_if_projectq def test_absorb_to_big(self): self.eng.add_fresh_qubit() eng2 = ProjectQEngine("Alice", 0) for _ in range(10): eng2.add_fresh_qubit() - with self.assertRaises(QuantumError): + with pytest.raises(QuantumError): self.eng.absorb(eng2) - @if_has_module + @enable_if_projectq def test_absorb_parts_both_empty(self): eng2 = ProjectQEngine("Alice", 0) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) - self.assertEqual(self.eng.activeQubits, 0) - self.assertEqual(len(self.eng.qubitReg), 0) + assert self.eng.activeQubits == 0 + assert len(self.eng.qubitReg) == 0 - @if_has_module + @enable_if_projectq def test_absorb_parts(self): self.eng.add_fresh_qubit() eng2 = ProjectQEngine("Alice", 0) eng2.add_fresh_qubit() self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) - self.assertEqual(self.eng.activeQubits, 2) - self.assertEqual(len(self.eng.qubitReg), 2) + assert self.eng.activeQubits == 2 + assert len(self.eng.qubitReg) == 2 state = self.eng.get_register_RI()[1] ref = [1, 0, 0, 0] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_absorb_parts_EPR(self): eng2 = ProjectQEngine("Alice", 0) num1 = eng2.add_fresh_qubit() @@ -335,25 +330,20 @@ def test_absorb_parts_EPR(self): eng2.apply_H(num1) eng2.apply_CNOT(num1, num2) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) - self.assertEqual(self.eng.activeQubits, 2) - self.assertEqual(len(self.eng.qubitReg), 2) + assert self.eng.activeQubits == 2 + assert len(self.eng.qubitReg) == 2 state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) + assert self._abs_inner_product(state, ref) == pytest.approx(1) - @if_has_module + @enable_if_projectq def test_absorb_parts_other_empty(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) eng2 = ProjectQEngine("Alice", 0) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state = self.eng.get_register_RI()[1] ref = [1 / np.sqrt(2), 1 / np.sqrt(2)] - self.assertAlmostEqual(self.abs_inner_product(state, ref), 1) - - -if __name__ == "__main__": - if _has_module: - unittest.main() + assert self._abs_inner_product(state, ref) == pytest.approx(1) diff --git a/tests/quick/engine/test_qutip_engine.py b/tests/quick/engine/test_qutip_engine.py index f4cc338a..061307dc 100644 --- a/tests/quick/engine/test_qutip_engine.py +++ b/tests/quick/engine/test_qutip_engine.py @@ -27,28 +27,21 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import unittest +import pytest from importlib.util import find_spec if find_spec("qutip") is not None: from simulaqron.virtual_node.qutip_simulator import QutipEngine - _has_module = True -else: + +enable_if_qutip = pytest.mark.skipif( + find_spec("qutip") is None, + reason="Qtip tests require the 'qutip' module" +) - _has_module = False - -def if_has_module(test): - def new_test(self): - if _has_module: - test(self) - - return new_test - - -class TestQutipEngine(unittest.TestCase): - @if_has_module +class TestQutipEngine: + @enable_if_qutip def test_tracing(self): se = QutipEngine("alice", 0, 10) se2 = QutipEngine("Alice", 0, 10) @@ -69,9 +62,9 @@ def test_tracing(self): se.remove_qubit(1) se2.remove_qubit(2) - self.assertEqual(se.qubitReg, se2.qubitReg) + assert se.qubitReg == se2.qubitReg - @if_has_module + @enable_if_qutip def test_gates(self): se = QutipEngine("alice", 0, 10) se.add_fresh_qubit() @@ -82,22 +75,17 @@ def test_gates(self): se.apply_H(0) se.apply_X(0) - self.assertEqual(savedQubit, se.qubitReg) + assert savedQubit == se.qubitReg - @if_has_module + @enable_if_qutip def test_measure(self): se = QutipEngine("alice", 0) se.add_fresh_qubit() outcome = se.measure_qubit(0) - self.assertEqual(outcome, 0) + assert outcome == 0 se.add_fresh_qubit() se.apply_X(0) outcome = se.measure_qubit(0) - self.assertEqual(outcome, 1) - - -if __name__ == '__main__': - if _has_module: - unittest.main() + assert outcome == 1 diff --git a/tests/quick/engine/test_stabilizer_engine.py b/tests/quick/engine/test_stabilizer_engine.py index d32b4c85..4802d388 100644 --- a/tests/quick/engine/test_stabilizer_engine.py +++ b/tests/quick/engine/test_stabilizer_engine.py @@ -1,4 +1,4 @@ -import unittest +import pytest import numpy as np from simulaqron.virtual_node.stabilizer_simulator import StabilizerEngine @@ -7,112 +7,113 @@ from simulaqron.general import SimUnsupportedError -class TestStabilizerEngine_init(unittest.TestCase): +class TestStabilizerEngineInit: def test_init(self): eng = StabilizerEngine("Alice", 0) - self.assertEqual(eng.maxQubits, 10) - self.assertEqual(eng.activeQubits, 0) - self.assertEqual(len(eng.qubitReg), 0) + assert eng.maxQubits == 10 + assert eng.activeQubits == 0 + assert len(eng.qubitReg) == 0 eng = StabilizerEngine("Alice", 0, 5) - self.assertEqual(eng.maxQubits, 5) - self.assertEqual(eng.activeQubits, 0) - self.assertEqual(len(eng.qubitReg), 0) + assert eng.maxQubits == 5 + assert eng.activeQubits == 0 + assert len(eng.qubitReg) == 0 -class TestStabilizerEngine(unittest.TestCase): - def setUp(self): +class TestStabilizerEngine: + @pytest.fixture(autouse=True) + def engine(self): self.eng = StabilizerEngine("Alice", 0) def test_add_fresh_qubit(self): num = self.eng.add_fresh_qubit() - self.assertEqual(num, 0) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert num == 0 + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 def test_add_to_many_fresh_qubits(self): for _ in range(10): self.eng.add_fresh_qubit() - with self.assertRaises(NoQubitError): + with pytest.raises(NoQubitError): self.eng.add_fresh_qubit() def test_add_qubit(self): new_state = [[0, 1]] num = self.eng.add_qubit(new_state) - self.assertEqual(num, 0) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert num == 0 + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state, _ = self.eng.get_register_RI() - self.assertEqual(StabilizerState(state), StabilizerState(new_state)) + assert StabilizerState(state) == StabilizerState(new_state) def test_add_qubit_H(self): new_state = [[1, 0]] num = self.eng.add_qubit(new_state) - self.assertEqual(num, 0) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert num == 0 + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state, _ = self.eng.get_register_RI() - self.assertEqual(StabilizerState(state), StabilizerState(new_state)) + assert StabilizerState(state) == StabilizerState(new_state) def test_remove_qubit(self): num = self.eng.add_fresh_qubit() self.eng.remove_qubit(num) - self.assertEqual(self.eng.activeQubits, 0) - self.assertEqual(len(self.eng.qubitReg), 0) - with self.assertRaises(QuantumError): + assert self.eng.activeQubits == 0 + assert len(self.eng.qubitReg) == 0 + with pytest.raises(QuantumError): self.eng.remove_qubit(num) def test_get_register_RI(self): self.eng.add_fresh_qubit() self.eng.add_fresh_qubit() state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState(2)) + assert StabilizerState(state) == StabilizerState(2) def test_H(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 0]])) + assert StabilizerState(state) == StabilizerState([[1, 0]]) def test_K(self): num = self.eng.add_fresh_qubit() self.eng.apply_K(num) state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 1]])) + assert StabilizerState(state) == StabilizerState([[1, 1]]) def test_X(self): num = self.eng.add_fresh_qubit() self.eng.apply_X(num) state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[0, 1, 1]])) + assert StabilizerState(state) == StabilizerState([[0, 1, 1]]) def test_Y(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) self.eng.apply_Y(num) state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 0, 1]])) + assert StabilizerState(state) == StabilizerState([[1, 0, 1]]) def test_Z(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) self.eng.apply_Z(num) state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 0, 1]])) + assert StabilizerState(state) == StabilizerState([[1, 0, 1]]) def test_Rx(self): num = self.eng.add_fresh_qubit() - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): self.eng.apply_rotation(num, (1, 0, 0), np.pi / 2) def test_Ry(self): num = self.eng.add_fresh_qubit() - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): self.eng.apply_rotation(num, (0, 1, 0), np.pi / 2) def test_Rz(self): num = self.eng.add_fresh_qubit() - with self.assertRaises(SimUnsupportedError): + with pytest.raises(SimUnsupportedError): self.eng.apply_rotation(num, (0, 0, 1), np.pi / 2) def test_cnot(self): @@ -121,7 +122,7 @@ def test_cnot(self): self.eng.apply_H(num1) self.eng.apply_CNOT(num1, num2) state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]])) + assert StabilizerState(state) == StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) def test_cz(self): num1 = self.eng.add_fresh_qubit() @@ -130,52 +131,52 @@ def test_cz(self): self.eng.apply_H(num2) self.eng.apply_CPHASE(num1, num2) state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 0, 0, 1], [0, 1, 1, 0]])) + assert StabilizerState(state) == StabilizerState([[1, 0, 0, 1], [0, 1, 1, 0]]) def test_measure0(self): num = self.eng.add_fresh_qubit() m = self.eng.measure_qubit(num) - self.assertEqual(m, 0) - self.assertEqual(self.eng.activeQubits, 0) + assert m == 0 + assert self.eng.activeQubits == 0 def test_measure1(self): num = self.eng.add_fresh_qubit() self.eng.apply_X(num) m = self.eng.measure_qubit(num) - self.assertEqual(m, 1) - self.assertEqual(self.eng.activeQubits, 0) + assert m == 1 + assert self.eng.activeQubits == 0 def test_measure_inplace(self): num = self.eng.add_fresh_qubit() m = self.eng.measure_qubit_inplace(num) - self.assertEqual(m, 0) - self.assertEqual(self.eng.activeQubits, 1) + assert m == 0 + assert self.eng.activeQubits == 1 def test_absorb_both_empty(self): eng2 = StabilizerEngine("Alice", 0) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 0) - self.assertEqual(len(self.eng.qubitReg), 0) + assert self.eng.activeQubits == 0 + assert len(self.eng.qubitReg) == 0 def test_absorb_other_empty(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) eng2 = StabilizerEngine("Alice", 0) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 0]])) + assert StabilizerState(state) == StabilizerState([[1, 0]]) def test_absorb_this_empty_H(self): eng2 = StabilizerEngine("Alice", 0) num = eng2.add_fresh_qubit() eng2.apply_H(num) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 0]])) + assert StabilizerState(state) == StabilizerState([[1, 0]]) def test_absorb_this_empty_CNOT(self): eng2 = StabilizerEngine("Alice", 0) @@ -184,10 +185,10 @@ def test_absorb_this_empty_CNOT(self): eng2.apply_H(num1) eng2.apply_CNOT(num1, num2) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 2) - self.assertEqual(len(self.eng.qubitReg), 2) + assert self.eng.activeQubits == 2 + assert len(self.eng.qubitReg) == 2 state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]])) + assert StabilizerState(state) == StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) def test_absorb_this_empty_GHZ(self): n = 5 @@ -197,14 +198,14 @@ def test_absorb_this_empty_GHZ(self): for i in range(1, n): eng2.apply_CNOT(qubits[0], qubits[i]) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, n) - self.assertEqual(len(self.eng.qubitReg), n) + assert self.eng.activeQubits == n + assert len(self.eng.qubitReg) == n state, _ = self.eng.get_register_RI() ref = [1 / np.sqrt(2)] + [0] * (2 ** n - 2) + [1 / np.sqrt(2)] ref = [[1] * n + [0] * n] for i in range(n - 1): ref += [[0] * n + [0] * i + [1] * 2 + [0] * (n - i - 2)] - self.assertTrue(StabilizerState(state) == StabilizerState(ref)) + assert StabilizerState(state) == StabilizerState(ref) def test_absorb_2GHZ(self): n = 5 @@ -215,14 +216,14 @@ def test_absorb_2GHZ(self): for i in range(1, n): eng.apply_CNOT(qubits[0], qubits[i]) self.eng.absorb(eng2) - self.assertEqual(self.eng.activeQubits, 2 * n) - self.assertEqual(len(self.eng.qubitReg), 2 * n) + assert self.eng.activeQubits == 2 * n + assert len(self.eng.qubitReg) == 2 * n def test_absorb_to_big_this_empty(self): eng2 = StabilizerEngine("Alice", 0, 11) for _ in range(11): eng2.add_fresh_qubit() - with self.assertRaises(QuantumError): + with pytest.raises(QuantumError): self.eng.absorb(eng2) def test_absorb_to_big(self): @@ -230,24 +231,24 @@ def test_absorb_to_big(self): eng2 = StabilizerEngine("Alice", 0) for _ in range(10): eng2.add_fresh_qubit() - with self.assertRaises(QuantumError): + with pytest.raises(QuantumError): self.eng.absorb(eng2) def test_absorb_parts_both_empty(self): eng2 = StabilizerEngine("Alice", 0) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) - self.assertEqual(self.eng.activeQubits, 0) - self.assertEqual(len(self.eng.qubitReg), 0) + assert self.eng.activeQubits == 0 + assert len(self.eng.qubitReg) == 0 def test_absorb_parts(self): self.eng.add_fresh_qubit() eng2 = StabilizerEngine("Alice", 0) eng2.add_fresh_qubit() self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) - self.assertEqual(self.eng.activeQubits, 2) - self.assertEqual(len(self.eng.qubitReg), 2) + assert self.eng.activeQubits == 2 + assert len(self.eng.qubitReg) == 2 state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[0, 0, 1, 0], [0, 0, 0, 1]])) + assert StabilizerState(state) == StabilizerState([[0, 0, 1, 0], [0, 0, 0, 1]]) def test_absorb_parts_EPR(self): eng2 = StabilizerEngine("Alice", 0) @@ -256,21 +257,17 @@ def test_absorb_parts_EPR(self): eng2.apply_H(num1) eng2.apply_CNOT(num1, num2) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) - self.assertEqual(self.eng.activeQubits, 2) - self.assertEqual(len(self.eng.qubitReg), 2) + assert self.eng.activeQubits == 2 + assert len(self.eng.qubitReg) == 2 state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]])) + assert StabilizerState(state) == StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) def test_absorb_parts_other_empty(self): num = self.eng.add_fresh_qubit() self.eng.apply_H(num) eng2 = StabilizerEngine("Alice", 0) self.eng.absorb_parts(*eng2.get_register_RI(), eng2.activeQubits) - self.assertEqual(self.eng.activeQubits, 1) - self.assertEqual(len(self.eng.qubitReg), 1) + assert self.eng.activeQubits == 1 + assert len(self.eng.qubitReg) == 1 state, _ = self.eng.get_register_RI() - self.assertTrue(StabilizerState(state) == StabilizerState([[1, 0]])) - - -if __name__ == "__main__": - unittest.main() + assert StabilizerState(state) == StabilizerState([[1, 0]]) diff --git a/tests/quick/general/test_host_config.py b/tests/quick/general/test_host_config.py index 81429871..44fd9600 100644 --- a/tests/quick/general/test_host_config.py +++ b/tests/quick/general/test_host_config.py @@ -1,12 +1,10 @@ -import unittest -import os +from pathlib import Path +from tempfile import NamedTemporaryFile from simulaqron.general.host_config import NetworksConfigConstructor, SocketsConfig -PATH_TO_HERE = os.path.abspath(os.path.dirname(__file__)) - -class TestNetworkConfig(unittest.TestCase): +class TestNetworkConfig: def test_read_write(self): network_config = NetworksConfigConstructor() @@ -15,30 +13,28 @@ def test_read_write(self): network_config.add_node("Charlie", network_name="test") dct1 = network_config.to_dict() - file_path = os.path.join(PATH_TO_HERE, "resources", "test.json") - network_config.write_to_file(file_path) + with NamedTemporaryFile(mode="w", delete_on_close=False) as temp_file: + network_config.write_to_file(temp_file.name) + temp_file.close() - network_config2 = NetworksConfigConstructor(file_path=file_path) - dct2 = network_config2.to_dict() + network_config2 = NetworksConfigConstructor(file_path=temp_file.name) + dct2 = network_config2.to_dict() - self.assertEqual(dct1, dct2) - self.assertIn("Alice", dct1["default"]["nodes"]) - self.assertIn("Bob", dct1["default"]["nodes"]) - self.assertIn("Charlie", dct1["test"]["nodes"]) + assert dct1 == dct2 + assert "Alice" in dct1["default"]["nodes"] + assert "Bob" in dct1["default"]["nodes"] + assert "Charlie" in dct1["test"]["nodes"] -class TestSocketsConfig(unittest.TestCase): +class TestSocketsConfig: def test_load_file(self): - file_path1 = os.path.join(PATH_TO_HERE, "resources", "sockets.cfg") - conf1 = SocketsConfig(file_path1) + this_file_folder = Path(__file__).parent + sockets_config_path = this_file_folder / "resources" / "sockets.cfg" + conf1 = SocketsConfig(str(sockets_config_path.resolve())) - file_path2 = os.path.join(PATH_TO_HERE, "resources", "network.json") - conf2 = SocketsConfig(file_path2, config_type="qnodeos") + network_config_path = this_file_folder / "resources" / "network.json" + conf2 = SocketsConfig(str(network_config_path.resolve()), config_type="qnodeos") for node_name, host in conf1.hostDict.items(): - self.assertEqual(host.port, conf2.hostDict[node_name].port) - self.assertEqual(host.hostname, conf2.hostDict[node_name].hostname) - - -if __name__ == '__main__': - unittest.main() + assert host.port == conf2.hostDict[node_name].port + assert host.hostname == conf2.hostDict[node_name].hostname diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index b31eab0c..58ae8182 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -1,114 +1,104 @@ import json import time -import unittest +from typing import List + +import pytest from timeit import default_timer as timer -from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor from simulaqron.settings import simulaqron_settings from simulaqron.network import Network -class TestInitNetwork(unittest.TestCase): - def setUp(self): - self.network = None - self.default_nodes = ["Alice", "Bob", "Charlie", "David", "Eve"] - self.default_topology = None - - def tearDown(self): - self.check_nodes_and_topology_in_file(self.network) - - @classmethod - def tearDownClass(cls): - simulaqron_settings.default_settings() - default_network_config_file = simulaqron_settings.network_config_file - network_config = NetworksConfigConstructor(default_network_config_file) - network_config.reset() - network_config.write_to_file() - simulaqron_settings.default_settings() - - def assert_nodes(self, nodes1, nodes2): - self.assertSetEqual(set(nodes1), set(nodes2)) +class TestInitNetwork: + def _assert_nodes(self, nodes1: List[str], nodes2: List[str]): + assert set(nodes1) == set(nodes2) - def assert_topology(self, topology1, topology2): + def _assert_topology(self, topology1, topology2): if topology1 is None: - self.assertIs(topology2, None) + assert topology2 is None return - self.assertEqual(len(topology1), len(topology2)) + assert len(topology1) == len(topology2) for key, neigh1 in topology1.items(): - self.assertIn(key, topology2) + assert key in topology2 neigh2 = topology2[key] - self.assert_nodes(neigh1, neigh2) + self._assert_nodes(neigh1, neigh2) - def check_nodes_and_topology_in_file(self, network): + def _check_nodes_and_topology_in_file(self, network: Network): network_config_file = simulaqron_settings.network_config_file with open(network_config_file, 'r') as f: network_config = json.load(f) nodes_in_file = list(network_config[network.name]["nodes"].keys()) - self.assert_nodes(nodes_in_file, network.nodes) + self._assert_nodes(nodes_in_file, network.nodes) topology_in_file = network_config[network.name]["topology"] - self.assert_topology(topology_in_file, network.topology) + self._assert_topology(topology_in_file, network.topology) + + @pytest.fixture(autouse=True) + def network(self): + self.network = None + self.default_nodes = ["Alice", "Bob", "Charlie", "David", "Eve"] + self.default_topology = None + yield + self._check_nodes_and_topology_in_file(self.network) def test_init_no_argument(self): self.network = Network(force=True) - self.assert_nodes(self.network.nodes, self.default_nodes) - self.assert_topology(self.network.topology, self.default_topology) + self._assert_nodes(self.network.nodes, self.default_nodes) + self._assert_topology(self.network.topology, self.default_topology) def test_init_node_argument(self): nodes = ["Test3", "Test4"] self.network = Network(nodes=nodes, force=True) - self.assert_nodes(self.network.nodes, nodes) - self.assert_topology(self.network.topology, self.default_topology) + self._assert_nodes(self.network.nodes, nodes) + self._assert_topology(self.network.topology, self.default_topology) def test_init_topology_argument(self): topology = {"Test1": [], "Test2": [], "Test3": []} nodes = list(topology.keys()) self.network = Network(topology=topology, force=True) - self.assert_nodes(self.network.nodes, nodes) - self.assert_topology(self.network.topology, topology) + self._assert_nodes(self.network.nodes, nodes) + self._assert_topology(self.network.topology, topology) def test_init_node_and_topology_argument(self): nodes = ["Test5", "Test6"] topology = {"Test5": ["Test6"], "Test6": ["Test5"]} self.network = Network(nodes=nodes, topology=topology, force=True) - self.assert_nodes(self.network.nodes, nodes) - self.assert_topology(self.network.topology, topology) + self._assert_nodes(self.network.nodes, nodes) + self._assert_topology(self.network.topology, topology) -class TestStartStopNetwork(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.nodes = ["Test1", "Test2", "Test3"] +class TestStartStopNetwork: + nodes = ["Test1", "Test2", "Test3"] def test_start(self): network = Network(nodes=self.nodes, force=True) - self.assertEqual(len(network.processes), 2 * len(self.nodes)) + assert len(network.processes) == 2 * len(self.nodes) for p in network.processes: - self.assertFalse(p.is_alive()) + assert p.is_alive() is False network.start(wait_until_running=True) - self.assertTrue(network.running) + assert network.running is True for p in network.processes: - self.assertTrue(p.is_alive()) + assert p.is_alive() is True def test_stop(self): network = Network(force=True) network.stop() for p in network.processes: - self.assertFalse(p.is_alive()) + assert p.is_alive() is False def test_start_stop(self): network = Network(force=True) network.start(wait_until_running=True) for p in network.processes: - self.assertTrue(p.is_alive()) + assert p.is_alive() is True network.stop() for p in network.processes: - self.assertFalse(p.is_alive()) + assert p.is_alive() is False def test_no_wait(self): network = Network(nodes=self.nodes, force=True) network.start(wait_until_running=False) - self.assertFalse(network.running) + assert network.running is False # Check that network starts running eventually max_time = 10 # s @@ -119,13 +109,9 @@ def test_no_wait(self): else: time.sleep(0.1) - self.assertTrue(network.running) + assert network.running is True def test_del(self): network = Network(force=True) network.start(wait_until_running=True) del network - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/quick/network_topology/test_restricted_topology.py b/tests/quick/network_topology/test_restricted_topology.py index 36df7027..95610956 100644 --- a/tests/quick/network_topology/test_restricted_topology.py +++ b/tests/quick/network_topology/test_restricted_topology.py @@ -1,8 +1,7 @@ -import unittest +import pytest import logging from functools import partial -from netqasm.logging.glob import set_log_level from netqasm.sdk import EPRSocket from netqasm.runtime.app_config import default_app_config @@ -12,12 +11,12 @@ from simulaqron.run import run_applications -@unittest.skip("Restricted topology is not yet supported in the new version of simulaqron") -class TestRestrictedTopology(unittest.TestCase): +@pytest.mark.skip(reason="Restricted topology is not yet supported in the new version of simulaqron") +class TestRestrictedTopology: @classmethod def setUpClass(cls): + # default_settings ensures that the local configuration is NOT loaded simulaqron_settings.default_settings() - simulaqron_settings._read_user = False simulaqron_settings.log_level = logging.CRITICAL nodes = ["Alice", "Bob", "Charlie"] cls.network = Network(nodes=nodes, topology="path", force=True) @@ -29,7 +28,6 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): cls.network.stop() - simulaqron_settings.default_settings() def test_EPR(self): outcomes = [] @@ -53,7 +51,7 @@ def recv_func(name, remote_name): default_app_config(create_name, partial(create_func, create_name, recv_name)), default_app_config(recv_name, partial(create_func, recv_name, create_name)), ], use_app_config=False) - self.assertEqual(sum(outcomes) % 2, 0) + assert sum(outcomes) % 2 == 0 return for sender_name, receiver_name in self.non_edges: @@ -62,8 +60,3 @@ def recv_func(name, remote_name): default_app_config(create_name, partial(create_func, create_name, recv_name)), default_app_config(recv_name, partial(create_func, recv_name, create_name)), ], use_app_config=False) - - -if __name__ == "__main__": - set_log_level("DEBUG") - unittest.main() diff --git a/tests/quick/stabilizer_states/test_stabilizer_states.py b/tests/quick/stabilizer_states/test_stabilizer_states.py index 42685927..7b4fe75a 100644 --- a/tests/quick/stabilizer_states/test_stabilizer_states.py +++ b/tests/quick/stabilizer_states/test_stabilizer_states.py @@ -1,51 +1,51 @@ -import unittest +import pytest import numpy as np import networkx as nx from simulaqron.toolbox.stabilizer_states import StabilizerState -class TestStabilizerStates(unittest.TestCase): +class TestStabilizerStates: def test_faulty_init(self): - with self.assertRaises(ValueError): + with pytest.raises(ValueError): StabilizerState([1]) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): StabilizerState([[[1]]]) def test_correct_init(self): state = StabilizerState([[0, 1]]) - self.assertAlmostEqual(state.num_qubits, 1) + assert state.num_qubits == pytest.approx(1) state = StabilizerState([[0, 1, 0]]) - self.assertAlmostEqual(state.num_qubits, 1) + assert state.num_qubits == pytest.approx(1) state = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) - self.assertAlmostEqual(state.num_qubits, 2) + assert state.num_qubits == pytest.approx(2) state = StabilizerState([[1, 1, 0, 0, 0], [0, 0, 1, 1, 1]]) - self.assertAlmostEqual(state.num_qubits, 2) + assert state.num_qubits == pytest.approx(2) - self.assertTrue(state == StabilizerState(state)) + assert state == StabilizerState(state) def test_symplectic_check(self): StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): StabilizerState([[1, 0, 0, 0], [0, 0, 1, 0]]) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): StabilizerState([[1, 1, 0, 0], [0, 0, 1, 0]]) - with self.assertRaises(ValueError): + with pytest.raises(ValueError): StabilizerState([[1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1], [0, 0, 0, 1, 0, 0]]) def test_networkx_init(self): n = 5 G = nx.complete_graph(n) graph_state = StabilizerState(G) - self.assertEqual(graph_state.num_qubits, n) + assert graph_state.num_qubits == n # Create a star graph state and check that this is SQC equiv to the GHZ state G = nx.star_graph(n - 1) @@ -58,7 +58,7 @@ def test_networkx_init(self): for i in range(1, n): GHZ_state.apply_CNOT(0, i) - self.assertTrue(graph_state == GHZ_state) + assert graph_state == GHZ_state def test_list_of_str_init(self): phip = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) @@ -66,47 +66,47 @@ def test_list_of_str_init(self): data = ["XX", "ZZ"] s1 = StabilizerState(data) - self.assertTrue(s1 == phip) + assert s1 == phip data = ["+1XX", "+1ZZ"] s1 = StabilizerState(data) - self.assertTrue(s1 == phip) + assert s1 == phip data = ["+1XX", "-1ZZ"] s1 = StabilizerState(data) - self.assertTrue(s1 == phim) + assert s1 == phim # Test faulty input data = ["XX", "-2ZZ"] - with self.assertRaises(ValueError): + with pytest.raises(ValueError): StabilizerState(data) data = ["XX", "ZZZ"] - with self.assertRaises(ValueError): + with pytest.raises(ValueError): StabilizerState(data) def test_init_of_class(self): state = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) - self.assertTrue(state == StabilizerState(state)) + assert state == StabilizerState(state) def test_add_qubit(self): s = StabilizerState() - self.assertEqual(s.num_qubits, 0) + assert s.num_qubits == 0 z0 = StabilizerState([[0, 1]]) s.add_qubit() - self.assertTrue(s == z0) + assert s == z0 def test_init_of_number_of_qubits(self): s = StabilizerState(1) z0 = StabilizerState([[0, 1]]) - self.assertEqual(s.num_qubits, 1) - self.assertTrue(s == z0) + assert s.num_qubits == 1 + assert s == z0 s = StabilizerState(2) - self.assertEqual(s.num_qubits, 2) - self.assertTrue(s == z0.tensor_product(z0)) + assert s.num_qubits == 2 + assert s == z0.tensor_product(z0) def test_eq(self): state1 = StabilizerState([[0, 1]]) @@ -116,24 +116,24 @@ def test_eq(self): state5 = StabilizerState([[1, 1, 0]]) state6 = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) - self.assertTrue(state1 == state1) - self.assertTrue(state1 == state2) - self.assertTrue(state1 == state3) - self.assertFalse(state1 == state4) - self.assertFalse(state1 == state5) - self.assertFalse(state5 == state6) + assert state1 == state1 + assert state1 == state2 + assert state1 == state3 + assert state1 != state4 + assert state1 != state5 + assert state5 != state6 def test_repr(self): s1 = StabilizerState([[0, 0, 1, 0, 0], [0, 0, 0, 1, 1]]) s2 = StabilizerState(eval(repr(s1))) - self.assertTrue(s1 == s2) + assert s1 == s2 def test_to_array(self): s1 = StabilizerState([[0, 0, 1, 0, 0], [0, 0, 0, 1, 1]]) s2 = StabilizerState(s1.to_array()) - self.assertTrue(s1 == s2) + assert s1 == s2 def test_tensor_product(self): s1 = StabilizerState([[0, 1]]) # The state |0> @@ -141,54 +141,52 @@ def test_tensor_product(self): s3 = s1 * s2 # This is then the state |00> s4 = StabilizerState([[0, 0, 1, 0], [0, 0, 0, 1]]) - self.assertEqual(s3.num_qubits, 2) - self.assertTrue(s3 == s4) + assert s3.num_qubits == 2 + assert s3 == s4 s1 = StabilizerState([[0, 1]]) # The state |0> s2 = StabilizerState([[0, 1, 1]]) # The state |1> s3 = s1 * s2 # This is then the state |01> s4 = StabilizerState([[0, 0, 1, 0, 0], [0, 0, 0, 1, 1]]) - self.assertEqual(s3.num_qubits, 2) - self.assertTrue(s3 == s4) + assert s3.num_qubits == 2 + assert s3 == s4 def test_Pauli_phase_tracking(self): S = StabilizerState() - self.assertFalse(S.Pauli_phase_tracking([False, False], [False, False])) - self.assertFalse(S.Pauli_phase_tracking([True, True], [True, True])) - self.assertFalse(S.Pauli_phase_tracking([True, False], [True, False])) - self.assertFalse(S.Pauli_phase_tracking([False, True], [False, True])) + assert not S.Pauli_phase_tracking([False, False], [False, False]) + assert not S.Pauli_phase_tracking([True, True], [True, True]) + assert not S.Pauli_phase_tracking([True, False], [True, False]) + assert not S.Pauli_phase_tracking([False, True], [False, True]) - self.assertTrue(S.Pauli_phase_tracking([True, True], [True, False])) - self.assertTrue(S.Pauli_phase_tracking([False, True], [True, True])) - self.assertTrue(S.Pauli_phase_tracking([True, False], [False, True])) + assert S.Pauli_phase_tracking([True, True], [True, False]) + assert S.Pauli_phase_tracking([False, True], [True, True]) + assert S.Pauli_phase_tracking([True, False], [False, True]) - self.assertEqual( - S.Pauli_phase_tracking([True, False], [True, True]), S.Pauli_phase_tracking([True, True], [False, True]) - ) - self.assertEqual( - S.Pauli_phase_tracking([False, True], [True, False]), S.Pauli_phase_tracking([True, True], [False, True]) - ) + assert (S.Pauli_phase_tracking([True, False], [True, True]) == + S.Pauli_phase_tracking([True, True], [False, True])) + assert (S.Pauli_phase_tracking([False, True], [True, False]) == + S.Pauli_phase_tracking([True, True], [False, True])) def test_gaussian_elimination(self): S = StabilizerState(["XZZ", "YIX", "IXX"]) S.put_in_standard_form() - self.assertTrue(np.array_equal(S.to_array(), StabilizerState(["+1XZZ", "-1ZYZ", "-1ZZY"]).to_array())) + assert np.array_equal(S.to_array(), StabilizerState(["+1XZZ", "-1ZYZ", "-1ZZY"]).to_array()) def test_apply_Pauli(self): s1 = StabilizerState([[0, 1]]) s2 = StabilizerState([[0, 1, 1]]) s1.apply_Z(0) - self.assertFalse(s1 == s2) + assert s1 != s2 s1.apply_X(0) - self.assertTrue(s1 == s2) + assert s1 == s2 s3 = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) s4 = StabilizerState([[1, 1, 0, 0, 0], [0, 0, 1, 1, 1]]) s3.apply_X(0) - self.assertTrue(s3 == s4) + assert s3 == s4 def test_apply_H(self): s1 = StabilizerState([[0, 1]]) @@ -198,13 +196,13 @@ def test_apply_H(self): s5 = StabilizerState([[1, 1, 1]]) s1.apply_H(0) - self.assertTrue(s1 == s2) + assert s1 == s2 s1.apply_H(0) - self.assertTrue(s1 == s3) + assert s1 == s3 s4.apply_H(0) - self.assertTrue(s4 == s5) + assert s4 == s5 def test_apply_K(self): z0 = StabilizerState([[0, 1]]) @@ -215,13 +213,13 @@ def test_apply_K(self): s2 = StabilizerState(x0) s1.apply_K(0) - self.assertTrue(s1 == y0) + assert s1 == y0 s1.apply_K(0) - self.assertTrue(s1 == z0) + assert s1 == z0 s2.apply_K(0) - self.assertTrue(s2 == x1) + assert s2 == x1 def test_apply_S(self): z0 = StabilizerState([[0, 1]]) @@ -232,13 +230,13 @@ def test_apply_S(self): s2 = StabilizerState(x0) s1.apply_S(0) - self.assertTrue(s1 == z0) + assert s1 == z0 s2.apply_S(0) - self.assertTrue(s2 == y0) + assert s2 == y0 s2.apply_S(0) - self.assertTrue(s2 == x1) + assert s2 == x1 def test_standard_form(self): s1 = StabilizerState([[1, 0, 0, 0], [0, 1, 0, 0]]) @@ -246,9 +244,9 @@ def test_standard_form(self): s3 = StabilizerState([[1, 0, 0, 0, 1], [1, 1, 0, 0, 0]]) s4 = StabilizerState([[1, 0, 0, 0, 1], [0, 1, 0, 0, 1]]) - self.assertTrue(s1 == s2) - self.assertFalse(s1 == s3) - self.assertTrue(s3 == s4) + assert s1 == s2 + assert s1 != s3 + assert s3 == s4 def test_apply_CNOT(self): @@ -257,14 +255,14 @@ def test_apply_CNOT(self): z1z1 = StabilizerState([[0, 0, 1, 0, 1], [0, 0, 0, 1, 1]]) z0z0.apply_X(0) z0z0.apply_CNOT(0, 1) - self.assertTrue(z0z0 == z1z1) + assert z0z0 == z1z1 # EPR pair z0z0 = StabilizerState([[0, 0, 1, 0], [0, 0, 0, 1]]) epr = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) z0z0.apply_H(0) z0z0.apply_CNOT(0, 1) - self.assertTrue(z0z0 == epr) + assert z0z0 == epr # Graph state z0z0 = StabilizerState([[0, 0, 1, 0], [0, 0, 0, 1]]) @@ -275,7 +273,7 @@ def test_apply_CNOT(self): z0z0.apply_H(1) z0z0.apply_CNOT(0, 1) z0z0.apply_H(1) - self.assertTrue(z0z0 == graph_state) + assert z0z0 == graph_state def test_apply_CZ(self): @@ -285,7 +283,7 @@ def test_apply_CZ(self): z0x0.apply_X(0) z0x0.apply_CZ(0, 1) z0x0.put_in_standard_form() - self.assertTrue(z0x0 == z1x1) + assert z0x0 == z1x1 # EPR pair z0z0 = StabilizerState([[0, 0, 1, 0], [0, 0, 0, 1]]) @@ -295,7 +293,7 @@ def test_apply_CZ(self): z0z0.apply_H(1) z0z0.apply_CZ(0, 1) z0z0.apply_H(1) - self.assertTrue(z0z0 == epr) + assert z0z0 == epr # Graph state z0z0 = StabilizerState([[0, 0, 1, 0], [0, 0, 0, 1]]) @@ -304,18 +302,18 @@ def test_apply_CZ(self): z0z0.apply_H(1) # Effective CPHASE z0z0.apply_CZ(0, 1) - self.assertTrue(z0z0 == graph_state) + assert z0z0 == graph_state def test_measure(self): for _ in range(20): z0 = StabilizerState([[0, 1]]) m = z0.measure(0) - self.assertEqual(m, 0) + assert m == 0 for _ in range(20): z1 = StabilizerState([[0, 1, 1]]) m = z1.measure(0) - self.assertEqual(m, 1) + assert m == 1 z0z0 = StabilizerState([[0, 0, 1, 0], [0, 0, 0, 1]]) z1z1 = StabilizerState([[0, 0, 1, 0, 1], [0, 0, 0, 1, 1]]) @@ -323,17 +321,17 @@ def test_measure(self): epr = StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]]) m0 = epr.measure(0, inplace=True) m1 = epr.measure(1, inplace=True) - self.assertEqual(m0, m1) + assert m0 == m1 if m0 == 0: - self.assertTrue(epr == z0z0) + assert epr == z0z0 else: - self.assertTrue(epr == z1z1) + assert epr == z1z1 outcomes = [] for _ in range(200): x0 = StabilizerState([[1, 0]]) outcomes.append(x0.measure(0)) - self.assertTrue(80 <= sum(outcomes) <= 120) + assert 80 <= sum(outcomes) <= 120 def test_GHZ(self): n = 5 @@ -343,14 +341,14 @@ def test_GHZ(self): for i in range(1, n): GHZ.apply_CNOT(0, i) outcomes = [GHZ.measure(0) for _ in range(n)] - self.assertNotIn(False, [outcomes[0] == outcomes[i] for i in range(1, n)]) + assert False not in [outcomes[0] == outcomes[i] for i in range(1, n)] def test_going_to_graph(self): one_Bell_pair = StabilizerState(["-1XX", "+1ZZ"]) G = nx.Graph() G.add_edge(0, 1) one_Bell_pair_graph = one_Bell_pair.find_SQC_equiv_graph_state() - self.assertTrue(G, one_Bell_pair_graph) + assert G, one_Bell_pair_graph two_EPR_pairs = StabilizerState( [[1, 0, 0, 1, 0, 0, 0, 0], [0, 1, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 1], [0, 0, 0, 0, 0, 1, 1, 0]] @@ -358,7 +356,7 @@ def test_going_to_graph(self): G.remove_edge(0, 1) G.add_edges_from([(0, 3), (1, 2)]) two_EPR_pairs_graph = two_EPR_pairs.find_SQC_equiv_graph_state() - self.assertTrue(G, two_EPR_pairs_graph) + assert G, two_EPR_pairs_graph n = 5 GHZ = StabilizerState(n) @@ -366,117 +364,103 @@ def test_going_to_graph(self): for i in range(1, n): GHZ.apply_CNOT(0, i) GHZ_graph, operations = GHZ.find_SQC_equiv_graph_state(return_operations=True) - self.assertTrue(GHZ_graph, nx.star_graph(n - 1)) - self.assertTrue(operations == [("H", i) for i in range(1, n)]) - - def test_contains(self): - tests = [ # stabilizer, expected - ("XX", True), - ("+1XX", True), - ("-1XX", False), - ("+1YY", False), - ("-1YY", True), - ("+1YI", False), - ("IY", False), - ] - - for stabilizer, expected in tests: - with self.subTest(stabilizer=stabilizer, expected=expected): - s = StabilizerState(["XX", "ZZ"]) - output = s.contains(stabilizer) - self.assertEqual(output, expected) - - def test_measure_eigenstate(self): - tests = [ # stabilizers, qubit, expected - (["ZI", "IZ"], 0, 0), - (["-1ZI", "IZ"], 0, 1), - (["ZI", "ZZ"], 0, 0), - (["IZ", "-1ZZ"], 0, 1), - (["IZ", "-1ZI"], 0, 1), - (["+1XIIII", "+1IXIII", "+1IIXII", "-1IIIZI", "+1IIIZZ"], 4, 1), - ] - - for stabilizers, qubit, expected in tests: - with self.subTest(stabilizers=stabilizers, qubit=qubit, expected=expected): + assert GHZ_graph, nx.star_graph(n - 1) + assert operations == [("H", i) for i in range(1, n)] + + @pytest.mark.parametrize("stabilizer, expected", + [ # stabilizer, expected + ("XX", True), + ("+1XX", True), + ("-1XX", False), + ("+1YY", False), + ("-1YY", True), + ("+1YI", False), + ("IY", False), + ]) + def test_contains(self, stabilizer, expected): + s = StabilizerState(["XX", "ZZ"]) + output = s.contains(stabilizer) + assert output == expected + + @pytest.mark.parametrize("stabilizers, qubit, expected", + [ # stabilizers, qubit, expected + (["ZI", "IZ"], 0, 0), + (["-1ZI", "IZ"], 0, 1), + (["ZI", "ZZ"], 0, 0), + (["IZ", "-1ZZ"], 0, 1), + (["IZ", "-1ZI"], 0, 1), + (["+1XIIII", "+1IXIII", "+1IIXII", "-1IIIZI", "+1IIIZZ"], 4, 1), + ]) + def test_measure_eigenstate(self, stabilizers, qubit, expected): + s = StabilizerState(stabilizers) + output = s.measure(qubit) + assert output == expected + + @pytest.mark.parametrize("stabilizers", + [ # stabilizers + ["ZI", "IZ"], + ["ZZ", "XX"], + ["ZX", "XZ"], + ["XXX", "ZZI", "IZZ"], + ["XIII", "IXII", "IIXI", "IIIX"], + ["XZII", "ZXZI", "IZXZ", "IIZX"], # line graph + ["XZIZ", "ZXZI", "IZXZ", "ZIZX"], # cycle graph + ["XZZZ", "ZXZZ", "ZZXZ", "ZZZX"], # complete graph + ["XZZZ", "ZXII", "ZIXI", "ZIIX"], # star graph + ]) + def test_correlations(self, stabilizers): + for stabilizer in stabilizers: + for _ in range(10): s = StabilizerState(stabilizers) - output = s.measure(qubit) - self.assertEqual(output, expected) - - def test_correlations(self): - tests = [ # stabilizers - ["ZI", "IZ"], - ["ZZ", "XX"], - ["ZX", "XZ"], - ["XXX", "ZZI", "IZZ"], - ["XIII", "IXII", "IIXI", "IIIX"], - ["XZII", "ZXZI", "IZXZ", "IIZX"], # line graph - ["XZIZ", "ZXZI", "IZXZ", "ZIZX"], # cycle graph - ["XZZZ", "ZXZZ", "ZZXZ", "ZZZX"], # complete graph - ["XZZZ", "ZXII", "ZIXI", "ZIIX"], # star graph - ] - - for stabilizers in tests: - for stabilizer in stabilizers: - for _ in range(10): - with self.subTest(stabilizers=stabilizers, stabilizer=stabilizer): - s = StabilizerState(stabilizers) - outcomes = [] - qubit = 0 - for pauli in stabilizer: - if pauli == 'X': - s.apply_H(qubit) - elif pauli == 'Y': - s.apply_K(qubit) - elif pauli == 'Z': - pass - else: - qubit += 1 - continue - outcomes.append(s.measure(qubit)) - self.assertEqual(sum(outcomes) % 2, 0) - - def test_standard_form(self): - tests = [ # stabilizers - ["ZI", "IZ"], - ["ZZ", "XX"], - ["ZX", "XZ"], - ["XXX", "ZZI", "IZZ"], - ["XIII", "IXII", "IIXI", "IIIX"], - ["XZII", "ZXZI", "IZXZ", "IIZX"], # line graph - ["XZIZ", "ZXZI", "IZXZ", "ZIZX"], # cycle graph - ["XZZZ", "ZXZZ", "ZZXZ", "ZZZX"], # complete graph - ["XZZZ", "ZXII", "ZIXI", "ZIIX"], # star graph - ] - - for stabilizers in tests: - with self.subTest(stabilizers=stabilizers): - state = StabilizerState(stabilizers) - state.put_in_standard_form() - # Check that there are no X or Y in the first column except the first row - for row in state._group[1:, :]: - self.assertFalse(row[0]) - - def test_reduce_when_measuring(self): - tests = [ # stabilizers - ["ZI", "IZ"], - ["ZZ", "XX"], - ["ZX", "XZ"], - ["XXX", "ZZI", "IZZ"], - ["XIII", "IXII", "IIXI", "IIIX"], - ["XZII", "ZXZI", "IZXZ", "IIZX"], # line graph - ["XZIZ", "ZXZI", "IZXZ", "ZIZX"], # cycle graph - ["XZZZ", "ZXZZ", "ZZXZ", "ZZZX"], # complete graph - ["XZZZ", "ZXII", "ZIXI", "ZIIX"], # star graph - ] - - for stabilizers in tests: - with self.subTest(stabilizers=stabilizers): - state = StabilizerState(stabilizers) - n = len(state) - for i in range(n): - state.measure(0) - self.assertEqual(len(state), n - i - 1) - - -if __name__ == "__main__": - unittest.main() + outcomes = [] + qubit = 0 + for pauli in stabilizer: + if pauli == 'X': + s.apply_H(qubit) + elif pauli == 'Y': + s.apply_K(qubit) + elif pauli == 'Z': + pass + else: + qubit += 1 + continue + outcomes.append(s.measure(qubit)) + assert sum(outcomes) % 2 == 0 + + @pytest.mark.parametrize("stabilizers", + [ # stabilizers + ["ZI", "IZ"], + ["ZZ", "XX"], + ["ZX", "XZ"], + ["XXX", "ZZI", "IZZ"], + ["XIII", "IXII", "IIXI", "IIIX"], + ["XZII", "ZXZI", "IZXZ", "IIZX"], # line graph + ["XZIZ", "ZXZI", "IZXZ", "ZIZX"], # cycle graph + ["XZZZ", "ZXZZ", "ZZXZ", "ZZZX"], # complete graph + ["XZZZ", "ZXII", "ZIXI", "ZIIX"], # star graph + ]) + def test_standard_form(self, stabilizers): + state = StabilizerState(stabilizers) + state.put_in_standard_form() + # Check that there are no X or Y in the first column except the first row + for row in state._group[1:, :]: + assert not row[0] + + @pytest.mark.parametrize("stabilizers", + [ # stabilizers + ["ZI", "IZ"], + ["ZZ", "XX"], + ["ZX", "XZ"], + ["XXX", "ZZI", "IZZ"], + ["XIII", "IXII", "IIXI", "IIIX"], + ["XZII", "ZXZI", "IZXZ", "IIZX"], # line graph + ["XZIZ", "ZXZI", "IZXZ", "ZIZX"], # cycle graph + ["XZZZ", "ZXZZ", "ZZXZ", "ZZZX"], # complete graph + ["XZZZ", "ZXII", "ZIXI", "ZIIX"], # star graph + ]) + def test_reduce_when_measuring(self, stabilizers): + state = StabilizerState(stabilizers) + n = len(state) + for i in range(n): + state.measure(0) + assert len(state) == n - i - 1 From 7dab1c0b7ef68a5fb113a0bcfd0fa7baf23f4856 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 14 Oct 2025 14:12:33 +0200 Subject: [PATCH 069/277] Dynamically load the qubit simulation engine to reflect changes on simulaqron configuration --- simulaqron/config/.keep | 0 simulaqron/virtual_node/virtual.py | 20 +++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) delete mode 100644 simulaqron/config/.keep diff --git a/simulaqron/config/.keep b/simulaqron/config/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 38df9b51..a6267bfe 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -50,15 +50,6 @@ from simulaqron.settings import simulaqron_settings, SimBackend from simulaqron.reactor import reactor -if simulaqron_settings.sim_backend == SimBackend.QUTIP: - from simulaqron.virtual_node.qutip_simulator import QutipEngine as QEngine -elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: - from simulaqron.virtual_node.project_q_simulator import ProjectQEngine as QEngine -elif simulaqron_settings.sim_backend == SimBackend.STABILIZER: - from simulaqron.virtual_node.stabilizer_simulator import StabilizerEngine as QEngine -else: - raise QuantumError(f"Unknown backend {simulaqron_settings.sim_backend}") - def reraise_remote_error(remote_err: RemoteError): """ @@ -416,6 +407,17 @@ def remote_new_register(self, maxQubits=10): self.numRegs = self.numRegs + 1 regNum = self.get_new_reg_num() + if simulaqron_settings.sim_backend == SimBackend.QUTIP: + simulator_pkg = importlib.import_module("simulaqron.virtual_node.qutip_simulator") + QEngine = simulator_pkg.QutipEngine + elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: + simulator_pkg = importlib.import_module("simulaqron.virtual_node.project_q_simulator") + QEngine = simulator_pkg.ProjectQEngine + elif simulaqron_settings.sim_backend == SimBackend.STABILIZER: + simulator_pkg = importlib.import_module("simulaqron.virtual_node.stabilizer_simulator") + QEngine = simulator_pkg.StabilizerEngine + else: + raise QuantumError(f"Unknown backend {simulaqron_settings.sim_backend}") newReg = QEngine(self.myID, regNum, maxQubits) self.registers[regNum] = newReg From 439158d99b5d2bd6a61f181868fdfd2b7bd51806 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 14 Oct 2025 14:34:37 +0200 Subject: [PATCH 070/277] Fix setup of some test cases --- tests/quick/netqasm/test_free_qubit.py | 4 +--- tests/quick/netqasm/test_get_qubit_state.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/quick/netqasm/test_free_qubit.py b/tests/quick/netqasm/test_free_qubit.py index 6377ba42..946956b1 100644 --- a/tests/quick/netqasm/test_free_qubit.py +++ b/tests/quick/netqasm/test_free_qubit.py @@ -16,10 +16,8 @@ class TestFreeQubit: @pytest.fixture(autouse=True) def network(self): simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + simulaqron_settings.sim_backend = SimBackend.PROJECTQ yield - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value reset() @staticmethod diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 644dd89d..009fbd63 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -22,8 +22,6 @@ def network(self): simulaqron_settings.default_settings() simulaqron_settings.sim_backend = SimBackend.PROJECTQ yield - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ reset() # Here we define the quantum programs used in the tests From e9764f2faca97a52f129a996eacf40316dacb54b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 14 Oct 2025 14:50:44 +0200 Subject: [PATCH 071/277] Fix deprecation warnings in test cases --- simulaqron/settings.py | 21 +++++++++------------ tests/quick/test_settings.py | 11 ++++++----- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/simulaqron/settings.py b/simulaqron/settings.py index 7f441a52..a5781cdd 100644 --- a/simulaqron/settings.py +++ b/simulaqron/settings.py @@ -37,6 +37,8 @@ from pathlib import Path from typing import Dict, Any +import simulaqron._default_config + # This is the name of the "local" simulaqron settings. # If a file named like this is found in the CWD, it will be # automatically loaded when creating the config file @@ -92,22 +94,17 @@ def update_settings(self, config: Dict[str, Any]): config["network_config_file"] = str(given_network_config) else: # If it doesn't exist, we load the default - resource = resources.path( - "simulaqron._default_config", - "default_network.json" - ) - with resource as network_path: - network_specs_path = network_path.resolve() - assert network_specs_path.exists() and network_path.is_file() - config["network_config_file"] = str(network_specs_path) + resource = resources.files(simulaqron._default_config).joinpath("default_network.json") + network_path = Path(str(resource)).resolve() + assert network_path.exists() and network_path.is_file() + config["network_config_file"] = str(network_path) config["sim_backend"] = SimBackend[config["sim_backend"].upper()] self._config.update(config) def default_settings(self): - default_settings = resources.path("simulaqron._default_config", "default_settings.json") - with default_settings as default_settings_path: - self._loaded_file = str(default_settings_path) - self.load_from_file(default_settings_path) + default_settings_path = resources.files(simulaqron._default_config).joinpath("default_settings.json") + self._loaded_file = str(default_settings_path) + self.load_from_file(Path(self._loaded_file)) def load_from_file(self, path: PathLike): file_path = Path(str(path)).resolve() diff --git a/tests/quick/test_settings.py b/tests/quick/test_settings.py index 3235f07b..62f09371 100644 --- a/tests/quick/test_settings.py +++ b/tests/quick/test_settings.py @@ -4,6 +4,7 @@ from importlib import resources from pathlib import Path +import simulaqron._default_config from simulaqron.settings import simulaqron_settings, SimBackend @@ -26,9 +27,9 @@ def test_default_settings(self): """ expected_settings = json.loads(__expected_default_settings) # For testing purposes, we need to "adjust" some of teh expected values: - expected_settings["sim_backend"] =SimBackend[expected_settings["sim_backend"].upper()] - with resources.path("simulaqron._default_config", "default_network.json") as path: - expected_settings["network_config_file"] = str(path) + expected_settings["sim_backend"] = SimBackend[expected_settings["sim_backend"].upper()] + path = resources.files(simulaqron._default_config).joinpath("default_network.json") + expected_settings["network_config_file"] = str(path) simulaqron_settings.default_settings() for key, value in expected_settings.items(): @@ -68,8 +69,8 @@ def test_non_existent_network_config(self): expected_settings = json.loads(_expected_settings) # For testing purposes, we need to "adjust" some of teh expected values: expected_settings["sim_backend"] =SimBackend[expected_settings["sim_backend"].upper()] - with resources.path("simulaqron._default_config", "default_network.json") as path: - expected_settings["network_config_file"] = str(path) + path = resources.files("simulaqron._default_config").joinpath("default_network.json") + expected_settings["network_config_file"] = str(path) _original_settings = json.loads(_original_settings) with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", delete_on_close=False) as file: From 28a4fc0ba5cd850457fd032986203e46122c8ddc Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 14 Oct 2025 14:50:58 +0200 Subject: [PATCH 072/277] Fallback to forking when creating new processes --- simulaqron/run/run.py | 2 +- tests/slow/merges/test_merges.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index f0bea4c8..b7dbc8b6 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -2,7 +2,7 @@ import os import signal -from multiprocess.context import SpawnContext as ProcessContext +from multiprocess.context import ForkContext as ProcessContext from multiprocess.pool import ApplyResult from importlib import reload from importlib.util import find_spec diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 51703341..56adb69c 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -189,7 +189,7 @@ def setup_node(name, node_code, classical_net_file, send_end): setup_local(name, virtualNet, classicalNet, lNode, node_code, send_end) def run_test(self, classical_net_file): - mp.set_start_method("spawn", force=True) + mp.set_start_method("fork", force=True) pipe_list = [] for name, node_code in zip(self.nodes, self.node_codes): recv_end, send_end = mp.Pipe(False) From 759b53c8cb926c8b9ac9683f17faeb2eb23d2458 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 15 Oct 2025 13:59:18 +0200 Subject: [PATCH 073/277] Test cases now use a temporal configuration to avoid leaving residues for further test cases --- simulaqron/network.py | 22 ++++++------- simulaqron/run/run.py | 2 +- simulaqron/settings.py | 4 +-- simulaqron/toolbox/manage_nodes.py | 25 ++++++++++----- tests/quick/general/test_classical_sockets.py | 22 +++++++++---- tests/quick/general/test_host_config.py | 2 +- tests/quick/netqasm/test_free_qubit.py | 30 +++++++++++------ tests/quick/netqasm/test_get_qubit_state.py | 29 +++++++++++------ tests/quick/network/test_network.py | 32 ++++++++++--------- 9 files changed, 103 insertions(+), 65 deletions(-) diff --git a/simulaqron/network.py b/simulaqron/network.py index 89fd7e50..4885ff83 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -29,8 +29,10 @@ import time import random +from os import PathLike + from multiprocess.context import ForkProcess as Process -from typing import List +from typing import List, Optional, Dict, Any import networkx as nx from timeit import default_timer as timer @@ -49,18 +51,20 @@ class Network: - def __init__(self, name=None, nodes=None, topology=None, network_config_file=None, force=False, new=True): + def __init__(self, name: str = "default", nodes: Optional[List[str]] = None, + topology: Optional[Dict[str, Any]] = None, network_config_file: PathLike | str = None, + force: bool = False, new: bool = True): """ Used to spin up a simulated network. If new=True then a fresh network with only the specified nodes (or the default Alice, Bob, Charlie, David and Eve) are created and overwriting the current network with - the same name in the network config file. Otherwise only the specified nodes are started without changing - the config file. Note that if the nodes does not currently exists and new=False, an ValueError is raised. + the same name in the network config file. Otherwise, only the specified nodes are started without changing + the config file. Note that if the nodes does not currently exist and new=False, an ValueError is raised. If force=False an input to confirm the overwriting is issued. - :param name: None or str (defualts to "default") + :param name: None or str (defaults to "default") :param nodes: None or list of str :param topology: None or dict :param network_config_file: None or str (defaults to simulaqron_settings.network_config_file @@ -68,11 +72,7 @@ def __init__(self, name=None, nodes=None, topology=None, network_config_file=Non :param new: bool """ self._running = False - - if name is None: - self.name = "default" - else: - self.name = name + self.name = name self.processes: List[Process] = [] self._logger = get_netqasm_logger(f"{self.__class__.__name__}({self.name})") @@ -97,7 +97,7 @@ def __init__(self, name=None, nodes=None, topology=None, network_config_file=Non answer = input(f"Do you want to add/replace the network {self.name} in the " f"file {self._network_config_file} with a network consisting " f"of the nodes {self.nodes}? (yes/no)") - if answer not in ["yes", "y"]: + if answer.lower() not in ["yes", "y"]: raise RuntimeError("User did not want to replace network in file") networks_config.add_network(node_names=self.nodes, network_name=self.name, topology=self.topology) networks_config.write_to_file(self._network_config_file) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index b7dbc8b6..22f72110 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -122,7 +122,7 @@ def run_applications( hardware: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" init_func: Callable = None, ) -> List[Dict[str, Any]]: - """Executes functions containing application scripts, + """Executes functions containing quantum applications. Parameters ---------- diff --git a/simulaqron/settings.py b/simulaqron/settings.py index a5781cdd..143d942c 100644 --- a/simulaqron/settings.py +++ b/simulaqron/settings.py @@ -31,7 +31,7 @@ # SETTINGS FOR SIMULAQRON ######################### import json -from enum import Enum +from enum import StrEnum from importlib import resources from os import PathLike from pathlib import Path @@ -45,7 +45,7 @@ SIMULAQRON_SETTINGS_FILENAME = "simulaqron_settings.json" -class SimBackend(Enum): +class SimBackend(StrEnum): STABILIZER = "stabilizer" PROJECTQ = "projectq" QUTIP = "qutip" diff --git a/simulaqron/toolbox/manage_nodes.py b/simulaqron/toolbox/manage_nodes.py index 8a0a19c2..63879927 100644 --- a/simulaqron/toolbox/manage_nodes.py +++ b/simulaqron/toolbox/manage_nodes.py @@ -1,11 +1,16 @@ -import os import json from contextlib import closing import socket +from importlib import resources +from os import PathLike +from pathlib import Path +from typing import Optional, Self + +import simulaqron._default_config class NetworksConfigConstructor: - def __init__(self, file_path=None): + def __init__(self, file_path: Optional[PathLike | str]): """ Used to construct the config file of networks.abs When all nodes and networks are added the content of this object can @@ -17,10 +22,14 @@ def __init__(self, file_path=None): """ self.networks = {} self.used_sockets = [] - self.file_path = file_path - if self.file_path is not None: - if os.path.exists(self.file_path): - self.read_from_file() + self.file_path = None if file_path is None else Path(str(file_path)) + if self.file_path is not None and self.file_path.exists(): + self.read_from_file(self.file_path) + + @classmethod + def default_network_constructor(cls) -> Self: + default_network_path = resources.files(simulaqron._default_config).joinpath("default_network.json") + return cls(Path(str(default_network_path))) def add_node(self, node_name, network_name="default", app_hostname=None, qnodeos_hostname=None, vnode_hostname=None, app_port=None, qnodeos_port=None, vnode_port=None, neighbors=None): @@ -205,7 +214,7 @@ def write_to_file(self, file_path=None): with open(file_path, 'w') as f: json.dump(dict, f, indent=4) - def read_from_file(self, file_path=None): + def read_from_file(self, file_path: PathLike | str): """ Reads config from a file. @@ -217,7 +226,7 @@ def read_from_file(self, file_path=None): if file_path is None: raise ValueError("Since this networks config was not initialized with a file_path you need to specify one") - if os.path.exists(file_path): + if Path(str(file_path)).exists(): with open(file_path, 'r') as f: dict = json.load(f) else: diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py index 18fefcb2..8c9c907e 100644 --- a/tests/quick/general/test_classical_sockets.py +++ b/tests/quick/general/test_classical_sockets.py @@ -1,3 +1,5 @@ +from tempfile import NamedTemporaryFile + import pytest from netqasm.runtime.application import default_app_instance @@ -5,17 +7,23 @@ from simulaqron.run.run import reset from simulaqron.sdk.socket import Socket from simulaqron.settings import simulaqron_settings, SimBackend +from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor class TestClassicalSocket: @pytest.fixture(autouse=True) - def network(self): - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value - yield - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value - reset() + def configurations(self): + with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as network_settings_file: + network_config = NetworksConfigConstructor.default_network_constructor() + network_config.write_to_file(network_settings_file.name) + with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as simulaqron_settings_file: + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + simulaqron_settings.network_config_file = network_settings_file.name + simulaqron_settings.save_to_file(simulaqron_settings_file.name) + simulaqron_settings.load_from_file(simulaqron_settings_file.name) + yield + reset() @staticmethod def alice_program_sender(): diff --git a/tests/quick/general/test_host_config.py b/tests/quick/general/test_host_config.py index 44fd9600..44af97e2 100644 --- a/tests/quick/general/test_host_config.py +++ b/tests/quick/general/test_host_config.py @@ -6,7 +6,7 @@ class TestNetworkConfig: def test_read_write(self): - network_config = NetworksConfigConstructor() + network_config = NetworksConfigConstructor(file_path=None) network_config.add_node("Alice") network_config.add_node("Bob") diff --git a/tests/quick/netqasm/test_free_qubit.py b/tests/quick/netqasm/test_free_qubit.py index 946956b1..f5f0c097 100644 --- a/tests/quick/netqasm/test_free_qubit.py +++ b/tests/quick/netqasm/test_free_qubit.py @@ -1,9 +1,12 @@ +from tempfile import NamedTemporaryFile + import pytest from netqasm.runtime.settings import set_simulator from simulaqron.run.run import reset, run_applications from simulaqron.settings import simulaqron_settings, SimBackend +from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor set_simulator("simulaqron") @@ -14,11 +17,18 @@ class TestFreeQubit: @pytest.fixture(autouse=True) - def network(self): - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ - yield - reset() + def configuration(self): + with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as network_settings_file: + network_config = NetworksConfigConstructor.default_network_constructor() + network_config.write_to_file(network_settings_file.name) + with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as simulaqron_settings_file: + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + simulaqron_settings.network_config_file = network_settings_file.name + simulaqron_settings.save_to_file(simulaqron_settings_file.name) + simulaqron_settings.load_from_file(simulaqron_settings_file.name) + yield + reset() @staticmethod def too_many_qubits(): @@ -97,7 +107,7 @@ def release_and_reuse_qubit_b(): return len(alice.active_qubits) # Here we define the quantum programs used in the tests - def test_too_many_qubits(self, network): + def test_too_many_qubits(self): apps = default_app_instance( [ ("Alice", TestFreeQubit.too_many_qubits) @@ -107,7 +117,7 @@ def test_too_many_qubits(self, network): _ = run_applications(apps, use_app_config=False, enable_logging=False) assert "Virtual address 2 is outside the unit module (app ID 0) which has length 2" in str(exc.value) - def test_release_qubit(self, network): + def test_release_qubit(self): apps = default_app_instance( [ ("Alice", TestFreeQubit.release_qubit) @@ -116,7 +126,7 @@ def test_release_qubit(self, network): result = run_applications(apps, use_app_config=False, enable_logging=False) assert result[0]["app_Alice"] == 1 - def test_release_qubit_b(self, network): + def test_release_qubit_b(self): apps = default_app_instance( [ ("Alice", TestFreeQubit.release_qubit_b) @@ -125,7 +135,7 @@ def test_release_qubit_b(self, network): result = run_applications(apps, use_app_config=False, enable_logging=False) assert result[0]["app_Alice"] == 1 - def test_release_and_reuse_qubit(self, network): + def test_release_and_reuse_qubit(self): apps = default_app_instance( [ ("Alice", TestFreeQubit.release_and_reuse_qubit) @@ -134,7 +144,7 @@ def test_release_and_reuse_qubit(self, network): result = run_applications(apps, use_app_config=False, enable_logging=False) assert result[0]["app_Alice"] == 2 - def test_release_and_reuse_qubit_b(self, network): + def test_release_and_reuse_qubit_b(self): apps = default_app_instance( [ ("Alice", TestFreeQubit.release_and_reuse_qubit_b) diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 009fbd63..04bce83a 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -1,4 +1,5 @@ import math +from tempfile import NamedTemporaryFile import numpy as np import pytest @@ -6,6 +7,7 @@ from netqasm.sdk.classical_communication.message import StructuredMessage from simulaqron.settings import simulaqron_settings, SimBackend +from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor set_simulator("simulaqron") @@ -18,11 +20,18 @@ class TestGetQubit: @pytest.fixture(autouse=True) - def network(self): - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ - yield - reset() + def configuration(self): + with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as network_settings_file: + network_config = NetworksConfigConstructor.default_network_constructor() + network_config.write_to_file(network_settings_file.name) + with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as simulaqron_settings_file: + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + simulaqron_settings.network_config_file = network_settings_file.name + simulaqron_settings.save_to_file(simulaqron_settings_file.name) + simulaqron_settings.load_from_file(simulaqron_settings_file.name) + yield + reset() # Here we define the quantum programs used in the tests @@ -109,7 +118,7 @@ def bob_teleport(): # From here down, the actual tests - def test_peek_new_unflushed_qubit(self, network): + def test_peek_new_unflushed_qubit(self): apps = default_app_instance( [ ("Alice", TestGetQubit.peek_new_unflushed_qubit) @@ -119,7 +128,7 @@ def test_peek_new_unflushed_qubit(self, network): _ = run_applications(apps, use_app_config=False, enable_logging=False) assert "Qubit 0 has unflushed operations" in str(exc.value) - def test_peek_unflushed_qubit(self, network): + def test_peek_unflushed_qubit(self): apps = default_app_instance( [ ("Alice", TestGetQubit.peek_unflushed_qubit) @@ -129,7 +138,7 @@ def test_peek_unflushed_qubit(self, network): _ = run_applications(apps, use_app_config=False, enable_logging=False) assert "Qubit 0 has unflushed operations" in str(exc.value) - def test_get_basic_state_local(self, network): + def test_get_basic_state_local(self): apps = default_app_instance( [ ("Alice", TestGetQubit.peek_init_qubit) @@ -139,7 +148,7 @@ def test_get_basic_state_local(self, network): # We expect the qubit to be initialized in the |0> state = [1 0] assert np.array_equal(raw_results[0]["app_Alice"], np.array([[1.0 + 0.0j, 0 + 0.0j], [0.0 + 0.0j, 0 + 0.0j]])) - def test_get_qubit_state_local(self, network): + def test_get_qubit_state_local(self): apps = default_app_instance( [ ("Alice", TestGetQubit.peek_local_qubit) @@ -154,7 +163,7 @@ def test_get_qubit_state_local(self, network): expected_x = np.array([0.0 + 0.0j, 1 + 0.0j]) assert np.array_equal(raw_results[0]["app_Alice"]["state_b"], np.outer(expected_x, expected_x)) - def test_get_qubit_state_teleport(self, network): + def test_get_qubit_state_teleport(self): apps = default_app_instance( [ ("Alice", TestGetQubit.alice_teleport), diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index 58ae8182..b39b6cc1 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -1,5 +1,6 @@ import json import time +from tempfile import NamedTemporaryFile from typing import List import pytest @@ -34,35 +35,36 @@ def _check_nodes_and_topology_in_file(self, network: Network): self._assert_topology(topology_in_file, network.topology) @pytest.fixture(autouse=True) - def network(self): - self.network = None - self.default_nodes = ["Alice", "Bob", "Charlie", "David", "Eve"] - self.default_topology = None - yield - self._check_nodes_and_topology_in_file(self.network) - - def test_init_no_argument(self): - self.network = Network(force=True) + def network_file(self): + with NamedTemporaryFile() as net_config_file: + self.network = None + self.default_nodes = ["Alice", "Bob", "Charlie", "David", "Eve"] + self.default_topology = None + yield net_config_file.name + self._check_nodes_and_topology_in_file(self.network) + + def test_init_no_argument(self, network_file: str): + self.network = Network(force=True, network_config_file=network_file) self._assert_nodes(self.network.nodes, self.default_nodes) self._assert_topology(self.network.topology, self.default_topology) - def test_init_node_argument(self): + def test_init_node_argument(self, network_file: str): nodes = ["Test3", "Test4"] - self.network = Network(nodes=nodes, force=True) + self.network = Network(nodes=nodes, force=True, network_config_file=network_file) self._assert_nodes(self.network.nodes, nodes) self._assert_topology(self.network.topology, self.default_topology) - def test_init_topology_argument(self): + def test_init_topology_argument(self, network_file: str): topology = {"Test1": [], "Test2": [], "Test3": []} nodes = list(topology.keys()) - self.network = Network(topology=topology, force=True) + self.network = Network(topology=topology, force=True, network_config_file=network_file) self._assert_nodes(self.network.nodes, nodes) self._assert_topology(self.network.topology, topology) - def test_init_node_and_topology_argument(self): + def test_init_node_and_topology_argument(self, network_file: str): nodes = ["Test5", "Test6"] topology = {"Test5": ["Test6"], "Test6": ["Test5"]} - self.network = Network(nodes=nodes, topology=topology, force=True) + self.network = Network(nodes=nodes, topology=topology, force=True, network_config_file=network_file) self._assert_nodes(self.network.nodes, nodes) self._assert_topology(self.network.topology, topology) From 06f552040b42c85ff544982a62362ba6bf1371a7 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 15 Oct 2025 15:01:00 +0200 Subject: [PATCH 074/277] NetworkConfigBuilder does not eagerly load nor save values unnecessarily --- simulaqron/general/host_config.py | 6 +- simulaqron/netqasm_backend/factory.py | 5 +- simulaqron/network.py | 17 +-- simulaqron/settings.py | 10 ++ simulaqron/simulaqron.py | 100 ++++++++++++---- simulaqron/toolbox/manage_nodes.py | 113 +++++++----------- tests/quick/general/test_classical_sockets.py | 4 +- tests/quick/general/test_host_config.py | 7 +- tests/quick/netqasm/test_free_qubit.py | 4 +- tests/quick/netqasm/test_get_qubit_state.py | 4 +- tests/quick/network/test_network.py | 11 +- tests/slow/sdk/test_two_qubit.py | 1 - 12 files changed, 161 insertions(+), 121 deletions(-) diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index 4fa23d85..2a697b13 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -34,7 +34,7 @@ from twisted.spread import pb from ipaddress import IPv4Address -from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder class Host(pb.Referenceable): @@ -91,7 +91,9 @@ def read_config(self, filename: str, network_name: str = "default", config_type: raise ValueError("Type needs to be either 'vnode', 'qnodeos' or 'app'") if network_name is None: network_name = "default" - network_config = NetworksConfigConstructor(file_path=filename).networks[network_name] + network_builder = NetworkConfigBuilder() + network_builder.read_from_file(filename) + network_config = network_builder.networks[network_name] nodes = network_config.nodes for node_name, node_config in nodes.items(): hostname = getattr(node_config, f"{config_type}_hostname") diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 67edde51..86fc7ebd 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -40,7 +40,7 @@ from simulaqron.netqasm_backend.qnodeos import SubroutineHandler from simulaqron.sdk.connection import RichErrorMessage from simulaqron.settings import simulaqron_settings -from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder from simulaqron.virtual_node.virtual import call_method @@ -202,7 +202,8 @@ def __init__( # topology self.topology = None if simulaqron_settings.network_config_file is not None: - networks_config = NetworksConfigConstructor(simulaqron_settings.network_config_file) + networks_config = NetworkConfigBuilder() + networks_config.read_from_file(simulaqron_settings.network_config_file) self.topology = networks_config.networks[network_name].topology def stop(self): diff --git a/simulaqron/network.py b/simulaqron/network.py index 4885ff83..b7f31712 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -39,7 +39,7 @@ from netqasm.logging.glob import get_netqasm_logger, get_log_level -from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder from simulaqron.settings import simulaqron_settings from simulaqron.start import start_vnode, start_qnodeos from simulaqron.sdk import SimulaQronConnection @@ -78,11 +78,12 @@ def __init__(self, name: str = "default", nodes: Optional[List[str]] = None, self._logger = get_netqasm_logger(f"{self.__class__.__name__}({self.name})") if network_config_file is None: - self._network_config_file = simulaqron_settings.network_config_file + network_config_file = simulaqron_settings.network_config_file else: - self._network_config_file = network_config_file + network_config_file = network_config_file - networks_config = NetworksConfigConstructor(file_path=self._network_config_file) + networks_config = NetworkConfigBuilder() + networks_config.read_from_file(network_config_file) if new: if nodes is None: @@ -95,12 +96,12 @@ def __init__(self, name: str = "default", nodes: Optional[List[str]] = None, self.topology = construct_topology_config(topology, self.nodes) if not force: answer = input(f"Do you want to add/replace the network {self.name} in the " - f"file {self._network_config_file} with a network consisting " + f"file {network_config_file} with a network consisting " f"of the nodes {self.nodes}? (yes/no)") if answer.lower() not in ["yes", "y"]: raise RuntimeError("User did not want to replace network in file") networks_config.add_network(node_names=self.nodes, network_name=self.name, topology=self.topology) - networks_config.write_to_file(self._network_config_file) + networks_config.write_to_file(network_config_file) else: if topology is not None: raise ValueError("If new is False a topology cannot be used.") @@ -108,7 +109,7 @@ def __init__(self, name: str = "default", nodes: Optional[List[str]] = None, node_names = networks_config.get_node_names(self.name) self.topology = networks_config.networks[self.name].topology else: - raise ValueError(f"Network {self.name} is not in the file {self._network_config_file}\n" + raise ValueError(f"Network {self.name} is not in the file {network_config_file}\n" f"If you wish to add this network to the file, use the --new flag.") if nodes is None: self.nodes = node_names @@ -117,7 +118,7 @@ def __init__(self, name: str = "default", nodes: Optional[List[str]] = None, for node_name in self.nodes: if node_name not in node_names: raise ValueError(f"Node {node_name} is not in the current network {self.name} " - f"in the file {self._network_config_file}\nIf you wish to overwrite " + f"in the file {network_config_file}\nIf you wish to overwrite " f"the current network in the file, use the --new flag.") self._setup_processes() diff --git a/simulaqron/settings.py b/simulaqron/settings.py index 143d942c..85dac5b0 100644 --- a/simulaqron/settings.py +++ b/simulaqron/settings.py @@ -116,6 +116,16 @@ def load_from_file(self, path: PathLike): else: raise FileNotFoundError(f"File {file_path} does not exist or is not a file") + def save_to_file(self, path: PathLike): + file_path = Path(str(path)).resolve() + with open(file_path, 'w') as file: + json.dump(self._config, file) + + def remove_local_config(self): + file_path = (Path.cwd() / SIMULAQRON_SETTINGS_FILENAME).resolve() + if file_path.exists(): + file_path.unlink() + def _get_setting(self, setting: str) -> Any: try: value = self._config[setting] diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 9e7e5f73..e0f2f797 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 import time +from typing import Optional, Callable + import click import logging from daemons.prefab import run @@ -8,8 +10,8 @@ import importlib.metadata as metadata from simulaqron.network import Network -from simulaqron.settings import simulaqron_settings, SimBackend -from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor +from simulaqron.settings import simulaqron_settings, SimBackend, SIMULAQRON_SETTINGS_FILENAME +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) # PID folder should be "LOCAL" @@ -21,9 +23,10 @@ class SimulaQronDaemon(run.RunDaemon): - def __init__(self, pidfile, name=None, nrnodes=None, nodes=None, topology=None, new=True): + def __init__(self, pidfile: Path, name: Optional[str] = None, nrnodes: Optional[int] = None, + nodes: Optional[str] = None, topology=None, new: bool = True): super().__init__(pidfile=pidfile) - self.name = name + self.name = name if name is not None else "default" self.nrnodes = nrnodes self.nodes = nodes self.topology = topology @@ -86,26 +89,26 @@ def version(): "--name", help="Give the network a name to be able to start multiple (default: default)", type=click.STRING, - default=None, + default="default", ) @click.option( "-N", "--nrnodes", - help="Number of nodes to start \n(WARNING: overwites existing config files)", + help="Number of nodes to start \n(WARNING: overwrites existing config files)", type=click.INT, default=None, ) @click.option( "-n", "--nodes", - help="Comma separated list of nodes to start \n(WARNING: overwites existing config files)", + help="Comma separated list of nodes to start \n(WARNING: overwirtes existing config files)", type=click.STRING, default=None, ) @click.option( "-t", "--topology", - help="Topology of network \n(WARNING: overwites existing config files)", + help="Topology of network \n(WARNING: overwrites existing config files)", type=click.STRING, default=None, ) @@ -126,11 +129,10 @@ def version(): is_flag=True, ) -def start(name, nrnodes, nodes, topology, force, keep): +def start(name: str, nrnodes: Optional[int], nodes: Optional[str], + topology: Optional[str], force: Optional[bool], keep: Optional[bool]): """Starts a network with the given parameters or from config files.""" new = not keep - if name is None: - name = "default" pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): logging.warning("Network with name %s is already running", name) @@ -138,6 +140,8 @@ def start(name, nrnodes, nodes, topology, force, keep): return if new: if not force: + # We will save this new network file in the current directory + simulaqron_settings.network_config_file = Path.cwd() / sim_backend.DEFAULT_NETWORK_CONFIG_FILE answer = input(f"Do you want to add/replace the network '{name}' in the file " f"{simulaqron_settings.network_config_file} with a new network? " f"(yes/no)") @@ -166,7 +170,7 @@ def start(name, nrnodes, nodes, topology, force, keep): type=click.STRING, default=None, ) -def stop(name): +def stop(name: Optional[str]): """Stops a network.""" if name is None: name = "default" @@ -189,13 +193,12 @@ def stop(name): help="Don't ask for confirmation.", is_flag=True, ) -def reset(force): +def reset(force: Optional[bool]): """Resets simulaqron""" if not force: answer = input("Are you sure you want to reset simulaqron?\nThis will revert settings and " - "network config files to the default values.\nNote, this will not edit or remove " - "the file at ~/.simulaqron.json if it exists, this you have to do manually if " - "you wish to revert all settings.\n" + "network config files to the default values.\nNote, this action will remove " + f"the file at {SIMULAQRON_SETTINGS_FILENAME} if it exists.\n" "(yes/no)") else: answer = "yes" @@ -210,6 +213,19 @@ def reset(force): else: print("Aborting!") +def updates_local_config(command_function: Callable): + def wrapper(*args, **kwargs): + local_settings = Path.cwd() / SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists(): + simulaqron_settings.load_from_file(local_settings) + else: + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) + command_function(*args, **kwargs) + simulaqron_settings.save_to_file(local_settings) + return wrapper + ############### # set command # @@ -223,12 +239,14 @@ def set(): @set.command() def default(): - """Sets all settings back to default""" + """Sets all settings back to default and saves it as a local configuration file""" simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(Path.cwd() / SIMULAQRON_SETTINGS_FILENAME) @set.command() @click.argument('value', type=click.Choice([b.value for b in SimBackend])) +@updates_local_config def sim_backend(value): """The backend to use (stabilizer, projectq, qutip).""" simulaqron_settings.sim_backend = value @@ -236,6 +254,7 @@ def sim_backend(value): @set.command() @click.argument('value', type=int) +@updates_local_config def max_qubits(value): """Max virt-qubits per node and max sim-qubits per register.""" simulaqron_settings.max_qubits = value @@ -243,6 +262,7 @@ def max_qubits(value): @set.command() @click.argument('value', type=int) +@updates_local_config def max_registers(value): """How many registers a node can hold.""" simulaqron_settings.max_registers = value @@ -250,6 +270,7 @@ def max_registers(value): @set.command() @click.argument('value', type=float) +@updates_local_config def conn_retry_time(value): """If setup fails, how long to wait until a retry.""" simulaqron_settings.conn_retry_time = value @@ -257,6 +278,7 @@ def conn_retry_time(value): @set.command() @click.argument('value', type=float) +@updates_local_config def recv_timeout(value): """When receiving a qubit or EPR pair, how long to wait until raising a timeout.""" simulaqron_settings.recv_timeout = value @@ -264,6 +286,7 @@ def recv_timeout(value): @set.command() @click.argument('value', type=float) +@updates_local_config def recv_retry_time(value): """When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received.""" simulaqron_settings.recv_retry_time = value @@ -271,6 +294,7 @@ def recv_retry_time(value): @set.command() @click.argument('value', type=int) +@updates_local_config def log_level(value): """Log level for both backend and frontend\n10=DEBUG, 20=INFO, 30=WARNING, 40=ERROR, 50=CRITICAL.""" simulaqron_settings.log_level = value @@ -278,6 +302,7 @@ def log_level(value): @set.command() @click.argument('value', type=str) +@updates_local_config def network_config_file(value): """The path to the network_config_file to be used""" simulaqron_settings.network_config_file = value @@ -285,6 +310,7 @@ def network_config_file(value): @set.command() @click.argument('value', type=click.Choice(["on", "off"])) +@updates_local_config def noisy_qubits(value): """Whether qubits should be noisy (on/off)""" if value == "on": @@ -295,6 +321,7 @@ def noisy_qubits(value): @set.command() @click.argument('value', type=float) +@updates_local_config def t1(value): """The effective T1 to be used for noisy qubits""" simulaqron_settings.t1 = value @@ -303,6 +330,16 @@ def t1(value): # get command # ############### +def loads_local_config(command_function: Callable): + def wrapper(*args, **kwargs): + local_settings = Path.cwd() / SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists(): + simulaqron_settings.load_from_file(Path.cwd() / SIMULAQRON_SETTINGS_FILENAME) + else: + simulaqron_settings.default_settings() + command_function(*args, **kwargs) + return wrapper + @cli.group() def get(): @@ -311,54 +348,63 @@ def get(): @get.command() +@loads_local_config def sim_backend(): """The backend to use (stabilizer, projectq, qutip).""" print(simulaqron_settings.sim_backend) @get.command() +@loads_local_config def max_qubits(): """Max virt-qubits per node and max sim-qubits per register.""" print(simulaqron_settings.max_qubits) @get.command() +@loads_local_config def max_registers(): """How many registers a node can hold.""" print(simulaqron_settings.max_registers) @get.command() +@loads_local_config def conn_retry_time(): """If setup fails, how long to wait until a retry.""" print(simulaqron_settings.conn_retry_time) @get.command() +@loads_local_config def recv_timeout(): """When receiving a qubit or EPR pair, how long to wait until raising a timeout.""" print(simulaqron_settings.recv_timeout) @get.command() +@loads_local_config def recv_retry_time(): """When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received.""" print(simulaqron_settings.recv_retry_time) @get.command() +@loads_local_config def log_level(): """Log level for both backend and frontend.""" print(simulaqron_settings.log_level) @get.command() +@loads_local_config def network_config_file(): """The path to the network_config_file to be used""" print(simulaqron_settings.network_config_file) @get.command() +@loads_local_config def noisy_qubits(): """Whether qubits should be noisy (on/off)""" if simulaqron_settings.noisy_qubits: @@ -368,6 +414,7 @@ def noisy_qubits(): @get.command() +@loads_local_config def t1(): """The effective T1 to be used for noisy qubits""" print(simulaqron_settings.t1) @@ -415,7 +462,7 @@ def nodes(): help="Force re-write of network_config_file.\n", is_flag=True, ) -def add(name, network_name=None, hostname=None, app_port=None, qnodeos_port=None, vnode_port=None, neighbors=None, +def add(name: Optional[str], network_name: Optional[str], hostname=None, app_port=None, qnodeos_port=None, vnode_port=None, neighbors=None, force=False): """ Add a node to the network. @@ -434,12 +481,13 @@ def add(name, network_name=None, hostname=None, app_port=None, qnodeos_port=None if neighbors is not None: neighbors = neighbors.split(',') neighbors = [neighbor.strip() for neighbor in neighbors] - networks_config = NetworksConfigConstructor(simulaqron_settings.network_config_file) + networks_config = NetworkConfigBuilder() + networks_config.read_from_file(simulaqron_settings.network_config_file) networks_config.add_node(node_name=name, network_name=network_name, app_hostname=hostname, qnodeos_hostname=hostname, vnode_hostname=hostname, app_port=app_port, qnodeos_port=qnodeos_port, vnode_port=vnode_port, neighbors=neighbors) - networks_config.write_to_file() + networks_config.write_to_file(simulaqron_settings.network_config_file) @nodes.command() @@ -465,9 +513,10 @@ def remove(name, network_name=None, force=False): if not _is_positive_answer(answer): print("Aborting!") return - networks_config = NetworksConfigConstructor(simulaqron_settings.network_config_file) + networks_config = NetworkConfigBuilder() + networks_config.read_from_file(simulaqron_settings.network_config_file) networks_config.remove_node(node_name=name, network_name=network_name) - networks_config.write_to_file() + networks_config.write_to_file(simulaqron_settings.network_config_file) @nodes.command() @@ -493,10 +542,10 @@ def default(network_name=None, force=False): if not _is_positive_answer(answer): print("Aborting!") return - networks_config = NetworksConfigConstructor(simulaqron_settings.network_config_file) + networks_config = NetworkConfigBuilder() node_names = ["Alice", "Bob", "Charlie", "David", "Eve"] networks_config.add_network(node_names=node_names, network_name=network_name) - networks_config.write_to_file() + networks_config.write_to_file(simulaqron_settings.network_config_file) @nodes.command() @@ -504,7 +553,8 @@ def default(network_name=None, force=False): help="The name of the network") def get(network_name=None): """Get the current nodes of the network.""" - networks_config = NetworksConfigConstructor(simulaqron_settings.network_config_file) + networks_config = NetworkConfigBuilder() + networks_config.read_from_file(simulaqron_settings.network_config_file) try: nodes = networks_config.get_node_names(network_name=network_name) except ValueError: diff --git a/simulaqron/toolbox/manage_nodes.py b/simulaqron/toolbox/manage_nodes.py index 63879927..03b69160 100644 --- a/simulaqron/toolbox/manage_nodes.py +++ b/simulaqron/toolbox/manage_nodes.py @@ -4,35 +4,30 @@ from importlib import resources from os import PathLike from pathlib import Path -from typing import Optional, Self +from typing import Optional, Self, Dict, List, Tuple, Any import simulaqron._default_config -class NetworksConfigConstructor: - def __init__(self, file_path: Optional[PathLike | str]): +class NetworkConfigBuilder: + def __init__(self): """ - Used to construct the config file of networks.abs - When all nodes and networks are added the content of this object can - be written to a file by calling the method 'write_to_file'. - - :param file_path: None or str - Path to the network config_file. If None an empty networkconfig constructor is initialized. - Otherwise the content of the file is loaded. + Used to construct the config file of networks. """ - self.networks = {} - self.used_sockets = [] - self.file_path = None if file_path is None else Path(str(file_path)) - if self.file_path is not None and self.file_path.exists(): - self.read_from_file(self.file_path) + self.networks: Dict[str, _NetworkConfig] = {} + self.used_sockets: List[Tuple[str, int]] = [] @classmethod - def default_network_constructor(cls) -> Self: + def using_default_network(cls) -> Self: default_network_path = resources.files(simulaqron._default_config).joinpath("default_network.json") - return cls(Path(str(default_network_path))) + new_builder = cls() + new_builder.read_from_file(Path(str(default_network_path))) + return new_builder - def add_node(self, node_name, network_name="default", app_hostname=None, qnodeos_hostname=None, vnode_hostname=None, - app_port=None, qnodeos_port=None, vnode_port=None, neighbors=None): + def add_node(self, node_name: str , network_name: str ="default", app_hostname: Optional[str] = None, + qnodeos_hostname: Optional[str] = None, vnode_hostname: Optional[str] = None, + app_port: Optional[int] = None, qnodeos_port: Optional[int] = None, + vnode_port: Optional[int] = None, neighbors: List[str] = None): """ Adds a node with the given name to a network (default: "default"). If hostnames are None they will default to 'localhost'. @@ -60,8 +55,6 @@ def add_node(self, node_name, network_name="default", app_hostname=None, qnodeos If None all current nodes in the network will be adjacent to the added node. :return: None """ - if network_name is None: - network_name = "default" socket_addresses = [(app_hostname, app_port), (qnodeos_hostname, qnodeos_port), (vnode_hostname, vnode_port)] for i, socket_address in enumerate(socket_addresses): hostname, port = socket_address @@ -99,7 +92,7 @@ def add_node(self, node_name, network_name="default", app_hostname=None, qnodeos vnode_port=vnode_port, neighbors=neighbors) self.networks[network_name] = network - def remove_node(self, node_name, network_name="default"): + def remove_node(self, node_name: str, network_name: str = "default"): """ Removes a node from the network. @@ -108,8 +101,6 @@ def remove_node(self, node_name, network_name="default"): :param network_name: str Name of the network (default: "default") """ - if network_name is None: - network_name = "default" if network_name in self.networks: nodes = self.networks[network_name].nodes nodes.pop(node_name, None) @@ -127,7 +118,8 @@ def reset(self): node_names = ["Alice", "Bob", "Charlie", "David", "Eve"] self.add_network(node_names=node_names) - def add_network(self, node_names, network_name="default", topology=None): + def add_network(self, node_names: List[str], network_name: str = "default", + topology: Optional[Dict[str, List[str]]] = None): """ Adds a new network to the config, with some specified nodes. @@ -138,8 +130,6 @@ def add_network(self, node_names, network_name="default", topology=None): :param topology: None or dict The topology of the network (optional) (default is fully connected) """ - if network_name is None: - network_name = "default" self.remove_network(network_name=network_name) for node_name in node_names: if topology is not None: @@ -148,18 +138,16 @@ def add_network(self, node_names, network_name="default", topology=None): neighbors = None self.add_node(node_name, network_name=network_name, neighbors=neighbors) - def remove_network(self, network_name="default"): + def remove_network(self, network_name: str = "default"): """ Removes a network from the config. :param network_name: str Name of the network (default: "default") """ - if network_name is None: - network_name = "default" self.networks.pop(network_name, None) - def get_nodes(self, network_name="default"): + def get_nodes(self, network_name: str = "default") -> List[Any]: """ Returns the node-config objects (_NodeConfig) in a network. @@ -167,15 +155,13 @@ def get_nodes(self, network_name="default"): Name of the network (default: "default") :return: list of _NodeConfig """ - if network_name is None: - network_name = "default" if network_name in self.networks: nodes = self.networks[network_name].nodes return list(nodes.values()) else: raise ValueError(f"{network_name} is not a network in this config") - def get_node_names(self, network_name="default"): + def get_node_names(self, network_name: str = "default"): """ Returns the names of the nodes in a network. @@ -183,36 +169,32 @@ def get_node_names(self, network_name="default"): Name of the network (default: "default") :return: list of str """ - if network_name is None: - network_name = "default" if network_name in self.networks: nodes = self.networks[network_name].nodes return list(nodes.keys()) else: raise ValueError(f"{network_name} is not a network in this config") - def to_dict(self): + def to_dict(self) -> Dict[str, Dict[str, Any]]: """ Constructs a dictionary with all the content that can be written to a json file :return: dict """ return {network_name: network.to_dict() for network_name, network in self.networks.items()} - def write_to_file(self, file_path=None): + def write_to_file(self, file_path: PathLike | str): """ Writes the content of this config to a file. - :param file_path: None or str - If a file_path was specified upon __init__ this will be used if file_path is None. + :param file_path: str + The path of the file to write the content to. """ - if file_path is None: - file_path = self.file_path if file_path is None: raise ValueError("Since this networks config was not initialized with a file_path you need to specify one") - dict = self.to_dict() + dictionary = self.to_dict() with open(file_path, 'w') as f: - json.dump(dict, f, indent=4) + json.dump(dictionary, f, indent=4) def read_from_file(self, file_path: PathLike | str): """ @@ -222,17 +204,15 @@ def read_from_file(self, file_path: PathLike | str): If a file_path was specified upon __init__ this will be used if file_path is None. """ if file_path is None: - file_path = self.file_path - if file_path is None: - raise ValueError("Since this networks config was not initialized with a file_path you need to specify one") + raise ValueError("No path specified to read the network configuration") if Path(str(file_path)).exists(): with open(file_path, 'r') as f: - dict = json.load(f) + dictionary = json.load(f) else: raise ValueError(f"No such file {file_path}") - for network_name, network_dict in dict.items(): + for network_name, network_dict in dictionary.items(): nodes_dict = network_dict["nodes"] topology = network_dict["topology"] network = _NetworkConfig() @@ -256,7 +236,7 @@ def read_from_file(self, file_path: PathLike | str): network.nodes[node_name] = node self.networks[network_name] = network - def _get_unused_port(self, hostname): + def _get_unused_port(self, hostname: str) -> int: """ Returns an unused port in the interval 8000 to 9000, if such exists, otherwise returns None. :param hostname: str @@ -266,8 +246,9 @@ def _get_unused_port(self, hostname): for port in range(8000, 9001): if self._check_port_available(hostname, port): return port + raise RuntimeError(f"No unused port in {hostname}") - def _check_port_available(self, hostname, port): + def _check_port_available(self, hostname: str, port: int) -> bool: """ Checks if the given port is not already set in the config files or used by some other process. :param hostname: str @@ -282,7 +263,7 @@ def _check_port_available(self, hostname, port): return self._check_socket_is_free(port) @staticmethod - def _check_socket_is_free(port): + def _check_socket_is_free(port: int) -> bool: """ Checks if a given socket on localhost is in use. This is done by trying to open the port and check if it succeeds. @@ -303,19 +284,13 @@ def __init__(self): """ Used by NetworksConfigConstructor to keep track of the config of a single network. """ - self.topology = None - self.nodes = {} + self.topology: Optional[Dict[str, List[str]]] = None + self.nodes: Dict[str, _NodeConfig] = {} def add_node( - self, - name, - app_hostname, - qnodeos_hostname, - vnode_hostname, - app_port, - qnodeos_port, - vnode_port, - neighbors, + self, name: str, app_hostname: Optional[str] = None, qnodeos_hostname: Optional[str] = None, + vnode_hostname: Optional[str] = None, app_port: Optional[int] = None, qnodeos_port: Optional[int] = None, + vnode_port: Optional[int] = None, neighbors: Optional[List[str]] = None, ): """ Adds a node with the given name to a network (default: "default"). @@ -323,7 +298,7 @@ def add_node( If the port numbers None, unused ones will be chosen between 8000 and 9000. If neighbors are specified a restricted topology can be constructed (default is fully connected). - :param node_name: str + :param name: str Name of the node, e.g. Alice :param app_hostname: str or None Hostname, e.g. localhost (default) or 192.168.0.1 @@ -344,7 +319,7 @@ def add_node( """ if neighbors is not None: if self.topology is None: - # Assume that whatever nodes were there before are fully connnected + # Assume that whatever nodes were there before are fully connected self.topology = {} node_names = self.nodes.keys() for node_name in node_names: @@ -362,7 +337,7 @@ def add_node( vnode_port=vnode_port, ) - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: """ Constructs a dictionary with all the config of this network. :return: dict @@ -372,7 +347,9 @@ def to_dict(self): class _NodeConfig: - def __init__(self, name, app_hostname, qnodeos_hostname, vnode_hostname, app_port, qnodeos_port, vnode_port): + def __init__(self, name: str, app_hostname: Optional[str], qnodeos_hostname: Optional[str], + vnode_hostname: Optional[str], app_port: Optional[int], qnodeos_port: Optional[int], + vnode_port: Optional[int]): """ Used by _NetworkConfig to keep track of the config of a single node. """ @@ -384,7 +361,7 @@ def __init__(self, name, app_hostname, qnodeos_hostname, vnode_hostname, app_por self.qnodeos_port = qnodeos_port self.vnode_port = vnode_port - def to_dict(self): + def to_dict(self) -> Dict[str, List[str | int | None]]: """ Constructs a dictionary with all the config of this node. :return: dict diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py index 8c9c907e..1bcf3986 100644 --- a/tests/quick/general/test_classical_sockets.py +++ b/tests/quick/general/test_classical_sockets.py @@ -7,14 +7,14 @@ from simulaqron.run.run import reset from simulaqron.sdk.socket import Socket from simulaqron.settings import simulaqron_settings, SimBackend -from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder class TestClassicalSocket: @pytest.fixture(autouse=True) def configurations(self): with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as network_settings_file: - network_config = NetworksConfigConstructor.default_network_constructor() + network_config = NetworkConfigBuilder.using_default_network() network_config.write_to_file(network_settings_file.name) with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as simulaqron_settings_file: simulaqron_settings.default_settings() diff --git a/tests/quick/general/test_host_config.py b/tests/quick/general/test_host_config.py index 44af97e2..c47e3b49 100644 --- a/tests/quick/general/test_host_config.py +++ b/tests/quick/general/test_host_config.py @@ -1,12 +1,12 @@ from pathlib import Path from tempfile import NamedTemporaryFile -from simulaqron.general.host_config import NetworksConfigConstructor, SocketsConfig +from simulaqron.general.host_config import NetworkConfigBuilder, SocketsConfig class TestNetworkConfig: def test_read_write(self): - network_config = NetworksConfigConstructor(file_path=None) + network_config = NetworkConfigBuilder() network_config.add_node("Alice") network_config.add_node("Bob") @@ -17,7 +17,8 @@ def test_read_write(self): network_config.write_to_file(temp_file.name) temp_file.close() - network_config2 = NetworksConfigConstructor(file_path=temp_file.name) + network_config2 = NetworkConfigBuilder() + network_config2.read_from_file(temp_file.name) dct2 = network_config2.to_dict() assert dct1 == dct2 diff --git a/tests/quick/netqasm/test_free_qubit.py b/tests/quick/netqasm/test_free_qubit.py index f5f0c097..d3e000c6 100644 --- a/tests/quick/netqasm/test_free_qubit.py +++ b/tests/quick/netqasm/test_free_qubit.py @@ -6,7 +6,7 @@ from simulaqron.run.run import reset, run_applications from simulaqron.settings import simulaqron_settings, SimBackend -from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder set_simulator("simulaqron") @@ -19,7 +19,7 @@ class TestFreeQubit: @pytest.fixture(autouse=True) def configuration(self): with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as network_settings_file: - network_config = NetworksConfigConstructor.default_network_constructor() + network_config = NetworkConfigBuilder.using_default_network() network_config.write_to_file(network_settings_file.name) with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as simulaqron_settings_file: simulaqron_settings.default_settings() diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 04bce83a..ae45bc8c 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -7,7 +7,7 @@ from netqasm.sdk.classical_communication.message import StructuredMessage from simulaqron.settings import simulaqron_settings, SimBackend -from simulaqron.toolbox.manage_nodes import NetworksConfigConstructor +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder set_simulator("simulaqron") @@ -22,7 +22,7 @@ class TestGetQubit: @pytest.fixture(autouse=True) def configuration(self): with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as network_settings_file: - network_config = NetworksConfigConstructor.default_network_constructor() + network_config = NetworkConfigBuilder.using_default_network() network_config.write_to_file(network_settings_file.name) with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as simulaqron_settings_file: simulaqron_settings.default_settings() diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index b39b6cc1..7b713a1b 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -36,23 +36,22 @@ def _check_nodes_and_topology_in_file(self, network: Network): @pytest.fixture(autouse=True) def network_file(self): - with NamedTemporaryFile() as net_config_file: + with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as net_config_file: self.network = None - self.default_nodes = ["Alice", "Bob", "Charlie", "David", "Eve"] - self.default_topology = None yield net_config_file.name self._check_nodes_and_topology_in_file(self.network) def test_init_no_argument(self, network_file: str): self.network = Network(force=True, network_config_file=network_file) - self._assert_nodes(self.network.nodes, self.default_nodes) - self._assert_topology(self.network.topology, self.default_topology) + default_nodes = ["Alice", "Bob", "Charlie", "David", "Eve"] + self._assert_nodes(self.network.nodes, default_nodes) + self._assert_topology(self.network.topology, None) def test_init_node_argument(self, network_file: str): nodes = ["Test3", "Test4"] self.network = Network(nodes=nodes, force=True, network_config_file=network_file) self._assert_nodes(self.network.nodes, nodes) - self._assert_topology(self.network.topology, self.default_topology) + self._assert_topology(self.network.topology, None) def test_init_topology_argument(self, network_file: str): topology = {"Test1": [], "Test2": [], "Test3": []} diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index e116782d..b6efaf42 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -242,4 +242,3 @@ def test_teleport(self): ] ) _ = run_applications(apps, use_app_config=False, enable_logging=False, num_rounds=self.iterations) - #print(_) From 189fbb3e92eb49d10ff1009fe0bbfd7636e46d78 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 17 Oct 2025 16:52:05 +0200 Subject: [PATCH 075/277] Use temporary files in tests to avoid overwriting default configuration files --- simulaqron/.simulaqron_pids/.keep | 0 simulaqron/toolbox/manage_nodes.py | 20 ++++++++++---------- tests/quick/network/test_network.py | 25 ++++++++++++++++++++++++- tests/slow/sdk/test_single_qubit.py | 21 +++++++++++++++------ tests/slow/sdk/test_two_qubit.py | 23 ++++++++++++++++------- 5 files changed, 65 insertions(+), 24 deletions(-) delete mode 100644 simulaqron/.simulaqron_pids/.keep diff --git a/simulaqron/.simulaqron_pids/.keep b/simulaqron/.simulaqron_pids/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/simulaqron/toolbox/manage_nodes.py b/simulaqron/toolbox/manage_nodes.py index 03b69160..aa294922 100644 --- a/simulaqron/toolbox/manage_nodes.py +++ b/simulaqron/toolbox/manage_nodes.py @@ -14,7 +14,7 @@ def __init__(self): """ Used to construct the config file of networks. """ - self.networks: Dict[str, _NetworkConfig] = {} + self.networks: Dict[str, NetworkConfig] = {} self.used_sockets: List[Tuple[str, int]] = [] @classmethod @@ -86,7 +86,7 @@ def add_node(self, node_name: str , network_name: str ="default", app_hostname: neighbors=neighbors, ) else: - network = _NetworkConfig() + network = NetworkConfig() network.add_node(name=node_name, app_hostname=app_hostname, qnodeos_hostname=qnodeos_hostname, vnode_hostname=vnode_hostname, app_port=app_port, qnodeos_port=qnodeos_port, vnode_port=vnode_port, neighbors=neighbors) @@ -215,7 +215,7 @@ def read_from_file(self, file_path: PathLike | str): for network_name, network_dict in dictionary.items(): nodes_dict = network_dict["nodes"] topology = network_dict["topology"] - network = _NetworkConfig() + network = NetworkConfig() network.topology = topology for node_name, node_dict in nodes_dict.items(): @@ -230,9 +230,9 @@ def read_from_file(self, file_path: PathLike | str): for socket_address in socket_addresses: if socket_address not in self.used_sockets: self.used_sockets.append(socket_address) - node = _NodeConfig(name=node_name, app_hostname=app_hostname, qnodeos_hostname=qnodeos_hostname, - vnode_hostname=vnode_hostname, app_port=app_port, qnodeos_port=qnodeos_port, - vnode_port=vnode_port) + node = NodeConfig(name=node_name, app_hostname=app_hostname, qnodeos_hostname=qnodeos_hostname, + vnode_hostname=vnode_hostname, app_port=app_port, qnodeos_port=qnodeos_port, + vnode_port=vnode_port) network.nodes[node_name] = node self.networks[network_name] = network @@ -279,13 +279,13 @@ def _check_socket_is_free(port: int) -> bool: return True -class _NetworkConfig: +class NetworkConfig: def __init__(self): """ Used by NetworksConfigConstructor to keep track of the config of a single network. """ self.topology: Optional[Dict[str, List[str]]] = None - self.nodes: Dict[str, _NodeConfig] = {} + self.nodes: Dict[str, NodeConfig] = {} def add_node( self, name: str, app_hostname: Optional[str] = None, qnodeos_hostname: Optional[str] = None, @@ -327,7 +327,7 @@ def add_node( self.topology[name] = neighbors - self.nodes[name] = _NodeConfig( + self.nodes[name] = NodeConfig( name=name, app_hostname=app_hostname, qnodeos_hostname=qnodeos_hostname, @@ -346,7 +346,7 @@ def to_dict(self) -> Dict[str, Any]: return {"nodes": nodes, "topology": self.topology} -class _NodeConfig: +class NodeConfig: def __init__(self, name: str, app_hostname: Optional[str], qnodeos_hostname: Optional[str], vnode_hostname: Optional[str], app_port: Optional[int], qnodeos_port: Optional[int], vnode_port: Optional[int]): diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index 7b713a1b..b18b10bb 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -8,6 +8,7 @@ from simulaqron.settings import simulaqron_settings from simulaqron.network import Network +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder class TestInitNetwork: @@ -36,7 +37,15 @@ def _check_nodes_and_topology_in_file(self, network: Network): @pytest.fixture(autouse=True) def network_file(self): + simulaqron_settings.default_settings() + # We initialize a temporary file with the default network config + network_builder = NetworkConfigBuilder() + network_builder.using_default_network() with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as net_config_file: + # We also need to specify the location of the temporal file as the network config file + simulaqron_settings.network_config_file = net_config_file.name + network_builder.write_to_file(net_config_file.name) + net_config_file.close() self.network = None yield net_config_file.name self._check_nodes_and_topology_in_file(self.network) @@ -71,7 +80,21 @@ def test_init_node_and_topology_argument(self, network_file: str): class TestStartStopNetwork: nodes = ["Test1", "Test2", "Test3"] - def test_start(self): + @pytest.fixture(autouse=True) + def network_file(self): + simulaqron_settings.default_settings() + # We initialize a temporary file with the default network config + network_builder = NetworkConfigBuilder() + network_builder.using_default_network() + with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as net_config_file: + # We also need to specify the location of the temporal file as the network config file + simulaqron_settings.network_config_file = net_config_file.name + network_builder.write_to_file(net_config_file.name) + net_config_file.close() + self.network = None + yield net_config_file.name + + def test_start(self, network_file: str): network = Network(nodes=self.nodes, force=True) assert len(network.processes) == 2 * len(self.nodes) for p in network.processes: diff --git a/tests/slow/sdk/test_single_qubit.py b/tests/slow/sdk/test_single_qubit.py index c8bff650..999e04f8 100644 --- a/tests/slow/sdk/test_single_qubit.py +++ b/tests/slow/sdk/test_single_qubit.py @@ -26,6 +26,8 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from tempfile import NamedTemporaryFile + import numpy as np import pytest from scipy.linalg import expm @@ -37,6 +39,7 @@ from simulaqron.sdk.connection import SimulaQronConnection from simulaqron.run.run import reset from simulaqron.general import SimUnsupportedError +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder def calc_exp_values(q): @@ -201,12 +204,18 @@ class TestSingleQubitGate: @pytest.fixture def network(self): - network = Network(nodes=["Alice"], force=True) - network.start() - yield network - - network.stop() - reset() + simulaqron_settings.default_settings() + with NamedTemporaryFile(suffix=".json", delete_on_close=False) as net_config_file: + simulaqron_settings.network_config_file = net_config_file.name + network_builder = NetworkConfigBuilder() + network_builder.using_default_network() + network_builder.write_to_file(net_config_file.name) + network = Network(nodes=["Alice"], force=True) + network.start() + yield network + + network.stop() + reset() def test_X_Gate(self, network): with SimulaQronConnection("Alice") as conn: diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index b6efaf42..02782301 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -26,6 +26,8 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from tempfile import NamedTemporaryFile + import numpy as np import pytest from netqasm.runtime.application import default_app_instance @@ -36,6 +38,8 @@ from simulaqron.run.run import run_applications from simulaqron.network import Network from simulaqron.run.run import reset +from simulaqron.settings import simulaqron_settings +from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder def calc_exp_values(q): @@ -181,14 +185,19 @@ class TestTwoQubitGates: @pytest.fixture def network(self): - print(f"Testing two qubit gates with {self.iterations} iterations\n") - - network = Network(nodes=["Alice", "Bob"], force=True) - network.start(wait_until_running=True) - yield network + simulaqron_settings.default_settings() + with NamedTemporaryFile(suffix=".json", delete_on_close=False) as net_config_file: + simulaqron_settings.network_config_file = net_config_file.name + network_builder = NetworkConfigBuilder() + network_builder.using_default_network() + network_builder.write_to_file(net_config_file.name) + network = Network(nodes=["Alice", "Bob"], force=True) + network.start(wait_until_running=True) + yield network + + network.stop() + reset() - network.stop() - reset() def test_CNOT_control(self, network): with SimulaQronConnection("Bob") as conn: From 74695da82db8009d35b718ca0fe67c4fdfc18d56 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 17 Oct 2025 16:59:45 +0200 Subject: [PATCH 076/277] Fix import order in the two qubits test --- simulaqron/network.py | 4 +--- tests/slow/sdk/test_two_qubit.py | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/simulaqron/network.py b/simulaqron/network.py index b7f31712..751ec488 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -29,12 +29,10 @@ import time import random +import networkx as nx from os import PathLike - from multiprocess.context import ForkProcess as Process from typing import List, Optional, Dict, Any - -import networkx as nx from timeit import default_timer as timer from netqasm.logging.glob import get_netqasm_logger, get_log_level diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index 02782301..d5d7143d 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -32,11 +32,13 @@ import pytest from netqasm.runtime.application import default_app_instance +# IMPORTANT - Import Network *before* SimulaQronConnection, so the message handlers +# can initialize correctly. +from simulaqron.network import Network from simulaqron.sdk.connection import SimulaQronConnection from netqasm.sdk import Qubit, EPRSocket from simulaqron.sdk.socket import Socket from simulaqron.run.run import run_applications -from simulaqron.network import Network from simulaqron.run.run import reset from simulaqron.settings import simulaqron_settings from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder From 73f807404d2bccd9913a889d1e24ae41b831807b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 20 Oct 2025 13:21:49 +0200 Subject: [PATCH 077/277] Fix usage of the network fixture in two qubits tests --- tests/slow/sdk/test_two_qubit.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index d5d7143d..200fc66b 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -193,6 +193,7 @@ def network(self): network_builder = NetworkConfigBuilder() network_builder.using_default_network() network_builder.write_to_file(net_config_file.name) + net_config_file.close() network = Network(nodes=["Alice", "Bob"], force=True) network.start(wait_until_running=True) yield network @@ -231,7 +232,7 @@ def test_CPHASE_target(self, network): # Tests using multiple nodes - def test_EPRS(self): + def test_EPRS(self, network): apps = default_app_instance( [ ("Alice", EPR_Alice), @@ -242,7 +243,7 @@ def test_EPRS(self): # both sides MUST measure the same state assert int(results[0]["app_Alice"]) == int(results[0]["app_Bob"]) - def test_teleport(self): + def test_teleport(self, network): # To avoid stalling the simulation, the applications *need* to run # in parallel. For this reason, we use the "run_applications" method # which spawns a process for each node From 09cdc2e4158c7078f3551a53b36386853daf0f88 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 20 Oct 2025 16:05:44 +0200 Subject: [PATCH 078/277] Move simulaqron and network settings into their own package --- simulaqron/general/host_config.py | 2 +- simulaqron/netqasm_backend/factory.py | 2 +- simulaqron/network.py | 2 +- simulaqron/run/run.py | 3 +- simulaqron/settings/__init__.py | 13 ++ .../network_config.py} | 190 +++++++++--------- .../simulaqron_config.py} | 3 - simulaqron/simulaqron.py | 5 +- simulaqron/virtual_node/virtual.py | 3 +- tests/quick/general/test_classical_sockets.py | 5 +- tests/quick/netqasm/test_free_qubit.py | 5 +- tests/quick/netqasm/test_get_qubit_state.py | 5 +- tests/quick/network/test_network.py | 2 +- tests/quick/test_settings.py | 3 +- tests/slow/merges/test_merges.py | 3 +- tests/slow/sdk/test_single_qubit.py | 5 +- tests/slow/sdk/test_two_qubit.py | 2 +- 17 files changed, 136 insertions(+), 117 deletions(-) create mode 100644 simulaqron/settings/__init__.py rename simulaqron/{toolbox/manage_nodes.py => settings/network_config.py} (99%) rename simulaqron/{settings.py => settings/simulaqron_config.py} (99%) diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index 2a697b13..a310e7de 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -34,7 +34,7 @@ from twisted.spread import pb from ipaddress import IPv4Address -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworkConfigBuilder class Host(pb.Referenceable): diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 86fc7ebd..5d04daa1 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -40,7 +40,7 @@ from simulaqron.netqasm_backend.qnodeos import SubroutineHandler from simulaqron.sdk.connection import RichErrorMessage from simulaqron.settings import simulaqron_settings -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworkConfigBuilder from simulaqron.virtual_node.virtual import call_method diff --git a/simulaqron/network.py b/simulaqron/network.py index 751ec488..a29f9c6a 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -37,7 +37,7 @@ from netqasm.logging.glob import get_netqasm_logger, get_log_level -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworkConfigBuilder from simulaqron.settings import simulaqron_settings from simulaqron.start import start_vnode, start_qnodeos from simulaqron.sdk import SimulaQronConnection diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 22f72110..b112180d 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -24,7 +24,8 @@ from netqasm.util.yaml import dump_yaml from simulaqron.network import Network from simulaqron.sdk import SimulaQronConnection -from simulaqron.settings import SimBackend, simulaqron_settings +from simulaqron.settings import simulaqron_settings +from simulaqron.settings.simulaqron_config import SimBackend logger = get_netqasm_logger() diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py new file mode 100644 index 00000000..662adc2a --- /dev/null +++ b/simulaqron/settings/__init__.py @@ -0,0 +1,13 @@ +from .network_config import NetworkConfigBuilder +from .simulaqron_config import Config + + +# Centralized way to store the config. It reads the local configuration +# if exists, otherwise, it simply populates the in-memory configs object +# with the default values +simulaqron_settings = Config() + +# We follow a similar approach with the network config builder: read the +# file pointed by the simulaqron_settings (if exists) or initialize a new +# builder that contains only the default network. +network_configs = NetworkConfigBuilder() diff --git a/simulaqron/toolbox/manage_nodes.py b/simulaqron/settings/network_config.py similarity index 99% rename from simulaqron/toolbox/manage_nodes.py rename to simulaqron/settings/network_config.py index aa294922..8c5c4fe6 100644 --- a/simulaqron/toolbox/manage_nodes.py +++ b/simulaqron/settings/network_config.py @@ -9,6 +9,100 @@ import simulaqron._default_config +class NodeConfig: + def __init__(self, name: str, app_hostname: Optional[str], qnodeos_hostname: Optional[str], + vnode_hostname: Optional[str], app_port: Optional[int], qnodeos_port: Optional[int], + vnode_port: Optional[int]): + """ + Used by _NetworkConfig to keep track of the config of a single node. + """ + self.name = name + self.app_hostname = app_hostname + self.qnodeos_hostname = qnodeos_hostname + self.vnode_hostname = vnode_hostname + self.app_port = app_port + self.qnodeos_port = qnodeos_port + self.vnode_port = vnode_port + + def to_dict(self) -> Dict[str, List[str | int | None]]: + """ + Constructs a dictionary with all the config of this node. + :return: dict + """ + return { + "app_socket": [self.app_hostname, self.app_port], + "qnodeos_socket": [self.qnodeos_hostname, self.qnodeos_port], + "vnode_socket": [self.vnode_hostname, self.vnode_port] + } + + +class NetworkConfig: + def __init__(self): + """ + Used by NetworksConfigConstructor to keep track of the config of a single network. + """ + self.topology: Optional[Dict[str, List[str]]] = None + self.nodes: Dict[str, NodeConfig] = {} + + def add_node( + self, name: str, app_hostname: Optional[str] = None, qnodeos_hostname: Optional[str] = None, + vnode_hostname: Optional[str] = None, app_port: Optional[int] = None, qnodeos_port: Optional[int] = None, + vnode_port: Optional[int] = None, neighbors: Optional[List[str]] = None, + ): + """ + Adds a node with the given name to a network (default: "default"). + If hostnames are None they will default to 'localhost'. + If the port numbers None, unused ones will be chosen between 8000 and 9000. + If neighbors are specified a restricted topology can be constructed (default is fully connected). + + :param name: str + Name of the node, e.g. Alice + :param app_hostname: str or None + Hostname, e.g. localhost (default) or 192.168.0.1 + :param qnodeos_hostname: str or None + Hostname, e.g. localhost (default) or 192.168.0.1 + :param vnode_hostname: str or None + Hostname, e.g. localhost (default) or 192.168.0.1 + :param app_port: int or None + Port number for the application + :param qnodeos_port: int or None + Port number for the qnodeos server + :param vnode_port: int or None + Port number for the virtual node + :param neighbors: (list of str) or None + A list of neighbors, of this node. + If None all current nodes in the network will be adjacent to the added node. + :return: None + """ + if neighbors is not None: + if self.topology is None: + # Assume that whatever nodes were there before are fully connected + self.topology = {} + node_names = self.nodes.keys() + for node_name in node_names: + self.topology[node_name] = [neigh for neigh in node_names if not neigh == node_name] + + self.topology[name] = neighbors + + self.nodes[name] = NodeConfig( + name=name, + app_hostname=app_hostname, + qnodeos_hostname=qnodeos_hostname, + vnode_hostname=vnode_hostname, + app_port=app_port, + qnodeos_port=qnodeos_port, + vnode_port=vnode_port, + ) + + def to_dict(self) -> Dict[str, Any]: + """ + Constructs a dictionary with all the config of this network. + :return: dict + """ + nodes = {node_name: node.to_dict() for node_name, node in self.nodes.items()} + return {"nodes": nodes, "topology": self.topology} + + class NetworkConfigBuilder: def __init__(self): """ @@ -147,7 +241,7 @@ def remove_network(self, network_name: str = "default"): """ self.networks.pop(network_name, None) - def get_nodes(self, network_name: str = "default") -> List[Any]: + def get_nodes(self, network_name: str = "default") -> List[NodeConfig]: """ Returns the node-config objects (_NodeConfig) in a network. @@ -277,97 +371,3 @@ def _check_socket_is_free(port: int) -> bool: except socket.error: return False return True - - -class NetworkConfig: - def __init__(self): - """ - Used by NetworksConfigConstructor to keep track of the config of a single network. - """ - self.topology: Optional[Dict[str, List[str]]] = None - self.nodes: Dict[str, NodeConfig] = {} - - def add_node( - self, name: str, app_hostname: Optional[str] = None, qnodeos_hostname: Optional[str] = None, - vnode_hostname: Optional[str] = None, app_port: Optional[int] = None, qnodeos_port: Optional[int] = None, - vnode_port: Optional[int] = None, neighbors: Optional[List[str]] = None, - ): - """ - Adds a node with the given name to a network (default: "default"). - If hostnames are None they will default to 'localhost'. - If the port numbers None, unused ones will be chosen between 8000 and 9000. - If neighbors are specified a restricted topology can be constructed (default is fully connected). - - :param name: str - Name of the node, e.g. Alice - :param app_hostname: str or None - Hostname, e.g. localhost (default) or 192.168.0.1 - :param qnodeos_hostname: str or None - Hostname, e.g. localhost (default) or 192.168.0.1 - :param vnode_hostname: str or None - Hostname, e.g. localhost (default) or 192.168.0.1 - :param app_port: int or None - Port number for the application - :param qnodeos_port: int or None - Port number for the qnodeos server - :param vnode_port: int or None - Port number for the virtual node - :param neighbors: (list of str) or None - A list of neighbors, of this node. - If None all current nodes in the network will be adjacent to the added node. - :return: None - """ - if neighbors is not None: - if self.topology is None: - # Assume that whatever nodes were there before are fully connected - self.topology = {} - node_names = self.nodes.keys() - for node_name in node_names: - self.topology[node_name] = [neigh for neigh in node_names if not neigh == node_name] - - self.topology[name] = neighbors - - self.nodes[name] = NodeConfig( - name=name, - app_hostname=app_hostname, - qnodeos_hostname=qnodeos_hostname, - vnode_hostname=vnode_hostname, - app_port=app_port, - qnodeos_port=qnodeos_port, - vnode_port=vnode_port, - ) - - def to_dict(self) -> Dict[str, Any]: - """ - Constructs a dictionary with all the config of this network. - :return: dict - """ - nodes = {node_name: node.to_dict() for node_name, node in self.nodes.items()} - return {"nodes": nodes, "topology": self.topology} - - -class NodeConfig: - def __init__(self, name: str, app_hostname: Optional[str], qnodeos_hostname: Optional[str], - vnode_hostname: Optional[str], app_port: Optional[int], qnodeos_port: Optional[int], - vnode_port: Optional[int]): - """ - Used by _NetworkConfig to keep track of the config of a single node. - """ - self.name = name - self.app_hostname = app_hostname - self.qnodeos_hostname = qnodeos_hostname - self.vnode_hostname = vnode_hostname - self.app_port = app_port - self.qnodeos_port = qnodeos_port - self.vnode_port = vnode_port - - def to_dict(self) -> Dict[str, List[str | int | None]]: - """ - Constructs a dictionary with all the config of this node. - :return: dict - """ - return { - "app_socket": [self.app_hostname, self.app_port], - "qnodeos_socket": [self.qnodeos_hostname, self.qnodeos_port], - "vnode_socket": [self.vnode_hostname, self.vnode_port] - } diff --git a/simulaqron/settings.py b/simulaqron/settings/simulaqron_config.py similarity index 99% rename from simulaqron/settings.py rename to simulaqron/settings/simulaqron_config.py index 85dac5b0..f34ee83d 100644 --- a/simulaqron/settings.py +++ b/simulaqron/settings/simulaqron_config.py @@ -248,6 +248,3 @@ def t1(self) -> float: @Decorator.set_setting def t1(self, t1: float): pass - - -simulaqron_settings = Config() diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index e0f2f797..2934f2d1 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -10,8 +10,9 @@ import importlib.metadata as metadata from simulaqron.network import Network -from simulaqron.settings import simulaqron_settings, SimBackend, SIMULAQRON_SETTINGS_FILENAME -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings import simulaqron_settings +from simulaqron.settings.simulaqron_config import SimBackend, SIMULAQRON_SETTINGS_FILENAME +from simulaqron.settings.network_config import NetworkConfigBuilder CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) # PID folder should be "LOCAL" diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index a6267bfe..e625492b 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -47,7 +47,8 @@ # We need this import (despite unused) to reraise local errors # We then supress the flake8 errors associated with that from simulaqron.general.errors import * # noqa: F401, F403 -from simulaqron.settings import simulaqron_settings, SimBackend +from simulaqron.settings import simulaqron_settings +from simulaqron.settings.simulaqron_config import SimBackend from simulaqron.reactor import reactor diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py index 1bcf3986..a3eaa0e0 100644 --- a/tests/quick/general/test_classical_sockets.py +++ b/tests/quick/general/test_classical_sockets.py @@ -6,8 +6,9 @@ from simulaqron.run import run_applications from simulaqron.run.run import reset from simulaqron.sdk.socket import Socket -from simulaqron.settings import simulaqron_settings, SimBackend -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings import simulaqron_settings +from simulaqron.settings.simulaqron_config import SimBackend +from simulaqron.settings.network_config import NetworkConfigBuilder class TestClassicalSocket: diff --git a/tests/quick/netqasm/test_free_qubit.py b/tests/quick/netqasm/test_free_qubit.py index d3e000c6..61951db1 100644 --- a/tests/quick/netqasm/test_free_qubit.py +++ b/tests/quick/netqasm/test_free_qubit.py @@ -5,8 +5,9 @@ from netqasm.runtime.settings import set_simulator from simulaqron.run.run import reset, run_applications -from simulaqron.settings import simulaqron_settings, SimBackend -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings import simulaqron_settings +from simulaqron.settings.simulaqron_config import SimBackend +from simulaqron.settings.network_config import NetworkConfigBuilder set_simulator("simulaqron") diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index ae45bc8c..5ea010ec 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -6,8 +6,9 @@ from netqasm.runtime.settings import set_simulator from netqasm.sdk.classical_communication.message import StructuredMessage -from simulaqron.settings import simulaqron_settings, SimBackend -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings import simulaqron_settings +from simulaqron.settings.simulaqron_config import SimBackend +from simulaqron.settings.network_config import NetworkConfigBuilder set_simulator("simulaqron") diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index b18b10bb..a301571e 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -8,7 +8,7 @@ from simulaqron.settings import simulaqron_settings from simulaqron.network import Network -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworkConfigBuilder class TestInitNetwork: diff --git a/tests/quick/test_settings.py b/tests/quick/test_settings.py index 62f09371..55fdb7f8 100644 --- a/tests/quick/test_settings.py +++ b/tests/quick/test_settings.py @@ -5,7 +5,8 @@ from pathlib import Path import simulaqron._default_config -from simulaqron.settings import simulaqron_settings, SimBackend +from simulaqron.settings import simulaqron_settings +from simulaqron.settings.simulaqron_config import SimBackend class TestSettings: diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 56adb69c..7406777a 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -10,7 +10,8 @@ from simulaqron.general.host_config import SocketsConfig from simulaqron.local.setup import setup_local, assemble_qubit from simulaqron.network import Network -from simulaqron.settings import simulaqron_settings, SimBackend +from simulaqron.settings import simulaqron_settings +from simulaqron.settings.simulaqron_config import SimBackend from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.reactor import reactor diff --git a/tests/slow/sdk/test_single_qubit.py b/tests/slow/sdk/test_single_qubit.py index 999e04f8..09399b34 100644 --- a/tests/slow/sdk/test_single_qubit.py +++ b/tests/slow/sdk/test_single_qubit.py @@ -34,12 +34,13 @@ from netqasm.sdk.qubit import Qubit -from simulaqron.settings import simulaqron_settings, SimBackend +from simulaqron.settings import simulaqron_settings +from simulaqron.settings.simulaqron_config import SimBackend from simulaqron.network import Network from simulaqron.sdk.connection import SimulaQronConnection from simulaqron.run.run import reset from simulaqron.general import SimUnsupportedError -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworkConfigBuilder def calc_exp_values(q): diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index 200fc66b..039bd8de 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -41,7 +41,7 @@ from simulaqron.run.run import run_applications from simulaqron.run.run import reset from simulaqron.settings import simulaqron_settings -from simulaqron.toolbox.manage_nodes import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworkConfigBuilder def calc_exp_values(q): From 83a384d1d76f5b653ff1a50961097822280e4244 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 23 Oct 2025 16:30:54 +0200 Subject: [PATCH 079/277] Heavily simplify SimulaqronSettings class to use dataclass serialization --- examples/WARNING | 1 + pyproject.toml | 1 + .../_default_config/default_settings.json | 13 - simulaqron/run/run.py | 4 +- simulaqron/sdk/connection.py | 2 +- simulaqron/sdk/socket.py | 2 +- simulaqron/settings/__init__.py | 47 +++- simulaqron/settings/network_config.py | 1 + simulaqron/settings/simulaqron_config.py | 254 +++++------------- simulaqron/start/start_qnodeos.py | 2 +- simulaqron/start/start_vnode.py | 2 +- tests/quick/test_settings.py | 37 ++- tests/slow/merges/test_merges.py | 13 +- 13 files changed, 140 insertions(+), 239 deletions(-) create mode 100644 examples/WARNING delete mode 100644 simulaqron/_default_config/default_settings.json diff --git a/examples/WARNING b/examples/WARNING new file mode 100644 index 00000000..5d0aa7c7 --- /dev/null +++ b/examples/WARNING @@ -0,0 +1 @@ +These examples are out of date; they use data structures that were modernized! \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 879eccd8..dfe6408f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ local_scheme = "no-local-version" # exclude "+" from version name so we ca name = "simulaqron" dynamic = ["version"] dependencies = [ + "dataclasses-serialization>=1.3.1, <2.0", "numpy>=1.14.0,<2.0.0", "dill>=0.4.0,<1.0.0", "scipy>=1.1.0,<2.0.0", diff --git a/simulaqron/_default_config/default_settings.json b/simulaqron/_default_config/default_settings.json deleted file mode 100644 index 4a1e9760..00000000 --- a/simulaqron/_default_config/default_settings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "_read_user": true, - "max_qubits": 20, - "max_registers": 1000, - "conn_retry_time": 0.5, - "recv_timeout": 100, - "recv_retry_time": 0.1, - "log_level": 30, - "sim_backend": "stabilizer", - "network_config_file": "default_network.json", - "noisy_qubits": false, - "t1": 1.0 -} \ No newline at end of file diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index b112180d..f348ac63 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -185,10 +185,10 @@ def run_applications( results: List[Dict[str, Any]] = [] if isinstance(network_cfg, str) or isinstance(network_cfg, PathLike): net_cfg = str(network_cfg) - simulaqron_settings.network_config_file = net_cfg + simulaqron_settings.network_config_file = Path(net_cfg).resolve() elif isinstance(network_cfg, Path): net_cfg = str(network_cfg.resolve()) - simulaqron_settings.network_config_file = net_cfg + simulaqron_settings.network_config_file = Path(net_cfg).resolve() else: net_cfg = None diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index cd50baa5..8f842c77 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -538,7 +538,7 @@ def get_imag_part(self) -> List[List[float]]: def _get_qnodeos_net_config(network_name: str) -> SocketsConfig: network_config_file = simulaqron_settings.network_config_file return SocketsConfig( - network_config_file, network_name=network_name, config_type="qnodeos" + str(network_config_file), network_name=network_name, config_type="qnodeos" ) diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index 23179c5b..6c76f74c 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -178,5 +178,5 @@ def _get_addr_info(self, name): def _get_app_net_config(self) -> SocketsConfig: network_config_file = simulaqron_settings.network_config_file - app_net = SocketsConfig(network_config_file, network_name=self._network_name, config_type="app") + app_net = SocketsConfig(str(network_config_file), network_name=self._network_name, config_type="app") return app_net diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index 662adc2a..b5385b23 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -1,13 +1,54 @@ +from enum import Enum +from pathlib import Path +from typing import Type + +from dataclasses_serialization.json import JSONSerializer +from dataclasses_serialization.serializer_base import DeserializationError + from .network_config import NetworkConfigBuilder -from .simulaqron_config import Config +from .simulaqron_config import SimulaqronConfig, SIMULAQRON_SETTINGS_FILENAME + + +# Registration of Serializer for python enums +@JSONSerializer.register_serializer(Enum) +def enum_serializer(obj: Enum) -> str: + return obj.name + + +# Registration of Deserializer for python enums +@JSONSerializer.register_deserializer(Enum) +def enum_deserializer(cls: Type[Enum], name: str) -> Enum: + try: + fixed_name = name.replace("-", "_").upper() + return cls[fixed_name] + except KeyError as ex: + raise DeserializationError( + f"String '{name}' could not be deserialized to a valid " + f"value of type '{cls.__name__}'." + ) from ex + + +# Registration of Serializer for python enums +@JSONSerializer.register_serializer(Path) +def path_serializer(obj: Path) -> str: + return str(obj.resolve()) + + +# Registration of Deserializer for python enums +@JSONSerializer.register_deserializer(Path) +def path_deserializer(cls: Type[Path], path: str) -> Path: + if path == "$DEFAULT_NETWORK": + return (cls.home() / ".simulaqron" / SIMULAQRON_SETTINGS_FILENAME).resolve() + return cls(path).resolve() # Centralized way to store the config. It reads the local configuration # if exists, otherwise, it simply populates the in-memory configs object # with the default values -simulaqron_settings = Config() +simulaqron_settings = SimulaqronConfig.load_from_known_sources() # We follow a similar approach with the network config builder: read the # file pointed by the simulaqron_settings (if exists) or initialize a new # builder that contains only the default network. -network_configs = NetworkConfigBuilder() +# network_configs = NetworkConfigBuilder() +# network_configs.read_from_file(simulaqron_settings.network_config_file) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 8c5c4fe6..e9dfdda6 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -1,4 +1,5 @@ import json +import socket from contextlib import closing import socket from importlib import resources diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index f34ee83d..f0a323f2 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -26,18 +26,20 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - ######################### # SETTINGS FOR SIMULAQRON ######################### import json -from enum import StrEnum -from importlib import resources +import logging +from dataclasses import dataclass, fields +from enum import Enum + +from dataclasses_serialization.json import JSONSerializer from os import PathLike from pathlib import Path -from typing import Dict, Any +from typing import Self -import simulaqron._default_config +from dataclasses_serialization.json import JSONSerializerMixin # This is the name of the "local" simulaqron settings. # If a file named like this is found in the CWD, it will be @@ -45,7 +47,7 @@ SIMULAQRON_SETTINGS_FILENAME = "simulaqron_settings.json" -class SimBackend(StrEnum): +class SimBackend(JSONSerializerMixin, Enum): STABILIZER = "stabilizer" PROJECTQ = "projectq" QUTIP = "qutip" @@ -57,194 +59,68 @@ def __repr__(self): return str(self) -class Config: - # Dictionary for settings - _config: Dict[str, Any] = {} - - class Decorator: - @classmethod - def get_setting(cls, method): - def updated_func(self): - return self._get_setting(method.__name__) - return updated_func - - @classmethod - def set_setting(cls, method): - def updated_func(self, value): - return self._set_setting(method.__name__, value) - return updated_func - - def __init__(self): - self._loaded_file = "" # Will be correctly setup when loading the config - - # We populate the object with the configuration - local_settings_file = (Path.cwd() / SIMULAQRON_SETTINGS_FILENAME).resolve() - if local_settings_file.exists() and local_settings_file.is_file(): - self._loaded_file = str(local_settings_file) - self.load_from_file(local_settings_file) +@dataclass +class SimulaqronConfig(JSONSerializerMixin): + # Default config + max_qubits: int = 20 + max_registers: int = 1000 + conn_retry_time: float = 0.5 + conn_max_retries: int = 10 + recv_timeout: int = 100 + recv_retry_time: float = 0.1 + recv_max_retries: int = 10 + log_level: int = logging.WARNING + sim_backend: SimBackend = SimBackend.STABILIZER + network_config_file: Path = (Path.home() / ".simulaqron" / "default_network.json").resolve() + noisy_qubits: bool = False + t1: float = 1.0 + + @classmethod + def _create_home_settings_folder(cls): + home_setting_folder = (Path.home() / ".simulaqron").resolve() + home_setting_folder.mkdir(parents=True, exist_ok=True) + + def load_from_file(self, file_path: Path | str): + if isinstance(file_path, str): + file_path = Path(file_path).resolve() + new_config = self._deserialize_from_file(file_path) + cls_fields = fields(self.__class__) + + for field in cls_fields: + new_val = getattr(new_config, field.name) + setattr(self, field.name, new_val) + + @classmethod + def _deserialize_from_file(cls, file_path: Path) -> Self: + with file_path.resolve().open("rt") as file: + config_content = json.load(file) + return JSONSerializer.deserialize(cls, config_content) + + @classmethod + def load_from_known_sources(cls) -> Self: + cwd_settings_file = (Path.cwd() / SIMULAQRON_SETTINGS_FILENAME).resolve() + home_settings_file = (Path.home() / ".simulaqron" / SIMULAQRON_SETTINGS_FILENAME).resolve() + if cwd_settings_file.exists() and cwd_settings_file.is_file(): + return cls._deserialize_from_file(cwd_settings_file) else: - self.default_settings() - - def update_settings(self, config: Dict[str, Any]): - # Update the config with the given data - if "network_config_file" in config: - # We need to resolve the path of the network config file - given_network_config = Path(config["network_config_file"]).resolve() - if given_network_config.exists() and given_network_config.is_file(): - config["network_config_file"] = str(given_network_config) + cls._create_home_settings_folder() + if home_settings_file.exists() and cwd_settings_file.is_file(): + return cls._deserialize_from_file(home_settings_file) else: - # If it doesn't exist, we load the default - resource = resources.files(simulaqron._default_config).joinpath("default_network.json") - network_path = Path(str(resource)).resolve() - assert network_path.exists() and network_path.is_file() - config["network_config_file"] = str(network_path) - config["sim_backend"] = SimBackend[config["sim_backend"].upper()] - self._config.update(config) + new_default_config = cls() + new_default_config.save_to_file(home_settings_file) + return new_default_config def default_settings(self): - default_settings_path = resources.files(simulaqron._default_config).joinpath("default_settings.json") - self._loaded_file = str(default_settings_path) - self.load_from_file(Path(self._loaded_file)) + default_config = SimulaqronConfig() + cls_fields = fields(self.__class__) - def load_from_file(self, path: PathLike): - file_path = Path(str(path)).resolve() - if file_path.exists() and file_path.is_file(): - self._loaded_file = str(file_path) - with open(file_path, 'r') as file: - config = json.load(file) - self.update_settings(config) - else: - raise FileNotFoundError(f"File {file_path} does not exist or is not a file") + for field in cls_fields: + new_val = getattr(default_config, field.name) + setattr(self, field.name, new_val) def save_to_file(self, path: PathLike): file_path = Path(str(path)).resolve() - with open(file_path, 'w') as file: - json.dump(self._config, file) - - def remove_local_config(self): - file_path = (Path.cwd() / SIMULAQRON_SETTINGS_FILENAME).resolve() - if file_path.exists(): - file_path.unlink() - - def _get_setting(self, setting: str) -> Any: - try: - value = self._config[setting] - except KeyError: - raise KeyError(f"Cannot find the setting {setting} in the file {self._loaded_file}") - return value - - def _set_setting(self, setting: str, value: Any): - self._config[setting] = value - - # Below are the settings, note that _get_setting and _set_setting are automatically - # called when a setting is set or get. - - @property - @Decorator.get_setting - def _read_user(self) -> bool: - pass - - @_read_user.setter - @Decorator.set_setting - def _read_user(self, _read_user: bool): - pass - - @property - @Decorator.get_setting - def sim_backend(self) -> SimBackend: - pass - - @sim_backend.setter - @Decorator.set_setting - def sim_backend(self, sim_backend: SimBackend): - pass - - @property - @Decorator.get_setting - def max_qubits(self) -> int: - pass - - @max_qubits.setter - @Decorator.set_setting - def max_qubits(self, max_qubits: int): - pass - - @property - @Decorator.get_setting - def max_registers(self) -> int: - pass - - @max_registers.setter - @Decorator.set_setting - def max_registers(self, max_registers: int): - pass - - @property - @Decorator.get_setting - def conn_retry_time(self: float): - pass - - @conn_retry_time.setter - @Decorator.set_setting - def conn_retry_time(self, conn_retry_time: float): - pass - - @property - @Decorator.get_setting - def recv_timeout(self) -> int: - pass - - @recv_timeout.setter - @Decorator.set_setting - def recv_timeout(self, recv_timeout: int): - pass - - @property - @Decorator.get_setting - def recv_retry_time(self) -> float: - pass - - @recv_retry_time.setter - @Decorator.set_setting - def recv_retry_time(self, recv_retry_time: float): - pass - - @property - @Decorator.get_setting - def log_level(self) -> int: - pass - - @log_level.setter - @Decorator.set_setting - def log_level(self, log_level: int): - pass - - @property - @Decorator.get_setting - def network_config_file(self) -> str: - pass - - @network_config_file.setter - @Decorator.set_setting - def network_config_file(self, app_file: str): - pass - - @property - @Decorator.get_setting - def noisy_qubits(self) -> bool: - pass - - @noisy_qubits.setter - @Decorator.set_setting - def noisy_qubits(self, noisy_qubits_: bool): - pass - - @property - @Decorator.get_setting - def t1(self) -> float: - pass - - @t1.setter - @Decorator.set_setting - def t1(self, t1: float): - pass + with file_path.open("wt") as file: + serialized = JSONSerializer.serialize(self) + json.dump(serialized, file) diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 4811f5a3..d4209012 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -118,7 +118,7 @@ def main(node_name: str, network_name="default", log_level="WARNING"): signal.signal(signal.SIGINT, sigterm_handler) # Since version 3.0.0 a single config file is used - network_config_file = simulaqron_settings.network_config_file + network_config_file = str(simulaqron_settings.network_config_file) # Read configuration files for the virtual quantum, as well as the classical network virtual_network = SocketsConfig(network_config_file, network_name=network_name, config_type="vnode") diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 032ad41e..4fe95c72 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -53,7 +53,7 @@ def main(name: str, network_name: str = "default", log_level: str = "WARNING"): logger.debug("Starting VIRTUAL NODE %s", name) if simulaqron_settings.network_config_file is not None: - virtual_file = simulaqron_settings.network_config_file + virtual_file = str(simulaqron_settings.network_config_file) else: virtual_file = simulaqron_settings.vnode_file be = Backend(name, virtual_file, network_name=network_name) diff --git a/tests/quick/test_settings.py b/tests/quick/test_settings.py index 55fdb7f8..35e5bc50 100644 --- a/tests/quick/test_settings.py +++ b/tests/quick/test_settings.py @@ -1,40 +1,37 @@ import tempfile import json import pytest -from importlib import resources +from dataclasses_serialization.json import JSONSerializer from pathlib import Path -import simulaqron._default_config from simulaqron.settings import simulaqron_settings -from simulaqron.settings.simulaqron_config import SimBackend +from simulaqron.settings.simulaqron_config import SimulaqronConfig class TestSettings: def test_default_settings(self): __expected_default_settings = """ { - "_read_user": true, "max_qubits": 20, "max_registers": 1000, "conn_retry_time": 0.5, + "conn_max_retries": 10, "recv_timeout": 100, "recv_retry_time": 0.1, + "recv_max_retries": 10, "log_level": 30, "sim_backend": "stabilizer", - "network_config_file": "CHANGE_ME", + "network_config_file": "HOME_SETTINGS_PATH", "noisy_qubits": false, "t1": 1.0 } """ - expected_settings = json.loads(__expected_default_settings) - # For testing purposes, we need to "adjust" some of teh expected values: - expected_settings["sim_backend"] = SimBackend[expected_settings["sim_backend"].upper()] - path = resources.files(simulaqron._default_config).joinpath("default_network.json") - expected_settings["network_config_file"] = str(path) + expected_settings_dict = json.loads(__expected_default_settings) + path = (Path.home() / ".simulaqron" / "default_network.json").resolve() + expected_settings_dict["network_config_file"] = str(path) + expected_settings = JSONSerializer.deserialize(SimulaqronConfig, expected_settings_dict) - simulaqron_settings.default_settings() - for key, value in expected_settings.items(): - assert getattr(simulaqron_settings, key) == value + assert simulaqron_settings == expected_settings def test_non_existent_network_config(self): _original_settings = """ @@ -62,16 +59,13 @@ def test_non_existent_network_config(self): "recv_retry_time": 0.05, "log_level": 30, "sim_backend": "projectq", - "network_config_file": "CHANGE_ME", + "network_config_file": "/not/existing/network.json", "noisy_qubits": false, "t1": 2.0 } """ - expected_settings = json.loads(_expected_settings) - # For testing purposes, we need to "adjust" some of teh expected values: - expected_settings["sim_backend"] =SimBackend[expected_settings["sim_backend"].upper()] - path = resources.files("simulaqron._default_config").joinpath("default_network.json") - expected_settings["network_config_file"] = str(path) + expected_settings_dict = json.loads(_expected_settings) + expected_settings = JSONSerializer.deserialize(SimulaqronConfig, expected_settings_dict) _original_settings = json.loads(_original_settings) with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", delete_on_close=False) as file: @@ -79,10 +73,9 @@ def test_non_existent_network_config(self): file.close() simulaqron_settings.load_from_file(Path(file.name)) - for key, value in expected_settings.items(): - assert getattr(simulaqron_settings, key) == value + assert simulaqron_settings == expected_settings def test_load_non_existent_config_file(self): with pytest.raises(FileNotFoundError) as error: simulaqron_settings.load_from_file("/non/existent/file") - assert "File /non/existent/file does not exist" in str(error.value) + assert "No such file or directory: '/non/existent/file'" in str(error.value) diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 7406777a..34f41365 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -1,12 +1,14 @@ import os import logging import unittest + import numpy as np -import multiprocessing as mp from twisted.spread import pb from twisted.internet.defer import inlineCallbacks +from multiprocess.context import ForkProcess as Process +from multiprocess.connection import Pipe from simulaqron.general.host_config import SocketsConfig from simulaqron.local.setup import setup_local, assemble_qubit from simulaqron.network import Network @@ -190,13 +192,12 @@ def setup_node(name, node_code, classical_net_file, send_end): setup_local(name, virtualNet, classicalNet, lNode, node_code, send_end) def run_test(self, classical_net_file): - mp.set_start_method("fork", force=True) pipe_list = [] for name, node_code in zip(self.nodes, self.node_codes): - recv_end, send_end = mp.Pipe(False) - p = mp.Process(target=self.setup_node, - args=[name, node_code, classical_net_file, send_end], - name=name) + recv_end, send_end = Pipe(False) + p = Process(target=self.setup_node, + args=[name, node_code, classical_net_file, send_end], + name=name) self.processes.append(p) pipe_list.append(recv_end) From 59a8df624b057c2b6ee0fbc562b2aff02f671daa Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 23 Oct 2025 16:44:11 +0200 Subject: [PATCH 080/277] Move serialization methods to its own private module --- simulaqron/settings/__init__.py | 41 ++---------------------- simulaqron/settings/_serialization.py | 46 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 39 deletions(-) create mode 100644 simulaqron/settings/_serialization.py diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index b5385b23..dd4214aa 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -1,46 +1,9 @@ -from enum import Enum -from pathlib import Path -from typing import Type - -from dataclasses_serialization.json import JSONSerializer -from dataclasses_serialization.serializer_base import DeserializationError - +from ._serialization import init_serialization from .network_config import NetworkConfigBuilder from .simulaqron_config import SimulaqronConfig, SIMULAQRON_SETTINGS_FILENAME -# Registration of Serializer for python enums -@JSONSerializer.register_serializer(Enum) -def enum_serializer(obj: Enum) -> str: - return obj.name - - -# Registration of Deserializer for python enums -@JSONSerializer.register_deserializer(Enum) -def enum_deserializer(cls: Type[Enum], name: str) -> Enum: - try: - fixed_name = name.replace("-", "_").upper() - return cls[fixed_name] - except KeyError as ex: - raise DeserializationError( - f"String '{name}' could not be deserialized to a valid " - f"value of type '{cls.__name__}'." - ) from ex - - -# Registration of Serializer for python enums -@JSONSerializer.register_serializer(Path) -def path_serializer(obj: Path) -> str: - return str(obj.resolve()) - - -# Registration of Deserializer for python enums -@JSONSerializer.register_deserializer(Path) -def path_deserializer(cls: Type[Path], path: str) -> Path: - if path == "$DEFAULT_NETWORK": - return (cls.home() / ".simulaqron" / SIMULAQRON_SETTINGS_FILENAME).resolve() - return cls(path).resolve() - +init_serialization() # Centralized way to store the config. It reads the local configuration # if exists, otherwise, it simply populates the in-memory configs object diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py new file mode 100644 index 00000000..99f9d1df --- /dev/null +++ b/simulaqron/settings/_serialization.py @@ -0,0 +1,46 @@ +from enum import Enum +from pathlib import Path +from typing import Type + +from dataclasses_serialization.json import JSONSerializer +from dataclasses_serialization.serializer_base import DeserializationError + +from ..settings.simulaqron_config import SIMULAQRON_SETTINGS_FILENAME + +def init_serialization(): + # Nothing to do here; we just need to execute this file + # to register the serializers + pass + + +# Registration of Serializer for python enums +@JSONSerializer.register_serializer(Enum) +def enum_serializer(obj: Enum) -> str: + return obj.name + + +# Registration of Deserializer for python enums +@JSONSerializer.register_deserializer(Enum) +def enum_deserializer(cls: Type[Enum], name: str) -> Enum: + try: + fixed_name = name.replace("-", "_").upper() + return cls[fixed_name] + except KeyError as ex: + raise DeserializationError( + f"String '{name}' could not be deserialized to a valid " + f"value of type '{cls.__name__}'." + ) from ex + + +# Registration of Serializer for python enums +@JSONSerializer.register_serializer(Path) +def path_serializer(obj: Path) -> str: + return str(obj.resolve()) + + +# Registration of Deserializer for python enums +@JSONSerializer.register_deserializer(Path) +def path_deserializer(cls: Type[Path], path: str) -> Path: + if path == "$DEFAULT_NETWORK": + return (cls.home() / ".simulaqron" / SIMULAQRON_SETTINGS_FILENAME).resolve() + return cls(path).resolve() From d72e9a68989411a703805e4541e08fe16f1052c2 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 27 Oct 2025 11:11:36 +0100 Subject: [PATCH 081/277] Add (de)serialization methods for SimulaqronConfig objects --- simulaqron/settings/_serialization.py | 39 +++++++++++++++-- simulaqron/settings/network_config.py | 35 ++++++++-------- simulaqron/settings/simulaqron_config.py | 53 +++++++++++++++++------- 3 files changed, 91 insertions(+), 36 deletions(-) diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index 99f9d1df..d146cd76 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -1,11 +1,12 @@ from enum import Enum from pathlib import Path -from typing import Type +from typing import Type, Dict, Any from dataclasses_serialization.json import JSONSerializer -from dataclasses_serialization.serializer_base import DeserializationError +from dataclasses_serialization.serializer_base import DeserializationError, dict_serialization + +from ..settings.simulaqron_config import SIMULAQRON_SETTINGS_FILENAME, SimulaqronConfig, SimBackend -from ..settings.simulaqron_config import SIMULAQRON_SETTINGS_FILENAME def init_serialization(): # Nothing to do here; we just need to execute this file @@ -16,7 +17,7 @@ def init_serialization(): # Registration of Serializer for python enums @JSONSerializer.register_serializer(Enum) def enum_serializer(obj: Enum) -> str: - return obj.name + return str(obj) # Registration of Deserializer for python enums @@ -44,3 +45,33 @@ def path_deserializer(cls: Type[Path], path: str) -> Path: if path == "$DEFAULT_NETWORK": return (cls.home() / ".simulaqron" / SIMULAQRON_SETTINGS_FILENAME).resolve() return cls(path).resolve() + + +@JSONSerializer.register_serializer(SimulaqronConfig) +def simulaqron_config_serializer(obj: SimulaqronConfig) -> str: + object_dict = dict(obj.__dict__) + net_cfg_path = str(obj.network_config_file) + del object_dict["_builder"] + del object_dict["_net_cfg_file"] + object_dict["network_config_file"] = net_cfg_path + serialized = JSONSerializer.serialize(dict_serialization(object_dict)) + return serialized + + +@JSONSerializer.register_deserializer(SimulaqronConfig) +def simulaqron_config_deserializer(cls: Type[SimulaqronConfig], obj: Dict[str, Any]) -> SimulaqronConfig: + new_obj = cls(network_config_file=obj["network_config_file"]) + new_obj.max_qubits = obj["max_qubits"] + new_obj.max_registers = obj["max_registers"] + new_obj.conn_retry_time = obj["conn_retry_time"] + if "conn_max_retries" in obj: + new_obj.conn_max_retries = obj["conn_max_retries"] + new_obj.recv_timeout = obj["recv_timeout"] + new_obj.recv_retry_time = obj["recv_retry_time"] + if "recv_max_retries" in obj: + new_obj.recv_max_retries = obj["recv_max_retries"] + new_obj.log_level = obj["log_level"] + new_obj.sim_backend = JSONSerializer.deserialize(SimBackend, obj["sim_backend"]) + new_obj.noisy_qubits = obj["noisy_qubits"] + new_obj.t1 = obj["t1"] + return new_obj diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index e9dfdda6..d5a88fe4 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -1,7 +1,7 @@ import json import socket from contextlib import closing -import socket +from dataclasses import dataclass from importlib import resources from os import PathLike from pathlib import Path @@ -10,20 +10,18 @@ import simulaqron._default_config +@dataclass class NodeConfig: - def __init__(self, name: str, app_hostname: Optional[str], qnodeos_hostname: Optional[str], - vnode_hostname: Optional[str], app_port: Optional[int], qnodeos_port: Optional[int], - vnode_port: Optional[int]): - """ - Used by _NetworkConfig to keep track of the config of a single node. - """ - self.name = name - self.app_hostname = app_hostname - self.qnodeos_hostname = qnodeos_hostname - self.vnode_hostname = vnode_hostname - self.app_port = app_port - self.qnodeos_port = qnodeos_port - self.vnode_port = vnode_port + """ + Used by _NetworkConfig to keep track of the config of a single node. + """ + name: str + app_hostname: Optional[str] + qnodeos_hostname: Optional[str] + vnode_hostname: Optional[str] + app_port: Optional[int] + qnodeos_port: Optional[int] + vnode_port: Optional[int] def to_dict(self) -> Dict[str, List[str | int | None]]: """ @@ -37,6 +35,7 @@ def to_dict(self) -> Dict[str, List[str | int | None]]: } +# @dataclass class NetworkConfig: def __init__(self): """ @@ -46,9 +45,9 @@ def __init__(self): self.nodes: Dict[str, NodeConfig] = {} def add_node( - self, name: str, app_hostname: Optional[str] = None, qnodeos_hostname: Optional[str] = None, - vnode_hostname: Optional[str] = None, app_port: Optional[int] = None, qnodeos_port: Optional[int] = None, - vnode_port: Optional[int] = None, neighbors: Optional[List[str]] = None, + self, name: str, app_hostname: Optional[str] = None, qnodeos_hostname: Optional[str] = None, + vnode_hostname: Optional[str] = None, app_port: Optional[int] = None, qnodeos_port: Optional[int] = None, + vnode_port: Optional[int] = None, neighbors: Optional[List[str]] = None, ): """ Adds a node with the given name to a network (default: "default"). @@ -119,7 +118,7 @@ def using_default_network(cls) -> Self: new_builder.read_from_file(Path(str(default_network_path))) return new_builder - def add_node(self, node_name: str , network_name: str ="default", app_hostname: Optional[str] = None, + def add_node(self, node_name: str, network_name: str = "default", app_hostname: Optional[str] = None, qnodeos_hostname: Optional[str] = None, vnode_hostname: Optional[str] = None, app_port: Optional[int] = None, qnodeos_port: Optional[int] = None, vnode_port: Optional[int] = None, neighbors: List[str] = None): diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index f0a323f2..11ce230d 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -31,16 +31,17 @@ ######################### import json import logging -from dataclasses import dataclass, fields +from dataclasses import dataclass, fields, InitVar from enum import Enum - -from dataclasses_serialization.json import JSONSerializer from os import PathLike from pathlib import Path from typing import Self +from dataclasses_serialization.json import JSONSerializer from dataclasses_serialization.json import JSONSerializerMixin +from ..settings.network_config import NetworkConfigBuilder + # This is the name of the "local" simulaqron settings. # If a file named like this is found in the CWD, it will be # automatically loaded when creating the config file @@ -61,6 +62,7 @@ def __repr__(self): @dataclass class SimulaqronConfig(JSONSerializerMixin): + network_config_file: InitVar[Path] = (Path.home() / ".simulaqron" / "default_network.json").resolve() # Default config max_qubits: int = 20 max_registers: int = 1000 @@ -71,10 +73,28 @@ class SimulaqronConfig(JSONSerializerMixin): recv_max_retries: int = 10 log_level: int = logging.WARNING sim_backend: SimBackend = SimBackend.STABILIZER - network_config_file: Path = (Path.home() / ".simulaqron" / "default_network.json").resolve() noisy_qubits: bool = False t1: float = 1.0 + def __post_init__(self, network_config_file: Path): + self.network_config_file = network_config_file if isinstance(network_config_file, Path) \ + else (Path.home() / ".simulaqron" / "default_network.json").resolve() + self._builder = NetworkConfigBuilder() + + @property + def builder(self) -> NetworkConfigBuilder: + return self._builder + + @property + def network_config_file(self) -> Path: + return self._net_cfg_file + + @network_config_file.setter + def network_config_file(self, value: Path): + # TODO - Insert the logic to reload the _builder + # when this property is updated + self._net_cfg_file = value + @classmethod def _create_home_settings_folder(cls): home_setting_folder = (Path.home() / ".simulaqron").resolve() @@ -100,16 +120,21 @@ def _deserialize_from_file(cls, file_path: Path) -> Self: def load_from_known_sources(cls) -> Self: cwd_settings_file = (Path.cwd() / SIMULAQRON_SETTINGS_FILENAME).resolve() home_settings_file = (Path.home() / ".simulaqron" / SIMULAQRON_SETTINGS_FILENAME).resolve() - if cwd_settings_file.exists() and cwd_settings_file.is_file(): - return cls._deserialize_from_file(cwd_settings_file) - else: - cls._create_home_settings_folder() - if home_settings_file.exists() and cwd_settings_file.is_file(): - return cls._deserialize_from_file(home_settings_file) - else: - new_default_config = cls() - new_default_config.save_to_file(home_settings_file) - return new_default_config + + files_to_load = [cwd_settings_file, home_settings_file] + + for file in files_to_load: + try: + if file.exists() and file.is_file(): + return cls._deserialize_from_file(file) + except json.JSONDecodeError: + # Nothing to do; try next one + pass + + # Ultimate case; we create a new config file in the ohme and load it + new_default_config = cls() + new_default_config.save_to_file(home_settings_file) + return new_default_config def default_settings(self): default_config = SimulaqronConfig() From 1aaa817990202b30ff982f0a0ae232056c2d6844 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 29 Oct 2025 11:49:48 +0100 Subject: [PATCH 082/277] Improve logging when setting up SimulaQron locally --- simulaqron/local/setup.py | 31 ++++++++++++++---------------- simulaqron/virtual_node/virtual.py | 9 +++++---- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index b32f1db0..a2b4f57e 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -27,16 +27,18 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import logging import time + +from netqasm.logging.glob import get_netqasm_logger from twisted.spread import pb from twisted.internet import error from twisted.internet.defer import DeferredList from twisted.internet.error import ReactorNotRunning -from simulaqron.settings import simulaqron_settings from simulaqron.reactor import reactor +_logger = get_netqasm_logger("setup-local") + ##################################################################################################### # @@ -62,31 +64,26 @@ def setup_local(myName, virtualNet, classicalNet, lNode, func, *args, **kwargs): args, kwargs additional arguments to be given to func """ - logging.basicConfig( - format="%(asctime)s:%(levelname)s:%(message)s", - level=simulaqron_settings.log_level, - ) - # Initialize Twisted callback framework dList = [] # If we are listed as a server node for the classical network, start this server if myName in classicalNet.hostDict: try: - logging.debug("LOCAL %s: Starting local classical communication server.", myName) + _logger.debug("LOCAL %s: Starting local classical communication server.", myName) nb = classicalNet.hostDict[myName] nb.root = lNode nb.factory = pb.PBServerFactory(nb.root) reactor.listenTCP(nb.port, nb.factory) except Exception as e: - logging.error("LOCAL %s: Cannot start classical communication servers: %s", myName, e) + _logger.error("LOCAL %s: Cannot start classical communication servers: %s", myName, e) return # Give the server some time to start up time.sleep(3) # Connect to the local virtual node simulating the "local" qubits - logging.debug("LOCAL %s: Connecting to local virtual node.", myName) + _logger.debug("LOCAL %s: Connecting to local virtual node.", myName) node = virtualNet.hostDict[myName] factory = pb.PBClientFactory() reactor.connectTCP(node.hostname, node.port, factory) @@ -97,7 +94,7 @@ def setup_local(myName, virtualNet, classicalNet, lNode, func, *args, **kwargs): for node in classicalNet.hostDict: nb = classicalNet.hostDict[node] if nb.name != myName: - logging.debug("LOCAL %s: Making classical connection to %s.", myName, nb.name) + _logger.debug("LOCAL %s: Making classical connection to %s.", myName, nb.name) nb.factory = pb.PBClientFactory() reactor.connectTCP(nb.hostname, nb.port, nb.factory) dList.append(nb.factory.getRootObject()) @@ -122,7 +119,7 @@ def setup_local(myName, virtualNet, classicalNet, lNode, func, *args, **kwargs): def init_register(resList, myName, virtualNet, classicalNet, lNode, func, *args, **kwargs): - logging.debug("LOCAL %s: All connections set up.", myName) + _logger.debug("LOCAL %s: All connections set up.", myName) # Retrieve the connection to the local virtual node, if successfull j = 0 @@ -131,7 +128,7 @@ def init_register(resList, myName, virtualNet, classicalNet, lNode, func, *args, if lNode is not None: lNode.set_virtual_node(virtRoot) else: - logging.error("LOCAL %s: Connection to virtual server failed!", myName) + _logger.error("LOCAL %s: Connection to virtual server failed!", myName) reactor.stop() # Retrieve connections to the classical nodes @@ -141,9 +138,9 @@ def init_register(resList, myName, virtualNet, classicalNet, lNode, func, *args, j = j + 1 if resList[j][0]: nb.root = resList[j][1] - logging.debug("LOCAL %s: Connected node %s with %s", myName, nb.name, nb.root) + _logger.debug("LOCAL %s: Connected node %s with %s", myName, nb.name, nb.root) else: - logging.error("LOCAL %s: Connection to %s failed!", myName, nb.name) + _logger.error("LOCAL %s: Connection to %s failed!", myName, nb.name) reactor.stop() # On the local virtual node, we still want to initialize a qubit register @@ -153,7 +150,7 @@ def init_register(resList, myName, virtualNet, classicalNet, lNode, func, *args, def fill_register(obj, myName, lNode, virtRoot, classicalNet, func, *args, **kwargs): - logging.debug("LOCAL %s: Created quantum register at virtual node.", myName) + _logger.debug("LOCAL %s: Created quantum register at virtual node.", myName) qReg = obj # If we run a server, record the handle to the local virtual register @@ -168,7 +165,7 @@ def localError(reason): """ Error handling for the connection. """ - logging.error("Critical error: %s", reason) + _logger.error("Critical error: %s", reason) try: reactor.stop() except ReactorNotRunning: diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index e625492b..7819de05 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -130,7 +130,7 @@ def start( self._logger.debug("Running reactor") reactor.run() except CannotListenError: - self._logger.error("NetQASM server address (%d) is already in use.", self.myID.port) + self._logger.exception("NetQASM server address (%d) is already in use.", self.myID.port) return except Exception as e: self._logger.exception("Critical error when starting local virtual node server: %s", e) @@ -273,11 +273,12 @@ def handle_connection_error(self, reason, node): try: reason.raiseException() - except ConnectionRefusedError: - self._logger.debug("Could not connect to %s, trying again...", node.name) + except ConnectionRefusedError as err: + self._logger.debug("Could not connect to %s (%s, %d), trying again...", + node.name, node.hostname, node.port, exc_info=err) reactor.callLater(simulaqron_settings.conn_retry_time, self.connect_to_node, node) except Exception as e: - self._logger.error(e) + self._logger.exception(e) reactor.stop() def get_virtual_id(self): From 39c8a19ea4203b14de02276a42f7e43dbd89aeb6 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 29 Oct 2025 11:51:06 +0100 Subject: [PATCH 083/277] Allow logging subprocesses stdout and stderr to files --- simulaqron/start/__init__.py | 4 ++-- simulaqron/start/start_qnodeos.py | 24 ++++++++++++++++++------ simulaqron/start/start_vnode.py | 17 ++++++++++++++--- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/simulaqron/start/__init__.py b/simulaqron/start/__init__.py index c38427e0..8d2d236c 100644 --- a/simulaqron/start/__init__.py +++ b/simulaqron/start/__init__.py @@ -1,2 +1,2 @@ -from .start_vnode import main as start_vnode -from .start_qnodeos import main as start_qnodeos +from .start_vnode import start_vnode +from .start_qnodeos import start_qnodeos diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index d4209012..a3f8bf1b 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +import logging +import os import sys import time import signal @@ -48,10 +50,10 @@ def connect_to_virt_node(my_name: str , netqasm_factory: NetQASMFactory, virtual # If connection succeeds do: defer_virtual_node.addCallback(init_register, my_name, netqasm_factory) # If connection fails do: - defer_virtual_node.addErrback(handle_connection_error, my_name, netqasm_factory, virtual_network) + defer_virtual_node.addErrback(handle_connection_error, my_name, netqasm_factory, virtual_network, virtual_node.hostname, virtual_node.port) -def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig): +def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig, virtual_node_hostname: str, virtual_node_port: int): """ Handles errors from trying to connect to local virtual node. If a ConnectionRefusedError is raised another try will be made after @@ -59,8 +61,9 @@ def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactor """ try: reason.raiseException() - except ConnectionRefusedError: - logger.debug("LOCAL %s: Could not connect, trying again...", my_name) + except ConnectionRefusedError as err: + # TODO - Implement checking of max number of connections + logger.debug("LOCAL %s: Could not connect to Virtual node (%s, %d), trying again...", my_name, virtual_node_hostname, virtual_node_port, exc_info=err) reactor.callLater( simulaqron_settings.conn_retry_time, connect_to_virt_node, @@ -105,12 +108,21 @@ def setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): else: reactor.stop() +stdout_file = None def sigterm_handler(_signo, _stack_frame): + global stdout_file + stdout_file.flush() + stdout_file.close() reactor.stop() -def main(node_name: str, network_name="default", log_level="WARNING"): +def start_qnodeos(node_name: str, network_name: str = "default", log_level: str = "WARNING"): + if simulaqron_settings.log_level == logging.DEBUG: + global stdout_file + stdout_file = open(f"stdout-stderr-qnos-{node_name}-{os.getpid()}.out.txt", "w") + sys.stdout = stdout_file + sys.stderr = stdout_file """Start the indicated backend NetQASM Server""" set_log_level(log_level) logger.debug("Starting QNodeOS at %s", node_name) @@ -149,4 +161,4 @@ def main(node_name: str, network_name="default", log_level="WARNING"): if __name__ == '__main__': - main(sys.argv[1]) + start_qnodeos(sys.argv[1]) diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 4fe95c72..135d6da8 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -27,7 +27,8 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - +import logging +import os import sys import signal from functools import partial @@ -40,13 +41,23 @@ logger = get_netqasm_logger("start_vnode") +stdout_file = None + def sigterm_handler(name, _signo, _stack_frame): print("Shutting down Node from signal %d." % _signo, flush=True) + global stdout_file + stdout_file.flush() + stdout_file.close() reactor.stop() -def main(name: str, network_name: str = "default", log_level: str = "WARNING"): +def start_vnode(name: str, network_name: str = "default", log_level: str = "WARNING"): + if simulaqron_settings.log_level == logging.DEBUG: + global stdout_file + stdout_file = open(f"stdout-stderr-vnode-{name}-{os.getpid()}.out.txt", "w") + sys.stdout = stdout_file + sys.stderr = stdout_file set_log_level(log_level) signal.signal(signal.SIGTERM, partial(sigterm_handler, name)) signal.signal(signal.SIGINT, partial(sigterm_handler, name)) @@ -62,4 +73,4 @@ def main(name: str, network_name: str = "default", log_level: str = "WARNING"): if __name__ == "__main__": - main(sys.argv[1]) + start_vnode(sys.argv[1]) From bffc42a0d38f666e3c0ab8ce2dfc4ab62aad5715 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 29 Oct 2025 11:53:26 +0100 Subject: [PATCH 084/277] Improve logging in test_merges.py --- tests/slow/merges/test_merges.py | 54 +++++++++++++++++++------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 34f41365..9ae5c339 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -1,5 +1,5 @@ +import sys import os -import logging import unittest import numpy as np @@ -9,6 +9,8 @@ from multiprocess.context import ForkProcess as Process from multiprocess.connection import Pipe +from netqasm.logging.glob import set_log_level, get_netqasm_logger +from logging import DEBUG from simulaqron.general.host_config import SocketsConfig from simulaqron.local.setup import setup_local, assemble_qubit from simulaqron.network import Network @@ -17,6 +19,8 @@ from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.reactor import reactor +_logger = get_netqasm_logger("test_merges") + class localNode(pb.Root): def __init__(self, node, classicalNet): @@ -44,7 +48,7 @@ def remote_receive_two_qubits(self, virtualNum): virtualNum number of the virtual qubit corresponding to the EPR pair received """ - logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + _logger.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) if self.num_qubits_received == 0: self.q1 = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) @@ -64,7 +68,7 @@ def got_both(self): virtualNum number of the virtual qubit corresponding to the EPR pair received """ - logging.debug("LOCAL %s: Got both qubits from Alice and Bob.", self.node.name) + _logger.debug("LOCAL %s: Got both qubits from Alice and Bob.", self.node.name) # We'll test an operation that will cause a merge of the two remote registers yield self.q1.callRemote("apply_H") @@ -102,7 +106,7 @@ def remote_receive_one_qubit(self, virtualNum, cnot_direction=0): virtualNum number of the virtual qubit corresponding to the EPR pair received """ - logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + _logger.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) # Get a reference to our side of the EPR pair qA = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) @@ -162,6 +166,7 @@ def setUpClass(cls): def tearDownClass(cls): for p in cls.processes: p.terminate() + p.join() cls.network.stop() reactor.crash() @@ -169,6 +174,11 @@ def tearDownClass(cls): @staticmethod def setup_node(name, node_code, classical_net_file, send_end): + if simulaqron_settings.log_level == DEBUG: + stdout_file = open(f"stdout-setup-node-{name}-{os.getpid()}.out.txt", "w") + stderr_file = open(f"stderr-setup-node-{name}-{os.getpid()}.out.txt", "w") + sys.stdout = stdout_file + sys.stderr = stderr_file # This file defines the network of virtual quantum nodes virtualFile = os.path.join(os.path.dirname(__file__), "configs", "network.json") @@ -235,7 +245,7 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) # Create 2 qubits qA = yield virtRoot.callRemote("new_qubit_inreg", qReg) @@ -286,7 +296,7 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) # Create a second register newReg = yield virtRoot.callRemote("add_register") @@ -343,14 +353,14 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) # Create qubit qA = yield virtRoot.callRemote("new_qubit_inreg", qReg) # Instruct the virtual node to transfer the qubit remoteNum = yield virtRoot.callRemote("send_qubit", qA, "Charlie") - logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) + _logger.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) # Tell Charlie the number of the virtual qubit so the can use it locally # and extend it to a GHZ state with Charlie @@ -374,14 +384,14 @@ def bob(qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) # Create qubits qB = yield virtRoot.callRemote("new_qubit_inreg", qReg) # Instruct the virtual node to transfer the qubit remoteNum = yield virtRoot.callRemote("send_qubit", qB, "Charlie") - logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) + _logger.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) # Tell Charlie the number of the virtual qubit so the can use it locally # and extend it to a GHZ state with Charlie @@ -404,7 +414,7 @@ def charlie(qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) send_end.send(True) def test(self): @@ -432,7 +442,7 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) # Create new register newReg = yield virtRoot.callRemote("new_register") @@ -444,8 +454,8 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): # Instruct the virtual node to transfer the qubit remoteNumA = yield virtRoot.callRemote("send_qubit", qA, "Bob") remoteNumB = yield virtRoot.callRemote("send_qubit", qB, "Bob") - logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNumA) - logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNumB) + _logger.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNumA) + _logger.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNumB) # Tell Charlie the number of the virtual qubit so the can use it locally # and extend it to a GHZ state with Charlie @@ -470,7 +480,7 @@ def bob(qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) send_end.send(True) def test(self): @@ -491,7 +501,7 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) # Create 2 qubits qA = yield virtRoot.callRemote("new_qubit_inreg", qReg) @@ -505,8 +515,8 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): # Instruct the virtual node to transfer the qubit remoteNumA = yield virtRoot.callRemote("send_qubit", qA, "Bob") remoteNumB = yield virtRoot.callRemote("send_qubit", qB, "Bob") - logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNumA) - logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNumB) + _logger.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNumA) + _logger.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNumB) # Tell Charlie the number of the virtual qubit so the can use it locally # and extend it to a GHZ state with Charlie @@ -534,14 +544,14 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) # Create qubit qA = yield virtRoot.callRemote("new_qubit_inreg", qReg) # Instruct the virtual node to transfer the qubit remoteNum = yield virtRoot.callRemote("send_qubit", qA, "Bob") - logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) + _logger.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) # Tell Bob the number of the virtual qubit so the can use it locally bob = classicalNet.hostDict["Bob"] @@ -566,14 +576,14 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): classicalNet servers in the classical communication network (dictionary of hosts) """ - logging.debug("LOCAL %s: Runing client side program.", myName) + _logger.debug("LOCAL %s: Runing client side program.", myName) # Create qubit qA = yield virtRoot.callRemote("new_qubit_inreg", qReg) # Instruct the virtual node to transfer the qubit remoteNum = yield virtRoot.callRemote("send_qubit", qA, "Bob") - logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) + _logger.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) # Tell Bob the number of the virtual qubit so the can use it locally bob = classicalNet.hostDict["Bob"] From e355e40847315696dfac03ab3e836a7d6f09caab Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 29 Oct 2025 11:54:59 +0100 Subject: [PATCH 085/277] Use temporal files for configurations in test_merges.py --- tests/slow/merges/test_merges.py | 37 +++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 9ae5c339..334cb1ff 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -1,6 +1,9 @@ import sys import os import unittest +from pathlib import Path +from shutil import copyfile +from tempfile import NamedTemporaryFile import numpy as np @@ -154,13 +157,23 @@ def setUpClass(cls): cls.processes = [] cls.processes_to_wait_for = None - simulaqron_settings.default_settings() - path_to_here = os.path.dirname(os.path.abspath(__file__)) - network_config_file = os.path.join(path_to_here, "configs", "network.json") - simulaqron_settings.network_config_file = network_config_file - nodes = ["Alice", "Bob", "Charlie"] - cls.network = Network(nodes=nodes, force=True) - cls.network.start() + with NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as simulaqron_settings_file: + cls._simulaqron_settings_file = simulaqron_settings_file + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ + simulaqron_settings.log_level = DEBUG + with NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as network_def_file: + cls._network_def_file = network_def_file + path_to_here = os.path.dirname(os.path.abspath(__file__)) + network_config_file = os.path.join(path_to_here, "configs", "network.json") + copyfile(network_config_file, network_def_file.name) + simulaqron_settings.network_config_file = network_def_file.name + simulaqron_settings.save_to_file(simulaqron_settings_file.name) + nodes = ["Alice", "Bob", "Charlie"] + cls.network = Network(nodes=nodes, force=True) + cls.network.start() + # cls.network = Network(nodes=nodes, force=True) + # cls.network.start() @classmethod def tearDownClass(cls): @@ -169,6 +182,14 @@ def tearDownClass(cls): p.join() cls.network.stop() + # Remove the files created for config + simulaqron_settings_file = Path(cls._simulaqron_settings_file.name) + network_settings_file = Path(cls._network_def_file.name) + cls._simulaqron_settings_file.close() + cls._network_def_file.close() + simulaqron_settings_file.unlink() + network_settings_file.unlink() + reactor.crash() simulaqron_settings.default_settings() @@ -180,7 +201,7 @@ def setup_node(name, node_code, classical_net_file, send_end): sys.stdout = stdout_file sys.stderr = stderr_file # This file defines the network of virtual quantum nodes - virtualFile = os.path.join(os.path.dirname(__file__), "configs", "network.json") + virtualFile = simulaqron_settings.network_config_file # This file defines the nodes acting as servers in the classical communication network classicalFile = os.path.join(os.path.dirname(__file__), "configs", classical_net_file) From c4cbf1c808c57cb69bb1febd6f671dc0870c3687 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 29 Oct 2025 14:20:25 +0100 Subject: [PATCH 086/277] Correctly unpack values comming from projectq in test_merges.py --- simulaqron/virtual_node/virtual.py | 6 +++--- tests/slow/merges/test_merges.py | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 7819de05..627eb79c 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -129,11 +129,11 @@ def start( self._logger.debug("Running reactor") reactor.run() - except CannotListenError: - self._logger.exception("NetQASM server address (%d) is already in use.", self.myID.port) + except CannotListenError as exc: + self._logger.debug("NetQASM server address (%d) is already in use.", self.myID.port, exc_info=exc) return except Exception as e: - self._logger.exception("Critical error when starting local virtual node server: %s", e) + self._logger.debug("Critical error when starting local virtual node server", exc_info=e) return diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 334cb1ff..90a0b0a6 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -84,7 +84,7 @@ def got_both(self): expectedRho = [[0.5, 0, 0, 0.5], [0, 0, 0, 0], [0, 0, 0, 0], [0.5, 0, 0, 0.5]] correct = np.all(np.isclose(rho, expectedRho)) elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: - (realvec, imagvec) = yield self.virtRoot.callRemote("get_register_RI", self.q1) + _, (realvec, imagvec) = yield self.virtRoot.callRemote("get_register_RI", self.q1) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] expectedState = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] correct = np.all(np.isclose(state, expectedState)) @@ -132,7 +132,7 @@ def remote_receive_one_qubit(self, virtualNum, cnot_direction=0): expectedRho = [[0.5, 0, 0, 0.5], [0, 0, 0, 0], [0, 0, 0, 0], [0.5, 0, 0, 0.5]] correct = np.all(np.isclose(rho, expectedRho)) elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: - (realvec, imagvec) = yield self.virtRoot.callRemote("get_register_RI", qA) + _, (realvec, imagvec) = yield self.virtRoot.callRemote("get_register_RI", qA) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] expectedState = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] correct = np.all(np.isclose(state, expectedState)) @@ -190,9 +190,6 @@ def tearDownClass(cls): simulaqron_settings_file.unlink() network_settings_file.unlink() - reactor.crash() - simulaqron_settings.default_settings() - @staticmethod def setup_node(name, node_code, classical_net_file, send_end): if simulaqron_settings.log_level == DEBUG: @@ -283,7 +280,7 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): expectedRho = [[0.5, 0, 0, 0.5], [0, 0, 0, 0], [0, 0, 0, 0], [0.5, 0, 0, 0.5]] correct = np.all(np.isclose(rho, expectedRho)) elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: - (realvec, imagvec, _, _, _) = yield virtRoot.callRemote("get_register", qA) + (_, (realvec, imagvec), _, _, _) = yield virtRoot.callRemote("get_register", qA) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] expectedState = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] correct = np.all(np.isclose(state, expectedState)) @@ -336,7 +333,7 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): expectedRho = [[0.5, 0, 0, 0.5], [0, 0, 0, 0], [0, 0, 0, 0], [0.5, 0, 0, 0.5]] correct = np.all(np.isclose(rho, expectedRho)) elif simulaqron_settings.sim_backend == SimBackend.PROJECTQ: - (realvec, imagvec, _, _, _) = yield virtRoot.callRemote("get_register", qA) + (_, (realvec, imagvec), _, _, _) = yield virtRoot.callRemote("get_register", qA) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] expectedState = [1 / np.sqrt(2), 0, 0, 1 / np.sqrt(2)] correct = np.all(np.isclose(state, expectedState)) From d375c20c2919defd0487bbaf85b8da591e105e3f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 29 Oct 2025 14:20:53 +0100 Subject: [PATCH 087/277] Implement a max retries when connecting a socket to avoid deadlocks --- simulaqron/sdk/socket.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index 6c76f74c..473c97df 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -13,6 +13,7 @@ class Socket(_Socket): RETRY_TIME = 0.1 + MAX_RETRIES = 10 def __init__( self, @@ -131,11 +132,13 @@ def _connect(self) -> socket.socket: server_name = self._remote_node_name addr = self._get_addr_info(name=server_name) app_socket = socket.socket(addr[0], addr[1], addr[2]) + attempt = 0 if self.is_server: self._logger.debug("Trying to open application socket as server") app_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) while True: + attempt += 1 try: app_socket.bind(addr[4]) except OSError as err: @@ -143,6 +146,8 @@ def _connect(self) -> socket.socket: "Could not bind socket since: %s\nTrying again in %ds...", err, self.RETRY_TIME ) + if attempt > self.MAX_RETRIES: + raise err time.sleep(self.RETRY_TIME) else: break @@ -153,15 +158,18 @@ def _connect(self) -> socket.socket: else: self._logger.debug("Trying to open application socket as client") while True: + attempt += 1 try: app_socket.settimeout(self._timeout) app_socket.connect(addr[4]) - except ConnectionRefusedError: + except ConnectionRefusedError as err: self._logger.debug( "Could not open application socket, trying again in %d s...", self.RETRY_TIME ) time.sleep(self.RETRY_TIME) + if attempt > self.MAX_RETRIES: + raise err else: break connected_socket = app_socket From a58df91022d2fab41e91e3e2fc4d224a9c867f4b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 29 Oct 2025 14:59:03 +0100 Subject: [PATCH 088/277] Set environment variable to use a single thread in OMP library and avoid deadlocks as much as possible --- Makefile | 19 +++++++------------ simulaqron/run/run.py | 4 ++++ tests/slow/merges/test_merges.py | 3 +-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Makefile b/Makefile index a2f4cae5..16775ae4 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,9 @@ PIP = pip3 EXAMPLES_DIR = examples SIMULAQRON_DIR = simulaqron TEST_DIR = tests -RESET_FILE = ${SIMULAQRON_DIR}/toolbox/reset.py + +# IMPORTANT: For running in makefile, we need to use only 1 thread in OMP library +export OMP_NUM_THREADS=1 clean: _delete_pyc _delete_pid _clear_build _reset @@ -25,18 +27,11 @@ requirements python-deps: install-optional: install @${PYTHON} -m pip install .\[opt\] -_reset: - @${PYTHON} ${RESET_FILE} - -_tests: - @${PYTHON} -m pytest ${TEST_DIR}/quick - -tests: _tests _reset - -_tests_all: - @${PYTHON} -m pytest ${TEST_DIR} +tests: + @${PYTHON} -m pytest -v ${TEST_DIR}/quick -tests_all: _tests_all _reset +tests_all: + @${PYTHON} -m pytest -v --capture=tee-sys ${TEST_DIR} install: test-deps @$(PYTHON) -m pip install -e . ${PIP_FLAGS} diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index f348ac63..46d63a14 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -163,6 +163,10 @@ def run_applications( The i-th entry of the list will correspond to the i-th execution round of the simulation. """ + # Before all; we need to instruct the OMP library to use a single thread to avoid + # heavy-processes deadlocks + os.environ["OMP_NUM_THREADS"] = "1" + # app_names = [app_cfg.app_name for app_cfg in app_cfgs] app_names: List[str] = [program.party for program in app_instance.app.programs] sim_backend: SimBackend = _SIMULAQRON_BACKENDS[formalism] diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 90a0b0a6..662ecc1a 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -12,7 +12,7 @@ from multiprocess.context import ForkProcess as Process from multiprocess.connection import Pipe -from netqasm.logging.glob import set_log_level, get_netqasm_logger +from netqasm.logging.glob import get_netqasm_logger from logging import DEBUG from simulaqron.general.host_config import SocketsConfig from simulaqron.local.setup import setup_local, assemble_qubit @@ -161,7 +161,6 @@ def setUpClass(cls): cls._simulaqron_settings_file = simulaqron_settings_file simulaqron_settings.default_settings() simulaqron_settings.sim_backend = SimBackend.PROJECTQ - simulaqron_settings.log_level = DEBUG with NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as network_def_file: cls._network_def_file = network_def_file path_to_here = os.path.dirname(os.path.abspath(__file__)) From 3c66799485077776989f54c071676a21d051dbe2 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 30 Oct 2025 14:49:26 +0100 Subject: [PATCH 089/277] Fix serialization of network config files and simulaqron settings --- simulaqron/settings/__init__.py | 2 +- simulaqron/settings/_serialization.py | 10 ++-- simulaqron/settings/network_config.py | 7 ++- simulaqron/settings/simulaqron_config.py | 33 +++++++---- simulaqron/simulaqron.py | 16 +++--- tests/quick/network/test_network.py | 4 +- tests/quick/test_settings.py | 70 ++++++++++++++++++++++-- 7 files changed, 110 insertions(+), 32 deletions(-) diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index dd4214aa..1d68690d 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -1,6 +1,6 @@ from ._serialization import init_serialization from .network_config import NetworkConfigBuilder -from .simulaqron_config import SimulaqronConfig, SIMULAQRON_SETTINGS_FILENAME +from .simulaqron_config import SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME init_serialization() diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index d146cd76..b85f1af4 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -3,9 +3,11 @@ from typing import Type, Dict, Any from dataclasses_serialization.json import JSONSerializer -from dataclasses_serialization.serializer_base import DeserializationError, dict_serialization +from dataclasses_serialization.serializer_base import (DeserializationError, + dict_serialization) -from ..settings.simulaqron_config import SIMULAQRON_SETTINGS_FILENAME, SimulaqronConfig, SimBackend +from ..settings.simulaqron_config import (DEFAULT_SIMULAQRON_SETTINGS_FILENAME, + SimulaqronConfig, SimBackend) def init_serialization(): @@ -43,7 +45,7 @@ def path_serializer(obj: Path) -> str: @JSONSerializer.register_deserializer(Path) def path_deserializer(cls: Type[Path], path: str) -> Path: if path == "$DEFAULT_NETWORK": - return (cls.home() / ".simulaqron" / SIMULAQRON_SETTINGS_FILENAME).resolve() + return (cls.home() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() return cls(path).resolve() @@ -60,7 +62,7 @@ def simulaqron_config_serializer(obj: SimulaqronConfig) -> str: @JSONSerializer.register_deserializer(SimulaqronConfig) def simulaqron_config_deserializer(cls: Type[SimulaqronConfig], obj: Dict[str, Any]) -> SimulaqronConfig: - new_obj = cls(network_config_file=obj["network_config_file"]) + new_obj = cls(network_config_file=JSONSerializer.deserialize(Path, obj["network_config_file"])) new_obj.max_qubits = obj["max_qubits"] new_obj.max_registers = obj["max_registers"] new_obj.conn_retry_time = obj["conn_retry_time"] diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index d5a88fe4..9c240a1c 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -113,6 +113,7 @@ def __init__(self): @classmethod def using_default_network(cls) -> Self: + # We use the embedded default network here default_network_path = resources.files(simulaqron._default_config).joinpath("default_network.json") new_builder = cls() new_builder.read_from_file(Path(str(default_network_path))) @@ -300,8 +301,10 @@ def read_from_file(self, file_path: PathLike | str): if file_path is None: raise ValueError("No path specified to read the network configuration") - if Path(str(file_path)).exists(): - with open(file_path, 'r') as f: + file_path = Path(str(file_path)) + + if file_path.exists(): + with file_path.open('r') as f: dictionary = json.load(f) else: raise ValueError(f"No such file {file_path}") diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 11ce230d..8bfd56b4 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -45,7 +45,8 @@ # This is the name of the "local" simulaqron settings. # If a file named like this is found in the CWD, it will be # automatically loaded when creating the config file -SIMULAQRON_SETTINGS_FILENAME = "simulaqron_settings.json" +DEFAULT_SIMULAQRON_SETTINGS_FILENAME = "simulaqron_settings.json" +DEFAULT_SIMULAQRON_NETWORK_FILENAME = "simulaqron_network.json" class SimBackend(JSONSerializerMixin, Enum): @@ -62,7 +63,7 @@ def __repr__(self): @dataclass class SimulaqronConfig(JSONSerializerMixin): - network_config_file: InitVar[Path] = (Path.home() / ".simulaqron" / "default_network.json").resolve() + network_config_file: InitVar[Path] = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() # Default config max_qubits: int = 20 max_registers: int = 1000 @@ -77,12 +78,19 @@ class SimulaqronConfig(JSONSerializerMixin): t1: float = 1.0 def __post_init__(self, network_config_file: Path): - self.network_config_file = network_config_file if isinstance(network_config_file, Path) \ - else (Path.home() / ".simulaqron" / "default_network.json").resolve() - self._builder = NetworkConfigBuilder() + if isinstance(network_config_file, Path) and network_config_file.exists() and network_config_file.is_file(): + self._builder = NetworkConfigBuilder() + net_cfg_file = network_config_file.resolve() + else: + # Given network config file is invalid or does not exist. Use the default one + # and write it to the expected location + net_cfg_file = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() + self._builder = NetworkConfigBuilder.using_default_network() + self._builder.write_to_file(net_cfg_file) + self.network_config_file = net_cfg_file @property - def builder(self) -> NetworkConfigBuilder: + def network_builder(self) -> NetworkConfigBuilder: return self._builder @property @@ -90,9 +98,12 @@ def network_config_file(self) -> Path: return self._net_cfg_file @network_config_file.setter - def network_config_file(self, value: Path): - # TODO - Insert the logic to reload the _builder - # when this property is updated + def network_config_file(self, value: Path | str): + if isinstance(value, str): + value = Path(value).resolve() + # If we set the network config file, update the NetworkConfigBuilder + if value.exists() and value.is_file(): + self._builder.read_from_file(value) self._net_cfg_file = value @classmethod @@ -118,8 +129,8 @@ def _deserialize_from_file(cls, file_path: Path) -> Self: @classmethod def load_from_known_sources(cls) -> Self: - cwd_settings_file = (Path.cwd() / SIMULAQRON_SETTINGS_FILENAME).resolve() - home_settings_file = (Path.home() / ".simulaqron" / SIMULAQRON_SETTINGS_FILENAME).resolve() + cwd_settings_file = (Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() + home_settings_file = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() files_to_load = [cwd_settings_file, home_settings_file] diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 2934f2d1..2b1e1e76 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -11,7 +11,7 @@ from simulaqron.network import Network from simulaqron.settings import simulaqron_settings -from simulaqron.settings.simulaqron_config import SimBackend, SIMULAQRON_SETTINGS_FILENAME +from simulaqron.settings.simulaqron_config import SimBackend, DEFAULT_SIMULAQRON_SETTINGS_FILENAME from simulaqron.settings.network_config import NetworkConfigBuilder CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -199,7 +199,7 @@ def reset(force: Optional[bool]): if not force: answer = input("Are you sure you want to reset simulaqron?\nThis will revert settings and " "network config files to the default values.\nNote, this action will remove " - f"the file at {SIMULAQRON_SETTINGS_FILENAME} if it exists.\n" + f"the file at {DEFAULT_SIMULAQRON_SETTINGS_FILENAME} if it exists.\n" "(yes/no)") else: answer = "yes" @@ -216,8 +216,8 @@ def reset(force: Optional[bool]): def updates_local_config(command_function: Callable): def wrapper(*args, **kwargs): - local_settings = Path.cwd() / SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): simulaqron_settings.load_from_file(local_settings) else: simulaqron_settings.default_settings() @@ -242,7 +242,7 @@ def set(): def default(): """Sets all settings back to default and saves it as a local configuration file""" simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(Path.cwd() / SIMULAQRON_SETTINGS_FILENAME) + simulaqron_settings.save_to_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) @set.command() @@ -333,9 +333,9 @@ def t1(value): def loads_local_config(command_function: Callable): def wrapper(*args, **kwargs): - local_settings = Path.cwd() / SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists(): - simulaqron_settings.load_from_file(Path.cwd() / SIMULAQRON_SETTINGS_FILENAME) + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) else: simulaqron_settings.default_settings() command_function(*args, **kwargs) diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index a301571e..be12013d 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -43,8 +43,8 @@ def network_file(self): network_builder.using_default_network() with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as net_config_file: # We also need to specify the location of the temporal file as the network config file - simulaqron_settings.network_config_file = net_config_file.name network_builder.write_to_file(net_config_file.name) + simulaqron_settings.network_config_file = net_config_file.name net_config_file.close() self.network = None yield net_config_file.name @@ -88,8 +88,8 @@ def network_file(self): network_builder.using_default_network() with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as net_config_file: # We also need to specify the location of the temporal file as the network config file - simulaqron_settings.network_config_file = net_config_file.name network_builder.write_to_file(net_config_file.name) + simulaqron_settings.network_config_file = net_config_file.name net_config_file.close() self.network = None yield net_config_file.name diff --git a/tests/quick/test_settings.py b/tests/quick/test_settings.py index 35e5bc50..923cead0 100644 --- a/tests/quick/test_settings.py +++ b/tests/quick/test_settings.py @@ -1,15 +1,71 @@ import tempfile import json import pytest -from dataclasses_serialization.json import JSONSerializer +import shutil from pathlib import Path +from tempfile import NamedTemporaryFile +from dataclasses_serialization.json import JSONSerializer from simulaqron.settings import simulaqron_settings -from simulaqron.settings.simulaqron_config import SimulaqronConfig +from simulaqron.settings.simulaqron_config import (SimulaqronConfig, + DEFAULT_SIMULAQRON_NETWORK_FILENAME, + DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + + +cwd_settings = (Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() +cwd_network = (Path.home() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() +home_settings = (Path.cwd() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() +home_network = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() class TestSettings: - def test_default_settings(self): + @pytest.fixture + def clean_settings(self): + # Load the setting files in cwd and home, saved them in a temp file + if cwd_settings.exists() and cwd_settings.is_file(): + orig_cwd_settings = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + shutil.copyfile(cwd_settings, orig_cwd_settings.name) + else: + orig_cwd_settings = None + if cwd_network.is_file() and cwd_network.is_file(): + orig_cwd_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + shutil.copyfile(cwd_network, orig_cwd_network.name) + else: + orig_cwd_network = None + if home_settings.exists() and home_settings.is_file(): + orig_home_settings = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + shutil.copyfile(home_settings, orig_home_settings.name) + else: + orig_home_settings = None + if home_network.is_file() and home_network.is_file(): + orig_home_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + shutil.copyfile(home_network, orig_home_network.name) + else: + orig_home_network = None + # Proceed with the test case + yield + # Restore the loaded files in the original locations + if orig_cwd_settings is not None: + shutil.copyfile(orig_cwd_settings.name, cwd_settings) + orig_cwd_settings.__exit__(None, None, None) + if orig_home_settings is not None: + shutil.copyfile(orig_home_settings.name, home_settings) + orig_home_settings.__exit__(None, None, None) + if orig_cwd_network is not None: + shutil.copyfile(orig_cwd_network.name, cwd_network) + orig_cwd_network.__exit__(None, None, None) + if orig_home_network is not None: + shutil.copyfile(orig_home_network.name, home_network) + orig_home_network.__exit__(None, None, None) + + @staticmethod + def _cleanup_config_files(): + files_to_check = [cwd_settings, cwd_network, home_settings, home_network] + for file in files_to_check: + if file.exists() and file.is_file(): + file.unlink() + + def test_create_default_settings(self, clean_settings): __expected_default_settings = """ { "max_qubits": 20, @@ -27,12 +83,18 @@ def test_default_settings(self): } """ expected_settings_dict = json.loads(__expected_default_settings) - path = (Path.home() / ".simulaqron" / "default_network.json").resolve() + path = (Path.home() / ".simulaqron" / "simulaqron_network.json").resolve() expected_settings_dict["network_config_file"] = str(path) expected_settings = JSONSerializer.deserialize(SimulaqronConfig, expected_settings_dict) assert simulaqron_settings == expected_settings + assert simulaqron_settings.network_builder is not None + assert simulaqron_settings.network_config_file.exists() + assert simulaqron_settings.network_config_file.is_file() + + TestSettings._cleanup_config_files() + def test_non_existent_network_config(self): _original_settings = """ { From dd32635298f41437dc8540a67eb5059ccc47ee33 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 30 Oct 2025 14:57:08 +0100 Subject: [PATCH 090/277] Minimize creation of NetworkBuilder objects and reuse automatically loaded with the seetings --- simulaqron/netqasm_backend/factory.py | 3 +-- simulaqron/network.py | 10 ++++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 5d04daa1..bfbcdec4 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -202,8 +202,7 @@ def __init__( # topology self.topology = None if simulaqron_settings.network_config_file is not None: - networks_config = NetworkConfigBuilder() - networks_config.read_from_file(simulaqron_settings.network_config_file) + networks_config = simulaqron_settings.network_builder self.topology = networks_config.networks[network_name].topology def stop(self): diff --git a/simulaqron/network.py b/simulaqron/network.py index a29f9c6a..9ded0ce3 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -75,13 +75,11 @@ def __init__(self, name: str = "default", nodes: Optional[List[str]] = None, self.processes: List[Process] = [] self._logger = get_netqasm_logger(f"{self.__class__.__name__}({self.name})") - if network_config_file is None: - network_config_file = simulaqron_settings.network_config_file + if network_config_file is not None: + simulaqron_settings.network_config_file = network_config_file else: - network_config_file = network_config_file - - networks_config = NetworkConfigBuilder() - networks_config.read_from_file(network_config_file) + network_config_file = simulaqron_settings.network_config_file + networks_config = simulaqron_settings.network_builder if new: if nodes is None: From d520f6a5a99b640761381d75d8d1090b67f2c26b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 30 Oct 2025 15:16:44 +0100 Subject: [PATCH 091/277] Add indent when serializing simulaqron settings --- simulaqron/settings/simulaqron_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 8bfd56b4..2d471be6 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -159,4 +159,4 @@ def save_to_file(self, path: PathLike): file_path = Path(str(path)).resolve() with file_path.open("wt") as file: serialized = JSONSerializer.serialize(self) - json.dump(serialized, file) + json.dump(serialized, file, indent=4) From 15cfd9e11db3f2d8dc19334191aa501995ba8a7c Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 30 Oct 2025 17:20:56 +0100 Subject: [PATCH 092/277] Fix test for settings --- tests/quick/test_settings.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/quick/test_settings.py b/tests/quick/test_settings.py index 923cead0..7ee59006 100644 --- a/tests/quick/test_settings.py +++ b/tests/quick/test_settings.py @@ -13,8 +13,8 @@ cwd_settings = (Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() -cwd_network = (Path.home() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() -home_settings = (Path.cwd() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() +cwd_network = (Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() +home_settings = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() home_network = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() @@ -25,36 +25,44 @@ def clean_settings(self): if cwd_settings.exists() and cwd_settings.is_file(): orig_cwd_settings = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() shutil.copyfile(cwd_settings, orig_cwd_settings.name) + cwd_settings.unlink() else: orig_cwd_settings = None if cwd_network.is_file() and cwd_network.is_file(): orig_cwd_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() shutil.copyfile(cwd_network, orig_cwd_network.name) + cwd_network.unlink() else: orig_cwd_network = None if home_settings.exists() and home_settings.is_file(): orig_home_settings = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() shutil.copyfile(home_settings, orig_home_settings.name) + home_settings.unlink() else: orig_home_settings = None if home_network.is_file() and home_network.is_file(): orig_home_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() shutil.copyfile(home_network, orig_home_network.name) + home_network.unlink() else: orig_home_network = None # Proceed with the test case yield # Restore the loaded files in the original locations if orig_cwd_settings is not None: + cwd_settings.touch() shutil.copyfile(orig_cwd_settings.name, cwd_settings) orig_cwd_settings.__exit__(None, None, None) if orig_home_settings is not None: + home_settings.touch() shutil.copyfile(orig_home_settings.name, home_settings) orig_home_settings.__exit__(None, None, None) if orig_cwd_network is not None: + cwd_network.touch() shutil.copyfile(orig_cwd_network.name, cwd_network) orig_cwd_network.__exit__(None, None, None) if orig_home_network is not None: + home_network.touch() shutil.copyfile(orig_home_network.name, home_network) orig_home_network.__exit__(None, None, None) @@ -82,6 +90,7 @@ def test_create_default_settings(self, clean_settings): "t1": 1.0 } """ + simulaqron_settings.default_settings() expected_settings_dict = json.loads(__expected_default_settings) path = (Path.home() / ".simulaqron" / "simulaqron_network.json").resolve() expected_settings_dict["network_config_file"] = str(path) From 0b08d710acf5fe75199de02aabb69fdf8a8cec50 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 30 Oct 2025 17:21:21 +0100 Subject: [PATCH 093/277] Improve way to finalize all set of applications. --- simulaqron/run/run.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 46d63a14..d92a217f 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -100,14 +100,20 @@ def _app_wrapper(**kwargs): # TODO - Signal handler for the SIGINT signal? # Call the app main function - return entry_function(**kwargs) + try: + return entry_function(**kwargs) + except BaseException as e: + _signal_other_apps() + raise e -def _signal_other_apps(exc: BaseException): +def _signal_other_apps(): global apps_pids assert apps_pids is not None for pid in apps_pids: - os.kill(pid, signal.SIGINT) + # Do not send SIGINT to self process + if pid != os.getpid(): + os.kill(pid, signal.SIGINT) def run_applications( @@ -241,7 +247,7 @@ def run_applications( kwds=inputs, # The error callback with get invoked in the child process, so # we tell other applications that they need to stop - error_callback=_signal_other_apps + #error_callback=_signal_other_apps ) app_futures.append(future) From 5c83436313e89de4e0f1ec80b4c3a4847eb4f58b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 31 Oct 2025 13:46:23 +0100 Subject: [PATCH 094/277] Do not block to wait a result of a node when using run_applications --- simulaqron/run/run.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index d92a217f..281988d0 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -39,11 +39,10 @@ } -def as_completed(futures: List[ApplyResult], names: List[str]) -> Generator[Tuple[ApplyResult, str], None, None]: +def as_completed(futures: List[ApplyResult], names: List[str]) -> List[Tuple[ApplyResult, str]]: if len(futures) is not len(names): raise RuntimeError("Not all registered applications have an associated name") - for future, name in zip(futures, names): - yield future, name + return [(future, name) for future, name in zip(futures, names)] def reset(save_loggers=False): @@ -261,8 +260,13 @@ def run_applications( # Join the application processes and the backend names = [f'app_{app_name}' for app_name in app_names] result = {} - for future, name in as_completed(app_futures, names): - result[name] = future.get() + futures = as_completed(app_futures, names) + while len(result) < len(app_names): + for future, name in futures: + if name in result: + continue + if future.ready(): + result[name] = future.get() # if results_file is not None: # save_results(results=results, results_file=results_file) if enable_logging: @@ -271,7 +275,6 @@ def run_applications( dump_yaml(data=result, file_path=path) results.append(result) network.stop() - finally: network.stop() reset(save_loggers=True) From b553e7d6cb68f3958fbdf3c0d98dea667adb7f63 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 31 Oct 2025 13:47:35 +0100 Subject: [PATCH 095/277] Use relative paths in the SimulaqronConfig class to point the network config file (will be used in the future) --- simulaqron/settings/_serialization.py | 3 ++- simulaqron/settings/simulaqron_config.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index b85f1af4..9b50d0f3 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -46,7 +46,8 @@ def path_serializer(obj: Path) -> str: def path_deserializer(cls: Type[Path], path: str) -> Path: if path == "$DEFAULT_NETWORK": return (cls.home() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() - return cls(path).resolve() + # If the path is given, we will resolve it later + return cls(path) @JSONSerializer.register_serializer(SimulaqronConfig) diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 2d471be6..41fb29e8 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -80,7 +80,7 @@ class SimulaqronConfig(JSONSerializerMixin): def __post_init__(self, network_config_file: Path): if isinstance(network_config_file, Path) and network_config_file.exists() and network_config_file.is_file(): self._builder = NetworkConfigBuilder() - net_cfg_file = network_config_file.resolve() + net_cfg_file = network_config_file else: # Given network config file is invalid or does not exist. Use the default one # and write it to the expected location From aa5da3c5386bec5e236c1e2796db484327732d6a Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 31 Oct 2025 17:12:03 +0100 Subject: [PATCH 096/277] Fix tiemout usage when connecting sockets --- .gitignore | 2 ++ simulaqron/sdk/socket.py | 30 ++++++++++++++++++------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index eaac5440..7dc413ff 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ cqc/backend/logFile* examples/**/log/* + +*.out.txt diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index 473c97df..0f19b100 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -12,8 +12,8 @@ class Socket(_Socket): - RETRY_TIME = 0.1 - MAX_RETRIES = 10 + RETRY_TIME = 0.2 + MAX_RETRIES = 20 def __init__( self, @@ -33,7 +33,7 @@ def __init__( self._use_callbacks = use_callbacks self._network_name = network_name - self._logger = get_netqasm_logger(f"{self.__class__.__name__}({app_name} <-> {remote_app_name})") + self._logger = get_netqasm_logger(f"{self.__class__.__name__}(L:{app_name} <-> R:{remote_app_name})") self._timeout = timeout # We define _app_socket as None as a default value, so the __del__ method # does not fail when the socket could not be connected correctly. @@ -41,6 +41,9 @@ def __init__( self._app_socket: socket.socket = self._connect() def __del__(self): + self.close() + + def close(self): if self._app_socket: self._app_socket.close() @@ -141,37 +144,40 @@ def _connect(self) -> socket.socket: attempt += 1 try: app_socket.bind(addr[4]) + break except OSError as err: self._logger.debug( - "Could not bind socket since: %s\nTrying again in %ds...", - err, self.RETRY_TIME + "Could not bind socket since: %s, Trying again in %ds (attempt %d of %d)...", + err, self.RETRY_TIME, attempt, self.MAX_RETRIES, exc_info=err ) if attempt > self.MAX_RETRIES: raise err time.sleep(self.RETRY_TIME) - else: - break app_socket.listen(1) app_socket.settimeout(self._timeout) conn, _ = app_socket.accept() + self._logger.debug("Classical socket as server accepted connection.") connected_socket = conn else: self._logger.debug("Trying to open application socket as client") while True: attempt += 1 try: - app_socket.settimeout(self._timeout) + #app_socket.settimeout(self._timeout) app_socket.connect(addr[4]) + break except ConnectionRefusedError as err: self._logger.debug( - "Could not open application socket, trying again in %d s...", - self.RETRY_TIME + "Could not open application socket, trying again in %f s (attempt %d of %d)...", + self.RETRY_TIME, attempt, self.MAX_RETRIES, exc_info=err ) time.sleep(self.RETRY_TIME) if attempt > self.MAX_RETRIES: raise err - else: - break + except Exception as err: + self._logger.exception("Could not open application socket due to unexpected error") + raise err + self._logger.debug("Classical socket connected as client") connected_socket = app_socket self._logger.debug("Application socket opened") From c0a1bb413d72afb65cde0c3b74637a1cef1392f4 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 3 Nov 2025 10:41:01 +0100 Subject: [PATCH 097/277] Add small sleep between checking if a node is ready of not --- simulaqron/run/run.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 281988d0..428575e4 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -8,7 +8,7 @@ from importlib.util import find_spec from os import PathLike from pathlib import Path -from typing import Callable, Optional, Any, Dict, List, Union, Generator, Tuple +from typing import Callable, Optional, Any, Dict, List, Union, Tuple from multiprocess.sharedctypes import SynchronizedArray from netqasm.logging.glob import get_netqasm_logger @@ -244,9 +244,6 @@ def run_applications( future: ApplyResult = executor.apply_async( _app_wrapper, kwds=inputs, - # The error callback with get invoked in the child process, so - # we tell other applications that they need to stop - #error_callback=_signal_other_apps ) app_futures.append(future) @@ -267,6 +264,7 @@ def run_applications( continue if future.ready(): result[name] = future.get() + time.sleep(0.1) # if results_file is not None: # save_results(results=results, results_file=results_file) if enable_logging: From 3d1a00d9a6b825c53ebf605de94217b70b920ee0 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 3 Nov 2025 10:56:44 +0100 Subject: [PATCH 098/277] Implement a configurable max waiting time for nodes to try avoiding deadlocks --- simulaqron/run/run.py | 10 ++++++++++ simulaqron/settings/_serialization.py | 2 ++ simulaqron/settings/simulaqron_config.py | 1 + 3 files changed, 13 insertions(+) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 428575e4..6de0bafa 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -1,6 +1,7 @@ import logging import os import signal +import time from multiprocess.context import ForkContext as ProcessContext from multiprocess.pool import ApplyResult @@ -258,6 +259,7 @@ def run_applications( names = [f'app_{app_name}' for app_name in app_names] result = {} futures = as_completed(app_futures, names) + start_time = time.time() while len(result) < len(app_names): for future, name in futures: if name in result: @@ -265,6 +267,14 @@ def run_applications( if future.ready(): result[name] = future.get() time.sleep(0.1) + waited_for = time.time() - start_time + if 0.0 < simulaqron_settings.max_app_waiting_time < waited_for: + raise TimeoutError("SimulaQron: max app waiting time exceeded; " + "app did not finish in time. Please check that " + "your code runs correctly standalone. If your " + "code takes a long time to run, please adjust the " + "value of 'max_app_waiting_time' in your simulaqron" + "settings file.") # if results_file is not None: # save_results(results=results, results_file=results_file) if enable_logging: diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index 9b50d0f3..514faf29 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -76,5 +76,7 @@ def simulaqron_config_deserializer(cls: Type[SimulaqronConfig], obj: Dict[str, A new_obj.log_level = obj["log_level"] new_obj.sim_backend = JSONSerializer.deserialize(SimBackend, obj["sim_backend"]) new_obj.noisy_qubits = obj["noisy_qubits"] + if "max_app_waiting_time" in obj: + new_obj.max_app_waiting_time = obj["max_app_waiting_time"] new_obj.t1 = obj["t1"] return new_obj diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 41fb29e8..9f7d2f60 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -75,6 +75,7 @@ class SimulaqronConfig(JSONSerializerMixin): log_level: int = logging.WARNING sim_backend: SimBackend = SimBackend.STABILIZER noisy_qubits: bool = False + max_app_waiting_time: float = -1.0 # In seconds, negative means unlimited waiting t1: float = 1.0 def __post_init__(self, network_config_file: Path): From a0678c795e98b9bec0ff59d1d7367aace8bfaca0 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 3 Nov 2025 12:57:01 +0100 Subject: [PATCH 099/277] Linter changes --- .../_default_config/default_network.json | 150 +++++++++--------- simulaqron/general/host_config.py | 2 +- simulaqron/local/setup.py | 3 +- simulaqron/netqasm_backend/executioner.py | 2 +- simulaqron/netqasm_backend/factory.py | 5 +- simulaqron/network.py | 14 +- simulaqron/reactor.py | 1 + simulaqron/run/run.py | 33 ++-- simulaqron/sdk/broadcast_channel.py | 1 + simulaqron/sdk/connection.py | 7 +- simulaqron/sdk/socket.py | 46 +++--- simulaqron/settings/__init__.py | 1 - simulaqron/simulaqron.py | 27 ++-- simulaqron/start/__init__.py | 2 +- simulaqron/start/start_qnodeos.py | 18 ++- simulaqron/start/start_vnode.py | 4 +- simulaqron/toolbox/stabilizer_states.py | 13 +- simulaqron/virtual_node/basics.py | 1 + simulaqron/virtual_node/quantum.py | 7 +- simulaqron/virtual_node/qutip_simulator.py | 4 +- simulaqron/virtual_node/virtual.py | 8 +- 21 files changed, 180 insertions(+), 169 deletions(-) diff --git a/simulaqron/_default_config/default_network.json b/simulaqron/_default_config/default_network.json index 823c574b..709ac04c 100644 --- a/simulaqron/_default_config/default_network.json +++ b/simulaqron/_default_config/default_network.json @@ -1,77 +1,77 @@ { - "default": { - "nodes": { - "Alice": { - "app_socket": [ - "localhost", - 8001 - ], - "qnodeos_socket": [ - "localhost", - 8002 - ], - "vnode_socket": [ - "localhost", - 8003 - ] - }, - "Bob": { - "app_socket": [ - "localhost", - 8010 - ], - "qnodeos_socket": [ - "localhost", - 8011 - ], - "vnode_socket": [ - "localhost", - 8012 - ] - }, - "Charlie": { - "app_socket": [ - "localhost", - 8020 - ], - "qnodeos_socket": [ - "localhost", - 8021 - ], - "vnode_socket": [ - "localhost", - 8022 - ] - }, - "David": { - "app_socket": [ - "localhost", - 8030 - ], - "qnodeos_socket": [ - "localhost", - 8031 - ], - "vnode_socket": [ - "localhost", - 8032 - ] - }, - "Eve": { - "app_socket": [ - "localhost", - 8033 - ], - "qnodeos_socket": [ - "localhost", - 8034 - ], - "vnode_socket": [ - "localhost", - 8035 - ] - } - }, - "topology": null - } + "default": { + "nodes": { + "Alice": { + "app_socket": [ + "localhost", + 8001 + ], + "qnodeos_socket": [ + "localhost", + 8002 + ], + "vnode_socket": [ + "localhost", + 8003 + ] + }, + "Bob": { + "app_socket": [ + "localhost", + 8010 + ], + "qnodeos_socket": [ + "localhost", + 8011 + ], + "vnode_socket": [ + "localhost", + 8012 + ] + }, + "Charlie": { + "app_socket": [ + "localhost", + 8020 + ], + "qnodeos_socket": [ + "localhost", + 8021 + ], + "vnode_socket": [ + "localhost", + 8022 + ] + }, + "David": { + "app_socket": [ + "localhost", + 8030 + ], + "qnodeos_socket": [ + "localhost", + 8031 + ], + "vnode_socket": [ + "localhost", + 8032 + ] + }, + "Eve": { + "app_socket": [ + "localhost", + 8033 + ], + "qnodeos_socket": [ + "localhost", + 8034 + ], + "vnode_socket": [ + "localhost", + 8035 + ] + } + }, + "topology": null + } } \ No newline at end of file diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index a310e7de..c6604af0 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -29,10 +29,10 @@ import socket import struct +from ipaddress import IPv4Address from typing import Dict, List from twisted.spread import pb -from ipaddress import IPv4Address from simulaqron.settings.network_config import NetworkConfigBuilder diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index a2b4f57e..69141912 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -30,10 +30,10 @@ import time from netqasm.logging.glob import get_netqasm_logger -from twisted.spread import pb from twisted.internet import error from twisted.internet.defer import DeferredList from twisted.internet.error import ReactorNotRunning +from twisted.spread import pb from simulaqron.reactor import reactor @@ -118,7 +118,6 @@ def setup_local(myName, virtualNet, classicalNet, lNode, func, *args, **kwargs): def init_register(resList, myName, virtualNet, classicalNet, lNode, func, *args, **kwargs): - _logger.debug("LOCAL %s: All connections set up.", myName) # Retrieve the connection to the local virtual node, if successfull diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index c0e3dba4..e55e9c83 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -833,7 +833,7 @@ def get_qubit_state( class VirtualQubitRef: - def __init__(self, qubit_id: int, timestamp: int , virt: pb.Referenceable): + def __init__(self, qubit_id: int, timestamp: int, virt: pb.Referenceable): self.qubit_id = qubit_id self.timestamp = timestamp self.virt = virt diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index bfbcdec4..ec0d34ee 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -40,7 +40,6 @@ from simulaqron.netqasm_backend.qnodeos import SubroutineHandler from simulaqron.sdk.connection import RichErrorMessage from simulaqron.settings import simulaqron_settings -from simulaqron.settings.network_config import NetworkConfigBuilder from simulaqron.virtual_node.virtual import call_method @@ -118,8 +117,8 @@ def log_handled_message(self, result): def log_error(self, failure): self._logger.error("Handling message failed with failure = %s", failure.value) self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(failure.value))) - #self.transport.abortConnection() - #yield None + # self.transport.abortConnection() + # yield None yield deferLater(reactor, 0.1, self.stop) def stop(self): diff --git a/simulaqron/network.py b/simulaqron/network.py index 9ded0ce3..1ef3606f 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -27,21 +27,23 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import time import random -import networkx as nx +import time from os import PathLike -from multiprocess.context import ForkProcess as Process -from typing import List, Optional, Dict, Any from timeit import default_timer as timer +from typing import List, Optional, Dict, Any +import networkx as nx +from multiprocess.context import ForkProcess as Process from netqasm.logging.glob import get_netqasm_logger, get_log_level -from simulaqron.settings.network_config import NetworkConfigBuilder from simulaqron.settings import simulaqron_settings from simulaqron.start import start_vnode, start_qnodeos +# WARNING - this import *needs* to be after importing start_vnode and start_qnodeos +# Otherwise the code that patches some netqasm internal definitions will not work correctly! from simulaqron.sdk import SimulaQronConnection + ######################################################################################### # Network class, sets up (part of) a simulated network. # # The processes consisting of the network are killed when the object goes out of scope. # @@ -219,7 +221,7 @@ def construct_topology_config(topology, nodes): elif topology == "complete": adjacency_dct = {} for i, node in enumerate(nodes): - adjacency_dct[node] = nodes[:i] + nodes[i + 1 :] + adjacency_dct[node] = nodes[:i] + nodes[i + 1:] elif topology == "ring": adjacency_dct = {} diff --git a/simulaqron/reactor.py b/simulaqron/reactor.py index c9633ac4..1e7a860b 100644 --- a/simulaqron/reactor.py +++ b/simulaqron/reactor.py @@ -1,4 +1,5 @@ from twisted.internet import asyncioreactor + asyncioreactor.install() from twisted.internet import reactor # noqa: E402 diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 6de0bafa..943aee5f 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -2,15 +2,14 @@ import os import signal import time - -from multiprocess.context import ForkContext as ProcessContext -from multiprocess.pool import ApplyResult from importlib import reload from importlib.util import find_spec from os import PathLike from pathlib import Path from typing import Callable, Optional, Any, Dict, List, Union, Tuple +from multiprocess.context import ForkContext as ProcessContext +from multiprocess.pool import ApplyResult from multiprocess.sharedctypes import SynchronizedArray from netqasm.logging.glob import get_netqasm_logger from netqasm.logging.output import (reset_struct_loggers, @@ -23,6 +22,7 @@ from netqasm.sdk.config import LogConfig from netqasm.sdk.shared_memory import SharedMemoryManager from netqasm.util.yaml import dump_yaml + from simulaqron.network import Network from simulaqron.sdk import SimulaQronConnection from simulaqron.settings import simulaqron_settings @@ -59,7 +59,7 @@ def reset(save_loggers=False): def setup_sim_backend(sim_backend: SimBackend): if sim_backend in [SimBackend.PROJECTQ, SimBackend.QUTIP]: - assert find_spec(sim_backend.value) is not None,\ + assert find_spec(sim_backend.value) is not None, \ f"To use {sim_backend} as backend you need to install the package" simulaqron_settings.sim_backend = sim_backend @@ -79,6 +79,7 @@ def configure_network(node_names: List[str], network_config_file: Optional[str]) # Note; this array *will not* store the pids of the QNodeOS and/or Vnode processes apps_pids: Optional[SynchronizedArray] = None + def _worker_initializer(synced_array: SynchronizedArray): # We simply store the reference of the synced object for this process global apps_pids @@ -117,17 +118,17 @@ def _signal_other_apps(): def run_applications( - app_instance: ApplicationInstance, - num_rounds: int = 1, - network_cfg: Union[str, PathLike, Path] = None, # WARNING - The type of this argument *cannot* be harmonized - nv_cfg: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" - log_cfg: LogConfig = None, - formalism: Formalism = Formalism.KET, - use_app_config: bool = True, - post_function: Optional[Callable] = None, - enable_logging: bool = True, - hardware: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" - init_func: Callable = None, + app_instance: ApplicationInstance, + num_rounds: int = 1, + network_cfg: Union[str, PathLike, Path] = None, # WARNING - The type of this argument *cannot* be harmonized + nv_cfg: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" + log_cfg: LogConfig = None, + formalism: Formalism = Formalism.KET, + use_app_config: bool = True, + post_function: Optional[Callable] = None, + enable_logging: bool = True, + hardware: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" + init_func: Callable = None, ) -> List[Dict[str, Any]]: """Executes functions containing quantum applications. @@ -210,7 +211,7 @@ def run_applications( # Create the executor pool process_ctx = ProcessContext() - synced_array = process_ctx.Array('i', len(app_instance.app.programs)) + synced_array = process_ctx.Array('i', len(app_instance.app.programs)) executor = process_ctx.Pool( processes=len(app_names) + 3, initializer=_worker_initializer, diff --git a/simulaqron/sdk/broadcast_channel.py b/simulaqron/sdk/broadcast_channel.py index 10feb685..fbbff85e 100644 --- a/simulaqron/sdk/broadcast_channel.py +++ b/simulaqron/sdk/broadcast_channel.py @@ -1,4 +1,5 @@ from netqasm.sdk.classical_communication.broadcast_channel import BroadcastChannelBySockets + from .socket import Socket diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 8f842c77..0883f6b5 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -2,10 +2,9 @@ import socket import time from enum import Enum - -from multiprocess.pool import Pool from typing import Type, Optional, Callable, List, Tuple, Set, Dict +from multiprocess.pool import Pool from netqasm.backend.messages import (MessageHeader, MsgDoneMessage, ReturnArrayMessage, ReturnRegMessage, ReturnMessage, deserialize_return_msg, @@ -483,7 +482,7 @@ def __init__(self, qubit_id: int, real_part: List[List[float]], imag_part: List[ self.qubit_id = qubit_id self.dim = len(real_part) - if self.dim > MAX_QUBIT_STATE_LEN : + if self.dim > MAX_QUBIT_STATE_LEN: logger.warning("Return qubit state message too long") for i in range(self.dim): for j in range(self.dim): @@ -491,7 +490,7 @@ def __init__(self, qubit_id: int, real_part: List[List[float]], imag_part: List[ self.imag_part[i][j] = imag_part[i][j] @property - def dimension(self) -> int : + def dimension(self) -> int: return self.dim def get_real_part(self) -> List[List[float]]: diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index 0f19b100..b62d932a 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -1,29 +1,29 @@ -import dill import socket import time from typing import Optional +import dill from netqasm.logging.glob import get_netqasm_logger from netqasm.sdk.classical_communication.message import StructuredMessage from netqasm.sdk.classical_communication.socket import Socket as _Socket + from simulaqron.general.host_config import SocketsConfig, Host from simulaqron.settings import simulaqron_settings class Socket(_Socket): - RETRY_TIME = 0.2 MAX_RETRIES = 20 def __init__( - self, - app_name: str, - remote_app_name: str, - socket_id: int = 0, - timeout: Optional[int] = None, - use_callbacks=False, - network_name="default", - log_config=None, + self, + app_name: str, + remote_app_name: str, + socket_id: int = 0, + timeout: Optional[int] = None, + use_callbacks=False, + network_name="default", + log_config=None, ): assert socket_id == 0, ( "SimulaQron socket does not support setting socket ID, this is instead done in the config file" @@ -75,10 +75,10 @@ def _base_recv(self, block: bool, timeout: float, maxsize: int) -> bytes: return raw_msg def recv( - self, - block: bool = True, - timeout: Optional[float] = None, - maxsize: Optional[int] = 1024 + self, + block: bool = True, + timeout: Optional[float] = None, + maxsize: Optional[int] = 1024 ) -> str: """Receive a message from the remote node.""" self._logger.debug("Receiving msg") @@ -88,10 +88,10 @@ def recv( return msg def recv_structured( - self, - block: bool = True, - timeout: Optional[float] = None, - maxsize: Optional[int] = 1024, + self, + block: bool = True, + timeout: Optional[float] = None, + maxsize: Optional[int] = 1024, ) -> StructuredMessage: self._logger.debug("Receiving structured msg") raw_msg = self._base_recv(block, timeout, maxsize) @@ -100,10 +100,10 @@ def recv_structured( return msg def recv_silent( - self, - block: bool = True, - timeout: Optional[float] = None, - maxsize: Optional[int] = None, + self, + block: bool = True, + timeout: Optional[float] = None, + maxsize: Optional[int] = None, ) -> str: return self.recv() @@ -163,7 +163,7 @@ def _connect(self) -> socket.socket: while True: attempt += 1 try: - #app_socket.settimeout(self._timeout) + # app_socket.settimeout(self._timeout) app_socket.connect(addr[4]) break except ConnectionRefusedError as err: diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index 1d68690d..96b12c30 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -2,7 +2,6 @@ from .network_config import NetworkConfigBuilder from .simulaqron_config import SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME - init_serialization() # Centralized way to store the config. It reads the local configuration diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 2b1e1e76..74ba728b 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -1,22 +1,22 @@ #!/usr/bin/env python3 +import importlib.metadata as metadata +import logging import time +from pathlib import Path from typing import Optional, Callable import click -import logging -from daemons.prefab import run from daemons.interfaces import exit -from pathlib import Path -import importlib.metadata as metadata +from daemons.prefab import run from simulaqron.network import Network from simulaqron.settings import simulaqron_settings -from simulaqron.settings.simulaqron_config import SimBackend, DEFAULT_SIMULAQRON_SETTINGS_FILENAME from simulaqron.settings.network_config import NetworkConfigBuilder +from simulaqron.settings.simulaqron_config import SimBackend, DEFAULT_SIMULAQRON_SETTINGS_FILENAME CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) # PID folder should be "LOCAL" -PID_FOLDER = Path.home() / ".simulaqron_pids" +PID_FOLDER = Path.home() / ".simulaqron_pids" # If the pid folder does not exist, create it if not PID_FOLDER.exists(): @@ -129,7 +129,6 @@ def version(): "If you want to supress this question, use the --force/-f flag.", is_flag=True, ) - def start(name: str, nrnodes: Optional[int], nodes: Optional[str], topology: Optional[str], force: Optional[bool], keep: Optional[bool]): """Starts a network with the given parameters or from config files.""" @@ -153,12 +152,13 @@ def start(name: str, nrnodes: Optional[int], nodes: Optional[str], try: d.start() except SystemExit as e: - if e.code == exit.PIDFILE_INACCESSIBLE or\ - e.code == exit.DAEMONIZE_FAILED: + if e.code == exit.PIDFILE_INACCESSIBLE or \ + e.code == exit.DAEMONIZE_FAILED: logging.debug(f"Failed to launch Simulaqron Daemon. " f"Exit code reported by daemons: {e.code}") print("Failed to launch SimulaQron Daemon. Aborted!") + ############### # stop command # ############### @@ -182,6 +182,7 @@ def stop(name: Optional[str]): d = SimulaQronDaemon(pidfile=pidfile) d.stop() + ################# # reset command # ################# @@ -214,6 +215,7 @@ def reset(force: Optional[bool]): else: print("Aborting!") + def updates_local_config(command_function: Callable): def wrapper(*args, **kwargs): local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME @@ -225,6 +227,7 @@ def wrapper(*args, **kwargs): simulaqron_settings.load_from_file(local_settings) command_function(*args, **kwargs) simulaqron_settings.save_to_file(local_settings) + return wrapper @@ -327,6 +330,7 @@ def t1(value): """The effective T1 to be used for noisy qubits""" simulaqron_settings.t1 = value + ############### # get command # ############### @@ -339,6 +343,7 @@ def wrapper(*args, **kwargs): else: simulaqron_settings.default_settings() command_function(*args, **kwargs) + return wrapper @@ -420,6 +425,7 @@ def t1(): """The effective T1 to be used for noisy qubits""" print(simulaqron_settings.t1) + ############### # node command # ############### @@ -463,7 +469,8 @@ def nodes(): help="Force re-write of network_config_file.\n", is_flag=True, ) -def add(name: Optional[str], network_name: Optional[str], hostname=None, app_port=None, qnodeos_port=None, vnode_port=None, neighbors=None, +def add(name: Optional[str], network_name: Optional[str], hostname=None, app_port=None, qnodeos_port=None, + vnode_port=None, neighbors=None, force=False): """ Add a node to the network. diff --git a/simulaqron/start/__init__.py b/simulaqron/start/__init__.py index 8d2d236c..8189fcc8 100644 --- a/simulaqron/start/__init__.py +++ b/simulaqron/start/__init__.py @@ -1,2 +1,2 @@ -from .start_vnode import start_vnode from .start_qnodeos import start_qnodeos +from .start_vnode import start_vnode diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index a3f8bf1b..86b30c96 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -1,16 +1,15 @@ #!/usr/bin/env python import logging import os +import signal import sys import time -import signal from timeit import default_timer as timer +from netqasm.logging.glob import get_netqasm_logger, set_log_level from twisted.internet.error import ConnectionRefusedError, CannotListenError from twisted.spread import pb -from netqasm.logging.glob import get_netqasm_logger, set_log_level - from simulaqron.reactor import reactor from simulaqron.netqasm_backend.factory import NetQASMFactory from simulaqron.netqasm_backend.qnodeos import SubroutineHandler @@ -32,7 +31,7 @@ def init_register(virt_root, my_name: str, node: NetQASMFactory): setup_netqasm_server(my_name, node) -def connect_to_virt_node(my_name: str , netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig): +def connect_to_virt_node(my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig): """Tries to connect to local virtual node. If connection is refused, we try again after a set amount of time @@ -50,10 +49,12 @@ def connect_to_virt_node(my_name: str , netqasm_factory: NetQASMFactory, virtual # If connection succeeds do: defer_virtual_node.addCallback(init_register, my_name, netqasm_factory) # If connection fails do: - defer_virtual_node.addErrback(handle_connection_error, my_name, netqasm_factory, virtual_network, virtual_node.hostname, virtual_node.port) + defer_virtual_node.addErrback(handle_connection_error, my_name, netqasm_factory, virtual_network, + virtual_node.hostname, virtual_node.port) -def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig, virtual_node_hostname: str, virtual_node_port: int): +def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig, + virtual_node_hostname: str, virtual_node_port: int): """ Handles errors from trying to connect to local virtual node. If a ConnectionRefusedError is raised another try will be made after @@ -63,7 +64,8 @@ def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactor reason.raiseException() except ConnectionRefusedError as err: # TODO - Implement checking of max number of connections - logger.debug("LOCAL %s: Could not connect to Virtual node (%s, %d), trying again...", my_name, virtual_node_hostname, virtual_node_port, exc_info=err) + logger.debug("LOCAL %s: Could not connect to Virtual node (%s, %d), trying again...", my_name, + virtual_node_hostname, virtual_node_port, exc_info=err) reactor.callLater( simulaqron_settings.conn_retry_time, connect_to_virt_node, @@ -108,8 +110,10 @@ def setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): else: reactor.stop() + stdout_file = None + def sigterm_handler(_signo, _stack_frame): global stdout_file stdout_file.flush() diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 135d6da8..634a8764 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -29,15 +29,15 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import logging import os -import sys import signal +import sys from functools import partial from netqasm.logging.glob import get_netqasm_logger, set_log_level +from simulaqron.reactor import reactor from simulaqron.virtual_node.virtual import Backend from simulaqron.settings import simulaqron_settings -from simulaqron.reactor import reactor logger = get_netqasm_logger("start_vnode") diff --git a/simulaqron/toolbox/stabilizer_states.py b/simulaqron/toolbox/stabilizer_states.py index b2828adf..45ee33df 100644 --- a/simulaqron/toolbox/stabilizer_states.py +++ b/simulaqron/toolbox/stabilizer_states.py @@ -7,10 +7,11 @@ # ########################################################################################## -import numpy as np +from random import randint + import networkx as nx +import numpy as np from scipy.linalg import block_diag -from random import randint class StabilizerState: @@ -495,9 +496,9 @@ def tensor_product(self, other): return self else: this_X_stab = self._group[:, : self.num_qubits] - this_Z_stab = self._group[:, self.num_qubits : -1] + this_Z_stab = self._group[:, self.num_qubits: -1] other_X_stab = other._group[:, : other.num_qubits] - other_Z_stab = other._group[:, other.num_qubits : -1] + other_Z_stab = other._group[:, other.num_qubits: -1] new_X_stab = block_diag(this_X_stab, other_X_stab) new_Z_stab = block_diag(this_Z_stab, other_Z_stab) @@ -736,7 +737,7 @@ def measure(self, position, inplace=False): if not inplace: # Simply remove first generator and columns for X and Z of this qubit X_part = tmp_matrix[1:n, 1:n] - Z_part_and_phase = tmp_matrix[1:n, n + 1 :] + Z_part_and_phase = tmp_matrix[1:n, n + 1:] self._group = np.concatenate((X_part, Z_part_and_phase), 1) self._nr_rows = n - 1 else: @@ -843,7 +844,7 @@ def find_SQC_equiv_graph_state(self, return_operations=False): operations.append(("Z", j)) # Spp is now in the form of (I,Gamma) where Gamma is the adj mat of the Graph # SQC equivalent to the stabilizer state. - adj_mat = Spp.to_array()[:, n : 2 * n] + adj_mat = Spp.to_array()[:, n: 2 * n] G = nx.from_numpy_array(adj_mat) if return_operations: diff --git a/simulaqron/virtual_node/basics.py b/simulaqron/virtual_node/basics.py index 02f0a2d5..06fe9d07 100644 --- a/simulaqron/virtual_node/basics.py +++ b/simulaqron/virtual_node/basics.py @@ -28,6 +28,7 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import abc + from twisted.spread import pb diff --git a/simulaqron/virtual_node/quantum.py b/simulaqron/virtual_node/quantum.py index 59b775c2..07c0ea11 100644 --- a/simulaqron/virtual_node/quantum.py +++ b/simulaqron/virtual_node/quantum.py @@ -30,12 +30,11 @@ import time import numpy as np -from twisted.spread import pb +from netqasm.logging.glob import get_netqasm_logger from twisted.internet.defer import DeferredLock -from twisted.internet.task import deferLater from twisted.internet.defer import inlineCallbacks - -from netqasm.logging.glob import get_netqasm_logger +from twisted.internet.task import deferLater +from twisted.spread import pb from simulaqron.reactor import reactor import simulaqron.settings as settings diff --git a/simulaqron/virtual_node/qutip_simulator.py b/simulaqron/virtual_node/qutip_simulator.py index 2023badd..e6cc293c 100644 --- a/simulaqron/virtual_node/qutip_simulator.py +++ b/simulaqron/virtual_node/qutip_simulator.py @@ -26,12 +26,12 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import math import cmath +import logging +import math from typing import Tuple, List import numpy as np -import logging try: import qutip as qp diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 627eb79c..b13ce3af 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -28,17 +28,15 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import importlib import random - from collections import deque from typing import Tuple, List -from twisted.spread import pb +from netqasm.logging.glob import get_netqasm_logger from twisted.internet.defer import inlineCallbacks, DeferredLock, Deferred, DeferredList -from twisted.internet.task import deferLater from twisted.internet.error import ConnectionRefusedError, CannotListenError +from twisted.internet.task import deferLater +from twisted.spread import pb from twisted.spread.pb import RemoteError, RemoteReference - -from netqasm.logging.glob import get_netqasm_logger from typing_extensions import Generator from simulaqron.virtual_node.basics import QuantumError, NoQubitError, VirtNetError From b6f3ddbaba7f7b5226de5b50030a86a7afa3d071 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 11 Nov 2025 14:31:34 +0100 Subject: [PATCH 100/277] Fix creation of config files and their parent folders when they don't exist --- simulaqron/settings/network_config.py | 12 +++++++++++- simulaqron/settings/simulaqron_config.py | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 9c240a1c..6aa9e118 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -287,8 +287,18 @@ def write_to_file(self, file_path: PathLike | str): if file_path is None: raise ValueError("Since this networks config was not initialized with a file_path you need to specify one") + # Create the Path object + file_path = Path(str(file_path)).resolve() + + # Create all the parent folder if they not exists + if not file_path.parent.exists(): + file_path.parent.mkdir(parents=True) + + # Poke the file, so it exists before opening + file_path.touch(exist_ok=True) + dictionary = self.to_dict() - with open(file_path, 'w') as f: + with file_path.open('wt') as f: json.dump(dictionary, f, indent=4) def read_from_file(self, file_path: PathLike | str): diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 9f7d2f60..988703a9 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -158,6 +158,14 @@ def default_settings(self): def save_to_file(self, path: PathLike): file_path = Path(str(path)).resolve() + + # Create all the parent folder if they not exists + if not file_path.parent.exists(): + file_path.parent.mkdir(parents=True) + + # Poke the file, so it exists before opening + file_path.touch(exist_ok=True) + with file_path.open("wt") as file: serialized = JSONSerializer.serialize(self) json.dump(serialized, file, indent=4) From 37b8729f1e990f8d5f07206ba34f57aef4a3768f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 12 Nov 2025 14:17:26 +0100 Subject: [PATCH 101/277] Update netqasm dependency to include bugfix of meas_basis/mov instructions --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dfe6408f..33abdc75 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dependencies = [ "networkx>=3.0,<4.0", "click>=8.0,<9.0", "daemons>=1.3.0,<2.0.0", - "netqasm>=2.0.2,<3.0.0", + "netqasm>=2.1.1,<3.0.0", "multiprocess>=0.70.18,<1.0" ] requires-python = ">=3.8,<3.13" From d6f8d8c28b671c4989ba4bd81f79f7bc0f52272d Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 21 Nov 2025 12:53:40 +0100 Subject: [PATCH 102/277] Fix bug when loading new simulaqron configs from a file --- simulaqron/settings/simulaqron_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 988703a9..7632c944 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -122,6 +122,8 @@ def load_from_file(self, file_path: Path | str): new_val = getattr(new_config, field.name) setattr(self, field.name, new_val) + self.network_config_file = new_config.network_config_file + @classmethod def _deserialize_from_file(cls, file_path: Path) -> Self: with file_path.resolve().open("rt") as file: From f71604ae1839006d0bab852b77613c35e948acc9 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Sun, 23 Nov 2025 20:57:36 +0100 Subject: [PATCH 103/277] Mayor refactoring in the network_config classes --- simulaqron/network.py | 41 ++-- simulaqron/settings/network_config.py | 258 +++++++++++++------------- 2 files changed, 153 insertions(+), 146 deletions(-) diff --git a/simulaqron/network.py b/simulaqron/network.py index 1ef3606f..75e461dd 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -56,6 +56,7 @@ def __init__(self, name: str = "default", nodes: Optional[List[str]] = None, force: bool = False, new: bool = True): """ Used to spin up a simulated network. + This class uses the already-loaded network configuration, and *makes no change to it*. If new=True then a fresh network with only the specified nodes (or the default Alice, Bob, Charlie, David and Eve) are created and overwriting the current network with @@ -205,7 +206,9 @@ def __str__(self): return f"Network '{self.name}', procs: {self.processes}" -def construct_topology_config(topology, nodes): +# Helper functions to build topologies + +def construct_topology_config(topology: str | Dict | None, nodes: List[str]) -> Optional[Dict[str, List[str]]]: """ Constructs a json file at config/topology.json, used to define the topology of the network. @@ -215,22 +218,22 @@ def construct_topology_config(topology, nodes): List of the names of the nodes. :return: None """ - if topology is not None: - if isinstance(topology, dict): + if isinstance(topology, str): + # Trick to get the integer after "random_connected": split on that string + topology = topology.split("random_connected") + + adjacency_dct = {} + match topology: + case dict() | None: return topology - elif topology == "complete": - adjacency_dct = {} + case ["complete"]: for i, node in enumerate(nodes): adjacency_dct[node] = nodes[:i] + nodes[i + 1:] - - elif topology == "ring": - adjacency_dct = {} + case ["ring"]: nn = len(nodes) for i, node in enumerate(nodes): adjacency_dct[node] = [nodes[(i - 1) % nn], nodes[(i + 1) % nn]] - - elif topology == "path": - adjacency_dct = {} + case ["path"]: nn = len(nodes) for i, node in enumerate(nodes): if i == 0: @@ -239,13 +242,12 @@ def construct_topology_config(topology, nodes): adjacency_dct[node] = [nodes[i - 1]] else: adjacency_dct[node] = [nodes[(i - 1) % nn], nodes[(i + 1) % nn]] - - elif topology == "random_tree": + case ["random_tree"]: adjacency_dct = get_random_tree(nodes) - - elif topology[:16] == "random_connected": + case ["", raw_nr_edges]: + # Here the "randon_connected" matches, and we also get the raw # of edges try: - nr_edges = int(topology[17:]) + nr_edges = int(raw_nr_edges) except ValueError: raise ValueError( "When specifying a random connected graph use the format 'random_connected_{nr_edges}'," @@ -257,12 +259,9 @@ def construct_topology_config(topology, nodes): "where 'nr_edges' is the number of edges of the graph." ) adjacency_dct = get_random_connected(nodes, nr_edges) - - else: + case _: raise ValueError("Unknown topology name") - return adjacency_dct - else: - return None + return adjacency_dct def get_random_tree(nodes): diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 6aa9e118..31c26224 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -1,11 +1,12 @@ import json import socket from contextlib import closing -from dataclasses import dataclass +from dataclasses import dataclass, field from importlib import resources from os import PathLike from pathlib import Path from typing import Optional, Self, Dict, List, Tuple, Any +from dataclasses_serialization.json import JSONSerializer import simulaqron._default_config @@ -13,61 +14,52 @@ @dataclass class NodeConfig: """ - Used by _NetworkConfig to keep track of the config of a single node. + Used by NetworkConfig to keep track of the config of a single node. """ name: str - app_hostname: Optional[str] - qnodeos_hostname: Optional[str] - vnode_hostname: Optional[str] - app_port: Optional[int] - qnodeos_port: Optional[int] - vnode_port: Optional[int] - - def to_dict(self) -> Dict[str, List[str | int | None]]: - """ - Constructs a dictionary with all the config of this node. - :return: dict - """ - return { - "app_socket": [self.app_hostname, self.app_port], - "qnodeos_socket": [self.qnodeos_hostname, self.qnodeos_port], - "vnode_socket": [self.vnode_hostname, self.vnode_port] - } + app_port: int + qnodeos_port: int + vnode_port: int + app_hostname: str = "localhost" + qnodeos_hostname: str = "localhost" + vnode_hostname: str = "localhost" -# @dataclass +@dataclass class NetworkConfig: - def __init__(self): - """ - Used by NetworksConfigConstructor to keep track of the config of a single network. - """ - self.topology: Optional[Dict[str, List[str]]] = None - self.nodes: Dict[str, NodeConfig] = {} + """ + Used by NetworksConfigConstructor to keep track of the config of a single network. + """ + + name: str + topology: Optional[Dict[str, List[str]]] = None + nodes: Dict[str, NodeConfig] = field(default_factory=dict) def add_node( - self, name: str, app_hostname: Optional[str] = None, qnodeos_hostname: Optional[str] = None, - vnode_hostname: Optional[str] = None, app_port: Optional[int] = None, qnodeos_port: Optional[int] = None, - vnode_port: Optional[int] = None, neighbors: Optional[List[str]] = None, + self, name: str, + app_hostname: str, qnodeos_hostname: str, vnode_hostname: str, + app_port: int, qnodeos_port: int, vnode_port: int, + neighbors: Optional[List[str]] = None, ): """ - Adds a node with the given name to a network (default: "default"). - If hostnames are None they will default to 'localhost'. + Adds a node with the given name to the network + If hostnames are not given they will default to 'localhost'. If the port numbers None, unused ones will be chosen between 8000 and 9000. If neighbors are specified a restricted topology can be constructed (default is fully connected). :param name: str Name of the node, e.g. Alice - :param app_hostname: str or None - Hostname, e.g. localhost (default) or 192.168.0.1 - :param qnodeos_hostname: str or None - Hostname, e.g. localhost (default) or 192.168.0.1 - :param vnode_hostname: str or None - Hostname, e.g. localhost (default) or 192.168.0.1 - :param app_port: int or None + :param app_hostname: str + Hostname (e.g. localhost) or IP address (e.g. 192.168.0.1) + :param qnodeos_hostname: str + Hostname (e.g. localhost) or IP address (e.g. 192.168.0.1) + :param vnode_hostname: str + Hostname (e.g. localhost) or IP address (e.g. 192.168.0.1) + :param app_port: int Port number for the application - :param qnodeos_port: int or None + :param qnodeos_port: int Port number for the qnodeos server - :param vnode_port: int or None + :param vnode_port: int Port number for the virtual node :param neighbors: (list of str) or None A list of neighbors, of this node. @@ -94,22 +86,17 @@ def add_node( vnode_port=vnode_port, ) - def to_dict(self) -> Dict[str, Any]: - """ - Constructs a dictionary with all the config of this network. - :return: dict - """ - nodes = {node_name: node.to_dict() for node_name, node in self.nodes.items()} - return {"nodes": nodes, "topology": self.topology} - + def remove_node(self, node_name: str): + self.nodes.pop(node_name, None) +@dataclass class NetworkConfigBuilder: - def __init__(self): - """ - Used to construct the config file of networks. - """ - self.networks: Dict[str, NetworkConfig] = {} - self.used_sockets: List[Tuple[str, int]] = [] + """ + Used to construct the config file of networks. + """ + + networks: Dict[str, NetworkConfig] = field(default_factory=dict) + used_sockets: List[Tuple[str, int]] = field(default_factory=list) @classmethod def using_default_network(cls) -> Self: @@ -119,10 +106,30 @@ def using_default_network(cls) -> Self: new_builder.read_from_file(Path(str(default_network_path))) return new_builder - def add_node(self, node_name: str, network_name: str = "default", app_hostname: Optional[str] = None, - qnodeos_hostname: Optional[str] = None, vnode_hostname: Optional[str] = None, - app_port: Optional[int] = None, qnodeos_port: Optional[int] = None, - vnode_port: Optional[int] = None, neighbors: List[str] = None): + def _correct_network_port_if_needed(self, hostname: str, port: int) -> int: + """ + Checks if the given port is valid (>0) and if it is free. If not, it will + allocate a new port in the range 8000-9000 which is free, and hence can be used + to listen to new connections + Args: + hostname: str + The hostname to test the port on. + port: int + The port number to test if it is usable + Returns: + A port number which is guaranteed to be valid, and ready to be used + to listen to connections on. + """ + if port < -1: + port = self._get_unused_port(hostname) + if not self._check_port_available(hostname, port): + raise ValueError(f"Socket address combination ({hostname}, {port}) is already in use.") + return port + + def add_node(self, node_name: str, network_name: str = "default", app_hostname: str = "localhost", + qnodeos_hostname: str = "localhost", vnode_hostname: str = "localhost", + app_port: int = -1, qnodeos_port: int = -1, + vnode_port: int = -1, neighbors: Optional[List[str]] = None): """ Adds a node with the given name to a network (default: "default"). If hostnames are None they will default to 'localhost'. @@ -133,85 +140,77 @@ def add_node(self, node_name: str, network_name: str = "default", app_hostname: Name of the node, e.g. Alice :param network_name: str Name of the network (default: "default") - :param app_hostname: str or None - Hostname, e.g. localhost (default) or 192.168.0.1 - :param qnodeos_hostname: str or None - Hostname, e.g. localhost (default) or 192.168.0.1 - :param vnode_hostname: str or None - Hostname, e.g. localhost (default) or 192.168.0.1 - :param app_port: int or None - Port number for the application - :param qnodeos_port: int or None - Port number for the qnodeos server - :param vnode_port: int or None - Port number for the virtual node + :param app_hostname: str + Hostname, e.g. localhost (the default if not given) or 192.168.0.1 + :param qnodeos_hostname: str + Hostname, e.g. localhost (the default if not given) or 192.168.0.1 + :param vnode_hostname: str + Hostname, e.g. localhost (the default if not given) or 192.168.0.1 + :param app_port: int + Port number for the application. A free port in the range 8000-9000 will be allocated if not given + :param qnodeos_port: int + Port number for the application. A free port in the range 8000-9000 will be allocated if not given + :param vnode_port: int + Port number for the application. A free port in the range 8000-9000 will be allocated if not given :param neighbors: (list of str) or None A list of neighbors, of this node. If None all current nodes in the network will be adjacent to the added node. :return: None """ - socket_addresses = [(app_hostname, app_port), (qnodeos_hostname, qnodeos_port), (vnode_hostname, vnode_port)] - for i, socket_address in enumerate(socket_addresses): - hostname, port = socket_address - if hostname is None: - hostname = "localhost" - if port is None: - port = self._get_unused_port(hostname) - else: - free = self._check_port_available(hostname, port) - if not free: - raise ValueError(f"Cannot add node {node_name}, since socket address " - f"({hostname}, {port}) is already in use.") - socket_address = (hostname, port) - self.used_sockets.append(socket_address) - socket_addresses[i] = socket_address - - app_hostname, app_port = socket_addresses[0] - qnodeos_hostname, qnodeos_port = socket_addresses[1] - vnode_hostname, vnode_port = socket_addresses[2] - if network_name in self.networks: - self.networks[network_name].add_node( - name=node_name, - app_hostname=app_hostname, - qnodeos_hostname=qnodeos_hostname, - vnode_hostname=vnode_hostname, - app_port=app_port, - qnodeos_port=qnodeos_port, - vnode_port=vnode_port, - neighbors=neighbors, - ) - else: - network = NetworkConfig() - network.add_node(name=node_name, app_hostname=app_hostname, qnodeos_hostname=qnodeos_hostname, - vnode_hostname=vnode_hostname, app_port=app_port, qnodeos_port=qnodeos_port, - vnode_port=vnode_port, neighbors=neighbors) - self.networks[network_name] = network + + try: + # Process app hostname/port + app_port = self._correct_network_port_if_needed(app_hostname, app_port) + self.used_sockets.append((app_hostname, app_port)) + + # Process qnodeos hostname/port + qnodeos_port = self._correct_network_port_if_needed(qnodeos_hostname, qnodeos_port) + self.used_sockets.append((qnodeos_hostname, qnodeos_port)) + + # Process qnodeos hostname/port + vnode_port = self._correct_network_port_if_needed(vnode_hostname, vnode_port) + self.used_sockets.append((vnode_hostname, vnode_port)) + except ValueError as e: + raise ValueError(f"Cannot add node {node_name}", e) + + if network_name not in self.networks: + # network doesn't exist, create a new one + network = NetworkConfig(network_name) + + # At this point, we are sure that the network exists in self.networks + network = self.networks[network_name] + network.add_node(name=node_name, + app_hostname=app_hostname, + qnodeos_hostname=qnodeos_hostname, + vnode_hostname=vnode_hostname, + app_port=app_port, + qnodeos_port=qnodeos_port, + vnode_port=vnode_port, + neighbors=neighbors) def remove_node(self, node_name: str, network_name: str = "default"): """ Removes a node from the network. :param node_name: str - Name of the node, e.g. Alice + Name of the node to remove, e.g. Alice :param network_name: str - Name of the network (default: "default") + Name of the network to delete the node from (default: "default") """ if network_name in self.networks: - nodes = self.networks[network_name].nodes - nodes.pop(node_name, None) + self.networks[network_name].remove_node(node_name) + else: + raise ValueError(f"Unknown network name {network_name}") def reset(self): """ Resets the current object to a single network ("default") - with the nodes Alice, Bob, Charlie, David and Eve. - Note that this does not overwrite any config file but can be done - by calling 'write_to_file'. - :return: + with the nodes "Alice", "Bob", "Charlie", "David" and "Eve". """ - for network_name in list(self.networks.keys()): + for network_name in self.networks.keys(): self.remove_network(network_name=network_name) node_names = ["Alice", "Bob", "Charlie", "David", "Eve"] - self.add_network(node_names=node_names) + self.add_network(node_names=node_names, network_name="default") def add_network(self, node_names: List[str], network_name: str = "default", topology: Optional[Dict[str, List[str]]] = None): @@ -270,13 +269,6 @@ def get_node_names(self, network_name: str = "default"): else: raise ValueError(f"{network_name} is not a network in this config") - def to_dict(self) -> Dict[str, Dict[str, Any]]: - """ - Constructs a dictionary with all the content that can be written to a json file - :return: dict - """ - return {network_name: network.to_dict() for network_name, network in self.networks.items()} - def write_to_file(self, file_path: PathLike | str): """ Writes the content of this config to a file. @@ -297,9 +289,8 @@ def write_to_file(self, file_path: PathLike | str): # Poke the file, so it exists before opening file_path.touch(exist_ok=True) - dictionary = self.to_dict() with file_path.open('wt') as f: - json.dump(dictionary, f, indent=4) + f.write(JSONSerializer.serialize(self)) def read_from_file(self, file_path: PathLike | str): """ @@ -343,6 +334,23 @@ def read_from_file(self, file_path: PathLike | str): network.nodes[node_name] = node self.networks[network_name] = network + # Helper properties and pythonic accessors + @property + def nodes(self) -> List[NodeConfig]: + """ + Access the nodes of the default network held by this configuration. + Returns: + A list of NodeConfig objects. + """ + return self.get_nodes(network_name="default") + + def __getattr__(self, item: str) -> NetworkConfig: + if isinstance(item, str): + return self.networks[item] + else: + raise ValueError(f"Item '{item}' cannot be matched to a network in this config.") + + # Helper functions def _get_unused_port(self, hostname: str) -> int: """ Returns an unused port in the interval 8000 to 9000, if such exists, otherwise returns None. From 4386fea7d0b433716ed06ebca1fada57fca02e82 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 11:22:49 +0100 Subject: [PATCH 104/277] Add test case to check the default network configuration --- tests/quick/test_networks_settings.py | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/quick/test_networks_settings.py diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py new file mode 100644 index 00000000..f545e791 --- /dev/null +++ b/tests/quick/test_networks_settings.py @@ -0,0 +1,57 @@ +import json +import pytest +import shutil +from importlib import resources +from pathlib import Path +from tempfile import NamedTemporaryFile +from dataclasses_serialization.json import JSONSerializer + +import simulaqron._default_config +from simulaqron.settings import network_config, NetworkConfigBuilder +from simulaqron.settings.network_config import DEFAULT_SIMULAQRON_NETWORK_FILENAME + + +cwd_network = (Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() +home_network = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() + +class TestNetworksSettings: + @pytest.fixture + def clean_settings(self): + # Load the setting files in cwd and home, saved them in a temp file + if cwd_network.is_file() and cwd_network.is_file(): + orig_cwd_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + shutil.copyfile(cwd_network, orig_cwd_network.name) + cwd_network.unlink() + else: + orig_cwd_network = None + if home_network.is_file() and home_network.is_file(): + orig_home_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + shutil.copyfile(home_network, orig_home_network.name) + home_network.unlink() + else: + orig_home_network = None + # Proceed with the test case + yield + # Restore the loaded files in the original locations + if orig_cwd_network is not None: + cwd_network.touch() + shutil.copyfile(orig_cwd_network.name, cwd_network) + orig_cwd_network.__exit__(None, None, None) + if orig_home_network is not None: + home_network.touch() + shutil.copyfile(orig_home_network.name, home_network) + orig_home_network.__exit__(None, None, None) + + files_to_check = [cwd_network, home_network] + for file in files_to_check: + if file.exists() and file.is_file(): + file.unlink() + + def test_create_default_settings(self, clean_settings): + # Load the "raw" default network + default_network_path = Path(str(resources.files(simulaqron._default_config).joinpath("default_network.json"))) + + expected_net_cfg_dict = json.loads(default_network_path.read_text()) + expected_net_cfg = JSONSerializer.deserialize(NetworkConfigBuilder, expected_net_cfg_dict) + + assert network_config == expected_net_cfg From 85796ba770a2a2b5a3a989768a8a5b21ea684a00 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 11:36:10 +0100 Subject: [PATCH 105/277] Add serialization methods for network config objects --- simulaqron/settings/__init__.py | 7 ++- simulaqron/settings/_serialization.py | 74 ++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index 96b12c30..a76beb31 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -1,6 +1,6 @@ -from ._serialization import init_serialization from .network_config import NetworkConfigBuilder from .simulaqron_config import SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME +from ._serialization import init_serialization init_serialization() @@ -9,6 +9,11 @@ # with the default values simulaqron_settings = SimulaqronConfig.load_from_known_sources() +# Centralized way to store the config of the network. It reads the local +# configuration if exists, otherwise, it simply populates the in-memory +# configs object with the default values +network_config = NetworkConfigBuilder.load_from_known_sources() + # We follow a similar approach with the network config builder: read the # file pointed by the simulaqron_settings (if exists) or initialize a new # builder that contains only the default network. diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index 514faf29..616c2d47 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -1,11 +1,12 @@ from enum import Enum from pathlib import Path -from typing import Type, Dict, Any +from typing import Type, Dict, Any, List from dataclasses_serialization.json import JSONSerializer from dataclasses_serialization.serializer_base import (DeserializationError, dict_serialization) +from .network_config import NodeConfig, NetworkConfig, NetworkConfigBuilder from ..settings.simulaqron_config import (DEFAULT_SIMULAQRON_SETTINGS_FILENAME, SimulaqronConfig, SimBackend) @@ -63,7 +64,7 @@ def simulaqron_config_serializer(obj: SimulaqronConfig) -> str: @JSONSerializer.register_deserializer(SimulaqronConfig) def simulaqron_config_deserializer(cls: Type[SimulaqronConfig], obj: Dict[str, Any]) -> SimulaqronConfig: - new_obj = cls(network_config_file=JSONSerializer.deserialize(Path, obj["network_config_file"])) + new_obj = cls() new_obj.max_qubits = obj["max_qubits"] new_obj.max_registers = obj["max_registers"] new_obj.conn_retry_time = obj["conn_retry_time"] @@ -80,3 +81,72 @@ def simulaqron_config_deserializer(cls: Type[SimulaqronConfig], obj: Dict[str, A new_obj.max_app_waiting_time = obj["max_app_waiting_time"] new_obj.t1 = obj["t1"] return new_obj + + +@JSONSerializer.register_serializer(NodeConfig) +def node_config_serializer(obj: NodeConfig) -> str: + node_config_dict = { + "app_socket": [obj.app_hostname, obj.app_port], + "qnodeos_socket": [obj.qnodeos_hostname, obj.qnodeos_port], + "vnode_socket": [obj.vnode_hostname, obj.vnode_port] + } + return JSONSerializer.serialize(node_config_dict) + + +@JSONSerializer.register_deserializer(NodeConfig) +def node_config_deserializer(cls: Type[NodeConfig], obj: Dict[str, Any]) -> NodeConfig: + node_name = list(obj.keys())[0] + app_socket_host = obj[node_name]["app_socket"][0] + app_socket_port: int = obj[node_name]["app_socket"][1] + qnodeos_socket_host = obj[node_name]["qnodeos_socket"][0] + qnodeos_socket_port: int = obj[node_name]["qnodeos_socket"][1] + vnode_socket_host = obj[node_name]["vnode_socket"][0] + vnode_socket_port: int = obj[node_name]["vnode_socket"][1] + return cls( + name=node_name, + app_hostname=app_socket_host, + qnodeos_hostname=qnodeos_socket_host, + vnode_hostname=vnode_socket_host, + app_port=app_socket_port, + qnodeos_port=qnodeos_socket_port, + vnode_port=vnode_socket_port + ) + + +@JSONSerializer.register_serializer(NetworkConfig) +def network_config_serializer(obj: NetworkConfig) -> str: + nodes_dict = { + "nodes": { + node_cfg.name: JSONSerializer.serialize(node_cfg) + for node_cfg in obj.nodes.values() + }, + "topology": JSONSerializer.serialize(obj.topology) + } + return JSONSerializer.serialize(nodes_dict) + +@JSONSerializer.register_deserializer(NetworkConfig) +def network_config_deserializer(cls: Type[NetworkConfig], obj: Dict[str, Any]) -> NetworkConfig: + net_cfg = cls(obj["name"]) + for raw_node in obj["nodes"]: + node_cfg = JSONSerializer.deserialize(NodeConfig, raw_node) + net_cfg.add_node_config(node_cfg) + net_cfg.topology = obj["topology"] + return net_cfg + + +@JSONSerializer.register_serializer(NetworkConfigBuilder) +def network_config_builder_serializer(obj: NetworkConfigBuilder) -> str: + networks_dict = { + network_name: JSONSerializer.serialize(network_obj) + for network_name, network_obj in obj.networks.items() + } + return JSONSerializer.serialize(networks_dict) + + +@JSONSerializer.register_deserializer(NetworkConfigBuilder) +def network_config_builder_deserializer(cls: Type[NetworkConfigBuilder], obj: List[Dict]) -> NetworkConfigBuilder: + new_obj = cls() + for raw_network in obj: + network_spec: NetworkConfig = JSONSerializer.deserialize(NetworkConfig, raw_network) + new_obj.add_network_config(network_spec) + return new_obj From 3dd508664007e7cf84e662906440bf419ff9c77e Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 13:07:30 +0100 Subject: [PATCH 106/277] Update network configuration files to match the new format --- .../distributed/teleport/network-alice.json | 67 ++++---- .../distributed/teleport/network-bob.json | 67 ++++---- .../_default_config/default_network.json | 151 ++++++++++-------- tests/quick/general/resources/network.json | 29 ++-- tests/slow/merges/configs/network.json | 95 ++++++----- 5 files changed, 219 insertions(+), 190 deletions(-) diff --git a/examples/distributed/teleport/network-alice.json b/examples/distributed/teleport/network-alice.json index 1156091d..b64c44ff 100644 --- a/examples/distributed/teleport/network-alice.json +++ b/examples/distributed/teleport/network-alice.json @@ -1,35 +1,40 @@ -{ - "default": { - "nodes": { - "Alice": { - "app_socket": [ - "192.168.0.160", - 8001 - ], - "qnodeos_socket": [ - "192.168.0.160", - 8005 - ], - "vnode_socket": [ - "192.168.0.160", - 8020 - ] +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": [ + "192.168.0.160", + 8001 + ], + "qnodeos_socket": [ + "192.168.0.160", + 8005 + ], + "vnode_socket": [ + "192.168.0.160", + 8020 + ] + } }, - "Bob": { - "app_socket": [ - "192.168.20.249", - 8032 - ], - "qnodeos_socket": [ - "192.168.20.249", - 8037 - ], - "vnode_socket": [ - "192.168.20.249", - 8050 - ] + { + "Bob": { + "app_socket": [ + "192.168.20.249", + 8032 + ], + "qnodeos_socket": [ + "192.168.20.249", + 8037 + ], + "vnode_socket": [ + "192.168.20.249", + 8050 + ] + } } - }, + ], "topology": null } -} \ No newline at end of file +] \ No newline at end of file diff --git a/examples/distributed/teleport/network-bob.json b/examples/distributed/teleport/network-bob.json index 16b99a21..cd40c73a 100644 --- a/examples/distributed/teleport/network-bob.json +++ b/examples/distributed/teleport/network-bob.json @@ -1,35 +1,40 @@ -{ - "default": { - "nodes": { - "Alice": { - "app_socket": [ - "localhost", - 8001 - ], - "qnodeos_socket": [ - "localhost", - 8005 - ], - "vnode_socket": [ - "localhost", - 8020 - ] +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": [ + "localhost", + 8001 + ], + "qnodeos_socket": [ + "localhost", + 8005 + ], + "vnode_socket": [ + "localhost", + 8020 + ] + } }, - "Bob": { - "app_socket": [ - "localhost", - 8032 - ], - "qnodeos_socket": [ - "localhost", - 8037 - ], - "vnode_socket": [ - "localhost", - 8050 - ] + { + "Bob": { + "app_socket": [ + "localhost", + 8032 + ], + "qnodeos_socket": [ + "localhost", + 8037 + ], + "vnode_socket": [ + "localhost", + 8050 + ] + } } - }, + ], "topology": null } -} \ No newline at end of file +] \ No newline at end of file diff --git a/simulaqron/_default_config/default_network.json b/simulaqron/_default_config/default_network.json index 709ac04c..8cd2bd75 100644 --- a/simulaqron/_default_config/default_network.json +++ b/simulaqron/_default_config/default_network.json @@ -1,77 +1,88 @@ -{ - "default": { - "nodes": { - "Alice": { - "app_socket": [ - "localhost", - 8001 - ], - "qnodeos_socket": [ - "localhost", - 8002 - ], - "vnode_socket": [ - "localhost", - 8003 - ] +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": [ + "localhost", + 8001 + ], + "qnodeos_socket": [ + "localhost", + 8002 + ], + "vnode_socket": [ + "localhost", + 8003 + ] + } }, - "Bob": { - "app_socket": [ - "localhost", - 8010 - ], - "qnodeos_socket": [ - "localhost", - 8011 - ], - "vnode_socket": [ - "localhost", - 8012 - ] + { + "Bob": { + "app_socket": [ + "localhost", + 8010 + ], + "qnodeos_socket": [ + "localhost", + 8011 + ], + "vnode_socket": [ + "localhost", + 8012 + ] + } }, - "Charlie": { - "app_socket": [ - "localhost", - 8020 - ], - "qnodeos_socket": [ - "localhost", - 8021 - ], - "vnode_socket": [ - "localhost", - 8022 - ] + { + "Charlie": { + "app_socket": [ + "localhost", + 8020 + ], + "qnodeos_socket": [ + "localhost", + 8021 + ], + "vnode_socket": [ + "localhost", + 8022 + ] + } }, - "David": { - "app_socket": [ - "localhost", - 8030 - ], - "qnodeos_socket": [ - "localhost", - 8031 - ], - "vnode_socket": [ - "localhost", - 8032 - ] + { + "David": { + "app_socket": [ + "localhost", + 8030 + ], + "qnodeos_socket": [ + "localhost", + 8031 + ], + "vnode_socket": [ + "localhost", + 8032 + ] + } }, - "Eve": { - "app_socket": [ - "localhost", - 8033 - ], - "qnodeos_socket": [ - "localhost", - 8034 - ], - "vnode_socket": [ - "localhost", - 8035 - ] + { + "Eve": { + "app_socket": [ + "localhost", + 8040 + ], + "qnodeos_socket": [ + "localhost", + 8041 + ], + "vnode_socket": [ + "localhost", + 8042 + ] + } } - }, + ], "topology": null } -} \ No newline at end of file +] \ No newline at end of file diff --git a/tests/quick/general/resources/network.json b/tests/quick/general/resources/network.json index 7844a94c..93643c4e 100644 --- a/tests/quick/general/resources/network.json +++ b/tests/quick/general/resources/network.json @@ -1,21 +1,22 @@ -{ - "default": +[ { - "nodes": - { - "Alice": + "name": "default", + "nodes": [ { - "app_socket": ["localhost", 8000], - "qnodeos_socket": ["localhost", 8001], - "vnode_socket": ["localhost", 8002] + "Alice": { + "app_socket": ["localhost", 8000], + "qnodeos_socket": ["localhost", 8001], + "vnode_socket": ["localhost", 8002] + } }, - "Bob": { - "app_socket": ["localhost", 8003], - "qnodeos_socket": ["localhost", 8004], - "vnode_socket": ["localhost", 8005] + "Bob": { + "app_socket": ["localhost", 8003], + "qnodeos_socket": ["localhost", 8004], + "vnode_socket": ["localhost", 8005] + } } - }, + ], "topology": null } -} +] diff --git a/tests/slow/merges/configs/network.json b/tests/slow/merges/configs/network.json index 715086f4..c0da1d7f 100644 --- a/tests/slow/merges/configs/network.json +++ b/tests/slow/merges/configs/network.json @@ -1,49 +1,56 @@ -{ - "default": { - "nodes": { - "Alice": { - "app_socket": [ - "localhost", - 8001 - ], - "qnodeos_socket": [ - "localhost", - 8003 - ], - "vnode_socket": [ - "localhost", - 8014 - ] +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": [ + "localhost", + 8001 + ], + "qnodeos_socket": [ + "localhost", + 8003 + ], + "vnode_socket": [ + "localhost", + 8014 + ] + } }, - "Bob": { - "app_socket": [ - "localhost", - 8015 - ], - "qnodeos_socket": [ - "localhost", - 8016 - ], - "vnode_socket": [ - "localhost", - 8021 - ] + { + "Bob": { + "app_socket": [ + "localhost", + 8015 + ], + "qnodeos_socket": [ + "localhost", + 8016 + ], + "vnode_socket": [ + "localhost", + 8021 + ] + } }, - "Charlie": { - "app_socket": [ - "localhost", - 8024 - ], - "qnodeos_socket": [ - "localhost", - 8025 - ], - "vnode_socket": [ - "localhost", - 8032 - ] + { + "Charlie": { + "app_socket": [ + "localhost", + 8024 + ], + "qnodeos_socket": [ + "localhost", + 8025 + ], + "vnode_socket": [ + "localhost", + 8032 + ] + } } - }, + ], "topology": null } -} \ No newline at end of file +] \ No newline at end of file From bfd56d7bf1ae04a836170df70f54036379f16dc9 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 13:23:15 +0100 Subject: [PATCH 107/277] Add logic to deserialize the network configuration classes --- simulaqron/settings/network_config.py | 87 +++++++++++++++++++++------ 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 31c26224..7ee75a9e 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -1,4 +1,5 @@ import json +import shutil import socket from contextlib import closing from dataclasses import dataclass, field @@ -10,6 +11,8 @@ import simulaqron._default_config +DEFAULT_SIMULAQRON_NETWORK_FILENAME = "simulaqron_network.json" + @dataclass class NodeConfig: @@ -24,6 +27,14 @@ class NodeConfig: qnodeos_hostname: str = "localhost" vnode_hostname: str = "localhost" + def __eq__(self, other) -> bool: + if not isinstance(other, NodeConfig): + return False + return (self.name == other.name and self.app_port == other.app_port and + self.qnodeos_port == other.qnodeos_port and self.vnode_port == other.vnode_port and + self.app_hostname == other.app_hostname and self.qnodeos_hostname == other.qnodeos_hostname and + self.vnode_hostname == other.vnode_hostname) + @dataclass class NetworkConfig: @@ -89,6 +100,19 @@ def add_node( def remove_node(self, node_name: str): self.nodes.pop(node_name, None) + def add_node_config(self, node_cfg: NodeConfig): + self.nodes[node_cfg.name] = node_cfg + + @property + def nodes_names(self) -> List[str]: + return list(self.nodes.keys()) + + def __eq__(self, other) -> bool: + if not isinstance(other, NetworkConfig): + return False + nodes_are_equal = [this_node == other_node for this_node, other_node in zip(self.nodes, other.nodes)] + return self.name == other.name and self.topology == other.topology and all(nodes_are_equal) + @dataclass class NetworkConfigBuilder: """ @@ -126,6 +150,21 @@ def _correct_network_port_if_needed(self, hostname: str, port: int) -> int: raise ValueError(f"Socket address combination ({hostname}, {port}) is already in use.") return port + def add_network_config(self, net_cfg: NetworkConfig): + """ + Method used to deserialize NetworksSpecsConfig. + Args: + net_cfg: NetworkConfig + The network configu object to add to the specifications. + """ + self.networks[net_cfg.name] = net_cfg + + # Update the used_sockets object + for node_name, node_config in net_cfg.nodes.items(): + self.used_sockets.append((node_config.app_hostname, node_config.app_port)) + self.used_sockets.append((node_config.qnodeos_hostname, node_config.qnodeos_port)) + self.used_sockets.append((node_config.vnode_hostname, node_config.vnode_port)) + def add_node(self, node_name: str, network_name: str = "default", app_hostname: str = "localhost", qnodeos_hostname: str = "localhost", vnode_hostname: str = "localhost", app_port: int = -1, qnodeos_port: int = -1, @@ -316,23 +355,31 @@ def read_from_file(self, file_path: PathLike | str): network = NetworkConfig() network.topology = topology - for node_name, node_dict in nodes_dict.items(): - app_hostname, app_port = node_dict["app_socket"] - qnodeos_hostname, qnodeos_port = node_dict["qnodeos_socket"] - vnode_hostname, vnode_port = node_dict["vnode_socket"] - socket_addresses = [ - (app_hostname, app_port), - (qnodeos_hostname, qnodeos_port), - (vnode_hostname, vnode_port), - ] - for socket_address in socket_addresses: - if socket_address not in self.used_sockets: - self.used_sockets.append(socket_address) - node = NodeConfig(name=node_name, app_hostname=app_hostname, qnodeos_hostname=qnodeos_hostname, - vnode_hostname=vnode_hostname, app_port=app_port, qnodeos_port=qnodeos_port, - vnode_port=vnode_port) - network.nodes[node_name] = node - self.networks[network_name] = network + @classmethod + def _deserialize_from_file(cls, file_path: Path) -> Self: + with file_path.resolve().open("rt") as file: + config_content = json.load(file) + return JSONSerializer.deserialize(cls, config_content) + + @classmethod + def load_from_known_sources(cls) -> Self: + cwd_networks_file = (Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() + home_networks_file = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() + + files_to_load = [cwd_networks_file, home_networks_file] + + for file in files_to_load: + try: + if file.exists() and file.is_file(): + return cls._deserialize_from_file(file) + except json.JSONDecodeError: + # Nothing to do; try next one + pass + + # Ultimate case; we create a new config file in the home and load it + default_net_cfg_path = Path(str(resources.files(simulaqron._default_config).joinpath("default_network.json"))) + shutil.copyfile(default_net_cfg_path, home_networks_file) + return cls._deserialize_from_file(home_networks_file) # Helper properties and pythonic accessors @property @@ -351,6 +398,12 @@ def __getattr__(self, item: str) -> NetworkConfig: raise ValueError(f"Item '{item}' cannot be matched to a network in this config.") # Helper functions + def __eq__(self, other: Any) -> bool: + if not isinstance(other, NetworkConfigBuilder): + return False + nodes_eq = [current_node == other_node for current_node, other_node in zip(self.nodes, other.nodes)] + return all(nodes_eq) + def _get_unused_port(self, hostname: str) -> int: """ Returns an unused port in the interval 8000 to 9000, if such exists, otherwise returns None. From a80975dba15083e6f7a8dba64494964564e4f488 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 14:51:40 +0100 Subject: [PATCH 108/277] Add more tests for new network configuration structure --- tests/quick/general/test_host_config.py | 24 +-------- tests/quick/test_networks_settings.py | 66 +++++++++++++++++++++++++ tests/quick/test_settings.py | 3 +- 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/tests/quick/general/test_host_config.py b/tests/quick/general/test_host_config.py index c47e3b49..fce19be3 100644 --- a/tests/quick/general/test_host_config.py +++ b/tests/quick/general/test_host_config.py @@ -4,29 +4,7 @@ from simulaqron.general.host_config import NetworkConfigBuilder, SocketsConfig -class TestNetworkConfig: - def test_read_write(self): - network_config = NetworkConfigBuilder() - - network_config.add_node("Alice") - network_config.add_node("Bob") - network_config.add_node("Charlie", network_name="test") - - dct1 = network_config.to_dict() - with NamedTemporaryFile(mode="w", delete_on_close=False) as temp_file: - network_config.write_to_file(temp_file.name) - temp_file.close() - - network_config2 = NetworkConfigBuilder() - network_config2.read_from_file(temp_file.name) - dct2 = network_config2.to_dict() - - assert dct1 == dct2 - assert "Alice" in dct1["default"]["nodes"] - assert "Bob" in dct1["default"]["nodes"] - assert "Charlie" in dct1["test"]["nodes"] - - +# TODO - Move these test to the new test class class TestSocketsConfig: def test_load_file(self): this_file_folder = Path(__file__).parent diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py index f545e791..569c3bf7 100644 --- a/tests/quick/test_networks_settings.py +++ b/tests/quick/test_networks_settings.py @@ -47,6 +47,10 @@ def clean_settings(self): if file.exists() and file.is_file(): file.unlink() + @pytest.fixture + def reset_net_cfg(self): + network_config.load_from_known_sources() + def test_create_default_settings(self, clean_settings): # Load the "raw" default network default_network_path = Path(str(resources.files(simulaqron._default_config).joinpath("default_network.json"))) @@ -55,3 +59,65 @@ def test_create_default_settings(self, clean_settings): expected_net_cfg = JSONSerializer.deserialize(NetworkConfigBuilder, expected_net_cfg_dict) assert network_config == expected_net_cfg + + + def test_add_node(self, reset_net_cfg): + network_config.remove_all_networks() + + network_config.add_node("Alice") + network_config.add_node("Bob") + network_config.add_node("Charlie", network_name="test") + + # We expect 2 nodes, since Charlie belongs to network "test" and not "default" + assert len(network_config.nodes) == 2 + assert len(network_config.get_nodes("test")) == 1 + + @pytest.mark.skip(reason="TODO - Implement this test") + def test_remove_node(self): + network_config.remove_all_networks() + + network_config.add_node("Alice") + network_config.add_node("Bob") + network_config.add_node("Charlie", network_name="test") + # TODO - Finish this test + + @pytest.mark.skip(reason="TODO - Implement this test") + def test_add_network(self): + network_config.remove_all_networks() + + network_config.add_node("Alice") + network_config.add_node("Bob") + network_config.add_node("Charlie", network_name="test") + # TODO - Finish this test + + @pytest.mark.skip(reason="TODO - Implement this test") + def test_remove_network(self): + network_config.remove_all_networks() + + network_config.add_node("Alice") + network_config.add_node("Bob") + network_config.add_node("Charlie", network_name="test") + + network_config + # TODO - Finish this test + + @pytest.mark.skip(reason="TODO - Implement this test") + def test_serialize_network_config(self): + network_config.remove_all_networks() + + network_config.add_node("Alice") + network_config.add_node("Bob") + + with NamedTemporaryFile(mode="w", delete_on_close=False) as temp_file: + network_config.write_to_file(temp_file.name) + temp_file.close() + + network_config2 = NetworkConfigBuilder() + network_config2.read_from_file(temp_file.name) + dct2 = network_config2.to_dict() + + assert dct1 == dct2 + assert "Alice" in dct1["default"]["nodes"] + assert "Bob" in dct1["default"]["nodes"] + assert "Charlie" in dct1["test"]["nodes"] + # TODO - Finish this test diff --git a/tests/quick/test_settings.py b/tests/quick/test_settings.py index 7ee59006..a54e48ac 100644 --- a/tests/quick/test_settings.py +++ b/tests/quick/test_settings.py @@ -1,4 +1,3 @@ -import tempfile import json import pytest import shutil @@ -139,7 +138,7 @@ def test_non_existent_network_config(self): expected_settings = JSONSerializer.deserialize(SimulaqronConfig, expected_settings_dict) _original_settings = json.loads(_original_settings) - with tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", delete_on_close=False) as file: + with NamedTemporaryFile(mode="w+", encoding="utf-8", delete_on_close=False) as file: json.dump(_original_settings, file) file.close() From 8f8f136bf44c6dd4c5273b08d4e430067d0171f9 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 14:52:47 +0100 Subject: [PATCH 109/277] Fix bugs when adding new nodes to a network --- simulaqron/settings/network_config.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 7ee75a9e..9b7834b2 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -144,7 +144,7 @@ def _correct_network_port_if_needed(self, hostname: str, port: int) -> int: A port number which is guaranteed to be valid, and ready to be used to listen to connections on. """ - if port < -1: + if port < 0: port = self._get_unused_port(hostname) if not self._check_port_available(hostname, port): raise ValueError(f"Socket address combination ({hostname}, {port}) is already in use.") @@ -215,6 +215,7 @@ def add_node(self, node_name: str, network_name: str = "default", app_hostname: if network_name not in self.networks: # network doesn't exist, create a new one network = NetworkConfig(network_name) + self.networks[network.name] = network # At this point, we are sure that the network exists in self.networks network = self.networks[network_name] @@ -241,16 +242,6 @@ def remove_node(self, node_name: str, network_name: str = "default"): else: raise ValueError(f"Unknown network name {network_name}") - def reset(self): - """ - Resets the current object to a single network ("default") - with the nodes "Alice", "Bob", "Charlie", "David" and "Eve". - """ - for network_name in self.networks.keys(): - self.remove_network(network_name=network_name) - node_names = ["Alice", "Bob", "Charlie", "David", "Eve"] - self.add_network(node_names=node_names, network_name="default") - def add_network(self, node_names: List[str], network_name: str = "default", topology: Optional[Dict[str, List[str]]] = None): """ @@ -308,6 +299,10 @@ def get_node_names(self, network_name: str = "default"): else: raise ValueError(f"{network_name} is not a network in this config") + def remove_all_networks(self): + for network_name in self.network_names: + self.remove_network(network_name) + def write_to_file(self, file_path: PathLike | str): """ Writes the content of this config to a file. @@ -391,6 +386,10 @@ def nodes(self) -> List[NodeConfig]: """ return self.get_nodes(network_name="default") + @property + def network_names(self) -> List[str]: + return list(self.networks.keys()) + def __getattr__(self, item: str) -> NetworkConfig: if isinstance(item, str): return self.networks[item] From b529ae71e7d373c6dfd256aef58293d321249ae5 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 15:04:27 +0100 Subject: [PATCH 110/277] Update test for remove node from network --- tests/quick/test_networks_settings.py | 43 ++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py index 569c3bf7..88be12dc 100644 --- a/tests/quick/test_networks_settings.py +++ b/tests/quick/test_networks_settings.py @@ -1,4 +1,6 @@ import json +from typing import Tuple + import pytest import shutil from importlib import resources @@ -8,8 +10,7 @@ import simulaqron._default_config from simulaqron.settings import network_config, NetworkConfigBuilder -from simulaqron.settings.network_config import DEFAULT_SIMULAQRON_NETWORK_FILENAME - +from simulaqron.settings.network_config import DEFAULT_SIMULAQRON_NETWORK_FILENAME, NodeConfig cwd_network = (Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() home_network = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() @@ -60,6 +61,12 @@ def test_create_default_settings(self, clean_settings): assert network_config == expected_net_cfg + @staticmethod + def _check_node_config(node: NodeConfig) -> Tuple[bool, ...]: + return (node.app_hostname == "localhost", 8000 <= node.app_port <= 9000, + node.qnodeos_hostname == "localhost", 8000 <= node.qnodeos_port <= 9000, + node.vnode_hostname == "localhost", 8000 <= node.vnode_port <= 9000,) + def test_add_node(self, reset_net_cfg): network_config.remove_all_networks() @@ -72,14 +79,42 @@ def test_add_node(self, reset_net_cfg): assert len(network_config.nodes) == 2 assert len(network_config.get_nodes("test")) == 1 - @pytest.mark.skip(reason="TODO - Implement this test") + assert network_config.nodes[0].name == "Alice" + assert network_config.nodes[1].name == "Bob" + assert network_config.get_nodes("test")[0].name == "Charlie" + + assert TestNetworksSettings._check_node_config(network_config.nodes[0]) + assert TestNetworksSettings._check_node_config(network_config.nodes[1]) + assert TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0]) + def test_remove_node(self): network_config.remove_all_networks() network_config.add_node("Alice") network_config.add_node("Bob") network_config.add_node("Charlie", network_name="test") - # TODO - Finish this test + + network_config.remove_node("Alice") + + # We expect 1 node, since Charlie belongs to network "test" and not "default" + assert len(network_config.nodes) == 1 + assert len(network_config.get_nodes("test")) == 1 + + assert network_config.nodes[0].name == "Bob" + assert network_config.get_nodes("test")[0].name == "Charlie" + + assert TestNetworksSettings._check_node_config(network_config.nodes[0]) + assert TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0]) + + network_config.remove_node("Charlie", network_name="test") + + # We expect only 1 node, and 1 network, since network "test" is now empty + assert len(network_config.nodes) == 1 + assert len(network_config.networks) == 1 + + assert network_config.nodes[0].name == "Bob" + + assert TestNetworksSettings._check_node_config(network_config.nodes[0]) @pytest.mark.skip(reason="TODO - Implement this test") def test_add_network(self): From a1917ed213c5a685a86b28430cf1916732b2cc22 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 15:17:29 +0100 Subject: [PATCH 111/277] Implement correct deletion of nodes from a network --- simulaqron/settings/network_config.py | 31 ++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 9b7834b2..53e75e7f 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -97,12 +97,25 @@ def add_node( vnode_port=vnode_port, ) - def remove_node(self, node_name: str): - self.nodes.pop(node_name, None) + def remove_node(self, node_name: str) -> NodeConfig | None: + """ + Removes the node with the given name and returns it. Returns none if the given + node name was not found in this network. + Args: + node_name: str + The name of the node to remove. None if the node name does not exist. + Returns: + The removed node. None if the given name was not found. + """ + return self.nodes.pop(node_name, None) def add_node_config(self, node_cfg: NodeConfig): self.nodes[node_cfg.name] = node_cfg + @property + def is_empty(self) -> bool: + return len(self.nodes) <= 0 + @property def nodes_names(self) -> List[str]: return list(self.nodes.keys()) @@ -238,7 +251,19 @@ def remove_node(self, node_name: str, network_name: str = "default"): Name of the network to delete the node from (default: "default") """ if network_name in self.networks: - self.networks[network_name].remove_node(node_name) + old_node = self.networks[network_name].remove_node(node_name) + if old_node is None: + # node_name did not exist; just continue + return + + # Remove the tuples from the used sockets + self.used_sockets.remove((old_node.app_hostname, old_node.app_port)) + self.used_sockets.remove((old_node.qnodeos_hostname, old_node.qnodeos_port)) + self.used_sockets.remove((old_node.vnode_hostname, old_node.vnode_port)) + + # Remove the network if it's now empty + if self.networks[network_name].is_empty: + self.networks.pop(network_name) else: raise ValueError(f"Unknown network name {network_name}") From 23423e1993739873496a7fe2c43b1ed7d6cdddac Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 15:29:48 +0100 Subject: [PATCH 112/277] Add test for adding network --- simulaqron/settings/network_config.py | 3 +++ tests/quick/test_networks_settings.py | 34 ++++++++++++++++++--------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 53e75e7f..44125e78 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -279,6 +279,9 @@ def add_network(self, node_names: List[str], network_name: str = "default", :param topology: None or dict The topology of the network (optional) (default is fully connected) """ + if isinstance(node_names, str): + # The user passes a string... they probably meant to add a single node, so we make it a list + node_names = [node_names] self.remove_network(network_name=network_name) for node_name in node_names: if topology is not None: diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py index 88be12dc..ec0cbcfd 100644 --- a/tests/quick/test_networks_settings.py +++ b/tests/quick/test_networks_settings.py @@ -52,6 +52,12 @@ def clean_settings(self): def reset_net_cfg(self): network_config.load_from_known_sources() + @staticmethod + def _check_node_config(node: NodeConfig) -> Tuple[bool, ...]: + return (node.app_hostname == "localhost", 8000 <= node.app_port <= 9000, + node.qnodeos_hostname == "localhost", 8000 <= node.qnodeos_port <= 9000, + node.vnode_hostname == "localhost", 8000 <= node.vnode_port <= 9000,) + def test_create_default_settings(self, clean_settings): # Load the "raw" default network default_network_path = Path(str(resources.files(simulaqron._default_config).joinpath("default_network.json"))) @@ -61,12 +67,6 @@ def test_create_default_settings(self, clean_settings): assert network_config == expected_net_cfg - @staticmethod - def _check_node_config(node: NodeConfig) -> Tuple[bool, ...]: - return (node.app_hostname == "localhost", 8000 <= node.app_port <= 9000, - node.qnodeos_hostname == "localhost", 8000 <= node.qnodeos_port <= 9000, - node.vnode_hostname == "localhost", 8000 <= node.vnode_port <= 9000,) - def test_add_node(self, reset_net_cfg): network_config.remove_all_networks() @@ -116,14 +116,26 @@ def test_remove_node(self): assert TestNetworksSettings._check_node_config(network_config.nodes[0]) - @pytest.mark.skip(reason="TODO - Implement this test") def test_add_network(self): network_config.remove_all_networks() - network_config.add_node("Alice") - network_config.add_node("Bob") - network_config.add_node("Charlie", network_name="test") - # TODO - Finish this test + network_config.add_network("Alice", network_name="test") + network_config.add_network(["Bob"], network_name="test-b") + + # We expect 1 node in each network since Charlie belongs to network "test" and not "default" + with pytest.raises(ValueError) as err: + len(network_config.nodes) + assert str(err.value) == "default is not a network in this config" + assert len(network_config.get_nodes("test")) == 1 + assert len(network_config.get_nodes("test-b")) == 1 + + assert network_config.get_nodes("test")[0].name == "Alice" + assert network_config.get_nodes("test-b")[0].name == "Bob" + + assert TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0]) + assert TestNetworksSettings._check_node_config(network_config.get_nodes("test-b")[0]) + + @pytest.mark.skip(reason="TODO - Implement this test") def test_remove_network(self): From b294eaecb7bc5271601a44a644063f6f9771c161 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 15:35:33 +0100 Subject: [PATCH 113/277] Add extra assertions in the network config tests --- tests/quick/test_networks_settings.py | 51 +++++++++++++++++++-------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py index ec0cbcfd..4d907f91 100644 --- a/tests/quick/test_networks_settings.py +++ b/tests/quick/test_networks_settings.py @@ -67,6 +67,18 @@ def test_create_default_settings(self, clean_settings): assert network_config == expected_net_cfg + # Each node uses 3 sockets, so we expect 15 used socket pairs + assert len(network_config.used_sockets) == 15 + + assert network_config.nodes[0].name == "Alice" + assert network_config.nodes[1].name == "Bob" + assert network_config.nodes[2].name == "Charlie" + assert network_config.nodes[3].name == "David" + assert network_config.nodes[4].name == "Eve" + + for node in network_config.nodes: + assert all(TestNetworksSettings._check_node_config(node)) + def test_add_node(self, reset_net_cfg): network_config.remove_all_networks() @@ -79,15 +91,18 @@ def test_add_node(self, reset_net_cfg): assert len(network_config.nodes) == 2 assert len(network_config.get_nodes("test")) == 1 + # Each node uses 3 sockets, so we expect 9 used socket pairs + assert len(network_config.used_sockets) == 9 + assert network_config.nodes[0].name == "Alice" assert network_config.nodes[1].name == "Bob" assert network_config.get_nodes("test")[0].name == "Charlie" - assert TestNetworksSettings._check_node_config(network_config.nodes[0]) - assert TestNetworksSettings._check_node_config(network_config.nodes[1]) - assert TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0]) + for node in network_config.nodes: + assert all(TestNetworksSettings._check_node_config(node)) + assert all(TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0])) - def test_remove_node(self): + def test_remove_node(self, reset_net_cfg): network_config.remove_all_networks() network_config.add_node("Alice") @@ -100,11 +115,14 @@ def test_remove_node(self): assert len(network_config.nodes) == 1 assert len(network_config.get_nodes("test")) == 1 + # Each node uses 3 sockets, so we expect 6 used socket pairs + assert len(network_config.used_sockets) == 6 + assert network_config.nodes[0].name == "Bob" assert network_config.get_nodes("test")[0].name == "Charlie" - assert TestNetworksSettings._check_node_config(network_config.nodes[0]) - assert TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0]) + assert all(TestNetworksSettings._check_node_config(network_config.nodes[0])) + assert all(TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0])) network_config.remove_node("Charlie", network_name="test") @@ -114,31 +132,34 @@ def test_remove_node(self): assert network_config.nodes[0].name == "Bob" - assert TestNetworksSettings._check_node_config(network_config.nodes[0]) + assert all(TestNetworksSettings._check_node_config(network_config.nodes[0])) - def test_add_network(self): + def test_add_network(self, reset_net_cfg): network_config.remove_all_networks() network_config.add_network("Alice", network_name="test") network_config.add_network(["Bob"], network_name="test-b") - # We expect 1 node in each network since Charlie belongs to network "test" and not "default" + # We expect that network "default" will not exist with pytest.raises(ValueError) as err: len(network_config.nodes) assert str(err.value) == "default is not a network in this config" + + # We expect 1 node in each network since Alice belongs to network "test" and Bob to "test-b" assert len(network_config.get_nodes("test")) == 1 assert len(network_config.get_nodes("test-b")) == 1 + # Each node uses 3 sockets, so we expect 6 used socket pairs + assert len(network_config.used_sockets) == 6 + assert network_config.get_nodes("test")[0].name == "Alice" assert network_config.get_nodes("test-b")[0].name == "Bob" - assert TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0]) - assert TestNetworksSettings._check_node_config(network_config.get_nodes("test-b")[0]) - - + assert all(TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0])) + assert all(TestNetworksSettings._check_node_config(network_config.get_nodes("test-b")[0])) @pytest.mark.skip(reason="TODO - Implement this test") - def test_remove_network(self): + def test_remove_network(self, reset_net_cfg): network_config.remove_all_networks() network_config.add_node("Alice") @@ -149,7 +170,7 @@ def test_remove_network(self): # TODO - Finish this test @pytest.mark.skip(reason="TODO - Implement this test") - def test_serialize_network_config(self): + def test_serialize_network_config(self, reset_net_cfg): network_config.remove_all_networks() network_config.add_node("Alice") From 06f37bb5c1cae0782b739b9a744f1220fdcadab0 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 16:01:39 +0100 Subject: [PATCH 114/277] Fix bug to also remove sockets tuples when removing full networks from network config --- simulaqron/settings/network_config.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 44125e78..0bfb13e7 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -297,7 +297,12 @@ def remove_network(self, network_name: str = "default"): :param network_name: str Name of the network (default: "default") """ - self.networks.pop(network_name, None) + removed_network = self.networks.pop(network_name, None) + if removed_network is not None: + for _, node_cfg in removed_network.nodes.items(): + self.used_sockets.remove((node_cfg.app_hostname, node_cfg.app_port)) + self.used_sockets.remove((node_cfg.qnodeos_hostname, node_cfg.qnodeos_port)) + self.used_sockets.remove((node_cfg.vnode_hostname, node_cfg.vnode_port)) def get_nodes(self, network_name: str = "default") -> List[NodeConfig]: """ From d04fa2d97d951aab41fe6f1064557a369fca51d5 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 25 Nov 2025 16:25:49 +0100 Subject: [PATCH 115/277] Add test for remove network --- tests/quick/test_networks_settings.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py index 4d907f91..e783ee08 100644 --- a/tests/quick/test_networks_settings.py +++ b/tests/quick/test_networks_settings.py @@ -158,7 +158,6 @@ def test_add_network(self, reset_net_cfg): assert all(TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0])) assert all(TestNetworksSettings._check_node_config(network_config.get_nodes("test-b")[0])) - @pytest.mark.skip(reason="TODO - Implement this test") def test_remove_network(self, reset_net_cfg): network_config.remove_all_networks() @@ -166,8 +165,14 @@ def test_remove_network(self, reset_net_cfg): network_config.add_node("Bob") network_config.add_node("Charlie", network_name="test") - network_config - # TODO - Finish this test + network_config.remove_network("default") + # We expect 1 node in each network since Alice belongs to network "test" and Bob to "test-b" + assert len(network_config.get_nodes("test")) == 1 + + # Each node uses 3 sockets, so we expect 3 used socket pairs + assert len(network_config.used_sockets) == 3 + assert network_config.get_nodes("test")[0].name == "Charlie" + assert all(TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0])) @pytest.mark.skip(reason="TODO - Implement this test") def test_serialize_network_config(self, reset_net_cfg): From 6dbc78bef5cbfdd98d6bd16adcb0cd2ebf798637 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 12:36:53 +0100 Subject: [PATCH 116/277] Add test for serializing network config --- tests/quick/test_networks_settings.py | 81 ++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 14 deletions(-) diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py index e783ee08..4a89b946 100644 --- a/tests/quick/test_networks_settings.py +++ b/tests/quick/test_networks_settings.py @@ -1,5 +1,5 @@ import json -from typing import Tuple +from typing import Tuple, List import pytest import shutil @@ -174,23 +174,76 @@ def test_remove_network(self, reset_net_cfg): assert network_config.get_nodes("test")[0].name == "Charlie" assert all(TestNetworksSettings._check_node_config(network_config.get_nodes("test")[0])) - @pytest.mark.skip(reason="TODO - Implement this test") + @staticmethod + def _build_expected_config(alice_ports: List[int], bob_ports: List[int]): + return ("[\n" + " {\n" + " \"name\": \"default\"\n" + " \"nodes\": [\n" + " {\n" + " \"Alice\": {\n" + " \"app_socket\": [\n" + " \"localhost\",\n" + f" {alice_ports[0]},\n" + " ],\n" + " \"qnodeos_socket\": [\n" + " \"localhost\",\n" + f" {alice_ports[1]},\n" + " ],\n" + " \"vnode_socket\": [\n" + " \"localhost\",\n" + f" {alice_ports[2]},\n" + " ]\n" + " }\n" + " ],\n" + " \"topology\": null\n" + " },\n" + " {\n" + " \"name\": \"test\"\n" + " \"nodes\": [\n" + " {\n" + " \"Bob\": {\n" + " \"app_socket\": [\n" + " \"localhost\",\n" + f" {bob_ports[0]},\n" + " ],\n" + " \"qnodeos_socket\": [\n" + " \"localhost\",\n" + f" {bob_ports[1]},\n" + " ],\n" + " \"vnode_socket\": [\n" + " \"localhost\",\n" + f" {bob_ports[2]},\n" + " ]\n" + " }\n" + " ],\n" + " \"topology\": null\n" + " }\n" + "]") + def test_serialize_network_config(self, reset_net_cfg): network_config.remove_all_networks() network_config.add_node("Alice") - network_config.add_node("Bob") + network_config.add_node("Bob", network_name="test") - with NamedTemporaryFile(mode="w", delete_on_close=False) as temp_file: - network_config.write_to_file(temp_file.name) - temp_file.close() + alice_ports = [ + network_config.get_nodes(network_name="default")[0].app_port, + network_config.get_nodes(network_name="default")[0].qnodeos_port, + network_config.get_nodes(network_name="default")[0].vnode_port + ] - network_config2 = NetworkConfigBuilder() - network_config2.read_from_file(temp_file.name) - dct2 = network_config2.to_dict() + bob_ports = [ + network_config.get_nodes(network_name="test")[0].app_port, + network_config.get_nodes(network_name="test")[0].qnodeos_port, + network_config.get_nodes(network_name="test")[0].vnode_port + ] + + expected_network_config = TestNetworksSettings._build_expected_config(alice_ports, bob_ports) + + with NamedTemporaryFile(mode="wt", delete_on_close=False) as temp_file: + network_config.write_to_file(temp_file.name) + temp_file.flush() - assert dct1 == dct2 - assert "Alice" in dct1["default"]["nodes"] - assert "Bob" in dct1["default"]["nodes"] - assert "Charlie" in dct1["test"]["nodes"] - # TODO - Finish this test + serialized_content = Path(temp_file.name).read_text() + assert serialized_content == expected_network_config From b50216665fd329dca23f35dfe4a648dd555681eb Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 12:38:30 +0100 Subject: [PATCH 117/277] Declare network config classes as serializable --- simulaqron/settings/network_config.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 0bfb13e7..175b9af7 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -7,7 +7,7 @@ from os import PathLike from pathlib import Path from typing import Optional, Self, Dict, List, Tuple, Any -from dataclasses_serialization.json import JSONSerializer +from dataclasses_serialization.json import JSONSerializer, JSONSerializerMixin import simulaqron._default_config @@ -15,7 +15,7 @@ @dataclass -class NodeConfig: +class NodeConfig(JSONSerializerMixin): """ Used by NetworkConfig to keep track of the config of a single node. """ @@ -37,7 +37,7 @@ def __eq__(self, other) -> bool: @dataclass -class NetworkConfig: +class NetworkConfig(JSONSerializerMixin): """ Used by NetworksConfigConstructor to keep track of the config of a single network. """ @@ -127,7 +127,7 @@ def __eq__(self, other) -> bool: return self.name == other.name and self.topology == other.topology and all(nodes_are_equal) @dataclass -class NetworkConfigBuilder: +class NetworkConfigBuilder(JSONSerializerMixin): """ Used to construct the config file of networks. """ @@ -357,7 +357,7 @@ def write_to_file(self, file_path: PathLike | str): file_path.touch(exist_ok=True) with file_path.open('wt') as f: - f.write(JSONSerializer.serialize(self)) + json.dump(JSONSerializer.serialize(self), f, indent=4) def read_from_file(self, file_path: PathLike | str): """ From 8682f792a972d381c8d05952db9d816d84864fee Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 12:39:17 +0100 Subject: [PATCH 118/277] Fix return types of network config serializer functions --- simulaqron/settings/_serialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index 616c2d47..446faedd 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -84,7 +84,7 @@ def simulaqron_config_deserializer(cls: Type[SimulaqronConfig], obj: Dict[str, A @JSONSerializer.register_serializer(NodeConfig) -def node_config_serializer(obj: NodeConfig) -> str: +def node_config_serializer(obj: NodeConfig) -> Dict[str, List[str | int]]: node_config_dict = { "app_socket": [obj.app_hostname, obj.app_port], "qnodeos_socket": [obj.qnodeos_hostname, obj.qnodeos_port], @@ -114,7 +114,7 @@ def node_config_deserializer(cls: Type[NodeConfig], obj: Dict[str, Any]) -> Node @JSONSerializer.register_serializer(NetworkConfig) -def network_config_serializer(obj: NetworkConfig) -> str: +def network_config_serializer(obj: NetworkConfig) -> Dict[str, Any]: nodes_dict = { "nodes": { node_cfg.name: JSONSerializer.serialize(node_cfg) @@ -135,7 +135,7 @@ def network_config_deserializer(cls: Type[NetworkConfig], obj: Dict[str, Any]) - @JSONSerializer.register_serializer(NetworkConfigBuilder) -def network_config_builder_serializer(obj: NetworkConfigBuilder) -> str: +def network_config_builder_serializer(obj: NetworkConfigBuilder) -> Dict[str, Any]: networks_dict = { network_name: JSONSerializer.serialize(network_obj) for network_name, network_obj in obj.networks.items() From 877051ec18ea58e6b9bc4b4480ee09e1523c592b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 12:52:43 +0100 Subject: [PATCH 119/277] Fix expected results from network config serialization test --- tests/quick/test_networks_settings.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py index 4a89b946..c599213c 100644 --- a/tests/quick/test_networks_settings.py +++ b/tests/quick/test_networks_settings.py @@ -178,43 +178,45 @@ def test_remove_network(self, reset_net_cfg): def _build_expected_config(alice_ports: List[int], bob_ports: List[int]): return ("[\n" " {\n" - " \"name\": \"default\"\n" + " \"name\": \"default\",\n" " \"nodes\": [\n" " {\n" " \"Alice\": {\n" " \"app_socket\": [\n" " \"localhost\",\n" - f" {alice_ports[0]},\n" + f" {alice_ports[0]}\n" " ],\n" " \"qnodeos_socket\": [\n" " \"localhost\",\n" - f" {alice_ports[1]},\n" + f" {alice_ports[1]}\n" " ],\n" " \"vnode_socket\": [\n" " \"localhost\",\n" - f" {alice_ports[2]},\n" + f" {alice_ports[2]}\n" " ]\n" + " }\n" " }\n" " ],\n" " \"topology\": null\n" " },\n" " {\n" - " \"name\": \"test\"\n" + " \"name\": \"test\",\n" " \"nodes\": [\n" " {\n" " \"Bob\": {\n" " \"app_socket\": [\n" " \"localhost\",\n" - f" {bob_ports[0]},\n" + f" {bob_ports[0]}\n" " ],\n" " \"qnodeos_socket\": [\n" " \"localhost\",\n" - f" {bob_ports[1]},\n" + f" {bob_ports[1]}\n" " ],\n" " \"vnode_socket\": [\n" " \"localhost\",\n" - f" {bob_ports[2]},\n" + f" {bob_ports[2]}\n" " ]\n" + " }\n" " }\n" " ],\n" " \"topology\": null\n" From c1b6bf50d1309457e4e372339f270ab458c48721 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 12:54:18 +0100 Subject: [PATCH 120/277] Fix network config serialization methods --- simulaqron/settings/_serialization.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index 446faedd..1d8253de 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -116,10 +116,11 @@ def node_config_deserializer(cls: Type[NodeConfig], obj: Dict[str, Any]) -> Node @JSONSerializer.register_serializer(NetworkConfig) def network_config_serializer(obj: NetworkConfig) -> Dict[str, Any]: nodes_dict = { - "nodes": { - node_cfg.name: JSONSerializer.serialize(node_cfg) + "name" : obj.name, + "nodes": [ + { node_cfg.name: JSONSerializer.serialize(node_cfg) } for node_cfg in obj.nodes.values() - }, + ], "topology": JSONSerializer.serialize(obj.topology) } return JSONSerializer.serialize(nodes_dict) @@ -135,12 +136,8 @@ def network_config_deserializer(cls: Type[NetworkConfig], obj: Dict[str, Any]) - @JSONSerializer.register_serializer(NetworkConfigBuilder) -def network_config_builder_serializer(obj: NetworkConfigBuilder) -> Dict[str, Any]: - networks_dict = { - network_name: JSONSerializer.serialize(network_obj) - for network_name, network_obj in obj.networks.items() - } - return JSONSerializer.serialize(networks_dict) +def network_config_builder_serializer(obj: NetworkConfigBuilder) -> List[Dict[str, Any]]: + return [JSONSerializer.serialize(network) for network in obj.networks.values()] @JSONSerializer.register_deserializer(NetworkConfigBuilder) From f7a76e6dfe644d9318d278a37fe7eed2e8aff4ad Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 13:07:28 +0100 Subject: [PATCH 121/277] Add test for deserializing network configs --- tests/quick/test_networks_settings.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py index c599213c..d32359ec 100644 --- a/tests/quick/test_networks_settings.py +++ b/tests/quick/test_networks_settings.py @@ -249,3 +249,12 @@ def test_serialize_network_config(self, reset_net_cfg): serialized_content = Path(temp_file.name).read_text() assert serialized_content == expected_network_config + + def test_deserialize_network_config(self, reset_net_cfg): + raw_config = TestNetworksSettings._build_expected_config([8020, 8021, 8022], [8050, 8051, 8052]) + with NamedTemporaryFile(mode="wt", delete_on_close=False) as temp_file: + temp_file.write(raw_config) + temp_file.flush() + + network_config.read_from_file(temp_file.name) + assert json.dumps(JSONSerializer.serialize(network_config), indent=4) == raw_config From ac02d5da4c93aefb0b305349e6339b5671ba18b9 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 13:07:59 +0100 Subject: [PATCH 122/277] Fix implementation for "read_from_file" for network config classes --- simulaqron/settings/network_config.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 175b9af7..5cb1c13a 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -2,7 +2,7 @@ import shutil import socket from contextlib import closing -from dataclasses import dataclass, field +from dataclasses import dataclass, field, fields from importlib import resources from os import PathLike from pathlib import Path @@ -372,16 +372,15 @@ def read_from_file(self, file_path: PathLike | str): file_path = Path(str(file_path)) if file_path.exists(): - with file_path.open('r') as f: - dictionary = json.load(f) + new_config = self._deserialize_from_file(file_path) else: raise ValueError(f"No such file {file_path}") - for network_name, network_dict in dictionary.items(): - nodes_dict = network_dict["nodes"] - topology = network_dict["topology"] - network = NetworkConfig() - network.topology = topology + cls_fields = fields(self.__class__) + + for class_field in cls_fields: + new_val = getattr(new_config, class_field.name) + setattr(self, class_field.name, new_val) @classmethod def _deserialize_from_file(cls, file_path: Path) -> Self: From 494e733ae2a3798d80787f406089dc84979916fc Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 13:21:10 +0100 Subject: [PATCH 123/277] Fix simulaqron settings tests --- ...ettings.py => test_simulaqron_settings.py} | 72 +------------------ 1 file changed, 1 insertion(+), 71 deletions(-) rename tests/quick/{test_settings.py => test_simulaqron_settings.py} (51%) diff --git a/tests/quick/test_settings.py b/tests/quick/test_simulaqron_settings.py similarity index 51% rename from tests/quick/test_settings.py rename to tests/quick/test_simulaqron_settings.py index a54e48ac..149b171a 100644 --- a/tests/quick/test_settings.py +++ b/tests/quick/test_simulaqron_settings.py @@ -7,14 +7,11 @@ from simulaqron.settings import simulaqron_settings from simulaqron.settings.simulaqron_config import (SimulaqronConfig, - DEFAULT_SIMULAQRON_NETWORK_FILENAME, DEFAULT_SIMULAQRON_SETTINGS_FILENAME) cwd_settings = (Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() -cwd_network = (Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() home_settings = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() -home_network = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() class TestSettings: @@ -27,24 +24,12 @@ def clean_settings(self): cwd_settings.unlink() else: orig_cwd_settings = None - if cwd_network.is_file() and cwd_network.is_file(): - orig_cwd_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() - shutil.copyfile(cwd_network, orig_cwd_network.name) - cwd_network.unlink() - else: - orig_cwd_network = None if home_settings.exists() and home_settings.is_file(): orig_home_settings = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() shutil.copyfile(home_settings, orig_home_settings.name) home_settings.unlink() else: orig_home_settings = None - if home_network.is_file() and home_network.is_file(): - orig_home_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() - shutil.copyfile(home_network, orig_home_network.name) - home_network.unlink() - else: - orig_home_network = None # Proceed with the test case yield # Restore the loaded files in the original locations @@ -56,18 +41,10 @@ def clean_settings(self): home_settings.touch() shutil.copyfile(orig_home_settings.name, home_settings) orig_home_settings.__exit__(None, None, None) - if orig_cwd_network is not None: - cwd_network.touch() - shutil.copyfile(orig_cwd_network.name, cwd_network) - orig_cwd_network.__exit__(None, None, None) - if orig_home_network is not None: - home_network.touch() - shutil.copyfile(orig_home_network.name, home_network) - orig_home_network.__exit__(None, None, None) @staticmethod def _cleanup_config_files(): - files_to_check = [cwd_settings, cwd_network, home_settings, home_network] + files_to_check = [cwd_settings, home_settings] for file in files_to_check: if file.exists() and file.is_file(): file.unlink() @@ -84,7 +61,6 @@ def test_create_default_settings(self, clean_settings): "recv_max_retries": 10, "log_level": 30, "sim_backend": "stabilizer", - "network_config_file": "HOME_SETTINGS_PATH", "noisy_qubits": false, "t1": 1.0 } @@ -97,54 +73,8 @@ def test_create_default_settings(self, clean_settings): assert simulaqron_settings == expected_settings - assert simulaqron_settings.network_builder is not None - assert simulaqron_settings.network_config_file.exists() - assert simulaqron_settings.network_config_file.is_file() - TestSettings._cleanup_config_files() - def test_non_existent_network_config(self): - _original_settings = """ - { - "_read_user": false, - "max_qubits": 10, - "max_registers": 500, - "conn_retry_time": 0.25, - "recv_timeout": 10, - "recv_retry_time": 0.05, - "log_level": 30, - "sim_backend": "projectq", - "network_config_file": "/not/existing/network.json", - "noisy_qubits": false, - "t1": 2.0 - } - """ - _expected_settings = """ - { - "_read_user": false, - "max_qubits": 10, - "max_registers": 500, - "conn_retry_time": 0.25, - "recv_timeout": 10, - "recv_retry_time": 0.05, - "log_level": 30, - "sim_backend": "projectq", - "network_config_file": "/not/existing/network.json", - "noisy_qubits": false, - "t1": 2.0 - } - """ - expected_settings_dict = json.loads(_expected_settings) - expected_settings = JSONSerializer.deserialize(SimulaqronConfig, expected_settings_dict) - - _original_settings = json.loads(_original_settings) - with NamedTemporaryFile(mode="w+", encoding="utf-8", delete_on_close=False) as file: - json.dump(_original_settings, file) - file.close() - - simulaqron_settings.load_from_file(Path(file.name)) - assert simulaqron_settings == expected_settings - def test_load_non_existent_config_file(self): with pytest.raises(FileNotFoundError) as error: simulaqron_settings.load_from_file("/non/existent/file") From 1d2b1c45b491e62de1b39c9e8e87625ebfdb76b3 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 13:22:08 +0100 Subject: [PATCH 124/277] Remove unnecessary simulaqron settings serializer function and dead code --- simulaqron/settings/__init__.py | 2 +- simulaqron/settings/_serialization.py | 11 ------- simulaqron/settings/simulaqron_config.py | 37 +----------------------- 3 files changed, 2 insertions(+), 48 deletions(-) diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index a76beb31..401ff801 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -1,4 +1,4 @@ -from .network_config import NetworkConfigBuilder +from .network_config import NetworkConfigBuilder, DEFAULT_SIMULAQRON_NETWORK_FILENAME from .simulaqron_config import SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME from ._serialization import init_serialization diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index 1d8253de..50abd699 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -51,17 +51,6 @@ def path_deserializer(cls: Type[Path], path: str) -> Path: return cls(path) -@JSONSerializer.register_serializer(SimulaqronConfig) -def simulaqron_config_serializer(obj: SimulaqronConfig) -> str: - object_dict = dict(obj.__dict__) - net_cfg_path = str(obj.network_config_file) - del object_dict["_builder"] - del object_dict["_net_cfg_file"] - object_dict["network_config_file"] = net_cfg_path - serialized = JSONSerializer.serialize(dict_serialization(object_dict)) - return serialized - - @JSONSerializer.register_deserializer(SimulaqronConfig) def simulaqron_config_deserializer(cls: Type[SimulaqronConfig], obj: Dict[str, Any]) -> SimulaqronConfig: new_obj = cls() diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 7632c944..f4afb7b8 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -31,7 +31,7 @@ ######################### import json import logging -from dataclasses import dataclass, fields, InitVar +from dataclasses import dataclass, fields from enum import Enum from os import PathLike from pathlib import Path @@ -40,13 +40,10 @@ from dataclasses_serialization.json import JSONSerializer from dataclasses_serialization.json import JSONSerializerMixin -from ..settings.network_config import NetworkConfigBuilder - # This is the name of the "local" simulaqron settings. # If a file named like this is found in the CWD, it will be # automatically loaded when creating the config file DEFAULT_SIMULAQRON_SETTINGS_FILENAME = "simulaqron_settings.json" -DEFAULT_SIMULAQRON_NETWORK_FILENAME = "simulaqron_network.json" class SimBackend(JSONSerializerMixin, Enum): @@ -63,7 +60,6 @@ def __repr__(self): @dataclass class SimulaqronConfig(JSONSerializerMixin): - network_config_file: InitVar[Path] = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() # Default config max_qubits: int = 20 max_registers: int = 1000 @@ -78,35 +74,6 @@ class SimulaqronConfig(JSONSerializerMixin): max_app_waiting_time: float = -1.0 # In seconds, negative means unlimited waiting t1: float = 1.0 - def __post_init__(self, network_config_file: Path): - if isinstance(network_config_file, Path) and network_config_file.exists() and network_config_file.is_file(): - self._builder = NetworkConfigBuilder() - net_cfg_file = network_config_file - else: - # Given network config file is invalid or does not exist. Use the default one - # and write it to the expected location - net_cfg_file = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() - self._builder = NetworkConfigBuilder.using_default_network() - self._builder.write_to_file(net_cfg_file) - self.network_config_file = net_cfg_file - - @property - def network_builder(self) -> NetworkConfigBuilder: - return self._builder - - @property - def network_config_file(self) -> Path: - return self._net_cfg_file - - @network_config_file.setter - def network_config_file(self, value: Path | str): - if isinstance(value, str): - value = Path(value).resolve() - # If we set the network config file, update the NetworkConfigBuilder - if value.exists() and value.is_file(): - self._builder.read_from_file(value) - self._net_cfg_file = value - @classmethod def _create_home_settings_folder(cls): home_setting_folder = (Path.home() / ".simulaqron").resolve() @@ -122,8 +89,6 @@ def load_from_file(self, file_path: Path | str): new_val = getattr(new_config, field.name) setattr(self, field.name, new_val) - self.network_config_file = new_config.network_config_file - @classmethod def _deserialize_from_file(cls, file_path: Path) -> Self: with file_path.resolve().open("rt") as file: From 222235dcffc5e534649dc59530dcd165dfafd75a Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 26 Nov 2025 17:37:04 +0100 Subject: [PATCH 125/277] Simplify the SocketsConfig class to use the already loaded in-memory NetworksConfig object instead of reading file from disk --- simulaqron/general/host_config.py | 45 ++++----------------------- simulaqron/settings/network_config.py | 26 ++++++++++++++++ 2 files changed, 32 insertions(+), 39 deletions(-) diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index c6604af0..912639b5 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -34,11 +34,11 @@ from twisted.spread import pb -from simulaqron.settings.network_config import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworkConfigBuilder, NodeConfigType class Host(pb.Referenceable): - def __init__(self, name: str, hostname: str, port: str | int): + def __init__(self, name: str, hostname: str, port: int): """ Initialize the details of the host. For now, we just keep the following: @@ -49,7 +49,7 @@ def __init__(self, name: str, hostname: str, port: str | int): self.name = name self.hostname = hostname - self.port = int(port) + self.port = port # Lookup IP address addrs = socket.getaddrinfo(hostname, port, proto=socket.IPPROTO_TCP, family=socket.AF_INET) @@ -66,7 +66,7 @@ def __init__(self, name: str, hostname: str, port: str | int): class SocketsConfig(pb.Referenceable): - def __init__(self, filename: str, network_name: str = "default", config_type: str = "vnode"): + def __init__(self, nets_config: NetworkConfigBuilder, network_name: str = "default", config_type: str | NodeConfigType = "vnode"): """ Initialize by reading in the configuration file. @@ -76,41 +76,8 @@ def __init__(self, filename: str, network_name: str = "default", config_type: st # Dictionary where we will keep host details, indexed by node name (e.g. Alice) self.hostDict: Dict[str, Host] = {} - # Read config file - self.read_config(filename, network_name=network_name, config_type=config_type) - - def read_config(self, filename: str, network_name: str = "default", config_type: str = "vnode"): - """ - Reads the configuration file in which each line has the form: node name, hostname, port number. - For example: - Alice, localhost, 8888 - """ - with open(filename) as confFile: - if filename.endswith(".json"): - if config_type not in ["vnode", "qnodeos", "app"]: - raise ValueError("Type needs to be either 'vnode', 'qnodeos' or 'app'") - if network_name is None: - network_name = "default" - network_builder = NetworkConfigBuilder() - network_builder.read_from_file(filename) - network_config = network_builder.networks[network_name] - nodes = network_config.nodes - for node_name, node_config in nodes.items(): - hostname = getattr(node_config, f"{config_type}_hostname") - port = getattr(node_config, f"{config_type}_port") - self.hostDict[node_name] = Host(node_name, hostname, port) - - elif filename.endswith(".cfg"): - for line in confFile: - if not line.startswith("#"): - words = line.split(",") - - # We will simply ignore lines which are not of the right form - if len(words) == 3: - newHost = Host(words[0].strip(), words[1].strip(), words[2].strip()) - self.hostDict[words[0]] = newHost - else: - raise ValueError(f"Unknown file type {filename.split(".")[-1]}") + for node in nets_config.get_nodes(network_name): + self.hostDict[node.name] = Host(node.name, *node.get_config(config_type)) def print_details(self, name: str): """ diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 5cb1c13a..0516b9fe 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -3,6 +3,7 @@ import socket from contextlib import closing from dataclasses import dataclass, field, fields +from enum import StrEnum from importlib import resources from os import PathLike from pathlib import Path @@ -14,6 +15,12 @@ DEFAULT_SIMULAQRON_NETWORK_FILENAME = "simulaqron_network.json" +class NodeConfigType(StrEnum): + APP = "app" + QNODEOS = "qnodeos", + VNODE = "vnode" + + @dataclass class NodeConfig(JSONSerializerMixin): """ @@ -27,6 +34,25 @@ class NodeConfig(JSONSerializerMixin): qnodeos_hostname: str = "localhost" vnode_hostname: str = "localhost" + def get_config(self, config_type: str | NodeConfigType) -> Tuple[str, int]: + """ + Gets the corresponding host and port config tuple for the given type + Args: + config_type: str | NodeConfigType + The type of configuration to get. Can either be expressed as a string or a NodeConfigType. + Returns: + A tuple containing the host and port config for the given configuration type. + """ + if isinstance(config_type, str): + config_type = NodeConfigType(config_type) + match config_type: + case NodeConfigType.APP(): + return self.app_hostname, self.app_port + case NodeConfigType.QNODEOS(): + return self.qnodeos_hostname, self.qnodeos_port + case NodeConfigType.VNODE(): + return self.vnode_hostname, self.vnode_port + def __eq__(self, other) -> bool: if not isinstance(other, NodeConfig): return False From cf9ebaa27a4adcb21123a9d9938ef19e185e6af0 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 11:06:16 +0100 Subject: [PATCH 126/277] Delete some unused code. Update documentation of SocketsConfig class --- simulaqron/general/host_config.py | 20 ++++++-------------- simulaqron/local/setup.py | 5 ++++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index 912639b5..e01646fe 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -66,12 +66,15 @@ def __init__(self, name: str, hostname: str, port: int): class SocketsConfig(pb.Referenceable): - def __init__(self, nets_config: NetworkConfigBuilder, network_name: str = "default", config_type: str | NodeConfigType = "vnode"): + def __init__(self, nets_config: NetworkConfigBuilder, network_name: str = "default", + config_type: str | NodeConfigType = "vnode"): """ Initialize by reading in the configuration file. - With version 3.0.0 there is a single config used for all networks and all config types. - Therefore one needs to also specify the network_name and config_type ('vnode', 'app') + With version 4.0.0, we use the already in-memory information to create the SocketsConfig object. + This avoids reading the file multiple times, which might have been updated by other processes + in between reads. Additionally, this also simplifies the code, and reduces the potential source + of bugs in the configuration read/write code. """ # Dictionary where we will keep host details, indexed by node name (e.g. Alice) self.hostDict: Dict[str, Host] = {} @@ -110,14 +113,3 @@ def get_node_id_from_net_config(net_config: SocketsConfig, node_name: str) -> in if node_name not in net_config.hostDict: raise ValueError(f"node name {node_name} not in host_dict ({net_config.hostDict.keys()})") return list(sorted(net_config.hostDict.keys())).index(node_name) - - -def load_node_names(config_file: str) -> List[str]: - """ - Load list of nodes from Nodes.cfg file - - :param config_file: str - pointing to Nodes.cfg file - """ - with open(config_file, 'r') as f: - return [line.strip() for line in f.readlines()] diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index 69141912..41f0105c 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -28,6 +28,7 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import time +from typing import Callable from netqasm.logging.glob import get_netqasm_logger from twisted.internet import error @@ -35,6 +36,7 @@ from twisted.internet.error import ReactorNotRunning from twisted.spread import pb +from simulaqron.general.host_config import SocketsConfig from simulaqron.reactor import reactor _logger = get_netqasm_logger("setup-local") @@ -48,7 +50,8 @@ # and other classical communication servers. -def setup_local(myName, virtualNet, classicalNet, lNode, func, *args, **kwargs): +def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsConfig, + lNode: pb.Root, func: Callable, *args, **kwargs): """ Sets up - local classical communication server (if desired according to the configuration file) From 3230d301184d372f883c2b9ebdf0219f910b996b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 11:40:37 +0100 Subject: [PATCH 127/277] Update the tests of the Network class to use the new network_config global object --- simulaqron/netqasm_backend/factory.py | 7 +-- simulaqron/network.py | 88 +++++++-------------------- simulaqron/settings/network_config.py | 8 +-- tests/quick/network/test_network.py | 82 ++----------------------- 4 files changed, 33 insertions(+), 152 deletions(-) diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index ec0d34ee..a9a97419 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -39,7 +39,7 @@ from simulaqron.general.host_config import SocketsConfig, Host from simulaqron.netqasm_backend.qnodeos import SubroutineHandler from simulaqron.sdk.connection import RichErrorMessage -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config from simulaqron.virtual_node.virtual import call_method @@ -199,10 +199,7 @@ def __init__( # Read in topology, if specified. topology=None means fully connected # topology - self.topology = None - if simulaqron_settings.network_config_file is not None: - networks_config = simulaqron_settings.network_builder - self.topology = networks_config.networks[network_name].topology + self.topology = network_config[network_name].topology def stop(self): yield call_method(self.virtRoot, "stop_vnode") diff --git a/simulaqron/network.py b/simulaqron/network.py index 75e461dd..6c05d1af 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -29,15 +29,15 @@ import random import time -from os import PathLike from timeit import default_timer as timer -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict import networkx as nx from multiprocess.context import ForkProcess as Process from netqasm.logging.glob import get_netqasm_logger, get_log_level -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config +from simulaqron.settings.network_config import NodeConfig from simulaqron.start import start_vnode, start_qnodeos # WARNING - this import *needs* to be after importing start_vnode and start_qnodeos # Otherwise the code that patches some netqasm internal definitions will not work correctly! @@ -51,74 +51,28 @@ class Network: - def __init__(self, name: str = "default", nodes: Optional[List[str]] = None, - topology: Optional[Dict[str, Any]] = None, network_config_file: PathLike | str = None, - force: bool = False, new: bool = True): + def __init__(self, nodes: List[str], network_name: str = "default"): """ Used to spin up a simulated network. - This class uses the already-loaded network configuration, and *makes no change to it*. + This class uses the network configuration loaded in the global network_config object and + starts the nodes mentioned in the constructor of this class. - If new=True then a fresh network with only the specified nodes - (or the default Alice, Bob, Charlie, David and Eve) are created and overwriting the current network with - the same name in the network config file. Otherwise, only the specified nodes are started without changing - the config file. Note that if the nodes does not currently exist and new=False, an ValueError is raised. - - If force=False an input to confirm the overwriting is issued. - - :param name: None or str (defaults to "default") - :param nodes: None or list of str - :param topology: None or dict - :param network_config_file: None or str (defaults to simulaqron_settings.network_config_file - :param force: bool - :param new: bool + :param network_name: str + The name of network to start. Defaults to "default". + :param nodes: list of str + A list of strings with the node names to start. """ self._running = False - self.name = name + self.name = network_name self.processes: List[Process] = [] self._logger = get_netqasm_logger(f"{self.__class__.__name__}({self.name})") - if network_config_file is not None: - simulaqron_settings.network_config_file = network_config_file - else: - network_config_file = simulaqron_settings.network_config_file - networks_config = simulaqron_settings.network_builder - - if new: - if nodes is None: - if isinstance(topology, dict): - self.nodes = list(topology.keys()) - else: - self.nodes = ["Alice", "Bob", "Charlie", "David", "Eve"] - else: - self.nodes = nodes - self.topology = construct_topology_config(topology, self.nodes) - if not force: - answer = input(f"Do you want to add/replace the network {self.name} in the " - f"file {network_config_file} with a network consisting " - f"of the nodes {self.nodes}? (yes/no)") - if answer.lower() not in ["yes", "y"]: - raise RuntimeError("User did not want to replace network in file") - networks_config.add_network(node_names=self.nodes, network_name=self.name, topology=self.topology) - networks_config.write_to_file(network_config_file) - else: - if topology is not None: - raise ValueError("If new is False a topology cannot be used.") - if self.name in networks_config.networks: - node_names = networks_config.get_node_names(self.name) - self.topology = networks_config.networks[self.name].topology - else: - raise ValueError(f"Network {self.name} is not in the file {network_config_file}\n" - f"If you wish to add this network to the file, use the --new flag.") - if nodes is None: - self.nodes = node_names - else: - self.nodes = nodes - for node_name in self.nodes: - if node_name not in node_names: - raise ValueError(f"Node {node_name} is not in the current network {self.name} " - f"in the file {network_config_file}\nIf you wish to overwrite " - f"the current network in the file, use the --new flag.") + # Determine the nodes to start, usiing the in-memory network config + self._nodes_to_start: List[NodeConfig] = [] + for node in network_config.get_nodes(network_name): + if node.name in nodes: + self._nodes_to_start.append(node) self._setup_processes() @@ -129,10 +83,10 @@ def running(self): """ if self._running: return True - for node in self.nodes: + for node in self._nodes_to_start: try: SimulaQronConnection.try_connection( - name=node, + name=node.name, network_name=self.name, ) except ConnectionRefusedError: @@ -154,12 +108,12 @@ def _setup_processes(self): """ Setup the processes forming the network, however they are not started yet. """ - for node in self.nodes: + for node in self._nodes_to_start: process_virtual = Process( - target=start_vnode, args=(node, self.name, get_log_level()), name=f"VirtNode {node}" + target=start_vnode, args=(node.name, self.name, get_log_level()), name=f"VirtNode {node.name}" ) process_qnodeos = Process( - target=start_qnodeos, args=(node, self.name, get_log_level()), name=f"QnodeOSNode {node}" + target=start_qnodeos, args=(node.name, self.name, get_log_level()), name=f"QnodeOSNode {node.name}" ) self.processes += [process_virtual, process_qnodeos] diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 0516b9fe..40c3ac91 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -46,11 +46,11 @@ def get_config(self, config_type: str | NodeConfigType) -> Tuple[str, int]: if isinstance(config_type, str): config_type = NodeConfigType(config_type) match config_type: - case NodeConfigType.APP(): + case NodeConfigType.APP: return self.app_hostname, self.app_port - case NodeConfigType.QNODEOS(): + case NodeConfigType.QNODEOS: return self.qnodeos_hostname, self.qnodeos_port - case NodeConfigType.VNODE(): + case NodeConfigType.VNODE: return self.vnode_hostname, self.vnode_port def __eq__(self, other) -> bool: @@ -448,7 +448,7 @@ def nodes(self) -> List[NodeConfig]: def network_names(self) -> List[str]: return list(self.networks.keys()) - def __getattr__(self, item: str) -> NetworkConfig: + def __getitem__(self, item: str) -> NetworkConfig: if isinstance(item, str): return self.networks[item] else: diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index be12013d..8cb1c115 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -1,7 +1,5 @@ -import json import time from tempfile import NamedTemporaryFile -from typing import List import pytest from timeit import default_timer as timer @@ -11,74 +9,8 @@ from simulaqron.settings.network_config import NetworkConfigBuilder -class TestInitNetwork: - def _assert_nodes(self, nodes1: List[str], nodes2: List[str]): - assert set(nodes1) == set(nodes2) - - def _assert_topology(self, topology1, topology2): - if topology1 is None: - assert topology2 is None - return - assert len(topology1) == len(topology2) - for key, neigh1 in topology1.items(): - assert key in topology2 - neigh2 = topology2[key] - self._assert_nodes(neigh1, neigh2) - - def _check_nodes_and_topology_in_file(self, network: Network): - network_config_file = simulaqron_settings.network_config_file - with open(network_config_file, 'r') as f: - network_config = json.load(f) - nodes_in_file = list(network_config[network.name]["nodes"].keys()) - self._assert_nodes(nodes_in_file, network.nodes) - - topology_in_file = network_config[network.name]["topology"] - self._assert_topology(topology_in_file, network.topology) - - @pytest.fixture(autouse=True) - def network_file(self): - simulaqron_settings.default_settings() - # We initialize a temporary file with the default network config - network_builder = NetworkConfigBuilder() - network_builder.using_default_network() - with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as net_config_file: - # We also need to specify the location of the temporal file as the network config file - network_builder.write_to_file(net_config_file.name) - simulaqron_settings.network_config_file = net_config_file.name - net_config_file.close() - self.network = None - yield net_config_file.name - self._check_nodes_and_topology_in_file(self.network) - - def test_init_no_argument(self, network_file: str): - self.network = Network(force=True, network_config_file=network_file) - default_nodes = ["Alice", "Bob", "Charlie", "David", "Eve"] - self._assert_nodes(self.network.nodes, default_nodes) - self._assert_topology(self.network.topology, None) - - def test_init_node_argument(self, network_file: str): - nodes = ["Test3", "Test4"] - self.network = Network(nodes=nodes, force=True, network_config_file=network_file) - self._assert_nodes(self.network.nodes, nodes) - self._assert_topology(self.network.topology, None) - - def test_init_topology_argument(self, network_file: str): - topology = {"Test1": [], "Test2": [], "Test3": []} - nodes = list(topology.keys()) - self.network = Network(topology=topology, force=True, network_config_file=network_file) - self._assert_nodes(self.network.nodes, nodes) - self._assert_topology(self.network.topology, topology) - - def test_init_node_and_topology_argument(self, network_file: str): - nodes = ["Test5", "Test6"] - topology = {"Test5": ["Test6"], "Test6": ["Test5"]} - self.network = Network(nodes=nodes, topology=topology, force=True, network_config_file=network_file) - self._assert_nodes(self.network.nodes, nodes) - self._assert_topology(self.network.topology, topology) - - class TestStartStopNetwork: - nodes = ["Test1", "Test2", "Test3"] + nodes = ["Alice", "Bob", "Charlie"] @pytest.fixture(autouse=True) def network_file(self): @@ -89,13 +21,11 @@ def network_file(self): with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as net_config_file: # We also need to specify the location of the temporal file as the network config file network_builder.write_to_file(net_config_file.name) - simulaqron_settings.network_config_file = net_config_file.name net_config_file.close() - self.network = None yield net_config_file.name def test_start(self, network_file: str): - network = Network(nodes=self.nodes, force=True) + network = Network(nodes=self.nodes) assert len(network.processes) == 2 * len(self.nodes) for p in network.processes: assert p.is_alive() is False @@ -105,13 +35,13 @@ def test_start(self, network_file: str): assert p.is_alive() is True def test_stop(self): - network = Network(force=True) + network = Network(nodes=self.nodes) network.stop() for p in network.processes: assert p.is_alive() is False def test_start_stop(self): - network = Network(force=True) + network = Network(nodes=self.nodes) network.start(wait_until_running=True) for p in network.processes: assert p.is_alive() is True @@ -120,7 +50,7 @@ def test_start_stop(self): assert p.is_alive() is False def test_no_wait(self): - network = Network(nodes=self.nodes, force=True) + network = Network(nodes=self.nodes) network.start(wait_until_running=False) assert network.running is False @@ -136,6 +66,6 @@ def test_no_wait(self): assert network.running is True def test_del(self): - network = Network(force=True) + network = Network(nodes=self.nodes) network.start(wait_until_running=True) del network From de86133f8aec6a1c714733788157eb6a10de5454 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 12:04:49 +0100 Subject: [PATCH 128/277] Make the "run" entry point to use the given network config or use the default if needed --- simulaqron/run/run.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 943aee5f..18011532 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -25,7 +25,7 @@ from simulaqron.network import Network from simulaqron.sdk import SimulaQronConnection -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings.simulaqron_config import SimBackend logger = get_netqasm_logger() @@ -64,17 +64,6 @@ def setup_sim_backend(sim_backend: SimBackend): simulaqron_settings.sim_backend = sim_backend -def configure_network(node_names: List[str], network_config_file: Optional[str]): - new_network = True if network_config_file is None else False - return Network( - name="default", - nodes=node_names, - network_config_file=network_config_file, - force=True, - new=new_network - ) - - # Global array helper to store PIDs of the children processes running the applications # Note; this array *will not* store the pids of the QNodeOS and/or Vnode processes apps_pids: Optional[SynchronizedArray] = None @@ -196,15 +185,16 @@ def run_applications( results: List[Dict[str, Any]] = [] if isinstance(network_cfg, str) or isinstance(network_cfg, PathLike): net_cfg = str(network_cfg) - simulaqron_settings.network_config_file = Path(net_cfg).resolve() + network_config.read_from_file(net_cfg) elif isinstance(network_cfg, Path): net_cfg = str(network_cfg.resolve()) - simulaqron_settings.network_config_file = Path(net_cfg).resolve() + network_config.read_from_file(net_cfg) else: - net_cfg = None + # If no network config file was given, we keep with the default-loaded (pwd, or home) + pass for _ in range(num_rounds): - network = configure_network(app_names, net_cfg) + network = Network(network_name="default", nodes=network_config.get_nodes("default")) # Start the processes that support the simulator: QNodeOS + VirtualNode network.start() From a4647c1d219a1dd57c2493a15f263b2cfc7a8fc2 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 13:58:00 +0100 Subject: [PATCH 129/277] Adapt host_config tests for the new usage --- simulaqron/settings/network_config.py | 5 +++ tests/quick/general/test_host_config.py | 56 +++++++++++++++++++++---- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 40c3ac91..4742a003 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -408,6 +408,11 @@ def read_from_file(self, file_path: PathLike | str): new_val = getattr(new_config, class_field.name) setattr(self, class_field.name, new_val) + def read_from_legacy_files(self, app_file_path: PathLike | str, + qnodeos_file_path: PathLike | str, + vnode_file_path: PathLike | str): + raise NotImplementedError("Reading form legacy config files is not supported yet") + @classmethod def _deserialize_from_file(cls, file_path: Path) -> Self: with file_path.resolve().open("rt") as file: diff --git a/tests/quick/general/test_host_config.py b/tests/quick/general/test_host_config.py index fce19be3..b807e46d 100644 --- a/tests/quick/general/test_host_config.py +++ b/tests/quick/general/test_host_config.py @@ -1,19 +1,57 @@ from pathlib import Path -from tempfile import NamedTemporaryFile -from simulaqron.general.host_config import NetworkConfigBuilder, SocketsConfig +import pytest + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.settings import network_config +from simulaqron.settings.network_config import NodeConfigType -# TODO - Move these test to the new test class class TestSocketsConfig: - def test_load_file(self): + @staticmethod + def _assert_results(app_conf: SocketsConfig, qnodeos_conf: SocketsConfig, vnode_conf: SocketsConfig): + assert len(app_conf.hostDict) == 2 + assert "Alice" in app_conf.hostDict + assert "Bob" in app_conf.hostDict + assert app_conf.hostDict["Alice"].port == 8000 + assert app_conf.hostDict["Bob"].port == 8003 + + assert len(qnodeos_conf.hostDict) == 2 + assert "Alice" in qnodeos_conf.hostDict + assert "Bob" in qnodeos_conf.hostDict + assert qnodeos_conf.hostDict["Alice"].port == 8001 + assert qnodeos_conf.hostDict["Bob"].port == 8004 + + assert len(vnode_conf.hostDict) == 2 + assert "Alice" in vnode_conf.hostDict + assert "Bob" in vnode_conf.hostDict + assert vnode_conf.hostDict["Alice"].port == 8002 + assert vnode_conf.hostDict["Bob"].port == 8005 + + @pytest.mark.skip(reason="Reading network config from legacy format files is not implemented yet") + def test_load_legacy_net_config_file(self): this_file_folder = Path(__file__).parent sockets_config_path = this_file_folder / "resources" / "sockets.cfg" - conf1 = SocketsConfig(str(sockets_config_path.resolve())) + qnodeos_config_path = this_file_folder / "resources" / "qnodeos.cfg" + virtual_config_path = this_file_folder / "resources" / "virtual.cfg" + network_config.read_from_legacy_files(app_file_path=sockets_config_path, + qnodeos_config_path=qnodeos_config_path, + virtual_config_path=virtual_config_path) + + app_conf = SocketsConfig(network_config, config_type=NodeConfigType.APP) + qnodeos_conf = SocketsConfig(network_config, config_type="qnodeos") + vnode_conf = SocketsConfig(network_config) + TestSocketsConfig._assert_results(app_conf, qnodeos_conf, vnode_conf) + + + def test_load_new_net_config_file(self): + this_file_folder = Path(__file__).parent network_config_path = this_file_folder / "resources" / "network.json" - conf2 = SocketsConfig(str(network_config_path.resolve()), config_type="qnodeos") + network_config.read_from_file(network_config_path) + + app_conf = SocketsConfig(network_config, config_type=NodeConfigType.APP) + qnodeos_conf = SocketsConfig(network_config, config_type="qnodeos") + vnode_conf = SocketsConfig(network_config) - for node_name, host in conf1.hostDict.items(): - assert host.port == conf2.hostDict[node_name].port - assert host.hostname == conf2.hostDict[node_name].hostname + TestSocketsConfig._assert_results(app_conf, qnodeos_conf, vnode_conf) From 0466309dd50b1f695c9e00b015e9ecfdcbe15e61 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 14:15:59 +0100 Subject: [PATCH 130/277] Fix bug in the "run_applications" main entry point --- simulaqron/run/run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 18011532..1ee09440 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -194,7 +194,7 @@ def run_applications( pass for _ in range(num_rounds): - network = Network(network_name="default", nodes=network_config.get_nodes("default")) + network = Network(network_name="default", nodes=network_config.get_node_names("default")) # Start the processes that support the simulator: QNodeOS + VirtualNode network.start() From dec927bbdcdfd11283facc5ec1234eae3c8878f5 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 14:18:00 +0100 Subject: [PATCH 131/277] Make tests for free and get_qubit_state to rely on default configurations --- tests/quick/netqasm/test_free_qubit.py | 21 +-------------------- tests/quick/netqasm/test_get_qubit_state.py | 19 ------------------- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/tests/quick/netqasm/test_free_qubit.py b/tests/quick/netqasm/test_free_qubit.py index 61951db1..d604c8f9 100644 --- a/tests/quick/netqasm/test_free_qubit.py +++ b/tests/quick/netqasm/test_free_qubit.py @@ -1,13 +1,8 @@ -from tempfile import NamedTemporaryFile - import pytest from netqasm.runtime.settings import set_simulator -from simulaqron.run.run import reset, run_applications -from simulaqron.settings import simulaqron_settings -from simulaqron.settings.simulaqron_config import SimBackend -from simulaqron.settings.network_config import NetworkConfigBuilder +from simulaqron.run.run import run_applications set_simulator("simulaqron") @@ -17,20 +12,6 @@ class TestFreeQubit: - @pytest.fixture(autouse=True) - def configuration(self): - with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as network_settings_file: - network_config = NetworkConfigBuilder.using_default_network() - network_config.write_to_file(network_settings_file.name) - with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as simulaqron_settings_file: - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value - simulaqron_settings.network_config_file = network_settings_file.name - simulaqron_settings.save_to_file(simulaqron_settings_file.name) - simulaqron_settings.load_from_file(simulaqron_settings_file.name) - yield - reset() - @staticmethod def too_many_qubits(): with NetQASMConnection("Alice", max_qubits=2) as alice: diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 5ea010ec..bf882308 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -1,15 +1,10 @@ import math -from tempfile import NamedTemporaryFile import numpy as np import pytest from netqasm.runtime.settings import set_simulator from netqasm.sdk.classical_communication.message import StructuredMessage -from simulaqron.settings import simulaqron_settings -from simulaqron.settings.simulaqron_config import SimBackend -from simulaqron.settings.network_config import NetworkConfigBuilder - set_simulator("simulaqron") from netqasm.runtime.application import default_app_instance # noqa: E402 @@ -20,20 +15,6 @@ class TestGetQubit: - @pytest.fixture(autouse=True) - def configuration(self): - with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as network_settings_file: - network_config = NetworkConfigBuilder.using_default_network() - network_config.write_to_file(network_settings_file.name) - with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as simulaqron_settings_file: - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value - simulaqron_settings.network_config_file = network_settings_file.name - simulaqron_settings.save_to_file(simulaqron_settings_file.name) - simulaqron_settings.load_from_file(simulaqron_settings_file.name) - yield - reset() - # Here we define the quantum programs used in the tests @staticmethod From 59dfeb24e368289c548f098c0441d015f01361a7 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 14:19:05 +0100 Subject: [PATCH 132/277] Fix bug in connection to use a non-None value for default network name --- simulaqron/sdk/connection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 0883f6b5..0e4aa2c3 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -46,7 +46,7 @@ def __init__( compiler: Optional[Type[SubroutineTranspiler]] = None, socket_address=None, conn_retry_time: float = 0.1, - network_name: Optional[str] = None, + network_name: str = "default", ): super().__init__( app_name=app_name, @@ -96,7 +96,7 @@ def __init__( def try_connection( name: str, socket_address: Optional[Tuple[str, int]] = None, - network_name: str = None, + network_name: str = "default", ): # NOTE using retry_time=None causes an error to be raised of the connection cannot # be established, which can be used to check if the connection is available @@ -111,9 +111,9 @@ def try_connection( @staticmethod def _create_socket( name: str, + network_name: str, socket_address: Optional[Tuple[str, int]] = None, - network_name: str = None, - retry_time: Optional[float] = 0.1, + retry_time: float = 0.1, ) -> Tuple[SocketsConfig, socket.socket]: # Get network configuration and addresses addr, qnodeos_net = SimulaQronConnection._setup_network_data( From d29ef1d48c69feb405e50f4c9e938ca1409db1a7 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 14:40:38 +0100 Subject: [PATCH 133/277] Correctly reset configurations between tests in free and get_qubit_state tests --- tests/quick/netqasm/test_free_qubit.py | 6 ++++++ tests/quick/netqasm/test_get_qubit_state.py | 24 ++++++++++++++------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/quick/netqasm/test_free_qubit.py b/tests/quick/netqasm/test_free_qubit.py index d604c8f9..cfe5befc 100644 --- a/tests/quick/netqasm/test_free_qubit.py +++ b/tests/quick/netqasm/test_free_qubit.py @@ -3,6 +3,7 @@ from netqasm.runtime.settings import set_simulator from simulaqron.run.run import run_applications +from simulaqron.settings import simulaqron_settings, network_config set_simulator("simulaqron") @@ -12,6 +13,11 @@ class TestFreeQubit: + @pytest.fixture(autouse=True) + def reset_configs(self): + simulaqron_settings.default_settings() + network_config.using_default_network() + @staticmethod def too_many_qubits(): with NetQASMConnection("Alice", max_qubits=2) as alice: diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index bf882308..03b7d80e 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -5,6 +5,9 @@ from netqasm.runtime.settings import set_simulator from netqasm.sdk.classical_communication.message import StructuredMessage +from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings.simulaqron_config import SimBackend + set_simulator("simulaqron") from netqasm.runtime.application import default_app_instance # noqa: E402 @@ -14,9 +17,14 @@ from simulaqron.run.run import run_applications, reset # noqa: E402 -class TestGetQubit: - # Here we define the quantum programs used in the tests +class TestGetQubitState: + @pytest.fixture(autouse=True) + def reset_configs(self): + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + network_config.using_default_network() + # Here we define the quantum programs used in the tests @staticmethod def peek_new_unflushed_qubit(): with NetQASMConnection("Alice") as alice: @@ -103,7 +111,7 @@ def bob_teleport(): def test_peek_new_unflushed_qubit(self): apps = default_app_instance( [ - ("Alice", TestGetQubit.peek_new_unflushed_qubit) + ("Alice", TestGetQubitState.peek_new_unflushed_qubit) ] ) with pytest.raises(RuntimeError) as exc: @@ -113,7 +121,7 @@ def test_peek_new_unflushed_qubit(self): def test_peek_unflushed_qubit(self): apps = default_app_instance( [ - ("Alice", TestGetQubit.peek_unflushed_qubit) + ("Alice", TestGetQubitState.peek_unflushed_qubit) ] ) with pytest.raises(RuntimeError) as exc: @@ -123,7 +131,7 @@ def test_peek_unflushed_qubit(self): def test_get_basic_state_local(self): apps = default_app_instance( [ - ("Alice", TestGetQubit.peek_init_qubit) + ("Alice", TestGetQubitState.peek_init_qubit) ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) @@ -133,7 +141,7 @@ def test_get_basic_state_local(self): def test_get_qubit_state_local(self): apps = default_app_instance( [ - ("Alice", TestGetQubit.peek_local_qubit) + ("Alice", TestGetQubitState.peek_local_qubit) ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) @@ -148,8 +156,8 @@ def test_get_qubit_state_local(self): def test_get_qubit_state_teleport(self): apps = default_app_instance( [ - ("Alice", TestGetQubit.alice_teleport), - ("Bob", TestGetQubit.bob_teleport) + ("Alice", TestGetQubitState.alice_teleport), + ("Bob", TestGetQubitState.bob_teleport) ] ) raw_results = run_applications(apps, use_app_config=False, enable_logging=False) From 78a073d2b5a80edfcb2125d1b9976da2c18be022 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 16:15:39 +0100 Subject: [PATCH 134/277] Fix failed tests --- tests/quick/general/test_classical_sockets.py | 21 +++++-------------- tests/quick/netqasm/test_get_qubit_state.py | 2 +- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py index a3eaa0e0..14e40882 100644 --- a/tests/quick/general/test_classical_sockets.py +++ b/tests/quick/general/test_classical_sockets.py @@ -1,30 +1,19 @@ -from tempfile import NamedTemporaryFile - import pytest from netqasm.runtime.application import default_app_instance from simulaqron.run import run_applications -from simulaqron.run.run import reset from simulaqron.sdk.socket import Socket -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings.simulaqron_config import SimBackend -from simulaqron.settings.network_config import NetworkConfigBuilder class TestClassicalSocket: @pytest.fixture(autouse=True) def configurations(self): - with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as network_settings_file: - network_config = NetworkConfigBuilder.using_default_network() - network_config.write_to_file(network_settings_file.name) - with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as simulaqron_settings_file: - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value - simulaqron_settings.network_config_file = network_settings_file.name - simulaqron_settings.save_to_file(simulaqron_settings_file.name) - simulaqron_settings.load_from_file(simulaqron_settings_file.name) - yield - reset() + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ + network_config.using_default_network() + yield @staticmethod def alice_program_sender(): diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 03b7d80e..a4ee2144 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -21,7 +21,7 @@ class TestGetQubitState: @pytest.fixture(autouse=True) def reset_configs(self): simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ.value + simulaqron_settings.sim_backend = SimBackend.PROJECTQ network_config.using_default_network() # Here we define the quantum programs used in the tests From e1915f03be4ccf00e0e0d9142c065cf73db19687 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 16:45:24 +0100 Subject: [PATCH 135/277] Fix bug when resetting network config to the default value --- simulaqron/network.py | 2 +- simulaqron/settings/network_config.py | 8 ++++---- tests/quick/network/test_network.py | 13 ++----------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/simulaqron/network.py b/simulaqron/network.py index 6c05d1af..ceeefb9e 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -68,7 +68,7 @@ def __init__(self, nodes: List[str], network_name: str = "default"): self.processes: List[Process] = [] self._logger = get_netqasm_logger(f"{self.__class__.__name__}({self.name})") - # Determine the nodes to start, usiing the in-memory network config + # Determine the nodes to start, using the in-memory network config self._nodes_to_start: List[NodeConfig] = [] for node in network_config.get_nodes(network_name): if node.name in nodes: diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 4742a003..fabb3298 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -161,13 +161,13 @@ class NetworkConfigBuilder(JSONSerializerMixin): networks: Dict[str, NetworkConfig] = field(default_factory=dict) used_sockets: List[Tuple[str, int]] = field(default_factory=list) - @classmethod - def using_default_network(cls) -> Self: + def using_default_network(self): # We use the embedded default network here default_network_path = resources.files(simulaqron._default_config).joinpath("default_network.json") - new_builder = cls() + new_builder = NetworkConfigBuilder() new_builder.read_from_file(Path(str(default_network_path))) - return new_builder + self.networks = new_builder.networks + self.used_sockets = new_builder.used_sockets def _correct_network_port_if_needed(self, hostname: str, port: int) -> int: """ diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index 8cb1c115..087adda1 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -1,12 +1,10 @@ import time -from tempfile import NamedTemporaryFile import pytest from timeit import default_timer as timer -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config from simulaqron.network import Network -from simulaqron.settings.network_config import NetworkConfigBuilder class TestStartStopNetwork: @@ -15,14 +13,7 @@ class TestStartStopNetwork: @pytest.fixture(autouse=True) def network_file(self): simulaqron_settings.default_settings() - # We initialize a temporary file with the default network config - network_builder = NetworkConfigBuilder() - network_builder.using_default_network() - with NamedTemporaryFile(mode="w", suffix=".json", delete_on_close=False) as net_config_file: - # We also need to specify the location of the temporal file as the network config file - network_builder.write_to_file(net_config_file.name) - net_config_file.close() - yield net_config_file.name + network_config.using_default_network() def test_start(self, network_file: str): network = Network(nodes=self.nodes) From ae5798e6371702d4f154c01c924d343c462c31be Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 16:48:20 +0100 Subject: [PATCH 136/277] Use the loaded network config when creating SocketsConfig objects --- simulaqron/sdk/socket.py | 5 ++--- simulaqron/start/start_qnodeos.py | 9 +++------ simulaqron/start/start_vnode.py | 6 +----- simulaqron/virtual_node/virtual.py | 17 +++++------------ 4 files changed, 11 insertions(+), 26 deletions(-) diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index b62d932a..3c8a35df 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -8,7 +8,7 @@ from netqasm.sdk.classical_communication.socket import Socket as _Socket from simulaqron.general.host_config import SocketsConfig, Host -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config class Socket(_Socket): @@ -191,6 +191,5 @@ def _get_addr_info(self, name): return remote_host.addr def _get_app_net_config(self) -> SocketsConfig: - network_config_file = simulaqron_settings.network_config_file - app_net = SocketsConfig(str(network_config_file), network_name=self._network_name, config_type="app") + app_net = SocketsConfig(network_config, network_name=self._network_name, config_type="app") return app_net diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 86b30c96..370d35ef 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -14,7 +14,7 @@ from simulaqron.netqasm_backend.factory import NetQASMFactory from simulaqron.netqasm_backend.qnodeos import SubroutineHandler from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config logger = get_netqasm_logger("start_qnodeos") @@ -133,12 +133,9 @@ def start_qnodeos(node_name: str, network_name: str = "default", log_level: str signal.signal(signal.SIGTERM, sigterm_handler) signal.signal(signal.SIGINT, sigterm_handler) - # Since version 3.0.0 a single config file is used - network_config_file = str(simulaqron_settings.network_config_file) - # Read configuration files for the virtual quantum, as well as the classical network - virtual_network = SocketsConfig(network_config_file, network_name=network_name, config_type="vnode") - qnodeos_network = SocketsConfig(network_config_file, network_name=network_name, config_type="qnodeos") + virtual_network = SocketsConfig(network_config, network_name=network_name, config_type="vnode") + qnodeos_network = SocketsConfig(network_config, network_name=network_name, config_type="qnodeos") # Check if we are in the host-dictionary if node_name in qnodeos_network.hostDict: diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 634a8764..1ece8692 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -63,11 +63,7 @@ def start_vnode(name: str, network_name: str = "default", log_level: str = "WARN signal.signal(signal.SIGINT, partial(sigterm_handler, name)) logger.debug("Starting VIRTUAL NODE %s", name) - if simulaqron_settings.network_config_file is not None: - virtual_file = str(simulaqron_settings.network_config_file) - else: - virtual_file = simulaqron_settings.vnode_file - be = Backend(name, virtual_file, network_name=network_name) + be = Backend(name, network_name=network_name) be.start(max_qubits=simulaqron_settings.max_qubits, max_registers=simulaqron_settings.max_registers) logger.debug("Ending VIRTUAL NODE %s", name) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index b13ce3af..ece2a027 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -45,7 +45,7 @@ # We need this import (despite unused) to reraise local errors # We then supress the flake8 errors associated with that from simulaqron.general.errors import * # noqa: F401, F403 -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings.simulaqron_config import SimBackend from simulaqron.reactor import reactor @@ -90,23 +90,16 @@ def call_method(obj, method_name, *args, **kwargs): # forming the quantum network # class Backend: - def __init__(self, name: str, virtual_file: str, network_name: str = "default"): + def __init__(self, name: str, network_name: str = "default"): """ - Initialize. This will read the configuration file and populate the name,hostname,port information with the + Initialize. This will read the networks configuration and populate the name,hostname,port information with the information found in the configuration file for the given name. """ self._logger = get_netqasm_logger(f"{self.__class__.__name__}({name})") # Read the configuration file - try: - self.config = SocketsConfig(virtual_file, network_name=network_name, config_type="vnode") - self.myID: Host = self.config.hostDict[name] - except KeyError as e: - self._logger.error("No such name in the configuration file %s: %s", virtual_file, e) - raise e - except Exception as e: - self._logger.error("Error reading the configuration file %s: %s", virtual_file, e) - raise e + self.config = SocketsConfig(network_config, network_name=network_name, config_type="vnode") + self.myID: Host = self.config.hostDict[name] def start( self, From 503e78bdb4223ac2a7a5892f143b299324c64536 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 16:48:41 +0100 Subject: [PATCH 137/277] Small code changes --- simulaqron/sdk/connection.py | 103 +++++++++++++++++------------------ 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 0e4aa2c3..0755a008 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -22,7 +22,7 @@ from simulaqron.general import SimUnsupportedError from simulaqron.general.host_config import (SocketsConfig, get_node_id_from_net_config) -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config logger = get_netqasm_logger("SimulaQronConnection") @@ -105,7 +105,7 @@ def try_connection( name=name, socket_address=socket_address, network_name=network_name, - retry_time=None, + retry_time=-1.0, ) @staticmethod @@ -136,8 +136,8 @@ def _setup_network_data( ) -> Tuple[tuple[socket.AddressFamily, socket.SocketKind, int, str, tuple[str, int]], Optional[SocketsConfig]]: qnodeos_net: Optional[SocketsConfig] = None if socket_address is None: - qnodeos_net = _get_qnodeos_net_config(network_name=network_name) + qnodeos_net = SocketsConfig(network_config, network_name=network_name, config_type="qnodeos") # Host data if name in qnodeos_net.hostDict: myHost = qnodeos_net.hostDict[name] @@ -177,7 +177,7 @@ def _setup_socket( qnodeos_socket.connect(addr[4]) break except ConnectionRefusedError as err: - if retry_time is None or retry_time == 0: + if retry_time <= 0: raise err logger.debug( "App %s : Could not connect to NetQASM server, trying again...", @@ -279,46 +279,50 @@ def _handle_reply(self) -> int: self.buf = self.buf[len(ret_msg):] self._logger.debug("Got message %s", ret_msg) - if isinstance(ret_msg, MsgDoneMessage): - self._waiting_msg_ids.remove(ret_msg.msg_id) - self._done_msg_ids.add(ret_msg.msg_id) - # Call the registered callback, if any - if ret_msg.msg_id in self._messages_callbacks: - if SimulaQronConnection.PROCESS_POOL is None: - raise RuntimeError("Callback process pool was not set correctly") - if self._messages_callbacks[ret_msg.msg_id] is not None: - SimulaQronConnection.PROCESS_POOL.apply_async( - self._messages_callbacks[ret_msg.msg_id] - ) - del self._messages_callbacks[ret_msg.msg_id] - return ret_msg.msg_id - elif isinstance(ret_msg, ReturnRegMessage): - self._update_shared_memory( - entry=Register.from_raw(raw=ret_msg.register), - value=ret_msg.value, - ) - elif isinstance(ret_msg, ReturnArrayMessage): - self._update_shared_memory( - entry=Address(address=ret_msg.address), - value=ret_msg.values, - ) - elif isinstance(ret_msg, ReturnQubitStateMessage): - # We locally store the state info to return it later. We have to - # do this since _handle_reply cannot return values others than the - # message id when handling the reply of the original message - self._store_qubit_state( - ret_msg.qubit_id, - ret_msg.dimension, - ret_msg.get_real_part(), - ret_msg.get_imag_part() - ) - elif isinstance(ret_msg, RichErrorMessage): - if ret_msg.err_code == ErrorCode.UNSUPP.value: - raise SimUnsupportedError("Operation not supported") - else: - raise RuntimeError(f"Received error message from backend: {ret_msg.get_err_msg()}") - else: - raise NotImplementedError(f"Unknown return message of type {type(ret_msg)}") + match ret_msg: + case MsgDoneMessage(): + self._waiting_msg_ids.remove(ret_msg.msg_id) + self._done_msg_ids.add(ret_msg.msg_id) + # Call the registered callback, if any + if ret_msg.msg_id in self._messages_callbacks: + if SimulaQronConnection.PROCESS_POOL is None: + raise RuntimeError("Callback process pool was not set correctly") + if self._messages_callbacks[ret_msg.msg_id] is not None: + SimulaQronConnection.PROCESS_POOL.apply_async( + self._messages_callbacks[ret_msg.msg_id] + ) + del self._messages_callbacks[ret_msg.msg_id] + return ret_msg.msg_id + case ReturnRegMessage(): + self._update_shared_memory( + entry=Register.from_raw(raw=ret_msg.register), + value=ret_msg.value, + ) + return -1 + case ReturnArrayMessage(): + self._update_shared_memory( + entry=Address(address=ret_msg.address), + value=ret_msg.values, + ) + return -1 + case ReturnQubitStateMessage(): + # We locally store the state info to return it later. We have to + # do this since _handle_reply cannot return values others than the + # message id when handling the reply of the original message + self._store_qubit_state( + ret_msg.qubit_id, + ret_msg.dimension, + ret_msg.get_real_part(), + ret_msg.get_imag_part() + ) + return -1 + case RichErrorMessage(): + if ret_msg.err_code == ErrorCode.UNSUPP.value: + raise SimUnsupportedError("Operation not supported") + else: + raise RuntimeError(f"Received error message from backend: {ret_msg.get_err_msg()}") + case _: + raise NotImplementedError(f"Unknown return message of type {type(ret_msg)}") def block(self): while len(self._waiting_msg_ids) > 0: @@ -534,26 +538,19 @@ def get_imag_part(self) -> List[List[float]]: } -def _get_qnodeos_net_config(network_name: str) -> SocketsConfig: - network_config_file = simulaqron_settings.network_config_file - return SocketsConfig( - str(network_config_file), network_name=network_name, config_type="qnodeos" - ) - - class SimulaQronNetworkInfo(NetworkInfo): @classmethod def _get_node_id(cls, node_name: str) -> int: """Returns the node id for the node with the given name""" # TODO always use network name "default"? - _qnodeos_net = _get_qnodeos_net_config(network_name="default") + _qnodeos_net = SocketsConfig(network_config, config_type="qnodeos") return get_node_id_from_net_config(_qnodeos_net, node_name) @classmethod def _get_node_name(cls, node_id: int) -> str: """Returns the node name for the node with the given ID""" # TODO always use network name "default"? - _qnodeos_net = _get_qnodeos_net_config(network_name="default") + _qnodeos_net = SocketsConfig(network_config, config_type="qnodeos") for node_name, host in _qnodeos_net.hostDict.items(): if node_id == host.ip: return node_name From c4439f7bbb17df2faa082ef8bf1e725307767d10 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 23:01:51 +0100 Subject: [PATCH 138/277] Adapt simulaqron CLI code to use the new configurations --- examples/nativeMode/template/run.sh | 5 - simulaqron/simulaqron.py | 386 +++++++++++++--------------- 2 files changed, 172 insertions(+), 219 deletions(-) diff --git a/examples/nativeMode/template/run.sh b/examples/nativeMode/template/run.sh index bcd29207..3f0830ce 100755 --- a/examples/nativeMode/template/run.sh +++ b/examples/nativeMode/template/run.sh @@ -3,8 +3,3 @@ # Run the files for Alice, Bob or whatever nodes you construct python3 bobTest.py & python3 aliceTest.py - - - - - diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 74ba728b..5d03d751 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -3,15 +3,15 @@ import logging import time from pathlib import Path -from typing import Optional, Callable +from typing import Optional, Callable, List import click from daemons.interfaces import exit from daemons.prefab import run from simulaqron.network import Network -from simulaqron.settings import simulaqron_settings -from simulaqron.settings.network_config import NetworkConfigBuilder +from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings.network_config import DEFAULT_SIMULAQRON_NETWORK_FILENAME from simulaqron.settings.simulaqron_config import SimBackend, DEFAULT_SIMULAQRON_SETTINGS_FILENAME CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -23,42 +23,37 @@ Path.mkdir(PID_FOLDER) +class RunningSimulaQronDaemon(run.RunDaemon): + """ + SimulaQronDaemon class used to represent SimulaQron daemons that are already running. + This class is useful to stop the already-running daemons without needed to read all + the required configurations. + """ + def __init__(self, pidfile: Path): + assert pidfile is not None + super().__init__(pidfile=pidfile) + + class SimulaQronDaemon(run.RunDaemon): - def __init__(self, pidfile: Path, name: Optional[str] = None, nrnodes: Optional[int] = None, - nodes: Optional[str] = None, topology=None, new: bool = True): + def __init__(self, pidfile: Path, name: str, nodes: List[str]): super().__init__(pidfile=pidfile) - self.name = name if name is not None else "default" - self.nrnodes = nrnodes + self.name = name self.nodes = nodes - self.topology = topology - self.new = new def run(self): """Starts all nodes defined in netsim's config directory.""" - - if self.nrnodes or self.nodes or self.topology: - if self.nodes: - nodes = self.nodes.split(",") - else: - nodes = [] - - if self.nrnodes and (self.nrnodes > len(nodes)): - nodes += [f"Node{i}" for i in range(self.nrnodes - len(nodes))] - else: - nodes = self.nodes - - network = Network(name=self.name, nodes=nodes, topology=self.topology, new=self.new, force=True) + network = Network(network_name=self.name, nodes=self.nodes) network.start() while True: time.sleep(0.1) -def _is_positive_answer(answer): +def _is_positive_answer(answer: str): """ Used to check if an answer is positive from a user. """ - if answer in ["yes", "y"]: + if answer.lower() in ["yes", "y"]: return True return False @@ -88,67 +83,36 @@ def version(): @cli.command() @click.option( "--name", - help="Give the network a name to be able to start multiple (default: default)", - type=click.STRING, + help="Give the network a name to be able to start multiple (default: 'default')", + type=str, default="default", ) @click.option( "-N", "--nrnodes", - help="Number of nodes to start \n(WARNING: overwrites existing config files)", - type=click.INT, - default=None, + help="Number of nodes to start. This argument will start nodes named 'Node',\n" + "until the number of nodes is reached.", + type=int, + default=0, ) @click.option( "-n", "--nodes", - help="Comma separated list of nodes to start \n(WARNING: overwirtes existing config files)", - type=click.STRING, - default=None, + help="Comma separated list of nodes to start.", + type=str, + default="", ) -@click.option( - "-t", - "--topology", - help="Topology of network \n(WARNING: overwrites existing config files)", - type=click.STRING, - default=None, -) -@click.option( - "-f", - "--force", - help="Force re-write of network_config_file.\n" - "Note used if --keep flag is used.", - is_flag=True, -) -@click.option( - "--keep", - help="If set, the network_config_file won't be changed.\n" - "This is useful if you wish to start a subset of the nodes in the config " - "file without changing it.\n" - "If not set, simulaqron will ask if you really wan't to change the config-file.\n" - "If you want to supress this question, use the --force/-f flag.", - is_flag=True, -) -def start(name: str, nrnodes: Optional[int], nodes: Optional[str], - topology: Optional[str], force: Optional[bool], keep: Optional[bool]): +def start(name: str, nrnodes: int, nodes: str): """Starts a network with the given parameters or from config files.""" - new = not keep pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): logging.warning("Network with name %s is already running", name) logging.warning("The pidfile for this network is located at %s", pidfile) return - if new: - if not force: - # We will save this new network file in the current directory - simulaqron_settings.network_config_file = Path.cwd() / sim_backend.DEFAULT_NETWORK_CONFIG_FILE - answer = input(f"Do you want to add/replace the network '{name}' in the file " - f"{simulaqron_settings.network_config_file} with a new network? " - f"(yes/no)") - if not _is_positive_answer(answer): - print("Aborted!") - return - d = SimulaQronDaemon(pidfile=pidfile, name=name, nrnodes=nrnodes, nodes=nodes, topology=topology, new=new) + nodes = nodes.split(",") + if nrnodes > 0 and len(nodes) < nrnodes: + nodes += [f"Node{i}" for i in range(nrnodes - len(nodes))] + d = SimulaQronDaemon(pidfile=pidfile, name=name, nodes=nodes) try: d.start() except SystemExit as e: @@ -163,23 +127,21 @@ def start(name: str, nrnodes: Optional[int], nodes: Optional[str], # stop command # ############### - @cli.command() @click.option( "--name", help="Stop the network with then a given name (default: default)", type=click.STRING, - default=None, + default="default", ) -def stop(name: Optional[str]): +def stop(name: str): """Stops a network.""" - if name is None: - name = "default" + assert name is not None pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): logging.warning("Network with name %s is not running", name) return - d = SimulaQronDaemon(pidfile=pidfile) + d = RunningSimulaQronDaemon(pidfile=pidfile) d.stop() @@ -187,7 +149,6 @@ def stop(name: Optional[str]): # reset command # ################# - @cli.command() @click.option( "-f", @@ -195,23 +156,25 @@ def stop(name: Optional[str]): help="Don't ask for confirmation.", is_flag=True, ) -def reset(force: Optional[bool]): +def reset(force: bool): """Resets simulaqron""" + cwd_simulaqron_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME if not force: answer = input("Are you sure you want to reset simulaqron?\nThis will revert settings and " "network config files to the default values.\nNote, this action will remove " - f"the file at {DEFAULT_SIMULAQRON_SETTINGS_FILENAME} if it exists.\n" + f"the file at {cwd_simulaqron_settings} if it exists.\n" "(yes/no)") else: answer = "yes" if _is_positive_answer(answer): for entry in PID_FOLDER.iterdir(): if entry.suffix == ".pid": - d = SimulaQronDaemon(pidfile=entry) + d = RunningSimulaQronDaemon(pidfile=entry) d.stop() if entry.exists(): entry.unlink() simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(cwd_simulaqron_settings) else: print("Aborting!") @@ -219,9 +182,8 @@ def reset(force: Optional[bool]): def updates_local_config(command_function: Callable): def wrapper(*args, **kwargs): local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(local_settings) - else: + if not local_settings.exists(): + local_settings.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(local_settings) simulaqron_settings.load_from_file(local_settings) @@ -235,99 +197,129 @@ def wrapper(*args, **kwargs): # set command # ############### -@cli.group() +@cli.group( + help="Change a simulaqron setting" +) def set(): - """Change a setting""" pass -@set.command() +@set.command( + help="Sets all settings back to default and saves it as a local configuration file in the current folder." +) def default(): - """Sets all settings back to default and saves it as a local configuration file""" simulaqron_settings.default_settings() simulaqron_settings.save_to_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) -@set.command() -@click.argument('value', type=click.Choice([b.value for b in SimBackend])) +@set.command( + help="The backend to use (stabilizer, projectq, qutip)." +) +@click.argument( + "value", + type=click.Choice([b.value for b in SimBackend]) +) @updates_local_config def sim_backend(value): - """The backend to use (stabilizer, projectq, qutip).""" simulaqron_settings.sim_backend = value -@set.command() -@click.argument('value', type=int) +@set.command( + help="Max virt-qubits per node and max sim-qubits per register." +) +@click.argument( + 'value', + type=int +) @updates_local_config def max_qubits(value): - """Max virt-qubits per node and max sim-qubits per register.""" simulaqron_settings.max_qubits = value -@set.command() -@click.argument('value', type=int) +@set.command( + help="How many registers a node can hold." +) +@click.argument( + 'value', + type=int +) @updates_local_config def max_registers(value): - """How many registers a node can hold.""" simulaqron_settings.max_registers = value -@set.command() -@click.argument('value', type=float) +@set.command( + help="If setup fails, how long to wait until a retry." +) +@click.argument( + 'value', + type=float +) @updates_local_config def conn_retry_time(value): - """If setup fails, how long to wait until a retry.""" simulaqron_settings.conn_retry_time = value -@set.command() -@click.argument('value', type=float) +@set.command( + help="When receiving a qubit or EPR pair, how long to wait until raising a timeout." +) +@click.argument( + 'value', + type=float +) @updates_local_config def recv_timeout(value): - """When receiving a qubit or EPR pair, how long to wait until raising a timeout.""" simulaqron_settings.recv_timeout = value -@set.command() -@click.argument('value', type=float) +@set.command( + help="When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received." +) +@click.argument( + 'value', + type=float +) @updates_local_config def recv_retry_time(value): - """When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received.""" simulaqron_settings.recv_retry_time = value -@set.command() -@click.argument('value', type=int) +@set.command( + help="Log level for both backend and frontend\n10=DEBUG, 20=INFO, 30=WARNING, 40=ERROR, 50=CRITICAL." +) +@click.argument( + 'value', + type=int +) @updates_local_config def log_level(value): - """Log level for both backend and frontend\n10=DEBUG, 20=INFO, 30=WARNING, 40=ERROR, 50=CRITICAL.""" simulaqron_settings.log_level = value -@set.command() -@click.argument('value', type=str) -@updates_local_config -def network_config_file(value): - """The path to the network_config_file to be used""" - simulaqron_settings.network_config_file = value - - -@set.command() -@click.argument('value', type=click.Choice(["on", "off"])) +@set.command( + help="Whether qubits should be noisy (on/off)" +) +@click.argument( + 'value', + type=click.Choice(["on", "off"]) +) @updates_local_config def noisy_qubits(value): - """Whether qubits should be noisy (on/off)""" if value == "on": simulaqron_settings.noisy_qubits = True else: simulaqron_settings.noisy_qubits = False -@set.command() -@click.argument('value', type=float) +@set.command( + help="The effective T1 to be used for noisy qubits" +) +@click.argument( + 'value', + type=float +) @updates_local_config def t1(value): - """The effective T1 to be used for noisy qubits""" simulaqron_settings.t1 = value @@ -353,76 +345,86 @@ def get(): pass -@get.command() +@get.command( + help="The backend to use (stabilizer, projectq, qutip).", +) @loads_local_config def sim_backend(): - """The backend to use (stabilizer, projectq, qutip).""" print(simulaqron_settings.sim_backend) -@get.command() +@get.command( + help="Max virt-qubits per node and max sim-qubits per register." +) @loads_local_config def max_qubits(): - """Max virt-qubits per node and max sim-qubits per register.""" print(simulaqron_settings.max_qubits) -@get.command() +@get.command( + help="How many registers a node can hold." +) @loads_local_config def max_registers(): - """How many registers a node can hold.""" print(simulaqron_settings.max_registers) -@get.command() +@get.command( + help="If setup fails, how long to wait until a retry." +) @loads_local_config def conn_retry_time(): - """If setup fails, how long to wait until a retry.""" print(simulaqron_settings.conn_retry_time) -@get.command() +@get.command( + help="When receiving a qubit or EPR pair, how long to wait until raising a timeout." +) @loads_local_config def recv_timeout(): - """When receiving a qubit or EPR pair, how long to wait until raising a timeout.""" print(simulaqron_settings.recv_timeout) -@get.command() +@get.command( + help="When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received." +) @loads_local_config def recv_retry_time(): - """When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received.""" print(simulaqron_settings.recv_retry_time) -@get.command() +@get.command( + help="Log level for both backend and frontend." +) @loads_local_config def log_level(): - """Log level for both backend and frontend.""" print(simulaqron_settings.log_level) -@get.command() +@get.command( + help="The path to the network_config_file to be used" +) @loads_local_config def network_config_file(): - """The path to the network_config_file to be used""" print(simulaqron_settings.network_config_file) -@get.command() +@get.command( + help="Whether qubits should be noisy (on/off)" +) @loads_local_config def noisy_qubits(): - """Whether qubits should be noisy (on/off)""" if simulaqron_settings.noisy_qubits: print("on") else: print("off") -@get.command() +@get.command( + help="The effective T1 to be used for noisy qubits" +) @loads_local_config def t1(): - """The effective T1 to be used for noisy qubits""" print(simulaqron_settings.t1) @@ -430,7 +432,6 @@ def t1(): # node command # ############### - @cli.group() def nodes(): """ @@ -442,36 +443,29 @@ def nodes(): @nodes.command() -@click.argument('name', type=str) -@click.option('--network-name', type=str, +@click.argument('name', type=str, required=True) +@click.option('--network-name', type=str, default="default", help="The name of the network") -@click.option('--hostname', type=str, +@click.option('--hostname', type=str, default="localhost", help="The host name of the node, e.g. localhost (default) or 192.168.0.1\n" "If you wish to have different components on different hostname," "for example the qnodeos nodes on one computer and the virtual nodes on another," "you have to manually construct you config file.") -@click.option('--app-port', type=int, +@click.option('--app-port', type=int, default=-1, help="Port number for the application.\n \ If not specified a random unused port between 8000 and 9000 will be used.") -@click.option('--qnodeos-port', type=int, +@click.option('--qnodeos-port', type=int, default=-1, help="Port number for the qnodeos server.\n \ If not specified a random unused port between 8000 and 9000 will be used.") -@click.option('--vnode-port', type=int, +@click.option('--vnode-port', type=int, default=-1, help="Port number for the virtual node.\n \ If not specified a random unused port between 8000 and 9000 will be used.") @click.option('--neighbors', type=str, - help="The neighbors of the node in the network seperated by ',' (no space).\n \ + help="The neighbors of the node in the network separated by ',' (no space).\n \ For example '--neighbors Bob,Charlie,David'.\n \ If not specified all current nodes in the network will be neighbors.") -@click.option( - "-f", - "--force", - help="Force re-write of network_config_file.\n", - is_flag=True, -) -def add(name: Optional[str], network_name: Optional[str], hostname=None, app_port=None, qnodeos_port=None, - vnode_port=None, neighbors=None, - force=False): +def add(name: str, network_name: str, hostname: str , app_port: int, qnodeos_port: int, + vnode_port: int, neighbors: Optional[str]=None): """ Add a node to the network. @@ -479,92 +473,56 @@ def add(name: Optional[str], network_name: Optional[str], hostname=None, app_por HOSTNAME: The host name of the node, e.g. localhost or 192.168.0.1 """ - if not force: - answer = input(f"Do you want to add the node {name} to the " - f"network {network_name} in the file " - f"{simulaqron_settings.network_config_file}? (yes/no).") - if not _is_positive_answer(answer): - print("Aborting!") - return if neighbors is not None: neighbors = neighbors.split(',') neighbors = [neighbor.strip() for neighbor in neighbors] - networks_config = NetworkConfigBuilder() - networks_config.read_from_file(simulaqron_settings.network_config_file) - networks_config.add_node(node_name=name, network_name=network_name, + cwd_network_config_path = Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME + network_config.read_from_file(cwd_network_config_path) + network_config.add_node(node_name=name, network_name=network_name, app_hostname=hostname, qnodeos_hostname=hostname, vnode_hostname=hostname, app_port=app_port, qnodeos_port=qnodeos_port, vnode_port=vnode_port, neighbors=neighbors) - networks_config.write_to_file(simulaqron_settings.network_config_file) + network_config.write_to_file(cwd_network_config_path) @nodes.command() -@click.argument('name', type=str) -@click.option('--network-name', type=str, +@click.argument('name', type=str, required=True) +@click.option('--network-name', type=str, default="default", help="The name of the network") -@click.option( - "-f", - "--force", - help="Force re-write of network_config_file.\n", - is_flag=True, -) -def remove(name, network_name=None, force=False): +def remove(name: str, network_name: str): """ Remove a node to the network. NAME: The name of the node, e.g. Alice """ - if not force: - answer = input(f"Do you want to remove the node {name} to the network " - f"{network_name} in the file " - f"{simulaqron_settings.network_config_file}? (yes/no).") - if not _is_positive_answer(answer): - print("Aborting!") - return - networks_config = NetworkConfigBuilder() - networks_config.read_from_file(simulaqron_settings.network_config_file) - networks_config.remove_node(node_name=name, network_name=network_name) - networks_config.write_to_file(simulaqron_settings.network_config_file) + cwd_network_config_path = Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME + network_config.read_from_file(cwd_network_config_path) + network_config.remove_node(node_name=name, network_name=network_name) + network_config.write_to_file(cwd_network_config_path) @nodes.command() -@click.option('--network-name', type=str, - help="The name of the network") -@click.option( - "-f", - "--force", - help="Force re-write of network_config_file.\n", - is_flag=True, -) -def default(network_name=None, force=False): +def default(network_name: str): """ Sets the default nodes of the network. The default network consists of the five nodes: Alice, Bob, Charlie, David, Eve """ - if not force: - answer = input(f"Do you want to set the network {network_name} in the file " - f"{simulaqron_settings.network_config_file} to default, i.e. " - f"with nodes Alice, Bob, Charlie, David and Eve? (yes/no).") - if not _is_positive_answer(answer): - print("Aborting!") - return - networks_config = NetworkConfigBuilder() - node_names = ["Alice", "Bob", "Charlie", "David", "Eve"] - networks_config.add_network(node_names=node_names, network_name=network_name) - networks_config.write_to_file(simulaqron_settings.network_config_file) + cwd_network_config_path = Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME + network_config.using_default_network() + network_config.write_to_file(cwd_network_config_path) @nodes.command() -@click.option('--network-name', type=str, +@click.option('--network-name', type=str, default="default", help="The name of the network") -def get(network_name=None): +def get(network_name: str): """Get the current nodes of the network.""" - networks_config = NetworkConfigBuilder() - networks_config.read_from_file(simulaqron_settings.network_config_file) + cwd_network_config_path = Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME + network_config.read_from_file(cwd_network_config_path) try: - nodes = networks_config.get_node_names(network_name=network_name) + nodes = network_config.get_node_names(network_name=network_name) except ValueError: print(f"No network {network_name}") else: From 70b40a9019b73447b76487488de55e38a9e13d06 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 23:24:22 +0100 Subject: [PATCH 139/277] Allow to specify a network config file when starting a simulaqron network when using CLI --- simulaqron/simulaqron.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 5d03d751..136e54a7 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -81,6 +81,13 @@ def version(): ################# @cli.command() +@click.option( + "--network-config-file", + help=f"Use the given network config file. Defaults to the file named {DEFAULT_SIMULAQRON_NETWORK_FILENAME} " + f"on the current directory.", + type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), + default=Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME +) @click.option( "--name", help="Give the network a name to be able to start multiple (default: 'default')", @@ -102,8 +109,9 @@ def version(): type=str, default="", ) -def start(name: str, nrnodes: int, nodes: str): +def start(name: str, nrnodes: int, nodes: str, network_config_file: Path): """Starts a network with the given parameters or from config files.""" + network_config.read_from_file(network_config_file) pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): logging.warning("Network with name %s is already running", name) @@ -401,14 +409,6 @@ def log_level(): print(simulaqron_settings.log_level) -@get.command( - help="The path to the network_config_file to be used" -) -@loads_local_config -def network_config_file(): - print(simulaqron_settings.network_config_file) - - @get.command( help="Whether qubits should be noisy (on/off)" ) From 4a457416ada01a5fad768c8fb74f7d536aae3aee Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 2 Dec 2025 23:41:28 +0100 Subject: [PATCH 140/277] Correctly load and save files to cwd when using CLI --- simulaqron/simulaqron.py | 182 +++++++++++++++++++++++++++++---------- 1 file changed, 137 insertions(+), 45 deletions(-) diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 136e54a7..ebdcff7b 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -83,7 +83,7 @@ def version(): @cli.command() @click.option( "--network-config-file", - help=f"Use the given network config file. Defaults to the file named {DEFAULT_SIMULAQRON_NETWORK_FILENAME} " + help=f"Use the given network config file. Defaults to the file named '{DEFAULT_SIMULAQRON_NETWORK_FILENAME}' " f"on the current directory.", type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), default=Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME @@ -97,7 +97,7 @@ def version(): @click.option( "-N", "--nrnodes", - help="Number of nodes to start. This argument will start nodes named 'Node',\n" + help="Number of nodes to start. This argument will start nodes named 'Node', " "until the number of nodes is reached.", type=int, default=0, @@ -187,20 +187,6 @@ def reset(force: bool): print("Aborting!") -def updates_local_config(command_function: Callable): - def wrapper(*args, **kwargs): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() - simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) - command_function(*args, **kwargs) - simulaqron_settings.save_to_file(local_settings) - - return wrapper - - ############### # set command # ############### @@ -227,9 +213,16 @@ def default(): "value", type=click.Choice([b.value for b in SimBackend]) ) -@updates_local_config def sim_backend(value): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if not local_settings.exists(): + local_settings.touch() + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) simulaqron_settings.sim_backend = value + simulaqron_settings.save_to_file(local_settings) + print(f"Configuration saved to file: '{local_settings}'") @set.command( @@ -239,9 +232,16 @@ def sim_backend(value): 'value', type=int ) -@updates_local_config def max_qubits(value): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if not local_settings.exists(): + local_settings.touch() + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) simulaqron_settings.max_qubits = value + simulaqron_settings.save_to_file(local_settings) + print(f"Configuration saved to file: '{local_settings}'") @set.command( @@ -251,9 +251,16 @@ def max_qubits(value): 'value', type=int ) -@updates_local_config def max_registers(value): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if not local_settings.exists(): + local_settings.touch() + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) simulaqron_settings.max_registers = value + simulaqron_settings.save_to_file(local_settings) + print(f"Configuration saved to file: '{local_settings}'") @set.command( @@ -263,9 +270,16 @@ def max_registers(value): 'value', type=float ) -@updates_local_config def conn_retry_time(value): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if not local_settings.exists(): + local_settings.touch() + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) simulaqron_settings.conn_retry_time = value + simulaqron_settings.save_to_file(local_settings) + print(f"Configuration saved to file: '{local_settings}'") @set.command( @@ -275,9 +289,16 @@ def conn_retry_time(value): 'value', type=float ) -@updates_local_config def recv_timeout(value): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if not local_settings.exists(): + local_settings.touch() + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) simulaqron_settings.recv_timeout = value + simulaqron_settings.save_to_file(local_settings) + print(f"Configuration saved to file: '{local_settings}'") @set.command( @@ -287,9 +308,16 @@ def recv_timeout(value): 'value', type=float ) -@updates_local_config def recv_retry_time(value): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if not local_settings.exists(): + local_settings.touch() + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) simulaqron_settings.recv_retry_time = value + simulaqron_settings.save_to_file(local_settings) + print(f"Configuration saved to file: '{local_settings}'") @set.command( @@ -299,9 +327,16 @@ def recv_retry_time(value): 'value', type=int ) -@updates_local_config def log_level(value): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if not local_settings.exists(): + local_settings.touch() + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) simulaqron_settings.log_level = value + simulaqron_settings.save_to_file(local_settings) + print(f"Configuration saved to file: '{local_settings}'") @set.command( @@ -311,12 +346,19 @@ def log_level(value): 'value', type=click.Choice(["on", "off"]) ) -@updates_local_config def noisy_qubits(value): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if not local_settings.exists(): + local_settings.touch() + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) if value == "on": simulaqron_settings.noisy_qubits = True else: simulaqron_settings.noisy_qubits = False + simulaqron_settings.save_to_file(local_settings) + print(f"Configuration saved to file: '{local_settings}'") @set.command( @@ -326,26 +368,22 @@ def noisy_qubits(value): 'value', type=float ) -@updates_local_config def t1(value): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if not local_settings.exists(): + local_settings.touch() + simulaqron_settings.default_settings() + simulaqron_settings.save_to_file(local_settings) + simulaqron_settings.load_from_file(local_settings) simulaqron_settings.t1 = value + simulaqron_settings.save_to_file(local_settings) + print(f"Configuration saved to file: '{local_settings}'") ############### # get command # ############### -def loads_local_config(command_function: Callable): - def wrapper(*args, **kwargs): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - else: - simulaqron_settings.default_settings() - command_function(*args, **kwargs) - - return wrapper - @cli.group() def get(): @@ -356,64 +394,112 @@ def get(): @get.command( help="The backend to use (stabilizer, projectq, qutip).", ) -@loads_local_config def sim_backend(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + print(f"Configuration loaded from file: '{local_settings}':") + else: + print(f"Configuration from default configuration:") + simulaqron_settings.default_settings() print(simulaqron_settings.sim_backend) @get.command( help="Max virt-qubits per node and max sim-qubits per register." ) -@loads_local_config def max_qubits(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + print(f"Configuration loaded from file: '{local_settings}':") + else: + print(f"Configuration from default configuration:") + simulaqron_settings.default_settings() print(simulaqron_settings.max_qubits) @get.command( help="How many registers a node can hold." ) -@loads_local_config def max_registers(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + print(f"Configuration loaded from file: '{local_settings}':") + else: + print(f"Configuration from default configuration:") + simulaqron_settings.default_settings() print(simulaqron_settings.max_registers) @get.command( help="If setup fails, how long to wait until a retry." ) -@loads_local_config def conn_retry_time(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + print(f"Configuration loaded from file: '{local_settings}':") + else: + print(f"Configuration from default configuration:") + simulaqron_settings.default_settings() print(simulaqron_settings.conn_retry_time) @get.command( help="When receiving a qubit or EPR pair, how long to wait until raising a timeout." ) -@loads_local_config def recv_timeout(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + print(f"Configuration loaded from file: '{local_settings}':") + else: + print(f"Configuration from default configuration:") + simulaqron_settings.default_settings() print(simulaqron_settings.recv_timeout) @get.command( help="When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received." ) -@loads_local_config def recv_retry_time(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + print(f"Configuration loaded from file: '{local_settings}':") + else: + print(f"Configuration from default configuration:") + simulaqron_settings.default_settings() print(simulaqron_settings.recv_retry_time) @get.command( help="Log level for both backend and frontend." ) -@loads_local_config def log_level(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + print(f"Configuration loaded from file: '{local_settings}':") + else: + print(f"Configuration from default configuration:") + simulaqron_settings.default_settings() print(simulaqron_settings.log_level) @get.command( help="Whether qubits should be noisy (on/off)" ) -@loads_local_config def noisy_qubits(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + print(f"Configuration loaded from file: '{local_settings}':") + else: + print(f"Configuration from default configuration:") + simulaqron_settings.default_settings() if simulaqron_settings.noisy_qubits: print("on") else: @@ -423,8 +509,14 @@ def noisy_qubits(): @get.command( help="The effective T1 to be used for noisy qubits" ) -@loads_local_config def t1(): + local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME + if local_settings.exists() and local_settings.is_file(): + simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + print(f"Configuration loaded from file: '{local_settings}':") + else: + print(f"Configuration from default configuration:") + simulaqron_settings.default_settings() print(simulaqron_settings.t1) From 4cf43eee16d2f973108e422331ae4aa8565b2982 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 08:42:51 +0100 Subject: [PATCH 141/277] Add descriptive messages to the simulaqron CLI commands --- simulaqron/settings/network_config.py | 4 ++-- simulaqron/simulaqron.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index fabb3298..39385530 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -332,11 +332,11 @@ def remove_network(self, network_name: str = "default"): def get_nodes(self, network_name: str = "default") -> List[NodeConfig]: """ - Returns the node-config objects (_NodeConfig) in a network. + Returns the node-config objects (NodeConfig) in a network that belong to the given network. :param network_name: str Name of the network (default: "default") - :return: list of _NodeConfig + :return: list of NodeConfig """ if network_name in self.networks: nodes = self.networks[network_name].nodes diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index ebdcff7b..dd99aaa1 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -11,7 +11,7 @@ from simulaqron.network import Network from simulaqron.settings import simulaqron_settings, network_config -from simulaqron.settings.network_config import DEFAULT_SIMULAQRON_NETWORK_FILENAME +from simulaqron.settings.network_config import DEFAULT_SIMULAQRON_NETWORK_FILENAME, NodeConfig from simulaqron.settings.simulaqron_config import SimBackend, DEFAULT_SIMULAQRON_SETTINGS_FILENAME CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -575,6 +575,12 @@ def add(name: str, network_name: str, hostname: str , app_port: int, qnodeos_por app_port=app_port, qnodeos_port=qnodeos_port, vnode_port=vnode_port, neighbors=neighbors) network_config.write_to_file(cwd_network_config_path) + added_node: NodeConfig = network_config.get_nodes(network_name=network_name)[name] + print(f"Node with name '{added_node.name}' was added to the network with name '{network_name}'.\n" + f"Socket addresses are:\n" + f"* App/Classical: '({added_node.app_hostname}, {added_node.app_port})\n" + f"* QNodeOS: '({added_node.qnodeos_hostname}, {added_node.qnodeos_port})\n" + f"* Virtual Node: '({added_node.vnode_hostname}, {added_node.vnode_port})\n") @nodes.command() @@ -591,10 +597,11 @@ def remove(name: str, network_name: str): network_config.read_from_file(cwd_network_config_path) network_config.remove_node(node_name=name, network_name=network_name) network_config.write_to_file(cwd_network_config_path) + print(f"Node with name '{name}' was removed from the network with name '{network_name}'.\n") @nodes.command() -def default(network_name: str): +def default(): """ Sets the default nodes of the network. @@ -604,6 +611,7 @@ def default(network_name: str): cwd_network_config_path = Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME network_config.using_default_network() network_config.write_to_file(cwd_network_config_path) + print(f"Default network saved to file: '{cwd_network_config_path}'") @nodes.command() From ac5c0a7cbface82cf38804308fa96cab14f5ccb9 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 08:55:10 +0100 Subject: [PATCH 142/277] Small changes in pydoc --- simulaqron/local/setup.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index 41f0105c..96376970 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -53,18 +53,25 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsConfig, lNode: pb.Root, func: Callable, *args, **kwargs): """ - Sets up - - local classical communication server (if desired according to the configuration file) - - client connection to the local virtual node quantum backend - - client connections to all other classical communication servers - - Arguments - myName name of this node (string) - virtualNet servers of the virtual nodes (dictionary of host objects) - classicalNet servers on the classical communication network (dictionary of host objects) - lNode Twisted PB root to use as local server (if applicable) - func function to run if all connections are set up - args, kwargs additional arguments to be given to func + Sets up a local classical communication server (if desired according to the configuration file), + a client connection to the local virtual node quantum backend and a client connections to all other + classical communication servers + + Args: + myName (str): + name of this node + virtualNet (SocketsConfig): + servers of the virtual nodes (dictionary of host objects) + classicalNet (SocketsConfig): + servers on the classical communication network (dictionary of host objects) + lNode (pb.Root): + Twisted PB root to use as local server (if applicable) + func (Callable): + function to run if all connections are set up + *args (Any): + additional arguments to be given to ``func`` + **kwargs (Any): + additional keyword-based arguments to be passed to ``func`` """ # Initialize Twisted callback framework @@ -120,10 +127,11 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon # -def init_register(resList, myName, virtualNet, classicalNet, lNode, func, *args, **kwargs): +def init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig, classicalNet: SocketsConfig, + lNode: pb.Root, func: Callable, *args, **kwargs): _logger.debug("LOCAL %s: All connections set up.", myName) - # Retrieve the connection to the local virtual node, if successfull + # Retrieve the connection to the local virtual node, if successful j = 0 if resList[j][0]: virtRoot = resList[j][1] From 8085cfcfb81899330e5dfe20e355e28ace583d8b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 09:10:15 +0100 Subject: [PATCH 143/277] Fix small bug on the simulaqron "stop" command --- simulaqron/simulaqron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index dd99aaa1..256274c1 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -146,7 +146,7 @@ def stop(name: str): """Stops a network.""" assert name is not None pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" - if pidfile.exists(): + if not pidfile.exists(): logging.warning("Network with name %s is not running", name) return d = RunningSimulaQronDaemon(pidfile=pidfile) From b7445aca5286b04012c43073921431b195585797 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 09:49:48 +0100 Subject: [PATCH 144/277] Define local and home config file paths in a single place --- simulaqron/settings/__init__.py | 12 +- simulaqron/settings/_serialization.py | 16 +- simulaqron/settings/network_config.py | 8 +- simulaqron/settings/simulaqron_config.py | 11 +- simulaqron/simulaqron.py | 222 +++++++++++------------ 5 files changed, 128 insertions(+), 141 deletions(-) diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index 401ff801..cd566e3d 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -1,5 +1,5 @@ -from .network_config import NetworkConfigBuilder, DEFAULT_SIMULAQRON_NETWORK_FILENAME -from .simulaqron_config import SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME +from .network_config import NetworkConfigBuilder, DEFAULT_SIMULAQRON_NETWORK_FILENAME, LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS +from .simulaqron_config import SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME, LOCAL_SIMULAQRON_SETTINGS, HOME_SIMULAQRON_SETTINGS from ._serialization import init_serialization init_serialization() @@ -12,10 +12,4 @@ # Centralized way to store the config of the network. It reads the local # configuration if exists, otherwise, it simply populates the in-memory # configs object with the default values -network_config = NetworkConfigBuilder.load_from_known_sources() - -# We follow a similar approach with the network config builder: read the -# file pointed by the simulaqron_settings (if exists) or initialize a new -# builder that contains only the default network. -# network_configs = NetworkConfigBuilder() -# network_configs.read_from_file(simulaqron_settings.network_config_file) +network_config = NetworkConfigBuilder.load_from_known_sources() \ No newline at end of file diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index 50abd699..6f705546 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -3,12 +3,11 @@ from typing import Type, Dict, Any, List from dataclasses_serialization.json import JSONSerializer -from dataclasses_serialization.serializer_base import (DeserializationError, - dict_serialization) +from dataclasses_serialization.serializer_base import DeserializationError from .network_config import NodeConfig, NetworkConfig, NetworkConfigBuilder -from ..settings.simulaqron_config import (DEFAULT_SIMULAQRON_SETTINGS_FILENAME, - SimulaqronConfig, SimBackend) +from ..settings import HOME_SIMULAQRON_SETTINGS +from ..settings.simulaqron_config import SimulaqronConfig, SimBackend def init_serialization(): @@ -45,10 +44,11 @@ def path_serializer(obj: Path) -> str: # Registration of Deserializer for python enums @JSONSerializer.register_deserializer(Path) def path_deserializer(cls: Type[Path], path: str) -> Path: - if path == "$DEFAULT_NETWORK": - return (cls.home() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() - # If the path is given, we will resolve it later - return cls(path) + match path: + case "$DEFAULT_NETWORK": + return HOME_SIMULAQRON_SETTINGS + case _: + return cls(path) @JSONSerializer.register_deserializer(SimulaqronConfig) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 39385530..806ec69e 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -12,7 +12,11 @@ import simulaqron._default_config +# Some helpers paths that point to the usual locations where the +# configurations can reside: DEFAULT_SIMULAQRON_NETWORK_FILENAME = "simulaqron_network.json" +HOME_NETWORK_SETTINGS = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() +LOCAL_NETWORK_SETTINGS = (Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() class NodeConfigType(StrEnum): @@ -421,8 +425,8 @@ def _deserialize_from_file(cls, file_path: Path) -> Self: @classmethod def load_from_known_sources(cls) -> Self: - cwd_networks_file = (Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() - home_networks_file = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() + cwd_networks_file = LOCAL_NETWORK_SETTINGS.resolve() + home_networks_file = HOME_NETWORK_SETTINGS.resolve() files_to_load = [cwd_networks_file, home_networks_file] diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index f4afb7b8..c7b1e8d1 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -40,10 +40,11 @@ from dataclasses_serialization.json import JSONSerializer from dataclasses_serialization.json import JSONSerializerMixin -# This is the name of the "local" simulaqron settings. -# If a file named like this is found in the CWD, it will be -# automatically loaded when creating the config file +# Some helpers paths that point to the usual locations where the +# configurations can reside: DEFAULT_SIMULAQRON_SETTINGS_FILENAME = "simulaqron_settings.json" +LOCAL_SIMULAQRON_SETTINGS = (Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() +HOME_SIMULAQRON_SETTINGS = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() class SimBackend(JSONSerializerMixin, Enum): @@ -97,8 +98,8 @@ def _deserialize_from_file(cls, file_path: Path) -> Self: @classmethod def load_from_known_sources(cls) -> Self: - cwd_settings_file = (Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() - home_settings_file = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_SETTINGS_FILENAME).resolve() + cwd_settings_file = LOCAL_SIMULAQRON_SETTINGS.resolve() + home_settings_file = HOME_SIMULAQRON_SETTINGS.resolve() files_to_load = [cwd_settings_file, home_settings_file] diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 256274c1..b45cd29d 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -11,8 +11,9 @@ from simulaqron.network import Network from simulaqron.settings import simulaqron_settings, network_config -from simulaqron.settings.network_config import DEFAULT_SIMULAQRON_NETWORK_FILENAME, NodeConfig -from simulaqron.settings.simulaqron_config import SimBackend, DEFAULT_SIMULAQRON_SETTINGS_FILENAME +from simulaqron.settings import LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS +from simulaqron.settings.network_config import NodeConfig, DEFAULT_SIMULAQRON_NETWORK_FILENAME +from simulaqron.settings.simulaqron_config import SimBackend CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) # PID folder should be "LOCAL" @@ -86,7 +87,7 @@ def version(): help=f"Use the given network config file. Defaults to the file named '{DEFAULT_SIMULAQRON_NETWORK_FILENAME}' " f"on the current directory.", type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), - default=Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME + default=LOCAL_NETWORK_SETTINGS ) @click.option( "--name", @@ -166,11 +167,10 @@ def stop(name: str): ) def reset(force: bool): """Resets simulaqron""" - cwd_simulaqron_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME if not force: answer = input("Are you sure you want to reset simulaqron?\nThis will revert settings and " "network config files to the default values.\nNote, this action will remove " - f"the file at {cwd_simulaqron_settings} if it exists.\n" + f"the file at {LOCAL_SIMULAQRON_SETTINGS} if it exists.\n" "(yes/no)") else: answer = "yes" @@ -182,7 +182,7 @@ def reset(force: bool): if entry.exists(): entry.unlink() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(cwd_simulaqron_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) else: print("Aborting!") @@ -203,7 +203,7 @@ def set(): ) def default(): simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) @set.command( @@ -214,15 +214,14 @@ def default(): type=click.Choice([b.value for b in SimBackend]) ) def sim_backend(value): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.sim_backend = value - simulaqron_settings.save_to_file(local_settings) - print(f"Configuration saved to file: '{local_settings}'") + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -233,15 +232,14 @@ def sim_backend(value): type=int ) def max_qubits(value): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.max_qubits = value - simulaqron_settings.save_to_file(local_settings) - print(f"Configuration saved to file: '{local_settings}'") + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -252,15 +250,14 @@ def max_qubits(value): type=int ) def max_registers(value): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.max_registers = value - simulaqron_settings.save_to_file(local_settings) - print(f"Configuration saved to file: '{local_settings}'") + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -271,15 +268,14 @@ def max_registers(value): type=float ) def conn_retry_time(value): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.conn_retry_time = value - simulaqron_settings.save_to_file(local_settings) - print(f"Configuration saved to file: '{local_settings}'") + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -290,15 +286,14 @@ def conn_retry_time(value): type=float ) def recv_timeout(value): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.recv_timeout = value - simulaqron_settings.save_to_file(local_settings) - print(f"Configuration saved to file: '{local_settings}'") + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -309,15 +304,14 @@ def recv_timeout(value): type=float ) def recv_retry_time(value): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.recv_retry_time = value - simulaqron_settings.save_to_file(local_settings) - print(f"Configuration saved to file: '{local_settings}'") + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -328,15 +322,14 @@ def recv_retry_time(value): type=int ) def log_level(value): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.log_level = value - simulaqron_settings.save_to_file(local_settings) - print(f"Configuration saved to file: '{local_settings}'") + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -347,18 +340,17 @@ def log_level(value): type=click.Choice(["on", "off"]) ) def noisy_qubits(value): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) if value == "on": simulaqron_settings.noisy_qubits = True else: simulaqron_settings.noisy_qubits = False - simulaqron_settings.save_to_file(local_settings) - print(f"Configuration saved to file: '{local_settings}'") + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -369,15 +361,14 @@ def noisy_qubits(value): type=float ) def t1(value): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if not local_settings.exists(): - local_settings.touch() + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(local_settings) - simulaqron_settings.load_from_file(local_settings) + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.t1 = value - simulaqron_settings.save_to_file(local_settings) - print(f"Configuration saved to file: '{local_settings}'") + simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") ############### @@ -395,10 +386,9 @@ def get(): help="The backend to use (stabilizer, projectq, qutip).", ) def sim_backend(): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - print(f"Configuration loaded from file: '{local_settings}':") + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") else: print(f"Configuration from default configuration:") simulaqron_settings.default_settings() @@ -409,10 +399,9 @@ def sim_backend(): help="Max virt-qubits per node and max sim-qubits per register." ) def max_qubits(): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - print(f"Configuration loaded from file: '{local_settings}':") + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") else: print(f"Configuration from default configuration:") simulaqron_settings.default_settings() @@ -423,10 +412,9 @@ def max_qubits(): help="How many registers a node can hold." ) def max_registers(): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - print(f"Configuration loaded from file: '{local_settings}':") + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") else: print(f"Configuration from default configuration:") simulaqron_settings.default_settings() @@ -437,10 +425,9 @@ def max_registers(): help="If setup fails, how long to wait until a retry." ) def conn_retry_time(): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - print(f"Configuration loaded from file: '{local_settings}':") + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") else: print(f"Configuration from default configuration:") simulaqron_settings.default_settings() @@ -451,10 +438,9 @@ def conn_retry_time(): help="When receiving a qubit or EPR pair, how long to wait until raising a timeout." ) def recv_timeout(): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - print(f"Configuration loaded from file: '{local_settings}':") + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") else: print(f"Configuration from default configuration:") simulaqron_settings.default_settings() @@ -465,10 +451,9 @@ def recv_timeout(): help="When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received." ) def recv_retry_time(): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - print(f"Configuration loaded from file: '{local_settings}':") + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") else: print(f"Configuration from default configuration:") simulaqron_settings.default_settings() @@ -479,10 +464,9 @@ def recv_retry_time(): help="Log level for both backend and frontend." ) def log_level(): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - print(f"Configuration loaded from file: '{local_settings}':") + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") else: print(f"Configuration from default configuration:") simulaqron_settings.default_settings() @@ -493,10 +477,9 @@ def log_level(): help="Whether qubits should be noisy (on/off)" ) def noisy_qubits(): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - print(f"Configuration loaded from file: '{local_settings}':") + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") else: print(f"Configuration from default configuration:") simulaqron_settings.default_settings() @@ -510,10 +493,9 @@ def noisy_qubits(): help="The effective T1 to be used for noisy qubits" ) def t1(): - local_settings = Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME - if local_settings.exists() and local_settings.is_file(): - simulaqron_settings.load_from_file(Path.cwd() / DEFAULT_SIMULAQRON_SETTINGS_FILENAME) - print(f"Configuration loaded from file: '{local_settings}':") + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") else: print(f"Configuration from default configuration:") simulaqron_settings.default_settings() @@ -568,13 +550,12 @@ def add(name: str, network_name: str, hostname: str , app_port: int, qnodeos_por if neighbors is not None: neighbors = neighbors.split(',') neighbors = [neighbor.strip() for neighbor in neighbors] - cwd_network_config_path = Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME - network_config.read_from_file(cwd_network_config_path) + network_config.read_from_file(LOCAL_NETWORK_SETTINGS) network_config.add_node(node_name=name, network_name=network_name, app_hostname=hostname, qnodeos_hostname=hostname, vnode_hostname=hostname, app_port=app_port, qnodeos_port=qnodeos_port, vnode_port=vnode_port, neighbors=neighbors) - network_config.write_to_file(cwd_network_config_path) + network_config.write_to_file(LOCAL_NETWORK_SETTINGS) added_node: NodeConfig = network_config.get_nodes(network_name=network_name)[name] print(f"Node with name '{added_node.name}' was added to the network with name '{network_name}'.\n" f"Socket addresses are:\n" @@ -593,10 +574,14 @@ def remove(name: str, network_name: str): NAME: The name of the node, e.g. Alice """ - cwd_network_config_path = Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME - network_config.read_from_file(cwd_network_config_path) + + if not LOCAL_NETWORK_SETTINGS.exists() or not LOCAL_NETWORK_SETTINGS.is_file(): + print(f"WARNING - the file '{LOCAL_NETWORK_SETTINGS}' was not found. The loaded " + f"configuration corresponds to the one on '{HOME_NETWORK_SETTINGS}'") + else: + network_config.read_from_file(LOCAL_NETWORK_SETTINGS) network_config.remove_node(node_name=name, network_name=network_name) - network_config.write_to_file(cwd_network_config_path) + network_config.write_to_file(LOCAL_NETWORK_SETTINGS) print(f"Node with name '{name}' was removed from the network with name '{network_name}'.\n") @@ -608,10 +593,9 @@ def default(): The default network consists of the five nodes: Alice, Bob, Charlie, David, Eve """ - cwd_network_config_path = Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME network_config.using_default_network() - network_config.write_to_file(cwd_network_config_path) - print(f"Default network saved to file: '{cwd_network_config_path}'") + network_config.write_to_file(LOCAL_NETWORK_SETTINGS) + print(f"Default network saved to file: '{LOCAL_NETWORK_SETTINGS}'") @nodes.command() @@ -619,8 +603,12 @@ def default(): help="The name of the network") def get(network_name: str): """Get the current nodes of the network.""" - cwd_network_config_path = Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME - network_config.read_from_file(cwd_network_config_path) + + if not LOCAL_NETWORK_SETTINGS.exists() or not LOCAL_NETWORK_SETTINGS.is_file(): + print(f"WARNING - the file '{LOCAL_NETWORK_SETTINGS}' was not found. The loaded " + f"configuration corresponds to the one on '{HOME_NETWORK_SETTINGS}'") + else: + network_config.read_from_file(LOCAL_NETWORK_SETTINGS) try: nodes = network_config.get_node_names(network_name=network_name) except ValueError: From 0e16726cad81519fb5b95f5105b256a1316f70f0 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 11:16:56 +0100 Subject: [PATCH 145/277] Make the native teleport example work --- examples/nativeMode/teleport/aliceTest.py | 19 +++++++++++++------ examples/nativeMode/teleport/bobTest.py | 19 +++++++++++++------ simulaqron/local/setup.py | 6 +++--- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/examples/nativeMode/teleport/aliceTest.py b/examples/nativeMode/teleport/aliceTest.py index 0d7b709e..467069be 100644 --- a/examples/nativeMode/teleport/aliceTest.py +++ b/examples/nativeMode/teleport/aliceTest.py @@ -32,8 +32,9 @@ from simulaqron.local.setup import setup_local, assemble_qubit from simulaqron.general.host_config import SocketsConfig +from simulaqron.settings.network_config import NodeConfigType from simulaqron.toolbox.stabilizer_states import StabilizerState -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config, LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS from twisted.internet.defer import inlineCallbacks from twisted.spread import pb from twisted.internet import reactor @@ -73,7 +74,7 @@ def runClientNode(qReg, virtRoot, myName, classicalNet): realRho, imagRho = yield q1.callRemote("get_qubit") state = np.array(assemble_qubit(realRho, imagRho), dtype=complex) elif simulaqron_settings.sim_backend.value == "projectq": - realvec, imagvec = yield virtRoot.callRemote("get_register_RI", q1) + _, (realvec, imagvec) = yield virtRoot.callRemote("get_register_RI", q1) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] elif simulaqron_settings.sim_backend.value == "stabilizer": array, _ = yield virtRoot.callRemote("get_register_RI", q1) @@ -148,14 +149,20 @@ def main(): myName = "Alice" # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file + # network_file = simulaqron_settings.network_config_file + # virtualNet = SocketsConfig(network_file) # This file defines the nodes acting as servers in the classical communication network - classicalFile = "classicalNet.cfg" + # classicalFile = "classicalNet.cfg" + # classicalNet = SocketsConfig(classicalFile) + + # We load the local configuration files + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + network_config.read_from_file(LOCAL_NETWORK_SETTINGS) # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + virtualNet = SocketsConfig(network_config, config_type=NodeConfigType.VNODE) + classicalNet = SocketsConfig(network_config, config_type=NodeConfigType.APP) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index 6dd03d39..cae37ea8 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -33,7 +33,8 @@ from simulaqron.local.setup import setup_local, assemble_qubit from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config, LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS +from simulaqron.settings.network_config import NodeConfigType from simulaqron.toolbox.stabilizer_states import StabilizerState from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -116,7 +117,7 @@ def remote_recover_teleport(self, a, b, virtualNum): (realRho, imagRho) = yield eprB.callRemote("get_qubit") state = np.array(assemble_qubit(realRho, imagRho), dtype=complex) elif simulaqron_settings.sim_backend.value == "projectq": - realvec, imagvec = yield self.virtRoot.callRemote("get_register_RI", eprB) + _, (realvec, imagvec) = yield self.virtRoot.callRemote("get_register_RI", eprB) state = [r + (1j * j) for r, j in zip(realvec, imagvec)] elif simulaqron_settings.sim_backend.value == "stabilizer": array, _, = yield self.virtRoot.callRemote("get_register_RI", eprB) @@ -137,14 +138,20 @@ def main(): myName = "Bob" # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file + # network_file = simulaqron_settings.network_config_file + # virtualNet = SocketsConfig(network_file) # This file defines the nodes acting as servers in the classical communication network - classicalFile = "classicalNet.cfg" + # classicalFile = "classicalNet.cfg" + # classicalNet = SocketsConfig(classicalFile) + + # We load the local configuration files + simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + network_config.read_from_file(LOCAL_NETWORK_SETTINGS) # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + virtualNet = SocketsConfig(network_config, config_type=NodeConfigType.VNODE) + classicalNet = SocketsConfig(network_config, config_type=NodeConfigType.APP) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index 96376970..26b48c1d 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -80,8 +80,8 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon # If we are listed as a server node for the classical network, start this server if myName in classicalNet.hostDict: try: - _logger.debug("LOCAL %s: Starting local classical communication server.", myName) nb = classicalNet.hostDict[myName] + _logger.debug("LOCAL %s: Starting local classical communication server (%s: %s, %d).", myName, nb.name, nb.hostname, nb.port) nb.root = lNode nb.factory = pb.PBServerFactory(nb.root) reactor.listenTCP(nb.port, nb.factory) @@ -93,8 +93,8 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon time.sleep(3) # Connect to the local virtual node simulating the "local" qubits - _logger.debug("LOCAL %s: Connecting to local virtual node.", myName) node = virtualNet.hostDict[myName] + _logger.debug("LOCAL %s: Connecting to local virtual node (%s: %s, %d).", myName, node.name, node.hostname, node.port) factory = pb.PBClientFactory() reactor.connectTCP(node.hostname, node.port, factory) deferVirtual = factory.getRootObject() @@ -104,7 +104,7 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon for node in classicalNet.hostDict: nb = classicalNet.hostDict[node] if nb.name != myName: - _logger.debug("LOCAL %s: Making classical connection to %s.", myName, nb.name) + _logger.debug("LOCAL %s: Making classical connection to %s (%s: %s, %d).", myName, nb.name, nb.name, nb.hostname, nb.port) nb.factory = pb.PBClientFactory() reactor.connectTCP(nb.hostname, nb.port, nb.factory) dList.append(nb.factory.getRootObject()) From b7b291e1d63130e84d4114f638e0c4b94c89b260 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 11:17:21 +0100 Subject: [PATCH 146/277] Small fixes in the simulaqron cli entry point code --- simulaqron/simulaqron.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index b45cd29d..6d8b0a10 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -1,9 +1,8 @@ -#!/usr/bin/env python3 import importlib.metadata as metadata import logging import time from pathlib import Path -from typing import Optional, Callable, List +from typing import Optional, List import click from daemons.interfaces import exit @@ -50,15 +49,6 @@ def run(self): time.sleep(0.1) -def _is_positive_answer(answer: str): - """ - Used to check if an answer is positive from a user. - """ - if answer.lower() in ["yes", "y"]: - return True - return False - - @click.group(context_settings=CONTEXT_SETTINGS) def cli(): """Command line interface for interacting with SimulaQron.""" @@ -119,6 +109,9 @@ def start(name: str, nrnodes: int, nodes: str, network_config_file: Path): logging.warning("The pidfile for this network is located at %s", pidfile) return nodes = nodes.split(",") + if len(nodes) <=0: + print(f"WARNING - The list of nodes to start is empty. If you specified the --nrnodes option " + f"this can be normal. Please check your invocation line if needed.") if nrnodes > 0 and len(nodes) < nrnodes: nodes += [f"Node{i}" for i in range(nrnodes - len(nodes))] d = SimulaQronDaemon(pidfile=pidfile, name=name, nodes=nodes) @@ -174,7 +167,7 @@ def reset(force: bool): "(yes/no)") else: answer = "yes" - if _is_positive_answer(answer): + if answer.lower() in ["yes", "y"]: for entry in PID_FOLDER.iterdir(): if entry.suffix == ".pid": d = RunningSimulaQronDaemon(pidfile=entry) From d27aa317c89151f3c9b2dc2c72bd3640d9150729 Mon Sep 17 00:00:00 2001 From: StephanieWehner Date: Wed, 3 Dec 2025 11:36:09 +0100 Subject: [PATCH 147/277] Added to this branch --- examples/nativeMode/corrRNG/README.txt | 9 ++++ examples/nativeMode/corrRNG/aliceTest.py | 29 +++++-------- examples/nativeMode/corrRNG/bobTest.py | 42 ++++++++++++------- examples/nativeMode/corrRNG/classicalNet.json | 17 ++++++++ examples/nativeMode/corrRNG/run.sh | 5 +++ examples/nativeMode/corrRNG/terminate.sh | 12 ++++++ 6 files changed, 81 insertions(+), 33 deletions(-) create mode 100644 examples/nativeMode/corrRNG/classicalNet.json create mode 100644 examples/nativeMode/corrRNG/terminate.sh diff --git a/examples/nativeMode/corrRNG/README.txt b/examples/nativeMode/corrRNG/README.txt index 6b20f34d..3ed2b8e3 100644 --- a/examples/nativeMode/corrRNG/README.txt +++ b/examples/nativeMode/corrRNG/README.txt @@ -10,3 +10,12 @@ Bob proceeds to recover the teleported qubit. In this example, we simply print out the initial state to be teleported, as well as the final state received by Bob to check whether the teleportation worked correctly. + + +To start run: +- simulaqron start --nodes=Alice,Bob --keep + +sh run.sh + +To terminate run: +sh terminate.sh diff --git a/examples/nativeMode/corrRNG/aliceTest.py b/examples/nativeMode/corrRNG/aliceTest.py index 76be397c..dc7e5bf9 100644 --- a/examples/nativeMode/corrRNG/aliceTest.py +++ b/examples/nativeMode/corrRNG/aliceTest.py @@ -115,31 +115,24 @@ def remote_test(self): # def main(): - # In this example, we are Alice. - myName = "Alice" + myName = "Alice" # we are Alice # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file + virtualNet = SocketsConfig(str(simulaqron_settings.network_config_file), network_name="default", config_type="vnode") - # This file defines the nodes acting as servers in the classical communication network - classicalFile = "classicalNet.cfg" + # This file defines the network used for classical communication + classicalNet = SocketsConfig("classicalNet.json", network_name="default", config_type="app") - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) - - # Check if we should run a local classical server. If so, initialize the code - # to handle remote connections on the classical communication network + # Check if we should run a server (if this node is listed in classicalNet) if myName in classicalNet.hostDict: - lNode = localNode(classicalNet.hostDict[myName], classicalNet) + # Create the local classical server + logging.debug("LOCAL %s: Creating classical server.", myName) + myNode = localNode(virtualNet.hostDict[myName], classicalNet) else: - lNode = None - - # Set up the local classical server if applicable, and connect to the virtual - # node and other classical servers. Once all connections are set up, this will - # execute the function runClientNode - setup_local(myName, virtualNet, classicalNet, lNode, runClientNode) + myNode = None + # Connect and run + setup_local(myName, virtualNet, classicalNet, myNode, runClientNode) ################################################################################################## logging.basicConfig(format="%(asctime)s:%(levelname)s:%(message)s", level=logging.DEBUG) diff --git a/examples/nativeMode/corrRNG/bobTest.py b/examples/nativeMode/corrRNG/bobTest.py index 42d27f7a..fcfe2123 100644 --- a/examples/nativeMode/corrRNG/bobTest.py +++ b/examples/nativeMode/corrRNG/bobTest.py @@ -36,6 +36,8 @@ from twisted.internet.defer import inlineCallbacks from twisted.spread import pb +from qutip import Qobj + ##################################################################################################### # @@ -103,6 +105,18 @@ def remote_process_qubit(self, virtualNum): print("BOB: My Random Number is ", x, "\n") + def assemble_qubit(self, realM, imagM): + """ + Reconstitute the qubit as a qutip object from its real and imaginary components given as a list. + We need this since Twisted PB does not support sending complex valued object natively. + """ + M = realM + for s in range(len(M)): + for t in range(len(M)): + M[s][t] = realM[s][t] + 1j * imagM[s][t] + + return Qobj(M) + ##################################################################################################### # @@ -113,27 +127,25 @@ def main(): # In this example, we are Bob. myName = "Bob" - # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file - # This file defines the nodes acting as servers in the classical communication network - classicalFile = "classicalNet.cfg" + logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.DEBUG) + + # This file defines the network of virtual quantum nodes + virtualNet = SocketsConfig(str(simulaqron_settings.network_config_file), network_name="default", config_type="vnode") - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + # This file defines the network used for classical communication + classicalNet = SocketsConfig("classicalNet.json", network_name="default", config_type="app") - # Check if we should run a local classical server. If so, initialize the code - # to handle remote connections on the classical communication network + # Check if we should run a server (if this node is listed in classicalNet) if myName in classicalNet.hostDict: - lNode = localNode(classicalNet.hostDict[myName], classicalNet) + # Create the local classical server + myNode = localNode(virtualNet.hostDict[myName], classicalNet) else: - lNode = None + myNode = None + + # Connect and run + setup_local(myName, virtualNet, classicalNet, myNode, runClientNode) - # Set up the local classical server if applicable, and connect to the virtual - # node and other classical servers. Once all connections are set up, this will - # execute the function runClientNode - setup_local(myName, virtualNet, classicalNet, lNode, runClientNode) ################################################################################################## diff --git a/examples/nativeMode/corrRNG/classicalNet.json b/examples/nativeMode/corrRNG/classicalNet.json new file mode 100644 index 00000000..4206e4d9 --- /dev/null +++ b/examples/nativeMode/corrRNG/classicalNet.json @@ -0,0 +1,17 @@ +{ + "default": { + "nodes": { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + }, + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + "topology": null + } +} diff --git a/examples/nativeMode/corrRNG/run.sh b/examples/nativeMode/corrRNG/run.sh index 0de4867d..0d5b537e 100755 --- a/examples/nativeMode/corrRNG/run.sh +++ b/examples/nativeMode/corrRNG/run.sh @@ -1,5 +1,10 @@ #!/bin/sh +# Check if SimulaQron is already running +if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + simulaqron start --nodes=Alice,Bob --force +fi + python3 bobTest.py & python3 aliceTest.py diff --git a/examples/nativeMode/corrRNG/terminate.sh b/examples/nativeMode/corrRNG/terminate.sh new file mode 100644 index 00000000..3d302022 --- /dev/null +++ b/examples/nativeMode/corrRNG/terminate.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 +fi + From 5f5fb0594a7d9e9a7b4a4c98700cda4d85fa1981 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 11:38:32 +0100 Subject: [PATCH 148/277] Add missing JSON configuration files for teleport example --- .../teleport/simulaqron_network.json | 40 +++++++++++++++++++ .../teleport/simulaqron_settings.json | 14 +++++++ 2 files changed, 54 insertions(+) create mode 100644 examples/nativeMode/teleport/simulaqron_network.json create mode 100644 examples/nativeMode/teleport/simulaqron_settings.json diff --git a/examples/nativeMode/teleport/simulaqron_network.json b/examples/nativeMode/teleport/simulaqron_network.json new file mode 100644 index 00000000..e49fb07b --- /dev/null +++ b/examples/nativeMode/teleport/simulaqron_network.json @@ -0,0 +1,40 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": [ + "localhost", + 8001 + ], + "qnodeos_socket": [ + "localhost", + 8002 + ], + "vnode_socket": [ + "localhost", + 8003 + ] + } + }, + { + "Bob": { + "app_socket": [ + "localhost", + 8010 + ], + "qnodeos_socket": [ + "localhost", + 8011 + ], + "vnode_socket": [ + "localhost", + 8012 + ] + } + } + ], + "topology": null + } +] \ No newline at end of file diff --git a/examples/nativeMode/teleport/simulaqron_settings.json b/examples/nativeMode/teleport/simulaqron_settings.json new file mode 100644 index 00000000..bbfb47a2 --- /dev/null +++ b/examples/nativeMode/teleport/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "projectq", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} \ No newline at end of file From 89972b3e1c1f08184cbded578172a507edfcf56e Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 11:43:38 +0100 Subject: [PATCH 149/277] Update teleport README file --- examples/nativeMode/teleport/README.txt | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/examples/nativeMode/teleport/README.txt b/examples/nativeMode/teleport/README.txt index 6b20f34d..5d9a45f1 100644 --- a/examples/nativeMode/teleport/README.txt +++ b/examples/nativeMode/teleport/README.txt @@ -1,3 +1,4 @@ +# Description In this example, we have only two nodes: Alice and Bob. @@ -10,3 +11,49 @@ Bob proceeds to recover the teleported qubit. In this example, we simply print out the initial state to be teleported, as well as the final state received by Bob to check whether the teleportation worked correctly. + + +# How to run + +To run this example, first make sure that the python virtual environment is activated. You can easily check this +with the terminal command prompt, which shows the active python virtual environment in parentheses: +``` +(simulaqron) user@machine_name:~$ +``` +If you don't see the name of the virtual environment, please check the SimulaQron README file to know how to +create and activate it. + +Once the environment is active, we need to start the simulaqron network: +```bash +siumulaqron start --nodes Alice,Bob +``` + +This will read the JSON configuration files and start the SimulaQron virtual nodes for nodes `Alice` and `Bob`. + +After this, you can simply run the example by using the `run` script: +```bash +./run.sh +``` + +# How to stop the execution in case the test execution stalls. + +To fully stop the execution, you can use the `doNew` script. This will stop the current execution and run the +example once again: +```bash +./doNew.sh +``` + + +# Troubleshooting + +## Trying to start the network gives "Network with name is already running" message + +This usually happens when you stopped the network "manually" by killing processes (`kill -9`). If this happens, +please make sure that you kill all related processes (use `ps aux | grep python` to search) and then delete the +PID file for the running network. This file is located in `~/simulaqron_pids` and it is called +`simulaqron_network_.pid`: +```bash +rm ~/simulaqron_pids/simulaqron_network_.pid +``` + +After this, you can try to start the network again From b2d93004020042caa64ae257fcdb8b4ac6f2ebc2 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 12:18:49 +0100 Subject: [PATCH 150/277] Linter changes --- examples/nativeMode/teleport/bobTest.py | 2 +- simulaqron/general/host_config.py | 2 +- simulaqron/local/setup.py | 10 ++- simulaqron/sdk/socket.py | 2 +- simulaqron/settings/__init__.py | 8 ++- simulaqron/settings/_serialization.py | 3 +- simulaqron/settings/network_config.py | 12 ++-- simulaqron/simulaqron.py | 61 ++++++++++--------- tests/quick/engine/test_project_q_engine.py | 2 +- tests/quick/general/test_host_config.py | 7 +-- tests/quick/netqasm/test_get_qubit_state.py | 2 +- .../test_stabilizer_states.py | 26 ++++---- tests/quick/test_networks_settings.py | 2 +- tests/slow/sdk/test_two_qubit.py | 1 - 14 files changed, 75 insertions(+), 65 deletions(-) diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index cae37ea8..7c9aa6d2 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -125,7 +125,7 @@ def remote_recover_teleport(self, a, b, virtualNum): else: ValueError(f"Unknown backend {simulaqron_settings.sim_backend}") - print(f"Qubit is:\n{state}") + print(f"Qubit is: \n{state}") ##################################################################################################### diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index e01646fe..23b72589 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -30,7 +30,7 @@ import socket import struct from ipaddress import IPv4Address -from typing import Dict, List +from typing import Dict from twisted.spread import pb diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index 26b48c1d..611c98ee 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -81,7 +81,9 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon if myName in classicalNet.hostDict: try: nb = classicalNet.hostDict[myName] - _logger.debug("LOCAL %s: Starting local classical communication server (%s: %s, %d).", myName, nb.name, nb.hostname, nb.port) + _logger.debug("LOCAL %s: Starting local classical communication server (%s: %s, %d).", + myName, nb.name, nb.hostname, nb.port + ) nb.root = lNode nb.factory = pb.PBServerFactory(nb.root) reactor.listenTCP(nb.port, nb.factory) @@ -94,7 +96,8 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon # Connect to the local virtual node simulating the "local" qubits node = virtualNet.hostDict[myName] - _logger.debug("LOCAL %s: Connecting to local virtual node (%s: %s, %d).", myName, node.name, node.hostname, node.port) + _logger.debug("LOCAL %s: Connecting to local virtual node (%s: %s, %d).", myName, node.name, node.hostname, + node.port) factory = pb.PBClientFactory() reactor.connectTCP(node.hostname, node.port, factory) deferVirtual = factory.getRootObject() @@ -104,7 +107,8 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon for node in classicalNet.hostDict: nb = classicalNet.hostDict[node] if nb.name != myName: - _logger.debug("LOCAL %s: Making classical connection to %s (%s: %s, %d).", myName, nb.name, nb.name, nb.hostname, nb.port) + _logger.debug("LOCAL %s: Making classical connection to %s (%s: %s, %d).", myName, nb.name, nb.name, + nb.hostname, nb.port) nb.factory = pb.PBClientFactory() reactor.connectTCP(nb.hostname, nb.port, nb.factory) dList.append(nb.factory.getRootObject()) diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index 3c8a35df..eb671659 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -33,7 +33,7 @@ def __init__( self._use_callbacks = use_callbacks self._network_name = network_name - self._logger = get_netqasm_logger(f"{self.__class__.__name__}(L:{app_name} <-> R:{remote_app_name})") + self._logger = get_netqasm_logger(f"{self.__class__.__name__}(L: {app_name} <-> R: {remote_app_name})") self._timeout = timeout # We define _app_socket as None as a default value, so the __del__ method # does not fail when the socket could not be connected correctly. diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index cd566e3d..6332c1c5 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -1,5 +1,7 @@ -from .network_config import NetworkConfigBuilder, DEFAULT_SIMULAQRON_NETWORK_FILENAME, LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS -from .simulaqron_config import SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME, LOCAL_SIMULAQRON_SETTINGS, HOME_SIMULAQRON_SETTINGS +from .network_config import (NetworkConfigBuilder, DEFAULT_SIMULAQRON_NETWORK_FILENAME, + LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS) +from .simulaqron_config import (SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME, + LOCAL_SIMULAQRON_SETTINGS, HOME_SIMULAQRON_SETTINGS) from ._serialization import init_serialization init_serialization() @@ -12,4 +14,4 @@ # Centralized way to store the config of the network. It reads the local # configuration if exists, otherwise, it simply populates the in-memory # configs object with the default values -network_config = NetworkConfigBuilder.load_from_known_sources() \ No newline at end of file +network_config = NetworkConfigBuilder.load_from_known_sources() diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index 6f705546..12403ae3 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -107,13 +107,14 @@ def network_config_serializer(obj: NetworkConfig) -> Dict[str, Any]: nodes_dict = { "name" : obj.name, "nodes": [ - { node_cfg.name: JSONSerializer.serialize(node_cfg) } + {node_cfg.name: JSONSerializer.serialize(node_cfg)} for node_cfg in obj.nodes.values() ], "topology": JSONSerializer.serialize(obj.topology) } return JSONSerializer.serialize(nodes_dict) + @JSONSerializer.register_deserializer(NetworkConfig) def network_config_deserializer(cls: Type[NetworkConfig], obj: Dict[str, Any]) -> NetworkConfig: net_cfg = cls(obj["name"]) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 806ec69e..4fd80556 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -8,6 +8,7 @@ from os import PathLike from pathlib import Path from typing import Optional, Self, Dict, List, Tuple, Any + from dataclasses_serialization.json import JSONSerializer, JSONSerializerMixin import simulaqron._default_config @@ -60,10 +61,12 @@ def get_config(self, config_type: str | NodeConfigType) -> Tuple[str, int]: def __eq__(self, other) -> bool: if not isinstance(other, NodeConfig): return False - return (self.name == other.name and self.app_port == other.app_port and - self.qnodeos_port == other.qnodeos_port and self.vnode_port == other.vnode_port and - self.app_hostname == other.app_hostname and self.qnodeos_hostname == other.qnodeos_hostname and - self.vnode_hostname == other.vnode_hostname) + names_equal = self.name == other.name + app_sockets_equal = self.app_hostname == other.app_hostname and self.app_port == other.app_port + qnos_sockets_equal = self.qnodeos_hostname == other.qnodeos_hostname and self.qnodeos_port == other.qnodeos_port + vnode_sockets_equal = self.vnode_hostname == other.vnode_hostname and self.vnode_port == other.vnode_port + + return names_equal and app_sockets_equal and qnos_sockets_equal and vnode_sockets_equal @dataclass @@ -156,6 +159,7 @@ def __eq__(self, other) -> bool: nodes_are_equal = [this_node == other_node for this_node, other_node in zip(self.nodes, other.nodes)] return self.name == other.name and self.topology == other.topology and all(nodes_are_equal) + @dataclass class NetworkConfigBuilder(JSONSerializerMixin): """ diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 6d8b0a10..7e5d3ee2 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -9,8 +9,8 @@ from daemons.prefab import run from simulaqron.network import Network -from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings import LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS +from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings.network_config import NodeConfig, DEFAULT_SIMULAQRON_NETWORK_FILENAME from simulaqron.settings.simulaqron_config import SimBackend @@ -29,6 +29,7 @@ class RunningSimulaQronDaemon(run.RunDaemon): This class is useful to stop the already-running daemons without needed to read all the required configurations. """ + def __init__(self, pidfile: Path): assert pidfile is not None super().__init__(pidfile=pidfile) @@ -74,8 +75,8 @@ def version(): @cli.command() @click.option( "--network-config-file", - help=f"Use the given network config file. Defaults to the file named '{DEFAULT_SIMULAQRON_NETWORK_FILENAME}' " - f"on the current directory.", + help=f"Use the given network config file. Defaults to the file named " # noqa: E131 + f"'{DEFAULT_SIMULAQRON_NETWORK_FILENAME}' on the current directory.", # noqa: E131 type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), default=LOCAL_NETWORK_SETTINGS ) @@ -109,9 +110,9 @@ def start(name: str, nrnodes: int, nodes: str, network_config_file: Path): logging.warning("The pidfile for this network is located at %s", pidfile) return nodes = nodes.split(",") - if len(nodes) <=0: - print(f"WARNING - The list of nodes to start is empty. If you specified the --nrnodes option " - f"this can be normal. Please check your invocation line if needed.") + if len(nodes) <= 0: + print("WARNING - The list of nodes to start is empty. If you specified the --nrnodes option " + "this can be normal. Please check your invocation line if needed.") if nrnodes > 0 and len(nodes) < nrnodes: nodes += [f"Node{i}" for i in range(nrnodes - len(nodes))] d = SimulaQronDaemon(pidfile=pidfile, name=name, nodes=nodes) @@ -381,9 +382,9 @@ def get(): def sim_backend(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print(f"Configuration from default configuration:") + print("Configuration from default configuration") simulaqron_settings.default_settings() print(simulaqron_settings.sim_backend) @@ -394,9 +395,9 @@ def sim_backend(): def max_qubits(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print(f"Configuration from default configuration:") + print("Configuration from default configuration") simulaqron_settings.default_settings() print(simulaqron_settings.max_qubits) @@ -407,9 +408,9 @@ def max_qubits(): def max_registers(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print(f"Configuration from default configuration:") + print("Configuration from default configuration") simulaqron_settings.default_settings() print(simulaqron_settings.max_registers) @@ -420,9 +421,9 @@ def max_registers(): def conn_retry_time(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print(f"Configuration from default configuration:") + print("Configuration from default configuration") simulaqron_settings.default_settings() print(simulaqron_settings.conn_retry_time) @@ -433,9 +434,9 @@ def conn_retry_time(): def recv_timeout(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print(f"Configuration from default configuration:") + print("Configuration from default configuration") simulaqron_settings.default_settings() print(simulaqron_settings.recv_timeout) @@ -446,9 +447,9 @@ def recv_timeout(): def recv_retry_time(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print(f"Configuration from default configuration:") + print("Configuration from default configuration") simulaqron_settings.default_settings() print(simulaqron_settings.recv_retry_time) @@ -459,9 +460,9 @@ def recv_retry_time(): def log_level(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print(f"Configuration from default configuration:") + print("Configuration from default configuration") simulaqron_settings.default_settings() print(simulaqron_settings.log_level) @@ -472,9 +473,9 @@ def log_level(): def noisy_qubits(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print(f"Configuration from default configuration:") + print("Configuration from default configuration") simulaqron_settings.default_settings() if simulaqron_settings.noisy_qubits: print("on") @@ -488,9 +489,9 @@ def noisy_qubits(): def t1(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}':") + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print(f"Configuration from default configuration:") + print("Configuration from default configuration") simulaqron_settings.default_settings() print(simulaqron_settings.t1) @@ -531,8 +532,8 @@ def nodes(): help="The neighbors of the node in the network separated by ',' (no space).\n \ For example '--neighbors Bob,Charlie,David'.\n \ If not specified all current nodes in the network will be neighbors.") -def add(name: str, network_name: str, hostname: str , app_port: int, qnodeos_port: int, - vnode_port: int, neighbors: Optional[str]=None): +def add(name: str, network_name: str, hostname: str, app_port: int, qnodeos_port: int, + vnode_port: int, neighbors: Optional[str] = None): """ Add a node to the network. @@ -545,13 +546,13 @@ def add(name: str, network_name: str, hostname: str , app_port: int, qnodeos_por neighbors = [neighbor.strip() for neighbor in neighbors] network_config.read_from_file(LOCAL_NETWORK_SETTINGS) network_config.add_node(node_name=name, network_name=network_name, - app_hostname=hostname, qnodeos_hostname=hostname, vnode_hostname=hostname, - app_port=app_port, qnodeos_port=qnodeos_port, vnode_port=vnode_port, - neighbors=neighbors) + app_hostname=hostname, qnodeos_hostname=hostname, vnode_hostname=hostname, + app_port=app_port, qnodeos_port=qnodeos_port, vnode_port=vnode_port, + neighbors=neighbors) network_config.write_to_file(LOCAL_NETWORK_SETTINGS) added_node: NodeConfig = network_config.get_nodes(network_name=network_name)[name] print(f"Node with name '{added_node.name}' was added to the network with name '{network_name}'.\n" - f"Socket addresses are:\n" + "Socket addresses are: \n" f"* App/Classical: '({added_node.app_hostname}, {added_node.app_port})\n" f"* QNodeOS: '({added_node.qnodeos_hostname}, {added_node.qnodeos_port})\n" f"* Virtual Node: '({added_node.vnode_hostname}, {added_node.vnode_port})\n") diff --git a/tests/quick/engine/test_project_q_engine.py b/tests/quick/engine/test_project_q_engine.py index b60a73d7..ae6a8496 100644 --- a/tests/quick/engine/test_project_q_engine.py +++ b/tests/quick/engine/test_project_q_engine.py @@ -20,7 +20,7 @@ class TestProjectQEnginInit: def test_init(self): eng = ProjectQEngine("Alice", 0) assert eng.maxQubits == 10 - assert eng.activeQubits== 0 + assert eng.activeQubits == 0 assert len(eng.qubitReg) == 0 eng = ProjectQEngine("Alice", 0, 5) diff --git a/tests/quick/general/test_host_config.py b/tests/quick/general/test_host_config.py index b807e46d..f8856333 100644 --- a/tests/quick/general/test_host_config.py +++ b/tests/quick/general/test_host_config.py @@ -31,9 +31,9 @@ def _assert_results(app_conf: SocketsConfig, qnodeos_conf: SocketsConfig, vnode_ @pytest.mark.skip(reason="Reading network config from legacy format files is not implemented yet") def test_load_legacy_net_config_file(self): this_file_folder = Path(__file__).parent - sockets_config_path = this_file_folder / "resources" / "sockets.cfg" - qnodeos_config_path = this_file_folder / "resources" / "qnodeos.cfg" - virtual_config_path = this_file_folder / "resources" / "virtual.cfg" + sockets_config_path = this_file_folder / "resources" / "sockets.cfg" + qnodeos_config_path = this_file_folder / "resources" / "qnodeos.cfg" + virtual_config_path = this_file_folder / "resources" / "virtual.cfg" network_config.read_from_legacy_files(app_file_path=sockets_config_path, qnodeos_config_path=qnodeos_config_path, virtual_config_path=virtual_config_path) @@ -44,7 +44,6 @@ def test_load_legacy_net_config_file(self): TestSocketsConfig._assert_results(app_conf, qnodeos_conf, vnode_conf) - def test_load_new_net_config_file(self): this_file_folder = Path(__file__).parent network_config_path = this_file_folder / "resources" / "network.json" diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index a4ee2144..2dd837a4 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -14,7 +14,7 @@ from netqasm.sdk.external import NetQASMConnection, Socket, get_qubit_state # noqa: E402 from netqasm.sdk import Qubit, EPRSocket, set_qubit_state # noqa: E402 -from simulaqron.run.run import run_applications, reset # noqa: E402 +from simulaqron.run.run import run_applications # noqa: E402 class TestGetQubitState: diff --git a/tests/quick/stabilizer_states/test_stabilizer_states.py b/tests/quick/stabilizer_states/test_stabilizer_states.py index 7b4fe75a..ae9fa527 100644 --- a/tests/quick/stabilizer_states/test_stabilizer_states.py +++ b/tests/quick/stabilizer_states/test_stabilizer_states.py @@ -1,6 +1,6 @@ -import pytest -import numpy as np import networkx as nx +import numpy as np +import pytest from simulaqron.toolbox.stabilizer_states import StabilizerState @@ -163,10 +163,10 @@ def test_Pauli_phase_tracking(self): assert S.Pauli_phase_tracking([False, True], [True, True]) assert S.Pauli_phase_tracking([True, False], [False, True]) - assert (S.Pauli_phase_tracking([True, False], [True, True]) == - S.Pauli_phase_tracking([True, True], [False, True])) - assert (S.Pauli_phase_tracking([False, True], [True, False]) == - S.Pauli_phase_tracking([True, True], [False, True])) + assert (S.Pauli_phase_tracking([True, False], [True, True]) == S.Pauli_phase_tracking([True, True], + [False, True])) + assert (S.Pauli_phase_tracking([False, True], [True, False]) == S.Pauli_phase_tracking([True, True], + [False, True])) def test_gaussian_elimination(self): S = StabilizerState(["XZZ", "YIX", "IXX"]) @@ -369,13 +369,13 @@ def test_going_to_graph(self): @pytest.mark.parametrize("stabilizer, expected", [ # stabilizer, expected - ("XX", True), - ("+1XX", True), - ("-1XX", False), - ("+1YY", False), - ("-1YY", True), - ("+1YI", False), - ("IY", False), + ("XX", True), + ("+1XX", True), + ("-1XX", False), + ("+1YY", False), + ("-1YY", True), + ("+1YI", False), + ("IY", False), ]) def test_contains(self, stabilizer, expected): s = StabilizerState(["XX", "ZZ"]) diff --git a/tests/quick/test_networks_settings.py b/tests/quick/test_networks_settings.py index d32359ec..18e3e93d 100644 --- a/tests/quick/test_networks_settings.py +++ b/tests/quick/test_networks_settings.py @@ -15,6 +15,7 @@ cwd_network = (Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() home_network = (Path.home() / ".simulaqron" / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() + class TestNetworksSettings: @pytest.fixture def clean_settings(self): @@ -79,7 +80,6 @@ def test_create_default_settings(self, clean_settings): for node in network_config.nodes: assert all(TestNetworksSettings._check_node_config(node)) - def test_add_node(self, reset_net_cfg): network_config.remove_all_networks() diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index 039bd8de..d05e6f57 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -201,7 +201,6 @@ def network(self): network.stop() reset() - def test_CNOT_control(self, network): with SimulaQronConnection("Bob") as conn: # Test CNOT control From ef8527cdcb03033d7be63e08f3eedffd18993968 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 12:54:18 +0100 Subject: [PATCH 151/277] Changes on the corrRNG example --- examples/nativeMode/corrRNG/README.txt | 2 +- examples/nativeMode/corrRNG/aliceTest.py | 11 +-- examples/nativeMode/corrRNG/bobTest.py | 11 +-- examples/nativeMode/corrRNG/classicalNet.json | 31 ++++---- examples/nativeMode/corrRNG/terminate.sh | 0 .../_default_config/default_network.json | 75 ++++--------------- 6 files changed, 46 insertions(+), 84 deletions(-) mode change 100644 => 100755 examples/nativeMode/corrRNG/terminate.sh diff --git a/examples/nativeMode/corrRNG/README.txt b/examples/nativeMode/corrRNG/README.txt index 3ed2b8e3..bc2221cc 100644 --- a/examples/nativeMode/corrRNG/README.txt +++ b/examples/nativeMode/corrRNG/README.txt @@ -13,7 +13,7 @@ Bob to check whether the teleportation worked correctly. To start run: -- simulaqron start --nodes=Alice,Bob --keep +- simulaqron start --nodes=Alice,Bob --network-config-file classicalNet.json sh run.sh diff --git a/examples/nativeMode/corrRNG/aliceTest.py b/examples/nativeMode/corrRNG/aliceTest.py index dc7e5bf9..288a4dad 100644 --- a/examples/nativeMode/corrRNG/aliceTest.py +++ b/examples/nativeMode/corrRNG/aliceTest.py @@ -31,7 +31,7 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config from twisted.internet.defer import inlineCallbacks from twisted.internet import reactor from twisted.spread import pb @@ -117,11 +117,12 @@ def main(): myName = "Alice" # we are Alice - # This file defines the network of virtual quantum nodes - virtualNet = SocketsConfig(str(simulaqron_settings.network_config_file), network_name="default", config_type="vnode") + # This file defines the network of virtual quantum nodes and the network used for classical communication + network_config.read_from_file("classicalNet.json") - # This file defines the network used for classical communication - classicalNet = SocketsConfig("classicalNet.json", network_name="default", config_type="app") + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a server (if this node is listed in classicalNet) if myName in classicalNet.hostDict: diff --git a/examples/nativeMode/corrRNG/bobTest.py b/examples/nativeMode/corrRNG/bobTest.py index fcfe2123..e006897d 100644 --- a/examples/nativeMode/corrRNG/bobTest.py +++ b/examples/nativeMode/corrRNG/bobTest.py @@ -32,7 +32,7 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -130,11 +130,12 @@ def main(): logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.DEBUG) - # This file defines the network of virtual quantum nodes - virtualNet = SocketsConfig(str(simulaqron_settings.network_config_file), network_name="default", config_type="vnode") + # This file defines the network of virtual quantum nodes and the network used for classical communication + network_config.read_from_file("classicalNet.json") - # This file defines the network used for classical communication - classicalNet = SocketsConfig("classicalNet.json", network_name="default", config_type="app") + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a server (if this node is listed in classicalNet) if myName in classicalNet.hostDict: diff --git a/examples/nativeMode/corrRNG/classicalNet.json b/examples/nativeMode/corrRNG/classicalNet.json index 4206e4d9..33e6c79b 100644 --- a/examples/nativeMode/corrRNG/classicalNet.json +++ b/examples/nativeMode/corrRNG/classicalNet.json @@ -1,17 +1,22 @@ -{ - "default": { - "nodes": { - "Alice": { - "app_socket": ["localhost", 8821], - "qnodeos_socket": ["localhost", 8822], - "vnode_socket": ["localhost", 8823] +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } }, - "Bob": { - "app_socket": ["localhost", 8831], - "qnodeos_socket": ["localhost", 8832], - "vnode_socket": ["localhost", 8833] + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } } - }, + ], "topology": null } -} +] \ No newline at end of file diff --git a/examples/nativeMode/corrRNG/terminate.sh b/examples/nativeMode/corrRNG/terminate.sh old mode 100644 new mode 100755 diff --git a/simulaqron/_default_config/default_network.json b/simulaqron/_default_config/default_network.json index 8cd2bd75..1db74604 100644 --- a/simulaqron/_default_config/default_network.json +++ b/simulaqron/_default_config/default_network.json @@ -4,82 +4,37 @@ "nodes": [ { "Alice": { - "app_socket": [ - "localhost", - 8001 - ], - "qnodeos_socket": [ - "localhost", - 8002 - ], - "vnode_socket": [ - "localhost", - 8003 - ] + "app_socket": ["localhost", 8001], + "qnodeos_socket": ["localhost", 8002], + "vnode_socket": ["localhost", 8003] } }, { "Bob": { - "app_socket": [ - "localhost", - 8010 - ], - "qnodeos_socket": [ - "localhost", - 8011 - ], - "vnode_socket": [ - "localhost", - 8012 - ] + "app_socket": ["localhost", 8010], + "qnodeos_socket": ["localhost", 8011], + "vnode_socket": ["localhost", 8012] } }, { "Charlie": { - "app_socket": [ - "localhost", - 8020 - ], - "qnodeos_socket": [ - "localhost", - 8021 - ], - "vnode_socket": [ - "localhost", - 8022 - ] + "app_socket": ["localhost", 8020], + "qnodeos_socket": ["localhost", 8021], + "vnode_socket": ["localhost", 8022] } }, { "David": { - "app_socket": [ - "localhost", - 8030 - ], - "qnodeos_socket": [ - "localhost", - 8031 - ], - "vnode_socket": [ - "localhost", - 8032 - ] + "app_socket": ["localhost", 8030], + "qnodeos_socket": ["localhost", 8031], + "vnode_socket": ["localhost", 8032] } }, { "Eve": { - "app_socket": [ - "localhost", - 8040 - ], - "qnodeos_socket": [ - "localhost", - 8041 - ], - "vnode_socket": [ - "localhost", - 8042 - ] + "app_socket": ["localhost", 8040], + "qnodeos_socket": ["localhost", 8041], + "vnode_socket": ["localhost", 8042] } } ], From 9ed311d55c99fb5925c9ba305f8dfaa3b2463d1e Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 3 Dec 2025 13:15:18 +0100 Subject: [PATCH 152/277] Linter changes --- examples/nativeMode/corrRNG/aliceTest.py | 9 +++++---- examples/nativeMode/corrRNG/bobTest.py | 6 ++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/nativeMode/corrRNG/aliceTest.py b/examples/nativeMode/corrRNG/aliceTest.py index 288a4dad..d4756415 100644 --- a/examples/nativeMode/corrRNG/aliceTest.py +++ b/examples/nativeMode/corrRNG/aliceTest.py @@ -31,7 +31,7 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings import network_config from twisted.internet.defer import inlineCallbacks from twisted.internet import reactor from twisted.spread import pb @@ -115,7 +115,7 @@ def remote_test(self): # def main(): - myName = "Alice" # we are Alice + myName = "Alice" # we are Alice # This file defines the network of virtual quantum nodes and the network used for classical communication network_config.read_from_file("classicalNet.json") @@ -124,7 +124,7 @@ def main(): virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") - # Check if we should run a server (if this node is listed in classicalNet) + # Check if we should run a server (if this node is listed in classicalNet) if myName in classicalNet.hostDict: # Create the local classical server logging.debug("LOCAL %s: Creating classical server.", myName) @@ -132,9 +132,10 @@ def main(): else: myNode = None - # Connect and run + # Connect and run setup_local(myName, virtualNet, classicalNet, myNode, runClientNode) + ################################################################################################## logging.basicConfig(format="%(asctime)s:%(levelname)s:%(message)s", level=logging.DEBUG) main() diff --git a/examples/nativeMode/corrRNG/bobTest.py b/examples/nativeMode/corrRNG/bobTest.py index e006897d..67e8a30a 100644 --- a/examples/nativeMode/corrRNG/bobTest.py +++ b/examples/nativeMode/corrRNG/bobTest.py @@ -32,7 +32,7 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings import network_config from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -127,7 +127,6 @@ def main(): # In this example, we are Bob. myName = "Bob" - logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.DEBUG) # This file defines the network of virtual quantum nodes and the network used for classical communication @@ -137,7 +136,7 @@ def main(): virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") - # Check if we should run a server (if this node is listed in classicalNet) + # Check if we should run a server (if this node is listed in classicalNet) if myName in classicalNet.hostDict: # Create the local classical server myNode = localNode(virtualNet.hostDict[myName], classicalNet) @@ -148,7 +147,6 @@ def main(): setup_local(myName, virtualNet, classicalNet, myNode, runClientNode) - ################################################################################################## logging.basicConfig(format="%(asctime)s:%(levelname)s:%(message)s", level=logging.DEBUG) main() From 14e7b8faa919b706f714b69f2fac89ad91ccddc9 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 11:47:30 +0100 Subject: [PATCH 153/277] Move settings tests to its onw folder --- tests/quick/{ => settings}/test_networks_settings.py | 0 tests/quick/{ => settings}/test_simulaqron_settings.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/quick/{ => settings}/test_networks_settings.py (100%) rename tests/quick/{ => settings}/test_simulaqron_settings.py (100%) diff --git a/tests/quick/test_networks_settings.py b/tests/quick/settings/test_networks_settings.py similarity index 100% rename from tests/quick/test_networks_settings.py rename to tests/quick/settings/test_networks_settings.py diff --git a/tests/quick/test_simulaqron_settings.py b/tests/quick/settings/test_simulaqron_settings.py similarity index 100% rename from tests/quick/test_simulaqron_settings.py rename to tests/quick/settings/test_simulaqron_settings.py From b2c0d83cb737a12e2b0200bebfd9d214c0569acb Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 11:56:38 +0100 Subject: [PATCH 154/277] Add test case for reading (and correcting) an old json format network config --- .../quick/settings/resources/old_format.json | 17 +++++++++++++ .../quick/settings/test_networks_settings.py | 25 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/quick/settings/resources/old_format.json diff --git a/tests/quick/settings/resources/old_format.json b/tests/quick/settings/resources/old_format.json new file mode 100644 index 00000000..4206e4d9 --- /dev/null +++ b/tests/quick/settings/resources/old_format.json @@ -0,0 +1,17 @@ +{ + "default": { + "nodes": { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + }, + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + "topology": null + } +} diff --git a/tests/quick/settings/test_networks_settings.py b/tests/quick/settings/test_networks_settings.py index 18e3e93d..a6706997 100644 --- a/tests/quick/settings/test_networks_settings.py +++ b/tests/quick/settings/test_networks_settings.py @@ -258,3 +258,28 @@ def test_deserialize_network_config(self, reset_net_cfg): network_config.read_from_file(temp_file.name) assert json.dumps(JSONSerializer.serialize(network_config), indent=4) == raw_config + + def test_load_old_json_format(self): + this_file_folder = Path(__file__).parent + old_json_config_path = this_file_folder / "resources" / "old_format.json" + network_config.read_from_file(old_json_config_path) + + assert len(network_config.networks) == 1 + assert network_config.networks[0].name == "default" + assert len(network_config.nodes) == 2 + + assert network_config.nodes[0].name == "Alice" + assert network_config.nodes[0].app_hostname == "localhost" + assert network_config.nodes[0].app_port == 8821 + assert network_config.nodes[0].qnodeos_hostname == "localhost" + assert network_config.nodes[0].qnodeos_port == 8822 + assert network_config.nodes[0].vnode_hostname == "localhost" + assert network_config.nodes[0].qnodeos_port == 8823 + + assert network_config.nodes[1].name == "Bob" + assert network_config.nodes[1].app_hostname == "localhost" + assert network_config.nodes[1].app_port == 8831 + assert network_config.nodes[1].qnodeos_hostname == "localhost" + assert network_config.nodes[1].qnodeos_port == 8832 + assert network_config.nodes[1].vnode_hostname == "localhost" + assert network_config.nodes[1].qnodeos_port == 8833 From 1016eeaf9532159e1792a05676b12cf3679ef32b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 12:51:25 +0100 Subject: [PATCH 155/277] Adapt test for olf json format to use a temporal file that will be reformatted --- .../quick/settings/test_networks_settings.py | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/tests/quick/settings/test_networks_settings.py b/tests/quick/settings/test_networks_settings.py index a6706997..dec8b854 100644 --- a/tests/quick/settings/test_networks_settings.py +++ b/tests/quick/settings/test_networks_settings.py @@ -262,24 +262,30 @@ def test_deserialize_network_config(self, reset_net_cfg): def test_load_old_json_format(self): this_file_folder = Path(__file__).parent old_json_config_path = this_file_folder / "resources" / "old_format.json" - network_config.read_from_file(old_json_config_path) - assert len(network_config.networks) == 1 - assert network_config.networks[0].name == "default" - assert len(network_config.nodes) == 2 - - assert network_config.nodes[0].name == "Alice" - assert network_config.nodes[0].app_hostname == "localhost" - assert network_config.nodes[0].app_port == 8821 - assert network_config.nodes[0].qnodeos_hostname == "localhost" - assert network_config.nodes[0].qnodeos_port == 8822 - assert network_config.nodes[0].vnode_hostname == "localhost" - assert network_config.nodes[0].qnodeos_port == 8823 + with NamedTemporaryFile(mode="wt", delete_on_close=False) as temp_file: + shutil.copy(old_json_config_path, temp_file.name) + network_config.read_from_file(temp_file.name) - assert network_config.nodes[1].name == "Bob" - assert network_config.nodes[1].app_hostname == "localhost" - assert network_config.nodes[1].app_port == 8831 - assert network_config.nodes[1].qnodeos_hostname == "localhost" - assert network_config.nodes[1].qnodeos_port == 8832 - assert network_config.nodes[1].vnode_hostname == "localhost" - assert network_config.nodes[1].qnodeos_port == 8833 + raw_converted = json.load(temp_file) + assert isinstance(raw_converted, dict) + + assert len(network_config.networks) == 1 + assert network_config.networks[0].name == "default" + assert len(network_config.nodes) == 2 + + assert network_config.nodes[0].name == "Alice" + assert network_config.nodes[0].app_hostname == "localhost" + assert network_config.nodes[0].app_port == 8821 + assert network_config.nodes[0].qnodeos_hostname == "localhost" + assert network_config.nodes[0].qnodeos_port == 8822 + assert network_config.nodes[0].vnode_hostname == "localhost" + assert network_config.nodes[0].qnodeos_port == 8823 + + assert network_config.nodes[1].name == "Bob" + assert network_config.nodes[1].app_hostname == "localhost" + assert network_config.nodes[1].app_port == 8831 + assert network_config.nodes[1].qnodeos_hostname == "localhost" + assert network_config.nodes[1].qnodeos_port == 8832 + assert network_config.nodes[1].vnode_hostname == "localhost" + assert network_config.nodes[1].qnodeos_port == 8833 From 3e681a45fad37c45bd45a9c6ed72a895316afbe1 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 13:13:29 +0100 Subject: [PATCH 156/277] First version of the format conversion code --- simulaqron/settings/network_config.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 4fd80556..04f325ad 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -10,6 +10,7 @@ from typing import Optional, Self, Dict, List, Tuple, Any from dataclasses_serialization.json import JSONSerializer, JSONSerializerMixin +from docutils.nodes import node_class_names import simulaqron._default_config @@ -170,6 +171,11 @@ class NetworkConfigBuilder(JSONSerializerMixin): used_sockets: List[Tuple[str, int]] = field(default_factory=list) def using_default_network(self): + """ + Loads the default networks in the current networks configuration object. The default + configuration contains a single network named "default", which contains 5 nodes named + "Alice", "Bob", "Charlie", "David" and "Eve". + """ # We use the embedded default network here default_network_path = resources.files(simulaqron._default_config).joinpath("default_network.json") new_builder = NetworkConfigBuilder() @@ -417,14 +423,31 @@ def read_from_file(self, file_path: PathLike | str): setattr(self, class_field.name, new_val) def read_from_legacy_files(self, app_file_path: PathLike | str, - qnodeos_file_path: PathLike | str, - vnode_file_path: PathLike | str): + vnode_file_path: PathLike | str, + qnodeos_file_path: Optional[PathLike | str] = None): raise NotImplementedError("Reading form legacy config files is not supported yet") + @staticmethod + def _correct_old_format(config_content: Dict[str, Any] | List[Dict[str, Any]]) -> List[Dict[str, Any]]: + if isinstance(config_content, list): + return config_content + # Here we assume that we are working with a dictionary + assert isinstance(config_content, dict) + reformatted_config = [] + for network_name, net_spec in config_content.items(): + new_network = { + "name": network_name, + "nodes": [{node_name: node_spec} for node_name, node_spec in net_spec["nodes"].items()], + "topology": net_spec["topology"] + } + reformatted_config.append(new_network) + return reformatted_config + @classmethod def _deserialize_from_file(cls, file_path: Path) -> Self: with file_path.resolve().open("rt") as file: config_content = json.load(file) + config_content = NetworkConfigBuilder._correct_old_format(config_content) return JSONSerializer.deserialize(cls, config_content) @classmethod From 58edecc4fd54e4b0d9c244353707bd1889c569dd Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 13:39:58 +0100 Subject: [PATCH 157/277] Fix code to correct old json network format --- simulaqron/settings/network_config.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 04f325ad..f933d4ee 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -428,11 +428,17 @@ def read_from_legacy_files(self, app_file_path: PathLike | str, raise NotImplementedError("Reading form legacy config files is not supported yet") @staticmethod - def _correct_old_format(config_content: Dict[str, Any] | List[Dict[str, Any]]) -> List[Dict[str, Any]]: - if isinstance(config_content, list): - return config_content - # Here we assume that we are working with a dictionary - assert isinstance(config_content, dict) + def _is_old_json_format(config_content: Dict[str, Any] | List[Dict[str, Any]]) -> bool: + match config_content: + case list(): + return False + case dict(): + return True + case _: + raise ValueError("JSON network configi file does not have a valid format.") + + @staticmethod + def _correct_old_format(config_content: Dict[str, Any]) -> List[Dict[str, Any]]: reformatted_config = [] for network_name, net_spec in config_content.items(): new_network = { @@ -447,8 +453,11 @@ def _correct_old_format(config_content: Dict[str, Any] | List[Dict[str, Any]]) - def _deserialize_from_file(cls, file_path: Path) -> Self: with file_path.resolve().open("rt") as file: config_content = json.load(file) + if NetworkConfigBuilder._is_old_json_format(config_content): config_content = NetworkConfigBuilder._correct_old_format(config_content) - return JSONSerializer.deserialize(cls, config_content) + with file_path.open("wt") as file: + json.dump(config_content, file, indent=4) + return JSONSerializer.deserialize(cls, config_content) @classmethod def load_from_known_sources(cls) -> Self: From 8d1a4c2a05d5280cc5cb3239114503614ba70932 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 13:40:48 +0100 Subject: [PATCH 158/277] Fix test case for correcting old json network format --- tests/quick/settings/test_networks_settings.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/quick/settings/test_networks_settings.py b/tests/quick/settings/test_networks_settings.py index dec8b854..256e27ca 100644 --- a/tests/quick/settings/test_networks_settings.py +++ b/tests/quick/settings/test_networks_settings.py @@ -264,14 +264,19 @@ def test_load_old_json_format(self): old_json_config_path = this_file_folder / "resources" / "old_format.json" with NamedTemporaryFile(mode="wt", delete_on_close=False) as temp_file: + # We copy the content of the resource into a temp file, so we don't + # overwrite the resource for future test sessions shutil.copy(old_json_config_path, temp_file.name) + + # Read the file containing the old format network_config.read_from_file(temp_file.name) - raw_converted = json.load(temp_file) - assert isinstance(raw_converted, dict) + # Check that the file content was updated + raw_converted = json.load(Path(temp_file.name).open()) + assert isinstance(raw_converted, list) assert len(network_config.networks) == 1 - assert network_config.networks[0].name == "default" + assert "default" in network_config.networks assert len(network_config.nodes) == 2 assert network_config.nodes[0].name == "Alice" @@ -280,7 +285,7 @@ def test_load_old_json_format(self): assert network_config.nodes[0].qnodeos_hostname == "localhost" assert network_config.nodes[0].qnodeos_port == 8822 assert network_config.nodes[0].vnode_hostname == "localhost" - assert network_config.nodes[0].qnodeos_port == 8823 + assert network_config.nodes[0].vnode_port == 8823 assert network_config.nodes[1].name == "Bob" assert network_config.nodes[1].app_hostname == "localhost" @@ -288,4 +293,4 @@ def test_load_old_json_format(self): assert network_config.nodes[1].qnodeos_hostname == "localhost" assert network_config.nodes[1].qnodeos_port == 8832 assert network_config.nodes[1].vnode_hostname == "localhost" - assert network_config.nodes[1].qnodeos_port == 8833 + assert network_config.nodes[1].vnode_port == 8833 From 6b6f3994146b3bfca1a064e3bf483dc9c07942b1 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 15:50:14 +0100 Subject: [PATCH 159/277] Add a way to filter out loaded SocketsConfigs --- simulaqron/general/host_config.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index 23b72589..2fc6ed9a 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -30,7 +30,7 @@ import socket import struct from ipaddress import IPv4Address -from typing import Dict +from typing import Dict, List from twisted.spread import pb @@ -89,6 +89,21 @@ def print_details(self, name: str): host = self.hostDict[name] print("Host details of ", name, ": ", host.hostname, ":", host.port) + def filter(self, nodes_to_keep: List[str]): + """ + Filter the loaded sockets configurations to only contain the given names. + If a given node name is not found in the loaded one, it will simply be ignored + from the exclusion process (i.e. it will not break the process) + Args: + nodes_to_keep: List[str] + The node names to keep after filtering. + """ + nodes_kept = {} + for node_name in self.hostDict.keys(): + if node_name in nodes_to_keep: + nodes_kept[node_name] = self.hostDict[node_name] + self.hostDict = nodes_kept + def node_id(fam: socket.AddressFamily, ip: str) -> int: if fam == socket.AF_INET: From 0ac2262ddfd59f5eee110c996d95b3287242b32d Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 15:51:03 +0100 Subject: [PATCH 160/277] Fix flushing and closing files when not needed in signal handlers --- simulaqron/start/start_qnodeos.py | 5 +++-- simulaqron/start/start_vnode.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 370d35ef..be3f7f26 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -116,8 +116,9 @@ def setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): def sigterm_handler(_signo, _stack_frame): global stdout_file - stdout_file.flush() - stdout_file.close() + if stdout_file is not None: + stdout_file.flush() + stdout_file.close() reactor.stop() diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 1ece8692..cd5b1201 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -47,8 +47,9 @@ def sigterm_handler(name, _signo, _stack_frame): print("Shutting down Node from signal %d." % _signo, flush=True) global stdout_file - stdout_file.flush() - stdout_file.close() + if stdout_file is not None: + stdout_file.flush() + stdout_file.close() reactor.stop() From 03a8ef681fffaed0c71d511022f2ac6bf299c3cf Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 16:04:35 +0100 Subject: [PATCH 161/277] Fix merges test to use the new unified network configuration --- tests/slow/merges/test_merges.py | 74 +++++++++++++------------------- 1 file changed, 30 insertions(+), 44 deletions(-) diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 662ecc1a..d4da6093 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -1,9 +1,7 @@ import sys import os import unittest -from pathlib import Path -from shutil import copyfile -from tempfile import NamedTemporaryFile +from typing import Callable, List import numpy as np @@ -11,13 +9,14 @@ from twisted.internet.defer import inlineCallbacks from multiprocess.context import ForkProcess as Process -from multiprocess.connection import Pipe +from multiprocess.connection import Pipe, Connection from netqasm.logging.glob import get_netqasm_logger from logging import DEBUG from simulaqron.general.host_config import SocketsConfig from simulaqron.local.setup import setup_local, assemble_qubit from simulaqron.network import Network -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings.network_config import NodeConfigType from simulaqron.settings.simulaqron_config import SimBackend from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.reactor import reactor @@ -147,7 +146,6 @@ def remote_receive_one_qubit(self, virtualNum, cnot_direction=0): return bool(correct) -# @for_all_methods() class TestMerge(unittest.TestCase): @classmethod def setUpClass(cls): @@ -156,23 +154,15 @@ def setUpClass(cls): cls.processes = [] cls.processes_to_wait_for = None + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ - with NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as simulaqron_settings_file: - cls._simulaqron_settings_file = simulaqron_settings_file - simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ - with NamedTemporaryFile(mode="w+", suffix=".json", delete=False) as network_def_file: - cls._network_def_file = network_def_file - path_to_here = os.path.dirname(os.path.abspath(__file__)) - network_config_file = os.path.join(path_to_here, "configs", "network.json") - copyfile(network_config_file, network_def_file.name) - simulaqron_settings.network_config_file = network_def_file.name - simulaqron_settings.save_to_file(simulaqron_settings_file.name) - nodes = ["Alice", "Bob", "Charlie"] - cls.network = Network(nodes=nodes, force=True) - cls.network.start() - # cls.network = Network(nodes=nodes, force=True) - # cls.network.start() + path_to_here = os.path.dirname(os.path.abspath(__file__)) + network_config_file = os.path.join(path_to_here, "configs", "network.json") + network_config.read_from_file(network_config_file) + nodes = ["Alice", "Bob", "Charlie"] + cls.network = Network(nodes=nodes) + cls.network.start() @classmethod def tearDownClass(cls): @@ -181,30 +171,23 @@ def tearDownClass(cls): p.join() cls.network.stop() - # Remove the files created for config - simulaqron_settings_file = Path(cls._simulaqron_settings_file.name) - network_settings_file = Path(cls._network_def_file.name) - cls._simulaqron_settings_file.close() - cls._network_def_file.close() - simulaqron_settings_file.unlink() - network_settings_file.unlink() @staticmethod - def setup_node(name, node_code, classical_net_file, send_end): + def setup_node(name: str, node_code: Callable, nodes_in_classical_network: List[str], send_end: Connection): if simulaqron_settings.log_level == DEBUG: stdout_file = open(f"stdout-setup-node-{name}-{os.getpid()}.out.txt", "w") stderr_file = open(f"stderr-setup-node-{name}-{os.getpid()}.out.txt", "w") sys.stdout = stdout_file sys.stderr = stderr_file - # This file defines the network of virtual quantum nodes - virtualFile = simulaqron_settings.network_config_file + # We get the virtual nodes network configuration from the loaded network config + virtualNet = SocketsConfig(network_config, config_type=NodeConfigType.VNODE) - # This file defines the nodes acting as servers in the classical communication network - classicalFile = os.path.join(os.path.dirname(__file__), "configs", classical_net_file) + # We get the classical nodes network configuration from the loaded network config + classicalNet = SocketsConfig(network_config, config_type=NodeConfigType.APP) - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(virtualFile) - classicalNet = SocketsConfig(classicalFile) + # We also filter the nodes that will participate in the classical network, so we don't + # start local nodes unnecessarily + classicalNet.filter(nodes_in_classical_network) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network @@ -218,13 +201,13 @@ def setup_node(name, node_code, classical_net_file, send_end): # execute the function runClientNode setup_local(name, virtualNet, classicalNet, lNode, node_code, send_end) - def run_test(self, classical_net_file): + def run_test(self, nodes_in_class_network: List[str]): pipe_list = [] - for name, node_code in zip(self.nodes, self.node_codes): + for node_name, node_code in zip(self.nodes, self.node_codes): recv_end, send_end = Pipe(False) p = Process(target=self.setup_node, - args=[name, node_code, classical_net_file, send_end], - name=name) + args=[node_name, node_code, nodes_in_class_network, send_end], + name=node_name) self.processes.append(p) pipe_list.append(recv_end) @@ -296,7 +279,8 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): reactor.stop() def test(self): - self.run_test("Alice.cfg") + # Original arg: "Alice.cfg" -> specifies NO node in the classical network + self.run_test([]) class TestBothLocalNotSameReg(TestBothLocal): @@ -435,7 +419,8 @@ def charlie(qReg, virtRoot, myName, classicalNet, send_end): send_end.send(True) def test(self): - self.run_test("AliceBobCharlie.cfg") + # Original arg: "AliceBobCharlie.cfg" -> specifies "Alice", "Bob", and "Charlie" in the classical network + self.run_test(["Alice", "Bob", "Charlie"]) class TestBothRemoteSameNodeDiffReg(TestMerge): @@ -501,7 +486,8 @@ def bob(qReg, virtRoot, myName, classicalNet, send_end): send_end.send(True) def test(self): - self.run_test("AliceBob.cfg") + # Original arg: "AliceBob.cfg" -> specifies "Alice", and "Bob" in the classical network + self.run_test(["Alice", "Bob"]) class TestBothRemoteSameNodeSameReg(TestBothRemoteSameNodeDiffReg): From a7c8994404def576335732572bcfaabd7fd5d23f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 4 Dec 2025 16:20:16 +0100 Subject: [PATCH 162/277] Fix sdk qubits tests --- tests/slow/sdk/test_single_qubit.py | 52 +++++++++++++---------------- tests/slow/sdk/test_two_qubit.py | 40 ++++++++++------------ 2 files changed, 42 insertions(+), 50 deletions(-) diff --git a/tests/slow/sdk/test_single_qubit.py b/tests/slow/sdk/test_single_qubit.py index 09399b34..dbbca229 100644 --- a/tests/slow/sdk/test_single_qubit.py +++ b/tests/slow/sdk/test_single_qubit.py @@ -34,7 +34,7 @@ from netqasm.sdk.qubit import Qubit -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings.simulaqron_config import SimBackend from simulaqron.network import Network from simulaqron.sdk.connection import SimulaQronConnection @@ -199,54 +199,50 @@ def prep_I_state(): # TODO - We can test these things better when we have implemented a get_qubit_state function for simulaqron -# for now, we will permon tests based on the tomography function. +# for now, we will perform tests based on the tomography function. class TestSingleQubitGate: iterations: int = 1 - @pytest.fixture + @pytest.fixture(autouse=True) def network(self): simulaqron_settings.default_settings() - with NamedTemporaryFile(suffix=".json", delete_on_close=False) as net_config_file: - simulaqron_settings.network_config_file = net_config_file.name - network_builder = NetworkConfigBuilder() - network_builder.using_default_network() - network_builder.write_to_file(net_config_file.name) - network = Network(nodes=["Alice"], force=True) - network.start() - yield network - - network.stop() - reset() - - def test_X_Gate(self, network): + simulaqron_settings.sim_backend = SimBackend.PROJECTQ + network_config.using_default_network() + network = Network(nodes=["Alice"]) + network.start() + yield + network.stop() + reset() + + def test_X_Gate(self): with SimulaQronConnection("Alice") as conn: # Test X exp_values = calc_exp_values(prep_X_state()) ans = conn.test_preparation(prep_X, exp_values, iterations=self.iterations) assert ans - def test_Y_Gate(self, network): + def test_Y_Gate(self): with SimulaQronConnection("Alice") as conn: # Test Y exp_values = calc_exp_values(prep_Y_state()) ans = conn.test_preparation(prep_Y, exp_values, iterations=self.iterations) assert ans - def test_Z_Gate(self, network): + def test_Z_Gate(self): with SimulaQronConnection("Alice") as conn: # Test Z exp_values = calc_exp_values(prep_Z_state()) ans = conn.test_preparation(prep_Z, exp_values, iterations=self.iterations) assert ans - def test_H_Gate(self, network): + def test_H_Gate(self): with SimulaQronConnection("Alice") as conn: # Test H exp_values = calc_exp_values(prep_H_state()) ans = conn.test_preparation(prep_H, exp_values, iterations=self.iterations) assert ans - def test_T_Gate(self, network): + def test_T_Gate(self): with SimulaQronConnection("Alice") as conn: # Test T exp_values = calc_exp_values(prep_T_state()) @@ -257,14 +253,14 @@ def test_T_Gate(self, network): ans = conn.test_preparation(prep_T, exp_values, iterations=self.iterations) assert ans - def test_K_Gate(self, network): + def test_K_Gate(self): with SimulaQronConnection("Alice") as conn: # Test K exp_values = calc_exp_values(prep_K_state()) ans = conn.test_preparation(prep_K, exp_values, iterations=self.iterations) assert ans - def test_X_pi8Rot(self, network): + def test_X_pi8Rot(self): with SimulaQronConnection("Alice") as conn: # Test ROT_X pi/8 exp_values = calc_exp_values(prep_rot_state([1, 0, 0], np.pi / 8)) @@ -275,7 +271,7 @@ def test_X_pi8Rot(self, network): ans = conn.test_preparation(prep_rotx1, exp_values, iterations=self.iterations) assert ans - def test_X_5pi8Rot(self, network): + def test_X_5pi8Rot(self): with SimulaQronConnection("Alice") as conn: # Test ROT_X 5*pi/8 exp_values = calc_exp_values(prep_rot_state([1, 0, 0], 5 * np.pi / 8)) @@ -286,7 +282,7 @@ def test_X_5pi8Rot(self, network): ans = conn.test_preparation(prep_rotx2, exp_values, iterations=self.iterations) assert ans - def test_Y_pi8Rot(self, network): + def test_Y_pi8Rot(self): with SimulaQronConnection("Alice") as conn: # Test ROT_Y pi/8 exp_values = calc_exp_values(prep_rot_state([0, 1, 0], np.pi / 8)) @@ -297,7 +293,7 @@ def test_Y_pi8Rot(self, network): ans = conn.test_preparation(prep_roty1, exp_values, iterations=self.iterations) assert ans - def test_Y_5pi8Rot(self, network): + def test_Y_5pi8Rot(self): with SimulaQronConnection("Alice") as conn: # Test ROT_Y 5*pi/8 exp_values = calc_exp_values(prep_rot_state([0, 1, 0], 5 * np.pi / 8)) @@ -308,7 +304,7 @@ def test_Y_5pi8Rot(self, network): ans = conn.test_preparation(prep_roty2, exp_values, iterations=self.iterations) assert ans - def test_Z_pi8Rot(self, network): + def test_Z_pi8Rot(self): with SimulaQronConnection("Alice") as conn: # Test ROT_Z pi/8 exp_values = calc_exp_values(prep_rot_state([0, 0, 1], np.pi / 8)) @@ -319,7 +315,7 @@ def test_Z_pi8Rot(self, network): ans = conn.test_preparation(prep_rotz1, exp_values, iterations=self.iterations) assert ans - def test_Z_5pi8Rot(self, network): + def test_Z_5pi8Rot(self): with SimulaQronConnection("Alice") as conn: # Test ROT_Z 5*pi/8 exp_values = calc_exp_values(prep_rot_state([0, 0, 1], 5 * np.pi / 8)) @@ -330,7 +326,7 @@ def test_Z_5pi8Rot(self, network): ans = conn.test_preparation(prep_rotz2, exp_values, iterations=self.iterations) assert ans - def test_Reset(self, network): + def test_Reset(self): with SimulaQronConnection("Alice") as conn: # Test RESET exp_values = calc_exp_values(prep_I_state()) diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index d05e6f57..3c837943 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -40,8 +40,9 @@ from simulaqron.sdk.socket import Socket from simulaqron.run.run import run_applications from simulaqron.run.run import reset -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings.network_config import NetworkConfigBuilder +from simulaqron.settings.simulaqron_config import SimBackend def calc_exp_values(q): @@ -183,46 +184,41 @@ def prep_H_state(): # TODO - We can test these things better when we have implemented a get_qubit_state function for simulaqron # for now, we will perform tests based on the tomography function. class TestTwoQubitGates: - iterations = 1 + iterations: int = 1 - @pytest.fixture + @pytest.fixture(autouse=True) def network(self): simulaqron_settings.default_settings() - with NamedTemporaryFile(suffix=".json", delete_on_close=False) as net_config_file: - simulaqron_settings.network_config_file = net_config_file.name - network_builder = NetworkConfigBuilder() - network_builder.using_default_network() - network_builder.write_to_file(net_config_file.name) - net_config_file.close() - network = Network(nodes=["Alice", "Bob"], force=True) - network.start(wait_until_running=True) - yield network - - network.stop() - reset() - - def test_CNOT_control(self, network): + simulaqron_settings.sim_backend = SimBackend.PROJECTQ + network_config.using_default_network() + network = Network(nodes=["Alice", "Bob"]) + network.start(wait_until_running=True) + yield + network.stop() + reset() + + def test_CNOT_control(self): with SimulaQronConnection("Bob") as conn: # Test CNOT control exp_values = calc_exp_values(prep_mixed_state()) ans = conn.test_preparation(prep_CNOT_control, exp_values, iterations=self.iterations) assert ans - def test_CNOT_target(self, network): + def test_CNOT_target(self): with SimulaQronConnection("Bob") as conn: # Test CNOT target exp_values = calc_exp_values(prep_mixed_state()) ans = conn.test_preparation(prep_CNOT_target, exp_values, iterations=self.iterations) assert ans - def test_CPHASE_control(self, network): + def test_CPHASE_control(self): with SimulaQronConnection("Bob") as conn: # Test CPHASE control exp_values = calc_exp_values(prep_mixed_state()) ans = conn.test_preparation(prep_CPHASE_control, exp_values, iterations=self.iterations) assert ans - def test_CPHASE_target(self, network): + def test_CPHASE_target(self): with SimulaQronConnection("Bob") as conn: # Test CPHASE target exp_values = calc_exp_values(prep_mixed_state()) @@ -231,7 +227,7 @@ def test_CPHASE_target(self, network): # Tests using multiple nodes - def test_EPRS(self, network): + def test_EPRS(self): apps = default_app_instance( [ ("Alice", EPR_Alice), @@ -242,7 +238,7 @@ def test_EPRS(self, network): # both sides MUST measure the same state assert int(results[0]["app_Alice"]) == int(results[0]["app_Bob"]) - def test_teleport(self, network): + def test_teleport(self): # To avoid stalling the simulation, the applications *need* to run # in parallel. For this reason, we use the "run_applications" method # which spawns a process for each node From 1c707175f6ffb7d7a3ba14e9f011699337605a12 Mon Sep 17 00:00:00 2001 From: StephanieWehner Date: Fri, 5 Dec 2025 09:17:56 +0100 Subject: [PATCH 163/277] Changed how SimulaQron does logging: we no longer use the logging from the NetQASM library (which results in logging to /dev/null and unclear how to adapt it) but simply write out own log files for the deamons to /tmp. This now allows us to properly debug the code and pinpoint where/when what happens --- examples/nativeMode/corrRNG/aliceTest.py | 8 +++- examples/nativeMode/corrRNG/bobTest.py | 6 ++- simulaqron/local/setup.py | 30 ++++++------- simulaqron/netqasm_backend/factory.py | 6 +-- simulaqron/network.py | 9 ++-- simulaqron/run/run.py | 4 +- simulaqron/sdk/connection.py | 4 +- simulaqron/sdk/socket.py | 4 +- simulaqron/sim_util.py | 4 +- simulaqron/simulaqron.py | 23 +++++++--- simulaqron/start/start_qnodeos.py | 40 +++++++++++------- simulaqron/start/start_vnode.py | 42 ++++++++++++++----- simulaqron/virtual_node/basics.py | 2 +- simulaqron/virtual_node/quantum.py | 2 +- .../virtual_node/stabilizer_simulator.py | 2 +- simulaqron/virtual_node/virtual.py | 8 ++-- tests/slow/merges/test_merges.py | 4 +- 17 files changed, 123 insertions(+), 75 deletions(-) diff --git a/examples/nativeMode/corrRNG/aliceTest.py b/examples/nativeMode/corrRNG/aliceTest.py index d4756415..a858a4fc 100644 --- a/examples/nativeMode/corrRNG/aliceTest.py +++ b/examples/nativeMode/corrRNG/aliceTest.py @@ -127,7 +127,7 @@ def main(): # Check if we should run a server (if this node is listed in classicalNet) if myName in classicalNet.hostDict: # Create the local classical server - logging.debug("LOCAL %s: Creating classical server.", myName) + logging.debug("APP %s: Creating classical server.", myName) myNode = localNode(virtualNet.hostDict[myName], classicalNet) else: myNode = None @@ -137,5 +137,9 @@ def main(): ################################################################################################## -logging.basicConfig(format="%(asctime)s:%(levelname)s:%(message)s", level=logging.DEBUG) +logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=logging.DEBUG, + force=True +) main() diff --git a/examples/nativeMode/corrRNG/bobTest.py b/examples/nativeMode/corrRNG/bobTest.py index 67e8a30a..48ecdbe9 100644 --- a/examples/nativeMode/corrRNG/bobTest.py +++ b/examples/nativeMode/corrRNG/bobTest.py @@ -148,5 +148,9 @@ def main(): ################################################################################################## -logging.basicConfig(format="%(asctime)s:%(levelname)s:%(message)s", level=logging.DEBUG) +logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=logging.DEBUG, + force=True +) main() diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index 611c98ee..56138f3c 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -27,10 +27,10 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import logging import time from typing import Callable -from netqasm.logging.glob import get_netqasm_logger from twisted.internet import error from twisted.internet.defer import DeferredList from twisted.internet.error import ReactorNotRunning @@ -39,21 +39,21 @@ from simulaqron.general.host_config import SocketsConfig from simulaqron.reactor import reactor -_logger = get_netqasm_logger("setup-local") +_logger = logging.getLogger("setup-local") ##################################################################################################### # # setup_local # -# Sets up the local classical comms server (if applicable), and connects to the local virtual node -# and other classical communication servers. +# Sets up the local classical application level comms server (if applicable), and connects to the local +# virtual node and other classical communication servers. def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsConfig, lNode: pb.Root, func: Callable, *args, **kwargs): """ - Sets up a local classical communication server (if desired according to the configuration file), + Sets up a local classical applicaiton level communication server (if desired according to the configuration file), a client connection to the local virtual node quantum backend and a client connections to all other classical communication servers @@ -81,14 +81,14 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon if myName in classicalNet.hostDict: try: nb = classicalNet.hostDict[myName] - _logger.debug("LOCAL %s: Starting local classical communication server (%s: %s, %d).", + _logger.debug("SETUP_LOCAL %s: Starting local classical communication server (%s: %s, %d).", myName, nb.name, nb.hostname, nb.port ) nb.root = lNode nb.factory = pb.PBServerFactory(nb.root) reactor.listenTCP(nb.port, nb.factory) except Exception as e: - _logger.error("LOCAL %s: Cannot start classical communication servers: %s", myName, e) + _logger.error("SETUP_LOCAL %s: Cannot start classical communication servers: %s", myName, e) return # Give the server some time to start up @@ -96,7 +96,7 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon # Connect to the local virtual node simulating the "local" qubits node = virtualNet.hostDict[myName] - _logger.debug("LOCAL %s: Connecting to local virtual node (%s: %s, %d).", myName, node.name, node.hostname, + _logger.debug("SETUP_LOCAL %s: Connecting to local virtual node (%s: %s, %d).", myName, node.name, node.hostname, node.port) factory = pb.PBClientFactory() reactor.connectTCP(node.hostname, node.port, factory) @@ -107,7 +107,7 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon for node in classicalNet.hostDict: nb = classicalNet.hostDict[node] if nb.name != myName: - _logger.debug("LOCAL %s: Making classical connection to %s (%s: %s, %d).", myName, nb.name, nb.name, + _logger.debug("SETUP_LOCAL %s: Making classical connection to %s (%s: %s, %d).", myName, nb.name, nb.name, nb.hostname, nb.port) nb.factory = pb.PBClientFactory() reactor.connectTCP(nb.hostname, nb.port, nb.factory) @@ -133,7 +133,7 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon def init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig, classicalNet: SocketsConfig, lNode: pb.Root, func: Callable, *args, **kwargs): - _logger.debug("LOCAL %s: All connections set up.", myName) + _logger.debug("SETUP_LOCAL %s: All connections set up.", myName) # Retrieve the connection to the local virtual node, if successful j = 0 @@ -142,7 +142,7 @@ def init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig, if lNode is not None: lNode.set_virtual_node(virtRoot) else: - _logger.error("LOCAL %s: Connection to virtual server failed!", myName) + _logger.error("SETUP_LOCAL %s: Connection to virtual server failed!", myName) reactor.stop() # Retrieve connections to the classical nodes @@ -152,9 +152,9 @@ def init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig, j = j + 1 if resList[j][0]: nb.root = resList[j][1] - _logger.debug("LOCAL %s: Connected node %s with %s", myName, nb.name, nb.root) + _logger.debug("SETUP_LOCAL %s: Connected node %s with %s", myName, nb.name, nb.root) else: - _logger.error("LOCAL %s: Connection to %s failed!", myName, nb.name) + _logger.error("SETUP_LOCAL %s: Connection to %s failed!", myName, nb.name) reactor.stop() # On the local virtual node, we still want to initialize a qubit register @@ -164,7 +164,7 @@ def init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig, def fill_register(obj, myName, lNode, virtRoot, classicalNet, func, *args, **kwargs): - _logger.debug("LOCAL %s: Created quantum register at virtual node.", myName) + _logger.debug("SETUP_LOCAL %s: Created quantum register at virtual node.", myName) qReg = obj # If we run a server, record the handle to the local virtual register @@ -179,7 +179,7 @@ def localError(reason): """ Error handling for the connection. """ - _logger.error("Critical error: %s", reason) + _logger.error("SETUP_LOCAL: Critical error: %s", reason) try: reactor.stop() except ReactorNotRunning: diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index a9a97419..77998f33 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -30,7 +30,7 @@ from netqasm.backend.messages import MessageHeader, ErrorCode, deserialize_host_msg, Message, \ InitNewAppMessage -from netqasm.logging.glob import get_netqasm_logger +import logging from twisted.internet.defer import DeferredLock, inlineCallbacks from twisted.internet.protocol import Factory, Protocol, connectionDone from twisted.internet.task import deferLater @@ -81,7 +81,7 @@ def __init__(self, factory: "NetQASMFactory"): # Convenience self.name = self.factory.name - self._logger = get_netqasm_logger(f"{self.__class__.__name__}({self.name})") + self._logger = logging.getLogger(f"{self.__class__.__name__}({self.name})") self._logger.debug("Initialized Protocol") def connectionMade(self): @@ -195,7 +195,7 @@ def __init__( # Lock governing access to the qubitList self._lock = DeferredLock() - self._logger = get_netqasm_logger(f"{self.__class__.__name__}({name})") + self._logger = logging.getLogger(f"{self.__class__.__name__}({name})") # Read in topology, if specified. topology=None means fully connected # topology diff --git a/simulaqron/network.py b/simulaqron/network.py index ceeefb9e..077b81e2 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -34,10 +34,11 @@ import networkx as nx from multiprocess.context import ForkProcess as Process -from netqasm.logging.glob import get_netqasm_logger, get_log_level +import logging from simulaqron.settings import network_config from simulaqron.settings.network_config import NodeConfig +from simulaqron.settings import simulaqron_settings from simulaqron.start import start_vnode, start_qnodeos # WARNING - this import *needs* to be after importing start_vnode and start_qnodeos # Otherwise the code that patches some netqasm internal definitions will not work correctly! @@ -66,7 +67,7 @@ def __init__(self, nodes: List[str], network_name: str = "default"): self.name = network_name self.processes: List[Process] = [] - self._logger = get_netqasm_logger(f"{self.__class__.__name__}({self.name})") + self._logger = logging.getLogger(f"{self.__class__.__name__}({self.name})") # Determine the nodes to start, using the in-memory network config self._nodes_to_start: List[NodeConfig] = [] @@ -110,10 +111,10 @@ def _setup_processes(self): """ for node in self._nodes_to_start: process_virtual = Process( - target=start_vnode, args=(node.name, self.name, get_log_level()), name=f"VirtNode {node.name}" + target=start_vnode, args=(node.name, self.name, simulaqron_settings.log_level), name=f"VirtNode {node.name}" ) process_qnodeos = Process( - target=start_qnodeos, args=(node.name, self.name, get_log_level()), name=f"QnodeOSNode {node.name}" + target=start_qnodeos, args=(node.name, self.name, simulaqron_settings.log_level), name=f"QnodeOSNode {node.name}" ) self.processes += [process_virtual, process_qnodeos] diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 1ee09440..a3f9bbff 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -11,7 +11,7 @@ from multiprocess.context import ForkContext as ProcessContext from multiprocess.pool import ApplyResult from multiprocess.sharedctypes import SynchronizedArray -from netqasm.logging.glob import get_netqasm_logger +import logging from netqasm.logging.output import (reset_struct_loggers, save_all_struct_loggers) from netqasm.runtime import env, process_logs @@ -28,7 +28,7 @@ from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings.simulaqron_config import SimBackend -logger = get_netqasm_logger() +logger = logging.getLogger() # TODO similar code to squidasm.run.run, make base-class and subclasses? diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 0755a008..70120306 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -11,7 +11,7 @@ ErrorCode, Message, APP_ID) from netqasm.lang.ir import GenericInstr from netqasm.lang.operand import Address, Register -from netqasm.logging.glob import get_netqasm_logger +import logging from netqasm.sdk import EPRSocket from netqasm.sdk.config import LogConfig from netqasm.sdk.connection import BaseNetQASMConnection @@ -24,7 +24,7 @@ get_node_id_from_net_config) from simulaqron.settings import network_config -logger = get_netqasm_logger("SimulaQronConnection") +logger = logging.getLogger("SimulaQronConnection") class SimulaQronConnection(BaseNetQASMConnection): diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index eb671659..98d1c103 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -3,7 +3,7 @@ from typing import Optional import dill -from netqasm.logging.glob import get_netqasm_logger +import logging from netqasm.sdk.classical_communication.message import StructuredMessage from netqasm.sdk.classical_communication.socket import Socket as _Socket @@ -33,7 +33,7 @@ def __init__( self._use_callbacks = use_callbacks self._network_name = network_name - self._logger = get_netqasm_logger(f"{self.__class__.__name__}(L: {app_name} <-> R: {remote_app_name})") + self._logger = logging.getLogger(f"{self.__class__.__name__}(L: {app_name} <-> R: {remote_app_name})") self._timeout = timeout # We define _app_socket as None as a default value, so the __del__ method # does not fail when the socket could not be connected correctly. diff --git a/simulaqron/sim_util.py b/simulaqron/sim_util.py index 269426d7..9b721153 100644 --- a/simulaqron/sim_util.py +++ b/simulaqron/sim_util.py @@ -1,10 +1,10 @@ import numpy as np -from netqasm.logging.glob import get_netqasm_logger +import logging from netqasm.sdk import Qubit from simulaqron.sdk import SimulaQronConnection -logger = get_netqasm_logger("sim_util") +logger = logging.getLogger("sim_util") def get_qubit_state(qubit: Qubit, reduced_dm: bool = True) -> np.ndarray: diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 7e5d3ee2..45f00071 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -1,6 +1,7 @@ import importlib.metadata as metadata import logging import time +import sys from pathlib import Path from typing import Optional, List @@ -26,23 +27,31 @@ class RunningSimulaQronDaemon(run.RunDaemon): """ SimulaQronDaemon class used to represent SimulaQron daemons that are already running. - This class is useful to stop the already-running daemons without needed to read all + This class is useful to 5stop the already-running daemons without needed to read all the required configurations. """ def __init__(self, pidfile: Path): assert pidfile is not None - super().__init__(pidfile=pidfile) - + super().__init__( + pidfile=pidfile, + ) class SimulaQronDaemon(run.RunDaemon): def __init__(self, pidfile: Path, name: str, nodes: List[str]): - super().__init__(pidfile=pidfile) + super().__init__( + pidfile=pidfile, + ) self.name = name self.nodes = nodes def run(self): """Starts all nodes defined in netsim's config directory.""" + + # Let's make sure we can record the output where it's accessible + sys.stdout = open('/tmp/simulaqron.out', 'w', buffering=1) + sys.stderr = open('/tmp/simulaqron.err', 'w', buffering=1) + network = Network(network_name=self.name, nodes=self.nodes) network.start() @@ -141,6 +150,7 @@ def stop(name: str): """Stops a network.""" assert name is not None pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" + logging.debug(f"Trying to open PIDfile") if not pidfile.exists(): logging.warning("Network with name %s is not running", name) return @@ -613,7 +623,8 @@ def get(network_name: str): if __name__ == "__main__": logging.basicConfig( - format="%(asctime)s:%(levelname)s:%(message)s", + logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(filename)s:%(lineno)d:%(message)s", level=simulaqron_settings.log_level, - ) + )) cli() diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 370d35ef..9263b61d 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -6,7 +6,7 @@ import time from timeit import default_timer as timer -from netqasm.logging.glob import get_netqasm_logger, set_log_level +import logging from twisted.internet.error import ConnectionRefusedError, CannotListenError from twisted.spread import pb @@ -16,7 +16,7 @@ from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings, network_config -logger = get_netqasm_logger("start_qnodeos") +logger = logging.getLogger("start_vnode") _RETRY_TIME = 0.1 _TIMEOUT = 10 @@ -24,7 +24,7 @@ def init_register(virt_root, my_name: str, node: NetQASMFactory): """Retrieves the relevant root objects to talk to such remote connections""" - logger.debug("LOCAL %s: Connection to local virtual node successful", my_name) + logger.debug("START_QNODEOS %s: Connection to local virtual node successful", my_name) # Set the virtual node node.set_virtual_node(virt_root) # Start listening to NetQASM messages @@ -39,7 +39,7 @@ def connect_to_virt_node(my_name: str, netqasm_factory: NetQASMFactory, virtual_ """ virtual_node = virtual_network.hostDict[my_name] logger.debug( - "LOCAL %s: Trying to connect to local virtual node at %s, %d.", + "START_QNODEOS %s: Trying to connect to local virtual node at %s, %d.", my_name, virtual_node.hostname, virtual_node.port ) factory = pb.PBClientFactory() @@ -64,7 +64,7 @@ def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactor reason.raiseException() except ConnectionRefusedError as err: # TODO - Implement checking of max number of connections - logger.debug("LOCAL %s: Could not connect to Virtual node (%s, %d), trying again...", my_name, + logger.debug("START_QNODEOS %s: Could not connect to Virtual node (%s, %d), trying again...", my_name, virtual_node_hostname, virtual_node_port, exc_info=err) reactor.callLater( simulaqron_settings.conn_retry_time, @@ -75,7 +75,7 @@ def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactor ) except Exception as e: logger.error( - "LOCAL %s: Critical error when connection to local virtual node: %s", + "START_QNODEOS %s: Critical error when connection to local virtual node: %s", my_name, e, ) @@ -88,7 +88,7 @@ def setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): while timer() - t_start < _TIMEOUT: try: logger.debug( - "LOCAL %s: Starting local QNodeOS server, port %d.", + "START_QNODEOS %s: Starting local QNodeOS server, port %d.", my_name, netqasm_factory.host.port ) my_host = netqasm_factory.host @@ -98,13 +98,13 @@ def setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): break except CannotListenError: logger.error( - "LOCAL %s: NetQASM server address (%d) is already in use, trying again.", + "START_QNODEOS: %s: NetQASM server address (%d) is already in use, trying again.", my_name, my_host.port ) time.sleep(_RETRY_TIME) except Exception as e: logger.error( - "LOCAL %s: Critical error when starting NetQASM server: %s", my_name, e + "START_QNODEOS %s: Critical error when starting NetQASM server: %s", my_name, e ) reactor.stop() else: @@ -124,12 +124,20 @@ def sigterm_handler(_signo, _stack_frame): def start_qnodeos(node_name: str, network_name: str = "default", log_level: str = "WARNING"): if simulaqron_settings.log_level == logging.DEBUG: global stdout_file - stdout_file = open(f"stdout-stderr-qnos-{node_name}-{os.getpid()}.out.txt", "w") + stdout_file = open(f"/tmp/simulaqron-stdout-stderr-qnos-{node_name}-{os.getpid()}.out.txt", "w") sys.stdout = stdout_file sys.stderr = stdout_file + + # Force configure root logger with a handler + logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=logging.DEBUG, + force=True, + stream=stdout_file # send logs to the same file + ) + """Start the indicated backend NetQASM Server""" - set_log_level(log_level) - logger.debug("Starting QNodeOS at %s", node_name) + logger.debug("START_QNODEOS: Starting QNodeOS at %s", node_name) signal.signal(signal.SIGTERM, sigterm_handler) signal.signal(signal.SIGINT, sigterm_handler) @@ -140,7 +148,7 @@ def start_qnodeos(node_name: str, network_name: str = "default", log_level: str # Check if we are in the host-dictionary if node_name in qnodeos_network.hostDict: node_host_info = qnodeos_network.hostDict[node_name] - logger.debug("Setting up QNodeOS protocol factory for %s", node_name) + logger.debug("START_QNODEOS: Setting up QNodeOS protocol factory for %s", node_name) netqasm_factory = NetQASMFactory( node_host_info, node_name, @@ -149,16 +157,16 @@ def start_qnodeos(node_name: str, network_name: str = "default", log_level: str network_name=network_name, ) else: - logger.error("LOCAL %s: Cannot start classical communication servers.", node_name) + logger.error("START_QNODEOS %s: Cannot start classical communication servers.", node_name) return # Connect to the local virtual node simulating the "local" qubits - logger.debug(f"Connect to virtual node {node_name}") + logger.debug(f"START_QNODEOS: Connect to virtual node {node_name}") connect_to_virt_node(node_name, netqasm_factory, virtual_network) # Run reactor reactor.run() - logger.debug(f"Ending QNodeOS at {node_name}") + logger.debug(f"START_QNODEOS: Ending QNodeOS at {node_name}") if __name__ == '__main__': diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 1ece8692..0e3ced38 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -27,25 +27,25 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + import logging import os import signal import sys from functools import partial -from netqasm.logging.glob import get_netqasm_logger, set_log_level from simulaqron.reactor import reactor from simulaqron.virtual_node.virtual import Backend from simulaqron.settings import simulaqron_settings -logger = get_netqasm_logger("start_vnode") +logger = logging.getLogger("start_vnode") stdout_file = None def sigterm_handler(name, _signo, _stack_frame): - print("Shutting down Node from signal %d." % _signo, flush=True) + print("START_VNODE: Shutting down Node from signal %d." % _signo, flush=True) global stdout_file stdout_file.flush() stdout_file.close() @@ -53,19 +53,39 @@ def sigterm_handler(name, _signo, _stack_frame): def start_vnode(name: str, network_name: str = "default", log_level: str = "WARNING"): - if simulaqron_settings.log_level == logging.DEBUG: - global stdout_file - stdout_file = open(f"stdout-stderr-vnode-{name}-{os.getpid()}.out.txt", "w") - sys.stdout = stdout_file - sys.stderr = stdout_file - set_log_level(log_level) + """ Start the execution of a virtual simulaqron node. This node will simulate all quantum aspects + of the node, and is then reachable via Twisted PB (Simulaqron Native Mode) or - when also starting QNPU - + the QNPU Server which translates NetQASM to native mode. + """ + + # We will have our logging output be written to a file in order to not distract from the app + # logging that the user will later see on the screen + stdout_file = open(f"/tmp/simulaqron-stdout-stderr-vnode-{name}-{os.getpid()}.out.txt", "w") + sys.stdout = stdout_file + sys.stderr = stdout_file + + # Force configure root logger with a handler, ensure our log output to this file + # will allow us to trace back exactly where it came from in the codebase + logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=simulaqron_settings.log_level, + force=True, + stream=stdout_file # send logs to the same file + ) + + # Set up the handlers: those define what we will do when the process is terminated (by killing it) signal.signal(signal.SIGTERM, partial(sigterm_handler, name)) signal.signal(signal.SIGINT, partial(sigterm_handler, name)) - logger.debug("Starting VIRTUAL NODE %s", name) + # Let's now test logging works by printing a message we are starting + logger.debug("START_VNODE: Starting VIRTUAL NODE %s", name) + + # Start the backend with the parameters configured in the simulaqron log file be = Backend(name, network_name=network_name) be.start(max_qubits=simulaqron_settings.max_qubits, max_registers=simulaqron_settings.max_registers) - logger.debug("Ending VIRTUAL NODE %s", name) + + # Print a message we have terminated the node. + logger.debug("START_VNODE: Ending VIRTUAL NODE %s", name) if __name__ == "__main__": diff --git a/simulaqron/virtual_node/basics.py b/simulaqron/virtual_node/basics.py index 06fe9d07..2a492744 100644 --- a/simulaqron/virtual_node/basics.py +++ b/simulaqron/virtual_node/basics.py @@ -80,7 +80,7 @@ def __init__(self, node: str, num: int, maxQubits: int = 10): @abc.abstractmethod def add_fresh_qubit(self) -> int: """ - Add a new qubit initialized in the \|0\> state. + Add a new qubit initialized in the |0> state. :return: The qubit number :rtype: int """ diff --git a/simulaqron/virtual_node/quantum.py b/simulaqron/virtual_node/quantum.py index 07c0ea11..e9b578fc 100644 --- a/simulaqron/virtual_node/quantum.py +++ b/simulaqron/virtual_node/quantum.py @@ -30,7 +30,7 @@ import time import numpy as np -from netqasm.logging.glob import get_netqasm_logger +import logging from twisted.internet.defer import DeferredLock from twisted.internet.defer import inlineCallbacks from twisted.internet.task import deferLater diff --git a/simulaqron/virtual_node/stabilizer_simulator.py b/simulaqron/virtual_node/stabilizer_simulator.py index e2cfc80b..7c555eb1 100644 --- a/simulaqron/virtual_node/stabilizer_simulator.py +++ b/simulaqron/virtual_node/stabilizer_simulator.py @@ -56,7 +56,7 @@ def activeQubits(self): def add_fresh_qubit(self) -> int: """ - Add a new qubit initialized in the \|0\> state. + Add a new qubit initialized in the |0> state. """ # Check if we are still allowed to add qubits if self.activeQubits >= self.maxQubits: diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index ece2a027..1400fefb 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -31,7 +31,7 @@ from collections import deque from typing import Tuple, List -from netqasm.logging.glob import get_netqasm_logger +import logging from twisted.internet.defer import inlineCallbacks, DeferredLock, Deferred, DeferredList from twisted.internet.error import ConnectionRefusedError, CannotListenError from twisted.internet.task import deferLater @@ -95,7 +95,7 @@ def __init__(self, name: str, network_name: str = "default"): Initialize. This will read the networks configuration and populate the name,hostname,port information with the information found in the configuration file for the given name. """ - self._logger = get_netqasm_logger(f"{self.__class__.__name__}({name})") + self._logger = logging.getLogger(f"{self.__class__.__name__}({name})") # Read the configuration file self.config = SocketsConfig(network_config, network_name=network_name, config_type="vnode") @@ -147,7 +147,7 @@ def __init__(self, ID: Host, config: SocketsConfig, maxQubits maximum number of qubits to use in the default engine (default 10) maxRegister maximum number of registers """ - self._logger = get_netqasm_logger(f"{self.__class__.__name__}({ID.name})") + self._logger = logging.getLogger(f"{self.__class__.__name__}({ID.name})") # Store our own host identifiers and configuration self.myID = ID @@ -1248,7 +1248,7 @@ def __init__(self, virtNode: Host, simNode: Host, simQubit: SimulatedQubit, num: simQubit reference to the underlying qubit object (may be remote) num number ID among the virtual qubits """ - self._logger = get_netqasm_logger(f"{self.__class__.__name__}({virtNode.name}, {num})") + self._logger = logging.getLogger(f"{self.__class__.__name__}({virtNode.name}, {num})") # Node where this qubit is virtually located self.virtNode = virtNode diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 662ecc1a..8edae8e9 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -12,7 +12,7 @@ from multiprocess.context import ForkProcess as Process from multiprocess.connection import Pipe -from netqasm.logging.glob import get_netqasm_logger +import logging from logging import DEBUG from simulaqron.general.host_config import SocketsConfig from simulaqron.local.setup import setup_local, assemble_qubit @@ -22,7 +22,7 @@ from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.reactor import reactor -_logger = get_netqasm_logger("test_merges") +_logger = logging.getLogger("test_merges") class localNode(pb.Root): From 68ad1c253c610048d9e1bc38d5e082e02af1c6dd Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 5 Dec 2025 11:06:22 +0100 Subject: [PATCH 164/277] Update nativeMode template file --- examples/nativeMode/template/nodeTest.py | 32 ++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/examples/nativeMode/template/nodeTest.py b/examples/nativeMode/template/nodeTest.py index 45e91352..6f99f7f5 100644 --- a/examples/nativeMode/template/nodeTest.py +++ b/examples/nativeMode/template/nodeTest.py @@ -32,10 +32,12 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config from twisted.spread import pb from twisted.internet import reactor +from simulaqron.settings.network_config import NodeConfigType + ### # @@ -130,15 +132,25 @@ def main(): # In this example, we are YOURNAME myName = "YOURNAME" - # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file - - # This file defines the nodes acting as servers in the classical communication network - classicalFile = "./classicalNet.cfg" - - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + # This file defines the nodes on the network. This file contains all the information + # about classical and virtual nodes sockets + network_config_file = "network_config.json" + + # To make use of the network config file, we need to use the "network_config" to + # load this file in memory + network_config.read_from_file(network_config_file) + + # Use the loaded network configuration to get the virtual quantum, as well as the classical network + virtualNet = SocketsConfig(network_config, config_type=NodeConfigType.VNODE) + classicalNet = SocketsConfig(network_config, config_type=NodeConfigType.APP) + + # By default, *all nodes* described in the network configuration will be loaded in the SocketsConfig + # object. With this information, SimulaQron will start all of those nodes, either as local classical + # or virtual nodes (depending on the specified configuration type). + # In some cases, this is not desired, and we want ot start *a subset* of these nodes. + # To do this, we can use the method "filter" from the SocketsConfig object to specify the nodes + # we want to keep (and hence, start) + classicalNet.filter(["Alice", "Bob"]) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network From f5da50dcaba2de0ff573d72275b0623fc844b4d2 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 5 Dec 2025 11:08:04 +0100 Subject: [PATCH 165/277] Rename method for loading configuration from files to harmonize terminology --- examples/nativeMode/teleport/aliceTest.py | 2 +- examples/nativeMode/teleport/bobTest.py | 2 +- simulaqron/settings/__init__.py | 4 +-- simulaqron/settings/network_config.py | 2 +- simulaqron/settings/simulaqron_config.py | 4 +-- simulaqron/simulaqron.py | 36 +++++++++---------- .../quick/settings/test_networks_settings.py | 2 +- .../settings/test_simulaqron_settings.py | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/examples/nativeMode/teleport/aliceTest.py b/examples/nativeMode/teleport/aliceTest.py index 467069be..360f899f 100644 --- a/examples/nativeMode/teleport/aliceTest.py +++ b/examples/nativeMode/teleport/aliceTest.py @@ -157,7 +157,7 @@ def main(): # classicalNet = SocketsConfig(classicalFile) # We load the local configuration files - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) network_config.read_from_file(LOCAL_NETWORK_SETTINGS) # Read configuration files for the virtual quantum, as well as the classical network diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index 7c9aa6d2..f49382b0 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -146,7 +146,7 @@ def main(): # classicalNet = SocketsConfig(classicalFile) # We load the local configuration files - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) network_config.read_from_file(LOCAL_NETWORK_SETTINGS) # Read configuration files for the virtual quantum, as well as the classical network diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index 6332c1c5..5a9c5a63 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -9,9 +9,9 @@ # Centralized way to store the config. It reads the local configuration # if exists, otherwise, it simply populates the in-memory configs object # with the default values -simulaqron_settings = SimulaqronConfig.load_from_known_sources() +simulaqron_settings = SimulaqronConfig.read_from_known_sources() # Centralized way to store the config of the network. It reads the local # configuration if exists, otherwise, it simply populates the in-memory # configs object with the default values -network_config = NetworkConfigBuilder.load_from_known_sources() +network_config = NetworkConfigBuilder.read_from_known_sources() diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index f933d4ee..04d911ff 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -460,7 +460,7 @@ def _deserialize_from_file(cls, file_path: Path) -> Self: return JSONSerializer.deserialize(cls, config_content) @classmethod - def load_from_known_sources(cls) -> Self: + def read_from_known_sources(cls) -> Self: cwd_networks_file = LOCAL_NETWORK_SETTINGS.resolve() home_networks_file = HOME_NETWORK_SETTINGS.resolve() diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index c7b1e8d1..38f584e0 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -80,7 +80,7 @@ def _create_home_settings_folder(cls): home_setting_folder = (Path.home() / ".simulaqron").resolve() home_setting_folder.mkdir(parents=True, exist_ok=True) - def load_from_file(self, file_path: Path | str): + def read_from_file(self, file_path: Path | str): if isinstance(file_path, str): file_path = Path(file_path).resolve() new_config = self._deserialize_from_file(file_path) @@ -97,7 +97,7 @@ def _deserialize_from_file(cls, file_path: Path) -> Self: return JSONSerializer.deserialize(cls, config_content) @classmethod - def load_from_known_sources(cls) -> Self: + def read_from_known_sources(cls) -> Self: cwd_settings_file = LOCAL_SIMULAQRON_SETTINGS.resolve() home_settings_file = HOME_SIMULAQRON_SETTINGS.resolve() diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 7e5d3ee2..551ec6a8 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -212,7 +212,7 @@ def sim_backend(value): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.sim_backend = value simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -230,7 +230,7 @@ def max_qubits(value): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.max_qubits = value simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -248,7 +248,7 @@ def max_registers(value): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.max_registers = value simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -266,7 +266,7 @@ def conn_retry_time(value): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.conn_retry_time = value simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -284,7 +284,7 @@ def recv_timeout(value): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.recv_timeout = value simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -302,7 +302,7 @@ def recv_retry_time(value): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.recv_retry_time = value simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -320,7 +320,7 @@ def log_level(value): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.log_level = value simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -338,7 +338,7 @@ def noisy_qubits(value): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) if value == "on": simulaqron_settings.noisy_qubits = True else: @@ -359,7 +359,7 @@ def t1(value): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.t1 = value simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -381,7 +381,7 @@ def get(): ) def sim_backend(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: print("Configuration from default configuration") @@ -394,7 +394,7 @@ def sim_backend(): ) def max_qubits(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: print("Configuration from default configuration") @@ -407,7 +407,7 @@ def max_qubits(): ) def max_registers(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: print("Configuration from default configuration") @@ -420,7 +420,7 @@ def max_registers(): ) def conn_retry_time(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: print("Configuration from default configuration") @@ -433,7 +433,7 @@ def conn_retry_time(): ) def recv_timeout(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: print("Configuration from default configuration") @@ -446,7 +446,7 @@ def recv_timeout(): ) def recv_retry_time(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: print("Configuration from default configuration") @@ -459,7 +459,7 @@ def recv_retry_time(): ) def log_level(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: print("Configuration from default configuration") @@ -472,7 +472,7 @@ def log_level(): ) def noisy_qubits(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: print("Configuration from default configuration") @@ -488,7 +488,7 @@ def noisy_qubits(): ) def t1(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.load_from_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: print("Configuration from default configuration") diff --git a/tests/quick/settings/test_networks_settings.py b/tests/quick/settings/test_networks_settings.py index 256e27ca..60a6a426 100644 --- a/tests/quick/settings/test_networks_settings.py +++ b/tests/quick/settings/test_networks_settings.py @@ -51,7 +51,7 @@ def clean_settings(self): @pytest.fixture def reset_net_cfg(self): - network_config.load_from_known_sources() + network_config.read_from_known_sources() @staticmethod def _check_node_config(node: NodeConfig) -> Tuple[bool, ...]: diff --git a/tests/quick/settings/test_simulaqron_settings.py b/tests/quick/settings/test_simulaqron_settings.py index 149b171a..37096d22 100644 --- a/tests/quick/settings/test_simulaqron_settings.py +++ b/tests/quick/settings/test_simulaqron_settings.py @@ -77,5 +77,5 @@ def test_create_default_settings(self, clean_settings): def test_load_non_existent_config_file(self): with pytest.raises(FileNotFoundError) as error: - simulaqron_settings.load_from_file("/non/existent/file") + simulaqron_settings.read_from_file("/non/existent/file") assert "No such file or directory: '/non/existent/file'" in str(error.value) From 43b53d2d7182adfda9959c9616a5853106e676ab Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 5 Dec 2025 11:45:16 +0100 Subject: [PATCH 166/277] Add documentation for the new configuration system --- simulaqron/general/host_config.py | 4 +- simulaqron/settings/__init__.py | 4 +- simulaqron/settings/_serialization.py | 10 ++-- simulaqron/settings/network_config.py | 41 +++++++++++++---- simulaqron/settings/simulaqron_config.py | 46 ++++++++++++++++++- simulaqron/simulaqron.py | 40 ++++++++-------- .../quick/settings/test_networks_settings.py | 4 +- tests/slow/sdk/test_single_qubit.py | 2 +- tests/slow/sdk/test_two_qubit.py | 2 +- 9 files changed, 110 insertions(+), 43 deletions(-) diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index 2fc6ed9a..a5dedc28 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -34,7 +34,7 @@ from twisted.spread import pb -from simulaqron.settings.network_config import NetworkConfigBuilder, NodeConfigType +from simulaqron.settings.network_config import NetworksConfiguration, NodeConfigType class Host(pb.Referenceable): @@ -66,7 +66,7 @@ def __init__(self, name: str, hostname: str, port: int): class SocketsConfig(pb.Referenceable): - def __init__(self, nets_config: NetworkConfigBuilder, network_name: str = "default", + def __init__(self, nets_config: NetworksConfiguration, network_name: str = "default", config_type: str | NodeConfigType = "vnode"): """ Initialize by reading in the configuration file. diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index 5a9c5a63..86b2a104 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -1,4 +1,4 @@ -from .network_config import (NetworkConfigBuilder, DEFAULT_SIMULAQRON_NETWORK_FILENAME, +from .network_config import (NetworksConfiguration, DEFAULT_SIMULAQRON_NETWORK_FILENAME, LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS) from .simulaqron_config import (SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME, LOCAL_SIMULAQRON_SETTINGS, HOME_SIMULAQRON_SETTINGS) @@ -14,4 +14,4 @@ # Centralized way to store the config of the network. It reads the local # configuration if exists, otherwise, it simply populates the in-memory # configs object with the default values -network_config = NetworkConfigBuilder.read_from_known_sources() +network_config = NetworksConfiguration.read_from_known_sources() diff --git a/simulaqron/settings/_serialization.py b/simulaqron/settings/_serialization.py index 12403ae3..823a7a95 100644 --- a/simulaqron/settings/_serialization.py +++ b/simulaqron/settings/_serialization.py @@ -5,7 +5,7 @@ from dataclasses_serialization.json import JSONSerializer from dataclasses_serialization.serializer_base import DeserializationError -from .network_config import NodeConfig, NetworkConfig, NetworkConfigBuilder +from .network_config import NodeConfig, NetworkConfig, NetworksConfiguration from ..settings import HOME_SIMULAQRON_SETTINGS from ..settings.simulaqron_config import SimulaqronConfig, SimBackend @@ -125,13 +125,13 @@ def network_config_deserializer(cls: Type[NetworkConfig], obj: Dict[str, Any]) - return net_cfg -@JSONSerializer.register_serializer(NetworkConfigBuilder) -def network_config_builder_serializer(obj: NetworkConfigBuilder) -> List[Dict[str, Any]]: +@JSONSerializer.register_serializer(NetworksConfiguration) +def network_config_builder_serializer(obj: NetworksConfiguration) -> List[Dict[str, Any]]: return [JSONSerializer.serialize(network) for network in obj.networks.values()] -@JSONSerializer.register_deserializer(NetworkConfigBuilder) -def network_config_builder_deserializer(cls: Type[NetworkConfigBuilder], obj: List[Dict]) -> NetworkConfigBuilder: +@JSONSerializer.register_deserializer(NetworksConfiguration) +def network_config_builder_deserializer(cls: Type[NetworksConfiguration], obj: List[Dict]) -> NetworksConfiguration: new_obj = cls() for raw_network in obj: network_spec: NetworkConfig = JSONSerializer.deserialize(NetworkConfig, raw_network) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 04d911ff..229546d6 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -73,7 +73,7 @@ def __eq__(self, other) -> bool: @dataclass class NetworkConfig(JSONSerializerMixin): """ - Used by NetworksConfigConstructor to keep track of the config of a single network. + Used by NetworksConfiguration to keep track of the config of a single network. """ name: str @@ -162,7 +162,7 @@ def __eq__(self, other) -> bool: @dataclass -class NetworkConfigBuilder(JSONSerializerMixin): +class NetworksConfiguration(JSONSerializerMixin): """ Used to construct the config file of networks. """ @@ -174,11 +174,14 @@ def using_default_network(self): """ Loads the default networks in the current networks configuration object. The default configuration contains a single network named "default", which contains 5 nodes named - "Alice", "Bob", "Charlie", "David" and "Eve". + "Alice", "Bob", "Charlie", "David" and "Eve". Each node contains configuration to run + in `localhost` with a unique port between 8000 and 9000. + + For the specific configuration, you can check the file `simulaqron/_default_config/default_network.json`. """ # We use the embedded default network here default_network_path = resources.files(simulaqron._default_config).joinpath("default_network.json") - new_builder = NetworkConfigBuilder() + new_builder = NetworksConfiguration() new_builder.read_from_file(Path(str(default_network_path))) self.networks = new_builder.networks self.used_sockets = new_builder.used_sockets @@ -373,6 +376,9 @@ def get_node_names(self, network_name: str = "default"): raise ValueError(f"{network_name} is not a network in this config") def remove_all_networks(self): + """ + Deletes all the in-memory networks from the configuration. + """ for network_name in self.network_names: self.remove_network(network_name) @@ -435,7 +441,7 @@ def _is_old_json_format(config_content: Dict[str, Any] | List[Dict[str, Any]]) - case dict(): return True case _: - raise ValueError("JSON network configi file does not have a valid format.") + raise ValueError("JSON network config file does not have a valid format.") @staticmethod def _correct_old_format(config_content: Dict[str, Any]) -> List[Dict[str, Any]]: @@ -453,14 +459,28 @@ def _correct_old_format(config_content: Dict[str, Any]) -> List[Dict[str, Any]]: def _deserialize_from_file(cls, file_path: Path) -> Self: with file_path.resolve().open("rt") as file: config_content = json.load(file) - if NetworkConfigBuilder._is_old_json_format(config_content): - config_content = NetworkConfigBuilder._correct_old_format(config_content) + if NetworksConfiguration._is_old_json_format(config_content): + config_content = NetworksConfiguration._correct_old_format(config_content) with file_path.open("wt") as file: json.dump(config_content, file, indent=4) return JSONSerializer.deserialize(cls, config_content) @classmethod def read_from_known_sources(cls) -> Self: + """ + Reads the network configuration from usual locations. + This method will try to load the network configuration files *in the following order* + from (1) the current folder (`./simulaqron_network.json`) and, (2) simulaqron settings + in the user's home folder (`~/.simulaqron/simulaqron_network.json`). + + If none of these files exists, this method will create a network configuration in + user's home folder (`~/.simulaqron/simulaqron_network.json`) containing the default + SimulaQron configuration. + + To check the default configuration, check the documentation of `using_default_network`. + See Also: + using_default_network() + """ cwd_networks_file = LOCAL_NETWORK_SETTINGS.resolve() home_networks_file = HOME_NETWORK_SETTINGS.resolve() @@ -491,6 +511,11 @@ def nodes(self) -> List[NodeConfig]: @property def network_names(self) -> List[str]: + """ + Gets the loaded network names. + Returns: + A list of strings with the network names. + """ return list(self.networks.keys()) def __getitem__(self, item: str) -> NetworkConfig: @@ -501,7 +526,7 @@ def __getitem__(self, item: str) -> NetworkConfig: # Helper functions def __eq__(self, other: Any) -> bool: - if not isinstance(other, NetworkConfigBuilder): + if not isinstance(other, NetworksConfiguration): return False nodes_eq = [current_node == other_node for current_node, other_node in zip(self.nodes, other.nodes)] return all(nodes_eq) diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 38f584e0..cd6a08d0 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -81,6 +81,12 @@ def _create_home_settings_folder(cls): home_setting_folder.mkdir(parents=True, exist_ok=True) def read_from_file(self, file_path: Path | str): + """ + Reads the SimulaQron configuration from the given file path. + Args: + file_path: Path, str + A `pathlib.Path` or `str` representing the file path to read the configurations from. + """ if isinstance(file_path, str): file_path = Path(file_path).resolve() new_config = self._deserialize_from_file(file_path) @@ -98,6 +104,20 @@ def _deserialize_from_file(cls, file_path: Path) -> Self: @classmethod def read_from_known_sources(cls) -> Self: + """ + Reads the SimulaQron configuration from usual locations. + This method will try to load the configuration files *in the following order* from (1) + the current folder (`./simulaqron_settings.json`) and, (2) simulaqron settings in the + user's home folder (`~/.simulaqron/simulaqron_settings.json`). + + If none of these files exists, this method will create a SimulaQron configuration in + user's home folder (`~/.simulaqron/simulaqron_settings.json`) containing the default + SimulaQron configuration. + + To check the default configuration, check the documentation of `default_settings`. + See Also: + default_settings() + """ cwd_settings_file = LOCAL_SIMULAQRON_SETTINGS.resolve() home_settings_file = HOME_SIMULAQRON_SETTINGS.resolve() @@ -113,10 +133,26 @@ def read_from_known_sources(cls) -> Self: # Ultimate case; we create a new config file in the ohme and load it new_default_config = cls() - new_default_config.save_to_file(home_settings_file) + new_default_config.write_to_file(home_settings_file) return new_default_config def default_settings(self): + """ + Resets the current SimulaQron configuration object to its default configuration set. + The default configuration is: + * max_qubits = 20 + * max_registers = 1000 + * conn_retry_time = 0.5 + * conn_max_retries = 10 + * recv_timeout = 100 + * recv_retry_time = 0.1 + * recv_max_retries = 10 + * log_level = logging.WARNING + * sim_backend = SimBackend.STABILIZER + * noisy_qubits = False + * max_app_waiting_time = -1.0 # In seconds, negative means unlimited waiting + * t1: float = 1.0 + """ default_config = SimulaqronConfig() cls_fields = fields(self.__class__) @@ -124,7 +160,13 @@ def default_settings(self): new_val = getattr(default_config, field.name) setattr(self, field.name, new_val) - def save_to_file(self, path: PathLike): + def write_to_file(self, path: PathLike): + """ + Writes the current in-memory configuration (`simulaqron_config`) to the given file path. + Args: + path: + A `PathLike` object (even a string) representing the path to write the configuration to. + """ file_path = Path(str(path)).resolve() # Create all the parent folder if they not exists diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 551ec6a8..2293053e 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -176,7 +176,7 @@ def reset(force: bool): if entry.exists(): entry.unlink() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) else: print("Aborting!") @@ -197,7 +197,7 @@ def set(): ) def default(): simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @set.command( @@ -211,10 +211,10 @@ def sim_backend(value): if not LOCAL_SIMULAQRON_SETTINGS.exists(): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.sim_backend = value - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -229,10 +229,10 @@ def max_qubits(value): if not LOCAL_SIMULAQRON_SETTINGS.exists(): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.max_qubits = value - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -247,10 +247,10 @@ def max_registers(value): if not LOCAL_SIMULAQRON_SETTINGS.exists(): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.max_registers = value - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -265,10 +265,10 @@ def conn_retry_time(value): if not LOCAL_SIMULAQRON_SETTINGS.exists(): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.conn_retry_time = value - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -283,10 +283,10 @@ def recv_timeout(value): if not LOCAL_SIMULAQRON_SETTINGS.exists(): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.recv_timeout = value - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -301,10 +301,10 @@ def recv_retry_time(value): if not LOCAL_SIMULAQRON_SETTINGS.exists(): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.recv_retry_time = value - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -319,10 +319,10 @@ def log_level(value): if not LOCAL_SIMULAQRON_SETTINGS.exists(): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.log_level = value - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -337,13 +337,13 @@ def noisy_qubits(value): if not LOCAL_SIMULAQRON_SETTINGS.exists(): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) if value == "on": simulaqron_settings.noisy_qubits = True else: simulaqron_settings.noisy_qubits = False - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -358,10 +358,10 @@ def t1(value): if not LOCAL_SIMULAQRON_SETTINGS.exists(): LOCAL_SIMULAQRON_SETTINGS.touch() simulaqron_settings.default_settings() - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) simulaqron_settings.t1 = value - simulaqron_settings.save_to_file(LOCAL_SIMULAQRON_SETTINGS) + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") diff --git a/tests/quick/settings/test_networks_settings.py b/tests/quick/settings/test_networks_settings.py index 60a6a426..27f4981a 100644 --- a/tests/quick/settings/test_networks_settings.py +++ b/tests/quick/settings/test_networks_settings.py @@ -9,7 +9,7 @@ from dataclasses_serialization.json import JSONSerializer import simulaqron._default_config -from simulaqron.settings import network_config, NetworkConfigBuilder +from simulaqron.settings import network_config, NetworksConfiguration from simulaqron.settings.network_config import DEFAULT_SIMULAQRON_NETWORK_FILENAME, NodeConfig cwd_network = (Path.cwd() / DEFAULT_SIMULAQRON_NETWORK_FILENAME).resolve() @@ -64,7 +64,7 @@ def test_create_default_settings(self, clean_settings): default_network_path = Path(str(resources.files(simulaqron._default_config).joinpath("default_network.json"))) expected_net_cfg_dict = json.loads(default_network_path.read_text()) - expected_net_cfg = JSONSerializer.deserialize(NetworkConfigBuilder, expected_net_cfg_dict) + expected_net_cfg = JSONSerializer.deserialize(NetworksConfiguration, expected_net_cfg_dict) assert network_config == expected_net_cfg diff --git a/tests/slow/sdk/test_single_qubit.py b/tests/slow/sdk/test_single_qubit.py index dbbca229..c6aa4e81 100644 --- a/tests/slow/sdk/test_single_qubit.py +++ b/tests/slow/sdk/test_single_qubit.py @@ -40,7 +40,7 @@ from simulaqron.sdk.connection import SimulaQronConnection from simulaqron.run.run import reset from simulaqron.general import SimUnsupportedError -from simulaqron.settings.network_config import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworksConfiguration def calc_exp_values(q): diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index 3c837943..c510ebb6 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -41,7 +41,7 @@ from simulaqron.run.run import run_applications from simulaqron.run.run import reset from simulaqron.settings import simulaqron_settings, network_config -from simulaqron.settings.network_config import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworksConfiguration from simulaqron.settings.simulaqron_config import SimBackend From 0cd905ff55b3a6a4386d74a7078210006ba43eb0 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 5 Dec 2025 11:58:28 +0100 Subject: [PATCH 167/277] Rename cli module --- pyproject.toml | 2 +- simulaqron/{simulaqron.py => cli.py} | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) rename simulaqron/{simulaqron.py => cli.py} (98%) diff --git a/pyproject.toml b/pyproject.toml index 33abdc75..3e794895 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ include = ["simulaqron"] exclude = ["simulaqron/config"] [project.scripts] -simulaqron = "simulaqron.simulaqron:cli" +simulaqron = "simulaqron.cli::cli_entry_point" [project.urls] Homepage = "https://github.com/SoftwareQuTech/SimulaQron" \ No newline at end of file diff --git a/simulaqron/simulaqron.py b/simulaqron/cli.py similarity index 98% rename from simulaqron/simulaqron.py rename to simulaqron/cli.py index 2293053e..94d9cff8 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/cli.py @@ -51,7 +51,7 @@ def run(self): @click.group(context_settings=CONTEXT_SETTINGS) -def cli(): +def cli_entry_point(): """Command line interface for interacting with SimulaQron.""" pass @@ -60,7 +60,7 @@ def cli(): # version # ########### -@cli.command() +@cli_entry_point.command() def version(): """ Prints the version of simulqron. @@ -72,7 +72,7 @@ def version(): # start command # ################# -@cli.command() +@cli_entry_point.command() @click.option( "--network-config-file", help=f"Use the given network config file. Defaults to the file named " # noqa: E131 @@ -130,7 +130,7 @@ def start(name: str, nrnodes: int, nodes: str, network_config_file: Path): # stop command # ############### -@cli.command() +@cli_entry_point.command() @click.option( "--name", help="Stop the network with then a given name (default: default)", @@ -152,7 +152,7 @@ def stop(name: str): # reset command # ################# -@cli.command() +@cli_entry_point.command() @click.option( "-f", "--force", @@ -185,7 +185,7 @@ def reset(force: bool): # set command # ############### -@cli.group( +@cli_entry_point.group( help="Change a simulaqron setting" ) def set(): @@ -370,7 +370,7 @@ def t1(value): ############### -@cli.group() +@cli_entry_point.group() def get(): """Get a setting""" pass @@ -500,7 +500,7 @@ def t1(): # node command # ############### -@cli.group() +@cli_entry_point.group() def nodes(): """ Manage the nodes in the simulated network. @@ -616,4 +616,4 @@ def get(network_name: str): format="%(asctime)s:%(levelname)s:%(message)s", level=simulaqron_settings.log_level, ) - cli() + cli_entry_point() From 5b94bcd2631b0bbc1ee87ecd231c629e0da3807b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 5 Dec 2025 12:42:09 +0100 Subject: [PATCH 168/277] Some code refacto in the cli code --- simulaqron/cli.py | 168 +++++++++++++++++++--------------------------- 1 file changed, 68 insertions(+), 100 deletions(-) diff --git a/simulaqron/cli.py b/simulaqron/cli.py index 94d9cff8..bc350133 100644 --- a/simulaqron/cli.py +++ b/simulaqron/cli.py @@ -50,6 +50,50 @@ def run(self): time.sleep(0.1) +def _path_exists(path: Path) -> bool: + if not path.exists() or path.is_dir(): + return False + return True + + +def _load_local_settings_or_default(): + if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): + simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") + else: + print("Configuration from default configuration") + simulaqron_settings.default_settings() + + +def _create_local_settings_if_needed_and_load(): + if not LOCAL_SIMULAQRON_SETTINGS.exists(): + LOCAL_SIMULAQRON_SETTINGS.touch() + simulaqron_settings.default_settings() + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) + # At this point we are sure that the local settings exists, so we can + # load them using the function from above. + _load_local_settings_or_default() + + +def _load_local_network_or_default(): + if LOCAL_NETWORK_SETTINGS.exists() and LOCAL_NETWORK_SETTINGS.is_file(): + network_config.read_from_file(LOCAL_NETWORK_SETTINGS) + print(f"Network configuration loaded from file: '{LOCAL_NETWORK_SETTINGS}'") + else: + print("Configuration from default configuration") + network_config.default_settings() + + +def _create_local_networks_if_needed_and_load(): + if not LOCAL_NETWORK_SETTINGS.exists(): + LOCAL_NETWORK_SETTINGS.touch() + network_config.default_settings() + network_config.write_to_file(LOCAL_NETWORK_SETTINGS) + # At this point we are sure that the local settings exists, so we can + # load them using the function from above. + _load_local_network_or_default() + + @click.group(context_settings=CONTEXT_SETTINGS) def cli_entry_point(): """Command line interface for interacting with SimulaQron.""" @@ -103,6 +147,10 @@ def version(): ) def start(name: str, nrnodes: int, nodes: str, network_config_file: Path): """Starts a network with the given parameters or from config files.""" + if not _path_exists(network_config): + print(f"The given network config file '{network_config}' does not exist or it is a folder.\n" + "Please check the path given to the --network-config-file option.") + return network_config.read_from_file(network_config_file) pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): @@ -208,11 +256,7 @@ def default(): type=click.Choice([b.value for b in SimBackend]) ) def sim_backend(value): - if not LOCAL_SIMULAQRON_SETTINGS.exists(): - LOCAL_SIMULAQRON_SETTINGS.touch() - simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + _create_local_settings_if_needed_and_load() simulaqron_settings.sim_backend = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -226,11 +270,7 @@ def sim_backend(value): type=int ) def max_qubits(value): - if not LOCAL_SIMULAQRON_SETTINGS.exists(): - LOCAL_SIMULAQRON_SETTINGS.touch() - simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + _create_local_settings_if_needed_and_load() simulaqron_settings.max_qubits = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -244,11 +284,7 @@ def max_qubits(value): type=int ) def max_registers(value): - if not LOCAL_SIMULAQRON_SETTINGS.exists(): - LOCAL_SIMULAQRON_SETTINGS.touch() - simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + _create_local_settings_if_needed_and_load() simulaqron_settings.max_registers = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -262,11 +298,7 @@ def max_registers(value): type=float ) def conn_retry_time(value): - if not LOCAL_SIMULAQRON_SETTINGS.exists(): - LOCAL_SIMULAQRON_SETTINGS.touch() - simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + _create_local_settings_if_needed_and_load() simulaqron_settings.conn_retry_time = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -280,11 +312,7 @@ def conn_retry_time(value): type=float ) def recv_timeout(value): - if not LOCAL_SIMULAQRON_SETTINGS.exists(): - LOCAL_SIMULAQRON_SETTINGS.touch() - simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + _create_local_settings_if_needed_and_load() simulaqron_settings.recv_timeout = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -298,11 +326,7 @@ def recv_timeout(value): type=float ) def recv_retry_time(value): - if not LOCAL_SIMULAQRON_SETTINGS.exists(): - LOCAL_SIMULAQRON_SETTINGS.touch() - simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + _create_local_settings_if_needed_and_load() simulaqron_settings.recv_retry_time = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -316,11 +340,7 @@ def recv_retry_time(value): type=int ) def log_level(value): - if not LOCAL_SIMULAQRON_SETTINGS.exists(): - LOCAL_SIMULAQRON_SETTINGS.touch() - simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + _create_local_settings_if_needed_and_load() simulaqron_settings.log_level = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -334,11 +354,7 @@ def log_level(value): type=click.Choice(["on", "off"]) ) def noisy_qubits(value): - if not LOCAL_SIMULAQRON_SETTINGS.exists(): - LOCAL_SIMULAQRON_SETTINGS.touch() - simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + _create_local_settings_if_needed_and_load() if value == "on": simulaqron_settings.noisy_qubits = True else: @@ -355,11 +371,7 @@ def noisy_qubits(value): type=float ) def t1(value): - if not LOCAL_SIMULAQRON_SETTINGS.exists(): - LOCAL_SIMULAQRON_SETTINGS.touch() - simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) + _create_local_settings_if_needed_and_load() simulaqron_settings.t1 = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @@ -380,12 +392,7 @@ def get(): help="The backend to use (stabilizer, projectq, qutip).", ) def sim_backend(): - if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") - else: - print("Configuration from default configuration") - simulaqron_settings.default_settings() + _load_local_settings_or_default() print(simulaqron_settings.sim_backend) @@ -393,12 +400,7 @@ def sim_backend(): help="Max virt-qubits per node and max sim-qubits per register." ) def max_qubits(): - if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") - else: - print("Configuration from default configuration") - simulaqron_settings.default_settings() + _load_local_settings_or_default() print(simulaqron_settings.max_qubits) @@ -406,12 +408,7 @@ def max_qubits(): help="How many registers a node can hold." ) def max_registers(): - if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") - else: - print("Configuration from default configuration") - simulaqron_settings.default_settings() + _load_local_settings_or_default() print(simulaqron_settings.max_registers) @@ -419,12 +416,7 @@ def max_registers(): help="If setup fails, how long to wait until a retry." ) def conn_retry_time(): - if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") - else: - print("Configuration from default configuration") - simulaqron_settings.default_settings() + _load_local_settings_or_default() print(simulaqron_settings.conn_retry_time) @@ -432,12 +424,7 @@ def conn_retry_time(): help="When receiving a qubit or EPR pair, how long to wait until raising a timeout." ) def recv_timeout(): - if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") - else: - print("Configuration from default configuration") - simulaqron_settings.default_settings() + _load_local_settings_or_default() print(simulaqron_settings.recv_timeout) @@ -445,12 +432,7 @@ def recv_timeout(): help="When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received." ) def recv_retry_time(): - if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") - else: - print("Configuration from default configuration") - simulaqron_settings.default_settings() + _load_local_settings_or_default() print(simulaqron_settings.recv_retry_time) @@ -458,12 +440,7 @@ def recv_retry_time(): help="Log level for both backend and frontend." ) def log_level(): - if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") - else: - print("Configuration from default configuration") - simulaqron_settings.default_settings() + _load_local_settings_or_default() print(simulaqron_settings.log_level) @@ -471,12 +448,7 @@ def log_level(): help="Whether qubits should be noisy (on/off)" ) def noisy_qubits(): - if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") - else: - print("Configuration from default configuration") - simulaqron_settings.default_settings() + _load_local_settings_or_default() if simulaqron_settings.noisy_qubits: print("on") else: @@ -487,12 +459,7 @@ def noisy_qubits(): help="The effective T1 to be used for noisy qubits" ) def t1(): - if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): - simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") - else: - print("Configuration from default configuration") - simulaqron_settings.default_settings() + _load_local_settings_or_default() print(simulaqron_settings.t1) @@ -541,10 +508,11 @@ def add(name: str, network_name: str, hostname: str, app_port: int, qnodeos_port HOSTNAME: The host name of the node, e.g. localhost or 192.168.0.1 """ + _create_local_networks_if_needed_and_load() + network_config.read_from_file(LOCAL_NETWORK_SETTINGS) if neighbors is not None: neighbors = neighbors.split(',') neighbors = [neighbor.strip() for neighbor in neighbors] - network_config.read_from_file(LOCAL_NETWORK_SETTINGS) network_config.add_node(node_name=name, network_name=network_name, app_hostname=hostname, qnodeos_hostname=hostname, vnode_hostname=hostname, app_port=app_port, qnodeos_port=qnodeos_port, vnode_port=vnode_port, From c5398e361f4227b192c02a04ef80139f7609a9ad Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 5 Dec 2025 12:56:09 +0100 Subject: [PATCH 169/277] Add option to specify simulaqron setting when starting the network from cli --- docs/CQC.rst | 5 ++++- simulaqron/cli.py | 21 ++++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/CQC.rst b/docs/CQC.rst index 98b4cfd3..346d85f3 100644 --- a/docs/CQC.rst +++ b/docs/CQC.rst @@ -1,6 +1,9 @@ -The CQC interface +DEPRECATED - The CQC interface ================= +WARNING - The CQC interface ahas been deprecated, and it is no longer present in this release of SimulaQron. +Please refer to the NetQASM interface documentation to create quantum applications. + SimulaQron can be access from any programming language supporting network connections. Instructions to the quantum hardware simulation can be sent via the CQC interface described `here `_. A `C `_, `Python `_, and `Rust `_ Library for programming SimulaQron using the CQC Interface are provided. If you are new to SimulaQron, programming via the Python CQC is the easiest way to get started. diff --git a/simulaqron/cli.py b/simulaqron/cli.py index bc350133..badddef3 100644 --- a/simulaqron/cli.py +++ b/simulaqron/cli.py @@ -124,6 +124,13 @@ def version(): type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), default=LOCAL_NETWORK_SETTINGS ) +@click.option( + "--simulaqron-config-file", + help=f"Use the given simulaqron config file. Defaults to the file named " # noqa: E131 + f"'{DEFAULT_SIMULAQRON_NETWORK_FILENAME}' on the current directory.", # noqa: E131 + type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), + default=LOCAL_SIMULAQRON_SETTINGS +) @click.option( "--name", help="Give the network a name to be able to start multiple (default: 'default')", @@ -145,12 +152,20 @@ def version(): type=str, default="", ) -def start(name: str, nrnodes: int, nodes: str, network_config_file: Path): +def start(name: str, nrnodes: int, nodes: str, simulaqron_config_file: Path, network_config_file: Path): """Starts a network with the given parameters or from config files.""" - if not _path_exists(network_config): - print(f"The given network config file '{network_config}' does not exist or it is a folder.\n" + # Checks the simulaqron config + if not _path_exists(simulaqron_config_file): + print(f"The given simulaqron config file '{simulaqron_config_file}' does not exist or it is a folder.\n" + "Please check the path given to the --simulaqron-config-file option.") + return + # Checks the network config + if not _path_exists(network_config_file): + print(f"The given network config file '{network_config_file}' does not exist or it is a folder.\n" "Please check the path given to the --network-config-file option.") return + # Load SimulaQron and network configs + simulaqron_settings.read_from_file(simulaqron_config_file) network_config.read_from_file(network_config_file) pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): From 827f34b0026fbf606f429828820cd14cb084d1ad Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 5 Dec 2025 13:35:48 +0100 Subject: [PATCH 170/277] Check network and node names before starting simulaqron network from CLI --- simulaqron/cli.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/simulaqron/cli.py b/simulaqron/cli.py index badddef3..2083e730 100644 --- a/simulaqron/cli.py +++ b/simulaqron/cli.py @@ -167,12 +167,25 @@ def start(name: str, nrnodes: int, nodes: str, simulaqron_config_file: Path, net # Load SimulaQron and network configs simulaqron_settings.read_from_file(simulaqron_config_file) network_config.read_from_file(network_config_file) + # Check that the network name exists in the network configuration + if not name in network_config.networks: + print(f"The network '{name}' was not found in the network configuration file '{network_config_file}'.\n" + f"Please check the name you passed in the --name option and try again.") + return + # Check that the nodes to start exist in the given network + nodes = nodes.split(",") + for node_to_start in nodes: + if not node_to_start in network_config.networks[name]: + print(f"The node '{node_to_start}' was not found in the network named '{name} 'specified in" + f" the configuration file '{network_config_file}'.\nPlease check the list of names you " + f"passed in the --nodes option and try again.") + return + # Check that there is no other network with the same name running pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): logging.warning("Network with name %s is already running", name) logging.warning("The pidfile for this network is located at %s", pidfile) return - nodes = nodes.split(",") if len(nodes) <= 0: print("WARNING - The list of nodes to start is empty. If you specified the --nrnodes option " "this can be normal. Please check your invocation line if needed.") From 6692ed89d8bf9a10b8439b2e8d743adb0261994d Mon Sep 17 00:00:00 2001 From: StephanieWehner Date: Sat, 6 Dec 2025 14:56:20 +0100 Subject: [PATCH 171/277] Fixed async model mismatch between NetQASM and SimulaQron, make tests now all passed NetQASM uses Python generators, SimulaQron uses Twisted deferreds. handle_netqasm_message was fixed to bridge the two. This caused previously various weird errors including going into some infinite loop in which it kept allocating qubits --- simulaqron/netqasm_backend/executioner.py | 2 +- simulaqron/netqasm_backend/qnodeos.py | 63 ++++++++++++++++++++++- simulaqron/start/start_qnodeos.py | 4 +- simulaqron/virtual_node/quantum.py | 4 +- simulaqron/virtual_node/virtual.py | 1 + 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index e55e9c83..074a3c08 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -152,7 +152,7 @@ def cmd_new(self, physical_address): q = VirtualQubitRef(q_id, int(time.time()), virt) self.factory.qubitList[q_id] = q self._logger.info("Requested new physical qubit %d)", q_id) - + print(f"DEBUG: Added qubit {q_id} to qubitList", flush=True) # ADD THIS finally: self.factory._lock.release() diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index f1a6bb62..77f3c6ff 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -1,3 +1,7 @@ +import logging +import os +import sys + from typing import Optional, Dict, Callable, Generator, Any, List, Type from netqasm.backend.executor import Executor @@ -10,7 +14,7 @@ from simulaqron.netqasm_backend.executioner import VanillaSimulaQronExecutioner from simulaqron.sdk.connection import (NewMessageType, GetQubitStateMessage, ReturnQubitStateMessage) - +from simulaqron.settings import simulaqron_settings class SubroutineHandler(QNodeController): def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = None, # noqa: F821 @@ -18,6 +22,20 @@ def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = Non super().__init__(factory.name, instr_log_dir=instr_log_dir, flavour=flavour) self.factory = factory + self._logger = logging.getLogger("QnodeController") + # logging that the user will later see on the screen + #stdout_file = open(f"/tmp/simulaqron-stdout-stderr-netqasmQ-{os.getpid()}.out.txt", "w") + #sys.stdout = stdout_file + #sys.stderr = stdout_file + + # Force configure root logger with a handler, ensure our log output to this file + # will allow us to trace back exactly where it came from in the codebase + logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=simulaqron_settings.log_level, + force=True, + stream=sys.stdout # send logs to the same file + ) # Give a way for the executioner to return messages self._executor.add_return_msg_func(self._return_msg) @@ -35,11 +53,52 @@ def protocol(self, protocol: Protocol): @inlineCallbacks def handle_netqasm_message(self, msg_id: int, msg: Message): - yield from super().handle_netqasm_message( + """ + Handle incoming NetQASM messages by bridging two async models. + + NetQASM's executor uses Python generators (yield from) while SimulaQron + uses Twisted deferreds (@inlineCallbacks). This method bridges them by: + 1. Running the parent's generator manually + 2. Detecting whether each yielded item is a Twisted Deferred or a nested generator + 3. For Deferreds: yielding to Twisted's reactor to await completion + 4. For nested generators: consuming them fully and capturing their return value + + Without this bridge, nested generator return values (like physical_address + from _instr_qalloc) would be lost, causing None to propagate through the system. + This is also what caused the tests to fail, and probably other random weird things. + """ + print(f"DEBUG handle_netqasm_message: msg_id={msg_id}", flush=True) + gen = super().handle_netqasm_message( msg_id=msg_id, msg=msg, ) + # The following is a bug fix to properly wait for twisted deferreds + + try: + result = None + iteration = 0 + while True: + iteration = iteration + 1 + item = gen.send(result) + if hasattr(item, 'addCallback'): # Deferred + result = yield item + elif hasattr(item, '__next__'): # Nested generator - consume it + nested_result = None + try: + while True: + nested_item = item.send(nested_result) + if hasattr(nested_item, 'addCallback'): + nested_result = yield nested_item + else: + nested_result = None + except StopIteration as e: + result = e.value # Get the return value from the generator + else: + result = None + except StopIteration: + pass + def _handle_get_qubit_state(self, get_quibit_state_msg: GetQubitStateMessage) -> Generator[Any, None, None]: assert isinstance(self._executor, VanillaSimulaQronExecutioner) casted_executor: VanillaSimulaQronExecutioner = self._executor diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 9263b61d..6629f5b0 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -16,7 +16,7 @@ from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings, network_config -logger = logging.getLogger("start_vnode") +logger = logging.getLogger("start_qnodeos") _RETRY_TIME = 0.1 _TIMEOUT = 10 @@ -131,7 +131,7 @@ def start_qnodeos(node_name: str, network_name: str = "default", log_level: str # Force configure root logger with a handler logging.basicConfig( format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", - level=logging.DEBUG, + level=simulaqron_settings.log_level, force=True, stream=stdout_file # send logs to the same file ) diff --git a/simulaqron/virtual_node/quantum.py b/simulaqron/virtual_node/quantum.py index e9b578fc..68ddf1d1 100644 --- a/simulaqron/virtual_node/quantum.py +++ b/simulaqron/virtual_node/quantum.py @@ -80,9 +80,7 @@ def __init__(self, node, register, simNum, num=0): self.T1 = settings.simulaqron_settings.t1 self.last_accessed = time.time() - self._logger = get_netqasm_logger( - f"{self.__class__.__name__}(node={node.name}, sim_num={simNum})" - ) + self._logger = logging.getLogger() @inlineCallbacks def lock(self): diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 1400fefb..fb8a35f5 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -446,6 +446,7 @@ def remote_new_qubit(self, ignore_max_qubits=False): yield self._get_global_lock() try: + print(f"DEBUG new_qubit: len(virtQubits)={len(self.virtQubits)}, maxQubits={self.maxQubits}", flush=True) if (len(self.virtQubits) >= self.maxQubits) and (not ignore_max_qubits): self._logger.error("Maximum number of virtual qubits reached.") raise NoQubitError("Max virtual qubits reached") From 145ef740a2b58ee672332f5f6da1c079310ed5dd Mon Sep 17 00:00:00 2001 From: StephanieWehner Date: Sat, 6 Dec 2025 16:19:09 +0100 Subject: [PATCH 172/277] 3 things were fixed: - The teleport example now no longer uses run_application which makes no sense when running on multiple machines - The NetQASM server was fixed to allow only one application connection per node per time. - We cleanup once an app disconnects. The above change made this cleanup quite simple, as the NetQASM addition to simulaqron was not designed to support multiple apps per node. This is a bug fix which allows the simulaqron backend (virtual node, netqasm node) to keep running while the user applications can be executed multiple times. --- examples/distributed/teleport/README.txt | 63 +++++++++++++++++++ .../distributed/teleport/network-alice.json | 40 ------------ .../distributed/teleport/network-bob.json | 40 ------------ examples/distributed/teleport/run.sh | 14 +++++ .../teleport/simulaqron_network.json | 22 +++++++ .../distributed/teleport/teleport-alice.py | 38 +++-------- examples/distributed/teleport/teleport-bob.py | 37 +++-------- examples/distributed/teleport/terminate.sh | 14 +++++ simulaqron/netqasm_backend/factory.py | 53 +++++++++++++++- simulaqron/netqasm_backend/qnodeos.py | 6 +- simulaqron/virtual_node/virtual.py | 9 +++ 11 files changed, 191 insertions(+), 145 deletions(-) create mode 100644 examples/distributed/teleport/README.txt delete mode 100644 examples/distributed/teleport/network-alice.json delete mode 100644 examples/distributed/teleport/network-bob.json create mode 100755 examples/distributed/teleport/run.sh create mode 100644 examples/distributed/teleport/simulaqron_network.json create mode 100755 examples/distributed/teleport/terminate.sh diff --git a/examples/distributed/teleport/README.txt b/examples/distributed/teleport/README.txt new file mode 100644 index 00000000..b765bcb7 --- /dev/null +++ b/examples/distributed/teleport/README.txt @@ -0,0 +1,63 @@ + +Distributed version of the teleport example, using simple NetQASM code both for Alice and Bob. + +How to run this example: + +1. First of all, make sure you are not already running existing simulaqron programs. You can run + +simulaqron stop +sh terminate.sh + +which should get rid of all things running for the teleport example itself. If you want to be a bit +more radical and confident you are not running other things to preserve you can run: + +simulaqron stop +pkill -9 python + +This will kill ALL python processes run by you so beware. + +If you have a debugging enabled, you may also wish to wipe old log files by running + +rm /tmp/simulaqron* + + +Now you should have a clear slate! + +2. Now you can run this example: + +First, we want to start the simulaqron virtual node backend and the NetQASM frontend your apps will their NetQASM subroutines to. + + +** Single Machine +If you are running everything on the same machine, first we start the simulaqron backend by typing + +simulaqron start --node Alice,Bob + +This needs to be done only once. Now type + +sh run.sh + +There is no need to restart the simulaqron backend again if you want to re-run your example. + +** Multiple machines + +If you are starting on two different machines run: + +simulaqron start --node Alice +simulaqron start --node Bob + +on the machines you will use as Alice and Bob respectively. Again this needs to be run only once. + +Now you can run: +on Bob: +python teleport-bob.py + +on Alice: +python teleport-alice.py + +The code assumes you start Bob before starting Alice. Using your knowledge of network programming +from our ping pong example - do you have an idea to make this more robust? + + + + diff --git a/examples/distributed/teleport/network-alice.json b/examples/distributed/teleport/network-alice.json deleted file mode 100644 index b64c44ff..00000000 --- a/examples/distributed/teleport/network-alice.json +++ /dev/null @@ -1,40 +0,0 @@ -[ - { - "name": "default", - "nodes": [ - { - "Alice": { - "app_socket": [ - "192.168.0.160", - 8001 - ], - "qnodeos_socket": [ - "192.168.0.160", - 8005 - ], - "vnode_socket": [ - "192.168.0.160", - 8020 - ] - } - }, - { - "Bob": { - "app_socket": [ - "192.168.20.249", - 8032 - ], - "qnodeos_socket": [ - "192.168.20.249", - 8037 - ], - "vnode_socket": [ - "192.168.20.249", - 8050 - ] - } - } - ], - "topology": null - } -] \ No newline at end of file diff --git a/examples/distributed/teleport/network-bob.json b/examples/distributed/teleport/network-bob.json deleted file mode 100644 index cd40c73a..00000000 --- a/examples/distributed/teleport/network-bob.json +++ /dev/null @@ -1,40 +0,0 @@ -[ - { - "name": "default", - "nodes": [ - { - "Alice": { - "app_socket": [ - "localhost", - 8001 - ], - "qnodeos_socket": [ - "localhost", - 8005 - ], - "vnode_socket": [ - "localhost", - 8020 - ] - } - }, - { - "Bob": { - "app_socket": [ - "localhost", - 8032 - ], - "qnodeos_socket": [ - "localhost", - 8037 - ], - "vnode_socket": [ - "localhost", - 8050 - ] - } - } - ], - "topology": null - } -] \ No newline at end of file diff --git a/examples/distributed/teleport/run.sh b/examples/distributed/teleport/run.sh new file mode 100755 index 00000000..4362ae53 --- /dev/null +++ b/examples/distributed/teleport/run.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# Check if SimulaQron is already running +if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + simulaqron start --nodes=Alice,Bob --network-config-file ./networkConfig.json +fi + +python3 teleport-bob.py & +python3 teleport-alice.py + + + + + diff --git a/examples/distributed/teleport/simulaqron_network.json b/examples/distributed/teleport/simulaqron_network.json new file mode 100644 index 00000000..33e6c79b --- /dev/null +++ b/examples/distributed/teleport/simulaqron_network.json @@ -0,0 +1,22 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + } + ], + "topology": null + } +] \ No newline at end of file diff --git a/examples/distributed/teleport/teleport-alice.py b/examples/distributed/teleport/teleport-alice.py index 6166e392..a0522b52 100644 --- a/examples/distributed/teleport/teleport-alice.py +++ b/examples/distributed/teleport/teleport-alice.py @@ -1,51 +1,29 @@ -from pathlib import Path - from netqasm.runtime.settings import set_simulator -from netqasm.runtime.application import default_app_instance - -from simulaqron.run.run import run_applications - set_simulator("simulaqron") -from netqasm.sdk.external import NetQASMConnection # noqa: E402 -from netqasm.sdk import Qubit, EPRSocket # noqa: E402 +from simulaqron.settings import simulaqron_settings +simulaqron_settings.network_config_file = "./networkConfig.json" +from netqasm.sdk.external import NetQASMConnection +from netqasm.sdk import Qubit, EPRSocket def run_alice(): - epr_socket: EPRSocket = EPRSocket("Bob") + epr_socket = EPRSocket("Bob") with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: # Create a qubit q = Qubit(alice) q.H() - # Create entanglement epr = epr_socket.create_keep()[0] - # Teleport q.cnot(epr) q.H() m1 = q.measure() m2 = epr.measure() - return m1, m2 + return int(m1), int(m2) if __name__ == "__main__": - apps = default_app_instance( - [ - ("Alice", run_alice) - ] - ) - network_cfg_path = Path(__file__).parent / "network-alice.json" - raw_results = run_applications( - apps, use_app_config=False, enable_logging=False, network_cfg=network_cfg_path.resolve() - ) - - results = {} - - for name, raw_result in raw_results[0].items(): - if isinstance(raw_result, tuple): - results[name] = tuple(int(result) for result in raw_result) - else: - results[name] = int(raw_result) + results = run_alice() + print(f"Alice measurements: m1={results[0]}, m2={results[1]}") - print(results) diff --git a/examples/distributed/teleport/teleport-bob.py b/examples/distributed/teleport/teleport-bob.py index 08b17d14..338f7e08 100644 --- a/examples/distributed/teleport/teleport-bob.py +++ b/examples/distributed/teleport/teleport-bob.py @@ -1,41 +1,22 @@ -from pathlib import Path - from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") -from netqasm.runtime.application import default_app_instance - -from simulaqron.run.run import run_applications +from simulaqron.settings import simulaqron_settings +simulaqron_settings.network_config_file = "./networkConfig.json" -set_simulator("simulaqron") -from netqasm.sdk.external import NetQASMConnection # noqa: E402 -from netqasm.sdk import EPRSocket # noqa: E402 +from netqasm.sdk.external import NetQASMConnection +from netqasm.sdk import EPRSocket def run_bob(): - epr_socket: EPRSocket = EPRSocket("Alice") + epr_socket = EPRSocket("Alice") with NetQASMConnection("Bob", epr_sockets=[epr_socket]): entangled_qubit = epr_socket.recv_keep()[0] meas = entangled_qubit.measure() - return meas + return int(meas) if __name__ == "__main__": - apps = default_app_instance( - [ - ("Bob", run_bob) - ] - ) - network_cfg_path = Path(__file__).parent / "network-alice.json" - raw_results = run_applications( - apps, use_app_config=False, enable_logging=False, network_cfg=network_cfg_path.resolve() - ) - - results = {} - - for name, raw_result in raw_results[0].items(): - if isinstance(raw_result, tuple): - results[name] = tuple(int(result) for result in raw_result) - else: - results[name] = int(raw_result) + result = run_bob() + print(f"Bob measurement: {result}") - print(results) diff --git a/examples/distributed/teleport/terminate.sh b/examples/distributed/teleport/terminate.sh new file mode 100755 index 00000000..550b9d70 --- /dev/null +++ b/examples/distributed/teleport/terminate.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "teleport" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +simulaqron stop + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 +fi + diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 77998f33..68176123 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -85,10 +85,51 @@ def __init__(self, factory: "NetQASMFactory"): self._logger.debug("Initialized Protocol") def connectionMade(self): + self._logger.info("Connection made") pass def connectionLost(self, reason=connectionDone): - pass + self._logger.info(f"Connection lost: {reason}") + self.factory._active_protocol = None + self._cleanup_all() + + def _cleanup_all(self): + """Clean up all state - Note that we allow only ONE connection + to one NetQASM node server at a time, making the below safe. + + If we were to ever decide to allow multiple, the below is too radical + and decidedly unsafe. + """ + self._logger.info("Cleaning up all state") + + # Clear qubit list + self.factory.qubitList.clear() + + # Clear protocol class state + NetQASMProtocol._next_q_id.clear() + NetQASMProtocol._next_ent_id.clear() + + # Clear executioner class state + from simulaqron.netqasm_backend.executioner import VanillaSimulaQronExecutioner + VanillaSimulaQronExecutioner._next_create_id.clear() + + # Clear executioner instance state + executor = self.messageHandler._executor + if hasattr(executor, '_network_stack'): + executor._network_stack._sockets.clear() + if hasattr(executor, '_epr_create_requests'): + executor._epr_create_requests.clear() + if hasattr(executor, '_epr_recv_requests'): + executor._epr_recv_requests.clear() + + # Reset shared memory + try: + from netqasm.sdk.shared_memory import SharedMemoryManager + SharedMemoryManager.reset_memories() + except Exception as e: + self._logger.debug(f"Could not reset shared memory: {e}") + + self._logger.info("Cleanup complete") def dataReceived(self, data): """ @@ -201,6 +242,12 @@ def __init__( # topology self.topology = network_config[network_name].topology + # Track active connection - we will allow only one at a time + # as the code for the netqasm backend was not designed for multiple + # even though the virtual Node backend of SimulaQron would allow + # that + self._active_protocol = None + def stop(self): yield call_method(self.virtRoot, "stop_vnode") reactor.stop() @@ -209,7 +256,9 @@ def buildProtocol(self, addr): """ Return an instance of NetQASMProtocol when a connection is made. """ - return NetQASMProtocol(self) + protocol = NetQASMProtocol(self) + self._active_protocol = protocol + return protocol def set_virtual_node(self, virtRoot): """ diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 77f3c6ff..93384ae2 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -23,10 +23,6 @@ def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = Non self.factory = factory self._logger = logging.getLogger("QnodeController") - # logging that the user will later see on the screen - #stdout_file = open(f"/tmp/simulaqron-stdout-stderr-netqasmQ-{os.getpid()}.out.txt", "w") - #sys.stdout = stdout_file - #sys.stderr = stdout_file # Force configure root logger with a handler, ensure our log output to this file # will allow us to trace back exactly where it came from in the codebase @@ -34,7 +30,7 @@ def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = Non format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", level=simulaqron_settings.log_level, force=True, - stream=sys.stdout # send logs to the same file + stream=sys.stdout # send logs to the standard output, we set this earlier to be in /tmp ) # Give a way for the executioner to return messages diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index fb8a35f5..6f0a611c 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -314,6 +314,12 @@ def _q_num_to_obj(self, num): def remote_isLocked(self): return self._lock.locked + def remote_clear_recv_queues(self): + """Clear pending receive queues when an app disconnects""" + self.qubit_recv.clear() + self.qubit_recv_epr.clear() + self._logger.debug("Cleared receive queues") + @inlineCallbacks def _get_global_lock(self): self._logger.debug("GETTING LOCK") @@ -1786,3 +1792,6 @@ def __init__(self, fromName, toName, from_epr_socket_id, to_epr_socket_id, new_v self.to_epr_socket_id = to_epr_socket_id self.virt_num = new_virt_num self.rawEntInfo = rawEntInfo + + + From 62a645a88143b4f04f6e336e0d7da7525bc91526 Mon Sep 17 00:00:00 2001 From: StephanieWehner Date: Sun, 7 Dec 2025 23:06:25 +0100 Subject: [PATCH 173/277] Fixed config file issues in which one could not pass a file using simulaqron start ... -network-config-file FILE In doing so, we also demand the correct network config being passes to start_vnode etc instead of relying from unclear reads later on Some tests have been adapted since Network from network.py now takes another argument --- .../teleport/simulaqron_network.json | 8 +- .../distributed/teleport/teleport-alice.py | 2 +- examples/distributed/teleport/teleport-bob.py | 2 +- examples/nativeMode/corrRNG/run.sh | 2 +- simulaqron/network.py | 16 +++- simulaqron/run/run.py | 26 ++++--- simulaqron/settings/__init__.py | 3 +- simulaqron/settings/network_config.py | 73 ++++++++++++++----- simulaqron/simulaqron.py | 58 +++++++++++++-- simulaqron/start/start_qnodeos.py | 20 ++++- simulaqron/start/start_vnode.py | 16 +++- tests/quick/general/test_classical_sockets.py | 4 +- tests/quick/netqasm/test_free_qubit.py | 12 +-- tests/quick/netqasm/test_get_qubit_state.py | 12 +-- tests/quick/network/test_network.py | 12 +-- .../test_restricted_topology.py | 6 +- tests/slow/sdk/test_single_qubit.py | 2 +- tests/slow/sdk/test_two_qubit.py | 6 +- 18 files changed, 204 insertions(+), 76 deletions(-) diff --git a/examples/distributed/teleport/simulaqron_network.json b/examples/distributed/teleport/simulaqron_network.json index 33e6c79b..bdd299cf 100644 --- a/examples/distributed/teleport/simulaqron_network.json +++ b/examples/distributed/teleport/simulaqron_network.json @@ -11,12 +11,12 @@ }, { "Bob": { - "app_socket": ["localhost", 8831], - "qnodeos_socket": ["localhost", 8832], - "vnode_socket": ["localhost", 8833] + "app_socket": ["localhost", 9831], + "qnodeos_socket": ["localhost", 9832], + "vnode_socket": ["localhost", 9833] } } ], "topology": null } -] \ No newline at end of file +] diff --git a/examples/distributed/teleport/teleport-alice.py b/examples/distributed/teleport/teleport-alice.py index a0522b52..6cd07cd6 100644 --- a/examples/distributed/teleport/teleport-alice.py +++ b/examples/distributed/teleport/teleport-alice.py @@ -2,7 +2,7 @@ set_simulator("simulaqron") from simulaqron.settings import simulaqron_settings -simulaqron_settings.network_config_file = "./networkConfig.json" +simulaqron_settings.network_config_file = "./simulaqron_settings.json" from netqasm.sdk.external import NetQASMConnection from netqasm.sdk import Qubit, EPRSocket diff --git a/examples/distributed/teleport/teleport-bob.py b/examples/distributed/teleport/teleport-bob.py index 338f7e08..dbc630ca 100644 --- a/examples/distributed/teleport/teleport-bob.py +++ b/examples/distributed/teleport/teleport-bob.py @@ -2,7 +2,7 @@ set_simulator("simulaqron") from simulaqron.settings import simulaqron_settings -simulaqron_settings.network_config_file = "./networkConfig.json" +simulaqron_settings.network_config_file = "./simulaqron_settings.json" from netqasm.sdk.external import NetQASMConnection from netqasm.sdk import EPRSocket diff --git a/examples/nativeMode/corrRNG/run.sh b/examples/nativeMode/corrRNG/run.sh index 0d5b537e..2d65e764 100755 --- a/examples/nativeMode/corrRNG/run.sh +++ b/examples/nativeMode/corrRNG/run.sh @@ -2,7 +2,7 @@ # Check if SimulaQron is already running if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then - simulaqron start --nodes=Alice,Bob --force + simulaqron start --nodes=Alice,Bob --network-config-file classicalNet.json fi python3 bobTest.py & diff --git a/simulaqron/network.py b/simulaqron/network.py index 077b81e2..6cf6f591 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -35,6 +35,7 @@ import networkx as nx from multiprocess.context import ForkProcess as Process import logging +from pathlib import Path from simulaqron.settings import network_config from simulaqron.settings.network_config import NodeConfig @@ -52,7 +53,7 @@ class Network: - def __init__(self, nodes: List[str], network_name: str = "default"): + def __init__(self, nodes: List[str], network_config_file: Path, network_name: str = "default"): """ Used to spin up a simulated network. This class uses the network configuration loaded in the global network_config object and @@ -60,9 +61,14 @@ def __init__(self, nodes: List[str], network_name: str = "default"): :param network_name: str The name of network to start. Defaults to "default". + :param network_config_file: Path + Path to network config file (required). :param nodes: list of str A list of strings with the node names to start. """ + + self._network_config_file = network_config_file + self._running = False self.name = network_name @@ -111,10 +117,14 @@ def _setup_processes(self): """ for node in self._nodes_to_start: process_virtual = Process( - target=start_vnode, args=(node.name, self.name, simulaqron_settings.log_level), name=f"VirtNode {node.name}" + target=start_vnode, + args=(node.name, self._network_config_file, self.name, simulaqron_settings.log_level), + name=f"VirtNode {node.name}" ) process_qnodeos = Process( - target=start_qnodeos, args=(node.name, self.name, simulaqron_settings.log_level), name=f"QnodeOSNode {node.name}" + target=start_qnodeos, + args=(node.name, self._network_config_file, self.name, simulaqron_settings.log_level), + name=f"QnodeOSNode {node.name}" ) self.processes += [process_virtual, process_qnodeos] diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index a3f9bbff..9f1c2dce 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -26,6 +26,7 @@ from simulaqron.network import Network from simulaqron.sdk import SimulaQronConnection from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings import get_default_network_config_file from simulaqron.settings.simulaqron_config import SimBackend logger = logging.getLogger() @@ -183,19 +184,22 @@ def run_applications( app_instance.logging_cfg.comm_log_dir = timed_log_dir results: List[Dict[str, Any]] = [] - if isinstance(network_cfg, str) or isinstance(network_cfg, PathLike): - net_cfg = str(network_cfg) - network_config.read_from_file(net_cfg) - elif isinstance(network_cfg, Path): - net_cfg = str(network_cfg.resolve()) - network_config.read_from_file(net_cfg) - else: - # If no network config file was given, we keep with the default-loaded (pwd, or home) - pass - for _ in range(num_rounds): - network = Network(network_name="default", nodes=network_config.get_node_names("default")) + # Read the network config + if network_cfg is None: + network_cfg = get_default_network_config_file() + + network_cfg = Path(network_cfg).resolve() + network_config.read_from_file(network_cfg) + network_config.read_from_file(network_cfg) + + for _ in range(num_rounds): + network = Network( + nodes=network_config.get_node_names("default"), + network_config_file=network_cfg, + network_name="default", + ) # Start the processes that support the simulator: QNodeOS + VirtualNode network.start() diff --git a/simulaqron/settings/__init__.py b/simulaqron/settings/__init__.py index 6332c1c5..65c2a795 100644 --- a/simulaqron/settings/__init__.py +++ b/simulaqron/settings/__init__.py @@ -1,5 +1,6 @@ from .network_config import (NetworkConfigBuilder, DEFAULT_SIMULAQRON_NETWORK_FILENAME, - LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS) + LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS, + get_default_network_config_file) from .simulaqron_config import (SimulaqronConfig, DEFAULT_SIMULAQRON_SETTINGS_FILENAME, LOCAL_SIMULAQRON_SETTINGS, HOME_SIMULAQRON_SETTINGS) from ._serialization import init_serialization diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 4fd80556..928c2557 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -169,11 +169,21 @@ class NetworkConfigBuilder(JSONSerializerMixin): networks: Dict[str, NetworkConfig] = field(default_factory=dict) used_sockets: List[Tuple[str, int]] = field(default_factory=list) + + def using_default_network(self): + """ + Load the embedded default network configuration. + + Used for test isolation - always loads the embedded default + regardless of local config files. + """ + # We use the embedded default network here - default_network_path = resources.files(simulaqron._default_config).joinpath("default_network.json") + default_network_path = get_default_network_config_file(use_embedded=True) + new_builder = NetworkConfigBuilder() - new_builder.read_from_file(Path(str(default_network_path))) + new_builder.read_from_file(default_network_path) self.networks = new_builder.networks self.used_sockets = new_builder.used_sockets @@ -400,6 +410,7 @@ def read_from_file(self, file_path: PathLike | str): :param file_path: None or str If a file_path was specified upon __init__ this will be used if file_path is None. """ + if file_path is None: raise ValueError("No path specified to read the network configuration") @@ -429,23 +440,16 @@ def _deserialize_from_file(cls, file_path: Path) -> Self: @classmethod def load_from_known_sources(cls) -> Self: - cwd_networks_file = LOCAL_NETWORK_SETTINGS.resolve() - home_networks_file = HOME_NETWORK_SETTINGS.resolve() - - files_to_load = [cwd_networks_file, home_networks_file] - - for file in files_to_load: - try: - if file.exists() and file.is_file(): - return cls._deserialize_from_file(file) - except json.JSONDecodeError: - # Nothing to do; try next one - pass - - # Ultimate case; we create a new config file in the home and load it - default_net_cfg_path = Path(str(resources.files(simulaqron._default_config).joinpath("default_network.json"))) - shutil.copyfile(default_net_cfg_path, home_networks_file) - return cls._deserialize_from_file(home_networks_file) + """ + Load config from the default config file. + + Uses :func:`get_default_network_config_file` to resolve the path. + + :return: Loaded network configuration. + :rtype: NetworkConfigBuilder + """ + config_file = get_default_network_config_file() + return cls._deserialize_from_file(config_file) # Helper properties and pythonic accessors @property @@ -515,3 +519,34 @@ def _check_socket_is_free(port: int) -> bool: except socket.error: return False return True + +########### +# + +def get_default_network_config_file(use_embedded: bool = False) -> Path: + """ + Get the network config file path to use. + + :param use_embedded: If True, always use the embedded default (for tests). + If False, uses priority: LOCAL > HOME > embedded. + :type use_embedded: bool + :return: Path to the network config file. + :rtype: Path + """ + + # We will use an embedded default which is used in testing + if use_embedded: + return Path(str(resources.files(simulaqron._default_config).joinpath("default_network.json"))) + + # Implements using the local directory setting as a priority + if LOCAL_NETWORK_SETTINGS.exists(): + return LOCAL_NETWORK_SETTINGS + if HOME_NETWORK_SETTINGS.exists(): + return HOME_NETWORK_SETTINGS + + # Create default in HOME (matches load_from_known_sources behavior) + # XXX I have mixed feelings we should do this, but I leave it for now + default_net_cfg_path = Path(str(resources.files(simulaqron._default_config).joinpath("default_network.json"))) + HOME_NETWORK_SETTINGS.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(default_net_cfg_path, HOME_NETWORK_SETTINGS) + return HOME_NETWORK_SETTINGS diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index 45f00071..f124ae07 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -11,7 +11,7 @@ from simulaqron.network import Network from simulaqron.settings import LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS -from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings import simulaqron_settings, get_default_network_config_file, network_config from simulaqron.settings.network_config import NodeConfig, DEFAULT_SIMULAQRON_NETWORK_FILENAME from simulaqron.settings.simulaqron_config import SimBackend @@ -38,12 +38,41 @@ def __init__(self, pidfile: Path): ) class SimulaQronDaemon(run.RunDaemon): - def __init__(self, pidfile: Path, name: str, nodes: List[str]): + """ + Daemon process that runs a SimulaQron network in the background. + + This daemon spawns virtual nodes and QNodeOS servers for each node + in the network configuration. It runs until explicitly stopped. + + Attributes + ---------- + name : str + Name of the network (e.g., 'default'). + nodes : List[str] + List of node names to start (e.g., ['Alice', 'Bob']). + network_config_file : Path + Path to the network configuration JSON file. + """ + def __init__(self, pidfile: Path, name: str, nodes: List[str], network_config_file: Path): + """ + Initialize the SimulaQron daemon. + + :param pidfile: Path to the PID file used to track the daemon process. + :type pidfile: Path + :param name: Name of the network (e.g., 'default'). + :type name: str + :param nodes: List of node names to start (e.g., ['Alice', 'Bob']). + :type nodes: List[str] + :param network_config_file: Path to the network configuration file. + :type network_config_file: Path + + """ super().__init__( pidfile=pidfile, ) self.name = name self.nodes = nodes + self.network_config_file = network_config_file def run(self): """Starts all nodes defined in netsim's config directory.""" @@ -52,7 +81,15 @@ def run(self): sys.stdout = open('/tmp/simulaqron.out', 'w', buffering=1) sys.stderr = open('/tmp/simulaqron.err', 'w', buffering=1) - network = Network(network_name=self.name, nodes=self.nodes) + # Let's read the config file we should be working from + network_config.read_from_file(self.network_config_file) + + # Start the network to be simulated on this node + network = Network( + nodes=self.nodes, + network_config_file=self.network_config_file, + network_name=self.name, + ) network.start() while True: @@ -87,7 +124,7 @@ def version(): help=f"Use the given network config file. Defaults to the file named " # noqa: E131 f"'{DEFAULT_SIMULAQRON_NETWORK_FILENAME}' on the current directory.", # noqa: E131 type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), - default=LOCAL_NETWORK_SETTINGS + default=None ) @click.option( "--name", @@ -112,7 +149,13 @@ def version(): ) def start(name: str, nrnodes: int, nodes: str, network_config_file: Path): """Starts a network with the given parameters or from config files.""" - network_config.read_from_file(network_config_file) + + # Read the network configuration from the indicated file + if network_config_file is None: + network_config_file = get_default_network_config_file() + elif not network_config_file.exists(): + raise click.BadParameter(f"File '{network_config_file}' does not exist.") + pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): logging.warning("Network with name %s is already running", name) @@ -124,7 +167,10 @@ def start(name: str, nrnodes: int, nodes: str, network_config_file: Path): "this can be normal. Please check your invocation line if needed.") if nrnodes > 0 and len(nodes) < nrnodes: nodes += [f"Node{i}" for i in range(nrnodes - len(nodes))] - d = SimulaQronDaemon(pidfile=pidfile, name=name, nodes=nodes) + + # Let's start the simulaqron daemon. We will pass the config file so it will be available + # in the child process and load the same config + d = SimulaQronDaemon(pidfile=pidfile, name=name, nodes=nodes, network_config_file=network_config_file) try: d.start() except SystemExit as e: diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 6629f5b0..4553ee3e 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -9,6 +9,7 @@ import logging from twisted.internet.error import ConnectionRefusedError, CannotListenError from twisted.spread import pb +from pathlib import Path from simulaqron.reactor import reactor from simulaqron.netqasm_backend.factory import NetQASMFactory @@ -121,7 +122,24 @@ def sigterm_handler(_signo, _stack_frame): reactor.stop() -def start_qnodeos(node_name: str, network_name: str = "default", log_level: str = "WARNING"): +def start_qnodeos(node_name: str, network_config_file: Path, network_name: str = "default", log_level: str = "WARNING"): + """ + Start the QNPU that accepts NetQASM subroutines, and sends them as instructions to the SimulaQron virtual node + backend over twisted PB (Native Mode SimulaQron). + + :param name: Name of the node (e.g., 'Alice'). + :type name: str + :param network_config_file: Path to network config file. + :type network_config_file: Path + :param network_name: Name of the network (e.g., 'default'). + :type network_name: str + :param log_level: Logging level (e.g., 'DEBUG', 'INFO', 'WARNING'). + :type log_level: str + """ + + # Let's ensure we read the config file + network_config.read_from_file(network_config_file) + if simulaqron_settings.log_level == logging.DEBUG: global stdout_file stdout_file = open(f"/tmp/simulaqron-stdout-stderr-qnos-{node_name}-{os.getpid()}.out.txt", "w") diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 0e3ced38..44ffa896 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -33,11 +33,13 @@ import signal import sys from functools import partial +from pathlib import Path from simulaqron.reactor import reactor from simulaqron.virtual_node.virtual import Backend from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config logger = logging.getLogger("start_vnode") @@ -52,12 +54,24 @@ def sigterm_handler(name, _signo, _stack_frame): reactor.stop() -def start_vnode(name: str, network_name: str = "default", log_level: str = "WARNING"): +def start_vnode(name: str, network_config_file: Path, network_name: str = "default", log_level: str = "WARNING"): """ Start the execution of a virtual simulaqron node. This node will simulate all quantum aspects of the node, and is then reachable via Twisted PB (Simulaqron Native Mode) or - when also starting QNPU - the QNPU Server which translates NetQASM to native mode. + + :param name: Name of the node (e.g., 'Alice'). + :type name: str + :param network_config_file: Path to network config file. + :type network_config_file: Path + :param network_name: Name of the network (e.g., 'default'). + :type network_name: str + :param log_level: Logging level (e.g., 'DEBUG', 'INFO', 'WARNING'). + :type log_level: str """ + # Let's ensure we have read the config file. This relies on the right one being passed from network.py + network_config.read_from_file(network_config_file) + # We will have our logging output be written to a file in order to not distract from the app # logging that the user will later see on the screen stdout_file = open(f"/tmp/simulaqron-stdout-stderr-vnode-{name}-{os.getpid()}.out.txt", "w") diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py index 14e40882..97ba89a8 100644 --- a/tests/quick/general/test_classical_sockets.py +++ b/tests/quick/general/test_classical_sockets.py @@ -3,7 +3,7 @@ from simulaqron.run import run_applications from simulaqron.sdk.socket import Socket -from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings import simulaqron_settings, network_config, get_default_network_config_file from simulaqron.settings.simulaqron_config import SimBackend @@ -36,7 +36,7 @@ def test_classical_communication(self): ("Bob", TestClassicalSocket.bob_program_receiver), ] ) - _ = run_applications(apps, use_app_config=False, enable_logging=False) + _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) def test_unknown_local(self): with pytest.raises(ValueError) as ex: diff --git a/tests/quick/netqasm/test_free_qubit.py b/tests/quick/netqasm/test_free_qubit.py index cfe5befc..51702af9 100644 --- a/tests/quick/netqasm/test_free_qubit.py +++ b/tests/quick/netqasm/test_free_qubit.py @@ -3,7 +3,7 @@ from netqasm.runtime.settings import set_simulator from simulaqron.run.run import run_applications -from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings import simulaqron_settings, network_config, get_default_network_config_file set_simulator("simulaqron") @@ -102,7 +102,7 @@ def test_too_many_qubits(self): ] ) with pytest.raises(RuntimeError) as exc: - _ = run_applications(apps, use_app_config=False, enable_logging=False) + _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) assert "Virtual address 2 is outside the unit module (app ID 0) which has length 2" in str(exc.value) def test_release_qubit(self): @@ -111,7 +111,7 @@ def test_release_qubit(self): ("Alice", TestFreeQubit.release_qubit) ] ) - result = run_applications(apps, use_app_config=False, enable_logging=False) + result = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) assert result[0]["app_Alice"] == 1 def test_release_qubit_b(self): @@ -120,7 +120,7 @@ def test_release_qubit_b(self): ("Alice", TestFreeQubit.release_qubit_b) ] ) - result = run_applications(apps, use_app_config=False, enable_logging=False) + result = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) assert result[0]["app_Alice"] == 1 def test_release_and_reuse_qubit(self): @@ -129,7 +129,7 @@ def test_release_and_reuse_qubit(self): ("Alice", TestFreeQubit.release_and_reuse_qubit) ] ) - result = run_applications(apps, use_app_config=False, enable_logging=False) + result = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) assert result[0]["app_Alice"] == 2 def test_release_and_reuse_qubit_b(self): @@ -138,5 +138,5 @@ def test_release_and_reuse_qubit_b(self): ("Alice", TestFreeQubit.release_and_reuse_qubit_b) ] ) - result = run_applications(apps, use_app_config=False, enable_logging=False) + result = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) assert result[0]["app_Alice"] == 2 diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 2dd837a4..2c5981e8 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -5,7 +5,7 @@ from netqasm.runtime.settings import set_simulator from netqasm.sdk.classical_communication.message import StructuredMessage -from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings import simulaqron_settings, network_config, get_default_network_config_file from simulaqron.settings.simulaqron_config import SimBackend set_simulator("simulaqron") @@ -115,7 +115,7 @@ def test_peek_new_unflushed_qubit(self): ] ) with pytest.raises(RuntimeError) as exc: - _ = run_applications(apps, use_app_config=False, enable_logging=False) + _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) assert "Qubit 0 has unflushed operations" in str(exc.value) def test_peek_unflushed_qubit(self): @@ -125,7 +125,7 @@ def test_peek_unflushed_qubit(self): ] ) with pytest.raises(RuntimeError) as exc: - _ = run_applications(apps, use_app_config=False, enable_logging=False) + _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) assert "Qubit 0 has unflushed operations" in str(exc.value) def test_get_basic_state_local(self): @@ -134,7 +134,7 @@ def test_get_basic_state_local(self): ("Alice", TestGetQubitState.peek_init_qubit) ] ) - raw_results = run_applications(apps, use_app_config=False, enable_logging=False) + raw_results = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) # We expect the qubit to be initialized in the |0> state = [1 0] assert np.array_equal(raw_results[0]["app_Alice"], np.array([[1.0 + 0.0j, 0 + 0.0j], [0.0 + 0.0j, 0 + 0.0j]])) @@ -144,7 +144,7 @@ def test_get_qubit_state_local(self): ("Alice", TestGetQubitState.peek_local_qubit) ] ) - raw_results = run_applications(apps, use_app_config=False, enable_logging=False) + raw_results = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) #qubit A: H(|0>) = 1/sqrt(2) |0> + 1/sqrt(2) |1> = 1/sqrt(2) [1 0] + 1/sqrt(2) [0 1] expected_h = np.array([1.0 / math.sqrt(2.0) + 0.0j, 1.0 / math.sqrt(2.0) + 0.0j]) # Note: Due to loss in serialization, we allow a tolerance of 1e-5 when comparing all the members @@ -160,7 +160,7 @@ def test_get_qubit_state_teleport(self): ("Bob", TestGetQubitState.bob_teleport) ] ) - raw_results = run_applications(apps, use_app_config=False, enable_logging=False) + raw_results = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) assert np.isclose( raw_results[0]["app_Alice"]["alice_state"], raw_results[0]["app_Bob"]["bob_state"], diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index 087adda1..f0803dfa 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -3,7 +3,7 @@ import pytest from timeit import default_timer as timer -from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings import simulaqron_settings, network_config, get_default_network_config_file, get_default_network_config_file, get_default_network_config_file from simulaqron.network import Network @@ -16,7 +16,7 @@ def network_file(self): network_config.using_default_network() def test_start(self, network_file: str): - network = Network(nodes=self.nodes) + network = Network(nodes=self.nodes, network_config_file=get_default_network_config_file(use_embedded=True)) assert len(network.processes) == 2 * len(self.nodes) for p in network.processes: assert p.is_alive() is False @@ -26,13 +26,13 @@ def test_start(self, network_file: str): assert p.is_alive() is True def test_stop(self): - network = Network(nodes=self.nodes) + network = Network(nodes=self.nodes, network_config_file=get_default_network_config_file(use_embedded=True)) network.stop() for p in network.processes: assert p.is_alive() is False def test_start_stop(self): - network = Network(nodes=self.nodes) + network = Network(nodes=self.nodes, network_config_file=get_default_network_config_file(use_embedded=True)) network.start(wait_until_running=True) for p in network.processes: assert p.is_alive() is True @@ -41,7 +41,7 @@ def test_start_stop(self): assert p.is_alive() is False def test_no_wait(self): - network = Network(nodes=self.nodes) + network = Network(nodes=self.nodes, network_config_file=get_default_network_config_file(use_embedded=True)) network.start(wait_until_running=False) assert network.running is False @@ -57,6 +57,6 @@ def test_no_wait(self): assert network.running is True def test_del(self): - network = Network(nodes=self.nodes) + network = Network(nodes=self.nodes, network_config_file=get_default_network_config_file(use_embedded=True)) network.start(wait_until_running=True) del network diff --git a/tests/quick/network_topology/test_restricted_topology.py b/tests/quick/network_topology/test_restricted_topology.py index 95610956..a50a8d1b 100644 --- a/tests/quick/network_topology/test_restricted_topology.py +++ b/tests/quick/network_topology/test_restricted_topology.py @@ -6,7 +6,7 @@ from netqasm.runtime.app_config import default_app_config from simulaqron.network import Network -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, get_default_network_config_file from simulaqron.sdk.connection import SimulaQronConnection from simulaqron.run import run_applications @@ -47,7 +47,7 @@ def recv_func(name, remote_name): outcomes.append(m) for create_name, recv_name in self.edges: - run_applications([ + run_applications(network_cfg=get_default_network_config_file(use_embedded=True), apps=[ default_app_config(create_name, partial(create_func, create_name, recv_name)), default_app_config(recv_name, partial(create_func, recv_name, create_name)), ], use_app_config=False) @@ -56,7 +56,7 @@ def recv_func(name, remote_name): for sender_name, receiver_name in self.non_edges: with self.assertRaises(RuntimeError): # TODO correct error - run_applications([ + run_applications(network_cfg=get_default_network_config_file(use_embedded=True), apps=[ default_app_config(create_name, partial(create_func, create_name, recv_name)), default_app_config(recv_name, partial(create_func, recv_name, create_name)), ], use_app_config=False) diff --git a/tests/slow/sdk/test_single_qubit.py b/tests/slow/sdk/test_single_qubit.py index 09399b34..f57ceaf6 100644 --- a/tests/slow/sdk/test_single_qubit.py +++ b/tests/slow/sdk/test_single_qubit.py @@ -34,7 +34,7 @@ from netqasm.sdk.qubit import Qubit -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, get_default_network_config_file from simulaqron.settings.simulaqron_config import SimBackend from simulaqron.network import Network from simulaqron.sdk.connection import SimulaQronConnection diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index d05e6f57..2902f482 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -40,7 +40,7 @@ from simulaqron.sdk.socket import Socket from simulaqron.run.run import run_applications from simulaqron.run.run import reset -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, get_default_network_config_file from simulaqron.settings.network_config import NetworkConfigBuilder @@ -238,7 +238,7 @@ def test_EPRS(self, network): ("Bob", EPR_Bob) ] ) - results = run_applications(apps, use_app_config=False, enable_logging=False, num_rounds=self.iterations) + results = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False, num_rounds=self.iterations) # both sides MUST measure the same state assert int(results[0]["app_Alice"]) == int(results[0]["app_Bob"]) @@ -252,4 +252,4 @@ def test_teleport(self, network): ("Bob", teleport_bob) ] ) - _ = run_applications(apps, use_app_config=False, enable_logging=False, num_rounds=self.iterations) + _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False, num_rounds=self.iterations) From 9d8b53ae6abe0f36c0129828b97f81979b15487c Mon Sep 17 00:00:00 2001 From: StephanieWehner Date: Sun, 7 Dec 2025 23:16:41 +0100 Subject: [PATCH 174/277] Added a message one can use simulaqron COMMAND -h to get more options for that command, as otherwise one can - amongst other things... - never find network-config-file --- simulaqron/simulaqron.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/simulaqron/simulaqron.py b/simulaqron/simulaqron.py index f124ae07..6e91cdae 100644 --- a/simulaqron/simulaqron.py +++ b/simulaqron/simulaqron.py @@ -96,7 +96,7 @@ def run(self): time.sleep(0.1) -@click.group(context_settings=CONTEXT_SETTINGS) +@click.group(context_settings=CONTEXT_SETTINGS, epilog="Run 'simulaqron COMMAND --help' for more information on a command.") def cli(): """Command line interface for interacting with SimulaQron.""" pass @@ -121,8 +121,8 @@ def version(): @cli.command() @click.option( "--network-config-file", - help=f"Use the given network config file. Defaults to the file named " # noqa: E131 - f"'{DEFAULT_SIMULAQRON_NETWORK_FILENAME}' on the current directory.", # noqa: E131 + help=f"Path to network config file. If not specified, uses " + f"./{DEFAULT_SIMULAQRON_NETWORK_FILENAME} or ~/.simulaqron/{DEFAULT_SIMULAQRON_NETWORK_FILENAME}", type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), default=None ) From bbd5b4d04e06ef4ce730a221ace114ddef84fa2d Mon Sep 17 00:00:00 2001 From: StephanieWehner Date: Mon, 8 Dec 2025 00:03:18 +0100 Subject: [PATCH 175/277] Discovered NOONE had run make tests_all for years (probably not since Axel and I edited this). Marked merge tests as deprecated for now given the API needs a serious update (updated some but not all now) This was an amazingly diplomatic comment given what I think about this... Fixed sdk tests though to work again by using non stabilizer backend --- tests/slow/merges/configs/Alice.cfg | 11 ----- tests/slow/merges/configs/AliceBob.cfg | 12 ----- tests/slow/merges/configs/AliceBobCharlie.cfg | 13 ----- tests/slow/merges/test_merges.py | 47 ++++++++++++++----- .../optional_noise/test_optional_noise.py | 4 +- tests/slow/sdk/test_single_qubit.py | 3 +- tests/slow/sdk/test_two_qubit.py | 2 +- 7 files changed, 41 insertions(+), 51 deletions(-) delete mode 100644 tests/slow/merges/configs/Alice.cfg delete mode 100644 tests/slow/merges/configs/AliceBob.cfg delete mode 100644 tests/slow/merges/configs/AliceBobCharlie.cfg diff --git a/tests/slow/merges/configs/Alice.cfg b/tests/slow/merges/configs/Alice.cfg deleted file mode 100644 index da460eb8..00000000 --- a/tests/slow/merges/configs/Alice.cfg +++ /dev/null @@ -1,11 +0,0 @@ -# Network configuration file -# -# For each host its informal name, as well as its location in the network must -# be listed. -# -# [name], [hostname], [port number] -# - -# Alice, localhost, 8811 - - diff --git a/tests/slow/merges/configs/AliceBob.cfg b/tests/slow/merges/configs/AliceBob.cfg deleted file mode 100644 index 8551ef5e..00000000 --- a/tests/slow/merges/configs/AliceBob.cfg +++ /dev/null @@ -1,12 +0,0 @@ -# Network configuration file -# -# For each host its informal name, as well as its location in the network must -# be listed. -# -# [name], [hostname], [port number] -# - -Alice, localhost, 8811 -Bob, localhost, 8812 - - diff --git a/tests/slow/merges/configs/AliceBobCharlie.cfg b/tests/slow/merges/configs/AliceBobCharlie.cfg deleted file mode 100644 index e2ce2abd..00000000 --- a/tests/slow/merges/configs/AliceBobCharlie.cfg +++ /dev/null @@ -1,13 +0,0 @@ -# Network configuration file -# -# For each host its informal name, as well as its location in the network must -# be listed. -# -# [name], [hostname], [port number] -# - -Alice, localhost, 8811 -Bob, localhost, 8812 -Charlie, localhost, 8813 - - diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 8edae8e9..19fb6bf3 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -1,4 +1,5 @@ import sys +import pytest import os import unittest from pathlib import Path @@ -18,6 +19,8 @@ from simulaqron.local.setup import setup_local, assemble_qubit from simulaqron.network import Network from simulaqron.settings import simulaqron_settings +from simulaqron.settings.network_config import NetworkConfigBuilder +from simulaqron.settings.network_config import NetworkConfigBuilder from simulaqron.settings.simulaqron_config import SimBackend from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.reactor import reactor @@ -148,6 +151,8 @@ def remote_receive_one_qubit(self, virtualNum, cnot_direction=0): # @for_all_methods() +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestMerge(unittest.TestCase): @classmethod def setUpClass(cls): @@ -169,9 +174,9 @@ def setUpClass(cls): simulaqron_settings.network_config_file = network_def_file.name simulaqron_settings.save_to_file(simulaqron_settings_file.name) nodes = ["Alice", "Bob", "Charlie"] - cls.network = Network(nodes=nodes, force=True) + cls.network = Network(nodes=nodes, network_config_file=Path(network_def_file.name)) cls.network.start() - # cls.network = Network(nodes=nodes, force=True) + # cls.network = Network(nodes=nodes, network_config_file=Path(network_def_file.name)) # cls.network.start() @classmethod @@ -190,7 +195,7 @@ def tearDownClass(cls): network_settings_file.unlink() @staticmethod - def setup_node(name, node_code, classical_net_file, send_end): + def setup_node(name, node_code, send_end): if simulaqron_settings.log_level == DEBUG: stdout_file = open(f"stdout-setup-node-{name}-{os.getpid()}.out.txt", "w") stderr_file = open(f"stderr-setup-node-{name}-{os.getpid()}.out.txt", "w") @@ -200,11 +205,15 @@ def setup_node(name, node_code, classical_net_file, send_end): virtualFile = simulaqron_settings.network_config_file # This file defines the nodes acting as servers in the classical communication network - classicalFile = os.path.join(os.path.dirname(__file__), "configs", classical_net_file) + classicalFile = virtualFile # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(virtualFile) - classicalNet = SocketsConfig(classicalFile) + virtual_config = NetworkConfigBuilder() + virtual_config.read_from_file(virtualFile) + virtualNet = SocketsConfig(virtual_config) + classical_config = NetworkConfigBuilder() + classical_config.read_from_file(classicalFile) + classicalNet = SocketsConfig(classical_config) # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network @@ -218,12 +227,12 @@ def setup_node(name, node_code, classical_net_file, send_end): # execute the function runClientNode setup_local(name, virtualNet, classicalNet, lNode, node_code, send_end) - def run_test(self, classical_net_file): + def run_test(self): pipe_list = [] for name, node_code in zip(self.nodes, self.node_codes): recv_end, send_end = Pipe(False) p = Process(target=self.setup_node, - args=[name, node_code, classical_net_file, send_end], + args=[name, node_code, send_end], name=name) self.processes.append(p) pipe_list.append(recv_end) @@ -241,6 +250,9 @@ def run_test(self, classical_net_file): self.assertTrue(all(results)) +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothLocal(TestMerge): @classmethod def setUpClass(cls): @@ -296,9 +308,12 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): reactor.stop() def test(self): - self.run_test("Alice.cfg") + self.run_test() +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothLocalNotSameReg(TestBothLocal): @classmethod @inlineCallbacks @@ -349,6 +364,8 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): reactor.stop() +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothRemote(TestMerge): @classmethod def setUpClass(cls): @@ -435,9 +452,11 @@ def charlie(qReg, virtRoot, myName, classicalNet, send_end): send_end.send(True) def test(self): - self.run_test("AliceBobCharlie.cfg") + self.run_test() +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothRemoteSameNodeDiffReg(TestMerge): @classmethod def setUpClass(cls): @@ -501,9 +520,11 @@ def bob(qReg, virtRoot, myName, classicalNet, send_end): send_end.send(True) def test(self): - self.run_test("AliceBob.cfg") + self.run_test() +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothRemoteSameNodeSameReg(TestBothRemoteSameNodeDiffReg): @staticmethod @inlineCallbacks @@ -547,6 +568,8 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): reactor.stop() +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestRemoteAtoB(TestBothRemoteSameNodeDiffReg): @staticmethod @inlineCallbacks @@ -579,6 +602,8 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): reactor.stop() +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestRemoteBtoA(TestBothRemoteSameNodeDiffReg): @staticmethod @inlineCallbacks diff --git a/tests/slow/optional_noise/test_optional_noise.py b/tests/slow/optional_noise/test_optional_noise.py index ef8a60f5..0d1d9170 100644 --- a/tests/slow/optional_noise/test_optional_noise.py +++ b/tests/slow/optional_noise/test_optional_noise.py @@ -12,7 +12,7 @@ from simulaqron.sdk.connection import SimulaQronConnection from netqasm.sdk import Qubit from simulaqron.network import Network -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import simulaqron_settings, get_default_network_config_file def prep_z0(conn): @@ -63,7 +63,7 @@ def setUpClass(cls): simulaqron_settings.noisy_qubits = True simulaqron_settings.t1 = 0.0001 - cls.network = Network(nodes=["Alice"], force=True) + cls.network = Network(nodes=["Alice"], network_config_file=get_default_network_config_file(use_embedded=True)) cls.network.start() @classmethod diff --git a/tests/slow/sdk/test_single_qubit.py b/tests/slow/sdk/test_single_qubit.py index f57ceaf6..30addeb8 100644 --- a/tests/slow/sdk/test_single_qubit.py +++ b/tests/slow/sdk/test_single_qubit.py @@ -206,12 +206,13 @@ class TestSingleQubitGate: @pytest.fixture def network(self): simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.PROJECTQ with NamedTemporaryFile(suffix=".json", delete_on_close=False) as net_config_file: simulaqron_settings.network_config_file = net_config_file.name network_builder = NetworkConfigBuilder() network_builder.using_default_network() network_builder.write_to_file(net_config_file.name) - network = Network(nodes=["Alice"], force=True) + network = Network(nodes=["Alice"], network_config_file=get_default_network_config_file(use_embedded=True)) network.start() yield network diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index 2902f482..5007fccb 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -194,7 +194,7 @@ def network(self): network_builder.using_default_network() network_builder.write_to_file(net_config_file.name) net_config_file.close() - network = Network(nodes=["Alice", "Bob"], force=True) + network = Network(nodes=["Alice", "Bob"], network_config_file=get_default_network_config_file(use_embedded=True)) network.start(wait_until_running=True) yield network From 097d12ff59c3a0dfac7591cdafe81bee64992aef Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 8 Dec 2025 10:31:36 +0100 Subject: [PATCH 176/277] Delete "nrnodes" option in simulaqron cli --- simulaqron/cli.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/simulaqron/cli.py b/simulaqron/cli.py index 2083e730..01a2f49e 100644 --- a/simulaqron/cli.py +++ b/simulaqron/cli.py @@ -137,14 +137,6 @@ def version(): type=str, default="default", ) -@click.option( - "-N", - "--nrnodes", - help="Number of nodes to start. This argument will start nodes named 'Node', " - "until the number of nodes is reached.", - type=int, - default=0, -) @click.option( "-n", "--nodes", @@ -152,7 +144,7 @@ def version(): type=str, default="", ) -def start(name: str, nrnodes: int, nodes: str, simulaqron_config_file: Path, network_config_file: Path): +def start(name: str, nodes: str, simulaqron_config_file: Path, network_config_file: Path): """Starts a network with the given parameters or from config files.""" # Checks the simulaqron config if not _path_exists(simulaqron_config_file): @@ -174,6 +166,9 @@ def start(name: str, nrnodes: int, nodes: str, simulaqron_config_file: Path, net return # Check that the nodes to start exist in the given network nodes = nodes.split(",") + if len(nodes) <= 0: + print("The list of nodes to start is empty. Please check the list given in the --nodes argument.") + return for node_to_start in nodes: if not node_to_start in network_config.networks[name]: print(f"The node '{node_to_start}' was not found in the network named '{name} 'specified in" @@ -186,11 +181,6 @@ def start(name: str, nrnodes: int, nodes: str, simulaqron_config_file: Path, net logging.warning("Network with name %s is already running", name) logging.warning("The pidfile for this network is located at %s", pidfile) return - if len(nodes) <= 0: - print("WARNING - The list of nodes to start is empty. If you specified the --nrnodes option " - "this can be normal. Please check your invocation line if needed.") - if nrnodes > 0 and len(nodes) < nrnodes: - nodes += [f"Node{i}" for i in range(nrnodes - len(nodes))] d = SimulaQronDaemon(pidfile=pidfile, name=name, nodes=nodes) try: d.start() From 276441789c7dce9cfcbd503e230240c4a23d52c4 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 8 Dec 2025 10:32:28 +0100 Subject: [PATCH 177/277] First changes in the documentation for configuring nodes --- docs/ConfNodes.rst | 77 +++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index 5f628b2b..ab251bfc 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -5,38 +5,31 @@ Configuring the simulated network Starting the SimulaQron backend ------------------------------- -The backend of a SimulaQron network is a set of running virtual nodes and their corresponding CQC servers. To start the backend of a SimulaQron network run the command ``simulaqron start``. +The backend of a SimulaQron network is a set of running virtual nodes and their corresponding `VirtualNode` servers. Starting a SimulaQron network requires using SimulaQron and network configuration files. -With no arguments, a network is by default started with the five nodes Alice, Bob, Charlie, David and Eve. How to adjust the nodes and the topology of the network is described below. +To start the backend of a SimulaQron network run the command ``simulaqron start``. -.. warning:: ``simulaqron start`` can fail if any of the ports specified in the config files are already in use by a running SimulaQron network or another program. - -To configure networks see section :ref:`networkConfig`. -Finally for instructions on how to connect to an already runnning simulated network using CQC, see section :ref:`remoteNetwork`. - -If you want to start a network with for example the three nodes Alex, Bart, Curt, simply type:: - - simulaqron start --nodes Alex,Bart,Curt - -If you simply want a network with 10 nodes, type:: - - simulaqron start --nrnodes 10 +The command can receive certain arguments to control the simulated network: -This will start up a network where the nodes are called Node0, Node1, ..., Node9. +* ``--simulaqron-config-file=PATH`` (optional): Specifies a path for the SimulaQron config file to use for the backend. If not given, simulaqron will try to read a file named ``simulaqron_settings.json`` in the current folder. +* ``--network-config-file=PATH`` (optional): Specifies a path for the network config file to use for the backend. If not given, simulaqron will try to read a file named ``simulaqron_network.json`` in the current folder. +* ``--name=`` (optional): Specifies the name of the network to start. This name must correspond to one of the names specified on the given network configuration file. If this argument is not given, this value is defaulted to ``default``. +* ``--nodes `` (`required`): Specified which nodes to simulate. The ```` value is a comma separated list of the nodes names to start. All the specified node names must exist within the specified network inside the network configuration file. -The --nodes and --nrnodes can be combined. Let's say you want a network with 10 nodes and that three of the nodes are called Alice, Bob and Charlie, type:: +How to adjust the nodes and the topology of the network is described below. - simulaqron start --nodes Alice,Bob,Charlie --nrnodes 10 +.. warning:: ``simulaqron start`` can fail if any of the ports specified in the config files are already in use by a running SimulaQron network or another program. -Which will start up a network with the nodes Alice, Bob, Charlie, Node0, Node1, ..., Node6. If --nrnodes is less than the entries in --nodes, then --nrnodes is ignored. The two keywords can also be specified shorter as -nd and -nn respectively. So the above can also be done as:: +To configure networks see section :ref:`networkConfig`. +Finally for instructions on how to connect to an already running simulated network using CQC, see section :ref:`remoteNetwork`. - simulaqron start -n Alice,Bob,Charlie -N 10 +If you want to start a network with, for example, the three nodes Alex, Bart, Curt, simply type:: -You can also specify a topology of the network. For example if you want 10 nodes in a ring topology, type:: + simulaqron start --nodes Alex,Bart,Curt - simulaqron start --nrnodes 10 --topology ring +TODO -In this network Node :math:`i` can create EPR pairs and send qubits to Node :math:`i-1 \pmod{10}` and Node :math:`i+1 \pmod{10}`. However, if a CQC message is sent to for example Node2 to produce entanglement with Node5, a error message (CQC_ERR_UNSUPP) will be returned. The options for the automatically generated topologies are currently: +The options for the automatically generated topologies are currently: * `complete`: A fully connected. This is also used if the argument --topology is not used. * `ring`: A ring network, i.e. a connected topology where every node has exactly two neighbors. @@ -59,26 +52,11 @@ The network that is then started might look like this: To create a custom topology, see below. ---------------------- -Using the --keep flag ---------------------- -By default simulaqron will try to overwrite the current network config of a network your trying to start. -For example if you have a network called "my_network" with the nodes Alice, Bob and Charlie and you type:: - - simulaqron start --name=my_network --nodes=Alice,Bob - -simulaqron will ask you if you want to edit the config file to make "my_network" be a network with the nodes Alice and Bob. -However if you add the flag ``--keep``, simulaqron will simply start up Alice and Bob in the network "my_network" without editing the config file. -This is useful if your planning to simulated a network between multiple physical computers. -Since in this case, the node Charlie might be simulated at a differnent computer so you still want the addresses of Charlie in your config file but you don't want to start that node on your computer. - -.. note:: If you want to suppress the check from simulaqron whether you want to edit the network config file you can always add the flag ``--force`` (``-f``). - ----------------- Multiple networks ----------------- -To run multiple networks at the same time you need to given them different names by using the --name flag:: +To run multiple networks at the same time you need to give them different names by using the --name flag:: simulaqron start --name NETWORK @@ -107,16 +85,14 @@ which adds Maria to the network "OtherNetwork". You can also specify hostname and port numbers to be used for this node including what it's neighbors are using the arguments: * ``--hostname`` - * ``--app_port`` - * ``--cqc_port`` - * ``--vnode_port`` + * ``--app-port`` + * ``--qnodeos-port`` + * ``--vnode-port`` * ``--neighbors`` -SimulaQron will ask you before it makes any changes to the network config file. If you wan to suppress this you can add the flag ``--force`` (``-f``). - If you want to build up a (or many) more complex networks it can become tedious to do this through the CLI. You can instead write your own network config file. -This network config file should be a .json file and could for example look as follows. +This network config file should be a .json file and could, for example, look as follows. An example of such a file can be seen below which contains two networks ("default" and "small_network") which the nodes "Alice", "Bob" and "Test" respectively:: { @@ -127,7 +103,7 @@ An example of such a file can be seen below which contains two networks ("defaul "localhost", 8000 ], - "cqc_socket": [ + "qnodeos_socket": [ "localhost", 8001 ], @@ -141,7 +117,7 @@ An example of such a file can be seen below which contains two networks ("defaul "localhost", 8007 ], - "cqc_socket": [ + "qnodeos_socket": [ "localhost", 8008 ], @@ -174,7 +150,12 @@ An example of such a file can be seen below which contains two networks ("defaul } } -If you want simulaqron to use your custom network.json file simply set this in the settings by ``simulaqron set network-config-file your/path/my_network.json`` or add the following line to a file ``~/.simulaqron.json``: ``network_config_file: your/path/my_network.json``, where ``your/path/my_network.json`` is the path to your custom network config file. +If you want simulaqron to use your custom network.json file simply place it in the same folder where you are running your code, and name it ``simulaqron_network.json``. You can also use name it differently, but make sure that you manually load this file in your python code:: + + from simulaqron.settings import network_config + ... + + network_config.read_from_file("/path/to/your/config.json") The entries ``"topology"`` can be used to define the topology of the network. This could for example be:: @@ -185,7 +166,7 @@ This could for example be:: "Charlie": ["Bob"] } -descibing network where Alice is adjacent to Bob, Bob is adjacent to Alice and Charlie and Charlie is adjacent to Bob. +describing a network topology where Alice is adjacent to Bob, Bob is adjacent to Alice and Charlie and Charlie is adjacent to Bob. .. note:: Undirected topologies are also supported. That is, networks where for example Alice can send a qubit to Bob but Bob cannot send a qubit to Alice. From 26606fccaefb796a3de98592be14bee56a27d3d4 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 8 Dec 2025 13:12:07 +0100 Subject: [PATCH 178/277] Linter changes --- .../distributed/teleport/teleport-alice.py | 7 +- examples/distributed/teleport/teleport-bob.py | 8 +- simulaqron/cli.py | 78 ++++++++++++------- simulaqron/netqasm_backend/qnodeos.py | 4 +- simulaqron/run/run.py | 1 - simulaqron/settings/network_config.py | 2 - simulaqron/start/start_qnodeos.py | 2 +- simulaqron/virtual_node/virtual.py | 3 - tests/quick/general/test_classical_sockets.py | 7 +- tests/quick/netqasm/test_free_qubit.py | 35 +++++++-- tests/quick/netqasm/test_get_qubit_state.py | 35 +++++++-- tests/quick/network/test_network.py | 2 +- tests/slow/sdk/test_two_qubit.py | 18 ++++- 13 files changed, 137 insertions(+), 65 deletions(-) diff --git a/examples/distributed/teleport/teleport-alice.py b/examples/distributed/teleport/teleport-alice.py index 6cd07cd6..d0bf11c0 100644 --- a/examples/distributed/teleport/teleport-alice.py +++ b/examples/distributed/teleport/teleport-alice.py @@ -1,11 +1,9 @@ from netqasm.runtime.settings import set_simulator set_simulator("simulaqron") -from simulaqron.settings import simulaqron_settings -simulaqron_settings.network_config_file = "./simulaqron_settings.json" +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import Qubit, EPRSocket # noqa: E402 -from netqasm.sdk.external import NetQASMConnection -from netqasm.sdk import Qubit, EPRSocket def run_alice(): epr_socket = EPRSocket("Bob") @@ -26,4 +24,3 @@ def run_alice(): if __name__ == "__main__": results = run_alice() print(f"Alice measurements: m1={results[0]}, m2={results[1]}") - diff --git a/examples/distributed/teleport/teleport-bob.py b/examples/distributed/teleport/teleport-bob.py index dbc630ca..3705e72e 100644 --- a/examples/distributed/teleport/teleport-bob.py +++ b/examples/distributed/teleport/teleport-bob.py @@ -1,11 +1,8 @@ from netqasm.runtime.settings import set_simulator set_simulator("simulaqron") -from simulaqron.settings import simulaqron_settings -simulaqron_settings.network_config_file = "./simulaqron_settings.json" - -from netqasm.sdk.external import NetQASMConnection -from netqasm.sdk import EPRSocket +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 def run_bob(): @@ -19,4 +16,3 @@ def run_bob(): if __name__ == "__main__": result = run_bob() print(f"Bob measurement: {result}") - diff --git a/simulaqron/cli.py b/simulaqron/cli.py index ff61ddf1..d04e11c8 100644 --- a/simulaqron/cli.py +++ b/simulaqron/cli.py @@ -11,8 +11,9 @@ from simulaqron.network import Network from simulaqron.settings import LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS, HOME_NETWORK_SETTINGS -from simulaqron.settings import simulaqron_settings, get_default_network_config_file, network_config -from simulaqron.settings.network_config import NodeConfig, DEFAULT_SIMULAQRON_NETWORK_FILENAME +from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings.network_config import (NodeConfig, DEFAULT_SIMULAQRON_NETWORK_FILENAME, + get_default_network_config_file) from simulaqron.settings.simulaqron_config import SimBackend CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -137,7 +138,10 @@ def _create_local_networks_if_needed_and_load(): _load_local_network_or_default() -@click.group(context_settings=CONTEXT_SETTINGS, epilog="Run 'simulaqron COMMAND --help' for more information on a command.") +@click.group( + context_settings=CONTEXT_SETTINGS, + epilog="Run 'simulaqron COMMAND --help' for more information on a command." +) def cli_entry_point(): """Command line interface for interacting with SimulaQron.""" pass @@ -162,10 +166,10 @@ def version(): @cli_entry_point.command() @click.option( "--network-config-file", - help=f"Path to network config file. If not specified, uses " - f"./{DEFAULT_SIMULAQRON_NETWORK_FILENAME} or ~/.simulaqron/{DEFAULT_SIMULAQRON_NETWORK_FILENAME}", + help=f"Path to network config file. If not specified, uses ./{DEFAULT_SIMULAQRON_NETWORK_FILENAME} " # noqa: E131 + f"or ~/.simulaqron/{DEFAULT_SIMULAQRON_NETWORK_FILENAME}", # noqa: E131 type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), - default=LOCAL_NETWORK_SETTINGS + default=get_default_network_config_file() ) @click.option( "--simulaqron-config-file", @@ -189,43 +193,59 @@ def version(): ) def start(name: str, nodes: str, simulaqron_config_file: Path, network_config_file: Path): """Starts a network with the given parameters or from config files.""" - # if netconfig is None: - # network_config_file = get_default_network_config_file() # Checks the simulaqron config if not _path_exists(simulaqron_config_file): - raise click.BadParameter(f"The given simulaqron config file '{simulaqron_config_file}' does not exist or it is a folder.\n" - "Please check the path given to the --simulaqron-config-file option.") + raise click.BadOptionUsage( + option_name="simulaqron-config-file", + message=f"The given simulaqron config file '{simulaqron_config_file}' does not exist or it " + "is a folder.\nPlease check the path given to the --simulaqron-config-file option." # noqa: E131 + ) # Checks the network config if not _path_exists(network_config_file): - raise click.BadParameter(f"The given network config file '{network_config_file}' does not exist or it is a folder.\n" - "Please check the path given to the --network-config-file option.") + raise click.BadOptionUsage( + option_name="network-config-file", + message=f"The given network config file '{network_config_file}' does not exist or it is a " + "folder.\nPlease check the path given to the --network-config-file option." # noqa: E131 + ) # Load SimulaQron and network configs simulaqron_settings.read_from_file(simulaqron_config_file) network_config.read_from_file(network_config_file) # Check that the network name exists in the network configuration - if not name in network_config.networks: - raise click.BadParameter(f"The network '{name}' was not found in the network configuration file '{network_config_file}'.\n" - f"Please check the name you passed in the --name option and try again.") + if name not in network_config.networks: + raise click.BadOptionUsage( + option_name="name", + message=f"The network '{name}' was not found in the network configuration file " # noqa: E713 + f"'{network_config_file}'.\nPlease check the name you passed in the" # noqa: E131 + " --name option and try again." # noqa: E131 + ) # Check that the nodes to start exist in the given network nodes = nodes.split(",") if len(nodes) <= 0: - print("The list of nodes to start is empty. Please check the list given in the --nodes argument.") - return + raise click.BadOptionUsage( + option_name="nodes", + message="The list of nodes to start is empty. Please check the list given in " + "the --nodes argument." + ) for node_to_start in nodes: - if not node_to_start in network_config.networks[name]: - raise click.BadParameter(f"The node '{node_to_start}' was not found in the network named '{name} 'specified in" - f" the configuration file '{network_config_file}'.\nPlease check the list of names you " - f"passed in the --nodes option and try again.") + if node_to_start not in network_config.networks[name]: + raise click.BadOptionUsage( + option_name="nodes", + message=f"The node '{node_to_start}' was not found in the network named " # noqa: E713 + f"'{name} 'specified in the configuration file '{network_config_file}'.\n" # noqa: E131 + "Please check the list of names you passed in the --nodes option and try again." # noqa: E131 + ) # Check that there is no other network with the same name running pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): - logging.warning("Network with name %s is already running", name) - logging.warning("The pidfile for this network is located at %s", pidfile) - return + raise click.BadOptionUsage( + option_name="pidfile", + message=f"Network with name {name} is already running.\nThe pidfile for " + f"this network is located at {pidfile}" # noqa: E131 + ) # Let's start the simulaqron daemon. We will pass the config file so it will be available # in the child process and load the same config - d = SimulaQronDaemon(pidfile=pidfile, name=name, nodes=nodes) + d = SimulaQronDaemon(pidfile=pidfile, name=name, nodes=nodes, network_config_file=network_config_file) try: d.start() except SystemExit as e: @@ -233,7 +253,7 @@ def start(name: str, nodes: str, simulaqron_config_file: Path, network_config_fi e.code == exit.DAEMONIZE_FAILED: logging.debug(f"Failed to launch Simulaqron Daemon. " f"Exit code reported by daemons: {e.code}") - print("Failed to launch SimulaQron Daemon. Aborted!") + raise click.BadParameter("Failed to launch SimulaQron Daemon. Aborted!") ############### @@ -251,7 +271,7 @@ def stop(name: str): """Stops a network.""" assert name is not None pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" - logging.debug(f"Trying to open PIDfile") + logging.debug("Trying to open PIDfile") if not pidfile.exists(): logging.warning("Network with name %s is not running", name) return @@ -289,7 +309,7 @@ def reset(force: bool): simulaqron_settings.default_settings() simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) else: - print("Aborting!") + raise click.ClickException("Aborting!") ############### @@ -637,7 +657,7 @@ def get(network_name: str): try: nodes = network_config.get_node_names(network_name=network_name) except ValueError: - print(f"No network {network_name}") + raise click.BadParameter(f"No network {network_name}") else: print(("{} " * len(nodes))[:-1].format(*nodes)) diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 93384ae2..dcd860ac 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -1,5 +1,4 @@ import logging -import os import sys from typing import Optional, Dict, Callable, Generator, Any, List, Type @@ -16,6 +15,7 @@ ReturnQubitStateMessage) from simulaqron.settings import simulaqron_settings + class SubroutineHandler(QNodeController): def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = None, # noqa: F821 flavour: Optional[Flavour] = None): @@ -30,7 +30,7 @@ def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = Non format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", level=simulaqron_settings.log_level, force=True, - stream=sys.stdout # send logs to the standard output, we set this earlier to be in /tmp + stream=sys.stdout # send logs to the standard output, we set this earlier to be in /tmp ) # Give a way for the executioner to return messages diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 9f1c2dce..1d7bba16 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -193,7 +193,6 @@ def run_applications( network_config.read_from_file(network_cfg) network_config.read_from_file(network_cfg) - for _ in range(num_rounds): network = Network( nodes=network_config.get_node_names("default"), diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 4664819b..e6685627 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -554,8 +554,6 @@ def _check_socket_is_free(port: int) -> bool: return False return True -########### -# def get_default_network_config_file(use_embedded: bool = False) -> Path: """ diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index 2ea1be9e..d1bdbd91 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -97,7 +97,7 @@ def setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): break except CannotListenError: logger.error( - "START_QNODEOS: %s: NetQASM server address (%d) is already in use, trying again.", + "START_QNODEOS: %s: NetQASM server address (%d) is already in use, trying again.", my_name, my_host.port ) time.sleep(_RETRY_TIME) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 6f0a611c..35e18b7d 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -1792,6 +1792,3 @@ def __init__(self, fromName, toName, from_epr_socket_id, to_epr_socket_id, new_v self.to_epr_socket_id = to_epr_socket_id self.virt_num = new_virt_num self.rawEntInfo = rawEntInfo - - - diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py index 97ba89a8..1e997f3a 100644 --- a/tests/quick/general/test_classical_sockets.py +++ b/tests/quick/general/test_classical_sockets.py @@ -36,7 +36,12 @@ def test_classical_communication(self): ("Bob", TestClassicalSocket.bob_program_receiver), ] ) - _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + _ = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) def test_unknown_local(self): with pytest.raises(ValueError) as ex: diff --git a/tests/quick/netqasm/test_free_qubit.py b/tests/quick/netqasm/test_free_qubit.py index 51702af9..26da4f94 100644 --- a/tests/quick/netqasm/test_free_qubit.py +++ b/tests/quick/netqasm/test_free_qubit.py @@ -102,7 +102,12 @@ def test_too_many_qubits(self): ] ) with pytest.raises(RuntimeError) as exc: - _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + _ = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) assert "Virtual address 2 is outside the unit module (app ID 0) which has length 2" in str(exc.value) def test_release_qubit(self): @@ -111,7 +116,12 @@ def test_release_qubit(self): ("Alice", TestFreeQubit.release_qubit) ] ) - result = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + result = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) assert result[0]["app_Alice"] == 1 def test_release_qubit_b(self): @@ -120,7 +130,12 @@ def test_release_qubit_b(self): ("Alice", TestFreeQubit.release_qubit_b) ] ) - result = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + result = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) assert result[0]["app_Alice"] == 1 def test_release_and_reuse_qubit(self): @@ -129,7 +144,12 @@ def test_release_and_reuse_qubit(self): ("Alice", TestFreeQubit.release_and_reuse_qubit) ] ) - result = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + result = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) assert result[0]["app_Alice"] == 2 def test_release_and_reuse_qubit_b(self): @@ -138,5 +158,10 @@ def test_release_and_reuse_qubit_b(self): ("Alice", TestFreeQubit.release_and_reuse_qubit_b) ] ) - result = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + result = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) assert result[0]["app_Alice"] == 2 diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index 2c5981e8..b450abb1 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -115,7 +115,12 @@ def test_peek_new_unflushed_qubit(self): ] ) with pytest.raises(RuntimeError) as exc: - _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + _ = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) assert "Qubit 0 has unflushed operations" in str(exc.value) def test_peek_unflushed_qubit(self): @@ -125,7 +130,12 @@ def test_peek_unflushed_qubit(self): ] ) with pytest.raises(RuntimeError) as exc: - _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + _ = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) assert "Qubit 0 has unflushed operations" in str(exc.value) def test_get_basic_state_local(self): @@ -134,7 +144,12 @@ def test_get_basic_state_local(self): ("Alice", TestGetQubitState.peek_init_qubit) ] ) - raw_results = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + raw_results = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) # We expect the qubit to be initialized in the |0> state = [1 0] assert np.array_equal(raw_results[0]["app_Alice"], np.array([[1.0 + 0.0j, 0 + 0.0j], [0.0 + 0.0j, 0 + 0.0j]])) @@ -144,7 +159,12 @@ def test_get_qubit_state_local(self): ("Alice", TestGetQubitState.peek_local_qubit) ] ) - raw_results = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + raw_results = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) #qubit A: H(|0>) = 1/sqrt(2) |0> + 1/sqrt(2) |1> = 1/sqrt(2) [1 0] + 1/sqrt(2) [0 1] expected_h = np.array([1.0 / math.sqrt(2.0) + 0.0j, 1.0 / math.sqrt(2.0) + 0.0j]) # Note: Due to loss in serialization, we allow a tolerance of 1e-5 when comparing all the members @@ -160,7 +180,12 @@ def test_get_qubit_state_teleport(self): ("Bob", TestGetQubitState.bob_teleport) ] ) - raw_results = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False) + raw_results = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False + ) assert np.isclose( raw_results[0]["app_Alice"]["alice_state"], raw_results[0]["app_Bob"]["bob_state"], diff --git a/tests/quick/network/test_network.py b/tests/quick/network/test_network.py index f0803dfa..eea7eb03 100644 --- a/tests/quick/network/test_network.py +++ b/tests/quick/network/test_network.py @@ -3,7 +3,7 @@ import pytest from timeit import default_timer as timer -from simulaqron.settings import simulaqron_settings, network_config, get_default_network_config_file, get_default_network_config_file, get_default_network_config_file +from simulaqron.settings import simulaqron_settings, network_config, get_default_network_config_file from simulaqron.network import Network diff --git a/tests/slow/sdk/test_two_qubit.py b/tests/slow/sdk/test_two_qubit.py index 8d3bdcad..9109b259 100644 --- a/tests/slow/sdk/test_two_qubit.py +++ b/tests/slow/sdk/test_two_qubit.py @@ -26,8 +26,6 @@ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from tempfile import NamedTemporaryFile - import numpy as np import pytest from netqasm.runtime.application import default_app_instance @@ -233,7 +231,13 @@ def test_EPRS(self): ("Bob", EPR_Bob) ] ) - results = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False, num_rounds=self.iterations) + results = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False, + num_rounds=self.iterations + ) # both sides MUST measure the same state assert int(results[0]["app_Alice"]) == int(results[0]["app_Bob"]) @@ -247,4 +251,10 @@ def test_teleport(self): ("Bob", teleport_bob) ] ) - _ = run_applications(apps, network_cfg=get_default_network_config_file(use_embedded=True), use_app_config=False, enable_logging=False, num_rounds=self.iterations) + _ = run_applications( + apps, + network_cfg=get_default_network_config_file(use_embedded=True), + use_app_config=False, + enable_logging=False, + num_rounds=self.iterations + ) From bd4c4a6a0e839f4ebde7f9c143150380048eb78f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 12 Jan 2026 19:49:33 +0100 Subject: [PATCH 179/277] Finalize the documentation about how to start and configure SimulaQron --- docs/ConfNodes.rst | 116 ++++++++++++++++++--------------------------- 1 file changed, 46 insertions(+), 70 deletions(-) diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index ab251bfc..3c0ea873 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -14,66 +14,29 @@ The command can receive certain arguments to control the simulated network: * ``--simulaqron-config-file=PATH`` (optional): Specifies a path for the SimulaQron config file to use for the backend. If not given, simulaqron will try to read a file named ``simulaqron_settings.json`` in the current folder. * ``--network-config-file=PATH`` (optional): Specifies a path for the network config file to use for the backend. If not given, simulaqron will try to read a file named ``simulaqron_network.json`` in the current folder. * ``--name=`` (optional): Specifies the name of the network to start. This name must correspond to one of the names specified on the given network configuration file. If this argument is not given, this value is defaulted to ``default``. -* ``--nodes `` (`required`): Specified which nodes to simulate. The ```` value is a comma separated list of the nodes names to start. All the specified node names must exist within the specified network inside the network configuration file. +* ``--nodes `` (`required`): Specifies which nodes to simulate. The ```` value is a comma separated list of the nodes names to start. All the specified node names must exist within the specified network inside the network configuration file. How to adjust the nodes and the topology of the network is described below. .. warning:: ``simulaqron start`` can fail if any of the ports specified in the config files are already in use by a running SimulaQron network or another program. -To configure networks see section :ref:`networkConfig`. -Finally for instructions on how to connect to an already running simulated network using CQC, see section :ref:`remoteNetwork`. +If you want to start a network with, for example, the three nodes Alex, Bart, Curt from the network named ``network``, simply type:: -If you want to start a network with, for example, the three nodes Alex, Bart, Curt, simply type:: - - simulaqron start --nodes Alex,Bart,Curt - -TODO - -The options for the automatically generated topologies are currently: - -* `complete`: A fully connected. This is also used if the argument --topology is not used. -* `ring`: A ring network, i.e. a connected topology where every node has exactly two neighbors. -* `path`: A path network, i.e. a connected topology where every node has exactly two neighbors but there are no cycles. -* `random_tree`: Generates a random tree, i.e. a topology without cycles. -* `random_connected_{int}`: Generates a random connected graph with a specified number of edges. For example a random connected network on 10 nodes, can be specified as `random_connected_20`. Note that the number of edges for a network with :math:`n` nodes must be greater or equal to :math:`n-1` and less or equal to :math:`n(n-1)/1`. - -Along with setting up the network with the specified topology a .png figure is also generated and stored as config/topology.png. This is useful if a random network is used, to easily visualize the network used. - -As a final example let's combine all the arguments specified above and create a network using 15 nodes, where two of then are called Alice and Bob and the topology of the network is randomly generated as a connected graph with 20 edges:: - - simulaqron start -n Alice,Bob -N 15 -t random_connected_20 - -The network that is then started might look like this: - -.. image:: figs/topology.png - :width: 400px - :align: center - :alt: Programming SimulaQrons Interfaces - -To create a custom topology, see below. - ------------------ -Multiple networks ------------------ - -To run multiple networks at the same time you need to give them different names by using the --name flag:: - - simulaqron start --name NETWORK - -To stop a network with a specific name type:: - - simulaqron stop --name NETWORK - -.. note:: By default the network name is "default". To have multiple networks running at the same time the nodes cannot use the same port numbers. - -How multiple networks can be setup is described below. + simulaqron start --name network --nodes Alex,Bart,Curt .. _networkConfig: ----------------------- Configuring the network ----------------------- -Using the CLI you can add nodes to a network using for example:: +SimulaQron requires specifying a json-based network configuration. This configuration states the name of the node, and IP address/port tuples to correctly connect the SimulaQron simulations and classical communication sockets. + +For each configured node, you need to specify IP and address for 3 fields: +* The ``app_socket`` field, which specifies the IP and port for connecting classical communication sockets. +* The ``qnodeos_socket`` field, which specifies the IP and port for connecting the QnodeOS server, used to interpret NetQASM objects. +* The ``vnode_socket`` field, which specifies the IP and port for the SimulaQron VirtualNode object, which runs the quantum simulation. + +Using the CLI you can add nodes to a network:: simulaqron nodes add Maria @@ -81,7 +44,7 @@ which adds the node Maria to the default network "default". If you want add a no simulaqron nodes add Maria --network-name="OtherNetwork" -which adds Maria to the network "OtherNetwork". +which adds Maria to the network "OtherNetwork". With no extra arguments, this invocation will configure all the sockets fields on ``localhost``, assigning a random port in the 8000-9000 range. You can also specify hostname and port numbers to be used for this node including what it's neighbors are using the arguments: * ``--hostname`` @@ -90,9 +53,8 @@ You can also specify hostname and port numbers to be used for this node includin * ``--vnode-port`` * ``--neighbors`` -If you want to build up a (or many) more complex networks it can become tedious to do this through the CLI. -You can instead write your own network config file. -This network config file should be a .json file and could, for example, look as follows. +If you want to build up a (or many) more complex network, it can become tedious to do this through the CLI. +You can instead write your own network config file, as a .json file. An example of such a file can be seen below which contains two networks ("default" and "small_network") which the nodes "Alice", "Bob" and "Test" respectively:: { @@ -155,7 +117,7 @@ If you want simulaqron to use your custom network.json file simply place it in t from simulaqron.settings import network_config ... - network_config.read_from_file("/path/to/your/config.json") + network_config.read_from_file("/path/to/your/simulaqron_network.json") The entries ``"topology"`` can be used to define the topology of the network. This could for example be:: @@ -170,31 +132,45 @@ describing a network topology where Alice is adjacent to Bob, Bob is adjacent to .. note:: Undirected topologies are also supported. That is, networks where for example Alice can send a qubit to Bob but Bob cannot send a qubit to Alice. +--------------------------- +Generate network topologies +--------------------------- ------------------------------- -Starting a network from Python ------------------------------- +The simulaqron tool is also capable of automatically generating network configuration with certain network topologies. +The options for the automatically generated topologies are currently: -You can also start a network within a Python script (this is in fact what simulaqron does), by using the class :code:`simulaqron.network.Network`. To setup a network by name "test" with the nodes Alice, Bob and Charlie, where Bob is connected with Alice and Charlie but Alice and Charlie are not connected use the following code code:: +* `complete`: A fully connected. This is also used if the argument --topology is not used. +* `ring`: A ring network, i.e. a connected topology where every node has exactly two neighbors. +* `path`: A path network, i.e. a connected topology where every node has exactly two neighbors but there are no cycles. +* `random_tree`: Generates a random tree, i.e. a topology without cycles. +* `random_connected_{int}`: Generates a random connected graph with a specified number of edges. For example a random connected network on 10 nodes, can be specified as `random_connected_20`. Note that the number of edges for a network with :math:`n` nodes must be greater or equal to :math:`n-1` and less or equal to :math:`n(n-1)/1`. - from simulaqron.network import Network +TODO - Implement a command in the CLI to invoke the generation of topologies. +TODO - Document that CLI command. +Along with setting up the network with the specified topology a .png figure is also generated and stored as config/topology.png. This is useful if a random network is used, to easily visualize the network used. - def main(): - # Setup the network - nodes = ["Alice", "Bob", "Charlie"] - topology = {"Alice": ["Bob"], "Bob": ["Alice", "Charlie"], "Charlie": ["Bob"]} - network = Network(name="test", nodes=nodes, topology=topology) +The network that is then started might look like this: - # Start the network - network.start() +.. image:: figs/topology.png + :width: 400px + :align: center + :alt: Programming SimulaQron Interfaces - input("To stop the network, press enter...") +To create a custom topology, see below. +----------------- +Multiple networks +----------------- - if __name__ == '__main__': - main() +To run multiple networks at the same time you need to give them different names in the network configuration file, and then use the names to start them by using the --name flag:: -By default the method :code:`simulaqron.network.Network.start`, only returns when the network is running, i.e. all the connections are established. To avoid this use the argument :code:`wait_until_running=False`. + simulaqron start --name NETWORK -.. note:: The network will stop when the network-object goes out of scope and is handled by the Python garbade collector. The network can be manually stopped with the method :code:`simulaqron.network.Network.stop`. +To stop a network with a specific name type:: + + simulaqron stop --name NETWORK + +.. note:: By default the network name is "default". To have multiple networks running at the same time the nodes cannot use the same port numbers. + +How multiple networks can be setup is described below. From ad2928f4df02a97fd4f2f07437215aeb338ba890 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 12 Jan 2026 20:06:46 +0100 Subject: [PATCH 180/277] Finalize the documentation about how to start and configure SimulaQron --- docs/ConfNodes.rst | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index 3c0ea873..708e3332 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -119,7 +119,13 @@ If you want simulaqron to use your custom network.json file simply place it in t network_config.read_from_file("/path/to/your/simulaqron_network.json") -The entries ``"topology"`` can be used to define the topology of the network. +.. _network-topologies: + +------------------ +Network topologies +------------------ + +Each network configuration contains an entry named ``"topology"``, which can be used to define the topology of the network. This could for example be:: { @@ -136,7 +142,7 @@ describing a network topology where Alice is adjacent to Bob, Bob is adjacent to Generate network topologies --------------------------- -The simulaqron tool is also capable of automatically generating network configuration with certain network topologies. +The simulaqron tool is also capable of automatically generating certain well-known network topologies. The options for the automatically generated topologies are currently: * `complete`: A fully connected. This is also used if the argument --topology is not used. @@ -157,11 +163,11 @@ The network that is then started might look like this: :align: center :alt: Programming SimulaQron Interfaces -To create a custom topology, see below. +To create a custom topology, see section :ref: `network-topologies`. ------------------ -Multiple networks ------------------ +-------------------------- +Starting multiple networks +-------------------------- To run multiple networks at the same time you need to give them different names in the network configuration file, and then use the names to start them by using the --name flag:: @@ -173,4 +179,4 @@ To stop a network with a specific name type:: .. note:: By default the network name is "default". To have multiple networks running at the same time the nodes cannot use the same port numbers. -How multiple networks can be setup is described below. +The JSON configuration file can hold more than one network configuration. To check how to specify such configuration, please check the :ref: `networkConfig`. From 11611d959281d2bbfdf1a49cd6f4ad32873eb790 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 12 Jan 2026 21:03:29 +0100 Subject: [PATCH 181/277] Small update on the "getting started" documentation --- docs/GettingStarted.rst | 103 ++++++++++++---------------------------- 1 file changed, 31 insertions(+), 72 deletions(-) diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index acd2f1a4..d288f07f 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -5,13 +5,18 @@ Getting started Setup ----- -SimulaQron requires `Python 3 `_ along with the packages *cqc*, *twisted*, *numpy*, *scipy*, *networkx*, *flake8*, *click* and *daemons*. +SimulaQron requires `Python 3.12 `_ along with the packages *cqc*, *twisted*, *numpy*, *scipy*, *networkx*, *flake8*, *click* and *daemons*. ^^^^^^^^^^^^^^^^^^^^^^ Installation using pip ^^^^^^^^^^^^^^^^^^^^^^ -The easiest way to install SimulaQron is using pip (requires MacOS or Linux). Simply type :: +The easiest way to install SimulaQron is using pip (requires MacOS or Linux). Start by creating and activating a python virtual environment:: + + python3.12 -m venv simulaqron-venv + source simulaqron-venv/bin/activate + +Now, we can install SimulaQron by simply typing:: pip3 install simulaqron @@ -28,57 +33,6 @@ If you want to make sure that everything has been installed properly you can sta import simulaqron simulaqron.tests() -^^^^^^^^^^^^^^^^^^^^^^^^ -Installation from source -^^^^^^^^^^^^^^^^^^^^^^^^ - -If you want to get the source code, you can clone the git repository. Do:: - - git clone https://github.com/SoftwareQuTech/SimulaQron.git - -You will then -need to set the following environment variable in order to execute the code. Assuming that -you use bash (e.g., standard on OSX or the GIT Bash install on Windows 10), otherwise set the same variables using your favorite shell.:: - - export PYTHONPATH=yourPath/SimulaQron:$PYTHONPATH - -where yourPath is the directory containing SimulaQron. You can add this to your ~/.bashrc or ~/.bash_profile file. - -.. note:: - If you want to use SimulaQron in the same way as when installed using pip you can use an alias by for example - - alias simulaqron=yourPath/SimulaQron/simulaqron/SimulaQron.py - -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Verifying the installation (if installed from source) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To run SimulaQron you need to following Python packages: - -* cqc -* numpy -* scipy -* twisted -* networkx -* flake8 -* click -* daemons -* qutip (optional) -* projectq (optional) - -To verify that that SimulaQron is working on your computer, type:: - - make verify - -in a terminal at the root of the repository. This command will clear any .pyc files in this directory, check that the needed python packages are installed (and if not install these using pip) and run the automated tests. By default a shorter version of the tests are run. If you wish to run the full tests type :code:`make full_tests`. Not however that the full tests can take quite some time since they perform quantum tomography to tests operations on the qubits. -By default the *stabilizer* engine will be used. - -.. note:: During the tests you might see quite some error messages. This is to be expected since some tests test that errors are handled correctly when something goes wrong. If the tests pass it will say OK in the end. - -.. If you wish to run the tests with the *qutip* backend instead, type :code:`make tests_qutip` or :code:`make full_tests_qutip`. If you want to run all tests with all three backends, type :code:`make full_tests_allBackends`. Note that running the full tests with all backends takes a lot of time. - -.. If :code:`make` does not work for you, you can also run the test by typing :code:`sh tests/runTests.sh --quick` (not including tomography tests) or :code:`sh tests/runTests.sh --full` (full tests). - ------------------------ Testing a simple example ------------------------ @@ -86,13 +40,13 @@ Testing a simple example Before delving into how to write any program yourself, let's first simply run one of the existing examples when programming SimulaQron through the Python library (see https://softwarequtech.github.io/CQC-Python/examples.html). Remember from the Overview that SimulaQron has two parts: the first are the virtual node servers that act simulate the hardware at each node as well as the quantum communication between them in a transparent manner. The second are the applications themselves which can be written in two ways, the direct way is to use the native mode using the Python Twisted framework connecting to the virtual node servers, see :doc:`Examples`. -The recommended way however is the use the provided Python library that calls the virtual nodes by making use of the classical/quantum combiner interface. -We will here illustrate how to use SimulaQron with the Python library. +The recommended way however is the use the NetQASM library that calls the virtual nodes by making use of the classical/quantum combiner interface. +We will here illustrate how to use SimulaQron with the NetQASM library. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Starting the SimulaQron backend ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -By default SimulaQron uses the five nodes Alice, Bob, Charlie, David and Eve on your local computers. In this example there will be two processes for each node listening to incoming messages on a certain port number. These make up the simulation backend and the CQC server. To start the processes and thus the backend of SimulaQron simply type:: +By default SimulaQron uses the five nodes Alice, Bob, Charlie, David and Eve on your local computers. In this example there will be three processes for each node listening to incoming messages on a certain port number. These make up the simulation backend, the NetQASM server and the classical communication server. To start the processes and thus the backend of SimulaQron simply type:: simulaqron start @@ -121,16 +75,17 @@ Our objective will be to realize the following protocol which will generate 1 sh * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. +TODO - Update the link references and the names of the examples (NetQASM vs pythonLib) The examples can be found in the repo `pythonLib `_. Before seeing how this example works, let us simply run the code:: - cd examples/pythonLib/corrRNG - sh run.sh + cd examples/nativeMode/corrRNG + sh run.sh You should be seeing the following two lines:: - App Alice: Measurement outcome is: 0/1 - App Bob: Measurement outcome is: 0/1 + App Alice: Measurement outcome is: 0/1 + App Bob: Measurement outcome is: 0/1 Note that the order of these two lines may differ, as it does not matter who measures first. So what is actually going on here? Let us first look at how we will realize the example by making an additional step (3) explicit: @@ -141,26 +96,27 @@ Note that the order of these two lines may differ, as it does not matter who mea * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. While the task we want to realize here is completely trivial, the addition of step 3 does however already highlight a range of choices on how to realize step 3 and the need to find good abstractions to allow easy application development. -One way to realize step 3 would be to hardwire Alices and Bobs measurements: if the hardware can identify the correct qubits from the entanglement generation, then we could instruct it to measure it immediately without asking for a notification from the entanglement generation process. It is clear that in a network that is a bit larger than our tiny three node setup, identifying the right setup requires a link between the underlying qubits and classical control information: this is the objective of the classical/quantum combiner. +One way to realize step 3 would be to hardwire Alice's and Bob's measurements: if the hardware can identify the correct qubits from the entanglement generation, then we could instruct it to measure it immediately without asking for a notification from the entanglement generation process. It is clear that in a network that is a bit larger than our tiny three node setup, identifying the right setup requires a link between the underlying qubits and classical control information: this is the objective of the classical/quantum combiner. The script run.sh executes the following two python scripts:: - #!/bin/sh + #!/bin/sh - python3 aliceTest.py - python3 bobTest.py & + python3 aliceTest.py + python3 bobTest.py & Let us now look at the programs for Alice and Bob. -We first initialize an object of the class ``CQCConnection`` which will do all the communication to the virtual through the CQC interface. -Qubits can then be created by initializing a qubit-object, which takes a ``CQCConnection`` as an input. -On these qubits operations can be applied and they can also be sent to other nodes in the network by use of the ``CQCConnection``. +We first initialize an object of the class ``NetQASMConnection`` which will do all the communication to the virtual through the NetQASM interface. +Qubits can then be created by initializing a qubit-object, which takes a ``NetQASMConnection`` as an input. +On these qubits operations can be applied and they can also be sent to other nodes in the network by use of the ``NetQASMConnection``. The full code in aliceTest.py is:: + # Create an EPR Socket between "Alice" and "Bob" + epr_socket = EPRSocket("Alice", "Bob") # Initialize the connection - with CQCConnection("Alice") as Alice: - + with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as Alice: # Create an EPR pair - q = Alice.createEPR("Bob") + q = epr_socker.create_keep("Bob", number=1)[0] # Measure qubit m=q.measure() @@ -171,11 +127,13 @@ The full code in aliceTest.py is:: Similarly the code in bobTest.py read:: + # Create an EPR Socket between "Bob" and "Alice" + epr_socket = EPRSocket("Bob", "Alice") # Initialize the connection - with CQCConnection("Bob") as Bob: + with NetQASMConnection("Bob", epr_sockets=[epr_socket]) as Bob: # Receive qubit - q=Bob.recvEPR() + q=epr_socker.receive_keep("Alice")[0] # Measure qubit m=q.measure() @@ -184,6 +142,7 @@ Similarly the code in bobTest.py read:: print("| "+to_print+" |") print("|"+"-"*(len(to_print)+2)+"|") +TODO - Update the link references and the names of the examples (NetQASM vs pythonLib) For further examples, see the examples/ folder and for the docs of the Python library see https://softwarequtech.github.io/CQC-Python/index.html. -------- From 408b6740d128de464d65cb8cc8421ac48e1b3451 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 15:05:54 +0100 Subject: [PATCH 182/277] Add documentation for the simulaqron settings --- docs/GettingStarted.rst | 48 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index d288f07f..00ed6f1c 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -157,8 +157,9 @@ To set a setting, for example to use the projectQ backend, type:: simulaqron set backend projectq -Alternatively, you can add a file ``.simulaqron.json`` in your home folder (i.e. ``~``). -For example this file could look like:: +This will create a file named ``simulaqron_settings.json`` in the current folder. This new file contains a full set of simulaqron configuration, including the setting that was just configured (using the `projectq` backend, in the example). + +It is also possible to manually create this file:: { "backend": "projectq", @@ -167,4 +168,47 @@ For example this file could look like:: which would set the backend to be use ProjectQ and the log-level to be debug (10). Any setting in this file will override the settings set in the CLI. +Is is also possible to create a configuration file that contains all the default configurations:: + + simulaqron set default + +This command will create a file with the following configuration:: + + { + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "stabilizer", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 + } + +Section :ref: `settings_fields` provides a description about each one of the configuration options in the file. + +Alternatively, you can place the ``simulaqron_settings.json`` file in the folder ``~/.simulaqron`` (i.e. a folder named ``.simulaqron`` in your home folder). Doing so will make your settings persist across different projects you implement using simulaqron + .. note:: Settings needs to be set before starting the SimulaQron backend. If the backend is already running, stop it, set the settings and start it again. + +^^^^^^^^^^^^^^^^^^^ +Settings precedence +^^^^^^^^^^^^^^^^^^^ + +Since the simulaqron configuration file can be placed in several places, SimulaQron will follow a priority for reading the settings: + +* Settings file placed in the current working folder. +* Settings file placed in the ``~/.simulaqron`` folder. +* If none of the above is found, SimulaQron will create a settings file in the ``~/.simulaqron`` folder, then it will try to load it in that place. + +.. _settings_fields: + +^^^^^^^^^^^^^^^ +Settings Fields +^^^^^^^^^^^^^^^ + +TODO From 600026aeece786b15dba0045df43523fc253452b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 15:59:07 +0100 Subject: [PATCH 183/277] Add documentation for the specific settings on the simulaqron_settings file --- docs/GettingStarted.rst | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index 00ed6f1c..d2bb561e 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -145,6 +145,8 @@ Similarly the code in bobTest.py read:: TODO - Update the link references and the names of the examples (NetQASM vs pythonLib) For further examples, see the examples/ folder and for the docs of the Python library see https://softwarequtech.github.io/CQC-Python/index.html. +.. _settings: + -------- Settings -------- @@ -191,7 +193,7 @@ This command will create a file with the following configuration:: Section :ref: `settings_fields` provides a description about each one of the configuration options in the file. -Alternatively, you can place the ``simulaqron_settings.json`` file in the folder ``~/.simulaqron`` (i.e. a folder named ``.simulaqron`` in your home folder). Doing so will make your settings persist across different projects you implement using simulaqron +Alternatively, you can place the ``simulaqron_settings.json`` file in the folder ``~/.simulaqron`` (i.e. a folder named ``.simulaqron`` in your home folder). Doing so will make your settings persist across different projects you implement using simulaqron. .. note:: Settings needs to be set before starting the SimulaQron backend. If the backend is already running, stop it, set the settings and start it again. @@ -211,4 +213,23 @@ Since the simulaqron configuration file can be placed in several places, SimulaQ Settings Fields ^^^^^^^^^^^^^^^ -TODO +The SimulaQron settings file contains a set of fields to control the configurations of the SimulaQron simulation: + +* ``max_qubit``: Maximum number of qubits to simulate on the Virtual Node. +* ``max_registers``: Maximmum number of registers to use in the Virtual Node. +* ``conn_retry_time``: Number of seconds to wait between connection retries. +* ``conn_max_retries``: Maximum number of times to retry a connection before failing the whole execution. +* ``recv_timeout``: Maximum number of milliseconds to wait for the messages when trying to create EPR pairs. +* ``recv_retry_time``: Maximum number of milliseconds to wait between attempts to create EPR pairs. +* ``recv_max_retries``: Maximum number of tries to attempt when creating EPR pairs. +* ``log_level``: The log level to use for SimulaQron. The integer value in this field must match the values exposed by the python ``logging`` package. For more information about the specific values for each logging level, please check the official python documentation https://docs.python.org/3/library/logging.html#logging-levels. +* ``sim_backend``: The backend qubit simulation that SimulaQron will use to emulate qubits. Currently, three backends are supported: "projectq", "qutip" and "stabilizer". +* ``noisy_qubits``: Whether to enable noisy qubits simulation or not. Setting this to ``true``will randomly apply a Pauli gate after every operation, emulating noise on the qubit backend. +* ``max_app_waiting_time``: Maximum time (in seconds) to wait before considering the running application as stalled. A value of ``-1.0`` will disable the stalling waiting time, allowing SimulaQron to wait undefinitely. +* ``t1``: T1 parameter to use when applying noise on the emulated qubits. This value is only used when the ``noisy_qubit`` option is set to ``true``. + +The default value of all these fields can be seen in the :ref: `settings` section. + +.. note:: An application can become "stalled" in certain configurations, leaving the application to look "hung". This leads to a deadlock of the application. SimulaQron will wait for the configured time before considering the application as "stalled" and kill all the processes. + +.. warning:: Please correctly configure the ``max_app_waiting_time`` to allow your application to wait for any potential "slow" peers. A low value on this field might lead SimulaQron to killing your application prematurely. From f3303ca72c89f704b2f3e0ad7926cdc4cb9d1269 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 16:33:39 +0100 Subject: [PATCH 184/277] Update "examples" documentation. Format changes to avoid super long lines.- --- docs/ConfNodes.rst | 71 ++++++++++++++++++++++++++++------------- docs/Examples.rst | 26 ++++++++------- docs/GettingStarted.rst | 2 +- docs/NetQASM.rst | 68 +++++++++++++++++++++++++++++++++++++++ docs/index.rst | 41 ++++++++++++++---------- 5 files changed, 157 insertions(+), 51 deletions(-) create mode 100644 docs/NetQASM.rst diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index 708e3332..46d1706f 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -5,22 +5,31 @@ Configuring the simulated network Starting the SimulaQron backend ------------------------------- -The backend of a SimulaQron network is a set of running virtual nodes and their corresponding `VirtualNode` servers. Starting a SimulaQron network requires using SimulaQron and network configuration files. +The backend of a SimulaQron network is a set of running virtual nodes and their corresponding `VirtualNode` servers. +Starting a SimulaQron network requires using SimulaQron and network configuration files. To start the backend of a SimulaQron network run the command ``simulaqron start``. The command can receive certain arguments to control the simulated network: -* ``--simulaqron-config-file=PATH`` (optional): Specifies a path for the SimulaQron config file to use for the backend. If not given, simulaqron will try to read a file named ``simulaqron_settings.json`` in the current folder. -* ``--network-config-file=PATH`` (optional): Specifies a path for the network config file to use for the backend. If not given, simulaqron will try to read a file named ``simulaqron_network.json`` in the current folder. -* ``--name=`` (optional): Specifies the name of the network to start. This name must correspond to one of the names specified on the given network configuration file. If this argument is not given, this value is defaulted to ``default``. -* ``--nodes `` (`required`): Specifies which nodes to simulate. The ```` value is a comma separated list of the nodes names to start. All the specified node names must exist within the specified network inside the network configuration file. +* ``--simulaqron-config-file=PATH`` (optional): Specifies a path for the SimulaQron config file to use for the backend. + If not given, simulaqron will try to read a file named ``simulaqron_settings.json`` in the current folder. +* ``--network-config-file=PATH`` (optional): Specifies a path for the network config file to use for the backend. If + not given, simulaqron will try to read a file named ``simulaqron_network.json`` in the current folder. +* ``--name=`` (optional): Specifies the name of the network to start. This name must correspond to one + of the names specified on the given network configuration file. If this argument is not given, this value is + defaulted to ``default``. +* ``--nodes `` (`required`): Specifies which nodes to simulate. The ```` value is a comma + separated list of the nodes names to start. All the specified node names must exist within the specified network + inside the network configuration file. How to adjust the nodes and the topology of the network is described below. -.. warning:: ``simulaqron start`` can fail if any of the ports specified in the config files are already in use by a running SimulaQron network or another program. +.. warning:: ``simulaqron start`` can fail if any of the ports specified in the config files are already in use by a + running SimulaQron network or another program. -If you want to start a network with, for example, the three nodes Alex, Bart, Curt from the network named ``network``, simply type:: +If you want to start a network with, for example, the three nodes Alex, Bart, Curt from the network named ``network``, +simply type:: simulaqron start --name network --nodes Alex,Bart,Curt @@ -29,12 +38,15 @@ If you want to start a network with, for example, the three nodes Alex, Bart, Cu ----------------------- Configuring the network ----------------------- -SimulaQron requires specifying a json-based network configuration. This configuration states the name of the node, and IP address/port tuples to correctly connect the SimulaQron simulations and classical communication sockets. +SimulaQron requires specifying a json-based network configuration. This configuration states the name of the node, +and IP address/port tuples to correctly connect the SimulaQron simulations and classical communication sockets. For each configured node, you need to specify IP and address for 3 fields: * The ``app_socket`` field, which specifies the IP and port for connecting classical communication sockets. -* The ``qnodeos_socket`` field, which specifies the IP and port for connecting the QnodeOS server, used to interpret NetQASM objects. -* The ``vnode_socket`` field, which specifies the IP and port for the SimulaQron VirtualNode object, which runs the quantum simulation. +* The ``qnodeos_socket`` field, which specifies the IP and port for connecting the QnodeOS server, used to interpret + NetQASM objects. +* The ``vnode_socket`` field, which specifies the IP and port for the SimulaQron VirtualNode object, which runs the + quantum simulation. Using the CLI you can add nodes to a network:: @@ -44,8 +56,10 @@ which adds the node Maria to the default network "default". If you want add a no simulaqron nodes add Maria --network-name="OtherNetwork" -which adds Maria to the network "OtherNetwork". With no extra arguments, this invocation will configure all the sockets fields on ``localhost``, assigning a random port in the 8000-9000 range. -You can also specify hostname and port numbers to be used for this node including what it's neighbors are using the arguments: +which adds Maria to the network "OtherNetwork". With no extra arguments, this invocation will configure all the +sockets fields on ``localhost``, assigning a random port in the 8000-9000 range. +You can also specify hostname and port numbers to be used for this node including what it's neighbors are using the +arguments: * ``--hostname`` * ``--app-port`` @@ -55,7 +69,8 @@ You can also specify hostname and port numbers to be used for this node includin If you want to build up a (or many) more complex network, it can become tedious to do this through the CLI. You can instead write your own network config file, as a .json file. -An example of such a file can be seen below which contains two networks ("default" and "small_network") which the nodes "Alice", "Bob" and "Test" respectively:: +An example of such a file can be seen below which contains two networks ("default" and "small_network") which the +nodes "Alice", "Bob" and "Test" respectively:: { "default": { @@ -112,7 +127,9 @@ An example of such a file can be seen below which contains two networks ("defaul } } -If you want simulaqron to use your custom network.json file simply place it in the same folder where you are running your code, and name it ``simulaqron_network.json``. You can also use name it differently, but make sure that you manually load this file in your python code:: +If you want simulaqron to use your custom network.json file simply place it in the same folder where you are running +your code, and name it ``simulaqron_network.json``. You can also use name it differently, but make sure that you +manually load this file in your python code:: from simulaqron.settings import network_config ... @@ -125,8 +142,8 @@ If you want simulaqron to use your custom network.json file simply place it in t Network topologies ------------------ -Each network configuration contains an entry named ``"topology"``, which can be used to define the topology of the network. -This could for example be:: +Each network configuration contains an entry named ``"topology"``, which can be used to define the topology of the +network. This could for example be:: { "Alice": ["Bob"], @@ -134,9 +151,11 @@ This could for example be:: "Charlie": ["Bob"] } -describing a network topology where Alice is adjacent to Bob, Bob is adjacent to Alice and Charlie and Charlie is adjacent to Bob. +describing a network topology where Alice is adjacent to Bob, Bob is adjacent to Alice and Charlie and Charlie is +adjacent to Bob. -.. note:: Undirected topologies are also supported. That is, networks where for example Alice can send a qubit to Bob but Bob cannot send a qubit to Alice. +.. note:: Undirected topologies are also supported. That is, networks where for example Alice can send a qubit to +Bob but Bob cannot send a qubit to Alice. --------------------------- Generate network topologies @@ -149,12 +168,15 @@ The options for the automatically generated topologies are currently: * `ring`: A ring network, i.e. a connected topology where every node has exactly two neighbors. * `path`: A path network, i.e. a connected topology where every node has exactly two neighbors but there are no cycles. * `random_tree`: Generates a random tree, i.e. a topology without cycles. -* `random_connected_{int}`: Generates a random connected graph with a specified number of edges. For example a random connected network on 10 nodes, can be specified as `random_connected_20`. Note that the number of edges for a network with :math:`n` nodes must be greater or equal to :math:`n-1` and less or equal to :math:`n(n-1)/1`. +* `random_connected_{int}`: Generates a random connected graph with a specified number of edges. For example a random + connected network on 10 nodes, can be specified as `random_connected_20`. Note that the number of edges for a network + with :math:`n` nodes must be greater or equal to :math:`n-1` and less or equal to :math:`n(n-1)/1`. TODO - Implement a command in the CLI to invoke the generation of topologies. TODO - Document that CLI command. -Along with setting up the network with the specified topology a .png figure is also generated and stored as config/topology.png. This is useful if a random network is used, to easily visualize the network used. +Along with setting up the network with the specified topology a .png figure is also generated and stored as +config/topology.png. This is useful if a random network is used, to easily visualize the network used. The network that is then started might look like this: @@ -169,7 +191,8 @@ To create a custom topology, see section :ref: `network-topologies`. Starting multiple networks -------------------------- -To run multiple networks at the same time you need to give them different names in the network configuration file, and then use the names to start them by using the --name flag:: +To run multiple networks at the same time you need to give them different names in the network configuration file, +and then use the names to start them by using the --name flag:: simulaqron start --name NETWORK @@ -177,6 +200,8 @@ To stop a network with a specific name type:: simulaqron stop --name NETWORK -.. note:: By default the network name is "default". To have multiple networks running at the same time the nodes cannot use the same port numbers. +.. note:: By default the network name is "default". To have multiple networks running at the same time the nodes + cannot use the same port numbers. -The JSON configuration file can hold more than one network configuration. To check how to specify such configuration, please check the :ref: `networkConfig`. +The JSON configuration file can hold more than one network configuration. To check how to specify such configuration, +please check the :ref: `networkConfig`. diff --git a/docs/Examples.rst b/docs/Examples.rst index 286df10b..b77780be 100644 --- a/docs/Examples.rst +++ b/docs/Examples.rst @@ -5,22 +5,26 @@ One way to program SimulaQron is directly via its 'native interface' using Twist This means writing a client program connecting directly to the local virtual quantum node, and issuing instructions to such simulated quantum hardware. Programming SimulaQron in its native interface is evidently Python specific, and meant primarily as an internal interface allowing one to explore higher level abstractions built on top of it. -One such abstraction is the classical-quantum combiner (CQC) interface, which we aim to make available on the 2020 quantum -internet demonstrator. For programming in a universal, i.e., not Python specific interface see :doc:`CQC`. +One such abstraction is the NetQASM interface.For programming in a universal, i.e., not Python specific interface +see :doc:`NetQASM`. -The examples below assume that you have already made your way through :doc:`GettingStarted`: you have the virtual node servers -up and running, and ran the simple example of generating correlated randomness. Further examples can also be found in examples/nativeMode. +The examples below assume that you have already made your way through :doc:`GettingStarted`: you have the virtual +node servers up and running, and ran the simple example of generating correlated randomness. Further examples can +also be found in examples/nativeMode. -.. note:: The 'native' mode is not the recommended way to program applications for SimulaQron, instead use either the `Python `_ (recommended if you are gettings started), `C `_, or the `Rust `_ Library. +TODO - Update the link to the CQC interface. + +.. note:: The 'native' mode is not the recommended way to program applications for SimulaQron, instead use the + `NetQASM `_ interface. .. toctree:: - :maxdepth: 2 - :caption: Native mode examples: + :maxdepth: 2 + :caption: Native mode examples: - NativeModeCorrRng - NativeModeTemplate - NativeModeTeleport - NativeModeGraphState + NativeModeCorrRng + NativeModeTemplate + NativeModeTeleport + NativeModeGraphState diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index d2bb561e..98dd4387 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -222,7 +222,7 @@ The SimulaQron settings file contains a set of fields to control the configurati * ``recv_timeout``: Maximum number of milliseconds to wait for the messages when trying to create EPR pairs. * ``recv_retry_time``: Maximum number of milliseconds to wait between attempts to create EPR pairs. * ``recv_max_retries``: Maximum number of tries to attempt when creating EPR pairs. -* ``log_level``: The log level to use for SimulaQron. The integer value in this field must match the values exposed by the python ``logging`` package. For more information about the specific values for each logging level, please check the official python documentation https://docs.python.org/3/library/logging.html#logging-levels. +* ``log_level``: The log level to use for SimulaQron. The integer value in this field must match the values exposed by the python ``logging`` package. For more information about the specific values for each logging level, please check the `official python documentation for the logging package `_. * ``sim_backend``: The backend qubit simulation that SimulaQron will use to emulate qubits. Currently, three backends are supported: "projectq", "qutip" and "stabilizer". * ``noisy_qubits``: Whether to enable noisy qubits simulation or not. Setting this to ``true``will randomly apply a Pauli gate after every operation, emulating noise on the qubit backend. * ``max_app_waiting_time``: Maximum time (in seconds) to wait before considering the running application as stalled. A value of ``-1.0`` will disable the stalling waiting time, allowing SimulaQron to wait undefinitely. diff --git a/docs/NetQASM.rst b/docs/NetQASM.rst new file mode 100644 index 00000000..893be6ec --- /dev/null +++ b/docs/NetQASM.rst @@ -0,0 +1,68 @@ +DEPRECATED - The CQC interface +================= + +TODO - Update this document to explain the NetQASM interface! + +WARNING - The CQC interface ahas been deprecated, and it is no longer present in this release of SimulaQron. +Please refer to the NetQASM interface documentation to create quantum applications. + +SimulaQron can be access from any programming language supporting network connections. Instructions to the quantum hardware simulation can be sent via the CQC interface described `here `_. + +A `C `_, `Python `_, and `Rust `_ Library for programming SimulaQron using the CQC Interface are provided. If you are new to SimulaQron, programming via the Python CQC is the easiest way to get started. + +^^^^^^^^^^^^ +Installation +^^^^^^^^^^^^ + +If you have installed SimulaQron using pip, the cqc interface for python should already be installed. +If needed you can also only install the CQC interface in Python using pip by typing:: + + pip3 install cqc + +^^^^^ +Usage +^^^^^ + +The python library provides a way to program a protocol on a network where the nodes listen to instructions through the classical-quantum combiner (CQC) interface. In the following examples the network is simulated by SimulaQron. But the same examples could be executed on a network with real quantum hardware which allows for instructions through the CQC interface, which is the aim for the 2020 quantum internet demonstrator. + +To use the Python library you first need instantiate an object from the class :code:`cqc.pythonLib.CQCConnection`. This should be done in a `context `_, that is using a :code:`with`-statement as follows:: + + with CQCConnection("Alice") as alice: + # your program + +This is to make sure the CQCConnection is correctly closed by the end of the program and that the qubits in the backend are released, even if an error occurs in your program. + +.. note:: It is still possible to initialize a :code:`CQCConnection` in the old way, i.e. without :code:`with`, however you will receive a warning message everytime you create a qubit. + +Let's look at an extremely trivial example were we have the node `Alice` allocate a qubit, perform a Hadamard gate and measure the qubit:: + + with CQCConnection("Alice") as alice: + q = qubit(Alice) + q.H() + m = q.measure() + print(m) + +.. note:: If you do not specify the argument ``socket_address`` specifying the hostname and port of the cqc server receiving incoming CQC messages, you need to have simulaqron installed. The python library then tries to use the socket address of this nodes specified in simulaqron. + +A object from the :class:`qubit`-class is created with the :class:`CQCConnection` as argument, such that whenever an operation is applied to the qubit a CQC message will be sent to the simulation backend to actually perform this operation on the simulated qubit. +For more examples using the Python library see :doc:`GettingStarted` and https://softwarequtech.github.io/CQC-Python/examples.html + +.. _remoteNetwork: + +---------------------------------------- +Connecting to a remote simulated network +---------------------------------------- + +If a simulated network (consisting of virtual nodes and CQC servers) are setup on a remote computer (or on your own computer), CQC messages can be sent to the correct address and port numbers to control the nodes of the network. In this section we describe how to do this. + +Given the ip and port number of the CQC server of a node, you can send CQC messages over TCP using in any way you prefer. To know how these messages should look like to perform certain instructions, refer to https://softwarequtech.github.io/CQC-Python/interface.html + +An easier way to send CQC messages to a CQC server of a node is to use the provided Python library. +Assuming that you know the hostname and port number of the CQC server, you can then easily instantiate an object of the class :class:`~cqc.pythonLib.CQCConnection` which will communicate with the CQC server for you, using the CQC interface. +You can directly specify the ip and port number as follows:: + + cqc = CQCConnection("Alice", socket_address=("1.1.1.1", 8801)) + +More information on how to then actually allocating qubits, manipulating these and creating simulated entanglement see https://softwarequtech.github.io/CQC-Python/useful_commands.html + +We give some more detailed information below on how the classical communication between nodes in the application layer can be realized and also provide some useful commands to program a protocol using the Python library. diff --git a/docs/index.rst b/docs/index.rst index 1f2851a4..47a62e01 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,22 +8,31 @@ SimulaQron Documentation Welcome to the Quantum Internet simulator SimulaQron! -SimulaQron is a distributed simulation of the end nodes in a future quantum internet with the specific goal to explore application development. -The end nodes in a quantum internet are few qubit processors, which may exchange qubits using -a quantum internet. -Specifically, SimulaQron -allows the installation of a local simulation program on each computer in the network that provides the illusion of having a local quantum processor to potential applications. -The local simulation programs on each classical computer connect to each other classically, forming a simulated quantum internet allowing the exchange of simulated qubits between the different -network nodes, as well as the creation of simulated entanglement. - -SimulaQron is written in `Python `_ and uses the `Twisted `_ Perspective Broker. -To perform the local qubit simulation, three different backends have so far been implemented: Using `QuTip `_ and mixed state, using `Project Q `_ and pure states and finally using stabilizer formalism. -However, any other quantum simulator with a python interface can easily be used as a local backend. -The main challenge of SimulaQron is to allow the simulation of virtual qubits at different network nodes: since these may be entangled they cannot be simulated on one network node, which is solved by a transparent distributed simulation on top of in principle any local simulation engine. - -We also have a `paper `_ that describe the design of SimulaQron, which is also freely available on `arxiv `_. - -The documentation below assumes familiarity with classical network programming concepts, Python, Twisted, as well as an elementary understanding of quantum information. More information on a competition at `Our website `_ +SimulaQron is a distributed simulation of the end nodes in a future quantum internet with the specific goal to explore +application development. The end nodes in a quantum internet are few qubit processors, which may exchange qubits using +a quantum internet. + +Specifically, SimulaQron allows the installation of a local simulation program on each computer in the network that +provides the illusion of having a local quantum processor to potential applications. + +The local simulation programs on each classical computer connect to each other classically, forming a simulated quantum +internet allowing the exchange of simulated qubits between the different network nodes, as well as the creation of +simulated entanglement. + +SimulaQron is written in `Python `_ and uses the `Twisted `_ +Perspective Broker. To perform the local qubit simulation, three different backends have so far been implemented: +Using `QuTip `_ and mixed state, using `Project Q `_ and pure states and +finally using stabilizer formalism. However, any other quantum simulator with a python interface can easily be used as +a local backend. The main challenge of SimulaQron is to allow the simulation of virtual qubits at different network +nodes: since these may be entangled they cannot be simulated on one network node, which is solved by a transparent +distributed simulation on top of in principle any local simulation engine. + +We also have a `paper `_ that describe the design of +SimulaQron, which is also freely available on `arxiv `_. + +The documentation below assumes familiarity with classical network programming concepts, Python, Twisted, as well as +an elementary understanding of quantum information. More information on a competition at +`Our website `_ SimulaQron can be installed from pip by the command :code:`pip3 install simulaqron` on MacOS and Linux. From fc4e98ddecf62136efe2008869eec05547fd1283 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 18:05:34 +0100 Subject: [PATCH 185/277] Update and reformat documentation on overview.rst --- docs/Overview.rst | 124 ++++++++++++++++++++++++++++++---------------- docs/index.rst | 2 +- 2 files changed, 81 insertions(+), 45 deletions(-) diff --git a/docs/Overview.rst b/docs/Overview.rst index 02889253..bfaa6f2d 100644 --- a/docs/Overview.rst +++ b/docs/Overview.rst @@ -5,25 +5,42 @@ Overview Programming SimulaQron ---------------------- -There are two ways to program SimulaQron to run applications. The first is to use SimulaQron's native interface via `Twisted `_ Perspective Broker. This interface is specific to Python, and not (easily) accessible from other languages. We will call this interface SimulaQron's native interface throughout. While you may play with simple applications this way, the main purpose of this interface is to allow the development of higher protocol layers and abstractions which will ultimately be used to program quantum networks. -In the light of the alternate interface below it may appear inefficient to export an intermediary interface. However, the purpose of SimulaQron is precisely to explore and play with higher layer abstractions on top of any hardware, or its simulated version, SimulaQron. As such it is best to think of SimulaQron as a piece of simulated hardware with its own native interface, which we may first abstract into a higher level command language for programming. Examples of how to program SimulaQron in native mode can be found in :doc:`Examples`. +There are two ways to program SimulaQron to run applications. The first is to use SimulaQron's native interface via +`Twisted `_ Perspective Broker. This interface is specific to Python, and not (easily) +accessible from other languages. We will call this interface SimulaQron's native interface throughout. While you may +play with simple applications this way, the main purpose of this interface is to allow the development of higher +protocol layers and abstractions which will ultimately be used to program quantum networks. -The second way to run applications is via a higher level interface bundled with SimulaQron, called the classical-quantum combiner (CQC) interface. This interface is universally accessible from any language. It comes with a C and Python library, where the Python CQC is definitely the best place to get started if you have never programmed SimulaQron before. An evolved version of a C library and interface is targeted to be available on the planned 2020 quantum internet demonstrator connecting several Dutch cities. If you want your applications to later use real quantum hardware more easily instead of SimulaQron, then this is the interface to use. Internally, the CQC included in this package, uses SimulaQron's native interface from above in place of real quantum hardware. Examples of how to program using the CQC can be found in :doc:`CQC`. +In the light of the alternate interface below it may appear inefficient to export an intermediary interface. However, +the purpose of SimulaQron is precisely to explore and play with higher layer abstractions on top of any hardware, or +its simulated version, SimulaQron. As such it is best to think of SimulaQron as a piece of simulated hardware with its +own native interface, which we may first abstract into a higher level command language for programming. Examples of +how to program SimulaQron in native mode can be found in :doc:`Examples`. -.. note:: The CQC packages and libraries have been moved to seperate repos at `pythonLib `_ and `cLib `_. The python library can be installed using pip by the command :code:`pip3 install cqc`. +The second way to run applications is via a higher level interface, the NetQASM interface. If you want your +applications to later use real quantum hardware more easily instead of SimulaQron, then this is the interface to use. +Examples of how to program using the NetQASM can be found in :doc:`NetQASM`. + +.. warning:: Update this image (I don't have the sources though... I might need to make them from scratch) .. image:: figs/CQC_schematic_v3.png :width: 400px :align: center - :alt: Programming SimulaQrons Interfaces + :alt: SimulaQron Programming Interfaces -Practically, SimulaQron's Backend is a server process running on each local classical computer (or on a single classical computer), emulating quantum hardware. The backend can be programmed directly using Twisted PB aka native mode. For clarity, not efficiency, the CQC Backend is a separate server process, which connects to the SimulaQron backend using Twisted PB. It accepts incoming connections, and using a general packet format can be programmed using any programming language. Libraries for C and Python are provided. If you are starting out, programming in the Python CQC library is by far the easiest way to get going! Further information about the Python library can be found in https://softwarequtech.github.io/CQC-Python/index.html and explicit examples using this library in https://softwarequtech.github.io/CQC-Python/examples.html. +Practically, SimulaQron's Backend is a server process running on each local classical computer (or on a single +classical computer), emulating quantum hardware. The backend can be programmed directly using Twisted PB (native mode). +For clarity, not efficiency, the NetqASM Backend is a separate server process, which connects to the SimulaQron backend +using Twisted PB. If you are starting out, programming in the Python NetQASM library is by far the easiest way to get +going! Further information about the Python NetQASM library can be found in https://netqasm.readthedocs.io/en/latest/. ------------------------------- How SimulaQron works internally ------------------------------- -Let us here briefly sketch how the SimulaQron backend works internally. Further details can be found in our `paper `_. +Let us here briefly sketch how the SimulaQron backend works internally. Further details can be found in our +`paper `_. + The simulator consists of two parts and has a relatively modular design: @@ -31,50 +48,63 @@ The simulator consists of two parts and has a relatively modular design: Virtual quantum nodes ^^^^^^^^^^^^^^^^^^^^^ -A virtual quantum node is a server program running on a particular computer that pretends to be quantum hardware, simulating qubits and quantum communication. -That is, you may think of these nodes as fake hardware programmable directly via SimulaQron's native interface. Each node presents -a number of virtual qubits for you to use. These virtual qubits would correspond to the physical qubits -available at this node, would this be a real physical implementation of the quantum network node. By connecting to the virtual quantum node server, a -client program that depends on using quantum hardware, may manipulate these qubits as if they were local physical qubit and also -instruct to send them to remote nodes. -The virtual quantum node servers are identified -by their common names (eg Alice, Bob, Charlie), and amongst themselves connect classically to form a virtual simulation -network as a backend. - -An important internal element in SimulaQron is the distinction between virtual qubits and simulated qubits. Virtual qubits -are the qubits as they would be present in real quantum hardware. A virtual qubit is local to each virtual quantum node server -and may be manipulated there. Each virtual qubit is simulated somewhere, by a simulated qubit. Importantly, this simulated qubit -may be located at a different simulating node than the node holding the corresponding virtual qubit. -To see why this is necessary, note that -entangled qubits cannot be locally represented by any form of classical information (hence -the quantum advantage of entanglement in the first place!). As such, if two (or more) virtual nodes share -qubits which are somehow entangled with each other, then these qubits will actually need to be simulated +A virtual quantum node is a server program running on a particular computer that pretends to be quantum hardware, +simulating qubits and quantum communication. That is, you may think of these nodes as fake hardware programmable +directly via SimulaQron's native interface. Each node presents a number of virtual qubits for you to use. These +virtual qubits would correspond to the physical qubits available at this node, would this be a real physical +implementation of the quantum network node. By connecting to the virtual quantum node server, a client program that +depends on using quantum hardware, may manipulate these qubits as if they were local physical qubit and also instruct +to send them to remote nodes. + +The virtual quantum node servers are identified by their common names (eg Alice, Bob, Charlie), and amongst themselves +connect classically to form a virtual simulation network as a backend. + +An important internal element in SimulaQron is the distinction between virtual qubits and simulated qubits. Virtual +qubits are the qubits as they would be present in real quantum hardware. A virtual qubit is local to each virtual +quantum node server and may be manipulated there. Each virtual qubit is simulated somewhere, by a simulated qubit. +Importantly, this simulated qubit may be located at a different simulating node than the node holding the corresponding +virtual qubit. + +To see why this is necessary, note that entangled qubits cannot be locally represented by any form of classical +information (hence the quantum advantage of entanglement in the first place!). As such, if two (or more) virtual +nodes share qubits which are somehow entangled with each other, then these qubits will actually need to be simulated at just one of these nodes. That is, they appear to be virtually local (as if they were real physical qubits), yet they are actually simulated at just one of the network nodes. As you might imagine, this also means that if we want to perform an entangling gate between two qubits which are virtually local, but actually simulated in two different backend quantum registers, then a merge of these registers is required before the entangling gate can be executed. This is all handled transparently by the backend provided here. For programming network applications using SimulaQron you will not need to -deal with this backend simulation. +deal with this backend simulation. Nevertheless, as a guide to the backend, it consists of three essential components: -* quantumEngine - There are currenlty three different quantumEngines implemented: Using `QuTip `_ and mixed state, using `Project Q `_ and pure states and finally using stabilizer formalism. - This corresponds to one quantum register full of qubits across which gates can be performed. Should you wish to use a different backend, you may wish to add a different engine. +* quantumEngine - There are currenlty three different quantumEngines implemented: Using `QuTip `_ + and mixed state, using `Project Q `_ and pure states and finally using stabilizer formalism. + This corresponds to one quantum register full of qubits across which gates can be performed. Should you wish to + use a different backend, you may wish to add a different engine. The three current backends give different runtimes due to how quantum states are stored and manipulated. - In the stabilizer formalism, only Clifford operations can be performed and the simulation is in fact efficient in the number of qubits. - See the figure below for a comparison of runtimes to create a GHZ-state on a number of qubits using the three different backends. + In the stabilizer formalism, only Clifford operations can be performed and the simulation is in fact efficient + in the number of qubits. + See the figure below for a comparison of runtimes to create a GHZ-state on a number of qubits using the three + different backends. .. image:: figs/runtime_qutip_vs_projectq_vs_stabilizer.png :width: 700px :align: center :alt: Runtime of creating a GHZ-state using the three different backends currently implemented in SimulaQron. -* simulatedQubit - for each qubit simulated in that register, there is a simQubit object. This is local to each node. It exports remote method calls. These methods are only called by the virtual node network itself: when a virtual node discovers the qubit is actually simulated remotely, it passes on this call by calling the relevant method on the remote qubit object. +* simulatedQubit - for each qubit simulated in that register, there is a simQubit object. This is local to each node. + It exports remote method calls. These methods are only called by the virtual node network itself: when a virtual + node discovers the qubit is actually simulated remotely, it passes on this call by calling the relevant method on + the remote qubit object. -* virtualQubit - this is the object representing a virtually local qubit. This carries information about the remote simulating qubit. These virtualQubit objects can in turn be accessed by the clients who can access these virtual qubits as if they were real local physical qubits without having to know where they are actually simulated. That is, the client obtains a pointer to the relevant virtual qubit object on which it can perform operations directly. +* virtualQubit - this is the object representing a virtually local qubit. This carries information about the remote + simulating qubit. These virtualQubit objects can in turn be accessed by the clients who can access these virtual + qubits as if they were real local physical qubits without having to know where they are actually simulated. That + is, the client obtains a pointer to the relevant virtual qubit object on which it can perform operations directly. -* virtualNode - this is the local virtual node which accepts requests to get a virtual qubit object, send qubits to other nodes, read out the state of qubits, perform gates etc. +* virtualNode - this is the local virtual node which accepts requests to get a virtual qubit object, send qubits to + other nodes, read out the state of qubits, perform gates etc. * backEnd - starts up the virtual node backend @@ -87,9 +117,10 @@ Nevertheless, as a guide to the backend, it consists of three essential componen The local client engine ^^^^^^^^^^^^^^^^^^^^^^^ -The second part is a framework for writing applications that use the virtually simulated quantum -network. Such an application needs to connect locally to the virtual quantum node server simulating the underlying hardware (for programming -in native mode), or to the CQC interface. It is up to these applications to exchange any classical communication required to execute the protocol. +The second part is a framework for writing applications that use the virtually simulated quantum network. Such an +application needs to connect locally to the virtual quantum node server simulating the underlying hardware (for +programming in native mode), or to the NetQASM interface. It is up to these applications to exchange any classical +communication required to execute the protocol. -------------------------- @@ -100,7 +131,8 @@ Report bugs and contribute Bugs and feature requests ^^^^^^^^^^^^^^^^^^^^^^^^^^ -For bugs, feature requests, suggestions or other general questions please use GitHubs issue tracker in the repository (located under ‘Issues’ on the main page of the repository). +For bugs, feature requests, suggestions or other general questions please use GitHubs issue tracker in the +repository (located under ‘Issues’ on the main page of the repository). Please start your message with specifying one of the four labels below for easier handling of issues, for example:: Type: bug @@ -109,13 +141,17 @@ Please start your message with specifying one of the four labels below for easie Always provide enough information to assess the issue and separate different issues into different messages. -* *bug*: This is for bugs that are encountered. Please provide a way to reproduce the bug, preferably with a minimal example, and explain what goes wrong. +* *bug*: This is for bugs that are encountered. Please provide a way to reproduce the bug, preferably with a minimal + example, and explain what goes wrong. -* *feature request*: Is there a feature that you think should be provided? Explain the details of the feature and why you think this should be implemented. +* *feature request*: Is there a feature that you think should be provided? Explain the details of the feature and why + you think this should be implemented. -* *help wanted*: If there is something you having trouble with but which is not necessarily a bug. Also use this for other general questions. +* *help wanted*: If there is something you having trouble with but which is not necessarily a bug. Also use this for + other general questions. -* *suggestions*: If you have suggestions on how to improve the features already existing. Be clear on what exactly you think should be improved and explain why. +* *suggestions*: If you have suggestions on how to improve the features already existing. Be clear on what exactly + you think should be improved and explain why. ^^^^^^^^^^ Contribute @@ -123,9 +159,9 @@ Contribute If you would like to contribute with your own code to fix a bug or add an additional feature, this is most welcomed. Please then make a pull request on GitHub, which will be reviewed before approval. -For contributing use the *Develop*-branch. -Please make sure you run the automated tests below before submitting any code. -The easiest way to proceed is to: + +For contributing use the *Develop*-branch. Please make sure you run the automated tests below before submitting +any code. The easiest way to proceed is to: #. Fork the repository at the *Develop*-branch. #. Make the changes and commit these to your forked branch. diff --git a/docs/index.rst b/docs/index.rst index 47a62e01..6ebe54f5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -43,7 +43,7 @@ SimulaQron can be installed from pip by the command :code:`pip3 install simulaqr Overview GettingStarted ConfNodes - CQC + NetQASM Examples simulaqron From 46a549e2ba8d7661dbdbfea113a57d978d7c5612 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 18:08:22 +0100 Subject: [PATCH 186/277] Update all "TODO" notes to a warning in the final rendering --- docs/ConfNodes.rst | 5 +++-- docs/Examples.rst | 2 +- docs/GettingStarted.rst | 6 ++++-- docs/NetQASM.rst | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index 46d1706f..59769264 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -172,8 +172,9 @@ The options for the automatically generated topologies are currently: connected network on 10 nodes, can be specified as `random_connected_20`. Note that the number of edges for a network with :math:`n` nodes must be greater or equal to :math:`n-1` and less or equal to :math:`n(n-1)/1`. -TODO - Implement a command in the CLI to invoke the generation of topologies. -TODO - Document that CLI command. +.. warning:: Implement a command in the CLI to invoke the generation of topologies. + +.. warning:: Document that CLI command. Along with setting up the network with the specified topology a .png figure is also generated and stored as config/topology.png. This is useful if a random network is used, to easily visualize the network used. diff --git a/docs/Examples.rst b/docs/Examples.rst index b77780be..d95eba2b 100644 --- a/docs/Examples.rst +++ b/docs/Examples.rst @@ -12,7 +12,7 @@ The examples below assume that you have already made your way through :doc:`Gett node servers up and running, and ran the simple example of generating correlated randomness. Further examples can also be found in examples/nativeMode. -TODO - Update the link to the CQC interface. +.. warning:: Update the link to the CQC interface. .. note:: The 'native' mode is not the recommended way to program applications for SimulaQron, instead use the `NetQASM `_ interface. diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index 98dd4387..3e254182 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -75,7 +75,8 @@ Our objective will be to realize the following protocol which will generate 1 sh * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. -TODO - Update the link references and the names of the examples (NetQASM vs pythonLib) +.. warning:: Update the link references and the names of the examples (NetQASM vs pythonLib) + The examples can be found in the repo `pythonLib `_. Before seeing how this example works, let us simply run the code:: @@ -142,7 +143,8 @@ Similarly the code in bobTest.py read:: print("| "+to_print+" |") print("|"+"-"*(len(to_print)+2)+"|") -TODO - Update the link references and the names of the examples (NetQASM vs pythonLib) +.. warning:: Update the link references and the names of the examples (NetQASM vs pythonLib) + For further examples, see the examples/ folder and for the docs of the Python library see https://softwarequtech.github.io/CQC-Python/index.html. .. _settings: diff --git a/docs/NetQASM.rst b/docs/NetQASM.rst index 893be6ec..141abc4d 100644 --- a/docs/NetQASM.rst +++ b/docs/NetQASM.rst @@ -1,7 +1,7 @@ -DEPRECATED - The CQC interface +The NetQASM interface ================= -TODO - Update this document to explain the NetQASM interface! +.. warning:: Update this document to explain the NetQASM instead of the CQC interface! WARNING - The CQC interface ahas been deprecated, and it is no longer present in this release of SimulaQron. Please refer to the NetQASM interface documentation to create quantum applications. From 0d0f7c1a5f70b5253b34414cb91f11d07972765c Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 18:23:25 +0100 Subject: [PATCH 187/277] Fix warning when rendering documentation --- docs/CQC.rst | 66 ----------------------- docs/ConfNodes.rst | 3 +- docs/GettingStarted.rst | 116 +++++++++++++++++++++++++++------------- docs/NetQASM.rst | 2 +- 4 files changed, 82 insertions(+), 105 deletions(-) delete mode 100644 docs/CQC.rst diff --git a/docs/CQC.rst b/docs/CQC.rst deleted file mode 100644 index 346d85f3..00000000 --- a/docs/CQC.rst +++ /dev/null @@ -1,66 +0,0 @@ -DEPRECATED - The CQC interface -================= - -WARNING - The CQC interface ahas been deprecated, and it is no longer present in this release of SimulaQron. -Please refer to the NetQASM interface documentation to create quantum applications. - -SimulaQron can be access from any programming language supporting network connections. Instructions to the quantum hardware simulation can be sent via the CQC interface described `here `_. - -A `C `_, `Python `_, and `Rust `_ Library for programming SimulaQron using the CQC Interface are provided. If you are new to SimulaQron, programming via the Python CQC is the easiest way to get started. - -^^^^^^^^^^^^ -Installation -^^^^^^^^^^^^ - -If you have installed SimulaQron using pip, the cqc interface for python should already be installed. -If needed you can also only install the CQC interface in Python using pip by typing:: - - pip3 install cqc - -^^^^^ -Usage -^^^^^ - -The python library provides a way to program a protocol on a network where the nodes listen to instructions through the classical-quantum combiner (CQC) interface. In the following examples the network is simulated by SimulaQron. But the same examples could be executed on a network with real quantum hardware which allows for instructions through the CQC interface, which is the aim for the 2020 quantum internet demonstrator. - -To use the Python library you first need instantiate an object from the class :code:`cqc.pythonLib.CQCConnection`. This should be done in a `context `_, that is using a :code:`with`-statement as follows:: - - with CQCConnection("Alice") as alice: - # your program - -This is to make sure the CQCConnection is correctly closed by the end of the program and that the qubits in the backend are released, even if an error occurs in your program. - -.. note:: It is still possible to initialize a :code:`CQCConnection` in the old way, i.e. without :code:`with`, however you will receive a warning message everytime you create a qubit. - -Let's look at an extremely trivial example were we have the node `Alice` allocate a qubit, perform a Hadamard gate and measure the qubit:: - - with CQCConnection("Alice") as alice: - q = qubit(Alice) - q.H() - m = q.measure() - print(m) - -.. note:: If you do not specify the argument ``socket_address`` specifying the hostname and port of the cqc server receiving incoming CQC messages, you need to have simulaqron installed. The python library then tries to use the socket address of this nodes specified in simulaqron. - -A object from the :class:`qubit`-class is created with the :class:`CQCConnection` as argument, such that whenever an operation is applied to the qubit a CQC message will be sent to the simulation backend to actually perform this operation on the simulated qubit. -For more examples using the Python library see :doc:`GettingStarted` and https://softwarequtech.github.io/CQC-Python/examples.html - -.. _remoteNetwork: - ----------------------------------------- -Connecting to a remote simulated network ----------------------------------------- - -If a simulated network (consisting of virtual nodes and CQC servers) are setup on a remote computer (or on your own computer), CQC messages can be sent to the correct address and port numbers to control the nodes of the network. In this section we describe how to do this. - -Given the ip and port number of the CQC server of a node, you can send CQC messages over TCP using in any way you prefer. To know how these messages should look like to perform certain instructions, refer to https://softwarequtech.github.io/CQC-Python/interface.html - -An easier way to send CQC messages to a CQC server of a node is to use the provided Python library. -Assuming that you know the hostname and port number of the CQC server, you can then easily instantiate an object of the class :class:`~cqc.pythonLib.CQCConnection` which will communicate with the CQC server for you, using the CQC interface. -You can directly specify the ip and port number as follows:: - - cqc = CQCConnection("Alice", socket_address=("1.1.1.1", 8801)) - -More information on how to then actually allocating qubits, manipulating these and creating simulated entanglement see https://softwarequtech.github.io/CQC-Python/useful_commands.html - -We give some more detailed information below on how the classical communication between nodes in the application layer can be realized and also provide some useful commands to program a protocol using the Python library. diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index 59769264..edf54184 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -42,6 +42,7 @@ SimulaQron requires specifying a json-based network configuration. This configur and IP address/port tuples to correctly connect the SimulaQron simulations and classical communication sockets. For each configured node, you need to specify IP and address for 3 fields: + * The ``app_socket`` field, which specifies the IP and port for connecting classical communication sockets. * The ``qnodeos_socket`` field, which specifies the IP and port for connecting the QnodeOS server, used to interpret NetQASM objects. @@ -155,7 +156,7 @@ describing a network topology where Alice is adjacent to Bob, Bob is adjacent to adjacent to Bob. .. note:: Undirected topologies are also supported. That is, networks where for example Alice can send a qubit to -Bob but Bob cannot send a qubit to Alice. + Bob but Bob cannot send a qubit to Alice. --------------------------- Generate network topologies diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index 3e254182..06f4e0bb 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -5,13 +5,15 @@ Getting started Setup ----- -SimulaQron requires `Python 3.12 `_ along with the packages *cqc*, *twisted*, *numpy*, *scipy*, *networkx*, *flake8*, *click* and *daemons*. +SimulaQron requires `Python 3.12 `_ along with the packages *cqc*, *twisted*, *numpy*, *scipy*, +*networkx*, *flake8*, *click* and *daemons*. ^^^^^^^^^^^^^^^^^^^^^^ Installation using pip ^^^^^^^^^^^^^^^^^^^^^^ -The easiest way to install SimulaQron is using pip (requires MacOS or Linux). Start by creating and activating a python virtual environment:: +The easiest way to install SimulaQron is using pip (requires MacOS or Linux). Start by creating and activating a +python virtual environment:: python3.12 -m venv simulaqron-venv source simulaqron-venv/bin/activate @@ -20,7 +22,8 @@ Now, we can install SimulaQron by simply typing:: pip3 install simulaqron -You can then make use of SimulaQron using the command :code:`simulaqron` in the terminal. For more information on how to use this command see below or type:: +You can then make use of SimulaQron using the command :code:`simulaqron` in the terminal. For more information on how +to use this command see below or type:: simulaqron -h @@ -28,7 +31,8 @@ To make sure you have the version compatible with this documentation type:: simulaqron version -If you want to make sure that everything has been installed properly you can start run the unittests. Open a interactive python console by typing `python3` and the:: +If you want to make sure that everything has been installed properly you can start run the unittests. Open an +interactive python console by typing `python3` and the:: import simulaqron simulaqron.tests() @@ -37,28 +41,40 @@ If you want to make sure that everything has been installed properly you can sta Testing a simple example ------------------------ -Before delving into how to write any program yourself, let's first simply run one of the existing examples when programming SimulaQron through the Python library (see https://softwarequtech.github.io/CQC-Python/examples.html). -Remember from the Overview that SimulaQron has two parts: the first are the virtual node servers that act simulate the hardware at each node as well as the quantum communication between them in a transparent manner. -The second are the applications themselves which can be written in two ways, the direct way is to use the native mode using the Python Twisted framework connecting to the virtual node servers, see :doc:`Examples`. -The recommended way however is the use the NetQASM library that calls the virtual nodes by making use of the classical/quantum combiner interface. -We will here illustrate how to use SimulaQron with the NetQASM library. +Before delving into how to write any program yourself, let's first simply run one of the existing examples when +programming SimulaQron through the Python library (see https://softwarequtech.github.io/CQC-Python/examples.html). +Remember from the Overview that SimulaQron has two parts: the first are the virtual node servers that act simulate +the hardware at each node as well as the quantum communication between them in a transparent manner. +The second are the applications themselves which can be written in two ways, the direct way is to use the native +mode using the Python Twisted framework connecting to the virtual node servers, see :doc:`Examples`. +The recommended way however is the use the NetQASM library that calls the virtual nodes by making use of the +çclassical/quantum combiner interface. We will here illustrate how to use SimulaQron with the NetQASM library. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Starting the SimulaQron backend ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -By default SimulaQron uses the five nodes Alice, Bob, Charlie, David and Eve on your local computers. In this example there will be three processes for each node listening to incoming messages on a certain port number. These make up the simulation backend, the NetQASM server and the classical communication server. To start the processes and thus the backend of SimulaQron simply type:: +By default SimulaQron uses the five nodes Alice, Bob, Charlie, David and Eve on your local computers. In this example +there will be three processes for each node listening to incoming messages on a certain port number. These make up +the simulation backend, the NetQASM server and the classical communication server. To start the processes and thus +the backend of SimulaQron simply type:: simulaqron start -.. warning:: Running :code:`simulaqron start` will be default start up servers on localhost (i.e., your own computer), using port numbers between 8000 and 9000, to form the simulated quantum internet hardware. SimulaQron does not provide any access control to its simulated hardware, so you are responsible to securing access should this be relevant for you. You can also run the different simulated nodes on different computers. We do not take any responsibility for problems caused by SimulaQron. +.. warning:: Running :code:`simulaqron start` will be default start up servers on localhost (i.e., your own computer), + using port numbers between 8000 and 9000, to form the simulated quantum internet hardware. SimulaQron does not + provide any access control to its simulated hardware, so you are responsible to securing access should this be + relevant for you. You can also run the different simulated nodes on different computers. We do not take any + responsibility for problems caused by SimulaQron. -For more information on what :code:`./cli/SimulaQron start` does, how to change the nodes and the ports of the network, the topology etc, see :doc:`ConfNodes`. +For more information on what :code:`./cli/SimulaQron start` does, how to change the nodes and the ports of the network, +the topology etc, see :doc:`ConfNodes`. To stop the backend, simply type:: simulaqron stop -If something went wrong (for example the process was killed before you stopped it) there might be leftover files which makes SimulaQron think that the network is still running. To reset this you can type:: +If something went wrong (for example the process was killed before you stopped it) there might be leftover files which +makes SimulaQron think that the network is still running. To reset this you can type:: simulaqron reset @@ -68,10 +84,13 @@ Note that this also kills any currently running network and resets any settings Running a protocol ^^^^^^^^^^^^^^^^^^^ -Having started the virtual quantum nodes as above, let us now run a simple test application, which already illustrates some of the aspects in realizing protocols. -Our objective will be to realize the following protocol which will generate 1 shared random bit between Alice and Bob. Evidently, there would be classical means to achieve this trivial task chosen for illustration. +Having started the virtual quantum nodes as above, let us now run a simple test application, which already illustrates +some of the aspects in realizing protocols. +Our objective will be to realize the following protocol which will generate 1 shared random bit between Alice and Bob. +Evidently, there would be classical means to achieve this trivial task chosen for illustration. -* Alice and Bob generates one EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` +* Alice and Bob generates one EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form + :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. @@ -88,16 +107,20 @@ You should be seeing the following two lines:: App Alice: Measurement outcome is: 0/1 App Bob: Measurement outcome is: 0/1 -Note that the order of these two lines may differ, as it does not matter who measures first. So what is actually going on here? Let us first look at how we will realize the example by making an additional step (3) explicit: +Note that the order of these two lines may differ, as it does not matter who measures first. So what is actually +going on here? Let us first look at how we will realize the example by making an additional step (3) explicit: -* Alice and Bob generate one EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` +* Alice and Bob generate one EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form + :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` * Alice and Bob are informed of the identifiers of the qubits and are informed that entanglement was generated. * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. -While the task we want to realize here is completely trivial, the addition of step 3 does however already highlight a range of choices on how to realize step 3 and the need to find good abstractions to allow easy application development. -One way to realize step 3 would be to hardwire Alice's and Bob's measurements: if the hardware can identify the correct qubits from the entanglement generation, then we could instruct it to measure it immediately without asking for a notification from the entanglement generation process. It is clear that in a network that is a bit larger than our tiny three node setup, identifying the right setup requires a link between the underlying qubits and classical control information: this is the objective of the classical/quantum combiner. +While the task we want to realize here is completely trivial, the addition of step 3 does however already highlight a +range of choices on how to realize step 3 and the need to find good abstractions to allow easy application development. +One way to realize step 3 would be to hardwire Alice's and Bob's measurements: if the hardware can identify the +correct qubits from the entanglement generation, then we could instruct it to measure it immediately without asking for a notification from the entanglement generation process. It is clear that in a network that is a bit larger than our tiny three node setup, identifying the right setup requires a link between the underlying qubits and classical control information: this is the objective of the classical/quantum combiner. The script run.sh executes the following two python scripts:: @@ -107,10 +130,12 @@ The script run.sh executes the following two python scripts:: python3 bobTest.py & Let us now look at the programs for Alice and Bob. -We first initialize an object of the class ``NetQASMConnection`` which will do all the communication to the virtual through the NetQASM interface. + +We first initialize an object of the class ``NetQASMConnection`` which will do all the communication to the virtual +through the NetQASM interface. Qubits can then be created by initializing a qubit-object, which takes a ``NetQASMConnection`` as an input. -On these qubits operations can be applied and they can also be sent to other nodes in the network by use of the ``NetQASMConnection``. -The full code in aliceTest.py is:: +On these qubits operations can be applied and they can also be sent to other nodes in the network by use of the +``NetQASMConnection``. The full code in aliceTest.py is:: # Create an EPR Socket between "Alice" and "Bob" epr_socket = EPRSocket("Alice", "Bob") @@ -145,7 +170,8 @@ Similarly the code in bobTest.py read:: .. warning:: Update the link references and the names of the examples (NetQASM vs pythonLib) -For further examples, see the examples/ folder and for the docs of the Python library see https://softwarequtech.github.io/CQC-Python/index.html. +For further examples, see the examples/ folder and for the docs of the Python library see +https://softwarequtech.github.io/CQC-Python/index.html. .. _settings: @@ -161,7 +187,8 @@ To set a setting, for example to use the projectQ backend, type:: simulaqron set backend projectq -This will create a file named ``simulaqron_settings.json`` in the current folder. This new file contains a full set of simulaqron configuration, including the setting that was just configured (using the `projectq` backend, in the example). +This will create a file named ``simulaqron_settings.json`` in the current folder. This new file contains a full set of +simulaqron configuration, including the setting that was just configured (using the `projectq` backend, in the example). It is also possible to manually create this file:: @@ -170,7 +197,8 @@ It is also possible to manually create this file:: "log_level": 10 } -which would set the backend to be use ProjectQ and the log-level to be debug (10). Any setting in this file will override the settings set in the CLI. +which would set the backend to be use ProjectQ and the log-level to be debug (10). Any setting in this file will +override the settings set in the CLI. Is is also possible to create a configuration file that contains all the default configurations:: @@ -195,19 +223,24 @@ This command will create a file with the following configuration:: Section :ref: `settings_fields` provides a description about each one of the configuration options in the file. -Alternatively, you can place the ``simulaqron_settings.json`` file in the folder ``~/.simulaqron`` (i.e. a folder named ``.simulaqron`` in your home folder). Doing so will make your settings persist across different projects you implement using simulaqron. +Alternatively, you can place the ``simulaqron_settings.json`` file in the folder ``~/.simulaqron`` (i.e. a folder +named ``.simulaqron`` in your home folder). Doing so will make your settings persist across different projects you +implement using simulaqron. -.. note:: Settings needs to be set before starting the SimulaQron backend. If the backend is already running, stop it, set the settings and start it again. +.. note:: Settings needs to be set before starting the SimulaQron backend. If the backend is already running, stop + it, set the settings and start it again. ^^^^^^^^^^^^^^^^^^^ Settings precedence ^^^^^^^^^^^^^^^^^^^ -Since the simulaqron configuration file can be placed in several places, SimulaQron will follow a priority for reading the settings: +Since the simulaqron configuration file can be placed in several places, SimulaQron will follow a priority for +reading the settings: * Settings file placed in the current working folder. * Settings file placed in the ``~/.simulaqron`` folder. -* If none of the above is found, SimulaQron will create a settings file in the ``~/.simulaqron`` folder, then it will try to load it in that place. +* If none of the above is found, SimulaQron will create a settings file in the ``~/.simulaqron`` folder, then it + will try to load it in that place. .. _settings_fields: @@ -224,14 +257,23 @@ The SimulaQron settings file contains a set of fields to control the configurati * ``recv_timeout``: Maximum number of milliseconds to wait for the messages when trying to create EPR pairs. * ``recv_retry_time``: Maximum number of milliseconds to wait between attempts to create EPR pairs. * ``recv_max_retries``: Maximum number of tries to attempt when creating EPR pairs. -* ``log_level``: The log level to use for SimulaQron. The integer value in this field must match the values exposed by the python ``logging`` package. For more information about the specific values for each logging level, please check the `official python documentation for the logging package `_. -* ``sim_backend``: The backend qubit simulation that SimulaQron will use to emulate qubits. Currently, three backends are supported: "projectq", "qutip" and "stabilizer". -* ``noisy_qubits``: Whether to enable noisy qubits simulation or not. Setting this to ``true``will randomly apply a Pauli gate after every operation, emulating noise on the qubit backend. -* ``max_app_waiting_time``: Maximum time (in seconds) to wait before considering the running application as stalled. A value of ``-1.0`` will disable the stalling waiting time, allowing SimulaQron to wait undefinitely. -* ``t1``: T1 parameter to use when applying noise on the emulated qubits. This value is only used when the ``noisy_qubit`` option is set to ``true``. +* ``log_level``: The log level to use for SimulaQron. The integer value in this field must match the values exposed + by the python ``logging`` package. For more information about the specific values for each logging level, please + check the `official python documentation for the logging package `_. +* ``sim_backend``: The backend qubit simulation that SimulaQron will use to emulate qubits. Currently, three backends + are supported: "projectq", "qutip" and "stabilizer". +* ``noisy_qubits``: Whether to enable noisy qubits simulation or not. Setting this to ``true`` will randomly apply a + Pauli gate after every operation, emulating noise on the qubit backend. +* ``max_app_waiting_time``: Maximum time (in seconds) to wait before considering the running application as stalled. + A value of ``-1.0`` will disable the stalling waiting time, allowing SimulaQron to wait undefinitely. +* ``t1``: T1 parameter to use when applying noise on the emulated qubits. This value is only used when the + ``noisy_qubit`` option is set to ``true``. The default value of all these fields can be seen in the :ref: `settings` section. -.. note:: An application can become "stalled" in certain configurations, leaving the application to look "hung". This leads to a deadlock of the application. SimulaQron will wait for the configured time before considering the application as "stalled" and kill all the processes. +.. note:: An application can become "stalled" in certain configurations, leaving the application to look "hung". This + leads to a deadlock of the application. SimulaQron will wait for the configured time before considering the + application as "stalled" and kill all the processes. -.. warning:: Please correctly configure the ``max_app_waiting_time`` to allow your application to wait for any potential "slow" peers. A low value on this field might lead SimulaQron to killing your application prematurely. +.. warning:: Please correctly configure the ``max_app_waiting_time`` to allow your application to wait for any + potential "slow" peers. A low value on this field might lead SimulaQron to killing your application prematurely. diff --git a/docs/NetQASM.rst b/docs/NetQASM.rst index 141abc4d..8f18386a 100644 --- a/docs/NetQASM.rst +++ b/docs/NetQASM.rst @@ -1,5 +1,5 @@ The NetQASM interface -================= +===================== .. warning:: Update this document to explain the NetQASM instead of the CQC interface! From 407c29a87c5d933c803ff7049fd9b9c35ad844b9 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 20:20:50 +0100 Subject: [PATCH 188/277] Update documentation of th native mode examples --- docs/NativeModeCorrRng.rst | 225 ++++++------ docs/NativeModeGraphState.rst | 619 +++++++++++++++++----------------- docs/NativeModeTeleport.rst | 335 +++++++++--------- docs/NativeModeTemplate.rst | 267 ++++++++------- 4 files changed, 744 insertions(+), 702 deletions(-) diff --git a/docs/NativeModeCorrRng.rst b/docs/NativeModeCorrRng.rst index b431d09f..f3a4378d 100644 --- a/docs/NativeModeCorrRng.rst +++ b/docs/NativeModeCorrRng.rst @@ -1,139 +1,168 @@ Generate correlated randomness ============================== -Having started the virtual quantum nodes, let us now run a simple test application, which already illustrates some of the aspects in realizing protocols. -Our objective will be to realize the following protocol which will generate 1 shared random bit between Alice and Bob. Evidently, there would be classical means to achieve this trivial task chosen for illustration. +.. warning:: Update the code of the examples below!!! -* Alice generates 1 EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` +Having started the virtual quantum nodes, let us now run a simple test application, which already illustrates some of +the aspects in realizing protocols. Our objective will be to realize the following protocol which will generate 1 +shared random bit between Alice and Bob. Evidently, there would be classical means to achieve this trivial task chosen +for illustration. +* Alice generates 1 EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form + :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` * She sends qubit :math:`B` to Bob. - * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. Before seeing how this example works, let us again simply run the code:: - cd examples/nativeMode/corrRNG - sh doNew.sh + cd examples/nativeMode/corrRNG + sh doNew.sh Next to a considerable about of debugging information, you should be seeing the following two lines:: - ALICE: My Random Number is 0/1 - BOB: My Random Number is 0/1 - -Note that the order of these two lines may differ, as it does not matter who measures first. So what is actually going on here ? Let us first look at how we will realize the example by making an additional step (3) explicit: + ALICE: My Random Number is 0/1 + BOB: My Random Number is 0/1 -* Alice generates 1 EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` +Note that the order of these two lines may differ, as it does not matter who measures first. So what is actually +going on here ? Let us first look at how we will realize the example by making an additional step (3) explicit: +* Alice generates 1 EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form + :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` * She sends qubit :math:`B` to Bob. - -* Bob is informed of the identifier of the qubit and is informed it has arrived. - +* Bob is informed of the identifier if the qubit and is informed it has arrived. * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. -While the task we want to realize here is completely trivial, the addition of step 3 does however already highlight a range of choices on how to realize step 3 and the need to find good abstractions to allow easy application development. -One way to realize step 3 would be to hardwire Bobs measurement: if the hardware can identify the correct qubit from Alice, then we could instruct it to measure it immediately without asking for a notification from Alice. It is clear that in a network that is a bit larger than our tiny three node setup, identifying the right setup requires a link between the underlying qubits and classical control information: this is the objective of the classical/quantum combiner, for which we will provide code in version 0.2 of SimulaQron. +While the task we want to realize here is completely trivial, the addition of step 3 does however already highlight a +range of choices on how to realize step 3 and the need to find good abstractions to allow easy application development. +One way to realize step 3 would be to hardwire Bobs measurement: if the hardware can identify the correct qubit from +Alice, then we could instruct it to measure it immediately without asking for a notification from Alice. +This version simply allows a completely barebones access to the virtual nodes without implementing such convenient +abstractions in order to allow you to explore such possibilities. To this end, we will here actually implement the +following protocol for mere illustration purposes. We emphasize that this would be inefficient on a real quantum +network since it requires Bob to store his qubit until Alice's control message arrives, which can be a significant +delay causing the qubit to decohere in the meantime. -This version simply allows a completely barebones access to the virtual nodes without implementing such convenient abstractions in order to allow you to explore such possibilities. To this end, we will here actually implement the following protocol for mere illustration purposes. We emphasize that this would be inefficient on a real quantum network since it requires Bob to store his qubit until Alice's control message arrives, which can be a significant delay causing the qubit to decohere in the meantime. - -* Alice generates 1 EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` - +* Alice generates 1 EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form + :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` * She sends qubit :math:`B` to Bob. - * Alice sends Bob the correct identifier of the qubit, and tells him to measure it. - * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. -To realize this, we thus need not only the connection to the virtual quantum node servers, but Alice and Bob themselves need to run a client/server to exchange classical control information. Before looking at the code, we node that the setup of these servers is again determined by a configuration file, namely config/classicalNet.cfg. This file defines which nodes act as servers in the classical communication network listening for control information to execute the protocol. You want to copy this to whatever example you are running. It takes the same format as above, where in our example only Bob will act run a server:: - - # Configuration file for servers on the classical communication network - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - # - - Bob, localhost, 8812 - -The first thing that happens if we execute the script doNew.sh is that after some setting up it will call run.sh, executing:: - - #!/bin/sh - - python3 bobTest.py & - python3 aliceTest.py - -Let us now look at the programs for Alice and Bob. Alice will merely run a client on the classical communication network that connects to Bob to be found in aliceTest.py. Using the template (see general Examples section) which establishes the connections to the local virtual nodes, we thus need to provide client code for Alice to implement the protocol above. The function runClientNode will automatically be executed once Alice connected to her local virtual quantum node simulating the underlying hardware, and to Bob's server:: - - ##################################################################################################### - # - # runClientNode - # - # This will be run on the local node if all communication links are set up (to the virtual node - # quantum backend, as well as the nodes in the classical communication network), and the local classical - # communication server is running (if applicable). - # - @inlineCallbacks - def runClientNode(qReg, virtRoot, myName, classicalNet): - """ - Code to execute for the local client node. Called if all connections are established. +To realize this, we thus need not only the connection to the virtual quantum node servers, but Alice and Bob +themselves need to run a client/server to exchange classical control information. Before looking at the code, we +know that the setup of these servers is again determined by a configuration file, namely ``classicalNet.json``. +This file defines which nodes act as servers in the classical communication network listening for control information +to execute the protocol. You want to copy this to whatever example you are running. It takes the same format as +above, where in our example only Bob will act run a server:: + + [ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + } + ], + "topology": null + } + ] + +The first thing that happens if we execute the script doNew.sh is that after some setting up it will call run.sh, +executing:: + + #!/bin/sh + + python3 bobTest.py & + python3 aliceTest.py + +Let us now look at the programs for Alice and Bob. Alice will merely run a client on the classical communication +network that connects to Bob to be found in aliceTest.py. Using the template (see general Examples section) which +establishes the connections to the local virtual nodes, we thus need to provide client code for Alice to implement +the protocol above. The function runClientNode will automatically be executed once Alice connected to her local +virtual quantum node simulating the underlying hardware, and to Bob's server:: + + ##################################################################################################### + # + # runClientNode + # + # This will be run on the local node if all communication links are set up (to the virtual node + # quantum backend, as well as the nodes in the classical communication network), and the local classical + # communication server is running (if applicable). + # + @inlineCallbacks + def runClientNode(qReg, virtRoot, myName, classicalNet): + """ + Code to execute for the local client node. Called if all connections are established. - Arguments - qReg quantum register (twisted object supporting remote method calls) - virtRoot virtual quantum ndoe (twisted object supporting remote method calls) - myName name of this node (string) - classicalNet servers in the classical communication network (dictionary of hosts) - """ + Arguments + qReg quantum register (twisted object supporting remote method calls) + virtRoot virtual quantum ndoe (twisted object supporting remote method calls) + myName name of this node (string) + classicalNet servers in the classical communication network (dictionary of hosts) + """ - logging.debug("LOCAL %s: Runing client side program.",myName) + logging.debug("LOCAL %s: Runing client side program.",myName) - # Create 2 qubits - qA = yield virtRoot.callRemote("new_qubit_inreg",qReg) - qB = yield virtRoot.callRemote("new_qubit_inreg",qReg) + # Create 2 qubits + qA = yield virtRoot.callRemote("new_qubit_inreg",qReg) + qB = yield virtRoot.callRemote("new_qubit_inreg",qReg) - # Put qubits A and B in a maximally entangled state - yield qA.callRemote("apply_H") - yield qA.callRemote("cnot_onto",qB) + # Put qubits A and B in a maximally entangled state + yield qA.callRemote("apply_H") + yield qA.callRemote("cnot_onto",qB) - # Send qubit B to Bob - # Instruct the virtual node to transfer the qubit - remoteNum = yield virtRoot.callRemote("send_qubit",qB, "Bob") + # Send qubit B to Bob + # Instruct the virtual node to transfer the qubit + remoteNum = yield virtRoot.callRemote("send_qubit",qB, "Bob") - # Tell Bob the ID of the qubit, and ask him to measure - bob = classicalNet.hostDict["Bob"] - yield bob.root.callRemote("process_qubit", remoteNum) + # Tell Bob the ID of the qubit, and ask him to measure + bob = classicalNet.hostDict["Bob"] + yield bob.root.callRemote("process_qubit", remoteNum) - # Measure qubit A to obtain a random number - x = yield qA.callRemote("measure") - print("ALICE: My Random Number is ",x,"\n") + # Measure qubit A to obtain a random number + x = yield qA.callRemote("measure") + print("ALICE: My Random Number is ",x,"\n") - reactor.stop() + reactor.stop() -Let us now look at Bob's server program to be found in bobTest.py. Observe that Alice will call process_qubit above. Not included in the code below are several standard methods that require no change to be used in examples.:: +Let us now look at Bob's server program to be found in bobTest.py. Observe that Alice will call process_qubit above. +Not included in the code below are several standard methods that require no change to be used in examples.:: - ##################################################################################################### - # - # localNode - # - # This will be run if the local node acts as a server on the classical communication network, - # accepting remote method calls from the other nodes. + ##################################################################################################### + # + # localNode + # + # This will be run if the local node acts as a server on the classical communication network, + # accepting remote method calls from the other nodes. - class localNode(pb.Root): + class localNode(pb.Root): - # This can be called by Alice to tell Bob to process the qubit - @inlineCallbacks - def remote_process_qubit(self, virtualNum): - """ - Recover the qubit and measure it to get a random number. + # This can be called by Alice to tell Bob to process the qubit + @inlineCallbacks + def remote_process_qubit(self, virtualNum): + """ + Recover the qubit and measure it to get a random number. - Arguments - virtualNum number of the virtual qubit corresponding to the EPR pair received - """ + Arguments + virtualNum number of the virtual qubit corresponding to the EPR pair received + """ - qB = yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) + qB = yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) - # Measure - x = yield qB.callRemote("measure") + # Measure + x = yield qB.callRemote("measure") - print("BOB: My Random Number is ", x, "\n") + print("BOB: My Random Number is ", x, "\n") diff --git a/docs/NativeModeGraphState.rst b/docs/NativeModeGraphState.rst index cb3aa001..8ca6e7f7 100644 --- a/docs/NativeModeGraphState.rst +++ b/docs/NativeModeGraphState.rst @@ -1,8 +1,11 @@ Distributing a graph state ========================== +.. warning:: Update the code of the examples below!!! + Here we consider a more complicated example, where we have four parties; Alice, Bob, Charlie and David. -They will distribute a graph state and transform this with local operations and classical communication to make a GHZ-like state. +They will distribute a graph state and transform this with local operations and classical communication to make +a GHZ-like state. Finally they measure their qubits in the correct bases to achieved correlated outcomes. (Note that this is not a efficient way to distribute a GHZ-state but an example illustrating how to use SimulaQron) @@ -10,20 +13,25 @@ Finally they measure their qubits in the correct bases to achieved correlated ou An overview ------------ -We will first give the main idea of the protocol by describing the evolution of the graph describing the graph state shared by the parties. +We will first give the main idea of the protocol by describing the evolution of the graph describing the graph +state shared by the parties. The actual order of the operations performed will be different in the actual implementation and is described below. -For more information on definition of graph states and their transformation under local operations see https://arxiv.org/abs/quant-ph/0602096. +For more information on definition of graph states and their transformation under local operations see +https://arxiv.org/abs/quant-ph/0602096. -A graph state described by a star graph is a GHZ-state up to Hadamard operations on the qubits which are the leaves of the graph. -The parties will therefore transform the shared state to make a star graph. -Alice, Bob, Charlie and David generate a graph state :math:`|P_4\rangle`, i.e. a graph state described by a path graph on four vertices, where Alice and David are the ends of the path. +A graph state described by a star graph is a GHZ-state up to Hadamard operations on the qubits which are the leaves +of the graph. The parties will therefore transform the shared state to make a star graph. +Alice, Bob, Charlie and David generate a graph state :math:`|P_4\rangle`, i.e. a graph state described by a path +graph on four vertices, where Alice and David are the ends of the path. The edge-set of this graph is .. math:: \{(A,B),(B,C),(C,D)\} -where :math:`A`, :math:`B`, :math:`C` and :math:`D` are the vertices corresponding to the parties Alice, Bob, Charlie and David, respectively. -Local Clifford operations are performed at :math:`B`, :math:`C` and :math:`D` which induces a graph operation called a local complementation at :math:`C` and therefore adds the edge :math:`(B,D)` to the graph. +where :math:`A`, :math:`B`, :math:`C` and :math:`D` are the vertices corresponding to the parties Alice, Bob, Charlie +and David, respectively. Local Clifford operations are performed at :math:`B`, :math:`C` and :math:`D` which induces +a graph operation called a local complementation at :math:`C` and therefore adds the edge :math:`(B,D)` to the graph. The graph is now a star graph with an additional edge :math:`(C,D)`. + This edge will be removed by the use of an additional qubit :math:`E` generated by David. David will entangle his qubit :math:`D` with this new qubit and therefore adding the edge :math:`(D,E)` to the graph. Qubit :math:`E` is then sent to Charlie which also entangles this with his qubit. @@ -31,9 +39,10 @@ The edge-set is then .. math:: \{(A,B),(B,C),(C,D),(B,D),(D,E),(C,E)(\} -Local Cliffords are performed at :math:`C`, :math:`D` and :math:`E` which induces a local complementation at :math:`E` and therefore removes the edge :math:`(C,D)`. -Finally qubit :math:`E` is measured in the standard basis to disconnect is from the rest of the graph state. -Depending on the measurement outcome, corrections are performed at :math:`C` and :math:`D`. +Local Cliffords are performed at :math:`C`, :math:`D` and :math:`E` which induces a local complementation at :math:`E` +and therefore removes the edge :math:`(C,D)`. Finally qubit :math:`E` is measured in the standard basis to disconnect +is from the rest of the graph state. Depending on the measurement outcome, corrections are performed at :math:`C` and +:math:`D`. ------------ The protocol @@ -43,100 +52,84 @@ We now describe the operations performed in the protocol, which effectively indu Although the order described here is slightly different the end result is still the same, since local operations commute. * Alice performs the following operations. - - #. Alice prepares two qubits in the state :math:`|+\rangle_A |+\rangle_B`. - - #. Alice performs a CPHASE operation between :math:`A` and :math:`B`. - - #. Alice sends :math:`B` to Bob. - - #. Alice measures qubit :math:`A` in the :math:`X`-basis. - + #. Alice prepares two qubits in the state :math:`|+\rangle_A |+\rangle_B`. + #. Alice performs a CPHASE operation between :math:`A` and :math:`B`. + #. Alice sends :math:`B` to Bob. + #. Alice measures qubit :math:`A` in the :math:`X`-basis. * Bob performs the following operations. - - #. Bob receives qubit :math:`B` from Alice. - - #. Bob prepares a qubit in the state :math:`|+\rangle_C`. - - #. Bob performs a CPHASE operation between :math:`B` and :math:`C`. - - #. Bob performs the operation :math:`\exp(\frac{\mathrm{i}\pi}{4}Z)` on :math:`B` (one of the operations to induced a local complementation at :math:`C`). - - #. Bob sends :math:`C` to Charlie. - - #. Bob measures qubit :math:`B` in the :math:`Z`-basis. - + #. Bob receives qubit :math:`B` from Alice. + #. Bob prepares a qubit in the state :math:`|+\rangle_C`. + #. Bob performs a CPHASE operation between :math:`B` and :math:`C`. + #. Bob performs the operation :math:`\exp(\frac{\mathrm{i}\pi}{4}Z)` on :math:`B` (one of the operations + to induced a local complementation at :math:`C`). + #. Bob sends :math:`C` to Charlie. + #. Bob measures qubit :math:`B` in the :math:`Z`-basis. * Charlie performs the following operations - - #. Charlie receives qubit :math:`C` from Bob - - #. Charlie prepares a qubit in the state :math:`|+\rangle_D` - - #. Charlie performs a CPHASE operation between :math:`C` and :math:`D`. - - #. Charlie performs the operations :math:`\exp(-\frac{\mathrm{i}\pi}{4}X)` and :math:`\exp(\frac{\mathrm{i}\pi}{4}Z)` on :math:`C` and :math:`D`, respectively (two of the operations to induced a local complementation at :math:`C`). - - #. Charlie sends :math:`D` to David. - - #. Charlie receives qubit :math:`E` from David - - #. Charlie performs a CPHASE operation between :math:`E` and :math:`C`. - - #. Charlie performs the operations :math:`\exp(-\frac{\mathrm{i}\pi}{4}X)` and :math:`\exp(\frac{\mathrm{i}\pi}{4}Z)` on :math:`E` and :math:`C`, respectively (two of the operations to induced a local complementation at :math:`E`). - - #. Charlie measures qubit :math:`E` in the :math:`Z`-basis. - - #. Charlie performs a :math:`Z`-operation on :math:`C` if the measurement outcome is :math:`1` and does nothing if it is :math:`0`. - - #. Charlie sends the measurement outcome to David. - - #. Charlie measures qubit :math:`C` in the :math:`X`-basis. - + #. Charlie receives qubit :math:`C` from Bob + #. Charlie prepares a qubit in the state :math:`|+\rangle_D` + #. Charlie performs a CPHASE operation between :math:`C` and :math:`D`. + #. Charlie performs the operations :math:`\exp(-\frac{\mathrm{i}\pi}{4}X)` and + :math:`\exp(\frac{\mathrm{i}\pi}{4}Z)` on :math:`C` and :math:`D`, respectively (two of the operations to + induced a local complementation at :math:`C`). + #. Charlie sends :math:`D` to David. + #. Charlie receives qubit :math:`E` from David + #. Charlie performs a CPHASE operation between :math:`E` and :math:`C`. + #. Charlie performs the operations :math:`\exp(-\frac{\mathrm{i}\pi}{4}X)` and + :math:`\exp(\frac{\mathrm{i}\pi}{4}Z)` on :math:`E` and :math:`C`, respectively (two of the operations to + induced a local complementation at :math:`E`). + #. Charlie measures qubit :math:`E` in the :math:`Z`-basis. + #. Charlie performs a :math:`Z`-operation on :math:`C` if the measurement outcome is :math:`1` and does nothing + if it is :math:`0`. + #. Charlie sends the measurement outcome to David. + #. Charlie measures qubit :math:`C` in the :math:`X`-basis. * David performs the following operations - - #. David receives qubit :math:`D` from Charlie - - #. David prepares a qubit in the state :math:`|+\rangle_E` - - #. David performs a CPHASE operation between :math:`D` and :math:`E` and sends :math:`E` to Bob. - - #. David performs the operation :math:`\exp(\frac{\mathrm{i}\pi}{4}Z)` on :math:`D` (one of the operations to induced a local complementation at :math:`E`). - - #. David receives the measurement outcome from Charlie and performs a :math:`Z`-operation on :math:`D` if this is :math:`1` and nothing if it is :math:`0`. - - #. David measures qubit :math:`D` in the :math:`X`-basis. + #. David receives qubit :math:`D` from Charlie + #. David prepares a qubit in the state :math:`|+\rangle_E` + #. David performs a CPHASE operation between :math:`D` and :math:`E` and sends :math:`E` to Bob. + #. David performs the operation :math:`\exp(\frac{\mathrm{i}\pi}{4}Z)` on :math:`D` (one of the operations to + induced a local complementation at :math:`E`). + #. David receives the measurement outcome from Charlie and performs a :math:`Z`-operation on :math:`D` if this + is :math:`1` and nothing if it is :math:`0`. + #. David measures qubit :math:`D` in the :math:`X`-basis. ----------- Setting up ----------- -We will run everything locally (localhost) using the standard virtualNodes.cfg file found in config that define the virtual quantum nodes run in the background to simulate the quantum hardware:: - - # Network configuration file - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - - Alice, localhost, 8801 - Bob, localhost, 8802 - Charlie, localhost, 8803 - David, localhost, 8804 - -As we can see from the proocol above, Alice is the one that initializes the protocol and the others listen. We will therefore run a client at Alice and servers at Bob, Charlie and David. Since we run everything locally, we may thus use for the configuration file classicalNet.cfg:: - - # Network configuration file - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - # - - Bob, localhost, 8812 - Charlie, localhost, 8813 - David, localhost, 8814 +.. warning:: Update the CFG files into a JSON ones and paste them here! +.. warning:: Re-write this description to use the single JSON file, and get the classical sockets config using the + the SocketsConfig builder + filter method. + +We will run everything locally (localhost) using the standard virtualNodes.json file found in config that define the +virtual quantum nodes run in the background to simulate the quantum hardware:: + + # Network configuration file + # + # For each host its informal name, as well as its location in the network must + # be listed. + # + # [name], [hostname], [port number] + + Alice, localhost, 8801 + Bob, localhost, 8802 + Charlie, localhost, 8803 + David, localhost, 8804 + +As we can see from the protocol above, Alice is the one that initializes the protocol and the others listen. We will +therefore run a client at Alice and servers at Bob, Charlie and David. Since we run everything locally, we may thus +use for the configuration file classicalNet.json:: + + # Network configuration file + # + # For each host its informal name, as well as its location in the network must + # be listed. + # + # [name], [hostname], [port number] + # + + Bob, localhost, 8812 + Charlie, localhost, 8813 + David, localhost, 8814 Let us now provide the actual program code for all the parties. @@ -146,288 +139,292 @@ Programming Alice Since Alice acts as a client, we will only need to fill in runClientNode. This gives:: - ##################################################################################################### - # - # runClientNode - # - # This will be run on the local node if all communication links are set up (to the virtual node - # quantum backend, as well as the nodes in the classical communication network), and the local classical - # communication server is running (if applicable). - # - @inlineCallbacks - def runClientNode(qReg, virtRoot, myName, classicalNet): - """ - Code to execute for the local client node. Called if all connections are established. - - Arguments - qReg quantum register (twisted object supporting remote method calls) - virtRoot virtual quantum ndoe (twisted object supporting remote method calls) - myName name of this node (string) - classicalNet servers in the classical communication network (dictionary of hosts) - """ - - logging.debug("LOCAL %s: Runing client side program.",myName) - - #Create 2 qubits - qA = yield virtRoot.callRemote("new_qubit_inreg",qReg) - qB = yield virtRoot.callRemote("new_qubit_inreg",qReg) - - #Make 2-qubit graph state - yield qA.callRemote("apply_H") - yield qB.callRemote("apply_H") - yield qA.callRemote("cphase_onto",qB) - - #send qubit B to Bob - #instruct virtual node to transfer qubit - remoteNum = yield virtRoot.callRemote("send_qubit",qB,"Bob") - logging.debug("LOCAL %s: Remote qubit is %d.",myName,remoteNum) - - #Tell number of virtual qubit to Bob and receive measurement outcome parity - bob=classicalNet.hostDict["Bob"] - yield bob.root.callRemote("receive_qubit",remoteNum) - - #Measure qubit (X-basis) - yield qA.callRemote("apply_H") - outcome=yield qA.callRemote("measure") - print("Alice outcome was:", outcome) - - reactor.stop() + ##################################################################################################### + # + # runClientNode + # + # This will be run on the local node if all communication links are set up (to the virtual node + # quantum backend, as well as the nodes in the classical communication network), and the local classical + # communication server is running (if applicable). + # + @inlineCallbacks + def runClientNode(qReg, virtRoot, myName, classicalNet): + """ + Code to execute for the local client node. Called if all connections are established. + + Arguments + qReg quantum register (twisted object supporting remote method calls) + virtRoot virtual quantum node (twisted object supporting remote method calls) + myName name of this node (string) + classicalNet servers in the classical communication network (dictionary of hosts) + """ + + logging.debug("LOCAL %s: Runing client side program.",myName) + + #Create 2 qubits + qA = yield virtRoot.callRemote("new_qubit_inreg",qReg) + qB = yield virtRoot.callRemote("new_qubit_inreg",qReg) + + #Make 2-qubit graph state + yield qA.callRemote("apply_H") + yield qB.callRemote("apply_H") + yield qA.callRemote("cphase_onto",qB) + + #send qubit B to Bob + #instruct virtual node to transfer qubit + remoteNum = yield virtRoot.callRemote("send_qubit",qB,"Bob") + logging.debug("LOCAL %s: Remote qubit is %d.",myName,remoteNum) + + #Tell number of virtual qubit to Bob and receive measurement outcome parity + bob=classicalNet.hostDict["Bob"] + yield bob.root.callRemote("receive_qubit",remoteNum) + + #Measure qubit (X-basis) + yield qA.callRemote("apply_H") + outcome=yield qA.callRemote("measure") + print("Alice outcome was:", outcome) + + reactor.stop() --------------- Programming Bob --------------- -Let us now program the code for Bob. Since he only acts as a server on the classical network, it is enough to edit the localNode portion of the template. Alice calls receive_qubit to convey the identifier of the virtual qubit.:: +Let us now program the code for Bob. Since he only acts as a server on the classical network, it is enough to edit +the localNode portion of the template. Alice calls receive_qubit to convey the identifier of the virtual qubit.:: - ##################################################################################################### - # - # localNode - # - # This will be run if the local node acts as a server on the classical communication network, - # accepting remote method calls from the other nodes. + ##################################################################################################### + # + # localNode + # + # This will be run if the local node acts as a server on the classical communication network, + # accepting remote method calls from the other nodes. - class localNode(pb.Root): + class localNode(pb.Root): - def __init__(self, node, classicalNet): + def __init__(self, node, classicalNet): - self.node = node - self.classicalNet = classicalNet + self.node = node + self.classicalNet = classicalNet - self.virtRoot = None - self.qReg = None + self.virtRoot = None + self.qReg = None - def set_virtual_node(self, virtRoot): - self.virtRoot = virtRoot + def set_virtual_node(self, virtRoot): + self.virtRoot = virtRoot - def set_virtual_reg(self, qReg): - self.qReg = qReg + def set_virtual_reg(self, qReg): + self.qReg = qReg - def remote_test(self): - return "Tested!" + def remote_test(self): + return "Tested!" - # This can be called by Alice (or other clients on the classical network) to inform Bob - # of an event. - @inlineCallbacks - def remote_receive_qubit(self, virtualNum): + # This can be called by Alice (or other clients on the classical network) to inform Bob + # of an event. + @inlineCallbacks + def remote_receive_qubit(self, virtualNum): - logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) + logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) - # Get ref of qubit - qB=yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) + # Get ref of qubit + qB=yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) - #Create new qubit - qC=yield self.virtRoot.callRemote("new_qubit_inreg",self.qReg) + #Create new qubit + qC=yield self.virtRoot.callRemote("new_qubit_inreg",self.qReg) - #Expand graph state - yield qC.callRemote("apply_H") - yield qB.callRemote("cphase_onto",qC) + #Expand graph state + yield qC.callRemote("apply_H") + yield qB.callRemote("cphase_onto",qC) - #Perform part of tau at C - yield qB.callRemote("apply_rotation",[0,0,1],-np.pi/2) + #Perform part of tau at C + yield qB.callRemote("apply_rotation",[0,0,1],-np.pi/2) - #send qubit C to Charlie - #instruct virtual node to transfer qubit - remoteNum = yield self.virtRoot.callRemote("send_qubit",qC,"Charlie") - logging.debug("LOCAL %s: Remote qubit is %d.","Bob",remoteNum) + #send qubit C to Charlie + #instruct virtual node to transfer qubit + remoteNum = yield self.virtRoot.callRemote("send_qubit",qC,"Charlie") + logging.debug("LOCAL %s: Remote qubit is %d.","Bob",remoteNum) - #Tell number of virtual qubit to Charlie and receive measurement outcome parity - charlie=self.classicalNet.hostDict["Charlie"] - yield charlie.root.callRemote("receive_qubit",remoteNum,"Bob") + #Tell number of virtual qubit to Charlie and receive measurement outcome parity + charlie=self.classicalNet.hostDict["Charlie"] + yield charlie.root.callRemote("receive_qubit",remoteNum,"Bob") - #Measure qubit (Z-basis) - outcome=yield qB.callRemote("measure") - print("Bob outcome was:", outcome) + #Measure qubit (Z-basis) + outcome=yield qB.callRemote("measure") + print("Bob outcome was:", outcome) ------------------- Programming Charlie ------------------- -Let us now program the code for Charlie. Since he only acts as a server on the classical network, it is enough to edit the localNode portion of the template. Both Bob and David calls receive_qubit to convey the identifier of the virtual qubit and depending on the sender Charlie does different things.:: +Let us now program the code for Charlie. Since he only acts as a server on the classical network, it is enough to +edit the localNode portion of the template. Both Bob and David calls receive_qubit to convey the identifier of the +virtual qubit and depending on the sender Charlie does different things.:: - ##################################################################################################### - # - # localNode - # - # This will be run if the local node acts as a server on the classical communication network, - # accepting remote method calls from the other nodes. + ##################################################################################################### + # + # localNode + # + # This will be run if the local node acts as a server on the classical communication network, + # accepting remote method calls from the other nodes. - class localNode(pb.Root): + class localNode(pb.Root): - def __init__(self, node, classicalNet): + def __init__(self, node, classicalNet): - self.node = node - self.classicalNet = classicalNet + self.node = node + self.classicalNet = classicalNet - self.virtRoot = None - self.qReg = None - self.qC = None #Maybe not the indented way + self.virtRoot = None + self.qReg = None + self.qC = None #Maybe not the indented way - def set_virtual_node(self, virtRoot): - self.virtRoot = virtRoot + def set_virtual_node(self, virtRoot): + self.virtRoot = virtRoot - def set_virtual_reg(self, qReg): - self.qReg = qReg + def set_virtual_reg(self, qReg): + self.qReg = qReg - def remote_test(self): - return "Tested!" + def remote_test(self): + return "Tested!" - # This can be called by Alice (or other clients on the classical network) to inform Bob - # of an event. - @inlineCallbacks - def remote_receive_qubit(self, virtualNum,sender): + # This can be called by Alice (or other clients on the classical network) to inform Bob + # of an event. + @inlineCallbacks + def remote_receive_qubit(self, virtualNum,sender): - if sender=="Bob": + if sender=="Bob": - logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) + logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) - # Get ref of qubit - self.qC=yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) - qC=self.qC + # Get ref of qubit + self.qC=yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) + qC=self.qC - #Create new qubit - qD=yield self.virtRoot.callRemote("new_qubit_inreg",self.qReg) + #Create new qubit + qD=yield self.virtRoot.callRemote("new_qubit_inreg",self.qReg) - #Expand graph state - yield qD.callRemote("apply_H") - yield qC.callRemote("cphase_onto",qD) + #Expand graph state + yield qD.callRemote("apply_H") + yield qC.callRemote("cphase_onto",qD) - #Perform part of tau at C - yield qC.callRemote("apply_rotation",[1,0,0],np.pi/2) - yield qD.callRemote("apply_rotation",[0,0,1],-np.pi/2) + #Perform part of tau at C + yield qC.callRemote("apply_rotation",[1,0,0],np.pi/2) + yield qD.callRemote("apply_rotation",[0,0,1],-np.pi/2) - # tmp=yield self.virtRoot.callRemote("get_register",qC) - # np.save("data_R",tmp[0]) - # np.save("data_I",tmp[1]) + # tmp=yield self.virtRoot.callRemote("get_register",qC) + # np.save("data_R",tmp[0]) + # np.save("data_I",tmp[1]) - #send qubit D to David - #instruct virtual node to transfer qubit - remoteNum = yield self.virtRoot.callRemote("send_qubit",qD,"David") - logging.debug("LOCAL %s: Remote qubit is %d.","Charlie",remoteNum) + #send qubit D to David + #instruct virtual node to transfer qubit + remoteNum = yield self.virtRoot.callRemote("send_qubit",qD,"David") + logging.debug("LOCAL %s: Remote qubit is %d.","Charlie",remoteNum) - #Tell number of virtual qubit to Charlie and receive measurement outcome parity - david=self.classicalNet.hostDict["David"] - yield david.root.callRemote("receive_qubit",remoteNum) + #Tell number of virtual qubit to Charlie and receive measurement outcome parity + david=self.classicalNet.hostDict["David"] + yield david.root.callRemote("receive_qubit",remoteNum) - #Measure qubit (X-basis) - yield qC.callRemote("apply_H") - outcome=yield qC.callRemote("measure") - print("Charlie outcome was:", outcome) + #Measure qubit (X-basis) + yield qC.callRemote("apply_H") + outcome=yield qC.callRemote("measure") + print("Charlie outcome was:", outcome) - elif sender=="David": + elif sender=="David": - logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) + logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) - # Get ref of qubit - qE=yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) - qC=self.qC + # Get ref of qubit + qE=yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) + qC=self.qC - # Expand graph state - yield qE.callRemote("cphase_onto",qC) + # Expand graph state + yield qE.callRemote("cphase_onto",qC) - #Do local part of tau - yield qE.callRemote("apply_rotation",[1,0,0],np.pi/2) - yield qC.callRemote("apply_rotation",[0,0,1],-np.pi/2) + #Do local part of tau + yield qE.callRemote("apply_rotation",[1,0,0],np.pi/2) + yield qC.callRemote("apply_rotation",[0,0,1],-np.pi/2) - #Measure extra qubit (Z-basis) - m=yield qE.callRemote("measure") - if m==1: - yield qC.callRemote("apply_Z") - return m + #Measure extra qubit (Z-basis) + m=yield qE.callRemote("measure") + if m==1: + yield qC.callRemote("apply_Z") + return m ----------------- Programming David ----------------- -Let us now program the code for David. Since he only acts as a server on the classical network, it is enough to edit the localNode portion of the template. Charlie calls receive_qubit to convey the identifier of the virtual qubit.:: +Let us now program the code for David. Since he only acts as a server on the classical network, it is enough to edit +the localNode portion of the template. Charlie calls receive_qubit to convey the identifier of the virtual qubit.:: - ##################################################################################################### - # - # localNode - # - # This will be run if the local node acts as a server on the classical communication network, - # accepting remote method calls from the other nodes. + ##################################################################################################### + # + # localNode + # + # This will be run if the local node acts as a server on the classical communication network, + # accepting remote method calls from the other nodes. - class localNode(pb.Root): + class localNode(pb.Root): - def __init__(self, node, classicalNet): + def __init__(self, node, classicalNet): - self.node = node - self.classicalNet = classicalNet + self.node = node + self.classicalNet = classicalNet - self.virtRoot = None - self.qReg = None + self.virtRoot = None + self.qReg = None - def set_virtual_node(self, virtRoot): - self.virtRoot = virtRoot + def set_virtual_node(self, virtRoot): + self.virtRoot = virtRoot - def set_virtual_reg(self, qReg): - self.qReg = qReg + def set_virtual_reg(self, qReg): + self.qReg = qReg - def remote_test(self): - return "Tested!" + def remote_test(self): + return "Tested!" - # This can be called by Alice (or other clients on the classical network) to inform Bob - # of an event. - @inlineCallbacks - def remote_receive_qubit(self, virtualNum): + # This can be called by Alice (or other clients on the classical network) to inform Bob + # of an event. + @inlineCallbacks + def remote_receive_qubit(self, virtualNum): - logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) + logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) - # Get ref of qubit - qD=yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) + # Get ref of qubit + qD=yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) - #Create new qubit - qE=yield self.virtRoot.callRemote("new_qubit_inreg",self.qReg) + #Create new qubit + qE=yield self.virtRoot.callRemote("new_qubit_inreg",self.qReg) - #Expand graph state - yield qE.callRemote("apply_H") - yield qD.callRemote("cphase_onto",qE) + #Expand graph state + yield qE.callRemote("apply_H") + yield qD.callRemote("cphase_onto",qE) - #send qubit E to Charlie - #instruct virtual node to transfer qubit - remoteNum = yield self.virtRoot.callRemote("send_qubit",qE,"Charlie") - logging.debug("LOCAL %s: Remote qubit is %d.","David",remoteNum) + #send qubit E to Charlie + #instruct virtual node to transfer qubit + remoteNum = yield self.virtRoot.callRemote("send_qubit",qE,"Charlie") + logging.debug("LOCAL %s: Remote qubit is %d.","David",remoteNum) - #Tell number of virtual qubit to Charlie and receive meas outcome - charlie=self.classicalNet.hostDict["Charlie"] - m=yield charlie.root.callRemote("receive_qubit",remoteNum,"David") + #Tell number of virtual qubit to Charlie and receive meas outcome + charlie=self.classicalNet.hostDict["Charlie"] + m=yield charlie.root.callRemote("receive_qubit",remoteNum,"David") - logging.debug("LOCAL %s: Got outcome %d.","David",m) - yield qD.callRemote("apply_rotation",[0,0,1],-np.pi/2) - if m==1: - yield qD.callRemote("apply_Z") + logging.debug("LOCAL %s: Got outcome %d.","David",m) + yield qD.callRemote("apply_rotation",[0,0,1],-np.pi/2) + if m==1: + yield qD.callRemote("apply_Z") - #Measure qubit (X-basis) - # tmp=yield self.virtRoot.callRemote("get_register",qD) - # np.save("data_R",tmp[0]) - # np.save("data_I",tmp[1]) - yield qD.callRemote("apply_H") - outcome=yield qD.callRemote("measure") - print("Davids outcome was:", outcome) + #Measure qubit (X-basis) + # tmp=yield self.virtRoot.callRemote("get_register",qD) + # np.save("data_R",tmp[0]) + # np.save("data_I",tmp[1]) + yield qD.callRemote("apply_H") + outcome=yield qD.callRemote("measure") + print("Davids outcome was:", outcome) -------- Starting @@ -435,15 +432,17 @@ Starting We first start the virtual quantum node backend, by executing:: - python3 simulaqron/run/startNode.py Alice & - python3 simulaqron/run/startNode.py Bob & - python3 simulaqron/run/startNode.py David & - python3 simulaqron/run/startNode.py Charlie & + python3 simulaqron/run/startNode.py Alice & + python3 simulaqron/run/startNode.py Bob & + python3 simulaqron/run/startNode.py David & + python3 simulaqron/run/startNode.py Charlie & -We then start up the programs for the parties themselves. These will connect to the virtual quantum nodes, and execute the quantum commands and classical communication outlined above, in the same directory as we placed classicalNet.cfg:: +We then start up the programs for the parties themselves. These will connect to the virtual quantum nodes, and +execute the quantum commands and classical communication outlined above, in the same directory as we placed +classicalNet.json:: - python3 bobTest.py & - python3 charlieTest.py & - python3 davidTest.py & - python3 aliceTest.py + python3 bobTest.py & + python3 charlieTest.py & + python3 davidTest.py & + python3 aliceTest.py diff --git a/docs/NativeModeTeleport.rst b/docs/NativeModeTeleport.rst index d9310b9d..132a0c8f 100644 --- a/docs/NativeModeTeleport.rst +++ b/docs/NativeModeTeleport.rst @@ -1,61 +1,51 @@ Teleporting a Qubit =================== -Let's now consider a very simple protocol, in which Alice first generates an EPR pair with Bob, and then teleports a qubit to Bob. To program it in SimulaQron's native mode, we will use the template described in :doc:`NativeModeTemplate`. +.. warning:: Update the code of the examples below!!! +.. warning:: Update the configuration file to use the JSON format!!! + +Let's now consider a very simple protocol, in which Alice first generates an EPR pair with Bob, and then teleports +a qubit to Bob. To program it in SimulaQron's native mode, we will use the template described in +:doc:`NativeModeTemplate`. ------------ The protocol ------------ -For completness, let's briefly recap the protocol: +For completeness, let's briefly recap the protocol: * For generating an EPR pair with Bob, Alice proceeds as follows: - - #. Alice prepares two qubits in the state :math:`|0\rangle_A |0\rangle_B` - - #. Alice applies a Hadamard transform to qubit A, giving :math:`|+\rangle_A |0\rangle_B` - - #. Alice applies a CNOT on qubits A and B, where qubit A is the control and B is the target, giving the state :math:`\frac{1}{\sqrt{2}}(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B)` - - #. Alice sends qubit B to Bob - + #. Alice prepares two qubits in the state :math:`|0\rangle_A |0\rangle_B` + #. Alice applies a Hadamard transform to qubit A, giving :math:`|+\rangle_A |0\rangle_B` + #. Alice applies a CNOT on qubits A and B, where qubit A is the control and B is the target, giving the state + :math:`\frac{1}{\sqrt{2}}(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B)` + #. Alice sends qubit B to Bob * Alice then teleports qubit q1 to Bob using the EPR pair: - - #. Alice applies a CNOT between q1 and qubit A, where q1 is the control and A is the target. - - #. Alice applies a Hadamard to qubit q1. - - #. Alice measures qubits q1 and A to obtain outcomes a and b. - - #. She sends a and b to Bob, who applies the correction unitary :math:`Z^a X^b` + #. Alice applies a CNOT between q1 and qubit A, where q1 is the control and A is the target. + #. Alice applies a Hadamard to qubit q1. + #. Alice measures qubits q1 and A to obtain outcomes a and b. + #. She sends a and b to Bob, who applies the correction unitary :math:`Z^a X^b` ----------- Setting up ----------- -We will run everything locally (localhost) using the standard virtualNodes.cfg file found in conig that define the virtual quantum nodes run in the background to simulate the quantum hardware:: - - # Network configuration file - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - - Alice, localhost, 8801 - Bob, localhost, 8802 +We will run everything locally (localhost) using the standard virtualNodes.json file that defines the nodes that run +the virtual quantum node in the background to simulate the quantum hardware:: -As we can see from the proocol above, only Alice communicates to Bob for which we will run a client at Alice' and a server at Bob's. Since we run everything locally, we may thus use for the configuration file classicalNet.cfg:: + # Network configuration file + # + # For each host its informal name, as well as its location in the network must + # be listed. + # + # [name], [hostname], [port number] - # Network configuration file - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - # + Alice, localhost, 8801 + Bob, localhost, 8802 - Bob, localhost, 8812 +We use this same file to specify the communication channels (sockets) for passing classical messages between the +declared nodes. The loaded network configuration can be used to construct ``SocketsConfig`` objects that contain the +sets of sockets used by a specific component of SimulaQron (either ``app``, ``vnode`` or ``qnodeos``). Let us now provide the actual program code for both Alice and Bob. @@ -65,137 +55,140 @@ Programming Alice Since Alice acts as a client, we will only need to fill in runClientNode. This gives:: - ##################################################################################################### - # - # runClientNode - # - # This will be run on the local node if all communication links are set up (to the virtual node - # quantum backend, as well as the nodes in the classical communication network), and the local classical - # communication server is running (if applicable). - # - @inlineCallbacks - def runClientNode(qReg, virtRoot, myName, classicalNet): - """ - Code to execute for the local client node. Called if all connections are established. - - Arguments - qReg quantum register (twisted object supporting remote method calls) - virtRoot virtual quantum ndoe (twisted object supporting remote method calls) - myName name of this node (string) - classicalNet servers in the classical communication network (dictionary of hosts) - """ - - logging.debug("LOCAL %s: Runing client side program.",myName) - - # Create 3 qubits - q1 = yield virtRoot.callRemote("new_qubit_inreg",qReg) - qA = yield virtRoot.callRemote("new_qubit_inreg",qReg) - qB = yield virtRoot.callRemote("new_qubit_inreg",qReg) - - # Prepare the first one in the |-> state - yield q1.callRemote("apply_H") - - # For information purposes, let's print the state of that qubit - (R,I) = yield q1.callRemote("get_qubit") - print("Qubit to be teleported is: ", assemble_qubit(R,I)) - - # Put qubits A and B in an EPR state - yield qA.callRemote("apply_H") - yield qA.callRemote("cnot_onto",qB) - - # Send qubit B to Bob - # Instruct the virtual node to transfer the qubit - remoteNum = yield virtRoot.callRemote("send_qubit",qB, "Bob") - logging.debug("LOCAL %s: Remote qubit is %d.",myName, remoteNum) - - # Apply the local teleportation operations - yield q1.callRemote("cnot_onto",qA) - yield q1.callRemote("apply_H") - - a = yield q1.callRemote("measure") - b = yield qA.callRemote("measure") - logging.debug("LOCAL %s: Correction info is a=%d, b=%d.",myName, a, b) - - # Tell Bob the number of the virtual qubit so the can use it locally - bob = classicalNet.hostDict["Bob"] - yield bob.root.callRemote("recover_teleport", a, b, remoteNum) - - reactor.stop() + ##################################################################################################### + # + # runClientNode + # + # This will be run on the local node if all communication links are set up (to the virtual node + # quantum backend, as well as the nodes in the classical communication network), and the local classical + # communication server is running (if applicable). + # + @inlineCallbacks + def runClientNode(qReg, virtRoot, myName, classicalNet): + """ + Code to execute for the local client node. Called if all connections are established. + + Arguments + qReg quantum register (twisted object supporting remote method calls) + virtRoot virtual quantum ndoe (twisted object supporting remote method calls) + myName name of this node (string) + classicalNet servers in the classical communication network (dictionary of hosts) + """ + + logging.debug("LOCAL %s: Runing client side program.",myName) + + # Create 3 qubits + q1 = yield virtRoot.callRemote("new_qubit_inreg",qReg) + qA = yield virtRoot.callRemote("new_qubit_inreg",qReg) + qB = yield virtRoot.callRemote("new_qubit_inreg",qReg) + + # Prepare the first one in the |-> state + yield q1.callRemote("apply_H") + + # For information purposes, let's print the state of that qubit + (R,I) = yield q1.callRemote("get_qubit") + print("Qubit to be teleported is: ", assemble_qubit(R,I)) + + # Put qubits A and B in an EPR state + yield qA.callRemote("apply_H") + yield qA.callRemote("cnot_onto",qB) + + # Send qubit B to Bob + # Instruct the virtual node to transfer the qubit + remoteNum = yield virtRoot.callRemote("send_qubit",qB, "Bob") + logging.debug("LOCAL %s: Remote qubit is %d.",myName, remoteNum) + + # Apply the local teleportation operations + yield q1.callRemote("cnot_onto",qA) + yield q1.callRemote("apply_H") + + a = yield q1.callRemote("measure") + b = yield qA.callRemote("measure") + logging.debug("LOCAL %s: Correction info is a=%d, b=%d.",myName, a, b) + + # Tell Bob the number of the virtual qubit so the can use it locally + bob = classicalNet.hostDict["Bob"] + yield bob.root.callRemote("recover_teleport", a, b, remoteNum) + + reactor.stop() --------------- Programming Bob --------------- -Let us now program the code for Bob. Since he only acts as a server on the classical network, it is enough to edit the localNode portion of the template. Alice calls recover_teleport to convey the classical measurement outcomes, as well as the identifier of the virtual qubit. Bob first asks his local virtual quantum node for a reference -for the qubit with that identfier and then processes the relevant quantum instructions to recover the qubit. We print the density matrix of the qubit at the end for illustration.:: - - - ##################################################################################################### - # - # localNode - # - # This will be run if the local node acts as a server on the classical communication network, - # accepting remote method calls from the other nodes. - - class localNode(pb.Root): - - def __init__(self, node, classicalNet): - - self.node = node - self.classicalNet = classicalNet - - self.virtRoot = None - self.qReg = None - - def set_virtual_node(self, virtRoot): - self.virtRoot = virtRoot - - def set_virtual_reg(self, qReg): - self.qReg = qReg - - def remote_test(self): - return "Tested!" - - # This can be called by Alice to tell Bob where to get the qubit and what corrections to apply - @inlineCallbacks - def remote_recover_teleport(self, a, b, virtualNum): - """ - Recover the qubit from teleportation. - - Arguments - a,b received measurement outcomes from Alice - virtualNum number of the virtual qubit corresponding to the EPR pair received - """ - - logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) - - # Get the reference to Alice's qubit from the local virtual node - eprB = yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) - - # Apply the desired correction info - logging.debug("LOCAL %s: Correction info is a=%d, b=%d.", self.node.name, a, b) - if b == 1: - yield eprB.callRemote("apply_X") - if a == 1: - yield eprB.callRemote("apply_Z") - - # Just print the qubit we received - (realRho, imagRho) = yield eprB.callRemote("get_qubit") - rho = self.assemble_qubit(realRho, imagRho) - - print("Qubit is:", rho) - - def assemble_qubit(self, realM, imagM): - """ - Reconstitute the qubit as a qutip object from its real and imaginary components given as a list. - We need this since Twisted PB does not support sending complex valued object natively. - """ - M = realM - for s in range(len(M)): - for t in range(len(M)): - M[s][t] = realM[s][t] + 1j * imagM[s][t] - - return Qobj(M) +Let us now program the code for Bob. Since he only acts as a server on the classical network, it is enough to edit the +localNode portion of the template. Alice calls recover_teleport to convey the classical measurement outcomes, as well +as the identifier of the virtual qubit. Bob first asks his local virtual quantum node for a reference for the qubit +with that identifier and then processes the relevant quantum instructions to recover the qubit. We print the density +matrix of the qubit at the end for illustration.:: + + + ##################################################################################################### + # + # localNode + # + # This will be run if the local node acts as a server on the classical communication network, + # accepting remote method calls from the other nodes. + + class localNode(pb.Root): + + def __init__(self, node, classicalNet): + + self.node = node + self.classicalNet = classicalNet + + self.virtRoot = None + self.qReg = None + + def set_virtual_node(self, virtRoot): + self.virtRoot = virtRoot + + def set_virtual_reg(self, qReg): + self.qReg = qReg + + def remote_test(self): + return "Tested!" + + # This can be called by Alice to tell Bob where to get the qubit and what corrections to apply + @inlineCallbacks + def remote_recover_teleport(self, a, b, virtualNum): + """ + Recover the qubit from teleportation. + + Arguments + a,b received measurement outcomes from Alice + virtualNum number of the virtual qubit corresponding to the EPR pair received + """ + + logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) + + # Get the reference to Alice's qubit from the local virtual node + eprB = yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) + + # Apply the desired correction info + logging.debug("LOCAL %s: Correction info is a=%d, b=%d.", self.node.name, a, b) + if b == 1: + yield eprB.callRemote("apply_X") + if a == 1: + yield eprB.callRemote("apply_Z") + + # Just print the qubit we received + (realRho, imagRho) = yield eprB.callRemote("get_qubit") + rho = self.assemble_qubit(realRho, imagRho) + + print("Qubit is:", rho) + + def assemble_qubit(self, realM, imagM): + """ + Reconstitute the qubit as a qutip object from its real and imaginary components given as a list. + We need this since Twisted PB does not support sending complex valued object natively. + """ + M = realM + for s in range(len(M)): + for t in range(len(M)): + M[s][t] = realM[s][t] + 1j * imagM[s][t] + + return Qobj(M) -------- Starting @@ -203,11 +196,13 @@ Starting We first start the virtual quantum node backend, by executing:: - python3 simulaqron/run/startNode.py Alice & - python3 simulaqron/run/startNode.py Bob & + python3 simulaqron/run/startNode.py Alice & + python3 simulaqron/run/startNode.py Bob & -We then start up the programs for Alice and Bob themselves. These will connect to the virtual quantum nodes, and execute the quantum commands and classical communication outlined above, in the same directory as we placed classicalNet.cfg:: +We then start up the programs for Alice and Bob themselves. These will connect to the virtual quantum nodes, and +execute the quantum commands and classical communication outlined above, in the same directory as we placed +simulaqron_network.json:: - python3 bobTest.py & - python3 aliceTest.py + python3 bobTest.py & + python3 aliceTest.py diff --git a/docs/NativeModeTemplate.rst b/docs/NativeModeTemplate.rst index 57e1c5df..96a6f74d 100644 --- a/docs/NativeModeTemplate.rst +++ b/docs/NativeModeTemplate.rst @@ -1,166 +1,185 @@ Template for programming in native mode ======================================= -In examples/nativeMode/template you will find a template that allows you to program relatively easily by filling in the relevant parts of the template. Let us now discuss this template in detail. - -#. The first step in programming your application protocol is to determine how many nodes are involved. For simplicity, let us here assume you only have two, called Alice and Bob. This will typically be obvious from the high level description of the quantum protocol that you are given. - -#. The next, and possibly less obvious step, is to determine how classical information is exchanged in the quantum protocol. That is, who sends classical messages to whom, at what time, and what actions are taken when those messages are received. Based on this, you need to decide which nodes run a server on the classical network, and which nodes may simply be a client program that connects to the servers to deliver messages. Let us here simply assume, Alice only sends information to Bob, who then acts upon the message received. In this case, we would make Alice a client and Bob a server. Note that one node can obviously fullfill both roles. - -#. The template will look for a file called classicalNet.cfg in the local directory to determine who acts as a server and what that nodes address details are. An example, if only Bob acts as a server would be:: - - # Network configuration file - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - # - - Bob, localhost, 8812 - -#. The next step is to check that on each network computer that you will run on, the global configuration file starting the virtual quantum nodes is set up correctly. See :doc:`GettingStarted` on how to perform such a configuration and start the local quantum virtual node backends. - -#. Now copy nodeTest.py to a separate file for each node. In our example above where we just have Alice (client only) and Bob (server only), you would copy nodeTest to aliceTest.py and bobTest.py. +.. warning:: Update the code of the examples below!!! +.. warning:: Update the configuration file to use the JSON format!!! +.. warning:: Update the explanation about how SimulaQron uses the ``simulaqron_network.json`` file!!!! + +In examples/nativeMode/template you will find a template that allows you to start programming relatively easily by +filling in the relevant parts of the template. Let us now discuss this template in detail: + +#. The first step in programming your application protocol is to determine how many nodes are involved. For simplicity, + let us here assume you only have two, called Alice and Bob. This will typically be obvious from the high level + description of the quantum protocol that you are given. +#. The next, and possibly less obvious step, is to determine how classical information is exchanged in the quantum + protocol. That is, who sends classical messages to whom, at what time, and what actions are taken when those + messages are received. Based on this, you need to decide which nodes run a server on the classical network, and + which nodes may simply be a client program that connects to the servers to deliver messages. Let us here simply + assume, Alice only sends information to Bob, who then acts upon the message received. In this case, we would make + Alice a client and Bob a server. Note that one node can obviously fulfill both roles. +#. The template will look for a file called ``simulaqron_network.json`` in the local directory to determine who acts + as a server and what that nodes address details are. Simulaqron will decide (lexicographically) which nodes acts as + a server. An example of the network configuration file is as follows:: + + # Network configuration file + # + # For each host its informal name, as well as its location in the network must + # be listed. + # + # [name], [hostname], [port number] + # + + Bob, localhost, 8812 + +#. The next step is to check that on each network computer that you will run on, the global configuration file + starting the virtual quantum nodes is set up correctly. See :doc:`GettingStarted` on how to perform such a + configuration and start the local quantum virtual node backends. +#. Now copy nodeTest.py to a separate file for each node. In our example above where we just have Alice (client + only) and Bob (server only), you would copy nodeTest to aliceTest.py and bobTest.py. --------------------- Programming a client --------------------- -Let us now see how we would program a protocol in which a node acts as a client. In our example above this would be Alice, requiring you to edit aliceTest.py. The template code already includes everything necessary to connect to the local virtual quantum node backend, so all you have to do is to write that classical client program communicating with Bob (and possibly directing the local quantum hardware simulated by the virtual node to perform certain tasks). Specifically, you would add your code at the indicated place below:: - - - ##################################################################################################### - # - # runClientNode - # - # This will be run on the local node if all communication links are set up (to the virtual node - # quantum backend, as well as the nodes in the classical communication network), and the local classical - # communication server is running (if applicable). - # - #@inlineCallbacks - def runClientNode(qReg, virtRoot, myName, classicalNet): - """ - Code to execute for the local client node. Called if all connections are established. +Let us now see how we would program a protocol in which a node acts as a client. In our example above this would be +Alice, requiring you to edit aliceTest.py. The template code already includes everything necessary to connect to the +local virtual quantum node backend, so all you have to do is to write that classical client program communicating +with Bob (and possibly directing the local quantum hardware simulated by the virtual node to perform certain tasks). +Specifically, you would add your code at the indicated place below:: + + ##################################################################################################### + # + # runClientNode + # + # This will be run on the local node if all communication links are set up (to the virtual node + # quantum backend, as well as the nodes in the classical communication network), and the local classical + # communication server is running (if applicable). + # + #@inlineCallbacks + def runClientNode(qReg, virtRoot, myName, classicalNet): + """ + Code to execute for the local client node. Called if all connections are established. - Arguments - qReg quantum register (twisted object supporting remote method calls) - virtRoot virtual quantum ndoe (twisted object supporting remote method calls) - myName name of this node (string) - classicalNet servers in the classical communication network (dictionary of hosts) - """ - - logging.debug("LOCAL %s: Runing client side program.",myName) - - # Here the code to execute for Alice acting as a client - # Uncomment @inlineCallbacks above if you use yield statements - - # Stop the server and client - you want to delete this if the nodes acts as a server - reactor.stop() - -That's all. As a simple example, this code would correspond to the protocol where Alice creates a qubit in the :math:`|+\rangle` state and send it to Bob.:: - - - ##################################################################################################### - # - # runClientNode - # - # This will be run on the local node if all communication links are set up (to the virtual node - # quantum backend, as well as the nodes in the classical communication network), and the local classical - # communication server is running (if applicable). - # - @inlineCallbacks - def runClientNode(qReg, virtRoot, myName, classicalNet): - """ - Code to execute for the local client node. Called if all connections are established. + Arguments + qReg quantum register (twisted object supporting remote method calls) + virtRoot virtual quantum ndoe (twisted object supporting remote method calls) + myName name of this node (string) + classicalNet servers in the classical communication network (dictionary of hosts) + """ + + logging.debug("LOCAL %s: Runing client side program.",myName) + + # Here the code to execute for Alice acting as a client + # Uncomment @inlineCallbacks above if you use yield statements + + # Stop the server and client - you want to delete this if the nodes acts as a server + reactor.stop() + +That's all. As a simple example, this code would correspond to the protocol where Alice creates a qubit in the +:math:`|+\rangle` state and send it to Bob.:: + + ##################################################################################################### + # + # runClientNode + # + # This will be run on the local node if all communication links are set up (to the virtual node + # quantum backend, as well as the nodes in the classical communication network), and the local classical + # communication server is running (if applicable). + # + @inlineCallbacks + def runClientNode(qReg, virtRoot, myName, classicalNet): + """ + Code to execute for the local client node. Called if all connections are established. - Arguments - qReg quantum register (twisted object supporting remote method calls) - virtRoot virtual quantum ndoe (twisted object supporting remote method calls) - myName name of this node (string) - classicalNet servers in the classical communication network (dictionary of hosts) - """ - - logging.debug("LOCAL %s: Runing client side program.",myName) + Arguments + qReg quantum register (twisted object supporting remote method calls) + virtRoot virtual quantum ndoe (twisted object supporting remote method calls) + myName name of this node (string) + classicalNet servers in the classical communication network (dictionary of hosts) + """ - # Prepare a new qubit - qA = yield virtRoot.callRemote("new_qubit_inreg",qReg) + logging.debug("LOCAL %s: Runing client side program.",myName) - # Apply the Hadamard transform - yield qA.callRemote("apply_H") + # Prepare a new qubit + qA = yield virtRoot.callRemote("new_qubit_inreg",qReg) - # Instruct the virtual node to transfer the qubit to Bob - remoteNum = yield virtRoot.callRemote("send_qubit",qB, "Bob") + # Apply the Hadamard transform + yield qA.callRemote("apply_H") - # Tell Bob to process the qubit - bob = classicalNet.hostDict["Bob"] - yield bob.root.callRemote("tell_bob", remoteNum) + # Instruct the virtual node to transfer the qubit to Bob + remoteNum = yield virtRoot.callRemote("send_qubit",qB, "Bob") - # Stop the server and client - you want to delete this if the nodes acts as a server - reactor.stop() + # Tell Bob to process the qubit + bob = classicalNet.hostDict["Bob"] + yield bob.root.callRemote("tell_bob", remoteNum) + # Stop the server and client - you want to delete this if the nodes acts as a server + reactor.stop() -------------------- Programming a server -------------------- -Let us now have a look on how to program a node that acts as a server on the classical network. In our example above this would be Bob, requiring you to edit bobTest.py. The template code already includes everything necessary to connect to the local virtual quantum node backend, so all you have to do is to write that classical server program communicating with Alice (and possibly directing the local quantum hardware simulated by the virtual node to perform certain tasks). Specifically, you would add your code at the indicated place below:: +Let us now have a look on how to program a node that acts as a server on the classical network. In our example above +this would be Bob, requiring you to edit bobTest.py. The template code already includes everything necessary to +connect to the local virtual quantum node backend, so all you have to do is to write that classical server program +communicating with Alice (and possibly directing the local quantum hardware simulated by the virtual node to perform +certain tasks). Specifically, you would add your code at the indicated place below:: + ##################################################################################################### + # + # localNode + # + # This will be run if the local node acts as a server on the classical communication network, + # accepting remote method calls from the other nodes. - ##################################################################################################### - # - # localNode - # - # This will be run if the local node acts as a server on the classical communication network, - # accepting remote method calls from the other nodes. - - class localNode(pb.Root): + class localNode(pb.Root): - def __init__(self, node, classicalNet): + def __init__(self, node, classicalNet): - self.node = node - self.classicalNet = classicalNet + self.node = node + self.classicalNet = classicalNet - self.virtRoot = None - self.qReg = None + self.virtRoot = None + self.qReg = None - def set_virtual_node(self, virtRoot): - self.virtRoot = virtRoot + def set_virtual_node(self, virtRoot): + self.virtRoot = virtRoot - def set_virtual_reg(self, qReg): - self.qReg = qReg + def set_virtual_reg(self, qReg): + self.qReg = qReg - def remote_test(self): - return "Tested!" + def remote_test(self): + return "Tested!" - # This can be called by Alice (or other clients on the classical network) to inform Bob - # of an event. Your code goes here. - # @inlineCallbacks - def remote_tell_bob(self, someInfo): + # This can be called by Alice (or other clients on the classical network) to inform Bob + # of an event. Your code goes here. + # @inlineCallbacks + def remote_tell_bob(self, someInfo): # Uncomment inlineCallbacks if you use yield here # Also remove the pass statement when executing actual code pass -Evidently, it depends on the program what actions Bob would perform precisely. Here, let us just assume Bob receives the qubit and applies Pauli X:: - - # This can be called by Alice to tell Bob where to get the qubit and what corrections to apply - @inlineCallbacks - def remote_tell_bob (self, virtualNum): - """ - Apply X +Evidently, it depends on the program what actions Bob would perform precisely. Here, let us just assume Bob receives +the qubit and applies Pauli X:: - Arguments - virtualNum number of the virtual qubit corresponding to the qubit received - """ + # This can be called by Alice to tell Bob where to get the qubit and what corrections to apply + @inlineCallbacks + def remote_tell_bob (self, virtualNum): + """ + Apply X - logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) + Arguments + virtualNum number of the virtual qubit corresponding to the qubit received + """ - # Get a reference to the qubit from the local virtual quantum node. - qB = yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) + logging.debug("LOCAL %s: Getting reference to qubit number %d.",self.node.name, virtualNum) - # Apply Pauli X - yield qB.callRemote("apply_X") + # Get a reference to the qubit from the local virtual quantum node. + qB = yield self.virtRoot.callRemote("get_virtual_ref",virtualNum) + # Apply Pauli X + yield qB.callRemote("apply_X") From 9f7cef78adf75c118a99c66826615f1f822fef73 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 20:25:41 +0100 Subject: [PATCH 189/277] Move native mode examples documentation to its own folder --- docs/Examples.rst | 8 ++++---- docs/{NativeModeCorrRng.rst => native-mode/CorrRng.rst} | 0 .../GraphState.rst} | 0 docs/{NativeModeTeleport.rst => native-mode/Teleport.rst} | 0 docs/{NativeModeTemplate.rst => native-mode/Template.rst} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename docs/{NativeModeCorrRng.rst => native-mode/CorrRng.rst} (100%) rename docs/{NativeModeGraphState.rst => native-mode/GraphState.rst} (100%) rename docs/{NativeModeTeleport.rst => native-mode/Teleport.rst} (100%) rename docs/{NativeModeTemplate.rst => native-mode/Template.rst} (100%) diff --git a/docs/Examples.rst b/docs/Examples.rst index d95eba2b..eb2fec9d 100644 --- a/docs/Examples.rst +++ b/docs/Examples.rst @@ -21,10 +21,10 @@ also be found in examples/nativeMode. :maxdepth: 2 :caption: Native mode examples: - NativeModeCorrRng - NativeModeTemplate - NativeModeTeleport - NativeModeGraphState + native-mode/CorrRng + native-mode/Template + native-mode/Teleport + native-mode/GraphState diff --git a/docs/NativeModeCorrRng.rst b/docs/native-mode/CorrRng.rst similarity index 100% rename from docs/NativeModeCorrRng.rst rename to docs/native-mode/CorrRng.rst diff --git a/docs/NativeModeGraphState.rst b/docs/native-mode/GraphState.rst similarity index 100% rename from docs/NativeModeGraphState.rst rename to docs/native-mode/GraphState.rst diff --git a/docs/NativeModeTeleport.rst b/docs/native-mode/Teleport.rst similarity index 100% rename from docs/NativeModeTeleport.rst rename to docs/native-mode/Teleport.rst diff --git a/docs/NativeModeTemplate.rst b/docs/native-mode/Template.rst similarity index 100% rename from docs/NativeModeTemplate.rst rename to docs/native-mode/Template.rst From 5d8e7727650f41cb3d648d6dd720bb0c7e1abd13 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 21:00:19 +0100 Subject: [PATCH 190/277] Update broken references in native mode examples documentation --- docs/native-mode/Teleport.rst | 2 +- docs/native-mode/Template.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/native-mode/Teleport.rst b/docs/native-mode/Teleport.rst index 132a0c8f..8dc77a4d 100644 --- a/docs/native-mode/Teleport.rst +++ b/docs/native-mode/Teleport.rst @@ -6,7 +6,7 @@ Teleporting a Qubit Let's now consider a very simple protocol, in which Alice first generates an EPR pair with Bob, and then teleports a qubit to Bob. To program it in SimulaQron's native mode, we will use the template described in -:doc:`NativeModeTemplate`. +:doc:`Template`. ------------ The protocol diff --git a/docs/native-mode/Template.rst b/docs/native-mode/Template.rst index 96a6f74d..0b679d8a 100644 --- a/docs/native-mode/Template.rst +++ b/docs/native-mode/Template.rst @@ -32,7 +32,7 @@ filling in the relevant parts of the template. Let us now discuss this template Bob, localhost, 8812 #. The next step is to check that on each network computer that you will run on, the global configuration file - starting the virtual quantum nodes is set up correctly. See :doc:`GettingStarted` on how to perform such a + starting the virtual quantum nodes is set up correctly. See :doc:`../GettingStarted` on how to perform such a configuration and start the local quantum virtual node backends. #. Now copy nodeTest.py to a separate file for each node. In our example above where we just have Alice (client only) and Bob (server only), you would copy nodeTest to aliceTest.py and bobTest.py. From 1c74dc73f7b39926369a055a4a0ff18e819fec51 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 21:00:35 +0100 Subject: [PATCH 191/277] Update documentation building script --- docs/Makefile | 4 +++- docs/conf.py | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 72d7e259..ab9d8c5c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -254,7 +254,9 @@ python-deps: _add_jekyll: touch build/.nojekyll -build: python-deps html _add_jekyll +build: html _add_jekyll + +install-deps: python-deps open: @echo "test" diff --git a/docs/conf.py b/docs/conf.py index ba7d6761..caa79aa9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,9 +17,7 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import os import importlib.metadata as metadata -# import sys # -- General configuration ------------------------------------------------ @@ -55,8 +53,8 @@ # General information about the project. project = "SimulaQron" -copyright = "2017, Stephanie Wehner and Axel Dahlberg" -author = "Stephanie Wehner and Axel Dahlberg" +copyright = "2026, Stephanie Wehner, Axel Dahlberg and Diego Rivera" +author = "Stephanie Wehner, Axel Dahlberg and Diego Rivera" # Get the version from simulaqron _base_version_line = metadata.version('simulaqron') From eea7cdfb71e6c672323394b87c3d380a1889026d Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 21:12:55 +0100 Subject: [PATCH 192/277] Update documentation building script --- docs/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index caa79aa9..eb5d62b5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -181,3 +181,6 @@ # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] + +# Also generate sphinx docs for constructors +autoclass_content = "both" From 47f41aa88c75a4f788592d03bd7382300b9d35fe Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 21:30:51 +0100 Subject: [PATCH 193/277] Update documentation of the simulaqron.general package --- docs/simulaqron.general.rst | 9 ++++++ simulaqron/general/errors.py | 4 +++ simulaqron/general/host_config.py | 52 ++++++++++++++++++++++--------- 3 files changed, 50 insertions(+), 15 deletions(-) diff --git a/docs/simulaqron.general.rst b/docs/simulaqron.general.rst index b17cf9c7..2df5c79a 100644 --- a/docs/simulaqron.general.rst +++ b/docs/simulaqron.general.rst @@ -13,6 +13,15 @@ simulaqron.general.host_config module :show-inheritance: +simulaqron.general.errors module +------------------------------------- + +.. automodule:: simulaqron.general.errors + :members: + :undoc-members: + :show-inheritance: + + Module contents --------------- diff --git a/simulaqron/general/errors.py b/simulaqron/general/errors.py index 2f52959a..814dd2fa 100644 --- a/simulaqron/general/errors.py +++ b/simulaqron/general/errors.py @@ -1,2 +1,6 @@ class SimUnsupportedError(RuntimeError): + """ + Raised when the SimulaQron does not support the configured simulation backend. + Check :py:class:`NodeConfigType` enum for valid values of the simulation backend. + """ pass diff --git a/simulaqron/general/host_config.py b/simulaqron/general/host_config.py index a5dedc28..031a04a2 100644 --- a/simulaqron/general/host_config.py +++ b/simulaqron/general/host_config.py @@ -40,11 +40,14 @@ class Host(pb.Referenceable): def __init__(self, name: str, hostname: str, port: int): """ - Initialize the details of the host. For now, we just keep the following: - - name informal name of the host (e.g. Alice) - hostname name of the node on the network (e.g. localhost or yournode.qutech.nl) - port port number on hostname + Class representing a host that runs a SimulaQron Virtual Node. It holds the following information: + + :param name: Informal name of the host (e.g. Alice) + :type name: str + :param hostname: Name of the node on the network (e.g. localhost or yournode.qutech.nl) + :type hostname: str + :param port: Port number on hostname + :type port: int """ self.name = name @@ -57,7 +60,7 @@ def __init__(self, name: str, hostname: str, port: int): self.family = addr[0] self.addr = addr - self.ip = node_id_from_addrinfo(addr) + self.ip = _node_id_from_addrinfo(addr) # Connection identifiers used after connected self.factory = 0 @@ -67,14 +70,24 @@ def __init__(self, name: str, hostname: str, port: int): class SocketsConfig(pb.Referenceable): def __init__(self, nets_config: NetworksConfiguration, network_name: str = "default", - config_type: str | NodeConfigType = "vnode"): + config_type: NodeConfigType | str = "vnode"): """ - Initialize by reading in the configuration file. + Structure used to hold the sockets configuration for a particular component. With version 4.0.0, we use the already in-memory information to create the SocketsConfig object. This avoids reading the file multiple times, which might have been updated by other processes in between reads. Additionally, this also simplifies the code, and reduces the potential source of bugs in the configuration read/write code. + + :param nets_config: :py:class:`NetworksConfiguration` object, containing the loaded ``simulaqron_network.json`` + file. + :type nets_config: NetworksConfiguration + :param network_name: The name of the network to use. This name must exist in the loaded network + configuration object. + :type network_name: str + :param config_type: The type of configuration to use. Valid values are instances of the + :py:class:`NodeConfigType` enum, or the string that represent each of those values. + :type config_type: NodeConfigType | str """ # Dictionary where we will keep host details, indexed by node name (e.g. Alice) self.hostDict: Dict[str, Host] = {} @@ -94,9 +107,9 @@ def filter(self, nodes_to_keep: List[str]): Filter the loaded sockets configurations to only contain the given names. If a given node name is not found in the loaded one, it will simply be ignored from the exclusion process (i.e. it will not break the process) - Args: - nodes_to_keep: List[str] - The node names to keep after filtering. + + :param nodes_to_keep: The node names to keep after filtering. + :type nodes_to_keep: List[str] """ nodes_kept = {} for node_name in self.hostDict.keys(): @@ -105,25 +118,34 @@ def filter(self, nodes_to_keep: List[str]): self.hostDict = nodes_kept -def node_id(fam: socket.AddressFamily, ip: str) -> int: +def _node_id(fam: socket.AddressFamily, ip: str) -> int: if fam == socket.AF_INET: return struct.unpack("!L", IPv4Address(ip).packed)[0] else: raise ValueError("No IPv6 yet :(") -def node_id_from_addrinfo( +def _node_id_from_addrinfo( addr: tuple[socket.AddressFamily, socket.SocketKind, int, str, tuple[str, int]] ) -> int: fam = addr[0] sockaddr = addr[4] ip = sockaddr[0] - return node_id(fam, ip) + return _node_id(fam, ip) def get_node_id_from_net_config(net_config: SocketsConfig, node_name: str) -> int: """ - NOTE node ID is the index of the node name of a sorted list of all the node names in the network. + Gets the node ID from the given sockets config and node name. + + .. note:: node ID is the index of the node name of a sorted list of all the node names in the network. + + :param net_config: SocketsConfig object. + :type net_config: SocketsConfig + :param node_name: The name of the node to get the node ID from. + :type node_name: str + :return: The node ID from the given sockets config and node name. + :rtype: int """ if node_name not in net_config.hostDict: raise ValueError(f"node name {node_name} not in host_dict ({net_config.hostDict.keys()})") From 00d84dccf06d5a736a4404384172a148aea4aa3f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 13 Jan 2026 21:50:21 +0100 Subject: [PATCH 194/277] Update documentation of the simulaqron.local package --- examples/nativeMode/teleport/aliceTest.py | 3 +- examples/nativeMode/teleport/bobTest.py | 3 +- simulaqron/local/setup.py | 62 +++++++++-------------- simulaqron/math.py | 21 ++++++++ tests/slow/merges/test_merges.py | 3 +- 5 files changed, 51 insertions(+), 41 deletions(-) create mode 100644 simulaqron/math.py diff --git a/examples/nativeMode/teleport/aliceTest.py b/examples/nativeMode/teleport/aliceTest.py index 360f899f..b2c8a175 100644 --- a/examples/nativeMode/teleport/aliceTest.py +++ b/examples/nativeMode/teleport/aliceTest.py @@ -30,7 +30,8 @@ import logging import numpy as np -from simulaqron.local.setup import setup_local, assemble_qubit +from simulaqron.math import assemble_qubit +from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig from simulaqron.settings.network_config import NodeConfigType from simulaqron.toolbox.stabilizer_states import StabilizerState diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index f49382b0..863e445c 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -31,7 +31,8 @@ import logging import numpy as np -from simulaqron.local.setup import setup_local, assemble_qubit +from simulaqron.math import assemble_qubit +from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings, network_config, LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS from simulaqron.settings.network_config import NodeConfigType diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index 56138f3c..b5fc84ca 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -53,25 +53,24 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsConfig, lNode: pb.Root, func: Callable, *args, **kwargs): """ - Sets up a local classical applicaiton level communication server (if desired according to the configuration file), + Sets up a local classical application level communication server (if desired according to the configuration file), a client connection to the local virtual node quantum backend and a client connections to all other - classical communication servers - - Args: - myName (str): - name of this node - virtualNet (SocketsConfig): - servers of the virtual nodes (dictionary of host objects) - classicalNet (SocketsConfig): - servers on the classical communication network (dictionary of host objects) - lNode (pb.Root): - Twisted PB root to use as local server (if applicable) - func (Callable): - function to run if all connections are set up - *args (Any): - additional arguments to be given to ``func`` - **kwargs (Any): - additional keyword-based arguments to be passed to ``func`` + classical communication servers. + + :param myName: Name of this node. + :type myName: str + :param virtualNet: Servers of the virtual nodes (dictionary of host objects). + :type virtualNet: SocketsConfig + :param classicalNet: Servers on the classical communication network (dictionary of host objects). + :type classicalNet: SocketsConfig + :param lNode: Twisted PB root to use as local server (if applicable). + :type lNode: pb.Root + :param func: Function to run if all connections are set up. + :type func: Callable + :param args: Additional arguments to be given to ``func``. + :type args: Any + :param kwargs: Additional keyword-based arguments to be passed to ``func``. + :type kwargs: Any """ # Initialize Twisted callback framework @@ -114,8 +113,8 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon dList.append(nb.factory.getRootObject()) deferList = DeferredList(dList, consumeErrors=True) - deferList.addCallback(init_register, myName, virtualNet, classicalNet, lNode, func, *args, **kwargs) - deferList.addErrback(localError) + deferList.addCallback(_init_register, myName, virtualNet, classicalNet, lNode, func, *args, **kwargs) + deferList.addErrback(_localError) try: reactor.run() except error.ReactorNotRestartable: @@ -131,7 +130,7 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon # -def init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig, classicalNet: SocketsConfig, +def _init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig, classicalNet: SocketsConfig, lNode: pb.Root, func: Callable, *args, **kwargs): _logger.debug("SETUP_LOCAL %s: All connections set up.", myName) @@ -159,11 +158,11 @@ def init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig, # On the local virtual node, we still want to initialize a qubit register defer = virtRoot.callRemote("add_register") - defer.addCallback(fill_register, myName, lNode, virtRoot, classicalNet, func, *args, **kwargs) - defer.addErrback(localError) + defer.addCallback(_fill_register, myName, lNode, virtRoot, classicalNet, func, *args, **kwargs) + defer.addErrback(_localError) -def fill_register(obj, myName, lNode, virtRoot, classicalNet, func, *args, **kwargs): +def _fill_register(obj, myName, lNode, virtRoot, classicalNet, func, *args, **kwargs): _logger.debug("SETUP_LOCAL %s: Created quantum register at virtual node.", myName) qReg = obj @@ -175,7 +174,7 @@ def fill_register(obj, myName, lNode, virtRoot, classicalNet, func, *args, **kwa func(qReg, virtRoot, myName, classicalNet, *args, **kwargs) -def localError(reason): +def _localError(reason): """ Error handling for the connection. """ @@ -184,16 +183,3 @@ def localError(reason): reactor.stop() except ReactorNotRunning: pass - - -def assemble_qubit(realM, imagM): - """ - Reconstitute the qubit as array from its real and imaginary components given as a list. - We need this since Twisted PB does not support sending complex valued object natively. - """ - M = realM - for s in range(len(M)): - for t in range(len(M)): - M[s][t] = realM[s][t] + 1j * imagM[s][t] - - return M diff --git a/simulaqron/math.py b/simulaqron/math.py new file mode 100644 index 00000000..2bb1ae92 --- /dev/null +++ b/simulaqron/math.py @@ -0,0 +1,21 @@ +import numpy as np + + +def assemble_qubit(realM: np.ndarray, imagM: np.ndarray) -> np.ndarray: + """ + Reconstitute the qubit as array from its real and imaginary components given as a list. + + :param realM: Real component of the qubit. + :type realM: np.ndarray + :param imagM: Imaginary component of the qubit. + :type imagM: np.ndarray + :return: Assembled qubit as vector of :py:type:`complex` numbers. + :rtype: np.array + """ + # We need this since Twisted PB does not support sending complex valued object natively. + M = realM + for s in range(len(M)): + for t in range(len(M)): + M[s][t] = realM[s][t] + 1j * imagM[s][t] + + return M \ No newline at end of file diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index adbf459a..3924cbe5 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -14,7 +14,8 @@ from multiprocess.connection import Pipe, Connection from logging import DEBUG from simulaqron.general.host_config import SocketsConfig -from simulaqron.local.setup import setup_local, assemble_qubit +from simulaqron.math import assemble_qubit +from simulaqron.local.setup import setup_local from simulaqron.network import Network from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings.network_config import NodeConfigType From 617fa627683c51127b54c95dee3499bd186e2a09 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 14 Jan 2026 16:31:39 +0100 Subject: [PATCH 195/277] Update documentation of the simulaqron package --- docs/simulaqron.netqasm_backend.rst | 24 ++++ docs/simulaqron.rst | 29 +++- docs/simulaqron.sdk.rst | 24 ++++ docs/simulaqron.settings.rst | 24 ++++ docs/simulaqron.start.rst | 24 ++++ simulaqron/cli.py | 199 ++++++++++++++++++++++------ simulaqron/network.py | 65 +++++---- simulaqron/reactor.py | 12 ++ simulaqron/sim_util.py | 23 ++-- 9 files changed, 337 insertions(+), 87 deletions(-) create mode 100644 docs/simulaqron.netqasm_backend.rst create mode 100644 docs/simulaqron.sdk.rst create mode 100644 docs/simulaqron.settings.rst create mode 100644 docs/simulaqron.start.rst diff --git a/docs/simulaqron.netqasm_backend.rst b/docs/simulaqron.netqasm_backend.rst new file mode 100644 index 00000000..4fca2cef --- /dev/null +++ b/docs/simulaqron.netqasm_backend.rst @@ -0,0 +1,24 @@ +simulaqron.netqasm_backend package +====================== + +.. warning:: UPDATE THIS STRUCTURE! + +Submodules +---------- + +simulaqron.run.run module +------------------------- + +.. automodule:: simulaqron.run.run + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: simulaqron.run + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/simulaqron.rst b/docs/simulaqron.rst index ef468349..bbac78ed 100644 --- a/docs/simulaqron.rst +++ b/docs/simulaqron.rst @@ -8,17 +8,29 @@ Subpackages simulaqron.general simulaqron.local + simulaqron.netqasm_backend simulaqron.run + simulaqron.sdk + simulaqron.settings + simulaqron.start simulaqron.toolbox simulaqron.virtual_node Submodules ---------- -simulaqron.SimulaQron module +simulaqron.cli module ---------------------------- -.. automodule:: simulaqron.simulaqron +.. automodule:: simulaqron.cli + :members: + :undoc-members: + :show-inheritance: + +simulaqron.math module +-------------------------- + +.. automodule:: simulaqron.math :members: :undoc-members: :show-inheritance: @@ -31,14 +43,21 @@ simulaqron.network module :undoc-members: :show-inheritance: -simulaqron.settings module --------------------------- +simulaqron.reactor module +------------------------- -.. automodule:: simulaqron.settings +.. automodule:: simulaqron.reactor :members: :undoc-members: :show-inheritance: +simulaqron.sim_util module +------------------------- + +.. automodule:: simulaqron.sim_util + :members: + :undoc-members: + :show-inheritance: Module contents --------------- diff --git a/docs/simulaqron.sdk.rst b/docs/simulaqron.sdk.rst new file mode 100644 index 00000000..93615f4d --- /dev/null +++ b/docs/simulaqron.sdk.rst @@ -0,0 +1,24 @@ +simulaqron.sdk package +====================== + +.. warning:: UPDATE THIS STRUCTURE! + +Submodules +---------- + +simulaqron.run.run module +------------------------- + +.. automodule:: simulaqron.run.run + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: simulaqron.run + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/simulaqron.settings.rst b/docs/simulaqron.settings.rst new file mode 100644 index 00000000..5ab45ce9 --- /dev/null +++ b/docs/simulaqron.settings.rst @@ -0,0 +1,24 @@ +simulaqron.settings package +====================== + +.. warning:: UPDATE THIS STRUCTURE! + +Submodules +---------- + +simulaqron.run.run module +------------------------- + +.. automodule:: simulaqron.run.run + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: simulaqron.run + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/simulaqron.start.rst b/docs/simulaqron.start.rst new file mode 100644 index 00000000..acb01b29 --- /dev/null +++ b/docs/simulaqron.start.rst @@ -0,0 +1,24 @@ +simulaqron.start package +====================== + +.. warning:: UPDATE THIS STRUCTURE! + +Submodules +---------- + +simulaqron.run.run module +------------------------- + +.. automodule:: simulaqron.run.run + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: simulaqron.run + :members: + :undoc-members: + :show-inheritance: diff --git a/simulaqron/cli.py b/simulaqron/cli.py index d04e11c8..a76d6cf1 100644 --- a/simulaqron/cli.py +++ b/simulaqron/cli.py @@ -38,24 +38,12 @@ def __init__(self, pidfile: Path): class SimulaQronDaemon(run.RunDaemon): - """ - Daemon process that runs a SimulaQron network in the background. - - This daemon spawns virtual nodes and QNodeOS servers for each node - in the network configuration. It runs until explicitly stopped. - - Attributes - ---------- - name : str - Name of the network (e.g., 'default'). - nodes : List[str] - List of node names to start (e.g., ['Alice', 'Bob']). - network_config_file : Path - Path to the network configuration JSON file. - """ def __init__(self, pidfile: Path, name: str, nodes: List[str], network_config_file: Path): """ - Initialize the SimulaQron daemon. + Daemon process that runs a SimulaQron network in the background. + + This daemon spawns virtual nodes and QNodeOS servers for each node + in the network configuration. It runs until explicitly stopped. :param pidfile: Path to the PID file used to track the daemon process. :type pidfile: Path @@ -192,7 +180,18 @@ def version(): default="", ) def start(name: str, nodes: str, simulaqron_config_file: Path, network_config_file: Path): - """Starts a network with the given parameters or from config files.""" + """ + Starts a network with the given parameters or from config files. + + :param name: Name of the network to start. + :type name: str + :param nodes: Comma separated list of nodes to start. + :type nodes: str + :param simulaqron_config_file: Path to simulaqron's config file. + :type simulaqron_config_file: Path + :param network_config_file: Path to network config file. + :type network_config_file: Path + """ # Checks the simulaqron config if not _path_exists(simulaqron_config_file): raise click.BadOptionUsage( @@ -268,7 +267,12 @@ def start(name: str, nodes: str, simulaqron_config_file: Path, network_config_fi default="default", ) def stop(name: str): - """Stops a network.""" + """ + Stops a network. + + :param name: Name of the network to stop. + :type name: str + """ assert name is not None pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" logging.debug("Trying to open PIDfile") @@ -291,7 +295,11 @@ def stop(name: str): is_flag=True, ) def reset(force: bool): - """Resets simulaqron""" + """ + Resets simulaqron. This command will stop any running network and reset the local SimulaQron + settings to their default. + :param force: Don't ask for confirmation, and immediately reset the simulaqron settings. + """ if not force: answer = input("Are you sure you want to reset simulaqron?\nThis will revert settings and " "network config files to the default values.\nNote, this action will remove " @@ -320,6 +328,9 @@ def reset(force: bool): help="Change a simulaqron setting" ) def set(): + """ + Change a SimulaQron setting. + """ pass @@ -327,6 +338,9 @@ def set(): help="Sets all settings back to default and saves it as a local configuration file in the current folder." ) def default(): + """ + Sets all settings back to default and saves it as a local configuration file in the current folder. + """ simulaqron_settings.default_settings() simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @@ -338,7 +352,13 @@ def default(): "value", type=click.Choice([b.value for b in SimBackend]) ) -def sim_backend(value): +def sim_backend(value: SimBackend): + """ + The backend to use (stabilizer, projectq, qutip). + + :param value: Value of the backend to use. This can either be ``stabilizer``, ``projectq`` or ``qutip``. + :type value: SimBackend + """ _create_local_settings_if_needed_and_load() simulaqron_settings.sim_backend = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @@ -352,7 +372,13 @@ def sim_backend(value): 'value', type=int ) -def max_qubits(value): +def max_qubits(value: int): + """ + Sets the max virt-qubits per node and max sim-qubits per register. + + :param value: Value of the max virt-qubits per node and max sim-qubits per register. + :type value: int + """ _create_local_settings_if_needed_and_load() simulaqron_settings.max_qubits = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @@ -366,7 +392,13 @@ def max_qubits(value): 'value', type=int ) -def max_registers(value): +def max_registers(value: int): + """ + Sets how many registers a node can hold. + + :param value: Value of the max registers a node can hold. + :type value: int + """ _create_local_settings_if_needed_and_load() simulaqron_settings.max_registers = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @@ -380,7 +412,12 @@ def max_registers(value): 'value', type=float ) -def conn_retry_time(value): +def conn_retry_time(value: float): + """ + Sets the conn_retry_time; how long to wait until a retry a connection to a SimulaQron component. + + :param value: Value of the conn_retry_time. + """ _create_local_settings_if_needed_and_load() simulaqron_settings.conn_retry_time = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @@ -394,7 +431,12 @@ def conn_retry_time(value): 'value', type=float ) -def recv_timeout(value): +def recv_timeout(value: float): + """ + Sets the recv_timeout in seconds before raising a timeout when receiving a qubit or EPR pair. + + :param value: Value of the recv_timeout. + """ _create_local_settings_if_needed_and_load() simulaqron_settings.recv_timeout = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @@ -402,13 +444,18 @@ def recv_timeout(value): @set.command( - help="When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received." + help="When receiving a qubit or EPR pair, how long to wait between attempts to receive an EPR pair half." ) @click.argument( 'value', type=float ) -def recv_retry_time(value): +def recv_retry_time(value: float): + """ + Sets the recv_retry_time value as the number of seconds to wait between attempts when receiving a qubit or EPR pair. + + :param value: Value of the recv_retry_time. + """ _create_local_settings_if_needed_and_load() simulaqron_settings.recv_retry_time = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @@ -422,7 +469,14 @@ def recv_retry_time(value): 'value', type=int ) -def log_level(value): +def log_level(value: int): + """ + Sets the log level for both backend and frontend. Possible values are 10=DEBUG, 20=INFO, 30=WARNING, + 40=ERROR, 50=CRITICAL. + + :param value: Value of the log_level. + :type value: int + """ _create_local_settings_if_needed_and_load() simulaqron_settings.log_level = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @@ -436,7 +490,13 @@ def log_level(value): 'value', type=click.Choice(["on", "off"]) ) -def noisy_qubits(value): +def noisy_qubits(value: str): + """ + Configures SimulaQron to simulate noisy qubits or not. + :param value: A string whether to noisy qubits or not. The string "no" will be interpreted as + not using noisy qubits. Any other string will be interpreted as using noisy qubits. + :type value: str + """ _create_local_settings_if_needed_and_load() if value == "on": simulaqron_settings.noisy_qubits = True @@ -453,7 +513,12 @@ def noisy_qubits(value): 'value', type=float ) -def t1(value): +def t1(value: float): + """ + Sets the T1 value for noisy qubits. + :param value: The T1 value for noisy qubits. + :type value: float + """ _create_local_settings_if_needed_and_load() simulaqron_settings.t1 = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) @@ -475,6 +540,9 @@ def get(): help="The backend to use (stabilizer, projectq, qutip).", ) def sim_backend(): + """ + Prints the current configured simulaqron backend. + """ _load_local_settings_or_default() print(simulaqron_settings.sim_backend) @@ -483,6 +551,9 @@ def sim_backend(): help="Max virt-qubits per node and max sim-qubits per register." ) def max_qubits(): + """ + Prints the current configured max virt-qubits per node and max sim-qubits per register. + """ _load_local_settings_or_default() print(simulaqron_settings.max_qubits) @@ -491,6 +562,9 @@ def max_qubits(): help="How many registers a node can hold." ) def max_registers(): + """ + Prints the current configured max number of register a node can hold. + """ _load_local_settings_or_default() print(simulaqron_settings.max_registers) @@ -499,6 +573,10 @@ def max_registers(): help="If setup fails, how long to wait until a retry." ) def conn_retry_time(): + """ + Prints the current configured conn_retry value; the number of seconds to wait before retrying + to connect to another node or SimulaQron component. + """ _load_local_settings_or_default() print(simulaqron_settings.conn_retry_time) @@ -507,6 +585,10 @@ def conn_retry_time(): help="When receiving a qubit or EPR pair, how long to wait until raising a timeout." ) def recv_timeout(): + """ + Prints the current configured recv_timeout value; the number of seconds to wait for receiving + an EPR half before raising a timeout error. + """ _load_local_settings_or_default() print(simulaqron_settings.recv_timeout) @@ -515,6 +597,10 @@ def recv_timeout(): help="When receiving a qubit or EPR pair, how long to wait between checks of whether a qubit is received." ) def recv_retry_time(): + """ + Prints the current configured recv_retry_time value; the number of seconds to wait between + attempts to receive an EPR half. + """ _load_local_settings_or_default() print(simulaqron_settings.recv_retry_time) @@ -523,6 +609,9 @@ def recv_retry_time(): help="Log level for both backend and frontend." ) def log_level(): + """ + Prints the current configured log level. + """ _load_local_settings_or_default() print(simulaqron_settings.log_level) @@ -531,6 +620,9 @@ def log_level(): help="Whether qubits should be noisy (on/off)" ) def noisy_qubits(): + """ + Prints whether SimulaQron has been configured to simulate noisy qubits or not. + """ _load_local_settings_or_default() if simulaqron_settings.noisy_qubits: print("on") @@ -542,6 +634,9 @@ def noisy_qubits(): help="The effective T1 to be used for noisy qubits" ) def t1(): + """ + Prints the current configured t1 value when simulating noisy qubits. + """ _load_local_settings_or_default() print(simulaqron_settings.t1) @@ -553,9 +648,10 @@ def t1(): @cli_entry_point.group() def nodes(): """ - Manage the nodes in the simulated network. + Manage the nodes in the simulated network. This command *will alter* the + ``simulaqron_network.json`` file in the current directory. - NOTE: This needs to be done before starting the network. + .. note:: This needs to be done before starting the network. """ pass @@ -585,11 +681,24 @@ def nodes(): def add(name: str, network_name: str, hostname: str, app_port: int, qnodeos_port: int, vnode_port: int, neighbors: Optional[str] = None): """ - Add a node to the network. - - NAME: The name of the node, e.g. Alice - - HOSTNAME: The host name of the node, e.g. localhost or 192.168.0.1 + Add a node to the network. This command *will modify* the ``simulaqron_network.json`` + file in the current directory. + + :param name: The name of the node to add. + :type name: str + :param network_name: The name of the network to add the node to. + :type network_name: str + :param hostname: The hostname of the machine that will run the node, e.g. localhost. + :type hostname: str + :param app_port: The port number for the application, e.g. 8000 + :type app_port: int + :param qnodeos_port: The port number for the qnodeos server, e.g. 8000 + :type qnodeos_port: int + :param vnode_port: The port number for the virtual node, e.g. 8000 + :type vnode_port: int + :param neighbors: A comma-separated list of neighbors of the given node. The given names + *will not be checked* if they exist in the given network. + :type neighbors: Optional[str] """ _create_local_networks_if_needed_and_load() network_config.read_from_file(LOCAL_NETWORK_SETTINGS) @@ -615,9 +724,13 @@ def add(name: str, network_name: str, hostname: str, app_port: int, qnodeos_port help="The name of the network") def remove(name: str, network_name: str): """ - Remove a node to the network. + Remove a node to the network. This command *will modify* the ``simulaqron_network.json`` + file in the current directory. - NAME: The name of the node, e.g. Alice + :param name: The name of the node to remove. + :type name: str + :param network_name: The name of the network to remove the node from. + :type network_name: str """ if not LOCAL_NETWORK_SETTINGS.exists() or not LOCAL_NETWORK_SETTINGS.is_file(): @@ -633,7 +746,8 @@ def remove(name: str, network_name: str): @nodes.command() def default(): """ - Sets the default nodes of the network. + Sets the default nodes of the network. This command *will modify* the ``simulaqron_network.json`` + file in the current directory. The default network consists of the five nodes: Alice, Bob, Charlie, David, Eve @@ -647,7 +761,12 @@ def default(): @click.option('--network-name', type=str, default="default", help="The name of the network") def get(network_name: str): - """Get the current nodes of the network.""" + """ + Print the nodes present in the given network. + + :param network_name: The name of the network to get the nodes from. + :type network_name: str + """ if not LOCAL_NETWORK_SETTINGS.exists() or not LOCAL_NETWORK_SETTINGS.is_file(): print(f"WARNING - the file '{LOCAL_NETWORK_SETTINGS}' was not found. The loaded " diff --git a/simulaqron/network.py b/simulaqron/network.py index 6cf6f591..dc82d0db 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -59,12 +59,12 @@ def __init__(self, nodes: List[str], network_config_file: Path, network_name: st This class uses the network configuration loaded in the global network_config object and starts the nodes mentioned in the constructor of this class. - :param network_name: str - The name of network to start. Defaults to "default". - :param network_config_file: Path - Path to network config file (required). - :param nodes: list of str - A list of strings with the node names to start. + :param nodes: A list of strings with the node names to start. + :type nodes: List[str] + :param network_config_file: Path to network config file (required). + :type network_config_file: str + :param network_name: The name of network to start. Defaults to "default". + :type network_name: str """ self._network_config_file = network_config_file @@ -84,9 +84,12 @@ def __init__(self, nodes: List[str], network_config_file: Path, network_name: st self._setup_processes() @property - def running(self): + def running(self) -> bool: """ - Is the network up and running? + Checks whether the network up and running. + + :return: True if the network up and running. False otherwise + :rtype: bool """ if self._running: return True @@ -114,6 +117,10 @@ def __del__(self): def _setup_processes(self): """ Setup the processes forming the network, however they are not started yet. + + This method creates the following *heavy processes* (either by forking or spawning): + * One for SimulaQron's Virtual Node. + * One for QNodeOS's (NetQASM interpreter) server. """ for node in self._nodes_to_start: process_virtual = Process( @@ -173,15 +180,17 @@ def __str__(self): # Helper functions to build topologies -def construct_topology_config(topology: str | Dict | None, nodes: List[str]) -> Optional[Dict[str, List[str]]]: +def construct_topology_config(topology: str | Dict | None, nodes: List[str]) -> Dict[str, List[str]]: """ - Constructs a json file at config/topology.json, used to define the topology of the network. - - :param topology: str - Should be one of the following: None, 'complete', 'ring', 'random_tree'. - :param nodes: list of str - List of the names of the nodes. - :return: None + Constructs a dictionary that maps the node names with their neighbours, representing a network topology. + + :param topology: The type of topology to generate. Should be one of the following: None, 'complete', + 'ring', 'random_tree'. + :type topology: str | Dict + :param nodes: List of the names of the nodes. + :type nodes: List[str] + :return: A dictionary where keys are the names of the nodes and values are a list of strings of their neighbors + :rtype: Dict[str, List[str]] """ if isinstance(topology, str): # Trick to get the integer after "random_connected": split on that string @@ -229,14 +238,14 @@ def construct_topology_config(topology: str | Dict | None, nodes: List[str]) -> return adjacency_dct -def get_random_tree(nodes): +def get_random_tree(nodes: List[str]) -> Dict[str, List[str]]: """ Constructs a dictionary describing a random tree, with the name of the vertices are taken from the 'nodes' - :param nodes: list of str - Name of the nodes to be used - :return: dct - keys are the names of the nodes and values their neighbors + :param nodes: Name of the nodes to be used + :type nodes: List[str] + :return: A dictionary where keys are the names of the nodes and values are a list of strings of their neighbors + :rtype: Dict[str, List[str]] """ tree = nx.random_tree(len(nodes)) @@ -250,17 +259,17 @@ def get_random_tree(nodes): return adjacency_dct -def get_random_connected(nodes, nr_edges): +def get_random_connected(nodes: List[str], nr_edges: int) -> Dict[str, List[str]]: """ Constructs a dictionary describing a random connected graph with a specified number of edges, with the name of the vertices are taken from the 'nodes' - :param nodes: list of str - Name of the nodes to be used - :param nr_edges: int - The number of edges that the graph should have. - :return: dct - keys are the names of the nodes and values their neighbors + :param nodes: Name of the nodes to be used + :type nodes: List[str] + :param nr_edges: The number of edges that the graph should have. + :type nr_edges: int + :return: A dictionary where keys are the names of the nodes and values are a list of strings of their neighbors + :rtype: Dict[str, List[str]] """ nn = len(nodes) min_edges = nn - 1 diff --git a/simulaqron/reactor.py b/simulaqron/reactor.py index 1e7a860b..b6853e43 100644 --- a/simulaqron/reactor.py +++ b/simulaqron/reactor.py @@ -7,3 +7,15 @@ __all__ = [ "reactor" ] + +""" +The reactor module helps installing the correct twisted reactor. +In SimulaQron, it is *highly encouraged* to use the rector offered by this module +rather than the one available in the ``twisted.internet`` package:: + +from simulaqron.reactor import reactor +... +reactor.start() +... +reactor.stop() +""" \ No newline at end of file diff --git a/simulaqron/sim_util.py b/simulaqron/sim_util.py index 9b721153..7569548e 100644 --- a/simulaqron/sim_util.py +++ b/simulaqron/sim_util.py @@ -10,20 +10,15 @@ def get_qubit_state(qubit: Qubit, reduced_dm: bool = True) -> np.ndarray: """Get the state of the qubit, only possible in simulation and can be used for debugging. - .. note:: The function gets the *current* state of the qubit(s). So make sure the subroutine is flushed - before calling the method. - - Parameters - ---------- - qubit : :class:`~netqasm.sdk.Qubit` - The qubit to get the state of . - reduced_dm : bool - Unused; declared to keep compatibility with other simulation engines - - Returns - ------- - np.array - The state as a density matrix. + .. note:: The function gets the *current* state of the qubit(s). So make sure the subroutine + is flushed before calling the method. + + :param qubit: The qubit(s) to get the state of. + :type qubit: Qubit + :param reduced_dm: Unused; declared to keep compatibility with other simulation engines + :type reduced_dm: bool + :return: The state as a density matrix. + :rtype: np.array """ # Since the qubit state data is maintained by the virtual node, we need to # find a way to "bypass" the QNodeOS layer and retrieve the qubit state from From 4a7d8acf7ab7a29d2734d34b9a5514ebdc244bf3 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 14 Jan 2026 21:58:07 +0100 Subject: [PATCH 196/277] Update documentation of the simulaqron.netqasm_backend package --- docs/simulaqron.netqasm_backend.rst | 23 +- simulaqron/netqasm_backend/executioner.py | 311 +++++++++++++++++----- simulaqron/netqasm_backend/factory.py | 90 +++++-- simulaqron/netqasm_backend/qnodeos.py | 40 ++- 4 files changed, 367 insertions(+), 97 deletions(-) diff --git a/docs/simulaqron.netqasm_backend.rst b/docs/simulaqron.netqasm_backend.rst index 4fca2cef..4856df2b 100644 --- a/docs/simulaqron.netqasm_backend.rst +++ b/docs/simulaqron.netqasm_backend.rst @@ -1,24 +1,37 @@ simulaqron.netqasm_backend package ====================== -.. warning:: UPDATE THIS STRUCTURE! - Submodules ---------- -simulaqron.run.run module +simulaqron.netqasm_backend.executioner module +------------------------- + +.. automodule:: simulaqron.netqasm_backend.executioner + :members: + :undoc-members: + :show-inheritance: + +simulaqron.netqasm_backend.factory module ------------------------- -.. automodule:: simulaqron.run.run +.. automodule:: simulaqron.netqasm_backend.factory :members: :undoc-members: :show-inheritance: +simulaqron.netqasm_backend.qnodeos module +------------------------- + +.. automodule:: simulaqron.netqasm_backend.qnodeos + :members: + :undoc-members: + :show-inheritance: Module contents --------------- -.. automodule:: simulaqron.run +.. automodule:: simulaqron.netqasm_backend :members: :undoc-members: :show-inheritance: diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 074a3c08..0e55ce45 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -4,7 +4,7 @@ from collections import defaultdict from enum import Enum from functools import partial -from typing import Generator, List, Tuple +from typing import Generator, List, Tuple, Callable, Dict import netqasm.lang.instr.core as core_instructions import netqasm.lang.instr.vanilla as vanilla_instructions @@ -15,10 +15,11 @@ from netqasm.qlink_compat import (Basis, BellState, LinkLayerErr, LinkLayerOKTypeK, LinkLayerOKTypeM, LinkLayerOKTypeR, RandomBasis, RequestType, - ReturnType) + ReturnType, LinkLayerCreate) from twisted.internet import task from twisted.internet.defer import inlineCallbacks, Deferred from twisted.spread import pb +from twisted.spread.flavors import Referenceable from simulaqron.reactor import reactor from simulaqron.general import SimUnsupportedError @@ -29,20 +30,27 @@ class UnknownQubitError(RuntimeError): + """ + Raised when the requested qubit ID could not be found. + """ pass +_VanillaRotInstr = vanilla_instructions.RotXInstruction | vanilla_instructions.RotYInstruction | vanilla_instructions.RotZInstruction +# TODO - This class is candidate to be deleted! Test and delete if not needed! class NetworkStack(BaseNetworkStack): - def __init__(self, executioner): - """This is just a wrapper around the executioners methods for entanglement generation + """ + This is just a wrapper around the executioners methods for entanglement generation in order to use the correct framework as used by the netqasm executioner. + + .. warning:: This class is candidate to be deleted! Test and delete if not needed! """ self._executioner = executioner - self._sockets = {} + self._sockets: Dict[int, Tuple[int, int]] = {} def put(self, request): - """Handles an request to the network stack""" + """Handles a request to the network stack""" raise NotImplementedError("NetworkStack.put") def setup_epr_socket(self, epr_socket_id, remote_node_id, remote_epr_socket_id, timeout=1): @@ -53,7 +61,6 @@ def setup_epr_socket(self, epr_socket_id, remote_node_id, remote_epr_socket_id, def get_purpose_id(self, remote_node_id: int, epr_socket_id: int) -> int: pass - class VanillaSimulaQronExecutioner(Executor): SIMULAQRON_OPS = { vanilla_instructions.GateXInstruction: "apply_X", @@ -84,6 +91,21 @@ class VanillaSimulaQronExecutioner(Executor): _num_bits_prob = 8 def __init__(self, *args, **kwargs): + """ + Creates a class that is capable of executing some "vanilla" NetQASM instructions using + the SimulaQron simulator. This is the main class that bridges the "QNodeOS" world with the + SimulaQron simulator. + Every instruction of the NetQASM subroutine is sent to an instance of this class, so the + "QNodeOS" server knows how to interact with SimulaQron's Virtual Node to command the execution + of the NetQASM instructions. + To this end, this class implements the ``netqasm.executor.backend.Executor`` class, overwriting + the methods that are invoked by the NetQASM library when executing a subroutine. + + :param args: Arguments directly passed to the NetQASM ``Executor`` constructor. + :type args: Any + :param kwargs: Keyword arguments directly passed to the NetQASM ``Executor`` constructor. + :type kwargs: Any + """ super().__init__(*args, **kwargs) self._return_msg_func = None self._factory = None @@ -91,16 +113,24 @@ def __init__(self, *args, **kwargs): @property def factory(self) -> "NetQASMFactory": # noqa: F821 + """ + The NetQASM Factory associated with this executioner. + """ return self._factory @property def node_id(self): + """ + Returns the ID of the node on which this simulation runs on. + """ return get_node_id_from_net_config(self.factory.qnodeos_net, self.name) @staticmethod def get_error_class(remote_err): """ This is a function to get the error class of a remote thrown error when using callRemote. + .. warning:: This method is candidate to be deleted. + :param remote_err: :obj:`twisted.spread.pb.RemoteError` :return: class """ @@ -112,10 +142,22 @@ def get_error_class(remote_err): return error_class - def add_return_msg_func(self, func): + def add_return_msg_func(self, func: Callable): + """ + Sets the function invoked for returning a message back to the client + + :param func: The function tobe used to return a message back to the client. + :type func: Callable + """ self._return_msg_func = func def add_factory(self, factory: "NetQASMFactory"): # noqa: F821 + """ + Sets the factory object used in the connection. + + :param factory: The factory object + :type factory: NetQASMFactory + """ self._factory = factory def _handle_command_exception(self, exc, prog_counter, traceback_str): @@ -138,12 +180,15 @@ def _instr_qalloc(self, subroutine_id: int, instr: core_instructions.QAllocInstr yield self.cmd_new(physical_address=physical_address) @inlineCallbacks - def cmd_new(self, physical_address): + def cmd_new(self, physical_address: int): """ - Request a new qubit. Since we don't need it, this python NetQASM just provides very crude timing information. - (return_q_id is used internally) - (ignore_max_qubits is used internally to ignore the check of number of virtual qubits at the node - such that the node can temporarily create a qubit for EPR creation.) + Request a new qubit. Since we don't need it, this python NetQASM just provides very crude timing + information (return_q_id is used internally). + Additionally, ignore_max_qubits is used internally to ignore the check of number of virtual qubits + at the node such that the node can temporarily create a qubit for EPR creation. + + :param physical_address: The physical address of the qubit to be created. + :type physical_address: int """ try: yield self.factory._lock.acquire() @@ -167,7 +212,8 @@ def _do_single_qubit_instr(self, instr, subroutine_id, address): qubit_id=position, ) - def _do_single_qubit_rotation(self, instr, subroutine_id, address, angle): + def _do_single_qubit_rotation(self, instr: core_instructions.RotationInstruction, subroutine_id: int, address: int, angle: float): + assert isinstance(instr, _VanillaRotInstr) position = self._get_position(subroutine_id=subroutine_id, address=address) axis = self._get_axis(instr=instr) yield self.apply_rotation( @@ -177,17 +223,25 @@ def _do_single_qubit_rotation(self, instr, subroutine_id, address, angle): ) @classmethod - def _get_axis(cls, instr): + def _get_axis(cls, instr: _VanillaRotInstr): axis = cls.ROTATION_AXIS.get(type(instr)) if axis is None: raise ValueError(f"Unknown rotation instruction {instr}") return axis @inlineCallbacks - def apply_rotation(self, axis, angle, qubit_id): + def apply_rotation(self, axis: Tuple[int, int, int], angle: float, qubit_id: int): """ - Apply a rotation of the qubit specified in cmd with an angle specified in xtra - around the axis + Executes a rotation of ``angle``radians around the ``axis`` axis of the specified + qubit ID. + + :param axis: The axis to rotate around. This axis is expected to be a tuple of + integers in the format (x, y, z). + :type axis: Tuple[int, int, int] + :param angle: The angle to rotate around. This angle should be in radians. + :type angle: float + :param qubit_id: The qubit to apply the rotation on. + :type qubit_id: int """ self._logger.debug("Applying a rotation around %s to physical qubit id %d", axis, qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) @@ -203,7 +257,17 @@ def _do_two_qubit_instr(self, instr, subroutine_id, address1, address2): ) @inlineCallbacks - def apply_two_qubit_gate(self, gate, qubit_id1, qubit_id2): + def apply_two_qubit_gate(self, gate: core_instructions.TwoQubitInstruction, qubit_id1: int, qubit_id2: int): + """ + Applies the given two-qubits gate to the physical qubit qubit_id1 and qubit_id2. + + :param gate: The two-qubit gate to apply. + :type gate: core_instructions.TwoQubitInstruction + :param qubit_id1: The qubit ID of the first qubit to use. + :type qubit_id1: int + :param qubit_id2: The qubit ID of the second qubit to use. + :type qubit_id2: int + """ self._logger.debug("Applying %s to physical qubit id %d target %d", gate, qubit_id1, qubit_id2) control = self.get_virt_qubit(qubit_id=qubit_id1) target = self.get_virt_qubit(qubit_id=qubit_id2) @@ -219,17 +283,31 @@ def _get_simulaqron_gate(cls, instr): return simulaqron_gate @inlineCallbacks - def apply_single_qubit_gate(self, gate, qubit_id): + def apply_single_qubit_gate(self, gate: core_instructions.SingleQubitInstruction, qubit_id: int): + """ + Applies the given single-qubit gate to the specified physical qubit ID. + + :param gate: The single-qubit gate to apply. + :type gate: core_instructions.SingleQubitInstruction + :param qubit_id: The qubit ID of the qubit to use. + :type qubit_id: int + """ virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) yield call_method(virt_qubit, gate) - def get_virt_qubit(self, qubit_id): + def get_virt_qubit(self, qubit_id: int) -> Referenceable: """ Get reference to the virtual qubit reference in SimulaQron given app and qubit id, if it exists. - If not found, send back no qubit error. - Caution: Twisted PB does not allow references to objects to be passed back between connections. + If not found, raises a :py:class:`UnknownQubitError`. + + .. Caution:: Twisted PB does not allow references to objects to be passed back between connections. If you need to pass a qubit reference back to the Twisted PB on a _different_ connection, then use get_virt_qubit_indep below. + + :param qubit_id: The qubit to get reference to. + :type qubit_id: int + :return: The virtual qubit reference as a ``twisted.spread.flavors.Referenceable`` object. + :rtype: twisted.spread.flavors.Referenceable """ if qubit_id not in self.factory.qubitList: raise UnknownQubitError(f"{self.name}: Qubit {qubit_id} not found") @@ -237,10 +315,14 @@ def get_virt_qubit(self, qubit_id): return qubit.virt @inlineCallbacks - def get_virt_qubit_num(self, qubit_id): + def get_virt_qubit_num(self, qubit_id: int): """ - Get NUMBER (not reference!) to virtual qubit in SimulaQron specific to this connection. - If not found, send back no qubit error. + Get the *integer* qubit ID to virtual qubit in SimulaQron specific to this connection. + .. caution:: This method return a qubit ID (an integer), not a ``twisted.spread.flavors.Referenceable`` + object. If you need to get a twisted object, check :py:meth:`get_virt_qubit`. + + :param qubit_id: The qubit ID to get virtual qubit ID. + :type qubit_id: int """ # First let's get the general virtual qubit reference, if any virt = self.get_virt_qubit(qubit_id=qubit_id) @@ -253,9 +335,14 @@ def _do_meas(self, subroutine_id, q_address): return outcome @inlineCallbacks - def cmd_measure(self, qubit_id, inplace=True): + def cmd_measure(self, qubit_id: int, inplace=True): """ - Measure + Executes a measure on the given qubit ID. + + :param qubit_id: The qubit ID to execute measure on. + :type qubit_id: int + :param inplace: If True, execute the measurement inplace. + :type inplace: bool """ self._logger.debug("Measuring physical qubit id %d", qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) @@ -266,9 +353,14 @@ def cmd_measure(self, qubit_id, inplace=True): return outcome @inlineCallbacks - def cmd_reset(self, qubit_id, correct=True): + def cmd_reset(self, qubit_id: int, correct: bool=True): r""" - Reset Qubit to \|0\> + Reset the given qubit to the state :math:`|0>`. + + :param qubit_id: The qubit ID to reset. + :type qubit_id: int + :param correct: If True, apply a correction to ensure the qubit was reset. + :type correct: bool """ self._logger.debug("Reset physical qubit id %d", qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) @@ -278,7 +370,7 @@ def cmd_reset(self, qubit_id, correct=True): if correct and outcome: yield call_method(virt_qubit, "apply_X") - def _do_wait(self, delay=0.1): + def _do_wait(self, delay: float=0.1): d = task.deferLater(reactor, delay, lambda: self._logger.debug("Wait finished")) self._logger.debug("waiting a bit") yield d @@ -302,12 +394,12 @@ def _update_shared_memory(self, app_id: int, entry: operand.Register | operand.A def _do_create_epr( self, - subroutine_id, - remote_node_id, - epr_socket_id, - q_array_address, - arg_array_address, - ent_results_array_address, + subroutine_id: int, + remote_node_id: int, + epr_socket_id: int, + q_array_address: int | None, + arg_array_address: int, + ent_results_array_address: int, ): create_request = self._get_create_request( subroutine_id=subroutine_id, @@ -346,11 +438,11 @@ def _do_create_epr( def _do_recv_epr( self, - subroutine_id, - remote_node_id, - epr_socket_id, - q_array_address, - ent_results_array_address + subroutine_id: int, + remote_node_id: int, + epr_socket_id: int, + q_array_address: int | None, + ent_results_array_address: int ): app_id = self._get_app_id(subroutine_id=subroutine_id) num_pairs = self._get_num_pairs_from_array( @@ -375,8 +467,8 @@ def _do_recv_epr( qubit_id=qubit_id, ) - def _get_remote_epr_socket_id(self, epr_socket_id): - remote_entry = self.network_stack._sockets.get(epr_socket_id) + def _get_remote_epr_socket_id(self, epr_socket_id: int) -> int: + remote_entry: Tuple[int, int] = self.network_stack._sockets.get(epr_socket_id) if remote_entry is None: raise ValueError(f"Unknown EPR socket ID {epr_socket_id}") return remote_entry[1] @@ -384,16 +476,29 @@ def _get_remote_epr_socket_id(self, epr_socket_id): @inlineCallbacks def cmd_epr( self, - create_id, - remote_node_id, - epr_socket_id, - remote_epr_socket_id, - qubit_id, - create_request, + create_id: int, + remote_node_id: int, + epr_socket_id: int, + remote_epr_socket_id: int, + qubit_id: int, + create_request: LinkLayerCreate, ): """ Create EPR pair with another node. - Depending on the ips and ports this will either create an EPR-pair and send one part, or just receive. + Depending on the IPs and ports this will either create an EPR-pair and send one part, or just receive. + + :param create_id: The create ID. + :type create_id: int + :param remote_node_id: The remote node ID. + :type remote_node_id: int + :param epr_socket_id: The EPR socket ID. + :type epr_socket_id: int + :param remote_epr_socket_id: The remote EPR socket ID. + :type remote_epr_socket_id: int + :param qubit_id: The qubit ID. + :type qubit_id: int + :param create_request: The :py:class:`LinkLayerCreate` object with the EPR information. + :type create_request: LinkLayerCreate """ # Get ip and port of remote host for remote_node_name, remote_host in self.factory.qnodeos_net.hostDict.items(): @@ -508,7 +613,7 @@ def cmd_epr( self._logger.debug("finished cmd_epr") @inlineCallbacks - def _measure_epr_qubit(self, qubit_id, request, remote: bool): + def _measure_epr_qubit(self, qubit_id: int, request: LinkLayerCreate, remote: bool): # Check the arguments depending on if this is the local or remote qubit if remote: assert request.rotation_X_remote1 == 0, "Measure directly with rotations not yet supported" @@ -552,13 +657,16 @@ def _measure_epr_qubit(self, qubit_id, request, remote: bool): return outcome, basis # NOTE this method is copied from netsquid magic - def _get_probability_weights(self, probability_dist_spec, num_choices): + def _get_probability_weights(self, probability_dist_spec: List[int], num_choices: int): """ - Used internally by `_sample_basis_choice` to convert specified probability distribution to correct form - - :param probability_dist_spec: list of ints - :param num_choices: int - :return: list of ints + Used internally by `_sample_basis_choice` to convert specified probability distribution to the correct form. + + :param probability_dist_spec: The spec of the probability distribution. + :type probability_dist_spec: List[int] + :param num_choices: The number of choices + :tyoe num_choices: int + :return: The probability weights. + :rtype: List """ num_values = 2 ** self._num_bits_prob if num_choices == 2: @@ -611,10 +719,17 @@ def _sample_basis_choice(self, random_basis_set, probability_dist_spec): return basis @classmethod - def new_ent_id(cls, epr_socket_id, remote_node_id, remote_epr_socket_id): + def new_ent_id(cls, epr_socket_id: int, remote_node_id: int, remote_epr_socket_id: int): """ Returns a new unique entanglement id for the specified host_app_id, remote_node and remote_app_id. Used by cmd_epr. + + :param epr_socket_id: The EPR socket ID. + :type epr_socket_id: int + :param remote_epr_socket_id: The remote EPR socket ID. + :type remote_epr_socket_id: int + :param remote_node_id: The remote node ID. + :type remote_node_id: int """ pair_id = (epr_socket_id, remote_node_id, remote_epr_socket_id) ent_id = cls._next_ent_id[pair_id] @@ -630,14 +745,25 @@ def _get_new_create_id(cls, remote_node_id): @inlineCallbacks def send_epr_half( self, - qubit_id, - epr_socket_id, - remote_node_name, - remote_epr_socket_id, - ent_info + qubit_id: int, + epr_socket_id: int, + remote_node_name: str, + remote_epr_socket_id: int, + ent_info: LinkLayerOKTypeK ): """ Send qubit to another node. + + :param qubit_id: The qubit ID. + :type qubit_id: int + :param epr_socket_id: The EPR socket ID. + :type epr_socket_id: int + :param remote_node_name: The remote node name. + :type remote_node_name: str + :param remote_epr_socket_id: The remote EPR socket ID. + :type remote_epr_socket_id: int + :param ent_info: The local entanglement information. + :type ent_info: LinkLayerOKTypeK """ # Lookup the virtual qubit from identifier virt_num = yield self.get_virt_qubit_num(qubit_id=qubit_id) @@ -676,17 +802,29 @@ def send_epr_half( @inlineCallbacks def send_epr_outcome_half( self, - epr_socket_id, - remote_node_name, - remote_epr_socket_id, - ent_info, + epr_socket_id: int, + remote_node_name: str, + remote_epr_socket_id: int, + ent_info: LinkLayerOKTypeM, remote_outcome, - remote_basis + remote_basis: Basis ): """ Send outcome from measure directly to another node. - """ + :param epr_socket_id:The EPR socket ID. + :type epr_socket_id: int + :param remote_node_name: The remote node name. + :type remote_node_name: str + :param remote_epr_socket_id: The remote EPR socket ID. + :type remote_epr_socket_id: int + :param ent_info: The entanglement info objects. + :type ent_info: LinkLayerOKTypeM + :param remote_outcome: The outcome of the remote measurement. + :type remote_outcome: + :param remote_basis: The base on which the remote qubit was mesured. + :type remote_basis: Basis + """ # Update raw entanglement information for remote node remote_ent_info = LinkLayerOKTypeM( type=ent_info.type, @@ -738,9 +876,14 @@ def _update_qubit_id(ent_info, qubit_id): return ent_info.__class__(**dct) @inlineCallbacks - def cmd_epr_recv(self, epr_socket_id, qubit_id=None): + def cmd_epr_recv(self, epr_socket_id: int, qubit_id: int): """ Receive half of epr from another node. Block until qubit is received. + + :param epr_socket_id: The EPR socket ID. + :type epr_socket_id: int + :param qubit_id: The qubit ID. + :type qubit_id: int """ self._logger.debug("Asking to receive for EPR socket ID %d", epr_socket_id) @@ -791,7 +934,13 @@ def cmd_epr_recv(self, epr_socket_id, qubit_id=None): self._handle_epr_response(response=ent_info) - def remove_qubit_id(self, qubit_id): + def remove_qubit_id(self, qubit_id: int): + """ + Removes the qubit ID from the NetQASMFactory object. + + :param qubit_id: The qubit ID to remove. + :type qubit_id: int + """ self._logger.debug("Removing physical qubit with ID %d from handles to simulated qubits", qubit_id ) @@ -826,6 +975,14 @@ def _clear_phys_qubit_in_memory(self, physical_address): def get_qubit_state( self, qubit_id: int ) -> Generator[Deferred, Tuple[List[float], List[float]], Tuple[List[float], List[float]]]: + """ + Retrieves the state of the given qubit ID as a real and imaginary part. + + :param qubit_id: The qubit ID to retrieve the state. + :type qubit_id: int + :return: A tuple containing 2 lists of floats, representing the real and imaginary part. + :rtype: Generator[Deferred, Tuple[List[float], List[float]], Tuple[List[float], List[float]]] + """ self._logger.debug("Retrieving the state of qubit id %d", qubit_id) virt_qubit = self.get_virt_qubit(qubit_id=qubit_id) real_part, im_part = yield call_method(virt_qubit, "get_density_matrix_RI") @@ -834,6 +991,16 @@ def get_qubit_state( class VirtualQubitRef: def __init__(self, qubit_id: int, timestamp: int, virt: pb.Referenceable): + """ + Reference to a Virtual Qubit. + + :param qubit_id: The qubit ID to reference. + :type qubit_id: int + :param timestamp: A timestamp. + :type timestamp: int + :param virt: The Referenceable object pointing to the virtual qubit. + :type virt: pb.Referenceable + """ self.qubit_id = qubit_id self.timestamp = timestamp self.virt = virt diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index 68176123..a4a4a44b 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -44,6 +44,9 @@ class IncompleteMessageError(ValueError): + """ + Raised when trying to parse an incomplete NetQASM message. + """ pass @@ -56,7 +59,17 @@ class NetQASMProtocol(Protocol): _next_ent_id = {} def __init__(self, factory: "NetQASMFactory"): + """ + Implementation of a ``twisted.internet.protocol.Protocol`` class that handles + the connection to a server capable of handling NetQASM messages. + + This class is the entry point when a new NetQASM message is received. It is also + responsible for calling (in a "twisted deferred" way) the message handlers that + implement the logic of handling and executing the message. + :param factory: The :py:class:`NetQASMFactory` object that handles this object. + :type factory: NetQASMFactory + """ # NetQASM Factory, including our connection to the SimulaQron backend self.factory = factory @@ -85,16 +98,23 @@ def __init__(self, factory: "NetQASMFactory"): self._logger.debug("Initialized Protocol") def connectionMade(self): + """ + Implementation of the ``connectionMade`` method from the ``twisted.internet.protocol.Protocol`` class. + """ self._logger.info("Connection made") pass def connectionLost(self, reason=connectionDone): + """ + Implementation of the ``connectionLost`` method from the ``twisted.internet.protocol.Protocol`` class. + """ self._logger.info(f"Connection lost: {reason}") self.factory._active_protocol = None self._cleanup_all() def _cleanup_all(self): - """Clean up all state - Note that we allow only ONE connection + """ + Clean up all state - Note that we allow only ONE connection to one NetQASM node server at a time, making the below safe. If we were to ever decide to allow multiple, the below is too radical @@ -131,10 +151,21 @@ def _cleanup_all(self): self._logger.info("Cleanup complete") - def dataReceived(self, data): + def dataReceived(self, data: bytes): """ - Receive data. We will always wait to receive enough data for the + We will always wait to receive enough data for the header, and then the entire packet first before commencing processing. + + This method is the first entry point when the SimulaQron QNodeOS server + receives a NetQASM message. Once any data is received, it attaches it to + an internal buffer, and tries to parse a NetQASM message. + If a message cannot be parsed, the raw data will be stored and this method + will return, waiting form more data. It a message can be parsed, it will be + for further processing to the message handler (usually the + :py:class:´SubroutineHandler´ class). + + :param data: The data received from the remote. + :type data: bytes """ # Read whatever we received into a buffer if self.buf: @@ -148,14 +179,14 @@ def dataReceived(self, data): return d = self.messageHandler.handle_netqasm_message(msg_id=msg_id, msg=msg) - d.addCallback(self.log_handled_message) - d.addErrback(self.log_error) + d.addCallback(self._log_handled_message) + d.addErrback(self._log_error) - def log_handled_message(self, result): + def _log_handled_message(self, result): self._logger.info("Finished handling message with result = %s", result) @inlineCallbacks - def log_error(self, failure): + def _log_error(self, failure): self._logger.error("Handling message failed with failure = %s", failure.value) self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(failure.value))) # self.transport.abortConnection() @@ -163,6 +194,10 @@ def log_error(self, failure): yield deferLater(reactor, 0.1, self.stop) def stop(self): + """ + Stops this NetQASM protocol object. No further messages will be handled + after invoking this method. + """ self.factory.stop() def _parse_message(self): @@ -217,9 +252,22 @@ def __init__( network_name: str = "default" ): """ - Initialize NetQASM Factory. - - lhost details of the local host (class host) + Factory class that creates :py:class:`NetQASMProtocol` objects. This factory + will create one protocol instance per connection. + + This factory is handled by the internals of the twisted reactor, and should not be + instantiated by the user. + + :param host: The hostname to listen to new connections. + :type host: str + :param name: A name for this protocol factory. + :type name: str + :param qnodeos_net: The :py:class:`SocketsConfig` object containing the *QNodeOS* sockets specifications. + :type qnodeos_net: SocketsConfig + :param backend: The class of QNodeOS subroutine handlers that the protocol will forward messages to. + :type backend: Type[SubroutineHandler] + :param network_name: The name of the network to handle messages from. + :type network_name: str """ self.host = host @@ -249,6 +297,10 @@ def __init__( self._active_protocol = None def stop(self): + """ + Stops this instance of the factory. No more protocol objects will be created + after invoking this method. + """ yield call_method(self.virtRoot, "stop_vnode") reactor.stop() @@ -263,13 +315,18 @@ def buildProtocol(self, addr): def set_virtual_node(self, virtRoot): """ Set the virtual root allowing connections to the SimulaQron backend. + + :param virtRoot: The virtual root object. """ self.virtRoot = virtRoot - def lookup(self, ip, port): + def lookup(self, ip: int, port: int) -> str | None: """ - Lookup name of remote host used within SimulaQron given ip and - portnumber. + Lookup name of remote host used within SimulaQron given ip and port number. + :param ip: The IP address to look for. This value needs to be transformed into an integer value. + :param port: The port to look for. + :return: The name of the node that matches the IP-port pair. None if none matches. + :rtype: str | None """ for entry in self.qnodeos_net.hostDict: node = self.qnodeos_net.hostDict[entry] @@ -279,14 +336,13 @@ def lookup(self, ip, port): self._logger.debug("No such node") return None - def is_adjacent(self, remote_host_name): + def is_adjacent(self, remote_host_name: str): """ Checks if remote host is adjacent to this node, according to the specified topology. - :param remote_host_name: str - The name of the remote host - :return: + :param remote_host_name: The name of the remote host + :type remote_host_name: str """ # Check if a topology is defined, otherwise use fully connected if self.topology is None: diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index dcd860ac..7f83f53e 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -19,6 +19,25 @@ class SubroutineHandler(QNodeController): def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = None, # noqa: F821 flavour: Optional[Flavour] = None): + """ + Class that handles the NetQASM messages and bridges the NetQASM with the SimulaQron world. + + The main responsibility of this class is to "transform" the native python generators (used + by the NetQASM library) into twisted ``Deferred`` s. + + Each time the QNodeOS Server (specifically, the NetQASMProtocol instance) receives and correctly + parses a NetQASM message (e.g. a subroutine), it will be delegated to this class for further + processing. + + The main entry point that process the message is the :py:meth:`handle_netqasm_message` method. + + :param factory: The :py:class:`NetQASMFactory` object. + :type factory: NetQASMFactory + :param instr_log_dir: Directory used to write log files to. + :type instr_log_dir: str | None + :param flavour: NetQASM flavour to use. + :type flavour: Flavour | None + """ super().__init__(factory.name, instr_log_dir=instr_log_dir, flavour=flavour) self.factory = factory @@ -41,10 +60,12 @@ def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = Non @property def protocol(self) -> Protocol: + """Returns the :py:class:`NetQASMProtocol` object associated with this routine Handler.""" return self._protocol @protocol.setter def protocol(self, protocol: Protocol): + """Sets the :py:class:`NetQASMProtocol` object associated with this routine Handler.""" self._protocol = protocol @inlineCallbacks @@ -53,15 +74,20 @@ def handle_netqasm_message(self, msg_id: int, msg: Message): Handle incoming NetQASM messages by bridging two async models. NetQASM's executor uses Python generators (yield from) while SimulaQron - uses Twisted deferreds (@inlineCallbacks). This method bridges them by: + uses Twisted deferred's (@inlineCallbacks). This method bridges them by: 1. Running the parent's generator manually 2. Detecting whether each yielded item is a Twisted Deferred or a nested generator - 3. For Deferreds: yielding to Twisted's reactor to await completion + 3. For Deferred's: yielding to Twisted's reactor to await completion 4. For nested generators: consuming them fully and capturing their return value Without this bridge, nested generator return values (like physical_address from _instr_qalloc) would be lost, causing None to propagate through the system. This is also what caused the tests to fail, and probably other random weird things. + + :param msg_id: The id of the message to process. + :type msg_id: int + :param msg: The message to process. + :type msg: Message """ print(f"DEBUG handle_netqasm_message: msg_id={msg_id}", flush=True) gen = super().handle_netqasm_message( @@ -127,10 +153,18 @@ def _mark_message_finished(self, msg_id: int, msg: Message): self._return_msg(msg=ret_msg) def stop(self): + """ + Stops this instance of the SubroutineHandler. + """ self.factory.stop() def _return_msg(self, msg: Message): - """Return a message to the host""" + """ + Returns (by sending it back to the application server) a message to the host + + :param msg: The message to return. + :type msg: Message + """ assert self._protocol is not None, "Seems protocol of handler has not yet been set" self._logger.debug("sending message %s to host", msg) self.protocol._return_msg(msg=bytes(msg)) From f29cc80c5afa99448b93979683414386d6c98adf Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 15 Jan 2026 15:04:55 +0100 Subject: [PATCH 197/277] Update documentation of the simulaqron.run package --- simulaqron/run/run.py | 123 +++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 61 deletions(-) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 1d7bba16..7e776112 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -1,4 +1,3 @@ -import logging import os import signal import time @@ -31,8 +30,6 @@ logger = logging.getLogger() -# TODO similar code to squidasm.run.run, make base-class and subclasses? - _SIMULAQRON_BACKENDS = { Formalism.STAB: SimBackend.STABILIZER, @@ -41,13 +38,19 @@ } -def as_completed(futures: List[ApplyResult], names: List[str]) -> List[Tuple[ApplyResult, str]]: +def _as_completed(futures: List[ApplyResult], names: List[str]) -> List[Tuple[ApplyResult, str]]: if len(futures) is not len(names): raise RuntimeError("Not all registered applications have an associated name") return [(future, name) for future, name in zip(futures, names)] def reset(save_loggers=False): + """ + Resets the SimulaQron simulation to a clean state, leaving it ready for a new application execution. + + :param save_loggers: Whether to save the NetQASM's struct logs in a file or not. + :type save_loggers: bool + """ if save_loggers: save_all_struct_loggers() SharedMemoryManager.reset_memories() @@ -58,7 +61,7 @@ def reset(save_loggers=False): reload(logging) -def setup_sim_backend(sim_backend: SimBackend): +def _setup_sim_backend(sim_backend: SimBackend): if sim_backend in [SimBackend.PROJECTQ, SimBackend.QUTIP]: assert find_spec(sim_backend.value) is not None, \ f"To use {sim_backend} as backend you need to install the package" @@ -67,23 +70,23 @@ def setup_sim_backend(sim_backend: SimBackend): # Global array helper to store PIDs of the children processes running the applications # Note; this array *will not* store the pids of the QNodeOS and/or Vnode processes -apps_pids: Optional[SynchronizedArray] = None +_apps_pids: Optional[SynchronizedArray] = None def _worker_initializer(synced_array: SynchronizedArray): # We simply store the reference of the synced object for this process - global apps_pids - apps_pids = synced_array + global _apps_pids + _apps_pids = synced_array def _app_wrapper(**kwargs): - global apps_pids - assert apps_pids is not None + global _apps_pids + assert _apps_pids is not None assert "__instance_num" in kwargs and isinstance(kwargs["__instance_num"], int) assert "__entry_function" in kwargs and isinstance(kwargs["__entry_function"], Callable) # Save the pid for this worker - apps_pids[kwargs["__instance_num"]] = os.getpid() + _apps_pids[kwargs["__instance_num"]] = os.getpid() entry_function = kwargs["__entry_function"] del kwargs["__entry_function"] del kwargs["__instance_num"] @@ -99,14 +102,17 @@ def _app_wrapper(**kwargs): def _signal_other_apps(): - global apps_pids - assert apps_pids is not None - for pid in apps_pids: + global _apps_pids + assert _apps_pids is not None + for pid in _apps_pids: # Do not send SIGINT to self process if pid != os.getpid(): os.kill(pid, signal.SIGINT) +# The signature of this function was "harmonized" with the `run_applications` method exposed +# by the SquidASM simulator. The idea was to allow programs written in NetQASM to be executed +# both in SimulaQron and SquidASM _with minimal changes_. def run_applications( app_instance: ApplicationInstance, num_rounds: int = 1, @@ -120,45 +126,44 @@ def run_applications( hardware: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" init_func: Callable = None, ) -> List[Dict[str, Any]]: - """Executes functions containing quantum applications. - - Parameters - ---------- - app_instance : ApplicationInstance - Keys should be names of nodes - Values should be the functions - num_rounds : int - Number executions for this simulation - network_cfg: - Path of the network configuration file. - nv_cfg: Any - Unused argument. Any parameter given here will be ignored. - log_cfg: LogConfig - Configuration for the logging. - formalism: Formalism - Qubit formalism to use for the simulation. The SimulaQron - backend to use depends on this value. - use_app_config: bool - Whether to give app_config as argument to app's main() - post_function: Optional[Callable] - Function to execute after all rounds have been executed. - enable_logging: bool - Whether to enable logging. - hardware: Any - Unused argument. Any parameter given here will be ignored. - init_func: Callable - Function to execute to initialize the state of the child processes. The implemented - executor uses the *spawn* method for creating new processes. In this sense, the - child processes *do not receive* a copy of the full memory, but only what is needed. - In particular, all modules will be reimported in the child processes, hence any - state of the classes *will not transfer* to the child processes. - - Returns - ------- - List[Dict[str, Any]] - List of dictionaries describing the application names and the simulation results. - The i-th entry of the list will correspond to the i-th execution round of the - simulation. + """ + Executes functions containing quantum applications. + + :param app_instance: A ``netqasm.runtime.Application`` instance containing the names of the nodes + and the function that implements the application. The easiest way to create + this object is by using the ``default_app_instance`` from the + ``netqasm.runtime.application`` module. Please check the documentation from that + method to get more information. + :type app_instance: ApplicationInstance + :param num_rounds: Number executions for this simulation. + :type num_rounds: int + :param network_cfg: Path of the network configuration file. + :type network_cfg: str | Path | PathLike | None + :param nv_cfg: Unused argument. Any parameter given here will be ignored. + :type nv_cfg: Any + :param log_cfg: Configuration object for the logging. Check the documentation of :py:class:`LogConfig` + for more information abut how to configure the logging. + :type log_cfg: LogConfig + :param formalism: Qubit formalism to use for the simulation. The SimulaQron backend to use depends + on this value. + :type formalism: Formalism + :param use_app_config: Whether to give app_config as argument to app's main(). + :type use_app_config: bool + :param post_function: Function to execute after all rounds have been executed. + :type post_function: Optional[Callable] + :param enable_logging: Whether to enable logging. + :type enable_logging: bool + :param hardware: Unused argument. Any parameter given here will be ignored. + :type hardware: Any + :param init_func: Function to execute to initialize the state of the child processes. The implemented + executor uses the *spawn* method for creating new processes. In this sense, the + child processes *do not receive* a copy of the full memory, but only what is needed. + In particular, all modules will be reimported in the child processes, hence any + state of the classes *will not transfer* to the child processes. + :type init_func: Callable + :return: List of dictionaries describing the application names and the simulation results. + The i-th entry of the list will correspond to the i-th execution round of the simulation. + :rtype: List[Dict[str, Any]] """ # Before all; we need to instruct the OMP library to use a single thread to avoid # heavy-processes deadlocks @@ -214,10 +219,10 @@ def run_applications( try: with executor: SimulaQronConnection.PROCESS_POOL = executor - global apps_pids - apps_pids = synced_array + global _apps_pids + _apps_pids = synced_array logger.debug("Starting simulaqron sim_backend process with nodes %s", app_names) - setup_sim_backend(sim_backend) + _setup_sim_backend(sim_backend) # Start the application processes app_futures = [] @@ -252,7 +257,7 @@ def run_applications( # Join the application processes and the backend names = [f'app_{app_name}' for app_name in app_names] result = {} - futures = as_completed(app_futures, names) + futures = _as_completed(app_futures, names) start_time = time.time() while len(result) < len(app_names): for future, name in futures: @@ -285,7 +290,3 @@ def run_applications( process_logs.make_last_log(log_dir=timed_log_dir) return results - - -def save_results(results, results_file): - dump_yaml(data=results, file_path=results_file) From f66fafa7a67b07fe71fa1fe245d33fd4dbc45eea Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 15 Jan 2026 18:17:30 +0100 Subject: [PATCH 198/277] Update documentation of the simulaqron.sdk package --- docs/simulaqron.sdk.rst | 21 +++++-- simulaqron/sdk/broadcast_channel.py | 5 ++ simulaqron/sdk/connection.py | 89 +++++++++++++++++++++++++++-- simulaqron/sdk/socket.py | 71 ++++++++++++++++++++++- 4 files changed, 176 insertions(+), 10 deletions(-) diff --git a/docs/simulaqron.sdk.rst b/docs/simulaqron.sdk.rst index 93615f4d..de87005e 100644 --- a/docs/simulaqron.sdk.rst +++ b/docs/simulaqron.sdk.rst @@ -1,19 +1,32 @@ simulaqron.sdk package ====================== -.. warning:: UPDATE THIS STRUCTURE! - Submodules ---------- -simulaqron.run.run module +simulaqron.sdk.broadcast_channel module +------------------------- + +.. automodule:: simulaqron.sdk.broadcast_channel + :members: + :undoc-members: + :show-inheritance: + +simulaqron.sdk.connection module ------------------------- -.. automodule:: simulaqron.run.run +.. automodule:: simulaqron.sdk.connection :members: :undoc-members: :show-inheritance: +simulaqron.sdk.socket module +------------------------- + +.. automodule:: simulaqron.sdk.socket + :members: + :undoc-members: + :show-inheritance: Module contents --------------- diff --git a/simulaqron/sdk/broadcast_channel.py b/simulaqron/sdk/broadcast_channel.py index fbbff85e..7b07c383 100644 --- a/simulaqron/sdk/broadcast_channel.py +++ b/simulaqron/sdk/broadcast_channel.py @@ -4,4 +4,9 @@ class BroadcastChannel(BroadcastChannelBySockets): + """ + Implement a Broadcast channel over sockets + + .. warning:: This class is candidate to be deleted. Test and delete if possible! + """ _socket_class = Socket diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 70120306..3537c57d 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -48,6 +48,30 @@ def __init__( conn_retry_time: float = 0.1, network_name: str = "default", ): + """ + Main class representing the connection from NetQASM to the SimulaQron simulator. + This class implements the + + :param app_name: Name of the app to run. + :type app_name: str + :param app_id: The ID of the application. If not given, a new one will be created. + :type app_id: int | None + :param max_qubits: Maximum number of qubits tu simulate in the simulator. + :type max_qubits: int + :param log_config: Configuration of the logging. Check the documentation of + ``netqasm.sdk.config.LogConfig`` for more information about this. + :type log_config: LogConfig + :param epr_sockets: List of ``EPRSocket``s to use in the simulator. + :type epr_sockets: List[EPRSocket] + :param compiler: A transpiler object that transpiles the NetQASM instructions. + :type compiler: Type[SubroutineTranspiler] | None + :param socket_address: A tuple containing a hostname and port to use to connect to the QNodeOS server. + :type socket_address: Tuple[str, int] + :param conn_retry_time: Maximum time in seconds to wait between attempts to connect to the QNoseOS server. + :type conn_retry_time: float + :param network_name: The name of the network to connect to + :type network_name: str + """ super().__init__( app_name=app_name, # NOTE currently node_name and app_name are the same in simulaqron @@ -98,6 +122,21 @@ def try_connection( socket_address: Optional[Tuple[str, int]] = None, network_name: str = "default", ): + """ + Try to establish a connection to the specified node name. The connection can be made + by specifying the ``socket_address`` tuple (as a hostname and port number tuple) or + by specifying the node and network names. In the latter case, SimulaQron will search + for that node and network names on the loaded network configuration, and get the + corresponding socket configuration (hostname and port number) to connect to. + + :param name: The name of the node to connect to. + :type name: str + :param socket_address: A hostname-port pair to specify the hostname and port number + to connect to. This argument is optional. + :type socket_address: Tuple[str, int] | None + :param network_name: The name of the network to search the node name. + :type network_name: str + """ # NOTE using retry_time=None causes an error to be raised of the connection cannot # be established, which can be used to check if the connection is available logger.debug("Trying if connection is up yet") @@ -325,6 +364,9 @@ def _handle_reply(self) -> int: raise NotImplementedError(f"Unknown return message of type {type(ret_msg)}") def block(self): + """ + Blocks the handling of new messages until all the pending message IDs are acknowledged. + """ while len(self._waiting_msg_ids) > 0: self._logger.debug( "Blocking and waiting for msg IDs %s", self._waiting_msg_ids @@ -417,6 +459,15 @@ class GetQubitStateMessage(Message): TYPE = NewMessageType.GET_QUBIT_STATE def __init__(self, app_id: int = 0, qubit_id: int = 0): + """ + Implements a specific NetQASM message to get the state of a qubit from the + SimulaQron simulator. + + :param app_id: The app ID to get the qubit from. + :type app_id: int + :param qubit_id: The qubit ID to retrieve the state. + :type qubit_id: int + """ super().__init__(self.TYPE.value) self.app_id = app_id self.qubit_id = qubit_id @@ -431,8 +482,6 @@ class NewReturnMessageType(Enum): class RichErrorMessage(ReturnMessage): - """Enriched message to the Host that an error occurred at the quantum node controller.""" - _fields_ = [ ("err_code", ctypes.c_uint8), ("err_msg_len", ctypes.c_uint32), @@ -443,6 +492,14 @@ class RichErrorMessage(ReturnMessage): TYPE = NewReturnMessageType.ERR def __init__(self, err_code: ErrorCode, err_msg: str): + """ + Enriched message to the Host that an error occurred at the quantum node controller. + + :param err_code: The error code to report. + :type err_code: ErrorCode + :param err_msg: The error message. + :type err_msg: str + """ super().__init__(self.TYPE.value) err_bytes = err_msg.encode("utf-8") if len(err_bytes) > MAX_ERR_MSG_LEN: @@ -471,6 +528,16 @@ class ReturnQubitStateMessage(ReturnMessage): TYPE = NewReturnMessageType.RET_QUBIT_STATE def __init__(self, qubit_id: int, real_part: List[List[float]], imag_part: List[List[float]]): + """ + Specific NetQASM message used to transmit the qubit state back to the application. + + :param qubit_id: The qubit ID. + :type qubit_id: int + :param real_part: The real part of the qubit state. + :type real_part: List[List[float]] + :param imag_part: The imaginary part of the qubit state. + :type imag_part: List[List[float]] + """ super().__init__(self.TYPE.value) # Sanity checks - given matrices are square @@ -558,12 +625,26 @@ def _get_node_name(cls, node_id: int) -> str: @classmethod def get_node_id_for_app(cls, app_name: str) -> int: - """Returns the node id for the app with the given name""" + """ + Returns the node id for the app with the given name. + + :param app_name: The app name. + :type app_name: str + :return: The node ID. + :rtype: int + """ # NOTE app_name and node_name are for now the same in simulaqron return cls._get_node_id(node_name=app_name) @classmethod def get_node_name_for_app(cls, app_name: str) -> str: - """Returns the node name for the app with the given name""" + """ + Returns the node name for the app with the given name. + + :param app_name: The app name. + :type app_name: str + :return: The node name. + :rtype: str + """ # NOTE app_name and node_name are for now the same in simulaqron return app_name diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index 98d1c103..d9e05496 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -44,21 +44,42 @@ def __del__(self): self.close() def close(self): + """ + Closes this socket. Mo more messages can be sent or received after + invoking this method. + """ if self._app_socket: self._app_socket.close() def send(self, msg: str): - """Sends a message to the remote node.""" + """ + Sends a message to the remote node. + + :param msg: The message to send. + :type msg: str + """ self._logger.debug("Sending msg '%s'", msg) raw_msg = self._serialize_msg(msg=msg) self._app_socket.send(raw_msg) def send_structured(self, msg: StructuredMessage): + """ + Sends a message to the remote node as a ``StructuredMessage`` object. + + :param msg: The message to send. + :type msg: StructuredMessage + """ self._logger.debug("Sending structured msg '%s'", msg) raw_msg = self._serialize_structured_msg(msg=msg) self._app_socket.send(raw_msg) def send_silent(self, msg: str): + """ + Sends a message to the remote node without logging it. + + :param msg: The message to send. + :type msg: str + """ self.send(msg) def _base_recv(self, block: bool, timeout: float, maxsize: int) -> bytes: @@ -80,7 +101,18 @@ def recv( timeout: Optional[float] = None, maxsize: Optional[int] = 1024 ) -> str: - """Receive a message from the remote node.""" + """ + Receive a message from the remote node. + + :param block: Whether the underlying read operation should be blocking or not. + :type block: bool + :param timeout: Max time (in seconds) to wait for a message from the remote. + :type timeout: float | None + :param maxsize: Maximum size of bytes to read from the remote. + :type maxsize: int | None + :return: The received message as a string. + :rtype: str + """ self._logger.debug("Receiving msg") raw_msg = self._base_recv(block, timeout, maxsize) msg = self._deserialize_msg(raw_msg=raw_msg) @@ -93,6 +125,18 @@ def recv_structured( timeout: Optional[float] = None, maxsize: Optional[int] = 1024, ) -> StructuredMessage: + """ + Receives a message from the remote node and parses it as a ``StructuredMessage``. + + :param block: Whether the underlying read operation should be blocking or not. + :type block: bool + :param timeout: Max time (in seconds) to wait for a message from the remote. + :type timeout: float | None + :param maxsize: Maximum size of bytes to read from the remote. + :type maxsize: int | None + :return: The parsed message. + :rtype: StructuredMessage + """ self._logger.debug("Receiving structured msg") raw_msg = self._base_recv(block, timeout, maxsize) msg = self._deserialize_structured_msg(raw_msg=raw_msg) @@ -105,6 +149,20 @@ def recv_silent( timeout: Optional[float] = None, maxsize: Optional[int] = None, ) -> str: + """ + Receives a message without logging it. All arguments passed to this + invocation are ignored. For more fine-grain control, please check the + :py:meth:`recv` method. + + :param block: Ignored + :type block: bool + :param timeout: Ignored + :type timeout: float | None + :param maxsize: Ignored + :type maxsize: int | None + :return: The received message. + :rtype: str + """ return self.recv() @staticmethod @@ -125,6 +183,15 @@ def _deserialize_structured_msg(raw_msg: bytes) -> StructuredMessage: @property def is_server(self) -> bool: + """ + Check whether the local end of this socket will be acting as server or not. The decision + is made based on the node names: the name which is alphabetically before will act as server. + For example: If the socket connects "Alice" with "Bob", "Alice" will act as server, since + the string "Alice" comes lexicographically before the string "Bob". + + :return: Whether this end of the socket should act as server or not. + :rtype: bool + """ # Server will always be the "first" return self._node_name < self._remote_node_name From 1265111d801c36616b6b0c1370d8916bc2dbf405 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 15 Jan 2026 18:47:16 +0100 Subject: [PATCH 199/277] Update documentation of the simulaqron.start package --- docs/simulaqron.start.rst | 17 +++++++++++------ simulaqron/start/start_qnodeos.py | 30 +++++++++++++++--------------- simulaqron/start/start_vnode.py | 11 ++++++----- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/docs/simulaqron.start.rst b/docs/simulaqron.start.rst index acb01b29..3d3da686 100644 --- a/docs/simulaqron.start.rst +++ b/docs/simulaqron.start.rst @@ -1,19 +1,24 @@ simulaqron.start package -====================== - -.. warning:: UPDATE THIS STRUCTURE! +======================== Submodules ---------- -simulaqron.run.run module -------------------------- +simulaqron.start.start_qnodeos module +------------------------------------- -.. automodule:: simulaqron.run.run +.. automodule:: simulaqron.start.start_qnodeos :members: :undoc-members: :show-inheritance: +simulaqron.start.start_vnode module +----------------------------------- + +.. automodule:: simulaqron.start.start_vnode + :members: + :undoc-members: + :show-inheritance: Module contents --------------- diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index d1bdbd91..c1d84de4 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -21,16 +21,16 @@ _TIMEOUT = 10 -def init_register(virt_root, my_name: str, node: NetQASMFactory): +def _init_register(virt_root, my_name: str, node: NetQASMFactory): """Retrieves the relevant root objects to talk to such remote connections""" logger.debug("START_QNODEOS %s: Connection to local virtual node successful", my_name) # Set the virtual node node.set_virtual_node(virt_root) # Start listening to NetQASM messages - setup_netqasm_server(my_name, node) + _setup_netqasm_server(my_name, node) -def connect_to_virt_node(my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig): +def _connect_to_virt_node(my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig): """Tries to connect to local virtual node. If connection is refused, we try again after a set amount of time @@ -46,14 +46,14 @@ def connect_to_virt_node(my_name: str, netqasm_factory: NetQASMFactory, virtual_ reactor.connectTCP(virtual_node.hostname, virtual_node.port, factory) defer_virtual_node = factory.getRootObject() # If connection succeeds do: - defer_virtual_node.addCallback(init_register, my_name, netqasm_factory) + defer_virtual_node.addCallback(_init_register, my_name, netqasm_factory) # If connection fails do: - defer_virtual_node.addErrback(handle_connection_error, my_name, netqasm_factory, virtual_network, + defer_virtual_node.addErrback(_handle_connection_error, my_name, netqasm_factory, virtual_network, virtual_node.hostname, virtual_node.port) -def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig, - virtual_node_hostname: str, virtual_node_port: int): +def _handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactory, virtual_network: SocketsConfig, + virtual_node_hostname: str, virtual_node_port: int): """ Handles errors from trying to connect to local virtual node. If a ConnectionRefusedError is raised another try will be made after @@ -67,7 +67,7 @@ def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactor virtual_node_hostname, virtual_node_port, exc_info=err) reactor.callLater( simulaqron_settings.conn_retry_time, - connect_to_virt_node, + _connect_to_virt_node, my_name, netqasm_factory, virtual_network, @@ -81,7 +81,7 @@ def handle_connection_error(reason, my_name: str, netqasm_factory: NetQASMFactor reactor.stop() -def setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): +def _setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): """Setup NetQASM server to handle remote on the classical communication network.""" t_start = timer() while timer() - t_start < _TIMEOUT: @@ -113,7 +113,7 @@ def setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): stdout_file = None -def sigterm_handler(_signo, _stack_frame): +def _sigterm_handler(_signo, _stack_frame): global stdout_file if stdout_file is not None: stdout_file.flush() @@ -126,8 +126,8 @@ def start_qnodeos(node_name: str, network_config_file: Path, network_name: str = Start the QNPU that accepts NetQASM subroutines, and sends them as instructions to the SimulaQron virtual node backend over twisted PB (Native Mode SimulaQron). - :param name: Name of the node (e.g., 'Alice'). - :type name: str + :param node_name: Name of the node (e.g., 'Alice'). + :type node_name: str :param network_config_file: Path to network config file. :type network_config_file: Path :param network_name: Name of the network (e.g., 'default'). @@ -155,8 +155,8 @@ def start_qnodeos(node_name: str, network_config_file: Path, network_name: str = """Start the indicated backend NetQASM Server""" logger.debug("START_QNODEOS: Starting QNodeOS at %s", node_name) - signal.signal(signal.SIGTERM, sigterm_handler) - signal.signal(signal.SIGINT, sigterm_handler) + signal.signal(signal.SIGTERM, _sigterm_handler) + signal.signal(signal.SIGINT, _sigterm_handler) # Read configuration files for the virtual quantum, as well as the classical network virtual_network = SocketsConfig(network_config, network_name=network_name, config_type="vnode") @@ -179,7 +179,7 @@ def start_qnodeos(node_name: str, network_config_file: Path, network_name: str = # Connect to the local virtual node simulating the "local" qubits logger.debug(f"START_QNODEOS: Connect to virtual node {node_name}") - connect_to_virt_node(node_name, netqasm_factory, virtual_network) + _connect_to_virt_node(node_name, netqasm_factory, virtual_network) # Run reactor reactor.run() diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 14f3570a..50080508 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -44,8 +44,8 @@ stdout_file = None -def sigterm_handler(name, _signo, _stack_frame): - print("START_VNODE: Shutting down Node from signal %d." % _signo, flush=True) +def _sigterm_handler(name, _signo, _stack_frame): + print(f"START_VNODE: Shutting down Node '{name}' from signal {_signo}.", flush=True) global stdout_file if stdout_file is not None: stdout_file.flush() @@ -54,7 +54,8 @@ def sigterm_handler(name, _signo, _stack_frame): def start_vnode(name: str, network_config_file: Path, network_name: str = "default", log_level: str = "WARNING"): - """ Start the execution of a virtual simulaqron node. This node will simulate all quantum aspects + """ + Start the execution of a virtual simulaqron node. This node will simulate all quantum aspects of the node, and is then reachable via Twisted PB (Simulaqron Native Mode) or - when also starting QNPU - the QNPU Server which translates NetQASM to native mode. @@ -87,8 +88,8 @@ def start_vnode(name: str, network_config_file: Path, network_name: str = "defau ) # Set up the handlers: those define what we will do when the process is terminated (by killing it) - signal.signal(signal.SIGTERM, partial(sigterm_handler, name)) - signal.signal(signal.SIGINT, partial(sigterm_handler, name)) + signal.signal(signal.SIGTERM, partial(_sigterm_handler, name)) + signal.signal(signal.SIGINT, partial(_sigterm_handler, name)) # Let's now test logging works by printing a message we are starting logger.debug("START_VNODE: Starting VIRTUAL NODE %s", name) From ab1f263be0b18ae72bc83b1a160f866457b37bfb Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 15 Jan 2026 19:55:36 +0100 Subject: [PATCH 200/277] Update documentation of the simulaqron.settings package --- docs/simulaqron.general.rst | 2 +- docs/simulaqron.netqasm_backend.rst | 6 +- docs/simulaqron.rst | 6 +- docs/simulaqron.sdk.rst | 6 +- docs/simulaqron.settings.rst | 18 +- simulaqron/settings/network_config.py | 207 ++++++++++++++--------- simulaqron/settings/simulaqron_config.py | 19 ++- 7 files changed, 157 insertions(+), 107 deletions(-) diff --git a/docs/simulaqron.general.rst b/docs/simulaqron.general.rst index 2df5c79a..2df91ce2 100644 --- a/docs/simulaqron.general.rst +++ b/docs/simulaqron.general.rst @@ -14,7 +14,7 @@ simulaqron.general.host_config module simulaqron.general.errors module -------------------------------------- +-------------------------------- .. automodule:: simulaqron.general.errors :members: diff --git a/docs/simulaqron.netqasm_backend.rst b/docs/simulaqron.netqasm_backend.rst index 4856df2b..a149bafd 100644 --- a/docs/simulaqron.netqasm_backend.rst +++ b/docs/simulaqron.netqasm_backend.rst @@ -5,7 +5,7 @@ Submodules ---------- simulaqron.netqasm_backend.executioner module -------------------------- +--------------------------------------------- .. automodule:: simulaqron.netqasm_backend.executioner :members: @@ -13,7 +13,7 @@ simulaqron.netqasm_backend.executioner module :show-inheritance: simulaqron.netqasm_backend.factory module -------------------------- +----------------------------------------- .. automodule:: simulaqron.netqasm_backend.factory :members: @@ -21,7 +21,7 @@ simulaqron.netqasm_backend.factory module :show-inheritance: simulaqron.netqasm_backend.qnodeos module -------------------------- +----------------------------------------- .. automodule:: simulaqron.netqasm_backend.qnodeos :members: diff --git a/docs/simulaqron.rst b/docs/simulaqron.rst index bbac78ed..50ca528d 100644 --- a/docs/simulaqron.rst +++ b/docs/simulaqron.rst @@ -20,7 +20,7 @@ Submodules ---------- simulaqron.cli module ----------------------------- +--------------------- .. automodule:: simulaqron.cli :members: @@ -28,7 +28,7 @@ simulaqron.cli module :show-inheritance: simulaqron.math module --------------------------- +---------------------- .. automodule:: simulaqron.math :members: @@ -52,7 +52,7 @@ simulaqron.reactor module :show-inheritance: simulaqron.sim_util module -------------------------- +-------------------------- .. automodule:: simulaqron.sim_util :members: diff --git a/docs/simulaqron.sdk.rst b/docs/simulaqron.sdk.rst index de87005e..d5072f06 100644 --- a/docs/simulaqron.sdk.rst +++ b/docs/simulaqron.sdk.rst @@ -5,7 +5,7 @@ Submodules ---------- simulaqron.sdk.broadcast_channel module -------------------------- +--------------------------------------- .. automodule:: simulaqron.sdk.broadcast_channel :members: @@ -13,7 +13,7 @@ simulaqron.sdk.broadcast_channel module :show-inheritance: simulaqron.sdk.connection module -------------------------- +-------------------------------- .. automodule:: simulaqron.sdk.connection :members: @@ -21,7 +21,7 @@ simulaqron.sdk.connection module :show-inheritance: simulaqron.sdk.socket module -------------------------- +---------------------------- .. automodule:: simulaqron.sdk.socket :members: diff --git a/docs/simulaqron.settings.rst b/docs/simulaqron.settings.rst index 5ab45ce9..12c2aa74 100644 --- a/docs/simulaqron.settings.rst +++ b/docs/simulaqron.settings.rst @@ -1,15 +1,21 @@ simulaqron.settings package -====================== - -.. warning:: UPDATE THIS STRUCTURE! +=========================== Submodules ---------- -simulaqron.run.run module -------------------------- +simulaqron.settings.network module +---------------------------------- + +.. automodule:: simulaqron.settings.network + :members: + :undoc-members: + :show-inheritance: + +simulaqron.settings.simulaqron_config module +-------------------------------------------- -.. automodule:: simulaqron.run.run +.. automodule:: simulaqron.settings.simulaqron_config :members: :undoc-members: :show-inheritance: diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index e6685627..aab50f35 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -29,7 +29,9 @@ class NodeConfigType(StrEnum): @dataclass class NodeConfig(JSONSerializerMixin): """ - Used by NetworkConfig to keep track of the config of a single node. + Used by NetworkConfig to keep track of the node config of a single node. + This object holds the hostname and port info for the application, the SimulaQron + Virtual Node and the QNodeOS server. """ name: str app_port: int @@ -41,12 +43,13 @@ class NodeConfig(JSONSerializerMixin): def get_config(self, config_type: str | NodeConfigType) -> Tuple[str, int]: """ - Gets the corresponding host and port config tuple for the given type - Args: - config_type: str | NodeConfigType - The type of configuration to get. Can either be expressed as a string or a NodeConfigType. - Returns: - A tuple containing the host and port config for the given configuration type. + Gets the corresponding host and port config tuple for the given type. + + :param config_type: The type of configuration to get. Can either be expressed as + a string or a NodeConfigType. + :type config_type: str | NodeConfigType + :return: A tuple containing the host and port config for the given configuration type. + :rtype: Tuple[str, int] """ if isinstance(config_type, str): config_type = NodeConfigType(config_type) @@ -72,7 +75,9 @@ def __eq__(self, other) -> bool: @dataclass class NetworkConfig(JSONSerializerMixin): """ - Used by NetworksConfiguration to keep track of the config of a single network. + Used by NetworksConfiguration to keep track of the config of a single network. This object + holds the node configuration (as :py:class:`NodeConfig` instances) of all the nodes within + a network. """ name: str @@ -108,7 +113,6 @@ def add_node( :param neighbors: (list of str) or None A list of neighbors, of this node. If None all current nodes in the network will be adjacent to the added node. - :return: None """ if neighbors is not None: if self.topology is None: @@ -134,11 +138,11 @@ def remove_node(self, node_name: str) -> NodeConfig | None: """ Removes the node with the given name and returns it. Returns none if the given node name was not found in this network. - Args: - node_name: str - The name of the node to remove. None if the node name does not exist. - Returns: - The removed node. None if the given name was not found. + + :param node_name: The name of the node to remove. None if the node name does not exist. + :type node_name: str + :return: The removed node. None if the given name was not found. + :rtype: NodeConfig | None """ return self.nodes.pop(node_name, None) @@ -147,10 +151,16 @@ def add_node_config(self, node_cfg: NodeConfig): @property def is_empty(self) -> bool: + """ + Whether this network configuration is empty or not. + """ return len(self.nodes) <= 0 @property def nodes_names(self) -> List[str]: + """ + Gets a list of strings with the names of nodes on this network. + """ return list(self.nodes.keys()) def __eq__(self, other) -> bool: @@ -175,6 +185,9 @@ def using_default_network(self) -> Path: Used for test isolation - always loads the embedded default regardless of local config files. + + :return: The path of the file containing the default network configuration. + :rtype: Path """ # We use the embedded default network here @@ -191,15 +204,15 @@ def _correct_network_port_if_needed(self, hostname: str, port: int) -> int: """ Checks if the given port is valid (>0) and if it is free. If not, it will allocate a new port in the range 8000-9000 which is free, and hence can be used - to listen to new connections - Args: - hostname: str - The hostname to test the port on. - port: int - The port number to test if it is usable - Returns: - A port number which is guaranteed to be valid, and ready to be used - to listen to connections on. + to listen to new connections. + + :param hostname: The hostname to test the port on. + :type hostname: str + :param port: The port number to test if it is usable + :type port: int + :return: A port number which is guaranteed to be valid, and ready to be used + to listen to connections on. + :rtype: int """ if port < 0: port = self._get_unused_port(hostname) @@ -209,10 +222,9 @@ def _correct_network_port_if_needed(self, hostname: str, port: int) -> int: def add_network_config(self, net_cfg: NetworkConfig): """ - Method used to deserialize NetworksSpecsConfig. - Args: - net_cfg: NetworkConfig - The network configu object to add to the specifications. + Adds the given network config to the whole networks configuration. + :param net_cfg: The network configu object to add to the specifications. + :type net_cfg: NetworkConfig """ self.networks[net_cfg.name] = net_cfg @@ -232,26 +244,28 @@ def add_node(self, node_name: str, network_name: str = "default", app_hostname: If the port numbers None, unused ones will be chosen between 8000 and 9000. If neighbors are specified a restricted topology can be constructed (default is fully connected). - :param node_name: str - Name of the node, e.g. Alice - :param network_name: str - Name of the network (default: "default") - :param app_hostname: str - Hostname, e.g. localhost (the default if not given) or 192.168.0.1 - :param qnodeos_hostname: str - Hostname, e.g. localhost (the default if not given) or 192.168.0.1 - :param vnode_hostname: str - Hostname, e.g. localhost (the default if not given) or 192.168.0.1 - :param app_port: int - Port number for the application. A free port in the range 8000-9000 will be allocated if not given - :param qnodeos_port: int - Port number for the application. A free port in the range 8000-9000 will be allocated if not given - :param vnode_port: int - Port number for the application. A free port in the range 8000-9000 will be allocated if not given - :param neighbors: (list of str) or None - A list of neighbors, of this node. - If None all current nodes in the network will be adjacent to the added node. - :return: None + :param node_name: Name of the node, e.g. Alice. + :type node_name: str + :param network_name: Name of the network (default: "default"). + :type network_name: str + :param app_hostname: Hostname, e.g. localhost (the default if not given) or 192.168.0.1 + :type app_hostname: str + :param qnodeos_hostname: Hostname, e.g. localhost (the default if not given) or 192.168.0.1 + :type qnodeos_hostname: str + :param vnode_hostname: Hostname, e.g. localhost (the default if not given) or 192.168.0.1 + :type vnode_hostname: str + :param app_port: Port number for the application. A free port in the range 8000-9000 will + be allocated if not given + :type app_port: int + :param qnodeos_port: Port number for the application. A free port in the range 8000-9000 will + be allocated if not given. + :type qnodeos_port: int + :param vnode_port: Port number for the application. A free port in the range 8000-9000 will + be allocated if not given. + :type vnode_port: int + :param neighbors: A list of neighbors, of this node. If None all current nodes in the network + will be adjacent to the added node. + :type neighbors: List[str] | None """ try: @@ -289,10 +303,10 @@ def remove_node(self, node_name: str, network_name: str = "default"): """ Removes a node from the network. - :param node_name: str - Name of the node to remove, e.g. Alice - :param network_name: str - Name of the network to delete the node from (default: "default") + :param node_name: Name of the node to remove, e.g. Alice. + :type node_name: str + :param network_name: Name of the network to delete the node from (default: "default") + :type network_name: str """ if network_name in self.networks: old_node = self.networks[network_name].remove_node(node_name) @@ -316,12 +330,12 @@ def add_network(self, node_names: List[str], network_name: str = "default", """ Adds a new network to the config, with some specified nodes. - :param node_names: list of str - Name of the nodes, e.g. [Alice, Bob] - :param network_name: str - Name of the network (default: "default") - :param topology: None or dict - The topology of the network (optional) (default is fully connected) + :param node_names: Name of the nodes, e.g. [Alice, Bob] + :type node_names: List[str] + :param network_name: Name of the network (default: "default"). + :type network_name: str + :param topology: The topology of the network (optional) (default is fully connected) + :type topology: Dict[str, List[str]] | None """ if isinstance(node_names, str): # The user passes a string... they probably meant to add a single node, so we make it a list @@ -338,8 +352,8 @@ def remove_network(self, network_name: str = "default"): """ Removes a network from the config. - :param network_name: str - Name of the network (default: "default") + :param network_name: Name of the network (default: "default"). + :type network_name: str """ removed_network = self.networks.pop(network_name, None) if removed_network is not None: @@ -352,9 +366,10 @@ def get_nodes(self, network_name: str = "default") -> List[NodeConfig]: """ Returns the node-config objects (NodeConfig) in a network that belong to the given network. - :param network_name: str - Name of the network (default: "default") - :return: list of NodeConfig + :param network_name: Name of the network (default: "default") + :type network_name: str + :return: A list of :py:class:`NodeConfig` classes with the nodes configuration. + :rtype: List[NodeConfig] """ if network_name in self.networks: nodes = self.networks[network_name].nodes @@ -366,9 +381,10 @@ def get_node_names(self, network_name: str = "default"): """ Returns the names of the nodes in a network. - :param network_name: str - Name of the network (default: "default") - :return: list of str + :param network_name: Name of the network (default: "default"). + :type network_name: str + :return: A lit of node names in the given network. + :rtype: List[str] """ if network_name in self.networks: nodes = self.networks[network_name].nodes @@ -387,8 +403,8 @@ def write_to_file(self, file_path: PathLike | str): """ Writes the content of this config to a file. - :param file_path: str - The path of the file to write the content to. + :param file_path: The path of the file to write the content to. + :type file_path: PathLike | str """ if file_path is None: raise ValueError("Since this networks config was not initialized with a file_path you need to specify one") @@ -410,8 +426,9 @@ def read_from_file(self, file_path: PathLike | str): """ Reads config from a file. - :param file_path: None or str - If a file_path was specified upon __init__ this will be used if file_path is None. + :param file_path: If a file_path was specified upon __init__ this will be + used if file_path is None. + :type file_path: PathLike | str """ if file_path is None: @@ -433,6 +450,20 @@ def read_from_file(self, file_path: PathLike | str): def read_from_legacy_files(self, app_file_path: PathLike | str, vnode_file_path: PathLike | str, qnodeos_file_path: Optional[PathLike | str] = None): + """ + Constructs a network configuration from a set of legacy format (.cfg) network files. + + .. warning:: This method is not implemented yet, and simply raises ``NotImplementedException``. + + :param app_file_path: Path of the classical network file. + :type app_file_path: PathLike | str + :param vnode_file_path: Path of the virtual network file. + :type vnode_file_path: PathLike | str + :param qnodeos_file_path: Path of the QNodeOS network file. If this path is not give, + the same hosts as vnodes_file_path will be used, assigning a + new, random port in the 8000-9000 range. + :type qnodeos_file_path: PathLike | str + """ raise NotImplementedError("Reading form legacy config files is not supported yet") @staticmethod @@ -485,8 +516,9 @@ def read_from_known_sources(cls) -> Self: def nodes(self) -> List[NodeConfig]: """ Access the nodes of the default network held by this configuration. - Returns: - A list of NodeConfig objects. + + :return: A list of NodeConfig objects. + :rtype: list[NodeConfig] """ return self.get_nodes(network_name="default") @@ -494,8 +526,9 @@ def nodes(self) -> List[NodeConfig]: def network_names(self) -> List[str]: """ Gets the loaded network names. - Returns: - A list of strings with the network names. + + :return: A list of strings with the network names. + :rtype: List[str] """ return list(self.networks.keys()) @@ -515,9 +548,11 @@ def __eq__(self, other: Any) -> bool: def _get_unused_port(self, hostname: str) -> int: """ Returns an unused port in the interval 8000 to 9000, if such exists, otherwise returns None. - :param hostname: str - Hostname, e.g. localhost or 192.168.0.1 - :return: int or None + + :param hostname: Hostname, e.g. localhost or 192.168.0.1 + :type hostname: str + :return: A random unused port number in the interval 8000 to 9000. + :rtype: int | None """ for port in range(8000, 9001): if self._check_port_available(hostname, port): @@ -527,11 +562,13 @@ def _get_unused_port(self, hostname: str) -> int: def _check_port_available(self, hostname: str, port: int) -> bool: """ Checks if the given port is not already set in the config files or used by some other process. - :param hostname: str - Hostname, e.g. localhost or 192.168.0.1 - :param port: int - The port number - :return: bool + + :param hostname: Hostname, e.g. localhost or 192.168.0.1 + :type hostname: str + :param port: The port number + :type port: int + :return: Whether the port is currently available or not + :rtype: bool """ if (hostname, port) in self.used_sockets: return False @@ -542,9 +579,13 @@ def _check_port_available(self, hostname: str, port: int) -> bool: def _check_socket_is_free(port: int) -> bool: """ Checks if a given socket on localhost is in use. + This is done by trying to open the port and check if it succeeds. - :param port: int - The port number + + :param port: The port number + :type port: int + :return: Whether the given port number is available on `localhost` or not. + :rtype: bool """ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: address = ('localhost', port) diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index cd6a08d0..9f9503c4 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -48,6 +48,9 @@ class SimBackend(JSONSerializerMixin, Enum): + """ + Enum used to list the supported SimulaQron backends. + """ STABILIZER = "stabilizer" PROJECTQ = "projectq" QUTIP = "qutip" @@ -61,6 +64,9 @@ def __repr__(self): @dataclass class SimulaqronConfig(JSONSerializerMixin): + """ + Holds the general SimulaQron config. + """ # Default config max_qubits: int = 20 max_registers: int = 1000 @@ -83,9 +89,8 @@ def _create_home_settings_folder(cls): def read_from_file(self, file_path: Path | str): """ Reads the SimulaQron configuration from the given file path. - Args: - file_path: Path, str - A `pathlib.Path` or `str` representing the file path to read the configurations from. + :param file_path: A `pathlib.Path` or `str` representing the file path to read the configurations from. + :type file_path: Path | str """ if isinstance(file_path, str): file_path = Path(file_path).resolve() @@ -115,8 +120,7 @@ def read_from_known_sources(cls) -> Self: SimulaQron configuration. To check the default configuration, check the documentation of `default_settings`. - See Also: - default_settings() + See Also: :py:meth:`default_settings` """ cwd_settings_file = LOCAL_SIMULAQRON_SETTINGS.resolve() home_settings_file = HOME_SIMULAQRON_SETTINGS.resolve() @@ -163,9 +167,8 @@ def default_settings(self): def write_to_file(self, path: PathLike): """ Writes the current in-memory configuration (`simulaqron_config`) to the given file path. - Args: - path: - A `PathLike` object (even a string) representing the path to write the configuration to. + :param path:A `PathLike` object (even a string) representing the path to write the configuration to. + :type path: PathLike """ file_path = Path(str(path)).resolve() From 86966afd9fb6eb993a688173ebb77a9b5125ac89 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 15 Jan 2026 22:24:51 +0100 Subject: [PATCH 201/277] Update documentation of the simulaqron.virtual_node package --- docs/simulaqron.toolbox.rst | 32 -- docs/simulaqron.virtual_node.rst | 2 +- simulaqron/toolbox/stabilizer_states.py | 7 +- simulaqron/virtual_node/basics.py | 117 +++-- .../virtual_node/project_q_simulator.py | 133 ++++-- simulaqron/virtual_node/quantum.py | 31 +- simulaqron/virtual_node/qutip_simulator.py | 156 +++++-- .../virtual_node/stabilizer_simulator.py | 116 +++-- simulaqron/virtual_node/virtual.py | 435 +++++++++++------- 9 files changed, 690 insertions(+), 339 deletions(-) diff --git a/docs/simulaqron.toolbox.rst b/docs/simulaqron.toolbox.rst index 56745e39..b469e77d 100644 --- a/docs/simulaqron.toolbox.rst +++ b/docs/simulaqron.toolbox.rst @@ -4,38 +4,6 @@ simulaqron.toolbox package Submodules ---------- -simulaqron.toolbox.get_simulaqron_path module ---------------------------------------------- - -.. automodule:: simulaqron.toolbox.get_simulaqron_path - :members: - :undoc-members: - :show-inheritance: - -simulaqron.toolbox.has_module module ------------------------------------- - -.. automodule:: simulaqron.toolbox.has_module - :members: - :undoc-members: - :show-inheritance: - -simulaqron.toolbox.manage_nodes module --------------------------------------- - -.. automodule:: simulaqron.toolbox.manage_nodes - :members: - :undoc-members: - :show-inheritance: - -simulaqron.toolbox.reset module -------------------------------- - -.. automodule:: simulaqron.toolbox.reset - :members: - :undoc-members: - :show-inheritance: - simulaqron.toolbox.stabilizer_states module ------------------------------------------- diff --git a/docs/simulaqron.virtual_node.rst b/docs/simulaqron.virtual_node.rst index aa4239d3..55de195c 100644 --- a/docs/simulaqron.virtual_node.rst +++ b/docs/simulaqron.virtual_node.rst @@ -36,7 +36,7 @@ simulaqron.virtual_node.qutip_simulator module :undoc-members: :show-inheritance: -simulaqron.virtual_node.stabilizerSimulator module +simulaqron.virtual_node.stabilizer_simulator module -------------------------------------------------- .. automodule:: simulaqron.virtual_node.stabilizer_simulator diff --git a/simulaqron/toolbox/stabilizer_states.py b/simulaqron/toolbox/stabilizer_states.py index 45ee33df..a3d12781 100644 --- a/simulaqron/toolbox/stabilizer_states.py +++ b/simulaqron/toolbox/stabilizer_states.py @@ -23,7 +23,7 @@ class StabilizerState: Pauli2bool = {"I": (False, False), "X": (True, False), "Y": (True, True), "Z": (False, True)} - def __init__(self, data=None, check_symplectic=True): + def __init__(self, data=None, check_symplectic: bool = True): """ This class represent a stabilizer state and allows to be manipulated using Clifford operations and Pauli-measurements. @@ -82,8 +82,8 @@ def __init__(self, data=None, check_symplectic=True): The entangled state (|01> + |10>)/sqrt(2) can be created as: StabilizerState([[1, 1, 0, 0, 0], 0, 0, 1, 1, 1]]) - :param check_symplectic: bool - Whether to check if all stabilizers commute or not. + :param check_symplectic: Whether to check if all stabilizers commute or not. + :type check_symplectic: bool """ if data is None: self._group = np.empty(shape=(0, 0), dtype=bool) @@ -265,6 +265,7 @@ def boolean_gaussian_elimination(matrix, return_pivot_columns=False): """ Given a boolean matrix returns the matrix in row reduced echelon form where entries are seen as elements of GF(2), i.e. intergers modulus 2. + :param matrix: The boolean matrix :type matrix: :obj:`numpy.array` :return: diff --git a/simulaqron/virtual_node/basics.py b/simulaqron/virtual_node/basics.py index 2a492744..53bec9e6 100644 --- a/simulaqron/virtual_node/basics.py +++ b/simulaqron/virtual_node/basics.py @@ -55,17 +55,18 @@ def __str__(self): class QuantumEngine(pb.Referenceable, abc.ABC): """ Basic quantum engine. Abstract class meant to be subclassed to implement different simulation backends. - - Attributes: - Arguments - node node this register is started from - num number of this register - maxQubits maximum number of qubits this register supports """ def __init__(self, node: str, num: int, maxQubits: int = 10): """ - Initialize the simple engine. If no number is given for maxQubits, the assumption will be 10. + Initialize the simple engine. + + :param node: Node name this register is started from. + :type node: str + :param num: Number of this register. + :type num: int + :param maxQubits: maximum number of qubits this register supports. If not give, it will default to 10. + :type maxQubits: int """ self.maxQubits = maxQubits @@ -80,7 +81,8 @@ def __init__(self, node: str, num: int, maxQubits: int = 10): @abc.abstractmethod def add_fresh_qubit(self) -> int: """ - Add a new qubit initialized in the |0> state. + Add a new qubit initialized in the :math:`|0>` state. + :return: The qubit number :rtype: int """ @@ -90,6 +92,9 @@ def add_fresh_qubit(self) -> int: def add_qubit(self, newQubit) -> int: """ Add new qubit in the state described by the density matrix newQubit + + :param newQubit: The new qubit state + :type newQubit: Any :return: The qubit number :rtype: int """ @@ -99,6 +104,9 @@ def add_qubit(self, newQubit) -> int: def remove_qubit(self, qubitNum: int) -> None: """ Removes the qubit with the desired number qubitNum + + :param qubitNum: Qubit number + :type qubitNum: int :rtype: None """ pass @@ -117,8 +125,9 @@ def get_register_RI(self): def get_density_matrix_RI(self): """ Retrieves the entire register in real and imaginary parts and returns the result of - the outer product. In other words, if the qubit is in state |q>, this function - returns the density matrix |q>`, this function + returns the density matrix :math:`|q> int: """ - Add a new qubit initialized in the \|0\> state. + Add a new qubit initialized in the :math:`|0>` state. + + :return: The ID of the new qubit allocated. + :rtype: int """ # Check if we are still allowed to add qubits if self.activeQubits >= self.maxQubits: @@ -90,6 +97,10 @@ def add_fresh_qubit(self) -> int: def add_qubit(self, newQubit): """ Add new qubit in the state described by the vector newQubit ([a, b]) + + :param newQubit: The density matrix of the new qubit. + :return: The ID of the new qubit allocated. + :rtype: int """ norm = np.dot(np.array(newQubit), np.array(newQubit).conj()) @@ -107,6 +118,9 @@ def add_qubit(self, newQubit): def remove_qubit(self, qubitNum): """ Removes the qubit with the desired number qubitNum + + :param qubitNum: Qubit number + :type qubitNum: int """ if (qubitNum + 1) > self.activeQubits: raise QuantumError("No such qubit to remove") @@ -129,6 +143,13 @@ def _get_internal_qubit_state(self) -> Tuple[Dict[int, int], List[complex]]: return q_reg_order, state def get_register_RI(self) -> Tuple[Dict[int, int], Tuple[Tuple[float, ...], Tuple[float, ...]]]: + """ + Retrieves the entire register in real and imaginary parts and returns the result as a + list. Twisted only likes to send real valued lists, not complex ones. + + :return: The qubit states real and imaginary parts. + :rtype: Tuple[Tuple[float, ...], Tuple[float, ...]] + """ q_reg_order, state = self._get_internal_qubit_state() # Note previously the format of real and imaginary numbers were @@ -140,6 +161,13 @@ def get_register_RI(self) -> Tuple[Dict[int, int], Tuple[Tuple[float, ...], Tupl return q_reg_order, (Re, Im) def get_density_matrix_RI(self) -> Tuple[List[float], List[float]]: + """ + Retrieves the density matrix of the qubit as a real and imaginary part. Twisted only + likes to send real valued lists, not complex ones. + + :return: The qubit density matrix real and imaginary parts. + :rtype: Tuple[List[float], List[float]] + """ # Get the internal state of the qubit, and compute the outer product |q> Tuple[List[float], List[float]]: def apply_H(self, qubitNum): """ Applies a Hadamard gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.apply_onequbit_gate(pQ.ops.H, qubitNum) def apply_K(self, qubitNum): """ Applies a K gate to the qubits with number qubitNum. Maps computational basis to Y eigenbasis. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.apply_onequbit_gate(pQ.ops.H, qubitNum) self.apply_onequbit_gate(pQ.ops.S, qubitNum) @@ -164,6 +198,9 @@ def apply_K(self, qubitNum): def apply_X(self, qubitNum): """ Applies a X gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.apply_onequbit_gate(pQ.ops.X, qubitNum) @@ -171,6 +208,9 @@ def apply_X(self, qubitNum): def apply_Z(self, qubitNum): """ Applies a Z gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.apply_onequbit_gate(pQ.ops.Z, qubitNum) @@ -178,6 +218,9 @@ def apply_Z(self, qubitNum): def apply_Y(self, qubitNum): """ Applies a Y gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.apply_onequbit_gate(pQ.ops.Y, qubitNum) @@ -185,20 +228,23 @@ def apply_Y(self, qubitNum): def apply_T(self, qubitNum): """ Applies a T gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.apply_onequbit_gate(pQ.ops.T, qubitNum) - def apply_rotation(self, qubitNum, n, a): + def apply_rotation(self, qubitNum: int, n: Tuple[float, float, float], a: float): """ Applies a rotation around the axis n with the angle a to qubit with number qubitNum. If n is zero a ValueError is raised. - :param qubitNum: int - Qubit number - :param n: tuple of floats - A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) - :param a: float - The rotation angle in radians. + :param qubitNum: Qubit number + :type qubitNum: int + :param n: A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) + :type n: Tuple[float, float, float] + :param a: The rotation angle in radians. + :type a: float """ n = tuple(n) if n == (1, 0, 0): @@ -213,23 +259,33 @@ def apply_rotation(self, qubitNum, n, a): def apply_CNOT(self, qubitNum1, qubitNum2): """ Applies the CNOT to the qubit with the numbers qubitNum1 and qubitNum2. + + :param qubitNum1: Qubit number 1. + :type qubitNum1: int + :param qubitNum1: Qubit number 2. + :type qubitNum1: int """ self.apply_twoqubit_gate(pQ.ops.CNOT, qubitNum1, qubitNum2) def apply_CPHASE(self, qubitNum1, qubitNum2): """ Applies the CPHASE to the qubit with the numbers qubitNum1 and qubitNum2. + + :param qubitNum1: Qubit number 1. + :type qubitNum1: int + :param qubitNum1: Qubit number 2. + :type qubitNum1: int """ self.apply_twoqubit_gate(pQ.ops.CZ, qubitNum1, qubitNum2) - def apply_onequbit_gate(self, gate, qubitNum): + def apply_onequbit_gate(self, gate, qubitNum: int): """ Applies a unitary gate to the specified qubit. - Arguments: - gate The project Q gate to be applied - qubitNum the number of the qubit this gate is applied to + :param gate: The project Q gate to be applied. + :param qubitNum: The number of the qubit this gate is applied to. + :type qubitNum: int """ if (qubitNum + 1) > self.activeQubits: @@ -237,14 +293,16 @@ def apply_onequbit_gate(self, gate, qubitNum): gate | self.qubitReg[qubitNum] - def apply_twoqubit_gate(self, gate, qubit1, qubit2): + def apply_twoqubit_gate(self, gate, qubit1: int, qubit2: int): """ Applies a unitary gate to the two specified qubits. Arguments: - gate The project Q gate to be applied - qubit1 the first qubit - qubit2 the second qubit + :param gate: The project Q gate to be applied + :param qubit1: The first qubit + :type qubit1: int + :param qubit2: The second qubit + :type qubit2: int """ if (qubit1 + 1) > self.activeQubits: raise QuantumError("No such qubit to act as a control qubit") @@ -257,13 +315,13 @@ def apply_twoqubit_gate(self, gate, qubit1, qubit2): gate | (self.qubitReg[qubit1], self.qubitReg[qubit2]) - def measure_qubit_inplace(self, qubitNum): + def measure_qubit_inplace(self, qubitNum: int): """ Measures the desired qubit in the standard basis. This returns the classical outcome. The quantum register - is in the post-measurment state corresponding to the obtained outcome. + is in the post-measurement state corresponding to the obtained outcome. - Arguments: - qubitNum qubit to be measured + :param qubitNum: The number of the qubit to measure. + :type qubitNum: int """ # Check we have such a qubit... @@ -279,12 +337,12 @@ def measure_qubit_inplace(self, qubitNum): # return measurement outcome return outcome - def measure_qubit(self, qubitNum): + def measure_qubit(self, qubitNum: int): """ Measures the desired qubit in the standard basis. This returns the classical outcome and deletes the qubit. - Arguments: - qubitNum qubit to be measured + :param qubitNum: The number of the qubit to measure. + :type qubitNum: int """ outcome = self.measure_qubit_inplace(qubitNum) @@ -295,15 +353,23 @@ def measure_qubit(self, qubitNum): return outcome - def replace_qubit(self, qubitNum, state): + def replace_qubit(self, qubitNum: int, state): """ Replaces the qubit at position qubitNum with the one given by state. + + :param qubitNum: Qubit to be replaced + :type qubitNum: int + :param state: New state to write in the place of the old qubit. + :type state: Any """ raise NotImplementedError("Currently you cannot replace a qubit using project Q as backend") def absorb(self, other): """ Absorb the qubits from the other engine into this one. This is done by tensoring the state at the end. + + :param other: The other qubit to absorb. + :type other: int """ # Check whether there is space newNum = self.activeQubits + other.activeQubits @@ -323,10 +389,11 @@ def absorb_parts(self, R, I, activeQ): """ Absorb the qubits, given in pieces - Arguments: - R real part of the qubit state as a list - I imaginary part as a list - activeQ active number of qubits + :param R: Real part of the qubit state as a list. + :type R: List[float] + :param I: Imaginary part as a list. + :type I: List[float] + :param activeQ: Active number of qubits """ # Check whether there is space newNum = self.activeQubits + activeQ diff --git a/simulaqron/virtual_node/quantum.py b/simulaqron/virtual_node/quantum.py index 68ddf1d1..60c430dc 100644 --- a/simulaqron/virtual_node/quantum.py +++ b/simulaqron/virtual_node/quantum.py @@ -36,24 +36,34 @@ from twisted.internet.task import deferLater from twisted.spread import pb +from simulaqron.general.host_config import Host from simulaqron.reactor import reactor import simulaqron.settings as settings +from simulaqron.virtual_node.basics import QuantumEngine class SimulatedQubit(pb.Referenceable): """ Simulated qubit object in the specified local simulation engine. - - **Arguments** - :node: network node that this qubit lives at - :register: register on that node that the qubit is in - .. note:: Qubit objects are local to each node that is simulating a particular quantum register. A qubit object provides the backing for a virtual qubit, which may be at another node. """ - def __init__(self, node, register, simNum, num=0): + def __init__(self, node: Host, register: QuantumEngine, simNum: int, num: int = 0): + """ + Creates a new simulated qubit object in the local simulation engine. + + :param node: Network node that this qubit lives at. + :type node: Host + :param register: Register on that node that the qubit is in. + :type register: QuantumEngine + :param simNum: The number of the simulated qubit. This value is unique at each virtual node. + :type simNum: int + :param num: Number in the register. + :type num: int + """ # Node where this qubit is located self.node = node @@ -174,6 +184,7 @@ def remote_apply_T(self): def remote_apply_rotation(self, *args): """ Apply rotation around axis n with angle a. + Arguments: n A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) a The rotation angle in radians. @@ -195,7 +206,7 @@ def remote_measure_inplace(self): Measure the qubit in the standard basis. This does NOT delete the qubit, but replace the relevant qubit with the measurement outcome. - Returns the measurement outcome. + :return: The measurement outcome. """ self._apply_random_pauli_noise() outcome = self.register.measure_qubit_inplace(self.num) @@ -205,7 +216,7 @@ def remote_measure(self): """ Measure the qubit in the standard basis. This does delete the qubit. - Returns the measurement outcome. + :return: The measurement outcome. """ # Measure the qubit @@ -217,8 +228,7 @@ def remote_cnot_onto(self, targetNum): """ Performs a CNOT operation with this qubit as control, and the other qubit as target. - Arguments - targetNum the qubit to use as the target of the CNOT + :param targetNum: The qubit to use as the target of the CNOT """ self._logger.debug("VIRTUAL NODE %s: CNOT from %d to %d", self.node.name, self.num, targetNum) @@ -229,8 +239,7 @@ def remote_cphase_onto(self, targetNum): """ Performs a CPHASE operation with this qubit as control, and the other qubit as target. - Arguments - targetNum the qubit to use as the target of the CPHASE + :param targetNum: the qubit to use as the target of the CPHASE """ self._apply_random_pauli_noise() self.register.apply_CPHASE(self.num, targetNum) diff --git a/simulaqron/virtual_node/qutip_simulator.py b/simulaqron/virtual_node/qutip_simulator.py index e6cc293c..8cb6d7e2 100644 --- a/simulaqron/virtual_node/qutip_simulator.py +++ b/simulaqron/virtual_node/qutip_simulator.py @@ -32,6 +32,7 @@ from typing import Tuple, List import numpy as np +from qutip import Qobj try: import qutip as qp @@ -46,14 +47,18 @@ class QutipEngine(QuantumEngine): """ Basic quantum engine which uses QuTip. Works with density matrices and in principle allows full quantum dynamics via QuTip. Subsequently, this is quite slow. - - Attributes: - maxQubits: maximum number of qubits this engine will support. """ def __init__(self, node: str, num: int, maxQubits: int = 10): """ - Initialize the simple engine. If no number is given for maxQubits, the assumption will be 10. + Initialize the Qutip engine. + + :param node: Node name this register is started from. + :type node: str + :param num: Number of this register. + :type num: int + :param maxQubits: Maximum number of qubits this engine will support. + :type maxQubits: int """ super().__init__(node=node, num=num, maxQubits=maxQubits) @@ -63,7 +68,10 @@ def __init__(self, node: str, num: int, maxQubits: int = 10): def add_fresh_qubit(self) -> int: """ - Add a new qubit initialized in the \|0\> state. + Add a new qubit initialized in the :math:`|0>` state. + + :return: The ID of the new qubit allocated. + :rtype: int """ # Prepare a clean qubit state in |0> @@ -75,7 +83,11 @@ def add_fresh_qubit(self) -> int: def add_qubit(self, newQubit): """ - Add new qubit in the state described by the density matrix newQubit + Add new qubit in the state described by the density matrix newQubit. + + :param newQubit: The density matrix of the new qubit. + :return: The ID of the new qubit allocated. + :rtype: int """ # Check if we are still allowed to add qubits @@ -96,9 +108,12 @@ def add_qubit(self, newQubit): return num - def remove_qubit(self, qubitNum): + def remove_qubit(self, qubitNum: int): """ - Removes the qubit with the desired number qubitNum + Removes the qubit with the desired number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ if (qubitNum + 1) > self.activeQubits: raise QuantumError("No such qubit to remove") @@ -121,14 +136,16 @@ def remove_qubit(self, qubitNum): # Update the number of qubits self.activeQubits = self.activeQubits - 1 - def get_qubits_RI(self, qList): + def get_qubits_RI(self, qList: List[int]) -> Tuple[List[float], List[float]]: """ Retrieves the qubits in the list and returns the result as a list divided into a real and imaginary part. Twisted only likes to send real values lists, not complex ones. - Arguments - qList list of qubits to retrieve, e.g. [1, 4] + :param qList: List of qubits to retrieve, e.g. [1, 4] + :type qList: List[int] + :return: The qubits states real and imaginary parts. + :rtype: Tuple[List[float], List[float]] """ rho = self.get_qubits(qList) Re = rho.full().real.tolist() @@ -140,6 +157,9 @@ def get_register_RI(self) -> Tuple[List[float], List[float]]: """ Retrieves the entire register in real and imaginary parts and returns the result as a list. Twisted only likes to send real valued lists, not complex ones. + + :return: The qubit states real and imaginary parts. + :rtype: Tuple[List[float], List[float]] """ Re = self.qubitReg.full().real.tolist() Im = self.qubitReg.full().imag.tolist() @@ -147,6 +167,17 @@ def get_register_RI(self) -> Tuple[List[float], List[float]]: return Re, Im def get_density_matrix_RI(self) -> Tuple[Tuple[float], Tuple[float]]: + """ + Retrieves the density matrix of the qubit as a real and imaginary part. Twisted only + likes to send real valued lists, not complex ones. + + :return: The qubit density matrix real and imaginary parts. + :rtype: Tuple[List[float], List[float]] + """ + Re = self.qubitReg.full().real.tolist() + Im = self.qubitReg.full().imag.tolist() + + return Re, Im # Qutip uses density matrices as the internal representation, so we don't need # to compute the outer product to get the result real_part, im_part = self.get_register_RI() @@ -155,6 +186,9 @@ def get_density_matrix_RI(self) -> Tuple[Tuple[float], Tuple[float]]: def apply_H(self, qubitNum): """ Applies a Hadamard gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ f = math.sqrt(2) @@ -164,6 +198,9 @@ def apply_H(self, qubitNum): def apply_K(self, qubitNum): """ Applies a K gate to the qubits with number qubitNum. Maps computational basis to Y eigenbasis. + + :param qubitNum: Qubit number + :type qubitNum: int """ f = math.sqrt(2) @@ -174,6 +211,9 @@ def apply_K(self, qubitNum): def apply_X(self, qubitNum): """ Applies a X gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ X = qp.Qobj([[0, 1], [1, 0]], dims=[[2], [2]]) @@ -182,6 +222,9 @@ def apply_X(self, qubitNum): def apply_Z(self, qubitNum): """ Applies a Z gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ Z = qp.Qobj([[1, 0], [0, -1]], dims=[[2], [2]]) @@ -190,6 +233,9 @@ def apply_Z(self, qubitNum): def apply_Y(self, qubitNum): """ Applies a Y gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ i = complex(0, 1) @@ -199,6 +245,9 @@ def apply_Y(self, qubitNum): def apply_T(self, qubitNum): """ Applies a T gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ i = complex(0, 1) Y = qp.Qobj([[1, 0], [0, cmath.exp(i * np.pi / 4)]], dims=[[2], [2]]) @@ -209,13 +258,12 @@ def apply_rotation(self, qubitNum, n, a): Applies a rotation around the axis n with the angle a to qubit with number qubitNum. If n is zero a ValueError is raised. - :param qubitNum: int - Qubit number - :param n: tuple - A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) - :param a: float - The rotation angle in radians. - :rtype: None + :param qubitNum: Qubit number + :type qubitNum: int + :param n: A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) + :type n: Tuple[float, float, float] + :param a: The rotation angle in radians. + :type a: float """ nNorm = np.linalg.norm(n) if nNorm == 0: @@ -223,9 +271,14 @@ def apply_rotation(self, qubitNum, n, a): R = (-1j * a / (2 * nNorm) * (n[0] * qp.sigmax() + n[1] * qp.sigmay() + n[2] * qp.sigmaz())).expm() self.apply_onequbit_gate(R, qubitNum) - def apply_CNOT(self, qubitNum1, qubitNum2): + def apply_CNOT(self, qubitNum1: int, qubitNum2: int): """ Applies the CNOT to the qubit with the numbers qubitNum1 and qubitNum2. + + :param qubitNum1: Qubit number 1. + :type qubitNum1: int + :param qubitNum1: Qubit number 2. + :type qubitNum1: int """ # Construct the CNOT matrix @@ -237,6 +290,11 @@ def apply_CNOT(self, qubitNum1, qubitNum2): def apply_CPHASE(self, qubitNum1, qubitNum2): """ Applies the CPHASE to the qubit with the numbers qubitNum1 and qubitNum2. + + :param qubitNum1: Qubit number 1. + :type qubitNum1: int + :param qubitNum1: Qubit number 2. + :type qubitNum1: int """ # Construct the CPHASE matrix @@ -245,9 +303,12 @@ def apply_CPHASE(self, qubitNum1, qubitNum2): # Apply it to the desired qubits self.apply_twoqubit_gate(cphase, qubitNum1, qubitNum2) - def get_qubits(self, list): + def get_qubits(self, list: List[int]): """ Returns the qubits with numbers in list. + + :param list: List of qubits to retrieve. + :type list: List[int] """ # Qutip distinguishes between system dimensionality and matrix dimensionality @@ -262,13 +323,14 @@ def get_qubits(self, list): logging.debug("Dimensions %s", self.qubitReg.dims) return self.qubitReg.ptrace(list) - def apply_onequbit_gate(self, gateU, qubitNum): + def apply_onequbit_gate(self, gateU: Qobj, qubitNum: int): """ Applies a unitary gate to the specified qubit. - Arguments: - gateU unitary to apply as Qobj - qubitNum the number of the qubit this gate is applied to + :param gateU: Unitary to apply as Qobj. + :type gateU: Qobj + :param qubitNum: The number of the qubit this gate is applied to + :type qubitNum: int """ overallU = gate_ops.gate_expand_1toN(gateU, self.activeQubits, qubitNum) @@ -287,14 +349,16 @@ def apply_onequbit_gate(self, gateU, qubitNum): # Apply the unitary self.qubitReg = overallU * self.qubitReg * overallU.dag() - def apply_twoqubit_gate(self, gateU, qubit1, qubit2): + def apply_twoqubit_gate(self, gateU: Qobj, qubit1: int, qubit2: int): """ Applies a unitary gate to the two specified qubits. - Arguments: - gateU unitary to apply as Qobj - qubit1 the first qubit - qubit2 the second qubit + :param gateU: Unitary to apply as Qobj. + :type gateU: Qobj + :param qubit1: The first qubit + :type qubit1: int + :param qubit2: The second qubit + :type qubit2: int """ # Construct the overall unitary @@ -313,13 +377,13 @@ def apply_twoqubit_gate(self, gateU, qubit1, qubit2): # Apply the unitary self.qubitReg = overallU * self.qubitReg * overallU.dag() - def measure_qubit_inplace(self, qubitNum): + def measure_qubit_inplace(self, qubitNum: int): """ Measures the desired qubit in the standard basis. This returns the classical outcome. The quantum register - is in the post-measurment state corresponding to the obtained outcome. + is in the post-measurement state corresponding to the obtained outcome. - Arguments: - qubitNum qubit to be measured + :param qubitNum: The number of the qubit to measure. + :type qubitNum: int """ # Check we have such a qubit... @@ -353,21 +417,27 @@ def measure_qubit_inplace(self, qubitNum): # return measurement outcome return outcome - def measure_qubit(self, qubitNum): + def measure_qubit(self, qubitNum: int): """ Measures the desired qubit in the standard basis. This returns the classical outcome and deletes the qubit. - Arguments: - qubitNum qubit to be measured + :param qubitNum: The number of the qubit to measure. + :type qubitNum: int """ outcome = self.measure_qubit_inplace(qubitNum) self.remove_qubit(qubitNum) return outcome - def replace_qubit(self, qubitNum, state): + def replace_qubit(self, qubitNum: int, state): """ Replaces the qubit at position qubitNum with the one given by state. + + :param qubitNum: Qubit to be replaced + :type qubitNum: int + :param state: New state to write in the place of the old qubit. + :type state: Any + :param """ # Remove the qubit currently there by tracing it out @@ -385,6 +455,9 @@ def replace_qubit(self, qubitNum, state): def absorb(self, other): """ Absorb the qubits from the other engine into this one. This is done by tensoring the state at the end. + + :param other: The other qubit to absorb. + :type other: int """ # Check whether there is space @@ -404,10 +477,11 @@ def absorb_parts(self, R, I, activeQ): """ Absorb the qubits, given in pieces - Arguments: - R real part of the qubit state as a list - I imaginary part as a list - activeQ active number of qubits + :param R: Real part of the qubit state as a list. + :type R: List[float] + :param I: Imaginary part as a list. + :type I: List[float] + :param activeQ: Active number of qubits """ # Convert the real and imaginary parts given as lists into a qutip object diff --git a/simulaqron/virtual_node/stabilizer_simulator.py b/simulaqron/virtual_node/stabilizer_simulator.py index 7c555eb1..1948eb9c 100644 --- a/simulaqron/virtual_node/stabilizer_simulator.py +++ b/simulaqron/virtual_node/stabilizer_simulator.py @@ -36,14 +36,18 @@ class StabilizerEngine(QuantumEngine): """ Basic quantum engine which uses stabilizer formalism. Thus only Clifford operations can be performed - - Attributes: - maxQubits: maximum number of qubits this engine will support. """ def __init__(self, node: str, num: int, maxQubits: int = 10): """ - Initialize the simple engine. If no number is given for maxQubits, the assumption will be 10. + Initialize the stabilizer engine. + + :param node: Node name this register is started from. + :type node: str + :param num: Number of this register. + :type num: int + :param maxQubits: Maximum number of qubits this engine will support. + :type maxQubits: int """ super().__init__(node=node, num=num, maxQubits=maxQubits) @@ -56,7 +60,10 @@ def activeQubits(self): def add_fresh_qubit(self) -> int: """ - Add a new qubit initialized in the |0> state. + Add a new qubit initialized in the :math:`|0>` state. + + :return: The ID of the new qubit allocated. + :rtype: int """ # Check if we are still allowed to add qubits if self.activeQubits >= self.maxQubits: @@ -73,6 +80,10 @@ def add_qubit(self, newQubit): """ Add new qubit in the state described by the array containing the generators of the stabilizer group. This should be in the form required by the StabilizerState class. + + :param newQubit: The density matrix of the new qubit. + :return: The ID of the new qubit allocated. + :rtype: int """ # Create the qubit @@ -90,6 +101,9 @@ def add_qubit(self, newQubit): def remove_qubit(self, qubitNum): """ Removes the qubit with the desired number qubitNum + + :param qubitNum: Qubit number + :type qubitNum: int """ if (qubitNum + 1) > self.activeQubits: raise QuantumError("No such qubit to remove") @@ -102,6 +116,9 @@ def get_register_RI(self): not complex ones. Since this is in stabilizer formalism the real part will be the boolean matrix describing the generators and the imaginary part will be None + + :return: The qubit states real and imaginary parts. + :rtype: Tuple[List[float], List[float]] """ Re = self.qubitReg.to_array().tolist() @@ -110,24 +127,40 @@ def get_register_RI(self): return Re, Im def get_density_matrix_RI(self) -> Tuple[Tuple[float], Tuple[float]]: + """ + Retrieves the density matrix of the qubit as a real and imaginary part. Twisted only + likes to send real valued lists, not complex ones. + + :return: The qubit density matrix real and imaginary parts. + :rtype: Tuple[List[float], List[float]] + """ # TODO - Implement this raise NotImplementedError("get_density_matrix_RI is not implemented for stabilizer engine.") def apply_H(self, qubitNum): """ Applies a Hadamard gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.qubitReg.apply_H(qubitNum) def apply_K(self, qubitNum): """ Applies a K gate to the qubits with number qubitNum. Maps computational basis to Y eigenbasis. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.qubitReg.apply_K(qubitNum) def apply_X(self, qubitNum): """ Applies a X gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.qubitReg.apply_X(qubitNum) @@ -135,6 +168,9 @@ def apply_X(self, qubitNum): def apply_Z(self, qubitNum): """ Applies a Z gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.qubitReg.apply_Z(qubitNum) @@ -142,6 +178,9 @@ def apply_Z(self, qubitNum): def apply_Y(self, qubitNum): """ Applies a Y gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ self.qubitReg.apply_Y(qubitNum) @@ -149,6 +188,9 @@ def apply_Y(self, qubitNum): def apply_T(self, qubitNum): """ Applies a T gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int """ raise SimUnsupportedError("Cannot apply T gate in stabilizer formalism") @@ -157,24 +199,34 @@ def apply_rotation(self, qubitNum, n, a): Applies a rotation around the axis n with the angle a to qubit with number qubitNum. If n is zero a ValueError is raised. - :param qubitNum: int - Qubit number - :param n: tuple of floats - A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) - :param a: float - The rotation angle in radians. + :param qubitNum: Qubit number + :type qubitNum: int + :param n: A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) + :type n: Tuple[float, float, float] + :param a: The rotation angle in radians. + :type a: float """ raise SimUnsupportedError("Cannot apply arbitrary rotation gate in stabilizer formalism") def apply_CNOT(self, qubitNum1, qubitNum2): """ Applies the CNOT to the qubit with the numbers qubitNum1 and qubitNum2. + + :param qubitNum1: Qubit number 1. + :type qubitNum1: int + :param qubitNum1: Qubit number 2. + :type qubitNum1: int """ self.qubitReg.apply_CNOT(qubitNum1, qubitNum2) def apply_CPHASE(self, qubitNum1, qubitNum2): """ Applies the CPHASE to the qubit with the numbers qubitNum1 and qubitNum2. + + :param qubitNum1: Qubit number 1. + :type qubitNum1: int + :param qubitNum1: Qubit number 2. + :type qubitNum1: int """ self.qubitReg.apply_CZ(qubitNum1, qubitNum2) @@ -183,9 +235,8 @@ def apply_onequbit_gate(self, gate, qubitNum): """ Applies a unitary gate to the specified qubit. - Arguments: - gate The project Q gate to be applied - qubitNum the number of the qubit this gate is applied to + ..warning:: This method is unsupported in the Stabilizer engine. + Invoking it will raise ``SimUnsupportedError``. """ raise SimUnsupportedError("Cannot apply arbitrary one qubit gate in stabilizer formalism") @@ -194,20 +245,18 @@ def apply_twoqubit_gate(self, gate, qubit1, qubit2): """ Applies a unitary gate to the two specified qubits. - Arguments: - gate The project Q gate to be applied - qubit1 the first qubit - qubit2 the second qubit + ..warning:: This method is unsupported in the Stabilizer engine. + Invoking it will raise ``SimUnsupportedError``. """ raise SimUnsupportedError("Cannot apply arbitrary two qubit gate in stabilizer formalism") - def measure_qubit_inplace(self, qubitNum): + def measure_qubit_inplace(self, qubitNum: int): """ Measures the desired qubit in the standard basis. This returns the classical outcome. The quantum register - is in the post-measurment state corresponding to the obtained outcome. + is in the post-measurement state corresponding to the obtained outcome. - Arguments: - qubitNum qubit to be measured + :param qubitNum: The number of the qubit to measure. + :type qubitNum: int """ # Check we have such a qubit... @@ -219,26 +268,32 @@ def measure_qubit_inplace(self, qubitNum): # return measurement outcome return outcome - def measure_qubit(self, qubitNum): + def measure_qubit(self, qubitNum: int): """ Measures the desired qubit in the standard basis. This returns the classical outcome and deletes the qubit. - Arguments: - qubitNum qubit to be measured + :param qubitNum: The number of the qubit to measure. + :type qubitNum: int """ outcome = self.qubitReg.measure(qubitNum, inplace=False) return outcome - def replace_qubit(self, qubitNum, state): + def replace_qubit(self, qubitNum: int, state): """ Replaces the qubit at position qubitNum with the one given by state. + + ..warning:: This method is unsupported in the Stabilizer engine. + Invoking it will raise ``SimUnsupportedError``. """ raise NotImplementedError("Currently you cannot replace a qubit using stabilizer formalism") def absorb(self, other): """ Absorb the qubits from the other engine into this one. This is done by tensoring the state at the end. + + :param other: The other qubit to absorb. + :type other: int """ # Check whether there is space @@ -252,10 +307,11 @@ def absorb_parts(self, R, I, activeQ): """ Absorb the qubits, given in pieces - Arguments: - R The array describing the stabilizer state (from StabilizerState.to_array) - I Unused - activeQ active number of qubits + :param R: Real part of the qubit state as a list. + :type R: List[float] + :param I: Unused. + :type I: List[float] + :param activeQ: Active number of qubits """ # Check whether there is space newNum = self.activeQubits + activeQ diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 35e18b7d..68b567cd 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -29,7 +29,7 @@ import importlib import random from collections import deque -from typing import Tuple, List +from typing import Tuple, List, Any import logging from twisted.internet.defer import inlineCallbacks, DeferredLock, Deferred, DeferredList @@ -37,7 +37,7 @@ from twisted.internet.task import deferLater from twisted.spread import pb from twisted.spread.pb import RemoteError, RemoteReference -from typing_extensions import Generator +from typing_extensions import Generator, Self from simulaqron.virtual_node.basics import QuantumError, NoQubitError, VirtNetError from simulaqron.virtual_node.quantum import SimulatedQubit @@ -53,8 +53,9 @@ def reraise_remote_error(remote_err: RemoteError): """ This is a function re-raises the error thrown remotely + :param remote_err: :obj:`twisted.spread.pb.RemoteError` - :return: class + :raises: A python class that was encoded in the Error """ # Get names of remote package and remote error class error_pkg_name = [pkg.decode() for pkg in remote_err.remoteType.split(b".")[:-1]] @@ -69,7 +70,18 @@ def reraise_remote_error(remote_err: RemoteError): @inlineCallbacks def call_method(obj, method_name, *args, **kwargs): - """Convenience method to call a method on an object, or a remote reference to an object""" + """ + Convenience method to call a method on an object, or a remote reference to an object + + :param obj: Object to call the method on. + :type obj: Any + :param method_name: Name of the method to call. + :type method_name: str + :param args: Positional arguments to pass to the method. + :type args: Any + :param kwargs: Keyword arguments to pass to the method. + :type kwargs: Any + """ if isinstance(obj, RemoteReference): try: output = yield obj.callRemote(method_name, *args, **kwargs) @@ -92,8 +104,14 @@ def call_method(obj, method_name, *args, **kwargs): class Backend: def __init__(self, name: str, network_name: str = "default"): """ - Initialize. This will read the networks configuration and populate the name,hostname,port information with the - information found in the configuration file for the given name. + Create the Virtual Node backend. This will read the networks configuration and + populate the name,hostname,port information with the information found in the + configuration file for the given name. + + :param name: Node name to start. + :type name: str + :network_name: Name of the network to start. + :type network_name: str """ self._logger = logging.getLogger(f"{self.__class__.__name__}({name})") @@ -109,8 +127,10 @@ def start( """ Start listening to requests from other nodes. - Arguments - maxQubits maximum qubits in the default register + :param max_qubits: Maximum qubits in the default register. + :type max_qubits: int + :param max_registers: Maximum number of registers in the default register. + :type max_registers: int """ try: @@ -142,10 +162,11 @@ def __init__(self, ID: Host, config: SocketsConfig, """ Initialize storing also our own name, hostname and port. - Arguments: - ID host identifier of this node - maxQubits maximum number of qubits to use in the default engine (default 10) - maxRegister maximum number of registers + :param ID: Host identifier of this node + :param maxQubits: Maximum number of qubits to use in the default engine (default 10) + :type maxQubits: int + :param maxRegisters: Maximum number of registers + :type maxRegisters: int """ self._logger = logging.getLogger(f"{self.__class__.__name__}({ID.name})") @@ -196,8 +217,8 @@ def __init__(self, ID: Host, config: SocketsConfig, def connectNet(self): """ - Initialize the connections to the other virtual nodes in the network according to the available - configuration. + Initialize the connections to the other virtual nodes in the network according + to the available configuration. """ for key in self.config.hostDict: @@ -209,9 +230,9 @@ def connectNet(self): def remote_check_connections(self): """ - Checks if all connections are up. (Just checks if the number of - connections equal the number of nodes in config-file) + Checks if all connections are up. """ + # Just checks if the number of connections equal the number of nodes in config-file return len(self.conn) == len(self.config.hostDict) @inlineCallbacks @@ -219,6 +240,9 @@ def get_connection(self, name): """ Returns the connection specified by 'name'. If no such connection is up yet but name is in the configuration file, wait and try again. + + :param name: The name of the node connection to retrieve. + :type name: str """ if name in self.conn: return self.conn[name] @@ -232,9 +256,13 @@ def get_connection(self, name): ) return conn_to_return - def connect_to_node(self, node): + def connect_to_node(self, node: Host): """ - Connects to other node. If node not up yet, waits for CONF_WAIT_TIME seconds. + Connects to other node. If node not up yet, waits for ``conn_retry_time`` seconds + as configured in the ``simulaqron_settings.json`` file. + + :param node: The node to connect to. + :type node: Host """ self._logger.debug("Trying to connect to node %s.", node.name) node.factory = pb.PBClientFactory() @@ -243,9 +271,13 @@ def connect_to_node(self, node): defer.addCallback(self.handle_connection, node) defer.addErrback(self.handle_connection_error, node) - def handle_connection(self, obj, node): + def handle_connection(self, obj, node: Host): """ Callback obtaining twisted root object when connection to the node given by the node details 'node'. + + :param obj: Twisted root object. + :param node: The node that was connected to. + :type node: Host """ self._logger.debug("New connection to %s.", node.name) # Retrieve the root object: virtualNode on the remote @@ -257,8 +289,8 @@ def handle_connection(self, obj, node): def handle_connection_error(self, reason, node): """ Handles errors from trying to connect to other node. - If a ConnectionRefusedError is raised another try will be made after CONF_WAIT_TIME seconds. - CONF_WAIT_TIME is set in 'settings.py'. + If a ConnectionRefusedError is raised another try will be made after `conn_retry_time`` seconds + as configured in the ``simulaqron_settings.json`` file. Any other error is raised again. """ @@ -272,7 +304,7 @@ def handle_connection_error(self, reason, node): self._logger.exception(e) reactor.stop() - def get_virtual_id(self): + def _get_virtual_id(self): """ This is a crude and horrible cludge to generate unique IDs for virtual qubits. """ @@ -287,7 +319,7 @@ def get_virtual_id(self): if used == 0: return j - def get_sim_id(self): + def _get_sim_id(self): """ Similarly, this is a crude and horrible cludge to generate unique IDs for simulated qubits. """ @@ -343,16 +375,18 @@ def remote_release_global_lock(self): @inlineCallbacks def _lock_reg_qubits(self, qubit): """ - Acquire the lock on all qubits in the same register as the local sim qubit qubit. + Acquire the lock on all qubits in the same register as the local sim qubit. """ for q in self.simQubits: if q.register == qubit.register: yield q.lock() @inlineCallbacks - def remote_lock_reg_qubits(self, qubitNum): + def remote_lock_reg_qubits(self, qubitNum: int): """ Acquire the lock on all qubits in the same register as qubitNum. + + :param qubitNum: The qubit number to lock. """ yield self._lock_reg_qubits(self._q_num_to_obj(qubitNum)) @@ -366,18 +400,20 @@ def _unlock_reg_qubits(self, qubit): yield q.unlock() @inlineCallbacks - def remote_unlock_reg_qubits(self, qubitNum): + def remote_unlock_reg_qubits(self, qubitNum: int): """ Release the lock on all qubits in the same register as qubitNum. + + :param qubitNum: The qubit number on which to lock all qubits. """ yield self._unlock_reg_qubits(self._q_num_to_obj(qubitNum)) - def remote_add_register(self, maxQubits=10): + def remote_add_register(self, maxQubits: int = 10): """ - Adds a new register to the node.. + Adds a new register to the node. - Arguments: - maxQubits maximum number of qubits to use in the default engine + :param maxQubits: Maximum number of qubits to use in the default engine + :type maxQubits: int """ # TODO We have two methods that do the same thing, should deprecate one of them return self.remote_new_register(maxQubits=maxQubits) @@ -390,13 +426,13 @@ def get_new_reg_num(self): self._next_reg_num += 1 return reg_num - def remote_new_register(self, maxQubits=10): + def remote_new_register(self, maxQubits: int = 10): """ Initialize a local register. Right now, this simple creates a register according to the simple engine backend using qubit. - Arguments: - maxQubits maximum number of qubits to use in the default engine (default 10) + :param maxQubits: Maximum number of qubits to use in the default engine (default 10) + :type maxQubits: int """ # Make sure that reg numbers are assigned correctly @@ -438,13 +474,14 @@ def remote_delete_register(self, reg): self.numRegs -= 1 @inlineCallbacks - def remote_new_qubit(self, ignore_max_qubits=False): + def remote_new_qubit(self, ignore_max_qubits: bool = False): """ Create a new qubit in the default local register. - :param ignore_max_qubits: bool - Used to ignore the check if max virtual qubits is reached. This is used when creating EPR pairs - to be able to temporarily create a qubit. + :param ignore_max_qubits: Used to ignore the check if max virtual qubits is reached. + This is used when creating EPR pairs to be able to temporarily + create a qubit. + :type ignore_max_qubits: bool """ self._logger.debug("Request to create new qubit.") @@ -458,7 +495,7 @@ def remote_new_qubit(self, ignore_max_qubits=False): raise NoQubitError("Max virtual qubits reached") else: # Qubit in the simulation backend, initialized to |0> - simNum = self.get_sim_id() + simNum = self._get_sim_id() # Create a new register newReg = self.remote_add_register() @@ -469,7 +506,7 @@ def remote_new_qubit(self, ignore_max_qubits=False): self.simQubits.append(simQubit) # Virtual qubit - newNum = self.get_virtual_id() + newNum = self._get_virtual_id() newQubit = VirtualQubit(self.myID, self.myID, simQubit, newNum) self.virtQubits.append(newQubit) finally: @@ -496,13 +533,13 @@ def remote_new_qubit_inreg(self, reg): raise NoQubitError("Max virtual qubits reached") else: # Qubit in the local simulation backend, initialized to |0> - simNum = self.get_sim_id() + simNum = self._get_sim_id() simQubit = SimulatedQubit(self.myID, reg, simNum) simQubit.make_fresh() self.simQubits.append(simQubit) # Virtual qubit - newNum = self.get_virtual_id() + newNum = self._get_virtual_id() newQubit = VirtualQubit(self.myID, self.myID, simQubit, newNum) self.virtQubits.append(newQubit) finally: @@ -511,15 +548,18 @@ def remote_new_qubit_inreg(self, reg): return newQubit @inlineCallbacks - def remote_netqasm_send_qubit(self, num, targetName, app_id, remote_app_id): + def remote_netqasm_send_qubit(self, num: int, targetName: str, app_id: int, remote_app_id: int): """ Send interface for NetQASM to add the qubit to the remote nodes received list for an application. - Arguments: - num number of virtual qubit to send - targetName name of the node to send to - app_id application asking to have this qubit delivered - remote_app_id application ID to deliver the qubit to + :param num: Number of virtual qubit to send + :type num: int + :param targetName: Name of the node to send to + :type targetName: str + :param app_id: Application asking to have this qubit delivered + :type app_id: int + :param remote_app_id: Application ID to deliver the qubit to + :type remote_app_id: int """ self._logger.debug("request to send qubit %d to %s", num, targetName) @@ -547,9 +587,18 @@ def remote_netqasm_send_qubit(self, num, targetName, app_id, remote_app_id): except RemoteError as remote_err: reraise_remote_error(remote_err) - def remote_netqasm_add_recv_list(self, fromName, from_epr_socket_id, to_epr_socket_id, new_virt_num=None): + def remote_netqasm_add_recv_list(self, fromName, from_epr_socket_id: int, to_epr_socket_id: int, new_virt_num=None): """ Add an item to the received list for use in NetQASM. + + :param fromName: Name of the node to receive the qubit. + :type fromName: str + :param from_epr_socket_id: EPR socket ID + :type from_epr_socket_id: int + :param to_epr_socket_id: EPR socket ID + :type to_epr_socket_id: int + :param new_virt_num: New virtual qubit number + :type new_virt_num: int """ if not (to_epr_socket_id in self.qubit_recv): @@ -569,6 +618,9 @@ def remote_netqasm_add_recv_list(self, fromName, from_epr_socket_id, to_epr_sock def remote_netqasm_get_recv(self, to_epr_socket_id): """ Retrieve the next qubit with the given app ID form the received list. + + :param to_epr_socket_id: EPR socket ID + :type to_epr_socket_id: int """ self._logger.debug("Trying to retrieve qubit on EPR socket ID %d from recv list", to_epr_socket_id) @@ -589,16 +641,19 @@ def remote_netqasm_get_recv(self, to_epr_socket_id): return self.remote_get_virtual_ref(qc.virt_num) @inlineCallbacks - def remote_netqasm_send_epr_half(self, num, targetName, app_id, remote_app_id, rawEntInfo): + def remote_netqasm_send_epr_half(self, num: int, targetName: str, app_id: int, remote_app_id: int, rawEntInfo): """ Send interface for NetQASM to add the qubit to the remote nodes received list for an application. - Arguments: - num number of virtual qubit to send - targetName name of the node to send to - app_id application asking to have this qubit delivered - remote_app_id application ID to deliver the qubit to - entInfo entanglement information + :param num: Number of virtual qubit to send + :type num: int + :param targetName: Name of the node to send to + :type targetName: str + :param app_id: Application asking to have this qubit delivered + :type app_id: int + :param remote_app_id: Application ID to deliver the qubit to + :type remote_app_id: int + :param rawEntInfo: Entanglement information """ if num is None: # Only an outcome from measure directly so no qubit @@ -632,6 +687,16 @@ def remote_netqasm_send_epr_half(self, num, targetName, app_id, remote_app_id, r def remote_netqasm_add_epr_list(self, fromName, from_epr_socket_id, to_epr_socket_id, new_virt_num, rawEntInfo): """ Add an item to the epr list for use in NetQASM. + + :param fromName: Name of the node to receive the qubit. + :type fromName: str + :param from_epr_socket_id: EPR socket ID + :type from_epr_socket_id: int + :param to_epr_socket_id: EPR socket ID + :type to_epr_socket_id: int + :param new_virt_num: New virtual qubit number + :type new_virt_num: int + :param rawEntInfo: Entanglement information """ if not (to_epr_socket_id in self.qubit_recv_epr): @@ -650,9 +715,12 @@ def remote_netqasm_add_epr_list(self, fromName, from_epr_socket_id, to_epr_socke ) self._logger.debug("Added a qubit on EPR socket ID %d to epr list", to_epr_socket_id) - def remote_netqasm_get_epr_recv(self, to_epr_socket_id): + def remote_netqasm_get_epr_recv(self, to_epr_socket_id: int): """ Retrieve the next qubit (half of an EPR-pair) with the given app ID from the received list. + + :param to_epr_socket_id: EPR socket ID + :type to_epr_socket_id: int """ self._logger.debug("Trying to retrieve qubit on EPR socket ID %d from epr list", to_epr_socket_id) # Get the list corresponding to the specified application ID @@ -675,14 +743,15 @@ def remote_netqasm_get_epr_recv(self, to_epr_socket_id): return self.remote_get_virtual_ref(qc.virt_num), qc.rawEntInfo @inlineCallbacks - def remote_send_qubit(self, qubit, targetName): + def remote_send_qubit(self, qubit: int, targetName: Host): """ Sends the qubit to the specified target node. This creates a new virtual qubit object at the remote node with the right qubit and backend details. - Arguments - qubit virtual qubit to be sent - targetName target ndoe to place qubit at (host object) + :param qubit: Virtual qubit to be sent + :type qubit: int + :param targetName: Target node to place qubit at. + :type targetName: Host """ self._logger.debug("Request to send qubit sim Num %d to %s.", qubit.num, targetName) if qubit.active != 1: @@ -741,15 +810,16 @@ def remote_send_qubit(self, qubit, targetName): return newNum @inlineCallbacks - def remote_transfer_qubit(self, simQubitNum, targetName): + def remote_transfer_qubit(self, simQubitNum: int, targetName: Host): """ Transfer the qubit to the destination node if we are the simulating node. The reason why we cannot do this directly is that Twisted PB does not allow objects to be passed between connecting nodes. Only between the creator of the object and its immediate connections. - Arguments - simQubitNum simulated qubit number to be sent - targetName target node to place qubit at (host object) + :param simQubitNum: Simulated qubit number to be sent + :type simQubitNum: int + :param targetName: Target node to place qubit at (host object) + :type targetName: Host """ self._logger.debug("Request to transfer qubit to %s.", targetName) @@ -775,13 +845,14 @@ def remote_transfer_qubit(self, simQubitNum, targetName): return newNum @inlineCallbacks - def remote_add_qubit(self, name, simQubit): + def remote_add_qubit(self, name: str, simQubit: SimulatedQubit): """ Add a qubit to the local virtual node. - Arguments - name name of the node simulating this qubit - simQubit simulated qubit reference in the backend we're adding + :param name: Name of the node simulating this qubit + :type name: str + :param simQubit: Simulated qubit reference in the backend we're adding + :type simQubit: SimulatedQubit """ self._logger.debug("Request to add qubit from %s.", name) @@ -801,7 +872,7 @@ def remote_add_qubit(self, name, simQubit): raise NoQubitError("Max virtual qubits reached") # Generate a new virtual qubit object for the qubit now at this node - newNum = self.get_virtual_id() + newNum = self._get_virtual_id() newQubit = VirtualQubit(self.myID, nb, simQubit, newNum) # Add to local list @@ -811,12 +882,12 @@ def remote_add_qubit(self, name, simQubit): return newNum - def remote_get_virtual_ref(self, num): + def remote_get_virtual_ref(self, num: int): """ - Return a virual qubit object for the given number. + Return a virtual qubit object for the given number. - Arguments - num number of the virtual qubit + :param num: Number of the virtual qubit + :type num: int """ for q in self.virtQubits: @@ -826,24 +897,24 @@ def remote_get_virtual_ref(self, num): return None @inlineCallbacks - def remote_remove_sim_qubit_num(self, delNum): + def remote_remove_sim_qubit_num(self, delNum: int): """ - Removes the simulated qubit delQubit from the node and also from the underlying engine. Relies on this qubit - having been locked. + Removes the simulated qubit delQubit from the node and also from the underlying engine. + Relies on this qubit having been locked. - Arguments - delNum simID of the simulated qubit to delete + :param delNum: simID of the simulated qubit to delete + :type delNum: int """ yield self._remove_sim_qubit(self._q_num_to_obj(delNum)) @inlineCallbacks - def _remove_sim_qubit(self, delQubit): + def _remove_sim_qubit(self, delQubit: SimulatedQubit): """ Removes the simulated qubit object. - Arguments - delQubit simulated qubit object to delete + :param delQubit: Simulated qubit object to delete. + :type delQubit: SimulatedQubit """ # Caution: Only qubits simulated at this node can be removed if delQubit not in self.simQubits: @@ -892,14 +963,13 @@ def _remove_sim_qubit(self, delQubit): if q.register == delRegister: q.unlock() - def remote_merge_regs(self, num1, num2): + def remote_merge_regs(self, num1: int, num2: int): """ Merges the two local quantum registers. Note that these register may simulate virtual qubits across different network nodes. This will ignore maxQubits and simply create one large register allowing twice maxQubits qubits. - Arguments - num1 number of the first qubit - num2 number of the second qubit + :param num1: Number of the first qubit + :param num2: Number of the second qubit """ # Lookup the qubit objects corresponding to these numbers @@ -911,14 +981,15 @@ def remote_merge_regs(self, num1, num2): self.local_merge_regs(q1, q2) - def local_merge_regs(self, qubit1, qubit2): + def local_merge_regs(self, qubit1: SimulatedQubit, qubit2): """ Merges the two local quantum registers. Note that these register may simulate virtual qubits across different network nodes. This will ignore maxQubits and simply create one large register allowing twice maxQubits qubits. - Arguments - qubit1 qubit1 in reg1, called from remote having access to only qubits - qubit2 qubit2 in reg2 + :param qubit1: qubit1 in reg1, called from remote having access to only qubits + :type qubit1: SimulatedQubit + :param qubit2: qubit2 in reg2 + :type qubit2: SimulatedQubit """ self._logger.debug( "Request to merge local register for qubits simNum %d and simNum %d.", qubit1.simNum, qubit2.simNum @@ -960,14 +1031,15 @@ def local_merge_regs(self, qubit1, qubit2): self.remote_delete_register(reg2) @inlineCallbacks - def remote_merge_from(self, simNodeName, simQubitNum, localReg): + def remote_merge_from(self, simNodeName: str, simQubitNum: int, localReg): """ Bring a remote register to this node. - Arguments - simNodeName name of the node who simulates right now - simQubitNum simulation number of qubit whose register we will merge - localReg local register to merge with + :param simNodeName: Name of the node who simulates right now + :type simNodeName: str + :param simQubitNum: Simulation number of qubit whose register we will merge + :type simQubitNum: int + :param localReg: Local register to merge with """ self._logger.debug("Merging from %s", simNodeName) @@ -1002,7 +1074,7 @@ def remote_merge_from(self, simNodeName, simQubitNum, localReg): # Make new qubit objects for k in range(activeQ): - simNum = self.get_sim_id() + simNum = self._get_sim_id() newQubit = SimulatedQubit(self.myID, localReg, simNum, offset + k) # Lock the qubit directly until merge is finished yield newQubit.lock() @@ -1028,16 +1100,18 @@ def remote_merge_from(self, simNodeName, simQubitNum, localReg): return newD[oldQubitNum] @inlineCallbacks - def remote_update_virtual_merge(self, newSimNodeName, oldSimNodeName, oldRegNum, newD): + def remote_update_virtual_merge(self, newSimNodeName: str, oldSimNodeName: str, oldRegNum: int, newD): """ Update the virtual qubits to the new simulating node, if applicable. This is extremely inefficient due to not keeping register information in virtualQubit. - Arguments - newSimNodeName new node simulating this qubit - oldSimNodeName old node simulating the qubit - oldReg old register - newD dictionary mapping qubit numbers to qubit objects at the new simulating node + :param newSimNodeName: New node simulating this qubit + :type newSimNodeName: str + :param oldSimNodeName: Old node simulating the qubit + :type oldSimNodeName: str + :param oldRegNum: Old register number + :type oldRegNum: int + :param newD: Dictionary mapping qubit numbers to qubit objects at the new simulating node. """ self._logger.debug("Request to update local virtual qubits.") @@ -1088,6 +1162,8 @@ def remote_get_register_RI(self, qubit): """ Return the real and imaginary part of the (possibly remote) simulated register which contains this virtual qubit. + + :param qubit: Qubit object to get the register from. """ if isinstance(qubit, VirtualQubit): realM, imagM = yield qubit.remote_get_register_RI() @@ -1098,6 +1174,8 @@ def remote_get_register_RI(self, qubit): def remote_get_register(self, qubit): """ Return the value of a locally simulated register which contains this virtual qubit. + + :param qubit: Qubit object to get the register from. """ (realM, imagM) = qubit.simQubit.register.get_register_RI() @@ -1107,11 +1185,14 @@ def remote_get_register(self, qubit): return (realM, imagM, activeQ, oldRegNum, oldQubitNum) - def remote_get_register_del(self, qubitNum): + def remote_get_register_del(self, qubitNum: int): """ Return the value of a locally simulated register, and remove the simulated qubits from this node. - Caution: virtual qubits not updated. + .. caution:: virtual qubits not updated. + + :param qubitNum: Qubit number to get the register from and remove. + :type qubitNum: int """ assert self._lock.locked, "Virtual node is not locked" @@ -1145,13 +1226,13 @@ def remote_get_register_del(self, qubitNum): return (realM, imagM, activeQ, oldRegNum, oldQubitNum) @inlineCallbacks - def remote_get_multiple_qubits(self, qList): + def remote_get_multiple_qubits(self, qList: List[SimulatedQubit]): """ Return the state of multiple qubits virtually located at this node. This will fail if the qubits are not in the same register or thus also simulating node. - Arguments - qList list of virtual qubits of which to retrieve the state + :param qList: List of virtual qubits of which to retrieve the state + :type qList: List[SimulatedQubit] """ localSim = False @@ -1192,9 +1273,12 @@ def remote_get_multiple_qubits(self, qList): return (R, I) - def remote_get_state(self, simNumList): + def remote_get_state(self, simNumList: List[int]): """ Return the state of multiple qubits corresponding to the IDs in simNumList. + + :param simNumList: List of simulated qubit numbers. + :type simNumList: List[int] """ # Convert simulation numbers to register and real number in register @@ -1219,8 +1303,15 @@ def remote_get_state(self, simNumList): return (realM, imagM) - def remote_sim_qubit_num_in_same_reg(self, sim_qubit_num1, sim_qubit_num2): - """Checks if two qubits are in the same register""" + def remote_sim_qubit_num_in_same_reg(self, sim_qubit_num1: int, sim_qubit_num2: int): + """ + Checks if two qubits are in the same register + + :param sim_qubit_num1: Qubit number 1. + :type sim_qubit_num1: int + :param sim_qubit_num2: Qubit number 2. + :type sim_qubit_num2: int + """ sim_qubit1 = self._q_num_to_obj(sim_qubit_num1) sim_qubit2 = self._q_num_to_obj(sim_qubit_num2) assert sim_qubit1 is not None, "Sim num {sim_qubit_num1} not in this node" @@ -1249,11 +1340,14 @@ def __init__(self, virtNode: Host, simNode: Host, simQubit: SimulatedQubit, num: """ Creates a virtual qubit object simulated in the specified simulation register backend - Arguments - virtNode node where this qubit is virtually located - simNode node where this qubit is simulated - simQubit reference to the underlying qubit object (may be remote) - num number ID among the virtual qubits + :param virtNode: Node where this qubit is virtually located + :type virtNode: Host + :param simNode: Node where this qubit is simulated + :type simNode: Host + :param simQubit: Reference to the underlying qubit object (it can be remote) + :type simQubit: SimulatedQubit + :param num: Number ID among the virtual qubits + :type num: int """ self._logger = logging.getLogger(f"{self.__class__.__name__}({virtNode.name}, {num})") @@ -1276,15 +1370,16 @@ def __init__(self, virtNode: Host, simNode: Host, simQubit: SimulatedQubit, num: self.num = num @inlineCallbacks - def _single_gate(self, name, *args): + def _single_gate(self, name: str, *args: Any): """ Apply the single gate function to the underlying qubit. This is an internal method used by all the other single qubit calls, which will perform the correct local or remote method calls as applicable after performing the necessary locking. - Arguments - name name of the method corresponding to the name. For example: name = apply_X - param parameters for gates such as rotations (axis,angle) + :param name: Name of the method corresponding to the name. For example: "apply_X" + :type name: str + :param args: Arguments for gates such as rotations (axis,angle) + :type args: Any """ self._logger.debug("applying gate %s to virtual qubit %d", name, self.num) if self.active != 1: @@ -1345,21 +1440,25 @@ def remote_apply_T(self): yield self._single_gate("apply_T") @inlineCallbacks - def remote_apply_rotation(self, n, a): + def remote_apply_rotation(self, n: Tuple[int, int, int], a: float): """ Apply rotation around axis n with angle a. - Arguments: - n A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) - a The rotation angle in radians. + + :param n: A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) + :type n: Tuple[int, int, int] + :param a: The rotation angle in radians. + :type a: float """ yield self._single_gate("apply_rotation", n, a) @inlineCallbacks - def remote_measure(self, inplace=False): + def remote_measure(self, inplace: bool = False): """ Measure the qubit in the standard basis. If inplace=False, this does delete the qubit from the simulation. - Returns the measurement outcome. + :param inplace: Whether to perform the measurement in place or not. + :type inplace: bool + :return: The measurement outcome. """ if self.active != 1: @@ -1388,7 +1487,7 @@ def remote_measure(self, inplace=False): return outcome @inlineCallbacks - def _lock_nodes(self, target): + def _lock_nodes(self, target: Self): """ Wrapper to acquire the global register lock on nodes that involve the qubits, and local node. This can in fact be everyting from a single node if both qubits are simulated locally or three nodes @@ -1400,14 +1499,10 @@ def _lock_nodes(self, target): Furthermore, when waiting for locks of a simulating node, this might change in the meantime, so we check that indeed the simulating nodes are the same after acquiring the locks. If not, we try again until success. - Parameters - ---------- - target : :class:`~.virtualQubit` - virtual qubit of the target qubit - - Returns - ------- - list: The nodes that have been locked so that they can be unlocked again by the caller + :params target: Virtual qubit of the target qubit + :type target: VirtualQubit + :return: The nodes that have been locked so that they can be unlocked again by the caller + :rtype: List[Host] """ local_node = self.virtNode control_sim_node = self.simNode @@ -1449,9 +1544,12 @@ def _lock_nodes(self, target): return list(ds.keys()) @inlineCallbacks - def _lock_inreg(self, qubit): + def _lock_inreg(self, qubit: Self): """ - Lock all qubits in the same register as the virtual qubit qubit. + Lock all qubits in the same register as the virtual qubit. + + :param qubit: The virtual qubit to lock all qubits in the same register. + :type qubit: VirtualQubit """ try: @@ -1464,9 +1562,12 @@ def _lock_inreg(self, qubit): reraise_remote_error(remote_err) @inlineCallbacks - def _unlock_inreg(self, qubit): + def _unlock_inreg(self, qubit: Self): """ - Lock all qubits in the same register as the virtual qubit qubit. + Unlock all qubits in the same register as the virtual qubit. + + :param qubit: The virtual qubit to unlock all qubits in the same register. + :type qubit: VirtualQubit """ try: @@ -1479,42 +1580,43 @@ def _unlock_inreg(self, qubit): reraise_remote_error(remote_err) @inlineCallbacks - def remote_cnot_onto(self, target): + def remote_cnot_onto(self, target: Self): """ Performs a CNOT operation with this qubit as control, and the other qubit as target. - Arguments - target the virtual qubit to use as the target of the CNOT + :param target: The virtual qubit to use as the target of the CNOT + :type target: VirtualQubit """ yield self._two_qubit_gate(target, "cnot_onto") @inlineCallbacks - def remote_cphase_onto(self, target): + def remote_cphase_onto(self, target: Self): """ Performs a CPHASE operation with this qubit as control, and the other qubit as target. - Arguments - target the virtual qubit to use as the target of the CPHASE + :param target: The virtual qubit to use as the target of the CPHASE + :type target: VirtualQubit """ yield self._two_qubit_gate(target, "cphase_onto") @inlineCallbacks - def _two_qubit_gate(self, target, name): + def _two_qubit_gate(self, target: Self, name: str): """ Perform a two qubit gate including all the required locking. - Arguments - target second virtual qubit (beyond self which is the first) - name name of the gate to perform + :param target: Second virtual qubit (beyond self which is the first) + :type target: VirtualQubit + :param name: Name of the gate to perform + :type name: str """ if self.active != 1 or target.active != 1: self._logger.error("Attempt to manipulate qubits no longer at this node.") return - localName = "".join(["remote_", name]) + localName = f"remote_{name}" self._logger.debug("Doing 2 qubit gate name %s and local call %s", name, localName) # First lock the relevant nodes @@ -1671,6 +1773,9 @@ def remote_get_number(self): """ Returns the number of this qubit in whatever local register it is in. Not useful for the client, but convenient for debugging. + + :return: The number of qubits in the register where this qubit resides. + :rtype: int """ if self.active != 1: @@ -1690,18 +1795,27 @@ def remote_get_number(self): def remote_get_virt_num(self): """ Returns the number of the virtual qubit. + + :return: The number of the virtual qubit. + :rtype: int """ return self.num def remote_get_virtNode(self): """ Returns the virtNode of this virtual qubit + + :return: The name of the virtual node of this qubit. + :rtype: str """ return self.virtNode.name def remote_get_simNode(self): """ Returns the simNode of this virtual qubit + + :return: the name of the simNode of this qubit. + :rtype: str """ return self.simNode.name @@ -1710,6 +1824,9 @@ def remote_get_qubit(self): """ Returns the state of this qubit in real and imaginary parts separated. This is required single Twisted cannot natively transfer complex valued objects. + + :return: The real and imaginary part of the state of this qubit. + :rtype: Tule[float, float] """ if self.active != 1: @@ -1732,6 +1849,12 @@ def remote_get_qubit(self): def remote_get_density_matrix_RI( self ) -> Generator[Deferred, Tuple[List[float], List[float]], Tuple[List[float], List[float]]]: + """ + Returns the density matrix of this qubit in real and imaginary parts separated. + + :return: The density matrix decomposed in real and imaginary part. + :rtype: Tuple[List[float], List[float]], Tuple[List[float], List[float]]] + """ # This function calls itself recursively *on the remote* if the simulated node # is not the current node # Otherwise, it calls the corresponding function of the local simulated qubit @@ -1743,6 +1866,12 @@ def remote_get_density_matrix_RI( @inlineCallbacks def remote_get_register_RI(self): + """ + Gets the register of this qubit as imaginary and real pars separated. + + :return: The register of this qubit as imaginary and real pars separated. + :rtype: Tuple[List[float], List[float]] + """ if self.simNode == self.virtNode: realM, imagM = self.simQubit.register.get_register_RI() else: @@ -1750,16 +1879,14 @@ def remote_get_register_RI(self): return realM, imagM @inlineCallbacks - def _lock_simulating_node(self, exclude=None): - """Aquires a global lock on the simulating node + def _lock_simulating_node(self, exclude: List[Host] = None): + """Acquires a global lock on the simulating node Since the simulating node can change while trying to acquire the lock, we check if this happened and if so, try again. - Parameters - ---------- - exclude : list - List of hosts which to exclude since they might already have been locked + :param exclude: List of hosts which to exclude since they might already have been locked. + :type exclude: List[Host] """ if exclude is None: exclude = [] From b92c2fca6eaf6161db1fb6a2caba197375e18014 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 15 Jan 2026 23:15:52 +0100 Subject: [PATCH 202/277] Fix warnings when compiling python docs --- docs/simulaqron.netqasm_backend.rst | 2 +- docs/simulaqron.sdk.rst | 2 +- docs/simulaqron.settings.rst | 8 +- docs/simulaqron.start.rst | 2 +- docs/simulaqron.virtual_node.rst | 2 +- simulaqron/math.py | 2 +- simulaqron/netqasm_backend/executioner.py | 3 + simulaqron/settings/simulaqron_config.py | 24 ++--- simulaqron/toolbox/stabilizer_states.py | 92 +++++++++---------- simulaqron/virtual_node/qutip_simulator.py | 7 +- .../virtual_node/stabilizer_simulator.py | 2 +- 11 files changed, 69 insertions(+), 77 deletions(-) diff --git a/docs/simulaqron.netqasm_backend.rst b/docs/simulaqron.netqasm_backend.rst index a149bafd..abac417f 100644 --- a/docs/simulaqron.netqasm_backend.rst +++ b/docs/simulaqron.netqasm_backend.rst @@ -1,5 +1,5 @@ simulaqron.netqasm_backend package -====================== +================================== Submodules ---------- diff --git a/docs/simulaqron.sdk.rst b/docs/simulaqron.sdk.rst index d5072f06..214a8df0 100644 --- a/docs/simulaqron.sdk.rst +++ b/docs/simulaqron.sdk.rst @@ -31,7 +31,7 @@ simulaqron.sdk.socket module Module contents --------------- -.. automodule:: simulaqron.run +.. automodule:: simulaqron.sdk :members: :undoc-members: :show-inheritance: diff --git a/docs/simulaqron.settings.rst b/docs/simulaqron.settings.rst index 12c2aa74..f829840f 100644 --- a/docs/simulaqron.settings.rst +++ b/docs/simulaqron.settings.rst @@ -4,10 +4,10 @@ simulaqron.settings package Submodules ---------- -simulaqron.settings.network module ----------------------------------- +simulaqron.settings.network_config module +----------------------------------------- -.. automodule:: simulaqron.settings.network +.. automodule:: simulaqron.settings.network_config :members: :undoc-members: :show-inheritance: @@ -24,7 +24,7 @@ simulaqron.settings.simulaqron_config module Module contents --------------- -.. automodule:: simulaqron.run +.. automodule:: simulaqron.settings :members: :undoc-members: :show-inheritance: diff --git a/docs/simulaqron.start.rst b/docs/simulaqron.start.rst index 3d3da686..8a15025b 100644 --- a/docs/simulaqron.start.rst +++ b/docs/simulaqron.start.rst @@ -23,7 +23,7 @@ simulaqron.start.start_vnode module Module contents --------------- -.. automodule:: simulaqron.run +.. automodule:: simulaqron.start :members: :undoc-members: :show-inheritance: diff --git a/docs/simulaqron.virtual_node.rst b/docs/simulaqron.virtual_node.rst index 55de195c..5404c3a4 100644 --- a/docs/simulaqron.virtual_node.rst +++ b/docs/simulaqron.virtual_node.rst @@ -37,7 +37,7 @@ simulaqron.virtual_node.qutip_simulator module :show-inheritance: simulaqron.virtual_node.stabilizer_simulator module --------------------------------------------------- +--------------------------------------------------- .. automodule:: simulaqron.virtual_node.stabilizer_simulator :members: diff --git a/simulaqron/math.py b/simulaqron/math.py index 2bb1ae92..ec6d3f5f 100644 --- a/simulaqron/math.py +++ b/simulaqron/math.py @@ -9,7 +9,7 @@ def assemble_qubit(realM: np.ndarray, imagM: np.ndarray) -> np.ndarray: :type realM: np.ndarray :param imagM: Imaginary component of the qubit. :type imagM: np.ndarray - :return: Assembled qubit as vector of :py:type:`complex` numbers. + :return: Assembled qubit as vector of complex numbers. :rtype: np.array """ # We need this since Twisted PB does not support sending complex valued object natively. diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 0e55ce45..be3db9e8 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -45,6 +45,7 @@ def __init__(self, executioner): in order to use the correct framework as used by the netqasm executioner. .. warning:: This class is candidate to be deleted! Test and delete if not needed! + """ self._executioner = executioner self._sockets: Dict[int, Tuple[int, int]] = {} @@ -301,6 +302,7 @@ def get_virt_qubit(self, qubit_id: int) -> Referenceable: If not found, raises a :py:class:`UnknownQubitError`. .. Caution:: Twisted PB does not allow references to objects to be passed back between connections. + If you need to pass a qubit reference back to the Twisted PB on a _different_ connection, then use get_virt_qubit_indep below. @@ -318,6 +320,7 @@ def get_virt_qubit(self, qubit_id: int) -> Referenceable: def get_virt_qubit_num(self, qubit_id: int): """ Get the *integer* qubit ID to virtual qubit in SimulaQron specific to this connection. + .. caution:: This method return a qubit ID (an integer), not a ``twisted.spread.flavors.Referenceable`` object. If you need to get a twisted object, check :py:meth:`get_virt_qubit`. diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 9f9503c4..0bebde97 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -144,18 +144,18 @@ def default_settings(self): """ Resets the current SimulaQron configuration object to its default configuration set. The default configuration is: - * max_qubits = 20 - * max_registers = 1000 - * conn_retry_time = 0.5 - * conn_max_retries = 10 - * recv_timeout = 100 - * recv_retry_time = 0.1 - * recv_max_retries = 10 - * log_level = logging.WARNING - * sim_backend = SimBackend.STABILIZER - * noisy_qubits = False - * max_app_waiting_time = -1.0 # In seconds, negative means unlimited waiting - * t1: float = 1.0 + * max_qubits = 20 + * max_registers = 1000 + * conn_retry_time = 0.5 + * conn_max_retries = 10 + * recv_timeout = 100 + * recv_retry_time = 0.1 + * recv_max_retries = 10 + * log_level = logging.WARNING + * sim_backend = SimBackend.STABILIZER + * noisy_qubits = False + * max_app_waiting_time = -1.0 # In seconds, negative means unlimited waiting + * t1: float = 1.0 """ default_config = SimulaqronConfig() cls_fields = fields(self.__class__) diff --git a/simulaqron/toolbox/stabilizer_states.py b/simulaqron/toolbox/stabilizer_states.py index a3d12781..6bf24338 100644 --- a/simulaqron/toolbox/stabilizer_states.py +++ b/simulaqron/toolbox/stabilizer_states.py @@ -29,59 +29,49 @@ def __init__(self, data=None, check_symplectic: bool = True): Clifford operations and Pauli-measurements. If check_symplectic=True then a check will be made that all stabilizers commute, by checking - That the matrix is symplectic. Otherwise no check is made. - - :param data: - Can be one of the following: - - A binary array of rank 2: - A binary array representing the generators of the stabilizer group. - If the array is n-by-2n a stabilizer state on n qubits will be represented. - The n first columns are the X-stabilizers and the n last the Z-stabilizer. - If the array is n-by-(2n+1), the last column is seen as the phase for each generator - as follows: - 0 -> 1 - 1 -> -1 - - An array of rank 1 containing 'str': - Then each string is assumed to be a generator as for example "XXZIY" - Note that each string in the array should have the same length. - If the number of strings is 'n' then a stabilizer state on 'n' qubits is created. - If the strings have length 'n' then it is assumed that the phase is '+1'. - An explicit phase can be added to the start of the string as for example: "-1XXXY". - Creating a Bell-pair: - StabilizerState(["XX", "ZZ"]) # The state (|00> + |11>) / sqrt(2) - - 'None' (default): - Then this is seen as a stabilizer state on no qubits, i.e. a complex number. - To add a qubit to such a state one can do: - s = StabilizerState() - s.add_qubit() # This is now in the state |0> - - 'int': - Then a stabilizer state on this many qubits are created, all in the state |0> as: - StabilizerState(5) # This is the then the state |00000> - - 'networkx.Graph': - Then the graph state corresponding to this graph will be created. - This assumes that the nodes are numbered from 0 to n - 1, where n is the number of nodes. - For example: - StabilizerState(networkx.complete_graph(5)) # Single qubit Clifford equiv. to a GHZ state + That the matrix is symplectic. Otherwise, no check is made. Examples: - A qubit in the state |0> can be created as: - StabilizerState([[0, 1]]) - - A qubit in the state |1> can be created as: - StabilizerState([[0, 1, 1]]) - - The entangled state (|00> + |11>)/sqrt(2) can be created as: - StabilizerState([[1, 1, 0, 0], - 0, 0, 1, 1]]) - - The entangled state (|01> + |10>)/sqrt(2) can be created as: - StabilizerState([[1, 1, 0, 0, 0], - 0, 0, 1, 1, 1]]) + A qubit in the state :math:`|0>` can be created as ``StabilizerState([[0, 1]])``. + + A qubit in the state :math:`|1>` can be created as ``StabilizerState([[0, 1, 1]])``. + + The entangled state :math:`(|00> + |11>)/\sqrt(2)` can be created as + ``StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]])``. + + The entangled state :math:`(|01> + |10>)/\sqrt(2)` can be created as + ``StabilizerState([[1, 1, 0, 0, 0], [0, 0, 1, 1, 1]])``. + + :param data: Can be one of the following: + + * A binary array of rank 2 representing the generators of the stabilizer group. + If the array is n-by-2n a stabilizer state on n qubits will be represented. + The n first columns are the X-stabilizers and the n last the Z-stabilizer. + If the array is n-by-(2n+1), the last column is seen as the phase for each generator + as follows: + 0 -> 1 + 1 -> -1 + * An array of rank 1 containing ``str``: + Then each string is assumed to be a generator as for example ``XXZIY`` + Note that each string in the array should have the same length. + If the number of strings is ``n`` then a stabilizer state on ``n`` qubits is created. + If the strings have length ``n`` then it is assumed that the phase is ``+1``. + An explicit phase can be added to the start of the string as for example: ``-1XXXY``. + Creating a Bell-pair: + ``StabilizerState(["XX", "ZZ"]) # The state (|00> + |11>) / sqrt(2)`` + * ``None`` (default): + Then this is seen as a stabilizer state on no qubits, i.e. a complex number. + To add a qubit to such a state one can do: + ``s = StabilizerState()`` + ``s.add_qubit() # This is now in the state |0>`` + * ``int``: + Then a stabilizer state on this many qubits are created, all in the state :math:`|0>` as: + ``StabilizerState(5) # This is the then the state |00000>`` + * ``networkx.Graph``: + Then the graph state corresponding to this graph will be created. + This assumes that the nodes are numbered from 0 to n - 1, where n is the number of nodes. + For example: + ``StabilizerState(networkx.complete_graph(5)) # Single qubit Clifford equiv. to a GHZ state`` :param check_symplectic: Whether to check if all stabilizers commute or not. :type check_symplectic: bool """ diff --git a/simulaqron/virtual_node/qutip_simulator.py b/simulaqron/virtual_node/qutip_simulator.py index 8cb6d7e2..8c10d95e 100644 --- a/simulaqron/virtual_node/qutip_simulator.py +++ b/simulaqron/virtual_node/qutip_simulator.py @@ -45,13 +45,12 @@ class QutipEngine(QuantumEngine): """ - Basic quantum engine which uses QuTip. Works with density matrices and in principle allows full quantum - dynamics via QuTip. Subsequently, this is quite slow. + Basic quantum engine which uses QuTip. Works with density matrices and in principle allows + full quantum dynamics via QuTip. Subsequently, this is quite slow. """ - def __init__(self, node: str, num: int, maxQubits: int = 10): """ - Initialize the Qutip engine. + Initializes the Qutip engine :param node: Node name this register is started from. :type node: str diff --git a/simulaqron/virtual_node/stabilizer_simulator.py b/simulaqron/virtual_node/stabilizer_simulator.py index 1948eb9c..1a1d2b53 100644 --- a/simulaqron/virtual_node/stabilizer_simulator.py +++ b/simulaqron/virtual_node/stabilizer_simulator.py @@ -35,7 +35,7 @@ class StabilizerEngine(QuantumEngine): """ - Basic quantum engine which uses stabilizer formalism. Thus only Clifford operations can be performed + Basic quantum engine which uses stabilizer formalism. Thus, only Clifford operations can be performed """ def __init__(self, node: str, num: int, maxQubits: int = 10): From 3cad396f618766ba526a7d47cd4c877da114c064 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 16 Jan 2026 20:01:42 +0100 Subject: [PATCH 203/277] Fix and re-enable test_merges --- Makefile | 3 +++ tests/slow/merges/test_merges.py | 36 +++++++++++++++++--------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index 16775ae4..e00887d6 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,9 @@ install-optional: install tests: @${PYTHON} -m pytest -v ${TEST_DIR}/quick +tests_slow: + @${PYTHON} -m pytest -v ${TEST_DIR}/slow + tests_all: @${PYTHON} -m pytest -v --capture=tee-sys ${TEST_DIR} diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 3924cbe5..ebe6afe2 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -1,4 +1,6 @@ import sys +from pathlib import Path + import pytest import os import unittest @@ -148,8 +150,8 @@ def remote_receive_one_qubit(self, virtualNum, cnot_direction=0): return bool(correct) -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestMerge(unittest.TestCase): @classmethod def setUpClass(cls): @@ -165,7 +167,7 @@ def setUpClass(cls): network_config_file = os.path.join(path_to_here, "configs", "network.json") network_config.read_from_file(network_config_file) nodes = ["Alice", "Bob", "Charlie"] - cls.network = Network(nodes=nodes) + cls.network = Network(nodes=nodes, network_config_file=Path(network_config_file)) cls.network.start() @classmethod @@ -228,8 +230,8 @@ def run_test(self, nodes_in_class_network: List[str]): self.assertTrue(all(results)) -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothLocal(TestMerge): @classmethod def setUpClass(cls): @@ -289,8 +291,8 @@ def test(self): self.run_test([]) -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothLocalNotSameReg(TestBothLocal): @classmethod @inlineCallbacks @@ -341,8 +343,8 @@ def alice(cls, qReg, virtRoot, myName, classicalNet, send_end): reactor.stop() -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothRemote(TestMerge): @classmethod def setUpClass(cls): @@ -433,8 +435,8 @@ def test(self): self.run_test(["Alice", "Bob", "Charlie"]) -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothRemoteSameNodeDiffReg(TestMerge): @classmethod def setUpClass(cls): @@ -502,8 +504,8 @@ def test(self): self.run_test(["Alice", "Bob"]) -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestBothRemoteSameNodeSameReg(TestBothRemoteSameNodeDiffReg): @staticmethod @inlineCallbacks @@ -547,8 +549,8 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): reactor.stop() -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestRemoteAtoB(TestBothRemoteSameNodeDiffReg): @staticmethod @inlineCallbacks @@ -581,8 +583,8 @@ def alice(qReg, virtRoot, myName, classicalNet, send_end): reactor.stop() -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") -@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs refactoring to use NetQASM") +#@pytest.mark.skip(reason="Uses deprecated internal APIs - needs updating to new API") class TestRemoteBtoA(TestBothRemoteSameNodeDiffReg): @staticmethod @inlineCallbacks From f421fe1a0014c824b8ef57ce2e03725ae3881004 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 16 Jan 2026 20:22:16 +0100 Subject: [PATCH 204/277] Add a note about moving the merge tests to NetQASM API --- tests/slow/merges/test_merges.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index ebe6afe2..24eea83f 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -27,6 +27,22 @@ _logger = logging.getLogger("test_merges") +# Diego Rivera: 2026-01-16: +# Note about moving these tests to NetQASM API. +# NetQASM is a library that needs to be implemented by the simulators. It also +# aims to be a higher-level abstraction layer, so it does not expose implementation +# details from the underlying quantum simulator. +# On the other hand, these tests aim to check the functionality of merging two +# qubits into a single register. This functionality *is specific to the SimulaQron +# simulator*, since it is required to allow quantum simulation distributed in +# multiple hosts. +# Being this said, I see two main difficulties to move these tests to NetQASM API: +# * Since NetQASM aims to hide all implementation details, there is no NetQASM +# primitive to express "moving a qubit from one host to another". +# * The functionality tested here is SimulaQron-specific. It would be difficult +# to rewrite these tests in NetQASM, which might hide APIs needed to fully test +# the underlying functionality. + class localNode(pb.Root): def __init__(self, node, classicalNet): From 8a47166d5e23d815d36d2c6f39ca22d2f729fcb1 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 16 Jan 2026 20:34:39 +0100 Subject: [PATCH 205/277] Fix entry point declaration for CLI tool --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3e794895..d4835cbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ include = ["simulaqron"] exclude = ["simulaqron/config"] [project.scripts] -simulaqron = "simulaqron.cli::cli_entry_point" +simulaqron = "simulaqron.cli:cli_entry_point" [project.urls] Homepage = "https://github.com/SoftwareQuTech/SimulaQron" \ No newline at end of file From 0b14c319c36541062838f0780f4b7a153d4310e1 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 16 Jan 2026 22:25:18 +0100 Subject: [PATCH 206/277] Linter changes --- simulaqron/local/setup.py | 2 +- simulaqron/math.py | 2 +- simulaqron/netqasm_backend/executioner.py | 21 ++++++++++++++++----- simulaqron/network.py | 2 +- simulaqron/reactor.py | 2 +- tests/slow/merges/test_merges.py | 1 - 6 files changed, 20 insertions(+), 10 deletions(-) diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index b5fc84ca..23a2c13d 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -131,7 +131,7 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon def _init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig, classicalNet: SocketsConfig, - lNode: pb.Root, func: Callable, *args, **kwargs): + lNode: pb.Root, func: Callable, *args, **kwargs): _logger.debug("SETUP_LOCAL %s: All connections set up.", myName) # Retrieve the connection to the local virtual node, if successful diff --git a/simulaqron/math.py b/simulaqron/math.py index ec6d3f5f..d178fef7 100644 --- a/simulaqron/math.py +++ b/simulaqron/math.py @@ -18,4 +18,4 @@ def assemble_qubit(realM: np.ndarray, imagM: np.ndarray) -> np.ndarray: for t in range(len(M)): M[s][t] = realM[s][t] + 1j * imagM[s][t] - return M \ No newline at end of file + return M diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index be3db9e8..eba9658e 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -4,7 +4,7 @@ from collections import defaultdict from enum import Enum from functools import partial -from typing import Generator, List, Tuple, Callable, Dict +from typing import Generator, List, Tuple, Callable, Dict, Union import netqasm.lang.instr.core as core_instructions import netqasm.lang.instr.vanilla as vanilla_instructions @@ -35,7 +35,11 @@ class UnknownQubitError(RuntimeError): """ pass -_VanillaRotInstr = vanilla_instructions.RotXInstruction | vanilla_instructions.RotYInstruction | vanilla_instructions.RotZInstruction + +_VanillaRotInstr = Union[vanilla_instructions.RotXInstruction, + vanilla_instructions.RotYInstruction, + vanilla_instructions.RotZInstruction] + # TODO - This class is candidate to be deleted! Test and delete if not needed! class NetworkStack(BaseNetworkStack): @@ -62,6 +66,7 @@ def setup_epr_socket(self, epr_socket_id, remote_node_id, remote_epr_socket_id, def get_purpose_id(self, remote_node_id: int, epr_socket_id: int) -> int: pass + class VanillaSimulaQronExecutioner(Executor): SIMULAQRON_OPS = { vanilla_instructions.GateXInstruction: "apply_X", @@ -213,7 +218,13 @@ def _do_single_qubit_instr(self, instr, subroutine_id, address): qubit_id=position, ) - def _do_single_qubit_rotation(self, instr: core_instructions.RotationInstruction, subroutine_id: int, address: int, angle: float): + def _do_single_qubit_rotation( + self, + instr: core_instructions.RotationInstruction, + subroutine_id: int, + address: int, + angle: float + ): assert isinstance(instr, _VanillaRotInstr) position = self._get_position(subroutine_id=subroutine_id, address=address) axis = self._get_axis(instr=instr) @@ -356,7 +367,7 @@ def cmd_measure(self, qubit_id: int, inplace=True): return outcome @inlineCallbacks - def cmd_reset(self, qubit_id: int, correct: bool=True): + def cmd_reset(self, qubit_id: int, correct: bool = True): r""" Reset the given qubit to the state :math:`|0>`. @@ -373,7 +384,7 @@ def cmd_reset(self, qubit_id: int, correct: bool=True): if correct and outcome: yield call_method(virt_qubit, "apply_X") - def _do_wait(self, delay: float=0.1): + def _do_wait(self, delay: float = 0.1): d = task.deferLater(reactor, delay, lambda: self._logger.debug("Wait finished")) self._logger.debug("waiting a bit") yield d diff --git a/simulaqron/network.py b/simulaqron/network.py index dc82d0db..87cdd550 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -30,7 +30,7 @@ import random import time from timeit import default_timer as timer -from typing import List, Optional, Dict +from typing import List, Dict import networkx as nx from multiprocess.context import ForkProcess as Process diff --git a/simulaqron/reactor.py b/simulaqron/reactor.py index b6853e43..1d88a417 100644 --- a/simulaqron/reactor.py +++ b/simulaqron/reactor.py @@ -18,4 +18,4 @@ reactor.start() ... reactor.stop() -""" \ No newline at end of file +""" diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 24eea83f..3880199b 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -1,7 +1,6 @@ import sys from pathlib import Path -import pytest import os import unittest import logging From 09250a13469ffe463c151e0ba5b5b48ae2c9a947 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 16 Jan 2026 22:28:54 +0100 Subject: [PATCH 207/277] Add projectq and qutip as test deps --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d4835cbb..e5b01c83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,9 @@ classifiers = [ [project.optional-dependencies] test = [ "flake8>=5.0.0,<6.0.0", - "pytest>=7.1, <8.0" + "pytest>=7.1, <8.0", + "projectq<=0.8.0", + "qutip<5.0.0" ] opt = [ "projectq<=0.8.0", From 28f18611edd0496803ec428967e89ded61403b9f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 2 Feb 2026 17:22:14 +0100 Subject: [PATCH 208/277] Fix bugs in the CLI entry module. --- simulaqron/cli.py | 10 +++++----- simulaqron/settings/simulaqron_config.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/simulaqron/cli.py b/simulaqron/cli.py index a76d6cf1..14d7fce4 100644 --- a/simulaqron/cli.py +++ b/simulaqron/cli.py @@ -14,7 +14,7 @@ from simulaqron.settings import simulaqron_settings, network_config from simulaqron.settings.network_config import (NodeConfig, DEFAULT_SIMULAQRON_NETWORK_FILENAME, get_default_network_config_file) -from simulaqron.settings.simulaqron_config import SimBackend +from simulaqron.settings.simulaqron_config import SimBackend, get_default_simulaqron_config_file CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) # PID folder should be "LOCAL" @@ -156,15 +156,15 @@ def version(): "--network-config-file", help=f"Path to network config file. If not specified, uses ./{DEFAULT_SIMULAQRON_NETWORK_FILENAME} " # noqa: E131 f"or ~/.simulaqron/{DEFAULT_SIMULAQRON_NETWORK_FILENAME}", # noqa: E131 - type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), + type=click.Path(exists=False, dir_okay=False, resolve_path=True, path_type=Path), default=get_default_network_config_file() ) @click.option( "--simulaqron-config-file", help=f"Use the given simulaqron config file. Defaults to the file named " # noqa: E131 f"'{DEFAULT_SIMULAQRON_NETWORK_FILENAME}' on the current directory.", # noqa: E131 - type=click.Path(exists=True, dir_okay=False, resolve_path=True, path_type=Path), - default=LOCAL_SIMULAQRON_SETTINGS + type=click.Path(exists=False, dir_okay=False, resolve_path=True, path_type=Path), + default=get_default_simulaqron_config_file() ) @click.option( "--name", @@ -226,7 +226,7 @@ def start(name: str, nodes: str, simulaqron_config_file: Path, network_config_fi "the --nodes argument." ) for node_to_start in nodes: - if node_to_start not in network_config.networks[name]: + if node_to_start not in network_config.networks[name].nodes: raise click.BadOptionUsage( option_name="nodes", message=f"The node '{node_to_start}' was not found in the network named " # noqa: E713 diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 0bebde97..18296e9b 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -182,3 +182,26 @@ def write_to_file(self, path: PathLike): with file_path.open("wt") as file: serialized = JSONSerializer.serialize(self) json.dump(serialized, file, indent=4) + +def get_default_simulaqron_config_file(): + """ + Get the simulaqron config file path to use. + + :return: Path to the simulaqron config file. + :rtype: Path + """ + # Implements using the local directory setting as a priority + if LOCAL_SIMULAQRON_SETTINGS.exists(): + return LOCAL_SIMULAQRON_SETTINGS + if HOME_SIMULAQRON_SETTINGS.exists(): + return HOME_SIMULAQRON_SETTINGS + + from . import simulaqron_settings + + # Create default in HOME (matches load_from_known_sources behavior) + # XXX I have mixed feelings we should do this, but I leave it for now + simulaqron_settings.default_settings() + HOME_SIMULAQRON_SETTINGS.parent.mkdir(parents=True, exist_ok=True) + simulaqron_settings.write_to_file(HOME_SIMULAQRON_SETTINGS) + return HOME_SIMULAQRON_SETTINGS + From 9126731102776d60977e358c3a6102fae174fbbb Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Mon, 2 Feb 2026 18:37:46 +0100 Subject: [PATCH 209/277] Small linter changes --- simulaqron/settings/simulaqron_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 18296e9b..9ab113e4 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -183,6 +183,7 @@ def write_to_file(self, path: PathLike): serialized = JSONSerializer.serialize(self) json.dump(serialized, file, indent=4) + def get_default_simulaqron_config_file(): """ Get the simulaqron config file path to use. @@ -204,4 +205,3 @@ def get_default_simulaqron_config_file(): HOME_SIMULAQRON_SETTINGS.parent.mkdir(parents=True, exist_ok=True) simulaqron_settings.write_to_file(HOME_SIMULAQRON_SETTINGS) return HOME_SIMULAQRON_SETTINGS - From 3523f11126414dd6be5ea05faa32a6771a6e7349 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 3 Feb 2026 19:16:10 +0100 Subject: [PATCH 210/277] Allow classical socket to be configured ot act as a server or client, instead of the socket to decide who acts as a socket --- examples/WARNING | 1 - simulaqron/sdk/socket.py | 23 +++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) delete mode 100644 examples/WARNING diff --git a/examples/WARNING b/examples/WARNING deleted file mode 100644 index 5d0aa7c7..00000000 --- a/examples/WARNING +++ /dev/null @@ -1 +0,0 @@ -These examples are out of date; they use data structures that were modernized! \ No newline at end of file diff --git a/simulaqron/sdk/socket.py b/simulaqron/sdk/socket.py index d9e05496..a63c8135 100644 --- a/simulaqron/sdk/socket.py +++ b/simulaqron/sdk/socket.py @@ -21,9 +21,10 @@ def __init__( remote_app_name: str, socket_id: int = 0, timeout: Optional[int] = None, - use_callbacks=False, - network_name="default", + use_callbacks: bool = False, + network_name: str = "default", log_config=None, + as_server: Optional[bool] = None, ): assert socket_id == 0, ( "SimulaQron socket does not support setting socket ID, this is instead done in the config file" @@ -38,7 +39,7 @@ def __init__( # We define _app_socket as None as a default value, so the __del__ method # does not fail when the socket could not be connected correctly. self._app_socket = None - self._app_socket: socket.socket = self._connect() + self._app_socket: socket.socket = self._connect(as_server) def __del__(self): self.close() @@ -182,7 +183,7 @@ def _deserialize_structured_msg(raw_msg: bytes) -> StructuredMessage: return dill.loads(raw_msg) @property - def is_server(self) -> bool: + def _should_be_server(self) -> bool: """ Check whether the local end of this socket will be acting as server or not. The decision is made based on the node names: the name which is alphabetically before will act as server. @@ -195,16 +196,22 @@ def is_server(self) -> bool: # Server will always be the "first" return self._node_name < self._remote_node_name - def _connect(self) -> socket.socket: - if self.is_server: + def _connect(self, as_server: Optional[bool] = None) -> socket.socket: + self_is_server: bool + if as_server is not None: + self_is_server = as_server server_name = self._node_name else: - server_name = self._remote_node_name + self_is_server = self._should_be_server + if self_is_server: + server_name = self._node_name + else: + server_name = self._remote_node_name addr = self._get_addr_info(name=server_name) app_socket = socket.socket(addr[0], addr[1], addr[2]) attempt = 0 - if self.is_server: + if self_is_server: self._logger.debug("Trying to open application socket as server") app_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) while True: From 0d03e86ff75613e1f34c74a4d16636a47e51d708 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 3 Feb 2026 21:06:00 +0100 Subject: [PATCH 211/277] Add first early version of the new SDK protocol definition --- simulaqron/sdk/protocol.py | 125 +++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 simulaqron/sdk/protocol.py diff --git a/simulaqron/sdk/protocol.py b/simulaqron/sdk/protocol.py new file mode 100644 index 00000000..da49d95d --- /dev/null +++ b/simulaqron/sdk/protocol.py @@ -0,0 +1,125 @@ +from enum import IntEnum +from pathlib import Path +from typing import Any, List, Callable, Optional + +from netqasm.sdk.external import NetQASMConnection +from simulaqron.sdk import SimulaQronConnection, Socket + + +class ServingStatus(IntEnum): + CONTINUE = 0 + STOP = 1 + + +class SimulaQronState: + # State of the SimulaQron simulator + def __init__(self): + """ + Keeps the state of the quantum memories across multiple invocations of the message handler. + A handler can use this object to make a quantum memory + """ + # TODO - Implement this constructor! + pass + + def add_return_values(self, *ret_val: Any) -> None: + """ + Adds a return value to the message handler. The returned value will be sent back to + the other endpoint of the connection. + + :param ret_val: The values to return. + :type ret_val: Any + """ + # TODO - Implement this! + pass + + def continue_serving(self) -> ServingStatus: + """ + Creates a "continue" value used to signal the server to keep waiting for new messages. + + :return: The "continue" value of the ``ServingStatus`` class. + :rtype: ServingStatus + """ + return ServingStatus.CONTINUE + + def stop_serving(self) -> ServingStatus: + """ + Creates a "stop" value used to signal the server to stop serving new messages. + + :return: The "stop" value of the ``ServingStatus`` class. + :rtype: ServingStatus + """ + return ServingStatus.STOP + + +class SimulaQronProtocol: + def __init__(self, network_config: str | Path, name: str): + self._network_config = network_config + self._node_name = name + # TODO - Load the network configuration in simulaqron! + pass + + +class SimulaQronClassicalClient(SimulaQronProtocol): + def __init__(self, network_config: str | Path, name: str): + """ + Classical client used to send classical messages to remote nodes. The given node name + must exist on the given network configuration. + + :param network_config: The path of the Network configuration. + :type network_config: str | Path + :param name: The name of the node to connect to. The name *must* exist in the network + configuration file. + :type name: str + """ + super().__init__(network_config, name) + + def connect_to(self, node_name: str) -> None: + """ + Connects to the node with the given name. + + :param node_name: The name of the node to connect to. The name *must* exist in the + configuration file given when constructing this client. + :type node_name: str + """ + pass + + def send_message(self, message: str) -> None: + """ + Sends a message to the remote endpoint of the connection. + + :param message: The message to send. + :type message: str + """ + + +class SimulaQronClassicalServer(SimulaQronProtocol): + def __init__(self, network_config: str | Path, name: str, simulaqron_connection: Optional[SimulaQronConnection] = None): + super().__init__(network_config, name) + self._message_handlers: List[Callable[[SimulaQronState, str], ServingStatus]] = [] + self._connection = simulaqron_connection + self._message_handlers: List[Callable[[SimulaQronState, str, NetQASMConnection], ServingStatus]] = [] + # TODO - Define what else to do in the constructor + + def register_message_handler(self, handler: Callable[[SimulaQronState, str], ServingStatus]) -> None: + """ + Registers the given function as a message handler. The given function must have the following signature:: + + def handler(state: SimulaQronState, message: str, connection: NetQASMConnection) -> ServingStatus: + return ServingStatus.CONTINUE + + The passed function will be called once a message arrives form the remote. + The function must return a value to signal the server loop to keep handling or not. + Any other value that must be returned to the remote, must be passed using the ``add_return_values`` method + from the ``SimulaQronState`` object passed to the handler. + + :param handler: The function to be used as a handler. + :type handler: Callable[[SimulaQronState, str], ServingStatus] + """ + self._message_handlers.append(handler) + + def start_serving(self) -> None: + """ + Starts the serving the clients using the registered handlers. + """ + # TODO - Implement + pass From 512f496dfbe96beec811bbdd9b7147deddb9b67f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 4 Feb 2026 16:39:18 +0100 Subject: [PATCH 212/277] Add classical communication example using the new SDK. --- .../classical-client-server/example_client.py | 30 ++++++++++++++++++ .../classical-client-server/example_server.py | 31 +++++++++++++++++++ .../simulaqron_network.json | 22 +++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 examples/new-sdk/classical-client-server/example_client.py create mode 100644 examples/new-sdk/classical-client-server/example_server.py create mode 100644 examples/new-sdk/classical-client-server/simulaqron_network.json diff --git a/examples/new-sdk/classical-client-server/example_client.py b/examples/new-sdk/classical-client-server/example_client.py new file mode 100644 index 00000000..eefc29b1 --- /dev/null +++ b/examples/new-sdk/classical-client-server/example_client.py @@ -0,0 +1,30 @@ +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.settings import network_config +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings.network_config import NodeConfigType + +async def client_code(reader: StreamReader, writer: StreamWriter): + data = "Hello World!".encode("utf-8") + writer.write(data) + print(f"Sent message '{data.decode("utf-8")}'") + result = await reader.read(255) + print(f"Received message: '{result.decode("utf-8")}'") + writer.close() + +if __name__ == "__main__": + # Load the file network configuration file + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Get the socket configuration for the sockets used for the application layer + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + + # Create the client + client = SimulaQronClassicalClient(sockets_config) + + # Run the client. The given function will be called once the connection with + #m the given server was established + client.run_client("Alice", client_code) \ No newline at end of file diff --git a/examples/new-sdk/classical-client-server/example_server.py b/examples/new-sdk/classical-client-server/example_server.py new file mode 100644 index 00000000..dfae7317 --- /dev/null +++ b/examples/new-sdk/classical-client-server/example_server.py @@ -0,0 +1,31 @@ +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.settings import network_config +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalServer +from simulaqron.settings.network_config import NodeConfigType + +async def connection_handler(reader: StreamReader, writer: StreamWriter): + result = await reader.read(255) + print(f"Received message: '{result.decode("utf-8")}'") + writer.write(result) + writer.close() + +if __name__ == "__main__": + # Load the file network configuration file + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Get the socket configuration for the sockets used for the application layer + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + + # Create the server + server = SimulaQronClassicalServer(sockets_config, "Alice") + + # Register a new client handler. The given function will be called once a new client + # opens a connection with this node. + server.register_client_handler(connection_handler) + + # Start serving the clients + server.start_serving() \ No newline at end of file diff --git a/examples/new-sdk/classical-client-server/simulaqron_network.json b/examples/new-sdk/classical-client-server/simulaqron_network.json new file mode 100644 index 00000000..bdd299cf --- /dev/null +++ b/examples/new-sdk/classical-client-server/simulaqron_network.json @@ -0,0 +1,22 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 9831], + "qnodeos_socket": ["localhost", 9832], + "vnode_socket": ["localhost", 9833] + } + } + ], + "topology": null + } +] From 05bd58db2a541ad80076185d080d81519eefd48b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 4 Feb 2026 16:39:18 +0100 Subject: [PATCH 213/277] Update the specification of the new SDK to create clients and servers --- simulaqron/sdk/protocol.py | 106 ++++++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 43 deletions(-) diff --git a/simulaqron/sdk/protocol.py b/simulaqron/sdk/protocol.py index da49d95d..19cb0c7d 100644 --- a/simulaqron/sdk/protocol.py +++ b/simulaqron/sdk/protocol.py @@ -1,9 +1,9 @@ +import asyncio +from asyncio import StreamWriter, StreamReader from enum import IntEnum -from pathlib import Path -from typing import Any, List, Callable, Optional +from typing import Any, Awaitable, Optional, Callable -from netqasm.sdk.external import NetQASMConnection -from simulaqron.sdk import SimulaQronConnection, Socket +from simulaqron.general.host_config import SocketsConfig class ServingStatus(IntEnum): @@ -51,37 +51,47 @@ def stop_serving(self) -> ServingStatus: return ServingStatus.STOP -class SimulaQronProtocol: - def __init__(self, network_config: str | Path, name: str): - self._network_config = network_config - self._node_name = name - # TODO - Load the network configuration in simulaqron! - pass - - -class SimulaQronClassicalClient(SimulaQronProtocol): - def __init__(self, network_config: str | Path, name: str): +class SimulaQronClassicalClient: + def __init__(self, sockets_config: SocketsConfig): """ Classical client used to send classical messages to remote nodes. The given node name must exist on the given network configuration. - :param network_config: The path of the Network configuration. - :type network_config: str | Path - :param name: The name of the node to connect to. The name *must* exist in the network - configuration file. - :type name: str + :param sockets_config: The sockets configuration for the whole network. + :type sockets_config: SocketsConfig """ - super().__init__(network_config, name) + self._sockets_config = sockets_config - def connect_to(self, node_name: str) -> None: + async def _run_client(self, hostname: str, port: int, callback: Callable[[StreamReader, StreamWriter], Awaitable[None]]): + reader, writer = await asyncio.open_connection(hostname, port) + await callback(reader, writer) + + + def run_client(self, node_name: str, callback: Callable[[StreamReader, StreamWriter], Awaitable[None]]) -> None: """ - Connects to the node with the given name. + Connects to the node with the given name. Once the connection has been established, + the given callback will be executed to start the interaction with the server. + The given function must have the following signature:: + + async def connected_handler(reader: StreamReader, writer: StreamWriter): + # Send a message to the server + writer.write("Hello world!".encode("utf-8")) + # Afterwards, you might want to receive an answer + message = await reader.read(255) + print(message.decode("utf-8")) + + After calling this function, the :param node_name: The name of the node to connect to. The name *must* exist in the configuration file given when constructing this client. :type node_name: str + :param callback: The function to be called when the connection is established. + :type callback: Callable[[StreamReader, StreamWriter], Awaitable[None]] """ - pass + if node_name not in self._sockets_config.hostDict: + raise RuntimeError(f"The node with name '{node_name}' is not on the network configuration.") + socket_config = self._sockets_config.hostDict[node_name] + asyncio.run(self._run_client(socket_config.hostname, socket_config.port, callback)) def send_message(self, message: str) -> None: """ @@ -90,36 +100,46 @@ def send_message(self, message: str) -> None: :param message: The message to send. :type message: str """ + pass -class SimulaQronClassicalServer(SimulaQronProtocol): - def __init__(self, network_config: str | Path, name: str, simulaqron_connection: Optional[SimulaQronConnection] = None): - super().__init__(network_config, name) - self._message_handlers: List[Callable[[SimulaQronState, str], ServingStatus]] = [] - self._connection = simulaqron_connection - self._message_handlers: List[Callable[[SimulaQronState, str, NetQASMConnection], ServingStatus]] = [] - # TODO - Define what else to do in the constructor +class SimulaQronClassicalServer: + def __init__(self, sockets_config: SocketsConfig, name: str): + self._node_name = name + self._sockets_data = sockets_config.hostDict[self._node_name] + self._connection_handler: Optional[Callable[[StreamReader, StreamWriter], Awaitable[None]]] = None - def register_message_handler(self, handler: Callable[[SimulaQronState, str], ServingStatus]) -> None: + def register_client_handler(self, handler: Callable[[StreamReader, StreamWriter], Awaitable[None]]) -> None: """ - Registers the given function as a message handler. The given function must have the following signature:: + Registers the given function as a client handler. The given function must have the following signature:: - def handler(state: SimulaQronState, message: str, connection: NetQASMConnection) -> ServingStatus: - return ServingStatus.CONTINUE + async def handler(reader: StreamReader, writer: StreamWriter): + reader = await reader.read(255) + ... + # Handle a new connection here + # E.g. send a response to the client + writer.write("answer".encode("utf-8")) - The passed function will be called once a message arrives form the remote. - The function must return a value to signal the server loop to keep handling or not. - Any other value that must be returned to the remote, must be passed using the ``add_return_values`` method - from the ``SimulaQronState`` object passed to the handler. + The passed function will be called once a new client connects. - :param handler: The function to be used as a handler. - :type handler: Callable[[SimulaQronState, str], ServingStatus] + :param handler: The function to be used as a handler. This *must* be a python coroutine + (python "async" function). + :type handler: Callable[[StreamReader, StreamWriter], Awaitable[None]] """ - self._message_handlers.append(handler) + self._connection_handler = handler + + async def _build_server(self): + if self._connection_handler is None: + print("No connection handler - Did you forget to register it?") + return + server = await asyncio.start_server(self._connection_handler, self._sockets_data.hostname, self._sockets_data.port) + print(f"BOB INFO: === {self._node_name} Server ===") + print(f"BOB DEBUG: Listening on {self._sockets_data.hostname}:{self._sockets_data.port}") + async with server: + await server.serve_forever() def start_serving(self) -> None: """ Starts the serving the clients using the registered handlers. """ - # TODO - Implement - pass + asyncio.run(self._build_server()) From 844acb6931a946653131aa2dc436545caaf59f62 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 4 Feb 2026 18:23:31 +0100 Subject: [PATCH 214/277] Update documentation on the new SDK for simulaqron. --- simulaqron/netqasm_backend/executioner.py | 2 +- simulaqron/sdk/protocol.py | 107 +++++++--------------- 2 files changed, 33 insertions(+), 76 deletions(-) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index eba9658e..6b57930e 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -244,7 +244,7 @@ def _get_axis(cls, instr: _VanillaRotInstr): @inlineCallbacks def apply_rotation(self, axis: Tuple[int, int, int], angle: float, qubit_id: int): """ - Executes a rotation of ``angle``radians around the ``axis`` axis of the specified + Executes a rotation of ``angle`` radians around the ``axis`` axis of the specified qubit ID. :param axis: The axis to rotate around. This axis is expected to be a tuple of diff --git a/simulaqron/sdk/protocol.py b/simulaqron/sdk/protocol.py index 19cb0c7d..4bff9456 100644 --- a/simulaqron/sdk/protocol.py +++ b/simulaqron/sdk/protocol.py @@ -1,61 +1,15 @@ import asyncio from asyncio import StreamWriter, StreamReader -from enum import IntEnum -from typing import Any, Awaitable, Optional, Callable +from typing import Awaitable, Optional, Callable from simulaqron.general.host_config import SocketsConfig -class ServingStatus(IntEnum): - CONTINUE = 0 - STOP = 1 - - -class SimulaQronState: - # State of the SimulaQron simulator - def __init__(self): - """ - Keeps the state of the quantum memories across multiple invocations of the message handler. - A handler can use this object to make a quantum memory - """ - # TODO - Implement this constructor! - pass - - def add_return_values(self, *ret_val: Any) -> None: - """ - Adds a return value to the message handler. The returned value will be sent back to - the other endpoint of the connection. - - :param ret_val: The values to return. - :type ret_val: Any - """ - # TODO - Implement this! - pass - - def continue_serving(self) -> ServingStatus: - """ - Creates a "continue" value used to signal the server to keep waiting for new messages. - - :return: The "continue" value of the ``ServingStatus`` class. - :rtype: ServingStatus - """ - return ServingStatus.CONTINUE - - def stop_serving(self) -> ServingStatus: - """ - Creates a "stop" value used to signal the server to stop serving new messages. - - :return: The "stop" value of the ``ServingStatus`` class. - :rtype: ServingStatus - """ - return ServingStatus.STOP - - class SimulaQronClassicalClient: def __init__(self, sockets_config: SocketsConfig): """ - Classical client used to send classical messages to remote nodes. The given node name - must exist on the given network configuration. + Classical client used to send classical messages to remote nodes. The given socket configs + object contains the specification of the available nodes on the network to connect to. :param sockets_config: The sockets configuration for the whole network. :type sockets_config: SocketsConfig @@ -63,29 +17,39 @@ def __init__(self, sockets_config: SocketsConfig): self._sockets_config = sockets_config async def _run_client(self, hostname: str, port: int, callback: Callable[[StreamReader, StreamWriter], Awaitable[None]]): + """ + Python coroutine that opens the connection and runs the function provided by the user. + """ reader, writer = await asyncio.open_connection(hostname, port) await callback(reader, writer) + writer.close() def run_client(self, node_name: str, callback: Callable[[StreamReader, StreamWriter], Awaitable[None]]) -> None: """ - Connects to the node with the given name. Once the connection has been established, - the given callback will be executed to start the interaction with the server. - The given function must have the following signature:: + Runs a function implementing a client that connects to the node with the given name. + Once the connection has been established, the given callback will be executed to start + the interaction with the server. + The given client function must have the following signature:: - async def connected_handler(reader: StreamReader, writer: StreamWriter): - # Send a message to the server - writer.write("Hello world!".encode("utf-8")) - # Afterwards, you might want to receive an answer - message = await reader.read(255) - print(message.decode("utf-8")) + async def connected_handler(reader: StreamReader, writer: StreamWriter): + # Send a message to the server + writer.write("Hello world!".encode("utf-8")) + # Afterwards, you might want to receive an answer + message = await reader.read(255) + print(message.decode("utf-8")) - After calling this function, the + Where ``reader`` and ``writer`` objects are the streams used to read and write + messages to/from the server respectively. + Once the execution of the given function, the client will close the connection to + the server. :param node_name: The name of the node to connect to. The name *must* exist in the configuration file given when constructing this client. :type node_name: str - :param callback: The function to be called when the connection is established. + :param callback: The function to be called when the connection is established. This function + implements the logic for interacting with the server. The passed function + *must* be a python "async" function. :type callback: Callable[[StreamReader, StreamWriter], Awaitable[None]] """ if node_name not in self._sockets_config.hostDict: @@ -93,15 +57,6 @@ async def connected_handler(reader: StreamReader, writer: StreamWriter): socket_config = self._sockets_config.hostDict[node_name] asyncio.run(self._run_client(socket_config.hostname, socket_config.port, callback)) - def send_message(self, message: str) -> None: - """ - Sends a message to the remote endpoint of the connection. - - :param message: The message to send. - :type message: str - """ - pass - class SimulaQronClassicalServer: def __init__(self, sockets_config: SocketsConfig, name: str): @@ -113,13 +68,15 @@ def register_client_handler(self, handler: Callable[[StreamReader, StreamWriter] """ Registers the given function as a client handler. The given function must have the following signature:: - async def handler(reader: StreamReader, writer: StreamWriter): - reader = await reader.read(255) - ... - # Handle a new connection here - # E.g. send a response to the client - writer.write("answer".encode("utf-8")) + async def handler(reader: StreamReader, writer: StreamWriter): + reader = await reader.read(255) + ... + # Handle a new connection here + # E.g. send a response to the client + writer.write("answer".encode("utf-8")) + Where ``reader`` and ``writer`` objects are the streams used to read and write + messages to/from the client respectively. The passed function will be called once a new client connects. :param handler: The function to be used as a handler. This *must* be a python coroutine From 379a126e589c4a443fcac5c7fd92abaf24a064bd Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 4 Feb 2026 19:59:09 +0100 Subject: [PATCH 215/277] Add documentation for the simple client-server example using the new SDK --- examples/distributed/teleport/run.sh | 3 +- .../new-sdk/classical-client-server/README.md | 76 +++++++++++++++++++ ...xample_client.py => example_client_bob.py} | 5 +- ...mple_server.py => example_server_alice.py} | 4 +- .../new-sdk/classical-client-server/run.sh | 15 ++++ 5 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 examples/new-sdk/classical-client-server/README.md rename examples/new-sdk/classical-client-server/{example_client.py => example_client_bob.py} (87%) rename examples/new-sdk/classical-client-server/{example_server.py => example_server_alice.py} (87%) create mode 100755 examples/new-sdk/classical-client-server/run.sh diff --git a/examples/distributed/teleport/run.sh b/examples/distributed/teleport/run.sh index 4362ae53..31517233 100755 --- a/examples/distributed/teleport/run.sh +++ b/examples/distributed/teleport/run.sh @@ -2,7 +2,8 @@ # Check if SimulaQron is already running if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then - simulaqron start --nodes=Alice,Bob --network-config-file ./networkConfig.json + # If not, start simulaqron backend for both nodes + simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json fi python3 teleport-bob.py & diff --git a/examples/new-sdk/classical-client-server/README.md b/examples/new-sdk/classical-client-server/README.md new file mode 100644 index 00000000..3a209c6b --- /dev/null +++ b/examples/new-sdk/classical-client-server/README.md @@ -0,0 +1,76 @@ +# Classical Client-Server example + +Very simple example to demonstrate how to write client-server applications which exchange +classical messages. + + +# How to run + +There are two ways to run this example: + + +## On a single machine + +The simplest way to run this example is on a single machine. To do this, you can use the +provided bash script: + +```shell +./run.sh +``` + +Alternatively, you can manually start the client and teh server in two different terminals. +First, start the server on one terminal + +```shell +python example_server_alice.py +``` + +which start the server and keeps listening for new incoming connections. Then start the client: + +```shell +python example_client_bob.py +``` + +which will connect to the server and send a message that will be sent back by the server. + + +## On different machines + +To run this example on different machines, it is necessary that both machines can reach each +other via a network (or the internet). Additionally, you need to know the IP addresses of both +machines. + +Assuming that the server (alice) will run on a machine with IP `192.168.0.1` and the client (bob) +will run on the machine with IP `192.168.0.2`, modify the `simulaqron_network.json` file *on both* +the server and the client to look like this: + +```json +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["192.168.0.1", 8821], + "qnodeos_socket": ["192.168.0.1", 8822], + "vnode_socket": ["192.168.0.1", 8823] + } + }, + { + "Bob": { + "app_socket": ["192.168.0.2", 9831], + "qnodeos_socket": ["192.168.0.2", 9832], + "vnode_socket": ["192.168.0.2", 9833] + } + } + ], + "topology": null + } +] +``` + +Note that the `localhost` entries from Alice were changed to `192.168.0.1`. Similarly, the +`localhost` entries from Bob were changed to `192.168.0.2`. + +After these modifications, start the server and client in their respective terminals as described +before. diff --git a/examples/new-sdk/classical-client-server/example_client.py b/examples/new-sdk/classical-client-server/example_client_bob.py similarity index 87% rename from examples/new-sdk/classical-client-server/example_client.py rename to examples/new-sdk/classical-client-server/example_client_bob.py index eefc29b1..ec31a26b 100644 --- a/examples/new-sdk/classical-client-server/example_client.py +++ b/examples/new-sdk/classical-client-server/example_client_bob.py @@ -9,12 +9,13 @@ async def client_code(reader: StreamReader, writer: StreamWriter): data = "Hello World!".encode("utf-8") writer.write(data) - print(f"Sent message '{data.decode("utf-8")}'") + print(f"Client sent message '{data.decode("utf-8")}'") result = await reader.read(255) - print(f"Received message: '{result.decode("utf-8")}'") + print(f"Client received message: '{result.decode("utf-8")}'") writer.close() if __name__ == "__main__": + # This is "Bob" - the client # Load the file network configuration file network_config_file = Path("simulaqron_network.json") network_config.read_from_file(network_config_file) diff --git a/examples/new-sdk/classical-client-server/example_server.py b/examples/new-sdk/classical-client-server/example_server_alice.py similarity index 87% rename from examples/new-sdk/classical-client-server/example_server.py rename to examples/new-sdk/classical-client-server/example_server_alice.py index dfae7317..e458cd01 100644 --- a/examples/new-sdk/classical-client-server/example_server.py +++ b/examples/new-sdk/classical-client-server/example_server_alice.py @@ -8,11 +8,13 @@ async def connection_handler(reader: StreamReader, writer: StreamWriter): result = await reader.read(255) - print(f"Received message: '{result.decode("utf-8")}'") + print(f"Server received message: '{result.decode("utf-8")}'") writer.write(result) + print(f"Server send message: '{result.decode("utf-8")}'") writer.close() if __name__ == "__main__": + # This is "Alice" - the server # Load the file network configuration file network_config_file = Path("simulaqron_network.json") network_config.read_from_file(network_config_file) diff --git a/examples/new-sdk/classical-client-server/run.sh b/examples/new-sdk/classical-client-server/run.sh new file mode 100755 index 00000000..9c28001a --- /dev/null +++ b/examples/new-sdk/classical-client-server/run.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Running SimulaQron backend is not needed for this example. +## Check if SimulaQron is already running +#if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then +# # If not, start simulaqron backend for both nodes +# simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json +#fi + +# Run the server +python3 example_server_alice.py & +# Wait 1 second for the server to fully start +sleep 1 +# Run the client +python3 example_client_bob.py & From 39d4d577be31ba0f964046f662b23783e5e2b9b2 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 4 Feb 2026 22:38:16 +0100 Subject: [PATCH 216/277] Adapt the "distributed" version of the teleport to use the new SDK to run both in a single machine, and distributed. --- examples/distributed/teleport/README.txt | 63 ---------- .../distributed/teleport/teleport-alice.py | 26 ---- examples/distributed/teleport/teleport-bob.py | 18 --- examples/new-sdk/teleport/README.md | 113 ++++++++++++++++++ .../{distributed => new-sdk}/teleport/run.sh | 8 +- .../teleport/simulaqron_network.json | 0 examples/new-sdk/teleport/teleport-alice.py | 52 ++++++++ examples/new-sdk/teleport/teleport-bob.py | 45 +++++++ .../teleport/terminate.sh | 0 simulaqron/sdk/protocol.py | 23 ++-- 10 files changed, 227 insertions(+), 121 deletions(-) delete mode 100644 examples/distributed/teleport/README.txt delete mode 100644 examples/distributed/teleport/teleport-alice.py delete mode 100644 examples/distributed/teleport/teleport-bob.py create mode 100644 examples/new-sdk/teleport/README.md rename examples/{distributed => new-sdk}/teleport/run.sh (80%) rename examples/{distributed => new-sdk}/teleport/simulaqron_network.json (100%) create mode 100644 examples/new-sdk/teleport/teleport-alice.py create mode 100644 examples/new-sdk/teleport/teleport-bob.py rename examples/{distributed => new-sdk}/teleport/terminate.sh (100%) diff --git a/examples/distributed/teleport/README.txt b/examples/distributed/teleport/README.txt deleted file mode 100644 index b765bcb7..00000000 --- a/examples/distributed/teleport/README.txt +++ /dev/null @@ -1,63 +0,0 @@ - -Distributed version of the teleport example, using simple NetQASM code both for Alice and Bob. - -How to run this example: - -1. First of all, make sure you are not already running existing simulaqron programs. You can run - -simulaqron stop -sh terminate.sh - -which should get rid of all things running for the teleport example itself. If you want to be a bit -more radical and confident you are not running other things to preserve you can run: - -simulaqron stop -pkill -9 python - -This will kill ALL python processes run by you so beware. - -If you have a debugging enabled, you may also wish to wipe old log files by running - -rm /tmp/simulaqron* - - -Now you should have a clear slate! - -2. Now you can run this example: - -First, we want to start the simulaqron virtual node backend and the NetQASM frontend your apps will their NetQASM subroutines to. - - -** Single Machine -If you are running everything on the same machine, first we start the simulaqron backend by typing - -simulaqron start --node Alice,Bob - -This needs to be done only once. Now type - -sh run.sh - -There is no need to restart the simulaqron backend again if you want to re-run your example. - -** Multiple machines - -If you are starting on two different machines run: - -simulaqron start --node Alice -simulaqron start --node Bob - -on the machines you will use as Alice and Bob respectively. Again this needs to be run only once. - -Now you can run: -on Bob: -python teleport-bob.py - -on Alice: -python teleport-alice.py - -The code assumes you start Bob before starting Alice. Using your knowledge of network programming -from our ping pong example - do you have an idea to make this more robust? - - - - diff --git a/examples/distributed/teleport/teleport-alice.py b/examples/distributed/teleport/teleport-alice.py deleted file mode 100644 index d0bf11c0..00000000 --- a/examples/distributed/teleport/teleport-alice.py +++ /dev/null @@ -1,26 +0,0 @@ -from netqasm.runtime.settings import set_simulator -set_simulator("simulaqron") - -from netqasm.sdk.external import NetQASMConnection # noqa: E402 -from netqasm.sdk import Qubit, EPRSocket # noqa: E402 - - -def run_alice(): - epr_socket = EPRSocket("Bob") - with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: - # Create a qubit - q = Qubit(alice) - q.H() - # Create entanglement - epr = epr_socket.create_keep()[0] - # Teleport - q.cnot(epr) - q.H() - m1 = q.measure() - m2 = epr.measure() - return int(m1), int(m2) - - -if __name__ == "__main__": - results = run_alice() - print(f"Alice measurements: m1={results[0]}, m2={results[1]}") diff --git a/examples/distributed/teleport/teleport-bob.py b/examples/distributed/teleport/teleport-bob.py deleted file mode 100644 index 3705e72e..00000000 --- a/examples/distributed/teleport/teleport-bob.py +++ /dev/null @@ -1,18 +0,0 @@ -from netqasm.runtime.settings import set_simulator -set_simulator("simulaqron") - -from netqasm.sdk.external import NetQASMConnection # noqa: E402 -from netqasm.sdk import EPRSocket # noqa: E402 - - -def run_bob(): - epr_socket = EPRSocket("Alice") - with NetQASMConnection("Bob", epr_sockets=[epr_socket]): - entangled_qubit = epr_socket.recv_keep()[0] - meas = entangled_qubit.measure() - return int(meas) - - -if __name__ == "__main__": - result = run_bob() - print(f"Bob measurement: {result}") diff --git a/examples/new-sdk/teleport/README.md b/examples/new-sdk/teleport/README.md new file mode 100644 index 00000000..e90e6c21 --- /dev/null +++ b/examples/new-sdk/teleport/README.md @@ -0,0 +1,113 @@ +# Quantum teleport example +Quantum teleport example, using simple NetQASM code both for Alice and Bob. + + +# How to run + +First of all, make sure you are not already running existing simulaqron programs. + + +## Initial cleanup + +You can run + +```shell +simulaqron stop +sh terminate.sh +``` + +which should get rid of all things running for the teleport example itself. If you want to be a bit +more radical and confident you are not running other things to preserve you can run: + +```shell +simulaqron stop +pkill -9 python +``` + +This will kill ALL python processes run by you so beware. If you have a debugging enabled, you may also +wish to wipe old log files by running: + +```shell +rm /tmp/simulaqron* +``` + +This should leave you a clear slate. We can now start the application. + + +## On a single machine +If you are running everything on the same machine, first we start the simulaqron backend by typing + +```shell +simulaqron start --node Alice,Bob +``` + +This needs to be done only once. Now type + +```shell +sh run.sh +``` + +WARNING: It seems restarting the simulaqron backend is needed! - There is no need to restart the simulaqron backend again if you want to re-run your example. + + +## On multiple machines + +If you are starting on two different machines run, you first need to update the network configuration +file. To run this example on different machines, it is necessary that both machines can reach each +other via a network (or the internet). Additionally, you need to know the IP addresses of both +machines. + +Assuming that the server (alice) will run on a machine with IP `192.168.0.1` and the client (bob) +will run on the machine with IP `192.168.0.2`, modify the `simulaqron_network.json` file *on both* +the server and the client to look like this: + +```json +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["192.168.0.1", 8821], + "qnodeos_socket": ["192.168.0.1", 8822], + "vnode_socket": ["192.168.0.1", 8823] + } + }, + { + "Bob": { + "app_socket": ["192.168.0.2", 9831], + "qnodeos_socket": ["192.168.0.2", 9832], + "vnode_socket": ["192.168.0.2", 9833] + } + } + ], + "topology": null + } +] +``` + +Note that the `localhost` entries from Alice were changed to `192.168.0.1`. Similarly, the +`localhost` entries from Bob were changed to `192.168.0.2`. + +After these modifications, yu can start the simulaqron backends by invoking: + +```shell +simulaqron start --node Alice +simulaqron start --node Bob +``` + +on the machines you will use as Alice and Bob respectively. Again this needs to be run only once. +Now you can run: + +* on Bob: +```shell +python teleport-bob.py +``` + +* on Alice: +```shell +python teleport-alice.py +``` + +The code assumes you start Bob before starting Alice. Using your knowledge of network programming +from our ping pong example - do you have an idea to make this more robust? diff --git a/examples/distributed/teleport/run.sh b/examples/new-sdk/teleport/run.sh similarity index 80% rename from examples/distributed/teleport/run.sh rename to examples/new-sdk/teleport/run.sh index 31517233..88ee01fc 100755 --- a/examples/distributed/teleport/run.sh +++ b/examples/new-sdk/teleport/run.sh @@ -7,9 +7,9 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then fi python3 teleport-bob.py & +SERVERPID=$! +sleep 1 python3 teleport-alice.py - - - - +# Kill the server after it was used +kill -9 $SERVERPID diff --git a/examples/distributed/teleport/simulaqron_network.json b/examples/new-sdk/teleport/simulaqron_network.json similarity index 100% rename from examples/distributed/teleport/simulaqron_network.json rename to examples/new-sdk/teleport/simulaqron_network.json diff --git a/examples/new-sdk/teleport/teleport-alice.py b/examples/new-sdk/teleport/teleport-alice.py new file mode 100644 index 00000000..0f9c26f9 --- /dev/null +++ b/examples/new-sdk/teleport/teleport-alice.py @@ -0,0 +1,52 @@ +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings import network_config +from simulaqron.settings.network_config import NodeConfigType + +# This is recipe to use NetQASM with simulaqron backend. +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +# Importing NetQASM connection, Qubit and EPR socket must be *after* +# setting the simulator for NetQASM +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import Qubit, EPRSocket # noqa: E402 + + +async def run_alice(reader: StreamReader, writer: StreamWriter): + epr_socket = EPRSocket("Bob") + with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: + # Create a qubit + q = Qubit(alice) + q.H() + # Create entanglement + epr = epr_socket.create_keep()[0] + # Teleport + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + # Any value that comes from NetQASm *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + m1_val = int(m1) + m2_val = int(m2) + message = f"{m1_val}:{m2_val}" + writer.write(message.encode("utf-8")) + return m1_val, m2_val + + +if __name__ == "__main__": + # Load the file network configuration file + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Get the socket configuration for the sockets used for the application layer + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + + # Create the client + client = SimulaQronClassicalClient(sockets_config) + results = client.run_client("Bob", run_alice) + print(f"Alice measurements: m1={results[0]}, m2={results[1]}") diff --git a/examples/new-sdk/teleport/teleport-bob.py b/examples/new-sdk/teleport/teleport-bob.py new file mode 100644 index 00000000..5afaab90 --- /dev/null +++ b/examples/new-sdk/teleport/teleport-bob.py @@ -0,0 +1,45 @@ +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalServer +from simulaqron.settings import network_config +from simulaqron.settings.network_config import NodeConfigType + +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +async def run_bob(reader: StreamReader, writer: StreamWriter): + # We wait for the classical message first + corrections_bytes = await reader.read(255) + corrections = corrections_bytes.decode("utf-8").split(":") + epr_socket = EPRSocket("Alice") + with NetQASMConnection("Bob", epr_sockets=[epr_socket]): + entangled_qubit = epr_socket.recv_keep()[0] + + if int(corrections[0]) == 1: + entangled_qubit.X() + if int(corrections[1]) == 1: + entangled_qubit.Z() + meas = entangled_qubit.measure() + # Any value that comes from NetQASm *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + meas_val = int(meas) + print(f"Bob measurement: {meas_val}") + +if __name__ == "__main__": + # Load the file network configuration file + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Get the socket configuration for the sockets used for the application layer + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + + # Create the client + server = SimulaQronClassicalServer(sockets_config, "Bob") + server.register_client_handler(run_bob) + server.start_serving() diff --git a/examples/distributed/teleport/terminate.sh b/examples/new-sdk/teleport/terminate.sh similarity index 100% rename from examples/distributed/teleport/terminate.sh rename to examples/new-sdk/teleport/terminate.sh diff --git a/simulaqron/sdk/protocol.py b/simulaqron/sdk/protocol.py index 4bff9456..fdfabe2c 100644 --- a/simulaqron/sdk/protocol.py +++ b/simulaqron/sdk/protocol.py @@ -1,9 +1,11 @@ import asyncio from asyncio import StreamWriter, StreamReader -from typing import Awaitable, Optional, Callable +from typing import Awaitable, Optional, Callable, Coroutine, Any, TypeVar from simulaqron.general.host_config import SocketsConfig +_T = TypeVar("_T") + class SimulaQronClassicalClient: def __init__(self, sockets_config: SocketsConfig): @@ -16,16 +18,17 @@ def __init__(self, sockets_config: SocketsConfig): """ self._sockets_config = sockets_config - async def _run_client(self, hostname: str, port: int, callback: Callable[[StreamReader, StreamWriter], Awaitable[None]]): + async def _run_client(self, hostname: str, port: int, callback: Coroutine[Any, Any, _T]) -> _T: """ Python coroutine that opens the connection and runs the function provided by the user. """ reader, writer = await asyncio.open_connection(hostname, port) - await callback(reader, writer) + result = await callback(reader, writer) writer.close() + return result - def run_client(self, node_name: str, callback: Callable[[StreamReader, StreamWriter], Awaitable[None]]) -> None: + def run_client(self, server_name: str, callback: Coroutine[Any, Any, _T]) -> _T: """ Runs a function implementing a client that connects to the node with the given name. Once the connection has been established, the given callback will be executed to start @@ -44,18 +47,18 @@ async def connected_handler(reader: StreamReader, writer: StreamWriter): Once the execution of the given function, the client will close the connection to the server. - :param node_name: The name of the node to connect to. The name *must* exist in the + :param server_name: The name of the server to connect to. The name *must* exist in the configuration file given when constructing this client. - :type node_name: str + :type server_name: str :param callback: The function to be called when the connection is established. This function implements the logic for interacting with the server. The passed function *must* be a python "async" function. :type callback: Callable[[StreamReader, StreamWriter], Awaitable[None]] """ - if node_name not in self._sockets_config.hostDict: - raise RuntimeError(f"The node with name '{node_name}' is not on the network configuration.") - socket_config = self._sockets_config.hostDict[node_name] - asyncio.run(self._run_client(socket_config.hostname, socket_config.port, callback)) + if server_name not in self._sockets_config.hostDict: + raise RuntimeError(f"The node with name '{server_name}' is not on the network configuration.") + socket_config = self._sockets_config.hostDict[server_name] + return asyncio.run(self._run_client(socket_config.hostname, socket_config.port, callback)) class SimulaQronClassicalServer: From 93089c3a85e2886b89ff9e195dde68d1cb7f8a97 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 4 Feb 2026 22:52:12 +0100 Subject: [PATCH 217/277] Linter changes --- .../classical-client-server/example_client_bob.py | 4 +++- .../classical-client-server/example_server_alice.py | 4 +++- examples/new-sdk/teleport/teleport-alice.py | 2 +- examples/new-sdk/teleport/teleport-bob.py | 1 + simulaqron/sdk/protocol.py | 9 ++++++--- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/new-sdk/classical-client-server/example_client_bob.py b/examples/new-sdk/classical-client-server/example_client_bob.py index ec31a26b..cc8f832b 100644 --- a/examples/new-sdk/classical-client-server/example_client_bob.py +++ b/examples/new-sdk/classical-client-server/example_client_bob.py @@ -6,6 +6,7 @@ from simulaqron.sdk.protocol import SimulaQronClassicalClient from simulaqron.settings.network_config import NodeConfigType + async def client_code(reader: StreamReader, writer: StreamWriter): data = "Hello World!".encode("utf-8") writer.write(data) @@ -14,6 +15,7 @@ async def client_code(reader: StreamReader, writer: StreamWriter): print(f"Client received message: '{result.decode("utf-8")}'") writer.close() + if __name__ == "__main__": # This is "Bob" - the client # Load the file network configuration file @@ -28,4 +30,4 @@ async def client_code(reader: StreamReader, writer: StreamWriter): # Run the client. The given function will be called once the connection with #m the given server was established - client.run_client("Alice", client_code) \ No newline at end of file + client.run_client("Alice", client_code) diff --git a/examples/new-sdk/classical-client-server/example_server_alice.py b/examples/new-sdk/classical-client-server/example_server_alice.py index e458cd01..9b9cf751 100644 --- a/examples/new-sdk/classical-client-server/example_server_alice.py +++ b/examples/new-sdk/classical-client-server/example_server_alice.py @@ -6,6 +6,7 @@ from simulaqron.sdk.protocol import SimulaQronClassicalServer from simulaqron.settings.network_config import NodeConfigType + async def connection_handler(reader: StreamReader, writer: StreamWriter): result = await reader.read(255) print(f"Server received message: '{result.decode("utf-8")}'") @@ -13,6 +14,7 @@ async def connection_handler(reader: StreamReader, writer: StreamWriter): print(f"Server send message: '{result.decode("utf-8")}'") writer.close() + if __name__ == "__main__": # This is "Alice" - the server # Load the file network configuration file @@ -30,4 +32,4 @@ async def connection_handler(reader: StreamReader, writer: StreamWriter): server.register_client_handler(connection_handler) # Start serving the clients - server.start_serving() \ No newline at end of file + server.start_serving() diff --git a/examples/new-sdk/teleport/teleport-alice.py b/examples/new-sdk/teleport/teleport-alice.py index 0f9c26f9..061fdaf9 100644 --- a/examples/new-sdk/teleport/teleport-alice.py +++ b/examples/new-sdk/teleport/teleport-alice.py @@ -33,7 +33,7 @@ async def run_alice(reader: StreamReader, writer: StreamWriter): # *after* the connection is closed (or after flushing the connection, untested) m1_val = int(m1) m2_val = int(m2) - message = f"{m1_val}:{m2_val}" + message = f"{m1_val}:{m2_val}" # noqa: E231 writer.write(message.encode("utf-8")) return m1_val, m2_val diff --git a/examples/new-sdk/teleport/teleport-bob.py b/examples/new-sdk/teleport/teleport-bob.py index 5afaab90..60d68d36 100644 --- a/examples/new-sdk/teleport/teleport-bob.py +++ b/examples/new-sdk/teleport/teleport-bob.py @@ -31,6 +31,7 @@ async def run_bob(reader: StreamReader, writer: StreamWriter): meas_val = int(meas) print(f"Bob measurement: {meas_val}") + if __name__ == "__main__": # Load the file network configuration file network_config_file = Path("simulaqron_network.json") diff --git a/simulaqron/sdk/protocol.py b/simulaqron/sdk/protocol.py index fdfabe2c..ad98f42c 100644 --- a/simulaqron/sdk/protocol.py +++ b/simulaqron/sdk/protocol.py @@ -27,7 +27,6 @@ async def _run_client(self, hostname: str, port: int, callback: Coroutine[Any, A writer.close() return result - def run_client(self, server_name: str, callback: Coroutine[Any, Any, _T]) -> _T: """ Runs a function implementing a client that connects to the node with the given name. @@ -92,9 +91,13 @@ async def _build_server(self): if self._connection_handler is None: print("No connection handler - Did you forget to register it?") return - server = await asyncio.start_server(self._connection_handler, self._sockets_data.hostname, self._sockets_data.port) + server = await asyncio.start_server( + self._connection_handler, + self._sockets_data.hostname, + self._sockets_data.port + ) print(f"BOB INFO: === {self._node_name} Server ===") - print(f"BOB DEBUG: Listening on {self._sockets_data.hostname}:{self._sockets_data.port}") + print(f"BOB DEBUG: Listening on {self._sockets_data.hostname}:{self._sockets_data.port}") # noqa: E231 async with server: await server.serve_forever() From c2ad9ff729eafa0e67e92c72773e0330541e55c6 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 5 Feb 2026 16:13:53 +0100 Subject: [PATCH 218/277] Move other native mode examples to the new network configuration format --- examples/nativeMode/WARNINIG | 5 --- examples/nativeMode/corrRNG/aliceTest.py | 1 - examples/nativeMode/corrRNG/doNew.sh | 11 +++--- examples/nativeMode/corrRNG/run.sh | 8 +++-- examples/nativeMode/corrRNG/terminate.sh | 8 +++-- examples/nativeMode/extendGHZ/aliceTest.py | 18 ++++------ examples/nativeMode/extendGHZ/bobTest.py | 20 +++++------ examples/nativeMode/extendGHZ/charlieTest.py | 20 +++++------ .../nativeMode/extendGHZ/classicalNet.cfg | 13 ------- .../nativeMode/extendGHZ/classicalNet.json | 29 +++++++++++++++ examples/nativeMode/extendGHZ/doNew.sh | 11 +++--- examples/nativeMode/extendGHZ/run.sh | 11 +++++- examples/nativeMode/extendGHZ/terminate.sh | 16 +++++++++ examples/nativeMode/graphState/aliceTest.py | 19 ++++------ examples/nativeMode/graphState/bobTest.py | 19 ++++------ examples/nativeMode/graphState/charlieTest.py | 19 ++++------ .../nativeMode/graphState/classicalNet.cfg | 13 ------- .../nativeMode/graphState/classicalNet.json | 36 +++++++++++++++++++ examples/nativeMode/graphState/davidTest.py | 19 ++++------ examples/nativeMode/graphState/doNew.sh | 11 +++--- examples/nativeMode/graphState/run.sh | 14 +++++++- examples/nativeMode/graphState/terminate.sh | 16 +++++++++ examples/nativeMode/teleport/aliceTest.py | 15 ++------ examples/nativeMode/teleport/bobTest.py | 15 ++------ examples/nativeMode/teleport/doNew.sh | 11 +++--- examples/nativeMode/teleport/run.sh | 11 +++++- examples/nativeMode/teleport/terminate.sh | 16 +++++++++ 27 files changed, 232 insertions(+), 173 deletions(-) delete mode 100644 examples/nativeMode/WARNINIG delete mode 100644 examples/nativeMode/extendGHZ/classicalNet.cfg create mode 100644 examples/nativeMode/extendGHZ/classicalNet.json create mode 100755 examples/nativeMode/extendGHZ/terminate.sh delete mode 100644 examples/nativeMode/graphState/classicalNet.cfg create mode 100644 examples/nativeMode/graphState/classicalNet.json mode change 100644 => 100755 examples/nativeMode/graphState/doNew.sh mode change 100644 => 100755 examples/nativeMode/graphState/run.sh create mode 100755 examples/nativeMode/graphState/terminate.sh create mode 100755 examples/nativeMode/teleport/terminate.sh diff --git a/examples/nativeMode/WARNINIG b/examples/nativeMode/WARNINIG deleted file mode 100644 index 91745d8b..00000000 --- a/examples/nativeMode/WARNINIG +++ /dev/null @@ -1,5 +0,0 @@ -Writing SimulaQron applications using the "native" interface is deprecated. - -The examples in this folder are here just for completeness. -If you want to create a SimulaQron application, please refer to the example -in the "netqasm" folder. diff --git a/examples/nativeMode/corrRNG/aliceTest.py b/examples/nativeMode/corrRNG/aliceTest.py index a858a4fc..a175cf58 100644 --- a/examples/nativeMode/corrRNG/aliceTest.py +++ b/examples/nativeMode/corrRNG/aliceTest.py @@ -114,7 +114,6 @@ def remote_test(self): # main # def main(): - myName = "Alice" # we are Alice # This file defines the network of virtual quantum nodes and the network used for classical communication diff --git a/examples/nativeMode/corrRNG/doNew.sh b/examples/nativeMode/corrRNG/doNew.sh index 25d9c5d9..b00dbb94 100755 --- a/examples/nativeMode/corrRNG/doNew.sh +++ b/examples/nativeMode/corrRNG/doNew.sh @@ -1,7 +1,4 @@ -#!/usr/bin/env sh -TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) -if [ "$TEST_PIDS" != "" ] -then - kill -9 $TEST_PIDS -fi -sh run.sh +#!/usr/bin/env bash + +./terminate.sh +./run.sh diff --git a/examples/nativeMode/corrRNG/run.sh b/examples/nativeMode/corrRNG/run.sh index 2d65e764..ba6a394b 100755 --- a/examples/nativeMode/corrRNG/run.sh +++ b/examples/nativeMode/corrRNG/run.sh @@ -1,8 +1,12 @@ -#!/bin/sh +#!/usr/bin/env bash # Check if SimulaQron is already running if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then - simulaqron start --nodes=Alice,Bob --network-config-file classicalNet.json + if ! simulaqron start --nodes=Alice,Bob --network-config-file classicalNet.json + then + echo "SimulaQron could not start correctly!" + exit 1 + fi fi python3 bobTest.py & diff --git a/examples/nativeMode/corrRNG/terminate.sh b/examples/nativeMode/corrRNG/terminate.sh index 3d302022..035e30d5 100755 --- a/examples/nativeMode/corrRNG/terminate.sh +++ b/examples/nativeMode/corrRNG/terminate.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) if [ "$TEST_PIDS" != "" ] then @@ -7,6 +7,10 @@ fi # Check if SimulaQron is running if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then - cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi fi diff --git a/examples/nativeMode/extendGHZ/aliceTest.py b/examples/nativeMode/extendGHZ/aliceTest.py index 71d23628..2ff4a61d 100644 --- a/examples/nativeMode/extendGHZ/aliceTest.py +++ b/examples/nativeMode/extendGHZ/aliceTest.py @@ -27,12 +27,11 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os import logging from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config from twisted.internet.defer import inlineCallbacks from twisted.spread import pb from twisted.internet import reactor @@ -71,7 +70,7 @@ def runClientNode(qReg, virtRoot, myName, classicalNet): # Send qubit B to Bob # Instruct the virtual node to transfer the qubit remoteNum = yield virtRoot.callRemote("send_qubit", qB, "Bob") - logging.debug("LOCAL {}: Remote qubit is %d.", myName, remoteNum) + logging.debug("LOCAL %s: Remote qubit is %d.", myName, remoteNum) # Tell Bob the number of the virtual qubit so the can use it locally # and extend it to a GHZ state with Charlie @@ -120,15 +119,12 @@ def main(): # In this example, we are Alice. myName = "Alice" - # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file + # This file defines the network of virtual quantum nodes and the network used for classical communication + network_config.read_from_file("classicalNet.json") - # This file defines the nodes acting as servers in the classical communication network - classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") - - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/extendGHZ/bobTest.py b/examples/nativeMode/extendGHZ/bobTest.py index 98e176e5..434934f4 100644 --- a/examples/nativeMode/extendGHZ/bobTest.py +++ b/examples/nativeMode/extendGHZ/bobTest.py @@ -27,12 +27,11 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os import logging from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -116,7 +115,7 @@ def remote_receive_epr(self, virtualNum): # Measure our qubit outcome = yield eprB.callRemote("measure") - print("Bob's outcome was: {}".format(outcome)) + print(f"Bob's outcome was: {outcome}") ##################################################################################################### @@ -124,18 +123,15 @@ def remote_receive_epr(self, virtualNum): # main # def main(): - # In this example, we are Charlie. + # In this example, we are Bob. myName = "Bob" - # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file + # This file defines the network of virtual quantum nodes and the network used for classical communication + network_config.read_from_file("classicalNet.json") - # This file defines the nodes acting as servers in the classical communication network - classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") - - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/extendGHZ/charlieTest.py b/examples/nativeMode/extendGHZ/charlieTest.py index c6a8b679..f6b05d09 100644 --- a/examples/nativeMode/extendGHZ/charlieTest.py +++ b/examples/nativeMode/extendGHZ/charlieTest.py @@ -27,12 +27,11 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os import logging from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -102,7 +101,7 @@ def remote_receive_ghz(self, virtualNum): # Measure it outcome = yield q.callRemote("measure") - print("Charlie's outcome was: {}".format(outcome)) + print(f"Charlie's outcome was: {outcome}") ##################################################################################################### @@ -110,18 +109,17 @@ def remote_receive_ghz(self, virtualNum): # main # def main(): - # In this example, we are Bob. + # In this example, we are Charlie. myName = "Charlie" - # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file + logging.basicConfig(format='%(asctime)s:%(levelname)s:%(message)s', level=logging.DEBUG) - # This file defines the nodes acting as servers in the classical communication network - classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") + # This file defines the network of virtual quantum nodes and the network used for classical communication + network_config.read_from_file("classicalNet.json") - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/extendGHZ/classicalNet.cfg b/examples/nativeMode/extendGHZ/classicalNet.cfg deleted file mode 100644 index 27e0f071..00000000 --- a/examples/nativeMode/extendGHZ/classicalNet.cfg +++ /dev/null @@ -1,13 +0,0 @@ -# Network configuration file -# -# For each host its informal name, as well as its location in the network must -# be listed. -# -# [name], [hostname], [port number] -# - -# Alice, localhost, 8811 -Bob, localhost, 8812 -Charlie, localhost, 8813 - - diff --git a/examples/nativeMode/extendGHZ/classicalNet.json b/examples/nativeMode/extendGHZ/classicalNet.json new file mode 100644 index 00000000..fbfcbbfb --- /dev/null +++ b/examples/nativeMode/extendGHZ/classicalNet.json @@ -0,0 +1,29 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + { + "Charlie": { + "app_socket": ["localhost", 8841], + "qnodeos_socket": ["localhost", 8842], + "vnode_socket": ["localhost", 8843] + } + } + ], + "topology": null + } +] \ No newline at end of file diff --git a/examples/nativeMode/extendGHZ/doNew.sh b/examples/nativeMode/extendGHZ/doNew.sh index 25d9c5d9..b00dbb94 100755 --- a/examples/nativeMode/extendGHZ/doNew.sh +++ b/examples/nativeMode/extendGHZ/doNew.sh @@ -1,7 +1,4 @@ -#!/usr/bin/env sh -TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) -if [ "$TEST_PIDS" != "" ] -then - kill -9 $TEST_PIDS -fi -sh run.sh +#!/usr/bin/env bash + +./terminate.sh +./run.sh diff --git a/examples/nativeMode/extendGHZ/run.sh b/examples/nativeMode/extendGHZ/run.sh index 94476aa3..5c9126de 100755 --- a/examples/nativeMode/extendGHZ/run.sh +++ b/examples/nativeMode/extendGHZ/run.sh @@ -1,4 +1,13 @@ -#!/bin/sh +#!/usr/bin/env bash + +# Check if SimulaQron is already running +if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron start --nodes=Alice,Bob,Charlie --network-config-file classicalNet.json + then + echo "SimulaQron could not start correctly" + exit 1 + fi +fi python3 bobTest.py & python3 charlieTest.py & diff --git a/examples/nativeMode/extendGHZ/terminate.sh b/examples/nativeMode/extendGHZ/terminate.sh new file mode 100755 index 00000000..39747653 --- /dev/null +++ b/examples/nativeMode/extendGHZ/terminate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi +fi + diff --git a/examples/nativeMode/graphState/aliceTest.py b/examples/nativeMode/graphState/aliceTest.py index 946a8ea4..07174254 100644 --- a/examples/nativeMode/graphState/aliceTest.py +++ b/examples/nativeMode/graphState/aliceTest.py @@ -27,13 +27,11 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os import logging from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config from twisted.internet.defer import inlineCallbacks from twisted.spread import pb from twisted.internet import reactor @@ -134,18 +132,15 @@ def remote_tell_bob(self, someInfo): def main(): - # In this example, we are YOURNAME + # In this example, we are Alice myName = "Alice" - # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file - - # This file defines the nodes acting as servers in the classical communication network - classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") + # This file defines the network of virtual quantum nodes and the network used for classical communication + network_config.read_from_file("classicalNet.json") - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/graphState/bobTest.py b/examples/nativeMode/graphState/bobTest.py index 74291d3a..54c2e919 100644 --- a/examples/nativeMode/graphState/bobTest.py +++ b/examples/nativeMode/graphState/bobTest.py @@ -27,14 +27,12 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os import logging import numpy as np from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -133,18 +131,15 @@ def remote_receive_qubit(self, virtualNum): def main(): - # In this example, we are YOURNAME + # In this example, we are Bob myName = "Bob" - # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file - - # This file defines the nodes acting as servers in the classical communication network - classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") + # This file defines the network of virtual quantum nodes and the network used for classical communication + network_config.read_from_file("classicalNet.json") - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/graphState/charlieTest.py b/examples/nativeMode/graphState/charlieTest.py index a7c98794..25186550 100644 --- a/examples/nativeMode/graphState/charlieTest.py +++ b/examples/nativeMode/graphState/charlieTest.py @@ -27,14 +27,12 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os import logging import numpy as np from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -164,18 +162,15 @@ def remote_receive_qubit(self, virtualNum, sender): def main(): - # In this example, we are YOURNAME + # In this example, we are Charlie myName = "Charlie" - # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file - - # This file defines the nodes acting as servers in the classical communication network - classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") + # This file defines the network of virtual quantum nodes and the network used for classical communication + network_config.read_from_file("classicalNet.json") - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/graphState/classicalNet.cfg b/examples/nativeMode/graphState/classicalNet.cfg deleted file mode 100644 index d2560941..00000000 --- a/examples/nativeMode/graphState/classicalNet.cfg +++ /dev/null @@ -1,13 +0,0 @@ -# Network configuration file -# -# For each host its informal name, as well as its location in the network must -# be listed. -# -# [name], [hostname], [port number] -# - -# Alice, localhost, 8811 -Bob, localhost, 8812 -Charlie, localhost, 8813 -David, localhost, 8814 - diff --git a/examples/nativeMode/graphState/classicalNet.json b/examples/nativeMode/graphState/classicalNet.json new file mode 100644 index 00000000..61b05b60 --- /dev/null +++ b/examples/nativeMode/graphState/classicalNet.json @@ -0,0 +1,36 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + { + "Charlie": { + "app_socket": ["localhost", 8841], + "qnodeos_socket": ["localhost", 8842], + "vnode_socket": ["localhost", 8843] + } + }, + { + "David": { + "app_socket": ["localhost", 8871], + "qnodeos_socket": ["localhost", 8872], + "vnode_socket": ["localhost", 8873] + } + } + ], + "topology": null + } +] \ No newline at end of file diff --git a/examples/nativeMode/graphState/davidTest.py b/examples/nativeMode/graphState/davidTest.py index 8496266e..2cdd8f5d 100644 --- a/examples/nativeMode/graphState/davidTest.py +++ b/examples/nativeMode/graphState/davidTest.py @@ -27,14 +27,12 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -import os import logging import numpy as np from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings import simulaqron_settings +from simulaqron.settings import network_config from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -139,18 +137,15 @@ def remote_receive_qubit(self, virtualNum): def main(): - # In this example, we are YOURNAME + # In this example, we are David myName = "David" - # This file defines the network of virtual quantum nodes - network_file = simulaqron_settings.network_config_file - - # This file defines the nodes acting as servers in the classical communication network - classicalFile = os.path.join(os.path.dirname(__file__), "classicalNet.cfg") + # This file defines the network of virtual quantum nodes and the network used for classical communication + network_config.read_from_file("classicalNet.json") - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_file) - classicalNet = SocketsConfig(classicalFile) + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/graphState/doNew.sh b/examples/nativeMode/graphState/doNew.sh old mode 100644 new mode 100755 index 25d9c5d9..e6ce4cbd --- a/examples/nativeMode/graphState/doNew.sh +++ b/examples/nativeMode/graphState/doNew.sh @@ -1,7 +1,4 @@ -#!/usr/bin/env sh -TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) -if [ "$TEST_PIDS" != "" ] -then - kill -9 $TEST_PIDS -fi -sh run.sh +#!/usr/bin/env bash + +./terminate.sh +./run.sh \ No newline at end of file diff --git a/examples/nativeMode/graphState/run.sh b/examples/nativeMode/graphState/run.sh old mode 100644 new mode 100755 index 52624332..097588e0 --- a/examples/nativeMode/graphState/run.sh +++ b/examples/nativeMode/graphState/run.sh @@ -1,6 +1,18 @@ -#!/bin/sh +#!/usr/bin/env bash + +# Check if SimulaQron is already running +if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron start --nodes=Alice,Bob,Charlie,David --network-config-file classicalNet.json + then + echo "SimulaQron could not start correctly" + exit 1 + fi +fi python3 bobTest.py & +sleep 1 python3 charlieTest.py & +sleep 1 python3 davidTest.py & +sleep 1 python3 aliceTest.py diff --git a/examples/nativeMode/graphState/terminate.sh b/examples/nativeMode/graphState/terminate.sh new file mode 100755 index 00000000..39747653 --- /dev/null +++ b/examples/nativeMode/graphState/terminate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi +fi + diff --git a/examples/nativeMode/teleport/aliceTest.py b/examples/nativeMode/teleport/aliceTest.py index b2c8a175..c8fbf957 100644 --- a/examples/nativeMode/teleport/aliceTest.py +++ b/examples/nativeMode/teleport/aliceTest.py @@ -33,7 +33,6 @@ from simulaqron.math import assemble_qubit from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig -from simulaqron.settings.network_config import NodeConfigType from simulaqron.toolbox.stabilizer_states import StabilizerState from simulaqron.settings import simulaqron_settings, network_config, LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS from twisted.internet.defer import inlineCallbacks @@ -149,21 +148,13 @@ def main(): # In this example, we are Alice. myName = "Alice" - # This file defines the network of virtual quantum nodes - # network_file = simulaqron_settings.network_config_file - # virtualNet = SocketsConfig(network_file) - - # This file defines the nodes acting as servers in the classical communication network - # classicalFile = "classicalNet.cfg" - # classicalNet = SocketsConfig(classicalFile) - # We load the local configuration files simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) network_config.read_from_file(LOCAL_NETWORK_SETTINGS) - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_config, config_type=NodeConfigType.VNODE) - classicalNet = SocketsConfig(network_config, config_type=NodeConfigType.APP) + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index 863e445c..7fdb6c44 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -35,7 +35,6 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings, network_config, LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS -from simulaqron.settings.network_config import NodeConfigType from simulaqron.toolbox.stabilizer_states import StabilizerState from twisted.internet.defer import inlineCallbacks from twisted.spread import pb @@ -138,21 +137,13 @@ def main(): # In this example, we are Bob. myName = "Bob" - # This file defines the network of virtual quantum nodes - # network_file = simulaqron_settings.network_config_file - # virtualNet = SocketsConfig(network_file) - - # This file defines the nodes acting as servers in the classical communication network - # classicalFile = "classicalNet.cfg" - # classicalNet = SocketsConfig(classicalFile) - # We load the local configuration files simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) network_config.read_from_file(LOCAL_NETWORK_SETTINGS) - # Read configuration files for the virtual quantum, as well as the classical network - virtualNet = SocketsConfig(network_config, config_type=NodeConfigType.VNODE) - classicalNet = SocketsConfig(network_config, config_type=NodeConfigType.APP) + # Using the config, we then get the right sockets configuration type + virtualNet = SocketsConfig(network_config, network_name="default", config_type="vnode") + classicalNet = SocketsConfig(network_config, network_name="default", config_type="app") # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network diff --git a/examples/nativeMode/teleport/doNew.sh b/examples/nativeMode/teleport/doNew.sh index 25d9c5d9..b00dbb94 100755 --- a/examples/nativeMode/teleport/doNew.sh +++ b/examples/nativeMode/teleport/doNew.sh @@ -1,7 +1,4 @@ -#!/usr/bin/env sh -TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) -if [ "$TEST_PIDS" != "" ] -then - kill -9 $TEST_PIDS -fi -sh run.sh +#!/usr/bin/env bash + +./terminate.sh +./run.sh diff --git a/examples/nativeMode/teleport/run.sh b/examples/nativeMode/teleport/run.sh index 0de4867d..3a3d3b3a 100755 --- a/examples/nativeMode/teleport/run.sh +++ b/examples/nativeMode/teleport/run.sh @@ -1,4 +1,13 @@ -#!/bin/sh +#!/usr/bin/env bash + +# Check if SimulaQron is already running +if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json + then + echo "SimulaQron could not start correctly" + exit 1 + fi +fi python3 bobTest.py & python3 aliceTest.py diff --git a/examples/nativeMode/teleport/terminate.sh b/examples/nativeMode/teleport/terminate.sh new file mode 100755 index 00000000..39747653 --- /dev/null +++ b/examples/nativeMode/teleport/terminate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi +fi + From 3ad36d45ab1b7c2b90979af256087290a1d62a4c Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 5 Feb 2026 18:32:16 +0100 Subject: [PATCH 219/277] Update the template for native mode applications --- examples/nativeMode/template/README.md | 114 ++++++++++++++++++ examples/nativeMode/template/doNew.sh | 4 + .../nativeMode/template/network_config.json | 36 ++++++ examples/nativeMode/template/nodeTest.py | 8 -- examples/nativeMode/template/run.sh | 14 ++- examples/nativeMode/template/terminate.sh | 16 +++ 6 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 examples/nativeMode/template/README.md create mode 100755 examples/nativeMode/template/doNew.sh create mode 100644 examples/nativeMode/template/network_config.json create mode 100755 examples/nativeMode/template/terminate.sh diff --git a/examples/nativeMode/template/README.md b/examples/nativeMode/template/README.md new file mode 100644 index 00000000..ddf985c6 --- /dev/null +++ b/examples/nativeMode/template/README.md @@ -0,0 +1,114 @@ +# SimulaQron native mode template + +You can use the files in this folder as a template for implementing new SimulaQron applications. +Please check the comments inside the `nodeTest.py` file to learn how to modify it. + + +# Update network configuration + +Additionally, this template also contains a file that you can use for defining the network: +`network_config.json`. + +This file contains a single network, named "default". This network contains 4 nodes named +"Alice", "Bob", "Charlie" and "David". + +Each one of those nodes defines 3 entries: +* `app_socket`: The hostname and port used by other nodes to send classical messages to this node. + SimulaQron applications will bind to this hostname and port to listen for classical messages coming + from other nodes. +* `qnodeos_socket`: The hostname and port to bind the QNodeOS server. This configuration is required + when using SimulaQron's NetQASM interface. +* `vnode_socket`: The hostname and port to bind SimulaQron's Virtual Node server. This server is in + charge of executing the quantum simulation. Configuring this entry is required in all cases.s + + +## Adding new nodes + +To add a new node named "Eva", follow these steps: +1. Open the config.json file in a text editor. +2. Locate the "nodes" array inside the "default" object. +3. Add a new object to the "nodes" array for "Eva". The structure should match the existing nodes: +```json +{ + "Eva": { + "app_socket": ["localhost", PORT_NUMBER], + "qnodeos_socket": ["localhost", PORT_NUMBER], + "vnode_socket": ["localhost", PORT_NUMBER] + } +} +``` + Replace PORT_NUMBER with unique port numbers for each socket type (e.g., 8851, 8852, 8853). The result should + look like: +```json +Copy + +{ + "name": "default", + "nodes": [ + {"Alice": {...}}, + {"Bob": {...}}, + {"Charlie": {...}}, + {"David": {...}}, + { + "Eva": { + "app_socket": ["localhost", 8851], + "qnodeos_socket": ["localhost", 8852], + "vnode_socket": ["localhost", 8853] + } + } + ], + "topology": null +} +``` + +4. Save the file after making the changes. + + +# Additional tools + +This folder also contains a few bash scripts you can use for executing you application. + + +## `run.sh` + +This script is a helper to quickly start the SimulaQron backend for a list of specified nodes, and then +start the application code for those same nodes. + +This script needs a small change depending on how many nodes you want to run *on the current machine*. +If you open this file, you can see this content: +```shell +#!/usr/bin/env bash + +# Check if SimulaQron is already running +if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron start --nodes=Alice,Bob --network-config-file network_config.json + then + echo "SimulaQron could not start correctly" + exit 1 + fi +fi + + +# Run the files for Alice, Bob or whatever nodes you construct +python3 bobTest.py & +python3 aliceTest.py +``` + +Change the `--nodes` option of the `simulaqron start` command, with the list (separated with commas, no spaces) +of nodes you want to start locally. Also, change the `python3` lines to also start all your python programs +that implement the nodes. + +Finally, if you changed the name of the network configuration file, also reflect this change by changing +the argument next to the `--network-config-file` option. + + +## `terminate.sh` + +This script can be used to stop the SimulaQron backend and terminate all the applications processes +that are still running. + + +## `doNew.sh` + +This is ascript that you can use to start a new instance of your application from a clean state. +This scrip simply invokes `terminate.sh` and `run.sh` sequentially. diff --git a/examples/nativeMode/template/doNew.sh b/examples/nativeMode/template/doNew.sh new file mode 100755 index 00000000..b00dbb94 --- /dev/null +++ b/examples/nativeMode/template/doNew.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +./terminate.sh +./run.sh diff --git a/examples/nativeMode/template/network_config.json b/examples/nativeMode/template/network_config.json new file mode 100644 index 00000000..61b05b60 --- /dev/null +++ b/examples/nativeMode/template/network_config.json @@ -0,0 +1,36 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + { + "Charlie": { + "app_socket": ["localhost", 8841], + "qnodeos_socket": ["localhost", 8842], + "vnode_socket": ["localhost", 8843] + } + }, + { + "David": { + "app_socket": ["localhost", 8871], + "qnodeos_socket": ["localhost", 8872], + "vnode_socket": ["localhost", 8873] + } + } + ], + "topology": null + } +] \ No newline at end of file diff --git a/examples/nativeMode/template/nodeTest.py b/examples/nativeMode/template/nodeTest.py index 6f99f7f5..d36dd398 100644 --- a/examples/nativeMode/template/nodeTest.py +++ b/examples/nativeMode/template/nodeTest.py @@ -144,14 +144,6 @@ def main(): virtualNet = SocketsConfig(network_config, config_type=NodeConfigType.VNODE) classicalNet = SocketsConfig(network_config, config_type=NodeConfigType.APP) - # By default, *all nodes* described in the network configuration will be loaded in the SocketsConfig - # object. With this information, SimulaQron will start all of those nodes, either as local classical - # or virtual nodes (depending on the specified configuration type). - # In some cases, this is not desired, and we want ot start *a subset* of these nodes. - # To do this, we can use the method "filter" from the SocketsConfig object to specify the nodes - # we want to keep (and hence, start) - classicalNet.filter(["Alice", "Bob"]) - # Check if we should run a local classical server. If so, initialize the code # to handle remote connections on the classical communication network if myName in classicalNet.hostDict: diff --git a/examples/nativeMode/template/run.sh b/examples/nativeMode/template/run.sh index 3f0830ce..c1792453 100755 --- a/examples/nativeMode/template/run.sh +++ b/examples/nativeMode/template/run.sh @@ -1,4 +1,16 @@ -#!/bin/sh +#!/usr/bin/env bash + +# Check if SimulaQron is already running +if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + # TODO - Modify the list of nodes to start on this machine + # TODO - Change the filename of the network configuration if you changed that + if ! simulaqron start --nodes=Alice,Bob --network-config-file network_config.json + then + echo "SimulaQron could not start correctly" + exit 1 + fi +fi + # Run the files for Alice, Bob or whatever nodes you construct python3 bobTest.py & diff --git a/examples/nativeMode/template/terminate.sh b/examples/nativeMode/template/terminate.sh new file mode 100755 index 00000000..39747653 --- /dev/null +++ b/examples/nativeMode/template/terminate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi +fi + From 7fca7b471e7568aa93f949d8ed6456b73754ca53 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 5 Feb 2026 18:44:26 +0100 Subject: [PATCH 220/277] Update the new SDK teleport example --- examples/nativeMode/template/README.md | 2 +- examples/new-sdk/teleport/README.md | 4 ++-- .../teleport/{teleport-alice.py => aliceTest.py} | 0 .../new-sdk/teleport/{teleport-bob.py => bobTest.py} | 0 examples/new-sdk/teleport/doNew.sh | 4 ++++ examples/new-sdk/teleport/run.sh | 8 ++------ examples/new-sdk/teleport/terminate.sh | 10 ++++++---- 7 files changed, 15 insertions(+), 13 deletions(-) rename examples/new-sdk/teleport/{teleport-alice.py => aliceTest.py} (100%) rename examples/new-sdk/teleport/{teleport-bob.py => bobTest.py} (100%) create mode 100755 examples/new-sdk/teleport/doNew.sh diff --git a/examples/nativeMode/template/README.md b/examples/nativeMode/template/README.md index ddf985c6..4942c499 100644 --- a/examples/nativeMode/template/README.md +++ b/examples/nativeMode/template/README.md @@ -111,4 +111,4 @@ that are still running. ## `doNew.sh` This is ascript that you can use to start a new instance of your application from a clean state. -This scrip simply invokes `terminate.sh` and `run.sh` sequentially. +This script simply invokes `terminate.sh` and `run.sh` sequentially. diff --git a/examples/new-sdk/teleport/README.md b/examples/new-sdk/teleport/README.md index e90e6c21..9bdda551 100644 --- a/examples/new-sdk/teleport/README.md +++ b/examples/new-sdk/teleport/README.md @@ -101,12 +101,12 @@ Now you can run: * on Bob: ```shell -python teleport-bob.py +python bobTest.py ``` * on Alice: ```shell -python teleport-alice.py +python aliceTest.py ``` The code assumes you start Bob before starting Alice. Using your knowledge of network programming diff --git a/examples/new-sdk/teleport/teleport-alice.py b/examples/new-sdk/teleport/aliceTest.py similarity index 100% rename from examples/new-sdk/teleport/teleport-alice.py rename to examples/new-sdk/teleport/aliceTest.py diff --git a/examples/new-sdk/teleport/teleport-bob.py b/examples/new-sdk/teleport/bobTest.py similarity index 100% rename from examples/new-sdk/teleport/teleport-bob.py rename to examples/new-sdk/teleport/bobTest.py diff --git a/examples/new-sdk/teleport/doNew.sh b/examples/new-sdk/teleport/doNew.sh new file mode 100755 index 00000000..b00dbb94 --- /dev/null +++ b/examples/new-sdk/teleport/doNew.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +./terminate.sh +./run.sh diff --git a/examples/new-sdk/teleport/run.sh b/examples/new-sdk/teleport/run.sh index 88ee01fc..4f143d20 100755 --- a/examples/new-sdk/teleport/run.sh +++ b/examples/new-sdk/teleport/run.sh @@ -6,10 +6,6 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json fi -python3 teleport-bob.py & -SERVERPID=$! +python3 bobTest.py & sleep 1 -python3 teleport-alice.py - -# Kill the server after it was used -kill -9 $SERVERPID +python3 aliceTest.py diff --git a/examples/new-sdk/teleport/terminate.sh b/examples/new-sdk/teleport/terminate.sh index 550b9d70..39747653 100755 --- a/examples/new-sdk/teleport/terminate.sh +++ b/examples/new-sdk/teleport/terminate.sh @@ -1,14 +1,16 @@ #!/usr/bin/env sh -TEST_PIDS=$(ps aux | grep python | grep -E "teleport" | awk {'print $2'}) +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) if [ "$TEST_PIDS" != "" ] then kill -9 $TEST_PIDS fi -simulaqron stop - # Check if SimulaQron is running if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then - cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi fi From c89b723c11657a97a9685b8e714177f325b0b1d5 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 5 Feb 2026 20:11:51 +0100 Subject: [PATCH 221/277] Add template for creating client-server applications using the new SDK --- .../new-sdk/classical-client-server/doNew.sh | 4 + .../classical-client-server/terminate.sh | 16 +++ examples/new-sdk/teleport/README.md | 25 +---- examples/new-sdk/teleport/aliceTest.py | 2 +- examples/new-sdk/teleport/bobTest.py | 2 +- examples/new-sdk/teleport/run.sh | 26 ++++- .../new-sdk/template-client-server/README.md | 101 ++++++++++++++++++ .../new-sdk/template-client-server/doNew.sh | 4 + .../template-client-server/nodeTest-client.py | 83 ++++++++++++++ .../template-client-server/nodeTest-server.py | 78 ++++++++++++++ .../new-sdk/template-client-server/run.sh | 30 ++++++ .../simulaqron_network.json | 36 +++++++ .../simulaqron_settings.json | 14 +++ .../template-client-server/terminate.sh | 16 +++ 14 files changed, 410 insertions(+), 27 deletions(-) create mode 100755 examples/new-sdk/classical-client-server/doNew.sh create mode 100755 examples/new-sdk/classical-client-server/terminate.sh create mode 100644 examples/new-sdk/template-client-server/README.md create mode 100755 examples/new-sdk/template-client-server/doNew.sh create mode 100644 examples/new-sdk/template-client-server/nodeTest-client.py create mode 100644 examples/new-sdk/template-client-server/nodeTest-server.py create mode 100755 examples/new-sdk/template-client-server/run.sh create mode 100644 examples/new-sdk/template-client-server/simulaqron_network.json create mode 100644 examples/new-sdk/template-client-server/simulaqron_settings.json create mode 100755 examples/new-sdk/template-client-server/terminate.sh diff --git a/examples/new-sdk/classical-client-server/doNew.sh b/examples/new-sdk/classical-client-server/doNew.sh new file mode 100755 index 00000000..b00dbb94 --- /dev/null +++ b/examples/new-sdk/classical-client-server/doNew.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +./terminate.sh +./run.sh diff --git a/examples/new-sdk/classical-client-server/terminate.sh b/examples/new-sdk/classical-client-server/terminate.sh new file mode 100755 index 00000000..a09237db --- /dev/null +++ b/examples/new-sdk/classical-client-server/terminate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "example" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi +fi + diff --git a/examples/new-sdk/teleport/README.md b/examples/new-sdk/teleport/README.md index 9bdda551..0e44e59e 100644 --- a/examples/new-sdk/teleport/README.md +++ b/examples/new-sdk/teleport/README.md @@ -12,20 +12,11 @@ First of all, make sure you are not already running existing simulaqron programs You can run ```shell -simulaqron stop -sh terminate.sh +./terminate.sh ``` -which should get rid of all things running for the teleport example itself. If you want to be a bit -more radical and confident you are not running other things to preserve you can run: - -```shell -simulaqron stop -pkill -9 python -``` - -This will kill ALL python processes run by you so beware. If you have a debugging enabled, you may also -wish to wipe old log files by running: +which should get rid of all things running for the teleport example itself. If you have a +debugging enabled, you may also wish to wipe old log files by running: ```shell rm /tmp/simulaqron* @@ -35,16 +26,10 @@ This should leave you a clear slate. We can now start the application. ## On a single machine -If you are running everything on the same machine, first we start the simulaqron backend by typing - -```shell -simulaqron start --node Alice,Bob -``` - -This needs to be done only once. Now type +If you are running everything on the same machine, simply type: ```shell -sh run.sh +./run.sh ``` WARNING: It seems restarting the simulaqron backend is needed! - There is no need to restart the simulaqron backend again if you want to re-run your example. diff --git a/examples/new-sdk/teleport/aliceTest.py b/examples/new-sdk/teleport/aliceTest.py index 061fdaf9..ad2d48a2 100644 --- a/examples/new-sdk/teleport/aliceTest.py +++ b/examples/new-sdk/teleport/aliceTest.py @@ -29,7 +29,7 @@ async def run_alice(reader: StreamReader, writer: StreamWriter): q.H() m1 = q.measure() m2 = epr.measure() - # Any value that comes from NetQASm *need* to be retrieved ("casted" to int) + # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) # *after* the connection is closed (or after flushing the connection, untested) m1_val = int(m1) m2_val = int(m2) diff --git a/examples/new-sdk/teleport/bobTest.py b/examples/new-sdk/teleport/bobTest.py index 60d68d36..9113ad23 100644 --- a/examples/new-sdk/teleport/bobTest.py +++ b/examples/new-sdk/teleport/bobTest.py @@ -40,7 +40,7 @@ async def run_bob(reader: StreamReader, writer: StreamWriter): # Get the socket configuration for the sockets used for the application layer sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) - # Create the client + # Create the server server = SimulaQronClassicalServer(sockets_config, "Bob") server.register_client_handler(run_bob) server.start_serving() diff --git a/examples/new-sdk/teleport/run.sh b/examples/new-sdk/teleport/run.sh index 4f143d20..0c364ae0 100755 --- a/examples/new-sdk/teleport/run.sh +++ b/examples/new-sdk/teleport/run.sh @@ -1,9 +1,25 @@ -#!/bin/sh +#!/usr/bin/env bash -# Check if SimulaQron is already running -if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then - # If not, start simulaqron backend for both nodes - simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json +# Flag used to determine whether to start simulaqron backend or not +# This is useful when using this script to run the application in different machines. +START_SIMULAQRON=true + +# Process the arguments, if any +while [[ $# -gt 0 ]]; do + case $1 in + -n|--no-start-simulaqron) + START_SIMULAQRON=false + shift + ;; + esac +done + +if [ "$START_SIMULAQRON" = true ]; then + # Check if SimulaQron is already running + if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + # If not, start simulaqron backend for both nodes + simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json + fi fi python3 bobTest.py & diff --git a/examples/new-sdk/template-client-server/README.md b/examples/new-sdk/template-client-server/README.md new file mode 100644 index 00000000..92a3c458 --- /dev/null +++ b/examples/new-sdk/template-client-server/README.md @@ -0,0 +1,101 @@ +# SimulaQron Client-Server template +This folder can be used as a template for creating client-server applications. +This template uses SimulaQron's new SDK, to register a handler when a new client +connects to the server. Additionally, it also provides a simple way to execute +client code once the connection to the server has been established. + + +# How to run + +First of all, make sure you are not already running existing simulaqron programs. + + +## Initial cleanup + +You can run + +```shell +sh terminate.sh +``` + +which should get rid of all things running for the teleport example itself. If you have a +debugging enabled, you may also wish to wipe old log files by running: + +```shell +rm /tmp/simulaqron* +``` + +This should leave you a clear slate. We can now start the application. + + +## On a single machine +If you are running everything on the same machine, simply type: + +```shell +./run.sh +``` + +WARNING: It seems restarting the simulaqron backend is needed! - There is no need to restart the simulaqron backend again if you want to re-run your example. + + +## On multiple machines + +If you are starting on two different machines run, you first need to update the network configuration +file. To run this example on different machines, it is necessary that both machines can reach each +other via a network (or the internet). Additionally, you need to know the IP addresses of both +machines. + +Assuming that the server (alice) will run on a machine with IP `192.168.0.1` and the client (bob) +will run on the machine with IP `192.168.0.2`, modify the `simulaqron_network.json` file *on both* +the server and the client to look like this: + +```json +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["192.168.0.1", 8821], + "qnodeos_socket": ["192.168.0.1", 8822], + "vnode_socket": ["192.168.0.1", 8823] + } + }, + { + "Bob": { + "app_socket": ["192.168.0.2", 9831], + "qnodeos_socket": ["192.168.0.2", 9832], + "vnode_socket": ["192.168.0.2", 9833] + } + } + ], + "topology": null + } +] +``` + +Note that the `localhost` entries from Alice were changed to `192.168.0.1`. Similarly, the +`localhost` entries from Bob were changed to `192.168.0.2`. + +After these modifications, yu can start the simulaqron backends by invoking: + +```shell +simulaqron start --node Alice +simulaqron start --node Bob +``` + +on the machines you will use as Alice and Bob respectively. Again this needs to be run only once. +Now you can run: + +* on Bob: +```shell +python bobTest.py +``` + +* on Alice: +```shell +python aliceTest.py +``` + +The code assumes you start Bob before starting Alice. Using your knowledge of network programming +from our ping pong example - do you have an idea to make this more robust? diff --git a/examples/new-sdk/template-client-server/doNew.sh b/examples/new-sdk/template-client-server/doNew.sh new file mode 100755 index 00000000..b00dbb94 --- /dev/null +++ b/examples/new-sdk/template-client-server/doNew.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +./terminate.sh +./run.sh diff --git a/examples/new-sdk/template-client-server/nodeTest-client.py b/examples/new-sdk/template-client-server/nodeTest-client.py new file mode 100644 index 00000000..a8375de6 --- /dev/null +++ b/examples/new-sdk/template-client-server/nodeTest-client.py @@ -0,0 +1,83 @@ +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + +# This is recipe to use NetQASM with simulaqron backend. +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +# Importing NetQASM connection, Qubit and EPR socket must be *after* +# setting the simulator for NetQASM +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import Qubit, EPRSocket # noqa: E402 + + +# This function contains the code of the classical client +# "reader" is an object connected to the server, which can be used to read data from the server +# "writer" is an object connected to the server, which can be used to send data to the server +async def run_client(reader: StreamReader, writer: StreamWriter): + # To send a messsage, we can simply use the "wirte" method from the "writer" object + # The argument *must* be a python bytes object, which we can get by encoding (using + # the UTF-8 charmap) any python string + message = "Hello World" + writer.write(message.encode("utf-8")) + + # If you want to receive a message (such a response) from the server, you can + # use the "read" method from the "reader" object. The argument is an integer that + # configures the maximum bytes that we are allowed to read in a single operation. + # Note: Since "read" is a python coroutine, we need to "await" it, so python can + # execute other coroutines until the data becomes available to read. + answer = await reader.read(100) + + this_node_name = "Alice" + other_node_name = "Bob" + + # We ca create an EPR socket with the other node + epr_socket = EPRSocket(other_node_name) + # To start executing quantum operations, we need to create a NetQASM connection + with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as alice: + # Create a qubit + q = Qubit(alice) + q.H() + # Create an entangled qubit with the other node + epr = epr_socket.create_keep()[0] + # Teleport + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + m1_val = int(m1) + m2_val = int(m2) + return m1_val, m2_val + + +if __name__ == "__main__": + # Load the simulaqron settings file + simulaqron_config_file = Path("simulaqron_settings.json") + simulaqron_settings.read_from_file(simulaqron_config_file) + + # Load the file network configuration file + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Some data for this node: + network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" + node_name = "YourName" # A node with this name *must* exist in "simulaqron_network.json" + + # Get the socket configuration for the sockets used for the application layer + sockets_config = SocketsConfig(network_config, network_name, NodeConfigType.APP) + + # Name of one node to classically connect to + server_name = "Bob" + + # Create the client + client = SimulaQronClassicalClient(sockets_config) + # Run a classical client invoking the `run_client` method. This also has the effect to + # immediately execute the `run_client` method. + results = client.run_client(server_name, run_client) diff --git a/examples/new-sdk/template-client-server/nodeTest-server.py b/examples/new-sdk/template-client-server/nodeTest-server.py new file mode 100644 index 00000000..b1476860 --- /dev/null +++ b/examples/new-sdk/template-client-server/nodeTest-server.py @@ -0,0 +1,78 @@ +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings import network_config +from simulaqron.settings.network_config import NodeConfigType + +# This is recipe to use NetQASM with simulaqron backend. +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +# Importing NetQASM connection, Qubit and EPR socket must be *after* +# setting the simulator for NetQASM +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import Qubit, EPRSocket # noqa: E402 + + +# This function contains the code to serve *one client ocnnected to this server*. +# "reader" is an object connected to the client, which can be used to read data from the client +# "writer" is an object connected to the client, which can be used to send data to the client +async def serve_client(reader: StreamReader, writer: StreamWriter): + # If you want to receive a message (such a query) from the client, you can + # use the "read" method from the "reader" object. The argument is an integer that + # configures the maximum bytes that we are allowed to read in a single operation. + # Note: Since "read" is a python coroutine, we need to "await" it, so python can + # execute other coroutines (such as serving a new client) until the data becomes + # available to read. + answer = await reader.read(100) + + # To send a messsage, we can simply use the "wirte" method from the "writer" object + # The argument *must* be a python bytes object, which we can get by encoding (using + # the UTF-8 charmap) any python string + message = "Hello World" + writer.write(message.encode("utf-8")) + + this_node_name = "Bob" + other_node_name = "Alice" + + epr_socket = EPRSocket(other_node_name) + with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as bob: + # Create a qubit + q = Qubit(bob) + q.H() + # Create entanglement + epr = epr_socket.create_keep()[0] + # Teleport + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + m1_val = int(m1) + m2_val = int(m2) + return m1_val, m2_val + + +if __name__ == "__main__": + # Load the simulaqron settings file + simulaqron_config_file = Path("simulaqron_settings.json") + simulaqron_settings.read_from_file(simulaqron_config_file) + + # Load the file network configuration file + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Some data for this node: + network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" + node_name = "YourName" # A node with this name *must* exist in "simulaqron_network.json" + + # Get the socket configuration for the sockets used for the application layer + sockets_config = SocketsConfig(network_config, network_name, NodeConfigType.APP) + + # Create the server + server = SimulaQronClassicalServer(sockets_config, node_name) + server.register_client_handler(serve_client) + server.start_serving() diff --git a/examples/new-sdk/template-client-server/run.sh b/examples/new-sdk/template-client-server/run.sh new file mode 100755 index 00000000..5f676ff6 --- /dev/null +++ b/examples/new-sdk/template-client-server/run.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# Flag used to determine whether to start simulaqron backend or not +# This is useful when using this script to run the application in different machines. +START_SIMULAQRON=true + +# Process the arguments, if any +while [[ $# -gt 0 ]]; do + case $1 in + -n|--no-start-simulaqron) + START_SIMULAQRON=false + shift + ;; + esac +done + +if [ "$START_SIMULAQRON" = true ]; then + # Check if SimulaQron is already running + if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + # TODO - Modify the list of nodes to start on this machine + # TODO - Change the filename of the network configuration if you changed that + # TODO - Change the filename of the simulaqron configuration if you changed that + # If not, start simulaqron backend for both nodes + simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json --simulaqron-config-file simulaqron_settings.json + fi +fi + +python3 nodeTest-server.py & +sleep 1 +python3 nodeTest-client.py diff --git a/examples/new-sdk/template-client-server/simulaqron_network.json b/examples/new-sdk/template-client-server/simulaqron_network.json new file mode 100644 index 00000000..61b05b60 --- /dev/null +++ b/examples/new-sdk/template-client-server/simulaqron_network.json @@ -0,0 +1,36 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + { + "Charlie": { + "app_socket": ["localhost", 8841], + "qnodeos_socket": ["localhost", 8842], + "vnode_socket": ["localhost", 8843] + } + }, + { + "David": { + "app_socket": ["localhost", 8871], + "qnodeos_socket": ["localhost", 8872], + "vnode_socket": ["localhost", 8873] + } + } + ], + "topology": null + } +] \ No newline at end of file diff --git a/examples/new-sdk/template-client-server/simulaqron_settings.json b/examples/new-sdk/template-client-server/simulaqron_settings.json new file mode 100644 index 00000000..bbfb47a2 --- /dev/null +++ b/examples/new-sdk/template-client-server/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "projectq", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} \ No newline at end of file diff --git a/examples/new-sdk/template-client-server/terminate.sh b/examples/new-sdk/template-client-server/terminate.sh new file mode 100755 index 00000000..39747653 --- /dev/null +++ b/examples/new-sdk/template-client-server/terminate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi +fi + From ab8a0dd0f333543e36f7df772f6bed7fa691ba54 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 5 Feb 2026 21:10:43 +0100 Subject: [PATCH 222/277] Add template for creating quantum local applications using the new SDK --- .../new-sdk/template-quantum-local/doNew.sh | 4 ++ .../template-quantum-local/nodeTest.py | 57 +++++++++++++++++++ .../new-sdk/template-quantum-local/run.sh | 28 +++++++++ .../simulaqron_network.json | 36 ++++++++++++ .../simulaqron_settings.json | 14 +++++ .../template-quantum-local/terminate.sh | 16 ++++++ simulaqron/sdk/protocol.py | 12 ++++ 7 files changed, 167 insertions(+) create mode 100755 examples/new-sdk/template-quantum-local/doNew.sh create mode 100644 examples/new-sdk/template-quantum-local/nodeTest.py create mode 100755 examples/new-sdk/template-quantum-local/run.sh create mode 100644 examples/new-sdk/template-quantum-local/simulaqron_network.json create mode 100644 examples/new-sdk/template-quantum-local/simulaqron_settings.json create mode 100755 examples/new-sdk/template-quantum-local/terminate.sh diff --git a/examples/new-sdk/template-quantum-local/doNew.sh b/examples/new-sdk/template-quantum-local/doNew.sh new file mode 100755 index 00000000..b00dbb94 --- /dev/null +++ b/examples/new-sdk/template-quantum-local/doNew.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +./terminate.sh +./run.sh diff --git a/examples/new-sdk/template-quantum-local/nodeTest.py b/examples/new-sdk/template-quantum-local/nodeTest.py new file mode 100644 index 00000000..51ab80f1 --- /dev/null +++ b/examples/new-sdk/template-quantum-local/nodeTest.py @@ -0,0 +1,57 @@ +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + +# This is recipe to use NetQASM with simulaqron backend. +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +# Importing NetQASM connection, Qubit and EPR socket must be *after* +# setting the simulator for NetQASM +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import Qubit, EPRSocket # noqa: E402 + + +# This function contains the code of the classical client +def quantum_program(this_node_name: str) -> int: + # To start executing quantum operations, we need to create a NetQASM connection + with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as alice: + # Create a qubit + q = Qubit(alice) + + # Perform some local quantum operations + q.H() + q.X() + m1 = q.measure() + # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + m1_val = int(m1) + return m1_val + + +if __name__ == "__main__": + # Load the simulaqron settings file + simulaqron_config_file = Path("simulaqron_settings.json") + simulaqron_settings.read_from_file(simulaqron_config_file) + + # Load the file network configuration file + # We still need this file to correctly interact with the SimulaQron backend (QNodeOS and Virtual Node) + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Some data for this node: + network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" + node_name = "YourName" # A node with this name *must* exist in "simulaqron_network.json" + + # Get the socket configuration for the sockets used for the application layer + sockets_config = SocketsConfig(network_config, network_name, NodeConfigType.APP) + + # Name of one node to classically connect to + server_name = "Bob" + + result = quantum_program() + print(result) diff --git a/examples/new-sdk/template-quantum-local/run.sh b/examples/new-sdk/template-quantum-local/run.sh new file mode 100755 index 00000000..8e64e566 --- /dev/null +++ b/examples/new-sdk/template-quantum-local/run.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +# Flag used to determine whether to start simulaqron backend or not +# This is useful when using this script to run the application in different machines. +START_SIMULAQRON=true + +# Process the arguments, if any +while [[ $# -gt 0 ]]; do + case $1 in + -n|--no-start-simulaqron) + START_SIMULAQRON=false + shift + ;; + esac +done + +if [ "$START_SIMULAQRON" = true ]; then + # Check if SimulaQron is already running + if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + # TODO - Modify the list of nodes to start on this machine + # TODO - Change the filename of the network configuration if you changed that + # TODO - Change the filename of the simulaqron configuration if you changed that + # If not, start simulaqron backend for both nodes + simulaqron start --nodes=Alice --network-config-file simulaqron_network.json --simulaqron-config-file simulaqron_settings.json + fi +fi + +python3 nodeTest.py diff --git a/examples/new-sdk/template-quantum-local/simulaqron_network.json b/examples/new-sdk/template-quantum-local/simulaqron_network.json new file mode 100644 index 00000000..61b05b60 --- /dev/null +++ b/examples/new-sdk/template-quantum-local/simulaqron_network.json @@ -0,0 +1,36 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + { + "Charlie": { + "app_socket": ["localhost", 8841], + "qnodeos_socket": ["localhost", 8842], + "vnode_socket": ["localhost", 8843] + } + }, + { + "David": { + "app_socket": ["localhost", 8871], + "qnodeos_socket": ["localhost", 8872], + "vnode_socket": ["localhost", 8873] + } + } + ], + "topology": null + } +] \ No newline at end of file diff --git a/examples/new-sdk/template-quantum-local/simulaqron_settings.json b/examples/new-sdk/template-quantum-local/simulaqron_settings.json new file mode 100644 index 00000000..bbfb47a2 --- /dev/null +++ b/examples/new-sdk/template-quantum-local/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "projectq", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} \ No newline at end of file diff --git a/examples/new-sdk/template-quantum-local/terminate.sh b/examples/new-sdk/template-quantum-local/terminate.sh new file mode 100755 index 00000000..39747653 --- /dev/null +++ b/examples/new-sdk/template-quantum-local/terminate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi +fi + diff --git a/simulaqron/sdk/protocol.py b/simulaqron/sdk/protocol.py index ad98f42c..1263e22f 100644 --- a/simulaqron/sdk/protocol.py +++ b/simulaqron/sdk/protocol.py @@ -62,6 +62,18 @@ async def connected_handler(reader: StreamReader, writer: StreamWriter): class SimulaQronClassicalServer: def __init__(self, sockets_config: SocketsConfig, name: str): + """ + Classical server used to server classical clients to remote nodes. The given socket configs + object contains the specification of the available nodes on the network that this server can + interact with. Please note that this configuration *does not limit* the clients that can + connect to this server. + + :param sockets_config: The sockets configuration for the whole network. + :type sockets_config: SocketsConfig + :param name: The name of the server to connect to. The name *must* exist in the + configuration file given when constructing this server. + :type name: str + """ self._node_name = name self._sockets_data = sockets_config.hostDict[self._node_name] self._connection_handler: Optional[Callable[[StreamReader, StreamWriter], Awaitable[None]]] = None From 27fc33a0e060ceee5dfea7383f30532dcd9075a4 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 5 Feb 2026 21:22:07 +0100 Subject: [PATCH 223/277] Linter changes --- examples/new-sdk/template-client-server/nodeTest-client.py | 3 +++ examples/new-sdk/template-client-server/nodeTest-server.py | 7 +++++-- examples/new-sdk/template-quantum-local/nodeTest.py | 6 ++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/examples/new-sdk/template-client-server/nodeTest-client.py b/examples/new-sdk/template-client-server/nodeTest-client.py index a8375de6..999761d9 100644 --- a/examples/new-sdk/template-client-server/nodeTest-client.py +++ b/examples/new-sdk/template-client-server/nodeTest-client.py @@ -32,6 +32,9 @@ async def run_client(reader: StreamReader, writer: StreamWriter): # Note: Since "read" is a python coroutine, we need to "await" it, so python can # execute other coroutines until the data becomes available to read. answer = await reader.read(100) + # Messages received from the server come as "bytes", which need to be decoded + # (using UTF-8 encoding) before using it as a string. + print(answer.decode("utf-8")) this_node_name = "Alice" other_node_name = "Bob" diff --git a/examples/new-sdk/template-client-server/nodeTest-server.py b/examples/new-sdk/template-client-server/nodeTest-server.py index b1476860..84d5d675 100644 --- a/examples/new-sdk/template-client-server/nodeTest-server.py +++ b/examples/new-sdk/template-client-server/nodeTest-server.py @@ -2,8 +2,8 @@ from pathlib import Path from simulaqron.general.host_config import SocketsConfig -from simulaqron.sdk.protocol import SimulaQronClassicalClient -from simulaqron.settings import network_config +from simulaqron.sdk.protocol import SimulaQronClassicalServer +from simulaqron.settings import network_config, simulaqron_settings from simulaqron.settings.network_config import NodeConfigType # This is recipe to use NetQASM with simulaqron backend. @@ -27,6 +27,9 @@ async def serve_client(reader: StreamReader, writer: StreamWriter): # execute other coroutines (such as serving a new client) until the data becomes # available to read. answer = await reader.read(100) + # mMessages received from the client come as "bytes", which need to be decoded + # (using UTF-8 encoding) before using it as a string. + print(answer.decode("utf-8")) # To send a messsage, we can simply use the "wirte" method from the "writer" object # The argument *must* be a python bytes object, which we can get by encoding (using diff --git a/examples/new-sdk/template-quantum-local/nodeTest.py b/examples/new-sdk/template-quantum-local/nodeTest.py index 51ab80f1..9be2f6c4 100644 --- a/examples/new-sdk/template-quantum-local/nodeTest.py +++ b/examples/new-sdk/template-quantum-local/nodeTest.py @@ -1,8 +1,6 @@ -from asyncio import StreamReader, StreamWriter from pathlib import Path from simulaqron.general.host_config import SocketsConfig -from simulaqron.sdk.protocol import SimulaQronClassicalClient from simulaqron.settings import network_config, simulaqron_settings from simulaqron.settings.network_config import NodeConfigType @@ -13,13 +11,13 @@ # Importing NetQASM connection, Qubit and EPR socket must be *after* # setting the simulator for NetQASM from netqasm.sdk.external import NetQASMConnection # noqa: E402 -from netqasm.sdk import Qubit, EPRSocket # noqa: E402 +from netqasm.sdk import Qubit # noqa: E402 # This function contains the code of the classical client def quantum_program(this_node_name: str) -> int: # To start executing quantum operations, we need to create a NetQASM connection - with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as alice: + with NetQASMConnection(this_node_name) as alice: # Create a qubit q = Qubit(alice) From b7030146413f54eea06beedced41a1007418836a Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 6 Feb 2026 18:11:25 +0100 Subject: [PATCH 224/277] Small update on the new SDK application templates --- examples/new-sdk/template-client-server/doNew.sh | 1 + examples/new-sdk/template-quantum-local/doNew.sh | 1 + examples/new-sdk/template-quantum-local/nodeTest.py | 12 ++---------- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/examples/new-sdk/template-client-server/doNew.sh b/examples/new-sdk/template-client-server/doNew.sh index b00dbb94..9dda467e 100755 --- a/examples/new-sdk/template-client-server/doNew.sh +++ b/examples/new-sdk/template-client-server/doNew.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash ./terminate.sh +sleep 1 ./run.sh diff --git a/examples/new-sdk/template-quantum-local/doNew.sh b/examples/new-sdk/template-quantum-local/doNew.sh index b00dbb94..9dda467e 100755 --- a/examples/new-sdk/template-quantum-local/doNew.sh +++ b/examples/new-sdk/template-quantum-local/doNew.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash ./terminate.sh +sleep 1 ./run.sh diff --git a/examples/new-sdk/template-quantum-local/nodeTest.py b/examples/new-sdk/template-quantum-local/nodeTest.py index 9be2f6c4..c1353a9f 100644 --- a/examples/new-sdk/template-quantum-local/nodeTest.py +++ b/examples/new-sdk/template-quantum-local/nodeTest.py @@ -1,8 +1,6 @@ from pathlib import Path -from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import network_config, simulaqron_settings -from simulaqron.settings.network_config import NodeConfigType # This is recipe to use NetQASM with simulaqron backend. from netqasm.runtime.settings import set_simulator @@ -43,13 +41,7 @@ def quantum_program(this_node_name: str) -> int: # Some data for this node: network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" - node_name = "YourName" # A node with this name *must* exist in "simulaqron_network.json" + node_name = "Alice" # A node with this name *must* exist in "simulaqron_network.json" - # Get the socket configuration for the sockets used for the application layer - sockets_config = SocketsConfig(network_config, network_name, NodeConfigType.APP) - - # Name of one node to classically connect to - server_name = "Bob" - - result = quantum_program() + result = quantum_program(node_name) print(result) From 4190426eedc881ee2fa220d21e4b19e7844538b4 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 6 Feb 2026 18:11:37 +0100 Subject: [PATCH 225/277] Add corrRNG example using new SDK --- examples/new-sdk/corrRNG/aliceTest.py | 60 +++++++++++++++++++ examples/new-sdk/corrRNG/bobTest.py | 60 +++++++++++++++++++ examples/new-sdk/corrRNG/doNew.sh | 5 ++ examples/new-sdk/corrRNG/run.sh | 26 ++++++++ .../new-sdk/corrRNG/simulaqron_network.json | 36 +++++++++++ .../new-sdk/corrRNG/simulaqron_settings.json | 14 +++++ examples/new-sdk/corrRNG/terminate.sh | 16 +++++ 7 files changed, 217 insertions(+) create mode 100644 examples/new-sdk/corrRNG/aliceTest.py create mode 100644 examples/new-sdk/corrRNG/bobTest.py create mode 100755 examples/new-sdk/corrRNG/doNew.sh create mode 100755 examples/new-sdk/corrRNG/run.sh create mode 100644 examples/new-sdk/corrRNG/simulaqron_network.json create mode 100644 examples/new-sdk/corrRNG/simulaqron_settings.json create mode 100755 examples/new-sdk/corrRNG/terminate.sh diff --git a/examples/new-sdk/corrRNG/aliceTest.py b/examples/new-sdk/corrRNG/aliceTest.py new file mode 100644 index 00000000..6e053aff --- /dev/null +++ b/examples/new-sdk/corrRNG/aliceTest.py @@ -0,0 +1,60 @@ +import logging + +from pathlib import Path + +from simulaqron.settings import network_config, simulaqron_settings + +# This is recipe to use NetQASM with simulaqron backend. +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +# Importing NetQASM connection, Qubit and EPR socket must be *after* +# setting the simulator for NetQASM +from netqasm.logging.glob import set_log_level # noqa: E402 +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +# This function contains the code of the classical client +def quantum_program(this_node_name: str, remote_node_name: str) -> int: + logging.debug("LOCAL %s: Running client side program.", this_node_name) + + epr_socket = EPRSocket(remote_node_name) + # To start executing quantum operations, we need to create a NetQASM connection + with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as alice: + # Create an entangled qubit + epr = epr_socket.create_keep()[0] + + # And simply measure it + m1 = epr.measure() + # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + m1_val = int(m1) + return m1_val + + +if __name__ == "__main__": + logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=logging.DEBUG, + force=True + ) + # We set the netqasm log level to "info" to avoid verbose output from the internals. + set_log_level(logging.INFO) + + # Load the simulaqron settings file + simulaqron_config_file = Path("simulaqron_settings.json") + simulaqron_settings.read_from_file(simulaqron_config_file) + + # Load the file network configuration file + # We still need this file to correctly interact with the SimulaQron backend (QNodeOS and Virtual Node) + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Some data for this node: + network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" + node_name = "Alice" # A node with this name *must* exist in "simulaqron_network.json" + other_node_name = "Bob" + + result = quantum_program(node_name, other_node_name) + print(f"{node_name}: My Random Number is '{result}'") diff --git a/examples/new-sdk/corrRNG/bobTest.py b/examples/new-sdk/corrRNG/bobTest.py new file mode 100644 index 00000000..765d035f --- /dev/null +++ b/examples/new-sdk/corrRNG/bobTest.py @@ -0,0 +1,60 @@ +import logging + +from pathlib import Path + +from simulaqron.settings import network_config, simulaqron_settings + +# This is recipe to use NetQASM with simulaqron backend. +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +# Importing NetQASM connection, Qubit and EPR socket must be *after* +# setting the simulator for NetQASM +from netqasm.logging.glob import set_log_level # noqa: E402 +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +# This function contains the code of the classical client +def bob_program(this_node_name: str, remote_node_name: str) -> int: + logging.debug("LOCAL %s: Running client side program.", this_node_name) + + epr_socket = EPRSocket(remote_node_name) + # To start executing quantum operations, we need to create a NetQASM connection + with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as alice: + # Receive an entangled qubit + epr = epr_socket.recv_keep()[0] + + # And simply measure it + m1 = epr.measure() + # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + m1_val = int(m1) + return m1_val + + +if __name__ == "__main__": + logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=logging.DEBUG, + force=True + ) + # We set the netqasm log level to "info" to avoid verbose output from the internals. + set_log_level(logging.INFO) + + # Load the simulaqron settings file + simulaqron_config_file = Path("simulaqron_settings.json") + simulaqron_settings.read_from_file(simulaqron_config_file) + + # Load the file network configuration file + # We still need this file to correctly interact with the SimulaQron backend (QNodeOS and Virtual Node) + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Some data for this node: + network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" + node_name = "Bob" # A node with this name *must* exist in "simulaqron_network.json" + other_node_name = "Alice" + + result = bob_program(node_name, other_node_name) + print(f"{node_name}: My Random Number is '{result}'") diff --git a/examples/new-sdk/corrRNG/doNew.sh b/examples/new-sdk/corrRNG/doNew.sh new file mode 100755 index 00000000..9dda467e --- /dev/null +++ b/examples/new-sdk/corrRNG/doNew.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +./terminate.sh +sleep 1 +./run.sh diff --git a/examples/new-sdk/corrRNG/run.sh b/examples/new-sdk/corrRNG/run.sh new file mode 100755 index 00000000..92f5d5c7 --- /dev/null +++ b/examples/new-sdk/corrRNG/run.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +# Flag used to determine whether to start simulaqron backend or not +# This is useful when using this script to run the application in different machines. +START_SIMULAQRON=true + +# Process the arguments, if any +while [[ $# -gt 0 ]]; do + case $1 in + -n|--no-start-simulaqron) + START_SIMULAQRON=false + shift + ;; + esac +done + +if [ "$START_SIMULAQRON" = true ]; then + # Check if SimulaQron is already running + if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + # If not, start simulaqron backend for both nodes + simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json --simulaqron-config-file simulaqron_settings.json + fi +fi + +python3 aliceTest.py & +python3 bobTest.py diff --git a/examples/new-sdk/corrRNG/simulaqron_network.json b/examples/new-sdk/corrRNG/simulaqron_network.json new file mode 100644 index 00000000..61b05b60 --- /dev/null +++ b/examples/new-sdk/corrRNG/simulaqron_network.json @@ -0,0 +1,36 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + { + "Charlie": { + "app_socket": ["localhost", 8841], + "qnodeos_socket": ["localhost", 8842], + "vnode_socket": ["localhost", 8843] + } + }, + { + "David": { + "app_socket": ["localhost", 8871], + "qnodeos_socket": ["localhost", 8872], + "vnode_socket": ["localhost", 8873] + } + } + ], + "topology": null + } +] \ No newline at end of file diff --git a/examples/new-sdk/corrRNG/simulaqron_settings.json b/examples/new-sdk/corrRNG/simulaqron_settings.json new file mode 100644 index 00000000..bbfb47a2 --- /dev/null +++ b/examples/new-sdk/corrRNG/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "projectq", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} \ No newline at end of file diff --git a/examples/new-sdk/corrRNG/terminate.sh b/examples/new-sdk/corrRNG/terminate.sh new file mode 100755 index 00000000..39747653 --- /dev/null +++ b/examples/new-sdk/corrRNG/terminate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi +fi + From c92e704caad52ad1da9a37248bcd7295eece1681 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Fri, 6 Feb 2026 21:16:54 +0100 Subject: [PATCH 226/277] Add first version of the "extended GHZ" example using the new SDK --- examples/new-sdk/extendGHZ/aliceTest.py | 88 +++++++++++++++ examples/new-sdk/extendGHZ/bobTest.py | 102 ++++++++++++++++++ examples/new-sdk/extendGHZ/charlieTest.py | 79 ++++++++++++++ examples/new-sdk/extendGHZ/doNew.sh | 5 + examples/new-sdk/extendGHZ/run.sh | 29 +++++ .../new-sdk/extendGHZ/simulaqron_network.json | 36 +++++++ .../extendGHZ/simulaqron_settings.json | 14 +++ examples/new-sdk/extendGHZ/terminate.sh | 16 +++ simulaqron/sdk/protocol.py | 26 ++++- 9 files changed, 391 insertions(+), 4 deletions(-) create mode 100644 examples/new-sdk/extendGHZ/aliceTest.py create mode 100644 examples/new-sdk/extendGHZ/bobTest.py create mode 100644 examples/new-sdk/extendGHZ/charlieTest.py create mode 100755 examples/new-sdk/extendGHZ/doNew.sh create mode 100755 examples/new-sdk/extendGHZ/run.sh create mode 100644 examples/new-sdk/extendGHZ/simulaqron_network.json create mode 100644 examples/new-sdk/extendGHZ/simulaqron_settings.json create mode 100755 examples/new-sdk/extendGHZ/terminate.sh diff --git a/examples/new-sdk/extendGHZ/aliceTest.py b/examples/new-sdk/extendGHZ/aliceTest.py new file mode 100644 index 00000000..c88b5927 --- /dev/null +++ b/examples/new-sdk/extendGHZ/aliceTest.py @@ -0,0 +1,88 @@ +import logging + +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings import network_config, simulaqron_settings + +# This is recipe to use NetQASM with simulaqron backend. +from netqasm.runtime.settings import set_simulator + +from simulaqron.settings.network_config import NodeConfigType + +set_simulator("simulaqron") + +# Importing NetQASM connection, Qubit and EPR socket must be *after* +# setting the simulator for NetQASM +from netqasm.logging.glob import set_log_level # noqa: E402 +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +# This function contains the code of the classical client +async def alice_program(reader: StreamReader, writer: StreamWriter) -> int: + # This is "Alice": the start node of the GHZ chain + this_node_name = "Alice" + remote_node_name = "Bob" # A node with this name *must* exist in "simulaqron_network.json" + + logging.debug("LOCAL %s: Running client side program.", this_node_name) + + epr_socket = EPRSocket(remote_node_name) + # To start executing quantum operations, we need to create a NetQASM connection + with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]): + # Create an entangled qubit with Bob + epr = epr_socket.create_keep()[0] + + writer.write("receive_qubit".encode("utf-8")) + answer = await reader.read(100) + + assert answer.decode("utf-8") == "continue" + + m1 = epr.measure() + # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + m1_val = int(m1) + return m1_val + + +if __name__ == "__main__": + logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=logging.DEBUG, + force=True + ) + # We set the netqasm log level to "info" to avoid verbose output from the internals. + set_log_level(logging.INFO) + + # Load the simulaqron settings file + simulaqron_config_file = Path("simulaqron_settings.json") + simulaqron_settings.read_from_file(simulaqron_config_file) + + # Load the file network configuration file + # We still need this file to correctly interact with the SimulaQron backend (QNodeOS and Virtual Node) + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Some data for this node: + network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" + node_name = "Alice" + other_node_name = "Bob" # A node with this name *must* exist in "simulaqron_network.json" + + classical_sockets = SocketsConfig(network_config, network_name, NodeConfigType.APP) + + # The general nodes interaction of this application is + # Alice ----> Bob ----> Charlie ----> Bob ----> Alice + # This means: + # 1. Alice entangles a qubit with Bob, and waits until Bob has finished. + # 2. Bob received the entangled qubit, creates a new local one and entangles it with the one received + + # This node will act as a classical client, waiting for a classical answer from Bob + # to measure the qubit + client = SimulaQronClassicalClient(classical_sockets) + + result = client.run_client(other_node_name, alice_program) + #result = alice_program(1, 0) + + print(f"{node_name}: My outcome is '{result}'") diff --git a/examples/new-sdk/extendGHZ/bobTest.py b/examples/new-sdk/extendGHZ/bobTest.py new file mode 100644 index 00000000..b56c04ea --- /dev/null +++ b/examples/new-sdk/extendGHZ/bobTest.py @@ -0,0 +1,102 @@ +import logging + +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient, SimulaQronClassicalServer +from simulaqron.settings import network_config, simulaqron_settings + +# This is recipe to use NetQASM with simulaqron backend. +from netqasm.runtime.settings import set_simulator + +from simulaqron.settings.network_config import NodeConfigType + +set_simulator("simulaqron") + +# Importing NetQASM connection, Qubit and EPR socket must be *after* +# setting the simulator for NetQASM +from netqasm.logging.glob import set_log_level # noqa: E402 +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +async def send_to_charlie(reader: StreamReader, writer: StreamWriter): + writer.write("receive_qubit".encode("utf-8")) + message = await reader.read(100) + assert message.decode("utf-8") == "continue" + + +# This function contains the code of the classical client +async def bob_program(reader: StreamReader, writer: StreamWriter) -> int: + # This is "Bob": the middle node of the GHZ chain + this_node_name = "Bob" + start_node_name = "Alice" # A node with this name *must* exist in "simulaqron_network.json" + end_node_name = "Charlie" # A node with this name *must* exist in "simulaqron_network.json" + + logging.debug("LOCAL %s: Running client side program.", this_node_name) + + message = await reader.read(100) + assert message.decode("utf-8") == "receive_qubit" + + epr_socket_alice = EPRSocket(start_node_name) + epr_socket_charlie = EPRSocket(end_node_name) + + sockets = SocketsConfig(network_config, "default", NodeConfigType.APP) + + charlie_client = SimulaQronClassicalClient(sockets) + # To start executing quantum operations, we need to create a NetQASM connection + with NetQASMConnection(this_node_name, epr_sockets=[epr_socket_alice, epr_socket_charlie]) as bob: + # Receive an entangled qubit + epr_alice = epr_socket_alice.recv_keep()[0] + + # Create a new entangled with Charlie + epr_charlie = epr_socket_charlie.create_keep()[0] + + await charlie_client.connect_and_run(end_node_name, send_to_charlie) + + # Create the GHZ state by entangling the fresh qubit + epr_alice.cnot(epr_charlie) + + writer.write("continue".encode("utf-8")) + + # And simply measure it + m1 = epr_charlie.measure() + # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + m1_val = int(m1) + print(f"{this_node_name}: My outcome is '{m1_val}'") + return 0 + + +if __name__ == "__main__": + logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=logging.DEBUG, + force=True + ) + # We set the netqasm log level to "info" to avoid verbose output from the internals. + set_log_level(logging.INFO) + + # Load the simulaqron settings file + simulaqron_config_file = Path("simulaqron_settings.json") + simulaqron_settings.read_from_file(simulaqron_config_file) + + # Load the file network configuration file + # We still need this file to correctly interact with the SimulaQron backend (QNodeOS and Virtual Node) + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Some data for this node: + network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" + node_name = "Bob" # A node with this name *must* exist in "simulaqron_network.json" + start_node_name = "Alice" # A node with this name *must* exist in "simulaqron_network.json" + end_node_name = "Charlie" # A node with this name *must* exist in "simulaqron_network.json" + + classical_sockets = SocketsConfig(network_config, network_name, NodeConfigType.APP) + + server = SimulaQronClassicalServer(classical_sockets, node_name) + client = SimulaQronClassicalClient(classical_sockets) + + server.register_client_handler(bob_program) + server.start_serving() diff --git a/examples/new-sdk/extendGHZ/charlieTest.py b/examples/new-sdk/extendGHZ/charlieTest.py new file mode 100644 index 00000000..5746d182 --- /dev/null +++ b/examples/new-sdk/extendGHZ/charlieTest.py @@ -0,0 +1,79 @@ +import logging + +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalServer +from simulaqron.settings import network_config, simulaqron_settings + +# This is recipe to use NetQASM with simulaqron backend. +from netqasm.runtime.settings import set_simulator + +from simulaqron.settings.network_config import NodeConfigType + +set_simulator("simulaqron") + +# Importing NetQASM connection, Qubit and EPR socket must be *after* +# setting the simulator for NetQASM +from netqasm.logging.glob import set_log_level # noqa: E402 +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +# This function contains the code of the classical client +async def charlie_program(reader: StreamReader, writer: StreamWriter) -> int: + # This is "Charlie": the end node of the GHZ chain + this_node_name = "Charlie" + remote_node_name = "Bob" + logging.debug("LOCAL %s: Running client side program.", this_node_name) + + message = await reader.read(100) + assert message.decode("utf-8") == "receive_qubit" + epr_socket = EPRSocket(remote_node_name) + # To start executing quantum operations, we need to create a NetQASM connection + with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as charlie: + # Receive an entangled qubit + epr = epr_socket.recv_keep()[0] + + writer.write("continue".encode("utf-8")) + + print("here2") + # And simply measure it + m1 = epr.measure() + # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) + # *after* the connection is closed (or after flushing the connection, untested) + m1_val = int(m1) + + print(f"{this_node_name}: My outcome is '{m1_val}'") + return 0 + + +if __name__ == "__main__": + logging.basicConfig( + format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + level=logging.DEBUG, + force=True + ) + # We set the netqasm log level to "info" to avoid verbose output from the internals. + set_log_level(logging.INFO) + + # Load the simulaqron settings file + simulaqron_config_file = Path("simulaqron_settings.json") + simulaqron_settings.read_from_file(simulaqron_config_file) + + # Load the file network configuration file + # We still need this file to correctly interact with the SimulaQron backend (QNodeOS and Virtual Node) + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + # Some data for this node: + network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" + node_name = "Charlie" # A node with this name *must* exist in "simulaqron_network.json" + other_node_name = "Bob" + + sockets = SocketsConfig(network_config, network_name, NodeConfigType.APP) + + server = SimulaQronClassicalServer(sockets, node_name) + server.register_client_handler(charlie_program) + server.start_serving() diff --git a/examples/new-sdk/extendGHZ/doNew.sh b/examples/new-sdk/extendGHZ/doNew.sh new file mode 100755 index 00000000..9dda467e --- /dev/null +++ b/examples/new-sdk/extendGHZ/doNew.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +./terminate.sh +sleep 1 +./run.sh diff --git a/examples/new-sdk/extendGHZ/run.sh b/examples/new-sdk/extendGHZ/run.sh new file mode 100755 index 00000000..6f12a6bc --- /dev/null +++ b/examples/new-sdk/extendGHZ/run.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# Flag used to determine whether to start simulaqron backend or not +# This is useful when using this script to run the application in different machines. +START_SIMULAQRON=true + +# Process the arguments, if any +while [[ $# -gt 0 ]]; do + case $1 in + -n|--no-start-simulaqron) + START_SIMULAQRON=false + shift + ;; + esac +done + +if [ "$START_SIMULAQRON" = true ]; then + # Check if SimulaQron is already running + if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + # If not, start simulaqron backend for both nodes + simulaqron start --nodes=Alice,Bob,Charlie --network-config-file simulaqron_network.json --simulaqron-config-file simulaqron_settings.json + fi +fi + +python3 charlieTest.py & +sleep 1 +python3 bobTest.py & +sleep 1 +python3 aliceTest.py diff --git a/examples/new-sdk/extendGHZ/simulaqron_network.json b/examples/new-sdk/extendGHZ/simulaqron_network.json new file mode 100644 index 00000000..61b05b60 --- /dev/null +++ b/examples/new-sdk/extendGHZ/simulaqron_network.json @@ -0,0 +1,36 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + { + "Charlie": { + "app_socket": ["localhost", 8841], + "qnodeos_socket": ["localhost", 8842], + "vnode_socket": ["localhost", 8843] + } + }, + { + "David": { + "app_socket": ["localhost", 8871], + "qnodeos_socket": ["localhost", 8872], + "vnode_socket": ["localhost", 8873] + } + } + ], + "topology": null + } +] \ No newline at end of file diff --git a/examples/new-sdk/extendGHZ/simulaqron_settings.json b/examples/new-sdk/extendGHZ/simulaqron_settings.json new file mode 100644 index 00000000..bbfb47a2 --- /dev/null +++ b/examples/new-sdk/extendGHZ/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "projectq", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} \ No newline at end of file diff --git a/examples/new-sdk/extendGHZ/terminate.sh b/examples/new-sdk/extendGHZ/terminate.sh new file mode 100755 index 00000000..39747653 --- /dev/null +++ b/examples/new-sdk/extendGHZ/terminate.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +TEST_PIDS=$(ps aux | grep python | grep -E "Test" | awk {'print $2'}) +if [ "$TEST_PIDS" != "" ] +then + kill -9 $TEST_PIDS +fi + +# Check if SimulaQron is running +if [ -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + if ! simulaqron stop + then + # Kill the process, only if simulaqron could not be stopped gracefully + cat $HOME/.simulaqron_pids/simulaqron_network_default.pid | xargs kill -9 + fi +fi + diff --git a/simulaqron/sdk/protocol.py b/simulaqron/sdk/protocol.py index 1263e22f..c30904c0 100644 --- a/simulaqron/sdk/protocol.py +++ b/simulaqron/sdk/protocol.py @@ -18,6 +18,24 @@ def __init__(self, sockets_config: SocketsConfig): """ self._sockets_config = sockets_config + async def connect_and_run(self, server_name: str, callback: Coroutine[Any, Any, _T]) -> _T: + """ + "Async" version of the `run_client` method, which can be awaited in a context of a python coroutine. + For more information check the documentation of the `run_client` method. + + :param server_name: The name of the server to connect to. The name *must* exist in the + configuration file given when constructing this client. + :type server_name: str + :param callback: The function to be called when the connection is established. This function + implements the logic for interacting with the server. The passed function + *must* be a python "async" function. + :type callback: Callable[[StreamReader, StreamWriter], Awaitable[None]] + """ + if server_name not in self._sockets_config.hostDict: + raise RuntimeError(f"The node with name '{server_name}' is not on the network configuration.") + socket_config = self._sockets_config.hostDict[server_name] + return await self._run_client(socket_config.hostname, socket_config.port, callback) + async def _run_client(self, hostname: str, port: int, callback: Coroutine[Any, Any, _T]) -> _T: """ Python coroutine that opens the connection and runs the function provided by the user. @@ -63,14 +81,14 @@ async def connected_handler(reader: StreamReader, writer: StreamWriter): class SimulaQronClassicalServer: def __init__(self, sockets_config: SocketsConfig, name: str): """ - Classical server used to server classical clients to remote nodes. The given socket configs + Classical server used to serve classical clients sending classical messages. The given socket configs object contains the specification of the available nodes on the network that this server can interact with. Please note that this configuration *does not limit* the clients that can connect to this server. :param sockets_config: The sockets configuration for the whole network. :type sockets_config: SocketsConfig - :param name: The name of the server to connect to. The name *must* exist in the + :param name: The node name of the server. The name *must* exist in the configuration file given when constructing this server. :type name: str """ @@ -108,8 +126,8 @@ async def _build_server(self): self._sockets_data.hostname, self._sockets_data.port ) - print(f"BOB INFO: === {self._node_name} Server ===") - print(f"BOB DEBUG: Listening on {self._sockets_data.hostname}:{self._sockets_data.port}") # noqa: E231 + print(f"{self._node_name.lower()} INFO: === {self._node_name} Server ===") + print(f"{self._node_name.lower()} DEBUG: Listening on {self._sockets_data.hostname}:{self._sockets_data.port}") # noqa: E231 async with server: await server.serve_forever() From c0700b0e6f7c9a065e92c2d93daf2dcee5aebc2b Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 17:48:43 +0100 Subject: [PATCH 227/277] Switch examples to explicit flush(); add eventBased examples and docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Examples: use explicit flush() instead of with-block for multi-round programs All new-sdk examples now open explicitly, call to execute queued operations, read measurement results, then call — rather than wrapping everything in a block. The pattern works for single-round programs but makes it hard for students to extend to more realistic applications. To do classical processing between quantum rounds — measure, read the result, and apply a correction gate based on it — students need to call mid-circuit before the connection closes. The explicit pattern teaches this from the start, and scales naturally to multi-round protocols: sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) q = Qubit(sim_conn) m = q.measure() sim_conn.flush() # executes all queued operations; m is now readable result = int(m) # classical decision — more quantum ops can follow sim_conn.close() The example demonstrates this end-to-end: it measures a qubit mid-circuit, makes a classical decision, and applies a correction gate based on the result — a pattern that is impossible with the plain block. ## New: event-based examples Four new examples in implement a state-machine pattern for protocols that interleave classical negotiation with quantum operations: - — basic quantum ping-pong between two nodes - — adds a classical handshake before each quantum round - — correlated random number generation via entanglement - — same, with classical verification of correlations **nativeMode/graphState**: added selecting the backend; the default stabilizer backend raises for (used for Rz(−π/2) = S†). Updated accordingly. ## Documentation - reorganised into three levels (new-sdk, event-based, native-mode) with a short description of when to use each, and toctrees for all example families including the new event-based ones - (new): RST pages for all four event-based examples - (new): RST pages for all new-sdk examples - : removed stale warnings, updated config snippets from the old .cfg format to the current JSON format ## Tests - (new): verifies that makes measurement results readable mid-circuit, including multi-round programs with classical branching between flush calls - : added a clarifying comment explaining why merge tests belong in SimulaQron (not NetQASM) and what the tests actually exercise ## Core simulator fixes **flush() / _wait_for_done now works correctly** - gains a field so the client's loop can unblock for the right subroutine instead of spinning forever - Errors are deferred via and raised after exits, so can still run cleanly - and populate from , which sets before each subroutine **Shutdown race condition fixed** - Removed from in ; the decorator was triggering a premature reactor shutdown (causing mid-measurement) while also raising a **ProjectQ backend: numpy serialization** - now wraps values with ; Twisted PB cannot serialize and was silently producing objects on the receiving end, breaking nativeMode/teleport **QuTiP backend: probability clamping** - clamps and renormalises probabilities before to prevent from floating-point rounding after multi-qubit gate sequences (affected nativeMode/graphState) **Network process management** - Fixed typo → in - no longer hangs: then as fallback; removed duplicate call in Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 26 + docs/Examples.rst | 55 +- docs/event-based/Overview.rst | 78 ++ docs/event-based/PingPong.rst | 76 ++ docs/event-based/PolitePingPong.rst | 94 ++ docs/event-based/QuantumCorrRNG.rst | 104 +++ docs/event-based/QuantumCorrRNGVerified.rst | 129 +++ docs/native-mode/CorrRng.rst | 6 +- docs/native-mode/GraphState.rst | 74 +- docs/native-mode/Teleport.rst | 86 +- docs/native-mode/Template.rst | 43 +- docs/new-sdk/CorrRNG.rst | 84 ++ docs/new-sdk/ExtendGHZ.rst | 85 ++ docs/new-sdk/MidCircuitLogic.rst | 100 ++ docs/new-sdk/Overview.rst | 94 ++ docs/new-sdk/Teleport.rst | 99 ++ docs/new-sdk/Template.rst | 124 +++ examples/eventBased/pingPong/pingpongAlice.py | 56 ++ examples/eventBased/pingPong/pingpongBob.py | 71 ++ examples/eventBased/pingPong/run.sh | 29 + .../pingPong/simulaqron_network.json | 22 + .../pingPong/simulaqron_settings.json | 14 + examples/eventBased/pingPong/terminate.sh | 5 + .../eventBased/politePingPong/politeAlice.py | 154 ++++ .../eventBased/politePingPong/politeBob.py | 145 +++ examples/eventBased/politePingPong/run.sh | 29 + .../politePingPong/simulaqron_network.json | 22 + .../politePingPong/simulaqron_settings.json | 14 + .../eventBased/politePingPong/terminate.sh | 5 + .../eventBased/quantumCorrRNG/aliceTest.py | 135 +++ examples/eventBased/quantumCorrRNG/bobTest.py | 131 +++ examples/eventBased/quantumCorrRNG/run.sh | 32 + .../quantumCorrRNG/simulaqron_network.json | 22 + .../quantumCorrRNG/simulaqron_settings.json | 14 + .../eventBased/quantumCorrRNG/terminate.sh | 3 + .../quantumCorrRNGVerified/aliceTest.py | 162 ++++ .../quantumCorrRNGVerified/bobTest.py | 167 ++++ .../eventBased/quantumCorrRNGVerified/run.sh | 32 + .../simulaqron_network.json | 22 + .../simulaqron_settings.json | 14 + .../quantumCorrRNGVerified/terminate.sh | 3 + examples/nativeMode/graphState/run.sh | 2 +- .../graphState/simulaqron_settings.json | 14 + examples/netqasm/teleport/teleport.py | 56 -- .../example_client_bob.py | 4 +- .../example_server_alice.py | 4 +- examples/new-sdk/corrRNG/aliceTest.py | 33 +- examples/new-sdk/corrRNG/bobTest.py | 33 +- .../new-sdk/corrRNG/simulaqron_settings.json | 2 +- examples/new-sdk/extendGHZ/aliceTest.py | 37 +- examples/new-sdk/extendGHZ/bobTest.py | 44 +- examples/new-sdk/extendGHZ/charlieTest.py | 39 +- examples/new-sdk/midCircuitLogic/nodeTest.py | 100 ++ examples/new-sdk/midCircuitLogic/run.sh | 20 + .../midCircuitLogic/simulaqron_network.json | 15 + .../midCircuitLogic/simulaqron_settings.json | 14 + examples/new-sdk/midCircuitLogic/terminate.sh | 2 + examples/new-sdk/teleport/aliceTest.py | 35 +- examples/new-sdk/teleport/bobTest.py | 30 +- .../template-client-server/nodeTest-client.py | 47 +- .../template-client-server/nodeTest-server.py | 40 +- .../template-quantum-local/nodeTest.py | 35 +- simulaqron/netqasm_backend/executioner.py | 16 +- simulaqron/netqasm_backend/factory.py | 7 +- simulaqron/netqasm_backend/qnodeos.py | 114 ++- simulaqron/network.py | 11 +- simulaqron/run/run.py | 1 - simulaqron/sdk/connection.py | 33 +- .../virtual_node/project_q_simulator.py | 855 +++++++++--------- simulaqron/virtual_node/qutip_simulator.py | 22 + simulaqron/virtual_node/virtual.py | 1 - tests/quick/test_ping_pong.py | 87 ++ tests/quick/test_polite_ping_pong.py | 192 ++++ tests/slow/merges/test_merges.py | 8 + tests/slow/sdk/test_flush.py | 153 ++++ 75 files changed, 3967 insertions(+), 799 deletions(-) create mode 100644 CLAUDE.md create mode 100644 docs/event-based/Overview.rst create mode 100644 docs/event-based/PingPong.rst create mode 100644 docs/event-based/PolitePingPong.rst create mode 100644 docs/event-based/QuantumCorrRNG.rst create mode 100644 docs/event-based/QuantumCorrRNGVerified.rst create mode 100644 docs/new-sdk/CorrRNG.rst create mode 100644 docs/new-sdk/ExtendGHZ.rst create mode 100644 docs/new-sdk/MidCircuitLogic.rst create mode 100644 docs/new-sdk/Overview.rst create mode 100644 docs/new-sdk/Teleport.rst create mode 100644 docs/new-sdk/Template.rst create mode 100644 examples/eventBased/pingPong/pingpongAlice.py create mode 100644 examples/eventBased/pingPong/pingpongBob.py create mode 100644 examples/eventBased/pingPong/run.sh create mode 100644 examples/eventBased/pingPong/simulaqron_network.json create mode 100644 examples/eventBased/pingPong/simulaqron_settings.json create mode 100644 examples/eventBased/pingPong/terminate.sh create mode 100644 examples/eventBased/politePingPong/politeAlice.py create mode 100644 examples/eventBased/politePingPong/politeBob.py create mode 100644 examples/eventBased/politePingPong/run.sh create mode 100644 examples/eventBased/politePingPong/simulaqron_network.json create mode 100644 examples/eventBased/politePingPong/simulaqron_settings.json create mode 100644 examples/eventBased/politePingPong/terminate.sh create mode 100644 examples/eventBased/quantumCorrRNG/aliceTest.py create mode 100644 examples/eventBased/quantumCorrRNG/bobTest.py create mode 100644 examples/eventBased/quantumCorrRNG/run.sh create mode 100644 examples/eventBased/quantumCorrRNG/simulaqron_network.json create mode 100644 examples/eventBased/quantumCorrRNG/simulaqron_settings.json create mode 100644 examples/eventBased/quantumCorrRNG/terminate.sh create mode 100644 examples/eventBased/quantumCorrRNGVerified/aliceTest.py create mode 100644 examples/eventBased/quantumCorrRNGVerified/bobTest.py create mode 100644 examples/eventBased/quantumCorrRNGVerified/run.sh create mode 100644 examples/eventBased/quantumCorrRNGVerified/simulaqron_network.json create mode 100644 examples/eventBased/quantumCorrRNGVerified/simulaqron_settings.json create mode 100644 examples/eventBased/quantumCorrRNGVerified/terminate.sh mode change 100755 => 100644 examples/nativeMode/graphState/run.sh create mode 100644 examples/nativeMode/graphState/simulaqron_settings.json delete mode 100644 examples/netqasm/teleport/teleport.py create mode 100644 examples/new-sdk/midCircuitLogic/nodeTest.py create mode 100755 examples/new-sdk/midCircuitLogic/run.sh create mode 100644 examples/new-sdk/midCircuitLogic/simulaqron_network.json create mode 100644 examples/new-sdk/midCircuitLogic/simulaqron_settings.json create mode 100755 examples/new-sdk/midCircuitLogic/terminate.sh create mode 100644 tests/quick/test_ping_pong.py create mode 100644 tests/quick/test_polite_ping_pong.py create mode 100644 tests/slow/sdk/test_flush.py diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..3e89652c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,26 @@ +# SimulaQron Development Conventions + +## Hard Requirements + +### Examples +- Examples MUST always use separate files per node (e.g. `nodeTest-client.py`, `nodeTest-server.py`), never single-file programs. Students must be able to run each node on a separate machine. +- Follow the `new-sdk/template-client-server` structure: two node scripts + `run.sh` + `terminate.sh` + config JSONs. +- All nodes should print their own output (not rely on return values visible only to the test harness). + +### Connection Pattern +- Prefer creating a `NetQASMConnection` once and reusing it with `flush()` for multi-round quantum programs, rather than opening/closing with `with` each round. +- `conn.flush()` is the sync point that makes measurement results readable via `int(m)`. + +### Backends +- Use `stabilizer` backend by default in examples and tests unless non-Clifford gates are needed. +- `projectq` is deprecated / hard to install. Prefer `qutip` when full state simulation is needed. + +## File Conventions +- **Examples:** `examples/new-sdk/` — all new examples use the new SDK +- **Tests:** `tests/slow/sdk/` — SDK-level integration tests +- **Core code:** `simulaqron/` — simulator source +- **Docs:** `docs/` — Sphinx documentation (currently outdated, being rewritten) + +## Virtual Environment +- The project venv is at `.venv/` — activate with `source .venv/bin/activate` +- Install with `pip install -e .` (skip `[test]` if projectq build fails) diff --git a/docs/Examples.rst b/docs/Examples.rst index eb2fec9d..190792bf 100644 --- a/docs/Examples.rst +++ b/docs/Examples.rst @@ -1,21 +1,45 @@ -Programming via SimulaQron's native Python Twisted Interface (specific to SimulaQron) -===================================================================================== +SimulaQron Programming Examples +================================ -One way to program SimulaQron is directly via its 'native interface' using Twisted. -This means writing a client program connecting directly to the local virtual quantum node, and issuing instructions -to such simulated quantum hardware. Programming SimulaQron in its native interface is evidently Python specific, and -meant primarily as an internal interface allowing one to explore higher level abstractions built on top of it. -One such abstraction is the NetQASM interface.For programming in a universal, i.e., not Python specific interface -see :doc:`NetQASM`. +SimulaQron offers three ways to write quantum network programs, from highest-level to lowest-level: -The examples below assume that you have already made your way through :doc:`GettingStarted`: you have the virtual -node servers up and running, and ran the simple example of generating correlated randomness. Further examples can -also be found in examples/nativeMode. +1. **New SDK** (``examples/new-sdk/``) — The recommended approach using the NetQASM SDK. + Programs use ``NetQASMConnection`` and ``EPRSocket`` for quantum operations, and + ``SimulaQronClassicalClient``/``SimulaQronClassicalServer`` for classical messaging. + Start here if you are new to SimulaQron. -.. warning:: Update the link to the CQC interface. +2. **Event-based** (``examples/eventBased/``) — Builds on the new SDK by adding a state-machine + pattern for classical messaging. Each node defines states, message handlers, and a dispatch + table. This is the recommended pattern for protocols that interleave classical negotiation + with quantum operations. -.. note:: The 'native' mode is not the recommended way to program applications for SimulaQron, instead use the - `NetQASM `_ interface. +3. **Native mode** (``examples/nativeMode/``) — The low-level Twisted interface that talks + directly to SimulaQron's virtual quantum nodes. This is Python-specific and more verbose, + but gives full control over the simulation backend. + +The examples below assume that you have already made your way through :doc:`GettingStarted`: +you have the virtual node servers up and running. + +.. toctree:: + :maxdepth: 2 + :caption: New SDK examples: + + new-sdk/Overview + new-sdk/Template + new-sdk/CorrRNG + new-sdk/Teleport + new-sdk/ExtendGHZ + new-sdk/MidCircuitLogic + +.. toctree:: + :maxdepth: 2 + :caption: Event-based examples: + + event-based/Overview + event-based/PingPong + event-based/PolitePingPong + event-based/QuantumCorrRNG + event-based/QuantumCorrRNGVerified .. toctree:: :maxdepth: 2 @@ -25,6 +49,3 @@ also be found in examples/nativeMode. native-mode/Template native-mode/Teleport native-mode/GraphState - - - diff --git a/docs/event-based/Overview.rst b/docs/event-based/Overview.rst new file mode 100644 index 00000000..512af163 --- /dev/null +++ b/docs/event-based/Overview.rst @@ -0,0 +1,78 @@ +Event-Based Programming Overview +================================ + +The event-based examples show how to structure quantum network protocols using +a **state machine** pattern. This is the recommended approach for protocols that +interleave classical negotiation with quantum operations. + +Prerequisites: you should understand the :doc:`../new-sdk/Overview` first. + +The state machine pattern +------------------------- + +Each node's behaviour is defined by: + +1. **States** — string constants naming each phase of the protocol +2. **Handlers** — async functions that execute when a specific message arrives in + a specific state. Each handler performs an action and returns the next state. +3. **Dispatch table** — a dictionary mapping ``(current_state, message)`` to handler. + This *is* the state machine. +4. **Event loop** — reads messages, looks up the transition, calls the handler. + +Example structure:: + + # States + STATE_WAITING_ACCEPT = "WAITING_ACCEPT" + STATE_DONE = "DONE" + + # Handler + async def handle_yes(writer: StreamWriter) -> str: + # ... do something ... + return STATE_DONE + + # Dispatch table + DISPATCH = { + (STATE_WAITING_ACCEPT, "yes"): handle_yes, + } + + # Event loop + async def run_alice(reader, writer): + state = STATE_WAITING_ACCEPT + while state != STATE_DONE: + data = await reader.read(255) + msg = data.decode("utf-8") + handler = DISPATCH.get((state, msg)) + if handler is None: + continue # ignore unknown messages + state = await handler(writer) + +Why this pattern? +----------------- + +- **Clear protocol structure**: the dispatch table maps directly to a state + transition diagram, making the protocol easy to read and verify. +- **Extensible**: adding a new state or message is just adding a handler and + a dispatch entry — no changes to the event loop. +- **Quantum integration**: handlers can create ``NetQASMConnection``, perform + quantum operations, and ``flush()`` — keeping quantum code isolated in + focused handler functions. + +Examples +-------- + +The examples progress from purely classical to quantum: + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Example + - What it teaches + * - :doc:`PingPong` + - Basic event loop with simple if/else message handling + * - :doc:`PolitePingPong` + - Full state machine pattern with dispatch table + * - :doc:`QuantumCorrRNG` + - Adding quantum operations (EPR + measure) to event handlers + * - :doc:`QuantumCorrRNGVerified` + - Multi-state protocol: negotiate, quantum, then classical verification diff --git a/docs/event-based/PingPong.rst b/docs/event-based/PingPong.rst new file mode 100644 index 00000000..60796191 --- /dev/null +++ b/docs/event-based/PingPong.rst @@ -0,0 +1,76 @@ +Ping-Pong: Basic Event Loop +=========================== + +The simplest event-based example. Alice sends a sequence of messages to Bob, +who replies to each one. No state machine — just a simple if/else to choose +the reply. Found in ``examples/eventBased/pingPong/``. + +This is purely classical (no quantum operations). It demonstrates the basic +async event loop pattern before adding state machines and quantum in later examples. + +Alice (client) +-------------- + +Alice sends a list of messages and prints Bob's replies:: + + async def run_alice(reader: StreamReader, writer: StreamWriter): + messages = ["ping", "ping", "hello", "ping"] + + for msg in messages: + print(f"Alice: sending '{msg}'") + writer.write(msg.encode("utf-8")) + await writer.drain() + + reply_data = await reader.read(255) + reply = reply_data.decode("utf-8") + print(f"Alice: received '{reply}'") + +Bob (server) +------------ + +Bob replies "pong" to "ping" and "no way!" to anything else:: + + async def run_bob(reader: StreamReader, writer: StreamWriter): + while True: + data = await reader.read(255) + if not data: + break # Alice disconnected + + message = data.decode("utf-8") + if message == "ping": + reply = "pong" + else: + reply = "no way!" + + writer.write(reply.encode("utf-8")) + await writer.drain() + +Key concepts +------------ + +- **reader/writer**: The async streams for classical TCP communication. + ``writer.write()`` sends bytes, ``await reader.read(N)`` receives up to N bytes. +- **await writer.drain()**: Ensures the write buffer is flushed to the network. +- **Connection lifecycle**: Bob's loop exits when ``reader.read()`` returns empty + bytes, meaning Alice has disconnected. + +Running +------- + +:: + + cd examples/eventBased/pingPong + sh run.sh + +Expected output:: + + Alice: sending 'ping' + Bob: received 'ping' + Bob: sending 'pong' + Alice: received 'pong' + Alice: sending 'ping' + ... + Alice: sending 'hello' + Bob: received 'hello' + Bob: sending 'no way!' + Alice: received 'no way!' diff --git a/docs/event-based/PolitePingPong.rst b/docs/event-based/PolitePingPong.rst new file mode 100644 index 00000000..81b00a14 --- /dev/null +++ b/docs/event-based/PolitePingPong.rst @@ -0,0 +1,94 @@ +Polite Ping-Pong: The State Machine Pattern +============================================ + +Extends the basic ping-pong into a proper state machine with dispatch tables. +Alice and Bob have a polite exchange: ping, pong, thank you, you're welcome. +Found in ``examples/eventBased/politePingPong/``. + +This is still purely classical, but introduces the **state machine pattern** +that all subsequent quantum examples will use. + +Alice's state diagram +--------------------- + +:: + + (connect) + │ send "ping" + ▼ + WAITING_PONG + │ recv "pong" → send "thank you" + ▼ + WAITING_YOURE_WELCOME + │ recv "you're welcome" + ▼ + DONE + +Alice's dispatch table +---------------------- + +:: + + ALICE_DISPATCH = { + (STATE_WAITING_PONG, "pong"): handle_pong, + (STATE_WAITING_YOURE_WELCOME, "you're welcome"): handle_youre_welcome, + } + +Each handler is a focused function that performs one action and returns the next state:: + + async def handle_pong(writer: StreamWriter) -> str: + reply = "thank you" + writer.write(reply.encode("utf-8")) + await writer.drain() + return STATE_WAITING_YOURE_WELCOME + +The event loop +-------------- + +The event loop is generic — it works for any protocol by just changing the dispatch +table and initial state:: + + async def run_alice(reader: StreamReader, writer: StreamWriter): + # Initial action (before the loop) + writer.write(b"ping") + await writer.drain() + + state = STATE_WAITING_PONG + + while state != STATE_DONE: + data = await reader.read(255) + if not data: + break + msg = data.decode("utf-8") + + handler = ALICE_DISPATCH.get((state, msg)) + if handler is None: + print(f"no transition for '{msg}' — ignoring.") + continue + + state = await handler(writer) + +Key concepts +------------ + +- **Dispatch table = state machine**: the ``(state, message) → handler`` mapping + *is* the protocol definition. Every valid transition is listed; anything not + listed is automatically rejected. +- **Handlers are independent**: each handler only knows about its own transition, + making the code modular and easy to extend. +- **The event loop is reusable**: the same loop structure works for every example + that follows. + +Bob's side +---------- + +Bob uses the same pattern with his own states (``WAITING_PING``, ``WAITING_THANKS``, +``DONE``) and dispatch table. See ``politeBob.py`` for the full code. + +Running +------- + +:: + + cd examples/eventBased/politePingPong + sh run.sh diff --git a/docs/event-based/QuantumCorrRNG.rst b/docs/event-based/QuantumCorrRNG.rst new file mode 100644 index 00000000..bc956c77 --- /dev/null +++ b/docs/event-based/QuantumCorrRNG.rst @@ -0,0 +1,104 @@ +Quantum Correlated RNG: Adding Quantum to Event Handlers +========================================================= + +This example combines the state machine pattern with quantum operations. +Alice proposes generating shared randomness; if Bob agrees, both create an EPR +pair and measure their half. Found in ``examples/eventBased/quantumCorrRNG/``. + +This is the first example that runs quantum operations inside an event handler. + +The protocol +------------ + +1. Alice sends "generate randomness?" to Bob. +2. Bob replies "yes". +3. Both create an EPR pair and measure their half. +4. Both get the same correlated random bit. + +Alice's state diagram +--------------------- + +:: + + (connect) + │ send "generate randomness?" + ▼ + WAITING_ACCEPT + │ recv "yes" → EPR create + measure + ▼ + DONE + +Quantum inside a handler +------------------------- + +The key insight: quantum operations happen inside a handler function, using the +same ``sim_conn`` + ``flush()`` pattern from the new-sdk examples:: + + async def handle_yes(writer: StreamWriter) -> str: + epr_socket = EPRSocket("Bob") + + # Open a connection to the quantum backend (not to Bob — that's the + # classical reader/writer). + sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + + # Create an EPR pair with Bob and keep our half + epr = epr_socket.create_keep()[0] + m = epr.measure() + + # flush() sends all queued quantum operations to the backend and waits + # for them to complete. After this, int(m) gives the real value. + sim_conn.flush() + + result = int(m) + sim_conn.close() + + print(f"Alice: my random bit is {result}") + return STATE_DONE + +Bob's handler is symmetric, using ``recv_keep()`` instead of ``create_keep()``. + +Bob's handler +------------- + +:: + + async def handle_generate(writer: StreamWriter) -> str: + # Classical: agree + writer.write("yes".encode("utf-8")) + await writer.drain() + + # Quantum: receive our half of the EPR pair + epr_socket = EPRSocket("Alice") + sim_conn = NetQASMConnection("Bob", epr_sockets=[epr_socket]) + + epr = epr_socket.recv_keep()[0] + m = epr.measure() + sim_conn.flush() + + result = int(m) + sim_conn.close() + + print(f"Bob: my random bit is {result}") + return STATE_DONE + +Key concepts +------------ + +- **Quantum in handlers**: The handler creates a ``NetQASMConnection``, performs + quantum operations, calls ``flush()``, reads results, and closes — all within + one state transition. +- **Classical before quantum**: Bob first sends "yes" (classical), *then* does the + quantum operation. The state machine cleanly separates negotiation from execution. +- **sim_conn vs reader/writer**: ``sim_conn`` talks to the local quantum backend. + ``reader``/``writer`` talk to the other node classically. These are independent. + +Running +------- + +:: + + cd examples/eventBased/quantumCorrRNG + sh run.sh + +.. note:: Unlike the purely classical examples, this one requires the SimulaQron + backend to be running. The ``run.sh`` script starts it automatically. diff --git a/docs/event-based/QuantumCorrRNGVerified.rst b/docs/event-based/QuantumCorrRNGVerified.rst new file mode 100644 index 00000000..a84ecfc1 --- /dev/null +++ b/docs/event-based/QuantumCorrRNGVerified.rst @@ -0,0 +1,129 @@ +Quantum Correlated RNG with Verification +========================================= + +Extends the previous example by adding a classical verification step after the +quantum measurement. After both nodes measure their EPR halves, Alice sends her +result to Bob so he can confirm the correlation. +Found in ``examples/eventBased/quantumCorrRNGVerified/``. + +This demonstrates the full cycle: +**classical negotiation → quantum operation → classical verification** + +The protocol +------------ + +1. Alice sends "generate randomness?" to Bob. +2. Bob replies "yes", then both create an EPR pair and measure. +3. Alice sends her measurement result to Bob. +4. Bob compares both results and sends back a verification message. + +Alice's state diagram +--------------------- + +:: + + (connect) + │ send "generate randomness?" + ▼ + WAITING_ACCEPT + │ recv "yes" → EPR create + measure, send result to Bob + ▼ + WAITING_VERIFICATION + │ recv "verified: ..." + ▼ + DONE + +Bob's state diagram +------------------- + +:: + + WAITING_PROPOSAL + │ recv "generate randomness?" → send "yes", EPR recv + measure + ▼ + WAITING_ALICE_RESULT + │ recv Alice's bit → compare, send verdict + ▼ + DONE + +Multi-state quantum protocol +----------------------------- + +The key difference from the simple version: Bob now has **two states** after the +quantum operation. His handler stores the measurement result, and a second handler +processes Alice's result:: + + bob_result = None # module-level storage + + async def handle_generate(writer: StreamWriter) -> str: + global bob_result + + # Classical: agree + writer.write("yes".encode("utf-8")) + await writer.drain() + + # Quantum: receive EPR pair and measure + epr_socket = EPRSocket("Alice") + sim_conn = NetQASMConnection("Bob", epr_sockets=[epr_socket]) + epr = epr_socket.recv_keep()[0] + m = epr.measure() + sim_conn.flush() + + bob_result = int(m) + sim_conn.close() + return STATE_WAITING_ALICE_RESULT + + async def handle_alice_result(writer: StreamWriter, alice_result: int) -> str: + match = alice_result == bob_result + verdict = f"verified: Alice={alice_result}, Bob={bob_result}, match={match}" + writer.write(verdict.encode("utf-8")) + await writer.drain() + return STATE_DONE + +Handling variable messages +-------------------------- + +In ``WAITING_ALICE_RESULT``, the message content is Alice's measurement bit — it +could be "0" or "1". Since the exact message is not known ahead of time, Bob's +event loop handles this state specially rather than through the dispatch table:: + + while state != STATE_DONE: + data = await reader.read(255) + msg = data.decode("utf-8") + + # Special handling: in WAITING_ALICE_RESULT, the message IS the data + if state == STATE_WAITING_ALICE_RESULT: + alice_bit = int(msg) + state = await handle_alice_result(writer, alice_bit) + continue + + # Normal dispatch for other states + handler = BOB_DISPATCH.get((state, msg)) + ... + +Key concepts +------------ + +- **Multi-phase protocols**: The state machine naturally handles protocols with + multiple phases (negotiate → quantum → verify). +- **Data-carrying messages**: Not all messages are fixed strings. When a state + expects variable data (like a measurement result), handle it directly in the + event loop rather than through the dispatch table. +- **Cross-handler state**: Use module-level variables (like ``bob_result``) to + pass data between handlers that execute in different states. + +Running +------- + +:: + + cd examples/eventBased/quantumCorrRNGVerified + sh run.sh + +Expected output:: + + Alice: sending 'generate randomness?' + Bob: sending 'yes' + Alice: my random bit is 0 + Bob: my random bit is 0 + Bob: verified: Alice=0, Bob=0, match=True diff --git a/docs/native-mode/CorrRng.rst b/docs/native-mode/CorrRng.rst index f3a4378d..bffa5beb 100644 --- a/docs/native-mode/CorrRng.rst +++ b/docs/native-mode/CorrRng.rst @@ -1,8 +1,6 @@ Generate correlated randomness ============================== -.. warning:: Update the code of the examples below!!! - Having started the virtual quantum nodes, let us now run a simple test application, which already illustrates some of the aspects in realizing protocols. Our objective will be to realize the following protocol which will generate 1 shared random bit between Alice and Bob. Evidently, there would be classical means to achieve this trivial task chosen @@ -53,8 +51,8 @@ To realize this, we thus need not only the connection to the virtual quantum nod themselves need to run a client/server to exchange classical control information. Before looking at the code, we know that the setup of these servers is again determined by a configuration file, namely ``classicalNet.json``. This file defines which nodes act as servers in the classical communication network listening for control information -to execute the protocol. You want to copy this to whatever example you are running. It takes the same format as -above, where in our example only Bob will act run a server:: +to execute the protocol. You want to copy this to whatever example you are running. It takes the JSON format, +where in our example we have Alice and Bob:: [ { diff --git a/docs/native-mode/GraphState.rst b/docs/native-mode/GraphState.rst index 8ca6e7f7..1a15e766 100644 --- a/docs/native-mode/GraphState.rst +++ b/docs/native-mode/GraphState.rst @@ -1,8 +1,6 @@ Distributing a graph state ========================== -.. warning:: Update the code of the examples below!!! - Here we consider a more complicated example, where we have four parties; Alice, Bob, Charlie and David. They will distribute a graph state and transform this with local operations and classical communication to make a GHZ-like state. @@ -96,40 +94,48 @@ Although the order described here is slightly different the end result is still Setting up ----------- -.. warning:: Update the CFG files into a JSON ones and paste them here! -.. warning:: Re-write this description to use the single JSON file, and get the classical sockets config using the - the SocketsConfig builder + filter method. - -We will run everything locally (localhost) using the standard virtualNodes.json file found in config that define the -virtual quantum nodes run in the background to simulate the quantum hardware:: - - # Network configuration file - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - - Alice, localhost, 8801 - Bob, localhost, 8802 - Charlie, localhost, 8803 - David, localhost, 8804 +We will run everything locally (localhost) using a single ``classicalNet.json`` configuration file that defines all +nodes and their socket assignments:: + + [ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + }, + { + "Charlie": { + "app_socket": ["localhost", 8841], + "qnodeos_socket": ["localhost", 8842], + "vnode_socket": ["localhost", 8843] + } + }, + { + "David": { + "app_socket": ["localhost", 8871], + "qnodeos_socket": ["localhost", 8872], + "vnode_socket": ["localhost", 8873] + } + } + ], + "topology": null + } + ] As we can see from the protocol above, Alice is the one that initializes the protocol and the others listen. We will -therefore run a client at Alice and servers at Bob, Charlie and David. Since we run everything locally, we may thus -use for the configuration file classicalNet.json:: - - # Network configuration file - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - # - - Bob, localhost, 8812 - Charlie, localhost, 8813 - David, localhost, 8814 +therefore run a client at Alice and servers at Bob, Charlie and David. Let us now provide the actual program code for all the parties. diff --git a/docs/native-mode/Teleport.rst b/docs/native-mode/Teleport.rst index 8dc77a4d..0c5f0b0d 100644 --- a/docs/native-mode/Teleport.rst +++ b/docs/native-mode/Teleport.rst @@ -1,9 +1,6 @@ Teleporting a Qubit =================== -.. warning:: Update the code of the examples below!!! -.. warning:: Update the configuration file to use the JSON format!!! - Let's now consider a very simple protocol, in which Alice first generates an EPR pair with Bob, and then teleports a qubit to Bob. To program it in SimulaQron's native mode, we will use the template described in :doc:`Template`. @@ -30,18 +27,31 @@ For completeness, let's briefly recap the protocol: Setting up ----------- -We will run everything locally (localhost) using the standard virtualNodes.json file that defines the nodes that run -the virtual quantum node in the background to simulate the quantum hardware:: - - # Network configuration file - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - - Alice, localhost, 8801 - Bob, localhost, 8802 +We will run everything locally (localhost) using the standard ``simulaqron_network.json`` file that defines the +nodes that run the virtual quantum node in the background to simulate the quantum hardware:: + + [ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + } + ], + "topology": null + } + ] We use this same file to specify the communication channels (sockets) for passing classical messages between the declared nodes. The loaded network configuration can be used to construct ``SocketsConfig`` objects that contain the @@ -85,9 +95,19 @@ Since Alice acts as a client, we will only need to fill in runClientNode. This g # Prepare the first one in the |-> state yield q1.callRemote("apply_H") - # For information purposes, let's print the state of that qubit - (R,I) = yield q1.callRemote("get_qubit") - print("Qubit to be teleported is: ", assemble_qubit(R,I)) + # For information purposes, let's print the state of that qubit. + # The method to retrieve the state depends on the simulation backend. + if simulaqron_settings.sim_backend.value == "qutip": + realRho, imagRho = yield q1.callRemote("get_qubit") + state = np.array(assemble_qubit(realRho, imagRho), dtype=complex) + elif simulaqron_settings.sim_backend.value == "projectq": + _, (realvec, imagvec) = yield virtRoot.callRemote("get_register_RI", q1) + state = [r + (1j * j) for r, j in zip(realvec, imagvec)] + elif simulaqron_settings.sim_backend.value == "stabilizer": + array, _ = yield virtRoot.callRemote("get_register_RI", q1) + state = StabilizerState(array) + + print("Qubit to be teleported is:\n{}".format(state)) # Put qubits A and B in an EPR state yield qA.callRemote("apply_H") @@ -172,23 +192,19 @@ matrix of the qubit at the end for illustration.:: if a == 1: yield eprB.callRemote("apply_Z") - # Just print the qubit we received - (realRho, imagRho) = yield eprB.callRemote("get_qubit") - rho = self.assemble_qubit(realRho, imagRho) - - print("Qubit is:", rho) - - def assemble_qubit(self, realM, imagM): - """ - Reconstitute the qubit as a qutip object from its real and imaginary components given as a list. - We need this since Twisted PB does not support sending complex valued object natively. - """ - M = realM - for s in range(len(M)): - for t in range(len(M)): - M[s][t] = realM[s][t] + 1j * imagM[s][t] - - return Qobj(M) + # Just print the qubit we received. + # The method to retrieve the state depends on the simulation backend. + if simulaqron_settings.sim_backend.value == "qutip": + (realRho, imagRho) = yield eprB.callRemote("get_qubit") + state = np.array(assemble_qubit(realRho, imagRho), dtype=complex) + elif simulaqron_settings.sim_backend.value == "projectq": + _, (realvec, imagvec) = yield self.virtRoot.callRemote("get_register_RI", eprB) + state = [r + (1j * j) for r, j in zip(realvec, imagvec)] + elif simulaqron_settings.sim_backend.value == "stabilizer": + array, _, = yield self.virtRoot.callRemote("get_register_RI", eprB) + state = StabilizerState(array) + + print(f"Qubit is: \n{state}") -------- Starting diff --git a/docs/native-mode/Template.rst b/docs/native-mode/Template.rst index 0b679d8a..91350e18 100644 --- a/docs/native-mode/Template.rst +++ b/docs/native-mode/Template.rst @@ -1,10 +1,6 @@ Template for programming in native mode ======================================= -.. warning:: Update the code of the examples below!!! -.. warning:: Update the configuration file to use the JSON format!!! -.. warning:: Update the explanation about how SimulaQron uses the ``simulaqron_network.json`` file!!!! - In examples/nativeMode/template you will find a template that allows you to start programming relatively easily by filling in the relevant parts of the template. Let us now discuss this template in detail: @@ -17,19 +13,32 @@ filling in the relevant parts of the template. Let us now discuss this template which nodes may simply be a client program that connects to the servers to deliver messages. Let us here simply assume, Alice only sends information to Bob, who then acts upon the message received. In this case, we would make Alice a client and Bob a server. Note that one node can obviously fulfill both roles. -#. The template will look for a file called ``simulaqron_network.json`` in the local directory to determine who acts - as a server and what that nodes address details are. Simulaqron will decide (lexicographically) which nodes acts as - a server. An example of the network configuration file is as follows:: - - # Network configuration file - # - # For each host its informal name, as well as its location in the network must - # be listed. - # - # [name], [hostname], [port number] - # - - Bob, localhost, 8812 +#. The template will look for a network configuration JSON file in the local directory to determine node + addresses and socket assignments. SimulaQron will decide (lexicographically) which nodes act as servers. + An example of the network configuration file (``network_config.json``) is as follows:: + + [ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + } + ], + "topology": null + } + ] #. The next step is to check that on each network computer that you will run on, the global configuration file starting the virtual quantum nodes is set up correctly. See :doc:`../GettingStarted` on how to perform such a diff --git a/docs/new-sdk/CorrRNG.rst b/docs/new-sdk/CorrRNG.rst new file mode 100644 index 00000000..145e3c23 --- /dev/null +++ b/docs/new-sdk/CorrRNG.rst @@ -0,0 +1,84 @@ +Correlated Random Number Generation +==================================== + +This example demonstrates how two nodes can generate a shared random bit using +quantum entanglement. Found in ``examples/new-sdk/corrRNG/``. + +The protocol +------------ + +1. Alice creates an EPR pair (two maximally entangled qubits) with Bob. +2. Both Alice and Bob measure their respective qubits. +3. Because the qubits are entangled, both measurements yield the same random bit. + +The state before measurement is: + +.. math:: |\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right) + +Alice's code +------------ + +From ``aliceTest.py``:: + + def run_alice(this_node_name: str, remote_node_name: str) -> int: + epr_socket = EPRSocket(remote_node_name) + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob. + # Bob is reached via EPRSocket for quantum and reader/writer for classical. + sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) + + # Create an entangled qubit + epr = epr_socket.create_keep()[0] + + # And simply measure it + m1 = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 is just a future/promise. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). + m1_val = int(m1) + sim_conn.close() + return m1_val + +Bob's code +---------- + +From ``bobTest.py`` — the only difference is ``recv_keep()`` instead of ``create_keep()``:: + + def run_bob(this_node_name: str, remote_node_name: str) -> int: + epr_socket = EPRSocket(remote_node_name) + sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) + + # Receive an entangled qubit (Alice created the pair) + epr = epr_socket.recv_keep()[0] + + m1 = epr.measure() + sim_conn.flush() + + m1_val = int(m1) + sim_conn.close() + return m1_val + +Key concepts +------------ + +- **EPRSocket** handles entanglement between nodes. One side calls ``create_keep()``, + the other calls ``recv_keep()``. +- **sim_conn** is the connection to the quantum backend, not to the other node. +- Both nodes get the same random bit because measuring one half of an EPR pair + determines the outcome of the other. + +Running +------- + +:: + + cd examples/new-sdk/corrRNG + sh run.sh + +Expected output (the bit value is random):: + + Alice: My Random Number is '0' + Bob: My Random Number is '0' diff --git a/docs/new-sdk/ExtendGHZ.rst b/docs/new-sdk/ExtendGHZ.rst new file mode 100644 index 00000000..b45d7978 --- /dev/null +++ b/docs/new-sdk/ExtendGHZ.rst @@ -0,0 +1,85 @@ +Extending a GHZ State +===================== + +This example creates a three-party GHZ (Greenberger-Horne-Zeilinger) state +across Alice, Bob, and Charlie. Found in ``examples/new-sdk/extendGHZ/``. + +A GHZ state is a maximally entangled state shared between three (or more) parties: + +.. math:: |GHZ\rangle = \frac{1}{\sqrt{2}} \left(|000\rangle + |111\rangle\right) + +The protocol +------------ + +1. Alice creates an EPR pair with Bob and tells Bob to proceed. +2. Bob receives Alice's entangled qubit, creates a *new* EPR pair with Charlie, + and applies a CNOT to extend the entanglement into a GHZ state. +3. Charlie receives his entangled qubit from Bob. +4. All three measure — their outcomes are correlated. + +The communication flow is:: + + Alice ──EPR──► Bob ──EPR──► Charlie + Alice ──msg──► Bob ──msg──► Charlie + Alice ◄──msg── Bob ◄──msg── Charlie + +Alice's code +------------ + +From ``aliceTest.py``:: + + async def run_alice(reader: StreamReader, writer: StreamWriter) -> int: + epr_socket = EPRSocket("Bob") + + # sim_conn is our connection to the quantum backend, not to Bob. + sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + + # Create an entangled qubit with Bob + epr = epr_socket.create_keep()[0] + + # Tell Bob to proceed + writer.write("receive_qubit".encode("utf-8")) + answer = await reader.read(100) + assert answer.decode("utf-8") == "continue" + + m1 = epr.measure() + sim_conn.flush() + m1_val = int(m1) + sim_conn.close() + return m1_val + +Bob's code +---------- + +Bob is the key node — he has EPR sockets to *both* Alice and Charlie:: + + sim_conn = NetQASMConnection("Bob", + epr_sockets=[epr_socket_alice, epr_socket_charlie]) + + # Receive entangled qubit from Alice + epr_alice = epr_socket_alice.recv_keep()[0] + + # Create new entangled pair with Charlie + epr_charlie = epr_socket_charlie.create_keep()[0] + + # Extend into GHZ: CNOT from Alice's qubit onto Charlie's + epr_alice.cnot(epr_charlie) + +Key concepts +------------ + +- **Multiple EPR sockets**: A single ``NetQASMConnection`` can hold EPR sockets + to multiple remote nodes. +- **Three-party coordination**: Bob acts as both a server (for Alice) and a client + (to Charlie), using both ``SimulaQronClassicalServer`` and + ``SimulaQronClassicalClient``. +- **CNOT extends entanglement**: Applying CNOT between two entangled qubits from + different pairs creates a GHZ state. + +Running +------- + +:: + + cd examples/new-sdk/extendGHZ + sh run.sh diff --git a/docs/new-sdk/MidCircuitLogic.rst b/docs/new-sdk/MidCircuitLogic.rst new file mode 100644 index 00000000..9d5d012d --- /dev/null +++ b/docs/new-sdk/MidCircuitLogic.rst @@ -0,0 +1,100 @@ +Mid-Circuit Classical Logic +========================== + +This example demonstrates how to use ``flush()`` to read measurement results +*during* a quantum program, make classical decisions, and continue with more +quantum operations — all within a single connection. +Found in ``examples/new-sdk/midCircuitLogic/``. + +Why mid-circuit logic? +---------------------- + +Without ``flush()``, measurement results are futures that only resolve when +the connection closes. With ``flush()``, you can read them at any point and +branch your program accordingly. + +This is essential for protocols like quantum error correction, adaptive +measurements, and feed-forward circuits. + +The protocol +------------ + +A simple 3-round protocol on a single node: + +1. **Round 1**: Prepare :math:`|+\rangle` and measure. +2. **Round 2**: Based on round 1's outcome, prepare the *opposite* state. +3. **Round 3**: Based on the XOR of rounds 1 and 2, decide what to prepare. + +The code +-------- + +From ``nodeTest.py``:: + + def run_node(node_name: str): + results = [] + + # sim_conn is our connection to the quantum backend (SimulaQron). + # All qubit operations are queued through this connection. + sim_conn = NetQASMConnection(node_name) + + # --- Round 1: prepare |+> and measure --- + q1 = Qubit(sim_conn) + q1.H() + m1 = q1.measure() + # flush() after each round so we can use the result to decide + # what to do next (mid-circuit classical logic). + sim_conn.flush() + # int(m) extracts the measurement outcome — only valid after flush(). + r1 = int(m1) + results.append(r1) + + # --- Round 2: classical decision --- + # If round 1 gave 0, prepare |1>. If 1, prepare |0>. + q2 = Qubit(sim_conn) + if r1 == 0: + q2.X() + m2 = q2.measure() + sim_conn.flush() + r2 = int(m2) + results.append(r2) + + # --- Round 3: compound classical logic --- + xor = r1 ^ r2 + q3 = Qubit(sim_conn) + if xor: + q3.X() + m3 = q3.measure() + sim_conn.flush() + r3 = int(m3) + results.append(r3) + + sim_conn.close() + +Key concepts +------------ + +- **Multiple flush() calls**: Each ``flush()`` is a sync point. Between flushes, + you can use Python control flow (``if``/``else``, loops) based on previous + measurement results. +- **Single connection**: You do not need to create a new ``NetQASMConnection`` for + each round — create it once, flush multiple times, close at the end. +- **New qubits after flush**: You can allocate new ``Qubit`` objects on the same + connection after a flush — earlier qubits that were measured are gone, but the + connection stays open. + +Expected output +--------------- + +:: + + Round 1: measured |+> -> 0 (or 1) + Round 2: measured -> 1 (always opposite of round 1) + Round 3: measured -> 1 (always equals r1 XOR r2, which is always 1) + +Running +------- + +:: + + cd examples/new-sdk/midCircuitLogic + sh run.sh diff --git a/docs/new-sdk/Overview.rst b/docs/new-sdk/Overview.rst new file mode 100644 index 00000000..5fda5af1 --- /dev/null +++ b/docs/new-sdk/Overview.rst @@ -0,0 +1,94 @@ +New SDK Overview +================ + +The new SDK is the recommended way to write quantum network programs for SimulaQron. +It uses the **NetQASM** library for quantum operations and SimulaQron's own classical +networking classes for message passing between nodes. + +Key concepts +------------ + +NetQASMConnection (``sim_conn``) + Your connection to the **quantum backend** (SimulaQron's virtual quantum node). + This is *not* a connection to another party — it is how your node talks to its + local simulated quantum hardware. All qubit operations are queued through this + connection. + + Create it once and reuse it:: + + sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + +EPRSocket + Used to create or receive entangled qubit pairs with a remote node. + ``create_keep()`` on one side, ``recv_keep()`` on the other:: + + epr_socket = EPRSocket("Bob") + epr = epr_socket.create_keep()[0] # Alice's side + epr = epr_socket.recv_keep()[0] # Bob's side + +flush() + Sends all queued quantum operations to the backend and waits for them to complete. + Before ``flush()``, measurement results are just futures/promises. + After ``flush()``, you can read them with ``int(m)``:: + + m = qubit.measure() + sim_conn.flush() # execute everything + result = int(m) # NOW this works + + You can call ``flush()`` multiple times on the same connection — this is how + mid-circuit classical logic works (see :doc:`MidCircuitLogic`). + +Qubit + A qubit allocated on the local quantum backend. Pass ``sim_conn`` so the + backend knows where to allocate it:: + + q = Qubit(sim_conn) + q.H() # Hadamard + q.X() # Pauli X + +Classical networking + ``SimulaQronClassicalClient`` and ``SimulaQronClassicalServer`` handle TCP + connections between nodes. Your quantum program function receives + ``(reader, writer)`` for sending/receiving classical messages:: + + async def run_alice(reader: StreamReader, writer: StreamWriter): + writer.write(b"hello") + reply = await reader.read(255) + +File structure +-------------- + +Every example follows the same structure: + +- One Python file per node (e.g. ``aliceTest.py``, ``bobTest.py``) +- ``simulaqron_network.json`` — defines node names and socket ports +- ``simulaqron_settings.json`` — backend settings (``stabilizer`` by default) +- ``run.sh`` — starts the SimulaQron backend and launches all node scripts +- ``terminate.sh`` — stops background processes + +To run any example:: + + cd examples/new-sdk/ + sh run.sh + +Examples +-------- + +The examples are ordered from simplest to most complex: + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Example + - What it teaches + * - :doc:`Template` + - Basic setup: single node with local qubits, and client-server template + * - :doc:`CorrRNG` + - EPR pairs between two nodes, correlated measurement + * - :doc:`Teleport` + - Quantum teleportation with classical correction messages + * - :doc:`ExtendGHZ` + - Three-party entanglement, multiple EPR sockets on one connection + * - :doc:`MidCircuitLogic` + - Multiple ``flush()`` calls for mid-circuit classical decisions diff --git a/docs/new-sdk/Teleport.rst b/docs/new-sdk/Teleport.rst new file mode 100644 index 00000000..0e3942b5 --- /dev/null +++ b/docs/new-sdk/Teleport.rst @@ -0,0 +1,99 @@ +Quantum Teleportation +===================== + +This example implements the standard quantum teleportation protocol, where Alice +teleports a qubit to Bob using a pre-shared EPR pair and two classical bits. +Found in ``examples/new-sdk/teleport/``. + +The protocol +------------ + +1. Alice and Bob share an EPR pair (entangled qubits A and B). +2. Alice has a qubit *q* she wants to teleport to Bob. +3. Alice applies CNOT(q, A) and then H(q). +4. Alice measures both *q* and *A*, obtaining bits *a* and *b*. +5. Alice sends *a* and *b* to Bob via a classical message. +6. Bob applies correction gates: X if *b* = 1, Z if *a* = 1. +7. Bob's qubit B is now in the same state as Alice's original qubit *q*. + +Alice's code +------------ + +From ``aliceTest.py`` — Alice creates the EPR pair, performs the teleportation +circuit, and sends the correction bits to Bob:: + + async def run_alice(reader: StreamReader, writer: StreamWriter): + epr_socket = EPRSocket("Bob") + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob. + sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + + # Create a qubit to teleport + q = Qubit(sim_conn) + q.H() + # Create entanglement + epr = epr_socket.create_keep()[0] + # Teleport circuit: CNOT + H + measure both + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + + # flush() executes all queued quantum operations + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). + m1_val = int(m1) + m2_val = int(m2) + sim_conn.close() + + # Send correction bits to Bob via classical channel + message = f"{m1_val}:{m2_val}" + writer.write(message.encode("utf-8")) + +Bob's code +---------- + +From ``bobTest.py`` — Bob waits for Alice's correction bits, then applies them:: + + async def run_bob(reader: StreamReader, writer: StreamWriter): + # Wait for the classical correction message first + corrections_bytes = await reader.read(255) + corrections = corrections_bytes.decode("utf-8").split(":") + + epr_socket = EPRSocket("Alice") + sim_conn = NetQASMConnection("Bob", epr_sockets=[epr_socket]) + + entangled_qubit = epr_socket.recv_keep()[0] + + # Apply teleportation corrections based on Alice's classical message + if int(corrections[0]) == 1: + entangled_qubit.X() + if int(corrections[1]) == 1: + entangled_qubit.Z() + meas = entangled_qubit.measure() + + sim_conn.flush() + + meas_val = int(meas) + sim_conn.close() + print(f"Bob measurement: {meas_val}") + +Key concepts +------------ + +- **Classical + quantum coordination**: Alice must send classical bits to Bob so he + can apply corrections. This requires the client-server pattern + (``SimulaQronClassicalClient``/``SimulaQronClassicalServer``). +- **Order matters**: Bob must receive the classical message *before* applying + corrections to his entangled qubit. +- The teleported state is reconstructed perfectly regardless of the random + measurement outcomes — the corrections compensate. + +Running +------- + +:: + + cd examples/new-sdk/teleport + sh run.sh diff --git a/docs/new-sdk/Template.rst b/docs/new-sdk/Template.rst new file mode 100644 index 00000000..ecb31ff3 --- /dev/null +++ b/docs/new-sdk/Template.rst @@ -0,0 +1,124 @@ +Template: Getting started +========================= + +SimulaQron provides two starter templates in ``examples/new-sdk/``: + +- ``template-quantum-local/`` — single node, local qubit operations only +- ``template-client-server/`` — two nodes with classical messaging and quantum operations + +Single-node template +-------------------- + +The simplest possible program: create a qubit, apply gates, measure. +Found in ``examples/new-sdk/template-quantum-local/nodeTest.py``:: + + def run_node(this_node_name: str) -> int: + # sim_conn is our connection to the quantum backend (SimulaQron). + # All qubit operations are queued through this connection. + sim_conn = NetQASMConnection(this_node_name) + + # Create a qubit — note we pass sim_conn so the backend knows where + # to allocate it. + q = Qubit(sim_conn) + + # Perform some local quantum operations + q.H() + q.X() + m1 = q.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 is just a future/promise. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). + m1_val = int(m1) + sim_conn.close() + return m1_val + +Key points: + +- ``NetQASMConnection`` connects to the local SimulaQron backend, not to another node. +- ``Qubit(sim_conn)`` allocates a qubit on that backend. +- Gates like ``H()``, ``X()`` are queued — nothing executes until ``flush()``. +- ``int(m)`` only works after ``flush()``. + +Client-server template +---------------------- + +For two-node programs, one node runs as a **client** and the other as a **server**. +Found in ``examples/new-sdk/template-client-server/``. + +The server (``nodeTest-server.py``) starts first and listens for connections:: + + server = SimulaQronClassicalServer(sockets_config, "Bob") + server.register_client_handler(run_bob) + server.start_serving() + +The client (``nodeTest-client.py``) connects to the server:: + + client = SimulaQronClassicalClient(sockets_config) + results = client.run_client("Bob", run_alice) + +Both ``run_alice`` and ``run_bob`` receive ``(reader, writer)`` for classical messaging. +Inside these functions you can also create quantum connections:: + + async def run_alice(reader: StreamReader, writer: StreamWriter): + # Classical messaging + writer.write(b"Hello World") + answer = await reader.read(100) + + # Quantum operations + epr_socket = EPRSocket("Bob") + sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + epr = epr_socket.create_keep()[0] + m = epr.measure() + sim_conn.flush() + result = int(m) + sim_conn.close() + +Configuration files +------------------- + +``simulaqron_network.json`` defines the nodes and their socket ports:: + + [ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + } + ], + "topology": null + } + ] + +``simulaqron_settings.json`` configures the simulation backend:: + + { + "max_qubits": 20, + "max_registers": 1000, + "sim_backend": "stabilizer", + ... + } + +Use ``stabilizer`` backend by default. Use ``qutip`` when you need non-Clifford gates. + +Running +------- + +:: + + cd examples/new-sdk/template-client-server + sh run.sh diff --git a/examples/eventBased/pingPong/pingpongAlice.py b/examples/eventBased/pingPong/pingpongAlice.py new file mode 100644 index 00000000..0c382152 --- /dev/null +++ b/examples/eventBased/pingPong/pingpongAlice.py @@ -0,0 +1,56 @@ +""" +Ping-Pong client (Alice). + +Alice connects to Bob and sends a sequence of messages. +After each message she waits for Bob's reply and prints it. + +Try changing the messages list to see how Bob reacts! + +This is a purely classical example — no quantum operations. +It demonstrates the event-based programming pattern that we will +later extend with quantum operations (teleportation, etc.). +""" +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + + +async def run_alice(reader: StreamReader, writer: StreamWriter): + """ + Alice sends a sequence of messages and prints Bob's replies. + + Try changing this list to see what Bob does with different messages! + """ + messages = ["ping", "ping", "hello", "ping"] + + for msg in messages: + # Send a message to Bob + print(f"Alice: sending '{msg}'") + writer.write(msg.encode("utf-8")) + await writer.drain() + + # Wait for Bob's reply + reply_data = await reader.read(255) + reply = reply_data.decode("utf-8") + print(f"Alice: received '{reply}'") + + print("Alice: done, disconnecting.") + + +if __name__ == "__main__": + # Load configuration files — paths are relative to this script's location + # so the script can be run from any working directory. + _here = Path(__file__).parent + simulaqron_settings.read_from_file(_here / "simulaqron_settings.json") + network_config.read_from_file(_here / "simulaqron_network.json") + + # Set up Alice's client using the network configuration + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + client = SimulaQronClassicalClient(sockets_config) + + print("Alice: connecting to Bob...") + client.run_client("Bob", run_alice) diff --git a/examples/eventBased/pingPong/pingpongBob.py b/examples/eventBased/pingPong/pingpongBob.py new file mode 100644 index 00000000..f791a9af --- /dev/null +++ b/examples/eventBased/pingPong/pingpongBob.py @@ -0,0 +1,71 @@ +""" +Ping-Pong server (Bob). + +Bob listens for messages from Alice. For each message he receives: + - If the message is "ping", he replies with "pong". + - For anything else, he replies with "no way!". + +Bob keeps listening until Alice disconnects. + +This is a purely classical example — no quantum operations. +It demonstrates the event-based programming pattern that we will +later extend with quantum operations (teleportation, etc.). +""" +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalServer +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + + +async def run_bob(reader: StreamReader, writer: StreamWriter): + """ + Bob's event loop. + + Each iteration: + 1. Wait for a message from Alice + 2. Decide on a reply based on the message content + 3. Send the reply back + """ + print("Bob: Alice connected, waiting for messages...", flush=True) + + while True: + # Wait until Alice sends something + data = await reader.read(255) + + # If we get empty data, Alice has disconnected + if not data: + print("Bob: Alice disconnected.", flush=True) + break + + message = data.decode("utf-8") + print(f"Bob: received '{message}'", flush=True) + + # Decide on a reply + if message == "ping": + reply = "pong" + else: + reply = "no way!" + + # Send the reply + print(f"Bob: sending '{reply}'", flush=True) + writer.write(reply.encode("utf-8")) + await writer.drain() + + +if __name__ == "__main__": + # Load configuration files — paths are relative to this script's location + # so the script can be run from any working directory. + _here = Path(__file__).parent + simulaqron_settings.read_from_file(_here / "simulaqron_settings.json") + network_config.read_from_file(_here / "simulaqron_network.json") + + # Set up Bob's server using the network configuration + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + server = SimulaQronClassicalServer(sockets_config, "Bob") + server.register_client_handler(run_bob) + + print("Bob: starting server...") + server.start_serving() diff --git a/examples/eventBased/pingPong/run.sh b/examples/eventBased/pingPong/run.sh new file mode 100644 index 00000000..5b31708d --- /dev/null +++ b/examples/eventBased/pingPong/run.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Runs the ping-pong example. +# +# This example is purely classical — no quantum operations — so the +# SimulaQron backend (QNodeOS / virtual nodes) does not need to be started. +# +# On two separate machines, run these commands in separate terminals: +# Machine B (Bob): python3 pingpongBob.py +# Machine A (Alice): python3 pingpongAlice.py +# +# On a single machine (this script): +# Bob starts in the background, Alice in the foreground. + +# cd to the example directory so relative paths in the scripts work correctly +cd "$(dirname "$0")" + +# -u disables Python's output buffering so Bob's prints appear immediately +# even when running in the background on the same machine. +python3 -u pingpongBob.py & +BOB_PID=$! + +sleep 1 + +python3 pingpongAlice.py + +# Alice is done — shut Bob down +kill $BOB_PID 2>/dev/null +wait $BOB_PID 2>/dev/null diff --git a/examples/eventBased/pingPong/simulaqron_network.json b/examples/eventBased/pingPong/simulaqron_network.json new file mode 100644 index 00000000..097d91d5 --- /dev/null +++ b/examples/eventBased/pingPong/simulaqron_network.json @@ -0,0 +1,22 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + } + ], + "topology": null + } +] diff --git a/examples/eventBased/pingPong/simulaqron_settings.json b/examples/eventBased/pingPong/simulaqron_settings.json new file mode 100644 index 00000000..255323d1 --- /dev/null +++ b/examples/eventBased/pingPong/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "stabilizer", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} diff --git a/examples/eventBased/pingPong/terminate.sh b/examples/eventBased/pingPong/terminate.sh new file mode 100644 index 00000000..90ba329b --- /dev/null +++ b/examples/eventBased/pingPong/terminate.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# This example is purely classical — no SimulaQron backend is running. +# If you ran run.sh, Bob was already killed automatically when Alice finished. +# Nothing to do here. +echo "No SimulaQron backend to stop for this example." diff --git a/examples/eventBased/politePingPong/politeAlice.py b/examples/eventBased/politePingPong/politeAlice.py new file mode 100644 index 00000000..ca603c7a --- /dev/null +++ b/examples/eventBased/politePingPong/politeAlice.py @@ -0,0 +1,154 @@ +""" +Polite Ping-Pong — Alice (client). + +Alice's behaviour is defined as a finite state machine. She always +initiates the exchange by sending "ping" before entering the event loop. +The event loop then reads one message at a time, looks up the +(current_state, message) pair in the dispatch table, and calls the +corresponding handler. The handler performs an action and returns the +next state. + +Alice's state diagram +--------------------- + + ┌─ (connect) ─────────────────────────────┐ + │ send "ping" │ + ▼ │ + ──────────────► WAITING_PONG (start) + │ + recv "pong" + send "thank you" + │ + ▼ + WAITING_YOURE_WELCOME + │ + recv "you're welcome" + │ + ▼ + DONE + +As a transition table (this maps directly to ALICE_DISPATCH below): + + Current state │ Message received │ Action │ Next state + ───────────────────────┼────────────────────┼──────────────────┼────────────────────── + WAITING_PONG │ "pong" │ send "thank you" │ WAITING_YOURE_WELCOME + WAITING_YOURE_WELCOME │ "you're welcome" │ (none) │ DONE + +Any (state, message) pair NOT in the table is rejected with a warning; +the state does not change and the loop continues. + +Note: the initial "ping" is sent before the loop starts — it is not a +state transition but simply Alice's opening move as the initiator. +""" +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + + +# ── States ─────────────────────────────────────────────────────────────────── + +STATE_WAITING_PONG = "WAITING_PONG" +STATE_WAITING_YOURE_WELCOME = "WAITING_YOURE_WELCOME" +STATE_DONE = "DONE" + + +# ── Handlers ───────────────────────────────────────────────────────────────── + +async def handle_pong(writer: StreamWriter) -> str: + """ + Transition: WAITING_PONG ──[recv "pong"]──► WAITING_YOURE_WELCOME + + Alice receives a pong and politely says thank you. + """ + reply = "thank you" + print(f"Alice: sending '{reply}'") + writer.write(reply.encode("utf-8")) + await writer.drain() + return STATE_WAITING_YOURE_WELCOME + + +async def handle_youre_welcome(writer: StreamWriter) -> str: + """ + Transition: WAITING_YOURE_WELCOME ──[recv "you're welcome"]──► DONE + + Alice receives the final courtesy and the exchange is complete. + No reply is needed. + """ + print("Alice: received 'you're welcome' — exchange complete.") + return STATE_DONE + + +# ── Dispatch table ──────────────────────────────────────────────────────────── +# Maps (current_state, message) → handler. +# This table IS the state machine: every valid transition is listed here, +# and anything not listed is automatically an invalid transition. + +ALICE_DISPATCH = { + (STATE_WAITING_PONG, "pong"): handle_pong, + (STATE_WAITING_YOURE_WELCOME, "you're welcome"): handle_youre_welcome, +} + + +# ── Event loop ──────────────────────────────────────────────────────────────── + +async def run_alice(reader: StreamReader, writer: StreamWriter): + """ + Alice's event loop. + + Alice initiates by sending "ping", then enters the state machine loop: + 1. Read the next message from Bob. + 2. Look up (current_state, message) in ALICE_DISPATCH. + 3. If found, call the handler and move to the returned next state. + 4. If not found, log a warning and stay in the current state. + Loop exits when the state reaches STATE_DONE or the connection drops. + """ + # Initial action: Alice always opens the exchange with a ping. + # This happens before the loop — it is not a state transition. + opening = "ping" + print(f"Alice: sending '{opening}'") + writer.write(opening.encode("utf-8")) + await writer.drain() + + state = STATE_WAITING_PONG + + while state != STATE_DONE: + # 1. Wait for the next event (message from Bob) + data = await reader.read(255) + if not data: + print(f"Alice [{state}]: connection dropped unexpectedly.") + break + msg = data.decode("utf-8") + print(f"Alice [{state}]: received '{msg}'") + + # 2. Look up the transition + handler = ALICE_DISPATCH.get((state, msg)) + + # 3a. Invalid transition — warn and stay in current state + if handler is None: + print( + f"Alice [{state}]: no transition for message '{msg}' — ignoring." + ) + continue + + # 3b. Valid transition — execute handler, advance state + state = await handler(writer) + + print(f"Alice: event loop finished (final state: {state}).") + + +# ── Entry point ─────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + _here = Path(__file__).parent + simulaqron_settings.read_from_file(_here / "simulaqron_settings.json") + network_config.read_from_file(_here / "simulaqron_network.json") + + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + client = SimulaQronClassicalClient(sockets_config) + + print("Alice: connecting to Bob...") + client.run_client("Bob", run_alice) diff --git a/examples/eventBased/politePingPong/politeBob.py b/examples/eventBased/politePingPong/politeBob.py new file mode 100644 index 00000000..7619503c --- /dev/null +++ b/examples/eventBased/politePingPong/politeBob.py @@ -0,0 +1,145 @@ +""" +Polite Ping-Pong — Bob (server). + +Bob's behaviour is defined as a finite state machine. The event loop +reads one message at a time, looks up the (current_state, message) pair +in the dispatch table, and calls the corresponding handler. The handler +performs an action (typically sending a reply) and returns the next state. + +Bob's state diagram +------------------- + + recv "ping" send "pong" + WAITING_PING ─────────────────────────────────────► WAITING_THANKS + │ + recv "thank you" + send "you're welcome" + │ + ▼ + DONE + +As a transition table (this maps directly to BOB_DISPATCH below): + + Current state │ Message received │ Action │ Next state + ─────────────────┼──────────────────┼────────────────────────┼────────────────── + WAITING_PING │ "ping" │ send "pong" │ WAITING_THANKS + WAITING_THANKS │ "thank you" │ send "you're welcome" │ DONE + +Any (state, message) pair NOT in the table is rejected with a warning; +the state does not change and the loop continues. +""" +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalServer +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + + +# ── States ─────────────────────────────────────────────────────────────────── +# Each constant names a state in Bob's state machine. +# The string value is used in log output so keep it human-readable. + +STATE_WAITING_PING = "WAITING_PING" +STATE_WAITING_THANKS = "WAITING_THANKS" +STATE_DONE = "DONE" + + +# ── Handlers ───────────────────────────────────────────────────────────────── +# One handler per valid transition. A handler receives the writer (to send +# a reply) and returns the next state. It does NOT need to validate the +# current state — that is guaranteed by the dispatch table. + +async def handle_ping(writer: StreamWriter) -> str: + """ + Transition: WAITING_PING ──[recv "ping"]──► WAITING_THANKS + + Bob receives a ping and replies with a pong. + """ + reply = "pong" + print(f"Bob: sending '{reply}'", flush=True) + writer.write(reply.encode("utf-8")) + await writer.drain() + return STATE_WAITING_THANKS + + +async def handle_thank_you(writer: StreamWriter) -> str: + """ + Transition: WAITING_THANKS ──[recv "thank you"]──► DONE + + Bob receives a thank you and replies politely before finishing. + """ + reply = "you're welcome" + print(f"Bob: sending '{reply}'", flush=True) + writer.write(reply.encode("utf-8")) + await writer.drain() + return STATE_DONE + + +# ── Dispatch table ──────────────────────────────────────────────────────────── +# Maps (current_state, message) → handler. +# This table IS the state machine: every valid transition is listed here, +# and anything not listed is automatically an invalid transition. + +BOB_DISPATCH = { + (STATE_WAITING_PING, "ping"): handle_ping, + (STATE_WAITING_THANKS, "thank you"): handle_thank_you, +} + + +# ── Event loop ──────────────────────────────────────────────────────────────── + +async def run_bob(reader: StreamReader, writer: StreamWriter): + """ + Bob's event loop. + + Repeatedly: + 1. Read the next message from Alice. + 2. Look up (current_state, message) in BOB_DISPATCH. + 3. If found, call the handler and move to the returned next state. + 4. If not found, log a warning and stay in the current state. + Loop exits when the state reaches STATE_DONE or the connection drops. + """ + print("Bob: Alice connected.", flush=True) + state = STATE_WAITING_PING + + while state != STATE_DONE: + # 1. Wait for the next event (message from Alice) + data = await reader.read(255) + if not data: + print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True) + break + msg = data.decode("utf-8") + print(f"Bob [{state}]: received '{msg}'", flush=True) + + # 2. Look up the transition + handler = BOB_DISPATCH.get((state, msg)) + + # 3a. Invalid transition — warn and stay in current state + if handler is None: + print( + f"Bob [{state}]: no transition for message '{msg}' — ignoring.", + flush=True, + ) + continue + + # 3b. Valid transition — execute handler, advance state + state = await handler(writer) + + print(f"Bob: event loop finished (final state: {state}).", flush=True) + + +# ── Entry point ─────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + _here = Path(__file__).parent + simulaqron_settings.read_from_file(_here / "simulaqron_settings.json") + network_config.read_from_file(_here / "simulaqron_network.json") + + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + server = SimulaQronClassicalServer(sockets_config, "Bob") + server.register_client_handler(run_bob) + + print("Bob: starting server...") + server.start_serving() diff --git a/examples/eventBased/politePingPong/run.sh b/examples/eventBased/politePingPong/run.sh new file mode 100644 index 00000000..5bd1b95e --- /dev/null +++ b/examples/eventBased/politePingPong/run.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Runs the polite ping-pong example. +# +# This example is purely classical — no quantum operations — so the +# SimulaQron backend (QNodeOS / virtual nodes) does not need to be started. +# +# On two separate machines, run these commands in separate terminals: +# Machine B (Bob): python3 politeBob.py +# Machine A (Alice): python3 politeAlice.py +# +# On a single machine (this script): +# Bob starts in the background, Alice in the foreground. + +# cd to the example directory so relative paths in the scripts work correctly +cd "$(dirname "$0")" + +# -u disables Python's output buffering so Bob's prints appear immediately +# even when running in the background on the same machine. +python3 -u politeBob.py & +BOB_PID=$! + +sleep 1 + +python3 politeAlice.py + +# Alice is done — shut Bob down +kill $BOB_PID 2>/dev/null +wait $BOB_PID 2>/dev/null diff --git a/examples/eventBased/politePingPong/simulaqron_network.json b/examples/eventBased/politePingPong/simulaqron_network.json new file mode 100644 index 00000000..097d91d5 --- /dev/null +++ b/examples/eventBased/politePingPong/simulaqron_network.json @@ -0,0 +1,22 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + } + ], + "topology": null + } +] diff --git a/examples/eventBased/politePingPong/simulaqron_settings.json b/examples/eventBased/politePingPong/simulaqron_settings.json new file mode 100644 index 00000000..255323d1 --- /dev/null +++ b/examples/eventBased/politePingPong/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "stabilizer", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} diff --git a/examples/eventBased/politePingPong/terminate.sh b/examples/eventBased/politePingPong/terminate.sh new file mode 100644 index 00000000..90ba329b --- /dev/null +++ b/examples/eventBased/politePingPong/terminate.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# This example is purely classical — no SimulaQron backend is running. +# If you ran run.sh, Bob was already killed automatically when Alice finished. +# Nothing to do here. +echo "No SimulaQron backend to stop for this example." diff --git a/examples/eventBased/quantumCorrRNG/aliceTest.py b/examples/eventBased/quantumCorrRNG/aliceTest.py new file mode 100644 index 00000000..0fdf67aa --- /dev/null +++ b/examples/eventBased/quantumCorrRNG/aliceTest.py @@ -0,0 +1,135 @@ +""" +Quantum Correlated Random Number Generation — Alice (client). + +Combines event-based classical messaging (state machine pattern from +politePingPong) with quantum operations. Alice proposes generating +shared randomness with Bob. If Bob agrees, both create an EPR pair +and measure their half, obtaining a correlated random bit. + +Alice's state diagram +--------------------- + + ┌─ (connect) ─────────────────────────────────┐ + │ send "generate randomness?" │ + ▼ │ + WAITING_ACCEPT (start) + │ + recv "yes" + quantum: create EPR, measure, flush + │ + ▼ + DONE + +Transition table: + + Current state │ Message received │ Action │ Next state + ─────────────────┼──────────────────┼───────────────────────────────┼─────────── + WAITING_ACCEPT │ "yes" │ create EPR pair, measure │ DONE +""" +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +# ── States ─────────────────────────────────────────────────────────────────── + +STATE_WAITING_ACCEPT = "WAITING_ACCEPT" +STATE_DONE = "DONE" + + +# ── Handlers ───────────────────────────────────────────────────────────────── + +async def handle_yes(writer: StreamWriter) -> str: + """ + Transition: WAITING_ACCEPT ──[recv "yes"]──► DONE + + Bob agreed — create an EPR pair with him and measure our half. + """ + epr_socket = EPRSocket("Bob") + + # Open a connection to the quantum backend (not to Bob — that's the + # classical reader/writer). We pass the EPR socket so the backend + # knows which nodes will share entanglement. + sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + + # Create an EPR pair with Bob and keep our half + epr = epr_socket.create_keep()[0] + m = epr.measure() + + # flush() sends all queued quantum operations to the backend and waits + # for them to complete. After this call, measurement results like m + # become real values (before flush they are just promises/futures). + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush() + result = int(m) + sim_conn.close() + + print(f"Alice: my random bit is {result}") + return STATE_DONE + + +# ── Dispatch table ──────────────────────────────────────────────────────────── + +ALICE_DISPATCH = { + (STATE_WAITING_ACCEPT, "yes"): handle_yes, +} + + +# ── Event loop ──────────────────────────────────────────────────────────────── + +async def run_alice(reader: StreamReader, writer: StreamWriter): + """ + Alice's event loop. + + Opens by proposing shared randomness, then enters the state machine. + """ + # Initial action: propose generating randomness + opening = "generate randomness?" + print(f"Alice: sending '{opening}'") + writer.write(opening.encode("utf-8")) + await writer.drain() + + state = STATE_WAITING_ACCEPT + + while state != STATE_DONE: + data = await reader.read(255) + if not data: + print(f"Alice [{state}]: connection dropped unexpectedly.") + break + msg = data.decode("utf-8") + print(f"Alice [{state}]: received '{msg}'") + + handler = ALICE_DISPATCH.get((state, msg)) + + if handler is None: + print(f"Alice [{state}]: no transition for '{msg}' — ignoring.") + continue + + state = await handler(writer) + + print(f"Alice: event loop finished (final state: {state}).") + + +# ── Entry point ─────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + _here = Path(__file__).parent + simulaqron_settings.read_from_file(_here / "simulaqron_settings.json") + network_config.read_from_file(_here / "simulaqron_network.json") + + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + client = SimulaQronClassicalClient(sockets_config) + + print("Alice: connecting to Bob...") + client.run_client("Bob", run_alice) diff --git a/examples/eventBased/quantumCorrRNG/bobTest.py b/examples/eventBased/quantumCorrRNG/bobTest.py new file mode 100644 index 00000000..00e80f62 --- /dev/null +++ b/examples/eventBased/quantumCorrRNG/bobTest.py @@ -0,0 +1,131 @@ +""" +Quantum Correlated Random Number Generation — Bob (server). + +Combines event-based classical messaging (state machine pattern from +politePingPong) with quantum operations. Bob waits for Alice's proposal. +If she asks to generate shared randomness, he agrees, then both create +an EPR pair and measure their half, obtaining a correlated random bit. + +Bob's state diagram +------------------- + + recv "generate randomness?" + send "yes" + quantum: receive EPR, measure, flush + WAITING_PROPOSAL ───────────────────────────────────► DONE + +Transition table: + + Current state │ Message received │ Action │ Next state + ───────────────────┼──────────────────────────┼───────────────────────────────┼─────────── + WAITING_PROPOSAL │ "generate randomness?" │ send "yes", recv EPR, measure │ DONE +""" +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalServer +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +# ── States ─────────────────────────────────────────────────────────────────── + +STATE_WAITING_PROPOSAL = "WAITING_PROPOSAL" +STATE_DONE = "DONE" + + +# ── Handlers ───────────────────────────────────────────────────────────────── + +async def handle_generate(writer: StreamWriter) -> str: + """ + Transition: WAITING_PROPOSAL ──[recv "generate randomness?"]──► DONE + + Alice wants shared randomness — agree, then receive our half of the + EPR pair and measure it. + """ + # Classical: agree + reply = "yes" + print(f"Bob: sending '{reply}'", flush=True) + writer.write(reply.encode("utf-8")) + await writer.drain() + + # Quantum: receive our half of the EPR pair from Alice and measure it. + # sim_conn is the connection to the quantum backend (not to Alice — + # that's the classical reader/writer above). + epr_socket = EPRSocket("Alice") + sim_conn = NetQASMConnection("Bob", epr_sockets=[epr_socket]) + + epr = epr_socket.recv_keep()[0] + m = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. int(m) only works after flush(). + sim_conn.flush() + + result = int(m) + sim_conn.close() + + print(f"Bob: my random bit is {result}", flush=True) + return STATE_DONE + + +# ── Dispatch table ──────────────────────────────────────────────────────────── + +BOB_DISPATCH = { + (STATE_WAITING_PROPOSAL, "generate randomness?"): handle_generate, +} + + +# ── Event loop ──────────────────────────────────────────────────────────────── + +async def run_bob(reader: StreamReader, writer: StreamWriter): + """ + Bob's event loop. + + Waits for messages from Alice and dispatches via the state machine. + """ + print("Bob: Alice connected.", flush=True) + state = STATE_WAITING_PROPOSAL + + while state != STATE_DONE: + data = await reader.read(255) + if not data: + print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True) + break + msg = data.decode("utf-8") + print(f"Bob [{state}]: received '{msg}'", flush=True) + + handler = BOB_DISPATCH.get((state, msg)) + + if handler is None: + print( + f"Bob [{state}]: no transition for '{msg}' — ignoring.", + flush=True, + ) + continue + + state = await handler(writer) + + print(f"Bob: event loop finished (final state: {state}).", flush=True) + + +# ── Entry point ─────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + _here = Path(__file__).parent + simulaqron_settings.read_from_file(_here / "simulaqron_settings.json") + network_config.read_from_file(_here / "simulaqron_network.json") + + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + server = SimulaQronClassicalServer(sockets_config, "Bob") + server.register_client_handler(run_bob) + + print("Bob: starting server...", flush=True) + server.start_serving() diff --git a/examples/eventBased/quantumCorrRNG/run.sh b/examples/eventBased/quantumCorrRNG/run.sh new file mode 100644 index 00000000..0e6cac82 --- /dev/null +++ b/examples/eventBased/quantumCorrRNG/run.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# +# Runs the quantum correlated RNG example. +# +# Unlike the purely classical ping-pong examples, this one uses quantum +# operations (EPR pairs) so the SimulaQron backend must be running. +# +# On two separate machines, run these commands in separate terminals: +# Machine B (Bob): python3 bobTest.py +# Machine A (Alice): python3 aliceTest.py +# +# On a single machine (this script): +# SimulaQron starts first, then Bob in the background, then Alice. + +cd "$(dirname "$0")" + +# Start SimulaQron backend if not already running +if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + simulaqron start --nodes=Alice,Bob \ + --network-config-file simulaqron_network.json \ + --simulaqron-config-file simulaqron_settings.json +fi + +python3 -u bobTest.py & +BOB_PID=$! + +sleep 1 + +python3 aliceTest.py + +kill $BOB_PID 2>/dev/null +wait $BOB_PID 2>/dev/null diff --git a/examples/eventBased/quantumCorrRNG/simulaqron_network.json b/examples/eventBased/quantumCorrRNG/simulaqron_network.json new file mode 100644 index 00000000..097d91d5 --- /dev/null +++ b/examples/eventBased/quantumCorrRNG/simulaqron_network.json @@ -0,0 +1,22 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + } + ], + "topology": null + } +] diff --git a/examples/eventBased/quantumCorrRNG/simulaqron_settings.json b/examples/eventBased/quantumCorrRNG/simulaqron_settings.json new file mode 100644 index 00000000..255323d1 --- /dev/null +++ b/examples/eventBased/quantumCorrRNG/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "stabilizer", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} diff --git a/examples/eventBased/quantumCorrRNG/terminate.sh b/examples/eventBased/quantumCorrRNG/terminate.sh new file mode 100644 index 00000000..6b7cab8c --- /dev/null +++ b/examples/eventBased/quantumCorrRNG/terminate.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# Stop the SimulaQron backend +simulaqron stop diff --git a/examples/eventBased/quantumCorrRNGVerified/aliceTest.py b/examples/eventBased/quantumCorrRNGVerified/aliceTest.py new file mode 100644 index 00000000..2f67f84e --- /dev/null +++ b/examples/eventBased/quantumCorrRNGVerified/aliceTest.py @@ -0,0 +1,162 @@ +""" +Quantum Correlated RNG with Verification — Alice (client). + +Extends quantumCorrRNG by adding a classical verification step after the +quantum measurement. After both nodes measure their EPR halves, Alice +sends her result to Bob so he can confirm the correlation. + +This demonstrates the full cycle: + classical negotiation -> quantum operation -> classical verification + +Alice's state diagram +--------------------- + + ┌─ (connect) ─────────────────────────────────┐ + │ send "generate randomness?" │ + ▼ │ + WAITING_ACCEPT (start) + │ + recv "yes" + quantum: create EPR, measure, flush + send result to Bob + │ + ▼ + WAITING_VERIFICATION + │ + recv "verified: ..." + │ + ▼ + DONE + +Transition table: + + Current state │ Message received │ Action │ Next state + ───────────────────────┼──────────────────────┼─────────────────────────────────┼───────────────────── + WAITING_ACCEPT │ "yes" │ EPR create+measure, send result │ WAITING_VERIFICATION + WAITING_VERIFICATION │ "verified: ..." │ print confirmation │ DONE +""" +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalClient +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +# ── States ─────────────────────────────────────────────────────────────────── + +STATE_WAITING_ACCEPT = "WAITING_ACCEPT" +STATE_WAITING_VERIFICATION = "WAITING_VERIFICATION" +STATE_DONE = "DONE" + + +# ── Handlers ───────────────────────────────────────────────────────────────── + +async def handle_yes(writer: StreamWriter) -> str: + """ + Transition: WAITING_ACCEPT ──[recv "yes"]──► WAITING_VERIFICATION + + Bob agreed — create an EPR pair, measure our half, and send the + result to Bob for verification. + """ + epr_socket = EPRSocket("Bob") + + # Open a connection to the quantum backend (not to Bob — that's the + # classical reader/writer). + sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + + # Create an EPR pair with Bob and keep our half + epr = epr_socket.create_keep()[0] + m = epr.measure() + + # flush() sends all queued quantum operations to the backend and waits + # for them to complete. After this, int(m) gives the real value. + sim_conn.flush() + + result = int(m) + sim_conn.close() + + print(f"Alice: my random bit is {result}") + + # Send our result to Bob so he can verify the correlation + writer.write(str(result).encode("utf-8")) + await writer.drain() + + return STATE_WAITING_VERIFICATION + + +async def handle_verification(writer: StreamWriter) -> str: + """ + Transition: WAITING_VERIFICATION ──[recv "verified: ..."]──► DONE + + Bob confirmed whether the results match. + """ + # The message content is printed by the event loop, nothing else to do. + return STATE_DONE + + +# ── Dispatch table ──────────────────────────────────────────────────────────── + +ALICE_DISPATCH = { + (STATE_WAITING_ACCEPT, "yes"): handle_yes, +} + + +# ── Event loop ──────────────────────────────────────────────────────────────── + +async def run_alice(reader: StreamReader, writer: StreamWriter): + """ + Alice's event loop. + + Opens by proposing shared randomness, then enters the state machine. + """ + opening = "generate randomness?" + print(f"Alice: sending '{opening}'") + writer.write(opening.encode("utf-8")) + await writer.drain() + + state = STATE_WAITING_ACCEPT + + while state != STATE_DONE: + data = await reader.read(255) + if not data: + print(f"Alice [{state}]: connection dropped unexpectedly.") + break + msg = data.decode("utf-8") + print(f"Alice [{state}]: received '{msg}'") + + # In WAITING_VERIFICATION, any message from Bob is the verification + if state == STATE_WAITING_VERIFICATION: + state = STATE_DONE + continue + + handler = ALICE_DISPATCH.get((state, msg)) + + if handler is None: + print(f"Alice [{state}]: no transition for '{msg}' — ignoring.") + continue + + state = await handler(writer) + + print(f"Alice: event loop finished (final state: {state}).") + + +# ── Entry point ─────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + _here = Path(__file__).parent + simulaqron_settings.read_from_file(_here / "simulaqron_settings.json") + network_config.read_from_file(_here / "simulaqron_network.json") + + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + client = SimulaQronClassicalClient(sockets_config) + + print("Alice: connecting to Bob...") + client.run_client("Bob", run_alice) diff --git a/examples/eventBased/quantumCorrRNGVerified/bobTest.py b/examples/eventBased/quantumCorrRNGVerified/bobTest.py new file mode 100644 index 00000000..4062342e --- /dev/null +++ b/examples/eventBased/quantumCorrRNGVerified/bobTest.py @@ -0,0 +1,167 @@ +""" +Quantum Correlated RNG with Verification — Bob (server). + +Extends quantumCorrRNG by adding a classical verification step after the +quantum measurement. After both nodes measure their EPR halves, Alice +sends her result to Bob so he can confirm the correlation. + +This demonstrates the full cycle: + classical negotiation -> quantum operation -> classical verification + +Bob's state diagram +------------------- + + recv "generate randomness?" + send "yes" + quantum: receive EPR, measure, flush + WAITING_PROPOSAL ─────────────────────────────► WAITING_ALICE_RESULT + │ + recv Alice's result + compare, send verification + │ + ▼ + DONE + +Transition table: + + Current state │ Message received │ Action │ Next state + ───────────────────────┼────────────────────────┼──────────────────────────────────┼──────────────────── + WAITING_PROPOSAL │ "generate randomness?" │ send "yes", recv EPR, measure │ WAITING_ALICE_RESULT + WAITING_ALICE_RESULT │ │ compare results, send verdict │ DONE +""" +from asyncio import StreamReader, StreamWriter +from pathlib import Path + +from simulaqron.general.host_config import SocketsConfig +from simulaqron.sdk.protocol import SimulaQronClassicalServer +from simulaqron.settings import network_config, simulaqron_settings +from simulaqron.settings.network_config import NodeConfigType + +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import EPRSocket # noqa: E402 + + +# ── States ─────────────────────────────────────────────────────────────────── + +STATE_WAITING_PROPOSAL = "WAITING_PROPOSAL" +STATE_WAITING_ALICE_RESULT = "WAITING_ALICE_RESULT" +STATE_DONE = "DONE" + +# Bob stores his measurement result here so the verification handler can +# compare it with Alice's result. +bob_result = None + + +# ── Handlers ───────────────────────────────────────────────────────────────── + +async def handle_generate(writer: StreamWriter) -> str: + """ + Transition: WAITING_PROPOSAL ──[recv "generate randomness?"]──► WAITING_ALICE_RESULT + + Alice wants shared randomness — agree, then receive our half of the + EPR pair and measure it. Store result for later verification. + """ + global bob_result + + # Classical: agree + reply = "yes" + print(f"Bob: sending '{reply}'", flush=True) + writer.write(reply.encode("utf-8")) + await writer.drain() + + # Quantum: receive our half of the EPR pair from Alice and measure it. + # sim_conn is the connection to the quantum backend (not to Alice). + epr_socket = EPRSocket("Alice") + sim_conn = NetQASMConnection("Bob", epr_sockets=[epr_socket]) + + epr = epr_socket.recv_keep()[0] + m = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. int(m) only works after flush(). + sim_conn.flush() + + bob_result = int(m) + sim_conn.close() + + print(f"Bob: my random bit is {bob_result}", flush=True) + return STATE_WAITING_ALICE_RESULT + + +async def handle_alice_result(writer: StreamWriter, alice_result: int) -> str: + """ + Transition: WAITING_ALICE_RESULT ──[recv result]──► DONE + + Alice sent her measurement result. Compare with ours and send + a verification message back. + """ + match = alice_result == bob_result + verdict = f"verified: Alice={alice_result}, Bob={bob_result}, match={match}" + print(f"Bob: {verdict}", flush=True) + writer.write(verdict.encode("utf-8")) + await writer.drain() + return STATE_DONE + + +# ── Dispatch table ──────────────────────────────────────────────────────────── + +BOB_DISPATCH = { + (STATE_WAITING_PROPOSAL, "generate randomness?"): handle_generate, +} + + +# ── Event loop ──────────────────────────────────────────────────────────────── + +async def run_bob(reader: StreamReader, writer: StreamWriter): + """ + Bob's event loop. + + Waits for messages from Alice and dispatches via the state machine. + """ + print("Bob: Alice connected.", flush=True) + state = STATE_WAITING_PROPOSAL + + while state != STATE_DONE: + data = await reader.read(255) + if not data: + print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True) + break + msg = data.decode("utf-8") + print(f"Bob [{state}]: received '{msg}'", flush=True) + + # In WAITING_ALICE_RESULT, the message is Alice's measurement bit + if state == STATE_WAITING_ALICE_RESULT: + alice_bit = int(msg) + state = await handle_alice_result(writer, alice_bit) + continue + + handler = BOB_DISPATCH.get((state, msg)) + + if handler is None: + print( + f"Bob [{state}]: no transition for '{msg}' — ignoring.", + flush=True, + ) + continue + + state = await handler(writer) + + print(f"Bob: event loop finished (final state: {state}).", flush=True) + + +# ── Entry point ─────────────────────────────────────────────────────────────── + +if __name__ == "__main__": + _here = Path(__file__).parent + simulaqron_settings.read_from_file(_here / "simulaqron_settings.json") + network_config.read_from_file(_here / "simulaqron_network.json") + + sockets_config = SocketsConfig(network_config, "default", NodeConfigType.APP) + server = SimulaQronClassicalServer(sockets_config, "Bob") + server.register_client_handler(run_bob) + + print("Bob: starting server...", flush=True) + server.start_serving() diff --git a/examples/eventBased/quantumCorrRNGVerified/run.sh b/examples/eventBased/quantumCorrRNGVerified/run.sh new file mode 100644 index 00000000..0e6cac82 --- /dev/null +++ b/examples/eventBased/quantumCorrRNGVerified/run.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# +# Runs the quantum correlated RNG example. +# +# Unlike the purely classical ping-pong examples, this one uses quantum +# operations (EPR pairs) so the SimulaQron backend must be running. +# +# On two separate machines, run these commands in separate terminals: +# Machine B (Bob): python3 bobTest.py +# Machine A (Alice): python3 aliceTest.py +# +# On a single machine (this script): +# SimulaQron starts first, then Bob in the background, then Alice. + +cd "$(dirname "$0")" + +# Start SimulaQron backend if not already running +if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + simulaqron start --nodes=Alice,Bob \ + --network-config-file simulaqron_network.json \ + --simulaqron-config-file simulaqron_settings.json +fi + +python3 -u bobTest.py & +BOB_PID=$! + +sleep 1 + +python3 aliceTest.py + +kill $BOB_PID 2>/dev/null +wait $BOB_PID 2>/dev/null diff --git a/examples/eventBased/quantumCorrRNGVerified/simulaqron_network.json b/examples/eventBased/quantumCorrRNGVerified/simulaqron_network.json new file mode 100644 index 00000000..097d91d5 --- /dev/null +++ b/examples/eventBased/quantumCorrRNGVerified/simulaqron_network.json @@ -0,0 +1,22 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + }, + { + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] + } + } + ], + "topology": null + } +] diff --git a/examples/eventBased/quantumCorrRNGVerified/simulaqron_settings.json b/examples/eventBased/quantumCorrRNGVerified/simulaqron_settings.json new file mode 100644 index 00000000..255323d1 --- /dev/null +++ b/examples/eventBased/quantumCorrRNGVerified/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "stabilizer", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} diff --git a/examples/eventBased/quantumCorrRNGVerified/terminate.sh b/examples/eventBased/quantumCorrRNGVerified/terminate.sh new file mode 100644 index 00000000..6b7cab8c --- /dev/null +++ b/examples/eventBased/quantumCorrRNGVerified/terminate.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# Stop the SimulaQron backend +simulaqron stop diff --git a/examples/nativeMode/graphState/run.sh b/examples/nativeMode/graphState/run.sh old mode 100755 new mode 100644 index 097588e0..48e91500 --- a/examples/nativeMode/graphState/run.sh +++ b/examples/nativeMode/graphState/run.sh @@ -2,7 +2,7 @@ # Check if SimulaQron is already running if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then - if ! simulaqron start --nodes=Alice,Bob,Charlie,David --network-config-file classicalNet.json + if ! simulaqron start --nodes=Alice,Bob,Charlie,David --network-config-file classicalNet.json --simulaqron-config-file simulaqron_settings.json then echo "SimulaQron could not start correctly" exit 1 diff --git a/examples/nativeMode/graphState/simulaqron_settings.json b/examples/nativeMode/graphState/simulaqron_settings.json new file mode 100644 index 00000000..a24e69e5 --- /dev/null +++ b/examples/nativeMode/graphState/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "qutip", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} diff --git a/examples/netqasm/teleport/teleport.py b/examples/netqasm/teleport/teleport.py deleted file mode 100644 index 68d206ff..00000000 --- a/examples/netqasm/teleport/teleport.py +++ /dev/null @@ -1,56 +0,0 @@ -from netqasm.runtime.settings import set_simulator - -set_simulator("simulaqron") - -from netqasm.runtime.application import default_app_instance # noqa: E402 -from netqasm.sdk.external import NetQASMConnection # noqa: E402 -from netqasm.sdk import Qubit, EPRSocket # noqa: E402 - -from simulaqron.run.run import run_applications # noqa: E402 - - -def run_alice(): - epr_socket: EPRSocket = EPRSocket("Bob") - with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: - # Create a qubit - q = Qubit(alice) - alice.flush() - q.H() - - # Create entanglement - epr = epr_socket.create_keep()[0] - - # Teleport - q.cnot(epr) - q.H() - m1 = q.measure() - m2 = epr.measure() - return m1, m2 - - -def run_bob(): - epr_socket: EPRSocket = EPRSocket("Alice") - with NetQASMConnection("Bob", epr_sockets=[epr_socket]): - entangled_qubit = epr_socket.recv_keep()[0] - meas = entangled_qubit.measure() - return meas - - -if __name__ == "__main__": - apps = default_app_instance( - [ - ("Alice", run_alice), - ("Bob", run_bob) - ] - ) - raw_results = run_applications(apps, use_app_config=False, enable_logging=False) - - results = {} - - for name, raw_result in raw_results[0].items(): - if isinstance(raw_result, tuple): - results[name] = tuple(int(result) for result in raw_result) - else: - results[name] = int(raw_result) - - print(results) diff --git a/examples/new-sdk/classical-client-server/example_client_bob.py b/examples/new-sdk/classical-client-server/example_client_bob.py index cc8f832b..985a71bb 100644 --- a/examples/new-sdk/classical-client-server/example_client_bob.py +++ b/examples/new-sdk/classical-client-server/example_client_bob.py @@ -7,7 +7,7 @@ from simulaqron.settings.network_config import NodeConfigType -async def client_code(reader: StreamReader, writer: StreamWriter): +async def run_bob(reader: StreamReader, writer: StreamWriter): data = "Hello World!".encode("utf-8") writer.write(data) print(f"Client sent message '{data.decode("utf-8")}'") @@ -30,4 +30,4 @@ async def client_code(reader: StreamReader, writer: StreamWriter): # Run the client. The given function will be called once the connection with #m the given server was established - client.run_client("Alice", client_code) + client.run_client("Alice", run_bob) diff --git a/examples/new-sdk/classical-client-server/example_server_alice.py b/examples/new-sdk/classical-client-server/example_server_alice.py index 9b9cf751..0b766999 100644 --- a/examples/new-sdk/classical-client-server/example_server_alice.py +++ b/examples/new-sdk/classical-client-server/example_server_alice.py @@ -7,7 +7,7 @@ from simulaqron.settings.network_config import NodeConfigType -async def connection_handler(reader: StreamReader, writer: StreamWriter): +async def run_alice(reader: StreamReader, writer: StreamWriter): result = await reader.read(255) print(f"Server received message: '{result.decode("utf-8")}'") writer.write(result) @@ -29,7 +29,7 @@ async def connection_handler(reader: StreamReader, writer: StreamWriter): # Register a new client handler. The given function will be called once a new client # opens a connection with this node. - server.register_client_handler(connection_handler) + server.register_client_handler(run_alice) # Start serving the clients server.start_serving() diff --git a/examples/new-sdk/corrRNG/aliceTest.py b/examples/new-sdk/corrRNG/aliceTest.py index 6e053aff..71ba28e8 100644 --- a/examples/new-sdk/corrRNG/aliceTest.py +++ b/examples/new-sdk/corrRNG/aliceTest.py @@ -15,21 +15,26 @@ from netqasm.sdk import EPRSocket # noqa: E402 -# This function contains the code of the classical client -def quantum_program(this_node_name: str, remote_node_name: str) -> int: - logging.debug("LOCAL %s: Running client side program.", this_node_name) - +def run_alice(this_node_name: str, remote_node_name: str) -> int: epr_socket = EPRSocket(remote_node_name) - # To start executing quantum operations, we need to create a NetQASM connection - with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as alice: - # Create an entangled qubit - epr = epr_socket.create_keep()[0] - - # And simply measure it - m1 = epr.measure() - # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob. + # Bob is reached via EPRSocket for quantum and reader/writer for classical. + sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) + + # Create an entangled qubit + epr = epr_socket.create_keep()[0] + + # And simply measure it + m1 = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 is just a future/promise. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). m1_val = int(m1) + sim_conn.close() return m1_val @@ -56,5 +61,5 @@ def quantum_program(this_node_name: str, remote_node_name: str) -> int: node_name = "Alice" # A node with this name *must* exist in "simulaqron_network.json" other_node_name = "Bob" - result = quantum_program(node_name, other_node_name) + result = run_alice(node_name, other_node_name) print(f"{node_name}: My Random Number is '{result}'") diff --git a/examples/new-sdk/corrRNG/bobTest.py b/examples/new-sdk/corrRNG/bobTest.py index 765d035f..adad6b11 100644 --- a/examples/new-sdk/corrRNG/bobTest.py +++ b/examples/new-sdk/corrRNG/bobTest.py @@ -15,21 +15,26 @@ from netqasm.sdk import EPRSocket # noqa: E402 -# This function contains the code of the classical client -def bob_program(this_node_name: str, remote_node_name: str) -> int: - logging.debug("LOCAL %s: Running client side program.", this_node_name) - +def run_bob(this_node_name: str, remote_node_name: str) -> int: epr_socket = EPRSocket(remote_node_name) - # To start executing quantum operations, we need to create a NetQASM connection - with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as alice: - # Receive an entangled qubit - epr = epr_socket.recv_keep()[0] - - # And simply measure it - m1 = epr.measure() - # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Alice. + # Alice is reached via EPRSocket for quantum and reader/writer for classical. + sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) + + # Receive an entangled qubit + epr = epr_socket.recv_keep()[0] + + # And simply measure it + m1 = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 is just a future/promise. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). m1_val = int(m1) + sim_conn.close() return m1_val @@ -56,5 +61,5 @@ def bob_program(this_node_name: str, remote_node_name: str) -> int: node_name = "Bob" # A node with this name *must* exist in "simulaqron_network.json" other_node_name = "Alice" - result = bob_program(node_name, other_node_name) + result = run_bob(node_name, other_node_name) print(f"{node_name}: My Random Number is '{result}'") diff --git a/examples/new-sdk/corrRNG/simulaqron_settings.json b/examples/new-sdk/corrRNG/simulaqron_settings.json index bbfb47a2..84c98ffd 100644 --- a/examples/new-sdk/corrRNG/simulaqron_settings.json +++ b/examples/new-sdk/corrRNG/simulaqron_settings.json @@ -11,4 +11,4 @@ "noisy_qubits": false, "max_app_waiting_time": -1.0, "t1": 1.0 -} \ No newline at end of file +} diff --git a/examples/new-sdk/extendGHZ/aliceTest.py b/examples/new-sdk/extendGHZ/aliceTest.py index c88b5927..3003dcba 100644 --- a/examples/new-sdk/extendGHZ/aliceTest.py +++ b/examples/new-sdk/extendGHZ/aliceTest.py @@ -21,29 +21,34 @@ from netqasm.sdk import EPRSocket # noqa: E402 -# This function contains the code of the classical client -async def alice_program(reader: StreamReader, writer: StreamWriter) -> int: +async def run_alice(reader: StreamReader, writer: StreamWriter) -> int: # This is "Alice": the start node of the GHZ chain this_node_name = "Alice" remote_node_name = "Bob" # A node with this name *must* exist in "simulaqron_network.json" - logging.debug("LOCAL %s: Running client side program.", this_node_name) - epr_socket = EPRSocket(remote_node_name) - # To start executing quantum operations, we need to create a NetQASM connection - with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]): - # Create an entangled qubit with Bob - epr = epr_socket.create_keep()[0] - writer.write("receive_qubit".encode("utf-8")) - answer = await reader.read(100) + # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob. + # Bob is reached via EPRSocket for quantum and reader/writer for classical. + sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) + + # Create an entangled qubit with Bob + epr = epr_socket.create_keep()[0] + + writer.write("receive_qubit".encode("utf-8")) + answer = await reader.read(100) + + assert answer.decode("utf-8") == "continue" + + m1 = epr.measure() - assert answer.decode("utf-8") == "continue" + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 is just a future/promise. + sim_conn.flush() - m1 = epr.measure() - # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) + # int(m) extracts the measurement outcome — only valid after flush(). m1_val = int(m1) + sim_conn.close() return m1_val @@ -82,7 +87,7 @@ async def alice_program(reader: StreamReader, writer: StreamWriter) -> int: # to measure the qubit client = SimulaQronClassicalClient(classical_sockets) - result = client.run_client(other_node_name, alice_program) - #result = alice_program(1, 0) + result = client.run_client(other_node_name, run_alice) + #result = run_alice(1, 0) print(f"{node_name}: My outcome is '{result}'") diff --git a/examples/new-sdk/extendGHZ/bobTest.py b/examples/new-sdk/extendGHZ/bobTest.py index b56c04ea..563df82a 100644 --- a/examples/new-sdk/extendGHZ/bobTest.py +++ b/examples/new-sdk/extendGHZ/bobTest.py @@ -27,15 +27,12 @@ async def send_to_charlie(reader: StreamReader, writer: StreamWriter): assert message.decode("utf-8") == "continue" -# This function contains the code of the classical client -async def bob_program(reader: StreamReader, writer: StreamWriter) -> int: +async def run_bob(reader: StreamReader, writer: StreamWriter) -> int: # This is "Bob": the middle node of the GHZ chain this_node_name = "Bob" start_node_name = "Alice" # A node with this name *must* exist in "simulaqron_network.json" end_node_name = "Charlie" # A node with this name *must* exist in "simulaqron_network.json" - logging.debug("LOCAL %s: Running client side program.", this_node_name) - message = await reader.read(100) assert message.decode("utf-8") == "receive_qubit" @@ -45,26 +42,35 @@ async def bob_program(reader: StreamReader, writer: StreamWriter) -> int: sockets = SocketsConfig(network_config, "default", NodeConfigType.APP) charlie_client = SimulaQronClassicalClient(sockets) - # To start executing quantum operations, we need to create a NetQASM connection - with NetQASMConnection(this_node_name, epr_sockets=[epr_socket_alice, epr_socket_charlie]) as bob: - # Receive an entangled qubit - epr_alice = epr_socket_alice.recv_keep()[0] - # Create a new entangled with Charlie - epr_charlie = epr_socket_charlie.create_keep()[0] + # sim_conn is our connection to the quantum backend (SimulaQron), not to + # Alice or Charlie. They are reached via EPRSockets for quantum and + # reader/writer for classical. + sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket_alice, epr_socket_charlie]) + + # Receive an entangled qubit from Alice + epr_alice = epr_socket_alice.recv_keep()[0] + + # Create a new entangled pair with Charlie + epr_charlie = epr_socket_charlie.create_keep()[0] + + await charlie_client.connect_and_run(end_node_name, send_to_charlie) + + # Create the GHZ state by entangling the fresh qubit + epr_alice.cnot(epr_charlie) - await charlie_client.connect_and_run(end_node_name, send_to_charlie) + writer.write("continue".encode("utf-8")) - # Create the GHZ state by entangling the fresh qubit - epr_alice.cnot(epr_charlie) + # And simply measure it + m1 = epr_charlie.measure() - writer.write("continue".encode("utf-8")) + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 is just a future/promise. + sim_conn.flush() - # And simply measure it - m1 = epr_charlie.measure() - # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) + # int(m) extracts the measurement outcome — only valid after flush(). m1_val = int(m1) + sim_conn.close() print(f"{this_node_name}: My outcome is '{m1_val}'") return 0 @@ -98,5 +104,5 @@ async def bob_program(reader: StreamReader, writer: StreamWriter) -> int: server = SimulaQronClassicalServer(classical_sockets, node_name) client = SimulaQronClassicalClient(classical_sockets) - server.register_client_handler(bob_program) + server.register_client_handler(run_bob) server.start_serving() diff --git a/examples/new-sdk/extendGHZ/charlieTest.py b/examples/new-sdk/extendGHZ/charlieTest.py index 5746d182..0fa64da2 100644 --- a/examples/new-sdk/extendGHZ/charlieTest.py +++ b/examples/new-sdk/extendGHZ/charlieTest.py @@ -21,30 +21,33 @@ from netqasm.sdk import EPRSocket # noqa: E402 -# This function contains the code of the classical client -async def charlie_program(reader: StreamReader, writer: StreamWriter) -> int: +async def run_charlie(reader: StreamReader, writer: StreamWriter) -> int: # This is "Charlie": the end node of the GHZ chain this_node_name = "Charlie" remote_node_name = "Bob" - logging.debug("LOCAL %s: Running client side program.", this_node_name) - message = await reader.read(100) assert message.decode("utf-8") == "receive_qubit" epr_socket = EPRSocket(remote_node_name) - # To start executing quantum operations, we need to create a NetQASM connection - with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as charlie: - # Receive an entangled qubit - epr = epr_socket.recv_keep()[0] - - writer.write("continue".encode("utf-8")) - - print("here2") - # And simply measure it - m1 = epr.measure() - # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) - m1_val = int(m1) + # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob. + # Bob is reached via EPRSocket for quantum and reader/writer for classical. + sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) + + # Receive an entangled qubit + epr = epr_socket.recv_keep()[0] + + writer.write("continue".encode("utf-8")) + + # And simply measure it + m1 = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 is just a future/promise. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). + m1_val = int(m1) + sim_conn.close() print(f"{this_node_name}: My outcome is '{m1_val}'") return 0 @@ -75,5 +78,5 @@ async def charlie_program(reader: StreamReader, writer: StreamWriter) -> int: sockets = SocketsConfig(network_config, network_name, NodeConfigType.APP) server = SimulaQronClassicalServer(sockets, node_name) - server.register_client_handler(charlie_program) + server.register_client_handler(run_charlie) server.start_serving() diff --git a/examples/new-sdk/midCircuitLogic/nodeTest.py b/examples/new-sdk/midCircuitLogic/nodeTest.py new file mode 100644 index 00000000..4eb29472 --- /dev/null +++ b/examples/new-sdk/midCircuitLogic/nodeTest.py @@ -0,0 +1,100 @@ +""" +Mid-circuit classical logic example. + +Demonstrates how to use sim_conn.flush() to read measurement results +mid-circuit, make classical decisions based on them, and then continue +with more quantum operations. + +Without flush(), measurement results are "futures" that only become real +values after the connection is closed. With flush(), you can read them at +any point and branch your quantum program accordingly. + +This example implements a simple 3-round protocol: + + Round 1: Prepare |+> and measure. + Round 2: Based on the round-1 outcome, prepare the *opposite* state. + Round 3: Based on the XOR of rounds 1 and 2, decide what to prepare. + +All three rounds happen inside ONE connection — no need to create multiple +connections. +""" +from pathlib import Path + +from simulaqron.settings import network_config, simulaqron_settings + +from netqasm.runtime.settings import set_simulator +set_simulator("simulaqron") + +from netqasm.sdk.external import NetQASMConnection # noqa: E402 +from netqasm.sdk import Qubit # noqa: E402 + + +def run_node(node_name: str): + results = [] + + # sim_conn is our connection to the quantum backend (SimulaQron). + # All qubit operations are queued through this connection. + sim_conn = NetQASMConnection(node_name) + + # --- Round 1: prepare |+> and measure --- + q1 = Qubit(sim_conn) + q1.H() # |0> -> |+> + m1 = q1.measure() + # flush() executes all queued quantum operations and makes measurement + # results available. We call it after each round so we can use the + # result to decide what to do next (mid-circuit classical logic). + sim_conn.flush() + # int(m) extracts the measurement outcome — only valid after flush(). + r1 = int(m1) + results.append(r1) + print(f" Round 1: measured |+> -> {r1}") + + # --- Round 2: classical decision --- + # If round 1 gave 0, prepare |1>. If 1, prepare |0>. + q2 = Qubit(sim_conn) + if r1 == 0: + q2.X() # flip to |1> + print(" Round 2: r1 was 0, so preparing |1>") + else: + print(" Round 2: r1 was 1, so preparing |0>") + m2 = q2.measure() + sim_conn.flush() + r2 = int(m2) + results.append(r2) + print(f" Round 2: measured -> {r2} (should be {1 - r1})") + + # --- Round 3: compound classical logic --- + # XOR of r1 and r2. Since r2 = 1-r1, XOR is always 1. + xor = r1 ^ r2 + q3 = Qubit(sim_conn) + if xor: + q3.X() + print(f" Round 3: r1 XOR r2 = {xor}, preparing |1>") + else: + print(f" Round 3: r1 XOR r2 = {xor}, preparing |0>") + m3 = q3.measure() + sim_conn.flush() + r3 = int(m3) + results.append(r3) + print(f" Round 3: measured -> {r3} (should be {xor})") + + sim_conn.close() + + # Summary + print(f"\nAll rounds: {results}") + print(f"Check: r2 == 1-r1? {results[1] == 1 - results[0]}") + print(f"Check: r3 == r1 XOR r2? {results[2] == results[0] ^ results[1]}") + return results + + +if __name__ == "__main__": + simulaqron_config_file = Path("simulaqron_settings.json") + simulaqron_settings.read_from_file(simulaqron_config_file) + + network_config_file = Path("simulaqron_network.json") + network_config.read_from_file(network_config_file) + + node_name = "Alice" + + print(f"=== Mid-circuit classical logic demo ({node_name}) ===\n") + run_node(node_name) diff --git a/examples/new-sdk/midCircuitLogic/run.sh b/examples/new-sdk/midCircuitLogic/run.sh new file mode 100755 index 00000000..f1d4394f --- /dev/null +++ b/examples/new-sdk/midCircuitLogic/run.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +START_SIMULAQRON=true + +while [[ $# -gt 0 ]]; do + case $1 in + -n|--no-start-simulaqron) + START_SIMULAQRON=false + shift + ;; + esac +done + +if [ "$START_SIMULAQRON" = true ]; then + if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then + simulaqron start --nodes=Alice --network-config-file simulaqron_network.json --simulaqron-config-file simulaqron_settings.json + fi +fi + +python3 nodeTest.py diff --git a/examples/new-sdk/midCircuitLogic/simulaqron_network.json b/examples/new-sdk/midCircuitLogic/simulaqron_network.json new file mode 100644 index 00000000..8841c11b --- /dev/null +++ b/examples/new-sdk/midCircuitLogic/simulaqron_network.json @@ -0,0 +1,15 @@ +[ + { + "name": "default", + "nodes": [ + { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] + } + } + ], + "topology": null + } +] diff --git a/examples/new-sdk/midCircuitLogic/simulaqron_settings.json b/examples/new-sdk/midCircuitLogic/simulaqron_settings.json new file mode 100644 index 00000000..255323d1 --- /dev/null +++ b/examples/new-sdk/midCircuitLogic/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "stabilizer", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} diff --git a/examples/new-sdk/midCircuitLogic/terminate.sh b/examples/new-sdk/midCircuitLogic/terminate.sh new file mode 100755 index 00000000..320276b6 --- /dev/null +++ b/examples/new-sdk/midCircuitLogic/terminate.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +simulaqron stop diff --git a/examples/new-sdk/teleport/aliceTest.py b/examples/new-sdk/teleport/aliceTest.py index ad2d48a2..5b179bd1 100644 --- a/examples/new-sdk/teleport/aliceTest.py +++ b/examples/new-sdk/teleport/aliceTest.py @@ -18,21 +18,30 @@ async def run_alice(reader: StreamReader, writer: StreamWriter): epr_socket = EPRSocket("Bob") - with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as alice: - # Create a qubit - q = Qubit(alice) - q.H() - # Create entanglement - epr = epr_socket.create_keep()[0] - # Teleport - q.cnot(epr) - q.H() - m1 = q.measure() - m2 = epr.measure() - # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob. + # Bob is reached via EPRSocket for quantum and reader/writer for classical. + sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + + # Create a qubit to teleport + q = Qubit(sim_conn) + q.H() + # Create entanglement + epr = epr_socket.create_keep()[0] + # Teleport circuit: CNOT + H + measure both + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 and m2 are just futures/promises. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). m1_val = int(m1) m2_val = int(m2) + sim_conn.close() message = f"{m1_val}:{m2_val}" # noqa: E231 writer.write(message.encode("utf-8")) return m1_val, m2_val diff --git a/examples/new-sdk/teleport/bobTest.py b/examples/new-sdk/teleport/bobTest.py index 9113ad23..ec4a95c9 100644 --- a/examples/new-sdk/teleport/bobTest.py +++ b/examples/new-sdk/teleport/bobTest.py @@ -18,17 +18,27 @@ async def run_bob(reader: StreamReader, writer: StreamWriter): corrections_bytes = await reader.read(255) corrections = corrections_bytes.decode("utf-8").split(":") epr_socket = EPRSocket("Alice") - with NetQASMConnection("Bob", epr_sockets=[epr_socket]): - entangled_qubit = epr_socket.recv_keep()[0] - - if int(corrections[0]) == 1: - entangled_qubit.X() - if int(corrections[1]) == 1: - entangled_qubit.Z() - meas = entangled_qubit.measure() - # Any value that comes from NetQASm *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Alice. + # Alice is reached via EPRSocket for quantum and reader/writer for classical. + sim_conn = NetQASMConnection("Bob", epr_sockets=[epr_socket]) + + entangled_qubit = epr_socket.recv_keep()[0] + + # Apply teleportation corrections based on Alice's classical message + if int(corrections[0]) == 1: + entangled_qubit.X() + if int(corrections[1]) == 1: + entangled_qubit.Z() + meas = entangled_qubit.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), meas is just a future/promise. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). meas_val = int(meas) + sim_conn.close() print(f"Bob measurement: {meas_val}") diff --git a/examples/new-sdk/template-client-server/nodeTest-client.py b/examples/new-sdk/template-client-server/nodeTest-client.py index 999761d9..35e05799 100644 --- a/examples/new-sdk/template-client-server/nodeTest-client.py +++ b/examples/new-sdk/template-client-server/nodeTest-client.py @@ -16,10 +16,9 @@ from netqasm.sdk import Qubit, EPRSocket # noqa: E402 -# This function contains the code of the classical client # "reader" is an object connected to the server, which can be used to read data from the server # "writer" is an object connected to the server, which can be used to send data to the server -async def run_client(reader: StreamReader, writer: StreamWriter): +async def run_alice(reader: StreamReader, writer: StreamWriter): # To send a messsage, we can simply use the "wirte" method from the "writer" object # The argument *must* be a python bytes object, which we can get by encoding (using # the UTF-8 charmap) any python string @@ -39,24 +38,32 @@ async def run_client(reader: StreamReader, writer: StreamWriter): this_node_name = "Alice" other_node_name = "Bob" - # We ca create an EPR socket with the other node + # We can create an EPR socket with the other node epr_socket = EPRSocket(other_node_name) - # To start executing quantum operations, we need to create a NetQASM connection - with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as alice: - # Create a qubit - q = Qubit(alice) - q.H() - # Create an entangled qubit with the other node - epr = epr_socket.create_keep()[0] - # Teleport - q.cnot(epr) - q.H() - m1 = q.measure() - m2 = epr.measure() - # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob. + # Bob is reached via EPRSocket for quantum and reader/writer for classical. + sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) + + # Create a qubit + q = Qubit(sim_conn) + q.H() + # Create an entangled qubit with the other node + epr = epr_socket.create_keep()[0] + # Teleport circuit: CNOT + H + measure both + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 and m2 are just futures/promises. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). m1_val = int(m1) m2_val = int(m2) + sim_conn.close() return m1_val, m2_val @@ -81,6 +88,6 @@ async def run_client(reader: StreamReader, writer: StreamWriter): # Create the client client = SimulaQronClassicalClient(sockets_config) - # Run a classical client invoking the `run_client` method. This also has the effect to - # immediately execute the `run_client` method. - results = client.run_client(server_name, run_client) + # Run a classical client invoking the `run_alice` method. This also has the effect to + # immediately execute the `run_alice` method. + results = client.run_client(server_name, run_alice) diff --git a/examples/new-sdk/template-client-server/nodeTest-server.py b/examples/new-sdk/template-client-server/nodeTest-server.py index 84d5d675..d8d9a90a 100644 --- a/examples/new-sdk/template-client-server/nodeTest-server.py +++ b/examples/new-sdk/template-client-server/nodeTest-server.py @@ -16,10 +16,9 @@ from netqasm.sdk import Qubit, EPRSocket # noqa: E402 -# This function contains the code to serve *one client ocnnected to this server*. # "reader" is an object connected to the client, which can be used to read data from the client # "writer" is an object connected to the client, which can be used to send data to the client -async def serve_client(reader: StreamReader, writer: StreamWriter): +async def run_bob(reader: StreamReader, writer: StreamWriter): # If you want to receive a message (such a query) from the client, you can # use the "read" method from the "reader" object. The argument is an integer that # configures the maximum bytes that we are allowed to read in a single operation. @@ -41,21 +40,30 @@ async def serve_client(reader: StreamReader, writer: StreamWriter): other_node_name = "Alice" epr_socket = EPRSocket(other_node_name) - with NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) as bob: - # Create a qubit - q = Qubit(bob) - q.H() - # Create entanglement - epr = epr_socket.create_keep()[0] - # Teleport - q.cnot(epr) - q.H() - m1 = q.measure() - m2 = epr.measure() - # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Alice. + # Alice is reached via EPRSocket for quantum and reader/writer for classical. + sim_conn = NetQASMConnection(this_node_name, epr_sockets=[epr_socket]) + + # Create a qubit + q = Qubit(sim_conn) + q.H() + # Create entanglement + epr = epr_socket.create_keep()[0] + # Teleport circuit: CNOT + H + measure both + q.cnot(epr) + q.H() + m1 = q.measure() + m2 = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 and m2 are just futures/promises. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). m1_val = int(m1) m2_val = int(m2) + sim_conn.close() return m1_val, m2_val @@ -77,5 +85,5 @@ async def serve_client(reader: StreamReader, writer: StreamWriter): # Create the server server = SimulaQronClassicalServer(sockets_config, node_name) - server.register_client_handler(serve_client) + server.register_client_handler(run_bob) server.start_serving() diff --git a/examples/new-sdk/template-quantum-local/nodeTest.py b/examples/new-sdk/template-quantum-local/nodeTest.py index c1353a9f..5c728bf9 100644 --- a/examples/new-sdk/template-quantum-local/nodeTest.py +++ b/examples/new-sdk/template-quantum-local/nodeTest.py @@ -12,20 +12,27 @@ from netqasm.sdk import Qubit # noqa: E402 -# This function contains the code of the classical client -def quantum_program(this_node_name: str) -> int: - # To start executing quantum operations, we need to create a NetQASM connection - with NetQASMConnection(this_node_name) as alice: - # Create a qubit - q = Qubit(alice) - - # Perform some local quantum operations - q.H() - q.X() - m1 = q.measure() - # Any value that comes from NetQASM *need* to be retrieved ("casted" to int) - # *after* the connection is closed (or after flushing the connection, untested) +def run_node(this_node_name: str) -> int: + # sim_conn is our connection to the quantum backend (SimulaQron). + # All qubit operations are queued through this connection. + sim_conn = NetQASMConnection(this_node_name) + + # Create a qubit — note we pass sim_conn so the backend knows where + # to allocate it. + q = Qubit(sim_conn) + + # Perform some local quantum operations + q.H() + q.X() + m1 = q.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 is just a future/promise. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). m1_val = int(m1) + sim_conn.close() return m1_val @@ -43,5 +50,5 @@ def quantum_program(this_node_name: str) -> int: network_name = "default" # A network with this name *must* exist in "simulaqron_network.json" node_name = "Alice" # A node with this name *must* exist in "simulaqron_network.json" - result = quantum_program(node_name) + result = run_node(node_name) print(result) diff --git a/simulaqron/netqasm_backend/executioner.py b/simulaqron/netqasm_backend/executioner.py index 6b57930e..15415577 100644 --- a/simulaqron/netqasm_backend/executioner.py +++ b/simulaqron/netqasm_backend/executioner.py @@ -116,6 +116,9 @@ def __init__(self, *args, **kwargs): self._return_msg_func = None self._factory = None self._network_stack = NetworkStack(self) + # Tracks the msg_id of the subroutine currently being executed, set by + # SubroutineHandler before each call so error messages can carry it back. + self._current_msg_id: int = 0 @property def factory(self) -> "NetQASMFactory": # noqa: F821 @@ -169,9 +172,17 @@ def add_factory(self, factory: "NetQASMFactory"): # noqa: F821 def _handle_command_exception(self, exc, prog_counter, traceback_str): self._logger.error("At line %d: %s\n%s", prog_counter, exc, traceback_str) if isinstance(exc, SimUnsupportedError): - self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.UNSUPP, err_msg="Unsupported simulation engine")) + self._return_msg(msg=RichErrorMessage( + err_code=ErrorCode.UNSUPP, + err_msg="Unsupported simulation engine", + msg_id=self._current_msg_id, + )) else: - self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(exc))) + self._return_msg(msg=RichErrorMessage( + err_code=ErrorCode.GENERAL, + err_msg=str(exc), + msg_id=self._current_msg_id, + )) def _return_msg(self, msg): if self._return_msg_func is None: @@ -203,7 +214,6 @@ def cmd_new(self, physical_address: int): q = VirtualQubitRef(q_id, int(time.time()), virt) self.factory.qubitList[q_id] = q self._logger.info("Requested new physical qubit %d)", q_id) - print(f"DEBUG: Added qubit {q_id} to qubitList", flush=True) # ADD THIS finally: self.factory._lock.release() diff --git a/simulaqron/netqasm_backend/factory.py b/simulaqron/netqasm_backend/factory.py index a4a4a44b..55c63cb3 100644 --- a/simulaqron/netqasm_backend/factory.py +++ b/simulaqron/netqasm_backend/factory.py @@ -188,7 +188,12 @@ def _log_handled_message(self, result): @inlineCallbacks def _log_error(self, failure): self._logger.error("Handling message failed with failure = %s", failure.value) - self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(failure.value))) + # Use the executor's current msg_id so the client's _wait_for_done loop + # can unblock for the correct subroutine rather than hanging forever. + msg_id = getattr(getattr(self.messageHandler, '_executor', None), '_current_msg_id', 0) + if not isinstance(msg_id, int): + msg_id = 0 + self._return_msg(msg=RichErrorMessage(err_code=ErrorCode.GENERAL, err_msg=str(failure.value), msg_id=msg_id)) # self.transport.abortConnection() # yield None yield deferLater(reactor, 0.1, self.stop) diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 7f83f53e..e2fc03af 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -43,14 +43,17 @@ def __init__(self, factory: "NetQASMFactory", instr_log_dir: Optional[str] = Non self.factory = factory self._logger = logging.getLogger("QnodeController") - # Force configure root logger with a handler, ensure our log output to this file - # will allow us to trace back exactly where it came from in the codebase - logging.basicConfig( - format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", - level=simulaqron_settings.log_level, - force=True, - stream=sys.stdout # send logs to the standard output, we set this earlier to be in /tmp - ) + # NOTE: Commented out because basicConfig(force=True) clobbers the root logger's + # handlers on every new connection, silently destroying any logging config set up + # at startup. If log output goes missing mid-run, this was the culprit. + # If you need to configure logging, do it once at process startup (e.g. in run.py), + # not here. + # logging.basicConfig( + # format="%(asctime)s:%(levelname)s:%(name)s:%(filename)s:%(lineno)d:%(message)s", + # level=simulaqron_settings.log_level, + # force=True, + # stream=sys.stdout # send logs to the standard output, we set this earlier to be in /tmp + # ) # Give a way for the executioner to return messages self._executor.add_return_msg_func(self._return_msg) @@ -89,45 +92,84 @@ def handle_netqasm_message(self, msg_id: int, msg: Message): :param msg: The message to process. :type msg: Message """ - print(f"DEBUG handle_netqasm_message: msg_id={msg_id}", flush=True) + # Let the executioner know which subroutine we're processing so that any + # RichErrorMessage it sends back carries the correct msg_id for the client + # to unblock its _wait_for_done loop. + self._executor._current_msg_id = msg_id + gen = super().handle_netqasm_message( msg_id=msg_id, msg=msg, ) - # The following is a bug fix to properly wait for twisted deferreds - - try: - result = None - iteration = 0 - while True: - iteration = iteration + 1 + # Bridge between two async models: + # - NetQASM's executor uses Python generators (yield from) + # - SimulaQron uses Twisted deferreds (@inlineCallbacks) + # + # We manually drive the netqasm generator, detecting whether each + # yielded item is a Twisted Deferred (wait for it) or a nested + # generator (consume it manually, propagating its return value back). + # + # The gen.throw() call below is critical: when a nested generator + # (e.g. super()._instr_qalloc) raises an exception, we must re-throw + # it into the outer generator so netqasm's try/except in + # _execute_commands can catch it and call _handle_command_exception, + # which sends RichErrorMessage back to the client. Without this, + # the exception escapes the runner, _handle_command_exception never + # runs, and the client hangs waiting for a reply that never comes. + result = None + while True: + try: item = gen.send(result) - if hasattr(item, 'addCallback'): # Deferred - result = yield item - elif hasattr(item, '__next__'): # Nested generator - consume it - nested_result = None + except StopIteration: + break # generator finished normally + + if hasattr(item, 'addCallback'): # Twisted Deferred + result = yield item + elif hasattr(item, '__next__'): # Nested generator — consume manually + nested_result = None + try: + while True: + nested_item = item.send(nested_result) + if hasattr(nested_item, 'addCallback'): + nested_result = yield nested_item + else: + nested_result = None + except StopIteration as e: + result = e.value # propagate return value back to outer gen + except Exception as e: + # Nested generator raised (e.g. "virtual address outside unit module"). + # Re-throw into the outer generator so netqasm can handle it. try: - while True: - nested_item = item.send(nested_result) - if hasattr(nested_item, 'addCallback'): - nested_result = yield nested_item - else: - nested_result = None - except StopIteration as e: - result = e.value # Get the return value from the generator - else: - result = None - except StopIteration: - pass - - def _handle_get_qubit_state(self, get_quibit_state_msg: GetQubitStateMessage) -> Generator[Any, None, None]: + item2 = gen.throw(type(e), e) + # Outer generator caught the exception and yielded again — + # process item2 exactly like any other yielded item. + result = None + if hasattr(item2, 'addCallback'): + result = yield item2 + elif hasattr(item2, '__next__'): + nested_result2 = None + try: + while True: + ni2 = item2.send(nested_result2) + if hasattr(ni2, 'addCallback'): + nested_result2 = yield ni2 + else: + nested_result2 = None + except StopIteration as se2: + result = se2.value + except StopIteration: + break # outer generator finished after handling the error + else: + result = None + + def _handle_get_qubit_state(self, get_qubit_state_msg: GetQubitStateMessage) -> Generator[Any, None, None]: assert isinstance(self._executor, VanillaSimulaQronExecutioner) casted_executor: VanillaSimulaQronExecutioner = self._executor # The ProjectQ backend also returns an unused mapping; we need to fix that - realvec, imagvec = yield from casted_executor.get_qubit_state(get_quibit_state_msg.qubit_id) + realvec, imagvec = yield from casted_executor.get_qubit_state(get_qubit_state_msg.qubit_id) # Return a message to the connection object - self._return_qubit_state(get_quibit_state_msg.qubit_id, realvec, imagvec) + self._return_qubit_state(get_qubit_state_msg.qubit_id, realvec, imagvec) def _return_qubit_state(self, qubit_id: int, real_part: List[List[float]], imag_part: List[List[float]]): qubit_state_message = ReturnQubitStateMessage(qubit_id, real_part, imag_part) diff --git a/simulaqron/network.py b/simulaqron/network.py index 87cdd550..ca3cc041 100644 --- a/simulaqron/network.py +++ b/simulaqron/network.py @@ -146,7 +146,7 @@ def start(self, wait_until_running=False): for p in self.processes: if not p.is_alive(): self._logger.debug("Starting process %s", p.name) - p.deamon = True + p.daemon = True p.start() if wait_until_running: @@ -165,11 +165,14 @@ def stop(self): self._running = False self._logger.info("Stopping network with name %s", self.name) for p in self.processes: - while p.is_alive(): - time.sleep(0.1) + if p.is_alive(): try: p.terminate() - p.join() + p.join(timeout=5) + if p.is_alive(): + # Process ignored SIGTERM — force-kill it so we never hang here + self._logger.warning("Process %s did not stop after SIGTERM, killing it", p.name) + p.kill() except Exception as err: self._logger.warning("Could not terminate one of the processes in the" "network due to error: %s", err) diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index 7e776112..eae52185 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -281,7 +281,6 @@ def run_applications( path = os.path.join(timed_log_dir, "results.yaml") dump_yaml(data=result, file_path=path) results.append(result) - network.stop() finally: network.stop() reset(save_loggers=True) diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index 3537c57d..f55345a6 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -104,6 +104,11 @@ def __init__( # Keep track of finished msg IDs self._done_msg_ids: Set[int] = set() + # Stores an error received from the backend so it can be raised after + # _wait_for_done unblocks, rather than raising inside _handle_reply + # (which would leave the msg_id in _waiting_msg_ids and break close()) + self._pending_error: Optional[Exception] = None + # Buffer for returned messages self.buf = b"" @@ -283,6 +288,10 @@ def _wait_for_done(self, msg_id: Optional[int] = None, callback: Optional[Callab # Wait for another don continue self._logger.debug("Received done for msg ID %d", done_msg_id) + if self._pending_error is not None: + err = self._pending_error + self._pending_error = None + raise err def _read_more_data(self): """Reads in some more data on the socket to qnodeos""" @@ -320,6 +329,11 @@ def _handle_reply(self) -> int: self._logger.debug("Got message %s", ret_msg) match ret_msg: case MsgDoneMessage(): + if ret_msg.msg_id in self._done_msg_ids: + # Duplicate: already handled by a preceding RichErrorMessage for + # the same subroutine. The backend sends both; skip this one so + # _wait_for_done keeps looking for the message it actually needs. + return -1 self._waiting_msg_ids.remove(ret_msg.msg_id) self._done_msg_ids.add(ret_msg.msg_id) # Call the registered callback, if any @@ -356,10 +370,18 @@ def _handle_reply(self) -> int: ) return -1 case RichErrorMessage(): + # Treat the error as terminal for this msg_id: unblock _wait_for_done + # so the client loop exits cleanly and close() can still run afterward. + if ret_msg.msg_id in self._waiting_msg_ids: + self._waiting_msg_ids.remove(ret_msg.msg_id) + self._done_msg_ids.add(ret_msg.msg_id) if ret_msg.err_code == ErrorCode.UNSUPP.value: - raise SimUnsupportedError("Operation not supported") + self._pending_error = SimUnsupportedError("Operation not supported") else: - raise RuntimeError(f"Received error message from backend: {ret_msg.get_err_msg()}") + self._pending_error = RuntimeError( + f"Quantum node rejected request: {ret_msg.get_err_msg()}" + ) + return ret_msg.msg_id case _: raise NotImplementedError(f"Unknown return message of type {type(ret_msg)}") @@ -483,6 +505,7 @@ class NewReturnMessageType(Enum): class RichErrorMessage(ReturnMessage): _fields_ = [ + ("msg_id", ctypes.c_uint32), ("err_code", ctypes.c_uint8), ("err_msg_len", ctypes.c_uint32), ("err_msg", MAX_ERR_MSG_LEN * ctypes.c_uint8), @@ -491,7 +514,7 @@ class RichErrorMessage(ReturnMessage): # This works because the enum types are mapped to the very same value TYPE = NewReturnMessageType.ERR - def __init__(self, err_code: ErrorCode, err_msg: str): + def __init__(self, err_code: ErrorCode, err_msg: str, msg_id: int = 0): """ Enriched message to the Host that an error occurred at the quantum node controller. @@ -499,8 +522,12 @@ def __init__(self, err_code: ErrorCode, err_msg: str): :type err_code: ErrorCode :param err_msg: The error message. :type err_msg: str + :param msg_id: The ID of the subroutine message that caused the error, so the + client can unblock its _wait_for_done loop for that message. + :type msg_id: int """ super().__init__(self.TYPE.value) + self.msg_id = msg_id err_bytes = err_msg.encode("utf-8") if len(err_bytes) > MAX_ERR_MSG_LEN: logger.warning("Reported error message too long") diff --git a/simulaqron/virtual_node/project_q_simulator.py b/simulaqron/virtual_node/project_q_simulator.py index 19f28a92..f505ff5f 100644 --- a/simulaqron/virtual_node/project_q_simulator.py +++ b/simulaqron/virtual_node/project_q_simulator.py @@ -1,427 +1,428 @@ -# -# Copyright (c) 2017, Stephanie Wehner and Axel Dahlberg -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# 3. All advertising materials mentioning features or use of this software -# must display the following acknowledgement: -# This product includes software developed by Stephanie Wehner, QuTech. -# 4. Neither the name of the QuTech organization nor the -# names of its contributors may be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY -# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY -# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from typing import Tuple, Dict, List - -try: - import projectq as pQ -except ImportError: - raise RuntimeError("If you want to use the projectq backend you need to install the python package 'projectq'") -import numpy as np - -from simulaqron.virtual_node.basics import QuantumEngine, QuantumError, NoQubitError - - -class ProjectQEngine(QuantumEngine): - """ - Basic quantum engine which uses ProjectQ. - """ - - def __init__(self, node: str, num: int, maxQubits: int = 10): - """ - Initialize the ProjectQ engine. - - :param node: Node name this register is started from. - :type node: str - :param num: Number of this register. - :type num: int - :param maxQubits: Maximum number of qubits this engine will support. - :type maxQubits: int - """ - - super().__init__(node=node, num=num, maxQubits=maxQubits) - - # We start with no active qubits - self.activeQubits = 0 - - self.eng = pQ.MainEngine() - self.qubitReg = [] - - def __del__(self): - """ - Measures out all the current qubits, needed for projectQs garbage collectors. - """ - # Check first that project Q garbage collector not already removed qubits - self.eng.flush() - if not len(self.eng.backend.cheat()[0]) == 0: - for _ in range(self.activeQubits): - self.measure_qubit(0) - - def add_fresh_qubit(self) -> int: - """ - Add a new qubit initialized in the :math:`|0>` state. - - :return: The ID of the new qubit allocated. - :rtype: int - """ - # Check if we are still allowed to add qubits - if self.activeQubits >= self.maxQubits: - raise NoQubitError("No more qubits available in register.") - - # Prepare a clean qubit state in |0> - qubit = self.eng.allocate_qubit()[0] - - self.qubitReg.append(qubit) - - num = self.activeQubits - - self.activeQubits += 1 - - return num - - def add_qubit(self, newQubit): - """ - Add new qubit in the state described by the vector newQubit ([a, b]) - - :param newQubit: The density matrix of the new qubit. - :return: The ID of the new qubit allocated. - :rtype: int - """ - - norm = np.dot(np.array(newQubit), np.array(newQubit).conj()) - if not norm <= 1: - raise QuantumError(f"State {newQubit} is not normalized.") - - # Create a fresh qubit - num = self.add_fresh_qubit() - - # Transform the new qubit into the correct state - pQ.ops.StatePreparation(newQubit) | self.qubitReg[num] - - return num - - def remove_qubit(self, qubitNum): - """ - Removes the qubit with the desired number qubitNum - - :param qubitNum: Qubit number - :type qubitNum: int - """ - if (qubitNum + 1) > self.activeQubits: - raise QuantumError("No such qubit to remove") - - self.measure_qubit(qubitNum) - - def _get_internal_qubit_state(self) -> Tuple[Dict[int, int], List[complex]]: - """ - Retrieves the entire register in real and imaginary parts and returns the result as a - list. Twisted only likes to send real valued lists, not complex ones. - """ - self.eng.flush() - order, state = self.eng.backend.cheat() - # Update the order based on the positions in the qubitReg - # and not of the qubit IDs - q_reg_order: Dict[int, int] = {} - for i, q in enumerate(self.qubitReg): - q_reg_order[i] = order[q.id] - - return q_reg_order, state - - def get_register_RI(self) -> Tuple[Dict[int, int], Tuple[Tuple[float, ...], Tuple[float, ...]]]: - """ - Retrieves the entire register in real and imaginary parts and returns the result as a - list. Twisted only likes to send real valued lists, not complex ones. - - :return: The qubit states real and imaginary parts. - :rtype: Tuple[Tuple[float, ...], Tuple[float, ...]] - """ - q_reg_order, state = self._get_internal_qubit_state() - - # Note previously the format of real and imaginary numbers were - # expected, use the same even though Re will be the qubit mapping - # and Im the state - Re = tuple(n.real for n in state) - Im = tuple(n.imag for n in state) - - return q_reg_order, (Re, Im) - - def get_density_matrix_RI(self) -> Tuple[List[float], List[float]]: - """ - Retrieves the density matrix of the qubit as a real and imaginary part. Twisted only - likes to send real valued lists, not complex ones. - - :return: The qubit density matrix real and imaginary parts. - :rtype: Tuple[List[float], List[float]] - """ - # Get the internal state of the qubit, and compute the outer product |q> self.activeQubits: - raise QuantumError("No such qubit to apply a single qubit gate to") - - gate | self.qubitReg[qubitNum] - - def apply_twoqubit_gate(self, gate, qubit1: int, qubit2: int): - """ - Applies a unitary gate to the two specified qubits. - - Arguments: - :param gate: The project Q gate to be applied - :param qubit1: The first qubit - :type qubit1: int - :param qubit2: The second qubit - :type qubit2: int - """ - if (qubit1 + 1) > self.activeQubits: - raise QuantumError("No such qubit to act as a control qubit") - - if (qubit2 + 1) > self.activeQubits: - raise QuantumError("No such qubit to act as a target qubit") - - if qubit1 == qubit2: - raise QuantumError("Control and target are equal") - - gate | (self.qubitReg[qubit1], self.qubitReg[qubit2]) - - def measure_qubit_inplace(self, qubitNum: int): - """ - Measures the desired qubit in the standard basis. This returns the classical outcome. The quantum register - is in the post-measurement state corresponding to the obtained outcome. - - :param qubitNum: The number of the qubit to measure. - :type qubitNum: int - """ - - # Check we have such a qubit... - if (qubitNum + 1) > self.activeQubits: - raise QuantumError("No such qubit to be measured.") - - pQ.ops.Measure | self.qubitReg[qubitNum] - - self.eng.flush() - - outcome = int(self.qubitReg[qubitNum]) - - # return measurement outcome - return outcome - - def measure_qubit(self, qubitNum: int): - """ - Measures the desired qubit in the standard basis. This returns the classical outcome and deletes the qubit. - - :param qubitNum: The number of the qubit to measure. - :type qubitNum: int - """ - outcome = self.measure_qubit_inplace(qubitNum) - - self.qubitReg.pop(qubitNum) - - # Update the number of qubits - self.activeQubits = self.activeQubits - 1 - - return outcome - - def replace_qubit(self, qubitNum: int, state): - """ - Replaces the qubit at position qubitNum with the one given by state. - - :param qubitNum: Qubit to be replaced - :type qubitNum: int - :param state: New state to write in the place of the old qubit. - :type state: Any - """ - raise NotImplementedError("Currently you cannot replace a qubit using project Q as backend") - - def absorb(self, other): - """ - Absorb the qubits from the other engine into this one. This is done by tensoring the state at the end. - - :param other: The other qubit to absorb. - :type other: int - """ - # Check whether there is space - newNum = self.activeQubits + other.activeQubits - if newNum > self.maxQubits: - raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") - - # Check whether there are in fact qubits to tensor up.... - if self.activeQubits == 0: - self.eng = other.eng - self.qubitReg = list(other.qubitReg) - self.activeQubits = other.activeQubits - elif other.activeQubits > 0: - data = other.get_register_RI() - self.absorb_parts(*data, other.activeQubits) - - def absorb_parts(self, R, I, activeQ): - """ - Absorb the qubits, given in pieces - - :param R: Real part of the qubit state as a list. - :type R: List[float] - :param I: Imaginary part as a list. - :type I: List[float] - :param activeQ: Active number of qubits - """ - # Check whether there is space - newNum = self.activeQubits + activeQ - if newNum > self.maxQubits: - raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") - - if activeQ > 0: - # Unpack the ordering of qubits and the real and imaginary part - order, (R, I) = R, I - - # Convert the real and imaginary parts to a state - state = [re + im * 1j for re, im in zip(R, I)] - - # Allocate qubits in this engine for the new qubits from the other engine - qreg = self.eng.allocate_qureg(activeQ) - - # Put the new qubits in the correct state - pQ.ops.StatePreparation(state) | qreg - - # Put the qubits in the correct order - # The `order` is a mapping from the previous qubit IDs - # to the bit position in the state. The qubits in the `qreg` - # are therefore in the old bit positions which needs to be updated. - new_qubits = [None] * len(qreg) - for old_q_id, old_bit_pos in order.items(): - new_qubits[old_q_id] = qreg[old_bit_pos] - - # Add the qubits to the list of qubits - self.qubitReg += new_qubits - - self.activeQubits = newNum +# +# Copyright (c) 2017, Stephanie Wehner and Axel Dahlberg +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Stephanie Wehner, QuTech. +# 4. Neither the name of the QuTech organization nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from typing import Tuple, Dict, List + +try: + import projectq as pQ +except ImportError: + raise RuntimeError("If you want to use the projectq backend you need to install the python package 'projectq'") +import numpy as np + +from simulaqron.virtual_node.basics import QuantumEngine, QuantumError, NoQubitError + + +class ProjectQEngine(QuantumEngine): + """ + Basic quantum engine which uses ProjectQ. + """ + + def __init__(self, node: str, num: int, maxQubits: int = 10): + """ + Initialize the ProjectQ engine. + + :param node: Node name this register is started from. + :type node: str + :param num: Number of this register. + :type num: int + :param maxQubits: Maximum number of qubits this engine will support. + :type maxQubits: int + """ + + super().__init__(node=node, num=num, maxQubits=maxQubits) + + # We start with no active qubits + self.activeQubits = 0 + + self.eng = pQ.MainEngine() + self.qubitReg = [] + + def __del__(self): + """ + Measures out all the current qubits, needed for projectQs garbage collectors. + """ + # Check first that project Q garbage collector not already removed qubits + self.eng.flush() + if not len(self.eng.backend.cheat()[0]) == 0: + for _ in range(self.activeQubits): + self.measure_qubit(0) + + def add_fresh_qubit(self) -> int: + """ + Add a new qubit initialized in the :math:`|0>` state. + + :return: The ID of the new qubit allocated. + :rtype: int + """ + # Check if we are still allowed to add qubits + if self.activeQubits >= self.maxQubits: + raise NoQubitError("No more qubits available in register.") + + # Prepare a clean qubit state in |0> + qubit = self.eng.allocate_qubit()[0] + + self.qubitReg.append(qubit) + + num = self.activeQubits + + self.activeQubits += 1 + + return num + + def add_qubit(self, newQubit): + """ + Add new qubit in the state described by the vector newQubit ([a, b]) + + :param newQubit: The density matrix of the new qubit. + :return: The ID of the new qubit allocated. + :rtype: int + """ + + norm = np.dot(np.array(newQubit), np.array(newQubit).conj()) + if not norm <= 1: + raise QuantumError(f"State {newQubit} is not normalized.") + + # Create a fresh qubit + num = self.add_fresh_qubit() + + # Transform the new qubit into the correct state + pQ.ops.StatePreparation(newQubit) | self.qubitReg[num] + + return num + + def remove_qubit(self, qubitNum): + """ + Removes the qubit with the desired number qubitNum + + :param qubitNum: Qubit number + :type qubitNum: int + """ + if (qubitNum + 1) > self.activeQubits: + raise QuantumError("No such qubit to remove") + + self.measure_qubit(qubitNum) + + def _get_internal_qubit_state(self) -> Tuple[Dict[int, int], List[complex]]: + """ + Retrieves the entire register in real and imaginary parts and returns the result as a + list. Twisted only likes to send real valued lists, not complex ones. + """ + self.eng.flush() + order, state = self.eng.backend.cheat() + # Update the order based on the positions in the qubitReg + # and not of the qubit IDs + q_reg_order: Dict[int, int] = {} + for i, q in enumerate(self.qubitReg): + q_reg_order[i] = order[q.id] + + return q_reg_order, state + + def get_register_RI(self) -> Tuple[Dict[int, int], Tuple[Tuple[float, ...], Tuple[float, ...]]]: + """ + Retrieves the entire register in real and imaginary parts and returns the result as a + list. Twisted only likes to send real valued lists, not complex ones. + + :return: The qubit states real and imaginary parts. + :rtype: Tuple[Tuple[float, ...], Tuple[float, ...]] + """ + q_reg_order, state = self._get_internal_qubit_state() + + # Note previously the format of real and imaginary numbers were + # expected, use the same even though Re will be the qubit mapping + # and Im the state. + # Use float() to convert numpy.float64 → Python float so Twisted PB + # can serialize the values (numpy scalar types become Unpersistable). + Re = tuple(float(n.real) for n in state) + Im = tuple(float(n.imag) for n in state) + + return q_reg_order, (Re, Im) + + def get_density_matrix_RI(self) -> Tuple[List[float], List[float]]: + """ + Retrieves the density matrix of the qubit as a real and imaginary part. Twisted only + likes to send real valued lists, not complex ones. + + :return: The qubit density matrix real and imaginary parts. + :rtype: Tuple[List[float], List[float]] + """ + _, raw_qubit_state = self._get_internal_qubit_state() + qubit_state = np.array(raw_qubit_state) + density_matrix = np.outer(qubit_state, qubit_state) + return density_matrix.real.tolist(), density_matrix.imag.tolist() + + def apply_H(self, qubitNum): + """ + Applies a Hadamard gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int + """ + self.apply_onequbit_gate(pQ.ops.H, qubitNum) + + def apply_K(self, qubitNum): + """ + Applies a K gate to the qubits with number qubitNum. Maps computational basis to Y eigenbasis. + + :param qubitNum: Qubit number + :type qubitNum: int + """ + self.apply_onequbit_gate(pQ.ops.H, qubitNum) + self.apply_onequbit_gate(pQ.ops.S, qubitNum) + self.apply_onequbit_gate(pQ.ops.H, qubitNum) + self.apply_onequbit_gate(pQ.ops.Z, qubitNum) + + def apply_X(self, qubitNum): + """ + Applies a X gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int + """ + + self.apply_onequbit_gate(pQ.ops.X, qubitNum) + + def apply_Z(self, qubitNum): + """ + Applies a Z gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int + """ + + self.apply_onequbit_gate(pQ.ops.Z, qubitNum) + + def apply_Y(self, qubitNum): + """ + Applies a Y gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int + """ + + self.apply_onequbit_gate(pQ.ops.Y, qubitNum) + + def apply_T(self, qubitNum): + """ + Applies a T gate to the qubits with number qubitNum. + + :param qubitNum: Qubit number + :type qubitNum: int + """ + self.apply_onequbit_gate(pQ.ops.T, qubitNum) + + def apply_rotation(self, qubitNum: int, n: Tuple[float, float, float], a: float): + """ + Applies a rotation around the axis n with the angle a to qubit with number qubitNum. If n is zero a ValueError + is raised. + + :param qubitNum: Qubit number + :type qubitNum: int + :param n: A tuple of three numbers specifying the rotation axis, e.g n=(1,0,0) + :type n: Tuple[float, float, float] + :param a: The rotation angle in radians. + :type a: float + """ + n = tuple(n) + if n == (1, 0, 0): + self.apply_onequbit_gate(pQ.ops.Rx(a), qubitNum) + elif n == (0, 1, 0): + self.apply_onequbit_gate(pQ.ops.Ry(a), qubitNum) + elif n == (0, 0, 1): + self.apply_onequbit_gate(pQ.ops.Rz(a), qubitNum) + else: + raise NotImplementedError("Can only do rotations around X, Y, or Z axis right now") + + def apply_CNOT(self, qubitNum1, qubitNum2): + """ + Applies the CNOT to the qubit with the numbers qubitNum1 and qubitNum2. + + :param qubitNum1: Qubit number 1. + :type qubitNum1: int + :param qubitNum1: Qubit number 2. + :type qubitNum1: int + """ + self.apply_twoqubit_gate(pQ.ops.CNOT, qubitNum1, qubitNum2) + + def apply_CPHASE(self, qubitNum1, qubitNum2): + """ + Applies the CPHASE to the qubit with the numbers qubitNum1 and qubitNum2. + + :param qubitNum1: Qubit number 1. + :type qubitNum1: int + :param qubitNum1: Qubit number 2. + :type qubitNum1: int + """ + + self.apply_twoqubit_gate(pQ.ops.CZ, qubitNum1, qubitNum2) + + def apply_onequbit_gate(self, gate, qubitNum: int): + """ + Applies a unitary gate to the specified qubit. + + :param gate: The project Q gate to be applied. + :param qubitNum: The number of the qubit this gate is applied to. + :type qubitNum: int + """ + + if (qubitNum + 1) > self.activeQubits: + raise QuantumError("No such qubit to apply a single qubit gate to") + + gate | self.qubitReg[qubitNum] + + def apply_twoqubit_gate(self, gate, qubit1: int, qubit2: int): + """ + Applies a unitary gate to the two specified qubits. + + Arguments: + :param gate: The project Q gate to be applied + :param qubit1: The first qubit + :type qubit1: int + :param qubit2: The second qubit + :type qubit2: int + """ + if (qubit1 + 1) > self.activeQubits: + raise QuantumError("No such qubit to act as a control qubit") + + if (qubit2 + 1) > self.activeQubits: + raise QuantumError("No such qubit to act as a target qubit") + + if qubit1 == qubit2: + raise QuantumError("Control and target are equal") + + gate | (self.qubitReg[qubit1], self.qubitReg[qubit2]) + + def measure_qubit_inplace(self, qubitNum: int): + """ + Measures the desired qubit in the standard basis. This returns the classical outcome. The quantum register + is in the post-measurement state corresponding to the obtained outcome. + + :param qubitNum: The number of the qubit to measure. + :type qubitNum: int + """ + + # Check we have such a qubit... + if (qubitNum + 1) > self.activeQubits: + raise QuantumError("No such qubit to be measured.") + + pQ.ops.Measure | self.qubitReg[qubitNum] + + self.eng.flush() + + outcome = int(self.qubitReg[qubitNum]) + + # return measurement outcome + return outcome + + def measure_qubit(self, qubitNum: int): + """ + Measures the desired qubit in the standard basis. This returns the classical outcome and deletes the qubit. + + :param qubitNum: The number of the qubit to measure. + :type qubitNum: int + """ + outcome = self.measure_qubit_inplace(qubitNum) + + self.qubitReg.pop(qubitNum) + + # Update the number of qubits + self.activeQubits = self.activeQubits - 1 + + return outcome + + def replace_qubit(self, qubitNum: int, state): + """ + Replaces the qubit at position qubitNum with the one given by state. + + :param qubitNum: Qubit to be replaced + :type qubitNum: int + :param state: New state to write in the place of the old qubit. + :type state: Any + """ + raise NotImplementedError("Currently you cannot replace a qubit using project Q as backend") + + def absorb(self, other): + """ + Absorb the qubits from the other engine into this one. This is done by tensoring the state at the end. + + :param other: The other qubit to absorb. + :type other: int + """ + # Check whether there is space + newNum = self.activeQubits + other.activeQubits + if newNum > self.maxQubits: + raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") + + # Check whether there are in fact qubits to tensor up.... + if self.activeQubits == 0: + self.eng = other.eng + self.qubitReg = list(other.qubitReg) + self.activeQubits = other.activeQubits + elif other.activeQubits > 0: + data = other.get_register_RI() + self.absorb_parts(*data, other.activeQubits) + + def absorb_parts(self, R, I, activeQ): + """ + Absorb the qubits, given in pieces + + :param R: Real part of the qubit state as a list. + :type R: List[float] + :param I: Imaginary part as a list. + :type I: List[float] + :param activeQ: Active number of qubits + """ + # Check whether there is space + newNum = self.activeQubits + activeQ + if newNum > self.maxQubits: + raise QuantumError("Cannot merge: qubits exceed the maximum available.\n") + + if activeQ > 0: + # Unpack the ordering of qubits and the real and imaginary part + order, (R, I) = R, I + + # Convert the real and imaginary parts to a state + state = [re + im * 1j for re, im in zip(R, I)] + + # Allocate qubits in this engine for the new qubits from the other engine + qreg = self.eng.allocate_qureg(activeQ) + + # Put the new qubits in the correct state + pQ.ops.StatePreparation(state) | qreg + + # Put the qubits in the correct order + # The `order` is a mapping from the previous qubit IDs + # to the bit position in the state. The qubits in the `qreg` + # are therefore in the old bit positions which needs to be updated. + new_qubits = [None] * len(qreg) + for old_q_id, old_bit_pos in order.items(): + new_qubits[old_q_id] = qreg[old_bit_pos] + + # Add the qubits to the list of qubits + self.qubitReg += new_qubits + + self.activeQubits = newNum diff --git a/simulaqron/virtual_node/qutip_simulator.py b/simulaqron/virtual_node/qutip_simulator.py index 8c10d95e..74491794 100644 --- a/simulaqron/virtual_node/qutip_simulator.py +++ b/simulaqron/virtual_node/qutip_simulator.py @@ -37,6 +37,17 @@ try: import qutip as qp import qutip.qip.operations.gates as gate_ops + + # qutip-qip >= 0.3 renamed/moved gate expansion helpers. + # Patch the old names as shims so the rest of this file needs no changes. + if not hasattr(gate_ops, "gate_expand_1toN"): + from qutip_qip.operations import expand_operator as _expand_op + gate_ops.gate_expand_1toN = lambda U, N, t: _expand_op(U, N, t) + + if not hasattr(qp, "gate_expand_2toN"): + from qutip_qip.operations import expand_operator as _expand_op + qp.gate_expand_2toN = lambda U, N, t1, t2: _expand_op(U, N, [t1, t2]) + except ImportError: raise RuntimeError("If you want to use the qutip backend you need to install the python package 'qutip'") @@ -404,6 +415,17 @@ def measure_qubit_inplace(self, qubitNum: int): obj = M1 * self.qubitReg p1 = obj.tr().real + # Clamp and renormalize to handle tiny negative values that can arise + # from floating-point rounding after multi-qubit gate sequences. + p0 = max(0.0, p0) + p1 = max(0.0, p1) + total = p0 + p1 + if total > 0: + p0 /= total + p1 /= total + else: + p0, p1 = 0.5, 0.5 + # Sample the measurement outcome from these probabilities outcome = int(np.random.choice([0, 1], 1, p=[p0, p1])) diff --git a/simulaqron/virtual_node/virtual.py b/simulaqron/virtual_node/virtual.py index 68b567cd..3ffb9fed 100644 --- a/simulaqron/virtual_node/virtual.py +++ b/simulaqron/virtual_node/virtual.py @@ -1318,7 +1318,6 @@ def remote_sim_qubit_num_in_same_reg(self, sim_qubit_num1: int, sim_qubit_num2: assert sim_qubit2 is not None, "Sim num {sim_qubit_num2} not in this node" return sim_qubit1.register == sim_qubit2.register - @inlineCallbacks def remote_stop_vnode(self): # sys.stdout.write(f"Stopping virtual node pid {os.getpid()}") # print(f"Stopping virtual node pid {os.getpid()}", flush=True) diff --git a/tests/quick/test_ping_pong.py b/tests/quick/test_ping_pong.py new file mode 100644 index 00000000..17de77ef --- /dev/null +++ b/tests/quick/test_ping_pong.py @@ -0,0 +1,87 @@ +""" +Test for the eventBased/pingPong example. + +Runs both Alice and Bob in a single process using asyncio tasks. +""" +import asyncio +import pytest + + +async def bob_handler(reader, writer): + """Bob's event loop — same logic as the example.""" + replies = [] + while True: + data = await reader.read(255) + if not data: + break + message = data.decode("utf-8") + if message == "ping": + reply = "pong" + else: + reply = "no way!" + replies.append((message, reply)) + writer.write(reply.encode("utf-8")) + await writer.drain() + return replies + + +async def run_scenario(port, messages): + """Run a full ping-pong and return Alice's results + Bob's log.""" + bob_log = [] + handler_done = asyncio.Event() + + async def on_connect(reader, writer): + result = await bob_handler(reader, writer) + bob_log.extend(result) + handler_done.set() + + server = await asyncio.start_server(on_connect, "localhost", port) + + # Alice connects and exchanges messages + alice_task = asyncio.create_task(_run_alice(port, messages)) + alice_results = await alice_task + + # Wait for Bob's handler to finish + await asyncio.wait_for(handler_done.wait(), timeout=5.0) + + # Shut down + server.close() + + return alice_results, bob_log + + +async def _run_alice(port, messages): + r, w = await asyncio.open_connection("localhost", port) + results = [] + for msg in messages: + w.write(msg.encode("utf-8")) + await w.drain() + reply_data = await r.read(255) + results.append((msg, reply_data.decode("utf-8"))) + w.close() + await w.wait_closed() + return results + + +@pytest.mark.parametrize("messages,expected", [ + ( + ["ping"], + [("ping", "pong")], + ), + ( + ["hello"], + [("hello", "no way!")], + ), + ( + ["ping", "ping", "hello", "ping"], + [("ping", "pong"), ("ping", "pong"), ("hello", "no way!"), ("ping", "pong")], + ), + ( + ["hi", "what", "test"], + [("hi", "no way!"), ("what", "no way!"), ("test", "no way!")], + ), +]) +def test_ping_pong(messages, expected): + alice, bob = asyncio.run(run_scenario(19876, messages)) + assert alice == expected + assert bob == expected diff --git a/tests/quick/test_polite_ping_pong.py b/tests/quick/test_polite_ping_pong.py new file mode 100644 index 00000000..b3d7b572 --- /dev/null +++ b/tests/quick/test_polite_ping_pong.py @@ -0,0 +1,192 @@ +""" +Test for the eventBased/politePingPong example. + +Runs both Alice and Bob in a single process using asyncio tasks. +Exercises the (state, message) dispatch table state machine and verifies +that each node traverses the expected sequence of states. +""" +import asyncio +import pytest + + +# ── Bob state machine (mirrors politeBob.py) ───────────────────────────────── + +STATE_WAITING_PING = "WAITING_PING" +STATE_WAITING_THANKS = "WAITING_THANKS" +STATE_DONE = "DONE" + + +async def bob_handler(reader, writer): + """ + In-process replica of run_bob from politeBob.py. + + Returns the sequence of (state_before, message, reply) triples so the + test can check every transition. + """ + log = [] + state = STATE_WAITING_PING + + while state != STATE_DONE: + data = await reader.read(255) + if not data: + break + msg = data.decode("utf-8") + + if state == STATE_WAITING_PING and msg == "ping": + reply = "pong" + writer.write(reply.encode("utf-8")) + await writer.drain() + log.append((state, msg, reply)) + state = STATE_WAITING_THANKS + + elif state == STATE_WAITING_THANKS and msg == "thank you": + reply = "you're welcome" + writer.write(reply.encode("utf-8")) + await writer.drain() + log.append((state, msg, reply)) + state = STATE_DONE + + else: + # Invalid transition — ignore (no reply, state unchanged) + log.append((state, msg, None)) + + return log + + +# ── Alice state machine (mirrors politeAlice.py) ────────────────────────────── + +STATE_WAITING_PONG = "WAITING_PONG" +STATE_WAITING_YOURE_WELCOME = "WAITING_YOURE_WELCOME" +ALICE_DONE = "DONE" + + +async def alice_handler(port): + """ + In-process replica of run_alice from politeAlice.py. + + Returns the sequence of (state_before, sent, received) triples. + """ + reader, writer = await asyncio.open_connection("localhost", port) + log = [] + state = STATE_WAITING_PONG + + # Initial action: send "ping" before entering the loop + opening = "ping" + writer.write(opening.encode("utf-8")) + await writer.drain() + + while state != ALICE_DONE: + data = await reader.read(255) + if not data: + break + msg = data.decode("utf-8") + + if state == STATE_WAITING_PONG and msg == "pong": + reply = "thank you" + writer.write(reply.encode("utf-8")) + await writer.drain() + log.append((state, msg, reply)) + state = STATE_WAITING_YOURE_WELCOME + + elif state == STATE_WAITING_YOURE_WELCOME and msg == "you're welcome": + log.append((state, msg, None)) + state = ALICE_DONE + + else: + log.append((state, msg, None)) + + writer.close() + await writer.wait_closed() + return log + + +# ── Test harness ────────────────────────────────────────────────────────────── + +async def run_exchange(port): + """Run a complete polite ping-pong exchange and return both logs.""" + bob_log = [] + handler_done = asyncio.Event() + + async def on_connect(reader, writer): + result = await bob_handler(reader, writer) + bob_log.extend(result) + handler_done.set() + + server = await asyncio.start_server(on_connect, "localhost", port) + + alice_log = await asyncio.create_task(alice_handler(port)) + + await asyncio.wait_for(handler_done.wait(), timeout=5.0) + server.close() + + return alice_log, bob_log + + +# ── Tests ───────────────────────────────────────────────────────────────────── + +def test_full_exchange_completes(): + """The happy path: Alice and Bob complete the full polite exchange.""" + alice_log, bob_log = asyncio.run(run_exchange(19877)) + + # Bob's two transitions + assert len(bob_log) == 2 + assert bob_log[0] == (STATE_WAITING_PING, "ping", "pong") + assert bob_log[1] == (STATE_WAITING_THANKS, "thank you", "you're welcome") + + # Alice's two transitions + assert len(alice_log) == 2 + assert alice_log[0] == (STATE_WAITING_PONG, "pong", "thank you") + assert alice_log[1] == (STATE_WAITING_YOURE_WELCOME, "you're welcome", None) + + +def test_bob_rejects_unexpected_message_in_wrong_state(): + """ + If Bob receives an unexpected message it stays in its current state + (the invalid-transition branch logs the message with reply=None). + """ + + async def run(): + log = [] + handler_done = asyncio.Event() + + async def on_connect(reader, writer): + result = await bob_handler(reader, writer) + log.extend(result) + handler_done.set() + + server = await asyncio.start_server(on_connect, "localhost", 19878) + + # Alice sends "thank you" first (wrong message for WAITING_PING), + # then the correct "ping". + reader, writer = await asyncio.open_connection("localhost", 19878) + + # Wrong message in WAITING_PING state + writer.write(b"thank you") + await writer.drain() + # Give Bob time to process (no reply expected) + await asyncio.sleep(0.05) + + # Now the correct sequence + writer.write(b"ping") + await writer.drain() + reply = await reader.read(255) + assert reply.decode() == "pong" + + writer.write(b"thank you") + await writer.drain() + reply2 = await reader.read(255) + assert reply2.decode() == "you're welcome" + + writer.close() + await writer.wait_closed() + await asyncio.wait_for(handler_done.wait(), timeout=5.0) + server.close() + return log + + log = asyncio.run(run()) + + # First entry: invalid transition (no reply) + assert log[0] == (STATE_WAITING_PING, "thank you", None) + # Second and third: normal transitions + assert log[1] == (STATE_WAITING_PING, "ping", "pong") + assert log[2] == (STATE_WAITING_THANKS, "thank you", "you're welcome") diff --git a/tests/slow/merges/test_merges.py b/tests/slow/merges/test_merges.py index 3880199b..37b284af 100644 --- a/tests/slow/merges/test_merges.py +++ b/tests/slow/merges/test_merges.py @@ -41,6 +41,14 @@ # * The functionality tested here is SimulaQron-specific. It would be difficult # to rewrite these tests in NetQASM, which might hide APIs needed to fully test # the underlying functionality. +# Stephanie: 2026-03-08 +# There is no point in moving this test to netqasm: this is is not the role of netqasm here +# best refer to the documentaiton or the simulaqron paper of what the architeture of simulaqron +# is. And in this architecture the merge is done by the simulaqron backend always, I dont get +# why one would consider moving this to netqasm. It's something that only happens for simulated +# qubits. One can, however, make a similar test through the netqasm interface in addition to this one +# and this may be valuable. But this does not mean making merges. The below also doesnt make +# manual merges: rather it takes actions that will trigger a merge in the backend. class localNode(pb.Root): diff --git a/tests/slow/sdk/test_flush.py b/tests/slow/sdk/test_flush.py new file mode 100644 index 00000000..3811a317 --- /dev/null +++ b/tests/slow/sdk/test_flush.py @@ -0,0 +1,153 @@ +# +# Tests for mid-circuit flush behavior. +# +# The NetQASM programming model batches quantum operations inside a +# `with NetQASMConnection(...) as conn:` block. Measurement results are +# "futures" — calling int(m) before the batch has executed is undefined. +# +# There are two ways to force execution: +# 1. Exit the `with` block (automatic flush on __exit__). +# 2. Call conn.flush() inside the block. +# +# These tests verify that flush() works mid-circuit so that users can: +# - Read measurement results inside the with block +# - Make classical decisions based on those results +# - Continue with more quantum operations +# +import pytest + +from netqasm.sdk.qubit import Qubit + +from simulaqron.settings import simulaqron_settings, network_config +from simulaqron.settings.simulaqron_config import SimBackend +from simulaqron.network import Network +from simulaqron.sdk.connection import SimulaQronConnection +from simulaqron.run.run import reset + + +class TestFlush: + @pytest.fixture(autouse=True) + def network(self): + simulaqron_settings.default_settings() + simulaqron_settings.sim_backend = SimBackend.STABILIZER + default_net_cfg_path = network_config.using_default_network() + network = Network(nodes=["Alice"], network_config_file=default_net_cfg_path) + network.start() + yield + network.stop() + reset() + + def test_flush_makes_measurement_readable(self): + """After flush(), int(m) should return 0 or 1.""" + with SimulaQronConnection("Alice") as conn: + q = Qubit(conn) + q.H() + m = q.measure() + conn.flush() + m_val = int(m) + assert m_val in (0, 1) + + def test_flush_deterministic_zero(self): + """Measure |0> — should always give 0.""" + with SimulaQronConnection("Alice") as conn: + q = Qubit(conn) + # no gates → |0> + m = q.measure() + conn.flush() + m_val = int(m) + assert m_val == 0 + + def test_flush_deterministic_one(self): + """Prepare |1> with X gate — should always give 1.""" + with SimulaQronConnection("Alice") as conn: + q = Qubit(conn) + q.X() + m = q.measure() + conn.flush() + m_val = int(m) + assert m_val == 1 + + def test_classical_decision_after_flush(self): + """ + Mid-circuit classical logic: measure, flush, decide, then do more + quantum operations based on the measurement result. + + Prepare |1> (via X), measure, flush, read result. + If result is 1, prepare a new qubit in |0> (no gate). + If result is 0, prepare a new qubit in |1> (X gate). + Either way, the second qubit's measurement should be (1 - first). + """ + with SimulaQronConnection("Alice") as conn: + q1 = Qubit(conn) + q1.X() # |1> + m1 = q1.measure() + conn.flush() + m1_val = int(m1) + assert m1_val == 1 + + # Classical decision: prepare opposite state + q2 = Qubit(conn) + if m1_val == 1: + pass # leave as |0> + else: + q2.X() # flip to |1> + m2 = q2.measure() + conn.flush() + m2_val = int(m2) + assert m2_val == 0 + assert m1_val + m2_val == 1 + + def test_multiple_flush_rounds(self): + """ + Three rounds of measure-flush-decide, each building on previous results. + This simulates a state-machine-like quantum program. + """ + results = [] + with SimulaQronConnection("Alice", max_qubits=5) as conn: + # Round 1: prepare |+>, measure + q1 = Qubit(conn) + q1.H() + m1 = q1.measure() + conn.flush() + r1 = int(m1) + results.append(r1) + assert r1 in (0, 1) + + # Round 2: if round 1 gave 0, prepare |1>; if 1, prepare |0> + q2 = Qubit(conn) + if r1 == 0: + q2.X() + m2 = q2.measure() + conn.flush() + r2 = int(m2) + results.append(r2) + # r2 should be the opposite of r1 + assert r2 == 1 - r1 + + # Round 3: prepare qubit based on XOR of previous results + q3 = Qubit(conn) + if r1 ^ r2 == 1: + q3.X() + m3 = q3.measure() + conn.flush() + r3 = int(m3) + results.append(r3) + # r1 XOR r2 is always 1 (since r2 = 1-r1), so r3 should be 1 + assert r3 == 1 + + assert len(results) == 3 + + def test_flush_then_exit_still_works(self): + """ + Values read after flush() inside the block should still be + valid after exiting the block. + """ + with SimulaQronConnection("Alice") as conn: + q = Qubit(conn) + q.X() + m = q.measure() + conn.flush() + inner_val = int(m) + + outer_val = int(m) + assert inner_val == outer_val == 1 From e828af3ef6e51762554d84d29c94961b5a877360 Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 18:54:41 +0100 Subject: [PATCH 228/277] Ensure all new-sdk examples are consistent - Add missing simulaqron_settings.json to teleport - Add --simulaqron-config-file flag to teleport/run.sh - Add sleep 1 to doNew.sh in teleport and classical-client-server - Add missing doNew.sh to midCircuitLogic - Normalize Bob ports to 8831-8833 in teleport and classical-client-server - Fix shebang in classical-client-server/run.sh (#!/bin/sh -> #!/usr/bin/env bash) Co-Authored-By: Claude Sonnet 4.6 --- examples/new-sdk/classical-client-server/doNew.sh | 1 + examples/new-sdk/classical-client-server/run.sh | 2 +- .../simulaqron_network.json | 6 +++--- examples/new-sdk/midCircuitLogic/doNew.sh | 5 +++++ examples/new-sdk/teleport/doNew.sh | 1 + examples/new-sdk/teleport/run.sh | 2 +- examples/new-sdk/teleport/simulaqron_network.json | 6 +++--- examples/new-sdk/teleport/simulaqron_settings.json | 14 ++++++++++++++ 8 files changed, 29 insertions(+), 8 deletions(-) mode change 100755 => 100644 examples/new-sdk/classical-client-server/doNew.sh mode change 100755 => 100644 examples/new-sdk/classical-client-server/run.sh create mode 100644 examples/new-sdk/midCircuitLogic/doNew.sh mode change 100755 => 100644 examples/new-sdk/teleport/doNew.sh mode change 100755 => 100644 examples/new-sdk/teleport/run.sh create mode 100644 examples/new-sdk/teleport/simulaqron_settings.json diff --git a/examples/new-sdk/classical-client-server/doNew.sh b/examples/new-sdk/classical-client-server/doNew.sh old mode 100755 new mode 100644 index b00dbb94..9dda467e --- a/examples/new-sdk/classical-client-server/doNew.sh +++ b/examples/new-sdk/classical-client-server/doNew.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash ./terminate.sh +sleep 1 ./run.sh diff --git a/examples/new-sdk/classical-client-server/run.sh b/examples/new-sdk/classical-client-server/run.sh old mode 100755 new mode 100644 index 9c28001a..d4ae9637 --- a/examples/new-sdk/classical-client-server/run.sh +++ b/examples/new-sdk/classical-client-server/run.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/bin/env bash # Running SimulaQron backend is not needed for this example. ## Check if SimulaQron is already running diff --git a/examples/new-sdk/classical-client-server/simulaqron_network.json b/examples/new-sdk/classical-client-server/simulaqron_network.json index bdd299cf..097d91d5 100644 --- a/examples/new-sdk/classical-client-server/simulaqron_network.json +++ b/examples/new-sdk/classical-client-server/simulaqron_network.json @@ -11,9 +11,9 @@ }, { "Bob": { - "app_socket": ["localhost", 9831], - "qnodeos_socket": ["localhost", 9832], - "vnode_socket": ["localhost", 9833] + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] } } ], diff --git a/examples/new-sdk/midCircuitLogic/doNew.sh b/examples/new-sdk/midCircuitLogic/doNew.sh new file mode 100644 index 00000000..c9bbe231 --- /dev/null +++ b/examples/new-sdk/midCircuitLogic/doNew.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +./terminate.sh +sleep 1 +./run.sh diff --git a/examples/new-sdk/teleport/doNew.sh b/examples/new-sdk/teleport/doNew.sh old mode 100755 new mode 100644 index b00dbb94..9dda467e --- a/examples/new-sdk/teleport/doNew.sh +++ b/examples/new-sdk/teleport/doNew.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash ./terminate.sh +sleep 1 ./run.sh diff --git a/examples/new-sdk/teleport/run.sh b/examples/new-sdk/teleport/run.sh old mode 100755 new mode 100644 index 0c364ae0..ce42b335 --- a/examples/new-sdk/teleport/run.sh +++ b/examples/new-sdk/teleport/run.sh @@ -18,7 +18,7 @@ if [ "$START_SIMULAQRON" = true ]; then # Check if SimulaQron is already running if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then # If not, start simulaqron backend for both nodes - simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json + simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json --simulaqron-config-file simulaqron_settings.json fi fi diff --git a/examples/new-sdk/teleport/simulaqron_network.json b/examples/new-sdk/teleport/simulaqron_network.json index bdd299cf..097d91d5 100644 --- a/examples/new-sdk/teleport/simulaqron_network.json +++ b/examples/new-sdk/teleport/simulaqron_network.json @@ -11,9 +11,9 @@ }, { "Bob": { - "app_socket": ["localhost", 9831], - "qnodeos_socket": ["localhost", 9832], - "vnode_socket": ["localhost", 9833] + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] } } ], diff --git a/examples/new-sdk/teleport/simulaqron_settings.json b/examples/new-sdk/teleport/simulaqron_settings.json new file mode 100644 index 00000000..b67c274e --- /dev/null +++ b/examples/new-sdk/teleport/simulaqron_settings.json @@ -0,0 +1,14 @@ +{ + "max_qubits": 20, + "max_registers": 1000, + "conn_retry_time": 0.5, + "conn_max_retries": 10, + "recv_timeout": 100, + "recv_retry_time": 0.1, + "recv_max_retries": 10, + "log_level": 30, + "sim_backend": "projectq", + "noisy_qubits": false, + "max_app_waiting_time": -1.0, + "t1": 1.0 +} From bef16516cf3d347dc82d391ebbdf94d3e172b290 Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 19:25:26 +0100 Subject: [PATCH 229/277] Fix all linter errors (make lint now exits 0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove leftover and unused import from qnodeos.py (debugging leftovers) - Remove unnecessary declarations in run.py, start_qnodeos.py, and start_vnode.py — the variable is only read in those scopes, not assigned, so is not needed and flake8 F824 flagged it - Add to the existing noqa comment in protocol.py for the long debug print line - Fix double space after in extendGHZ/bobTest.py (E271) - Add to aligned state constant blocks in eventBased examples and test — alignment is intentional for readability of the dispatch tables - Add to aligned dispatch table entries and assert tuples for the same reason - Remove unused from test_polite_ping_pong.py Co-Authored-By: Claude Sonnet 4.6 --- examples/eventBased/politePingPong/politeAlice.py | 6 +++--- examples/eventBased/politePingPong/politeBob.py | 6 +++--- examples/eventBased/quantumCorrRNG/aliceTest.py | 2 +- examples/eventBased/quantumCorrRNG/bobTest.py | 2 +- .../quantumCorrRNGVerified/aliceTest.py | 4 ++-- .../eventBased/quantumCorrRNGVerified/bobTest.py | 4 ++-- examples/new-sdk/extendGHZ/bobTest.py | 2 +- simulaqron/netqasm_backend/qnodeos.py | 2 -- simulaqron/run/run.py | 2 -- simulaqron/sdk/protocol.py | 2 +- simulaqron/start/start_qnodeos.py | 1 - simulaqron/start/start_vnode.py | 1 - tests/quick/test_polite_ping_pong.py | 15 +++++++-------- 13 files changed, 21 insertions(+), 28 deletions(-) diff --git a/examples/eventBased/politePingPong/politeAlice.py b/examples/eventBased/politePingPong/politeAlice.py index ca603c7a..6f6b2010 100644 --- a/examples/eventBased/politePingPong/politeAlice.py +++ b/examples/eventBased/politePingPong/politeAlice.py @@ -51,9 +51,9 @@ # ── States ─────────────────────────────────────────────────────────────────── -STATE_WAITING_PONG = "WAITING_PONG" +STATE_WAITING_PONG = "WAITING_PONG" # noqa: E221 STATE_WAITING_YOURE_WELCOME = "WAITING_YOURE_WELCOME" -STATE_DONE = "DONE" +STATE_DONE = "DONE" # noqa: E221 # ── Handlers ───────────────────────────────────────────────────────────────── @@ -88,7 +88,7 @@ async def handle_youre_welcome(writer: StreamWriter) -> str: # and anything not listed is automatically an invalid transition. ALICE_DISPATCH = { - (STATE_WAITING_PONG, "pong"): handle_pong, + (STATE_WAITING_PONG, "pong"): handle_pong, # noqa: E241 (STATE_WAITING_YOURE_WELCOME, "you're welcome"): handle_youre_welcome, } diff --git a/examples/eventBased/politePingPong/politeBob.py b/examples/eventBased/politePingPong/politeBob.py index 7619503c..aeaa1c77 100644 --- a/examples/eventBased/politePingPong/politeBob.py +++ b/examples/eventBased/politePingPong/politeBob.py @@ -41,9 +41,9 @@ # Each constant names a state in Bob's state machine. # The string value is used in log output so keep it human-readable. -STATE_WAITING_PING = "WAITING_PING" +STATE_WAITING_PING = "WAITING_PING" # noqa: E221 STATE_WAITING_THANKS = "WAITING_THANKS" -STATE_DONE = "DONE" +STATE_DONE = "DONE" # noqa: E221 # ── Handlers ───────────────────────────────────────────────────────────────── @@ -83,7 +83,7 @@ async def handle_thank_you(writer: StreamWriter) -> str: # and anything not listed is automatically an invalid transition. BOB_DISPATCH = { - (STATE_WAITING_PING, "ping"): handle_ping, + (STATE_WAITING_PING, "ping"): handle_ping, # noqa: E241 (STATE_WAITING_THANKS, "thank you"): handle_thank_you, } diff --git a/examples/eventBased/quantumCorrRNG/aliceTest.py b/examples/eventBased/quantumCorrRNG/aliceTest.py index 0fdf67aa..dc092956 100644 --- a/examples/eventBased/quantumCorrRNG/aliceTest.py +++ b/examples/eventBased/quantumCorrRNG/aliceTest.py @@ -44,7 +44,7 @@ # ── States ─────────────────────────────────────────────────────────────────── STATE_WAITING_ACCEPT = "WAITING_ACCEPT" -STATE_DONE = "DONE" +STATE_DONE = "DONE" # noqa: E221 # ── Handlers ───────────────────────────────────────────────────────────────── diff --git a/examples/eventBased/quantumCorrRNG/bobTest.py b/examples/eventBased/quantumCorrRNG/bobTest.py index 00e80f62..0b17869b 100644 --- a/examples/eventBased/quantumCorrRNG/bobTest.py +++ b/examples/eventBased/quantumCorrRNG/bobTest.py @@ -38,7 +38,7 @@ # ── States ─────────────────────────────────────────────────────────────────── STATE_WAITING_PROPOSAL = "WAITING_PROPOSAL" -STATE_DONE = "DONE" +STATE_DONE = "DONE" # noqa: E221 # ── Handlers ───────────────────────────────────────────────────────────────── diff --git a/examples/eventBased/quantumCorrRNGVerified/aliceTest.py b/examples/eventBased/quantumCorrRNGVerified/aliceTest.py index 2f67f84e..adff0520 100644 --- a/examples/eventBased/quantumCorrRNGVerified/aliceTest.py +++ b/examples/eventBased/quantumCorrRNGVerified/aliceTest.py @@ -52,9 +52,9 @@ # ── States ─────────────────────────────────────────────────────────────────── -STATE_WAITING_ACCEPT = "WAITING_ACCEPT" +STATE_WAITING_ACCEPT = "WAITING_ACCEPT" # noqa: E221 STATE_WAITING_VERIFICATION = "WAITING_VERIFICATION" -STATE_DONE = "DONE" +STATE_DONE = "DONE" # noqa: E221 # ── Handlers ───────────────────────────────────────────────────────────────── diff --git a/examples/eventBased/quantumCorrRNGVerified/bobTest.py b/examples/eventBased/quantumCorrRNGVerified/bobTest.py index 4062342e..5ba6e46d 100644 --- a/examples/eventBased/quantumCorrRNGVerified/bobTest.py +++ b/examples/eventBased/quantumCorrRNGVerified/bobTest.py @@ -46,9 +46,9 @@ # ── States ─────────────────────────────────────────────────────────────────── -STATE_WAITING_PROPOSAL = "WAITING_PROPOSAL" +STATE_WAITING_PROPOSAL = "WAITING_PROPOSAL" # noqa: E221 STATE_WAITING_ALICE_RESULT = "WAITING_ALICE_RESULT" -STATE_DONE = "DONE" +STATE_DONE = "DONE" # noqa: E221 # Bob stores his measurement result here so the verification handler can # compare it with Alice's result. diff --git a/examples/new-sdk/extendGHZ/bobTest.py b/examples/new-sdk/extendGHZ/bobTest.py index 563df82a..caff88ad 100644 --- a/examples/new-sdk/extendGHZ/bobTest.py +++ b/examples/new-sdk/extendGHZ/bobTest.py @@ -23,7 +23,7 @@ async def send_to_charlie(reader: StreamReader, writer: StreamWriter): writer.write("receive_qubit".encode("utf-8")) - message = await reader.read(100) + message = await reader.read(100) assert message.decode("utf-8") == "continue" diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index e2fc03af..4c35738c 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -1,5 +1,4 @@ import logging -import sys from typing import Optional, Dict, Callable, Generator, Any, List, Type @@ -13,7 +12,6 @@ from simulaqron.netqasm_backend.executioner import VanillaSimulaQronExecutioner from simulaqron.sdk.connection import (NewMessageType, GetQubitStateMessage, ReturnQubitStateMessage) -from simulaqron.settings import simulaqron_settings class SubroutineHandler(QNodeController): diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index eae52185..a584a627 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -80,7 +80,6 @@ def _worker_initializer(synced_array: SynchronizedArray): def _app_wrapper(**kwargs): - global _apps_pids assert _apps_pids is not None assert "__instance_num" in kwargs and isinstance(kwargs["__instance_num"], int) assert "__entry_function" in kwargs and isinstance(kwargs["__entry_function"], Callable) @@ -102,7 +101,6 @@ def _app_wrapper(**kwargs): def _signal_other_apps(): - global _apps_pids assert _apps_pids is not None for pid in _apps_pids: # Do not send SIGINT to self process diff --git a/simulaqron/sdk/protocol.py b/simulaqron/sdk/protocol.py index c30904c0..0ef19605 100644 --- a/simulaqron/sdk/protocol.py +++ b/simulaqron/sdk/protocol.py @@ -127,7 +127,7 @@ async def _build_server(self): self._sockets_data.port ) print(f"{self._node_name.lower()} INFO: === {self._node_name} Server ===") - print(f"{self._node_name.lower()} DEBUG: Listening on {self._sockets_data.hostname}:{self._sockets_data.port}") # noqa: E231 + print(f"{self._node_name.lower()} DEBUG: Listening on {self._sockets_data.hostname}:{self._sockets_data.port}") # noqa: E231,E501 async with server: await server.serve_forever() diff --git a/simulaqron/start/start_qnodeos.py b/simulaqron/start/start_qnodeos.py index c1d84de4..7ce64441 100644 --- a/simulaqron/start/start_qnodeos.py +++ b/simulaqron/start/start_qnodeos.py @@ -114,7 +114,6 @@ def _setup_netqasm_server(my_name: str, netqasm_factory: NetQASMFactory): def _sigterm_handler(_signo, _stack_frame): - global stdout_file if stdout_file is not None: stdout_file.flush() stdout_file.close() diff --git a/simulaqron/start/start_vnode.py b/simulaqron/start/start_vnode.py index 50080508..45b72d8b 100644 --- a/simulaqron/start/start_vnode.py +++ b/simulaqron/start/start_vnode.py @@ -46,7 +46,6 @@ def _sigterm_handler(name, _signo, _stack_frame): print(f"START_VNODE: Shutting down Node '{name}' from signal {_signo}.", flush=True) - global stdout_file if stdout_file is not None: stdout_file.flush() stdout_file.close() diff --git a/tests/quick/test_polite_ping_pong.py b/tests/quick/test_polite_ping_pong.py index b3d7b572..85ac6c6f 100644 --- a/tests/quick/test_polite_ping_pong.py +++ b/tests/quick/test_polite_ping_pong.py @@ -6,14 +6,13 @@ that each node traverses the expected sequence of states. """ import asyncio -import pytest # ── Bob state machine (mirrors politeBob.py) ───────────────────────────────── -STATE_WAITING_PING = "WAITING_PING" +STATE_WAITING_PING = "WAITING_PING" # noqa: E221 STATE_WAITING_THANKS = "WAITING_THANKS" -STATE_DONE = "DONE" +STATE_DONE = "DONE" # noqa: E221 async def bob_handler(reader, writer): @@ -55,9 +54,9 @@ async def bob_handler(reader, writer): # ── Alice state machine (mirrors politeAlice.py) ────────────────────────────── -STATE_WAITING_PONG = "WAITING_PONG" +STATE_WAITING_PONG = "WAITING_PONG" # noqa: E221 STATE_WAITING_YOURE_WELCOME = "WAITING_YOURE_WELCOME" -ALICE_DONE = "DONE" +ALICE_DONE = "DONE" # noqa: E221 async def alice_handler(port): @@ -130,12 +129,12 @@ def test_full_exchange_completes(): # Bob's two transitions assert len(bob_log) == 2 - assert bob_log[0] == (STATE_WAITING_PING, "ping", "pong") + assert bob_log[0] == (STATE_WAITING_PING, "ping", "pong") # noqa: E241 assert bob_log[1] == (STATE_WAITING_THANKS, "thank you", "you're welcome") # Alice's two transitions assert len(alice_log) == 2 - assert alice_log[0] == (STATE_WAITING_PONG, "pong", "thank you") + assert alice_log[0] == (STATE_WAITING_PONG, "pong", "thank you") # noqa: E241 assert alice_log[1] == (STATE_WAITING_YOURE_WELCOME, "you're welcome", None) @@ -188,5 +187,5 @@ async def on_connect(reader, writer): # First entry: invalid transition (no reply) assert log[0] == (STATE_WAITING_PING, "thank you", None) # Second and third: normal transitions - assert log[1] == (STATE_WAITING_PING, "ping", "pong") + assert log[1] == (STATE_WAITING_PING, "ping", "pong") # noqa: E241 assert log[2] == (STATE_WAITING_THANKS, "thank you", "you're welcome") From 04450e7f1a00885125d20388b0f135ea85221dd5 Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 19:59:25 +0100 Subject: [PATCH 230/277] Fix SyntaxWarning in stabilizer_states.py docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \sqrt in a non-raw docstring is an invalid escape sequence in Python 3.12. Escape to \sqrt — Sphinx renders it identically. Co-Authored-By: Claude Sonnet 4.6 --- simulaqron/toolbox/stabilizer_states.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simulaqron/toolbox/stabilizer_states.py b/simulaqron/toolbox/stabilizer_states.py index 6bf24338..33580641 100644 --- a/simulaqron/toolbox/stabilizer_states.py +++ b/simulaqron/toolbox/stabilizer_states.py @@ -36,10 +36,10 @@ def __init__(self, data=None, check_symplectic: bool = True): A qubit in the state :math:`|1>` can be created as ``StabilizerState([[0, 1, 1]])``. - The entangled state :math:`(|00> + |11>)/\sqrt(2)` can be created as + The entangled state :math:`(|00> + |11>)/\\sqrt(2)` can be created as ``StabilizerState([[1, 1, 0, 0], [0, 0, 1, 1]])``. - The entangled state :math:`(|01> + |10>)/\sqrt(2)` can be created as + The entangled state :math:`(|01> + |10>)/\\sqrt(2)` can be created as ``StabilizerState([[1, 1, 0, 0, 0], [0, 0, 1, 1, 1]])``. :param data: Can be one of the following: From d721c96f5d919a7ffa0e0b8a1c049fc7a9a1702b Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 20:06:35 +0100 Subject: [PATCH 231/277] Fix CI: add make examples target, replace netqasm.yaml with slow tests - Add make examples target running the 6 new-sdk and 4 nativeMode examples (excludes the two skeleton templates and the eventBased examples) - Replace netqasm.yaml (which cloned an external repo and installed the deprecated projectq) with a simple slow-test workflow running make tests_slow - Add tests_slow, tests_all and examples to .PHONY Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/netqasm.yaml | 22 +++++----------------- Makefile | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.github/workflows/netqasm.yaml b/.github/workflows/netqasm.yaml index a480ac41..b372345a 100644 --- a/.github/workflows/netqasm.yaml +++ b/.github/workflows/netqasm.yaml @@ -1,29 +1,17 @@ -name: NetQASM backend tests +name: Slow tests on: push jobs: - examples: - name: Run examples + slow-tests: + name: Run slow tests runs-on: ubuntu-latest steps: - uses: actions/checkout@master - uses: actions/setup-python@master with: python-version: "3.12" - - name: Clone and install netqasm - run: | - git clone https://github.com/QuTech-Delft/netqasm.git - cd netqasm - make install - cd .. - name: Install simulaqron run: make install - - name: Install pybind - run: pip install pybind11 - - name: Install projectq - run: pip install -Iv projectq==0.8.0 - - name: Run examples - env: - NETQASM_SIMULATOR: simulaqron - run: make -C netqasm external-examples + - name: Run slow tests + run: make tests_slow diff --git a/Makefile b/Makefile index e00887d6..f72e9b86 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,21 @@ tests_slow: tests_all: @${PYTHON} -m pytest -v --capture=tee-sys ${TEST_DIR} +examples: + @echo "--- new-sdk examples ---" + @cd examples/new-sdk/corrRNG && timeout 90 bash run.sh + @cd examples/new-sdk/extendGHZ && timeout 90 bash run.sh + @cd examples/new-sdk/teleport && timeout 90 bash run.sh + @cd examples/new-sdk/classical-client-server && timeout 90 bash run.sh + @cd examples/new-sdk/template-quantum-local && timeout 90 bash run.sh + @cd examples/new-sdk/midCircuitLogic && timeout 90 bash run.sh + @echo "--- nativeMode examples ---" + @cd examples/nativeMode/corrRNG && timeout 90 bash run.sh + @cd examples/nativeMode/extendGHZ && timeout 90 bash run.sh + @cd examples/nativeMode/teleport && timeout 90 bash run.sh + @cd examples/nativeMode/graphState && timeout 90 bash run.sh + @echo "All examples passed." + install: test-deps @$(PYTHON) -m pip install -e . ${PIP_FLAGS} @@ -60,4 +75,4 @@ _build: build: _clear_build _build -.PHONY: clean lint python-deps tests full_tests verify build +.PHONY: clean lint python-deps tests tests_slow tests_all examples full_tests verify build From 05f3493c47c54fd4c47b8780c6b2d4e61da31a58 Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 20:31:29 +0100 Subject: [PATCH 232/277] Fix missing sleep between node launches in nativeMode examples Without a sleep, the second node tries to connect to the first node's classical server before it is ready, causing the connection to fail. Matches the pattern already used correctly in graphState. Co-Authored-By: Claude Sonnet 4.6 --- examples/nativeMode/corrRNG/run.sh | 1 + examples/nativeMode/extendGHZ/run.sh | 2 ++ examples/nativeMode/teleport/run.sh | 1 + examples/nativeMode/template/run.sh | 1 + 4 files changed, 5 insertions(+) mode change 100755 => 100644 examples/nativeMode/corrRNG/run.sh mode change 100755 => 100644 examples/nativeMode/extendGHZ/run.sh mode change 100755 => 100644 examples/nativeMode/teleport/run.sh mode change 100755 => 100644 examples/nativeMode/template/run.sh diff --git a/examples/nativeMode/corrRNG/run.sh b/examples/nativeMode/corrRNG/run.sh old mode 100755 new mode 100644 index ba6a394b..111f4f3e --- a/examples/nativeMode/corrRNG/run.sh +++ b/examples/nativeMode/corrRNG/run.sh @@ -10,6 +10,7 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then fi python3 bobTest.py & +sleep 1 python3 aliceTest.py diff --git a/examples/nativeMode/extendGHZ/run.sh b/examples/nativeMode/extendGHZ/run.sh old mode 100755 new mode 100644 index 5c9126de..30e03289 --- a/examples/nativeMode/extendGHZ/run.sh +++ b/examples/nativeMode/extendGHZ/run.sh @@ -10,7 +10,9 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then fi python3 bobTest.py & +sleep 1 python3 charlieTest.py & +sleep 1 python3 aliceTest.py diff --git a/examples/nativeMode/teleport/run.sh b/examples/nativeMode/teleport/run.sh old mode 100755 new mode 100644 index 3a3d3b3a..24202403 --- a/examples/nativeMode/teleport/run.sh +++ b/examples/nativeMode/teleport/run.sh @@ -10,6 +10,7 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then fi python3 bobTest.py & +sleep 1 python3 aliceTest.py diff --git a/examples/nativeMode/template/run.sh b/examples/nativeMode/template/run.sh old mode 100755 new mode 100644 index c1792453..8713f221 --- a/examples/nativeMode/template/run.sh +++ b/examples/nativeMode/template/run.sh @@ -14,4 +14,5 @@ fi # Run the files for Alice, Bob or whatever nodes you construct python3 bobTest.py & +sleep 1 python3 aliceTest.py From 080bffc29adb40ad42e3dc01ea83829c4e2ce15c Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 20:36:29 +0100 Subject: [PATCH 233/277] Fix virtRoot race condition in nativeMode server nodes When the initiating node (Alice/Bob) finishes setup and immediately calls a remote method on a peer, that peer may not have finished connecting to its virtual node yet, leaving virtRoot as None. Add a deferLater polling loop (matching the pattern already used in extendGHZ/bobTest.py) to each remote handler that uses virtRoot, so it waits until setup_local has set the connection before proceeding. Affected: corrRNG/bobTest, teleport/bobTest, extendGHZ/charlieTest, graphState/bobTest, graphState/charlieTest, graphState/davidTest. Co-Authored-By: Claude Sonnet 4.6 --- examples/nativeMode/corrRNG/bobTest.py | 7 +++++++ examples/nativeMode/extendGHZ/charlieTest.py | 7 +++++++ examples/nativeMode/graphState/bobTest.py | 7 +++++++ examples/nativeMode/graphState/charlieTest.py | 7 +++++++ examples/nativeMode/graphState/davidTest.py | 7 +++++++ examples/nativeMode/teleport/bobTest.py | 7 +++++++ 6 files changed, 42 insertions(+) diff --git a/examples/nativeMode/corrRNG/bobTest.py b/examples/nativeMode/corrRNG/bobTest.py index 48ecdbe9..5178dd27 100644 --- a/examples/nativeMode/corrRNG/bobTest.py +++ b/examples/nativeMode/corrRNG/bobTest.py @@ -33,7 +33,9 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import network_config +from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks +from twisted.internet.task import deferLater from twisted.spread import pb from qutip import Qobj @@ -98,6 +100,11 @@ def remote_process_qubit(self, virtualNum): virtualNum number of the virtual qubit corresponding to the EPR pair received """ + # Wait until our virtual node connection is ready — Alice may call us + # before setup_local has finished connecting to the virtual node. + while self.virtRoot is None: + yield deferLater(reactor, 0.05, lambda: None) + qB = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) # Measure diff --git a/examples/nativeMode/extendGHZ/charlieTest.py b/examples/nativeMode/extendGHZ/charlieTest.py index f6b05d09..21210453 100644 --- a/examples/nativeMode/extendGHZ/charlieTest.py +++ b/examples/nativeMode/extendGHZ/charlieTest.py @@ -32,7 +32,9 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import network_config +from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks +from twisted.internet.task import deferLater from twisted.spread import pb @@ -97,6 +99,11 @@ def remote_receive_ghz(self, virtualNum): logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + # Wait until our virtual node connection is ready — Bob may call us + # before setup_local has finished connecting to the virtual node. + while self.virtRoot is None: + yield deferLater(reactor, 0.05, lambda: None) + q = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) # Measure it diff --git a/examples/nativeMode/graphState/bobTest.py b/examples/nativeMode/graphState/bobTest.py index 54c2e919..f3f79047 100644 --- a/examples/nativeMode/graphState/bobTest.py +++ b/examples/nativeMode/graphState/bobTest.py @@ -33,7 +33,9 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import network_config +from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks +from twisted.internet.task import deferLater from twisted.spread import pb @@ -94,6 +96,11 @@ def remote_receive_qubit(self, virtualNum): logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + # Wait until our virtual node connection is ready — Alice may call us + # before setup_local has finished connecting to the virtual node. + while self.virtRoot is None: + yield deferLater(reactor, 0.05, lambda: None) + # Get ref of qubit qB = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) diff --git a/examples/nativeMode/graphState/charlieTest.py b/examples/nativeMode/graphState/charlieTest.py index 25186550..2ace3d13 100644 --- a/examples/nativeMode/graphState/charlieTest.py +++ b/examples/nativeMode/graphState/charlieTest.py @@ -33,7 +33,9 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import network_config +from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks +from twisted.internet.task import deferLater from twisted.spread import pb @@ -97,6 +99,11 @@ def remote_receive_qubit(self, virtualNum, sender): logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + # Wait until our virtual node connection is ready — Bob may call us + # before setup_local has finished connecting to the virtual node. + while self.virtRoot is None: + yield deferLater(reactor, 0.05, lambda: None) + # Get ref of qubit self.qC = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) qC = self.qC diff --git a/examples/nativeMode/graphState/davidTest.py b/examples/nativeMode/graphState/davidTest.py index 2cdd8f5d..bf82bd10 100644 --- a/examples/nativeMode/graphState/davidTest.py +++ b/examples/nativeMode/graphState/davidTest.py @@ -33,7 +33,9 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import network_config +from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks +from twisted.internet.task import deferLater from twisted.spread import pb @@ -94,6 +96,11 @@ def remote_receive_qubit(self, virtualNum): logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + # Wait until our virtual node connection is ready — Charlie may call us + # before setup_local has finished connecting to the virtual node. + while self.virtRoot is None: + yield deferLater(reactor, 0.05, lambda: None) + # Get ref of qubit qD = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index 7fdb6c44..9036b58f 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -36,7 +36,9 @@ from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import simulaqron_settings, network_config, LOCAL_SIMULAQRON_SETTINGS, LOCAL_NETWORK_SETTINGS from simulaqron.toolbox.stabilizer_states import StabilizerState +from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks +from twisted.internet.task import deferLater from twisted.spread import pb @@ -102,6 +104,11 @@ def remote_recover_teleport(self, a, b, virtualNum): logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + # Wait until our virtual node connection is ready — Alice may call us + # before setup_local has finished connecting to the virtual node. + while self.virtRoot is None: + yield deferLater(reactor, 0.05, lambda: None) + eprB = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) # Apply the desired correction info From 28b9bccf8beacf6c5e603ef8a663233907f253c9 Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 20:51:18 +0100 Subject: [PATCH 234/277] Changed examples to not include all (e.g. not NativeMode) --- Makefile | 22 +++++++++++++++------- pyproject.toml | 3 +-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index f72e9b86..5dc6a0b2 100644 --- a/Makefile +++ b/Makefile @@ -36,20 +36,25 @@ tests_slow: tests_all: @${PYTHON} -m pytest -v --capture=tee-sys ${TEST_DIR} +_example_cleanup: + @simulaqron stop 2>/dev/null || true + @sleep 2 + examples: @echo "--- new-sdk examples ---" @cd examples/new-sdk/corrRNG && timeout 90 bash run.sh + @$(MAKE) _example_cleanup @cd examples/new-sdk/extendGHZ && timeout 90 bash run.sh + @$(MAKE) _example_cleanup @cd examples/new-sdk/teleport && timeout 90 bash run.sh + @$(MAKE) _example_cleanup @cd examples/new-sdk/classical-client-server && timeout 90 bash run.sh + @$(MAKE) _example_cleanup @cd examples/new-sdk/template-quantum-local && timeout 90 bash run.sh + @$(MAKE) _example_cleanup @cd examples/new-sdk/midCircuitLogic && timeout 90 bash run.sh - @echo "--- nativeMode examples ---" - @cd examples/nativeMode/corrRNG && timeout 90 bash run.sh - @cd examples/nativeMode/extendGHZ && timeout 90 bash run.sh - @cd examples/nativeMode/teleport && timeout 90 bash run.sh - @cd examples/nativeMode/graphState && timeout 90 bash run.sh - @echo "All examples passed." + @$(MAKE) _example_cleanup + @echo "All new sdk examples passed." install: test-deps @$(PYTHON) -m pip install -e . ${PIP_FLAGS} @@ -57,6 +62,9 @@ install: test-deps _verified: @echo "SimulaQron is verified!" +ci: lint tests tests_slow examples + @echo "All CI checks passed." + verify: clean python-deps lint tests _verified _remove_build: @@ -75,4 +83,4 @@ _build: build: _clear_build _build -.PHONY: clean lint python-deps tests tests_slow tests_all examples full_tests verify build +.PHONY: clean lint python-deps tests tests_slow tests_all examples _example_cleanup ci full_tests verify build diff --git a/pyproject.toml b/pyproject.toml index e5b01c83..6fd80d6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,6 @@ classifiers = [ test = [ "flake8>=5.0.0,<6.0.0", "pytest>=7.1, <8.0", - "projectq<=0.8.0", "qutip<5.0.0" ] opt = [ @@ -71,4 +70,4 @@ exclude = ["simulaqron/config"] simulaqron = "simulaqron.cli:cli_entry_point" [project.urls] -Homepage = "https://github.com/SoftwareQuTech/SimulaQron" \ No newline at end of file +Homepage = "https://github.com/SoftwareQuTech/SimulaQron" From 946f86e6fe8969dcba8828d0d25b758e9482cec9 Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 22:06:43 +0100 Subject: [PATCH 235/277] Fix nativeMode race conditions: add PB retry logic to setup_local and _wait_ready guards to server nodes - Replace blocking time.sleep(3) in setup_local with _connect_with_retry(), mirroring the vnode retry pattern (conn_retry_time/conn_max_retries settings) - Fix missing return after reactor.stop() in _init_register (caused UnboundLocalError on virtRoot when connection failed) - Add _wait_ready() polling guard to all nativeMode server node files so remote calls wait for virtRoot/qReg to be initialized - Add flush=True to server node prints (fixes stdout buffering when output is redirected) - Add sleep 5 after simulaqron start in all nativeMode run.sh scripts - Update Makefile examples target with per-example terminate.sh cleanup Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + CLAUDE.md | 3 + Makefile | 21 ++--- examples/nativeMode/corrRNG/bobTest.py | 13 +-- examples/nativeMode/corrRNG/run.sh | 3 +- examples/nativeMode/extendGHZ/bobTest.py | 12 ++- examples/nativeMode/extendGHZ/charlieTest.py | 15 ++-- examples/nativeMode/extendGHZ/run.sh | 4 +- examples/nativeMode/graphState/bobTest.py | 15 ++-- examples/nativeMode/graphState/charlieTest.py | 15 ++-- examples/nativeMode/graphState/davidTest.py | 15 ++-- examples/nativeMode/graphState/run.sh | 2 + examples/nativeMode/teleport/bobTest.py | 15 ++-- examples/nativeMode/teleport/run.sh | 3 +- examples/nativeMode/template/run.sh | 2 +- simulaqron/local/setup.py | 81 ++++++++++++++----- 16 files changed, 147 insertions(+), 73 deletions(-) diff --git a/.gitignore b/.gitignore index 7dc413ff..a234c35c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ .idea/* .vscode/* +.claude/* cqc/backend/logFile* examples/**/log/* diff --git a/CLAUDE.md b/CLAUDE.md index 3e89652c..2c0d29cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,9 @@ - **Core code:** `simulaqron/` — simulator source - **Docs:** `docs/` — Sphinx documentation (currently outdated, being rewritten) +## Git / Commit Policy +- **Never commit or push without explicit user approval.** Always show a summary of staged changes and wait for a clear "go ahead" before running `git commit`. + ## Virtual Environment - The project venv is at `.venv/` — activate with `source .venv/bin/activate` - Install with `pip install -e .` (skip `[test]` if projectq build fails) diff --git a/Makefile b/Makefile index 5dc6a0b2..8eb42b6c 100644 --- a/Makefile +++ b/Makefile @@ -36,25 +36,18 @@ tests_slow: tests_all: @${PYTHON} -m pytest -v --capture=tee-sys ${TEST_DIR} -_example_cleanup: - @simulaqron stop 2>/dev/null || true - @sleep 2 - examples: @echo "--- new-sdk examples ---" @cd examples/new-sdk/corrRNG && timeout 90 bash run.sh - @$(MAKE) _example_cleanup + @cd examples/new-sdk/corrRNG && bash terminate.sh && sleep 3 @cd examples/new-sdk/extendGHZ && timeout 90 bash run.sh - @$(MAKE) _example_cleanup + @cd examples/new-sdk/extendGHZ && bash terminate.sh && sleep 3 @cd examples/new-sdk/teleport && timeout 90 bash run.sh - @$(MAKE) _example_cleanup - @cd examples/new-sdk/classical-client-server && timeout 90 bash run.sh - @$(MAKE) _example_cleanup - @cd examples/new-sdk/template-quantum-local && timeout 90 bash run.sh - @$(MAKE) _example_cleanup + @cd examples/new-sdk/teleport && bash terminate.sh && sleep 3 @cd examples/new-sdk/midCircuitLogic && timeout 90 bash run.sh - @$(MAKE) _example_cleanup - @echo "All new sdk examples passed." + @cd examples/new-sdk/midCircuitLogic && bash terminate.sh && sleep 3 + @cd examples/nativeMode/teleport && bash terminate.sh && sleep 3 + @echo "Chosen examples passed." install: test-deps @$(PYTHON) -m pip install -e . ${PIP_FLAGS} @@ -83,4 +76,4 @@ _build: build: _clear_build _build -.PHONY: clean lint python-deps tests tests_slow tests_all examples _example_cleanup ci full_tests verify build +.PHONY: clean lint python-deps tests tests_slow tests_all examples ci full_tests verify build diff --git a/examples/nativeMode/corrRNG/bobTest.py b/examples/nativeMode/corrRNG/bobTest.py index 5178dd27..4b3e3566 100644 --- a/examples/nativeMode/corrRNG/bobTest.py +++ b/examples/nativeMode/corrRNG/bobTest.py @@ -89,6 +89,12 @@ def set_virtual_reg(self, qReg): def remote_test(self): return "Tested!" + @inlineCallbacks + def _wait_ready(self): + """Wait until setup_local has connected us to the virtual node.""" + while self.virtRoot is None or self.qReg is None: + yield deferLater(reactor, 0.1, lambda: None) + # This can be called by Alice to tell Bob to process the qubit @inlineCallbacks @@ -100,17 +106,14 @@ def remote_process_qubit(self, virtualNum): virtualNum number of the virtual qubit corresponding to the EPR pair received """ - # Wait until our virtual node connection is ready — Alice may call us - # before setup_local has finished connecting to the virtual node. - while self.virtRoot is None: - yield deferLater(reactor, 0.05, lambda: None) + yield self._wait_ready() qB = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) # Measure x = yield qB.callRemote("measure") - print("BOB: My Random Number is ", x, "\n") + print("BOB: My Random Number is ", x, "\n", flush=True) def assemble_qubit(self, realM, imagM): """ diff --git a/examples/nativeMode/corrRNG/run.sh b/examples/nativeMode/corrRNG/run.sh index 111f4f3e..7c98c80b 100644 --- a/examples/nativeMode/corrRNG/run.sh +++ b/examples/nativeMode/corrRNG/run.sh @@ -9,8 +9,9 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then fi fi +sleep 5 + python3 bobTest.py & -sleep 1 python3 aliceTest.py diff --git a/examples/nativeMode/extendGHZ/bobTest.py b/examples/nativeMode/extendGHZ/bobTest.py index 434934f4..80192684 100644 --- a/examples/nativeMode/extendGHZ/bobTest.py +++ b/examples/nativeMode/extendGHZ/bobTest.py @@ -32,7 +32,9 @@ from simulaqron.local.setup import setup_local from simulaqron.general.host_config import SocketsConfig from simulaqron.settings import network_config +from twisted.internet import reactor from twisted.internet.defer import inlineCallbacks +from twisted.internet.task import deferLater from twisted.spread import pb @@ -85,6 +87,12 @@ def set_virtual_reg(self, qReg): def remote_test(self): return "Tested!" + @inlineCallbacks + def _wait_ready(self): + """Wait until setup_local has connected us to the virtual node.""" + while self.virtRoot is None or self.qReg is None: + yield deferLater(reactor, 0.1, lambda: None) + # This can be called by Alice to tell Bob where to get the qubit and what corrections to apply @inlineCallbacks @@ -97,6 +105,8 @@ def remote_receive_epr(self, virtualNum): virtualNum number of the virtual qubit corresponding to the EPR pair received """ + yield self._wait_ready() + logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) # Get a reference to our side of the EPR pair @@ -115,7 +125,7 @@ def remote_receive_epr(self, virtualNum): # Measure our qubit outcome = yield eprB.callRemote("measure") - print(f"Bob's outcome was: {outcome}") + print(f"Bob's outcome was: {outcome}", flush=True) ##################################################################################################### diff --git a/examples/nativeMode/extendGHZ/charlieTest.py b/examples/nativeMode/extendGHZ/charlieTest.py index 21210453..33b15fcc 100644 --- a/examples/nativeMode/extendGHZ/charlieTest.py +++ b/examples/nativeMode/extendGHZ/charlieTest.py @@ -86,6 +86,12 @@ def set_virtual_reg(self, qReg): def remote_test(self): return "Tested!" + @inlineCallbacks + def _wait_ready(self): + """Wait until setup_local has connected us to the virtual node.""" + while self.virtRoot is None or self.qReg is None: + yield deferLater(reactor, 0.1, lambda: None) + # This can be called by Alice or Bob to tell Charlie where to get the qubit and what to do next @inlineCallbacks @@ -97,18 +103,15 @@ def remote_receive_ghz(self, virtualNum): virtualNum number of the virtual qubit corresponding to the EPR pair received """ - logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + yield self._wait_ready() - # Wait until our virtual node connection is ready — Bob may call us - # before setup_local has finished connecting to the virtual node. - while self.virtRoot is None: - yield deferLater(reactor, 0.05, lambda: None) + logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) q = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) # Measure it outcome = yield q.callRemote("measure") - print(f"Charlie's outcome was: {outcome}") + print(f"Charlie's outcome was: {outcome}", flush=True) ##################################################################################################### diff --git a/examples/nativeMode/extendGHZ/run.sh b/examples/nativeMode/extendGHZ/run.sh index 30e03289..eea455d4 100644 --- a/examples/nativeMode/extendGHZ/run.sh +++ b/examples/nativeMode/extendGHZ/run.sh @@ -9,10 +9,10 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then fi fi +sleep 5 + python3 bobTest.py & -sleep 1 python3 charlieTest.py & -sleep 1 python3 aliceTest.py diff --git a/examples/nativeMode/graphState/bobTest.py b/examples/nativeMode/graphState/bobTest.py index f3f79047..df40e314 100644 --- a/examples/nativeMode/graphState/bobTest.py +++ b/examples/nativeMode/graphState/bobTest.py @@ -88,18 +88,21 @@ def set_virtual_reg(self, qReg): def remote_test(self): return "Tested!" + @inlineCallbacks + def _wait_ready(self): + """Wait until setup_local has connected us to the virtual node.""" + while self.virtRoot is None or self.qReg is None: + yield deferLater(reactor, 0.1, lambda: None) + # This can be called by Alice (or other clients on the classical network) to inform Bob # of an event. @inlineCallbacks def remote_receive_qubit(self, virtualNum): - logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + yield self._wait_ready() - # Wait until our virtual node connection is ready — Alice may call us - # before setup_local has finished connecting to the virtual node. - while self.virtRoot is None: - yield deferLater(reactor, 0.05, lambda: None) + logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) # Get ref of qubit qB = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) @@ -125,7 +128,7 @@ def remote_receive_qubit(self, virtualNum): # Measure qubit (Z-basis) outcome = yield qB.callRemote("measure") - print("Bob outcome was:", outcome) + print("Bob outcome was:", outcome, flush=True) ##################################################################################################### diff --git a/examples/nativeMode/graphState/charlieTest.py b/examples/nativeMode/graphState/charlieTest.py index 2ace3d13..d9c1adab 100644 --- a/examples/nativeMode/graphState/charlieTest.py +++ b/examples/nativeMode/graphState/charlieTest.py @@ -89,21 +89,24 @@ def set_virtual_reg(self, qReg): def remote_test(self): return "Tested!" + @inlineCallbacks + def _wait_ready(self): + """Wait until setup_local has connected us to the virtual node.""" + while self.virtRoot is None or self.qReg is None: + yield deferLater(reactor, 0.1, lambda: None) + # This can be called by Alice (or other clients on the classical network) to inform Bob # of an event. @inlineCallbacks def remote_receive_qubit(self, virtualNum, sender): + yield self._wait_ready() + if sender == "Bob": logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) - # Wait until our virtual node connection is ready — Bob may call us - # before setup_local has finished connecting to the virtual node. - while self.virtRoot is None: - yield deferLater(reactor, 0.05, lambda: None) - # Get ref of qubit self.qC = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) qC = self.qC @@ -135,7 +138,7 @@ def remote_receive_qubit(self, virtualNum, sender): # Measure qubit (X-basis) yield qC.callRemote("apply_H") outcome = yield qC.callRemote("measure") - print("Charlie outcome was:", outcome) + print("Charlie outcome was:", outcome, flush=True) elif sender == "David": diff --git a/examples/nativeMode/graphState/davidTest.py b/examples/nativeMode/graphState/davidTest.py index bf82bd10..ee449101 100644 --- a/examples/nativeMode/graphState/davidTest.py +++ b/examples/nativeMode/graphState/davidTest.py @@ -88,18 +88,21 @@ def set_virtual_reg(self, qReg): def remote_test(self): return "Tested!" + @inlineCallbacks + def _wait_ready(self): + """Wait until setup_local has connected us to the virtual node.""" + while self.virtRoot is None or self.qReg is None: + yield deferLater(reactor, 0.1, lambda: None) + # This can be called by Alice (or other clients on the classical network) to inform Bob # of an event. @inlineCallbacks def remote_receive_qubit(self, virtualNum): - logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + yield self._wait_ready() - # Wait until our virtual node connection is ready — Charlie may call us - # before setup_local has finished connecting to the virtual node. - while self.virtRoot is None: - yield deferLater(reactor, 0.05, lambda: None) + logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) # Get ref of qubit qD = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) @@ -131,7 +134,7 @@ def remote_receive_qubit(self, virtualNum): # np.save("data_I",tmp[1]) yield qD.callRemote("apply_H") outcome = yield qD.callRemote("measure") - print("Davids outcome was:", outcome) + print("Davids outcome was:", outcome, flush=True) ##################################################################################################### diff --git a/examples/nativeMode/graphState/run.sh b/examples/nativeMode/graphState/run.sh index 48e91500..3a9a5f5c 100644 --- a/examples/nativeMode/graphState/run.sh +++ b/examples/nativeMode/graphState/run.sh @@ -9,6 +9,8 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then fi fi +sleep 5 + python3 bobTest.py & sleep 1 python3 charlieTest.py & diff --git a/examples/nativeMode/teleport/bobTest.py b/examples/nativeMode/teleport/bobTest.py index 9036b58f..2f265e0b 100644 --- a/examples/nativeMode/teleport/bobTest.py +++ b/examples/nativeMode/teleport/bobTest.py @@ -90,6 +90,12 @@ def set_virtual_reg(self, qReg): def remote_test(self): return "Tested!" + @inlineCallbacks + def _wait_ready(self): + """Wait until setup_local has connected us to the virtual node.""" + while self.virtRoot is None or self.qReg is None: + yield deferLater(reactor, 0.1, lambda: None) + # This can be called by Alice to tell Bob where to get the qubit and what corrections to apply @inlineCallbacks @@ -102,12 +108,9 @@ def remote_recover_teleport(self, a, b, virtualNum): virtualNum number of the virtual qubit corresponding to the EPR pair received """ - logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) + yield self._wait_ready() - # Wait until our virtual node connection is ready — Alice may call us - # before setup_local has finished connecting to the virtual node. - while self.virtRoot is None: - yield deferLater(reactor, 0.05, lambda: None) + logging.debug("LOCAL %s: Getting reference to qubit number %d.", self.node.name, virtualNum) eprB = yield self.virtRoot.callRemote("get_virtual_ref", virtualNum) @@ -132,7 +135,7 @@ def remote_recover_teleport(self, a, b, virtualNum): else: ValueError(f"Unknown backend {simulaqron_settings.sim_backend}") - print(f"Qubit is: \n{state}") + print(f"Qubit is: \n{state}", flush=True) ##################################################################################################### diff --git a/examples/nativeMode/teleport/run.sh b/examples/nativeMode/teleport/run.sh index 24202403..260fba9d 100644 --- a/examples/nativeMode/teleport/run.sh +++ b/examples/nativeMode/teleport/run.sh @@ -9,8 +9,9 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then fi fi +sleep 5 + python3 bobTest.py & -sleep 1 python3 aliceTest.py diff --git a/examples/nativeMode/template/run.sh b/examples/nativeMode/template/run.sh index 8713f221..b344887a 100644 --- a/examples/nativeMode/template/run.sh +++ b/examples/nativeMode/template/run.sh @@ -11,8 +11,8 @@ if [ ! -f ~/.simulaqron_pids/simulaqron_network_default.pid ]; then fi fi +sleep 5 # Run the files for Alice, Bob or whatever nodes you construct python3 bobTest.py & -sleep 1 python3 aliceTest.py diff --git a/simulaqron/local/setup.py b/simulaqron/local/setup.py index 23a2c13d..38127eae 100644 --- a/simulaqron/local/setup.py +++ b/simulaqron/local/setup.py @@ -28,25 +28,79 @@ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import logging -import time from typing import Callable from twisted.internet import error -from twisted.internet.defer import DeferredList +from twisted.internet.defer import Deferred, DeferredList from twisted.internet.error import ReactorNotRunning from twisted.spread import pb from simulaqron.general.host_config import SocketsConfig from simulaqron.reactor import reactor +from simulaqron.settings import simulaqron_settings _logger = logging.getLogger("setup-local") +# Connection retry settings (uses simulaqron's config values) +_CONN_RETRY_TIME = None # Lazy-loaded from simulaqron_settings +_CONN_MAX_RETRIES = None + + +def _get_retry_settings(): + """Load retry settings from simulaqron config (lazy, to avoid import-time issues).""" + global _CONN_RETRY_TIME, _CONN_MAX_RETRIES + if _CONN_RETRY_TIME is None: + try: + _CONN_RETRY_TIME = simulaqron_settings.conn_retry_time + _CONN_MAX_RETRIES = simulaqron_settings.conn_max_retries + except Exception: + _CONN_RETRY_TIME = 0.5 + _CONN_MAX_RETRIES = 10 + return _CONN_RETRY_TIME, _CONN_MAX_RETRIES + + +def _connect_with_retry(hostname, port, name="unknown"): + """ + Connect to a PB server with retry logic. Returns a Deferred that fires + with the PB root object once the connection succeeds, or errbacks after + exhausting all retries. + + This mirrors the retry pattern used by the virtual nodes in virtual.py. + """ + retry_time, max_retries = _get_retry_settings() + result_deferred = Deferred() + + def attempt(retries_left): + factory = pb.PBClientFactory() + reactor.connectTCP(hostname, port, factory) + d = factory.getRootObject() + d.addCallback(on_success) + d.addErrback(on_failure, retries_left) + + def on_success(root): + if not result_deferred.called: + result_deferred.callback(root) + + def on_failure(failure, retries_left): + if retries_left > 0: + _logger.debug("Connection to %s (%s:%d) failed, retrying in %ss (%d retries left)...", + name, hostname, port, retry_time, retries_left) + reactor.callLater(retry_time, attempt, retries_left - 1) + else: + _logger.error("Connection to %s (%s:%d) failed after all retries.", name, hostname, port) + if not result_deferred.called: + result_deferred.errback(failure) + + # Schedule the first attempt for when the reactor is running + reactor.callWhenRunning(attempt, max_retries) + return result_deferred + ##################################################################################################### # # setup_local # -# Sets up the local classical application level comms server (if applicable), and connects to the local +# Sets up the local classical application level comms server (if applicable), and connects to the local # virtual node and other classical communication servers. @@ -73,9 +127,6 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon :type kwargs: Any """ - # Initialize Twisted callback framework - dList = [] - # If we are listed as a server node for the classical network, start this server if myName in classicalNet.hostDict: try: @@ -90,27 +141,19 @@ def setup_local(myName: str, virtualNet: SocketsConfig, classicalNet: SocketsCon _logger.error("SETUP_LOCAL %s: Cannot start classical communication servers: %s", myName, e) return - # Give the server some time to start up - time.sleep(3) - - # Connect to the local virtual node simulating the "local" qubits + # Connect to the local virtual node simulating the "local" qubits (with retry) node = virtualNet.hostDict[myName] _logger.debug("SETUP_LOCAL %s: Connecting to local virtual node (%s: %s, %d).", myName, node.name, node.hostname, node.port) - factory = pb.PBClientFactory() - reactor.connectTCP(node.hostname, node.port, factory) - deferVirtual = factory.getRootObject() - dList.append(deferVirtual) + dList = [_connect_with_retry(node.hostname, node.port, name=f"vnode-{node.name}")] - # Set up a connection to all the other nodes in the classical network + # Set up connections to all other nodes in the classical network (with retry) for node in classicalNet.hostDict: nb = classicalNet.hostDict[node] if nb.name != myName: _logger.debug("SETUP_LOCAL %s: Making classical connection to %s (%s: %s, %d).", myName, nb.name, nb.name, nb.hostname, nb.port) - nb.factory = pb.PBClientFactory() - reactor.connectTCP(nb.hostname, nb.port, nb.factory) - dList.append(nb.factory.getRootObject()) + dList.append(_connect_with_retry(nb.hostname, nb.port, name=nb.name)) deferList = DeferredList(dList, consumeErrors=True) deferList.addCallback(_init_register, myName, virtualNet, classicalNet, lNode, func, *args, **kwargs) @@ -143,6 +186,7 @@ def _init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig else: _logger.error("SETUP_LOCAL %s: Connection to virtual server failed!", myName) reactor.stop() + return # Retrieve connections to the classical nodes for node in classicalNet.hostDict: @@ -155,6 +199,7 @@ def _init_register(resList: DeferredList, myName: str, virtualNet: SocketsConfig else: _logger.error("SETUP_LOCAL %s: Connection to %s failed!", myName, nb.name) reactor.stop() + return # On the local virtual node, we still want to initialize a qubit register defer = virtRoot.callRemote("add_register") From feeb218dfd8ebe8b2f65ec05f625bbfce21c6b0b Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 22:12:17 +0100 Subject: [PATCH 236/277] Remove slow tests workflow from push CI Co-Authored-By: Claude Opus 4.6 --- .github/workflows/netqasm.yaml | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 .github/workflows/netqasm.yaml diff --git a/.github/workflows/netqasm.yaml b/.github/workflows/netqasm.yaml deleted file mode 100644 index b372345a..00000000 --- a/.github/workflows/netqasm.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: Slow tests - -on: push - -jobs: - slow-tests: - name: Run slow tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - uses: actions/setup-python@master - with: - python-version: "3.12" - - name: Install simulaqron - run: make install - - name: Run slow tests - run: make tests_slow From c9cf55b03297c5338363240a88bcb1087df98749 Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 22:26:49 +0100 Subject: [PATCH 237/277] Switch default backend to qutip, remove projectq dependency from CI Examples use stabilizer explicitly for speed; global default is now qutip so students can use non-Clifford gates without confusion. Also fixes numpy deprecation warning in qutip_simulator.py. Co-Authored-By: Claude Opus 4.6 --- examples/new-sdk/corrRNG/simulaqron_settings.json | 2 +- examples/new-sdk/extendGHZ/simulaqron_settings.json | 2 +- examples/new-sdk/teleport/simulaqron_settings.json | 2 +- .../new-sdk/template-client-server/simulaqron_settings.json | 2 +- .../new-sdk/template-quantum-local/simulaqron_settings.json | 2 +- simulaqron/run/run.py | 2 +- simulaqron/settings/simulaqron_config.py | 4 ++-- simulaqron/virtual_node/qutip_simulator.py | 2 +- tests/quick/general/test_classical_sockets.py | 2 +- tests/quick/netqasm/test_get_qubit_state.py | 2 +- tests/quick/settings/test_simulaqron_settings.py | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/new-sdk/corrRNG/simulaqron_settings.json b/examples/new-sdk/corrRNG/simulaqron_settings.json index 84c98ffd..255323d1 100644 --- a/examples/new-sdk/corrRNG/simulaqron_settings.json +++ b/examples/new-sdk/corrRNG/simulaqron_settings.json @@ -7,7 +7,7 @@ "recv_retry_time": 0.1, "recv_max_retries": 10, "log_level": 30, - "sim_backend": "projectq", + "sim_backend": "stabilizer", "noisy_qubits": false, "max_app_waiting_time": -1.0, "t1": 1.0 diff --git a/examples/new-sdk/extendGHZ/simulaqron_settings.json b/examples/new-sdk/extendGHZ/simulaqron_settings.json index bbfb47a2..3b9820a3 100644 --- a/examples/new-sdk/extendGHZ/simulaqron_settings.json +++ b/examples/new-sdk/extendGHZ/simulaqron_settings.json @@ -7,7 +7,7 @@ "recv_retry_time": 0.1, "recv_max_retries": 10, "log_level": 30, - "sim_backend": "projectq", + "sim_backend": "stabilizer", "noisy_qubits": false, "max_app_waiting_time": -1.0, "t1": 1.0 diff --git a/examples/new-sdk/teleport/simulaqron_settings.json b/examples/new-sdk/teleport/simulaqron_settings.json index b67c274e..882d8785 100644 --- a/examples/new-sdk/teleport/simulaqron_settings.json +++ b/examples/new-sdk/teleport/simulaqron_settings.json @@ -7,7 +7,7 @@ "recv_retry_time": 0.1, "recv_max_retries": 10, "log_level": 30, - "sim_backend": "projectq", + "sim_backend": "stabilizer", "noisy_qubits": false, "max_app_waiting_time": -1.0, "t1": 1.0 diff --git a/examples/new-sdk/template-client-server/simulaqron_settings.json b/examples/new-sdk/template-client-server/simulaqron_settings.json index bbfb47a2..3b9820a3 100644 --- a/examples/new-sdk/template-client-server/simulaqron_settings.json +++ b/examples/new-sdk/template-client-server/simulaqron_settings.json @@ -7,7 +7,7 @@ "recv_retry_time": 0.1, "recv_max_retries": 10, "log_level": 30, - "sim_backend": "projectq", + "sim_backend": "stabilizer", "noisy_qubits": false, "max_app_waiting_time": -1.0, "t1": 1.0 diff --git a/examples/new-sdk/template-quantum-local/simulaqron_settings.json b/examples/new-sdk/template-quantum-local/simulaqron_settings.json index bbfb47a2..3b9820a3 100644 --- a/examples/new-sdk/template-quantum-local/simulaqron_settings.json +++ b/examples/new-sdk/template-quantum-local/simulaqron_settings.json @@ -7,7 +7,7 @@ "recv_retry_time": 0.1, "recv_max_retries": 10, "log_level": 30, - "sim_backend": "projectq", + "sim_backend": "stabilizer", "noisy_qubits": false, "max_app_waiting_time": -1.0, "t1": 1.0 diff --git a/simulaqron/run/run.py b/simulaqron/run/run.py index a584a627..c551054d 100644 --- a/simulaqron/run/run.py +++ b/simulaqron/run/run.py @@ -117,7 +117,7 @@ def run_applications( network_cfg: Union[str, PathLike, Path] = None, # WARNING - The type of this argument *cannot* be harmonized nv_cfg: Any = None, # Unused; it's here for harmonization with squidasm "simulate_application" log_cfg: LogConfig = None, - formalism: Formalism = Formalism.KET, + formalism: Formalism = Formalism.DM, use_app_config: bool = True, post_function: Optional[Callable] = None, enable_logging: bool = True, diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 9ab113e4..76a4a4a6 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -76,7 +76,7 @@ class SimulaqronConfig(JSONSerializerMixin): recv_retry_time: float = 0.1 recv_max_retries: int = 10 log_level: int = logging.WARNING - sim_backend: SimBackend = SimBackend.STABILIZER + sim_backend: SimBackend = SimBackend.QUTIP noisy_qubits: bool = False max_app_waiting_time: float = -1.0 # In seconds, negative means unlimited waiting t1: float = 1.0 @@ -152,7 +152,7 @@ def default_settings(self): * recv_retry_time = 0.1 * recv_max_retries = 10 * log_level = logging.WARNING - * sim_backend = SimBackend.STABILIZER + * sim_backend = SimBackend.QUTIP * noisy_qubits = False * max_app_waiting_time = -1.0 # In seconds, negative means unlimited waiting * t1: float = 1.0 diff --git a/simulaqron/virtual_node/qutip_simulator.py b/simulaqron/virtual_node/qutip_simulator.py index 74491794..db005c28 100644 --- a/simulaqron/virtual_node/qutip_simulator.py +++ b/simulaqron/virtual_node/qutip_simulator.py @@ -427,7 +427,7 @@ def measure_qubit_inplace(self, qubitNum: int): p0, p1 = 0.5, 0.5 # Sample the measurement outcome from these probabilities - outcome = int(np.random.choice([0, 1], 1, p=[p0, p1])) + outcome = np.random.choice([0, 1], p=[p0, p1]).item() # Compute the post-measurement state, getting rid of the measured qubit if outcome == 0: diff --git a/tests/quick/general/test_classical_sockets.py b/tests/quick/general/test_classical_sockets.py index 1e997f3a..b2c12a6d 100644 --- a/tests/quick/general/test_classical_sockets.py +++ b/tests/quick/general/test_classical_sockets.py @@ -11,7 +11,7 @@ class TestClassicalSocket: @pytest.fixture(autouse=True) def configurations(self): simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ + simulaqron_settings.sim_backend = SimBackend.STABILIZER network_config.using_default_network() yield diff --git a/tests/quick/netqasm/test_get_qubit_state.py b/tests/quick/netqasm/test_get_qubit_state.py index b450abb1..339798cc 100644 --- a/tests/quick/netqasm/test_get_qubit_state.py +++ b/tests/quick/netqasm/test_get_qubit_state.py @@ -21,7 +21,7 @@ class TestGetQubitState: @pytest.fixture(autouse=True) def reset_configs(self): simulaqron_settings.default_settings() - simulaqron_settings.sim_backend = SimBackend.PROJECTQ + simulaqron_settings.sim_backend = SimBackend.QUTIP network_config.using_default_network() # Here we define the quantum programs used in the tests diff --git a/tests/quick/settings/test_simulaqron_settings.py b/tests/quick/settings/test_simulaqron_settings.py index 37096d22..fa6e28d3 100644 --- a/tests/quick/settings/test_simulaqron_settings.py +++ b/tests/quick/settings/test_simulaqron_settings.py @@ -60,7 +60,7 @@ def test_create_default_settings(self, clean_settings): "recv_retry_time": 0.1, "recv_max_retries": 10, "log_level": 30, - "sim_backend": "stabilizer", + "sim_backend": "qutip", "noisy_qubits": false, "t1": 1.0 } From c55f88f452f80a9071731ae048cb5eb9d3db218b Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Sun, 8 Mar 2026 22:29:49 +0100 Subject: [PATCH 238/277] Add make ci requirement to CLAUDE.md commit policy Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2c0d29cf..b8eb0c59 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,8 @@ - **Docs:** `docs/` — Sphinx documentation (currently outdated, being rewritten) ## Git / Commit Policy -- **Never commit or push without explicit user approval.** Always show a summary of staged changes and wait for a clear "go ahead" before running `git commit`. +- **Always run `make ci` before committing.** All linting, tests, and examples must pass before any commit is created. +- Never commit or push without explicit user approval. ## Virtual Environment - The project venv is at `.venv/` — activate with `source .venv/bin/activate` From efb647747697cb9a023e80648329d940141e14e0 Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Wed, 11 Mar 2026 13:16:48 +0100 Subject: [PATCH 239/277] docs: overhaul Sphinx documentation, remove all CQC references, update to NetQASM SDK - Rewrite index.rst with structured landing page (features, quick start, navigation) - Rewrite NetQASM.rst from scratch replacing obsolete CQC content with NetQASM SDK docs - Fix GettingStarted.rst: update code to conn/flush/close pattern, fix broken links and typos - Fix Overview.rst: replace CQC diagram reference, fix NetQASM capitalization - Fix ConfNodes.rst: cqc_socket -> qnodeos_socket, replace TODO warnings, add cross-refs - Update native-mode docs: add deprecation notes, fix JSON config format, fix file references Co-Authored-By: Claude Opus 4.6 --- docs/ConfNodes.rst | 13 +- docs/GettingStarted.rst | 111 +++++++++-------- docs/NetQASM.rst | 212 ++++++++++++++++++++++++++------ docs/Overview.rst | 10 +- docs/index.rst | 75 +++++++---- docs/native-mode/CorrRng.rst | 34 +++-- docs/native-mode/GraphState.rst | 58 ++++----- docs/native-mode/Teleport.rst | 34 +++-- docs/native-mode/Template.rst | 38 +++--- 9 files changed, 363 insertions(+), 222 deletions(-) diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index edf54184..dd79c775 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -1,6 +1,12 @@ Configuring the simulated network ================================= +SimulaQron uses two configuration files: + +* ``simulaqron_network.json`` — defines nodes, their socket ports, and network topology (described on this page) +* ``simulaqron_settings.json`` — configures the simulation backend, timeouts, and other settings + (see :ref:`settings` in :doc:`GettingStarted`) + ------------------------------- Starting the SimulaQron backend ------------------------------- @@ -114,7 +120,7 @@ nodes "Alice", "Bob" and "Test" respectively:: "localhost", 8031 ], - "cqc_socket": [ + "qnodeos_socket": [ "localhost", 8043 ], @@ -173,9 +179,8 @@ The options for the automatically generated topologies are currently: connected network on 10 nodes, can be specified as `random_connected_20`. Note that the number of edges for a network with :math:`n` nodes must be greater or equal to :math:`n-1` and less or equal to :math:`n(n-1)/1`. -.. warning:: Implement a command in the CLI to invoke the generation of topologies. - -.. warning:: Document that CLI command. +.. note:: Topology generation via the CLI is planned but not yet implemented. For now, specify topologies + directly in the ``simulaqron_network.json`` file (see :ref:`network-topologies`). Along with setting up the network with the specified topology a .png figure is also generated and stored as config/topology.png. This is useful if a random network is used, to easily visualize the network used. diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index 06f4e0bb..93c21b61 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -5,8 +5,8 @@ Getting started Setup ----- -SimulaQron requires `Python 3.12 `_ along with the packages *cqc*, *twisted*, *numpy*, *scipy*, -*networkx*, *flake8*, *click* and *daemons*. +SimulaQron requires `Python 3.12 `_ along with the packages *netqasm*, *twisted*, *numpy*, *scipy*, +*networkx*, *click* and *daemons*. ^^^^^^^^^^^^^^^^^^^^^^ Installation using pip @@ -41,14 +41,13 @@ interactive python console by typing `python3` and the:: Testing a simple example ------------------------ -Before delving into how to write any program yourself, let's first simply run one of the existing examples when -programming SimulaQron through the Python library (see https://softwarequtech.github.io/CQC-Python/examples.html). -Remember from the Overview that SimulaQron has two parts: the first are the virtual node servers that act simulate +Before delving into how to write any program yourself, let's first simply run one of the existing examples. +Remember from the Overview that SimulaQron has two parts: the first are the virtual node servers that simulate the hardware at each node as well as the quantum communication between them in a transparent manner. -The second are the applications themselves which can be written in two ways, the direct way is to use the native -mode using the Python Twisted framework connecting to the virtual node servers, see :doc:`Examples`. -The recommended way however is the use the NetQASM library that calls the virtual nodes by making use of the -çclassical/quantum combiner interface. We will here illustrate how to use SimulaQron with the NetQASM library. +The second are the applications themselves which can be written in two ways: the direct way is to use the native +mode using the Python Twisted framework connecting to the virtual node servers (see :doc:`Examples`), +and the recommended way is to use the NetQASM library that calls the virtual nodes via the NetQASM interface. +We will here illustrate how to use SimulaQron with the NetQASM library. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Starting the SimulaQron backend @@ -94,18 +93,16 @@ Evidently, there would be classical means to achieve this trivial task chosen fo * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. -.. warning:: Update the link references and the names of the examples (NetQASM vs pythonLib) - -The examples can be found in the repo `pythonLib `_. +The examples can be found in ``examples/new-sdk/`` (see :doc:`Examples` for the full list). Before seeing how this example works, let us simply run the code:: - cd examples/nativeMode/corrRNG + cd examples/new-sdk/corrRNG sh run.sh You should be seeing the following two lines:: - App Alice: Measurement outcome is: 0/1 - App Bob: Measurement outcome is: 0/1 + Alice: My Random Number is '0/1' + Bob: My Random Number is '0/1' Note that the order of these two lines may differ, as it does not matter who measures first. So what is actually going on here? Let us first look at how we will realize the example by making an additional step (3) explicit: @@ -131,47 +128,49 @@ The script run.sh executes the following two python scripts:: Let us now look at the programs for Alice and Bob. -We first initialize an object of the class ``NetQASMConnection`` which will do all the communication to the virtual -through the NetQASM interface. -Qubits can then be created by initializing a qubit-object, which takes a ``NetQASMConnection`` as an input. -On these qubits operations can be applied and they can also be sent to other nodes in the network by use of the -``NetQASMConnection``. The full code in aliceTest.py is:: - - # Create an EPR Socket between "Alice" and "Bob" - epr_socket = EPRSocket("Alice", "Bob") - # Initialize the connection - with NetQASMConnection("Alice", epr_sockets=[epr_socket]) as Alice: - # Create an EPR pair - q = epr_socker.create_keep("Bob", number=1)[0] - - # Measure qubit - m=q.measure() - to_print="App {}: Measurement outcome is: {}".format(Alice.name,m) - print("|"+"-"*(len(to_print)+2)+"|") - print("| "+to_print+" |") - print("|"+"-"*(len(to_print)+2)+"|") - -Similarly the code in bobTest.py read:: - - # Create an EPR Socket between "Bob" and "Alice" - epr_socket = EPRSocket("Bob", "Alice") - # Initialize the connection - with NetQASMConnection("Bob", epr_sockets=[epr_socket]) as Bob: - - # Receive qubit - q=epr_socker.receive_keep("Alice")[0] - - # Measure qubit - m=q.measure() - to_print="App {}: Measurement outcome is: {}".format(Bob.name,m) - print("|"+"-"*(len(to_print)+2)+"|") - print("| "+to_print+" |") - print("|"+"-"*(len(to_print)+2)+"|") - -.. warning:: Update the link references and the names of the examples (NetQASM vs pythonLib) - -For further examples, see the examples/ folder and for the docs of the Python library see -https://softwarequtech.github.io/CQC-Python/index.html. +We first create a ``NetQASMConnection`` which handles all communication with the local quantum backend. +An ``EPRSocket`` is used to create or receive entangled qubit pairs with a remote node. +The key pattern is: queue operations, call ``flush()`` to execute them, then read results with ``int(m)``. + +The core of aliceTest.py is:: + + epr_socket = EPRSocket("Bob") + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Bob. + sim_conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + + # Create an entangled qubit + epr = epr_socket.create_keep()[0] + + # Measure it + m1 = epr.measure() + + # flush() executes all queued quantum operations and makes measurement + # results available. Before flush(), m1 is just a future/promise. + sim_conn.flush() + + # int(m) extracts the measurement outcome — only valid after flush(). + m1_val = int(m1) + sim_conn.close() + +Similarly the core of bobTest.py is:: + + epr_socket = EPRSocket("Alice") + + # sim_conn is our connection to the quantum backend (SimulaQron), not to Alice. + sim_conn = NetQASMConnection("Bob", epr_sockets=[epr_socket]) + + # Receive an entangled qubit + epr = epr_socket.recv_keep()[0] + + # Measure it + m1 = epr.measure() + + sim_conn.flush() + m1_val = int(m1) + sim_conn.close() + +For further examples, see :doc:`Examples` and :doc:`NetQASM` for the full SDK reference. .. _settings: diff --git a/docs/NetQASM.rst b/docs/NetQASM.rst index 8f18386a..74500da0 100644 --- a/docs/NetQASM.rst +++ b/docs/NetQASM.rst @@ -1,68 +1,198 @@ The NetQASM interface ===================== -.. warning:: Update this document to explain the NetQASM instead of the CQC interface! +SimulaQron applications are written using the **NetQASM SDK**. This page describes the core concepts and +programming model. For complete working examples, see :doc:`Examples`. -WARNING - The CQC interface ahas been deprecated, and it is no longer present in this release of SimulaQron. -Please refer to the NetQASM interface documentation to create quantum applications. +------------ +Installation +------------ -SimulaQron can be access from any programming language supporting network connections. Instructions to the quantum hardware simulation can be sent via the CQC interface described `here `_. +The NetQASM library is included as a dependency of SimulaQron. Installing SimulaQron automatically installs +everything you need:: -A `C `_, `Python `_, and `Rust `_ Library for programming SimulaQron using the CQC Interface are provided. If you are new to SimulaQron, programming via the Python CQC is the easiest way to get started. + pip3 install simulaqron -^^^^^^^^^^^^ -Installation -^^^^^^^^^^^^ +-------------- +Core concepts +-------------- + +^^^^^^^^^^^^^^^^^^^^^ +NetQASMConnection +^^^^^^^^^^^^^^^^^^^^^ + +Your connection to the **local quantum backend** (SimulaQron's virtual quantum node). This is *not* a connection +to another party — it is how your node talks to its local simulated quantum hardware. All qubit operations are +queued through this connection. + +Create it once, use it throughout your program, and close it at the end:: + + from netqasm.sdk.external import NetQASMConnection + + conn = NetQASMConnection("Alice") + # ... queue quantum operations ... + conn.flush() + conn.close() + +If your program uses EPR pairs, pass the EPR sockets at creation time:: + + conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + +^^^^^^^^^^^^^^^^^^^^^ +Qubit +^^^^^^^^^^^^^^^^^^^^^ + +A qubit allocated on the local quantum backend. Pass the connection so the backend knows where to allocate it:: + + from netqasm.sdk import Qubit + + q = Qubit(conn) + q.H() # Hadamard + q.X() # Pauli X + q.cnot(other) # CNOT with another qubit + m = q.measure() + +Gates are **queued** — nothing executes until you call ``flush()``. + +^^^^^^^^^^^^^^^^^^^^^ +EPRSocket +^^^^^^^^^^^^^^^^^^^^^ + +Used to create or receive entangled qubit pairs with a remote node:: + + from netqasm.sdk import EPRSocket + + # On Alice's side: + epr_socket = EPRSocket("Bob") + epr = epr_socket.create_keep()[0] + + # On Bob's side: + epr_socket = EPRSocket("Alice") + epr = epr_socket.recv_keep()[0] + +^^^^^^^^^^^^^^^^^^^^^ +flush() +^^^^^^^^^^^^^^^^^^^^^ + +The **sync point** that executes all queued quantum operations and makes measurement results available. +Before ``flush()``, measurement results are just futures/promises. After ``flush()``, you can read them +with ``int(m)``:: + + m = q.measure() + conn.flush() # execute everything queued so far + result = int(m) # NOW this works + +You can call ``flush()`` multiple times on the same connection. This enables **mid-circuit classical logic** +— measure, read the result, and decide what to do next:: + + m1 = q.measure() + conn.flush() + if int(m1) == 1: + other_qubit.X() # conditional correction + conn.flush() + +See the mid-circuit logic example in :doc:`Examples` for a full demonstration. + +----------------------- +Minimal example +----------------------- + +A single-node program that creates a qubit, applies a Hadamard gate, and measures:: + + from netqasm.sdk.external import NetQASMConnection + from netqasm.sdk import Qubit + + conn = NetQASMConnection("Alice") + q = Qubit(conn) + q.H() + m = q.measure() + conn.flush() + print("Measurement outcome:", int(m)) + conn.close() + +----------------------- +Two-node EPR example +----------------------- + +Alice and Bob generate an EPR pair and each measure their qubit to get correlated random numbers. -If you have installed SimulaQron using pip, the cqc interface for python should already be installed. -If needed you can also only install the CQC interface in Python using pip by typing:: +**Alice** (creates the EPR pair):: - pip3 install cqc + epr_socket = EPRSocket("Bob") + conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + epr = epr_socket.create_keep()[0] + m = epr.measure() + conn.flush() + print("Alice:", int(m)) + conn.close() -^^^^^ -Usage -^^^^^ +**Bob** (receives the EPR pair):: -The python library provides a way to program a protocol on a network where the nodes listen to instructions through the classical-quantum combiner (CQC) interface. In the following examples the network is simulated by SimulaQron. But the same examples could be executed on a network with real quantum hardware which allows for instructions through the CQC interface, which is the aim for the 2020 quantum internet demonstrator. + epr_socket = EPRSocket("Alice") + conn = NetQASMConnection("Bob", epr_sockets=[epr_socket]) + epr = epr_socket.recv_keep()[0] + m = epr.measure() + conn.flush() + print("Bob:", int(m)) + conn.close() -To use the Python library you first need instantiate an object from the class :code:`cqc.pythonLib.CQCConnection`. This should be done in a `context `_, that is using a :code:`with`-statement as follows:: +Both sides will print the same random number (0 or 1), demonstrating quantum correlation. - with CQCConnection("Alice") as alice: - # your program +-------------------------- +Classical communication +-------------------------- -This is to make sure the CQCConnection is correctly closed by the end of the program and that the qubits in the backend are released, even if an error occurs in your program. +For exchanging classical messages between nodes (e.g. measurement outcomes for teleportation corrections), +SimulaQron provides ``SimulaQronClassicalClient`` and ``SimulaQronClassicalServer``. -.. note:: It is still possible to initialize a :code:`CQCConnection` in the old way, i.e. without :code:`with`, however you will receive a warning message everytime you create a qubit. +Your quantum program function receives ``(reader, writer)`` — standard asyncio streams — for sending and +receiving classical messages:: -Let's look at an extremely trivial example were we have the node `Alice` allocate a qubit, perform a Hadamard gate and measure the qubit:: + from asyncio import StreamReader, StreamWriter - with CQCConnection("Alice") as alice: - q = qubit(Alice) - q.H() - m = q.measure() - print(m) + async def run_alice(reader: StreamReader, writer: StreamWriter): + # Quantum operations + conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) + m = epr_socket.create_keep()[0].measure() + conn.flush() -.. note:: If you do not specify the argument ``socket_address`` specifying the hostname and port of the cqc server receiving incoming CQC messages, you need to have simulaqron installed. The python library then tries to use the socket address of this nodes specified in simulaqron. + # Send classical message to Bob + writer.write(str(int(m)).encode("utf-8")) + conn.close() -A object from the :class:`qubit`-class is created with the :class:`CQCConnection` as argument, such that whenever an operation is applied to the qubit a CQC message will be sent to the simulation backend to actually perform this operation on the simulated qubit. -For more examples using the Python library see :doc:`GettingStarted` and https://softwarequtech.github.io/CQC-Python/examples.html + async def run_bob(reader: StreamReader, writer: StreamWriter): + # Receive classical message from Alice + data = await reader.read(255) + correction = int(data.decode("utf-8")) -.. _remoteNetwork: + # Use correction in quantum operations + conn = NetQASMConnection("Bob", epr_sockets=[epr_socket]) + epr = epr_socket.recv_keep()[0] + if correction == 1: + epr.X() + conn.flush() + conn.close() ----------------------------------------- -Connecting to a remote simulated network ----------------------------------------- +See the :doc:`new-sdk/Template` page for how to set up the client and server, and the teleportation example +for a complete two-node program with classical messaging. -If a simulated network (consisting of virtual nodes and CQC servers) are setup on a remote computer (or on your own computer), CQC messages can be sent to the correct address and port numbers to control the nodes of the network. In this section we describe how to do this. +----------------------- +Configuration +----------------------- -Given the ip and port number of the CQC server of a node, you can send CQC messages over TCP using in any way you prefer. To know how these messages should look like to perform certain instructions, refer to https://softwarequtech.github.io/CQC-Python/interface.html +Each program needs two configuration files in its directory: -An easier way to send CQC messages to a CQC server of a node is to use the provided Python library. -Assuming that you know the hostname and port number of the CQC server, you can then easily instantiate an object of the class :class:`~cqc.pythonLib.CQCConnection` which will communicate with the CQC server for you, using the CQC interface. -You can directly specify the ip and port number as follows:: +* ``simulaqron_network.json`` — defines the nodes and their socket ports. See :doc:`ConfNodes` for details. +* ``simulaqron_settings.json`` — configures the simulation backend and other settings. See the + :ref:`settings` section in :doc:`GettingStarted`. - cqc = CQCConnection("Alice", socket_address=("1.1.1.1", 8801)) +The ``stabilizer`` backend is used by default and is recommended unless you need non-Clifford gates (use +``qutip`` in that case). -More information on how to then actually allocating qubits, manipulating these and creating simulated entanglement see https://softwarequtech.github.io/CQC-Python/useful_commands.html +----------------------- +Further reading +----------------------- -We give some more detailed information below on how the classical communication between nodes in the application layer can be realized and also provide some useful commands to program a protocol using the Python library. +* :doc:`Examples` — complete working examples from simple to complex +* :doc:`new-sdk/Overview` — detailed SDK concepts and file structure +* `NetQASM library documentation `_ diff --git a/docs/Overview.rst b/docs/Overview.rst index bfaa6f2d..0e045722 100644 --- a/docs/Overview.rst +++ b/docs/Overview.rst @@ -21,16 +21,14 @@ The second way to run applications is via a higher level interface, the NetQASM applications to later use real quantum hardware more easily instead of SimulaQron, then this is the interface to use. Examples of how to program using the NetQASM can be found in :doc:`NetQASM`. -.. warning:: Update this image (I don't have the sources though... I might need to make them from scratch) - -.. image:: figs/CQC_schematic_v3.png - :width: 400px +.. image:: figs/netqasm_architecture.png + :width: 500px :align: center :alt: SimulaQron Programming Interfaces Practically, SimulaQron's Backend is a server process running on each local classical computer (or on a single classical computer), emulating quantum hardware. The backend can be programmed directly using Twisted PB (native mode). -For clarity, not efficiency, the NetqASM Backend is a separate server process, which connects to the SimulaQron backend +For clarity, not efficiency, the NetQASM Backend is a separate server process, which connects to the SimulaQron backend using Twisted PB. If you are starting out, programming in the Python NetQASM library is by far the easiest way to get going! Further information about the Python NetQASM library can be found in https://netqasm.readthedocs.io/en/latest/. @@ -171,7 +169,7 @@ any code. The easiest way to proceed is to: Automated tests ^^^^^^^^^^^^^^^ -There are number of automated tests that test many (but not all) of the features of SimulaQron and the CQC interface. +There are number of automated tests that test many (but not all) of the features of SimulaQron and the NetQASM interface. See :doc:`GettingStarted` for how to run these. Some of the automated tests use quantum tomography and are thus inherently probabilistic. Therefore if you see that one of these fails, you can try to run the test again and see if it is consistent. diff --git a/docs/index.rst b/docs/index.rst index 6ebe54f5..8a23b966 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,50 +1,71 @@ -.. SimulaQron documentation master file, created by - sphinx-quickstart on Fri May 26 13:25:00 2017. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - SimulaQron Documentation ======================== Welcome to the Quantum Internet simulator SimulaQron! SimulaQron is a distributed simulation of the end nodes in a future quantum internet with the specific goal to explore -application development. The end nodes in a quantum internet are few qubit processors, which may exchange qubits using -a quantum internet. +application development. Each node in the simulated network provides the illusion of having a local quantum processor +to potential applications, while the nodes connect classically to allow the exchange of simulated qubits and the +creation of simulated entanglement. + +Key features +------------ + +* **Distributed quantum internet simulation** — install a local simulation program on each computer, or run all nodes + on a single machine +* **Three simulation backends** — stabilizer formalism (default, efficient), `QuTip `_ (mixed + state), and `ProjectQ `_ (pure state) +* **Two programming interfaces** — the NetQASM SDK (recommended) and a native Twisted mode for low-level access +* **Configurable network topologies** — complete, ring, path, random tree, or custom topologies +* **Classical communication** — built-in client/server framework for exchanging classical messages between nodes + +Quick start +----------- + +1. **Install**:: + + pip3 install simulaqron -Specifically, SimulaQron allows the installation of a local simulation program on each computer in the network that -provides the illusion of having a local quantum processor to potential applications. +2. **Configure your network** — create a ``simulaqron_network.json`` defining nodes and ports + (see :doc:`ConfNodes`) -The local simulation programs on each classical computer connect to each other classically, forming a simulated quantum -internet allowing the exchange of simulated qubits between the different network nodes, as well as the creation of -simulated entanglement. +3. **Write your program** using the NetQASM SDK:: -SimulaQron is written in `Python `_ and uses the `Twisted `_ -Perspective Broker. To perform the local qubit simulation, three different backends have so far been implemented: -Using `QuTip `_ and mixed state, using `Project Q `_ and pure states and -finally using stabilizer formalism. However, any other quantum simulator with a python interface can easily be used as -a local backend. The main challenge of SimulaQron is to allow the simulation of virtual qubits at different network -nodes: since these may be entangled they cannot be simulated on one network node, which is solved by a transparent -distributed simulation on top of in principle any local simulation engine. + from netqasm.sdk.external import NetQASMConnection + from netqasm.sdk import Qubit -We also have a `paper `_ that describe the design of -SimulaQron, which is also freely available on `arxiv `_. + conn = NetQASMConnection("Alice") + q = Qubit(conn) + q.H() + m = q.measure() + conn.flush() # execute queued operations + print(int(m)) # read measurement result + conn.close() -The documentation below assumes familiarity with classical network programming concepts, Python, Twisted, as well as -an elementary understanding of quantum information. More information on a competition at -`Our website `_ +4. **Run examples** — see :doc:`Examples` for complete working programs -SimulaQron can be installed from pip by the command :code:`pip3 install simulaqron` on MacOS and Linux. +Where to go next +---------------- + +* **New to SimulaQron?** Start with :doc:`GettingStarted` for installation and your first example +* **Want to write programs?** See :doc:`NetQASM` for the NetQASM SDK reference +* **Looking for examples?** See :doc:`Examples` — new SDK, event-based, and native-mode examples +* **Configuring networks and settings?** See :doc:`ConfNodes` +* **Architecture and internals?** See :doc:`Overview` +* **API reference?** See :ref:`modindex` + +We also have a `paper `_ describing the design of +SimulaQron, freely available on `arxiv `_. .. toctree:: :maxdepth: 2 :caption: Contents: - Overview GettingStarted - ConfNodes NetQASM Examples + ConfNodes + Overview simulaqron diff --git a/docs/native-mode/CorrRng.rst b/docs/native-mode/CorrRng.rst index bffa5beb..c9481903 100644 --- a/docs/native-mode/CorrRng.rst +++ b/docs/native-mode/CorrRng.rst @@ -1,6 +1,9 @@ Generate correlated randomness ============================== +.. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. + See :doc:`../new-sdk/Overview` and the SDK version of this example at :doc:`../new-sdk/CorrRNG`. + Having started the virtual quantum nodes, let us now run a simple test application, which already illustrates some of the aspects in realizing protocols. Our objective will be to realize the following protocol which will generate 1 shared random bit between Alice and Bob. Evidently, there would be classical means to achieve this trivial task chosen @@ -54,28 +57,23 @@ This file defines which nodes act as servers in the classical communication netw to execute the protocol. You want to copy this to whatever example you are running. It takes the JSON format, where in our example we have Alice and Bob:: - [ - { - "name": "default", - "nodes": [ - { - "Alice": { - "app_socket": ["localhost", 8821], - "qnodeos_socket": ["localhost", 8822], - "vnode_socket": ["localhost", 8823] - } + { + "default": { + "nodes": { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] }, - { - "Bob": { - "app_socket": ["localhost", 8831], - "qnodeos_socket": ["localhost", 8832], - "vnode_socket": ["localhost", 8833] - } + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] } - ], + }, "topology": null } - ] + } The first thing that happens if we execute the script doNew.sh is that after some setting up it will call run.sh, executing:: diff --git a/docs/native-mode/GraphState.rst b/docs/native-mode/GraphState.rst index 1a15e766..11da265a 100644 --- a/docs/native-mode/GraphState.rst +++ b/docs/native-mode/GraphState.rst @@ -1,6 +1,9 @@ Distributing a graph state ========================== +.. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. + See :doc:`../new-sdk/Overview`. + Here we consider a more complicated example, where we have four parties; Alice, Bob, Charlie and David. They will distribute a graph state and transform this with local operations and classical communication to make a GHZ-like state. @@ -94,45 +97,36 @@ Although the order described here is slightly different the end result is still Setting up ----------- -We will run everything locally (localhost) using a single ``classicalNet.json`` configuration file that defines all +We will run everything locally (localhost) using a single ``simulaqron_network.json`` configuration file that defines all nodes and their socket assignments:: - [ - { - "name": "default", - "nodes": [ - { - "Alice": { - "app_socket": ["localhost", 8821], - "qnodeos_socket": ["localhost", 8822], - "vnode_socket": ["localhost", 8823] - } + { + "default": { + "nodes": { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] }, - { - "Bob": { - "app_socket": ["localhost", 8831], - "qnodeos_socket": ["localhost", 8832], - "vnode_socket": ["localhost", 8833] - } + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] }, - { - "Charlie": { - "app_socket": ["localhost", 8841], - "qnodeos_socket": ["localhost", 8842], - "vnode_socket": ["localhost", 8843] - } + "Charlie": { + "app_socket": ["localhost", 8841], + "qnodeos_socket": ["localhost", 8842], + "vnode_socket": ["localhost", 8843] }, - { - "David": { - "app_socket": ["localhost", 8871], - "qnodeos_socket": ["localhost", 8872], - "vnode_socket": ["localhost", 8873] - } + "David": { + "app_socket": ["localhost", 8871], + "qnodeos_socket": ["localhost", 8872], + "vnode_socket": ["localhost", 8873] } - ], + }, "topology": null } - ] + } As we can see from the protocol above, Alice is the one that initializes the protocol and the others listen. We will therefore run a client at Alice and servers at Bob, Charlie and David. @@ -445,7 +439,7 @@ We first start the virtual quantum node backend, by executing:: We then start up the programs for the parties themselves. These will connect to the virtual quantum nodes, and execute the quantum commands and classical communication outlined above, in the same directory as we placed -classicalNet.json:: +simulaqron_network.json:: python3 bobTest.py & python3 charlieTest.py & diff --git a/docs/native-mode/Teleport.rst b/docs/native-mode/Teleport.rst index 0c5f0b0d..d8b49a49 100644 --- a/docs/native-mode/Teleport.rst +++ b/docs/native-mode/Teleport.rst @@ -1,6 +1,9 @@ Teleporting a Qubit =================== +.. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. + See :doc:`../new-sdk/Overview` and the SDK version of this example at :doc:`../new-sdk/Teleport`. + Let's now consider a very simple protocol, in which Alice first generates an EPR pair with Bob, and then teleports a qubit to Bob. To program it in SimulaQron's native mode, we will use the template described in :doc:`Template`. @@ -30,28 +33,23 @@ Setting up We will run everything locally (localhost) using the standard ``simulaqron_network.json`` file that defines the nodes that run the virtual quantum node in the background to simulate the quantum hardware:: - [ - { - "name": "default", - "nodes": [ - { - "Alice": { - "app_socket": ["localhost", 8821], - "qnodeos_socket": ["localhost", 8822], - "vnode_socket": ["localhost", 8823] - } + { + "default": { + "nodes": { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] }, - { - "Bob": { - "app_socket": ["localhost", 8831], - "qnodeos_socket": ["localhost", 8832], - "vnode_socket": ["localhost", 8833] - } + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] } - ], + }, "topology": null } - ] + } We use this same file to specify the communication channels (sockets) for passing classical messages between the declared nodes. The loaded network configuration can be used to construct ``SocketsConfig`` objects that contain the diff --git a/docs/native-mode/Template.rst b/docs/native-mode/Template.rst index 91350e18..f5fe8968 100644 --- a/docs/native-mode/Template.rst +++ b/docs/native-mode/Template.rst @@ -1,6 +1,9 @@ Template for programming in native mode ======================================= +.. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. + See :doc:`../new-sdk/Overview`. + In examples/nativeMode/template you will find a template that allows you to start programming relatively easily by filling in the relevant parts of the template. Let us now discuss this template in detail: @@ -15,30 +18,25 @@ filling in the relevant parts of the template. Let us now discuss this template Alice a client and Bob a server. Note that one node can obviously fulfill both roles. #. The template will look for a network configuration JSON file in the local directory to determine node addresses and socket assignments. SimulaQron will decide (lexicographically) which nodes act as servers. - An example of the network configuration file (``network_config.json``) is as follows:: - - [ - { - "name": "default", - "nodes": [ - { - "Alice": { - "app_socket": ["localhost", 8821], - "qnodeos_socket": ["localhost", 8822], - "vnode_socket": ["localhost", 8823] - } + An example of the network configuration file (``simulaqron_network.json``) is as follows:: + + { + "default": { + "nodes": { + "Alice": { + "app_socket": ["localhost", 8821], + "qnodeos_socket": ["localhost", 8822], + "vnode_socket": ["localhost", 8823] }, - { - "Bob": { - "app_socket": ["localhost", 8831], - "qnodeos_socket": ["localhost", 8832], - "vnode_socket": ["localhost", 8833] - } + "Bob": { + "app_socket": ["localhost", 8831], + "qnodeos_socket": ["localhost", 8832], + "vnode_socket": ["localhost", 8833] } - ], + }, "topology": null } - ] + } #. The next step is to check that on each network computer that you will run on, the global configuration file starting the virtual quantum nodes is set up correctly. See :doc:`../GettingStarted` on how to perform such a From e5ee468fa9e787cd9deba909e83af1d326f7a2be Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Wed, 11 Mar 2026 18:59:24 +0100 Subject: [PATCH 240/277] docs: rewrite ConfNodes for single vs multi-machine, fix Examples links, add architecture figure - ConfNodes.rst: clearly separate single-machine (CLI or run.sh) vs multi-machine (distributed) setup - Examples.rst: replace toctrees with descriptive links to each example page - Add netqasm_architecture.png (fixes broken image in Overview.rst) Co-Authored-By: Claude Opus 4.6 --- docs/ConfNodes.rst | 267 +++++++++++++++++------------ docs/Examples.rst | 43 +++-- docs/figs/netqasm_architecture.png | Bin 0 -> 99645 bytes 3 files changed, 185 insertions(+), 125 deletions(-) create mode 100644 docs/figs/netqasm_architecture.png diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index dd79c775..41d6100a 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -7,139 +7,178 @@ SimulaQron uses two configuration files: * ``simulaqron_settings.json`` — configures the simulation backend, timeouts, and other settings (see :ref:`settings` in :doc:`GettingStarted`) -------------------------------- -Starting the SimulaQron backend -------------------------------- +------------------------------------------- +Running all nodes on a single machine +------------------------------------------- -The backend of a SimulaQron network is a set of running virtual nodes and their corresponding `VirtualNode` servers. -Starting a SimulaQron network requires using SimulaQron and network configuration files. +When developing and testing, you typically run all simulated nodes on one computer. +In this case, all sockets use ``localhost`` and you just need distinct port numbers for each node. -To start the backend of a SimulaQron network run the command ``simulaqron start``. +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Using the SimulaQron CLI +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -The command can receive certain arguments to control the simulated network: +The ``simulaqron`` command manages the backend for you. To start a network with nodes Alice and Bob:: -* ``--simulaqron-config-file=PATH`` (optional): Specifies a path for the SimulaQron config file to use for the backend. - If not given, simulaqron will try to read a file named ``simulaqron_settings.json`` in the current folder. -* ``--network-config-file=PATH`` (optional): Specifies a path for the network config file to use for the backend. If - not given, simulaqron will try to read a file named ``simulaqron_network.json`` in the current folder. -* ``--name=`` (optional): Specifies the name of the network to start. This name must correspond to one - of the names specified on the given network configuration file. If this argument is not given, this value is - defaulted to ``default``. -* ``--nodes `` (`required`): Specifies which nodes to simulate. The ```` value is a comma - separated list of the nodes names to start. All the specified node names must exist within the specified network - inside the network configuration file. + simulaqron start --nodes Alice,Bob -How to adjust the nodes and the topology of the network is described below. +This reads ``simulaqron_network.json`` and ``simulaqron_settings.json`` from the current directory (or uses +defaults), starts the virtual node servers, the QNodeOS servers, and the classical communication servers for +each node listed. -.. warning:: ``simulaqron start`` can fail if any of the ports specified in the config files are already in use by a - running SimulaQron network or another program. +To stop the backend:: -If you want to start a network with, for example, the three nodes Alex, Bart, Curt from the network named ``network``, -simply type:: + simulaqron stop - simulaqron start --name network --nodes Alex,Bart,Curt +If something went wrong (e.g. the process was killed) and SimulaQron thinks the network is still running:: + + simulaqron reset + +The ``simulaqron start`` command accepts these arguments: + +* ``--nodes `` (**required**): Comma-separated list of node names to start. These must exist in + the network configuration file. +* ``--simulaqron-config-file=PATH`` (optional): Path to a SimulaQron settings file. Defaults to + ``simulaqron_settings.json`` in the current folder. +* ``--network-config-file=PATH`` (optional): Path to a network configuration file. Defaults to + ``simulaqron_network.json`` in the current folder. +* ``--name=`` (optional): Name of the network to start (must match a name in the config file). + Defaults to ``default``. + +.. warning:: ``simulaqron start`` will fail if any of the ports specified in the config files are already in + use by a running SimulaQron network or another program. + +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Using per-example run scripts +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Each example in ``examples/new-sdk/`` and ``examples/nativeMode/`` includes a ``run.sh`` script that starts +the SimulaQron backend and launches the node programs. This is the easiest way to try an example:: + + cd examples/new-sdk/corrRNG + sh run.sh + +The ``run.sh`` script reads the ``simulaqron_network.json`` and ``simulaqron_settings.json`` in the example +directory, so each example is self-contained. + +------------------------------------------- +Running nodes on separate machines +------------------------------------------- + +To simulate a real distributed quantum network, you can run each node on a different physical computer. +In this case, you need to: + +1. **Use real hostnames/IPs** instead of ``localhost`` in the ``simulaqron_network.json`` file. + Each node's sockets must be reachable from the other machines. + +2. **Copy the same** ``simulaqron_network.json`` **to every machine**. All nodes must agree on the + network configuration. + +3. **Start only the local node** on each machine. On the machine running Alice:: + + simulaqron start --nodes Alice + + On the machine running Bob:: + + simulaqron start --nodes Bob + +4. **Run your node program** on each machine after the backend is started. + +An example ``simulaqron_network.json`` for a distributed setup:: + + { + "default": { + "nodes": { + "Alice": { + "app_socket": ["192.168.1.10", 8000], + "qnodeos_socket": ["192.168.1.10", 8001], + "vnode_socket": ["192.168.1.10", 8004] + }, + "Bob": { + "app_socket": ["192.168.1.20", 8000], + "qnodeos_socket": ["192.168.1.20", 8001], + "vnode_socket": ["192.168.1.20", 8004] + } + }, + "topology": null + } + } + +.. note:: When running on separate machines, the port numbers can be the same on each machine since they + bind to different IP addresses. .. _networkConfig: ----------------------- Configuring the network ----------------------- -SimulaQron requires specifying a json-based network configuration. This configuration states the name of the node, -and IP address/port tuples to correctly connect the SimulaQron simulations and classical communication sockets. -For each configured node, you need to specify IP and address for 3 fields: +The network configuration file (``simulaqron_network.json``) defines nodes and their socket assignments. +For each node, you specify IP and port for three sockets: -* The ``app_socket`` field, which specifies the IP and port for connecting classical communication sockets. -* The ``qnodeos_socket`` field, which specifies the IP and port for connecting the QnodeOS server, used to interpret - NetQASM objects. -* The ``vnode_socket`` field, which specifies the IP and port for the SimulaQron VirtualNode object, which runs the - quantum simulation. +* ``app_socket`` — classical communication between application-level nodes +* ``qnodeos_socket`` — connection to the QNodeOS server that interprets NetQASM subroutines +* ``vnode_socket`` — connection to the SimulaQron VirtualNode that runs the quantum simulation -Using the CLI you can add nodes to a network:: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Using the CLI to add nodes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can build up a network incrementally using the CLI:: simulaqron nodes add Maria -which adds the node Maria to the default network "default". If you want add a node to another network you can do:: +This adds Maria to the default network with random ports on ``localhost``. To add to a different network:: simulaqron nodes add Maria --network-name="OtherNetwork" -which adds Maria to the network "OtherNetwork". With no extra arguments, this invocation will configure all the -sockets fields on ``localhost``, assigning a random port in the 8000-9000 range. -You can also specify hostname and port numbers to be used for this node including what it's neighbors are using the -arguments: +You can also specify explicit hostnames and ports: + +* ``--hostname`` +* ``--app-port`` +* ``--qnodeos-port`` +* ``--vnode-port`` +* ``--neighbors`` - * ``--hostname`` - * ``--app-port`` - * ``--qnodeos-port`` - * ``--vnode-port`` - * ``--neighbors`` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Writing the JSON config manually +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -If you want to build up a (or many) more complex network, it can become tedious to do this through the CLI. -You can instead write your own network config file, as a .json file. -An example of such a file can be seen below which contains two networks ("default" and "small_network") which the -nodes "Alice", "Bob" and "Test" respectively:: +For more complex setups, write the ``simulaqron_network.json`` file directly. +Here is an example with two networks ("default" and "small_network"):: { "default": { "nodes": { "Alice": { - "app_socket": [ - "localhost", - 8000 - ], - "qnodeos_socket": [ - "localhost", - 8001 - ], - "vnode_socket": [ - "localhost", - 8004 - ] + "app_socket": ["localhost", 8000], + "qnodeos_socket": ["localhost", 8001], + "vnode_socket": ["localhost", 8004] }, "Bob": { - "app_socket": [ - "localhost", - 8007 - ], - "qnodeos_socket": [ - "localhost", - 8008 - ], - "vnode_socket": [ - "localhost", - 8010 - ] + "app_socket": ["localhost", 8007], + "qnodeos_socket": ["localhost", 8008], + "vnode_socket": ["localhost", 8010] } }, "topology": null - } + }, "small_network": { "nodes": { "Test": { - "app_socket": [ - "localhost", - 8031 - ], - "qnodeos_socket": [ - "localhost", - 8043 - ], - "vnode_socket": [ - "localhost", - 8089 - ] + "app_socket": ["localhost", 8031], + "qnodeos_socket": ["localhost", 8043], + "vnode_socket": ["localhost", 8089] } }, "topology": null } } -If you want simulaqron to use your custom network.json file simply place it in the same folder where you are running -your code, and name it ``simulaqron_network.json``. You can also use name it differently, but make sure that you -manually load this file in your python code:: +Place this file in the same directory as your code and name it ``simulaqron_network.json``. +Alternatively, load a custom path in your Python code:: from simulaqron.settings import network_config - ... network_config.read_from_file("/path/to/your/simulaqron_network.json") @@ -149,35 +188,37 @@ manually load this file in your python code:: Network topologies ------------------ -Each network configuration contains an entry named ``"topology"``, which can be used to define the topology of the -network. This could for example be:: +Each network configuration contains a ``"topology"`` entry that defines which nodes can communicate +quantum information with each other. Setting it to ``null`` means fully connected (every node can reach +every other node). + +A custom topology is specified as a dictionary of adjacency lists:: { "Alice": ["Bob"], - "Bob": ["Alice", "Charlie"] + "Bob": ["Alice", "Charlie"], "Charlie": ["Bob"] } -describing a network topology where Alice is adjacent to Bob, Bob is adjacent to Alice and Charlie and Charlie is -adjacent to Bob. +This describes a network where Alice is adjacent to Bob, Bob is adjacent to Alice and Charlie, and Charlie +is adjacent to Bob. -.. note:: Undirected topologies are also supported. That is, networks where for example Alice can send a qubit to - Bob but Bob cannot send a qubit to Alice. +.. note:: Directed topologies are also supported. For example, Alice can send a qubit to Bob but Bob + cannot send a qubit to Alice. --------------------------- Generate network topologies --------------------------- -The simulaqron tool is also capable of automatically generating certain well-known network topologies. -The options for the automatically generated topologies are currently: +SimulaQron can automatically generate certain well-known network topologies: -* `complete`: A fully connected. This is also used if the argument --topology is not used. -* `ring`: A ring network, i.e. a connected topology where every node has exactly two neighbors. -* `path`: A path network, i.e. a connected topology where every node has exactly two neighbors but there are no cycles. -* `random_tree`: Generates a random tree, i.e. a topology without cycles. -* `random_connected_{int}`: Generates a random connected graph with a specified number of edges. For example a random - connected network on 10 nodes, can be specified as `random_connected_20`. Note that the number of edges for a network - with :math:`n` nodes must be greater or equal to :math:`n-1` and less or equal to :math:`n(n-1)/1`. +* ``complete``: Fully connected (default if no topology is specified) +* ``ring``: Every node has exactly two neighbors, forming a cycle +* ``path``: Every node has at most two neighbors, no cycles +* ``random_tree``: A random tree (connected, no cycles) +* ``random_connected_{int}``: A random connected graph with a specified number of edges (e.g. + ``random_connected_20`` for 20 edges). The number of edges must be between :math:`n-1` and + :math:`n(n-1)/2` for :math:`n` nodes. .. note:: Topology generation via the CLI is planned but not yet implemented. For now, specify topologies directly in the ``simulaqron_network.json`` file (see :ref:`network-topologies`). @@ -190,25 +231,25 @@ The network that is then started might look like this: .. image:: figs/topology.png :width: 400px :align: center - :alt: Programming SimulaQron Interfaces + :alt: Example network topology -To create a custom topology, see section :ref: `network-topologies`. +To create a custom topology, see section :ref:`network-topologies`. -------------------------- Starting multiple networks -------------------------- -To run multiple networks at the same time you need to give them different names in the network configuration file, -and then use the names to start them by using the --name flag:: +To run multiple networks at the same time, give them different names in the network configuration file +and use the ``--name`` flag:: - simulaqron start --name NETWORK + simulaqron start --name NETWORK --nodes Alice,Bob -To stop a network with a specific name type:: +To stop a specific network:: simulaqron stop --name NETWORK -.. note:: By default the network name is "default". To have multiple networks running at the same time the nodes - cannot use the same port numbers. +.. note:: By default the network name is "default". To have multiple networks running at the same time the + nodes cannot use the same port numbers. -The JSON configuration file can hold more than one network configuration. To check how to specify such configuration, -please check the :ref: `networkConfig`. +The JSON configuration file can hold more than one network configuration. See :ref:`networkConfig` above for +an example with multiple networks. diff --git a/docs/Examples.rst b/docs/Examples.rst index 190792bf..b502c2cd 100644 --- a/docs/Examples.rst +++ b/docs/Examples.rst @@ -20,9 +20,38 @@ SimulaQron offers three ways to write quantum network programs, from highest-lev The examples below assume that you have already made your way through :doc:`GettingStarted`: you have the virtual node servers up and running. +----------------- +New SDK examples +----------------- + +* :doc:`new-sdk/Overview` — Key concepts: ``NetQASMConnection``, ``EPRSocket``, ``flush()``, file structure +* :doc:`new-sdk/Template` — Getting started: single-node and client-server templates +* :doc:`new-sdk/CorrRNG` — EPR pairs between two nodes, correlated measurement +* :doc:`new-sdk/Teleport` — Quantum teleportation with classical correction messages +* :doc:`new-sdk/ExtendGHZ` — Three-party entanglement, multiple EPR sockets +* :doc:`new-sdk/MidCircuitLogic` — Multiple ``flush()`` calls for mid-circuit classical decisions + +--------------------- +Event-based examples +--------------------- + +* :doc:`event-based/Overview` — Event-based programming model and state machines +* :doc:`event-based/PingPong` — Classical ping-pong between two nodes +* :doc:`event-based/PolitePingPong` — State-machine message dispatch pattern +* :doc:`event-based/QuantumCorrRNG` — Quantum correlated RNG with state machine +* :doc:`event-based/QuantumCorrRNGVerified` — Correlated RNG with verification protocol + +--------------------- +Native mode examples +--------------------- + +* :doc:`native-mode/Template` — Template for programming in native (Twisted) mode +* :doc:`native-mode/CorrRng` — Correlated randomness using native mode +* :doc:`native-mode/Teleport` — Teleportation using native mode +* :doc:`native-mode/GraphState` — Distributing a graph state across four nodes + .. toctree:: - :maxdepth: 2 - :caption: New SDK examples: + :hidden: new-sdk/Overview new-sdk/Template @@ -30,21 +59,11 @@ you have the virtual node servers up and running. new-sdk/Teleport new-sdk/ExtendGHZ new-sdk/MidCircuitLogic - -.. toctree:: - :maxdepth: 2 - :caption: Event-based examples: - event-based/Overview event-based/PingPong event-based/PolitePingPong event-based/QuantumCorrRNG event-based/QuantumCorrRNGVerified - -.. toctree:: - :maxdepth: 2 - :caption: Native mode examples: - native-mode/CorrRng native-mode/Template native-mode/Teleport diff --git a/docs/figs/netqasm_architecture.png b/docs/figs/netqasm_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..a4df39356b7631f5d566a5298c7d18a3e22a27cb GIT binary patch literal 99645 zcmeFZXH?T$*Do4%D;A1dM5J3lM4EIVG(~z-s&tiJLKmc?D2VicbO=h5-a8~H3Q|Ky z2t_&p0#ZUr2)PUQ_SyTn=iWQUJzvh3_wQf?l9g3vpT9Ymp${|^sm?NUv6O(Z(-16zc9H-bG=PI*+(Qf$%n&lvP9qXQ?k&aKd#Qr+TDLQ_4+Ze zmH&DE-zYHgdfFqf*bY~2O-q~N9ln*c;M{)sX*=W7Znk<&qPeVdpm0dfVyp zo01c%*%pa#-Rle-a081+$P3DK@MmA(>p}Hd9W!RyA8L_3O^~GRn=@0v9?eX zvFM7i>=w;ZhAXJNeFkOvy_u_p3F;x;2N`GjB17~e&OEA@ z@9%~$sH%8~U-{q!lehg^J5MT}V2Up^zTgafU@iY5hoxG&+z$T4E;NfnLPqXL z3Xh&Z$BtW~l;-c$R-%Luj~YjD!f8||8JRGw{sJv>hZqcykiKK@@8-4lg{e}xdb&*n8gGy*C(9h@6qnZMS{20+`=~o8{00;H?XvJ z-t0BDT4^bewo4>bQ}Vh)&;1^n7H8If?pRV(j|p-v5%Z++&GGJ#YX8NrOjL;dk6ELR z*1t)c5oRNrWvxE2H@(V9pXLe;PjDw5@AgsES1-#VS7zBy^c??tPFZ?g>u-jpDUY^d zUw2Alet2BwU#dQtTolzfK60BR1204?Z`P$z{5?9553QAs6+((EZpfF^vV4V2l*?AY zupM9F>-6wdqD{4QBf(K_c%Xa`gX8%<;g6S7BOJ+QMzs#g5tzFl@;d2lc6^C- zalzl`%v1<*7=zyfWecQFKU$cn)8rXl^oX{>_Mc4dR@D!G^KM;UHF@cu9iG<-qV%&I zp7kX6;I8!bTAmYLm#W;nq}|G9d2`~Qb(MmF4QV!SOr)dQ+j`z-W$_n4)aQwHs-Fx2ko8=ul4uuYLr^}CwhMG3G%_Qaq4GNsmJKp zqnaT1`?=h`4yN@_UvbSw!6ooA|51w55mzVRIst3+j*ClgG;}jY51QX*UEbIG_MrYB zRtNe}%9)Kg`(Apy@V_?ROzU4ydaqkk$8zmrFy>h57~$t~168LX z)`R-@wtrlWzjaX7I_DL|+u;dS@o|64AZ&!H@XolpVq4?(dlV!}AdZ!(cZ>T{$s3nn zqmD;RSER>ePyUMlrWW03h6Q3VhHI>4OEnc{R!{I(9j^G-h;*yu@?u1V?}kK8(4A@Q z@$MWaR3aUdHS5zI%HK;lUy+d(u-c5X^wg_m`4X9%a3?IHiZ%1jiQfa58OM#C4s$VA zQ-7{fCgY+C6CIL}EO3dT9O>Ufnswu!B%ZUDMp`ss?!MeuqhuV|jl0?N`{TeJ0iMp? zimJ}x)_NB9%bJ&LepwM-tl>W=4%EQrXA#`YJfZh*r}Ok)mfm_;BlMpMdOB}!FEvbg z)uAxrV>5NYw$Kzc%J)0``};+Tsc3Q*=IYVs5tf&yT%i9s!qVA7A2FHArynp}hO>E8 zuGM@d0ufEtVEPZUxlH+e(utV1l|R788B9XH!Rrlh>1mJX zPA|Jt5$_Wz#{Y9JNIkWjVIzK;Zg#xKMI7CgF}m|JjG??s{uc}0=Q(`rOE4qwpHv&D zhG}@mAI@f)h2FabS!zUXbda?lsvA5aY%&DHXD__FfcQ99`v+cirD&RgB+}>^5Yff_ zaqlwv!0t5I#r>D)Xt0SjH|V#oja$6qf-<~ZeYndYG!bUn+=1TeQPR3AAG+BQCS0lS z@k;v`guT6zXGnOcAScfh9;a`b)qRqJLz!zM)bO?i+>u#8R~&2L*m`unYi<2fP*ziX z3pQ%X?>p`$Z=-fbjP$w(_~?8+o^z+N#Bb8{tZ-n2_PcEFA46uw8(RTs0X0^O&mBFa zCGy5{P8Erlo0PD5X^Mp>e$doxdKg-E#?` zD6s9PyF)jh&L_~ zMfpf_{55iwKU*mo5moo&F;^iF*^k5XRZctM?7AYu%Os&T5m@uL)9m*ueXW)w>U};B z!K7K~X=%R-o1o)g%00n`jGVzeG&IUnobu9`LahxjCiP9>?ttlK?{IM3JVdaY)akr9 z*Z44_tnjO&B6$D(nZz8?1>8Ao-OA?9gAD8(AuFpu^DTy}KIi4z3u(m?O!3I)X}zXY z-Rj=1B*ZpbjlZtc1^9U6k=VV8vh+1&b4op<#@S)hhfX9!d6iBA^UIo!}H&t5WKSy|uC7w#L}SB&IlwA&sp9=RTO zWFnnIENzyLe5(sIWbOrn@G;ez+B9JBUOAdLVmebeTEDI!+r^e+Mgj(wXS;LEQZ{Lp za$*Lq(a|hbey(a{sIP95*_3Y+GEP43513$2%xm!`?m6m;4UbA&&xZOL3QyYsNu22W z2o?*pPV8S~_iMp17o#&`+$%6(Az2%{6voPx?LJX9)GePx1*>g+MBKJ0rjX_dnK7f_ z+~TH{Y}j7nBOt>W#2aQM2)|YPKBnT5A^29hIh*Q3uOQ<(T&6vG$IWn4z1d6$1x(Z;`Z)S zDRuT{Y!`a)PY+H~zSdoIug}-1Qpe}-isiMh>%02C)S8JMgmUS&c<<`F8s7_v8cIqK zD}dQO@Ru)BS;^NKdf;d~^A7U6M~uPmgVcVCd&BnU}7G-a;DtqCdOLko(jFlFkU{nHgEZx!Cs z)irOLF@HM5&np*8pwdm{uQYqu6)cGnhFJb6yIFA>x6D6sH%8-*J3U?uRH8n?lGx;G)2~!URI{c6w z<=#6$3{&Mi#sTZ-{tIIx^bp7!(Sih-CUg~i(p~6v!O7%>@bEloRbTsKkc`-~qC}2i z`b_Kz1!cGul|zH$Ve-C)a~>jnH|wd|^QFzLgjJTa`fxs};wg~@A0{1^qM3bps@ zz$&AAF%rl@RAhRdR}km72gJ7!({4IynTc90j7D|SMSXH5C26&~BxKMt2xYLI{=IB;>8wF^|~85@2fuY6ZaklRAnHK`*f@kc7m9s zGhGbG-@_bKKi!pTaEs_zu`jkE#u_0DuQm}Rde#@bMv~KVqwKn4aAW$Z^xK94cchHO zqLOp$L)7&O`4>K;&14Aa{#%pwFli@3bD#Ipvd>E^=UM!l)@Pfp?;F=^(j>F!X&MX0 z%Y}-h+pI4q$4RM$-7qc^@Hkg38s+{a>7YR&0o`|0`qU8RegdtJ zJ6dgS0? z;rdhBJZvAJrH^gx=apmNwO7>>CpWdWIwJLheIP*rz$Phf6cK$12e>eeO z(}JnoPWGw)LR@hL`ltQWxREhAWz50cFurG4?&6Wn_BRFe|CmC0@0lxu5u%?&DWOn{sGh6zl*>xp&rRN+KQxr$R& zQV>1h_2f8Cvh>7VW@6jkNN(KH%XY;m5~+D=)A2 zdWlTtq6ccUc&2NTsfldXU&lIG?%&yoOA-{rmCQ4wp{no`!c>9vrU|~Y^6&AMm z!wAEwfLRWi65ITd-6=xa(Xs*1MqSiIS&j)B(C%Sug%|cLF@oXxw~6Bs?(aI=fTOy4 zn^%=dbJc!p1Gu#A%g(lUtsHw^W%!NJ`KG>!IwSpfz=5EP8O&%KY04Q=s4h$OZ;VT}E6T!d<}4be<`^?3Z)nxTwp++ce5eY|~=m9rxZKU)=` z6C_i4>~V?hPci%bdiw3nPwIk2KYN0ww45JI`D_`y8-WI}-C>C~b+c$Nm8j*Ka{y-E z+ioDNS{3(9LAh*)_~MVT=S8#*#ZJ8BLvq7P1a%s}DRez+Yw=6e@Zo#8lixwSCRI0$ z&Ug4&(d5zAcIRgA{b~Hq&(Hd?#6!;p(~%c52hoB><24#y^YZkE2ZIg#!M?O#>=mo+ zJ*Uf}(*ZDWtur@qWn2oWI?U%I+81qHq;U)q*t^Q#bFSWEWbjyK++|PfmA+b}_;On6 z@Pb?QMdtd_JhzWeeLtbbgijt2Cf3AsjI}4<&njs>R8KmrUBqdmb*lEzxJ$FeB;9E1 zp<4#=_!zZbt<%TPoX` zRRPWi65pu|M?}PyfqbT8NO~a^Ah=nX4@!ZKM~&R!dYtp6#`H^Qg+Y8ivhJZz&)lt& z6R-98(>>waU6CVWCWE%>1}0TNaMeW!LG1}n!$Wd6f>XKqZtOJ=Zn(Wppv+;AK!vVntv&$+ z%pok?NGp^qfN|1@GnV?UeCL}Gc1=E&Ywg)#Z2F|VKfviQEOS)uy3i>*jh;t%Wh__A zZ5=J`yb0^bF}`!2rR-(}J5Igu(2>f~g_4k(kEIfO6MacsQh3eSOUwEGEk_1Do+qs+ zZ?Kh?2&`?y-Gm-PM2%uiM4HaS45+XYDRyS~}V}q_1AUw|4cF9fQbLJ`x(2thYaT z0bxt6Pa$?3pm%*Ez2jN9yQi|@GczJp0V}Zbt90*9{T0UG#l%CxlBZDI2bzRm}>>!kE1VnE+|qS%Wb z2OFHJE~}`jS*ktUni#s9aM~N&eB($i`f0UGo)>OkU$}_&wlZ9;Jp{+eb ztEnz`F|RPw6`c6xX;?Mxh9%92zSTs&bkVB4>()o?5vjkI1xA}u#; zT~X6_F$|(8?Z$wG?;M>MBG#ez!#IsIUHshmBx^prS2*Hv0<|N2it-O!3HTcr#lEf= z<}a&Pc&gJSKL*;Kf(?jOCBSD4%_KTYlUN>VX%~Z#l7HX8N<}jQ7n`}Abx}lEEU&yw zgOgjuFwJ&)Xlo7aGcsxglIgXkWr+z@oicN2 z40cd5iko=nEg%aGyQJdJnYZ%!V%a%bZ!8_!ieZ=E)AFy*x^0hhk?b&2Iu9ORs6+(X zF1O>kbxZ^NJ#x{v-4|<5625;oP>IllyZf(KPT{=eFq$W2YS^(8v_!662;_&}+6r*_)Uqae*7#TM?2BTAQ~XJ|pQ3)Qn|OW6>XMuKBI=(g6rC8w zrIzZ%tH?^~OHg5(GFwfGgl{ebZVi)_O{%_Qfid3BpQP@tZ)<9*4WW9>3dxfPA4KQb zPLE9inQ!Gjd205yEmoB`j&ejTR+4IM4aS_WJ)geHEG>haU5k( zJTa}&#VnjNG}HAphw5_PN{D6|ZpDWFvITEJ!En?FNnpwUDenRmwO_TKs=yTyX=Ry4 ztbaG7@UV+ZYXPu;v_yv}kSUr0t%!s*08t|h#xXfyUaKmaM)f>TJLnpWGJcq98v`IH zP2;Lw7qby6gsZ$u&TD|%rXBu?A$j!ZBzQbWNrAk;KmJLi>&o}y?hFB*Oc`zf{r1+y zfuR3^^4*lET_vS*lX~;R=re|X88{~G;YC~@NW0F7XQmxW2^vmZ&lLgS5aHW5hPzuT z{f9G>yv)*dsta!`)%S@o%#kR~9$unM%1X<{AcN|FVc4Jl%IG^9II);BRftwcjc&M% z2{wOXy-kO&S|05Xv^TBpu`-ZSChXPLsg#Y%%|2Cj*{Jthb&w(-V(wyK9Wozd7*4S4 zVv=8dzvJgP)*X53gA)@}AK*)LmFl70EW4(bhVdHu@9eY>{fopmO?x?x8ByXUG-;VB z>oNNW+eU*jyqPBGjb*Qzl5hSpslX<>8Sm9vE$M3nt`zvM8|zB)Kr@A&8^ zy17n#c3TERwrY@xLEqjERUR2*by7nxv+%S#RYcet9Qme0sj*!r8W2czFZrKjWKhV# zy<7#7s69N7qpBb5N=@CU_7tF5P0CB%E)*T{lhHNEpS66=IN1;oQeJI&_~T5qXRWEj zLPlTp>*#}yANJp&(?iBF7VYH|dr$#B)7kRiJ8N#h1|J!VplaneVQ%AQKG|O|{9O7& zOAc;!{AcvG;XPAQj$%H=B5tm&80t8b*>mIMv9)4;XwMx>_Fd<}YBNJ8yk_U=`aiwX zf<7x$N)mR~>1NOQ<4W0w#KiIeYXIhjTPDZGmaNFuZ@KnY2s^zY@zi#DZ8TO`aG^fj zS#mS?l~%7x;&AmMj*6Qfg*?nMx!3RcNqy!p9BDr($2sQD8H)dEq=vUm?Ta&iL zJ9R$S^)izBr}i_F*r{-7MRT>UGEUsez14H;anf6XduCF@1$Ec3YPw1Juv60tmXtb$ z{H-pCBP`U$t<9Z%pZUN@?Bu~ir3=#XB9{53297s5n@VPnfBRalH88}DwgR%8W4n?6Q&JXZE;sN zqqb@t)Xq2tY4)Ma#EU%4Y;NM{$#VZ?m&VqE`eW%uCt&Idt~3$tjB_7L_JcT=UQ5B~ zZA9nS2dr}@F7AAOiM}m-N2jd@ zhS8Oj6!#B}N+7h(|5&2egnIZ)J>u*ms{&k0RbNfQTmlCDg)!9;W%1DH)Xq0bi3g7D z{q1kH0Iqj_Q$V4z;z!E-WB!{1f^@T!@#%kxWlx=+kSUt&G*B)#A(V@lO0BEdb-;*@ z>-|sq_YHXaE_AH?8BSjkE8XIJ;FKKWAR}aDQ{w+*rPJvYGai(pKI9YWY%I89q(Du6 zNpg68jf${0{%MwFDkcTQFEL3c#>QS!2i_yX+D6f9Cq)W33*@|)#*i&NQ#fndiBV<)8IzyC~8VbE46!2 z#r?S&2oJ<}JZ~W5eKMAL(f7pur88GAsx`c1J)>Ojxk**;zZX}EPr86uS`tB~>Z75YBZCjl z`2zd=Cx7oNg12#@kTEBwg8gYB`jC`<330I+333_EDsn>bZI>;K+%A%r3KFPcLCmAk zv9I9Tc`@@NWu|%r#8UZ=h4#B?>z}@~z4EZOe;M?tI?{o04Vb9mrN>b(Y$hWb`8=ci z%kXpto5N{89PQn2KTf?&OVB5>Q!KhD?qt5PZ(~)M(6w^aoVfio-;q^Bj%#C@G$B0r z6etsvlNfR*DW-2>>zZ%-+R|!ik82tw`9hwFr??zoj3|QvyEe>t#j1pGtSd1tfblegg!kM0JP{$MVov2CA?3{lxLfd;yq$-x~;+33n!7@HqBj+sxdW%U}ETm7c-Hxi<_Jirlgkx02@*yuf(Esod@p;cgkMMr`oB_T|X8&^0`B0D7rr>Jbet$u>onNIwsopDzs58dd+ zGeExwp>>K0opiQAKKLq_TaLu`u}7RB_I?k4nE@l@?DUsTl9=)OT1*GjTRk>P-qo;_ zmmR&RF+@J4e?;ikSG&jFh-jkX;@r@d;{#2i+s1t_qMP3X@A?UJ=%r%NY?M(@VNpq@ zPPy#@3l&1>%uNu3fa0+iby7+Dv|3*#47IU6D9IX(jw&M!J|S7OycK1W0a3+?ubjdH zJ~rRlYQGa-w+#ij2hZ-bb!rrRvO)yil_GjP4}t*}X1$OSIs*Qauc%V7K=nVK3aGfi z;^+V~j7Q>Y5CsCgJFMr=g{Tj6-r$X62lXIem|yOUrjIf~H84|_lcS(ERiLkx+z~X6 zHwDOB|Cowp&bEX8l%lF;LB7CVw_S@6fveToZANDN$IuoZ?^T1Q*a02A5s%{jdKKVx zRz4;U%t%oP>5PB=#Q{GBi*-uM&rMVAJo9xPfB}!WA19B+q4;f51pHhjbW&roxJZgO z#rgJ3*{6-IU~Y#mJgNfLvq-Z2y^}jacUPZ_=(7VPJ_ntV0Xwjg_32^u9JdCD1Bh7j z3XAKz$tIG~j|Oz?G1lXjOvW|y4zSnM7ZIf}`bf+0;}0k4?FfM})iB^Yojg%Yntz~L zmHLky%LIS@sgiV0IiFGVnt{)OUz?YPUi#4CjBy(-v#7!p=no{doTmVID;|TkaS2gi zZfY8Se!Ru>(#gH?@wZphY@9vx_E1OzY3?d!^qK}>u$E6)D%C3xJz*ovp)id4n%IZT zGRYL~{sl@_C(Ob-xfGV&-JXq!yKK7*!8tqRHk4{A+TW^YwEnDO+aZ2OpM|$=g!w0Q znLjaKfp~1d+3=2UIV?q=8m8w5oy_GBq4NfDc?Rq$B?83IbG4f8yudr<(3J8sBK^0e z(u%539ss)nsj8HNs+Yn&>n32MAa^YHct(a(o)yWOGd?v1<#tvpF?){I#eu@R<}I#; zUZStlfsBBx{9?Sh@*>5Xm%If-GoxwtI+(%pmFkA~%5x-YI~#-+-)4CQwQ;vtAyPqw zZaY5cr4CS}$vgGeZBmp+TL!tgaGw!A>|;)Qe)XHFJvtO-@I8>QWBo#e{9f{ovMUD% zl`MG>@4mu-G>$Qi!M9o)6vF(GcuU2g5)f(enb&e%Az!M2@^hT(U!sfz}lwD~Ir*&i*tvarrYKeP;_>XDa<~$*E?F{@ys5|LBuetO zYGW({CjA3IfA>mpQAbhtMe{xPl_i5}T>5C)jiuE@0J3{K;MnL&yD~`QBL1H^0yM(a zODCY8;Mr!5KG{PC##QQ6N05JiK8fk&j*Iee1!s_SIy0LSrfo+%cYB`A7yK{GsJKq|JFisM%Od4ODkAU0XVMy*$rx9uQ7KOnm% zYF*F7ZT2-HCH@x#wu+m=R$a%yh%PB0>H;O#CzcGwu(k^;!!L4W%CcNB{km=JUgTj7 z2&6vAQ}%kg6Nf6i{%E@ZgewN_@nDg9@^RWH42q-yPLrFbI`1@h)HzX;Si z^~MfM{&tqm5eR{5RASOt)wYFY!vaD<3TA(B5WV$WO25+Ay^9CNbtfZp@hrw+R_p{} zz8bY$WDXVKw0=p*#%qQ1D{xfNjd?Ag3C4RZM3CRA4ez{snM@C8N>wzQGD0^su5nyWTpnMG-|~m{De7+Y;)>a!WEC zG&r{RPJY;$Y69hZ*f&f8)zzLJ14&S_*U+eN`mXFIuNpo*aAIqHIH-g)=7FsS#1FdU zItB+jK&z9L3Zej3SdVMYWPdlTYd3TlaTgQb!Tiyuq+Y{5YF$g$eC1?3fV&or%!Zvj z4oo%#TvWm%M)=qE7FEZ>K~u`a>rd8YplAmm!6I+`2LxC)?9mGaI3PmMW+&S8jy^^U zR6~F)dTg(cC8sEM58_*LUu_$^O96}rv{0-RZ*1Wgn^u^sYAmYiJmWy3TxTn`0v2>( z6j^S3o(2k#r(+i?pKp~KQF!7nUF__~II^-qAjVjC-8>v*Z}9MX+~t(>!?Tgyj1CPW zUkKwrh%?{woE&=-tS9s1_P$W#uPe`%pIR9fr~ugi+=Ox8ASZ_j4d0^=Umg*=KVs}< zqtv8aaZRxF3Q*)905B(uw8l~K(dX0CeafTCdkt)A$w}{-xk-Ugzyb~H)U;&GdGo#P zA496{x~M0ALR-5tb5<$_4Eb~3`)n?Id5-Lz1m)xVAyIzltk;ZFo^Oup{Y)xw+?@Gk zfS4(4nhWh|Kk_Sn6%VN6!~b4@Q|8{e$S+!Hh?nQ#g=w?YA4wZVS-LuF8psK&+4%-}&qy`Y_XZ3#KP{UQ_ph1cu>nU#UwUsRMkS5DQv zs_XL7w{+Db^Sq7C6I(7zkset_O5>J07Y9JOWjW6Xl>^p2r+v>|9?0a()2DdcN!4vY zfPBS!Z)X=HdYH4=CbkVG#2qJJBfJ_UXPb@!R@q07#%30#Cnkav;5_ybl;&`Z0H8x9 zKy4S7^Iujc2|TWxf!=mz?fTgDVmTIGTu}vcf;V!J1J@8ce|{xA9h*Q|l4jSIK4C|_ zH{Lke%1t7xhWO;J1PW}@nbLH@;yf+v4mo|krG znC@>C+CKJHZ#*|c>?{@_N3nhfFEkVHZ31? zz+A`60_r1_O)8+o)n7c&7H6cF<%WM}wn@1)7kqMav z_Kt&};J$Ra33;H*atEO)bwO*eL@_vcyThY$T|>?-HZm2OCp2W4Z|e~K1zBk$J5!M`Ah9{^$ylz-ayQ_8F`NN0r)#?ftzIq?og>s%*ks6^|=_57i^lgn^FZjD7wV7+691uPeBm=;o zn(UE5^~)ASacv|AP(fOn1>TZz$XZr$7C6Tr*UGHA7)OL^S!Cf>{AT5D7`cMUg!k=# z_nc?&f=I8dDxf!MS@7MEF4yBE{mN1OxB`QqR*5nC6(9^iS5A*vsUbd1SfP*9^rrY7 z5~nAtC-l{dYAPvc!>EQ9oC;vt;pp2I?fs);Z}q~2;O?MWf9UF*2{QxzV<2Xer#f87 zz!kGf2xJw4!8U1@ShS;cw4_c(sOBcgRI3Ri{%e-g3(4>GN2~V-(w`j#ibJ9zm4xR& z)ck+$xQ)5p5VMgv;l17W^6F8>4f)(&?#n&VbH&RdR7g3we6{B-OjW-mu28e#uZHZv zYM#r24@;QG3SYj3XW)jJM~iDfLrY@e-Q|Q~qcY@WkeYGV6e{Hw^o+5#MZecnulv-e zWea9@cGykjT$h;hINdIHl2KVu-MkKM?We_>JbKiV2xa`!9UOOu{jQ6&_`J6|(`~`QL@?F{G?y^c2vz>nxoXi9>&YTWY5w~6VQ+9ssnYP( zDz>|vd9sd)eh*5EOTb{aOo{<3DW*T=Abi+|JI^brn-RmFG#tfVg&eqcv_6DYK)~l? zdhL7|W0Lq42R|#|qflmKF2AS!pG)y)0GBSccAV^7=k|okpF`8xSV_C=9&3Mu`&-Ud zEpm5;qnrqhtn~!e2&|A&@I4H_mCd?8p`#G9ZN^4SZrx?MFc}RNie_g(HfuXt*Cyuf zWY|7=G)%)I7dH3l_+J+?5>lj{D|2X=i;fT(2|pdLd25{qE_&AX+<+(`D~_dPPV zwVq3eA;OugdCa!sG2(m+qYSr#rw;U$-~FpSJ1v0&wrS|1u<885YC$vpVta4qe}B3% zHQv+9h`waQp2v4}7DZ)?_?rdbh_boct*P^2BXukvMAG?sd5o)joeUR7*bV-gU zwcIktmJtEJs;Q|8X#UsJXZ>hcKL=zl%{XQa_x$4#?A`vZeO%hD)*MOyI{4&c0|FvR zbG+#@81}6GXqcC8AGhFk#=n;1Top*^a;j(fAVpbo!u`umh(1ZCvw!4YEHr-lvCEo4 z$co1-Zp9;RXDLp|#&Gb4g>NU^)a@S1{6Y87=Lmxa=y8&Hi&+ zL|6DMTb$=x2f5(1*0_6%-{3wBE6{g1= zP9&T%I7WuT75;Ty<9;N^3VlTa|JtC>peKPrnAb{rXCpS=mM6}A@SnRRSDyOIaBnn= zjZ^nMG|2%?2o(rp_gm-9`QqHa{A<*ze?q|cL1CFg64oO+a#>I{)x=@tI{&Un*Fp^k zXxRSGb)eH>X5DJzWPI_1)R=l3z-CcD;@85&J1Zr@mfiopBa+538v=GmKw`u{=c)3# zc$9g`G!s1mz-%FD0AA$pTfF1NbDz5hj%KdOw%7Csor7{b9l93^ZyIELd6ZJ624 zzV8-0i|aqfut~d0s^=^7cGTzVQJSzhr5@HzQ>e7bu@}$OFY~tvJ`y(HZ(6T8RthQ* zj}AC%GVjaPaQ9YzKm6Izz|*;xoyGHy0`zv1q|TfB+pd|bYBaF08`i^)XahRdOa9>C zsmbE6`_Da_K*zQRNtO&WXjA0Uh@)a?wWCRfZ5i(UP)&z5Ni9D3pVb2$o1HFuk4-)H z4_MPy?HH$?J#|)?O@a`C;*8@MIV_-@{ZpCCaS?<`zn+XR@0QYSej!9HF%WZi{Qipt z!FB(|`9l}v!n!h{G3!pv*54x`z48vilv_SRY-Z(?xGSczFLJ2Xc}=9c;toy950x+4 z{T_EZ^vNTqj$T@!fW|#oO~vyw;079$)iB1q<9V}SIKPt0Gbkuy{`I8EHf7P3`)YQ3 z&lF2JhHP9rD@=_u-AUNa32x@3!m7p#_x*DL?Rl@GGAcqd+{?eC9Lfz&W_i)(UMU`0 zV9g}3&{Q7Yz=3!BXKFr)n+)7)1CIn2+Z(9sSaK>dWCxB7ms=ut8QXYE)uwQQ+kYz0 zPV|7R@z*2!YDj6=yHn6KOvHRxjohBrHbFhe@G#>{X7`kdz?D`fW2e3Fqd9WuK#<&N*+qK#Zv}eAMQq|)-Q_E6Vkts`iR}r?Bv9$c)=AmQ{qdvQ4}U$q z6AwU9@bBl1Ao%w0=b^$8Z1C4}rQ;^(KKc8(&v23vVuKhiPBB(J2I1XHg#56CSgNT7O13gN4R)#wR zF459%35b*yw1-Y|5?Y=mC7Nel1*45qDa{}`7 zk~C-Fw^_eakT-uwn?Y({9|wa5$Gj`)pNt7X+}(UNATe!3N^n!e7YT7br2OB%O0=w%)Z!L# zwo6U$YUeBV#V)mUbelQrmWlKtbX}<3H(`t557Z5k(f!BY_6Bz2Z)vj;I`)?g%BGz8 zTQbMg{+8EXtK)x3aiHkRvip&AmU62m@HGjwhe99eD{!tGSJ@3I;|k6OVtY1DF0mig z`yq*&DqQ+%A4V<4AkBB$=rdw}Eve>}9mkK05QUVA{Oi3cL)pmNb98S1pdvA&8`YB)_cU z>0j$#3Jwk~R_Y(`$+v1UC95>79D8i#?0ZZ?{^(TAmCl{KHIjz9L(8y)gxT9=?;w!B z{$Btsy&~hf&1sFnKbp4}_l~>24FuLW5NqKh*L~UlRG`%S-to$`U+>vw_SqPhrG3R1 zor1(JfT1t{dV4k}#nG?|S3XprFJu3R5%Qe{Y>M-*O=)znCmd^D1N#`$_+P0@`?s;L zOgfqgJQ>~f5+>mFb`!6e|H=zFlb{miOkl(S+nKXsyThq)>XJ-F_>6g{fC9C`WV&Cmei8hOdx zU=1 z1tjNNa^_mK1Z!7}1`PaCuE$y`uFv6v_I%eN)k8y!*En@u)k0LC@Pm1hy1_iI7C_r? z*gN(!o;@BInm@MJ<8qgC~bunOTcPdE;Z94qvt;FoYkl4a?w&&=*1Gc8!Ao0U5u{SE~T)yJ>3upo! z`yNiP9VkUgFVs#Q=G$=DO>)K-+wHMZ&xnCnDLfn}ls1cU7Pj|DCzsuXS^Z z{)GdU5H+r-LZgHz|DR0;H9Mas{BZ+IJER%rDm($s%03tZ>z{$yM@-PJz3wR}gT&1Y-=U-v-spOR*7egKG zJJH7;lLs6`xAX%x4kfp?b2cm$*&5gb4v;js+o_|5MS7hl>CKX5{HH=(n?Jvv?Od;0 z=`r4a7cj*sazc{c`^ZkIX`7f#c#US4bZu%LZtaz0PyudywR`>S?Pdp~h&i7rlXvXc zwY7Fd@~%v-)6S|gtf9|l4u!(0&wBD9ya`EOeX4xlWJowO=>psc^5Cm4uG5SeLB@`t zxMX&c5o2o}_C``!`;izV!pU?WQMVpqUp~Fj*6$!HU4LO(||Tv62p&k?~kgF-fE%-Vq(aU5qyK+dDD4wSM?R@kj@8 zGrGEP3mco8-1^1|>&BkXhKu}eYQnxpudS`|NG@fhg0t2AoKba~>uXzm=m){Mw$zlg zH|>r~Ny`L$I<}uCo^WevF{_RTn>UhGOJ{jYbJ6ScWI-;dgyf;3--%l@{ z^6AA3>!JYH7FsXxQ@5MmurcWN};xQroMd$q8VM=ArJ^Rx@f4VZscu_upP=G zcPEB?jMpdi3Wmzp-cxK&(=>`jNp1do^{k~uZnSYfNpicHv`^4S#jyq8EWNtXjYwG4 zF-d*1!_SL1y2vw_NhD^vE0I=HnuES+wSRlpsi&O2iQT&KG1f6e2>s4axRm=Sb`7+3 zz_1FOI6;~nI8xOvGiO;*X}kq|Zp_Kwum4y#)fCdvx4x!?-x{$sGhVZhzA-62V&a(1DUggkxgO-mLV&++%@ zBfr7Ff4i~ohHkhGghqU0PTO^+J(t~O@^h%R zri_r&-q#w9{7EO1{h?n<0wjAbf;Y_pvG?9V zQLWvZXcGh!5fD+y86-%~iXb34NMjQvC^;iYPK_WziA~NqH90g$P6{Gfau!LFC1;qo zIp6o2uj<~JntQ8e{+eBuWu3#`Z&>eo*7K~j_GUbA4`B5`CG|bMLQRt8(&KY8;LLV? zgPoLdLJcQ2ZnIr^VPj8td6Vt*4p5$_X;-#Si|Tg8OTHHxobj#B?a8q zPhAKTrz`at(sgW;X9*t@;vPsV^*Z@{a=w(N;`rV?tzYl5py5vgzVo;rUi#41mxD8s zWv;r#pF0FV6)4f^6FfJ zo9E?uoPGr^sml8cfw`V?MV0R~O)FR;Csji%o_|J%hN9w{cRP1~Xf~UAP7M6|<+>eH zY%_WM>T>hS+rjg4;M?cDv#Y(v{##}U4X!Mq^SGKNQsDhNCPyA0cM;U#BP0 zG9+YC*Wjvu=LD(IU67Z?lYu@jspzpiFHfzScrMcysZRP1#%$d#elq~0vTSA_*dmeV zyQNpA?jA1F?Ta7c^hMuXUee5-fj2e0*Vh-M+8Mk08|ELd>{Cysj_Dr;+!T9Pa&j6I z#dczIbs#h}HC5k!z*YMpdSgn+h(f5}$A3O`^C!H+Xu3D9V~smBkOA zIULUl4^bUkM^SohyrU{I?VpgvdctrWiUZItbmVg}c$1LId*|eV=~lx@zh%7>S0c-3 zY5!)8w_V=U#TIfqq~E}PXLemIim_k(qG$8jnTb^(rL)$Wp03OywMPJWt=(${K(2`6 zit^eCcB7Y<$X=1j!k){_JC9Vi<<*o1dXc3QvyF;bhHz2SHmp3MX9vTaRuj5mqRZoy zF6TF&*r;!>{YWsgbDN_`b$tqWPEDyIma7Wj97D(Z$s&&GrDIRDdQ;28ah$J}G&eys zloy;<-0fyAgR>u`ZY$fD9sVn&XlM{WsBA8q8RL9%#FQZB(RX}~nya%66kmx=(l7xO zO&(e}vWvjynpNCRUs@benKMa3BAJJu42W~T@yZ(Pxez~}KR%bwOBSS~ok##^>(q9R zHMN>jiHcI4IT#Q{>DWkyr`xR0#Hol~^vI*0T)L5XJTLb84Yv3PtF(Hxo$eT>&@Ca<$1xM#WD5N|6KzJptIte6dF; zX1(Ko5=iZ7n3plw8b~c#u-Uh^*}L4^&*a*iPy=N@TZHm>m9R&ebj$tqH&`8S2$e3=hTsl6V zrJm)mAmFRcOZ9gDU1p3+S!VyUG^NI{)&Hco{^9SU8ka+rF_%eSTq^zoXUFBfByRCh z#ao8R)%B^0xf%B_m8;*Mk&lyJD}dZ!_r}->$7N$M=VidvV;?jvqUp+4`NqRP00q$? z0T@OKW9?y*=F*3_CQr#EU5&YTU*FNcuhAkwyIcQ@%Up{-??AI#+j-IKtbDVxM*r99 zi~3`tA|$fw!6?RzSS9mFiCL1fAJ!&C0D++knD86Hr#r3)jhBPXZjPs>s+(n=vS>RI zUz0UrpQEn+Y^3u!_oBO3KhEejTDYB&EZjSi0tF>e-DKD+bXeC0k;r zZktdAw>}#yQ~_1X#*b{8)W+VaBIc7GYPhqm=hsNU4Mxk6(TMu(2C=+mLyN#dcU$yu zJ7XX#&;8;ROzgnjCeXlR?HhPts&;Yxdqr05cbftW>p-nI?kNj}^{4p^b{`(EY_dPH`WiXCUd|0-nTvDsUFBS;Rw`5$#wqc zkOb|KE7N-&D|W|+vGC>kLOaSU_@D1tVSVp9(*_PtnXR^_gZm@NC~leYg7+@J`kw$- z-ik>SH5tmyV;-@cS`qF^R34wvg^~lyr}f3 z>Se!L_~d!;sedJ<`HbSEu=%*9vhCFC8~Ti0>mTpMuTiR9zZt^F)0*d}#!P|x*GyNj z$*+qDp#hW%Y3qR0dEEJU;%ECx*p#hu)?oawl^viu3quF)k7cPUjZ4SIRK&k*o<&)@ z^ryHe#fzVR&vG8Po4$wED?)EBKlOVt1DHE=oE!zzHKkkpWQWnHzv|@q>8a`VZyLY_|;7~16#e0@5xZhH?l{^U%JBpVA?2&L@(-s{Z6#W}5X z_=c+Y?2V#G{cbZ?Z{m5`Q)v(nK;8>#P(SU}Y2)U30b0O^%=tU8zA&O$l*r$7ze6Vr zNdH8F8CUnJE`if1P%e>2&1N-HiDz4lEIvDC+tlJ6OkMpKZMw|+ZPRi8b?!IJ^gZXh zq1$T;h$4?azOQ#``$I~fT;;?mSyJd7lu6gl9?TTJJ;Bt;>W3@Fl zXLNIeot#~3G3>iSG$(sEY5p=5Kj~|B8u3{kqPgnaG4ZN2Tlq=j<)mwQW1Iy4j#m7f zdnb)qt43$i{CtL|?MdElee>n$`NGvvdC`}xwQ09A8||Y3tlBFe(g2-2U+=u)Y2I76 z>utX57FWhwGC2Odw~G%ngqKbwI63uo&NHAS2~SvVi)*A8D*58wRsl@#ZtdVSTi@r0 zMSp)?Ahq1j)0Pdr?PDLHL9u{Z<+Gb@XjqY=nOYaHTmSHxtJmwOqzh(n;ec3dJ=^Y1 zJ8!jHDQhR}&j_SSMIv{PPY>MwUec%>gRf_isQ%z;IM6(D^E#8P6nQ6Ho945}wZe7v z`$D_+cipsWdyOL?Zw|kn`%}lz2+o`RZ7n##GG_atfGL5uKVY zqjA4nHCs-9fvftE;_5uE$@*vM)u3jx;F!T)@9q5~7K<5yr_L^ar``4rZOfnR?Ae{f zPMX0?K^jj50fXf-~Y{sqGqdl8W%^V)9;Qobe!CU^PnR%-qlg2J(T1b=r0BSN)%xXcp_3 z7~E?4lHi$q{I$85Q@+A=r%g}5g#01U-F8=#x)l}+xr|*&v!{ozJy+SEye3t$TOYNN zL!6(l_oecjd4n9bkKvVa-o}*g19XxOQF1ma)X|;*7Pj{rpL5Or1vnEv*TH$i4CnN0 zZ#(Uxd0`L8d3HdA$67x@Acj}hWOh6}+LZ6=uh11cnnKF|oSKT$Y&u0zm4H_x55@nL z#XQuy`onM%=W{++yOc}O8P~XFHWv?6txFjj1TrpuU4$|%^EponH|Z5~F*P<84u5uT zufV#n-Y0U@@cI;~=~5;Ne$wY(KWUaa5gkbVP-tk)KP#JA~Hh+xE7k?p? z!iW|}%mveFjO`4)Di1KZm-xZwB|0ifvObm(3SYJ=nO-Tgpe+ZNXU(Z&mdC*j6xF-c z63xfY&npU>nH-cHUq=^bDz9>{3y>{~!^kE13b(z1QK{#U2xCJ-*Z0WNqpOID;E|b_h+G&+KwB(T9oK!p?PO zVov1;4NF%5fvTCky|E@2Mtr$K)|~>I%DGNs@N=hc1HYPJAzV27bwnmd*(u+OOhLFrdboPI@0uTv_6vD>|*< z?(D*U3S){eSWfhAtg7c$t}Jr#NYI8nU+{>~%-KNI-OMR(6sA~=6xCyH(T6GgH0S5# z=STSZ&KC#P)@ATOJ!ZJ{4?hHoYSWyhF3>2c9?mJ5ptD_uH87B8Y*>g&)4N!LF4>VY zgsKbKto7$AU!-snr0b;?A&v;e4u)lC+#0XY6(lepR;ePwvZPo}J<$x3GbF5VLlNU) zjW{*S>_G|i$`&wIU|*EE+yHOlc<#p37ncVKPa%d3op%p;qy z7#~%a3w-KFjXsX&Np6$|rW|08i^zP+@ycp8!)Y2YxZT1?%C=9QU>O>qi?79_pbC0@ z9SU7nnZXGX{2#fl63bM5n3D~DOR597wNC|I5VlxIcI?xx+pMVU6J6g>8OChsng8!4 z;H>Cfqbbh9v32p;U>!)j2&DmB<+?V=Z4(WxK6*1z`Ro-IeC9vyma!MkDWg$~OHXtR6x_buWJ%^O3}wRL#u9FV{Nq@LKa#{e4nN8-gVH+)? zCGL=TGW2Y(D|_5an{SU3K&=xNkbz-X^Pl%E;+l&OhW&eOYyWNwDAXu+pB>--nWNY( zxQ;y3#BNdg1-1o2{r|Jy;D2NiV7mXWtqcE)kFx%C5dwky(+lu_@&13}761E8@PB68 z(LBUZR12`D6nT~D);eBMw-tauDwYN?G%&Qh&%q^VVT^{B#7kFpRe>-B-Ypn_D9Nxc zDSBjGPf>zC9V0$kr`XvEk>~u=c;VNtU#Lywb*FXwEGS+_m06A;=aI;g$-z_Tk1^ZE zgV7A<(d))WB?C-yc-Pbfk!gU?byFB~F&dba9)M)K=6e?xp}h_JdhOD&=AXfdfHXb$ ze{HvcKvJK81P=3RtgfwF6%siz#mSXZq*Lp#)Si-m6JqG2rcnF#ZZGo?0IXbW41$)e ziP@Le1CDnpu+~3aNmK&A10kLbOtn5^(A;{vM9IYx6S86S|Ln(sY_Nj+ZNIGMCTZBz zTOX{A^pk^6j>8+RMv$;}7)%e9b&wNXI^(`xEVWq#I{y#-;W&mz(ej*F5X0?%uhUTH znm%$rzD+=&7nxm~0_$~eyo_-k16$y69oPPQUruE}+~faP3?!BP8q|im4g+}k8puG! zFbv%d0sqe~9EiG(W*$DomGjxe+CFr184N1zE>T2j zKqw@1hp^vcP1#O#FJb0<1OuXEB{cJX2#IdN(W-VoSTiH==$PHv37>nKr$09y#n{-` zh#!?5J)de?DVR5sDN>_fT1OFz1kZ*u79e>GCwewU-!C)euoqa$qYU3)jGwqEkcn@X zgsYhCJ1y8ar?Ye=QGk>on9NWp4XBpL~FGMN@P1?T`nr zLhVX}N7r-s$xVa{#AbBnJadtCfe65v z45bkKdyWg#uYFP-WD??3#=_4zDO1(@LFae!`Lw2C_Hs{H$8}jo+kD#x4Ad&$0C}b zmiUSmH|Sxo`^>1>?ETOiv^miqb6#bwS8|gP%D6{yZ2Z#A66>(NM~EeMj% zdl)QbV;4i>URgDzr7J=1jq87*4zFM_?S*WzvCzi~KU%>aAb(_L>+tixGGziiLyWK= z#{~nij_H<9@}vL-(=W}@D1?@dsTYnwOEn8d)<8p#Mj@b*!JazcsKs5^10XzI{|Ayj5vAXJ8-*Hk`h zP>D5%z3aLBoyo(aR|@hllYJNP!X~Y&MIB$kep-j6P{tPbga3LEiEwFcQ0 zY9ZUI?B(H?z4^k3PKEu8h05_W-k+fDL@tX*uyM(waw0iqF$npCSiW!CeK9~IB5HX3 zpeJQ24uDemv#v=-!m1`s!Waau6{>=>U}nrZtmxe!He^FJmaDm`bkLl0V{1!$+*cYkWWt|J)*NhYty?*Ci}*p<#axQ3*Z8fh>ZWJb#a0(Sk4+e1 zuUcyEKpcQllr%}ritRn%y9bds5r7E>;@<%KwKyFizL#u3O zN3(Ga=4W#ngV|!6pp#~gNmb){vg_XH{WY@Jbn^=m85@mQKq5IDuI>}fWy7f56L%Cu(qnoqd+RVritLITb2i(if?ji-WXj-W@&icH};;G6< zBKq_nA!85Pm~-#1<_5VhPFR5yl>ktu;z^wa+cA-4!9~-G}bJ z?XP%w{<1D$^l3q=mT)S|1w)H-lXOuj(>J;}pQbGCForXYoMso5BA@CjP=ipfitI!p zi%Q4pL1V1gtogo}_k>B_=ElkM9@>8qhI(`7e@Kn2Sj*)LLh^CK#&x|M=d}VM$|!yn zfG}P1BCG{rs_bx)x(OxKilsY^qf|@?6)6;ExkXv2;guTAr z<-!idR5T&_m8|QpJwtnYI~lcYcC>>Q^p4^#;&Eva6viMQMqPKlOL~+;I$I=!fl?_PDB_g>8CT;{n00({z5?Z3L>6w?P8S!1(OOw z2?kLY87Zk3^MAGEe%^Wse5mlI7_6?Y+Mi9=-FK|!mN7PakEf9jp+{Dp8Y>e6gOQY& zPzP3Wu~hP72KNInlN9%giFCQ*SPjJDg8JYeo4ROd$Y&n$|;BELKrPia!Eu z+}De2_+>`jw)*yA3bmO?TKZc$0BVtUITR(WAA!UHwxD*!Pmttn(SyqbSYYzl9_n4+kEz)2}ui$wAvk)j|iLWa^&VPonv z#`Rgz(>v%%97#z{-P+&Z55eIsPUX?vqWvH?W*r+6Vtv!PqX`FBS1Nx~q5wHPX1yGS zWD{bk&53sC4j>Q{x7Q@+9=U05IM$Im4=r_hTr%=wzSk7>1^Hv%sPQ$z@*Sc*iV|V` zbEdM>J^#Ivlg&tBj^Jf-`_|T}|11Y^;CE~I*8QW6&K)xM#&;X~AHV7U{{6cnffEj* z=rPlJ^;$}$U_o)=NTQWuK`8|__2l2vv9xCAxdy2Y4!ydnCtXJJ_czkJI(&V7{p=bK zG&0D`#zaUzS}Z%J)vv!XhE4hMV6Z-wpEWj9NMwg!ymqDg&zTv4XJRDO z=3V=m3c%KKApgYo^l5Ty>pVy-o7mgC%03Wkm)}xwu!7naB8w~q*V%!+KN8l|IKZh$ z2FjY*a$cof?z`Lpk%1Zt8;OL$o@A3&FP1TWmTkVH$nKgTR$Hu*)xJ8~5je8BHG5hc zfEr%cTWBqr# zpPbCD%R&BdQDkM8m04pqxH^6CY+yMP1A%8AU#lZ@>wpH(DI5v;rB1_sHWpKQU*l}B|ECE+l1j8Dy7Nu*+q)Tv#7Nu zBpYEg0vEA%2_-*rayb(FJ2{qmu-N3OIy6yX(me}RI0@V=X3uFb0b=E9cqVQ++qZA$Hv5>r z&#!6?3E{69JMV%z7{OxJk!*#QsAU)Zqq;q68<*aeOIeBb_xm+I9!5v!Cs=@{>3s%R zhTxA2!QuQI#UbzDiNJD3r4$D~T;G}%b)ERRZnEJP2-%n%e`(d;&R`*!ltrgvglW|}hE9TKbiq|=xUKD%v1KcGCV!Q9Wu8Ts?t`6X=^KJj9Q{%#Kce(M`QRzhI7ON+@xM> z>O!1rt7M|ciJ~x2V6kQ$QPVwuiwG7|_&K|Gsuqu~9RQC=DJdDgsB|Z^iMQjGDmJIpG(7)YIfw;*{&YfN%Ml@`)FX{J@fu5+iJwC+HrnncsZ> z6_`*X78+~QSQK6104&J0@k+U+XC+GC+)vf>?YHx$rrkDEzLhv=3TT!3Ld#l-2$`FQ z7Qzw)tTfy`JbsRiMS+DT2o%;wnY35&9L^6kxakH|(oj>AQIXq7ngim@A-`MMYs!a( z)f)-e)H_GSI$~{de*RR*boT}>gm0`<+6oz-B^ghd6tW)! zRfO4yTv>b3oRyU*XlgzNC7!HSMKRFiw7Oalj>D~&x`WC65eAF7#>9fKK)SffQK=?SN{GA{<+U-=K#?>CP$=!gXP%b9z>J$^mS;$n9AIW{s%%D$GCmx6ZGJuMRK4HL`r$xL0F@+Aex*SfG!EAO?u zzWP#zCCs5%YW@xOwDUy){QCO(xDbNp8n6+BDW4*eO%Xwvl(G*eqi`=enAaQo6Jd0K zj(vG~q(6io+(;iZ55o~=AOqL7?@*%M!S{)e1oER*O-KPFseC5A>mjr&x93_NT}HLd zz}Lqt*=zJQ6tvMY7?y&(8Gn-O+phmNp}$C&r+^cLq4>o%Mp^5|CM0tIp!Y|zNaO0d z6@m700?&XOupRgvC=(5C+(|-&+08!o9)$CrWyl{RIm0Ek{4yoywP{bbQ&om=4GK`CM&G2MQEf4XZ;2fA!_yUh7#)i>d%`-CZ#$kWUS5B z8rFEyYmPR@GS8C{T+NZ9I1QMa8*459)Kl?XeLJxyriU9E5BWTtOLxA!(RoDn@F6vU z6%Hs#NXW>f9&jW%$(KB4(q!)CQjWyGX?Tlxpp0+sxI?He)%ESxVApqqOcwbjfqE>X zHexZG(a~jpvRt1VygFhwS~OaD^8k)v$Tm@j+xPdcJvtT%3rA;UMDiz1d8zz~&tN2s zMF^mD0Gy06q6W8nr_f8d*vy}Eqa3hx=H!I+Nj?T z_B}zY{$Oc>huiV?oodV^vs5xUAD^pgUuAg*M?|55g}w+k_e!G0vt{102Vcrq$eJ@& z7PH-;(3I%tsHnBYssMWM-Fb-`>HzC?NN!#p`+<9-%>h=+?C3w$1pu~#5VP+0S>;0+ zLo$f-zgK44eeGFn{g=KzX?$m?UU4HgUfjc$*OJyK{DR_8&l#yX)e#!5uC7L*Vhqlz zN7kmAs>eT}UZ_d1e8vybiO{NMjDW++%?m$lM0hdtMQQi#oiP}HkBIP(STsKIF8SQ; z)+cym{Bu9qG40G7Qj1IE0Xeyu?n!U5KwaZ8xw91#x$;hWcSnB!^#9hl#Zbw1qwep$UQV#!pMk$oSb}cfCxTE^AzPTjvaKDb!mzP zY4vQFO@sdD9j*=BaFgyI1YKPjk#O7S=-%LmA(HA$N>JT1Jd8gFr(Xn(9~)GXk`WUV z$RLr`<_Y(MJ*a}E2UJ$7EFo!-;|H|jq>?Anpw=-m9Mr#EXC-;8wD8M~lK4RIGLlZT zbz#DEaP{jw80i?kZ>fB+qe=-WcM<^$g4a}w!}QUus}(6}<;R2Mcx~H+$?o%I zs(1|yIpefBSC4Bv0#vgkg7Y8jw0Zy9adoC3{q{sb;WeZG7cB+13?lG%NQ(vX&Ak`C zUB<#{7jNNR*@1)R{)72u>$>#xnS8W4Pa~^tW|WuLuH_bZ>41KiHpnyFGK!W?=#TJw zFU}QVR36h?gBe4cU-yLR*KE!ir2Hn=MyC1)*Oy*PG;T?+^D6F%`aLI1>cy;YWMys7 zBMZ)FK!R^m)8VCYLDKre8Cu_zAH6^Q8cKA0WanLZ z+!l-9-Q7(}_jQ$`h?qEqNnd#&{pHItud3+5kMJ)ymBm^y`1_fRo<&BNOO&oeg-H+d z(G!z;`w%@#qtUm+dL^($=|ck_%aOip`&g^%h9NQB%=3JxrePmiFl7GKO)_W@PRYg8 zf$ysb5uTj9G=9<3B^~nZ?OXbQpdfRIxozBNbkWUkY<80cUv@B$U*UNcA@c`T7q`s$ z&sEV*G8>v4gu|w9G61hK4t^yQ&e=sGDP*7^Leny^?Bw2f=Z>s;Ovs2OMky*dzeb*Q z{s?#e;B@|=qehYh(<4VZBRbQ5zAZRMX_V+dTxq}@yu^14zq{|oqA=?>Er~FUuMew) z39Y*f2ilE!+l(zQ?bMxalkfmT`t^5xN+SBu3jXA-CD{u*)ae~!>`FmsAV1EaGA@r_ z3$OC&Hxmyg{2sw2c&HdFXVMc-r4ECo5E?#%Z!A0E_}#e^NN^{R^#11rc{2JnzHDqv zU)r_xBsMFRgqmdtoL=JI#;n-8bp+GzDn7%zYICjF%Khw|oTHks3C;*;7_L2WdKk`E zMo3qlEjzX$Ke?e`(pyp<)5@FfjU`7|EWjhYU%+74e!Vckd6H5J)y<%R9(JZ@FYj$= zz&vlKx7n7?^0;hGcj(_q3OXg$=YTdJ?RGOgSbKjgN^6V3o#|HFn(jAa>o;Waal~?z zTarGr&&v9C`?`&Uo*tHDhJ7~-7vtWojAzY;t&4i(A7!OeTCkOmbSiVFC&wRBV?A2P zj*i24IQEK+K{?MB)FIyp3b7c4a3_UwlRNz~AtWXycA^P>`N*AKn?#a(JjjqcL@vA6 zGKm%*?%$#!v4J_Phh!7CuV=w^7cr+_%W}<;yg+nTib>%eFtfhy z3F~2AY5L5{`*{3q>o#yu^yEn}QTiC|%5%51`}6%Jsu8wl^p+GP{CW=z$cCO8gv1Xx z^UB~>PT+n1hFv-F9r=)+p8n@6M^tZ@Pbl8Uwus`-HkpuT#F(VWPq!eX1O&djQ?^OO z8wcsZa*hpJ+1^GcyKgIJY#XUx+THl733&*SP{XFYL-CrE!dmcihuU4@gAhY*|4AL1 zwRNu`8KacGSFGh~csrTVm-J|u>jwULlY~EIVaN5o`^8^;hRE;!xs10talhsiNAdiY z)~(3>5x=ZqBASvZKMhWsbP8!Gl0_#=po7hZoT43~(mNR>Bhf^d{u-|wi)aa1=Mp}( z8CLyHi=JGAn;6J!qGgWL~yHPgvIJqY#2#0``$@7``+8X(< zy%XJMp-ZHgA0@x9Ecd5X!P<#}am1L-CpEOR5=lboRn@*Sf;bH6z$mMk&Rz24d0c4Z zS|ST3g!M6cSGt1PUxLCeAQ5Bs_1NO>VT z)weL|4IW^A#(4d+VcJ@-ZKwO?uEqNRUdd;(qfud&049|Qxrl6PWtn_;zM)g7{Ovn z9OE1CTQ6E;&0pCrc?wEjOr_UrWj7TU=*Z08G{k`L4e*yLIYK+k`OPV4iPdOGZ+%qL z9D`5laM4FLAYE;Agr4O!s4bkY{WMX}SKd)bYR)EvZ4 z6Yyp*m(pTOGCD}%$OnIHT*^Du+zj+$U_M<*Tq=FZ(<{)HWDn!-nG zF4!V6>00mjJuiLdFD-qc`{>?ty_8ZIi~v3&n&N6X+c528`H*@L?l55zccFr} z(+Fm#_u|F3BM+u@+bUD#T7i3?7LjbZ;Efx@yDB~S<4UEPZ@V6T{&xHGH@p{boNfcB zS5t+;4!%AWe2$G7iibl%99)I0*ii_!ku+qd1sS1$%`pS<*3uUZ(!=B={KJK@9Fafb zd2;w*q;nWwQ4;F!ZWo#a1-V0~U;@y$g+#wuS&v9-XSU4GI%zWxLnwr7V>2I7Oisxt zh*+)krBHaV=aztKq+@1ldb6ZjG<#08I^=bL`eY1QD2^6~j8^e|tzzct3?|_IYYf1% z@UTDpJxiL*{3u!c$*c{UDT!wt1GVLS*pt+Bh<`KKmZBD zvQIIMIyK~uuLIvKdC3(p!b+`Lu9wHR0%~iz3L-E(CF$3axdnO&BQ^{BKL73ouna@7 zFVs??H}Q%nSep2@VB5Cc>6nZzKWPg!9oo8d_iot$qF$0e$gGpu8hDd@P3_~yr355- z6F$)qmKQC$6a+*55knYCxZHQ(C2oyGnD;*u3nnc| zck_}=#efMC6A&re3?`BYVqjy^eLoISzSqJ@_wx2HF+aMBlMaeJcT{k!`7fgn0q)Xj zgJGXx0=s3=Z3Gg85RJl{^71Oelp23E!-MimBmTNyfcXW^&5bv3)BEp3>I}HFxZl;p z4iX=*kT5HK(Nj1Xs~(kSwVSJTT-lusy9tTq;u4&)9_{)>zakf$U~Z{>pWT-jvrMa} zOeg6oVca1WxU6|Z1Ttow=HN5JPY zyI7|~W_V0Fp8HK;w1Z@~3G@dQuy68s)b{#LG@)AbcWYK2T%t-y!lXmvgtdRK>9fJ= zp4%_xO(1C7Iw`@J09o;DqCLNad}!5BXQRgY%LzY-462x4PGZ^GzIYUT%B7z$sJ-K= zkavsD2RmpT1ju_|UOO|gJP~o@AXXE95Gtdlz5oac)hiUM8Us@`+b2)Y)(`Sl{CC3h zRl&(?1Ii`qahyDB+{Se6-j&nJ8Ha(swX<-MCgv_VWK1r2Pq#$q;rlY3#~`egP4o<` z9=D+Jg*c_roc|NRQ}EdyFuk_EOxK13}!8WFN@5Lo}X-%G@DkL<&luXEW8vBKs zbJc6+oa8dcmA+4vV%44Mf|L=Cla{)uQ4b=4$kqT`h*kWGIlc;}sF4k;<=Er?sv^U%H}$FpUcj7U~gJfYI^<`a>Wx=*?#43lcVgTTycA4V5aJyROwH z?4NcWS>HW@0JH%R!N^iS{v!lWm^5hq0I0G8{lnYsD|_wh2MiByqh1$a zj^Fr>f_@UPb(z(!!DU|Ka{VZ4a~(+r^RIYlw{Q)KG3{$9<>cSB8Vem4(6^Uo}S|H^JCMQOtbEE2NT8yRZ~R??)=W{2@Cs(m z4r_ccMT=GkhxfVBT9EIhm~s~SrDN=66CC!!Md0t`v2gUU#UX{k=J#&Q@11}@NxBwk zv|;#k1~GR(j*OhXWwtU;fAb;W?e1QUaxC;Di|EU32-wb_Ls95-#GTNfpwmdefU{%^ z0mddhh*4yvwy$?otP1&@Uz41e?shz%mlk(d2qI^xKRR63gIPawt8fH6HI@W@Dlp*w zb2Oh&lK3Bg@A8a5^y8rSCO2Ue%Y7+R`Dzi;GXa_({M*`gl~@@Rv%gY7?ltNMR#iy@ zcqx_wEBEeM*<1KCXz4FW42gi;q93UME*@Cr7PQBhG+n`e(jn^rYp>D``}VxDd;n)gYc$-m)d zODKf0cO4M6WEFNG7(!(F7SMc6dKi}R^5MY&0f={aI8u)_Par#W(OP=yS!YkC($#$0 zuzP7?T3Vt~u=3k2ZjfL0;}Nm3-zjjxjSOw`cwyAV0b}cS`ujI&xAni+|MVaacrk1O zROeFW=O4`Yz8@jBZNar?@TeGGmAsS{4fKf8Vzl=@wXE!AHO=~1nka`7Y2knOgv~UE z?PWj>I>C4gJJI$2LFbq)UXXiPYT^6)#^am!uXp%)%QoTf?~!c$f&my3w|wJX!7fUs z#AWEd%IPcO{}@e3CLyS&_nWn@o!giZ6wGq+)L5Xb|jL> zi2m+^X506u)#~HZ$)umAA}IWcd@v&j@UnC3B9b>g0KlQ8&zv2aKHNCo5#?7__QQgI zF`I9jMv(`~3^Rx4c?J>9>wYAv0TCf7j^ehvaEc%}3GqrwKZww0$)dQN#~(P;@R$&P zf8eAwA+3Ih^w-bH4`NR5&L+@cYm`hUoAZ)UN8NS7gnxT0X=-}Qj**BsGFLK#`wmUX zhdCN{+5R}*s`1TK=hxj|f*^+fDg6W>?;+_!tmO)1g(F4pqS0%VelOZ;hzD2!SZ9#* zTeS33!!&%sZLaOMUK}TA{*a!Sgda-}C^SikDi$vc&v$qj8^p!d(p2CypAyNr!l=+4 zK#-RVOx_#LD`JICulbMVSYLoU?!AbdLqIvOe_=!DfZk+(%{diZUG_8_jT`i9;*209 zudR84GZX<8EX~J~ZpzH>?@x-WQO2X2dU8um_1DM}1orAxEM+Lt2jlA)q{Xz7mVqIG zWaKWfS{jH__Q&?5AUg+vu&UwZyExdAuMwd6PO!I>p10Cj|KveNJrk4G;Bs9Yrf1xU1fj;|3!lvkCB2I68VLsMHAy8JlD+x zp!QvoP40w{NgVm>trlYjAlEb-SSM=GePWpj&D6r%DT+S(G;Qlr)o4VN@|f8iC*t?- zw_YYWC&Us@F(u5(<0E2XFtZ{Zdx-#xCcY#4?#}=Zh=&kbjB_=GPAS`?KM@@ngOFc4 z*8N11jRY+<7`JgDbddU)fUnX|K+JIcq=X)tKb{{YQCNJ?x?&ttj*)WrP^ek;LQ6?B zEVd2=}}%@u4+cN<)LKNTc$4ms3zcqvH}%%L-HaM*qYO z+OD5*>$jwOa6q^|TFfWF&*(!HAr9frwJ3o1n#xFiz*5P(@sx`B+PLcSH;*qchDY5& z-pmLlAo)uj_ATwh^KQqQS$7M+YPFvdE$lzNH@+$d=D-`UAOO~Qd7a-@_(?Vp}*~D&$O6LMA4ZXB02&0e)YzWBG1qa^&5=!ea$V7rG<^CPn zmzpb5%Atgjaz)5o@`!$U3)jib)W12gJ8D-Q*vjH4_k&oDMtuW_A%X~#jI2veuFQ(+ zJ`hIX1;q#R0Gi}j>6of9e_+;l{3b45$ZTp~V5KU3bQ~7_g$zy&=`m$MAd>AxZLBX8 zu7@;x-nN3DUN`wUAV6YIk~Cud-VN2>G;s^R*g>_cuZ6?j-@*M{{v4QJi9oP%?2``! zDJM}5aQLYo+zzn+)xzW7P7D#weiSe6vX`YzHLtz7>MPOV_=211x2a3Xm78(D{n zsFiCVlpfv$xkJD&)eN>jqr|aZVgz;OOlvMW*JqIj$X?))@LwoBf4(Y|^R*Jf0W{IX zgoX9cbC7maV9fkIkYNP!=a@vH(6{ANDQB8seX}S}xAF7YtkQcF{}p+?Ax|3PpmX_5jcpmE7X4zuF zr|#mQC6(xQM=H(!Enl;HOs9(tZt7tcWK;)Y4T#MJP&FZw{O$y&%o}2T$jj*0I1Ic< z2M?S49k7@*y&5DRFYp~;nr^bFu+Md!NZ1l9j?_LbynzNz3gNiV2Zbh=O%;>NUWaJu zVZmncAkcVn?CCpB59w$9bXk6~$3i*E&A%oUpT63D3*RN=<;b5YNFU$BeJ~uAOxE-3 z1)e6`3xBvc3(qX)xeYPE^DX7jia4?ZW-DQ5n@qePx<-v@PY9ua0NIs_EEGNoe6q)dyX(D5aY{rjCuSxl9cb-X#9r!f`YK&;jx;c zd}77Wj=!bCd27GWV??DH@DdVnVR3`#ZEVUch3Bmd?nq(E`I;dG%P&2`kdVXMIk)^z$9Or{sFViwiBC>) zG%;MM;36Uu9|1EhmO^jVQ5a3>a}%a0ey`r(+Cy(bLpBH?>)E-?ZL3w5l%VDYu1LYP zbqf1)*T<3>e}kER(@07;gAXb!>LudHh<67=Pzz>fqiZFfdSoFeW*Ub6b1 zcgfN$T{v~!&9J1Tc4A;Q=YOT8#rp#G?FqyhV2ZIZw}_NqckJV&oppmNUP1l3{r-nM zZ?gl4BQ)h2xD#%pkzit?U|`*e2s7>`UCrW39OnT@AnJ6=nyHpLLHr^+D<8|_ znLLIY*Ux+?;$SwzJD%wG{_yG3?zpgk0BQfzJxY%I-DEWDEaE4`vp&rhmt_-`E5BNC zY1XQR3p|ofy|7yR2%q_b|M>nSe$X6y?tHm7zp(JLuD;f>dz^Q85DqyVJS60vbQW8K zzR#!3KePHM<9I0$`bULGWf&7uamy@O>2#36M(N6G(Zv`oMthJbb{=A!zj27?o^rHBHRVF3| z&9_&+DUXL3K7zaK?kS4Bcwtfm`I85r`2|!VGWwp|ofah!y_1vqAKhxk6m1;S_aII4 zxl9;+lU)Atk-tfIYU_^}u4?363sycp{4oO`qm8ugrHyZo&Qimlof650QEu&CQV14p zcbEs^aK2EBh>F7TdyWgy&}5OGhF{Gm;}!pPgV2noF;9pAr;~+=4_v-(H-Sz;fIh^{wahZ$Zkmie~Op3`&@A z|IvN=i0-m`f%T+dl;=Q`x?fPy+j~_VnhBAp8?o#NBA$>_arT9^{WC4G=d>ad!I2D= zZz>ZoXh~YEZrsRW7Zy^!YQt9MVtw?eRv(W8171}0>Wv_Uz<;CZDxj*|p6^Av5u{5% zKtU0tTfsm?QW~Trq`ONbq(wnMN~NSrx?3a_>E_bi9sjw{|8FhVTkrAk!gs!NX7=p8 zXHLYslVXQn@w*4^y~*OPt9gR_VGCX7t9pzU^iuWrOuAT$hBoV}8I*tVO0-E{D1oMT z>z4EHN0d(5mJiY6M=Yh()bJ4ZakigDX0B<4ORu=#;r(K}8L}hci|tSH^QTNx-&bmG z8sToHe~C%qUnWIgUhW`~x-UD<>~H+!L1USnR{P^5NE&yyfERjp>jLXOtZ9?=;;V_j zb+=UV8(Jy#=xFciT?qai95kc)9e;d;jQm0LRge1KtD7G_CTImPh&mHoo-Ll5dWI3w z%BxN4xGxV|hlZPdaS+OtdG-udUVSQfSdzqCm=hhSssUBAkKDTpD8LA6GFYZTxcGHO%#->lG3_)h+IgZWTqoRN3ba*hdg08 zc1sw8U0riH`FL%(2;Jk*`%P69L|lUk9j>%YQ~mLZPHwAF$GN#V$CcV@W}0xrKWQBr z)y{+n{qvK0qqX54$tA$U-kvARnFxryf%sfhPHjAY2L=q0Z7$mvr`22Yv8Rf9n~!dg z2KFyMR93#%7DoNHrskGiaC5}bxp&R^fk}@b-8G^la5ym;2eI*ddnD+I6?$4g|IaQ z4kiApW|)}?Aqx=De-1{r=P2o}^CM<|10UDP$-!~jR^wKw3NhKr$S)%(oT2vI6k)Qm zFY?MH!;GG2pX{q5j&u-F>)G+{%y!KcgzILlVOP8Zd0RN`RNf}I6@%H6T?rVyj~*nf zuCACXsjG+joLdNngy7kB^AFADS8u%eJzN6!xhkYkKkIv#suca{lYUQPj!3D^>*X>H zdn|-WPvXdeGbmUM4!!5;-79Ioe#yR2Q!_l=h%9v9m(_9T))}{J*A?HXWQoGbkgH_= z^kt;ViI?JTouq)^^Cwzry>kl-Of>6fUdFdrU%DBSizTa*5MXKN6=Y{isj8A+oNp>9 zy?F8N8CT`CsnbbfD8nZ=>|8c4a3UoyEZ`?4+t&blrE1ul99aZE(DSf*s-0Y2PBfLa z+G`#boE2hoijOxAFt)NxJ#JJDmuB=TdUGHja{6mw=vT;RLd}WC`S!m;_1y{1qpLCs z3V{>Wt&MNnfg#XSQmxD>v^|9tWZ&o$kuVQul^4G$S-*yN|AGKfT&(KRV=Snu(BeoJFLZ?{lFXYP5hA&jlA>%^a5wPL>H$nG-~L_|-c?LDBXZq)MbYEsA@K|x}Q&W;3cIAR-n zi=#i58H~i7Cv%2;?G;L0J{tG)4xI~Z#xt_1%L|u6pUc=x@r}t^z3Jai7=xen#C4m5 z(TftP>+lewe#plTklB6P887*r*GF-a@*R5d_tH1Ru_%({gVxsmlCf-?85ne2dyc;H z7NV&&h13L|enl%xBcF-?54u2@+J7LBfY}^m+Gxo0KsGTodj*KIIc=@>M|nlC+g-)R zJ|HUbe7RUz(F77SSKohUd|lUQ%|~OmS$l|t-w6#tC*nv_P*Y4(KE9(ju_FPPg#CqT zQcbXg!-A#f>6lA9DQ36Cc5jM`rYR&P`-h8gkFg#YYJ2%DYFm}}106}^gcWKi@&Pj&WCP#=_);iC}Fj7EL# z7ON7gF^ZbKKOw_HqN3PS$31WJMqO0Z)sdBp$*mrnRv3NJiLMvN^|Kq*lquVbuNC-c z0>0g>mG@#a5$7_F_=??)7`y?xNta$V7EYPM;7Gg3Oq6vP7Ib0Y0JDolLNkc`uZr$$Ix>x!%8 z6VBLjY$}$TH?K)4DWSb|e~o#u6?BPyU7LRKh1WWqopy767S`5)U%y0ZvkeDw3O1_t zEc9sAf!fpBbrlm~3cQe&{a1b`#`!9*{(q6NhfmwYCdj~M^&sl9jBT5JCdq)v^lWYrDfPpQj753$x%UVHVEi6w2O=Ne(2Z-Df=F% z5Zw9cqv|b69a}U8&n;}gTnKK%7MZhI%1g2#HuH(9Qd4^AClI2pde9REDX9(G&3;XL zMavu|V<~3TmRqq8K@c8XrO3|R9qZx{dP2G@RX=?Au+_|XK~-Yf8+=C_&d9;Wga;dA zs&x01ODBc3$AM?qTWn&-ip@YmTEhawI9Vn}IUT>+tC>0e*7Fd@Z_d1-dfbWg=tzOP}RI%Gr zZMU~{73@>a$o0@ET0*(3EZjm3IYi5QnY3-5Jml}u)dCwk9Gd%ElP-A9Y(ox!~xFTpnPABkM_!#6@6NlKt%L#+`%Sy z0!WqMYNZL)fq{Xa-QCUKl_;56Sjb9Dbjd9U2R_Zsy@Ej8+Q#TpLz|tQ>_oR-3H!`e zM1d`=%zEO_jKrO>g!GJz(yBL?lOD8?C@5*~zV!4N_xLg2Bf1%Uex?d#&}Pj*)=}?Y zx5cDLgXX~2w$-8f{*Bo}|C|5w0vx>YWgII@I*ad0o~RUdN9Z>P?%S>JSXZyt8RkRJCHDR8hqW#-zH(_1fL)T@3P-rociN0+twqB6NtR+|&iGlDOL*7bA zv%CLUvoxDe{MsK%^te=@OayMDB^qe!2kw8hgg{dv)}M@JEld`+->i zfbHDc+Ul-K-if$*>lQBX{q!5sf1~E+qASWwASa?tdbyly(mNb&U4_c#)3i&y2cFOA z7=Fb@)vD;Y>Cg9Hi-urpNu+G%8ZNz)&kVIlReM=SA08f_IZ^kuF-W)Cxvh=Yq|1!| z*wb#iZFn^-OinJ&)-A|xc->jbrX#}M*UmeLc|!fN1`A};9;sc=j={xGLntF_Xu-Q8AvnIJ8b;@4ZFvSIQPb-F zfjNO1TyuQ4mS_7UMm@R4ZEAS8_ovh{axevQ+y~qCg2Ca1vYJM5D*?@M!0Ra8jb2sG1dvvxGJlV>2oik>BFV5^AdT;<>#1~ z#$^8Q(-A!-8gc-3KN2VYr>c+Q}(>CS)wX|uMCDwJ1EOfJT-gq2F75r5?s4dUt^D1D`{rGt5qzL3+ z+4qop&iJ&G>0>$3V)eX>W#=8OdLkIaLr@hQ3i={RHR**Nm-KZ!O4im6a&)xd8lMna zpX2%1qM;yS#*ot_8HLU9-tyGLO=1WpVp2FT5SyRaXJ*^7bMRjBq_T#FeSEaNxG!Hq zw;z-AAET#6|J9((E0>NU^boB5C#=zY9hnM$BS!>^QqRAe5>$Pn7k`?m;zJC#ZVZ3+ zX$-C!z@*giwaBleu&JxWy5HY@D(sMNt~y}LhWbs_UuuTK?u9vd78Czu$Iw z;mG$y&a`~>F6!x_{s)`tOPjaSO3{~?wjZIKOlxZT(D_2O2LF2F@((6X&UDoTng6Z8 zwMW<4*$IP)RDlX^yyd^+47Hjj%IYw%J^43D<3ay(JiM}SvMamrjpE`;tF~r{>h-d* zsJLNAX#R329wvm8INUfa*rbm&<+%51Y1ND!?p=(tvI z)oFzJ_Kix}`nV^#du`nW``&5i;bU|acXv{MlJ}(e;>lL)hsf1-JUrREN~+r~2wZsl zk&*wwD8g5;9ZWNUghspnE&5j*YAJq1SQHvyJ{OEqcDv>)BV#LYn9II$RV{~b>f%86 zXx+vK9Wh*P!-$8EKjZ9k9u~3NN|^#9*?q!oh3K^5dI95#h0hi|z>M5rWo?EDjuT%z z)=QbiPSEU#IMSQhHDeWe9#*?wh(5Oi{Om;7|%-y?An zURZ+#m!i(==AZ9-@2|w#&d2meB_|UhlH6C^j(6%?Sxv3$&xvkpOmD>dRyW3|_cQ=4 za|IzQTQ+lXT2EVTx4G|*98GwhB2m(9G;CQ`u}vsA{*KoA$ej3*r}IKrCC8^P$A|Tj zt&_DLJeI@Uh=xGIRiiOow|TiGN7IAMag!1#+h}P;Yi*2-xUDTqljU3!wSxQYlM@ai zM~1EMWlDP4@Nu)US1YO@1ETG{m6S%(cNgX_R954vLQx*%UrQqQ(?-|TU@sKg|0A8v zNM2{BJ#Bu2-VUzXe*cCl)f`y|$~(WHB(;2trk&`7zpsQ~=J}0Vn@qN>ys1%hBtb=h zg?KJlak-Z%**56J_qAkdie!jwtqK2$3x_c3rhrzn-j|&MrY{R989i-xF3h05IWT#g zdQ~M;-fEXBv!`EKPj70$*{gADhK1XHuCizwo(rsF;sRDDPb`nuM%0tGi|8YcQlyQI zjWZ9$_gxxhgyO#xY0Q#IIypHdi7bH26heLq>ZR|UWN^?Rm}z1n&FB?ewq46K{T21l zTS$OBO{8Np{YQIau`v0rt-S!KiJ&s)206JiRCJ0gr78^fbIE^|Acpm;eS-b6++53w z3HHUD$s!|B;35bR+*D!f8`VCSXb)3=54-+~E+eDK9IZYK<$5?OUR>fZOG+_TW))s* zeKC%BA1j!p`t6IRxj79T(G$xWm(9ry;vcl#@w+j@t57E*?mv3uW-0#(^7;&=^#{ zR<+t4%F?2xlv*4R_K?v32MgaOe#1$0kf#AZEkdq%><(vIbdBPgfm~lAE7RW;1j*B< zxyfHA7oDg40Wu7`jucC>kZidKEm!w0hSQew>vd*DFiJ)wiE>s^m!uzE%;=_3t%@|ZC#ev$i=xHm@ArZ-kG4#o{Cwa zMkrpxTugHDwmgGMtd--v$j-$nyU(CV5)sSeL^+ZXOXnMGY^`l6-c_0KoRtoyL1TNF zsOS8o%9qW-I4F`_qN=EP@;%y6sUO$XJt!vjA7bI#T6V;(K(`{J=}nw`hlx9zK>js;a@1FI^ebEXK%hbmyu)8?PuU%V<7y z->5XsW3%7vdcM;$d9)e!y5FQFc}v21jp|_YlGNBZz39-kGLcHs+q1geY=onGako_~ z$m!KYmEd-NibR;Y-kp{==%>|3xIJOO2U%~V4Oi6E(2bSbVE0A8iswmPuXGkM8vVg( zRDg~qDPS}CcSZuHyxHM?4kp{x=WA29(6#RX+7X+GZmDn-2rVNhF-1H#FZODGH^fBG z`IGoY$Ej;*BxgB^>7qO|=p>TpzCZ#46ZD^`fkL(tO|R}<7ZoK$T*qM8cP-N$Y2vGPnEQ(s0GMZ*-#0mXYBF6|=HSjwfZ#;qf9)G!V9pkn^1> zz;~@*)*P&l1(9=pG8)RqJDb1T3DcXN>7HSjZe-`kmkE39b_*)1tK%bJEv0lyf5m_1 zF=ESA)3!W2+x3x#w$DZ9DCOA^)5|cRB($9;oQBFrt#{^*oZUKOC9!YKzm7azE9nKx zj}w)R&*`KY_}llu@rBR_(K1P^y`Q|Gu8Q-4>ijMu8gzhR%TE!`|M>N958ABDfrdjv zTw{uk6-&0#t;c<|Zs?a08WI}1kGP<{@6buc+&%8Ls9QMlnax67mAu-ybn4!(8QbY^ zq4I1ZzdBvMeEi=mD(3trP6bBkCZh=7L^hxaXY)YT(_Ha-0>;}1x@xsLz@ z*(goybyt9=w&PWkV4dBnrdJ>@kM|kI4OaehvBPyyVRbq6u>}4SEh|*vXo!S&Ix`c* z*p!*#J!(D%!o>{CoJpzRNLcRwkL%R35_Q{wGR6a5UAs}_IbM4tW9ZmM!<+M?{Q{rh z`pa`G0$KsF18N0mfVJVXWxc_t-t|iq#Q*aRF%B9lPb?DY|Ab^6t)PFp%0sUgppH?5 zQX}jU^O|``>hGgIe-q{5&NC7G_wkQY;e4I$dXen&0fGf$LG%;c`kHxo+$Lz52rq9I~+>|a~X9)i|^&7cvMmiFppu3-{O9FY{4 zlvLAQ#+#cP3qUYAE;4qmrgwAuI69K!fMVv(79}s+MoTSV;uf&Wo8E^KfZzc{&`ROJ z388~8GN@znn*R-eBzYkanFV~c>o&UUTvSR6DSLvTO%vcB01Rl*EigK&1(ao5na}i_ z&FNrvZ(7l|tl%b-^JU;%I!$;1PhG!;w;0AyYlP+NQW}v}0=kgRM^ZiYYj1LMuOSjw zqyFVJ`6qDtQS2s3Hpk-PV(dhft-%IfUMh{*$;8JLFKBj2Kup_@loAEtAW(#^;otB0 zqP6G4MMzuNic(7Q7t})TdpH15zUJogeM;>3`LhMe%QsOhy#bB-4g5nov@VInl4l5D z#2&+dHOmn{Onl@*W0W%JqNK2->t)p|1hJjeIpbz-xx7h{^9u>}Z-N+3lyv3;r>1;S zTr!YmWtL zzRj>`mR-^f171Hy$?>aiqNKPf2gCmsb>7^X_&e7hzW2O^_VWu55%rj$EWV-&_k0}m zZz)GjxtF4%qQ=EMcLvm5Sk0(=@3-Xaa)DB#iSQ)X_X|8BV#3&{zeE$;YJlFT)TGhH zlU19RR?5gmTAP{()FQ>i0AVbkS{X*gL#mP?*V+|dg8YD1K6NqGx4M6m%NzVG{I)`g zLYpV-pug2XV~Dbta&GCN)w*ikk|mutz{ran*|*=1Q&tA*=YZj>THD)6G@H zqAg4_PRxx%z{;I?eqIz#Ny^uhY>({TTcxH9x=ASpEJ^lE#|26N>U*Sc9 z#_vk0g{Hj>hz2Mrs`igi;xqi4qRtr?Dmnpy3wrC@Xj1u1viNvSD%i!4&OtWMm8W;u znaiqJ`t}>dGACdgufa)qAT5oq!$qmA($C|z^Q!AJzvv)Q@3Ldt!Tu@wIzx56Cj=7V zBmtBo1F<`lr|q%{X`L3fFU#Z9?Mt+Kt)MB62mD)Zk?#58)UrYBddWDNU_J@FX%CR( z-xroYq6D2;7yBna$f?aiE*mB&wTh5V&(jVE?1@tEawu>A^m?-DI=cI>>u)y2mphCq z3io5jcc*_ArH6kb!Qi=lyoil<{agQXTX%xX@e}xP#llw2*q^ir=!J0pfzs&+?{_aE zlz*-A6QxUsFnbd>ypJ<8{eM{et|zYJ)qTTnWQe`r-%tDpw+U`o+70qMpfy;V`k{I} zxr@j0guH7gq^7kHhp*{-mtDoukpT_rP$Ba3Lxl)WiIpCR#Yn-GNJ&e~C_$fe<(~zq|G0*!5GyZyS9fritzK5%ZY;eWVp6^N{a4*ekx|_u#9>s8dpW z(h-2(2gr1MB-{@i0-?t1{YW|jILd@KMG9JgXu`l3w&#u+^-cuF3w|&F*YN9tL7vh_ zIA<`#nT)<~SwragL*2t3QJa~pOmD_=%RgzifY4%C-{Dcu_q@ufUPncHSNFse_HA)z z`|7j0qgmdyVWkKKiIKM;=6x$JCT-LwQNE&<1KuZmVc|vnmUmHc+%xuxJ{OpZ(aFc6 z=Y7(|p!ghd-2lD)d91-~ zi%y>OGT95&*6PiC6q^A^$7OWA=H*@ZLqsQpVkvU9_DSJP*doLAP+_XcQou!9?)1=S zghLn|VN+k5uT9DazcfQHr$)(w)6w}wzxCqoF_c@j!2i}BJLBjckLuoPM~dM^OVm=iF~Ac-WV-Czd?-t}}x53zYkyJ~jNEGM~h zdd74y2NkrfTQmR*AM}`8RlQ;JkIS&TY4t(*5`n1ge;qV)S>m#2pcJ#t*idh3GVDTf z?Lq6fiygYsv+)(vjI8^gj7Lq(%{8@b63Wb0$FgILk3W6JH4m+x`!HMg<;n&g^Uu02 zL`kn$y(AkEU7)cEm5}e@VpR=SGOuZ+snf#vVxoI$m zjaRcf^P&5rPjqjP-a6S^?z~)udAe^I3b^*&K=Qvlb5P{on3BB4=yOIrn0HD(%LFQ&Mk%W`s=HEx5UK85e;vUj=L=pnXF%^`P7hDXudg|-CvmeNu)MRxVX+--n=nv zQ%W-KjPoxd>qqpH%lni3QM9+WFSJ9tcO@SG82Wd#`u5wqi1R?PX0IT5xLXtv(M2>{=6)n3S}AFt)i>(sy>&5)uY4 zT0l=v;pR1C1a~;k0ZqG|+*;gcUfDhkCV_+R_`cQ%>?cpKi<(hu_wJos?UE28cX6~7 zJJGZ|M;#P1C|A9>%Dg9gJa}?e;$4ZadN#%soa6LsQ8d zO-_3%BZLtk)fkzn;B!Ut@{DZGzQ4HE<8V6a2?J)ko!aJUPC*^X4O3WRP;L$V1Bt%d zB`}D)8|*C=#ff_m0pEiOdu_p{v#YD6%Bg<(cyUq7@eD4hN3IOyAbh<}XRYM>xZ6Mp zmGB<_DvhJ8Vl(=qV4-Pn$Xx}bX*MdCrM|{|)+@M7{x5o4TXBhql$RU>5A5vPvCxHX zIm>lZ#|V|kGa~M>vt!HL81xBdg|(9DY_y|GOnim7?%?3?+Hjxu#z(Yh1#xzNlIqZq zkeN#Xsy&U;m1h!-eRRgJ#wl*_-d0jn^d*+mVUp<)5)#ty_;5MhOf30ebF$Xrc@N_L z>A`x@s}7&7?e@!n%uL%hC1w zdA128ogE!pLdY2hJ||{%jf_wO%uXCxOQIJo`}+NR-#0J+KsC9S;&!tbDUQWz_V)Me z?TgXk7*ee#s>Xh#EVO)rMNsAESy_^ak;h?5D~i4xoD*8U7A5`q;N}A(%;1NA$PwMi zsNDgRA)nqtObfl!mYW=JMoiB<5Z}dNB1BGlJo)Bv$Z#eE>df}G$D^?C-dwuHb=V!I zDPAG7v&`OW)6>3(Qj&6yR zmd0-A?YYO(d8OZ*xd5vK*^VlQ~K;o>)re?eA45FkPcT`+B=xYn}5tG5GJU zFfcF>dY{3Q-MDdMdTpe?!b_CCwbkB?&)n6`t#ND&8=_UybfWHVURQYSE#`Q?i~sL-7nReR691(fCG ziOQ@_`&X1rL7^lGV_|uM#?7yIxV=4ZnhmAQHN1c(%L92UJiHwYq0L#R82DuN zsAO+iCMG5v+~s#)zFZG212XPWiiFoO^?i6dJg+}EEe8G2x&yz}F0!!DHZ3z#yOj7@ zOiT>3NY(5WWCXpa!9cc6_aI#O6DM}e+8`$BrmjIS_X>F&5XdJAML;>m%(+VRf`b*p z<@ki|3=$cXuUpN0`!=0{f%Ix3p?VTTl^Zwcqzq>@0ngm$ij`)LYUu2w)vby<;h*r> zlo1lraJgD2o`NogQxubv!;+?qOMqoGG(0SQ!NYU7Fi=aLMPy8qYq*AEYis)@C>%Bf z6M2cba76W!IYn0@B1=R=y@W(aAV0L|tDM(Eo_=IQ9!`}b5OemJ-e=p`HAl1G zutf7A6|{xd&qhj?ZHe1*aNfLm|G@(^>+y<+`im2?BvF?}xLxjdYLFMFlJ#WGQ*N+i zh_cChR!pAK^OUFkCp}>9?@ZO)K-f_Dux+9R&({ zad!+^<443F-@TiWd9G`tB6cu4H-{1Yu!&uKz?>YR!iREczA+h$P72?w(+=qB>N2;9?IeHS zNe&h_6gf^pHB520zJ^NNL+d~ocq%`Bi@J6=n_z7!UcW?OoT`Ba~I}~TyRXq zLhPuh!0D`QYu{}BlWsE@mzX85Hb`G{vSL`G=`e8X_U)Drtf3@~^_in{a;lh^$F7S> z<=f!y8?1UMEe`%jWyYSKj7`e<`Uk=7^xVV$GQg9}el&!Rjt-B<0bQLZsSyFEx_2;m1_$%&6R3dwS(uS5_~G&>&eJc9r1|8{=3I;6*4huY%;RxK=%V&=aO4)nRH zrWY53B)m>$n*w$bkX;UDmNQe{ywEy)9r5j(RHC@HU9s2S??nhi+ls{3$v%#hS3}E= zM;E7?w(Fy+;50G%lNNOKDi10@HJQD-dAkeRu-Xd&Ti2;a{Wfcfl=P15l{gKVip(o; z`eHEY(R9ig(D(&g|8w|$JEQhj<#-dK0Du24M(y(NWm!Fy4VU^1s6_tmd3W~qwuezi zZlA6U=7~z2d_UPSMcAG?i>T$FDQ{h@_h;T^d;U2LGJ11+J2pb`#S8zmwB2{%=Y&8w zAmnNz;9?=n!TfW~{Cw+yjVXO@{pCM`cB_MTyfxuI3vRca7kita%-4DMnU_KzS?j^N z&&=Mk%9&A(`@SFq_@;(8Z@Ui0`g+lr{D-r}9N_cq$VmVEzOA{p+iUS3m>w<7q1eon_yv~h zcC|g)C={43-q2)2pq!)^H)>ySGzd5wMFx!6}QUTjdxK z6oic`4vLWCu6w_iAwlalN^t4bSq>0Hjq>ezC~)Kn-AVs#R%kOdW$WzRmS9sCZc}&6 z5K37e@+g!7xj!_OedrY&KGDJ+i*{Yi^QOenw9&(d1aDJP7$L619{{%?vfTtKP1bQ^ zJYKIpEe!)$;R3r$JG%s1WSf)sc&&#*?#o&%aH~FQk4T!HpC|OWL=aGu2D(h?Mg)_w zZ?E2K15STvpr`7!Y>E$@Xw63oYWYb&zw%i`e<_L(MaZPT=u1K!;}``F+l_Bn%DC-~ z*kABvahSp_dJ^4i9dno1iwv4Y=H}-|ji<2QGQjD$cH3irVCZ+g?x|L}2Ryr9&TkbJ z8?6^?*<9t;>9}pB-4cbW173i-#=flsi+D@s-nh>t`ar(Rlvk8rv1t&|IeLujYdg=~ z#d;iKV&cYPUQ>CanI_7uwGst`?#}~4P(nt_b{@VCooU_-2z~abBmY;ei+PES-Xph? z!a{DqRHNbo+N$z>*C;6;Fh@=2m4EtbHTH$39G8$@{}W%e`@Vgx(d)3om-E!pv0Ss2C25u5F*b?ZP3-g5O) z+g~dw!HhI^cBYU|BxxJ|D{%?o?p|=qUcyIYT;%^2kql8iK*5H>|GR3-oDWpYk7fxG zq_|#sO#Xby4Gm>}r0V9SG*jNyzqn2%-1lyRNB4Le&d+VG!~LHuk+^N!+xwe(+RDnz zRORNzmrtY7(DE>mfF)r-o>uwq)&1e}g4v6w>B?aY;u)h(F#VUu^QiU=(gU*L4LU_T zB^_PZe5d3?=&kIwhJvR7*q1`b<%D;^@qTc_&2)l|+U(YhwBO=y22c;dfQRGlIopE! z;4*0?Eo1Vb`O0%|G4mJukxD9dsQ05u$B#=${^_#3o#1nu_vW&rh~vs%YL+Jv#axQ` zP%;^|J^=xQ+;u*N)petC@ha(`qMr4WgLQV9Su!@U{I#9 zcz2Nu6Pa;v`~>z{j9%f-xMjhcMTGmX16i&MQKN#z`)ZB!8!}|FeYTSbvS|4?a#~@Nbsm+y=dw$S%_zN63H;L$&Mgk5W|3CgFj!=dK}%4njP|U z)6q7u7M;;zk1iY-7gVH#pwxOunAK&*$IQQv1r;LrVd@cxBoU`J6q4sL#1L_=X`e+7 zyxLo`8?JUCM)X_0eT#i)00wpbe7$1{4|jLKrMZ(ld}&cEEO)dD;7&b1J55c^+Q>U? z9=)H9Gys}@QOT?#(^VA~5ou~}CbM_rLG&ZFCeDRiHgfgF%=$<|4r8Ih3#a$b5y6^Baba?gY2PLjTe6B4M1Ui^Rzxab5{y zZ#eDH_LvPVx6S0PD>D3Ydc3klYpCEE6dC~hJ~0jnJZmuH!!yB~aM4SfIK>`GNN3&4 z7RWEyJS8GotTkO;d*3!U_d=;IibrkTvMfhE;5_#BbBU_zYL**mED#O5KJeb}KRsjd zfB%c)x#1VRl>NRR^}&4NS_rPN-?wQXi1{NPvs{Y5U@@Uny@qaPR3-J{q>n_3Mjgr# zD&c#i*u?Bd z5LVVRFLm3bP#^m2q4~;qq7km^J1QzKCk`aY@-X=cddwE&l;dqELAYHu@YNn$h;)b< zG)V)=%JlsO2Xt4mu(IM(-p!c)6~ozQo}<9U%9i}VaF#lZh93)`vhKw?uKBYtYF>)f z!E*vcgUvgMp{8zX&t3^*)8;Sjm6XECk}Fu0j0|ZB zORY@B2zTN`=ujE`4GMfuag%9=8;r-1j0HN!cQz{9&w}WA;`x>zEIDQ{a;s!KoQXG0 zCKG0sqR~rtV0?b>0lK1JAjzMjWG`m{`D8KN^gk~Qmm!fG0{eo4i(BLlYes`5-8!%m z#v_+}rdT_xa&yk9;039ZcI@_3i3hHiIbexBO>d#fU#6FfPgX z7SE&y5fM=-8}s^u%zlI21q~SfgAPd8c_b6XoMq6{(=&Y>xOCK3Is3QobPF|H*b@G} zlTFlogH*hNz(VgHWkzuZ02MngZ)aK6K32oPF@!z(cpixau#nghNr;6Gu|N-_|MMKF_1Mi&A^iNn-e z-v5P`$418lG!dkH`}cXxa`pL`BawYQ`rHkTZ}MeWT`~dt%B4u;Jva~tq!!0xWb$?` z0S=m!TQF{0+<9-$2wZ{$rLm3<&A~%#CjY;4&^#yw6~0crHzC4y+YoOSc?hjN*tw&T zaG&9n?*5&pP+f)fXDVzXJ7ez-2;SgTJ(Y)7mEPtQNiP~WYiM8`1+>tQV+gdH4`0oh zC6pJ0W@fT~OJzh+j$0?I1+xe1ju&Njv;~lj*j5Ql+%WyBt}8)@tG(kRJCCCohR^9t z^W|3h;0@BJZztP7-3NFeEl?mxMSyB6gYFbA4F&+}ASEshZ!;Vn9W?n~t7-yPdJDySOkGP(6Mdz#6}o1?A74rv5t3 zU5|@dA1&>-ZpQ>g>IU55ab2jw;Pl1O@iBPvF5UtP*y;q4+;&l(ZM2NA*xjc&FuWmK zl@nE8;H-^N-j(Tl!DT~1BZBbu_TGP2H*XQLS$i^EYQYX^Z_5Z98yk#~qEte*inWL9 z3v6p-X}^avcdVXy5aBT0iPW+>sWA!@<+Q)qO#`&3-$TFKh`!aVSMdLo^!2qtAiHsq z!p}~;0_CjX{>o+{;7K4ps+1z0;uJ;hxFhL;ETg1ncueFl5T#Wofn882pe`v3*+#N& z83jnaDYfvKZvV6=V`pni5-tA6ywv3U8IAVk#W|*za`dlzbP{Kg69;;V?<0X}#{Co! z7^qfl4O~J`Qpu%ol(>iUh*!$M@^BHEpPzq69GDvu4(8|P{AZe`SqvmDmjH#%)u~9lT zsjORR-lF4VcX zFL|c!d>-}aRfF)Up8ujt7AZtTEXFK#`HyHE6U%x<9; zlGQMV3-51oj+3e~=V#!UBa8$Q_fx-;WzB7{M#=P5IjxFZ3JVM4m*L~!e0NC5R%za( zfD>>{6hrD=_%Dl%dnT6ma6jKt37o%D0ewVYm9tujf=KLR9h{t+q1&-NlG+GGy~A^b z()ziqbBJ}g9Ty)>C2{l;T#=(3x16fWn>_9Q`LkiO-X?!vW1avD9okAbHB77`Q|y7L zdjRFtQIXEeFf#UtguFu#WQ=y^f`JF-LTRp0Q8Z;BRX?TNwH(|)A}x07+z&Ux0s5aE z&2@Z4_@+J~J2GZ@JZ@@dzhylHTdNEb)mAzMTK}UUCtWd(jT9dutsgI8fFvd|I{H;; znMzjlJE#mVb96$8&4mBkX-XXYfNrOSbi#ux%WuV;Sv&vZ;@!y+QyM_Ttj)|m>a43F zllq*zvx%X6D?gY{mfVsP5gmQAl?W0P>P_u!>M4DLGC><=O5wu`yEE!Nec!;R?)>yn z)9DPhEA!pnxd%bqZm++*IK@OxRCjkK9v_#IcG->;8YJnLMV994)dwafhH!j^OpAkOuH8xD zM9Z7;&2wa=BvT0vE%0ut9->*gDxRAoZ$ExC#udp^3$+@%C^nnu>qGO~hW*c)eAB%f|cmBuWgQE3xV}na1{PAW371>{UHvhkjMg)-( zG=u?CXoxrwei{(-E%U~pvy(==XZ_aBPGG$c0%UOT_rzn zYog*zQO!}g`SzyW1xO|F**YY zaDzfajf1N{{|AhRF0ZsvUBkl^u!%q-2?;DiK`k1xxV-b+s=x{08)4(b1iEro*j>(~ zloa9^SxidGSgmsH+7oSn>pa@|FFwV0H-$j?GK;j-vOUy?sN(b3UpUogVOfyUzO`b?f>5~h1~aG1q^7{A?aX{bO_vh0=CQ2_oT zCKA@p$@*iWPEk+8n^GTxe0gtgiI-mcZ!k19T~G+VfBAh%N+d)A6oOsoa-*2? z+%0{?d#SAaVH?C!{qE13Q6D})4U`yXpnPVKY$QB8UZnQM3zx*liA$Z_;u7NC;5A=Tgk?+7$8{rp540@BU^KELh8BO zJp@I#(*`);42SaR<3taIb?Q8y+K#P_IGC@Ae@sXSZD}bv(_oJJ>tXT^xHC|$Nm|NW zHW?a4AilQ6@gOleFD^tG3Tk#fw6Z24rvGMr1Owo)U;9X7BU+@SM048`=g}!P4%hop z=mU@9N1cJq`pS}p(n+e#+P3R4vcJ=xse4;;Qx#ea9ncI3QtcfrBou&?lgQuSKdw8X z(o3}2lJ?1yCvu*-lT&F)uH@8+=xsD82|p#H-Of2?BUwFBn2ad71p z-ezT`ewBw4g;c*LKKuPXM%`=YveI~ zE)-HSAAzGy6>_ze1YC(gw6;E?usNN6OJ=(^d{yGsSH+v4%S)k<-^0fdQZh0)Y0A`~ z*@0yO=ATIp*Lq}5Q=#-Pe_UeK>dP{#0OL~VAt0ZeoLu^oPWwZP5WbUyLE^7vb(Tu1 zl9pBo1lXO&tk4y{5L7DMpqlwiDH94(=4>+6eD}1QD3?uAzjq zwsxb#jU>Nh8t4dpQ($v$Di=X}h#;ar31ZxwsK$hWlw6(x2TrD~963 zV0-q_KMfPT5Axm7ap{R5b;B36oT%7XnES{F&~%|MV z4xtF-%?IeKdaH!zlNPs)+QP2x*=SZf-$8(nMq%2A<<>Pklyd>DWzX4;#4 z)$$Sq)$AmmA?0Lra3CmYXj}n7-f~VTBA8CTNmIiVJ^877w?|}p0-C|0L`nor$qnr=y zO=er3#__LGKd_gBx&hFlM*Rh)=l%fYk;?*t$>mP^AHLUt$O>uZAmuy37bG2I51{U7bH08ioaP?5}m@62PR*qRTImJDr(?= zAhNY-HbX!J8h^s)E;kP)1w{milZ);4l~=%Y#3XreaF8YN03D$qA&d{@`SyA_C+4{k z=&n7Wb^?8^?I~GX>s4qw^9%@0fBv*}L`dkvM^Zf`ZuD*0nmzI61av(aqcKKzoOT5%k}t?YH?Z zWH~{jZsFN0AzIyb==IDERq4Hj^dY5$g%F8o<|snG#{J$BKR|GaxUWeagU^)4gn@x^ z{pQVgjO^^}0~OZuJ0_?-f0>`xr8#c}QjI1u?89d|VP!i8=e;E!*R`Vd94p&ZAS+;w zkpAe8_9NrdgY!FP+2(fu|3UTgff0QASb+H`O**I%Tl@R1A6N@;Wpx3phu443DTaLF z$9Wwl21tMhO{^sT8LViWz&ei@H1hrI=m?ZeFFvj#`7_R=Cj1~(@}@Q4K!OOr82Yp! zH98(XIUEHXCWPEMxuy8k&6SlvV9;BPBE_xMGJ=7A?0A~>BINS49(#K}mUCutQ5{+L z*3Yl|7n!Efc*QAuRXA zbvM1N)8pdat%AXcmjFw;P%H0E32zJmQ_G~3H1?MI{LK&)hpUCVVsyBHJT@q0w9v0CgObBL)u*4Grchm43MvP)fuwe)qev zgw3R*p|f4@vKHCvnNuvzA~aK>nHed8;yOv!(msU3>WQ zB?IVBD2#Yr{9FLA;TPpB$Tv(MJEmu631EiE4;V(l#6o_4xUj z91EPaFe-zo8;TAl|G!gO|Bt=5jH;@O`bRe%5-QzDcb8Iv(jkq~5{Hsb=}?izLxYG& zNlQtWlt@W~boW6T$-9ov^Njc2alib(|KB|r=L7rfv(}n>u9?3%*We%_=rSb?s2vLs zwn!&lgH4r{vZ<*FnXJSHHOJq-4~0aI0R)*|@foOLxqb@@{A98;kQghF-Hd+!vPaIE9|;CU-bRX_aJNd%7bc2cUiG)++?5Riids?QfS$*Z zb7p3S``5|F5F-$ZUQkMas>9r6!<73DaAzdWZG@ua88HcIL%il*(z&fkL;^k8*4~cR za`km;Cn6VoM>Ug!VP zzPA4Ga^Ta2PCC0bi)@LAak#YY@Cv2Md(f&w<;>t0$O9|^fA_1+O=iG3Khj1I+Qcov zr+S6|(z-t_`2G7A&_jT~0#YFD!D*e#GG?)%zKfr)B#wSUz(7=xrWgn3>U;zCCr9m5>*Fn9n~BnS>tV@-k(+pA`4=UR8T?^$F@P@ZS&CUtBb>EMNruftWL+T>aGVE*I15a zCV3UOlD=um{)q_*^xWKIFVb^zBCZ8Nc5DNf(o+wr7sUtw5B=Fk=JF@Yv-Nh{{|0Hw zOjf(cj|Mg;%II#qG&Qq6GQ9zPBpn?Iq!HsK>2eo4v(sO-NMBVEGc)JyK~d!nK;N>4 zcbo}8#+sTqLTOv}@kGF#cse&HwK2h~^QGAJ$t~nk;__5W#*U1ki#(e%rC z;7Fb2kU)`C`Is0O6HRyTaVO{%@jh|?juIOPAi_MFns<^>zTP$aKEOjPP7M%$_G&5g zjX&Ligs$}{>e1_ALdNtLS66?31q+}o#-=i$HM@4!% zYf-m-srbww;PGL%j*7y5B-pg=lJOWpW_CvNxskTx3V&27M?}be|9li*_`r@#F%<+YT@!2E4P94I8g3}*}!H3I6-<$0%~l1ebMw7?L;G<7r(E1 zd8L<=O}RK-V9jOb*L!fdR-1#zaaFmeq^to$-ON$*qS)8_59;+RQ{W8o_gji4?CJD5 z#;#povNtwzao$(LzSj}(t(_5R)8^!-pPy|W`*4|F6M!@Vk=+6aK);G(DXOg{R?ek_ zT*>2`^e4_cKxe@&Pb%nP78{W`fviE!^%d6d!2BGhTdqH0bnL7=+0)OfhaZt(8ZITsn>*3-gF~*asUr}3J@}5b|>aE zxx)dO!pc^is?^m6lkaO>1^a$|w-jg)$!5;EU!eHYnd&h?r+GYU7%7@J2HzO|_~sFV z!ei`vUUV_bAm`@SRsj;ked(f9hF?4GF*2h0G|?KAkOBrkCmTZo8(zJYDH_u)>}WNK z38!Ep6aGm07WQ-UZTXQu^~80Ie71d&PT}I#78kG>tJoxc+(GPt6SyiM5Q4Ygg^~W*HAVN0P*2|@Y#CSToY4W$npq;;zMd`94b#|X6>6@n_U@-MWXm+ z;tgdHq*hj2f$>yalI-k_J;|a)Tn`@XB-hq1-&hM(*iul`fh=29!6+k0`c5AsA=Oe3 zXsAT>R?bLp|Dq2s&z1>8JWTzMJ!ZV-zstw`gVUmJ&k+A()`tzHXJ>=MrXz{dLArgC zjfJJ_HksEBR*<2=)2FgHl(IXj9En*V;|uWR=#chJ$^m-+pF@(|AIKd4WT}JSXFzQw>`ij%=TTvg zzu!N!E&OQA&LJ!W^?sr)09xOX*zbsUc8+^~b?)Kbb_<#MPAhJdxUa*34_WfT-gA3O zaFZm86=8Kv5<*A0xmG{7@&ECBK<9k8mS)Ayx7Tivs?}EzR_2+^bEQlBp_KaL${LfP z$}}WTz|_a@%c5@H2!o0-`P`WUZB9Y8EjYpD8x+Mq9GUkU1JY&1_uD%_fR15ou>z@Mc7 zNUN?|7sA%Tvcv@u5SIWbpVz#zDu88g4^Jwq=pq#fgw`raN<^gjbV8& zG6^3wIeMKB*VLdqy2iwKG5k&tT3POv#&&XY0`)B75Cjm^z!oqAR*T-=AOJi8Tr(Rx zzj0G$QW7j#3=M4>yK8D{iqygD!n58u=82-4mWG+njSKZxV85`btT?{_bKQSpG_!97 z%{95P#(Her{tjXtZ)r&Hy_)d=lL-RO2s)%ukbpG9JU!nO6;UOAL<(DQWCkU-fPgC5 znm|`lSH~+l()&52AgMAM|Cr$qIusE>EF_t6TO0(bB@L4*$%$E;#|`$SIqLybth zo33%QVc`QprLVJ-Bp{$Jivj_Q{bect!c3fz2CvS8NHXm>IJ_?w3y<8=jSHB* z?H7A3k&a)YQ}*ES&YVqjP0H%s$`W6mYd7y_}>x`a7>?_t8tS1#L38*Jur1&IrlCI9#w5j0!@6H?j9oS#FWROY*>(j zI5Han{!e8Uy&ohB&MK_53Wz0rjtV`~0oeC2(6lin1jJMJmw)&>6ng-+qn1nqh49ob zrp%v7r@F1*y_c-dUxr!qS-rk0wTT1|Tcl;YWH zay78wL+JvZMwV`IY>BD`ha?gDS&&RthRkS$&~KaS{-+nZavvi{LyDFCv3 zG$3h$-oLXg=!*%3gieD}#t&_2!t-c+%(ei9dLJS=Z{N)-C zfXwUV9e@cDlaMR{ zs~U7~G0l1ub{n8?vp&+(0eKbD@$NB0+b6lmJH*76o_@f!e|bo{&@XDbP9zC>Fv-e( zdtL=X4UUeEY&<;jGBQ#v*PvW_zlW9X@Z#fCXio0%CLSAy76ipBMmd2U^C(5cVIy~P zGTTB-y`m-O@f3%kAf1d%$!9E7q%s2BRppK6g;>e1(!qiLL{W4D@5H5`wB+p-NGynl zhlh*dm_-hKkBir4^ArPCM`G{$#O;y%HFSLFXv=CYlSQa10EZGv5n#sUZLY-zH5Hcv zAn^=hj{r6OP)qABz|f%gq{hX?oNZ*^jFY?R=m>#y-XDGoJb+yRCpr|LD4!_MWyEww znv(RHA%#9LG4B!$f~Mleaqn#b${ZYf3`+D7CHnzj=hEH$8Qc!J=p=sxppF3UI>2fT zz&87!Od3@2d0${r-K7)xGxlJarl$y&Z2#WmVP3ce^RNQP?=x*&o4R zQvEsqu0M`6iXSA53325pAYuU)3MwL^cTAo}UQ}so&lP-{EZ?cZ{2pd$)cd|HSU?~c zkhT{#8YUiqF1jFd1RwRw=Bi)Z+j)crQFl)j@9gCzw>|E_=k5HodK!eK z<$(l2wwVL~@o&*oq96p0S8ae{ZEX$WSPLI~AAs5_Hjt$V(%~pM43K+_rvPv_Ahn6mLpZ;qSodO~L5ZB59ch&ThlL_i zpaQ<{&rr|rE-{>1?MY3i4)bNd&&UHz%x9LCIUo3!UqD?UA?7wh?y{laySC~=lr#s2 zB#>ExBA+-o!-LmTHJY1+g-e%#KMHcLzr59`oJ%Us^;cGd4-qYnu|(ch@tzbB2@a0S zv`LrQT{f8!2-rjFuHZ}rOiL>g=p#!5k}7Vk+ocontul^g$f*fjRK(su^gq`n6gNk*hw%EmEa>R6a-^eYenTP5Y8p++E@av z@n6!|ZvEhu?Hk|$&HcL7Z!xt-d#xBEZZKIG?L#DV58g2cTJvL>h=$Gk-@WsIL3?|a{hwO5N; z4}}{rTw0WJXZWB3fJ`bV@X|0SB>tZt*9B+-m&3J;z<5Ef41T=)71fc)fIvy`yI-lz zE{=TAfEK~hMMthUjkGJMPxV?hA~Y&Dkjqn5Z7hNP`FgIFBcH{j1s}P~g(r`zE3TQ) zFd1;a|B~nql?C3^)N7=}s>x#y4uo4J!gANI?e+Oq~ zCQsf&S}7~07#OZAYosU-OCK|AgX-gtT!2VhgdCerhFz6COhi|2Nz!`+N8~zFd-P>($?!&?c70W-{ z;*}6?SI{R03|H3(h!3h|OLUDz=3`A(e!j){-0_StJRE1bjokMZBp~=_O%=J5?t~kZ zbY_U>H48>9;wuk4<>laDc*nmCL8cVZ@*qZZyT;K9psB)XDE}Ry%G;oXU~_Zodk;H$ zYpcuCe62dXtQFNmnF&#m3LtdKV0eQ)mUmyhLT~#gJAzyasM2BN<}Pa7>-)8_@jD+F ztaa0ke>&Qu%AFu|S8A-lK)SO(FN2yL@BZ_O-Zt^CtaHX?n&QFid_Lef4=N~xIgp79 znA4=mjC{w%wd93fWSTg2>`qVdQ5K-ngSic-M9FKr`8Lf#UCwX$W7j)l3YgzU;9i9C zKO>B3KaJu~N;iC)E?No1Q76a<|1*vOUbKAV=qT69DoG-%^%A+@NiJB*<7A--dQhOu z+pQFXy0L~pgm~_vK74|K9EF@*K;S?_>l1nqr$LG1#?dBYL`O_vZg`})RLt_1H2zo4 z)C=3&uk}hA>EHdVmO+vjJZ63i#IS+m7!k3;w`vu~@$SnykhteH-?QHNd@K%9G`D!- zz9QyH{V*swnFiu= z*qeI$)c?;565<4bn1h1|klaUU#Wu@TUOhOEqPY6;m>_zTC>Q-PF`1VTYTORS!rJa` zaO*?q@<&KmsX=jZF)A@xau9!M)H2@kr)uv)&{Nv?%$W2`CbR^!2T)Y?p7b zvMQ86V*F=iaV%;@WA^}?SXmVw$XPSpZRGQ?J9FV*W;l6-L;0?=i{z zmi6UJj-Mt^(8Q-t->$n|sEPQ6%&ikX+M<4aabKj%X5|`nVQG0eT*SezlOP-r-IgD0 zdd;VwA&7+jCEEj^?^5L!^@dm1zMpDL%3fl%8bKf{jw&t9dv@k6%aB!;`|o-hs4O13 zxZnW#J?8_5Uk|#RDKRXXWC}1BKioNcdo{$n$il#bI1_^cQJqhYk3lU&!e3Kli&L}) z8^{Ofr$Cn)a+}@rR7Ia`{Nruf_4|I1CIn<>XiE#BgyigJRyy!*k@BRpo&2K)_IY0r z|0O;-S!c&%rf_On+lNO)wK#yE_)*LZAu%mD@Zzz{X)GQ(I{xXSP;Ym6p{%2Gre40x zAbT^waE<;j5OQ{PefThl7Zj;JjsZ3Ie+fsa@GKR#u1-#)sCqS$2X&@mW8;e$crDSP zcZJmYK&Me!Sd;_m!p7=q4B!E0cXoCpoAP%dMfOLHgkXLf=DVaMB*O0LlP>SJxDC6Q zntU?6@3uBTAt;{Zr}2k9u|yMw~_xcYyLk-t`KE|e#)5Vl8hpUXUW za*|l#qa{K2a|*~Q=&ta;uP8|&cbHzfg*-PGuv{*5!E3RTosxpB>o5&Z3InUtd9gDj zMa?>eLH`|8&jPT*8yyakzp5ptXLuVQR7~gl8F-5Tb5cQ0jwynMtX%!Yj;3kMGAT48 ztMLg*Z{+5awGlD^jQ1!@BZ-#-qgFSHLMkRaoQ+segn?H1r_l5Aa@GgROfN(&HntY% zA6=Z3EN*Pjdp-q|RE*CC{_TekXeD}BkSK-V932sni!soRyI&n87p?io$xvqeAX*3* zq#=4GY?^;-*Z|8)6{hlQe{-49m(N}lRPMVCMsYE$p~c}N&k;w#OXy(&2n{t>W-z-r zWe|cA5O4*`wUt~PV}mv-yiW897{UKC{c|dzGcW3IREUhsy2@@9$IDCqUG{(5b&EaG zj+pp+QG!7GlLs_<5+_f+W8P-HV*xSq{)aDiP`qN?rf|);TC*GsrM+L)MIyr~w;Ae} zmhJ+p2h|L!osDA8wg3i5XH4Yed|Ar}$JK?8rs!ro*J`7{4f-}Xc5Cqs)lxG3W+ZT{83FPM|@w2{~ zPRs`ru*FOwxvN{GrUpG0HCz2tDG%@Mu zo`9_!l+CN~FPq4Bk%!7^o;aDmxp|g`_GQmfV4auc2>~~$pwzJY5bq?6g$FU|9&~{M zT4F>BhJ;?-i&yF-hy-unRtOkp6cjHtIL06`{MNO{3Sng=;ylB(Anvi5qjTN|)#{T! z(ud-dbARve2gSyUTMDF3-uqW~8HfRp&8JUV0Kd{|#s`7Tpig0uSzAX^LM0v0D8F)B ziAVt_4Q6w=mYo%)6oBpet%P<0a{quVyX2dDEsPN^d0Ov6A{d)@2NqQTVVM{=7`ts8j=FLnM1A35{ zce{LMYAU+?90iQ<{)>;~@bBOE@bPK1vhP$A?!{jo%9!1F9BbW3D-LODssF$~8t{I#5-DS;qaUDWMY2tP2BItYB9~0ew zgYR`CDQJ98xI2>s_a&f4Q9dg#PZ)Ao>V9L6#2*j5f5*woD-%7rif_2Km&&Za$xGKu z$n%kQ{`BQq(uu~NScBQK;NrT2(Q`jX4>GnM?^}l#?e7=n}jr9SXjubQP z*sy4=r1v`2-uZXQxW>9$cOTklMyb^j_KbTvzAtWe(c)z%hPU4l5;NLajJO6I4EbR{eKVo8MX9 zfWpJyu@ts%1Q&y`zH%$(Q_%^JNPA5%_h;uJ5HNkU@)V-v?_jW2qa{ijZhIRWo$PEY zr8%kn#vIsV`!z8R=sm0O;rd4KkJhbB@cks|>Bc62U*T_W< zB-vG$E-q@~6UM5~80N8TF4F%VoOkW5o);379!Q4NsZjd4j&pha^QGgJ0!@ZX;G<)S zBX#RM8V@p*YXu-EMz?6J+G{T$Q8{t^4wY)IK<4Flg;ELc8BtVg(3dxsH{#Q%0`pBu zPVcX7x4I}F--fa2>0CZMIOlETvME4OHYK2sK%%n4Wo>Ezakpog&ec+DYlG>NFOU29 z(S<5!9uZ1b%OBk64>Jgke0%MmCf%ZNS%w9vVj=oVQzuNQw5Yq`M`@OUUts;hr_9Dw zYk9}pHAYn3*Mm$AYFe30si_g2A?N22TU$TMyXnBFe4(|X3MpX8w(jn#0ky0!#o4zol>XfS6mnC(;W>ol`GVPm{+MQ9A}es;n(R61A_5ugL@+(82*>$D9}$$ z1d@yW=1}GS!va(X-n5Nps9b+)2y``DIB$7UFJdM@C{_DXi6gOD`=EiD+n(-8{v+fo z&(DvfLQ04rTL5*i_4ZZ;txR?}8NF_@h-b*&EM5U9#ALczcr)0kX4JI%TUlWre3Db=mrG3(a)4<-%Dtt}60cq_1hsPM6pbBk6=4ZIFmR;v88FUV zVW1G4T3AS0h1DAl2j#f^>aLus^6{x3ot@2nkz~&a##~SZ`w=M=-@SwXSpCtVjn#Lg zE7-D^WLSEYm-x56*dGbKx=km)P!g4wC%T_Bbl-HhwjMuWeMa`v7UcZ1 z+^;=g2HDwNABEe~8Y4Oz>+5j^N`D~bv{I>7LE+F)g}j1_LYTls|Iz0a!cLb#X}(01 zN)#0t>b6;j+4}rF_~jrIcT!GiX(K5wLSIBh0}Vj5`T}JowyJCUAO-sY|uG^sT zp{dlBZDf2gzM_F(aovI>tl0o;u8e6ZGW0yKy8w4YbV_8yz{q_09WKRSU;-KrTlcCQ zF)8!u6iK)UX7ISeP~<$!j{-)8N~vn+>}*)}Y?+Yxsx&8r31}Gm#cw9LS_PJo0H?wk zEuS-CD*YKGoT~|=m9Fof9%bU=6_cx1h?vzI)F3vk5JQwbQcSjg76tSCq>2y-uy)jo zajf5by@*04>M?2258pmEkrYl1)e8mcSzW&Vl{FjG5#ORmgD6K|!yUX9aA+Viq4&5Dh zOFojGU6!ULuf2Z#suvAb)z-$kh9UFihj9zfraseg)gXq-NfY(DsDMs3JQ|o-uLiW1zJ&`Cpq=}7IGI@ zEBf-LY83!}w5YdKM)=4MGiuo_x3)%*5pfWue8skLHo(NHDHb}Ee&OI;M9+78;%R?< zNt-(oH46o$az!uz>uUJ=s(n`z<~p$~vk>UBT{7X%jGTdk^Y5vssmX?%vru?|?r=r; zI;dT$ude{Qt*D5zOedlV4a~s#oHF_|Nmqg{hpuj!XpGn%LRzS@#jB=Fsk0vqtvALt z8^xI%1%KdeTU&mq;Gs0UN*SJ-dVbL$>||0lY#VS7f&`VCXg2}so05_;@#2hDFt&cW zg^Ij(tq1S~UpN-sOd}{-Z<(K+XWjIN52SiGOA?f5d@s=W&Vjw*jR`b6Rpj#1TrJ&P zExGLM)t;`TCi6U}+Gz`!+W{8GTO_{mpNUcm043x>;$U?b9x!ig&-@yu<>e)FxOU8z zImm1bJVP;##54kG@}ckFKR1r?G5*mHrN8Xk8q;@DtCfIQ0Un`5C7aDBGj)y?$(htr z_CHpKf2_N45W8}ax}l38*@3CEyvV5ZGtkx{sjj~MlX|WbM6m0W2Fb6$T9_9W%v|b4 z@kT=z65|{%@%2es zU!H;kvX3j3^sp9=guAn%jd(Y&;InL-97;s6p|pcPQT6p{!D>>O+P9rc06F-`I$UOO zEjTEtioje7?mvER^k`U5@FL0gGR1e0D2f|xfLhN)tOye;Q>vTmoD6;ylk*2yr1_&0 z`Z|9%NP{p6J&q=^y|=dxkQJ)$DI^#+BV}%1{n;85-BJG{0a*_%EaZJkFt#Oao|v7t z$RkO+J(;3ZXQXayJf)Y6&o~6g0gF8OUg7$3>tUDibjZUSwkhc@m(Bg)U@C?90@9Q# zY@46R*4s^`My2jGsX$1oFc&mEQ40*L5lD6aMuOTp1M)h;LPDQHvAT)7T`qnfBChL{ zbDifwivj*5OMmFWUh_rs#Mqc_Jx@&a@T?=>NCUGKOEXJ`IxrwOGY1PbzO!ejtL-BV zBEAeE%Aqq_fmK(zF}vN3cUJFm0^vusmg(9BeWFqa?>Grrw7B@N?)?0iJu@rvV)CHZ zrFcGZXJYi6Vh3=0S()MNECz*iBIGPzrvikZJHSLnM<=q<2F~ce^cTVx!l#_UNg;1!o;44I|g2qlheCW zN=Qtt+Q1l#FlK9^O;c{@Ozx%9aPl|4%fXr%*D*pOF2YtkaPC^VD@Y)czqN*bZrxUMxkS8POd97T(Ksh%eQ%rtkOf&y>-O^;Xx` zw>J)vl9RdZ0P1rEeZ!Al79^N*c1|d@3B!}SuFcHEos* zz11lg1%|RN_$$yMk?UUJBI#+i&*Mh?x{H3jvNbkwEfMa6@}3gta}{lmG0RHZ;}a2u zkL4e+_$K5ui{HKD2bMiB&!gz_4KW`MgLbG1zgk+hhs9!?uu)6gCtQ_UK4*{973csj zXoNC0Ns}j8o1@c42ykQ#Fd~$5MHaIQ?W(1wR8EzfjfY`gzJ4!?8+R?}8p;KxZlUzy zp$ny)q!|nEJJnB@zUOkUF?Hk}1|$ZUPa;1RJ_PmM zb=fJ@DO{FO&DBchh@t?zSWyD|b+`MgmD$3U{sSYPpvtI30$7v+oxhMF$8limECPP? zT*+1m7ggKKE7`{f7ih+%Zpzjd%RGs_B+*d`LIMI0%1b0&Cs+h8(p~S@Tq7FF$WnbcK-|6_|4j1RdR6WId<^F-q%Xei$R+R<&Kl85cpu7Jwo27#MncZu7#>;5{YTHpMl!C-Yeq|5M_}8uU&Q{O#=}|Xj zM!WBLSG^F)c2B#)MS&g)d}7n9_OT?_E*e|yw&b@{JP%L(+Fkxt<(CN;?PR^(SfO2a zBxal#DrKKH@PK)R?PnZGXEqTnKczk>zurP1fSxfiHom{VOkh@T@AV@J7%l<=c|J}2 z0RyVMb<=2wwx++vlUXWoyiVsD*e6CtJYEua>ZfgHz)KU{>tZ&mY{NvJt-9J;Ri=(B zC>vp+0+YX6w?sszWZWQF)i_aEH(Fk3DzX>e%=@k=QgM&;b`x@uLOW}ZeM29;-}$$=}g?LG^^2m(~1pY_b{0w=7I z13ScU)}tsV^4Gdnfd=62+1Z!Co5es0#E0DVs2h}XBY>SxNx{w@K@cVYI}tqf_P_mV z+p%u7(BYK;# zC>-eq@KkR5UHd~l!3OgRN2*O{XSdpImJ%bLUNuRu^H335CF6MJcGU*>l;c ztdcXr1MKqFe_eE1Rz~m5`W^mwM8i$pTHMKj<~C68VK|3C)F__ZQ`#GS)q+)LBo7GI zX>L+yq&M>rXF;btr&~W@8FxeL2?6D zvP%6o$$+9%V+MA&Bm4dw_j6~n`2M`ao}VsgP^PZ#YCstRl{Q zKD@id#I347Z8|`7twbZwuVX_%DH|TxE_qgHX-Y$0d!CrHg$}7Yf+NZb(6A!bYfiqt zEK22en=oYNj!#J6k$g_h7T?2`tiqY09v!QqlCPb?O$!Cx|Jx-=3&wT_F)Yy=nt-pF3YhROjO^-^o7T*ao=HLKYnkXh8@Z zH}du9^3Ogq;#!a0gk?%V$tlsjoMp52U0R!(;Fq4N2aZ}lEYq?_Z0bRdt)sddK4vEj ze3X5E^R44IpV&TZIS4E9D*Ak->8ib`eDtUwGvFYeb1S2W7K(|@Md5qB)+~H&55I}X z*%n?b?OB@>p3-|74-9|Oz>J`}R$ZP6Fh9)#9lbguU23{_-*uP*XC%+U#A4{`cE4^1 zBn4=aIKOWf0b zV?unQbt=a`{(gZeK9X7`dQLQvLY>!q@Tz%ZX#!LL2gMr1lDV|#36Ka3c)1=eIybqo z0D7b5zv>NKKnIS_yX25W*tJwoz_Z~*c$cBgH$IK5IDLJ6_1Cg`pd_+WnD3U*-e_Tg zPGL-#fWmARiOPAYyYS0y_|?;c_1WF- zX!)&ffG(Y!0LgD?0JZdYur?@jOmCoy{+%X9Fvt4mf{8oRTM8R>_3#-9?lbVYdUzkm zp4p(yLEVB;;M(Z$h$0g+Y5#VfNg3Up?~O9pj7q+<$1iT% zgf;BNg^XKVQj2~=Nhzs)g*6ZaoFyKC$iS(-Ic{J`);lk~6x0UlIx9|FYJR8UIUyDO zOBZT@ZZ9+{t)eYDu<(M<*yi=Sytv`Sqlzst@oz^k*95im9$sz|#a4Yz{u+O-LoB=DNQ^QQTc@@|T#(~;cCYm^{H7YqXT*2Q-= z2AhteDYm&eGaVBtsRF4wMZGM*GAvSk6Kx)>4cu6r5|Ptb_Qb+M9hTh~TgCGaIo-_D zJGB08AohutQAP>{ja0fU3;Jv7G%MmJb(M1qMgs7% z8>AQZ2b8T$GdqZmoEUIlt^BIyJn^Wt1m0y-IOFJye|TyJ=SyF|Uw4HcyATzA zw}L34=oZ!mZEZOE`SMIMT}ytudHK)x-SluxQg#pNox}jK-&cQg%`I7Hx`Aiw}Vg!4toP2695jafFG4pz0?7NS27#~E=lR^u`&IPyn z_(DUceR8{2;d6F*RtN*fJWghk82@Va3`a-Ibfm?W*WKWxRj{o71$yvl_KuraR6+HJ ziF|{%qs&*Ms?)Pq&jcm)i>5~GCrmEv!c?#TK9-#N^5mNiM~hg*CuVpCW9NF>R2{;A zYf}vbg=r&MPe{}XbbjJG{>l(5*pGS7hi4$8en<^5bs9%3D@Mx~vqccgm2Vw?CaD(! zHtWonoA|1(pm)sbP;$!Gc+Q?Xn5olJS2wa;Ac& z(AYGC;Pfi(jba5|?ZxTR(|Oaz+5>F^1n3;|2lQmo=1Eo=ywyHhC$pfkG&vw_AEWDb z3Km(k_Ox2gC$!0h1$a5P(G}bmgro@#V#7i|(A&)QbySeITK>xJ{N>vq)cv-zWZ!)M zETtSA*ydM}70HBOP&eNjmwNK~{->9*#5?#Q-m$*5qIe^`fh&yqv=W^2^pKgsf--SR zC!)^U(5DSmda$Xy&)7-0GS{WpszlRvR=cT8lB|*DR~1r&kd~+D93sASdEXuhFew@v zvk{6Byfsw_zfm2ogzv^3y>F0CAIuMMA5r5}&bS}|PQAPPpyc%M)m63J^Av+6+6=}` z>Ql4uRMLivyI$OUBdAJ;%-mvsq}vRF)dZ_-15|f@;B>_bD|`_SrG*4l#C2@FzETH7 zb&DH*`ph4&L;Nl2t3Ujt!9FwfBz5Y8*ol5f>#4F2Xr$ZaQsrgCcKC}`y6#hee2Ws) z8r9+SEcpIb8;CFQ#)tEG)TqoJMc+H*-ic%|% zk6OWm18#_yVw<|x z{5@N1Vqwo2&uM(${+$H9#~@5%tzEfnbgR!VAX_t&4!tcz>_iTh<^;PnBWzlM0q2dY z(&*>OC>^u14~7_V3Vx=)|)FX?||-N#wG(DM#Ymyv{yseZgb?4m>4hii|~d_C)j#d`P_>xV0*=^_dx4iqT_i6V6Y-@glQ&%K<#DlSHLE^RmHf!LFz;XEUfn@13%O8J6UF4U)3!KXF#9&F9jL z0GfP@E1jyc)bzGkcI>o?f&PcxmZVWw;tDMyckT`%OyBR z4rVaw^FcaM+M=R?AlBI3?sAAL<;w;jORUi=+N)#MTfyJHysTMz_v=<%cFb2&ej+ua zqpCd?w~4c*yt*$#XEd<*>g~*|hMpg=U+bk4Oikl{h>Nr~l}1*)<3j^A-mvF`AA*Ce zs%Y^b%o>3K(c_>CL3f44Y75v%NHJv^*Jc*k$#?fEjVv13HLGdqW(%+Jjcl% zZ?ib^MwdBSh)jsmaZV=hxgfsE1Nu!}L0`$3YF_Lm|Koa-l8Ia3X!lbRi|`~6`U(=v zn;nYpv2;Uxh+VFg#*!Z8yJtPp zEAXVUB_qvyK|Axj_f48t7>$D}=z`A*E37YCDIE`yXI9d!T(h6`P^@^9bB8@9XW?T5 zC7duUi*Cgcz$PH~;f00DRG+Jd$xIkBUsHOX)|-?)yZy=^Zb@n#+KLM&mXoPT)k3)Z z+(Os7Efo4`JbByJB-_^}@56X`zdw|O ze_MXP7jKz)wc|wR@*7N2rQm+%_JYeCl@jIsfj{+W7avsH3K1rhH+{rKQ)&#AYX&m# z8DWNJqefIhHxh!0%aaD%5S_>^b1&gqiZ?Mni4hIi`Zi+1aKF?PKIO#614~*k%xt(ydfc z(s4|w6c$JjO%O%UKM&vj3E)R!Z5T6UUY`Jt3`79J{<-l53m)oyQ>7rHD%dLf`*hH1dFe|6V_zgFW%z^}jcel=xqg{N;!LWrqI?nITCTNWv7H zBwo{NA|nHk6IJlyiP{#6Bhl^}@KiZcoNX3X|A-yeJFb<+iU${KE4 zs{Qjc1mgiZM9ig|>Uo#9;J^Q4jY7&rjS@ZI$669S1!8#ZWghjk|K1cNLjeNt<6hv+ zMOo^USF98QF%6nsoh#o3XI}rkQc{t#Tu~MI5w#Ed_bGK;XV+&2X|xBhuz11Yj^9uFJl ziLIC9OHY9=mbN$Sr(|0v zAmG7l%=`D3+&b;&YxN&Jk#?P#I5KsMCvDox&S1xef-Lb=BABTXFNNbxTsDPMJ!^MZ zvlIsN-|YV34cPglJp6h<&;93nl+0S-4O{MCdcAp9dPrY8J~MA(KNlVkH)82AhNp=x zo^^;4z57eapPI&Gyn*dOdEWa61iEO@Lvx>W!a3f}#{GH2220mYT%B!Qi`_0`X^moT z*tHJvLQE6=a&u&d|9L+_qH8WWWY?G35k8Z*&{7Na{E47&O@))4A6&081rDpSdIwP4agQUGwkooXAvLY@4~( zS*idj>4S4)#}gN&UU(_c@!)#jytwDDQYLYb9RERl{ZUKF9A`j5I|&NJS6O%TDBfR& z;;J8wuHKAo(+7r%n#Mj;7ce1iF|wyrud5}p>GStDT2n{Jsm5al{f!X&ce?vgufFXC z$UN6Kl}!XcvmC8_J->QQMp$f8FTQD>Cy1JsU2I%qpNM+ZoI@2~inOf2G$b2ty6Fll zPG2*|tPV6iCA)MAOw*79%)x=5N{birTkv$=Z|;`Uz{ZEnjxpa46c7W?+rR)0TH>U0+77ZK-a+8(G8yxl7sS}@QIonnu z6Qt?u34-munYHJ_4Pe_`PRqUljsdt*4qw1aUb2b<@iIU?B3Ir?ST_5`-noC_2^ovA zI($t>%;j`paKN8{S-5~TYMS`?UUT(3+{|}7 zS4tB|Q}XXs_3)srn#yao2KKw!e7Mv7S23Xt;yg^he%x`!BQsr^$<}1}uT_Tpb`Xw{ zSFU!hgO}f*+}?Pe+<2JBL$~#IUSsO$#LR^4(j5DLg$x8~*N-l5Y>JEQTv{uU4YOAQ z+E^@?5_V};=~moNto`qMe2-R*PCBcpJg>q)RQa;d{s_@JG*u74zA72ZcKE;&1kN)Vm{KvD;K5MVN)?72j978f2_p09wdAT;;?vdf& zT-XTJ?ednyf87dRIX=<&zqSb+(li5oiTl(Vzl?3mAEP;3b2bwF`z8kIuZXjZ_RUEg zHDll&{he4w6Xu&O1lLj-)35()NWo(dE<2#jUOZlY@T$E@*7!b)vwPRoW1D($B#!`=NaN$HsxpGWHdwI6V9|? z*V*Q|1d|}Jl3yPlAytKNhvOfHN9_C91UztN45!gRfMk*4??q=Be9V^0GGTM}9D2R; zlmC+7;!JSLC>X}0$i_5Nu|FHl6q2@bqwCjScso|_REzM%9~@?9l(tGn|FZ`S6~^Rv z-m8$Iv+l)?F|bU!?M$RhdOTje4`aU~8OEUid>tAn#d>Y&10Z!S2n+ZRX)p8rV|C&U#3eh&RK!iB-+66MHKs?xzm+ zMiaLC$)^Hpjkfl0EIHGNbwAWaF7?km#}kL!xwAWqa+OrnCm*ZVi4riaMRmn6$Rlpc zRi{eTFSt2c(3&)vqnbuESn-+}Rf8A{-MIUc*WL-)zs%GNnU)JgTS6f9TzbOyQ!gQMma)uOQnwUS^In4 z6&NNK9*K6iG&+A#*-bgOTz$WXKGr~-L^5v-8dK+Myv6$X5DW1=(;xj83H61vS=`3S z$>PN*T^8iEZcW|C)Ed7@V#{s>S6_O6fA+6+LKB&S; zFZN_?8L``8?eYzR*$#im@=?Gajddu#&&jBc#bS?|W~xet0YsEvd}N zeKKcBqtUwiy5m7{yI{E8zic&PT&T9!o#xo}71XP&8LXRNaXGfmlE0gV=l{P*v(t!Q z81Gu8U+lI1UljJD^=qjr#2WeM?-@gG?R*Eb224SCtkR9HzroefCC8Zm{+LUolm9C1 zdtgjw=^bc)E%?gxZySo#Nx5aZb+Wfr{zRjtFGE_$_Sh$>C%Tm9pDFm~`@eGURh*Zu zr^a&JkQOG#NjKH_ey_w)oTT!MeFh zi&1Ou@A{G>H!iL>CLI+NmWNSy4DZx(3Num!JU*Io85vbe?TeXuT}!9+TIZ{2s^nJi z(l?9OeS|Cy!Lqt96*fj62!HdVsBJ3Uzx)_P8pAD5>veW9Z1Z$VUdt!edTFEN_zBR^ z|LZUHJRGy}Aa?wItr>r)%^{^wnwM);-m;MBfBuf2)1)Xk4GxFXDl^|?_k7kh%trC= zuY>COdE9Fs=K+i6r5oRhGx&<%s`YsJ{pWAXMdRVvm)^1AaHKV1Pw^+MMru7D|MRim zLrfc@#L`1jPiLIu8w+V>{K)^gHKnsq+q3mC@;75+&cqyddn5RfUI?mn|IgQHHBa5$ zz0CM;$DtbSUAR>tS(Po4{@49i8YsPiyhA-j#}ch!HsW~^)0+N3ytjp z{`=lum07OhDL&8Sg6`R`v`q7#M@vOhbyQ27Ft|aS@OpScY56}dgfbOPb7i7y zH?fSTvg;=M&Cl=E65{^*zD}%>GAr+M!p9kfsG=W@{?B(Q?@?G9=h%)jd5QZ9{qOfA zn3}u@l0RbBaqx2f=P7adckG1N)Dp%WZV*2c68X`76;#d|3X;TS4lIXXr9`k zD!S8TDXO}^X|}Re)$i`Y`bJ^wW^=7l0gVH+(A3O{kk{ z3HDGtCq%rt=O1qnr>cIZQ%rfrcK>nZhu)~{WZ9e)`J6}PC0>o)SW+&$T+S6uW8)YO zGS82jgW6QT^~>QI=%rFKlV>+l8a*c#%JiCm>(E@dsw$h>JCJ1(AC@K3!>o+``h*hL zm-z;pi<|qeGyTv}7bn9`(zWYdq|xFvl=|}&6mC6s z*9$y=C>n*3t1witHOs8)TQF)gITy~y(o#DhIahJpSXp_;cXbk2Vb9Q&eTjjAu{-c4 zr_ga*I9K(M{~L$7*)B_&*(nK#Y+|MNpS>cweOr>tb}4@7{5}k8h#J! zuj zem=_0-H>Cfd*aRE3lhd(l;>Zu!fhvczlO^fnvI2mW~;I}f20Yo+gYyS`R0@x5_A0p zmXwrKPVWBEk?8gme{Y2~g{x}~=v@F+jyF6M2w>c4Z>LM$w4AE(1`23g75mBjl~V&yqUsJ6g@?;OvtLtRP&{)VsJj#)Ge|)AiFyX3=3|)N5md^zsDs zqkj=-F#;jdSh4+j+54%Q%)Bbxjq9uJ>C~B^`g-yx#?xDuXNRm+Z$P}|nf@P85Sbl+ zSG>S`aWFuCS#@$k&8V_RCUCYwR?O&UoUA3}bei%Pq*t|CLT3;+yrJ`_^Ssh!@Oo;+MEtGDZh`dV4kRf`inyv3Aoc?_kNFH%C$;b-wbfP_n{GPiH#Jr27V%10y**4 znh)E4pV)ww|6SPzTdqd+O`vWK!vwovWP~2|^c)jpG43!kzv-(2d1dm(rchP(%mDf* zPI>wVAZbM_9z>X`6%`Q?l=kw?JJ*TNpQ+&o*II!s>0OCIrJhv=rJ*4WP-50+>uq0L zohM0f?w1%{U)3H8ZjU=kklPgx`rkJM4PI#^{ZQFVnaw;NELi=r(%POrKX`84ASkzZ z`N#X!Pn(n9^O;|)E+X1DYvNE+27=mAuYZ!NUK%9ZT_ZMLtA|?f#`gtcK?h^uN8RQV z0Vh;RfIX(CeWSsjQvrZ^p`7Z4KnCMgDj`Q7vFY2C)p zefZ=#F2~)lK^|XRxE^+ZD#22ZmQkVIH#*=<;c~wyb)P8EvIHqP9Z=^;-E3^cJpK`8 zv-0-45ufwHvr;4N4@ZNh1^mowylH(#Mfx2wB}bbh1q66^P7^NK!Ax1+cOg_NssRDe zg!hK6sg(bW?}Gh>!rgY+<*1Js>A$HX|5nhf2{Q}az#DWNDivbVPv?n#_HAQD<1v1|^1(_OrB z@%VB2XpwHHJD=I8(C?U2jr=Nl0UCfi9&Pe|?uxq>H#6hw@3#<1>-#k_f(Cce0ZSCa zWsSM9*~Ltj+h>G}YmMh}>;r;7rV_7fK7wcr(4kaRSEmXi3)-#p;y3x+{avW-b*Tow6)&GGxd zxKDthEA1cprV-B6*_nnWkmMB8)D&=atDOaleFORI{htVH;7Kt#+V%7M;ST;+o0MuK zv9&MOD~owD($C@vyp3?0(31ToS3t?bs-!EoT5lBlu32SLD*BlM-H-R4zs$gsq0x!e z2KksU|3I~}xCimFqvH+L#`0VrTpFgvZrh~a%udybSX!oHm_?P^-4R@i>?T(j>-sKVWc2L(Yf0cOZ@vGfGxx|E?e1D2oqUUS zlaKkCK7(?8^T3-PmUGG~psm#Rsi@%6U1&=|Lh^nAs@$zB=k*9W&xsR)M0e-123;b# z%4r#EYyYuxB@XRI!ki;$BO&B>{vjY_duqd;ukJ4%$C1}5&;riWOLzB=pw>M2R=t*3 zcgU4dQS)qTZ_%h`r!fwEx$NRDATqQDvjnP3IMW{27+~A+5_sd78t@;wcD~I4#wTA zdywyleSy{UB)He0Q^GEu2iC-{ld+O3Wk6nT$!lt(pA`oqGDcF0U&pzfE7CSASO|Ca z_KgHTkg8&?`DXA}rCLYJps$XQn2U!BO%o)(DCZ9S#qh(weZR$rT--gKmkGj;m8tJ? zk!~R%a7=)M(<@Z(I$8A;^vcch;@Jf2kxC5fJqzkJH5Xsxl$6{D+mv&Qg-N{v_@onC zKrcO;v}b7O!J|hFK;Z3Tn!d3w9!$J8;{Ewn)A;zS`uyZ%9GByVc;?rG@+Hz!_LljE ziLq3~(={J3Xx=2UxLlL*9tAiTL&t~&IFoCwYzBUo2ddQ$;bQyy$H(`Bo0>x3m3m0O z%^D=+w)wHbqE+u6_xquu`CmIuzo8;sHH2qREdA6VN*N0JIOIv6VN|Sy@mY-EZkHGe zhIT}!JuU~f6$a4EX*aTJ`SSZW3FvZV(GzL=Ow=WOG#*I&1$`x$sD|{fq-%pS6P;1& zFYyhIT))Q)dfaBNJ%|#J`0=(4v~jeOUt%|LejGY*5f{7T{o>jdd>ruoSWJg)QN7qA zL7`yo=g89hLOX5yCjL{^0PqdVq^!h2rMtBi;}7o$CLCc93i%&TorCh*cJ&j=su76M z^wKv@MAOzMZeHL{J`KUoj8?*zEqIyZ>PnJG5Ynl@KR*u@?d_)a^PO^ z9W4P&Yix9(Wg_`r=fP)fdM(!^xOP11V?U1*Eht{YNv8h?nVpG=3CMHH4?9qVe_fnn zm|nanr$BeQIz!h|D$j%_ko(I~+3_u@Ft$GZzu`)u&MZuJ0zqddni|_C2%PSuGP*u0ybbUA_ z^-2KAYA5+r_{*QgE`!b(T4G-xA3s(W9i2I=rJz%OClFtHM4ZH94;pYKjT@JypOkXE z>+6$#e4mJV>H!Bu%uf$zXCGP$mJfSeNS+iiSUiKbTwGjgc3aMY3mC}kF*MyzRm>*O z&_Jokv&68e$bOyNX)UWYSz+puG%-obQ(4QcTnbgba&(ldGwIr&)~Y{8nUhhg4v$7= zLrh&AZ*DHtYVNltqgokZp6btD%gjC_koe|J4NCn3l-;maZUX`Q2(#&EFFCpE&J%}C zRbMtkEDBZ|0{7EJQ%vGP<@#U>bYQCO!F}jHnBAJK4jgcaPILOQ$&29OHSWxMVOnUB zY!f|D#M}VG$f$loj#yCL-7>(PoVhciWoorG@kmt6TX!V?@MNT3^HW-ympeHy;({DY z|H>|``#Fdg?ms1A|G~^*uIfKJDi4tk1P3n|V?!58qV%_4Q)5|J?p|lF6yJUR z6oeVEuq2!#Cz2y4Jo_Fy-HR%oe{vRohB;PWKPW@N`}aHzb-D3i7QgCU@|&;r=aG3! zz4{v-5|nS=3c96z$0Bq7DAMReP){RoFW^=J)|h`gZRelQN_ii>0x?`)eA3?5X)qPR zdHW^I|7*PZkAT1wCMKWPPrUluZy$w(Snch74h$`C4i&Ti5f~WevX38NYnvjUl>!1d zwK*7kBq)trH#{_OaAhxLcYQ!z)u&BRDW@zFo7ATV|=nP_<$n3?k-%EgDsJgVXJr z^3F#a0Y851pIJ92rT=cFnLXL9-p9Coo8w)!@!5gDUqDqdxv#hRi)bdYE}oaLDL_Fi zZl&?q<7)r8OgvXG=X5+!m;e1{AHPQHaRtOEH^Rbbz|_Z%A!_!^2OqK0 z<@|T&Uj~M#jf2qzHs9cuVB{i;NoJ$LA1Ow)(a|(K_9ng!Ugz3d6VV4ZdZ67$wJ>T@$W=TFl)8cu)J$7M%*@8ie2xcFr?Qi6n*mTyXp2kyZ% z{>oM9S&e=vrXl3&dhgrw9Oqg{4Wy^;M$pX{>b29P>a)gTf&w&fT$3kj)oa4ODK9b} za7YWi4KX1YtNI8m+EzV=<>1q;Ywikknn> zvU7)x*(JCjemCrXAuspS;3q6WM~P>uSP#)@EC=qsdXuLaSQDY(wf;AR>^A8Ay?%&{ zbmXxc*=Jvy7>i+u@hwv|tuz_wpKyZAD>c`w-CW;>yXG6acv?Q$>zwS+(LM!GAqdi86LMNmL!Ro} z=m0y2Kt2|kUoQ|Wuu1t@tgQI~KT}3*b$g=TW|0_+)bE*dYi(t|M&N_sYP;3)NR3RMI&bv2W>s1_uz~gpcv5~tq}3pA=Px9 zbjyYhnmh+ZKUm+YkGbS!A);k5?4|;8eZhkcXiV39ECh)J2u7$?kKn+|zGX;nV`ukE z&9V!$wCcZWm3er4Za@l_O7B1ZcJt3!o$K|b$KxbsgxLoZ5dtR&fGg!#k2Wzl*SFtp0!_PdF)EJER7VYk!eK7zveKZB@uug$*2*^^NH_OX!d)x~2t zVx8Pz{fAixLzRrl;*zAV`k2TPiy6Hw;LY!LMeVk1h3j{akr^-@rkY)0@oI=I zo`Y=6Nf_s0|JWPHyA#*+3>g;3q82AsDg&IK}B!= zSbwU{LNCzT z+9xpl>#bs9&7cdNIfgLJjcY@K$=37PoWZ}FAI|`fdh0~xJ}3tX1`$6eBX=z%953I- zza%sL1TH^l+nu{?Kw4zbqxB1fKK@MZBrZWF<9@haePwy8xqFqi~b46fzFZUPD?PkpwyttpD)Fo-(^$v zUxvIpB2ofVdUfUkdAzN=Z{#yWS}#ruUTql$9tVNZPt)KX;6y!IA9=+d5}2b(?{>U} zj0oVd;~wxmkGcd$?-(Cap1BJ%1~uQ69pqU1>0NLCD8Q2=Xx+XgpT9K~cwp)ySfQLT zU}R-I?hr9i`Gv{Io~zzEZY+52r^)kf%ZY%wc=Z92>9kvU_{*FYVvT*ey|F56)x{g% z_2N~@e7^FndU|IUqMsU<2j?H7V|u#WRe%3>bSy>N_vq`m9$_Vhf;P<|DWkVcW>X|t z^&_LF1*S=^ON^;k6Mi)cO=C(+v=s>lsEyk-piHn^dq$jx^c*k7M;t%nK@{$^} z4?r;ArljdKdmDJ1S6dM&@lPGH6 zWI!zfbp{qtdLfSWO&1R#)fsjW7MAWUHP*d+m)1A8HbmIRVN*$JK3>-H>!aJV4|`1r zZ?u!AEG-S0kAK?$dvS4*WW!p%NP!RuoX4Kr`JRMx+N)DTWSRKUz?hiulau*tHq&~W zK>Vl8N1KieBA6r`I1sak(YiF_sMQ$0j(OISJI`tUSAv~m@OZhC$Xc=quzLz)Engtq z0;z5#1M^pqUL=63jndv1328LXj&VoIqxVob{Ub2=(x!6PdE&3!gMQtD`C;!4d@_0Y z>=zfa#n^5Bt#97NOMh3)u{PhnF+ehvmU>Jo1o_IJq4KJ4{cK6fB-}r=N!A&ca$+K} zn`zk|Hj>yI(Nqd7`^+-pW!3RO#=|%|Kb2PPEMG!o4(sLI-zhJg><_|VV>AQC2rQKF z1G7dHz}wWxGg}~XS@BaDQ-XzS-`w4emE^jYXf*Z-CCUGrvl__UVx(G^TPV6@6HQlMupi3^c81v^3 zy_njwSHKwj0Gu{@N~-IIM(_Q;a?3a2mrmf&1V9WMBAb>mThy9Z&Xto|oXB5IN&Ca! zoiTDZd1y^E_QiZ_X#Vb%@KW)@hlvd7SA7*VX78kbj8-H~-Im0@qdy!|>ptA_U;x$W-Z^Y9nHXg^QI^n!{({#-| zpDA)(o&3NSh&LeJfYe)8`(d%x-|xqj4mL3*!*lezVjEZJX^ssTpFF?c&$MP+<8yuS zts(ORn-iz`O*F^(SWbKCCw+yoX>yN_W+pM(!8INuRt=Vkbo#?0C>vbVbb zT#fofmE8m5Xv}CjhR(L%dA0<7KT2kKR#5JZu_4WjeJ@&Ye#KQ$-Pk_RWOO9&o;_PB zA%HeSKM@kZQZ*G56tgbj$M>i*I6M;#-1pg)EmR?&xe0Z>mX`IBdQ5*o8_cbbH+HbF zSgV2$Or?TzfP;IAP45oMMnXag9UaSQ)xA>H2`+ooJaNp(i4br{m_PySK6t4u!+H7y z4f1jpvpEIs&s4rjNHt$Ez=ONH*PUgoF8zpc1FGw4ASz0*4^3ZmQ2 zk_(HA+r9VF-=7mA+{qr^x;g~`J9u)tT(iAL+81_!fq+U$N|3~vE%Az12^Z!?y=jSK$wL=8Mk&B zsZ{*t(@#o)`*CH$j+u;#LgO4p0?F8<6B9_nLM?|*;9S2fCYSg-M0o3XO*2?4|4K5q zm$aDGkeS);gI&Onh3N2dni?;!nVAi}(p^`ZLGH90#94CIh@zs)kQq`Z|F7v4?xE)9 z-XDW+|0=IdQty&>R>xA(hc3^pBX1{X6x!N+7Ega+?SQqxFxikzN7t}Oc1_qt`jrmP z=(!orP2SQR8w>3iEJ+%f`LmQX)MJ8MHHXzTC z-Mbb08cP@lx}1Vy+-Akk50QlaF6J0<~zQs@M@Y zRi>uu7|}<3d~oXqFgoTAU%Wn~WoaH#y!b%H$yuP<%0^uvx2o#>f@#c)lF=-D<;6Ax zF`a+dAr!g^=}IN7VH1eScCXGYf29y#GHa;EvlzVUlths!G7 zM1h9imkth0?w5zA;fpz#u{`#qa1WVl!d$KTyBPR2iu%>TB-j1dq{PIZu*gi9ye94m z2xz8GF=v3phan^F54o(JChYpzP{CoUp|-oSQS^`tyR0Q8BYRKN=kc?uJF(Uess}F> zh@d$p@mY05G8ifo*1-H^p^I{R8=KOVU59YmS{d*zP{F&%eq^_U1g@U3Y){132|kGY zA8pET-|$Z0=OgC)?D+Qn2?zGal2Au;>GC^7K^{c(*xk1dH$s+>&!;9|GQAWU_oWDD zuv%rF4;BmDnur~OOqLOwv#59YpP6yWTVzl85w0uAq~LXwetO>;Z-P=_?&s{gk2;>O zO*9w@M$Js|Tg@M+cr8}lVPlp3ru_aHTaIX|MP-_=uimo;;*cjd1+p((X&LzI(-XbM zB=z;rGguSURQAJ zusJ`3+JDv3@^WDL)Hh!g3<2b~!)&@&7v7_Vvz&pNAaZscYC=4z>A}eZef`Cm zb{Z`xK=~V;HhOENLeLEg)hr)^&Q21w{ZlEeJzFMnQ*64sqxj3gNM9iegTxUbAuH@| ztlj3LI`)?6d8)vxB&yd;LwPi#&2{cXRVc#CzBa09cVh%Uo{t~5zrMV|{whz6@~Vxc zeR>>BO^OIAk@|991#XSkH%f*+E4S%Sb^}?*$M+IcpQ?QO^ODkAb9DusfZP)AJ4tsg zZD%JtVRE0;fnagTj#oYR2I==#3KA$X-%E+uZeAI%*AGI>E?_&n;#}XmB<{ub;Q3bn z?Q$k{@rF%1ul4XZNi4+4EMnT0zDM_4h~3N?$jD$w*i07V+6RQE#;XWSjBG8N%Mm7G zuMuqNMI*A5P?gW>kD0==Ef!)_ocyW>xo${(__>olu-DWvU02vj>C>DmY1x`Y`w|vL zrwTcc7dbs-_+>@n$n23HK70V_v&7#E?eouUmr7FI={uqeO6i4=BlM9Y(TrHxO2FLQ zg4!+oqT9J3EgjuCA7gX}i*bL`(8<&6*8V!qNaQ%1N++n4!8sME_=zn|{BHAaf8Ad2 zQBF=z8v*kowIq`ykVfql6seoNIHvS7WpBS?ZV!~<$wAcGkhM*Xe433>wledFD2A*T zW@Hf+WvUAXxQasy3#%WuYz-*4c4VnvCV0K=!Y+iWwjaM0x@7B{RF&x|@ebu7?-mVS zV&-(}@P=|RTk3>o?wOiRW#-8d6-L_wsj+=X!3w7(E@AzBeQSYdNbk4J_<}VR^|<84 zq2i;CCA7D#^EBPRpHMY;_mpscsI*9>fd~ma%G=VC{(XnR)eao%TyUfZ^OU_jNW4TH zS)T6ptg0Y%z@NH(8!4E_o{`~@rh&PX8_(gWH~TJHse+jOv3D2*4eH==2jbU;-oF2E zK+8(K=7a&0T!3(DN?)aHNpC=bX=Hw7#dF1Ib;{;vo;-~;+YXyyw@9ASh|yRko8fO^ z@+{N^$WY`K?ce;hJD-V55bgH`XWs3|Q0x~f{5)oDZ-(i4wdwrGozq~{FvMI z^%B$dhLD3uMUrmbGq^lIhO0L%tmw*is-+i$?k!7AqYUScCHH)~`Nugm_uZ}_Ch2JF zndH>0(rSsxV&x!+2Us#{Ufh^+b4;mwDdTh^3m5SwelQ4Gs5{){Z@L%B_Fjs*u7^;L zz=Is}x*Ci+ZxoR6jvUa*SaE8T+-?HaLUA=v`>vuaOX~4PtM}9Kl#;nd2k9M*$Zig+ z{csuXW8;UEl~Y4KC(+b-Qu?QxBFAk07{uS?njEF+B3hVj6ltN&t?mTbUN!dBnlet<_%p%yxy{(?dIUBsgs-#YcNL=fitA{ij1eU zSvPdD_9QVO!K*nidaX4wPM<`_(vq1zOF>S~WS{ppG%Y>4!?d_eFtc6nI5N5`FKJLzduq|)pLc2f1}xLN>K& zAB)u&Zwcc(7pR>^|Kyo_Lj9YJ=Tpf#=lpNx9P0(&2VA8aXryP;j*3_MG?QO=zBQ!F z>35pUQ`QfQ-e%)v_}1h69D~DUyhhDfe5CU1ROkuYwf)M<))N(-XUsznsojfceUm0_^B*UnC{oZWcYX1}0Jem)Zv=KO}1f82&3 zyseYHp85#twPN!b(j*W;o7h9a)1oC3gzM8nWmwiLR#%A(BDYQYUbv=#7qqMSZ3uMk zkm5M6Lt276EWQ&%=nmBvg?i0ACEcl-a_R2Mx?00`jbmfCw;t3vLTt34w|}nQ+&a=d z4RFXcEHGj}g`R{rP(4F}_w^$BWv2FwDRW9k+CZDRD4jr!^$siR+i%K9 z(vr8?)3fQXZzU|I?|+vH$ufQPKvSQROlwt7#j~Y*tUYxkKx!^9PT^M*MJ#5>y&Dog zKZ@sN0K6`Cl6GwPG83G;+(iH+06}N8J0tqdDVOd0pbg8#^KHbeMz_@=bvKr6*~Bhn z*lDB4ICXcdgrLY|C}~K*LS3CwDfivu5BWC4!H9F{!PAn`);E<$o8&Lz?&#~Wv6U1$ zS~^8;E_HrDc&3dpENnYRQI9NFw34Z|`3^P1hBlLRS4v z{l={COVtUdT8@ZgL)O-xrbVgNf%shqg3AjFerJs*Cr--kg)9aD#WFA-0fbavg$~Ep z2moie9#1l#c|7RoB%0gWLRD6_Ms0W5uKhTiCvrHCz*W5~e7DE-FH?rBqg+BUVrrGRzykoikf_UI7YC45W%Yl z86*~awY3iU#kl*t%F? zRFoh2%)hY!F^u)p=@k{X3Tlt?fB1?5Sj(V0-o)y>)&(2HDMbXauq*-~*`VrnMa1?b zpha6-(PjMBt&_lFgD9p;&4^9iEDQpsz+w?VKjlR5l?)DMId(*o0U{_Xt6Z$%OTfCh z@%EILfqC@Tuk?SAtN}6l^^sgyWkV(iKj*4f4(FPmAFe+p-kzu&$-N9A7xaS2CFyE9 zjHz5(Q7F9A~= z7R=@1k|H_My>ii}aNy)>ppq>9!dixDZ@}V}g?!q>!Wl{P`?tuL%>D+L`3ulJ@8bXE z*wo*)%;Xt%6-AAldq5UTU#E}mk&z*cjpk~`> zk~#?vivZ~MQqOQX=I1tGeESMh>fK#8M(IfToJMoqz-QeZ)$K`h+zun;vAgGfGOO%0 zu6Six`TKVl0eB1;1a%ncdt1_Y&CQRYq)$gG=>9jiQZWL~Bp_==UGCPnmoN{-SUi4< z2YD(b!t7-nE*TR5f6~^PMg1z`OBwc+k|~cJV*fsAMdf_;Cf;pV>OV8jl-=ELOu3&^ z^%bXxped|2XlFq-aCJEb?hDusX{{*;v2Is6ZmZ|bk3`%hDf7Q+)^f4i>fGnlo9v$Y z-rLs~E#iSdW=2LvtYX;))EI2|ulTkNVvt+V0s;PCr?kc-%e{6j*@$H-y}4sz>0Xuw`e{mvYVKFXdVT z*ZWvl#V7GwAMZXMG5p=4;`&;5PktadjMjQda~30fj1hB1B64tmI4g@uj6qg+@|!yk z95>isp)A99eR2QH8sKSlmq&&rM~buyPP@&Sq8*wpoBZlk)Eax7fHbvTuKRA;_K<{x z=8llU!W?Bcdz~xy--VhgyH&sPX?L;GTmXcy847q|3%t`CPS^m2KXAf+`QggrfHz&b z)r39G@z-T(sY}hzk1Ke zzn}5Oh%VA;-cHqzWJ`#ULgBG9${{yp|4T2N5CAlF0w?cr!I|>}AgDrnNToY}${ztN zEG(zJc9m59f;t}ZqI=WiO1URw=SO7yp~yg>cCeBnUe2lC9slx~+34giF~Fe%Qi05i zNF7lnigP~RV!HF}0Uq957bgK>m(u-PHz46Frnev&*BR~SXA-qoj(1-+?@Rll5w{LH z>)gF$mi}M0rOAaMhW6WitDP7%M!|e7KOgU&NT#iWN0u_K%SI91Hd>fblK}5K0_#Zp zRqSMy6A?gnBLxa#EfVW#E2k8E-bq6mMbub$h!^Pwp_{a182t@HLv3=pf8gtYHxABI zbEH`9mZm2nY5}wohs%-O#eR=Fsen3u1R~}Hz~S!6eCYfB??pyK*(ecGkMCe$gvGmJ zcw8Q0H#TwvboT*9Ifl*9r(qrqEwIDHZNKXcgpT6ASetL<@BUlsi%t>Z<0ROU!A=a)GW7P&~!WGZexgG)vT}*$k%%F zYjwczumkXYx@&`#M#H&uXNT+Fy}dYKeVdw_i9S&z|M)>jN0;Rn5+dew(nlZ54KW2E z^sgnOwu*m4st}Tgw~>0IgMcO{hVOKin7b~W-owsv`s&e-Arl0+J55}=Ij2A%^pVKf zdWixnpgBM6Prs`=aFIpIR4%YUEqR;qnuuntWbbsjgCe~BndvY)*Ys)pH!vm%yvK7R zS|c-mf36n-`!0c&YmjtY*}9f8($1t~eX@L}Lnszqak+ z4}nGl$$Bk~X+Hol54FM({(QAm6;IzF_cl2RmEvV&`$9`YAB_Y4+cUHL;HW^Spg7)c769Y}fb{Dr-?jt;D#)Vy8q5elfU?^2eb z;2_D?D-379FRORbp@@GNF8k3o5IXX~yxzl(!d_>*9h4brGyljgB;XhuPl zhnl`i+9CjMBbss8$+`7Gub&WfRjUgLAv0>6`8Uk>#IWFMYm*%Ee?eHT(}h>AM`PE$ z$sd_*Z1=@lBRUolc+lP~dEW2olP!NDwa@H9&Psf|%x!jY@gufsp+wtv+@1Qvm;>`c7;B(Spsh|yG!>#5XSy5ys2 zHj{DKGSF4zZ$9p@8m&=bMYO+EYi^eWkUy`F$man!*&iTRii+A%C`8)*xwH_<21Y z>_RpFc=FD`z$5e}4;8x3&QHnDr;wD_)ZuWm8%Y+v%&o)O1_ym8$Kw}lXXSKisOq9J zmjZeg`fMI}T>eD8x^fo}CA)V|J^gd|V=y{?Zs%q1*9Lb}>)4kdqyHN`jgR~;9I-WD zYN1~RK(_0M^+d?3=h?i>F3J?GOsB0>B>IjUv=RYdL7A(0i3}Jblc7q``et$5O7rs@ zl}&@x2uN)H9xZAT>o8N9dYyhR`VRtgoul*=2mk0kF(=j_Gaykp2;)~ri=H+%y1z&c z``YZ_;DD$U^uejvX9MwBM1(>g%jJdZ?tV|l!I&rjTt(xg(tiRb+hV%=hg>klvu8*o z9J!E_!VYxCC7OpU<^zT+-^`>$a~n0=!|iD`J3jE)0Yo?Q{!Pwb*Pb8du(@7Exqjld zHSl8{#SH6M{0GK1QT)g!bK2N;#*lshA%RLDrx`mx*ebc@Wdu^Sqq&Y&3Mz(}FRvEF%!9Tza6}+NFw9bGRre5oO zS4D-JmX_Aj%M01n&8;_8jO}1`09m&+MCko{PnA~9!kMS+rud%=3xoXvUwO*VfFdH9 zuyAl+PV_3~JYwf}FcaupU2qoKE^EefSrbPyzog>lzmI}~l6r*;lLvSwQRt~aWO4yV zl`JJmrwa$Cf6FkccTlW=8|Bq;J^3Ng9l*g;v9VzY2uvxKZBEy7o*i!o!`s}LCjSU^ z0!j{!hn$uf$%TdJ&CShk={INQ=AgaJ@fI2nARy|me38+x zsCaqNG&K{A#>*ZceB?zws7=6mD3929kM{kzZL~8V#A+FV= z0IY8!Y11>}bY~|nH8I{Tvec*uV&HWo(U>;=tLMaNs;XZ(=>9PHW}p4AU0@H*Pp!&N zZ(wse=YK4yNB2&cr3eP!wtf0aC`GjEs5b?< z@4M*`v;3lsWyVbBx5KZ8ehGa1eeQGbDmb2P=dpxcWlNpoM$;~t0Dovw);>(B3=GvZIcu|E)NQNeXzoUsk%9dIpucF zktP<1L(+}t>s|9|inf?-TP!9q9$1i(2rua|cPlioDU{ZE%xEpH_bB%DlLTA}^omk* zg1JyquQEB4>OKuS9_l3SK&S!}PquWo-_{iVIc;>QIkRTS=SP8lQ|HKAd9(AwOIZi3 z{EZ2DHF>tSDak5!4VixFwDq}0>{qCUNCUP0e8ecJ*2=4lQFHO6@#>YR%8uJ2?k3;* zHWkF0CAeK~w7P2=vvSS<{e><4I==i#`-3~}58tk`ShxAHBofJQIiF}Fj4k@s(Buja z_!5-KxtJpU^}oONqF6z+_E^LVU_c|` zj@dnQD0D*C;EQ~P7nyxRX>uvlyl9o>{a+rE1orc-PCbXLc22m!!{eKwE&Yy~nXQ_T zu=Pke{dHw^baeA=)NYu=W8xw-Y!L(uR~?Jj&@hePOYeEi{4yi%+nb*7j%U`kY3Nxp zn3!4)cc>(|J^B`QyXe-3U4^vtXr_dOoqhVuZwU+if&N)|W~}^ttKGwowSjC<0~-`; zrtR(!(RTLHdm_V)=BR8J&sx`=R3csF&=@Q)+Z#b_arMFYoT>ETbIhyAH49|@TK9bN zuiba{uuSfRwv-uj0H8$lIBKjUlZuj3P7n)Y{PS3eVKM}&9PtledT@tmAZ@B54Am6h zmXO~73VXB?#QAU}B7)+uU;n!%t9nR>diRtj`qp@kYHLMO1=_7&%_A>aOQ`vuV8NRx zbaeQnp7l998K>%wM~)BKcnU3BhmogvW=Z~X;3Ern{f}6>H);WFOnR;V{Njbk`|Y`) zxeY$(*A&XB8=vW8_;K6L(4r_Yxn6tA!;{(-6IM~U$n2$}0M+v7tH1ux-!?;X z57R|i*=WcFPhbBL6~~m(2X-VF@QXN>vK)OVjl#s>QlV0@v)^K2(Z4(c2x6kkmVK_q zd5Lsf7@`?f_OfAo*FSdw8iACW&_tf(OzwS#_hTfutKIhC`3U-fdvB@lYD=Dwie&!R z8dq5>L^s)^OJ@$?J-fv%$aIuXi!- z3R`VI{5p;|A@j1aD7>QV7%AVEyd@7K`mq-qtM|>#Cjw^S$%xTU#sQ)dpDCVM6(8y% zVN{t2FuDk2BjG`%;DnEHLXM49;rK=e*W!-dL6nkkMvM4)WD?NR)9gZVO(Bn+x~|J+ z=3G==XEK_9*EP?mm|mH-*WrQ?-hi;NrKKH|dgT*V75hrn3@bPaFJvSuUwm}9-}0+{ z;J*7)H3gT44l*)`+Utw6L!TFvLi%*x31pv~?L|gZ24+_*&MCwt>Zj1jcnZa(auN1; zy>oR(&zKFia`D^ksma|x3_Sr+`i9GhUMNC7CqC8E6`!c5F7JPN`dTlv-6rY!3iiv- z%=>Y-Uv_)73XU`Xb2NV%nMaEoC`LnTCgcOqe+4OJ@%NbyU)l*@KF`>!=tK0{=>2Yq z{aFi;`puTmVJ#zN9v;F;CHZXDs%~PN^m}BPL9M63YGEGZPUJBGr0N=dWsR=2Uf&R% zV0tuQUP9%^WBZz$w_7%F|D!P}H4 zjEp!Xz9~|RJqb?}j*CJk<3l=P>i+pm1fJ6w=VD!pxnXJw3vh?ndBaX0^bnTJc?Xdk zLf+7bMNk#;MBWaFq83R#M<`IB@8;kH)(=J z01W|Ho{xZ5ZWX0xWnUl1fG)?gMF*J{GzW{_8fYW&T zh9VkzH4_9#fE~d~TI!m4NcgtE%P&!yIOTkvEew~$SB-$_+hLX6(cUM=5(@UMzqh+J z6YbgM;UTM%YA^Omp!7hCNWegF_T`)N@e(c?8d|B@80p84H#p)S{j)Y-Y;yE`d1fpB z;x301MRwBvsqRX@n!2`lEDBly+o~-*4b&%tJP^v%NSLdFAwYb9MhPIuOdw2#$RI;0 zSgWEW2m&U8%-R4F5dj%2l|dnd(IO-P!yrTnGb9js=O)-c;H~%N?GMRXcb&V>9?!Y! z-m`xjtOBR+X74y-z0Ig7C}7}UJ6+-6*zM`RhONB>Fxx#OkSbPz7a}K$Ra{OE4YfDj zkp&;xE(ICyQ&bF_pHGeTw7QwO_;42}pokAbe%adxl_q=-TX9SaTV71Ywa98DtL&Io zZb#Ri`Sw)$>(Tc!U)H2<1B?B`bfLP6WeR`n>dkV&G2buZauAQwBL~#2S^3`|g}@aw^I=Q)VOA@M@hUoa6AxO( zLmx&6gz{?68z`D*Zb^4z7noJf$gaQz(V6sd>uP8_wz#dIoLc%V$diSOq3{S`PzJVZ z`;nkrdyT8H#}l3*mTH0$u$C8|iG+i5zt4VoxpeH_`k;Gw z%h6_4!nQ&4S+D}0Fn;m!u$hZnyh z??IlBP>(nT98mhF!I4sbM8`71k@n(ci0SEj6r3%TxKgT1sQgp;>#vo^_xEoHv2bXr z)uCW`q%lUJrotl`99wdWd!8Q_mUy-xsjd=~U+O}qN#M-8P&PyKp7R1i#!w!J8UNwl z8qxTYD}6i)_~gi`?yrt62JAbR>kRg@t*JoQPH3tLa6TxrMGjY8VXymWOtSe)#05>8 zAk++068!Cja-7%69+TtuT0@ZHniUApUh-faaX&)qwGd{e5v5QlRGs~=S-bcxFHuh?v)|Km&NUndoF=Yl1S6a8=}>ywHnPcax$_#$!vU6Fub?URwh zD6#sL6pf6#e>6QH8UDS=$joRaBA~J7gYrQTQ9n7u}FN3CCkv6gy>{H z%xA8x;dy1%79%rTlR7P&c*Mw6Fk(BZ1QVfVnE=))&o5MR&2~zbq*+F1N+AzhaV1Y< z;b=yZ-_&G{W!B|RDbgFOHBV6`kQl{C(m*-Aa(C$78a?0^8n%{n_9_A%VAqx9N(0Eu zfNnezH8Z=W;CNz8A5i_{$Txy~skWIot<`F?#%ZfaamoRjeecp<$qld@MVmp%yWpRrx|IM&x32n-hah1ahJa?pMvtKj!3l0SqiFoaQHcqhn+K>N(G9iB^k1pI)u| zp;Tay-n*t_IHn92*;~0CKo-)EU!D5af~MUv&liRdXj3OEE-3$2kvKHnQK42_0x`ZW zcxjL>>TSCPzonfw}#zU)1`6-sFN7<>y-U9q^3Bs%wdGUyB4p7Z)0H~Ju z5`jgUxM`5p#MWKz7r5MKI{|i<@T0J`X5o)?UWHjUTGRGoUSLsia47q>e2*2Q*znnp z9b+PD{A*1+4PtezjXfNB1=wL394$A(3FuMD2HJ)tvw#?3`1w zYb2k2Yf5{MWy2f^L*(4{uxoiZe>TX1Tj#}LUD+QzR87K>(XJ#Q%9>+ETUGw%Pp{Bf zLM-Oe!mMfxo)bNo%kaB1r6&V}X&3*t+N)5{sj#`?qnZlv$@)4t}umr{@~d?}!Q*ea@>-n0FBwH7+xGaY)Wd8Cd1T zhP&6yMy`0%-qUivGweZvNb&6HDx#E_;uo9$tc&YA*5{$MiL~e08x$e}$Ud)c#?}=3 zq4@v?F_V+C#EAN#{oxYh)vcpI>R)rvC_U=IkC!6V+%4Ji6xVn{RNpwAZs=(AWYayI zP#%AA;6yZr+=l~Qz$%}l73UUE3Ev3rgOu>>VVeX)bKE+HCR1}uXb(G7Yi6Omg28OC zCH^Z9ZM=OfjW<6m+j}mhMk^qqBzB_sz2y$5r=Nee@cC2l!|+&U@JsZ!Lkw<)3`p_uqmTlpHu&GPzymMrwl z{U=+3r@rlC7&^bHNn0b>_~oI6{VH=gIB59hXE+9OS!!BXk*$N9fSY8zI$8 zQJO-|ATTGO=DE@$x91@-5w`p^tal-SrPeX%5E}d3*;LieAb<@4-oaqHeT^L94m z>yL2YyshK;+*kq*!n&`e9k1?NPf?47{0!0S!f`{Ke{p#*HN4731UJ&GBM}4tg2A?5 zM4hm98CO;H;7(&kGRch3H=s*0HkJpcZypw>iPD>_4F#g>Ob*r{Jl;t)XiIIKbTYns z$Xtk{8+ugik4*OC_?oUzhjQO@Gsj0h+wWbORsxqFvNp%;D&p2nEW7F>0w&!V(c-<; zrfbt`7eYQW%Z!-V!>J#`V*fZs`Mvpo(PDhyn->2i#>{<+sQfcG{%HWEIeioRgn7QHBJiqxXFnWfHnKC_V|DFc8KXFVZH3q69qJ@f{m`Y4>p#v0Qmb6fE-B>f z?PA(r$+7ub`VP!Tp;e3bC&2!~FhUX$OVNYk}MC+jgBO7|wSI$c}2`wm)J zH3Z4lm8idDZzmLkou#{4@jSuXoYh%Bp4uRo&e%9fuK9Zq3%yVi*)Y3bnjrSOWplFJ z!upQm2jL#e*VGbjlbvy^5`#6ra%ns#C!~f5?r#L~$(iy-+)1*Y*@_G`UhLy!dC%tz zUkaCfZdcGu79}oXxU(Nl$E7AEP5K7cj`c8{WD_zn_OzasnpU&y6Hbe2$wrzBiw4zO z{*eP$MIo5k%W#a_W7fp{&AlEzv{hAO;&Z&NljNYb|g)_m6_ zqYz6jFgCfJA!EF3RWd$r-NkY2qFKYudJi0o_newsOsh_lE_4!E{~v#BhBmiRN=;n< T6+fZ`$&Whec%u9#&!7JTg2l*n literal 0 HcmV?d00001 From d346740981b7f48b8d62ff86b86aa0b3d3183be4 Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Wed, 11 Mar 2026 20:08:02 +0100 Subject: [PATCH 241/277] docs: fix broken :ref: syntax in GettingStarted.rst Remove stray space in two :ref: directives that broke Sphinx cross-references to settings_fields and settings labels. Co-Authored-By: Claude Opus 4.6 --- docs/GettingStarted.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index 93c21b61..05ee7fbf 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -220,7 +220,7 @@ This command will create a file with the following configuration:: "t1": 1.0 } -Section :ref: `settings_fields` provides a description about each one of the configuration options in the file. +Section :ref:`settings_fields` provides a description about each one of the configuration options in the file. Alternatively, you can place the ``simulaqron_settings.json`` file in the folder ``~/.simulaqron`` (i.e. a folder named ``.simulaqron`` in your home folder). Doing so will make your settings persist across different projects you @@ -268,7 +268,7 @@ The SimulaQron settings file contains a set of fields to control the configurati * ``t1``: T1 parameter to use when applying noise on the emulated qubits. This value is only used when the ``noisy_qubit`` option is set to ``true``. -The default value of all these fields can be seen in the :ref: `settings` section. +The default value of all these fields can be seen in the :ref:`settings` section. .. note:: An application can become "stalled" in certain configurations, leaving the application to look "hung". This leads to a deadlock of the application. SimulaQron will wait for the configured time before considering the From bfb8e4593914c0cedfb9c31e7f150d4d81f81d3d Mon Sep 17 00:00:00 2001 From: Stephanie Wehner Date: Wed, 11 Mar 2026 20:28:50 +0100 Subject: [PATCH 242/277] Replace Sphinx-specific directives with standard RST links for GitHub Sphinx :doc:, :ref:, and :code: directives don't render on GitHub's RST viewer. Convert all 56 instances across 12 doc files to standard RST hyperlinks (e.g. `Page `_) and double-backtick inline code so links work when viewing .rst files directly on GitHub. Co-Authored-By: Claude Opus 4.6 --- docs/ConfNodes.rst | 8 ++++---- docs/Examples.rst | 32 ++++++++++++++++---------------- docs/GettingStarted.rst | 18 +++++++++--------- docs/NetQASM.rst | 14 +++++++------- docs/Overview.rst | 6 +++--- docs/event-based/Overview.rst | 10 +++++----- docs/index.rst | 21 ++++++++++----------- docs/native-mode/CorrRng.rst | 2 +- docs/native-mode/GraphState.rst | 2 +- docs/native-mode/Teleport.rst | 4 ++-- docs/native-mode/Template.rst | 4 ++-- docs/new-sdk/Overview.rst | 12 ++++++------ 12 files changed, 66 insertions(+), 67 deletions(-) diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index 41d6100a..852f00d1 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -5,7 +5,7 @@ SimulaQron uses two configuration files: * ``simulaqron_network.json`` — defines nodes, their socket ports, and network topology (described on this page) * ``simulaqron_settings.json`` — configures the simulation backend, timeouts, and other settings - (see :ref:`settings` in :doc:`GettingStarted`) + (see the Settings section in `Getting Started `_) ------------------------------------------- Running all nodes on a single machine @@ -221,7 +221,7 @@ SimulaQron can automatically generate certain well-known network topologies: :math:`n(n-1)/2` for :math:`n` nodes. .. note:: Topology generation via the CLI is planned but not yet implemented. For now, specify topologies - directly in the ``simulaqron_network.json`` file (see :ref:`network-topologies`). + directly in the ``simulaqron_network.json`` file (see `Network topologies`_ below). Along with setting up the network with the specified topology a .png figure is also generated and stored as config/topology.png. This is useful if a random network is used, to easily visualize the network used. @@ -233,7 +233,7 @@ The network that is then started might look like this: :align: center :alt: Example network topology -To create a custom topology, see section :ref:`network-topologies`. +To create a custom topology, see section `Network topologies`_ above. -------------------------- Starting multiple networks @@ -251,5 +251,5 @@ To stop a specific network:: .. note:: By default the network name is "default". To have multiple networks running at the same time the nodes cannot use the same port numbers. -The JSON configuration file can hold more than one network configuration. See :ref:`networkConfig` above for +The JSON configuration file can hold more than one network configuration. See `Configuring the network`_ above for an example with multiple networks. diff --git a/docs/Examples.rst b/docs/Examples.rst index b502c2cd..ec522263 100644 --- a/docs/Examples.rst +++ b/docs/Examples.rst @@ -17,38 +17,38 @@ SimulaQron offers three ways to write quantum network programs, from highest-lev directly to SimulaQron's virtual quantum nodes. This is Python-specific and more verbose, but gives full control over the simulation backend. -The examples below assume that you have already made your way through :doc:`GettingStarted`: +The examples below assume that you have already made your way through `Getting Started `_: you have the virtual node servers up and running. ----------------- New SDK examples ----------------- -* :doc:`new-sdk/Overview` — Key concepts: ``NetQASMConnection``, ``EPRSocket``, ``flush()``, file structure -* :doc:`new-sdk/Template` — Getting started: single-node and client-server templates -* :doc:`new-sdk/CorrRNG` — EPR pairs between two nodes, correlated measurement -* :doc:`new-sdk/Teleport` — Quantum teleportation with classical correction messages -* :doc:`new-sdk/ExtendGHZ` — Three-party entanglement, multiple EPR sockets -* :doc:`new-sdk/MidCircuitLogic` — Multiple ``flush()`` calls for mid-circuit classical decisions +* `Overview `_ — Key concepts: ``NetQASMConnection``, ``EPRSocket``, ``flush()``, file structure +* `Template `_ — Getting started: single-node and client-server templates +* `CorrRNG `_ — EPR pairs between two nodes, correlated measurement +* `Teleport `_ — Quantum teleportation with classical correction messages +* `ExtendGHZ `_ — Three-party entanglement, multiple EPR sockets +* `MidCircuitLogic `_ — Multiple ``flush()`` calls for mid-circuit classical decisions --------------------- Event-based examples --------------------- -* :doc:`event-based/Overview` — Event-based programming model and state machines -* :doc:`event-based/PingPong` — Classical ping-pong between two nodes -* :doc:`event-based/PolitePingPong` — State-machine message dispatch pattern -* :doc:`event-based/QuantumCorrRNG` — Quantum correlated RNG with state machine -* :doc:`event-based/QuantumCorrRNGVerified` — Correlated RNG with verification protocol +* `Overview `_ — Event-based programming model and state machines +* `PingPong `_ — Classical ping-pong between two nodes +* `PolitePingPong `_ — State-machine message dispatch pattern +* `QuantumCorrRNG `_ — Quantum correlated RNG with state machine +* `QuantumCorrRNGVerified `_ — Correlated RNG with verification protocol --------------------- Native mode examples --------------------- -* :doc:`native-mode/Template` — Template for programming in native (Twisted) mode -* :doc:`native-mode/CorrRng` — Correlated randomness using native mode -* :doc:`native-mode/Teleport` — Teleportation using native mode -* :doc:`native-mode/GraphState` — Distributing a graph state across four nodes +* `Template `_ — Template for programming in native (Twisted) mode +* `CorrRng `_ — Correlated randomness using native mode +* `Teleport `_ — Teleportation using native mode +* `GraphState `_ — Distributing a graph state across four nodes .. toctree:: :hidden: diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index 05ee7fbf..01ad302c 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -22,7 +22,7 @@ Now, we can install SimulaQron by simply typing:: pip3 install simulaqron -You can then make use of SimulaQron using the command :code:`simulaqron` in the terminal. For more information on how +You can then make use of SimulaQron using the command ``simulaqron`` in the terminal. For more information on how to use this command see below or type:: simulaqron -h @@ -45,7 +45,7 @@ Before delving into how to write any program yourself, let's first simply run on Remember from the Overview that SimulaQron has two parts: the first are the virtual node servers that simulate the hardware at each node as well as the quantum communication between them in a transparent manner. The second are the applications themselves which can be written in two ways: the direct way is to use the native -mode using the Python Twisted framework connecting to the virtual node servers (see :doc:`Examples`), +mode using the Python Twisted framework connecting to the virtual node servers (see `Examples `_), and the recommended way is to use the NetQASM library that calls the virtual nodes via the NetQASM interface. We will here illustrate how to use SimulaQron with the NetQASM library. @@ -59,14 +59,14 @@ the backend of SimulaQron simply type:: simulaqron start -.. warning:: Running :code:`simulaqron start` will be default start up servers on localhost (i.e., your own computer), +.. warning:: Running ``simulaqron start`` will be default start up servers on localhost (i.e., your own computer), using port numbers between 8000 and 9000, to form the simulated quantum internet hardware. SimulaQron does not provide any access control to its simulated hardware, so you are responsible to securing access should this be relevant for you. You can also run the different simulated nodes on different computers. We do not take any responsibility for problems caused by SimulaQron. -For more information on what :code:`./cli/SimulaQron start` does, how to change the nodes and the ports of the network, -the topology etc, see :doc:`ConfNodes`. +For more information on what ``./cli/SimulaQron start`` does, how to change the nodes and the ports of the network, +the topology etc, see `Configuring the Network `_. To stop the backend, simply type:: @@ -93,7 +93,7 @@ Evidently, there would be classical means to achieve this trivial task chosen fo * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. -The examples can be found in ``examples/new-sdk/`` (see :doc:`Examples` for the full list). +The examples can be found in ``examples/new-sdk/`` (see `Examples `_ for the full list). Before seeing how this example works, let us simply run the code:: cd examples/new-sdk/corrRNG @@ -170,7 +170,7 @@ Similarly the core of bobTest.py is:: m1_val = int(m1) sim_conn.close() -For further examples, see :doc:`Examples` and :doc:`NetQASM` for the full SDK reference. +For further examples, see `Examples `_ and `The NetQASM Interface `_ for the full SDK reference. .. _settings: @@ -220,7 +220,7 @@ This command will create a file with the following configuration:: "t1": 1.0 } -Section :ref:`settings_fields` provides a description about each one of the configuration options in the file. +The section `Settings Fields`_ below provides a description about each one of the configuration options in the file. Alternatively, you can place the ``simulaqron_settings.json`` file in the folder ``~/.simulaqron`` (i.e. a folder named ``.simulaqron`` in your home folder). Doing so will make your settings persist across different projects you @@ -268,7 +268,7 @@ The SimulaQron settings file contains a set of fields to control the configurati * ``t1``: T1 parameter to use when applying noise on the emulated qubits. This value is only used when the ``noisy_qubit`` option is set to ``true``. -The default value of all these fields can be seen in the :ref:`settings` section. +The default value of all these fields can be seen in the Settings_ section above. .. note:: An application can become "stalled" in certain configurations, leaving the application to look "hung". This leads to a deadlock of the application. SimulaQron will wait for the configured time before considering the diff --git a/docs/NetQASM.rst b/docs/NetQASM.rst index 74500da0..3eef7303 100644 --- a/docs/NetQASM.rst +++ b/docs/NetQASM.rst @@ -2,7 +2,7 @@ The NetQASM interface ===================== SimulaQron applications are written using the **NetQASM SDK**. This page describes the core concepts and -programming model. For complete working examples, see :doc:`Examples`. +programming model. For complete working examples, see `Examples `_. ------------ Installation @@ -91,7 +91,7 @@ You can call ``flush()`` multiple times on the same connection. This enables **m other_qubit.X() # conditional correction conn.flush() -See the mid-circuit logic example in :doc:`Examples` for a full demonstration. +See the mid-circuit logic example in `Examples `_ for a full demonstration. ----------------------- Minimal example @@ -173,7 +173,7 @@ receiving classical messages:: conn.flush() conn.close() -See the :doc:`new-sdk/Template` page for how to set up the client and server, and the teleportation example +See the `Template `_ page for how to set up the client and server, and the teleportation example for a complete two-node program with classical messaging. ----------------------- @@ -182,9 +182,9 @@ Configuration Each program needs two configuration files in its directory: -* ``simulaqron_network.json`` — defines the nodes and their socket ports. See :doc:`ConfNodes` for details. +* ``simulaqron_network.json`` — defines the nodes and their socket ports. See `Configuring the Network `_ for details. * ``simulaqron_settings.json`` — configures the simulation backend and other settings. See the - :ref:`settings` section in :doc:`GettingStarted`. + Settings section in `Getting Started `_. The ``stabilizer`` backend is used by default and is recommended unless you need non-Clifford gates (use ``qutip`` in that case). @@ -193,6 +193,6 @@ The ``stabilizer`` backend is used by default and is recommended unless you need Further reading ----------------------- -* :doc:`Examples` — complete working examples from simple to complex -* :doc:`new-sdk/Overview` — detailed SDK concepts and file structure +* `Examples `_ — complete working examples from simple to complex +* `New SDK Overview `_ — detailed SDK concepts and file structure * `NetQASM library documentation `_ diff --git a/docs/Overview.rst b/docs/Overview.rst index 0e045722..cc762e64 100644 --- a/docs/Overview.rst +++ b/docs/Overview.rst @@ -15,11 +15,11 @@ In the light of the alternate interface below it may appear inefficient to expor the purpose of SimulaQron is precisely to explore and play with higher layer abstractions on top of any hardware, or its simulated version, SimulaQron. As such it is best to think of SimulaQron as a piece of simulated hardware with its own native interface, which we may first abstract into a higher level command language for programming. Examples of -how to program SimulaQron in native mode can be found in :doc:`Examples`. +how to program SimulaQron in native mode can be found in `Examples `_. The second way to run applications is via a higher level interface, the NetQASM interface. If you want your applications to later use real quantum hardware more easily instead of SimulaQron, then this is the interface to use. -Examples of how to program using the NetQASM can be found in :doc:`NetQASM`. +Examples of how to program using the NetQASM can be found in `The NetQASM Interface `_. .. image:: figs/netqasm_architecture.png :width: 500px @@ -170,7 +170,7 @@ Automated tests ^^^^^^^^^^^^^^^ There are number of automated tests that test many (but not all) of the features of SimulaQron and the NetQASM interface. -See :doc:`GettingStarted` for how to run these. +See `Getting Started `_ for how to run these. Some of the automated tests use quantum tomography and are thus inherently probabilistic. Therefore if you see that one of these fails, you can try to run the test again and see if it is consistent. If the tests are to slow on your computer you can also run the short version, which skips the quantum tomography tests. diff --git a/docs/event-based/Overview.rst b/docs/event-based/Overview.rst index 512af163..05ac2f10 100644 --- a/docs/event-based/Overview.rst +++ b/docs/event-based/Overview.rst @@ -5,7 +5,7 @@ The event-based examples show how to structure quantum network protocols using a **state machine** pattern. This is the recommended approach for protocols that interleave classical negotiation with quantum operations. -Prerequisites: you should understand the :doc:`../new-sdk/Overview` first. +Prerequisites: you should understand the `New SDK Overview <../new-sdk/Overview.rst>`_ first. The state machine pattern ------------------------- @@ -68,11 +68,11 @@ The examples progress from purely classical to quantum: * - Example - What it teaches - * - :doc:`PingPong` + * - `PingPong `_ - Basic event loop with simple if/else message handling - * - :doc:`PolitePingPong` + * - `PolitePingPong `_ - Full state machine pattern with dispatch table - * - :doc:`QuantumCorrRNG` + * - `QuantumCorrRNG `_ - Adding quantum operations (EPR + measure) to event handlers - * - :doc:`QuantumCorrRNGVerified` + * - `QuantumCorrRNGVerified `_ - Multi-state protocol: negotiate, quantum, then classical verification diff --git a/docs/index.rst b/docs/index.rst index 8a23b966..da148fa1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,7 +27,7 @@ Quick start pip3 install simulaqron 2. **Configure your network** — create a ``simulaqron_network.json`` defining nodes and ports - (see :doc:`ConfNodes`) + (see `Configuring the Network `_) 3. **Write your program** using the NetQASM SDK:: @@ -42,17 +42,16 @@ Quick start print(int(m)) # read measurement result conn.close() -4. **Run examples** — see :doc:`Examples` for complete working programs +4. **Run examples** — see `Examples `_ for complete working programs Where to go next ---------------- -* **New to SimulaQron?** Start with :doc:`GettingStarted` for installation and your first example -* **Want to write programs?** See :doc:`NetQASM` for the NetQASM SDK reference -* **Looking for examples?** See :doc:`Examples` — new SDK, event-based, and native-mode examples -* **Configuring networks and settings?** See :doc:`ConfNodes` -* **Architecture and internals?** See :doc:`Overview` -* **API reference?** See :ref:`modindex` +* **New to SimulaQron?** Start with `Getting Started `_ for installation and your first example +* **Want to write programs?** See `The NetQASM Interface `_ for the NetQASM SDK reference +* **Looking for examples?** See `Examples `_ — new SDK, event-based, and native-mode examples +* **Configuring networks and settings?** See `Configuring the Network `_ +* **Architecture and internals?** See `Overview `_ We also have a `paper `_ describing the design of SimulaQron, freely available on `arxiv `_. @@ -72,6 +71,6 @@ SimulaQron, freely available on `arxiv `_. Indices and tables ================== -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +* Index +* Module Index +* Search diff --git a/docs/native-mode/CorrRng.rst b/docs/native-mode/CorrRng.rst index c9481903..76598061 100644 --- a/docs/native-mode/CorrRng.rst +++ b/docs/native-mode/CorrRng.rst @@ -2,7 +2,7 @@ Generate correlated randomness ============================== .. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. - See :doc:`../new-sdk/Overview` and the SDK version of this example at :doc:`../new-sdk/CorrRNG`. + See `New SDK Overview <../new-sdk/Overview.rst>`_ and the SDK version of this example at `CorrRNG <../new-sdk/CorrRNG.rst>`_. Having started the virtual quantum nodes, let us now run a simple test application, which already illustrates some of the aspects in realizing protocols. Our objective will be to realize the following protocol which will generate 1 diff --git a/docs/native-mode/GraphState.rst b/docs/native-mode/GraphState.rst index 11da265a..1c3084b3 100644 --- a/docs/native-mode/GraphState.rst +++ b/docs/native-mode/GraphState.rst @@ -2,7 +2,7 @@ Distributing a graph state ========================== .. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. - See :doc:`../new-sdk/Overview`. + See `New SDK Overview <../new-sdk/Overview.rst>`_. Here we consider a more complicated example, where we have four parties; Alice, Bob, Charlie and David. They will distribute a graph state and transform this with local operations and classical communication to make diff --git a/docs/native-mode/Teleport.rst b/docs/native-mode/Teleport.rst index d8b49a49..353b224f 100644 --- a/docs/native-mode/Teleport.rst +++ b/docs/native-mode/Teleport.rst @@ -2,11 +2,11 @@ Teleporting a Qubit =================== .. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. - See :doc:`../new-sdk/Overview` and the SDK version of this example at :doc:`../new-sdk/Teleport`. + See `New SDK Overview <../new-sdk/Overview.rst>`_ and the SDK version of this example at `Teleport <../new-sdk/Teleport.rst>`_. Let's now consider a very simple protocol, in which Alice first generates an EPR pair with Bob, and then teleports a qubit to Bob. To program it in SimulaQron's native mode, we will use the template described in -:doc:`Template`. +`Template `_. ------------ The protocol diff --git a/docs/native-mode/Template.rst b/docs/native-mode/Template.rst index f5fe8968..244376bf 100644 --- a/docs/native-mode/Template.rst +++ b/docs/native-mode/Template.rst @@ -2,7 +2,7 @@ Template for programming in native mode ======================================= .. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. - See :doc:`../new-sdk/Overview`. + See `New SDK Overview <../new-sdk/Overview.rst>`_. In examples/nativeMode/template you will find a template that allows you to start programming relatively easily by filling in the relevant parts of the template. Let us now discuss this template in detail: @@ -39,7 +39,7 @@ filling in the relevant parts of the template. Let us now discuss this template } #. The next step is to check that on each network computer that you will run on, the global configuration file - starting the virtual quantum nodes is set up correctly. See :doc:`../GettingStarted` on how to perform such a + starting the virtual quantum nodes is set up correctly. See `Getting Started <../GettingStarted.rst>`_ on how to perform such a configuration and start the local quantum virtual node backends. #. Now copy nodeTest.py to a separate file for each node. In our example above where we just have Alice (client only) and Bob (server only), you would copy nodeTest to aliceTest.py and bobTest.py. diff --git a/docs/new-sdk/Overview.rst b/docs/new-sdk/Overview.rst index 5fda5af1..6804db22 100644 --- a/docs/new-sdk/Overview.rst +++ b/docs/new-sdk/Overview.rst @@ -36,7 +36,7 @@ flush() result = int(m) # NOW this works You can call ``flush()`` multiple times on the same connection — this is how - mid-circuit classical logic works (see :doc:`MidCircuitLogic`). + mid-circuit classical logic works (see `MidCircuitLogic `_). Qubit A qubit allocated on the local quantum backend. Pass ``sim_conn`` so the @@ -82,13 +82,13 @@ The examples are ordered from simplest to most complex: * - Example - What it teaches - * - :doc:`Template` + * - `Template `_ - Basic setup: single node with local qubits, and client-server template - * - :doc:`CorrRNG` + * - `CorrRNG `_ - EPR pairs between two nodes, correlated measurement - * - :doc:`Teleport` + * - `Teleport `_ - Quantum teleportation with classical correction messages - * - :doc:`ExtendGHZ` + * - `ExtendGHZ `_ - Three-party entanglement, multiple EPR sockets on one connection - * - :doc:`MidCircuitLogic` + * - `MidCircuitLogic `_ - Multiple ``flush()`` calls for mid-circuit classical decisions From 1479adf43ff41a198d90eb5bebc8b8c3311a366b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 12 Mar 2026 11:14:28 +0100 Subject: [PATCH 243/277] Upgrade minimal supported python verison to 3.10. Update dependencies in project specification, and fix Makefile. --- Makefile | 7 +++++++ docs/requirements.txt | 4 ++-- pyproject.toml | 9 ++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 8eb42b6c..3363e100 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,13 @@ requirements python-deps: @${PYTHON} -m pip install . install-optional: install + # Python setuptools 81 removed "dry_run" option when compiling C++ code + # this breaks the build of projectq + # As a hack, we install the bare minimum tools to build projectq, then + # we build and install it (ignoring any build requirement in the projectq + # package spec), and finally we install the rest of the optional requirements + @${PYTHON} -m pip install "setuptools<81" pybind11 + @${PYTHON} -m pip install "git+https://github.com/ProjectQ-Framework/ProjectQ.git@v0.8.0" --no-build-isolation @${PYTHON} -m pip install .\[opt\] tests: diff --git a/docs/requirements.txt b/docs/requirements.txt index ada2a3ba..7e5fc01d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ sphinx==6.1.3 sphinx-rtd-theme==1.2.0 Cython -qutip -projectq +qutip<5.0.0 +projectq<=0.8.0 diff --git a/pyproject.toml b/pyproject.toml index 6fd80d6c..383d464d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools >= 77.0.3", "wheel", "setuptools-scm>=8"] +requires = ["setuptools<81", "wheel", "setuptools-scm>=8"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] @@ -18,9 +18,10 @@ dependencies = [ "click>=8.0,<9.0", "daemons>=1.3.0,<2.0.0", "netqasm>=2.1.1,<3.0.0", - "multiprocess>=0.70.18,<1.0" + "multiprocess>=0.70.18,<1.0", + "StrEnum<1.0.0" ] -requires-python = ">=3.8,<3.13" +requires-python = ">=3.10,<3.13" authors = [ {name = "Axel Dahlberg", email = "e.a.dahlberg@tudelft.nl"} ] @@ -29,8 +30,6 @@ maintainers = [ ] description = "A simulator for developing Quantum Internet software" readme = "README.md" -license = "MIT" -license-files = ["LICENCE"] keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] classifiers = [ "Programming Language :: Python :: 3.8", From fd0728e9e093389387a8a4c897f76a62c7caad84 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 12 Mar 2026 11:15:25 +0100 Subject: [PATCH 244/277] =?UTF-8?q?Small=20changes=20to=20make=20the=20cod?= =?UTF-8?q?e=20comp=C3=A0tible=20with=20python=203.10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- simulaqron/settings/network_config.py | 5 +++-- simulaqron/settings/simulaqron_config.py | 2 +- tests/quick/settings/test_networks_settings.py | 15 ++++++++++----- tests/quick/settings/test_simulaqron_settings.py | 6 ++++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index aab50f35..008d3ff2 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -3,11 +3,12 @@ import socket from contextlib import closing from dataclasses import dataclass, field, fields -from enum import StrEnum from importlib import resources from os import PathLike from pathlib import Path -from typing import Optional, Self, Dict, List, Tuple, Any +from strenum import StrEnum +from typing import Optional, Dict, List, Tuple, Any +from typing_extensions import Self from dataclasses_serialization.json import JSONSerializer, JSONSerializerMixin diff --git a/simulaqron/settings/simulaqron_config.py b/simulaqron/settings/simulaqron_config.py index 76a4a4a6..13433c0b 100644 --- a/simulaqron/settings/simulaqron_config.py +++ b/simulaqron/settings/simulaqron_config.py @@ -35,7 +35,7 @@ from enum import Enum from os import PathLike from pathlib import Path -from typing import Self +from typing_extensions import Self from dataclasses_serialization.json import JSONSerializer from dataclasses_serialization.json import JSONSerializerMixin diff --git a/tests/quick/settings/test_networks_settings.py b/tests/quick/settings/test_networks_settings.py index 27f4981a..09ebc588 100644 --- a/tests/quick/settings/test_networks_settings.py +++ b/tests/quick/settings/test_networks_settings.py @@ -21,13 +21,13 @@ class TestNetworksSettings: def clean_settings(self): # Load the setting files in cwd and home, saved them in a temp file if cwd_network.is_file() and cwd_network.is_file(): - orig_cwd_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + orig_cwd_network = NamedTemporaryFile(suffix=".json", mode="w", delete=False).__enter__() shutil.copyfile(cwd_network, orig_cwd_network.name) cwd_network.unlink() else: orig_cwd_network = None if home_network.is_file() and home_network.is_file(): - orig_home_network = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + orig_home_network = NamedTemporaryFile(suffix=".json", mode="w", delete=False).__enter__() shutil.copyfile(home_network, orig_home_network.name) home_network.unlink() else: @@ -39,10 +39,12 @@ def clean_settings(self): cwd_network.touch() shutil.copyfile(orig_cwd_network.name, cwd_network) orig_cwd_network.__exit__(None, None, None) + Path(orig_cwd_network.name).unlink() if orig_home_network is not None: home_network.touch() shutil.copyfile(orig_home_network.name, home_network) orig_home_network.__exit__(None, None, None) + Path(orig_home_network.name).unlink() files_to_check = [cwd_network, home_network] for file in files_to_check: @@ -243,27 +245,29 @@ def test_serialize_network_config(self, reset_net_cfg): expected_network_config = TestNetworksSettings._build_expected_config(alice_ports, bob_ports) - with NamedTemporaryFile(mode="wt", delete_on_close=False) as temp_file: + with NamedTemporaryFile(mode="wt", delete=False) as temp_file: network_config.write_to_file(temp_file.name) temp_file.flush() serialized_content = Path(temp_file.name).read_text() assert serialized_content == expected_network_config + Path(temp_file.name).unlink() def test_deserialize_network_config(self, reset_net_cfg): raw_config = TestNetworksSettings._build_expected_config([8020, 8021, 8022], [8050, 8051, 8052]) - with NamedTemporaryFile(mode="wt", delete_on_close=False) as temp_file: + with NamedTemporaryFile(mode="wt", delete=False) as temp_file: temp_file.write(raw_config) temp_file.flush() network_config.read_from_file(temp_file.name) assert json.dumps(JSONSerializer.serialize(network_config), indent=4) == raw_config + Path(temp_file.name).unlink() def test_load_old_json_format(self): this_file_folder = Path(__file__).parent old_json_config_path = this_file_folder / "resources" / "old_format.json" - with NamedTemporaryFile(mode="wt", delete_on_close=False) as temp_file: + with NamedTemporaryFile(mode="wt", delete=False) as temp_file: # We copy the content of the resource into a temp file, so we don't # overwrite the resource for future test sessions shutil.copy(old_json_config_path, temp_file.name) @@ -294,3 +298,4 @@ def test_load_old_json_format(self): assert network_config.nodes[1].qnodeos_port == 8832 assert network_config.nodes[1].vnode_hostname == "localhost" assert network_config.nodes[1].vnode_port == 8833 + Path(temp_file.name).unlink() diff --git a/tests/quick/settings/test_simulaqron_settings.py b/tests/quick/settings/test_simulaqron_settings.py index fa6e28d3..d5962dae 100644 --- a/tests/quick/settings/test_simulaqron_settings.py +++ b/tests/quick/settings/test_simulaqron_settings.py @@ -19,13 +19,13 @@ class TestSettings: def clean_settings(self): # Load the setting files in cwd and home, saved them in a temp file if cwd_settings.exists() and cwd_settings.is_file(): - orig_cwd_settings = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + orig_cwd_settings = NamedTemporaryFile(suffix=".json", mode="w", delete=False).__enter__() shutil.copyfile(cwd_settings, orig_cwd_settings.name) cwd_settings.unlink() else: orig_cwd_settings = None if home_settings.exists() and home_settings.is_file(): - orig_home_settings = NamedTemporaryFile(suffix=".json", mode="w", delete_on_close=False).__enter__() + orig_home_settings = NamedTemporaryFile(suffix=".json", mode="w", delete=False).__enter__() shutil.copyfile(home_settings, orig_home_settings.name) home_settings.unlink() else: @@ -37,10 +37,12 @@ def clean_settings(self): cwd_settings.touch() shutil.copyfile(orig_cwd_settings.name, cwd_settings) orig_cwd_settings.__exit__(None, None, None) + Path(orig_cwd_settings.name).unlink() if orig_home_settings is not None: home_settings.touch() shutil.copyfile(orig_home_settings.name, home_settings) orig_home_settings.__exit__(None, None, None) + Path(orig_home_settings.name).unlink() @staticmethod def _cleanup_config_files(): From 4cb542cd5c913cf66e728c81372f65bb69c213aa Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Thu, 12 Mar 2026 11:15:51 +0100 Subject: [PATCH 245/277] Update changelog in preparation for release --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa7371a4..cc863464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,15 @@ For more details refer to the [documentation](https://softwarequtech.github.io/S Upcoming -------- -2025-07-15 (v4.0.1) +2026-03-13 (v4.0.1) ------------------- - Migrated project specification to use modern TOML approach. - Tested working with Python versions 3.10, 3.11 and 3.12. +- Added new SDK for easily creating SimulaQron applications. This new SDK allows to also create + client-server applications using event-based programming paradigm. +- Updated the SimulaQron network configuration file format. This is done to align the network + configuration file with the data required by SimulaQron "native" mode and the "NetQASM" mode. +- Updated documentation of most of the undocumented functions and to match the new developments. 2021-11-18 (v4.0.0) ------------------- From ee7f1ba16eba53de97a9a13f87ba139c80e19331 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 18 Mar 2026 14:33:25 +0100 Subject: [PATCH 246/277] Update readme file to update installation instructions directly from the repository --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a93e09f9..b09e4b38 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -SimulaQron - simple quantum network simulator (4.0.0) -===================================================== +# SimulaQron - simple quantum network simulator (4.0.0) The purpose of this simulator of quantum network nodes is to allow you to develop new applications for a future quantum internet, while we do not yet have real quantum network nodes available for testing. @@ -7,12 +6,73 @@ a future quantum internet, while we do not yet have real quantum network nodes a Since version 4.0, SimulaQron is compatible with [NetQASM](https://github.com/QuTech-Delft/netqasm). See its documentation for how to use SimulaQron as a backend for running NetQASM applications. -Installation: +## Installation + +### Linux + +Before proceeding, make sure you install Python 3.12. Please note that Python 3.13 or newer *is not supported*. +To install Python 3.12 in Debian-based distributions, you can first add the "deadsnakes" repository: + +```shell +sudo add-apt-repository -y "ppa:deadsnakes/ppa" +``` + +Then you can install Python 3.12 and the Python development package: + +```shell +sudo apt-get install python3.12-full python3.12-dev ``` -pip3 install simulaqron + +Additionally, you will need the `build-essential` package, to install tools used when building some SimulaQron dependencies:: + +```shell +sudo apt-get install build-essential cmake vim linux-headers-generic +``` + +After this, you can install this repository by using the Makefile: + +```shell +make install ``` -Documentation and examples are explained in the html documentation +Additionally, you can install SimulaQron with extra dependencies: +```shell +make install-optional +``` + +### Windows + +In Windows, SimulaQron can be installed in two similar ways: + +* Using WSL: *Windows for Linux Subsystems* (WSL) is a way to execute the linux kernel (and linux apps) + in a Windows environment. To install WSL, you can follow the [official microsoft documentation](https://learn.microsoft.com/en-us/windows/wsl/install). + After this you can install SimulaQron in WSL using the Linux instructions from above. +* Using a Linux Virtual Machine: It is also possible to create a Linux environment using a Virtual Machine + Hypervisor such as [Oracle VirtualBox](https://www.virtualbox.org/wiki/Downloads). After installing this, + create a new Virtual Machine and install a compatible linux version (such as [Ubuntu 24.04](https://ubuntu.com/download/desktop/thank-you?version=24.04.4&architecture=amd64<s=true)). + After the installation is finished, follow the instructions to install SimulaQron on a Linux machine as + presented above. + + +### macOS + +In macOS, the only supported way to install SimulaQron is by using a Virtual Machine. Considering this +please install a Virtual Machine Hypervisor such as [Oracle VirtualBox](https://www.virtualbox.org/wiki/Downloads), +and install a compatible operating system: + +* Intel-based Macs: This is the case for Mac computers with Intel processor.s You can directly install the ["amd64" + version of Ubuntu 24.04](https://ubuntu.com/download/desktop/thank-you?version=24.04.4&architecture=amd64<s=true). +* ARM-based Macs: This is the case for "Apple Silicon" processors (M1 or newer, including the A18 Macbook Neo). For + this type of Macs, you can install the ["arm64" version of Ubuntu 24.04](https://cdimage.ubuntu.com/releases/24.04/release/ubuntu-24.04.4-desktop-arm64.iso) + +After installing the Operating System on the virtual machine, please continue the installation of SimulaQron in +the virtual machine using the Linux instructions as mentioned above. + + +Documentation +------------- + +Documentation and examples are explained in the HTML documentation https://softwarequtech.github.io/SimulaQron/html/index.html For upcoming and previous changes see the file [CHANGELOG.md](CHANGELOG.md) From 096c084e339a04702db5a6fa3ce3d305e8454f22 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 18 Mar 2026 15:09:53 +0100 Subject: [PATCH 247/277] Update "getting started" documentation and fix bugs in CLI client to match the documentation --- docs/GettingStarted.rst | 57 ++++++++++++------- simulaqron/cli.py | 121 ++++++++++++++++++++++------------------ 2 files changed, 103 insertions(+), 75 deletions(-) diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index 01ad302c..af370266 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -6,14 +6,30 @@ Setup ----- SimulaQron requires `Python 3.12 `_ along with the packages *netqasm*, *twisted*, *numpy*, *scipy*, -*networkx*, *click* and *daemons*. +*networkx*, *click* and *daemons*. By following the installation instructions in the following sections, you will install +SimulaQron with all the required packages. ^^^^^^^^^^^^^^^^^^^^^^ Installation using pip ^^^^^^^^^^^^^^^^^^^^^^ -The easiest way to install SimulaQron is using pip (requires MacOS or Linux). Start by creating and activating a -python virtual environment:: +The easiest way to install SimulaQron is using pip. SimulaQron has been tested working in Linux, and WSL (under windows). +For installation on macOS, please use a Linux virtual machine to install SimulaQron. + +Before proceeding with the installation, you need to install Python 3.12. For Debian-based distributions (like Ubuntu) +you can install the *deadsnakes* repository to gain access to some specific python versions:: + + sudo add-apt-repository -y "ppa:deadsnakes/ppa" + +After adding the repository, you can install the *full* version of python, including the development package and the :: + + sudo apt-get install python3.12-full python3.12-dev + +Additionally, you will need the `build-essential` package, to install tools used when building some SimulaQron dependencies:: + + sudo apt-get install build-essential cmake vim linux-headers-generic + +To install SimulaQron, start by creating and activating a python virtual environment:: python3.12 -m venv simulaqron-venv source simulaqron-venv/bin/activate @@ -22,6 +38,10 @@ Now, we can install SimulaQron by simply typing:: pip3 install simulaqron +It is also recommended that you can also install optional dependencies to enable full support of qubit engines:: + + pip3 install simulaqron\[opt\] + You can then make use of SimulaQron using the command ``simulaqron`` in the terminal. For more information on how to use this command see below or type:: @@ -31,12 +51,6 @@ To make sure you have the version compatible with this documentation type:: simulaqron version -If you want to make sure that everything has been installed properly you can start run the unittests. Open an -interactive python console by typing `python3` and the:: - - import simulaqron - simulaqron.tests() - ------------------------ Testing a simple example ------------------------ @@ -52,6 +66,7 @@ We will here illustrate how to use SimulaQron with the NetQASM library. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Starting the SimulaQron backend ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + By default SimulaQron uses the five nodes Alice, Bob, Charlie, David and Eve on your local computers. In this example there will be three processes for each node listening to incoming messages on a certain port number. These make up the simulation backend, the NetQASM server and the classical communication server. To start the processes and thus @@ -59,13 +74,13 @@ the backend of SimulaQron simply type:: simulaqron start -.. warning:: Running ``simulaqron start`` will be default start up servers on localhost (i.e., your own computer), +.. warning:: Running ``simulaqron start`` will by default start up servers on localhost (i.e., your own computer), using port numbers between 8000 and 9000, to form the simulated quantum internet hardware. SimulaQron does not provide any access control to its simulated hardware, so you are responsible to securing access should this be relevant for you. You can also run the different simulated nodes on different computers. We do not take any responsibility for problems caused by SimulaQron. -For more information on what ``./cli/SimulaQron start`` does, how to change the nodes and the ports of the network, +For more information on what ``simulaqron start`` does, how to change the nodes and the ports of the network, the topology etc, see `Configuring the Network `_. To stop the backend, simply type:: @@ -77,7 +92,7 @@ makes SimulaQron think that the network is still running. To reset this you can simulaqron reset -Note that this also kills any currently running network and resets any settings or configurations. +Note that this also kills any currently running network and resets any local settings or configurations. ^^^^^^^^^^^^^^^^^^^ Running a protocol @@ -117,9 +132,12 @@ going on here? Let us first look at how we will realize the example by making an While the task we want to realize here is completely trivial, the addition of step 3 does however already highlight a range of choices on how to realize step 3 and the need to find good abstractions to allow easy application development. One way to realize step 3 would be to hardwire Alice's and Bob's measurements: if the hardware can identify the -correct qubits from the entanglement generation, then we could instruct it to measure it immediately without asking for a notification from the entanglement generation process. It is clear that in a network that is a bit larger than our tiny three node setup, identifying the right setup requires a link between the underlying qubits and classical control information: this is the objective of the classical/quantum combiner. +correct qubits from the entanglement generation, then we could instruct it to measure it immediately without asking +for a notification from the entanglement generation process. It is clear that in a network that is a bit larger than +our tiny three node setup, identifying the right setup requires a link between the underlying qubits and classical +control information: this is the objective of the classical/quantum combiner. -The script run.sh executes the following two python scripts:: +The script ``run.sh`` executes the following two python scripts:: #!/bin/sh @@ -132,7 +150,7 @@ We first create a ``NetQASMConnection`` which handles all communication with the An ``EPRSocket`` is used to create or receive entangled qubit pairs with a remote node. The key pattern is: queue operations, call ``flush()`` to execute them, then read results with ``int(m)``. -The core of aliceTest.py is:: +The core of ``aliceTest.py`` is:: epr_socket = EPRSocket("Bob") @@ -153,7 +171,7 @@ The core of aliceTest.py is:: m1_val = int(m1) sim_conn.close() -Similarly the core of bobTest.py is:: +Similarly the core of ``bobTest.py`` is:: epr_socket = EPRSocket("Alice") @@ -189,15 +207,14 @@ To set a setting, for example to use the projectQ backend, type:: This will create a file named ``simulaqron_settings.json`` in the current folder. This new file contains a full set of simulaqron configuration, including the setting that was just configured (using the `projectq` backend, in the example). -It is also possible to manually create this file:: +It is also possible to manually create this` `simulaqron_settings.json`` file with any text editor:: { "backend": "projectq", "log_level": 10 } -which would set the backend to be use ProjectQ and the log-level to be debug (10). Any setting in this file will -override the settings set in the CLI. +which would set the backend to be use ProjectQ and the log-level to be debug (10). Is is also possible to create a configuration file that contains all the default configurations:: @@ -214,7 +231,7 @@ This command will create a file with the following configuration:: "recv_retry_time": 0.1, "recv_max_retries": 10, "log_level": 30, - "sim_backend": "stabilizer", + "sim_backend": "qutip", "noisy_qubits": false, "max_app_waiting_time": -1.0, "t1": 1.0 diff --git a/simulaqron/cli.py b/simulaqron/cli.py index 14d7fce4..9f2a8c7f 100644 --- a/simulaqron/cli.py +++ b/simulaqron/cli.py @@ -91,9 +91,9 @@ def _path_exists(path: Path) -> bool: def _load_local_settings_or_default(): if LOCAL_SIMULAQRON_SETTINGS.exists() and LOCAL_SIMULAQRON_SETTINGS.is_file(): simulaqron_settings.read_from_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration loaded from file: '{LOCAL_SIMULAQRON_SETTINGS}'") else: - print("Configuration from default configuration") + click.echo("Configuration from default configuration") simulaqron_settings.default_settings() @@ -110,9 +110,9 @@ def _create_local_settings_if_needed_and_load(): def _load_local_network_or_default(): if LOCAL_NETWORK_SETTINGS.exists() and LOCAL_NETWORK_SETTINGS.is_file(): network_config.read_from_file(LOCAL_NETWORK_SETTINGS) - print(f"Network configuration loaded from file: '{LOCAL_NETWORK_SETTINGS}'") + click.echo(f"Network configuration loaded from file: '{LOCAL_NETWORK_SETTINGS}'") else: - print("Configuration from default configuration") + click.echo("Configuration from default configuration") network_config.default_settings() @@ -144,7 +144,7 @@ def version(): """ Prints the version of simulqron. """ - print(metadata.version('simulaqron')) + click.echo(metadata.version('simulaqron')) ################# @@ -218,21 +218,27 @@ def start(name: str, nodes: str, simulaqron_config_file: Path, network_config_fi " --name option and try again." # noqa: E131 ) # Check that the nodes to start exist in the given network - nodes = nodes.split(",") + start_all = False if len(nodes) <= 0: - raise click.BadOptionUsage( - option_name="nodes", - message="The list of nodes to start is empty. Please check the list given in " - "the --nodes argument." - ) - for node_to_start in nodes: - if node_to_start not in network_config.networks[name].nodes: - raise click.BadOptionUsage( - option_name="nodes", - message=f"The node '{node_to_start}' was not found in the network named " # noqa: E713 - f"'{name} 'specified in the configuration file '{network_config_file}'.\n" # noqa: E131 - "Please check the list of names you passed in the --nodes option and try again." # noqa: E131 - ) + click.echo(f"No nodes specified to start. Starting all nodes configured in '{network_config_file}'.") + start_all = True + nodes = [] + else: + nodes = nodes.split(",") + + if start_all: + for node_to_start in network_config.networks[name].nodes: + nodes.append(node_to_start) + else: + for node_to_start in nodes: + if node_to_start not in network_config.networks[name].nodes: + raise click.BadOptionUsage( + option_name="nodes", + message=f"The node '{node_to_start}' was not found in the network named " # noqa: E713 + f"'{name} 'specified in the configuration file '{network_config_file}'.\n" # noqa: E131 + "Please check the list of names you passed in the --nodes option " # noqa: E131 + "and try again." # noqa: E131 + ) # Check that there is no other network with the same name running pidfile = PID_FOLDER / f"simulaqron_network_{name}.pid" if pidfile.exists(): @@ -301,9 +307,9 @@ def reset(force: bool): :param force: Don't ask for confirmation, and immediately reset the simulaqron settings. """ if not force: - answer = input("Are you sure you want to reset simulaqron?\nThis will revert settings and " + answer = input("Are you sure you want to reset simulaqron?\nThis will revert local settings and " "network config files to the default values.\nNote, this action will remove " - f"the file at {LOCAL_SIMULAQRON_SETTINGS} if it exists.\n" + f"the file at {LOCAL_SIMULAQRON_SETTINGS} and {LOCAL_NETWORK_SETTINGS} if they exist.\n" "(yes/no)") else: answer = "yes" @@ -315,7 +321,12 @@ def reset(force: bool): if entry.exists(): entry.unlink() simulaqron_settings.default_settings() - simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) + if LOCAL_NETWORK_SETTINGS.exists(): + simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) + + network_config.using_default_network() + if LOCAL_NETWORK_SETTINGS.exists(): + network_config.write_to_file(LOCAL_NETWORK_SETTINGS) else: raise click.ClickException("Aborting!") @@ -352,7 +363,7 @@ def default(): "value", type=click.Choice([b.value for b in SimBackend]) ) -def sim_backend(value: SimBackend): +def backend(value: SimBackend): """ The backend to use (stabilizer, projectq, qutip). @@ -362,7 +373,7 @@ def sim_backend(value: SimBackend): _create_local_settings_if_needed_and_load() simulaqron_settings.sim_backend = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -382,7 +393,7 @@ def max_qubits(value: int): _create_local_settings_if_needed_and_load() simulaqron_settings.max_qubits = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -402,7 +413,7 @@ def max_registers(value: int): _create_local_settings_if_needed_and_load() simulaqron_settings.max_registers = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -421,7 +432,7 @@ def conn_retry_time(value: float): _create_local_settings_if_needed_and_load() simulaqron_settings.conn_retry_time = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -440,7 +451,7 @@ def recv_timeout(value: float): _create_local_settings_if_needed_and_load() simulaqron_settings.recv_timeout = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -459,7 +470,7 @@ def recv_retry_time(value: float): _create_local_settings_if_needed_and_load() simulaqron_settings.recv_retry_time = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -480,7 +491,7 @@ def log_level(value: int): _create_local_settings_if_needed_and_load() simulaqron_settings.log_level = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -503,7 +514,7 @@ def noisy_qubits(value: str): else: simulaqron_settings.noisy_qubits = False simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") @set.command( @@ -522,7 +533,7 @@ def t1(value: float): _create_local_settings_if_needed_and_load() simulaqron_settings.t1 = value simulaqron_settings.write_to_file(LOCAL_SIMULAQRON_SETTINGS) - print(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") + click.echo(f"Configuration saved to file: '{LOCAL_SIMULAQRON_SETTINGS}'") ############### @@ -539,12 +550,12 @@ def get(): @get.command( help="The backend to use (stabilizer, projectq, qutip).", ) -def sim_backend(): +def backend(): """ Prints the current configured simulaqron backend. """ _load_local_settings_or_default() - print(simulaqron_settings.sim_backend) + click.echo(simulaqron_settings.sim_backend) @get.command( @@ -555,7 +566,7 @@ def max_qubits(): Prints the current configured max virt-qubits per node and max sim-qubits per register. """ _load_local_settings_or_default() - print(simulaqron_settings.max_qubits) + click.echo(simulaqron_settings.max_qubits) @get.command( @@ -566,7 +577,7 @@ def max_registers(): Prints the current configured max number of register a node can hold. """ _load_local_settings_or_default() - print(simulaqron_settings.max_registers) + click.echo(simulaqron_settings.max_registers) @get.command( @@ -578,7 +589,7 @@ def conn_retry_time(): to connect to another node or SimulaQron component. """ _load_local_settings_or_default() - print(simulaqron_settings.conn_retry_time) + click.echo(simulaqron_settings.conn_retry_time) @get.command( @@ -590,7 +601,7 @@ def recv_timeout(): an EPR half before raising a timeout error. """ _load_local_settings_or_default() - print(simulaqron_settings.recv_timeout) + click.echo(simulaqron_settings.recv_timeout) @get.command( @@ -602,7 +613,7 @@ def recv_retry_time(): attempts to receive an EPR half. """ _load_local_settings_or_default() - print(simulaqron_settings.recv_retry_time) + click.echo(simulaqron_settings.recv_retry_time) @get.command( @@ -613,7 +624,7 @@ def log_level(): Prints the current configured log level. """ _load_local_settings_or_default() - print(simulaqron_settings.log_level) + click.echo(simulaqron_settings.log_level) @get.command( @@ -625,9 +636,9 @@ def noisy_qubits(): """ _load_local_settings_or_default() if simulaqron_settings.noisy_qubits: - print("on") + click.echo("on") else: - print("off") + click.echo("off") @get.command( @@ -638,7 +649,7 @@ def t1(): Prints the current configured t1 value when simulating noisy qubits. """ _load_local_settings_or_default() - print(simulaqron_settings.t1) + click.echo(simulaqron_settings.t1) ############### @@ -711,11 +722,11 @@ def add(name: str, network_name: str, hostname: str, app_port: int, qnodeos_port neighbors=neighbors) network_config.write_to_file(LOCAL_NETWORK_SETTINGS) added_node: NodeConfig = network_config.get_nodes(network_name=network_name)[name] - print(f"Node with name '{added_node.name}' was added to the network with name '{network_name}'.\n" - "Socket addresses are: \n" - f"* App/Classical: '({added_node.app_hostname}, {added_node.app_port})\n" - f"* QNodeOS: '({added_node.qnodeos_hostname}, {added_node.qnodeos_port})\n" - f"* Virtual Node: '({added_node.vnode_hostname}, {added_node.vnode_port})\n") + click.echo(f"Node with name '{added_node.name}' was added to the network with name '{network_name}'.\n" + "Socket addresses are: \n" + f"* App/Classical: '({added_node.app_hostname}, {added_node.app_port})\n" + f"* QNodeOS: '({added_node.qnodeos_hostname}, {added_node.qnodeos_port})\n" + f"* Virtual Node: '({added_node.vnode_hostname}, {added_node.vnode_port})\n") @nodes.command() @@ -734,13 +745,13 @@ def remove(name: str, network_name: str): """ if not LOCAL_NETWORK_SETTINGS.exists() or not LOCAL_NETWORK_SETTINGS.is_file(): - print(f"WARNING - the file '{LOCAL_NETWORK_SETTINGS}' was not found. The loaded " - f"configuration corresponds to the one on '{HOME_NETWORK_SETTINGS}'") + click.echo(f"WARNING - the file '{LOCAL_NETWORK_SETTINGS}' was not found. The loaded " + f"configuration corresponds to the one on '{HOME_NETWORK_SETTINGS}'") else: network_config.read_from_file(LOCAL_NETWORK_SETTINGS) network_config.remove_node(node_name=name, network_name=network_name) network_config.write_to_file(LOCAL_NETWORK_SETTINGS) - print(f"Node with name '{name}' was removed from the network with name '{network_name}'.\n") + click.echo(f"Node with name '{name}' was removed from the network with name '{network_name}'.\n") @nodes.command() @@ -754,7 +765,7 @@ def default(): """ network_config.using_default_network() network_config.write_to_file(LOCAL_NETWORK_SETTINGS) - print(f"Default network saved to file: '{LOCAL_NETWORK_SETTINGS}'") + click.echo(f"Default network saved to file: '{LOCAL_NETWORK_SETTINGS}'") @nodes.command() @@ -769,8 +780,8 @@ def get(network_name: str): """ if not LOCAL_NETWORK_SETTINGS.exists() or not LOCAL_NETWORK_SETTINGS.is_file(): - print(f"WARNING - the file '{LOCAL_NETWORK_SETTINGS}' was not found. The loaded " - f"configuration corresponds to the one on '{HOME_NETWORK_SETTINGS}'") + click.echo(f"WARNING - the file '{LOCAL_NETWORK_SETTINGS}' was not found. The loaded " + f"configuration corresponds to the one on '{HOME_NETWORK_SETTINGS}'") else: network_config.read_from_file(LOCAL_NETWORK_SETTINGS) try: @@ -778,7 +789,7 @@ def get(network_name: str): except ValueError: raise click.BadParameter(f"No network {network_name}") else: - print(("{} " * len(nodes))[:-1].format(*nodes)) + click.echo(("{} " * len(nodes))[:-1].format(*nodes)) if __name__ == "__main__": From 4eec4411c2c5114ef611dfa03a33e9205480c4a5 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 18 Mar 2026 15:36:08 +0100 Subject: [PATCH 248/277] Update "configure nodes" documentation and fix bugs in CLI client to match the documentation --- docs/ConfNodes.rst | 40 ++++++++++++++++----------- docs/GettingStarted.rst | 3 ++ simulaqron/cli.py | 2 +- simulaqron/settings/network_config.py | 6 ++++ 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index 852f00d1..ba1e99cc 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -1,5 +1,5 @@ -Configuring the simulated network -================================= +SimulaQron Configuration +======================== SimulaQron uses two configuration files: @@ -7,16 +7,16 @@ SimulaQron uses two configuration files: * ``simulaqron_settings.json`` — configures the simulation backend, timeouts, and other settings (see the Settings section in `Getting Started `_) -------------------------------------------- +------------------------------------- Running all nodes on a single machine -------------------------------------------- +------------------------------------- When developing and testing, you typically run all simulated nodes on one computer. In this case, all sockets use ``localhost`` and you just need distinct port numbers for each node. -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^ Using the SimulaQron CLI -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^ The ``simulaqron`` command manages the backend for you. To start a network with nodes Alice and Bob:: @@ -36,8 +36,9 @@ If something went wrong (e.g. the process was killed) and SimulaQron thinks the The ``simulaqron start`` command accepts these arguments: -* ``--nodes `` (**required**): Comma-separated list of node names to start. These must exist in - the network configuration file. +* ``--nodes `` (optional): Comma-separated list of node names to start. These must exist in + the network configuration file. If not given, SimulaQron will start all the defined nodes in + ``simulaqron_network.json``. * ``--simulaqron-config-file=PATH`` (optional): Path to a SimulaQron settings file. Defaults to ``simulaqron_settings.json`` in the current folder. * ``--network-config-file=PATH`` (optional): Path to a network configuration file. Defaults to @@ -48,9 +49,9 @@ The ``simulaqron start`` command accepts these arguments: .. warning:: ``simulaqron start`` will fail if any of the ports specified in the config files are already in use by a running SimulaQron network or another program. -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Using per-example run scripts -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Each example in ``examples/new-sdk/`` and ``examples/nativeMode/`` includes a ``run.sh`` script that starts the SimulaQron backend and launches the node programs. This is the easiest way to try an example:: @@ -61,9 +62,9 @@ the SimulaQron backend and launches the node programs. This is the easiest way t The ``run.sh`` script reads the ``simulaqron_network.json`` and ``simulaqron_settings.json`` in the example directory, so each example is self-contained. -------------------------------------------- +---------------------------------- Running nodes on separate machines -------------------------------------------- +---------------------------------- To simulate a real distributed quantum network, you can run each node on a different physical computer. In this case, you need to: @@ -120,9 +121,16 @@ For each node, you specify IP and port for three sockets: * ``qnodeos_socket`` — connection to the QNodeOS server that interprets NetQASM subroutines * ``vnode_socket`` — connection to the SimulaQron VirtualNode that runs the quantum simulation -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +You can easily copy the default network configuration by using the simulaqron CLI command:: + + simulaqron nodes default + +This will create a ``simulaqron_network.json`` file in the current folder with 5 nodes: `Alice`, +`Bob`, `Charlie`, `David` and `Eve`. + +^^^^^^^^^^^^^^^^^^^^^^^^^^ Using the CLI to add nodes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^ You can build up a network incrementally using the CLI:: @@ -140,9 +148,9 @@ You can also specify explicit hostnames and ports: * ``--vnode-port`` * ``--neighbors`` -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Writing the JSON config manually -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For more complex setups, write the ``simulaqron_network.json`` file directly. Here is an example with two networks ("default" and "small_network"):: diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index af370266..b0323e96 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -246,6 +246,9 @@ implement using simulaqron. .. note:: Settings needs to be set before starting the SimulaQron backend. If the backend is already running, stop it, set the settings and start it again. +It is also possible to create the default SimulaQron network configuration in the current folder. Check the +`Configuring the Network `_ document to check how to achieve this. + ^^^^^^^^^^^^^^^^^^^ Settings precedence ^^^^^^^^^^^^^^^^^^^ diff --git a/simulaqron/cli.py b/simulaqron/cli.py index 9f2a8c7f..3104d71a 100644 --- a/simulaqron/cli.py +++ b/simulaqron/cli.py @@ -721,7 +721,7 @@ def add(name: str, network_name: str, hostname: str, app_port: int, qnodeos_port app_port=app_port, qnodeos_port=qnodeos_port, vnode_port=vnode_port, neighbors=neighbors) network_config.write_to_file(LOCAL_NETWORK_SETTINGS) - added_node: NodeConfig = network_config.get_nodes(network_name=network_name)[name] + added_node: NodeConfig = network_config[network_name][name] click.echo(f"Node with name '{added_node.name}' was added to the network with name '{network_name}'.\n" "Socket addresses are: \n" f"* App/Classical: '({added_node.app_hostname}, {added_node.app_port})\n" diff --git a/simulaqron/settings/network_config.py b/simulaqron/settings/network_config.py index 008d3ff2..41a3e6c2 100644 --- a/simulaqron/settings/network_config.py +++ b/simulaqron/settings/network_config.py @@ -170,6 +170,12 @@ def __eq__(self, other) -> bool: nodes_are_equal = [this_node == other_node for this_node, other_node in zip(self.nodes, other.nodes)] return self.name == other.name and self.topology == other.topology and all(nodes_are_equal) + def __getitem__(self, item: str) -> NodeConfig | None: + for node_name, node_cfg in self.nodes.items(): + if node_name == item: + return node_cfg + return None + @dataclass class NetworksConfiguration(JSONSerializerMixin): From 6cecaaa798e93d346cab0e7d1c6900e7022cc45a Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 18 Mar 2026 16:24:20 +0100 Subject: [PATCH 249/277] Update "index" "Overview" and "NetQASM" documentation --- README.md | 21 +++++++++++++++++++-- docs/NetQASM.rst | 37 ++++++++++++++++++++----------------- docs/Overview.rst | 5 +++-- docs/index.rst | 2 +- 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index b09e4b38..5f4e24a8 100644 --- a/README.md +++ b/README.md @@ -69,8 +69,25 @@ After installing the Operating System on the virtual machine, please continue th the virtual machine using the Linux instructions as mentioned above. -Documentation -------------- +## Tests + +There are 2 sets of tests: quick and slow ones. To ease the execution, the `Makefile` provides two targets: +* `tests`: This target only run the quick tests. +* `tests_all`: This target runs quick and slow tests. + +To run a test target, simply invoke it with make: +```shell +make tests +``` + +or: +```shell +make tests_all +``` + + +## Documentation + Documentation and examples are explained in the HTML documentation https://softwarequtech.github.io/SimulaQron/html/index.html diff --git a/docs/NetQASM.rst b/docs/NetQASM.rst index 3eef7303..7ec519e3 100644 --- a/docs/NetQASM.rst +++ b/docs/NetQASM.rst @@ -17,13 +17,13 @@ everything you need:: Core concepts -------------- -^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^ NetQASMConnection -^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^ -Your connection to the **local quantum backend** (SimulaQron's virtual quantum node). This is *not* a connection -to another party — it is how your node talks to its local simulated quantum hardware. All qubit operations are -queued through this connection. +This objects represent your connection to the **local quantum backend** (SimulaQron's virtual quantum node). +This is *not* a connection to another party — it is how your node talks to its local simulated quantum hardware. +All qubit operations are queued through this connection. Create it once, use it throughout your program, and close it at the end:: @@ -36,11 +36,14 @@ Create it once, use it throughout your program, and close it at the end:: If your program uses EPR pairs, pass the EPR sockets at creation time:: + from netqasm.sdk import EPRSocket + + epr_socket = EPRSocket("Bob") conn = NetQASMConnection("Alice", epr_sockets=[epr_socket]) -^^^^^^^^^^^^^^^^^^^^^ +^^^^^ Qubit -^^^^^^^^^^^^^^^^^^^^^ +^^^^^ A qubit allocated on the local quantum backend. Pass the connection so the backend knows where to allocate it:: @@ -54,9 +57,9 @@ A qubit allocated on the local quantum backend. Pass the connection so the backe Gates are **queued** — nothing executes until you call ``flush()``. -^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^ EPRSocket -^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^ Used to create or receive entangled qubit pairs with a remote node:: @@ -70,9 +73,9 @@ Used to create or receive entangled qubit pairs with a remote node:: epr_socket = EPRSocket("Alice") epr = epr_socket.recv_keep()[0] -^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^ flush() -^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^ The **sync point** that executes all queued quantum operations and makes measurement results available. Before ``flush()``, measurement results are just futures/promises. After ``flush()``, you can read them @@ -93,9 +96,9 @@ You can call ``flush()`` multiple times on the same connection. This enables **m See the mid-circuit logic example in `Examples `_ for a full demonstration. ------------------------ +--------------- Minimal example ------------------------ +--------------- A single-node program that creates a qubit, applies a Hadamard gate, and measures:: @@ -110,9 +113,9 @@ A single-node program that creates a qubit, applies a Hadamard gate, and measure print("Measurement outcome:", int(m)) conn.close() ------------------------ +-------------------- Two-node EPR example ------------------------ +-------------------- Alice and Bob generate an EPR pair and each measure their qubit to get correlated random numbers. @@ -138,9 +141,9 @@ Alice and Bob generate an EPR pair and each measure their qubit to get correlate Both sides will print the same random number (0 or 1), demonstrating quantum correlation. --------------------------- +----------------------- Classical communication --------------------------- +----------------------- For exchanging classical messages between nodes (e.g. measurement outcomes for teleportation corrections), SimulaQron provides ``SimulaQronClassicalClient`` and ``SimulaQronClassicalServer``. diff --git a/docs/Overview.rst b/docs/Overview.rst index cc762e64..b2d967d4 100644 --- a/docs/Overview.rst +++ b/docs/Overview.rst @@ -76,7 +76,7 @@ deal with this backend simulation. Nevertheless, as a guide to the backend, it consists of three essential components: -* quantumEngine - There are currenlty three different quantumEngines implemented: Using `QuTip `_ +* quantumEngine - There are currently three different quantumEngines implemented: Using `QuTip `_ and mixed state, using `Project Q `_ and pure states and finally using stabilizer formalism. This corresponds to one quantum register full of qubits across which gates can be performed. Should you wish to use a different backend, you may wish to add a different engine. @@ -170,7 +170,8 @@ Automated tests ^^^^^^^^^^^^^^^ There are number of automated tests that test many (but not all) of the features of SimulaQron and the NetQASM interface. -See `Getting Started `_ for how to run these. +To run these tests, you need to clone the SimulaQron `GitHub repository `_, +and follow the ``README.md`` file to install and run the tests. Some of the automated tests use quantum tomography and are thus inherently probabilistic. Therefore if you see that one of these fails, you can try to run the test again and see if it is consistent. If the tests are to slow on your computer you can also run the short version, which skips the quantum tomography tests. diff --git a/docs/index.rst b/docs/index.rst index da148fa1..cd591076 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,7 @@ Key features * **Distributed quantum internet simulation** — install a local simulation program on each computer, or run all nodes on a single machine -* **Three simulation backends** — stabilizer formalism (default, efficient), `QuTip `_ (mixed +* **Three simulation backends** — stabilizer formalism (efficient), `QuTip `_ (default, mixed state), and `ProjectQ `_ (pure state) * **Two programming interfaces** — the NetQASM SDK (recommended) and a native Twisted mode for low-level access * **Configurable network topologies** — complete, ring, path, random tree, or custom topologies From 389dd2ea5cb323d24615300695cd730fdca0bb51 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 18 Mar 2026 16:32:15 +0100 Subject: [PATCH 250/277] Update documentation of "native" examples --- docs/native-mode/GraphState.rst | 8 ++++---- docs/native-mode/Teleport.rst | 6 ++++-- examples/nativeMode/corrRNG/run.sh | 0 examples/nativeMode/graphState/run.sh | 0 examples/nativeMode/teleport/run.sh | 0 5 files changed, 8 insertions(+), 6 deletions(-) mode change 100644 => 100755 examples/nativeMode/corrRNG/run.sh mode change 100644 => 100755 examples/nativeMode/graphState/run.sh mode change 100644 => 100755 examples/nativeMode/teleport/run.sh diff --git a/docs/native-mode/GraphState.rst b/docs/native-mode/GraphState.rst index 1c3084b3..689d8eb6 100644 --- a/docs/native-mode/GraphState.rst +++ b/docs/native-mode/GraphState.rst @@ -432,10 +432,7 @@ Starting We first start the virtual quantum node backend, by executing:: - python3 simulaqron/run/startNode.py Alice & - python3 simulaqron/run/startNode.py Bob & - python3 simulaqron/run/startNode.py David & - python3 simulaqron/run/startNode.py Charlie & + simulaqron start --nodes=Alice,Bob,Charlie,David --network-config-file classicalNet.json --simulaqron-config-file simulaqron_settings.json We then start up the programs for the parties themselves. These will connect to the virtual quantum nodes, and execute the quantum commands and classical communication outlined above, in the same directory as we placed @@ -446,3 +443,6 @@ simulaqron_network.json:: python3 davidTest.py & python3 aliceTest.py +You can easily start everything by using the a single helper script:: + + sh doNew.sh diff --git a/docs/native-mode/Teleport.rst b/docs/native-mode/Teleport.rst index 353b224f..6ff4d30c 100644 --- a/docs/native-mode/Teleport.rst +++ b/docs/native-mode/Teleport.rst @@ -210,8 +210,7 @@ Starting We first start the virtual quantum node backend, by executing:: - python3 simulaqron/run/startNode.py Alice & - python3 simulaqron/run/startNode.py Bob & + simulaqron start --nodes=Alice,Bob --network-config-file simulaqron_network.json We then start up the programs for Alice and Bob themselves. These will connect to the virtual quantum nodes, and execute the quantum commands and classical communication outlined above, in the same directory as we placed @@ -220,3 +219,6 @@ simulaqron_network.json:: python3 bobTest.py & python3 aliceTest.py +You can easily start everything by using the a single helper script:: + + sh doNew.sh diff --git a/examples/nativeMode/corrRNG/run.sh b/examples/nativeMode/corrRNG/run.sh old mode 100644 new mode 100755 diff --git a/examples/nativeMode/graphState/run.sh b/examples/nativeMode/graphState/run.sh old mode 100644 new mode 100755 diff --git a/examples/nativeMode/teleport/run.sh b/examples/nativeMode/teleport/run.sh old mode 100644 new mode 100755 From ce0f0b3e4c73435ac09cfd6b9a4d61e113893e5f Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Wed, 18 Mar 2026 18:26:30 +0100 Subject: [PATCH 251/277] Update documentation for native examples --- examples/nativeMode/corrRNG/README.md | 64 +++++++++++++++++++ examples/nativeMode/corrRNG/README.txt | 21 ------ examples/nativeMode/extendGHZ/README.md | 55 ++++++++++++++++ examples/nativeMode/extendGHZ/run.sh | 0 examples/nativeMode/graphState/README.md | 60 +++++++++++++++++ examples/nativeMode/graphState/README.txt | 4 -- .../teleport/{README.txt => README.md} | 13 ++-- examples/nativeMode/template/README.md | 4 +- 8 files changed, 189 insertions(+), 32 deletions(-) create mode 100644 examples/nativeMode/corrRNG/README.md delete mode 100644 examples/nativeMode/corrRNG/README.txt create mode 100644 examples/nativeMode/extendGHZ/README.md mode change 100644 => 100755 examples/nativeMode/extendGHZ/run.sh create mode 100644 examples/nativeMode/graphState/README.md delete mode 100644 examples/nativeMode/graphState/README.txt rename examples/nativeMode/teleport/{README.txt => README.md} (85%) diff --git a/examples/nativeMode/corrRNG/README.md b/examples/nativeMode/corrRNG/README.md new file mode 100644 index 00000000..119b0c01 --- /dev/null +++ b/examples/nativeMode/corrRNG/README.md @@ -0,0 +1,64 @@ +# Correlated Random Number Generator + +In this example, we have only two nodes: Alice and Bob. + +Alice and Bob will locally connect to their virtual nodes. The classical control communication is done by +letting Alice run a client and Bob a server. Alice generates the EPR pair, and sends half to Bob. She subsequently +performs the teleportation operation. She informs Bob of the outcome of the teleportation measurement, as well as the +identity of the virtual qubit he received (assumed to be unknown to Bob here). + +Bob proceeds to recover the teleported qubit. + +In this example, we simply print out the initial state to be teleported, as well as the final state received by +Bob to check whether the teleportation worked correctly. + + +# How to run + +To run this example, first make sure that the python virtual environment is activated. You can easily check this +with the terminal command prompt, which shows the active python virtual environment in parentheses: +``` +(simulaqron) user@machine_name:~$ +``` +If you don't see the name of the virtual environment, please check the SimulaQron README file to know how to +create and activate it. + +Once the environment is active, we need to start the simulaqron network: +```bash +simulaqron start --nodes=Alice,Bob --network-config-file classicalNet.json +``` + +This will read the JSON configuration files and start the SimulaQron virtual nodes for nodes `Alice` and `Bob`. + +After this, you can simply run the example by using the `run` script: +```bash +./run.sh +``` + +# How to stop the execution in case the test execution stalls. + +To fully stop the execution, you can use the `terninate` script: +```bash +./terminate.sh +``` + +Us you want to terminate the current execution and start a new one, you can use the `doNew` script. This will stop +the current execution and run the example once again: +```bash +./doNew.sh +``` + + +# Troubleshooting + +## Trying to start the network gives "Network with name is already running" message + +This usually happens when you stopped the network "manually" by killing processes (`kill -9`). If this happens, +please make sure that you kill all related processes (use `ps aux | grep python` to search) and then delete the +PID file for the running network. This file is located in `~/simulaqron_pids` and it is called +`simulaqron_network_.pid`: +```bash +rm ~/simulaqron_pids/simulaqron_network_.pid +``` + +After this, you can try to start the network again. diff --git a/examples/nativeMode/corrRNG/README.txt b/examples/nativeMode/corrRNG/README.txt deleted file mode 100644 index bc2221cc..00000000 --- a/examples/nativeMode/corrRNG/README.txt +++ /dev/null @@ -1,21 +0,0 @@ - -In this example, we have only two nodes: Alice and Bob. - -Alice and Bob will locally connect to their virtual nodes. The classical control communication is done by -letting Alice run a client and Bob a server. Alice generates the EPR pair, and sends half to Bob. She subsequently -performs the teleportation operation. She informs Bob of the outcome of the teleportation measurement, as well as the -identity of the virtual qubit he received (assumed to be unknown to Bob here). - -Bob proceeds to recover the teleported qubit. - -In this example, we simply print out the initial state to be teleported, as well as the final state received by -Bob to check whether the teleportation worked correctly. - - -To start run: -- simulaqron start --nodes=Alice,Bob --network-config-file classicalNet.json - -sh run.sh - -To terminate run: -sh terminate.sh diff --git a/examples/nativeMode/extendGHZ/README.md b/examples/nativeMode/extendGHZ/README.md new file mode 100644 index 00000000..de9402d5 --- /dev/null +++ b/examples/nativeMode/extendGHZ/README.md @@ -0,0 +1,55 @@ +# Extended GHZ states + +TODO + +# How to run + +To run this example, first make sure that the python virtual environment is activated. You can easily check this +with the terminal command prompt, which shows the active python virtual environment in parentheses: +``` +(simulaqron) user@machine_name:~$ +``` +If you don't see the name of the virtual environment, please check the SimulaQron README file to know how to +create and activate it. + +Once the environment is active, we need to start the simulaqron network: +```bash +simulaqron start --nodes=Alice,Bob,Charlie --network-config-file classicalNet.json +``` + +This will read the JSON configuration files and start the SimulaQron virtual nodes for nodes `Alice`, `Bob` and +`Charlie`. + +After this, you can simply run the example by using the `run` script: +```bash +./run.sh +``` + +# How to stop the execution in case the test execution stalls. + +To fully stop the execution, you can use the `terninate` script: +```bash +./terminate.sh +``` + +Us you want to terminate the current execution and start a new one, you can use the `doNew` script. This will stop +the current execution and run the example once again: +```bash +./doNew.sh +``` + + +# Troubleshooting + +## Trying to start the network gives "Network with name is already running" message + +This usually happens when you stopped the network "manually" by killing processes (`kill -9`). If this happens, +please make sure that you kill all related processes (use `ps aux | grep python` to search) and then delete the +PID file for the running network. This file is located in `~/simulaqron_pids` and it is called +`simulaqron_network_.pid`: +```bash +rm ~/simulaqron_pids/simulaqron_network_.pid +``` + +After this, you can try to start the network again. + diff --git a/examples/nativeMode/extendGHZ/run.sh b/examples/nativeMode/extendGHZ/run.sh old mode 100644 new mode 100755 diff --git a/examples/nativeMode/graphState/README.md b/examples/nativeMode/graphState/README.md new file mode 100644 index 00000000..bc9a1af9 --- /dev/null +++ b/examples/nativeMode/graphState/README.md @@ -0,0 +1,60 @@ +# Distributed Graph States + +In this example Alice, Bob, Charlie and David generate a graph state and transform this into a +GHZ-like state and measure their qubits to achieve correlated outcomes. For a complete description +of this example, see the description in the documentation under: + * Examples + * Programming via SimulaQron's native Python Twisted Interface (specific to SimulaQron) + * Distributing a graph state + +# How to run + +To run this example, first make sure that the python virtual environment is activated. You can easily check this +with the terminal command prompt, which shows the active python virtual environment in parentheses: +``` +(simulaqron) user@machine_name:~$ +``` +If you don't see the name of the virtual environment, please check the SimulaQron README file to know how to +create and activate it. + +Once the environment is active, we need to start the simulaqron network: +```bash +simulaqron start --nodes=Alice,Bob,Charlie,David --network-config-file classicalNet.json --simulaqron-config-file simulaqron_settings.json +``` + +This will read the JSON configuration files and start the SimulaQron virtual nodes for nodes `Alice`, `Bob`, +`Charlie` and `David`. + +After this, you can simply run the example by using the `run` script: +```bash +./run.sh +``` + +# How to stop the execution in case the test execution stalls. + +To fully stop the execution, you can use the `terninate` script: +```bash +./terminate.sh +``` + +Us you want to terminate the current execution and start a new one, you can use the `doNew` script. This will stop +the current execution and run the example once again: +```bash +./doNew.sh +``` + + +# Troubleshooting + +## Trying to start the network gives "Network with name is already running" message + +This usually happens when you stopped the network "manually" by killing processes (`kill -9`). If this happens, +please make sure that you kill all related processes (use `ps aux | grep python` to search) and then delete the +PID file for the running network. This file is located in `~/simulaqron_pids` and it is called +`simulaqron_network_.pid`: +```bash +rm ~/simulaqron_pids/simulaqron_network_.pid +``` + +After this, you can try to start the network again. + diff --git a/examples/nativeMode/graphState/README.txt b/examples/nativeMode/graphState/README.txt deleted file mode 100644 index 9ae7068c..00000000 --- a/examples/nativeMode/graphState/README.txt +++ /dev/null @@ -1,4 +0,0 @@ -In this example Alice, Bob, Charlie and David generate a graph state and transform this into a GHZ-like state and measure their qubits to acheive correlated outcomes. For a complete description of this example, see the desctiption in the documentation under - * Examples - * Programming via SimulaQron's native Python Twisted Interface (specific to SimulaQron) - * Distributing a graph state diff --git a/examples/nativeMode/teleport/README.txt b/examples/nativeMode/teleport/README.md similarity index 85% rename from examples/nativeMode/teleport/README.txt rename to examples/nativeMode/teleport/README.md index 5d9a45f1..7c322d95 100644 --- a/examples/nativeMode/teleport/README.txt +++ b/examples/nativeMode/teleport/README.md @@ -25,7 +25,7 @@ create and activate it. Once the environment is active, we need to start the simulaqron network: ```bash -siumulaqron start --nodes Alice,Bob +simulaqron start --nodes Alice,Bob ``` This will read the JSON configuration files and start the SimulaQron virtual nodes for nodes `Alice` and `Bob`. @@ -37,8 +37,13 @@ After this, you can simply run the example by using the `run` script: # How to stop the execution in case the test execution stalls. -To fully stop the execution, you can use the `doNew` script. This will stop the current execution and run the -example once again: +To fully stop the execution, you can use the `terninate` script: +```bash +./terminate.sh +``` + +Us you want to terminate the current execution and start a new one, you can use the `doNew` script. This will stop +the current execution and run the example once again: ```bash ./doNew.sh ``` @@ -56,4 +61,4 @@ PID file for the running network. This file is located in `~/simulaqron_pids` an rm ~/simulaqron_pids/simulaqron_network_.pid ``` -After this, you can try to start the network again +After this, you can try to start the network again. diff --git a/examples/nativeMode/template/README.md b/examples/nativeMode/template/README.md index 4942c499..5b35c755 100644 --- a/examples/nativeMode/template/README.md +++ b/examples/nativeMode/template/README.md @@ -25,7 +25,7 @@ Each one of those nodes defines 3 entries: ## Adding new nodes To add a new node named "Eva", follow these steps: -1. Open the config.json file in a text editor. +1. Open the `network_config.json` file in a text editor. 2. Locate the "nodes" array inside the "default" object. 3. Add a new object to the "nodes" array for "Eva". The structure should match the existing nodes: ```json @@ -40,8 +40,6 @@ To add a new node named "Eva", follow these steps: Replace PORT_NUMBER with unique port numbers for each socket type (e.g., 8851, 8852, 8853). The result should look like: ```json -Copy - { "name": "default", "nodes": [ From b70bcab3b075b2eee64d94c722c89ca5876e7e92 Mon Sep 17 00:00:00 2001 From: spoukke Date: Mon, 23 Mar 2026 13:51:18 +0100 Subject: [PATCH 252/277] update polite ping pong with new protocol --- .../eventBased/politePingPong/politeAlice.py | 157 +++++++----------- .../eventBased/politePingPong/politeBob.py | 112 +++++-------- 2 files changed, 94 insertions(+), 175 deletions(-) diff --git a/examples/eventBased/politePingPong/politeAlice.py b/examples/eventBased/politePingPong/politeAlice.py index 6f6b2010..cc62bf4b 100644 --- a/examples/eventBased/politePingPong/politeAlice.py +++ b/examples/eventBased/politePingPong/politeAlice.py @@ -1,44 +1,35 @@ """ Polite Ping-Pong — Alice (client). -Alice's behaviour is defined as a finite state machine. She always -initiates the exchange by sending "ping" before entering the event loop. -The event loop then reads one message at a time, looks up the -(current_state, message) pair in the dispatch table, and calls the -corresponding handler. The handler performs an action and returns the -next state. +Alice waits for Bob's "READY" signal before sending each "PING". After +NUM_ROUNDS pings she sends "BYE" instead, ending the exchange. Alice's state diagram --------------------- - ┌─ (connect) ─────────────────────────────┐ - │ send "ping" │ - ▼ │ - ──────────────► WAITING_PONG (start) - │ - recv "pong" - send "thank you" - │ - ▼ - WAITING_YOURE_WELCOME - │ - recv "you're welcome" - │ - ▼ - DONE - -As a transition table (this maps directly to ALICE_DISPATCH below): - - Current state │ Message received │ Action │ Next state - ───────────────────────┼────────────────────┼──────────────────┼────────────────────── - WAITING_PONG │ "pong" │ send "thank you" │ WAITING_YOURE_WELCOME - WAITING_YOURE_WELCOME │ "you're welcome" │ (none) │ DONE - -Any (state, message) pair NOT in the table is rejected with a warning; -the state does not change and the loop continues. - -Note: the initial "ping" is sent before the loop starts — it is not a -state transition but simply Alice's opening move as the initiator. + ┌─ (connect) ──────────────────────────────────────────┐ + │ │ + ▼ │ + WAITING_FOR_READY ──[recv "READY"]──► WAITING_FOR_PONG (start) + │ + recv "PONG" + │ + ▼ + WAITING_FOR_READY (next round) + ... after NUM_ROUNDS ... + │ + recv "READY" (last) + send "BYE" + ▼ + DONE + +Transition table: + + Current state │ Message │ Action │ Next state + ────────────────────┼──────────┼─────────────────────────────┼────────────────── + WAITING_FOR_READY │ "READY" │ send "PING" (or "BYE") │ WAITING_FOR_PONG + │ │ │ (or DONE) + WAITING_FOR_PONG │ "PONG" │ — │ WAITING_FOR_READY """ from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -49,92 +40,58 @@ from simulaqron.settings.network_config import NodeConfigType -# ── States ─────────────────────────────────────────────────────────────────── - -STATE_WAITING_PONG = "WAITING_PONG" # noqa: E221 -STATE_WAITING_YOURE_WELCOME = "WAITING_YOURE_WELCOME" -STATE_DONE = "DONE" # noqa: E221 - - -# ── Handlers ───────────────────────────────────────────────────────────────── - -async def handle_pong(writer: StreamWriter) -> str: - """ - Transition: WAITING_PONG ──[recv "pong"]──► WAITING_YOURE_WELCOME +NUM_ROUNDS = 5 - Alice receives a pong and politely says thank you. - """ - reply = "thank you" - print(f"Alice: sending '{reply}'") - writer.write(reply.encode("utf-8")) - await writer.drain() - return STATE_WAITING_YOURE_WELCOME - - -async def handle_youre_welcome(writer: StreamWriter) -> str: - """ - Transition: WAITING_YOURE_WELCOME ──[recv "you're welcome"]──► DONE - - Alice receives the final courtesy and the exchange is complete. - No reply is needed. - """ - print("Alice: received 'you're welcome' — exchange complete.") - return STATE_DONE +# ── States ─────────────────────────────────────────────────────────────────── +STATE_WAITING_FOR_READY = "WAITING_FOR_READY" +STATE_WAITING_FOR_PONG = "WAITING_FOR_PONG" # noqa: E221 +STATE_DONE = "DONE" # noqa: E221 -# ── Dispatch table ──────────────────────────────────────────────────────────── -# Maps (current_state, message) → handler. -# This table IS the state machine: every valid transition is listed here, -# and anything not listed is automatically an invalid transition. -ALICE_DISPATCH = { - (STATE_WAITING_PONG, "pong"): handle_pong, # noqa: E241 - (STATE_WAITING_YOURE_WELCOME, "you're welcome"): handle_youre_welcome, -} +# ── Event loop ─────────────────────────────────────────────────────────────── +async def run_alice(reader: StreamReader, writer: StreamWriter) -> None: + rounds_left = NUM_ROUNDS -# ── Event loop ──────────────────────────────────────────────────────────────── + async def handle_ready(writer: StreamWriter) -> str: + nonlocal rounds_left + if rounds_left > 0: + rounds_left -= 1 + round_num = NUM_ROUNDS - rounds_left + writer.write(b"PING\n") + print(f"Alice [round {round_num}]: sent PING") + return STATE_WAITING_FOR_PONG + else: + writer.write(b"BYE\n") + print("Alice: sent BYE, done.") + return STATE_DONE -async def run_alice(reader: StreamReader, writer: StreamWriter): - """ - Alice's event loop. + async def handle_pong(writer: StreamWriter) -> str: + print("Alice: received PONG") + return STATE_WAITING_FOR_READY - Alice initiates by sending "ping", then enters the state machine loop: - 1. Read the next message from Bob. - 2. Look up (current_state, message) in ALICE_DISPATCH. - 3. If found, call the handler and move to the returned next state. - 4. If not found, log a warning and stay in the current state. - Loop exits when the state reaches STATE_DONE or the connection drops. - """ - # Initial action: Alice always opens the exchange with a ping. - # This happens before the loop — it is not a state transition. - opening = "ping" - print(f"Alice: sending '{opening}'") - writer.write(opening.encode("utf-8")) - await writer.drain() + dispatch = { + (STATE_WAITING_FOR_READY, "READY"): handle_ready, + (STATE_WAITING_FOR_PONG, "PONG"): handle_pong, # noqa: E241 + } - state = STATE_WAITING_PONG + state = STATE_WAITING_FOR_READY while state != STATE_DONE: - # 1. Wait for the next event (message from Bob) - data = await reader.read(255) + data = await reader.readline() if not data: print(f"Alice [{state}]: connection dropped unexpectedly.") break - msg = data.decode("utf-8") + msg = data.decode().strip() print(f"Alice [{state}]: received '{msg}'") - # 2. Look up the transition - handler = ALICE_DISPATCH.get((state, msg)) + handler = dispatch.get((state, msg)) - # 3a. Invalid transition — warn and stay in current state if handler is None: - print( - f"Alice [{state}]: no transition for message '{msg}' — ignoring." - ) + print(f"Alice [{state}]: no transition for '{msg}' — ignoring.") continue - # 3b. Valid transition — execute handler, advance state state = await handler(writer) print(f"Alice: event loop finished (final state: {state}).") diff --git a/examples/eventBased/politePingPong/politeBob.py b/examples/eventBased/politePingPong/politeBob.py index aeaa1c77..a7ae53a5 100644 --- a/examples/eventBased/politePingPong/politeBob.py +++ b/examples/eventBased/politePingPong/politeBob.py @@ -1,32 +1,27 @@ """ Polite Ping-Pong — Bob (server). -Bob's behaviour is defined as a finite state machine. The event loop -reads one message at a time, looks up the (current_state, message) pair -in the dispatch table, and calls the corresponding handler. The handler -performs an action (typically sending a reply) and returns the next state. +Bob sends "READY" immediately on connection and after each "PONG", signalling +to Alice that he is ready for the next round. He handles "PING" (reply PONG +then READY) and "BYE" (close). Bob's state diagram ------------------- - recv "ping" send "pong" - WAITING_PING ─────────────────────────────────────► WAITING_THANKS - │ - recv "thank you" - send "you're welcome" - │ - ▼ - DONE + ┌─ (connect) ──────────────────────────────────────────┐ + │ send "READY" │ + ▼ │ +WAITING_FOR_PING_OR_BYE (start) + │ + recv "PING" → send "PONG", send "READY" → (stay) + recv "BYE" → DONE -As a transition table (this maps directly to BOB_DISPATCH below): +Transition table: - Current state │ Message received │ Action │ Next state - ─────────────────┼──────────────────┼────────────────────────┼────────────────── - WAITING_PING │ "ping" │ send "pong" │ WAITING_THANKS - WAITING_THANKS │ "thank you" │ send "you're welcome" │ DONE - -Any (state, message) pair NOT in the table is rejected with a warning; -the state does not change and the loop continues. + Current state │ Message │ Action │ Next state + ────────────────────────┼─────────┼──────────────────────────┼─────────────────────── + WAITING_FOR_PING_OR_BYE │ "PING" │ send "PONG", send "READY"│ WAITING_FOR_PING_OR_BYE + WAITING_FOR_PING_OR_BYE │ "BYE" │ — │ DONE """ from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -38,93 +33,60 @@ # ── States ─────────────────────────────────────────────────────────────────── -# Each constant names a state in Bob's state machine. -# The string value is used in log output so keep it human-readable. -STATE_WAITING_PING = "WAITING_PING" # noqa: E221 -STATE_WAITING_THANKS = "WAITING_THANKS" -STATE_DONE = "DONE" # noqa: E221 +STATE_WAITING_FOR_PING_OR_BYE = "WAITING_FOR_PING_OR_BYE" +STATE_DONE = "DONE" # noqa: E221 # ── Handlers ───────────────────────────────────────────────────────────────── -# One handler per valid transition. A handler receives the writer (to send -# a reply) and returns the next state. It does NOT need to validate the -# current state — that is guaranteed by the dispatch table. async def handle_ping(writer: StreamWriter) -> str: - """ - Transition: WAITING_PING ──[recv "ping"]──► WAITING_THANKS - - Bob receives a ping and replies with a pong. - """ - reply = "pong" - print(f"Bob: sending '{reply}'", flush=True) - writer.write(reply.encode("utf-8")) - await writer.drain() - return STATE_WAITING_THANKS - - -async def handle_thank_you(writer: StreamWriter) -> str: - """ - Transition: WAITING_THANKS ──[recv "thank you"]──► DONE - - Bob receives a thank you and replies politely before finishing. - """ - reply = "you're welcome" - print(f"Bob: sending '{reply}'", flush=True) - writer.write(reply.encode("utf-8")) - await writer.drain() + writer.write(b"PONG\n") + print("Bob: sent PONG", flush=True) + writer.write(b"READY\n") + print("Bob: sent READY", flush=True) + return STATE_WAITING_FOR_PING_OR_BYE + + +async def handle_bye(writer: StreamWriter) -> str: + print("Bob: received BYE, closing.", flush=True) return STATE_DONE # ── Dispatch table ──────────────────────────────────────────────────────────── -# Maps (current_state, message) → handler. -# This table IS the state machine: every valid transition is listed here, -# and anything not listed is automatically an invalid transition. BOB_DISPATCH = { - (STATE_WAITING_PING, "ping"): handle_ping, # noqa: E241 - (STATE_WAITING_THANKS, "thank you"): handle_thank_you, + (STATE_WAITING_FOR_PING_OR_BYE, "PING"): handle_ping, + (STATE_WAITING_FOR_PING_OR_BYE, "BYE"): handle_bye, # noqa: E241 } # ── Event loop ──────────────────────────────────────────────────────────────── -async def run_bob(reader: StreamReader, writer: StreamWriter): - """ - Bob's event loop. - - Repeatedly: - 1. Read the next message from Alice. - 2. Look up (current_state, message) in BOB_DISPATCH. - 3. If found, call the handler and move to the returned next state. - 4. If not found, log a warning and stay in the current state. - Loop exits when the state reaches STATE_DONE or the connection drops. - """ +async def run_bob(reader: StreamReader, writer: StreamWriter) -> None: print("Bob: Alice connected.", flush=True) - state = STATE_WAITING_PING + + writer.write(b"READY\n") + print("Bob: sent READY", flush=True) + state = STATE_WAITING_FOR_PING_OR_BYE while state != STATE_DONE: - # 1. Wait for the next event (message from Alice) - data = await reader.read(255) + data = await reader.readline() if not data: print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True) break - msg = data.decode("utf-8") + msg = data.decode().strip() print(f"Bob [{state}]: received '{msg}'", flush=True) - # 2. Look up the transition handler = BOB_DISPATCH.get((state, msg)) - # 3a. Invalid transition — warn and stay in current state if handler is None: print( - f"Bob [{state}]: no transition for message '{msg}' — ignoring.", + f"Bob [{state}]: no transition for '{msg}' — ignoring.", flush=True, ) continue - # 3b. Valid transition — execute handler, advance state state = await handler(writer) print(f"Bob: event loop finished (final state: {state}).", flush=True) @@ -141,5 +103,5 @@ async def run_bob(reader: StreamReader, writer: StreamWriter): server = SimulaQronClassicalServer(sockets_config, "Bob") server.register_client_handler(run_bob) - print("Bob: starting server...") + print("Bob: starting server...", flush=True) server.start_serving() From adc947841bee9769326b7fe3c9baa58ab6f65f8f Mon Sep 17 00:00:00 2001 From: spoukke Date: Mon, 23 Mar 2026 14:09:20 +0100 Subject: [PATCH 253/277] use explicit utf-8 --- examples/eventBased/politePingPong/politeAlice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/eventBased/politePingPong/politeAlice.py b/examples/eventBased/politePingPong/politeAlice.py index cc62bf4b..e74a546d 100644 --- a/examples/eventBased/politePingPong/politeAlice.py +++ b/examples/eventBased/politePingPong/politeAlice.py @@ -83,7 +83,7 @@ async def handle_pong(writer: StreamWriter) -> str: if not data: print(f"Alice [{state}]: connection dropped unexpectedly.") break - msg = data.decode().strip() + msg = data.decode("utf-8") print(f"Alice [{state}]: received '{msg}'") handler = dispatch.get((state, msg)) From c575bc159fc5b2aef5c30b7e65211d516a615b20 Mon Sep 17 00:00:00 2001 From: spoukke Date: Mon, 23 Mar 2026 14:13:57 +0100 Subject: [PATCH 254/277] move Alice's handlers and dispatch table to module scope in politeAlice.py --- .../eventBased/politePingPong/politeAlice.py | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/examples/eventBased/politePingPong/politeAlice.py b/examples/eventBased/politePingPong/politeAlice.py index e74a546d..e510c88b 100644 --- a/examples/eventBased/politePingPong/politeAlice.py +++ b/examples/eventBased/politePingPong/politeAlice.py @@ -48,34 +48,46 @@ STATE_WAITING_FOR_PONG = "WAITING_FOR_PONG" # noqa: E221 STATE_DONE = "DONE" # noqa: E221 +# ── Mutable round counter ───────────────────────────────────────────────────── + +rounds_left = NUM_ROUNDS + + +# ── Handlers ───────────────────────────────────────────────────────────────── + +async def handle_ready(writer: StreamWriter) -> str: + global rounds_left + if rounds_left > 0: + rounds_left -= 1 + round_num = NUM_ROUNDS - rounds_left + writer.write(b"PING\n") + print(f"Alice [round {round_num}]: sent PING") + return STATE_WAITING_FOR_PONG + else: + writer.write(b"BYE\n") + print("Alice: sent BYE, done.") + return STATE_DONE + + +async def handle_pong(writer: StreamWriter) -> str: + print("Alice: received PONG") + return STATE_WAITING_FOR_READY + + +# ── Dispatch table ──────────────────────────────────────────────────────────── + +ALICE_DISPATCH = { + (STATE_WAITING_FOR_READY, "READY"): handle_ready, + (STATE_WAITING_FOR_PONG, "PONG"): handle_pong, # noqa: E241 +} + # ── Event loop ─────────────────────────────────────────────────────────────── async def run_alice(reader: StreamReader, writer: StreamWriter) -> None: + global rounds_left rounds_left = NUM_ROUNDS - async def handle_ready(writer: StreamWriter) -> str: - nonlocal rounds_left - if rounds_left > 0: - rounds_left -= 1 - round_num = NUM_ROUNDS - rounds_left - writer.write(b"PING\n") - print(f"Alice [round {round_num}]: sent PING") - return STATE_WAITING_FOR_PONG - else: - writer.write(b"BYE\n") - print("Alice: sent BYE, done.") - return STATE_DONE - - async def handle_pong(writer: StreamWriter) -> str: - print("Alice: received PONG") - return STATE_WAITING_FOR_READY - - dispatch = { - (STATE_WAITING_FOR_READY, "READY"): handle_ready, - (STATE_WAITING_FOR_PONG, "PONG"): handle_pong, # noqa: E241 - } - state = STATE_WAITING_FOR_READY while state != STATE_DONE: @@ -86,7 +98,7 @@ async def handle_pong(writer: StreamWriter) -> str: msg = data.decode("utf-8") print(f"Alice [{state}]: received '{msg}'") - handler = dispatch.get((state, msg)) + handler = ALICE_DISPATCH.get((state, msg)) if handler is None: print(f"Alice [{state}]: no transition for '{msg}' — ignoring.") From 8bd1fffb2110db266aeaac1bc4ac006a4328fc61 Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 24 Mar 2026 15:03:36 +0100 Subject: [PATCH 255/277] Fix a bug to correctly cleanup after sending "Stop App" message. This allows a server to correctly handle the next client --- simulaqron/netqasm_backend/qnodeos.py | 8 +++++++- simulaqron/sdk/connection.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/simulaqron/netqasm_backend/qnodeos.py b/simulaqron/netqasm_backend/qnodeos.py index 4c35738c..d9e7578e 100644 --- a/simulaqron/netqasm_backend/qnodeos.py +++ b/simulaqron/netqasm_backend/qnodeos.py @@ -3,9 +3,10 @@ from typing import Optional, Dict, Callable, Generator, Any, List, Type from netqasm.backend.executor import Executor -from netqasm.backend.messages import MsgDoneMessage, Message, MessageType +from netqasm.backend.messages import MsgDoneMessage, Message, MessageType, StopAppMessage from netqasm.backend.qnodeos import QNodeController from netqasm.lang.instr import Flavour +from netqasm.sdk.shared_memory import SharedMemoryManager from twisted.internet.defer import inlineCallbacks from twisted.internet.protocol import Protocol @@ -173,6 +174,11 @@ def _return_qubit_state(self, qubit_id: int, real_part: List[List[float]], imag_ qubit_state_message = ReturnQubitStateMessage(qubit_id, real_part, imag_part) self._return_msg(msg=qubit_state_message) + def _handle_stop_app(self, msg: StopAppMessage) -> Generator[Any, None, None]: + yield from super()._handle_stop_app(msg) + # Clear the shared memory registries occupied in the QNodeOS backend + SharedMemoryManager.reset_memories() + # We override the _get_message_handlers method so we can also handle the "get qubit state" message def _get_message_handlers(self) -> Dict[NewMessageType | MessageType, Callable]: return { diff --git a/simulaqron/sdk/connection.py b/simulaqron/sdk/connection.py index f55345a6..df483d9e 100644 --- a/simulaqron/sdk/connection.py +++ b/simulaqron/sdk/connection.py @@ -152,6 +152,28 @@ def try_connection( retry_time=-1.0, ) + def close( + self, + clear_app: bool = True, + stop_backend: bool = False, + exception: bool = False, + ) -> None: + """ + Closes the SimulaQron connection. It also sends the corresponding messages + to the backend to clean up the states associated with the connection. + This leaves the quantum backend ready to be used with a new client. + + :param clear_app: Clear the application before closing the connection. + :type clear_app: bool + :param stop_backend: Stop the backend when closing the connection. + :type stop_backend: bool + :param exception: Whether the app is stopping due to an exception or not. + :type exception: bool + """ + super().close(clear_app, stop_backend, exception) + # Clear the shared memories occupied by this connection + SharedMemoryManager.reset_memories() + @staticmethod def _create_socket( name: str, From 27f0fd6363e14bd5e5426527668672e2895c7fd3 Mon Sep 17 00:00:00 2001 From: spoukke Date: Thu, 26 Mar 2026 13:48:32 +0100 Subject: [PATCH 256/277] make ping pong state machine more explicit --- examples/eventBased/pingPong/pingpongAlice.py | 108 ++++++++++++---- examples/eventBased/pingPong/pingpongBob.py | 122 +++++++++++++----- 2 files changed, 174 insertions(+), 56 deletions(-) diff --git a/examples/eventBased/pingPong/pingpongAlice.py b/examples/eventBased/pingPong/pingpongAlice.py index 0c382152..d54f67fa 100644 --- a/examples/eventBased/pingPong/pingpongAlice.py +++ b/examples/eventBased/pingPong/pingpongAlice.py @@ -1,15 +1,44 @@ """ -Ping-Pong client (Alice). +Ping-Pong — Alice (client). -Alice connects to Bob and sends a sequence of messages. -After each message she waits for Bob's reply and prints it. - -Try changing the messages list to see how Bob reacts! +Alice connects to Bob and they exchange PING / PONG messages for NUM_ROUNDS +rounds. Both sides know NUM_ROUNDS, so no BYE is needed — the connection +simply closes after the last round. This is a purely classical example — no quantum operations. -It demonstrates the event-based programming pattern that we will -later extend with quantum operations (teleportation, etc.). +It demonstrates the event-based state-machine pattern used throughout +SimulaQron examples. + +Alice's state diagram +--------------------- + + ┌─ (connect) ──────────────────────────────────────┐ + │ │ + ▼ │ + IDLE ──[send "PING"]──► PLAYING (start) + │ + recv "PONG" + │ + ▼ + IDLE (next round) + ... after NUM_ROUNDS ... + │ + recv "PONG" (last) + ▼ + DONE + +Transition table: + + State │ Event │ Action │ Next state + ─────────┼──────────────┼──────────────┼───────────── + IDLE │ (entry) │ send "PING" │ PLAYING + PLAYING │ recv "PONG" │ — │ IDLE + PLAYING │ recv "PONG" │ (last round) │ DONE + + IDLE → PLAYING is an *entry action*: Alice sends PING immediately on + entering IDLE, before waiting for the next message. """ + from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -19,27 +48,60 @@ from simulaqron.settings.network_config import NodeConfigType -async def run_alice(reader: StreamReader, writer: StreamWriter): - """ - Alice sends a sequence of messages and prints Bob's replies. +NUM_ROUNDS = 5 + +# ── States ─────────────────────────────────────────────────────────────────── + +STATE_IDLE = "IDLE" +STATE_PLAYING = "PLAYING" +STATE_DONE = "DONE" # noqa: E221 + + +# ── Event loop ─────────────────────────────────────────────────────────────── + +async def run_alice(reader: StreamReader, writer: StreamWriter) -> None: + rounds_done = 0 + + async def handle_pong(_writer: StreamWriter) -> str: + """Transition: PLAYING ──[recv "PONG"]──► IDLE (or DONE)""" + nonlocal rounds_done + print(f"Alice [round {rounds_done}]: received PONG") + if rounds_done < NUM_ROUNDS: + return STATE_IDLE + return STATE_DONE + + dispatch = { + (STATE_PLAYING, "PONG"): handle_pong, + } + + state = STATE_IDLE + + while state != STATE_DONE: + # Entry action: IDLE → send PING → PLAYING + if state == STATE_IDLE: + rounds_done += 1 + writer.write(b"PING\n") + print(f"Alice [round {rounds_done}]: sent PING") + state = STATE_PLAYING + + data = await reader.readline() + if not data: + print(f"Alice [{state}]: connection dropped unexpectedly.") + break + msg = data.decode("utf-8") + + handler = dispatch.get((state, msg)) - Try changing this list to see what Bob does with different messages! - """ - messages = ["ping", "ping", "hello", "ping"] + if handler is None: + print(f"Alice [{state}]: no transition for '{msg}' — ignoring.") + continue - for msg in messages: - # Send a message to Bob - print(f"Alice: sending '{msg}'") - writer.write(msg.encode("utf-8")) - await writer.drain() + state = await handler(writer) - # Wait for Bob's reply - reply_data = await reader.read(255) - reply = reply_data.decode("utf-8") - print(f"Alice: received '{reply}'") + print(f"Alice: event loop finished (final state: {state}).") - print("Alice: done, disconnecting.") +# ── Entry point ─────────────────────────────────────────────────────────────── if __name__ == "__main__": # Load configuration files — paths are relative to this script's location diff --git a/examples/eventBased/pingPong/pingpongBob.py b/examples/eventBased/pingPong/pingpongBob.py index f791a9af..e58228d2 100644 --- a/examples/eventBased/pingPong/pingpongBob.py +++ b/examples/eventBased/pingPong/pingpongBob.py @@ -1,16 +1,44 @@ """ -Ping-Pong server (Bob). +Ping-Pong — Bob (server). -Bob listens for messages from Alice. For each message he receives: - - If the message is "ping", he replies with "pong". - - For anything else, he replies with "no way!". - -Bob keeps listening until Alice disconnects. +Bob listens for Alice's PINGs and replies with PONGs. +Both sides know NUM_ROUNDS, so no BYE is needed — Bob stops after +sending the last PONG and the connection closes naturally. This is a purely classical example — no quantum operations. -It demonstrates the event-based programming pattern that we will -later extend with quantum operations (teleportation, etc.). +It demonstrates the event-based state-machine pattern used throughout +SimulaQron examples. + +Bob's state diagram +------------------- + + ┌─ (connect) ──────────────────────────────────────┐ + │ │ + ▼ │ + IDLE ──[recv "PING"]──► PLAYING (start) + │ + send "PONG" + │ + ▼ + IDLE (next round) + ... after NUM_ROUNDS ... + │ + recv "PING" (last) + ▼ + DONE + +Transition table: + + State │ Event │ Action │ Next state + ─────────┼──────────────┼──────────────┼───────────── + IDLE │ recv "PING" │ — │ PLAYING + PLAYING │ (entry) │ send "PONG" │ IDLE + PLAYING │ (entry) │ (last round) │ DONE + + PLAYING → IDLE/DONE is an *entry action*: Bob sends PONG immediately + on entering PLAYING, before waiting for the next message. """ + from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -20,40 +48,68 @@ from simulaqron.settings.network_config import NodeConfigType -async def run_bob(reader: StreamReader, writer: StreamWriter): - """ - Bob's event loop. +NUM_ROUNDS = 5 + +# ── States ─────────────────────────────────────────────────────────────────── + +STATE_IDLE = "IDLE" +STATE_PLAYING = "PLAYING" +STATE_DONE = "DONE" # noqa: E221 + + +# ── Handlers ───────────────────────────────────────────────────────────────── + +async def handle_ping(_writer: StreamWriter) -> str: + """Transition: IDLE ──[recv "PING"]──► PLAYING""" + print("Bob: received PING", flush=True) + return STATE_PLAYING - Each iteration: - 1. Wait for a message from Alice - 2. Decide on a reply based on the message content - 3. Send the reply back - """ - print("Bob: Alice connected, waiting for messages...", flush=True) - while True: - # Wait until Alice sends something - data = await reader.read(255) +# ── Dispatch table ──────────────────────────────────────────────────────────── - # If we get empty data, Alice has disconnected +BOB_DISPATCH = { + (STATE_IDLE, "PING"): handle_ping, +} + + +# ── Event loop ──────────────────────────────────────────────────────────────── + +async def run_bob(reader: StreamReader, writer: StreamWriter) -> None: + print("Bob: Alice connected.", flush=True) + rounds_done = 0 + state = STATE_IDLE + + while state != STATE_DONE: + # Entry action: PLAYING → send PONG → IDLE (or DONE) + if state == STATE_PLAYING: + rounds_done += 1 + writer.write(b"PONG\n") + print(f"Bob [round {rounds_done}]: sent PONG", flush=True) + state = STATE_IDLE if rounds_done < NUM_ROUNDS else STATE_DONE + continue + + data = await reader.readline() if not data: - print("Bob: Alice disconnected.", flush=True) + print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True) break + msg = data.decode("utf-8") + print(f"Bob [{state}]: received '{msg}'", flush=True) + + handler = BOB_DISPATCH.get((state, msg)) + + if handler is None: + print( + f"Bob [{state}]: no transition for '{msg}' — ignoring.", + flush=True, + ) + continue - message = data.decode("utf-8") - print(f"Bob: received '{message}'", flush=True) + state = await handler(writer) - # Decide on a reply - if message == "ping": - reply = "pong" - else: - reply = "no way!" + print(f"Bob: event loop finished (final state: {state}).", flush=True) - # Send the reply - print(f"Bob: sending '{reply}'", flush=True) - writer.write(reply.encode("utf-8")) - await writer.drain() +# ── Entry point ─────────────────────────────────────────────────────────────── if __name__ == "__main__": # Load configuration files — paths are relative to this script's location From 6ecc6cf8755f98bd127b49c80e6c352a9a72ac8c Mon Sep 17 00:00:00 2001 From: spoukke Date: Thu, 26 Mar 2026 13:49:00 +0100 Subject: [PATCH 257/277] make polite ping pong an extension to ping pong's state machines --- .../eventBased/politePingPong/politeAlice.py | 119 +++++++++--------- .../eventBased/politePingPong/politeBob.py | 83 +++++++----- 2 files changed, 108 insertions(+), 94 deletions(-) diff --git a/examples/eventBased/politePingPong/politeAlice.py b/examples/eventBased/politePingPong/politeAlice.py index e510c88b..da8f5f9f 100644 --- a/examples/eventBased/politePingPong/politeAlice.py +++ b/examples/eventBased/politePingPong/politeAlice.py @@ -1,36 +1,37 @@ """ Polite Ping-Pong — Alice (client). -Alice waits for Bob's "READY" signal before sending each "PING". After -NUM_ROUNDS pings she sends "BYE" instead, ending the exchange. +Extends plain ping pong with a greeting phase: Bob sends "HI" on connect, +Alice replies with "HI", then the game proceeds exactly as in ping pong. Alice's state diagram --------------------- - ┌─ (connect) ──────────────────────────────────────────┐ - │ │ - ▼ │ - WAITING_FOR_READY ──[recv "READY"]──► WAITING_FOR_PONG (start) - │ - recv "PONG" - │ - ▼ - WAITING_FOR_READY (next round) - ... after NUM_ROUNDS ... - │ - recv "READY" (last) - send "BYE" - ▼ - DONE + (connect) + │ + ▼ + WAITING_HI ──[recv "HI"]──► IDLE ← greeting phase + send "HI" │ + │ (entry action: send "PING") + ▼ + PLAYING + │ + recv "PONG" + ┌─────┴──────────────┐ + rounds left? done? + │ │ + ▼ ▼ + IDLE DONE Transition table: - Current state │ Message │ Action │ Next state - ────────────────────┼──────────┼─────────────────────────────┼────────────────── - WAITING_FOR_READY │ "READY" │ send "PING" (or "BYE") │ WAITING_FOR_PONG - │ │ │ (or DONE) - WAITING_FOR_PONG │ "PONG" │ — │ WAITING_FOR_READY + State │ Event │ Action │ Next state + ────────────┼──────────────┼──────────────┼──────────────── + WAITING_HI │ recv "HI" │ send "HI" │ IDLE + IDLE │ (entry) │ send "PING" │ PLAYING + PLAYING │ recv "PONG" │ — │ IDLE or DONE """ + from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -44,61 +45,53 @@ # ── States ─────────────────────────────────────────────────────────────────── -STATE_WAITING_FOR_READY = "WAITING_FOR_READY" -STATE_WAITING_FOR_PONG = "WAITING_FOR_PONG" # noqa: E221 -STATE_DONE = "DONE" # noqa: E221 - -# ── Mutable round counter ───────────────────────────────────────────────────── - -rounds_left = NUM_ROUNDS - - -# ── Handlers ───────────────────────────────────────────────────────────────── - -async def handle_ready(writer: StreamWriter) -> str: - global rounds_left - if rounds_left > 0: - rounds_left -= 1 - round_num = NUM_ROUNDS - rounds_left - writer.write(b"PING\n") - print(f"Alice [round {round_num}]: sent PING") - return STATE_WAITING_FOR_PONG - else: - writer.write(b"BYE\n") - print("Alice: sent BYE, done.") - return STATE_DONE - - -async def handle_pong(writer: StreamWriter) -> str: - print("Alice: received PONG") - return STATE_WAITING_FOR_READY - - -# ── Dispatch table ──────────────────────────────────────────────────────────── - -ALICE_DISPATCH = { - (STATE_WAITING_FOR_READY, "READY"): handle_ready, - (STATE_WAITING_FOR_PONG, "PONG"): handle_pong, # noqa: E241 -} +STATE_WAITING_HI = "WAITING_HI" +STATE_IDLE = "IDLE" # noqa: E221 +STATE_PLAYING = "PLAYING" # noqa: E221 +STATE_DONE = "DONE" # noqa: E221 # ── Event loop ─────────────────────────────────────────────────────────────── async def run_alice(reader: StreamReader, writer: StreamWriter) -> None: - global rounds_left - rounds_left = NUM_ROUNDS + rounds_done = 0 + + async def handle_hi(writer: StreamWriter) -> str: + """Transition: WAITING_HI ──[recv "HI"]──► IDLE""" + writer.write(b"HI\n") + print("Alice: sent HI — greeting done, game starting") + return STATE_IDLE + + async def handle_pong(_writer: StreamWriter) -> str: + """Transition: PLAYING ──[recv "PONG"]──► IDLE (or DONE)""" + nonlocal rounds_done + print(f"Alice [round {rounds_done}]: received PONG") + if rounds_done < NUM_ROUNDS: + return STATE_IDLE + return STATE_DONE + + dispatch = { + (STATE_WAITING_HI, "HI"): handle_hi, + (STATE_PLAYING, "PONG"): handle_pong, + } - state = STATE_WAITING_FOR_READY + state = STATE_WAITING_HI while state != STATE_DONE: + # Entry action: IDLE → send PING → PLAYING + if state == STATE_IDLE: + rounds_done += 1 + writer.write(b"PING\n") + print(f"Alice [round {rounds_done}]: sent PING") + state = STATE_PLAYING + data = await reader.readline() if not data: print(f"Alice [{state}]: connection dropped unexpectedly.") break msg = data.decode("utf-8") - print(f"Alice [{state}]: received '{msg}'") - handler = ALICE_DISPATCH.get((state, msg)) + handler = dispatch.get((state, msg)) if handler is None: print(f"Alice [{state}]: no transition for '{msg}' — ignoring.") diff --git a/examples/eventBased/politePingPong/politeBob.py b/examples/eventBased/politePingPong/politeBob.py index a7ae53a5..75869929 100644 --- a/examples/eventBased/politePingPong/politeBob.py +++ b/examples/eventBased/politePingPong/politeBob.py @@ -1,28 +1,36 @@ """ Polite Ping-Pong — Bob (server). -Bob sends "READY" immediately on connection and after each "PONG", signalling -to Alice that he is ready for the next round. He handles "PING" (reply PONG -then READY) and "BYE" (close). +Extends plain ping pong with a greeting phase: Bob sends "HI" immediately +on connect and waits for Alice's "HI" before the game begins. Bob's state diagram ------------------- - ┌─ (connect) ──────────────────────────────────────────┐ - │ send "READY" │ - ▼ │ -WAITING_FOR_PING_OR_BYE (start) - │ - recv "PING" → send "PONG", send "READY" → (stay) - recv "BYE" → DONE + (connect) → send "HI" + │ + ▼ + WAITING_HI ──[recv "HI"]──► IDLE ← greeting phase + │ + recv "PING" + ▼ + PLAYING + │ (entry action: send "PONG") + ┌─────┴──────────────┐ + rounds left? done? + │ │ + ▼ ▼ + IDLE DONE Transition table: - Current state │ Message │ Action │ Next state - ────────────────────────┼─────────┼──────────────────────────┼─────────────────────── - WAITING_FOR_PING_OR_BYE │ "PING" │ send "PONG", send "READY"│ WAITING_FOR_PING_OR_BYE - WAITING_FOR_PING_OR_BYE │ "BYE" │ — │ DONE + State │ Event │ Action │ Next state + ────────────┼──────────────┼──────────────┼──────────────── + WAITING_HI │ recv "HI" │ — │ IDLE + IDLE │ recv "PING" │ — │ PLAYING + PLAYING │ (entry) │ send "PONG" │ IDLE or DONE """ + from asyncio import StreamReader, StreamWriter from pathlib import Path @@ -32,32 +40,35 @@ from simulaqron.settings.network_config import NodeConfigType +NUM_ROUNDS = 5 + # ── States ─────────────────────────────────────────────────────────────────── -STATE_WAITING_FOR_PING_OR_BYE = "WAITING_FOR_PING_OR_BYE" -STATE_DONE = "DONE" # noqa: E221 +STATE_WAITING_HI = "WAITING_HI" +STATE_IDLE = "IDLE" # noqa: E221 +STATE_PLAYING = "PLAYING" # noqa: E221 +STATE_DONE = "DONE" # noqa: E221 # ── Handlers ───────────────────────────────────────────────────────────────── -async def handle_ping(writer: StreamWriter) -> str: - writer.write(b"PONG\n") - print("Bob: sent PONG", flush=True) - writer.write(b"READY\n") - print("Bob: sent READY", flush=True) - return STATE_WAITING_FOR_PING_OR_BYE +async def handle_hi(_writer: StreamWriter) -> str: + """Transition: WAITING_HI ──[recv "HI"]──► IDLE""" + print("Bob: received HI — greeting done, game starting", flush=True) + return STATE_IDLE -async def handle_bye(writer: StreamWriter) -> str: - print("Bob: received BYE, closing.", flush=True) - return STATE_DONE +async def handle_ping(_writer: StreamWriter) -> str: + """Transition: IDLE ──[recv "PING"]──► PLAYING""" + print("Bob: received PING", flush=True) + return STATE_PLAYING # ── Dispatch table ──────────────────────────────────────────────────────────── BOB_DISPATCH = { - (STATE_WAITING_FOR_PING_OR_BYE, "PING"): handle_ping, - (STATE_WAITING_FOR_PING_OR_BYE, "BYE"): handle_bye, # noqa: E241 + (STATE_WAITING_HI, "HI"): handle_hi, + (STATE_IDLE, "PING"): handle_ping, # noqa: E241 } @@ -65,17 +76,27 @@ async def handle_bye(writer: StreamWriter) -> str: async def run_bob(reader: StreamReader, writer: StreamWriter) -> None: print("Bob: Alice connected.", flush=True) + rounds_done = 0 - writer.write(b"READY\n") - print("Bob: sent READY", flush=True) - state = STATE_WAITING_FOR_PING_OR_BYE + # Greet Alice before starting the game. + writer.write(b"HI\n") + print("Bob: sent HI", flush=True) + state = STATE_WAITING_HI while state != STATE_DONE: + # Entry action: PLAYING → send PONG → IDLE (or DONE) + if state == STATE_PLAYING: + rounds_done += 1 + writer.write(b"PONG\n") + print(f"Bob [round {rounds_done}]: sent PONG", flush=True) + state = STATE_IDLE if rounds_done < NUM_ROUNDS else STATE_DONE + continue + data = await reader.readline() if not data: print(f"Bob [{state}]: connection dropped unexpectedly.", flush=True) break - msg = data.decode().strip() + msg = data.decode("utf-8") print(f"Bob [{state}]: received '{msg}'", flush=True) handler = BOB_DISPATCH.get((state, msg)) From b018d94518ee3f9f70d4c882ecb4defd3f739f7b Mon Sep 17 00:00:00 2001 From: Diego Rivera Date: Tue, 31 Mar 2026 16:55:38 +0200 Subject: [PATCH 258/277] Update documentation. Fix broken internal links when building the documentation. --- docs/ConfNodes.rst | 10 ++-- docs/Examples.rst | 55 +++++++++++------ docs/GettingStarted.rst | 42 ++++++------- docs/NetQASM.rst | 32 ++++++---- docs/Overview.rst | 4 +- docs/event-based/Overview.rst | 10 ++-- docs/event-based/PingPong.rst | 2 +- docs/event-based/PolitePingPong.rst | 2 +- docs/event-based/QuantumCorrRNG.rst | 2 +- docs/event-based/QuantumCorrRNGVerified.rst | 2 +- docs/index.rst | 66 ++++++++++++++------- docs/native-mode/CorrRng.rst | 40 ++++++++++--- docs/native-mode/GraphState.rst | 11 +++- docs/native-mode/Teleport.rst | 15 ++++- docs/new-sdk/CorrRNG.rst | 2 +- docs/new-sdk/ExtendGHZ.rst | 2 +- docs/new-sdk/MidCircuitLogic.rst | 4 +- docs/new-sdk/Overview.rst | 14 ++--- docs/new-sdk/Teleport.rst | 16 ++--- docs/new-sdk/Template.rst | 2 +- simulaqron/virtual_node/qutip_simulator.py | 2 +- 21 files changed, 216 insertions(+), 119 deletions(-) diff --git a/docs/ConfNodes.rst b/docs/ConfNodes.rst index ba1e99cc..c4f6bb47 100644 --- a/docs/ConfNodes.rst +++ b/docs/ConfNodes.rst @@ -5,7 +5,7 @@ SimulaQron uses two configuration files: * ``simulaqron_network.json`` — defines nodes, their socket ports, and network topology (described on this page) * ``simulaqron_settings.json`` — configures the simulation backend, timeouts, and other settings - (see the Settings section in `Getting Started `_) + (see the Settings section in :ref:`Configuring Settings `) ------------------------------------- Running all nodes on a single machine @@ -57,7 +57,7 @@ Each example in ``examples/new-sdk/`` and ``examples/nativeMode/`` includes a `` the SimulaQron backend and launches the node programs. This is the easiest way to try an example:: cd examples/new-sdk/corrRNG - sh run.sh + bash run.sh The ``run.sh`` script reads the ``simulaqron_network.json`` and ``simulaqron_settings.json`` in the example directory, so each example is self-contained. @@ -229,7 +229,7 @@ SimulaQron can automatically generate certain well-known network topologies: :math:`n(n-1)/2` for :math:`n` nodes. .. note:: Topology generation via the CLI is planned but not yet implemented. For now, specify topologies - directly in the ``simulaqron_network.json`` file (see `Network topologies`_ below). + directly in the ``simulaqron_network.json`` file (see `Network topologies`_ above). Along with setting up the network with the specified topology a .png figure is also generated and stored as config/topology.png. This is useful if a random network is used, to easily visualize the network used. @@ -259,5 +259,5 @@ To stop a specific network:: .. note:: By default the network name is "default". To have multiple networks running at the same time the nodes cannot use the same port numbers. -The JSON configuration file can hold more than one network configuration. See `Configuring the network`_ above for -an example with multiple networks. +The JSON configuration file can hold more than one network configuration. See `Writing the JSON config manually`_ +above for an example with multiple networks. diff --git a/docs/Examples.rst b/docs/Examples.rst index ec522263..75c614ba 100644 --- a/docs/Examples.rst +++ b/docs/Examples.rst @@ -1,5 +1,5 @@ SimulaQron Programming Examples -================================ +=============================== SimulaQron offers three ways to write quantum network programs, from highest-level to lowest-level: @@ -17,38 +17,57 @@ SimulaQron offers three ways to write quantum network programs, from highest-lev directly to SimulaQron's virtual quantum nodes. This is Python-specific and more verbose, but gives full control over the simulation backend. -The examples below assume that you have already made your way through `Getting Started `_: -you have the virtual node servers up and running. +.. _get-examples: + +----------------------- +How to get the examples +----------------------- + +The code of the examples can be found in `SimulaQron GitHub repository `_. +Clone this repository using ``git``:: + + git clone https://github.com/SoftwareQuTech/SimulaQron.git + +All the examples can be found in the ``examples`` folder. + +When running one of the examples mentioned below, we assume that you have already made your way through +:doc:`Getting Started ` and you have the virtual node servers up and running. + +.. _new-sdk-examples: ----------------- New SDK examples ----------------- -* `Overview `_ — Key concepts: ``NetQASMConnection``, ``EPRSocket``, ``flush()``, file structure -* `Template `_ — Getting started: single-node and client-server templates -* `CorrRNG `_ — EPR pairs between two nodes, correlated measurement -* `Teleport `_ — Quantum teleportation with classical correction messages -* `ExtendGHZ `_ — Three-party entanglement, multiple EPR sockets -* `MidCircuitLogic `_ — Multiple ``flush()`` calls for mid-circuit classical decisions +* :doc:`Overview ` — Key concepts: ``NetQASMConnection``, ``EPRSocket``, ``flush()``, file structure +* :doc:`Template ` — Getting started: single-node and client-server templates +* :doc:`CorrRNG ` — EPR pairs between two nodes, correlated measurement +* :doc:`Teleport ` — Quantum teleportation with classical correction messages +* :doc:`ExtendGHZ ` — Three-party entanglement, multiple EPR sockets +* :doc:`MidCircuitLogic ` — Multiple ``flush()`` calls for mid-circuit classical decisions + +.. _event-based-examples: --------------------- Event-based examples --------------------- -* `Overview `_ — Event-based programming model and state machines -* `PingPong `_ — Classical ping-pong between two nodes -* `PolitePingPong `_ — State-machine message dispatch pattern -* `QuantumCorrRNG `_ — Quantum correlated RNG with state machine -* `QuantumCorrRNGVerified `_ — Correlated RNG with verification protocol +* :doc:`Overview ` — Event-based programming model and state machines +* :doc:`PingPong ` — Classical ping-pong between two nodes +* :doc:`PolitePingPong ` — State-machine message dispatch pattern +* :doc:`QuantumCorrRNG ` — Quantum correlated RNG with state machine +* :doc:`QuantumCorrRNGVerified ` — Correlated RNG with verification protocol + +.. _native-mode-examples: --------------------- Native mode examples --------------------- -* `Template `_ — Template for programming in native (Twisted) mode -* `CorrRng `_ — Correlated randomness using native mode -* `Teleport `_ — Teleportation using native mode -* `GraphState `_ — Distributing a graph state across four nodes +* :doc:`Template ` — Template for programming in native (Twisted) mode +* :doc:`CorrRng ` — Correlated randomness using native mode +* :doc:`Teleport ` — Teleportation using native mode +* :doc:`GraphState ` — Distributing a graph state across four nodes .. toctree:: :hidden: diff --git a/docs/GettingStarted.rst b/docs/GettingStarted.rst index b0323e96..b4e36a80 100644 --- a/docs/GettingStarted.rst +++ b/docs/GettingStarted.rst @@ -21,7 +21,7 @@ you can install the *deadsnakes* repository to gain access to some specific pyth sudo add-apt-repository -y "ppa:deadsnakes/ppa" -After adding the repository, you can install the *full* version of python, including the development package and the :: +After adding the repository, you can install the *full* version of python, including the development package:: sudo apt-get install python3.12-full python3.12-dev @@ -56,10 +56,10 @@ Testing a simple example ------------------------ Before delving into how to write any program yourself, let's first simply run one of the existing examples. -Remember from the Overview that SimulaQron has two parts: the first are the virtual node servers that simulate -the hardware at each node as well as the quantum communication between them in a transparent manner. +Remember from the :doc:`Overview` that SimulaQron has two parts: the first are the virtual node servers +that simulate the hardware at each node as well as the quantum communication between them in a transparent manner. The second are the applications themselves which can be written in two ways: the direct way is to use the native -mode using the Python Twisted framework connecting to the virtual node servers (see `Examples `_), +mode using the Python Twisted framework connecting to the virtual node servers (see :ref:`Native mode examples `), and the recommended way is to use the NetQASM library that calls the virtual nodes via the NetQASM interface. We will here illustrate how to use SimulaQron with the NetQASM library. @@ -81,7 +81,7 @@ the backend of SimulaQron simply type:: responsibility for problems caused by SimulaQron. For more information on what ``simulaqron start`` does, how to change the nodes and the ports of the network, -the topology etc, see `Configuring the Network `_. +the topology etc, see :doc:`Configuring the Network `. To stop the backend, simply type:: @@ -99,7 +99,8 @@ Running a protocol ^^^^^^^^^^^^^^^^^^^ Having started the virtual quantum nodes as above, let us now run a simple test application, which already illustrates -some of the aspects in realizing protocols. +some of the aspects in realizing protocols. Before proceeding, please download the SimulaQron examples as shown in the +:ref:`How to get the examples` section. Our objective will be to realize the following protocol which will generate 1 shared random bit between Alice and Bob. Evidently, there would be classical means to achieve this trivial task chosen for illustration. @@ -108,11 +109,12 @@ Evidently, there would be classical means to achieve this trivial task chosen fo * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. -The examples can be found in ``examples/new-sdk/`` (see `Examples `_ for the full list). -Before seeing how this example works, let us simply run the code:: +We will follow the example located in ``examples/new-sdk/coorRNG`` (see :ref:`New SDK Examples ` for +the full list). Before seeing how this works, let us simply run the code (assuming you're already in the ``SimulaQron`` +folder cloned from GitHub):: cd examples/new-sdk/corrRNG - sh run.sh + bash run.sh You should be seeing the following two lines:: @@ -125,21 +127,21 @@ going on here? Let us first look at how we will realize the example by making an * Alice and Bob generate one EPR pair, that is, two maximally entangled qubits :math:`A` and :math:`B` of the form :math:`|\Psi\rangle_{AB} = \frac{1}{\sqrt{2}} \left(|0\rangle_A |0\rangle_B + |1\rangle_A |1\rangle_B\right)` -* Alice and Bob are informed of the identifiers of the qubits and are informed that entanglement was generated. - * Both Alice and Bob measure their respective qubits to obtain a classical random number :math:`x \in \{0,1\}`. -While the task we want to realize here is completely trivial, the addition of step 3 does however already highlight a -range of choices on how to realize step 3 and the need to find good abstractions to allow easy application development. -One way to realize step 3 would be to hardwire Alice's and Bob's measurements: if the hardware can identify the +While the task we want to realize here is completely trivial, the addition of step 2 does however already highlight a +range of choices on how to realize step 2 and the need to find good abstractions to allow easy application development. +One way to realize step 2 would be to hardwire Alice's and Bob's measurements: if the hardware can identify the correct qubits from the entanglement generation, then we could instruct it to measure it immediately without asking for a notification from the entanglement generation process. It is clear that in a network that is a bit larger than -our tiny three node setup, identifying the right setup requires a link between the underlying qubits and classical +our tiny two node setup, identifying the right setup requires a link between the underlying qubits and classical control information: this is the objective of the classical/quantum combiner. The script ``run.sh`` executes the following two python scripts:: - #!/bin/sh + #!/usr/bin/env bash + + # Some code to start SimulaQron backend python3 aliceTest.py python3 bobTest.py & @@ -188,7 +190,7 @@ Similarly the core of ``bobTest.py`` is:: m1_val = int(m1) sim_conn.close() -For further examples, see `Examples `_ and `The NetQASM Interface `_ for the full SDK reference. +For further examples, see :doc:`Examples ` and :doc:`The NetQASM Interface ` for the full SDK reference. .. _settings: @@ -207,7 +209,7 @@ To set a setting, for example to use the projectQ backend, type:: This will create a file named ``simulaqron_settings.json`` in the current folder. This new file contains a full set of simulaqron configuration, including the setting that was just configured (using the `projectq` backend, in the example). -It is also possible to manually create this` `simulaqron_settings.json`` file with any text editor:: +It is also possible to manually create this ``simulaqron_settings.json`` file with any text editor:: { "backend": "projectq", @@ -247,7 +249,7 @@ implement using simulaqron. it, set the settings and start it again. It is also possible to create the default SimulaQron network configuration in the current folder. Check the -`Configuring the Network `_ document to check how to achieve this. +:doc:`Configuring the Network ` document to check how to achieve this. ^^^^^^^^^^^^^^^^^^^ Settings precedence @@ -270,7 +272,7 @@ Settings Fields The SimulaQron settings file contains a set of fields to control the configurations of the SimulaQron simulation: * ``max_qubit``: Maximum number of qubits to simulate on the Virtual Node. -* ``max_registers``: Maximmum number of registers to use in the Virtual Node. +* ``max_registers``: Maximum number of registers to use in the Virtual Node. * ``conn_retry_time``: Number of seconds to wait between connection retries. * ``conn_max_retries``: Maximum number of times to retry a connection before failing the whole execution. * ``recv_timeout``: Maximum number of milliseconds to wait for the messages when trying to create EPR pairs. diff --git a/docs/NetQASM.rst b/docs/NetQASM.rst index 7ec519e3..507a027b 100644 --- a/docs/NetQASM.rst +++ b/docs/NetQASM.rst @@ -2,16 +2,14 @@ The NetQASM interface ===================== SimulaQron applications are written using the **NetQASM SDK**. This page describes the core concepts and -programming model. For complete working examples, see `Examples `_. +programming model. For complete working examples, see :doc:`Examples `. ------------ Installation ------------ -The NetQASM library is included as a dependency of SimulaQron. Installing SimulaQron automatically installs -everything you need:: - - pip3 install simulaqron +The NetQASM library is included as a dependency of SimulaQron. For the instructions how to install SimulaQron +please check the :doc:`Getting Started` section. -------------- Core concepts @@ -94,7 +92,7 @@ You can call ``flush()`` multiple times on the same connection. This enables **m other_qubit.X() # conditional correction conn.flush() -See the mid-circuit logic example in `Examples `_ for a full demonstration. +See the mid-circuit logic example in :doc:`Examples ` for a full demonstration. --------------- Minimal example @@ -102,6 +100,8 @@ Minimal example A single-node program that creates a qubit, applies a Hadamard gate, and measures:: + from netqasm.runtime.settings import set_simulator + set_simulator("simulaqron") from netqasm.sdk.external import NetQASMConnection from netqasm.sdk import Qubit @@ -113,6 +113,9 @@ A single-node program that creates a qubit, applies a Hadamard gate, and measure print("Measurement outcome:", int(m)) conn.close() +If you want to run this minimal example, remember to start the SimulaQron backend before executing the code: +``simulaqron start``. + -------------------- Two-node EPR example -------------------- @@ -139,7 +142,9 @@ Alice and Bob generate an EPR pair and each measure their qubit to get correlate print("Bob:", int(m)) conn.close() -Both sides will print the same random number (0 or 1), demonstrating quantum correlation. +Both sides will print the same random number (0 or 1), demonstrating quantum correlation. A full working example +of this can be found in the ``new-sdk/corrRNG`` example. See the :doc:`Correlated RNG` section for +more details. ----------------------- Classical communication @@ -176,8 +181,8 @@ receiving classical messages:: conn.flush() conn.close() -See the `Template `_ page for how to set up the client and server, and the teleportation example -for a complete two-node program with classical messaging. +See the :doc:`Template ` page for how to set up the client and server, and the +:doc:`teleportation example ` for a complete two-node program with classical messaging. ----------------------- Configuration @@ -185,9 +190,10 @@ Configuration Each program needs two configuration files in its directory: -* ``simulaqron_network.json`` — defines the nodes and their socket ports. See `Configuring the Network `_ for details. +* ``simulaqron_network.json`` — defines the nodes and their socket ports. See :doc:`Configuring the Network ` + for details. * ``simulaqron_settings.json`` — configures the simulation backend and other settings. See the - Settings section in `Getting Started `_. + Settings section in :doc:`Getting Started `. The ``stabilizer`` backend is used by default and is recommended unless you need non-Clifford gates (use ``qutip`` in that case). @@ -196,6 +202,6 @@ The ``stabilizer`` backend is used by default and is recommended unless you need Further reading ----------------------- -* `Examples `_ — complete working examples from simple to complex -* `New SDK Overview `_ — detailed SDK concepts and file structure +* :doc:`Examples ` — complete working examples from simple to complex +* :doc:`New SDK Overview ` — detailed SDK concepts and file structure * `NetQASM library documentation `_ diff --git a/docs/Overview.rst b/docs/Overview.rst index b2d967d4..815da452 100644 --- a/docs/Overview.rst +++ b/docs/Overview.rst @@ -15,11 +15,11 @@ In the light of the alternate interface below it may appear inefficient to expor the purpose of SimulaQron is precisely to explore and play with higher layer abstractions on top of any hardware, or its simulated version, SimulaQron. As such it is best to think of SimulaQron as a piece of simulated hardware with its own native interface, which we may first abstract into a higher level command language for programming. Examples of -how to program SimulaQron in native mode can be found in `Examples `_. +how to program SimulaQron in native mode can be found in :doc:`Examples `. The second way to run applications is via a higher level interface, the NetQASM interface. If you want your applications to later use real quantum hardware more easily instead of SimulaQron, then this is the interface to use. -Examples of how to program using the NetQASM can be found in `The NetQASM Interface `_. +Examples of how to program using the NetQASM can be found in :doc:`The NetQASM Interface `. .. image:: figs/netqasm_architecture.png :width: 500px diff --git a/docs/event-based/Overview.rst b/docs/event-based/Overview.rst index 05ac2f10..776b04f0 100644 --- a/docs/event-based/Overview.rst +++ b/docs/event-based/Overview.rst @@ -5,7 +5,7 @@ The event-based examples show how to structure quantum network protocols using a **state machine** pattern. This is the recommended approach for protocols that interleave classical negotiation with quantum operations. -Prerequisites: you should understand the `New SDK Overview <../new-sdk/Overview.rst>`_ first. +Prerequisites: you should understand the :doc:`New SDK Overview <../new-sdk/Overview>` first. The state machine pattern ------------------------- @@ -68,11 +68,11 @@ The examples progress from purely classical to quantum: * - Example - What it teaches - * - `PingPong `_ + * - :doc:`PingPong ` - Basic event loop with simple if/else message handling - * - `PolitePingPong `_ + * - :doc:`PolitePingPong ` - Full state machine pattern with dispatch table - * - `QuantumCorrRNG `_ + * - :doc:`QuantumCorrRNG ` - Adding quantum operations (EPR + measure) to event handlers - * - `QuantumCorrRNGVerified `_ + * - :doc:`QuantumCorrRNGVerified ` - Multi-state protocol: negotiate, quantum, then classical verification diff --git a/docs/event-based/PingPong.rst b/docs/event-based/PingPong.rst index 60796191..2ff9b666 100644 --- a/docs/event-based/PingPong.rst +++ b/docs/event-based/PingPong.rst @@ -60,7 +60,7 @@ Running :: cd examples/eventBased/pingPong - sh run.sh + bash run.sh Expected output:: diff --git a/docs/event-based/PolitePingPong.rst b/docs/event-based/PolitePingPong.rst index 81b00a14..67254ece 100644 --- a/docs/event-based/PolitePingPong.rst +++ b/docs/event-based/PolitePingPong.rst @@ -91,4 +91,4 @@ Running :: cd examples/eventBased/politePingPong - sh run.sh + bash run.sh diff --git a/docs/event-based/QuantumCorrRNG.rst b/docs/event-based/QuantumCorrRNG.rst index bc956c77..60f38957 100644 --- a/docs/event-based/QuantumCorrRNG.rst +++ b/docs/event-based/QuantumCorrRNG.rst @@ -98,7 +98,7 @@ Running :: cd examples/eventBased/quantumCorrRNG - sh run.sh + bash run.sh .. note:: Unlike the purely classical examples, this one requires the SimulaQron backend to be running. The ``run.sh`` script starts it automatically. diff --git a/docs/event-based/QuantumCorrRNGVerified.rst b/docs/event-based/QuantumCorrRNGVerified.rst index a84ecfc1..7049a7b9 100644 --- a/docs/event-based/QuantumCorrRNGVerified.rst +++ b/docs/event-based/QuantumCorrRNGVerified.rst @@ -118,7 +118,7 @@ Running :: cd examples/eventBased/quantumCorrRNGVerified - sh run.sh + bash run.sh Expected output:: diff --git a/docs/index.rst b/docs/index.rst index cd591076..2e8ed83c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,36 +22,62 @@ Key features Quick start ----------- -1. **Install**:: +1. **Install dependencies**:: - pip3 install simulaqron + sudo add-apt-repository -y "ppa:deadsnakes/ppa" + sudo apt-get install python3.12-full python3.12-dev + sudo apt-get install build-essential cmake linux-headers-generic -2. **Configure your network** — create a ``simulaqron_network.json`` defining nodes and ports - (see `Configuring the Network `_) +2. **Create a python virtual environment**:: -3. **Write your program** using the NetQASM SDK:: + python3.12 -m venv simulaqron-venv - from netqasm.sdk.external import NetQASMConnection - from netqasm.sdk import Qubit +3. **Activate the virtual environment**:: - conn = NetQASMConnection("Alice") - q = Qubit(conn) - q.H() - m = q.measure() - conn.flush() # execute queued operations - print(int(m)) # read measurement result - conn.close() + source simulaqron-venv/bin/activate -4. **Run examples** — see `Examples `_ for complete working programs +4. **Install SimulaQron**:: + + pip3 install simulaqron + +5. **Configure your network** — create a ``simulaqron_network.json`` defining nodes and ports + (see :doc:`Configuring the Network `) + +6. **Start the simulaqron backend**:: + + simulaqron start + +7. **Write your first program** using the NetQASM SDK. Save this code as ``program.py``:: + + from netqasm.runtime.settings import set_simulator + set_simulator("simulaqron") + from netqasm.sdk.external import NetQASMConnection + from netqasm.sdk import Qubit + + conn = NetQASMConnection("Alice") + q = Qubit(conn) + q.H() + m = q.measure() + conn.flush() # execute queued operations + print(f"Qubit measurement: {int(m)}") # read measurement result + conn.close() + +8. **Execute your program**:: + + python program.py + +9. **Check the output**. Output should be ``Qubit measurement: 0/1``. Measurement should randomly be ``0`` or ``1``. + +10. **Run other more complex examples** — see :doc:`Examples ` for complete working programs Where to go next ---------------- -* **New to SimulaQron?** Start with `Getting Started `_ for installation and your first example -* **Want to write programs?** See `The NetQASM Interface `_ for the NetQASM SDK reference -* **Looking for examples?** See `Examples `_ — new SDK, event-based, and native-mode examples -* **Configuring networks and settings?** See `Configuring the Network `_ -* **Architecture and internals?** See `Overview `_ +* **New to SimulaQron?** Start with :doc:`Getting Started ` for installation and your first example +* **Want to write programs?** See :doc:`The NetQASM Interface ` for the NetQASM SDK reference +* **Looking for examples?** See :doc:`Examples ` — new SDK, event-based, and native-mode examples +* **Configuring networks and settings?** See :ref:`Configuring the Network ` and :ref:`Settings `. +* **Architecture and internals?** See :doc:`Overview ` We also have a `paper `_ describing the design of SimulaQron, freely available on `arxiv `_. diff --git a/docs/native-mode/CorrRng.rst b/docs/native-mode/CorrRng.rst index 76598061..0af23507 100644 --- a/docs/native-mode/CorrRng.rst +++ b/docs/native-mode/CorrRng.rst @@ -2,7 +2,7 @@ Generate correlated randomness ============================== .. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. - See `New SDK Overview <../new-sdk/Overview.rst>`_ and the SDK version of this example at `CorrRNG <../new-sdk/CorrRNG.rst>`_. + See :doc:`New SDK Overview <../new-sdk/Overview>` and the :doc:`new SDK version of this example <../new-sdk/CorrRNG>`. Having started the virtual quantum nodes, let us now run a simple test application, which already illustrates some of the aspects in realizing protocols. Our objective will be to realize the following protocol which will generate 1 @@ -17,7 +17,7 @@ for illustration. Before seeing how this example works, let us again simply run the code:: cd examples/nativeMode/corrRNG - sh doNew.sh + bash doNew.sh Next to a considerable about of debugging information, you should be seeing the following two lines:: @@ -53,9 +53,8 @@ delay causing the qubit to decohere in the meantime. To realize this, we thus need not only the connection to the virtual quantum node servers, but Alice and Bob themselves need to run a client/server to exchange classical control information. Before looking at the code, we know that the setup of these servers is again determined by a configuration file, namely ``classicalNet.json``. -This file defines which nodes act as servers in the classical communication network listening for control information -to execute the protocol. You want to copy this to whatever example you are running. It takes the JSON format, -where in our example we have Alice and Bob:: +You want to copy this to whatever example you are running. It takes the JSON format, where in our example we +have Alice and Bob:: { "default": { @@ -75,10 +74,10 @@ where in our example we have Alice and Bob:: } } -The first thing that happens if we execute the script doNew.sh is that after some setting up it will call run.sh, +The first thing that happens if we execute the script ``doNew.sh`` is that after some setting up it will call ``run.sh``, executing:: - #!/bin/sh + #!/usr/bin/env bash python3 bobTest.py & python3 aliceTest.py @@ -162,3 +161,30 @@ Not included in the code below are several standard methods that require no chan x = yield qB.callRemote("measure") print("BOB: My Random Number is ", x, "\n") + +-------- +Starting +-------- + +We first start the virtual quantum node backend, by executing:: + + simulaqron start --nodes=Alice,Bob --network-config-file classicalNet.json + +We then start up the programs for Alice and Bob themselves. These will connect to the virtual quantum nodes, and +execute the quantum commands and classical communication outlined above, in the same directory as we placed +``classicalNet.json``:: + + python3 bobTest.py & + python3 aliceTest.py + +You can easily start everything by using the a single helper script:: + + bash doNew.sh + +------- +Stoping +------- + +You can stop all the running processes by using the helper script:: + + bash terminate.sh diff --git a/docs/native-mode/GraphState.rst b/docs/native-mode/GraphState.rst index 689d8eb6..2148ad08 100644 --- a/docs/native-mode/GraphState.rst +++ b/docs/native-mode/GraphState.rst @@ -2,7 +2,7 @@ Distributing a graph state ========================== .. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. - See `New SDK Overview <../new-sdk/Overview.rst>`_. + See :doc:`New SDK Overview <../new-sdk/Overview>`. Here we consider a more complicated example, where we have four parties; Alice, Bob, Charlie and David. They will distribute a graph state and transform this with local operations and classical communication to make @@ -446,3 +446,12 @@ simulaqron_network.json:: You can easily start everything by using the a single helper script:: sh doNew.sh + +------- +Stoping +------- + +You can stop all the running processes by using the helper script:: + + bash terminate.sh + diff --git a/docs/native-mode/Teleport.rst b/docs/native-mode/Teleport.rst index 6ff4d30c..65f92aab 100644 --- a/docs/native-mode/Teleport.rst +++ b/docs/native-mode/Teleport.rst @@ -2,11 +2,11 @@ Teleporting a Qubit =================== .. note:: Native mode is the low-level Twisted interface. For new projects, the NetQASM SDK is recommended. - See `New SDK Overview <../new-sdk/Overview.rst>`_ and the SDK version of this example at `Teleport <../new-sdk/Teleport.rst>`_. + See :doc:`New SDK Overview <../new-sdk/Overview>` and the :doc:`new SDK version of this example <../new-sdk/Teleport>`. Let's now consider a very simple protocol, in which Alice first generates an EPR pair with Bob, and then teleports a qubit to Bob. To program it in SimulaQron's native mode, we will use the template described in -`Template `_. +:doc:`Template