Quantum Context and Simulator


Quantum Context

First and foremost, Scotty is a high-level quantum framework that defines abstract quantum primitives. At the top of it all we have QuantumContext—a trait with a handful of abstract methods. This is the heart of the framework and it’s meant to provide functions for quantum state and gate transitions that take place during circuit execution.

Here is a list of method signatures that QuantumContext provides:

trait QuantumContext {
  def tensorProduct(register: QubitRegister,
                    sp1: Superposition, sp2: Superposition): Superposition

  def densityMatrix(vector: Vector): Matrix

  def isUnitary(gate: TargetGate): Boolean

  def probabilities(sp: Superposition): Seq[StateData]

  def applyGate(state: Vector, gate: Gate): Unit

  def run(circuit: Circuit): State

  def measure(register: QubitRegister, state: Vector): Collapsed

  def runAndMeasure(circuit: Circuit): Collapsed

  def runExperiment(circuit: Circuit, trialsCount: Int): ExperimentResult
}

Vector and Matrix are defined in the following way:

type Vector = Array[Float]
type Matrix = Array[Array[Float]]

The framework makes no assumptions about the underlying state implementation. The only thing that’s expected is that Vector and Matrix arrays represent lists of complex numbers, each occupying two elements in the array for their real and imaginary components. There’s no built-in data type for complex numbers in Java, so we have to either represent them with objects (which is hugely inefficient for large arrays in most cases) or with two separate primitive elements. Scotty uses two floats to represent a complex number.

Java floats take up 32 bits each, so a complex number in Scotty takes up 64 bits. Since the state vector uses a Java array there’s a hard limit of Integer.MAX_VALUE elements that it can have, which corresponds to elements in our case. It means that Scotty can represent states of up to 29 qubits.

There are many possible use cases where you’d want to roll out your own implementation of QuantumContext. For example, if you have a distributed quantum simulator running in the cloud that you want to use to run bigger circuits then implementing QuantumContext would make a lot of sense. The obvious benefit is that you get to keep your Scala programs and algorithms written with Scotty unmodified. Write once, run anywhere.

Quantum Simulator

QuantumSimulator is a built-in implementation of the QuantumContext, which allows you to run up-to-29 qubit quantum circuit simulations. All simulation computations are (depending on your hardware) parallelizable on the CPU and you can control the underlying thread pool implementation by providing a custom execution context:

val ec = ExecutionContext.fromExecutor(
  new java.util.concurrent.ForkJoinPool(8)
)

QuantumSimulator(ec)

QuantumSimulator can also take a Random instance. It’s useful when you need to specify a random seed:

QuantumSimulator(new Random(System.nanoTime))

The simulator has a runExperiment method that runs multiple experiment trials. You can pass the number of trials to it and it will run them sequentially.

After all trials are done executing, the method returns an ExperimentResult which contains a list of Collapsed states after each experiment.

The stateStats lazy variable is a list of tuples containing a binary representation of the collapsed state and the total number of state occurrences. Suppose, we entangle two qubits and run the experiment 1000 times:

QuantumSimulator()
  .runExperiment(Circuit(H(0), CNOT(0, 1)), 1000)
  .stateStats.toHumanString

As expected, we are going to end up with roughly 50% of both qubits being in the 00 state and another 50% being in the 11 state:

00: 483
01: 0
10: 0
11: 517