Julia AOT plugins¶
This is the Julia counterpart to 01_c_plugins.ipynb and 02_rust_plugins.ipynb.
The plugin¶
uniform_noise is the one plugin that has both a propaq built-in
(UniformNoiseModel) and a C counterpart, so a single build is enough to check
correctness from both sides. Point PLUGIN at any other .jl file to build
that one instead.
from pathlib import Path
PLUGIN_DIR = Path("../julia").resolve()
BUILD_DIR = Path("_build").resolve()
BUILD_DIR.mkdir(exist_ok=True)
PLUGIN = "uniform_noise"
KIND = "noise" # "noise" or "trunc"
CONFIG = '{"damping": 0.01}'
SUBDIR = {"noise": "noise", "trunc": "truncation"}
JL_SRC = PLUGIN_DIR / SUBDIR[KIND] / f"{PLUGIN}.jl"
assert JL_SRC.exists(), JL_SRC
print(JL_SRC)
Build setup¶
Every .jl file under examples/plugins/julia/ is a standalone script.
PackageCompiler.create_library does not accept that directly, rather it
expects a directory containing a Julia package (a Project.toml plus a
src/<Name>.jl entry point).
import subprocess
result = subprocess.run(
[
"julia", "-e",
'''
using PackageCompiler
create_library("../julia/noise", "/tmp/_propaq_aot_probe")
''',
],
capture_output=True, text=True, timeout=120,
)
print("exit code:", result.returncode)
print(result.stderr.strip().splitlines()[0])
exit code: 1
ERROR: could not find project at "../julia/noise"
Wrapping the plugin in a package¶
We wrap the file in a thin package so that it's recognized.
import uuid
PKG_ROOT = BUILD_DIR / "packages"
def module_name(name):
return "".join(part.capitalize() for part in name.split("_")) + "Plugin"
MOD = module_name(PLUGIN)
PKG_DIR = PKG_ROOT / MOD
(PKG_DIR / "src").mkdir(parents=True, exist_ok=True)
(PKG_DIR / "Project.toml").write_text(
f'name = "{MOD}"\n'
f'uuid = "{uuid.uuid5(uuid.NAMESPACE_URL, f"propaq-plugin/{PLUGIN}")}"\n'
f'version = "0.1.0"\n'
)
(PKG_DIR / "src" / f"{MOD}.jl").write_text(
f"module {MOD}\n\ninclude(\"{JL_SRC}\")\n\nend # module\n"
)
print(f"{PLUGIN:<24} -> {PKG_DIR.relative_to(BUILD_DIR)}")
uniform_noise -> packages/UniformNoisePlugin
Building the library¶
create_library takes the wrapper package and emits
<dest>/lib/lib<lib_name>.so (.dylib on macOS). This is the slow step, so expect minutes, not seconds. The build is
skipped if the .so is already there.
LIB_ROOT = BUILD_DIR / "libs"
SO = LIB_ROOT / PLUGIN / "lib" / f"lib{PLUGIN}.so"
if not SO.exists():
print(f"building {PLUGIN} ...", flush=True)
subprocess.run(
[
"julia", "--startup-file=no", "-e",
f'''
using PackageCompiler
create_library(
"{PKG_DIR}", "{LIB_ROOT / PLUGIN}";
lib_name="{PLUGIN}",
force=true,
incremental=true,
)
''',
],
check=True,
)
SO = str(SO)
print("Built:", SO)
Circuit construction¶
We use the same circuit construction as the other notebooks.
%%writefile _build/harness.py
import random
from propaq import PauliString
from propaq.circuits import PauliCircuit, PauliRotation
from propaq.datatypes import PauliTermSum
from propaq.propagators import PauliPropagator
N_QUBITS = 4
random.seed(0)
def random_circuit(depth=40):
rotations = []
for _ in range(depth):
x = random.randint(0, 2**N_QUBITS - 1)
z = random.randint(0, 2**N_QUBITS - 1)
if x == 0 and z == 0:
x = 1
rotations.append(PauliRotation(PauliString(x, z, N_QUBITS), random.uniform(0.05, 0.6)))
return PauliCircuit(rotations)
def observable():
ts = PauliTermSum()
ts.add(PauliString(0, 1, N_QUBITS), 1.0) # Z on qubit 0
return ts
CIRCUIT = random_circuit()
OBSERVABLE = observable()
def run(noise=None, truncation=None, n_threads=4):
prop = PauliPropagator(noise=noise, truncation=truncation, n_threads=n_threads)
return prop.expectation_value(OBSERVABLE, CIRCUIT, initial_state=0).expectation_value
Overwriting _build/harness.py
import json
import sys
from propaq.noise import NativeNoiseModel, UniformNoiseModel
from propaq.truncation import NativeTruncator
sys.path.insert(0, str(BUILD_DIR))
from harness import CIRCUIT, run
print("Circuit depth:", len(CIRCUIT.rotations))
Circuit depth: 40
Comparison to propaq built-in models¶
uniform_noise implements exactly the same formula as UniformNoiseModel, so
the two should agree.
if KIND == "noise":
native = run(noise=NativeNoiseModel(SO, config=CONFIG))
else:
native = run(truncation=NativeTruncator(SO, config=CONFIG))
print(f"{PLUGIN:<24} native={native!r}")
if PLUGIN == "uniform_noise":
built_in = run(noise=UniformNoiseModel(damping=json.loads(CONFIG)["damping"]))
print(f"{'UniformNoiseModel':<24} built-in={built_in!r} match={native == built_in}")
uniform_noise native=0.2826014215933488 UniformNoiseModel built-in=0.2826014215933488 match=True
C vs Julia¶
The same plugin exists as a C source under examples/plugins/c/.
import textwrap
C_SRC = Path("../c").resolve() / SUBDIR[KIND] / f"{PLUGIN}.c"
C_SO = BUILD_DIR / f"{PLUGIN}.so"
subprocess.run(
["gcc", "-shared", "-fPIC", "-O2", "-Wall", "-Wextra", "-o", str(C_SO), str(C_SRC), "-lm"],
check=True,
)
DRIVER = BUILD_DIR / "_run_julia_plugin.py"
DRIVER.write_text(textwrap.dedent("""
import json
import sys
_, build_dir, kind, so, config = sys.argv
sys.path.insert(0, build_dir)
from harness import run
from propaq.noise import NativeNoiseModel
from propaq.truncation import NativeTruncator
if kind == "noise":
value = run(noise=NativeNoiseModel(so, config=config))
else:
value = run(truncation=NativeTruncator(so, config=config))
print(json.dumps(value))
"""))
if KIND == "noise":
c_val = run(noise=NativeNoiseModel(str(C_SO), config=CONFIG))
else:
c_val = run(truncation=NativeTruncator(str(C_SO), config=CONFIG))
proc = subprocess.run(
[sys.executable, str(DRIVER), str(BUILD_DIR), KIND, SO, CONFIG],
check=True, capture_output=True, text=True,
)
jl_val = json.loads(proc.stdout)
print(f"{PLUGIN:<24} C={c_val!r:<24} Julia={jl_val!r:<24} bit-identical={c_val == jl_val}")
uniform_noise C=0.2826014215933488 Julia=0.2826014215933488 bit-identical=True