Skip to content

API specification

The emu-sv API is based on the specification here. Concretely, the classes are as follows:

SVBackend

Bases: Backend

A backend for emulating Pulser sequences using state vectors and sparse matrices.

run(sequence, sv_config)

Emulates the given sequence.

PARAMETER DESCRIPTION
sequence

a Pulser sequence to simulate

TYPE: Sequence

sv_config

the backends config. Should be of type SVConfig

TYPE: BackendConfig

RETURNS DESCRIPTION
Results

the simulation results

Source code in emu_sv/sv_backend.py
def run(self, sequence: Sequence, sv_config: BackendConfig) -> Results:
    """
    Emulates the given sequence.

    Args:
        sequence: a Pulser sequence to simulate
        sv_config: the backends config. Should be of type SVConfig

    Returns:
        the simulation results
    """
    assert isinstance(sv_config, SVConfig)

    self.validate_sequence(sequence)

    results = Results()

    data = PulserData(sequence=sequence, config=sv_config, dt=sv_config.dt)
    omega, delta, phi = data.omega, data.delta, data.phi

    nsteps = omega.shape[0]
    nqubits = omega.shape[1]
    device = "cuda" if sv_config.gpu and DEVICE_COUNT > 0 else "cpu"

    if sv_config.initial_state is not None:
        state = sv_config.initial_state
        state.vector = state.vector.to(device)
    else:
        state = StateVector.make(nqubits, gpu=sv_config.gpu)

    dt = sv_config.dt * 1e-3  # ns to µs

    for step in range(nsteps):

        start = time()

        state.vector, H = do_time_step(
            dt,
            omega[step],
            delta[step],
            phi[step],
            data.full_interaction_matrix,
            state.vector,
            sv_config.krylov_tolerance,
        )

        for callback in sv_config.callbacks:
            callback(
                sv_config,
                (step + 1) * sv_config.dt,
                state,
                H,  # type: ignore[arg-type]
                results,
            )

        end = time()
        self.log_step_statistics(
            results,
            step=step,
            duration=end - start,
            timestep_count=nsteps,
            state=state,
            sv_config=sv_config,
        )

    return results

SVConfig

Bases: BackendConfig

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
initial_state

the initial state to use in the simulation

TYPE: StateVector | None DEFAULT: None

dt

the timestep size that the solver uses. Note that observables are only calculated if the evaluation_times are divisible by dt.

TYPE: int DEFAULT: 10

max_krylov_dim

the size of the krylov subspace that the Lanczos algorithm maximally builds

TYPE: int DEFAULT: 100

krylov_tolerance

the Lanczos algorithm uses this as the convergence tolerance

TYPE: float DEFAULT: 1e-10

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: bool DEFAULT: True

kwargs

arguments that are passed to the base class

TYPE: Any DEFAULT: {}

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,
    *,
    initial_state: StateVector | None = None,
    dt: int = 10,
    max_krylov_dim: int = 100,
    krylov_tolerance: float = 1e-10,
    gpu: bool = True,
    **kwargs: Any,
):
    super().__init__(**kwargs)

    self.initial_state = initial_state
    self.dt = dt
    self.max_krylov_dim = max_krylov_dim
    self.gpu = gpu
    self.krylov_tolerance = krylov_tolerance

    for num, obs in enumerate(self.callbacks):  # monkey patch
        obs_copy = copy.deepcopy(obs)
        if isinstance(obs, QubitDensity):
            obs_copy.apply = MethodType(  # type: ignore[method-assign]
                qubit_density_sv_impl, obs
            )
            self.callbacks[num] = obs_copy
        elif isinstance(obs, EnergyVariance):
            obs_copy.apply = MethodType(  # type: ignore[method-assign]
                energy_variance_sv_impl, obs
            )
            self.callbacks[num] = obs_copy
        elif isinstance(obs, SecondMomentOfEnergy):
            obs_copy.apply = MethodType(  # type: ignore[method-assign]
                second_moment_sv_impl, obs
            )
            self.callbacks[num] = obs_copy
        elif isinstance(obs, CorrelationMatrix):
            obs_copy.apply = MethodType(  # type: ignore[method-assign]
                correlation_matrix_sv_impl, obs
            )
            self.callbacks[num] = obs_copy

StateVector

Bases: State

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,
):
    # 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"

    device = "cuda" if gpu and DEVICE_COUNT > 0 else "cpu"
    self.vector = vector.to(dtype=dtype, device=device)

__add__(other)

Sum of two state vectors

PARAMETER DESCRIPTION
other

the vector to add to this vector

TYPE: State

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 also needs to be a StateVector"
    result = self.vector + other.vector
    return StateVector(result)

__rmul__(scalar)

Scalar multiplication

PARAMETER DESCRIPTION
scalar

the scalar to multiply with

TYPE: complex

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
    """
    result = scalar * self.vector

    return StateVector(result)

from_state_string(*, basis, nqubits, strings, **kwargs) staticmethod

Transforms a state given by a string into a state vector.

Construct a state from the pulser abstract representation https://pulser.readthedocs.io/en/stable/conventions.html

PARAMETER DESCRIPTION
basis

A tuple containing the basis states (e.g., ('r', 'g')).

TYPE: Iterable[str]

nqubits

the number of qubits.

TYPE: int

strings

A dictionary mapping state strings to complex or floats amplitudes.

TYPE: dict[str, complex]

RETURNS DESCRIPTION
StateVector

The resulting state.

Examples:

>>> basis = ("r","g")
>>> n = 2
>>> st=StateVector.from_state_string(basis=basis,nqubits=n,strings={"rr":1.0,"gg":1.0})
>>> print(st)
tensor([0.7071+0.j, 0.0000+0.j, 0.0000+0.j, 0.7071+0.j], dtype=torch.complex128)
Source code in emu_sv/state_vector.py
@staticmethod
def from_state_string(
    *,
    basis: Iterable[str],
    nqubits: int,
    strings: dict[str, complex],
    **kwargs: Any,
) -> StateVector:
    """Transforms a state given by a string into a state vector.

    Construct a state from the pulser abstract representation
    https://pulser.readthedocs.io/en/stable/conventions.html

    Args:
        basis: A tuple containing the basis states (e.g., ('r', 'g')).
        nqubits: the number of qubits.
        strings: A dictionary mapping state strings to complex or floats amplitudes.

    Returns:
        The resulting state.

    Examples:
        >>> basis = ("r","g")
        >>> n = 2
        >>> st=StateVector.from_state_string(basis=basis,nqubits=n,strings={"rr":1.0,"gg":1.0})
        >>> print(st)
        tensor([0.7071+0.j, 0.0000+0.j, 0.0000+0.j, 0.7071+0.j], dtype=torch.complex128)
    """

    basis = set(basis)
    if basis == {"r", "g"}:
        one = "r"
    elif basis == {"0", "1"}:
        one = "1"
    else:
        raise ValueError("Unsupported basis provided")

    accum_state = StateVector.zero(num_sites=nqubits, **kwargs)

    for state, amplitude in strings.items():
        bin_to_int = int(
            state.replace(one, "1").replace("g", "0"), 2
        )  # "0" basis is already in "0"
        accum_state.vector[bin_to_int] = torch.tensor([amplitude])

    accum_state._normalize()

    return accum_state

inner(other)

Compute . The type of other must be StateVector.

PARAMETER DESCRIPTION
other

the other state

TYPE: State

RETURNS DESCRIPTION
float | complex

the inner product

Source code in emu_sv/state_vector.py
def inner(self, other: State) -> float | complex:
    """
    Compute <self, other>. The type of other must be StateVector.

    Args:
        other: the other state

    Returns:
        the inner product
    """
    assert isinstance(
        other, StateVector
    ), "Other state also needs to be a StateVector"
    assert (
        self.vector.shape == other.vector.shape
    ), "States do not have the same number of sites"

    return torch.vdot(self.vector, other.vector).item()

make(num_sites, gpu=True) classmethod

Returns a State vector in ground state |000..0>. The vector in the output of StateVector has the shape (2,)*number of qubits

PARAMETER DESCRIPTION
num_sites

the number of qubits

TYPE: int

gpu

whether gpu or cpu

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
StateVector

The described state

Examples:

>>> StateVector.make(2)
tensor([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128)
Source code in emu_sv/state_vector.py
@classmethod
def make(cls, num_sites: int, gpu: bool = True) -> StateVector:
    """
    Returns a State vector in ground state |000..0>.
    The vector in the output of StateVector has the shape (2,)*number of qubits

    Args:
        num_sites: the number of qubits
        gpu: whether gpu or cpu

    Returns:
        The described state

    Examples:
        >>> StateVector.make(2)
        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()

Returns the norm of the state

RETURNS DESCRIPTION
float | complex

the norm of the state

Source code in emu_sv/state_vector.py
def norm(self) -> float | complex:
    """Returns the norm of the state

    Returns:
        the norm of the state
    """
    norm: float | complex = torch.linalg.vector_norm(self.vector).item()
    return norm

sample(num_shots=1000, 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: int DEFAULT: 1000

p_false_pos

the rate at which a 0 is read as a 1

TYPE: float DEFAULT: 0.0

p_false_neg

teh rate at which a 1 is read as a 0

TYPE: float DEFAULT: 0.0

RETURNS DESCRIPTION
Counter[str]

the measured bitstrings, by count

Source code in emu_sv/state_vector.py
def sample(
    self, num_shots: int = 1000, 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
    """

    probabilities = torch.abs(self.vector) ** 2
    probabilities /= probabilities.sum()  # multinomial does not normalize the input

    outcomes = torch.multinomial(probabilities, num_shots, replacement=True)

    # Convert outcomes to bitstrings and count occurrences
    counts = Counter([self._index_to_bitstring(outcome) for outcome in outcomes])

    # NOTE: false positives and negatives
    return counts

zero(num_sites, gpu=True) classmethod

Returns a zero uninitialized "state" vector. Warning, this has no physical meaning as-is!

PARAMETER DESCRIPTION
num_sites

the number of qubits

TYPE: int

gpu

whether gpu or cpu

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
StateVector

The zero state

Examples:

>>> StateVector.zero(2)
tensor([0.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=torch.complex128)
Source code in emu_sv/state_vector.py
@classmethod
def zero(cls, num_sites: int, gpu: bool = True) -> 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)
        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)

inner

Wrapper around StateVector.inner.

PARAMETER DESCRIPTION
left

StateVector argument

TYPE: StateVector

right

StateVector argument

TYPE: StateVector

RETURNS DESCRIPTION
Tensor

the inner product

Examples:

>>> factor = math.sqrt(2.0)
>>> basis = ("r","g")
>>> nqubits = 2
>>> 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)
>>> inner(state1,state2).item()
(0.4999999999999999+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")
        >>> nqubits = 2
        >>> 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)
        >>> inner(state1,state2).item()
        (0.4999999999999999+0j)
    """

    assert (left.vector.shape == right.vector.shape) and (
        left.vector.dim() == 1
    ), "Shape of a and b should be the same and both needs to be 1D tesnor"
    return torch.inner(left.vector, right.vector)

DenseOperator

Bases: Operator

Operators in EMU-SV are 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)

Returns the sum of two matrices

PARAMETER DESCRIPTION
other

the other operator

TYPE: Operator

RETURNS DESCRIPTION
DenseOperator

the summed operator

Source code in emu_sv/dense_operator.py
def __add__(self, other: Operator) -> DenseOperator:
    """
    Returns the sum of two matrices

    Args:
        other: the other operator

    Returns:
        the summed operator
    """
    assert isinstance(other, DenseOperator), "MPO can only be added to another MPO"

    return DenseOperator(self.matrix + other.matrix)

__matmul__(other)

Apply this operator to a other. The ordering is that self is applied after other.

PARAMETER DESCRIPTION
other

the operator to compose with self

TYPE: Operator

RETURNS DESCRIPTION
DenseOperator

the composed operator

Source code in emu_sv/dense_operator.py
def __matmul__(self, other: Operator) -> DenseOperator:
    """
    Apply this operator to a other. The ordering is that
    self is applied after other.

    Args:
        other: the operator to compose with self

    Returns:
        the composed operator
    """
    assert isinstance(
        other, DenseOperator
    ), "DenseOperator can only be multiplied with Operator"

    return DenseOperator(self.matrix @ other.matrix)

__mul__(other)

Applies this DenseOperator to the given StateVector.

PARAMETER DESCRIPTION
other

the state to apply this operator to

TYPE: State

RETURNS DESCRIPTION
StateVector

the resulting state

Source code in emu_sv/dense_operator.py
def __mul__(self, other: State) -> StateVector:
    """
    Applies this DenseOperator to the given StateVector.

    Args:
        other: the state to apply this operator to

    Returns:
        the resulting state
    """
    assert isinstance(
        other, StateVector
    ), "DenseOperator can only be applied to another DenseOperator"

    return StateVector(self.matrix @ other.vector)

__rmul__(scalar)

Multiply a DenseOperator by scalar.

PARAMETER DESCRIPTION
scalar

the scale factor to multiply with

TYPE: complex

RETURNS DESCRIPTION
DenseOperator

the scaled MPO

Source code in emu_sv/dense_operator.py
def __rmul__(self, scalar: complex) -> DenseOperator:
    """
    Multiply a DenseOperator by scalar.

    Args:
        scalar: the scale factor to multiply with

    Returns:
        the scaled MPO
    """

    return DenseOperator(self.matrix * scalar)

expect(state)

Compute the expectation value of self on the given state.

PARAMETER DESCRIPTION
state

the state with which to compute

TYPE: State

RETURNS DESCRIPTION
float | complex

the expectation

Source code in emu_sv/dense_operator.py
def expect(self, state: State) -> float | complex:
    """
    Compute the expectation value of self on the given state.

    Args:
        state: the state with which to compute

    Returns:
        the expectation
    """
    assert isinstance(
        state, StateVector
    ), "currently, only expectation values of StateVectors are \
    supported"

    return torch.vdot(state.vector, self.matrix @ state.vector).item()

from_operator_string(basis, nqubits, operations, operators={}, /, **kwargs) staticmethod

See the base class

PARAMETER DESCRIPTION
basis

the eigenstates in the basis to use e.g. ('r', 'g')

TYPE: Iterable[str]

nqubits

how many qubits there are in the state

TYPE: int

operations

which bitstrings make up the state with what weight

TYPE: FullOp

operators

additional symbols to be used in operations

TYPE: dict[str, QuditOp] DEFAULT: {}

RETURNS DESCRIPTION
DenseOperator

the operator in MPO form.

Source code in emu_sv/dense_operator.py
@staticmethod
def from_operator_string(
    basis: Iterable[str],
    nqubits: int,
    operations: FullOp,
    operators: dict[str, QuditOp] = {},
    /,
    **kwargs: Any,
) -> DenseOperator:
    """
    See the base class

    Args:
        basis: the eigenstates in the basis to use e.g. ('r', 'g')
        nqubits: how many qubits there are in the state
        operations: which bitstrings make up the state with what weight
        operators: additional symbols to be used in operations

    Returns:
        the operator in MPO form.
    """

    _validate_operator_targets(operations, nqubits)

    operators_with_tensors: dict[str, torch.Tensor | QuditOp] = dict(operators)

    basis = set(basis)
    if basis == {"r", "g"}:
        # operators_with_tensors will now contain the basis for single qubit ops,
        # and potentially user defined strings in terms of these
        operators_with_tensors |= {
            "gg": torch.tensor([[1.0, 0.0], [0.0, 0.0]], dtype=torch.complex128),
            "gr": torch.tensor([[0.0, 0.0], [1.0, 0.0]], dtype=torch.complex128),
            "rg": torch.tensor([[0.0, 1.0], [0.0, 0.0]], dtype=torch.complex128),
            "rr": torch.tensor([[0.0, 0.0], [0.0, 1.0]], dtype=torch.complex128),
        }
    elif basis == {"0", "1"}:
        # operators_with_tensors will now contain the basis for single qubit ops,
        # and potentially user defined strings in terms of these
        operators_with_tensors |= {
            "00": torch.tensor([[1.0, 0.0], [0.0, 0.0]], dtype=torch.complex128),
            "01": torch.tensor([[0.0, 0.0], [1.0, 0.0]], dtype=torch.complex128),
            "10": torch.tensor([[0.0, 1.0], [0.0, 0.0]], dtype=torch.complex128),
            "11": torch.tensor([[0.0, 0.0], [0.0, 1.0]], dtype=torch.complex128),
        }
    else:
        raise ValueError("Unsupported basis provided")

    accum_res = torch.zeros(2**nqubits, 2**nqubits, dtype=torch.complex128)
    for coeff, tensorop in operations:
        # this function will recurse through the operators_with_tensors,
        # and replace any definitions in terms of strings by the computed matrix
        def replace_operator_string(op: QuditOp | torch.Tensor) -> torch.Tensor:
            if isinstance(op, torch.Tensor):
                return op

            result = torch.zeros(2, 2, dtype=torch.complex128)
            for opstr, coeff in op.items():
                tensor = replace_operator_string(operators_with_tensors[opstr])
                operators_with_tensors[opstr] = tensor
                result += tensor * coeff
            return result

        total_op_per_qubit = [torch.eye(2, 2, dtype=torch.complex128)] * nqubits

        for op in tensorop:
            factor = replace_operator_string(op[0])
            for target_qubit in op[1]:
                total_op_per_qubit[target_qubit] = factor

        dense_op = total_op_per_qubit[0]
        for single_qubit_operator in total_op_per_qubit[1:]:
            dense_op = torch.kron(dense_op, single_qubit_operator)

        accum_res += coeff * dense_op
    return DenseOperator(accum_res)