Native plugins¶
Noise models and truncation policies sit in the propagator's innermost loop. A Python implementation is fine for prototyping, but every call pays GIL acquisition and interpreter dispatch, which will dominate the runtime of any large propagation.
The plugin ABI lets you write the same model in C, Rust, or AOT-compiled Julia, ship it as a shared library, and have propaq call it with no interpreter involvement.
Loading a plugin¶
from propaq.noise import NativeNoiseModel
from propaq.propagators import PauliPropagator
from propaq.truncation import NativeTruncator
prop = PauliPropagator(
noise=NativeNoiseModel(
path="./thermal_decay_noise.so",
config='{"gamma": 0.02, "beta": 0.8}',
),
truncation=NativeTruncator(
path="./pareto_truncator.so",
config='{"threshold": 1e-6, "alpha": 0.5}',
),
)
config is an optional JSON string handed once to the plugin's
propaq_*_create entry point, if it exports one.
Plugins run unsandboxed
Loading a plugin executes native code in-process with no isolation. Only
load libraries you trust, exactly as you would any other compiled dependency
you link against. Plugin code must never panic, unwind, or longjmp across
the call boundary.
Example implementations¶
The repository ships working plugins in all three languages under
examples/plugins/,
including re-implementations of the built-in UniformNoiseModel and
WeightTruncator that you can use to check your toolchain end to end.
The four plugin notebooks build and run them:
propaq Plugin ABI¶
propaq can load custom noise models and truncation policies from
dynamically loaded C, Rust, or AOT-compiled Julia shared libraries.
Functionality exists for Python implementations as well. However, this is not recommended, since both noise and truncation are hot in the propagator's inner loop and incur GIL overhead.
See examples/plugins for example implementations in C, Rust, and Julia.
Each language directory is split into noise/ and truncation/
subdirectories. Rust plugins are each a standalone cdylib crate under
rust/{noise,truncation}/<name>/ (own Cargo.toml); Julia plugins are
AOT-compiled via PackageCompiler.jl, and the resulting shared object is
treated as a C ABI library by propaq.
Dependencies¶
A plugin specifies what information it reads from the engine using dependencies.
By default, a plugin is able to read only the term's weight and coefficient magnitude. If it needs to read the term's entire bitmask, or the layer index, it must declare those
keys in its depends bitmask. This is meant to allow the engine to optimize the plugin's execution path and avoid unnecessary overhead.
#define PROPAQ_DEPENDS_KEY (1u << 0) /* reads `words` */
#define PROPAQ_DEPENDS_LAYER (1u << 1) /* reads `layer_index` / `n_layers` */
Export propaq_noise_depends / propaq_truncator_depends returning the OR of
the bits you need. Omitting the symbol means 0, a function of term weight.
Dependency permissions and costs¶
Noise:
depends |
Strategy | words |
Clifford deferral |
|---|---|---|---|
0 |
tabulated once at setup (n_units + 1 calls, total) |
NULL |
kept |
LAYER |
tabulated, rebuilt at each layer boundary | NULL |
kept |
KEY |
called per term, every layer | real | off |
KEY \| LAYER |
called per term, with the layer index | real | off |
Truncation is never tabulated, so we only toggle deferral to provide key access.
depends |
words |
Clifford deferral |
|---|---|---|
0 |
NULL |
kept |
LAYER |
NULL |
kept |
KEY (w or w/o LAYER) |
real | off |
Note that without declaring the appropriate permissions, words is NULL and the plugin
will crash immediately.
Example plugins¶
uniform_noise and weight_truncator are just re-implementations of the built-in
UniformNoiseModel and WeightTruncator policies, respectively, so they can be
used to verify that the ABI is working correctly.
| Kind | Built-in equivalent | C | Rust | Julia |
|---|---|---|---|---|
| Noise | UniformNoiseModel |
c/noise/uniform_noise.c |
rust/noise/uniform_noise/ |
julia/noise/uniform_noise.jl |
| Truncator | WeightTruncator |
c/truncation/weight_truncator.c |
rust/truncation/weight_truncator/ |
julia/truncation/weight_truncator.jl |
We also provide custom policies without built-in equivalents to demonstrate plugin syntax and semantics.
| Kind | Policy | depends |
Formula | Config keys | C | Rust | Julia |
|---|---|---|---|---|---|---|---|
| Noise | thermal_decay_noise |
0 |
exp(-(gamma * weight)^beta) (stretched exponential; beta=1 = plain exponential) |
gamma, beta |
c/noise/thermal_decay_noise.c |
rust/noise/thermal_decay_noise/ |
julia/noise/thermal_decay_noise.jl |
| Noise | drifting_noise |
0 |
exp(-damping * weight * (1 + drift_rate * call_index)) |
damping, drift_rate |
c/noise/drifting_noise.c |
rust/noise/drifting_noise/ |
julia/noise/drifting_noise.jl |
| Noise | depth_dependent_noise |
LAYER |
exp(-damping * weight * (1 + rate * layer_index / n_layers)) |
damping, rate |
c/noise/depth_dependent_noise.c |
rust/noise/depth_dependent_noise/ |
julia/noise/depth_dependent_noise.jl |
| Noise | qubit_local_noise |
KEY |
exp(-damping * \|support(term) & mask\|) (only the masked units are noisy) |
damping, mask |
c/noise/qubit_local_noise.c |
rust/noise/qubit_local_noise/ |
- |
| Truncator | pareto_truncator |
0 |
keep iff coeff_magnitude * exp(-alpha * weight) > threshold (joint score) |
threshold, alpha |
c/truncation/pareto_truncator.c |
rust/truncation/pareto_truncator/ |
julia/truncation/pareto_truncator.jl |
| Truncator | stochastic_truncator |
KEY |
keep with probability min(1, coeff_magnitude / threshold) (importance sampling) |
threshold, seed |
c/truncation/stochastic_truncator.c |
rust/truncation/stochastic_truncator/ |
julia/truncation/stochastic_truncator.jl |
| Truncator | support_truncator |
KEY |
keep iff coeff_magnitude * exp(-alpha * \|support(term) \\ mask\|) > threshold |
threshold, alpha, mask |
c/truncation/support_truncator.c |
rust/truncation/support_truncator/ |
- |
mask is an unsigned integer over unit indices, bit q selecting
qubit/site q, and addresses the first 64 units.
Noise ABI¶
Fixed extern "C" symbol names (C-stable types only, u32/f64/pointers/usize):
uint32_t propaq_noise_abi_version(void); // required, must equal 1
uint32_t propaq_noise_depends(void); // optional; absent => 0
void* propaq_noise_create(const char* config_json); // optional
void propaq_noise_destroy(void* ctx); // required iff create is present
double propaq_noise_factor(void* ctx, uint32_t basis_kind,
const uint64_t* words, size_t n_words,
uint32_t n_units, uint32_t weight,
uint32_t layer_index, uint32_t n_layers); // required
int32_t propaq_noise_factor_batch(void* ctx, uint32_t basis_kind,
const uint64_t* words, size_t n_words_per_term,
uint32_t n_units, const uint32_t* weights,
uint32_t layer_index, uint32_t n_layers,
double* out, size_t n_terms); // optional, faster
from propaq.noise import NativeNoiseModel
model = NativeNoiseModel("examples/plugins/c/noise/uniform_noise.so", config='{"damping": 0.001}')
propagator.set_noise(model)
Truncator ABI¶
uint32_t propaq_truncator_abi_version(void); // required, must equal 1
uint32_t propaq_truncator_depends(void); // optional; absent => 0
void* propaq_truncator_create(const char* config_json); // optional
void propaq_truncator_destroy(void* ctx); // required iff create is present
int32_t propaq_truncator_keep(void* ctx, uint32_t basis_kind,
const uint64_t* words, size_t n_words,
uint32_t n_units, uint32_t weight,
double coeff_magnitude,
uint32_t layer_index, uint32_t n_layers);
// required; nonzero = keep
int32_t propaq_truncator_keep_batch(void* ctx, uint32_t basis_kind,
const uint64_t* words, size_t n_words_per_term,
uint32_t n_units, const uint32_t* weights,
const double* coeff_magnitudes,
uint32_t layer_index, uint32_t n_layers,
uint8_t* out_keep, size_t n_terms); // optional, faster
coeff_magnitude is the term's real coefficient magnitude
(f64::abs). A native truncator fully replaces the built-in
weight/coefficient cutoff comparison for the run rather than composing
with it.
from propaq.truncation import NativeTruncator
trunc = NativeTruncator("examples/plugins/c/truncation/weight_truncator.so", config='{"max_weight": 4}')
propagator.set_truncation(trunc)
What a plugin receives¶
basis_kind is 0 for Pauli and 1 for Majorana. words is the term's
key as raw storage words, two bits per unit, interleaved:
- Pauli: bit
2qis qubitq's X component, bit2q + 1its Z component. - Majorana: bit
2kis modegamma_{2k}, bit2k + 1isgamma_{2k + 1}.
n_units is the register size in qubits (Pauli) or fermionic sites
(Majorana, so half the mode count). weight is the term's weight, passed
because the engine has computed it already. In the batch forms the terms
are contiguous, n_words_per_term words each.
layer_index is the zero-based circuit layer and n_layers the circuit's
layer count, so a model can normalize depth to layer_index / n_layers
without being told the circuit size separately.
The batch entry points¶
propaq_noise_factor_batch / propaq_truncator_keep_batch are an optional
performance path. These apply the plugin to a contiguous array of terms in one
call in an effort to amortize the overhead of FFI boundary crossing.
propaq_truncator_keep_batch is reachable only from the reclaim sweep
that follows a noise layer, which visits whole rows. An emit-time decision
is taken inside the branching loop one child at a time, so there is
nothing to batch there and propaq_truncator_keep carries it.
Safety¶
Loading a plugin runs unsandboxed native code in-process. Only load
libraries you trust, the same way you'd trust any other compiled
dependency you link against. Plugin code must never panic, unwind, or
longjmp across the call boundary.