Simulation plugins (NetQASM backend)¶
A plugin changes how SquidASM simulates the network, without changing NetQMPI or the program that runs on it. The program stays the same SPMD script; the configuration file names a plugin and passes it its parameters, and NetQMPI keeps the plugin active while SquidASM simulates.
netqasm:
plugin: readout_error:readout_error # module:callable
plugin_config: # handed to the plugin as is
prob_error_0: 0.05
prob_error_1: 0.20
netqmpi -n 2 app.py --netqasm --config config.yaml
Plugins are a feature of the NetQASM backend, with both
--netqasm and --netqasm1.0. The other backends ignore them.
When a plugin is needed¶
The netqasm block of the configuration describes the simulated hardware with
a handful of keys (see Configuration):
the number of qubits per node;
memory decoherence, through
t1andt2;one
gate_fidelityfor every gate;one
link_fidelityand one of three noise models for every link;generic or NV hardware.
That is all SquidASM’s network description can express. Anything else needs code, for example:
separate noise and durations for one- and two-qubit gates;
readout error;
another native gate set, or other decompositions of the gates a program uses;
links with another physical model, such as heralded links with a success probability per attempt.
A plugin supplies that code. Because it lives outside NetQMPI, a model of a particular platform (colour-centre processors with heralded double-click links, for instance) can be developed, versioned and shared on its own, and every NetQMPI program can run on it by changing its configuration file.
Using a plugin¶
plugin names the plugin as "module:callable", and plugin_config is an
arbitrary mapping that NetQMPI hands to it unread:
Key |
Default |
Meaning |
|---|---|---|
|
|
|
|
|
Parameters of the plugin; NetQMPI does not read them |
The module is imported the way python -m would import it: the working
directory first, then the installed packages. A plugin can therefore be a
single file next to the program, as in the examples, or a module of
an installed package, named with its full dotted path:
netqasm:
plugin: my_platform.netqmpi_plugin:simulation
A configuration that names a plugin which cannot be imported, or gives
plugin_config without a plugin, is refused before the program is traced
(see Errors).
Writing a plugin¶
The contract¶
A plugin is a callable that takes plugin_config, a dict, and returns a
context manager. NetQMPI enters the context manager just before the
simulation and exits it right after:
from contextlib import contextmanager
def my_plugin(config):
"""The entry point that `plugin:` names."""
params = check(config) # refuse unknown keys and bad values here
return _active(params)
@contextmanager
def _active(params):
original = ... # what SquidASM uses now
... # replace it with your own
try:
yield # SquidASM simulates here
finally:
... # put the original back
Between entering and exiting, SquidASM builds the network and simulates every shot of the run. Whatever the plugin has replaced is what SquidASM uses to do so.
Lifecycle¶
When |
What NetQMPI does |
|---|---|
The configuration is read |
Imports the module and checks that the callable exists, so a wrong name stops the run before any circuit is traced |
The last rank leaves its |
Calls the callable with |
Inside the context |
SquidASM builds the network and simulates all the shots of the run |
The simulation returns or raises |
Exits the context manager |
The callable is called once per run, and its context manager is entered once, around all the shots. Parameters are therefore validated when the callable is called, just before the simulation: validate them there, before replacing anything, so that a typo in the configuration file is reported rather than silently ignored.
Extension points¶
SquidASM has no API for plugging in a device, an executor or a link model of your own, so a plugin replaces, for the duration of the run, the names SquidASM builds them from. These are the ones a plugin usually needs, in SquidASM 0.13:
Replace |
To change |
SquidASM calls it with |
|---|---|---|
|
The quantum processor of every generic node: its instructions, their noise and duration, its memory |
Keywords |
|
How NetQASM instructions become operations on the processor: gate decompositions, measurement |
|
|
The pairs a link delivers: their state, fidelity and timing |
|
|
The link layer: which memory positions receive the halves of a pair |
|
For example, a plugin that slows every link down to one pair per
pair_interval nanoseconds, so that qubits decohere while they wait for their
pair:
from contextlib import contextmanager
from squidasm.sim.network import network
def slow_links(config):
return _slow_links(float(config.get("pair_interval", 50_000.0)))
@contextmanager
def _slow_links(pair_interval):
original = network.NetSquidNetwork.__dict__["_create_link_distributor"]
def distributor(self, link, state_delay=None):
return original(self, link, state_delay=pair_interval)
network.NetSquidNetwork._create_link_distributor = distributor
try:
yield
finally:
network.NetSquidNetwork._create_link_distributor = original
Guidelines¶
Validate
plugin_configin the callable, before anything is replaced, and refuse unknown keys. NetQMPI does not readplugin_config, so the plugin is the only place where a mistake in it can be caught.Restore everything in
finally, so that a failed simulation does not leave SquidASM modified for the next run in the same process. Read a method to restore from the class’s__dict__(SubroutineHandler.__dict__["_get_executor_class"]): a classmethod read through the class comes back bound, and putting that back would not restore the original.Replace the name SquidASM looks up. SquidASM builds nodes by calling
QDevicefrom the modulesquidasm.sim.network.network, so that is the attribute to replace; binding a name in the plugin’s own module changes nothing.Respect what NetQMPI still configures. A replacement for
QDevicereceivesnum_qubitsandmem_fidelitiesfrom thenetqasmblock (num_qubits,t1,t2) and must honour them. NetQMPI checks that every rank fits innum_qubitsbefore simulating, so a device with fewer positions would fail in the middle of the run instead.Draw random numbers from NetSquid (
netsquid.util.simtools.get_random_state()), so that seeding NetSquid makes a run with the plugin reproducible.Pin the SquidASM version. These are SquidASM internals rather than a public API. The examples are tested with SquidASM 0.13.4 (NetQASM 1.0) and 0.13.6 (NetQASM 2.0).
Package it like any Python module. A plugin used beyond one directory is best installed:
# pyproject.toml of the plugin [project] name = "my-platform" dependencies = ["netqmpi", "squidasm>=0.13,<0.14"]
and named from any directory as
my_platform.module:callable.
Examples¶
examples/plugins/ contains two complete plugins, each with a program and a
configuration file. Run them from their own directory, so that the plugin is
imported from there:
cd examples/plugins/readout_error
netqmpi -n 2 app.py --netqasm --config config.yaml
Readout error: replacing the executor¶
SquidASM measures without error. readout_error makes every measurement report
the wrong bit with a probability that depends on the outcome: prob_error_0
that a 0 reads as 1, and prob_error_1 that a 1 reads as 0. It returns a
subclass of SquidASM’s own executor from _get_executor_class, whose
measurement flips the outcome after the qubit has been measured. The
post-measurement state is therefore the true one and only the reported bit is
wrong, which is what a readout error is.
The error applies to every measurement the simulated nodes make, including the
Bell measurement that qsend performs, so it also degrades teleportation.
def readout_error(config: Mapping[str, Any]):
"""
The plugin entry point, named in the config file.
NetQMPI calls it once per run, with ``plugin_config``, just before the
simulation, and enters the context manager it returns for the length of
that simulation.
Args:
config: ``plugin_config``: ``prob_error_0`` and ``prob_error_1``,
each a probability in [0, 1] that defaults to 0.
Returns:
A context manager under which SquidASM measures with readout error.
Raises:
ValueError: On an unknown key or a value that is not a probability.
Validating here, before anything is patched, reports a typo in
the config file instead of silently ignoring it.
"""
unknown = set(config) - set(KEYS)
if unknown:
raise ValueError(f"readout_error: unknown keys {sorted(unknown)}; "
f"expected {list(KEYS)}.")
probs = {}
for key in KEYS:
value = float(config.get(key, 0.0))
if not 0.0 <= value <= 1.0:
raise ValueError(f"readout_error: {key} must be in [0, 1], got {value}.")
probs[key] = value
return _readout_error(probs["prob_error_0"], probs["prob_error_1"])
@contextmanager
def _readout_error(prob_error_0: float, prob_error_1: float) -> Iterator[None]:
"""
Make SquidASM's vanilla executor misread measurements while active.
Args:
prob_error_0: Probability that an outcome 0 is reported as 1.
prob_error_1: Probability that an outcome 1 is reported as 0.
"""
class ReadoutErrorExecutor(VanillaNetSquidExecutor):
"""SquidASM's executor for generic hardware, with readout error."""
def _meas_physical_qubit(self, position):
outcome = yield from super()._meas_physical_qubit(position)
flip = prob_error_0 if outcome == 0 else prob_error_1
# NetSquid's random state, so that ns.set_random_state(seed)
# makes a run with readout error reproducible too.
if flip and get_random_state().random_sample() < flip:
return 1 - outcome
return outcome
# The function stored in the class, not the bound classmethod, so that
# it can be put back exactly as it was.
original = SubroutineHandler.__dict__["_get_executor_class"]
def executor_class(cls, flavour=None):
if flavour is None or isinstance(flavour, VanillaFlavour):
return ReadoutErrorExecutor
return original.__func__(cls, flavour)
SubroutineHandler._get_executor_class = classmethod(executor_class)
try:
yield
finally:
SubroutineHandler._get_executor_class = original
Its program measures a qubit in |0⟩ and a qubit in |1⟩, so any outcome other
than 10 is a readout error:
$ netqmpi -n 2 app.py --netqasm --config config.yaml
rank 1: {'01': 2, '10': 144, '00': 43, '11': 11}
|0> read as 1 in 6.5% of 200 shots
|1> read as 0 in 22.5% of 200 shots
against prob_error_0: 0.05 and prob_error_1: 0.20; without the plugin every
shot reads 10.
Noise per gate type: replacing the device¶
SquidASM’s generic node applies one gate_fidelity to every gate and fixes the
durations of gates. gate_noise sets the depolarising probability and the
duration of one- and two-qubit gates separately. It replaces QDevice with a
factory that builds a NetSquid QuantumProcessor directly: SquidASM’s own
QDevice cannot simply be given other instructions, because its constructor
overwrites the noise model of every instruction with the one gate_fidelity
dictates.
def build_qdevice(params: GateNoise, name: str = "QDevice", num_qubits: int = 5,
mem_fidelities: Optional[list] = None,
**_: Any) -> QuantumProcessor:
"""
Build one node's processor.
Takes the keyword arguments SquidASM passes to ``QDevice``. Of those,
``gate_fidelity`` is ignored, since this plugin replaces it, and
``mem_fidelities`` carries the T1/T2 memory noise of the ``netqasm``
block.
Args:
params: Gate noise and durations.
name: Processor name.
num_qubits: Memory positions, from ``num_qubits`` in the config.
mem_fidelities: Memory noise model of each position.
Returns:
The processor.
"""
return QuantumProcessor(name, num_positions=num_qubits,
phys_instructions=params.instructions(),
memory_noise_models=mem_fidelities)
@contextmanager
def _gate_noise(params: GateNoise) -> Iterator[None]:
"""Make SquidASM build every generic node with ``params``."""
original = network.QDevice
def qdevice(**kwargs: Any) -> QuantumProcessor:
return build_qdevice(params, **kwargs)
network.QDevice = qdevice
try:
yield
finally:
network.QDevice = original
num_qubits and the memory noise (t1, t2) still come from the netqasm
block, and only gate_fidelity is replaced. The program prepares a Bell pair on
every rank and counts the shots in which the two bits disagree, which only noise
can cause:
$ netqmpi -n 2 app.py --netqasm --config config.yaml
rank 1: {'11': 80, '00': 81, '10': 20, '01': 19}
the two bits disagree in 19.5% of 200 shots
With the CNOT depolarising each qubit with probability 0.2, the bits disagree in 0.5 × (1 − 0.8²) = 18 % of the shots in expectation.
Testing a plugin¶
A plugin can be tested at two levels, as test/test_netqasm_plugin.py does for
both examples:
Its callable, without simulating: that it refuses unknown keys and bad values, that the objects it builds have the parameters it was given, and that it restores what it replaced when its context exits.
A run: build a
NetQASMRunConfigwith the plugin and run a small program through the NetQASM executor:from netqmpi.runtime.adapters.netqasm import NetQASMExecutorAdapter, NetQASMRunConfig config = NetQASMRunConfig(shots=4, plugin="readout_error:readout_error", plugin_config={"prob_error_0": 1.0, "prob_error_1": 1.0}) executor = NetQASMExecutorAdapter(1, config) executor.run(executor.build_apps("app.py", 1))
Choosing parameters at their limits, such as probabilities of 0 or 1, makes the expected outcome deterministic, so the test does not depend on statistics.
Errors¶
Message |
Cause |
|---|---|
|
|
|
The module is neither in the working directory nor installed |
|
The module has no callable by that name |
|
|
|
|
Errors in plugin_config itself are the plugin’s to report, when NetQMPI calls
it just before the simulation.