Emulator¶
Emulation of Guppy programs powered by the selene-sim package.
Provides a configurable interface for compiling Guppy functions into an emulator instance, and a configurable builder for setting instance options and executing.
Emulation returns EmulatorResult objects, which contain the result output
by the emulation. Results are recorded by calling output("tag", value) in the
Guppy program, and can include both quantum measurement outcomes and classical
outputs.
Basic emulation¶
Calling .emulator on a Guppy function compiles it into an
EmulatorInstance. The method has a required parameter
for the number of qubits to allocate. This cannot be
inferred from the program automatically as it can request an arbitrary number of qubits
at runtime.
Calling .run() on the instance runs the emulation, returning an
EmulatorResult object containing the results.
from guppylang import guppy
from guppylang.std.builtins import output
from guppylang.std.quantum import qubit, measure
@guppy
def foo() -> None:
q = qubit()
output("q", measure(q).read())
foo.emulator(n_qubits=1).run()
Change simulation method¶
The default simulation method is statevector simulation, powered
by the Quest selene plugin.
The simulation method can be changed by calling methods on the
EmulatorBuilder instance that return updated instances.
For example to use the stabilizer simulator with clifford circuits:
foo.emulator(1).stabilizer_sim().run()
See also EmulatorInstance.coinflip_sim() for a no-quantum simulation.
In addition arbitrary selene-sim Simulator plugins can be used
by calling EmulatorInstance.with_simulator().
Configuring emulator instances¶
In general the emulation can be configured by chaining with_* methods on the
EmulatorInstance object. Each method returns a new instance with the
updated configuration.
For example, the default number of shots run is 1, to change that:
foo.emulator(n_qubits=1).with_shots(1000).run()
Or update many options at once:
foo.emulator(n_qubits=1).with_shots(1000).with_seed(42).with_shot_offset(10).run()
See the EmulatorInstance documentation for a full list of options and their
defaults.
Noisy simulation¶
Selene-sim supports noisy simulation, which can be enabled by setting an error model on the emulator instance.
from selene_sim.backends.bundled_error_models import DepolarizingErrorModel
error_model = DepolarizingErrorModel(
random_seed=123141,
p_1q=1e-5,
p_2q=1.4e-4,
p_meas=1e-3,
p_init=1e-5
)
foo.emulator(1).with_error_model(error_model).run()
State results¶
Calling state_output("tag", q1, q2) in the Guppy program will record the
state of the qubits q1 and q2 in the outputs.
The particular representation of the state depends on the simulation backend,
the default statevector simulator returns a StateVector object
which is just a numpy array of complex amplitudes.
In general the qubits you request state for may not be all the qubits in the fully entangled state, in which case the remaining qubits are traced over and a probabilistic distribution over statws is returned.
The two methods EmulatorResult.partial_states() and
EmulatorResult.partial_state_dicts() extract state results
from the emulator output as PartialVector objects.
from guppylang import guppy
from guppylang.std.debug import state_output
from guppylang.std.quantum import qubit, measure, cx, h
@guppy
def foo() -> None:
# Measure one qubit in a bell state
q0 = qubit()
h(q0)
q1 = qubit()
cx(q0, q1)
state_output("q0", q0)
measure(q0)
measure(q1)
res = foo.emulator(2).run()
# get state outputs at shot 0, tag "q0"
res.partial_state_dicts()[0]["q0"].state_distribution()
Output is a uniform distribution over the two basis states of the qubit:
[TracedState(probability=0.5, state=array([1.+0.j, 0.+0.j])),
TracedState(probability=0.5, state=array([0.+0.j, 1.+0.j]))]
Emulator Entrypoint Arguments¶
Guppy functions taking parameters of certain types can be called with arguments when emulating. This capability is not supported when submitting programs to hardware. The following types are supported: int, float, bool, and array of these types.
The main benefit of this approach as opposed to hardcoding values in the function or using Python value capture is that the program is compiled once, meaning large parameter sweeps can be done without recompiling the program each time - often significantly improving performance. Emulations of variational algorithms in particular can benefit from this.
For an example of entrypoint arguments in practice, see the QAOA MaxCut example.
Bind parameter values using keyword arguments to EmulatorInstance.run():
from guppylang import guppy
from guppylang.std.array import array
@guppy
def entry_args(theta: float, k: array[int, 1]) -> None:
output("doubled", theta * 2.0)
output("k1", k[0] + 1)
entry_args.emulator(n_qubits=1).run(theta=1.5, k=[3])
# output
EmulatorResult(results=[QsysShot(entries=[('doubled', 3.0), ('k1', 4)])])
Given Guppy emulation is also faster over shots than multiple separate run calls,
parameter sweeps over shots are also supported via the
EmulatorInstance.run_per_shot() method, which takes a sequence of argument
mappings to run the program with for each shot:
entry_args.emulator(n_qubits=1).run_per_shot([
{"theta": 1.5, "k": [3]},
{"theta": 2.0, "k": [4]}
])
# output
EmulatorResult(results=[
QsysShot(entries=[('doubled', 3.0), ('k1', 4)]),
QsysShot(entries=[('doubled', 4.0), ('k1', 5)])
])
The number of shots is inferred from the length of the argument sequence, if it
conflicts with the value set with EmulatorInstance.with_shots() an error is
raised.
Target Quantinuum Platform¶
By default the emulator compiler targets the Quantinuum Helios platform. By specifying
the platform keyword argument to GuppyCompilableProgram.emulator()
you can target other Quantinuum platforms, which may have different gate sets and
native gate decompositions, for example to target the upcoming Sol platform:
foo.emulator(n_qubits=1, platform="sol").run()
When submitting to hardware through Nexus the target platform is automatically inferred from the Nexus backend, so this option is only relevant for local emulation.
This compilation target is also independent of the platform-specific standard libraries, like guppylang.std.qsystem.helios or guppylang.std.qsystem.sol, which can be used to program using platform-specific gates and features. The compiler supports automatic retargeting of these gates to the specified platform. So to run platform-specific code with the same compilation target, make sure the platform argument matches the standard library used:
from guppylang.std.qsystem.sol import phased_xx
from guppylang.std.angles import pi
@guppy
def foo() -> None:
q0 = qubit()
q1 = qubit()
# use Sol primitive gates
phased_xx(q0, q1, pi, pi)
measure(q0)
measure(q1)
# run on Sol platform
foo.emulator(n_qubits=2, platform="sol").run()
An emulator instance for running a compiled program. |
|
A result from running an emulator instance. |
|
Results from a single shot execution. |
|
Built-in immutable sequence. |
|
A builder class for creating EmulatorInstance objects. |
|
Partial state vector for simulator backends with statevector representation. |
|
Protocol for an emulator state type. |
|
The result of tracing out qubits from a SeleneQuestState, leaving a probabilistic mix of states. |
|