Useless Solutions Call for Useless Problems
- Published on
- ∘ 151 min read ∘ ––– views
Previous Article
Next Article
We're talkin' quantum; get in or get out
Useless Solutions Call for Useless Problems
Consider a toy problem wherein we're given a black box function which operates on four 1-bit inputs which we'll label and . Our function is perhaps better interpreted as a “circuit” with the following behavior:
The catch is that any subset of those cases might be ignored, deleted, faulty, flipped by a gamma ray,1 etc. E.g., we might receive an with the second case “deleted” resembling:
For an instance of this problem with inputs, there are possible subsets of “missing” statements which can be interpreted as toggles acting on our accumulator. ( since we discount the term which accumulates the effect of the other inputs).
We can enumerate the set of all possible configurations of this black box function with 4 arguments (three input bits and one accumulator bit):
Where the subscripts correspond to the cases that are present in that instance of the function. The problem statement is: given some unknown function , we want to determine which we’re dealing with in as few invocations or “queries” to as possible. ( is conventionally referred to as an oracle for this kind of problem).
A naive approach might be to select an arbitrary initial binary string, say , flip the first bit, pass it to and observe what happens. For this might look like:
This tells us that the first case (toggling contingent on toggling ) is present in our instance of . Note that this logarithmically eliminates fully half of the possible s (the ones where is absent, deleted, corrupted, etc.):
We can repeat with another input string to target the behavior of another of the cases, say:
which tells us that the case for is missing since was not toggled. So we can eliminate another half of the remaining possibilities:
And, finally, we isolate the behavior of the branch:
So we know that our given function must be , but that took total invocations for a mystery function with three inputs. Can we do better? We might experiment with more “sophisticated” input string combinations, but we’ll quickly find that regardless of our input to , we can never eliminate more than half the possible remaining answers at any iteration of this sluethy procedure.
To prove this, let’s consider what information we gain by sampling . Or –donning our Shannon deerstalker– perhaps a better question to ask is: how much information do we gain?
The answer is –perhaps obviously from the framing of the problem statement thus far– precisely 1 bit of information which gets encoded in the last bit of the output . So, with classical computing techniques, the answer is: … no, we cannot do better than queries to .
Suppose that there did exist a procedure which lets us identify the with only two invocations. We would have at our disposal 2 bits of information:
Leaving us with four possible combination of :
But there are eight possible choices for and 2 bits of information still only inform on four of those possibilites, so we can conclude that any such procedure which claims perfect accuracy is WRONG since there’s simply not enough information. Not with classical computing techniques, that is. With quantum computing, we can get the number of invocations of down to one singular sample for any naturally numbered by leveraging some sick linear algebra on the properties of qubits.
Quantum
A qubit is the fundamental datatype in quantum computing. Typically denoted , with the bracket notation indicating that the value of can exist somewhere in between the two basic states: and . The measure of a qubit’s propinquity to each basic state is denoted as an amplitude:
And we can omit the amplitudes' subscript when the basic state being referenced is unambiguous.
Crucially, we don’t need a concrete implementation of a device which can replicate superposition (though this is more achievable than we might initially assume) in order to reason about quantum quantities.
Properties of Qubits
Measuring a qubit’s amplitude collapses it to one of its basic states depending on the amplitudes which can be thought of as probabilities of observing the corresponding basic state. The exact rule governing which basic state is observed when a qubit collapses is given by the amplitude of that basic state squared:
E.g. for a qubit with amplitude we know that the probability of observing one of the two possible basic states must sum to one, so we can infer the amplitude of the other unknown basic state :
Note that this definition also allows for negative amplitudes, e.g. this is an equally valid quantum state for to be in:
Since .
Similarly to classical bits, for qubits, there are possible measurements which are the possible basic state configurations after measuring each of the qubits’ amplitudes. However, collections of qubits form quantum systems, the constituents of which cannot be meaningfully considered in isolation.
E.g. For qubits, we have a system of 4 basic states:
With some amplitude on each of the basic states where the sum of the squares of said amplitudes is one:
While it may seem natural to assume that each qubit on its own has distinct amplitudes on each of its basic states, this is not the case. Together, the qubits jointly have amplitudes on each of their basic states. This property is called quantum entanglement and is precisely what gives rise to the clever solutions found in quantum programming which are not possible with conventional approaches.
Quantum Programming
A quantum program takes as input qubits in some valid superposition of the basic states and takes them to some other (excluding the identity QP, I guess) valid super positions. In other words, a quantum program is any set of operations which satisfies the pre- and post-condition:
Where is expressed in base 102. A quantum operation we might be interested in to solve the problem from earlier would be qubit negation on qubits. This can be achieved by "swapping" amplitudes of corresponding basic states. The operation can be visualized on a dimensional cube. E.g., for :
The “toggle” or negation operation swaps a corner on the face of the cube where with The corner where , and
This operation is one of three named for Nobel prize winner Wolfgang Pauli, and is denoted or . We’ll cover the other two operations in a following section. Note that, without even knowing how to quantitatively represent this negation operation, we know that it is a valid one since it doesn’t violate the pre- and post-condition of squared amplitude summation to one.
To avoid having to bother with validation summations for each quantum operation we might want to define, we can institute an abstract constraint that these operations only define how each of the basic states are transformed in terms of one another rather than modifying amplitudes directly. This opens many direct analogs to linear algebra that might already be showing around the cracks of this ELI25AIDKALA3 explanation.
This abstraction constraint begs the question, though, what about non-basic states? E.g. what if we composed a 1-bit probabilistic operator with our negation “gate” which toggles the basic state of a qubit of with and does nothing the rest of the time:
We can visualize this behavior as a tree without having to worry our pretty little brains with any quantitative conception of what the hell we’re talking about:
We can see that the probability of observing any of the basic states of this system is just the product of the probabilities of descending down any branch of this amplitude tree that results in that basic state e.g. :
Quantum program analysis is almost identical to this RNG
example, the only difference being that quantum instructions operate on amplitudes which can be negative whereas a classical stochastic decision tree operates on probabilities on the unit interval. Conveniently for us, this means that some of the amplitudes might cancel each other out, whereas in the classical context, probabilities are strictly additive (in the monotonically increasing sense of the word).
So far the two operations we’ve defined ( and simple conditionals), don’t offer any further utility than those gates which are already available in classical circuitry as evidenced by their amplitude trees which don’t introduce any quantum branching, even for more "complex" arrangements therein:
It’s now time to introduce non-trivial non-basic qubit states. Let’s define a unary function: with the following amplitude tree:
We can easily verify that it’s a valid quantum operation since it maps valid quantum states to other valid quantum states. Now suppose we have another operation which takes some qubit in the zero state and transforms it to a qubit with some valid amplitudes on each of its basic states s.t. . We can (non-rigorously) check that is valid by passing as its input and examining the resultant amplitude tree:
Collapsing the amplitudes to measure the distribution over basic states, we get:
So, is valid . An example of an invalid operation might be assignment. Yes, that most basic of instructions we might expect to see in a program:
Verboten! We can develop some intuition about this by subjecting assignment to the same procedural treatment as :
Now, for a non-trivial quantum operation, and perhaps the most important (? depends who u ask, where my Hadamard truthers at), the Hadamard:4,5
Let’s observe how it behaves with some of our other operations:
And we can go a step further by enumerating the amplitudes on each of the basis states at each of the six steps of the program:
instr | ||||||||
---|---|---|---|---|---|---|---|---|
init | +1 | |||||||
Toggle(q1) | 0 | +1 | ||||||
Hadamard(q1) | +0.71 | +0.71 | ||||||
Hadamard(q3) | +0.5 | +0.5 | +0.5 | +0.5 | ||||
if q1 and q2 Toggle(q3) | +0.5 | +0.5 | +0.5 | +0.5 | ||||
Hadamard(q3) | +0.71 | +0.71 |
Note that – even though the amplitude appears on the basic state, the final amplitude on this state is zero because both of the branches where it appears have inverse amplitudes which cancel each other out. And herein lies the crux of quantum programming: designing algorithms where the amplitude of "undesirable" quantum states cancel out:
This isn’t always feasible, however. Sometimes the best we can do is leaving a subset of ”undesirable” output states with a minimal amount of amplitude and then relying on probabilistic correctness over a series of trials.
Designing Quantum Programs
Since the state of qubits can be fully described by a collection of amplitudes, it’s natural to model qubits as -dimensional vectors containing amplitudes on each of the basic states:
With a vector spanning eight dimensions (1 for each basis) corresponding to each of the basic states, we can easily visualize up to three of those dimensions. And, per the amplitude normalization constraint, we can geometrically represent the state of the qubits as a point on the -dimensional unit sphere. This is fun to say, but for any number of qubits greater than 1, we need at least 4-dimensional space which is more than you might be able to visualize.6 Before getting bogged down with -dimensional visualizations, let’s play with our linear algebraic interpretation of qubit state for which is nicely represented on the plane:
We can describe quantum instructions operating on qubits as matrices of transition amplitudes multiplied by the state vector.
Now, back to the harsh realities of -dimensional space, suppose we have two qubits and we want to compute the Hadamard matrix for this system, we need a matrix of corresponding shape: .
For , we can just fill in transition amplitude entries in the matrix for basis state pairs where doesn't change:
And, similarly we can compute for values where doesn't change:
Furthermore, we can compose them via normal matrix multiplication to compute – and order actually doesn't matter for these operations since they modify different qubits by construction:
and we can further simplify the notation since the magnitude of all the entries are equivalent, and all we really care about is the sign:
Note as well that the "convolution" of is itself composed of :
This may come in handy when implementing a function to generate , though the Kronecker product () achieve this naturally – we'll become intimately familiar with its behavior later on.
Mystery Toggles Revisited
Finally, we can return to our mystery problem on toggles and leverage the Hadamard to improve upon our classical solution.
The procedure is as follows:
- initialize qubits in the 0 state, one for each possible toggle as well as an accumulator,
- Toggle the accumulator,
- Apply ,
- Invoke the oracle on our input string,
- Apply again,
- Measure the amplitudes of the system to identify which cases are absent from .
To understand how and why this works, let's trace it for , with (only the second toggle is present).
First, we create our dimensional state vector representing all combinations of with all of the amplitude on the zero state:
Next, we toggle the accumulator bit, transferring all the amplitude from .
Then, we apply to the state vector:
and recall that matrix multiplication with a vector with a single entry of 1 essentially acts as a select
on that column s.t. our resultant product is the second row of our Hadamard matrix as a column vector:
Then, we invoke on the quantum state which –if we remember that far back– toggles the accumulator iff in the input string is set. This has the effect of applying to each entry in the vector, examining the middle bit of each basis state:
Note that, prior to this step, all states where the accumulator bit was 1 had negative amplitude, and all states where the accumulator bit was 0 had positive amplitude. When we invoke the oracle , the basis states where the accumulator doesn't change still have positive amplitude if the accumulator bit is unset and negative if it is set, just like before – but those basis states where the accumulator did get toggled now have the inverse amplitude pattern ( if , and if ) which encodes a sort of truth table for where positive amplitudes correspond to the accumulator being set.
In other words, taking the basis state which has negative amplitude implies that if , then will be after is called. Conversely, in the accumulator bit is unset since it has positive amplitude.
Now we run into another problem: if we were to try to measure the qubits now, the amplitudes would collapse to pure noise since all the magnitudes would cancel each other out and we'd effectively get a random 3-bit binary string as output. To account for this, we just plop in another and, curiously, the purpose of this operation is completely different from the columnal selection from earlier since our state vector is no longer of the correct form to pick a single entry.
Again, it's useful to visualize what happens when we perform this multiplication:
As we can see, as we compute the matrix multiplication all the amplitude values gradually accumulate on the basic state such that when we measure all the qubits, we learn the configuration of the mystery that we were given with . The proof that this program is correct for fixed is trivial via programmatically enumerating all variations of . This is moreso a result of the Hadamard operation placing each qubit into a state of superposition at the beginning of the algorithm, and then ejecting them from superposition after quantum computation.
If this seems like too convenient or mystically clever of a solution it's because this sequence of operations was discovered prior to the declaration of the problem statement. The problem of "mystery " was retconned after recognizing the utility of the Hadamard operation in order to motivate this demonstration of a quantum system offering constant time solution to a problem with linear complexity by classic means. This problem is known as the Bernstein-Vazirani problem, and is one of many similarly contrived examples of "quantum supremacy."
Formally, the Bernstein-Vazirani7 problem is posed: given an oracle where all that's known of is that it's output is the dot product between the input vector and a secret string modulo 2, we're tasked with finding s.t.
Illustrations and dramatization of the problem statement hopefully make this less brain numbing.
More Math
Now that the gloves are off in terms of the linear algebra, let's revisit some of the earlier handwavey definitions of qubits and quantum gates.
Qubits as vectors
A qubit can be expressed as a linear combination of the amplitudes and the system's basis states:
where , though we can largely ignore the complex components of pretty much all the systems and gates we'll cover (for explanations of the global phase described by complex components, ur gonna need an ELI35). As a linear combination, quantum states are easily expressed as vectors:
The basis states of a quantum state vector span the 2D Hilbert Space which is just a well-defined complex vector space where the inner product exists.
Normalization Condition
The aforementioned "validity" rule for quantum states is known as the normalization condition:
For an -dimensional system, we sum over the squares of the amplitudes of all basic states:
Quantum Operations
Operations on single cubits can be expressed as 2x2 unitary and hermitian matrices. Unitary to ensure the conservation of amplitude, and Hermitian to ensure that the eigenvalues (measurable quantities which need to correspond to real world things) are real valued.8
A matrix is said to be unitary if its inverse equals its transpose conjugate (also known as the hermitian adjoint) :
The hermitian adjoint operation is a linear map which transposes an complex matrix and conjugates each entry (negating the complex component).
For real matrices, the hermitian adjoint is just the transpose.
Bloch Sphere
Quantum operations on singular qubits can be visualized as maps between points on the unit sphere. As mentioned earlier, this also holds for systems of multiple qubits on hyper spheres but the utility of the Bloch sphere diminishes proportionately to one's ability to visualize higher dimensions and rotations therein.
Here, are the usual polar and azimuthal angles in spherical coordinate representation. The Pauli gates, then, are just rotations about these axes by radians. E.g.
again we drop the complex component since we're ignoring global phase of the quantum system, and just rotate dat boi about the x-axis (as observed in the planar example):
Similarly, this process yields a geometrically intuitive conception of the other two Pauli operations:
Note that each of these are involutory, meaning that their squares are equal to the identity matrix. From these Pauli matrices, we can also generalize to arbitrary rotation operations:
It should be clear(er) now that all qubit operations acting on an -dimensional quantum system map a point on the hypersphere to another point, which can always be achieved via some rotation, e.g.
so the Hadamard is just a rotation about the diagonal on the xz-plane of the Bloch sphere. Two applications of a rotation by will complete a full revolution about the sphere, returning to the original point.
Multi-qubit Systems
Recall that singular qubits occupy 2D Hilbert Spaces:
The state vector of a composite system of two qubits is given by the tensor product of the two constituent state vectors being combined, where the tensor product is defined as follows:
In other words, the basis vectors of a 2 qubit system in are comprehensively enumerated:
In general, the tensor product of two matrices is given by:
And since the tensor product is distributive, we can also write a combination of operations in such a way is explicates the fact that each quantum operator is acting independently on the composite state space:
With this representation of multi-qubit systems, we can introduce some multi-qubit operations.
Multi-qubit Operations
Thus far, we've only defined single-qubit gates/operations, but we can also perform some actions on multiple qubits at a time, with a few crucial distinctions from classical logic gates. For starters, quantum gates must be reversible. Whereas it's entirely common for classical logic gates to map multiple inputs to a single binary output, since quantum systems model state changes in physical systems, the modifications made to each of the inputs must also be expressed in the output. The above description is phrased as a constraint, but it's really just a natural conclusion from the prior definitions about quantum operations being unitary & hermitian – which lets us simply apply the adjoint of an operation to "undo" it.
classiscal gate | reversible? |
---|---|
AND | sometimes |
OR | no |
NOT | yes |
NAND | no |
XOR | no |
It should be straightforward to convince yourself that the input vector to these gates is not definitively knowable given the output bit/vector.
CNOT
The Controlled-NOT gate is analogous to the the classical XOR which can be thought of as addition given by the following truth table:
0 | 1 | |
---|---|---|
0 | 0 | 1 |
1 | 1 | 0 |
Observe that, for a 2 qubit system, remains unchanged, and the state vectors of are swapped:
Algebraically, we can express this as the matrix:
We can use CNOT to swap the states of two qubits the same way we might swap two classical variables' values without introducing a tmp
value with three XORs:
0 | 0 | 0 | 0 |
0 | 1 | 1 | 1 |
1 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
so .
and, in fact, we can represent any C-gate applied to an operation with this procedure:
However, as we'll see in the from-scratch implementation of quantum circuitry, the construction of Control matrices becomes far, far more involved for higher order matrices with non trivial control & target qubits (the trivial case being the 0th qubit acting as the control, and the th qubit being the target).
Toffoli
The Toffoli gate is effectively a CNOT on a 3-qubit system:
The Toffoli truth table indicates that remain unchanged, and only changes if both :
0 | 0 | 0 | 0 | 0 | 0 |
0 | 0 | 1 | 0 | 0 | 1 |
0 | 1 | 0 | 0 | 1 | 0 |
0 | 1 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 0 | 0 |
1 | 0 | 1 | 1 | 0 | 1 |
1 | 1 | 0 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 0 |
Entanglement
Recall from the section about multi-qubit states the expression:
When a 2-qubit composite state function can be expressed as a tensor product of the constituent qubit states like this, then the composite state is said to be separable. E.g.
For example, we can show that the following quantum state is separable:
The tensor product between and yields the following composite state:
And we can verify this is separable by checking if the product of the differences of amplitudes along the major and minor diagonals cancel each other out:
which shouldn't come as any surprise since this composite state was constructed from separated states via the tensor product :p. By means of counter example, let's take a look at a special quantum state in :
Since the amplitudes don't cancel out, this state is inseparable or entangled. This quantum state is one of four maximally entangled systems referred to as the Bell States.
and they can be constructed with some Hadamard and CNOT gates as follows:
construction of
construction of
construction of
construction of
But what does it mean to be maximally entangled? Before tackling this question, or motivating even why we care, it's time to really get our hands dirty.
Dir-Ac notation
Commonly referred to as "bra-ket" notation, but it's more fun and kind of conventional to summon up some new fangled notation to become one-up on your academic peers.
Thus far we should be relatively comfortable with kets which are equivalently expressible as vectors, but what the hell was Dirac smoking with those bras. First let's consider a linear function which maps a 2-dimensional vector to a scalar by just shitting out the component of its input:
is indeed a linear map since it satisfies the following properties:
Linear maps of this form which take an element of our vector space and shit out a scalar are called linear functionals. The algebraic representation of will be a 2x1 matrix, AKA a row vector:
All linear functionals in are two-dimensional row vectors. In other words, the set of all linear functions in is the set of all row vectors of the form , and thus this set forms its own vector space known as the dual space.
Formally, given a vector space , the dual space is the vector space of all linear functionals in . Linear functionals find natural relevance in quantum mechanics wherever we want to measure a quantum state which is the process of "collapsing" the quantum state into a single number, a process which resembles .
We denote linear functionals in quantum mechanics as those functions of the dual of the Hilbert space with "bras": , operating on kets. Note how when we smush a bra together with a ket, we get something that looks an awful lot like an inner product:
any acts on any vector and can be expressed as a dot product by translating into a column vector courtesy of the Riesz Representation Theorem.9 I guess this is like significant if you're smart enough to have mentally bifurcated row vector/matrix multiplication as different than the dot product and thus this revelation may seem inobvious or maybe even haram. But, obviously, my brain was already rotating row vectors into column vectors (or maybe the other way around, idek), without realizing the profundity of being able to do so.
For any linear functional , the action of is equivalent to taking the inner product with some unique vector :
So, it's no mistake that:
This is useful for increasingly many reasons (as I'm realizing). Take, for example, the orthonormal basis of the vector space of a quantum system to be:
and an arbitrary qubit of the system as:
Since all of the states are orthonormal, we can compute their amplitude coefficients as:
and for any orthonormal basis, this sum must be equal to the identity matrix e.g. for some arbitrary bases:
which checks out since:
Measures of Entanglement
There are two measures of entanglement we'll consider. The first being the Positive Partial Transpose, and the second being a more comfy Von Neumann entropic measure.
Positive Partial Transpose
Let , have a corresponding density matrix . The trace of (the sum of the eigenvalues, including duplicates), is given by:
which is the sum of the inner product with the basis vectors of the composite state. For an qubit system, we have 4 basis states, the trace of which is a scalar. The partial trace of a density matrix is the trace only on a part (or slice) of the Hilbert space, and yields what is known as the reduced density matrix:
So, for example:
the density matrix is:
we can see that the partial reduced density matrix collapses the outer product between the kets where the indices , thus the density matrix of is a 2x2, whereas is 4x4. We can recast in terms of the outer products of via substitution:
where is just the normalized version of and
so the trace of the square of the partial density matrix is then:
In other words, is normalized, so:
And so we can use the trace of the square of a partial density matrix as a relevant and bounded measure of the degree of entanglement.
For example, the partial positive transpose measure of entanglement of a separable state:
Which means that we have exact information about the quantum system since the reduced density matrix is a pure state given by a single outer product instead of e.g. a mixture of outer products.
An impure, or mixed state for the partial density matrix of a non-separable state would have the form:
Perfect mixed states are those where there is zero measurable information of the system, the partial density matrix of which are proportional to the identity matrix:
implying that we have equal likelihood of measuring a qubit's state in any of the basis states. The bounds for our measure of entanglement, then, is:
An example of a maximally entangled state could be any of the Bell states:
This, if nothing else, is an illustrative example of how linear functionals behave, but is a rather painful measure of entanglement to compute by hand.10
Von Neumann Entropy
An equally valid measure of disorder of a quantum system is Von Neumann Entropy, defined as:
For quantum systems is equivalent to measuring the state vector . We can show that Von Neumann entropy is equivalent to:
Working backwards from the classical definition, and recalling that the eigenvalues of correspond to the measurable quantities of our quantum system we replace with the eigenvalues :
We can derive this result from the definition of the density matrix:
which is equivalent to the spectral decomposition of the matrix formed by the outer product within the sum above:
This will be a diagonal matrix of the form
where specifies the dimensionality of the Hilbert space of the system, and the diagonal matrix is in the basis of . Plugging this expression of back into the Von Neumann definition, the sum can be written as the operation:
Note that is equivalent to the Kronecker delta:
so
And, since we know that since they correspond to the probabilities of measuring a given basic state, we can note that:
And we can sanity check this measure of entanglement as we did with the fucked up one I presented above. For a pure, separable state, we have:
And, for a finite11 Hilbert space of dimension , Von Neumann entropy is upper bounded by .
For example, for the near-Bell state:
The density matrix, and partial density matrices thereof will be:
Hidden Linear Function Problem
We'll consider now another problem13
Given a binary lattice , where , a binary vector , and an adjacency matrix for : . is said to be sparse in the sense that unless correspond to neighboring vertices on the lattice: :
e.g. for we might have a grid that resembles:
These inputs induce a quadratic form given by:
where are binary values. A quadratic form is a polynomial where all terms are degree two, and note that since all our variables are binary-valued, , so that second summation, though appearing to be degree one (and thus, not quadratic, is in fact equivalent to the sum of the squares of ).
We further restrict onto the linear subspace given by:
where is arithmetic on the binary ring i.e. mod 2. It's also worth noting that, by design, is the null space, or kernel of – that is:
Together, the definitions of and imply that there exists at least one vector s.t.
We can find all the solutions classically by exhaustively checking all like so:
import numpy as np
class HLF:
"""
A: a symmetric adjaceny matrix of "weights" for a lattice
b: vertice "weights"
"""
def __init__(self, A, b):
self.n = A.shape[0]
assert A.shape == (self.n, self.n)
assert b.shape == (self.n, )
# symmetric
assert np.array_equal(A, A.T)
# only neighboring cells can be connected
for i in range(self.n):
for j in range(self.n):
if A[i][j] == 1:
# I am the smartest man alive. Google and MathStackExchange BTFO'd
# I'd like to thank Daniel Shiffman and my whiteboard
assert (abs(abs(i - self.n) - abs(j - self.n))) % 2 == 1
self.A = A
self.b = b
Note that this code is adapted/corrected from an example on Google's quantumai page, but they botch a few crucial aspects of the problem constraints.
which specifies :
and the induced subset is the rather unexciting:
which we can verify via the definition, enumerating over all of and enough LaTeX to typeset God himself.14
Now we just repeat that enumeration process above to find some which satisfies:
This trivially yields the solutions:
which we can programmatically verify against all .
But this solution requires queries to since we iterate over all permutations of of multiple times to exhaust the search space.
Applying Quantum Circuits
Using a quantum approach, the output can be sampled from the distribution:
where if is a solution. is constructed as:
Notably, this can be accomplished with a constant-depth circuit:
CODE
Let's take a look at how we might implement this circuit from scratch. First, we know we'll need some representation of our gates, so I'll just throw em all into class as static fields:
class Gates:
H = np.array([[1, 1],
[1, -1])) * 1/np.sqrt(2)
X = np.array([[0, 1],
[1, 0]))
Z = np.array([[1, 0],
[0, -1]))
S = np.array([[1, 0],
[0, 0 - 1j]))
I = np.array([[1, 0],
[0, 1]))
T = np.array([[1, 0],
[0, np.exp(np.pi/4 * 1j)]))
# |0⟩⟨0|
proj0 = np.array([[1, 0],
[0, 0]))
# |1⟩⟨1|
proj1 = np.array([[0, 0],
[0, 1]))
We can apply these gates to some basic states like so:
c0 = 0 + 0j
c1 = 1 + 0j
ket0 = np.array([c1, c0])
ket1 = np.array([c0, c1])
print(f"X|0> = {Gates.X @ ket0}, X|1> = {Gates.X @ ket1}")
print(f"H|0> = {Gates.H @ ket0}, H|1> = {Gates.H @ ket1}")
X|0> = [0.+0.j 1.+0.j], X|1> = [1.+0.j 0.+0.j]
H|0> = [0.707+0.j 0.707+0.j], H|1> = [ 0.707+0.j -0.707+0.j]
and it will be useful to write a helper to plot them15 as vectors in the complex plane:
def plt_state_as_vecs(psi):
ket_alpha, ket_beta = psi
fig, axx = plt.subplots(1,2, figsize=(7,3))
ax = axx[0]
ax.axhline(0,ls='--',color='grey',alpha=0.2)
ax.axvline(0,ls='--',color='grey',alpha=0.2)
ax.plot(0, 0, color='blue',label=r'$\vert 0 \rangle$')
ax.plot(0, 0, color='red',label=r'$\vert 1 \rangle$')
ax.arrow(0,0,ket_alpha.real, ket_alpha.imag, head_width=0.05, head_length=0.1, color='blue')
ax.arrow(0,0,ket_beta.real, ket_beta.imag, head_width=0.05, head_length=0.1,
color='red')
ax.set_xlim([-1,1])
ax.set_ylim([-1,1])
ax.set_xlabel("Real")
ax.set_ylabel("Complex")
ax.legend(loc=0)
ax = axx[1]
pr_zero, pr_one = np.absolute(psi[0])**2, np.absolute(psi[1])**2
ax.bar([0,1],[pr_zero, pr_one])
ax.set_xticks([0,1])
ax.set_xticklabels([r'$\vert 0 \rangle$',r'$\vert 1 \rangle$'])
ax.set_ylim(0,1)
ax.grid(True)
plt.tight_layout()
e.g. we can visualize how the Hadamard places a basic state into superposition:
plt_state_as_vecs(ket1)
plt_state_as_vecs(Gates.H @ ket1)
And we'll also want a generic plotting function to visualize the probability distribution over an arbitrarily-sized quantum system:
def plt_measure(out_register):
n_qubits = int(np.log2(out_register.shape[0]))
fig, ax = plt.subplots(1,1)
ax.bar(range(2**n_qubits), basis_state_probs(out_register))
ax.set_xticks(range(2**n_qubits))
ax.set_xticklabels(stringify(n_qubits), rotation=45)
ax.set_ylim(-0,1)
ax.grid(True)
ax.set_ylabel(r'$P(S_c)$')
ax.set_xlabel(r'$S_c$')
plt.show()
In order to construct an -dimensional system of qubits, we'll want to define how gates compose. This can be a static helper function of the Gate
class defined above:
def Compose(gates):
return ft.reduce(np.kron, gates, np.array([1]))
which produces a matrix on the order of the product of all dimensions of gates
. Now, we can construct a system of four qubits and visualize it like so:
plt_measure(Gates.Compose([ket0, ket0, ket0, ket0]))
Or, non-graphically, we might just want to spit out the states and their corresponding likelihoods of observations:
def basis_state_probs(state_vector):
return np.array([np.absolute(s)**2 for s in state_vector])
def stringify(n_qubits=3):
state_strs = ['' for _ in range(2**n_qubits)]
basis_strs = ['0', '1']
for q in range(n_qubits):
for i in range(len(state_strs)):
b = basis_strs[((i//(2**q))) % 2]
state_strs[i] = state_strs[i] + b
return state_strs
out = Gates.Compose([ket0, ket0])
list(zip(stringify(out.shape[0]), basis_state_probs(out)))
which nicely displays the 2-qubit system initialized to the zero state:
[
('0000', 1.0),
('1000', 0.0),
('0100', 0.0),
('1100', 0.0)
]
We can take a stab at building that Toffoli circuit from earlier, noting that –even if an input qubit is not accessed or modified during an operation, we still need to maintain a uniform dimension of each moment of the circuit.16. Recall that the Toffoli gate relies on the CNOT matrix. Rather than just implementing a CNOT gate for qubits, though, we probably want to generalize a construction method for arbitrary . This is actually much easier said than done, but worth the effort.
Consider the general purpose 2x2 control gate defined by:
Where is the 2x2 identity matrix, and the pair of terms, are the projectors onto the states and of the control qubit. We can extrapolate to the control gate for a 3x3 with e.g. control = 1, target = 3:
We can expand this expression and label each term for clarity:
And if we swap the control and target indices, we can better understand the "role" of the uninvolved terms which effectively pad out the dimensionality:
Thinking about the control gate in terms of matrix addition is the only way I was able to think about this problem.
# within Gate class again
def Control(n, U, control, target):
"""
This is the most cracked piece of software I've ever written.
U: a unitary 2x2 matrix
control: the index of the control qubit
target: the index of the target qubit
n: the number of qubits in the circuit
returns CU(i, j): an NxN matrix constructed from some permutation of:
[|0⟩⟨0| ⊗ I ⊗ I] + [|1⟩⟨1| ⊗ I ⊗ U]
control, uninvolved, target + control, uninvolved, target
s.t. `control` and `target` align with their respected indices, and the
"uninvolved" identities are kroneckered to pad out the necessary dimensions
"""
left_ops = []
for i in range(n):
if i == control: left_ops += [Gates.proj0]
else: left_ops += [Gates.I]
right_ops = []
for i in range(n):
if i == control: right_ops += [Gates.proj1]
elif i == target: right_ops += [U]
else: right_ops += [Gates.I]
left = ft.reduce(np.kron, left_ops, np.array([1]))
right = ft.reduce(np.kron, right_ops, np.array([1]))
return left + right
So now we can easily express the Toffoli as a sequence of matrix multiplications applied to a state vector:
g1 = Gates.Compose([Gates.I, Gates.I, Gates.H])
g2 = Gates.Compose([Gates.I, Gates.Control(2, Gates.X, 0, 1)])
g3 = Gates.Compose([Gates.I, Gates.I, dagger(Gates.T)])
# note that we don't need to `Compose` when our gate is already appropriately sized
g4 = Gates.Control(3, Gates.X, 0, 2)
g5 = Gates.Compose([Gates.I, Gates.I, Gates.T])
g6 = Gates.Compose([Gates.I, Gates.Control(2, Gates.X, 0, 1)])
g7 = Gates.Compose([Gates.I, Gates.I, dagger(Gates.T)])
g8 = Gates.Control(3, Gates.X, 0, 2)
g9 = Gates.Compose([Gates.I, Gates.T, Gates.T])
g10 = Gates.Compose([Gates.Control(2, Gates.X, 0, 1), Gates.H])
g11 = Gates.Compose([Gates.T, dagger(Gates.T), Gates.I])
g12 = Gates.Compose([Gates.Control(2, Gates.X, 0, 1), Gates.I])
gs = [g1, g2, g3, g4, g5, g6, g7, g8, g9, g10, g11, g12]
toffoli = g1 @ g2 @ g3 @ g4 @ g5 @ g6 @ g7 @ g8 @ g9 @ g10 @ g11 @ g12
in_states = [ket1, ket1, ket1]
out = register @ toffoli
immediately we can also observe that chaining together multiplications is dumb, so we can implement another static composition utility within our Gate
class:
def compose_circuit(moments):
n_qubits = int(np.log2(moments[0].shape[0]))
return ft.reduce(np.dot, moments, np.identity(2**n_qubits))
and verify it works just the same via:
toffoli_ = Gates.compose_circuit(gs)
toffoli_ == toffoli
Let's apply these gates and composition methods to solve the Hidden Linear Function problem.
We'll instantiate the problem params:
A = np.array([
[0, 1, 1, 0],
[1, 0, 0, 0],
[1, 0, 0, 0],
[0, 0, 0, 0]
])
b = np.array([0, 0, 1, 0])
n = A.shape[0]
and define our circuit in terms of the moments described in our sketch from earlier:
# each moment needs to compose to an n x n gate
qubits = [ket0 for i in range(n)]
H1 = Gates.Compose([Gates.H for q in qubits])
CZs = []
for i in range(n):
for j in range(n):
if A[i][j] and i < j:
CZs.append(Gates.Control(n, Gates.Z, i, j))
S_b = np.array([1])
for b_i in b:
if b_i == 1: S_b = np.kron(S_b, Gates.S)
else: S_b = np.kron(S_b, Gates.I)
H4 = Gates.Compose([Gates.H for q in qubits])
HLF = Gates.compose_circuit([H1, *CZs, S_b, H4])
reg = Gates.Compose(qubits)
out = reg @ HLF
for p_out in list(zip(stringify(n), basis_state_probs(out))):
print(p_out)
plt_measure(out)
which shows us that each of the following are valid solutions:
which does indeed match our hand-computed choices of :
(note that the labels of the graph are in reverse order – exercise for the reader or whatever). We can doubly sanity check our solution by implementing the corrected instantiation of the problem using Google's cirq
API:
def generate_cirquit(A, b):
n = A.shape[0]
qubits = cirq.LineQubit.range(n)
circuit = cirq.Circuit()
# Hadamards
circuit += cirq.Moment([cirq.H(q) for q in qubits])
# CZ
for i in range(n):
for j in range(n):
if A[i, j] and i < j:
circuit += cirq.CZ(qubits[i], qubits[j])
# S
circuit += cirq.Moment([cirq.S(qubits[i]) for i in range(n) if b[i))
# Hadamards again
circuit += cirq.Moment([cirq.H(q) for q in qubits])
# Measurements
circuit += cirq.Moment([cirq.measure(qubits[i], key=str(i)) for i in range(n)])
counts = {}
for _ in range(100):
z = tuple(solve_problem(A, b, print_circuit=False))
if z in counts: counts[z] += 1
else: counts[z] = 1
which outputs a sampled distribution over the same 's we just found.
HLF wrap up
Again, this problem is psychopathic because it only exists as a variant of the Bernstein-Vazirani problem to prove that quantum circuits can be used to solve classically exponential sized problems with a constant depth of quantum gates.
Bernstein-Vazirani Solution
We can now also easily compose & check a quantum implementation of the Bernstein-Vazirani algorithm. First, the cirq
benchmark:
def generate_circuit_f(n, f=None):
qubits = cirq.LineQubit.range(n+1)
circuit = cirq.Circuit()
# H
circuit += cirq.Moment([cirq.H(q) for q in qubits])
# U
if f is None:
f = np.random.randint(2, size=n)
print(f"actual f: {f}")
for i in range(len(f)):
if f[i] == 1: circuit += cirq.CNOT(qubits[i], qubits[n])
# H again
circuit += cirq.Moment([cirq.H(q) for q in qubits])
# Measurements
circuit += cirq.Moment([cirq.measure(qubits[i], key=str(i)) for i in range(n+1)])
return circuit
n = 5
sim = cirq.CliffordSimulator()
circ = generate_circuit_f(n)
print(circ)
result = sim.simulate(circ, initial_state=1)
f = np.array([result.measurements[str(i)][0] for i in range(n+1)])
print("computed f:", f)
which produces the following circuit & output:
actual f: [1 1 1 0 1]
0: ───H───@───────────────H───M('0')───
│
1: ───H───┼───@───────────H───M('1')───
│ │
2: ───H───┼───┼───@───────H───M('2')───
│ │ │
3: ───H───┼───┼───┼───────H───M('3')───
│ │ │
4: ───H───┼───┼───┼───@───H───M('4')───
│ │ │ │
5: ───H───X───X───X───X───H───M('5')───
computed f: [1 1 1 0 1 1]
Real Problems Have No Solutions
Microsoft just patted themselves on the back so hard they dislodged a rib,17 announcing a new "topological" chip which offers a path to million-qubit systems. This number is significant since that's the order of magnitude required for Shor's algorithm to be remotely viable.18,19
And even then, there are already provisions for "post-quantum" crypto.20
Footnotes
Footnotes
This is a base joke. ↩
Explain Like I’m 25 And I Don’t Know Any Linear Algebra ↩
Q.v. Hadamard code in astronomical error correcting codes ↩
Q.v. Geometric Algebra ↩
Not me tho, I'm a certified rotator ↩
Ethan Bernstein and Umesh Vazirani. "Quantum Complexity Theory." SIAM Journal on Computing Vol. 26, No. 5. 1997. ↩
that's not to say that complex values have no "real world" significance. I'd be loath to perpetuate the notion of "imaginary" numbers being just that – it's just that, for our intents and purposes lol, the complex components are imaginary, negligible, and we just need some guard rails to ensure this model doesn't fly off the rails to silly town aka imaginaryville. ↩
Riesz, F. (1909). "Sur les opérations fonctionnelles linéaires". Comptes rendus de l'Académie des Sciences. Iss 149: 974–977. ↩
One of the passive observations I've made about quantum mechanics after putting together all this information is that it is incredibly tedious to trace any sort of sequence of operations on any quantum system with qubits since the size of the matrices involved is and quickly gets untenable. So, while at first the notation seems spooky and foreign, it grows on you rather quickly after scratching out a few 16x16 matrix multiplications... ↩
oh yeah, I should mention that there are infinitely dimensional quantum systems ↩
we take for convenience, which is consistent in the limit . ↩
Sergey Bravyi et al. "Quantum advantage with shallow circuits." ArXiv, April 2017. ↩
literally had to drop this stupid enumeration because it was too much latex to render on the web ↩
Appropriated from @cjbayesian ↩
https://news.microsoft.com/source/features/ai/microsofts-majorana-1-chip-carves-new-path-for-quantum-computing/ ↩
David Aasen et al. "Roadmap to fault tolerant quantum computation using topological qubit arrays." ArXiv, February 2025. ↩
Gidney, Craig and Ekereå Martin. "How to factor 2048 bit RSA integers in 8 hours using 20 million noisy qubits." ArXiv, April 2021. ↩
FIPS 203. Module-Lattice-Based Key-Encapsulation Mechanism Standard. ↩