Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions guppylang/defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ def emulator(
"""Compile this function for emulation with the selene-sim emulator.

Calls `compile()` to get the HUGR package and then builds it using the
provided `EmulatorBuilder` or a default one.
provided `EmulatorBuilder` configuration or a default one.


Args:
n_qubits: The number of qubits to allocate for the function.
builder: An optional `EmulatorBuilder` to use for building the emulator
instance. If not provided, a default `EmulatorBuilder` will be used.
instance. If not provided, the default `EmulatorBuilder` will be used.

Returns:
An `EmulatorInstance` that can be used to run the function in an emulator.
Expand Down
8 changes: 8 additions & 0 deletions guppylang/emulator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
"""
Emulator module for GuppyLang.

This module provides classes for building and executing emulators that can run
Guppy programs. It includes functionality for extracting emulator state,
building and configuring emulator instances, and processing emulation results.
"""

from .builder import EmulatorBuilder
from .instance import EmulatorInstance
from .result import EmulatorResult
Expand Down
27 changes: 25 additions & 2 deletions guppylang/emulator/builder.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
Compiling and building emulator instances for guppy programs.
"""

from __future__ import annotations

from dataclasses import dataclass, replace
Expand All @@ -18,7 +22,10 @@

@dataclass(frozen=True)
class EmulatorBuilder:
"""A builder class for creating EmulatorInstance objects."""
"""A builder class for creating EmulatorInstance objects.

Supports configuration parameters for compilation of emulator instances.
"""

# interface supported parameters
_name: str | None = None
Expand All @@ -35,27 +42,43 @@ class EmulatorBuilder:

@property
def name(self) -> str | None:
"""User specified name for the emulator instance. Defaults to None."""
return self._name

@property
def build_dir(self) -> Path | None:
"""Directory to store intermediate build files and execution results.
Defaults to None, in which case a temporary directory is used."""
return self._build_dir

@property
def verbose(self) -> bool:
"""Whether to print verbose output during the build process."""
return self._verbose

def with_name(self, value: str | None) -> Self:
"""Set the name for the emulator instance."""
return replace(self, _name=value)

def with_build_dir(self, value: Path | None) -> Self:
"""Set the build directory for the emulator instance,
see `EmulatorBuilder.build_dir`."""
return replace(self, _build_dir=value)

def with_verbose(self, value: bool) -> Self:
"""Set whether to print verbose output during the build process."""
return replace(self, _verbose=value)

def build(self, package: Package, n_qubits: int) -> EmulatorInstance:
"""Build an EmulatorInstance from a compiled package."""
"""Build an EmulatorInstance from a compiled package.

Args:
package: The compiled HUGR package to build the emulator from.
n_qubits: The number of qubits to allocate for the emulator instance.

Returns:
An EmulatorInstance that can be used to run the compiled program.
"""

instance = selene_sim.build( # type: ignore[attr-defined]
package,
Expand Down
66 changes: 60 additions & 6 deletions guppylang/emulator/instance.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
Configuring and executing emulator instances for guppy programs.
"""

from __future__ import annotations

from dataclasses import dataclass, field, replace
Expand Down Expand Up @@ -42,6 +46,14 @@ class _Options:

@dataclass(frozen=True)
class EmulatorInstance:
"""An emulator instance for running a compiled program.


Returned by `GuppyFunctionDefinition.emulator()`.
Contains configuration options for the emulator instance, such as the number of
qubits, the number of shots, the simulator backend, and more.
"""

_instance: SeleneInstance
_n_qubits: int
_options: _Options = field(default_factory=_Options)
Expand All @@ -57,107 +69,149 @@ def n_qubits(self) -> int:

@property
def shots(self) -> int:
"""Number of shots to run for each execution."""
return self._options._shots

@property
def simulator(self) -> Simulator:
"""Simulation backend used for running the emulator instance."""
return self._options._simulator

@property
def runtime(self) -> Runtime:
"""Runtime used for executing the emulator instance."""
return self._options._runtime

@property
def error_model(self) -> ErrorModel:
"""Device error model used for the emulator instance."""
return self._options._error_model

@property
def verbose(self) -> bool:
"""Whether to print verbose output during the emulator execution."""
return self._options._verbose

@property
def timeout(self) -> datetime.timedelta | None:
"""Timeout for the emulator execution, if any."""
return self._options._timeout

@property
def seed(self) -> int | None:
"""Random seed for the emulator instance, if any."""
return self._options._seed

@property
def shot_offset(self) -> int:
"""Offset for the shot numbers, shot counts will begin at this offset.
Defaults to 0.

This is useful for running multiple emulator instances in parallel"""
return self._options._shot_offset

@property
def shot_increment(self) -> int:
"""Value to increment shot numbers by for each repeated run.
Defaults to 1."""
return self._options._shot_increment

@property
def n_processes(self) -> int:
"""Number of processes to parallelise the emulator execution across.
Defaults to 1, meaning no parallelisation."""
return self._options._n_processes

def with_n_qubits(self, value: int) -> Self:
"""Update the number of qubits for the emulator instance."""
"""Set the number of qubits available in the emulator instance."""
return replace(self, _n_qubits=value)

def with_shots(self, value: int) -> Self:
"""Set the number of shots to run for each execution.
Defaults to 1."""
return self._with_option(_shots=value)

def with_simulator(self, value: Simulator) -> Self:
"""Set the simulation backend used for running the emulator instance.
Defaults to statevector simulation."""
return self._with_option(_simulator=value)

def with_runtime(self, value: Runtime) -> Self:
"""Set the runtime used for executing the emulator instance.
Defaults to SimpleRuntime."""
return self._with_option(_runtime=value)

def with_error_model(self, value: ErrorModel) -> Self:
"""Set the device error model used for the emulator instance.
Defaults to IdealErrorModel (no errors)."""
return self._with_option(_error_model=value)

def with_event_hook(self, value: EventHook) -> Self:
"""Set the event hook used for the emulator instance.
Defaults to NoEventHook."""
return self._with_option(_event_hook=value)

def with_verbose(self, value: bool) -> Self:
"""Set whether to print verbose output during the emulator execution.
Defaults to False."""
return self._with_option(_verbose=value)

def with_timeout(self, value: datetime.timedelta | None) -> Self:
"""Set the timeout for the emulator execution.
Defaults to None (no timeout)."""
return self._with_option(_timeout=value)

def with_results_logfile(self, value: Path | None) -> Self:
return self._with_option(_results_logfile=value)

def with_seed(self, value: int | None) -> Self:
"""Set the random seed for the emulator instance.
Defaults to None."""
new_options = replace(self._options, _seed=value)
# TODO flaky stateful, remove when selene simplifies
new_options._simulator.random_seed = value
out = replace(self, _options=new_options)
return out

def with_shot_offset(self, value: int) -> Self:
"""Set the offset for the shot numbers, shot counts will begin at this offset.
Defaults to 0.

This is useful for running multiple emulator instances in parallel."""
return self._with_option(_shot_offset=value)

def with_shot_increment(self, value: int) -> Self:
"""Set the value to increment shot numbers by for each repeated run.
Defaults to 1."""
return self._with_option(_shot_increment=value)

def with_n_processes(self, value: int) -> Self:
"""Set the number of processes to parallelise the emulator execution across.
Defaults to 1, meaning no parallelisation."""
return self._with_option(_n_processes=value)

def statevector_sim(self) -> Self:
"""Set the simulation backend to the default statevector simulator."""
return self.with_simulator(Quest())

def coinflip_sim(self) -> Self:
"""Set the simulation backend to the coinflip simulator.
This performs no quantum simulation, and flips a coin for each measurement."""
return self.with_simulator(Coinflip())

def stabilizer_sim(self) -> Self:
"""Set the simulation backend to the stabilizer simulator.
This only works for clifford circuits but is very fast."""
return self.with_simulator(Stim())

def run(self) -> EmulatorResult:
# TODO mention only runs one shot by default
"""Run the emulator instance and return the results.
By default runs one shot, this can be configured with `with_shots()`."""
result_stream = self._run_instance()

# TODO progress bar on consuming iterator?

return EmulatorResult(result_stream)

def _run_instance(self) -> Iterator[Iterator[TaggedResult]]:
"""Run the Selene instance with the given simulator."""
"""Run the Selene instance with the given simulator lazily."""
return self._instance.run_shots(
simulator=self.simulator,
n_qubits=self.n_qubits,
Expand Down
52 changes: 50 additions & 2 deletions guppylang/emulator/result.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
Emulation results and post-processing.
"""

from __future__ import annotations

from typing import TYPE_CHECKING
Expand All @@ -12,14 +16,58 @@


class EmulatorResult(QsysResult):
"""A result from running an emulator instance."""
r"""A result from running an emulator instance.


Collects data from `result("tag", val)` calls in the guppy program. Includes results
for all shots.

Includes conversions to traditional distributions over bitstrings if a tagging
convention is used, including conversion to a pytket BackendResult.

Under this convention, tags are assumed to be a name of a bit register unless
they fit the regex pattern `^([a-z][\w_]*)\[(\d+)\]$` (like `my_Reg[12]`) in which
case they are assumed to refer to the nth element of a bit register.

For results of the form ``` result("<register>", value) ``` `value` can be `{0, 1}`,
wherein the register is assumed to be length 1, or lists over those values, wherein
the list is taken to be the value of the entire register.

For results of the form ``` result("<register>[n]", value) ``` `value` can only be
`{0,1}`. The register is assumed to be at least `n+1` in size and unset elements are
assumed to be `0`.

Subsequent writes to the same register/element in the same shot will overwrite.

# TODO more docstring
To convert to a `BackendResult` all registers must be present in all shots, and
register sizes cannot change between shots.

"""

def partial_state_dicts(self) -> list[dict[str, PartialVector]]:
"""Extract state results from shot results in to dictionaries.

Looks for outputs from `state_result("tag", qs)` calls in the guppy program.

Returns:
A list of dictionaries, each dictionary containing the tag as the key and
the `PartialVector` as the value. Each dictionary corresponds to a shot.
Repeated tags in a shot will overwrite previous values.
"""
return [dict(x) for x in self.partial_states()]

def partial_states(self) -> list[list[tuple[str, PartialVector]]]:
"""Extract state results from shot results.


Looks for outputs from `state_result("tag", qs)` calls in the guppy program.

Returns:
A list (over shots) of lists. The outer list is over shots, and the inner
is over the state results in that shot.
Each inner list contains tuples of (string tag, PartialVector).
"""

def to_partial(x: tuple[str, SeleneQuestState]) -> tuple[str, PartialVector]:
return x[0], PartialVector._from_inner(x[1])

Expand Down
Loading
Loading