Building a QPU simulator in Julia - Part 1

May 7, 2019
four minutes.

Just for a kicks, I started rewriting Quko, my simple quantum computing simulator, in clojure. I thought that having powerful linear algebra libraries like Neanderthal would help simplify the task but it didn’t feel right. I decided to put it on hold until I got some inspiration.

The talk included Julia code for generating this Pointcaré section of the chaotic interaction between 3 spiking neural networks (SNNs) which I, ironically, converted to Kotlin.

So, I started looking again at complexity theory. I was lead via logistic maps, and other dynamic systems to a library called DynamicSystems.jl written in a language called Julia. I remembered having seen Julia two years ago from another talk on simulating spiking neural networks using Julia.

The things that were bothering me in Kotlin and Clojure when building the QPU simulator were complex numbers and linear algebra, specifically the Krondecker product, and it turns out that Julia supports both natively! So it was worth giving it a try.

Installing Julia on a Mac is as simple as you might expectYou can use it from Jupyter, if you prefer. I wrote down how to do that in a separate note. The community overlap with Python is understandable.

> brew install julia

Like Clojure, Julia has a nice REPL for developing and testing ideas quickly. Built-in support for vectors means that defining a qubit is easyJulia uses 64-bit floats by default. We don’t need that level of precision and 32-bit floats save memory, a simple but important consideration for QPU simulators. :

julia> qubit = Float32(1) * [1, 0]
2-element Array{Float32,1}:

Julia has dynamic typing so the variable type doesn’t need to be set explicitly. The Hadamard operatorDefined as is similarly easy to define with Julia’s matrix syntax:

julia> H = Float32(1/sqrt(2)) * [1 1; 1 -1]
2×2 Array{Float32,2}:
 0.707107   0.707107
 0.707107  -0.707107

julia> H * qubit
2-element Array{Float32,1}:

Taking it a step further we can use complex numbers natively. For example, we are able to define the gateDefined as almost as easily:

julia> halfX = Float32(0.5) * [1+im 1-im; 1-im 1+im]
2×2 Array{Complex{Float32},2}:
 0.5+0.5im  0.5-0.5im
 0.5-0.5im  0.5+0.5im

julia> halfX * qubit
2-element Array{Complex{Float32},1}:
 0.5f0 + 0.5f0im
 0.5f0 - 0.5f0im

We can test that it’s unitary using the conjugation operator, ', and the @test macro:

julia> using Test
julia> @test halfX'*halfX ≈ [1 0; 0 1]
Test Passed

It’s normal in Julia to use Unicode characters directly. This is strange at first but makes formulas very succinct. In the above test we used the characterIn the REPL, type \approx and then press Tab. to perform a machine-precision equality test.

Being a functional language, it’s straightforward to turn our operators into functions. X is the quantum NOT operator:

julia> X = (qubit) -> Float32(1) * [0 1; 1 0] * qubit
#3 (generic function with 1 method)

julia> @test X(X(qubit)) ≈ qubit
Test Passed

This language really is a close to perfect for my project! SerendipityTechniques for encouraging serendipity are an important part of the Cynefin framework, which I also happen to be studying at the moment. De Bono’s concept of Readiness in action again. is a wonderful thing.

Building a QPU simulator in Julia - Part 1 - May 7, 2019 - John Hearn