QgateSampler — Transparent SamplerV2 Middleware# QgateSampler — Transparent SamplerV2 Middleware¶
One-line swap, zero circuit changes, measurable physics improvement."!!! abstract "One-line swap, zero circuit changes, measurable physics improvement.
QgateSampler is a transparent drop-in replacement for Qiskit's SamplerV2``QgateSampler is a transparent drop-in replacement for Qiskit's SamplerV2
primitive. Pass it any IBM backend (or Aer simulator) and it autonomouslyprimitive. It wraps any existing sampler (Aer simulator or IBM hardware) and
injects lightweight probe qubits, applies Galton-filtered post-selection,autonomously injects lightweight probe qubits, applies Galton-filtered
and returns standard PrimitiveResult objects — all without touching yourpost-selection, and reconstructs clean PrimitiveResult objects — all without
circuits.modifying user circuits.
Installation## Installation¶
bashbash
pip install qgate[qiskit]pip install qgate[qiskit]
``````
Requires:Requires:
-
Python ≥ 3.9- Python ≥ 3.9
-
qiskit >= 1.0-qiskit >= 1.0 -
qiskit-aer >= 0.13-qiskit-aer >= 0.13 -
qiskit-ibm-runtime >= 0.20-qiskit-ibm-runtime >= 0.20 -
pydantic >= 2.0-pydantic >= 2.0
Quick Start## Quick Start¶
5 lines to filtered results### Simulator¶
pythonpython
from qiskit.circuit import QuantumCircuitfrom qiskit.circuit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeServicefrom qiskit.primitives import StatevectorSampler
from qgate import QgateSamplerfrom qgate import QgateSampler, SamplerConfig
1. Connect to IBM Quantum# Build any circuit¶
service = QiskitRuntimeService()qc = QuantumCircuit(2)
backend = service.backend("ibm_fez")qc.h(0)
qc.cx(0, 1)
2. Build your circuit (unchanged)qc.measure_all()¶
qc = QuantumCircuit(2)
qc.h(0)# Wrap the sampler — that's it
qc.cx(0, 1)sampler = QgateSampler(
qc.measure_all() inner=StatevectorSampler(),
config=SamplerConfig(), # sensible defaults
3. Wrap the backend — that's it)¶
sampler = QgateSampler(backend=backend)result = sampler.run([qc])
counts = result[0].data.meas.get_counts()
4. Run exactly like SamplerV2print(counts)¶
job = sampler.run([(qc,)])```
result = job.result()
IBM Hardware¶
5. Use the result exactly like before¶
counts = result[0].data.meas.get_counts()```python
print(counts) # {'00': ..., '11': ...} — higher-fidelity shots onlyfrom qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2
```
service = QiskitRuntimeService()
!!! tip "The key difference"backend = service.least_busy(operational=True, simulator=False)
With a standard `SamplerV2` you get **all** shots, including noise-corrupted
ones. With `QgateSampler` you get only the **high-fidelity** shots — thesampler = QgateSampler(
probe-injection and Galton filtering happen transparently. inner=SamplerV2(mode=backend),
config=SamplerConfig(
Simulator (Aer) probe_ry_angle=0.25,¶
galton_quantile=0.75,
```python ),
from qiskit_aer import AerSimulator)
from qgate import QgateSampler
Use exactly like SamplerV2¶
backend = AerSimulator()result = sampler.run([(qc,)])
sampler = QgateSampler(backend=backend)counts = result[0].data.meas.get_counts()
```
job = sampler.run([(qc,)])
result = job.result()---
counts = result[0].data.meas.get_counts()
```## How It Works
With a pre-configured SamplerV2```mermaid¶
flowchart LR
If you already have a SamplerV2 with custom options, pass it via the A["User Circuit"] --> B["Probe Injection"]
sampler= parameter: B --> C["Inner SamplerV2"]
C --> D["Galton Filter"]
```python D --> E["Clean PrimitiveResult"]
from qiskit_ibm_runtime import SamplerV2
style A fill:#e3f2fd
inner = SamplerV2(mode=backend, options={"default_shots": 4096}) style B fill:#fff3e0
sampler = QgateSampler(backend=backend, sampler=inner) style C fill:#e8f5e9
``` style D fill:#fce4ec
style E fill:#e3f2fd
---```
How It Works### 1. Probe Injection¶
``mermaidQgateSampleradds a single ancilla qubit (qgate_anc`) and a classical
flowchart LRregister (qgate_probe) to the user circuit. Controlled-RY gates are placed
A["Your Circuit"] --> B["① Probe Injection"]on nearest-neighbour qubit pairs. The probe rotation angle controls the
B --> C["② SamplerV2.run()"]sensitivity of the fidelity signal.
C --> D["③ Galton Filter"]
D --> E["④ Clean PrimitiveResult"]- **Zero depth overhead:** the probe gates are single-layer, controlled-RY
operations that do not increase the effective circuit depth.
style A fill:#e3f2fd- **Transparent:** the probe qubit and classical register are automatically
style B fill:#fff3e0 stripped from the returned results.
style C fill:#e8f5e9
style D fill:#fce4ec### 2. Inner Execution
style E fill:#e3f2fd
``The augmented circuit is transpiled and executed via the wrappedSamplerV2`.
This can be any Qiskit-compatible sampler — StatevectorSampler,
① Probe InjectionAerSimulator, or a real IBM backend.¶
QgateSampler adds a single ancilla qubit (qgate_anc) and a classical### 3. Galton Filtering
register (qgate_probe) to each circuit. Controlled-RY gates are placed
on nearest-neighbour qubit pairs. The probe rotation angle controls theEach shot receives a fidelity score based on the probe outcome. The
sensitivity of the fidelity signal.Galton adaptive threshold (a self-contained quantile estimator inspired
by the Galton board) maintains a rolling window of recent scores and
- Minimal depth increase: single-layer CRY gates — typically < 1% circuitdynamically sets the acceptance cutoff:
depth overhead.
- Fully transparent: the probe qubit and classical register are stripped- Shots with probe scores above the threshold are kept.
from the returned results — your downstream code never sees them.- Shots below the threshold are discarded as noise-corrupted.
- The threshold adapts automatically — no hand-tuning required.
② Inner Execution¶
4. Result Reconstruction¶
The augmented circuit is transpiled and executed via a standard SamplerV2.
This can target any Qiskit-compatible backend — AerSimulator or real IBMAccepted shots are reassembled into standard Qiskit data structures:
hardware.
BitArraywith correct shape and number of shots
③ Galton Filtering- DataBin with all user classical registers preserved¶
PubResultandPrimitiveResultfully compatible with downstream code
Each shot receives a fidelity score based on the probe outcome. The
Galton adaptive threshold — a self-contained quantile estimator inspired!!! tip "Downstream compatibility"
by the Galton board — maintains a rolling window of recent scores and Any code that works with SamplerV2 results (result[0].data.meas.get_counts(),
dynamically sets the acceptance cutoff: result[0].data.meas.bitcount(), etc.) works unchanged with QgateSampler.
-
Shots with probe scores above the threshold → kept ✅---
-
Shots below the threshold → discarded as noise-corrupted ❌
-
The threshold adapts automatically — no hand-tuning required.## API Reference
④ Result Reconstruction### QgateSampler¶
Accepted shots are reassembled into standard Qiskit data structures:```python
from qgate import QgateSampler
-
BitArraywith correct shape and number of shots -
DataBinwith all user classical registers preservedsampler = QgateSampler( -
PubResultandPrimitiveResultfully compatible with downstream code inner: SamplerV2, # any Qiskit SamplerV2 instanceconfig: SamplerConfig = ..., # optional configuration
!!! success "Downstream compatibility")
Any code that works with `SamplerV2` results — `result[0].data.meas.get_counts()`,```
`result[0].data.meas.bitcount()`, etc. — works unchanged with `QgateSampler`.
Methods¶
| Method | Signature | Description |
API Reference|---|---|---|¶
| run | run(pubs, **kwargs) → PrimitiveResult | Execute pubs through the filter pipeline |
QgateSampler¶
The run() method accepts the same arguments as SamplerV2.run():
```python
from qgate import QgateSampler, SamplerConfig- A list of PUBs (Primitive Unified Blocs): QuantumCircuit, (circuit,),
(circuit, param_values), or (circuit, param_values, shots)
sampler = QgateSampler(- Any additional keyword arguments are forwarded to the inner sampler.
backend, # any IBM/Aer backend
config=SamplerConfig(), # optional tuning (see below)### `SamplerConfig`
sampler=None, # optional pre-built SamplerV2
)```python
```from qgate import SamplerConfig
| Argument | Type | Required | Description |config = SamplerConfig(
|---|---|---|---| probe_ry_angle=0.25,
| backend | Backend | ✅ | IBM Runtime backend or AerSimulator | galton_quantile=0.75,
| config | SamplerConfig | ❌ | Filtering configuration (defaults are sensible) | galton_window=64,
| sampler | SamplerV2 | ❌ | Pre-initialised sampler; if None, one is created from backend | galton_warmup=32,
probe_pairs="nn",
Methods)¶
```
| Method | Returns | Description |
|---|---|---|SamplerConfig is a Pydantic v2 frozen model — immutable after creation,
| sampler.run(pubs, *, shots=None) | QgateSamplerResult | Execute PUBs through the filter pipeline |with full validation and serialisation support.
| job.result() | PrimitiveResult | Retrieve the filtered results (lazy evaluation) |
Parameters¶
Properties¶
| Parameter | Type | Default | Description |
| Property | Type | Description ||---|---|---|---|
|---|---|---|| probe_ry_angle | float | 0.25 | Controlled-RY rotation angle in radians. Higher values produce a stronger probe signal but may perturb the circuit state slightly. Recommended range: 0.1–0.5. |
| sampler.config | SamplerConfig | The active configuration (immutable) || galton_quantile | float | 0.75 | Acceptance quantile (0–1). A value of 0.75 keeps the top 25% of shots by fidelity score. Higher values are more selective. |
| sampler.backend | Backend | The underlying backend || galton_window | int | 64 | Rolling window size for the adaptive threshold estimator. Larger windows produce smoother thresholds but adapt more slowly. |
| sampler.current_threshold | float | Current Galton adaptive threshold value || galton_warmup | int | 32 | Minimum number of shots collected before the Galton threshold activates. During warmup, all shots are accepted. |
| sampler.in_warmup | bool | True if the Galton window is still warming up || probe_pairs | str | "nn" | Probe qubit pairing strategy. "nn" places probes on nearest-neighbour pairs; "all" places probes on all pairs (higher overhead, stronger signal). |
PUB formats---¶
The run() method accepts the same PUB formats as SamplerV2.run():## Advanced Usage
```python### Custom configuration for high-noise environments
sampler.run([qc]) # bare circuit
sampler.run([(qc,)]) # 1-tuple```python
sampler.run([(qc, param_values)]) # with parametersconfig = SamplerConfig(
sampler.run([(qc, param_values, 4096)]) # with parameters + shots probe_ry_angle=0.4, # stronger probe signal
``` galton_quantile=0.85, # keep only top 15%
galton_window=128, # larger window for stability
SamplerConfig)¶
```
```python
from qgate import SamplerConfig### Accessing filter metadata
config = SamplerConfig(```python
probe_angle=0.5, # stronger probe signalresult = sampler.run([qc])
target_acceptance=0.10, # keep top 10%pub_result = result[0]
window_size=2048, # smaller rolling window
)# Standard Qiskit access
```counts = pub_result.data.meas.get_counts()
bitarray = pub_result.data.meas
SamplerConfig is a Pydantic v2 frozen model — immutable after creation,
with full validation and serialisation support.# Filter metadata (when available)
metadata = pub_result.metadata
Parametersprint(f"Shots accepted: {bitarray.num_shots}")¶
```
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|### Using with VQE / QAOA / any variational algorithm
| probe_angle | float | π/6 ≈ 0.524 | (0, π] | Controlled-RY rotation angle in radians. Higher values produce a stronger probe signal but may perturb the circuit state. |
| target_acceptance | float | 0.05 | (0, 1) | Target fraction of shots to accept. 0.05 keeps the top 5%. Lower values = stricter filtering = higher fidelity. |```python
| window_size | int | 4096 | ≥ 64 | Rolling window capacity for the Galton adaptive threshold. Larger windows → smoother thresholds, slower adaptation. |from qiskit.circuit import QuantumCircuit, ParameterVector
| min_window_size | int | 100 | ≥ 1 | Minimum observations before the adaptive threshold activates. During warmup, baseline_threshold is used. |from qiskit_ibm_runtime import EstimatorV2, SamplerV2
| baseline_threshold | float | 0.65 | [0, 1] | Fallback threshold used during the warmup phase. |
| min_threshold | float | 0.3 | [0, 1] | Floor — threshold never drops below this value. |# QgateSampler works with parameterized circuits
| max_threshold | float | 0.95 | [0, 1] | Ceiling — threshold never exceeds this value. |theta = ParameterVector("θ", 4)
| use_quantile | bool | True | — | If True, use empirical quantile for threshold. If False, use z-score mode. |qc = QuantumCircuit(4)
| robust_stats | bool | True | — | If True and use_quantile=False, use median + MAD instead of mean + std. |for i in range(4):
| z_sigma | float | 1.645 | ≥ 0 | Number of σ above centre for z-score mode (only when use_quantile=False). | qc.ry(theta[i], i)
| optimization_level | int | 1 | [0, 3] | Qiskit transpiler optimization level. |for i in range(3):
| oversample_factor | float | 1.0 | [1, 20] | Request extra shots from backend to compensate for filtering. 1.0 = no oversampling. | qc.cx(i, i + 1)
qc.measure_all()
Recommended presets
=== "Conservative (default)"sampler = QgateSampler(
```python inner=SamplerV2(mode=backend),
SamplerConfig() # probe_angle=π/6, target_acceptance=5% config=SamplerConfig(),
```)
Good for most circuits. Accepts ~5% of shots with highest fidelity.
Parameters are bound normally via PUBs¶
=== "Aggressive filtering"result = sampler.run([(qc, [0.1, 0.2, 0.3, 0.4])])
```python```
SamplerConfig(target_acceptance=0.02, probe_angle=0.8)
```---
For very noisy backends. Keeps only top 2% — fewer shots, much higher quality.
Validation Results¶
=== "Gentle filtering"
```python### Real IBM Hardware
SamplerConfig(target_acceptance=0.20, probe_angle=0.3)
```| Backend | Architecture | Qubits | Protocol | Key Result |
For shallow circuits where most shots are already decent. Keeps top 20%.|---|---|---|---|---|
| IBM Fez | Heron r2 | 156 | 2Q Bell state, 100 shots | 95% Bell fidelity on filtered shots; probe stripped cleanly |
---| IBM Torino | Heron r2 | 133 | Utility-scale TFIM | Galton acceptance ~9.7%; cooling Δ = −0.080 |
| IBM Brisbane | Eagle r3 | 127 | 8Q TFIM VQE | 6.6% acceptance (vs 0% raw post-selection) |
Advanced Usage¶
E2E Physics Validation (Simulator)¶
Using with VQE / QAOA / any variational algorithm¶
10-trial paired experiment on 8-qubit TFIM at the quantum critical point,
```pythonusing an IBM Heron-class noise model (\(T_1 = 300\mu s\), \(T_2 = 150\mu s\),
from qiskit.circuit import QuantumCircuit, ParameterVector1Q depolarising \(= 10^{-3}\), 2Q depolarising \(= 10^{-2}\)):
Parameterized ansatz| Metric | Value |¶
theta = ParameterVector("θ", 4)|---|---|
qc = QuantumCircuit(4)| Mean MSE reduction | +0.69% |
for i in range(4):| Paired t-test | p = 1.26 × 10⁻⁴ |
qc.ry(theta[i], i)| Trials improved | **9 / 10** |
for i in range(3):| Validation protocol | VQE warm-up (ZZ-only Hamiltonian) → QgateSampler vs raw SamplerV2 |
qc.cx(i, i + 1)
qc.measure_all()### Statistical Bias Study (15 independent trials × 100K shots)
sampler = QgateSampler(backend=backend)| Experiment | Key Finding |
|---|---|
Parameters are bound normally via PUBs| Noise robustness | MSE↓ 13.6% → 20.7% as noise increases |¶
job = sampler.run([(qc, [0.1, 0.2, 0.3, 0.4])])| Qubit scaling (8–16Q) | Stable MSE↓ 14.5–16.5%, variance↓ up to 5,360× |
result = job.result()| Cross-algorithm | VQE 14.8%, QAOA 48.8%, Grover 24.4% MSE reduction |
counts = result[0].data.meas.get_counts()| Train/test split | 14.7% MSE↓ on blind test (p = 0.001), frozen threshold σ = 0.000 |
```
Inspecting the adaptive threshold¶
Frequently Asked Questions¶
```python
sampler = QgateSampler(backend=backend)??? question "Does QgateSampler modify my circuit?"
Only at the probe-injection layer. Your user-visible qubits and classical
After first run registers are untouched. The probe ancilla and probe classical register are¶
job = sampler.run([(qc,)]) added automatically and stripped from the returned results.
result = job.result()
How many shots do I lose to filtering?
print(f"Warmup: {sampler.in_warmup}") It depends on the noise environment and galton_quantile setting. Typical
print(f"Threshold: {sampler.current_threshold:.4f}") acceptance rates are 10–30% on real hardware. The accepted shots have
print(f"Shots accepted: {result[0].data.meas.num_shots}") significantly higher fidelity, improving downstream expectation values.
```
Can I use QgateSampler with Estimator?
Oversampling to maintain shot count QgateSampler wraps SamplerV2 specifically. For EstimatorV2 workflows,¶
use the `TrajectoryFilter` API directly, or sample bitstrings via
By default, filtering reduces the number of returned shots. Use QgateSampler and compute expectation values classically.
oversample_factor to automatically request extra shots from the backend:
What is the overhead?
```python - Quantum overhead: 1 additional qubit + single-layer CRY gates (negligible depth increase)
config = SamplerConfig( - Classical overhead: probe scoring and threshold computation (< 1 ms for typical shot counts)
target_acceptance=0.10, # keep top 10% - **Shot overhead:** you need ~3–10× more shots to compensate for filtering, but the filtered
oversample_factor=10.0, # request 10× more → ~same final count shots are far more accurate, often yielding a net TTS improvement.
)
sampler = QgateSampler(backend=backend, config=config)??? question "Is QgateSampler compatible with error mitigation (ZNE, PEC, etc.)?"
``` Yes. QgateSampler operates at the shot-filtering layer and is orthogonal to
circuit-level error mitigation techniques. You can compose them: run
--- QgateSampler for shot filtering, then apply ZNE or PEC on the filtered results.
Validation Results---¶
Real IBM Hardware## Patent Notice¶
| Backend | Architecture | Qubits | Protocol | Key Result |The algorithms implemented in QgateSampler are covered by pending patent
|---|---|---|---|---|applications:
| IBM Fez | Heron r2 | 156 | 2Q Bell state, 100 shots | 95% Bell fidelity on filtered shots; probe stripped cleanly |
| IBM Torino | Heron r2 | 133 | Utility-scale TFIM | Galton acceptance ~9.7%; cooling Δ = −0.080 | Patent pending
| IBM Brisbane | Eagle r3 | 127 | 8Q TFIM VQE | 6.6% acceptance (vs 0% raw post-selection) | Patent pending
E2E Physics Validation (Simulator)Licensed under the QGATE Source Available Evaluation License v1.2.¶
Academic research, internal evaluation, and peer review are freely permitted.
10-trial paired experiment on 8-qubit TFIM at the quantum critical point,Commercial deployment requires a separate license — contact
using an IBM Heron-class noise model (\(T_1 = 300\mu s\), \(T_2 = 150\mu s\),ranbuch@gmail.com.
1Q depolarising \(= 10^{-3}\), 2Q depolarising \(= 10^{-2}\)):
| Metric | Value |
|---|---|
| Mean MSE reduction | +0.69% |
| Paired t-test | p = 1.26 × 10⁻⁴ |
| Trials improved | 9 / 10 |
| Validation protocol | VQE warm-up (ZZ-only Hamiltonian) → QgateSampler vs raw SamplerV2 |
Statistical Bias Study (15 independent trials × 100K shots)¶
| Experiment | Key Finding |
|---|---|
| Noise robustness | MSE↓ 13.6% → 20.7% as noise increases |
| Qubit scaling (8–16Q) | Stable MSE↓ 14.5–16.5%, variance↓ up to 5,360× |
| Cross-algorithm | VQE 14.8%, QAOA 48.8%, Grover 24.4% MSE reduction |
| Train/test split | 14.7% MSE↓ on blind test (p = 0.001), frozen threshold σ = 0.000 |
Frequently Asked Questions¶
Does QgateSampler modify my circuit?
Only at the probe-injection layer. Your user-visible qubits and classical registers are untouched. The probe ancilla and probe classical register are added automatically and stripped from the returned results.
How many shots do I lose to filtering?
It depends on the noise environment and target_acceptance setting.
With the default target_acceptance=0.05, about 5% of shots are kept.
Use oversample_factor to request extra shots and maintain your target
count. The accepted shots have significantly higher fidelity, improving
downstream expectation values.
Can I use QgateSampler with EstimatorV2?
QgateSampler wraps SamplerV2 specifically. For EstimatorV2 workflows,
sample bitstrings via QgateSampler and compute expectation values
classically, or use the TrajectoryFilter API directly.
What is the overhead?
- Quantum overhead: 1 ancilla qubit + single-layer CRY gates (< 1% depth increase)
- Classical overhead: probe scoring + threshold computation (< 1 ms for typical shot counts)
- Shot overhead: you need more shots to compensate for filtering — use
oversample_factorto automate this. The filtered shots are far more accurate, often yielding a net TTS improvement.
Is QgateSampler compatible with error mitigation (ZNE, PEC, etc.)?
Yes. QgateSampler operates at the shot-filtering layer and is orthogonal to circuit-level error mitigation. You can compose them: run QgateSampler for shot filtering, then apply ZNE or PEC on the filtered results.
Do I need an IBM Quantum account?
For real hardware, yes — you need a free IBM Quantum
account. For local simulation, use AerSimulator with no account required.
Patent Notice¶
The algorithms implemented in QgateSampler are covered by pending patent
applications:
- Pending U.S. patent applications
- Pending Israeli patent application
Licensed under the QGATE Source Available Evaluation License v1.2. Academic research, internal evaluation, and peer review are freely permitted. Commercial deployment requires a separate license — contact ranbuch@gmail.com.