StateVector

State vector of a quantum state is expressed as StateVector. This holds \(2^n\) complex numbers where \(n\) is the number of qubits.

Indices of the qubit is numbered as \(0,1,\dots,n-1\). The state is represented by computational basis. For example [a, b, c, d] means \(a\ket{00}+b\ket{01}+c\ket{10}+d\ket{11}\). \(i\)-th bit from lower (counted from \(0\)) of the basis is a qubit \(i\). For example, \(\ket{110}\) means that qubit \(0\) is \(\ket{0}\), qubit \(1\) is \(\ket{1}\), and qubit \(2\) is \(\ket{2}\).

StateVector is constructed with single integer value n_qubits, which is the number of qubits. The state is initialized to \(\ket{0\dots 0}\).

from scaluq.default.f64 import StateVector
state = StateVector(2)
print(state)
Qubit Count : 2
Dimension : 4
State vector : 
  00 : (1,0)
  01 : (0,0)
  10 : (0,0)
  11 : (0,0)

Getting property of StateVector

n_qubits returns the number of qubits \(n\).

dim returns the dimension of the vector \(2^n\).

get_amplitudes returns the content of StateVector as type list[complex].

get_squared_norm() returns the squared norm of the state \(\braket{\phi, \phi}\) which must be equal to \(1\) with normalized states.

Initializing StateVector

In addition to the constructor, you can initialize state with some static functions.

Haar_random_state initialize state with Haar random state. You pass seed as an optional argument. Without passing seed, the random device of system is used.

uninitialized_state only allocate memory on the execution space without initializing. The content of the vector is undefined. It is not guaranteed to be normalized. Since allocating the vector by this function is faster than other initializing functions, you should use this if you will load other vector immediately after.

from scaluq.default.f64 import StateVector
state = StateVector.Haar_random_state(2)
print("Haar random (without seed): ", state.get_amplitudes())
state = StateVector.Haar_random_state(2)
print("Haar random (without seed): ", state.get_amplitudes())
state = StateVector.Haar_random_state(2, 0)
print("Haar random (seed = 0): ", state.get_amplitudes())
state = StateVector.Haar_random_state(2, 0)
print("Haar random (seed = 0): ", state.get_amplitudes())
state = StateVector.uninitialized_state(2) # the content is undefined
Haar random (without seed):  [(-0.12377148096652951+0.027715511836463032j), (0.23343008413304153-0.6125930779810899j), (0.348536530607012+0.16314293047597564j), (-0.6344277255462337-0.059671766025997705j)]
Haar random (without seed):  [(0.20861863181020227+0.36023007097415805j), (-0.7038208261050227+0.15424679536389918j), (0.032557696049571434+0.4498796978221459j), (0.3196074684536188+0.04422729148920198j)]
Haar random (seed = 0):  [(0.24602695668676106-0.3593147366777609j), (-0.2016366688947537+0.10904346570777179j), (-0.7078115548871466+0.3479734076173536j), (0.09795534521513291-0.3551589695281517j)]
Haar random (seed = 0):  [(0.24602695668676106-0.3593147366777609j), (-0.2016366688947537+0.10904346570777179j), (-0.7078115548871466+0.3479734076173536j), (0.09795534521513291-0.3551589695281517j)]

Loading StateVector

You can load the content of StateVector by various functions.

set_zero_state is used for setting the vector to \(\ket{00\dots0}=[1,0,\dots,0]\).

set_zero_norm_state is used for setting the vector to \(0=[0,0,\dots,0]\).

set_computational_basis is used for setting the vector to \(\ket{b}\) with input args \(0\leq b \leq 2^{n}-1\).

load is used for loading the other vector (with \(2^n\) length) directly as amplitudes.

from scaluq.default.f64 import StateVector
state = StateVector(2)
state.set_zero_state()
print("zero state:", state.get_amplitudes())
state.set_zero_norm_state()
print("zero norm state:", state.get_amplitudes())
state.load([0.5, 0.5, -0.5, 0.5])
print("loaded state:", state.get_amplitudes())
import numpy as np
state.load(np.array([1, 0, 0, 1]) / np.sqrt(2)) # You can also load numpy arrays
print("loaded numpy state:", state.get_amplitudes())
zero state: [(1+0j), 0j, 0j, 0j]
zero norm state: [0j, 0j, 0j, 0j]
loaded state: [(0.5+0j), (0.5+0j), (-0.5+0j), (0.5+0j)]
loaded numpy state: [(0.7071067811865475+0j), 0j, 0j, (0.7071067811865475+0j)]

View from StateVectorBatched

When you work with StateVectorBatched, you can extract a single state vector in two ways.

view_state_vector_at does not copy the amplitudes, so updating the returned StateVector also updates the corresponding batch in the original StateVectorBatched.

from scaluq.default.f64 import StateVectorBatched

states = StateVectorBatched(2, 2)

# View: in-place update to batch 0
state0_view = states.view_state_vector_at(0)
state0_view.set_computational_basis(3)
print("batch 0 in states:", states.get_state_vector_at(0).get_amplitudes())

# Copy: detached update from batch 1
state1_copy = states.get_state_vector_at(1)
state1_copy.set_computational_basis(2)
print("batch 1 in states:", states.get_state_vector_at(1).get_amplitudes())
print("detached copy:", state1_copy.get_amplitudes())
batch 0 in states: [0j, 0j, 0j, (1+0j)]
batch 1 in states: [(1+0j), 0j, 0j, 0j]
detached copy: [0j, 0j, (1+0j), 0j]

Operation to StateVector

You can perform some operations to StateVector.

add_state_vector_with_coef is used to update the vector by adding \(c\ket{\psi}\), where c is a complex number and \(\ket{\psi}\) is another state vector with same dimension.

multiply_coef is used to update the vector by multiplying a complex number.

import math
from scaluq.default.f64 import StateVector
phi = StateVector(2)
print("phi:", phi.get_amplitudes())
psi = StateVector.uninitialized_state(2)
psi.set_computational_basis(3)
print("psi:", psi.get_amplitudes())
phi.add_state_vector_with_coef(1j, psi)
print("phi after added psi:", phi.get_amplitudes())
phi.multiply_coef(1 / math.sqrt(2))
print("phi after multiplied coef:", phi.get_amplitudes())
phi: [(1+0j), 0j, 0j, 0j]
psi: [0j, 0j, 0j, (1+0j)]
phi after added psi: [(1+0j), 0j, 0j, 1j]
phi after multiplied coef: [(0.7071067811865475+0j), 0j, 0j, 0.7071067811865475j]

Get probabilistic measures of StateVector

You can get probabilistic measures of StateVector.

get_zero_probability is used to get the probability of getting \(0\) when the specified qubit is measured by Z-basis.

get_marginal_probability is used to get the marginal probability of getting specified result when some of qubits are measured simultaneously by Z-basis. The result is specified by a list of integer with length n. \(i\)-th value of elements means as follows:

  • 0: \(i\)-th qubit is measured and the result is \(0\).

  • 1: \(i\)-th qubit is measured and the result is \(1\).

  • StateVector.UNMEASURED: \(i\)-th qubit is not measured.

get_entropy is used to get the entropy of the vector, which is calculated by \(\sum_i -p_i \log_2 p_i\) (\(p_i\) (\(0\leq i<2^n\)) is \(|v_i|^2\) with \(v_i\) is the \(i\)-th amplitude of the vector).

sampling is used to perform sampling on the vector. With passing the number of sampling as sampling_count, a list of integers with length sampling_count is returned.

import math
from scaluq.default.f64 import StateVector
state = StateVector.uninitialized_state(2)
vec = [1/2, 0, 0, math.sqrt(3)/2 * 1j]
state.load(vec)
print("zero probability of 0:", state.get_zero_probability(0))
assert abs(state.get_zero_probability(0) - (abs(vec[0])**2 + abs(vec[2])**2)) < 1e-9
print("zero probability of 1:", state.get_zero_probability(1))
assert abs(state.get_zero_probability(1) - (abs(vec[0])**2 + abs(vec[1])**2)) < 1e-9
print("marginal probability of [1, UNMEASURED]:", state.get_marginal_probability([1, StateVector.UNMEASURED]))
assert abs(state.get_marginal_probability([1, StateVector.UNMEASURED]) - (abs(vec[1])**2 + abs(vec[3])**2)) < 1e-9
zero probability of 0: 0.25
zero probability of 1: 0.25
marginal probability of [1, UNMEASURED]: 0.7499999999999999