From 2bf32ca647559af0ec4e379f21b8e9b3f71ace58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20S=C3=A1nchez=20Ram=C3=ADrez?= <15837247+mofeing@users.noreply.github.com> Date: Mon, 23 Sep 2024 11:44:32 -0400 Subject: [PATCH] Convert Quimb TensorNetwork types to `TensorNetwork`, `Quantum` (#207) * Prepare TenetPythonCallExt for more Python packages * Convert from Quimb's TensorNetwork to `TensorNetwork` * Refactor type check for Qiskit QuantumCircuit conversion to `Tenet.Quantum` * Fix type check for Quimb's TensorNetwork conversion to `Tenet.TensorNetwork` * Implement Quimb's TensorNetworkGenVector, TensorNetworkGenOperator conversion to `Quantum` * Test Qiskit, Quimb integration * Fix conversion issues * Move CondaPkg.toml to project root Otherwise, `Pkg.test()` won't detect it. * Fix quimb Circuit option * Wrap python tests into a testset --- .gitignore | 164 ++++++++++++++++++ CondaPkg.toml | 5 + .../Qiskit.jl} | 22 +-- ext/TenetPythonCallExt/Quimb.jl | 44 +++++ ext/TenetPythonCallExt/TenetPythonCallExt.jl | 22 +++ test/Project.toml | 1 + test/integration/python/test_qiskit.jl | 18 ++ test/integration/python/test_quimb.jl | 19 ++ test/runtests.jl | 5 + 9 files changed, 286 insertions(+), 14 deletions(-) create mode 100644 CondaPkg.toml rename ext/{TenetPythonCallExt.jl => TenetPythonCallExt/Qiskit.jl} (75%) create mode 100644 ext/TenetPythonCallExt/Quimb.jl create mode 100644 ext/TenetPythonCallExt/TenetPythonCallExt.jl create mode 100644 test/integration/python/test_qiskit.jl create mode 100644 test/integration/python/test_quimb.jl diff --git a/.gitignore b/.gitignore index f80ed5c3..0914aac9 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,167 @@ $RECYCLE.BIN/ .julia *.excalidraw archive/ +test/.CondaPkg/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/CondaPkg.toml b/CondaPkg.toml new file mode 100644 index 00000000..3dc251e5 --- /dev/null +++ b/CondaPkg.toml @@ -0,0 +1,5 @@ +[deps] +qiskit = "" + +[pip.deps] +quimb = "" diff --git a/ext/TenetPythonCallExt.jl b/ext/TenetPythonCallExt/Qiskit.jl similarity index 75% rename from ext/TenetPythonCallExt.jl rename to ext/TenetPythonCallExt/Qiskit.jl index fed712d2..8dbc2f15 100644 --- a/ext/TenetPythonCallExt.jl +++ b/ext/TenetPythonCallExt/Qiskit.jl @@ -1,15 +1,11 @@ -module TenetPythonCallExt - -using Tenet -using PythonCall -using PythonCall.Core: pyisnone - -pyfullyqualname(pyobj) = join([pytype(pyobj).__module__, pytype(pyobj).__qualname__], '.') - -function Tenet.Quantum(pyobj::Py) - pyclassname = pyfullyqualname(pyobj) - if pyclassname != "qiskit.circuit.quantumcircuit.QuantumCircuit" - throw(ArgumentError("Expected a Qiskit's QuantumCircuit object, got $pyclassname")) +function Tenet.Quantum(::Val{:qiskit}, pyobj::Py) + qiskit = pyimport("qiskit") + if !pyissubclass(pytype(pyobj), qiskit.circuit.quantumcircuit.QuantumCircuit) + throw( + ArgumentError( + "Expected a qiskit.circuit.quantumcircuit.QuantumCircuit object, got $(pyfullyqualname(pyobj))" + ), + ) end n = length(pyobj.qregs[0]) @@ -54,5 +50,3 @@ function Tenet.Quantum(pyobj::Py) return Quantum(tn, sites) end - -end diff --git a/ext/TenetPythonCallExt/Quimb.jl b/ext/TenetPythonCallExt/Quimb.jl new file mode 100644 index 00000000..b482689f --- /dev/null +++ b/ext/TenetPythonCallExt/Quimb.jl @@ -0,0 +1,44 @@ +function Tenet.TensorNetwork(::Val{:quimb}, pyobj::Py) + quimb = pyimport("quimb") + if !pyissubclass(pytype(pyobj), quimb.tensor.tensor_core.TensorNetwork) + throw(ArgumentError("Expected a quimb.tensor.tensor_core.TensorNetwork object, got $(pyfullyqualname(pyobj))")) + end + + ts = map(pyobj.tensors) do tensor + array = pyconvert(Array, tensor.data) + inds = Symbol.(pyconvert(Array, tensor.inds)) + Tensor(array, inds) + end + + return TensorNetwork(ts) +end + +function Tenet.Quantum(::Val{:quimb}, pyobj::Py) + quimb = pyimport("quimb") + if pyissubclass(pytype(pyobj), quimb.tensor.circuit.Circuit) + return Quantum(pyobj.get_uni()) + elseif pyissubclass(pytype(pyobj), quimb.tensor.tensor_arbgeom.TensorNetworkGenVector) + return Quantum(Val(Symbol("quimb.tensor.tensor_arbgeom.TensorNetworkGenVector")), pyobj) + elseif pyissubclass(pytype(pyobj), quimb.tensor.tensor_arbgeom.TensorNetworkGenOperator) + return Quantum(Val(Symbol("quimb.tensor.tensor_arbgeom.TensorNetworkGenOperator")), pyobj) + else + throw(ArgumentError("Unknown treatment for object of class $(pyfullyqualname(pyobj))")) + end +end + +function Tenet.Quantum(::Val{Symbol("quimb.tensor.tensor_arbgeom.TensorNetworkGenVector")}, pyobj::Py) + tn = TensorNetwork(pyobj) + sitedict = Dict(Site(pyconvert(Int, i)) => pyconvert(Symbol, pyobj.site_ind(i)) for i in pyobj.sites) + return Quantum(tn, sitedict) +end + +function Tenet.Quantum(::Val{Symbol("quimb.tensor.tensor_arbgeom.TensorNetworkGenOperator")}, pyobj::Py) + tn = TensorNetwork(pyobj) + + sitedict = merge!( + Dict(Site(pyconvert(Int, i)) => pyconvert(Symbol, pyobj.lower_ind(i)) for i in pyobj.sites), + Dict(Site(pyconvert(Int, i); dual=true) => pyconvert(Symbol, pyobj.upper_ind(i)) for i in pyobj.sites), + ) + + return Quantum(tn, sitedict) +end diff --git a/ext/TenetPythonCallExt/TenetPythonCallExt.jl b/ext/TenetPythonCallExt/TenetPythonCallExt.jl new file mode 100644 index 00000000..836cae6c --- /dev/null +++ b/ext/TenetPythonCallExt/TenetPythonCallExt.jl @@ -0,0 +1,22 @@ +module TenetPythonCallExt + +using Tenet +using PythonCall +using PythonCall.Core: pyisnone + +pyfullyqualname(pyobj) = join([pytype(pyobj).__module__, pytype(pyobj).__qualname__], '.') + +function Tenet.TensorNetwork(pyobj::Py) + pymodule, _ = split(pyconvert(String, pytype(pyobj).__module__), "."; limit=2) + return TensorNetwork(Val(Symbol(pymodule)), pyobj) +end + +function Tenet.Quantum(pyobj::Py) + pymodule, _ = split(pyconvert(String, pytype(pyobj).__module__), "."; limit=2) + return Quantum(Val(Symbol(pymodule)), pyobj) +end + +include("Qiskit.jl") +include("Quimb.jl") + +end diff --git a/test/Project.toml b/test/Project.toml index 7edd2bd1..68d94e6d 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -16,6 +16,7 @@ NetworkLayout = "46757867-2c16-5918-afeb-47bfcb05e46a" OMEinsum = "ebe7aa44-baf0-506c-a96f-8464559b3922" Permutations = "2ae35dd2-176d-5d53-8349-f30d82d94d4f" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" Quac = "b9105292-1415-45cf-bff1-d6ccf71e6143" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/test/integration/python/test_qiskit.jl b/test/integration/python/test_qiskit.jl new file mode 100644 index 00000000..99c32bcf --- /dev/null +++ b/test/integration/python/test_qiskit.jl @@ -0,0 +1,18 @@ +@testset "qiskit" begin + using PythonCall + qiskit = pyimport("qiskit") + + circuit = qiskit.QuantumCircuit(3) + circuit.h(0) + circuit.h(1) + circuit.cx(1, 2) + circuit.cx(0, 2) + circuit.h(0) + circuit.h(1) + circuit.h(2) + + tn = Tenet.Quantum(circuit) + @test issetequal(sites(tn; set=:inputs), adjoint.(Site.([1, 2, 3]))) + @test issetequal(sites(tn; set=:outputs), Site.([1, 2, 3])) + @test Tenet.ntensors(tn) == 7 +end diff --git a/test/integration/python/test_quimb.jl b/test/integration/python/test_quimb.jl new file mode 100644 index 00000000..683908c3 --- /dev/null +++ b/test/integration/python/test_quimb.jl @@ -0,0 +1,19 @@ +@testset "quimb" begin + using PythonCall + qtn = pyimport("quimb.tensor") + + # NOTE quimb.circuit.Circuit splits gates by default + qc = qtn.Circuit(3; gate_opts=Dict(["contract" => false])) + gates = [("H", 0), ("H", 1), ("CNOT", 1, 2), ("CNOT", 0, 2), ("H", 0), ("H", 1), ("H", 2)] + qc.apply_gates(gates) + + tn = Tenet.Quantum(qc) + @test issetequal(sites(tn; set=:inputs), adjoint.(Site.([0, 1, 2]))) + @test issetequal(sites(tn; set=:outputs), Site.([0, 1, 2])) + @test Tenet.ntensors(tn) == 7 + + tn = Tenet.Quantum(qc.psi) + @test isempty(sites(tn; set=:inputs)) + @test issetequal(sites(tn; set=:outputs), Site.([0, 1, 2])) + @test Tenet.ntensors(tn) == 10 +end diff --git a/test/runtests.jl b/test/runtests.jl index 6a9bc8e7..e830be92 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -25,6 +25,11 @@ if VERSION >= v"1.10" include("integration/Quac_test.jl") include("integration/ITensors_test.jl") include("integration/ITensorNetworks_test.jl") + + @testset "Python" begin + include("integration/python/test_quimb.jl") + include("integration/python/test_qiskit.jl") + end end end