API specification
The emu-sv API is based on the specification here. Concretely, the classes are as follows:
SVBackend
Section titled “SVBackend”
Bases: EmulatorBackend
A backend for emulating Pulser sequences using state vectors and sparse matrices. Noisy simulation is supported by solving the Lindblad equation and using effective noise channel or jump operators
Source code in pulser/backend/abc.py
def __init__( self, sequence: pulser.Sequence, *, config: EmulationConfig | None = None, mimic_qpu: bool = False,) -> None: """Initializes the backend.""" super().__init__(sequence, mimic_qpu=mimic_qpu) config = config or self.default_config if not isinstance(config, EmulationConfig): raise TypeError( "'config' must be an instance of 'EmulationConfig', " f"not {type(config)}." ) # See the BackendConfig definition to see why this works self._config = type(self.default_config)(**config._backend_options)
run()
Section titled “
run()
”Emulates the given sequence.
RETURNS | DESCRIPTION |
---|---|
Results
|
the simulation results |
Source code in emu_sv/sv_backend.py
def run(self) -> Results: """ Emulates the given sequence.
Returns: the simulation results """ assert isinstance(self._config, SVConfig)
impl = create_impl(self._sequence, self._config) return impl._run()
SVConfig
Section titled “SVConfig”
Bases: EmulationConfig
The configuration of the emu-sv SVBackend. The kwargs passed to this class are passed on to the base class. See the API for that class for a list of available options.
PARAMETER | DESCRIPTION |
---|---|
dt
|
the timestep size that the solver uses. Note that observables are only calculated if the evaluation_times are divisible by dt.
TYPE:
|
max_krylov_dim
|
the size of the krylov subspace that the Lanczos algorithm maximally builds
TYPE:
|
krylov_tolerance
|
the Lanczos algorithm uses this as the convergence tolerance
TYPE:
|
gpu
|
Use 1 gpu if True, and a GPU is available, otherwise, cpu. Will cause errors if True when a gpu is not available
TYPE:
|
interaction_cutoff
|
Set interaction coefficients below this value to
TYPE:
|
log_level
|
How much to log. Set to
TYPE:
|
log_file
|
If specified, log to this file rather than stout.
TYPE:
|
kwargs
|
arguments that are passed to the base class
TYPE:
|
Examples:
>>> gpu = True>>> dt = 1 #this will impact the runtime>>> krylov_tolerance = 1e-8 #the simulation will be faster, but less accurate>>> SVConfig(gpu=gpu, dt=dt, krylov_tolerance=krylov_tolerance,>>> with_modulation=True) #the last arg is taken from the base class
Source code in emu_sv/sv_config.py
def __init__( self, *, dt: int = 10, max_krylov_dim: int = 100, krylov_tolerance: float = 1e-10, gpu: bool = True, interaction_cutoff: float = 0.0, log_level: int = logging.INFO, log_file: pathlib.Path | None = None, **kwargs: Any,): kwargs.setdefault("observables", [BitStrings(evaluation_times=[1.0])]) super().__init__( dt=dt, max_krylov_dim=max_krylov_dim, gpu=gpu, krylov_tolerance=krylov_tolerance, interaction_cutoff=interaction_cutoff, log_level=log_level, log_file=log_file, **kwargs, )
self.monkeypatch_observables()
self.logger = logging.getLogger("global_logger") if log_file is None: logging.basicConfig( level=log_level, format="%(message)s", stream=sys.stdout, force=True ) # default to stream = sys.stderr else: logging.basicConfig( level=log_level, format="%(message)s", filename=str(log_file), filemode="w", force=True, ) if (self.noise_model.runs != 1 and self.noise_model.runs is not None) or ( self.noise_model.samples_per_run != 1 and self.noise_model.samples_per_run is not None ): self.logger.warning( "Warning: The runs and samples_per_run " "values of the NoiseModel are ignored!" ) if "SPAM" in self.noise_model.noise_types: raise NotImplementedError( "SPAM errors are currently not supported in emu-sv." )
StateVector
Section titled “StateVector”
Bases: State[complex, Tensor]
Represents a quantum state vector in a computational basis.
This class extends the State
class to handle state vectors,
providing various utilities for initialization, normalization,
manipulation, and measurement. The state vector must have a length
that is a power of 2, representing 2ⁿ basis states for n qubits.
ATTRIBUTE | DESCRIPTION |
---|---|
vector |
1D tensor representation of a state vector.
|
gpu |
store the vector on GPU if True, otherwise on CPU
|
Source code in emu_sv/state_vector.py
def __init__( self, vector: torch.Tensor, *, gpu: bool = True, eigenstates: Sequence[Eigenstate] = ("r", "g"),): # NOTE: this accepts also zero vectors.
assert math.log2( len(vector) ).is_integer(), "The number of elements in the vector should be power of 2"
super().__init__(eigenstates=eigenstates) device = "cuda" if gpu and DEVICE_COUNT > 0 else "cpu" self.vector = vector.to(dtype=dtype, device=device)
n_qudits
property
Section titled “
n_qudits
property
”The number of qudits in the state.
__add__(other)
Section titled “
__add__(other)
”Sum of two state vectors
PARAMETER | DESCRIPTION |
---|---|
other
|
the vector to add to this vector
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
StateVector
|
The summed state |
Source code in emu_sv/state_vector.py
def __add__(self, other: State) -> StateVector: """Sum of two state vectors
Args: other: the vector to add to this vector
Returns: The summed state """ assert isinstance(other, StateVector), "`Other` state can only be a StateVector" assert ( self.eigenstates == other.eigenstates ), f"`Other` state has basis {other.eigenstates} != {self.eigenstates}" return StateVector( self.vector + other.vector, gpu=self.vector.is_cuda, eigenstates=self.eigenstates, )
__rmul__(scalar)
Section titled “
__rmul__(scalar)
”Scalar multiplication
PARAMETER | DESCRIPTION |
---|---|
scalar
|
the scalar to multiply with
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
StateVector
|
The scaled state |
Source code in emu_sv/state_vector.py
def __rmul__(self, scalar: complex) -> StateVector: """Scalar multiplication
Args: scalar: the scalar to multiply with
Returns: The scaled state """ return StateVector( scalar * self.vector, gpu=self.vector.is_cuda, eigenstates=self.eigenstates, )
inner(other)
Section titled “
inner(other)
”PARAMETER | DESCRIPTION |
---|---|
other
|
the other state
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
Tensor
|
the inner product |
Source code in emu_sv/state_vector.py
def inner(self, other: State) -> torch.Tensor: """ Compute . The type of other must be StateVector.
Args: other: the other state
Returns: the inner product """ assert isinstance(other, StateVector), "Other state must be a StateVector" assert ( self.vector.shape == other.vector.shape ), "States do not have the same shape"
# by our internal convention inner and norm return to cpu return torch.vdot(self.vector, other.vector).cpu()
make(num_sites, gpu=True)
classmethod
Section titled “
make(num_sites, gpu=True)
classmethod
”Returns a State vector in the ground state |00..0>.
PARAMETER | DESCRIPTION |
---|---|
num_sites
|
the number of qubits
TYPE:
|
gpu
|
whether gpu or cpu
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
StateVector
|
The described state |
Examples:
>>> StateVector.make(2,gpu=False)tensor([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128)
Source code in emu_sv/state_vector.py
@classmethoddef make(cls, num_sites: int, gpu: bool = True) -> StateVector: """ Returns a State vector in the ground state |00..0>.
Args: num_sites: the number of qubits gpu: whether gpu or cpu
Returns: The described state
Examples: >>> StateVector.make(2,gpu=False) tensor([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128)
"""
result = cls.zero(num_sites=num_sites, gpu=gpu) result.vector[0] = 1.0 return result
norm()
Section titled “
norm()
”Returns the norm of the state
RETURNS | DESCRIPTION |
---|---|
Tensor
|
the norm of the state |
Source code in emu_sv/state_vector.py
def norm(self) -> torch.Tensor: """Returns the norm of the state
Returns: the norm of the state """ nrm: torch.Tensor = torch.linalg.vector_norm(self.vector).cpu() return nrm
sample(*, num_shots=1000, one_state=None, p_false_pos=0.0, p_false_neg=0.0)
Section titled “
sample(*, num_shots=1000, one_state=None, p_false_pos=0.0, p_false_neg=0.0)
”Samples bitstrings, taking into account the specified error rates.
PARAMETER | DESCRIPTION |
---|---|
num_shots
|
how many bitstrings to sample
TYPE:
|
p_false_pos
|
the rate at which a 0 is read as a 1
TYPE:
|
p_false_neg
|
teh rate at which a 1 is read as a 0
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
Counter[str]
|
the measured bitstrings, by count |
Source code in emu_sv/state_vector.py
def sample( self, *, num_shots: int = 1000, one_state: Eigenstate | None = None, p_false_pos: float = 0.0, p_false_neg: float = 0.0,) -> Counter[str]: """ Samples bitstrings, taking into account the specified error rates.
Args: num_shots: how many bitstrings to sample p_false_pos: the rate at which a 0 is read as a 1 p_false_neg: teh rate at which a 1 is read as a 0
Returns: the measured bitstrings, by count """ assert p_false_neg == p_false_pos == 0.0, "Error rates must be 0.0"
probabilities = torch.abs(self.vector) ** 2
outcomes = torch.multinomial(probabilities, num_shots, replacement=True)
# Convert outcomes to bitstrings and count occurrences counts = Counter( [index_to_bitstring(self.n_qudits, outcome) for outcome in outcomes] )
# NOTE: false positives and negatives return counts
zero(num_sites, gpu=True, eigenstates=('r', 'g'))
classmethod
Section titled “
zero(num_sites, gpu=True, eigenstates=('r', 'g'))
classmethod
”Returns a zero uninitialized "state" vector. Warning, this has no physical meaning as-is!
PARAMETER | DESCRIPTION |
---|---|
num_sites
|
the number of qubits
TYPE:
|
gpu
|
whether gpu or cpu
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
StateVector
|
The zero state |
Examples:
>>> StateVector.zero(2,gpu=False)tensor([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128)
Source code in emu_sv/state_vector.py
@classmethoddef zero( cls, num_sites: int, gpu: bool = True, eigenstates: Sequence[Eigenstate] = ("r", "g"),) -> StateVector: """ Returns a zero uninitialized "state" vector. Warning, this has no physical meaning as-is!
Args: num_sites: the number of qubits gpu: whether gpu or cpu
Returns: The zero state
Examples: >>> StateVector.zero(2,gpu=False) tensor([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128) """
device = "cuda" if gpu and DEVICE_COUNT > 0 else "cpu" vector = torch.zeros(2**num_sites, dtype=dtype, device=device) return cls(vector, gpu=gpu, eigenstates=eigenstates)
Wrapper around StateVector.inner.
PARAMETER | DESCRIPTION |
---|---|
left
|
StateVector argument
TYPE:
|
right
|
StateVector argument
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
Tensor
|
the inner product |
Examples:
>>> factor = math.sqrt(2.0)>>> basis = ("r","g")>>> string_state1 = {"gg":1.0,"rr":1.0}>>> state1 = StateVector.from_state_string(basis=basis, ... nqubits=nqubits,strings=string_state1)>>> string_state2 = {"gr":1.0/factor,"rr":1.0/factor}>>> state2 = StateVector.from_state_string(basis=basis, ... nqubits=nqubits,strings=string_state2)
>>> state1 = StateVector.from_state_amplitudes(eigenstates=basis,... amplitudes=string_state1)>>> string_state2 = {"gr":1.0/factor,"rr":1.0/factor}>>> state2 = StateVector.from_state_amplitudes(eigenstates=basis,... amplitudes=string_state2)>>> inner(state1,state2).item()(0.49999999144286444+0j)
Source code in emu_sv/state_vector.py
def inner(left: StateVector, right: StateVector) -> torch.Tensor: """ Wrapper around StateVector.inner.
Args: left: StateVector argument right: StateVector argument
Returns: the inner product
Examples: >>> factor = math.sqrt(2.0) >>> basis = ("r","g") >>> string_state1 = {"gg":1.0,"rr":1.0} >>> state1 = StateVector.from_state_string(basis=basis, ... nqubits=nqubits,strings=string_state1) >>> string_state2 = {"gr":1.0/factor,"rr":1.0/factor} >>> state2 = StateVector.from_state_string(basis=basis, ... nqubits=nqubits,strings=string_state2)
>>> state1 = StateVector.from_state_amplitudes(eigenstates=basis, ... amplitudes=string_state1) >>> string_state2 = {"gr":1.0/factor,"rr":1.0/factor} >>> state2 = StateVector.from_state_amplitudes(eigenstates=basis, ... amplitudes=string_state2) >>> inner(state1,state2).item() (0.49999999144286444+0j) """
assert (left.vector.shape == right.vector.shape) and (left.vector.dim() == 1), ( "Shape of left.vector and right.vector should be", " the same and both need to be 1D tesnor", ) return left.inner(right)
DenseOperator
Section titled “DenseOperator”
Bases: Operator[complex, Tensor, StateVector]
DenseOperator in EMU-SV use dense matrices
Source code in emu_sv/dense_operator.py
def __init__( self, matrix: torch.Tensor, *, gpu: bool = True,): device = "cuda" if gpu and DEVICE_COUNT > 0 else "cpu" self.matrix = matrix.to(dtype=dtype, device=device)
__add__(other)
Section titled “
__add__(other)
”Element-wise addition of two DenseOperators.
PARAMETER | DESCRIPTION |
---|---|
other
|
a DenseOperator instance.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
DenseOperator
|
A new DenseOperator representing the sum. |
Source code in emu_sv/dense_operator.py
def __add__(self, other: Operator) -> DenseOperator: """ Element-wise addition of two DenseOperators.
Args: other: a DenseOperator instance.
Returns: A new DenseOperator representing the sum. """ assert isinstance( other, DenseOperator ), "DenseOperator can only be added to another DenseOperator." return DenseOperator(self.matrix + other.matrix)
__matmul__(other)
Section titled “
__matmul__(other)
”Compose two DenseOperators via matrix multiplication.
PARAMETER | DESCRIPTION |
---|---|
other
|
a DenseOperator instance.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
DenseOperator
|
A new DenseOperator representing the product |
Source code in emu_sv/dense_operator.py
def __matmul__(self, other: Operator) -> DenseOperator: """ Compose two DenseOperators via matrix multiplication.
Args: other: a DenseOperator instance.
Returns: A new DenseOperator representing the product `self @ other`. """ assert isinstance( other, DenseOperator ), "DenseOperator can only be multiplied with a DenseOperator." return DenseOperator(self.matrix @ other.matrix)
__rmul__(scalar)
Section titled “
__rmul__(scalar)
”Scalar multiplication of the DenseOperator.
PARAMETER | DESCRIPTION |
---|---|
scalar
|
a number to scale the operator.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
DenseOperator
|
A new DenseOperator scaled by the given scalar. |
Source code in emu_sv/dense_operator.py
def __rmul__(self, scalar: complex) -> DenseOperator: """ Scalar multiplication of the DenseOperator.
Args: scalar: a number to scale the operator.
Returns: A new DenseOperator scaled by the given scalar. """
return DenseOperator(scalar * self.matrix)
apply_to(other)
Section titled “
apply_to(other)
”Apply the DenseOperator to a given StateVector.
PARAMETER | DESCRIPTION |
---|---|
other
|
a StateVector instance.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
StateVector
|
A new StateVector after applying the operator. |
Source code in emu_sv/dense_operator.py
def apply_to(self, other: State) -> StateVector: """ Apply the DenseOperator to a given StateVector.
Args: other: a StateVector instance.
Returns: A new StateVector after applying the operator. """ assert isinstance( other, StateVector ), "DenseOperator can only be applied to a StateVector."
return StateVector(self.matrix @ other.vector)
expect(state)
Section titled “
expect(state)
”Compute the expectation value of the operator with respect to a state.
PARAMETER | DESCRIPTION |
---|---|
state
|
a StateVector instance.
TYPE:
|
RETURNS | DESCRIPTION |
---|---|
Tensor
|
The expectation value as a float or complex number. |
Source code in emu_sv/dense_operator.py
def expect(self, state: State) -> torch.Tensor: """ Compute the expectation value of the operator with respect to a state.
Args: state: a StateVector instance.
Returns: The expectation value as a float or complex number. """ assert isinstance( state, StateVector ), "Only expectation values of StateVectors are supported."
return torch.vdot(state.vector, self.apply_to(state).vector).cpu()