Rust native plugins¶
This is the Rust counterpart to 01_c_plugins.ipynb: it builds every Rust
plugin crate under examples/plugins/rust/, loads each one through
propaq's real NativeNoiseModel/NativeTruncator classes, and checks the
same diffability properties.
Building the plugins¶
In [ ]:
Copied!
import subprocess
from pathlib import Path
PLUGIN_DIR = Path("../rust").resolve()
BUILD_DIR = Path("_build").resolve()
BUILD_DIR.mkdir(exist_ok=True)
CRATES = {
"uniform_noise": PLUGIN_DIR / "noise/uniform_noise",
"thermal_decay_noise": PLUGIN_DIR / "noise/thermal_decay_noise",
"drifting_noise": PLUGIN_DIR / "noise/drifting_noise",
"depth_dependent_noise": PLUGIN_DIR / "noise/depth_dependent_noise",
"qubit_local_noise": PLUGIN_DIR / "noise/qubit_local_noise",
"weight_truncator": PLUGIN_DIR / "truncation/weight_truncator",
"pareto_truncator": PLUGIN_DIR / "truncation/pareto_truncator",
"stochastic_truncator": PLUGIN_DIR / "truncation/stochastic_truncator",
"support_truncator": PLUGIN_DIR / "truncation/support_truncator",
}
SO = {}
for name, crate_dir in CRATES.items():
subprocess.run(["cargo", "build", "--release"], cwd=crate_dir, check=True, capture_output=True)
built = next((crate_dir / "target/release").glob("lib*.so"))
out = BUILD_DIR / f"{name}.so"
out.write_bytes(built.read_bytes())
SO[name] = str(out)
print("Built:", *SO.values(), sep="\n ")
import subprocess
from pathlib import Path
PLUGIN_DIR = Path("../rust").resolve()
BUILD_DIR = Path("_build").resolve()
BUILD_DIR.mkdir(exist_ok=True)
CRATES = {
"uniform_noise": PLUGIN_DIR / "noise/uniform_noise",
"thermal_decay_noise": PLUGIN_DIR / "noise/thermal_decay_noise",
"drifting_noise": PLUGIN_DIR / "noise/drifting_noise",
"depth_dependent_noise": PLUGIN_DIR / "noise/depth_dependent_noise",
"qubit_local_noise": PLUGIN_DIR / "noise/qubit_local_noise",
"weight_truncator": PLUGIN_DIR / "truncation/weight_truncator",
"pareto_truncator": PLUGIN_DIR / "truncation/pareto_truncator",
"stochastic_truncator": PLUGIN_DIR / "truncation/stochastic_truncator",
"support_truncator": PLUGIN_DIR / "truncation/support_truncator",
}
SO = {}
for name, crate_dir in CRATES.items():
subprocess.run(["cargo", "build", "--release"], cwd=crate_dir, check=True, capture_output=True)
built = next((crate_dir / "target/release").glob("lib*.so"))
out = BUILD_DIR / f"{name}.so"
out.write_bytes(built.read_bytes())
SO[name] = str(out)
print("Built:", *SO.values(), sep="\n ")
The same tiny propagation harness as the C notebook¶
Same circuit-construction approach as 01_c_plugins.ipynb.
In [2]:
Copied!
import random
from propaq import PauliString
from propaq.circuits import PauliCircuit, PauliRotation
from propaq.datatypes import PauliTermSum
from propaq.noise import NativeNoiseModel, UniformNoiseModel
from propaq.propagators import PauliPropagator
from propaq.truncation import CoefficientTruncator, NativeTruncator, WeightTruncator
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
print("Circuit depth:", len(CIRCUIT.rotations))
import random
from propaq import PauliString
from propaq.circuits import PauliCircuit, PauliRotation
from propaq.datatypes import PauliTermSum
from propaq.noise import NativeNoiseModel, UniformNoiseModel
from propaq.propagators import PauliPropagator
from propaq.truncation import CoefficientTruncator, NativeTruncator, WeightTruncator
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
print("Circuit depth:", len(CIRCUIT.rotations))
Circuit depth: 40
Comparison to built-ins¶
In [3]:
Copied!
gamma = 0.01
built_in = run(noise=UniformNoiseModel(damping=gamma))
native = run(noise=NativeNoiseModel(SO["uniform_noise"], config=f'{{"damping": {gamma}}}'))
print(f"uniform_noise: native={native!r} built-in={built_in!r} match={native == built_in}")
thermal = run(noise=NativeNoiseModel(SO["thermal_decay_noise"], config=f'{{"gamma": {gamma}, "beta": 1.0}}'))
print(f"thermal_decay(beta=1): native={thermal!r} built-in={built_in!r} match={thermal == built_in}")
max_weight = 3
built_in_w = run(truncation=WeightTruncator(max_weight))
native_w = run(truncation=NativeTruncator(SO["weight_truncator"], config=f'{{"max_weight": {max_weight}}}'))
print(f"weight_truncator: native={native_w!r} built-in={built_in_w!r} match={native_w == built_in_w}")
threshold = 1e-3
built_in_c = run(truncation=CoefficientTruncator(threshold))
pareto = run(truncation=NativeTruncator(SO["pareto_truncator"], config=f'{{"threshold": {threshold}, "alpha": 0.0}}'))
print(f"pareto(alpha=0): native={pareto!r} built-in={built_in_c!r} match={pareto == built_in_c}")
gamma = 0.01
built_in = run(noise=UniformNoiseModel(damping=gamma))
native = run(noise=NativeNoiseModel(SO["uniform_noise"], config=f'{{"damping": {gamma}}}'))
print(f"uniform_noise: native={native!r} built-in={built_in!r} match={native == built_in}")
thermal = run(noise=NativeNoiseModel(SO["thermal_decay_noise"], config=f'{{"gamma": {gamma}, "beta": 1.0}}'))
print(f"thermal_decay(beta=1): native={thermal!r} built-in={built_in!r} match={thermal == built_in}")
max_weight = 3
built_in_w = run(truncation=WeightTruncator(max_weight))
native_w = run(truncation=NativeTruncator(SO["weight_truncator"], config=f'{{"max_weight": {max_weight}}}'))
print(f"weight_truncator: native={native_w!r} built-in={built_in_w!r} match={native_w == built_in_w}")
threshold = 1e-3
built_in_c = run(truncation=CoefficientTruncator(threshold))
pareto = run(truncation=NativeTruncator(SO["pareto_truncator"], config=f'{{"threshold": {threshold}, "alpha": 0.0}}'))
print(f"pareto(alpha=0): native={pareto!r} built-in={built_in_c!r} match={pareto == built_in_c}")
uniform_noise: native=0.2826014215933488 built-in=0.2826014215933488 match=True thermal_decay(beta=1): native=0.2826014215933488 built-in=0.2826014215933488 match=True weight_truncator: native=0.2825881577408065 built-in=0.2825881577408065 match=True pareto(alpha=0): native=0.5320707897129247 built-in=0.5320707897129247 match=True
C vs Rust¶
01_c_plugins.ipynb built the same plugins from C. Every one of them should
agree with its Rust counterpart.
In [4]:
Copied!
import subprocess
C_BUILD = Path("../c").resolve()
C_OUT = Path("_build_c").resolve()
C_OUT.mkdir(exist_ok=True)
C_SOURCES = {
"uniform_noise": C_BUILD / "noise/uniform_noise.c",
"thermal_decay_noise": C_BUILD / "noise/thermal_decay_noise.c",
"drifting_noise": C_BUILD / "noise/drifting_noise.c",
"depth_dependent_noise": C_BUILD / "noise/depth_dependent_noise.c",
"qubit_local_noise": C_BUILD / "noise/qubit_local_noise.c",
"weight_truncator": C_BUILD / "truncation/weight_truncator.c",
"pareto_truncator": C_BUILD / "truncation/pareto_truncator.c",
"stochastic_truncator": C_BUILD / "truncation/stochastic_truncator.c",
"support_truncator": C_BUILD / "truncation/support_truncator.c",
}
C_SO = {}
for name, src in C_SOURCES.items():
out = C_OUT / f"{name}.so"
subprocess.run(["gcc", "-shared", "-fPIC", "-O2", "-o", str(out), str(src), "-lm"], check=True)
C_SO[name] = str(out)
CASES = {
"uniform_noise": ('{"damping": 0.01}', "noise"),
"thermal_decay_noise": ('{"gamma": 0.02, "beta": 1.6}', "noise"),
"drifting_noise": ('{"damping": 0.01, "drift_rate": 0.0001}', "noise"),
"depth_dependent_noise": ('{"damping": 0.02, "rate": 3.0}', "noise"),
"qubit_local_noise": ('{"damping": 0.05, "mask": 5}', "noise"),
"weight_truncator": ('{"max_weight": 3}', "trunc"),
"pareto_truncator": ('{"threshold": 1e-2, "alpha": 2.0}', "trunc"),
"stochastic_truncator": ('{"threshold": 0.3, "seed": 11}', "trunc"),
"support_truncator": ('{"threshold": 1e-3, "alpha": 1.0, "mask": 3}', "trunc"),
}
all_match = True
for name, (cfg, kind) in CASES.items():
if kind == "noise":
c_val = run(noise=NativeNoiseModel(C_SO[name], config=cfg))
rs_val = run(noise=NativeNoiseModel(SO[name], config=cfg))
else:
c_val = run(truncation=NativeTruncator(C_SO[name], config=cfg))
rs_val = run(truncation=NativeTruncator(SO[name], config=cfg))
match = c_val == rs_val
all_match &= match
print(f"{name:<24} C={c_val!r:<24} Rust={rs_val!r:<24} bit-identical={match}")
print(f"\nall bit-identical at n_threads=4: {all_match}")
import subprocess
C_BUILD = Path("../c").resolve()
C_OUT = Path("_build_c").resolve()
C_OUT.mkdir(exist_ok=True)
C_SOURCES = {
"uniform_noise": C_BUILD / "noise/uniform_noise.c",
"thermal_decay_noise": C_BUILD / "noise/thermal_decay_noise.c",
"drifting_noise": C_BUILD / "noise/drifting_noise.c",
"depth_dependent_noise": C_BUILD / "noise/depth_dependent_noise.c",
"qubit_local_noise": C_BUILD / "noise/qubit_local_noise.c",
"weight_truncator": C_BUILD / "truncation/weight_truncator.c",
"pareto_truncator": C_BUILD / "truncation/pareto_truncator.c",
"stochastic_truncator": C_BUILD / "truncation/stochastic_truncator.c",
"support_truncator": C_BUILD / "truncation/support_truncator.c",
}
C_SO = {}
for name, src in C_SOURCES.items():
out = C_OUT / f"{name}.so"
subprocess.run(["gcc", "-shared", "-fPIC", "-O2", "-o", str(out), str(src), "-lm"], check=True)
C_SO[name] = str(out)
CASES = {
"uniform_noise": ('{"damping": 0.01}', "noise"),
"thermal_decay_noise": ('{"gamma": 0.02, "beta": 1.6}', "noise"),
"drifting_noise": ('{"damping": 0.01, "drift_rate": 0.0001}', "noise"),
"depth_dependent_noise": ('{"damping": 0.02, "rate": 3.0}', "noise"),
"qubit_local_noise": ('{"damping": 0.05, "mask": 5}', "noise"),
"weight_truncator": ('{"max_weight": 3}', "trunc"),
"pareto_truncator": ('{"threshold": 1e-2, "alpha": 2.0}', "trunc"),
"stochastic_truncator": ('{"threshold": 0.3, "seed": 11}', "trunc"),
"support_truncator": ('{"threshold": 1e-3, "alpha": 1.0, "mask": 3}', "trunc"),
}
all_match = True
for name, (cfg, kind) in CASES.items():
if kind == "noise":
c_val = run(noise=NativeNoiseModel(C_SO[name], config=cfg))
rs_val = run(noise=NativeNoiseModel(SO[name], config=cfg))
else:
c_val = run(truncation=NativeTruncator(C_SO[name], config=cfg))
rs_val = run(truncation=NativeTruncator(SO[name], config=cfg))
match = c_val == rs_val
all_match &= match
print(f"{name:<24} C={c_val!r:<24} Rust={rs_val!r:<24} bit-identical={match}")
print(f"\nall bit-identical at n_threads=4: {all_match}")
uniform_noise C=0.2826014215933488 Rust=0.2826014215933488 bit-identical=True thermal_decay_noise C=0.4306572686380927 Rust=0.4306572686380927 bit-identical=True drifting_noise C=0.2825671425612839 Rust=0.2825671425612839 bit-identical=True depth_dependent_noise C=0.04407469222175979 Rust=0.04407469222175979 bit-identical=True qubit_local_noise C=0.056175473701847785 Rust=0.056175473701847785 bit-identical=True weight_truncator C=0.2825881577408065 Rust=0.2825881577408065 bit-identical=True pareto_truncator C=0.29053486444243043 Rust=0.29053486444243043 bit-identical=True stochastic_truncator C=0.28681985333181026 Rust=0.28681985333181026 bit-identical=True support_truncator C=0.5212613986436822 Rust=0.5212613986436822 bit-identical=True all bit-identical at n_threads=4: True