Selene emulation in Nexus¶
Selene is Quantinuum’s emulation toolkit, offering different simulation backends, noise models and emulation runtime modes (including full emulation of the Helios runtime).
Programs for Selene are currently written in Guppy.
You can run Selene in your local programming environment, however Quantinuum Nexus provides Selene instances (with advanced features available) in the cloud.
To use this we’ll follow the basic pattern:
Write your Guppy program and compile it (to an intermediate representation called HUGR).
Upload this HUGR to the Nexus database so you can use it in Nexus jobs.
Define your SeleneConfig (or SelenePlusConfig) - a way to select the simulation/error/runtime mode for your job.
Submit an ExecuteJob to Nexus - along with your HUGRRefs and Selene configuration.
Wait for the job to complete and collect the results.
This notebook outlines examples of each of the Selene simulator backends in Nexus.
from datetime import datetime
from pprint import pprint
from typing import no_type_check
from guppylang import guppy
from guppylang.std.builtins import array, comptime, result
from guppylang.std.quantum import cx, h, measure, qubit, t
import qnexus as qnx
Set up a Nexus Project¶
qnx.login()
my_project_ref = qnx.projects.get_or_create(name="Selene emulation in Nexus")
qnx.context.set_active_project(my_project_ref)
Statevector simulation¶
Let’s prepare a simple program and run it on Selene using our StatevectorSimulator, which is built on the QuEST statevector simulator.
Unless specified, all Selene simulations use:
SimpleRuntime (i.e. not emulating the runtime of any particular hardware)
NoErrorModel (i.e. a noiseless simulation)
# Guppy program to prepare GHZ state on 4 qubits
@guppy
@no_type_check
def main() -> None:
qubit_top = qubit()
h(qubit_top)
qubits = array(qubit() for _ in range(3))
for q in qubits:
cx(qubit_top, q)
m = measure(q)
result("result", m)
m_top = measure(qubit_top)
result("result", m_top)
hugr = main.compile()
# Upload the compiled HUGR program to Nexus database, get a reference to it
ghz_hugr_program_ref = qnx.hugr.upload(
hugr_package=hugr,
name="4 qubit GHZ State Prep",
)
# Define our Selene configuration
config = qnx.models.SeleneConfig(
n_qubits=4,
simulator=qnx.models.StatevectorSimulator()
)
selene_quest_job_ref = qnx.start_execute_job(
programs=[ghz_hugr_program_ref],
n_shots=[100],
backend_config=config,
name=f"4 qubit GHZ on on {config.__class__.__name__} {datetime.now()}",
)
# Might take time during busy periods
qnx.jobs.wait_for(selene_quest_job_ref, timeout=None)
qsys_result = qnx.jobs.results(selene_quest_job_ref)[0].download_result()
qsys_result.collated_counts()
Stabilizer simulation¶
Now we will try using the StabilizerSimulator, which is built on Stim.
As a stabilizer simulator it can handle many more qubits, as long as only Clifford gates are used. We don’t set an upper bound but expect that simulating 1000s of qubits should be possible.
N = 200
@guppy
@no_type_check
def entangle(qs: array[qubit, comptime(N)]) -> None:
h(qs[0])
for i in range(len(qs)):
if i != 0:
cx(qs[0], qs[i])
@guppy
@no_type_check
def main() -> None: # noqa: F811
qubits = array(qubit() for _ in range(comptime(N)))
entangle(qubits)
for q in qubits:
m = measure(q)
result("result", m)
hugr = main.compile()
# Upload the compiled HUGR program to Nexus database, get a reference to it
many_qubits_hugr_program_ref = qnx.hugr.upload(
hugr_package=hugr,
name="Many Entangled Qubits",
)
# Our Selene Stim configuration
config = qnx.models.SeleneConfig(
n_qubits=N,
simulator=qnx.models.StabilizerSimulator(angle_threshold=0.01)
)
selene_stim_job_ref = qnx.start_execute_job(
programs=[many_qubits_hugr_program_ref],
n_shots=[100],
backend_config=config,
name=f"Many entangled qubits on {config.__class__.__name__} {datetime.now()}",
)
qnx.jobs.wait_for(selene_stim_job_ref)
qsys_result = qnx.jobs.results(selene_stim_job_ref)[0].download_result()
qsys_result.collated_counts()
Coinflip simulation¶
The CoinflipSimulator does not maintain any quantum state, but instead performs a ‘coin flip’ for any measurement operation - with a configurable bias.
N = 100
@guppy
@no_type_check
def entangle_and_t(qs: array[qubit, comptime(N)]) -> None:
h(qs[0])
for i in range(len(qs)):
if i != 0:
cx(qs[0], qs[i])
t(qs[i])
@guppy
@no_type_check
def main() -> None: # noqa: F811
qubits = array(qubit() for _ in range(comptime(N)))
entangle_and_t(qubits)
for q in qubits:
m = measure(q)
result("result", m)
hugr = main.compile()
# Upload the compiled HUGR program to Nexus database, get a reference to it
many_t_gates_hugr_program_ref = qnx.hugr.upload(
hugr_package=hugr,
name="Many T gates",
)
config = qnx.models.SeleneConfig(
n_qubits=N,
simulator=qnx.models.CoinflipSimulator(bias=1.0,),
)
selene_coinflip_job_ref = qnx.start_execute_job(
programs=[many_t_gates_hugr_program_ref],
n_shots=[100],
backend_config=config,
name=f"Many T Gates on {config.__class__.__name__} {datetime.now()}",
)
qnx.jobs.wait_for(selene_coinflip_job_ref)
qsys_result = qnx.jobs.results(selene_coinflip_job_ref)[0].download_result()
qsys_result.collated_counts()
Classical Replay simulation¶
The ClassicalReplaySimulator offers additional debugging capabilities for Guppy programs. Like the CoinflipSimulator it doesn’t maintain quantum state but instead measurements will correspond to pre-defined boolean values. You must provide a list of these measurement results in advance for each shot.
@guppy
@no_type_check
def main() -> None: # noqa: F811
q0: qubit = qubit()
h(q0)
c0: bool = measure(q0)
result("c0", c0)
if c0:
q1: qubit = qubit()
h(q1)
c1: bool = measure(q1)
result("c1", c1)
hugr = main.compile()
# Upload the compiled HUGR program to Nexus database, get a reference to it
conditional_hugr_program = qnx.hugr.upload(
hugr_package=hugr,
name="Conditional HUGR Program",
)
config = qnx.models.SeleneConfig(
n_qubits=2,
simulator=qnx.models.ClassicalReplaySimulator(measurements=[[True, False], [True, True], [False], [False]]),
)
selene_classical_replay_job_ref = qnx.start_execute_job(
programs=[conditional_hugr_program],
n_shots=[4],
backend_config=config,
name=f"Conditional actions on {config.__class__.__name__} {datetime.now()}",
)
qnx.jobs.wait_for(selene_classical_replay_job_ref)
qsys_result = qnx.jobs.results(selene_classical_replay_job_ref)[0].download_result()
pprint(qsys_result)
Selene Error Models¶
We currently provide a simple DepolarizingErrorModel. This can be used for any Selene configuration but will not have an effect on the debugging tools (e.g. CoinflipSimulator and ClassicalReplaySimulator).
config = qnx.models.SeleneConfig(
n_qubits=4,
error_model=qnx.models.DepolarizingErrorModel(
p_init=0.99,
p_1q=0.99,
p_meas=0.99,
),
)
noisy_selene_quest_job_ref = qnx.start_execute_job(
programs=[ghz_hugr_program_ref],
n_shots=[100],
backend_config=config,
name=f"Noisy 4 qubit GHZ for {config.__class__.__name__} {datetime.now()}",
)
qnx.jobs.wait_for(noisy_selene_quest_job_ref)
qsys_result = qnx.jobs.results(noisy_selene_quest_job_ref)[0].download_result()
qsys_result.collated_counts()
We can compare this noisy result with the noiseless one we obtained earlier.
qsys_result = qnx.jobs.results(selene_quest_job_ref)[0].download_result()
qsys_result.collated_counts()
Advanced SelenePlus Features¶
You will need access to advanced emulation features to use SelenePlus, which allows you to:
emulate the Helios runtime with
HeliosRuntimeconfigure error for Helios emulation with
QSystemErrorModelandHeliosCustomErrorModeluse Quantinuum’s
MatrixProductStateSimulator
Advanced Error Configuration¶
QSystemErrorModel will use a preconfigured noise model set by Quantinuum that most closely matches the quantum hardware.
HeliosCustomErrorModel will let you specify the individual error parameters by hand.
# Emulation of Helios hardware with Quantinuum's error model
config = qnx.models.SelenePlusConfig(
n_qubits=4,
runtime=qnx.models.HeliosRuntime(),
error_model=qnx.models.QSystemErrorModel(),
)
# Emulation of Helios with a custom error model defined by you
config = qnx.models.SelenePlusConfig(
n_qubits=4,
runtime=qnx.models.HeliosRuntime(),
error_model=qnx.models.HeliosCustomErrorModel(
error_params=qnx.models.HeliosErrorParams(
p_init=0.98,
p1_scale=0.97,
p_crosstalk_init=0.95,
# For more parameters see the docstring
)
),
)
Matrix Product State simulator¶
Our MatrixProductStateSimulator is developed in-house at Quantinuum.
The amount of qubits you can simulate depends on the level of fidelity you find acceptable (set via configuration - see docstring for details).
config = qnx.SelenePlusConfig(
n_qubits=N,
simulator=qnx.models.MatrixProductStateSimulator(
backend="auto",
seed=123141,
zero_threshold=0.01,
chi=50,
),
)
selene_lean_job_ref = qnx.start_execute_job(
programs=[many_t_gates_hugr_program_ref],
n_shots=[100],
backend_config=config,
name=f"Many T Gates on {config.__class__.__name__} {datetime.now()}",
)
qnx.jobs.wait_for(selene_lean_job_ref)
qsys_result = qnx.jobs.results(selene_lean_job_ref)[0].download_result()
qsys_result.collated_counts()
SelenePlus Runtimes¶
In additional to the default SimpleRuntime. We currently provide a HeliosRuntime - this will emulate the real runtime environment of Quantinuum’s Helios system.
config = qnx.models.SelenePlusConfig(
n_qubits=4,
runtime=qnx.models.HeliosRuntime(),
)
noisy_selene_quest_job_ref = qnx.start_execute_job(
programs=[ghz_hugr_program_ref],
n_shots=[100],
backend_config=config,
name=f"Noisy 4 qubit GHZ for {config.__class__.__name__} {datetime.now()}",
)
qnx.jobs.wait_for(noisy_selene_quest_job_ref)
qsys_result = qnx.jobs.results(noisy_selene_quest_job_ref)[0].download_result()
qsys_result.collated_counts()
Cloud-hosted Helios emulation¶
The above has notebook details how to use the Selene platform in Nexus, however if you have access to advanced emulation features then you will also have access to a lightweight cloud-hosted Helios emulator such as Helios-1E-lite.
This system uses SelenePlus features under the hood, and can be accessed with the HeliosConfig.
By default it will use StatevectorSimulator and QSystemErrorModel, but these can be configured if desired.
# Run on Helios-1E-lite in the cloud with default settings
config = qnx.models.HeliosConfig(
system_name="Helios-1E-lite",
emulator_config=qnx.models.HeliosEmulatorConfig(
n_qubits=20,
)
)
default_helios_lite_job_ref = qnx.start_execute_job(
programs=[ghz_hugr_program_ref],
backend_config=config,
name="Helios-1E-lite test Job",
n_shots=[100],
)
qnx.jobs.wait_for(default_helios_lite_job_ref)
qsys_result = qnx.jobs.results(default_helios_lite_job_ref)[0].download_result()
qsys_result.collated_counts()