Code
qcr:2606.16967.1

Simon's Algorithm with Amazon Braket

Simon's algorithm was the first quantum algorithm to demonstrate an exponential speedup over the best known classical algorithm, and it directly inspired Shor's factoring algorithm. This Amazon Braket notebook implements it on a simulator. The problem it solves is to find a hidden bit string s, given black-box access to a function that is two-to-one with that period (it returns the same value for inputs x and x XOR s). A classical algorithm must query the function roughly 2^(n/2) times to find a colliding pair, whereas Simon's algorithm needs only on the order of n quantum queries. The notebook constructs the oracle that encodes a chosen secret period, prepares the input register in superposition, applies the oracle to entangle inputs with their function values, and applies Hadamard transforms so that each measurement returns a random bit string orthogonal to the period. Collecting a linear number of such strings yields a system of linear equations solved classically by Gaussian elimination to recover s. By building the circuit with Braket primitives and combining quantum sampling with classical post-processing, the example showcases the hybrid structure and the query speedup that paved the way for Shor's algorithm.
Qubit
Circuit-based
Uploaded 4 days ago
11
Views
GitHub582
Citing this entry? Use this QCR ID
Uploaded by
QL
QCR Librarian

Overview

amazon-braket/amazon-braket-examples
582261
In [ ]:
# --- Setup cell added by QCR (not part of the original tutorial) ---
# Source: amazon-braket/amazon-braket-examples @ 0c0818f315479aab9deebed7e7ed7533ac581923, Apache License 2.0.
# Installs the example's dependencies. If a later cell still reports a missing
# package, restart the runtime/kernel and run again from the top.
%pip install -q amazon-braket-sdk==1.117.3 matplotlib

Simon's Algorithm

Abstract: We study a quantum algorithm known as Simon's algorithm, which provided the first example of an exponential speedup over the best known classical algorithm by using a quantum computer to solve a particular problem. Originally published in 1994, Simon's algorithm was a precursor to Shor's well-known factoring algorithm, and it served as inspiration for many of the seminal works in quantum computation that followed.

This notebook is aimed at users with some basic knowledge of quantum computing and quantum circuits.

Table of Contents

Simon's Problem Statement:

Suppose we’re given a function that maps bit strings to bit strings along with the promise that for some unknown -bit string , and where means bitwise addition modulo 2.

Said another way, there exists an unknown string such that, . When is non-zero, the function is two-to-one as it maps exactly two inputs to every unique output.

The goal of Simon's problem is to determine if is one-to-one, or two-to-one, or equivalently to find the secret string .

Since we're given the promise that , this means that whenever . Thus, one way to solve this problem is to find two inputs to the function that produce the same output; is then the XOR of those two input strings. See [1] for more details.

Example for n=3:

Consider the function defined by the truth table below.

By inspection, we can see that satisfies the properties described in the statement of Simon's problem. In particular, note that each output appears twice for two distinct inputs. We are given the promise that, for each of these two inputs and with the same output , we have for a yet to be determined , and therefore .

For concreteness, notice that the input strings and are both mapped by to the same output string . Taking the bitwise XOR of and we obtain the secret string :

Therefore, in this example, the secret string is .

In this specific example, we also see that the string is mapped to itself. Since for two inputs and with the same output, we must have that is also mapped to , since . Indeed, we see that maps to , as expected.

Classical Complexity

To solve Simon's problem classically, one needs to find two different inputs and for which . As we saw above, one can then determine . How hard is it to find two distinct inputs that map to the same output, given the function as a black box? For -bit strings, there are possible inputs. Thus, in the worst case, one would need to check at most different inputs to find a pair that maps to the same output; this provides an upper bound on the required query complexity.

It turns out that a lower bound on the classical query complexity of Simon's algorithm can also be found: . Proving this lower bound requires a little more work and is outside the scope of this notebook, so instead we will just provide some intuition.

As mentioned above, the goal is to find a pair of input strings and that map to the same output string -- a collision. Finding a collision in a set is an instance of the well-known (generalized) birthday problem [2]: within a group of people, what is the probability that two of them share the same birthday? One can turn this problem around and ask "how many people do we need in a room to ensure that the probability that at least two of them share a birthday is greater than some fixed number?" This latter question gets to the heart of solving Simon's problem classically: how many queries to the function do we need to make to guarantee that we find a collision, with high probability? In the case of the birthday problem, we would need enough people in the room so that when we generate all possible pairings of people, we would have about 365 possible pairs. That way, we'd have a good chance that at least one of those pairs of people share a birthday. Using this intuition, we need to query enough times to generate a set of pairs with roughly the same size as the number of possible inputs (). If we make queries to the function , we can generate pairs. Thus, we need to make queries such that to have a high probability of generating a collision, and therefore, .

Quantum Algorithm for Simon's Problem

Simon's algorithm is a scheme for solving the problem above using exponentially fewer queries to the function . In order for Simon's algorithm to work, one needs to be able to implement the unknown function using quantum logic. That is, given an input quantum state , one needs a unitary satisfying This unitary is an oracle for , and the goal is to query it as few times as possible to learn the secret string .

Quantum Circuit

Simon's algorithm involves both quantum and classical components. The quantum part of Simon's algorithm is used to query the oracle efficiently, while the classical component is used to process measurement results and determine the hidden string . A circuit for the quantum component of Simon's algorithm is shown below.

For a function acting on -bit strings, the circuit above acts on qubits, as needed for the definition of . Only the first qubits are measured; the remaining qubits are unused after the application of .

Running Simon's Algorithm

To solve Simon's problem, one needs to run the quantum circuit above several times. After each run of the circuit, the measurements of the first qubits produce an output bit string, which we denote by .

An analysis of the circuit above shows that each output bit string satisfies the following condition:

Let us now analyze the above circuit step-by-step:

  1. Initialize all qubits in the state. That is, we start in the state . We will use the shorthand

  2. Apply Hadamard gates to each of the first qubits, placing them in the equal superposition state: .

  3. Apply the oracle , which computes the function into the last qubits, giving the state

  4. Measure the last qubits, giving a random result . If is one-to-one, this output of corresponds to an input of . If is two-to-one, the output of corresponds to an input of either either or , where and are the two different inputs to that gave the same output . Hence we are left with the first qubits in the state;

    A. ,                    if is one-to-one; B. , where ,  if is two-to-one. Note that this step is not strictly necessary, since we do not need the measurement result, but we include it as it makes the analysis easier.
  5. Apply Hadamard gates to each of the first qubits. If is one-to-one, the state is mapped to where is the dot product between the two strings represented as vectors (modulo 2).

    Similarly, if is two-to-one, the state is mapped to .

  6. Measure the first qubits.

    1. If is one-to-one, measurements return a random bit string uniformly chosen from .
    2. If is two-to-one, measurements return a random bit string such that , since otherwise the amplitude cancels out. Using the criterion for Simons problem (i.e,. ), we find that

    Thus, in both cases we obtain a random bit string such that .

Therefore, each time we run the quantum circuit above, we find a bit string that is orthogonal to the secret string .

Classical Post-Processing

From the measurement results , we can form a system of equations:

There are equations and unknowns (the elements of ). If we run the quantum part enough times so that we find independent equations, then we can solve these equations (using, e.g., Gaussian elimination) to recover the secret string . This is precisely the classical post-processing required: solve the system of equations found above to recover the string . We refer the interested reader to the Appendix for details.

Quantum Complexity

How many queries do we need to make to in the quantum case? Above we saw that we need to run the quantum part of the algorithm times to generate a system of equations. We need to find linearly independent equations for the system to be determined. Thus, we need at least queries to to find such a system. It is possible, however, that we will get the same measurement outcome on different runs of the quantum algorithm, so we would need to re-do those runs that do not produce distinct measurement outcomes. Fortunately, these repeated outcomes are unlikely, so we only need queries to the oracle .

Comparing the quantum and classical algorithms, we saw that the classical algorithm requires at least queries to , whereas the quantum algorithm requires only . Thus, we have established an exponential speedup by using the quantum algorithm above.

Implementing Simon's Algorithm in Amazon Braket

In [1]:
# Imports and Setup
import matplotlib.pyplot as plt
import numpy as np

from braket.circuits import Circuit
from braket.devices import LocalSimulator

%matplotlib inline

# Sets the device to run the circuit on
device = LocalSimulator()

We also import a method called simons_oracle, which generates a circuit implementing an example oracle function. The code for simons_oracle is defined in the simons_utils.py module, and it is shown in the Appendix for completeness.

In [2]:
# Import local utils
from simons_utils import simons_oracle  # noqa: F401

We now define the secret string, :

In [3]:
s = "101011"

# Other examples to try:
# s = '011'
# s = '00000'
# s = '1'
# Generate a random string of random length from 1 to 10:
# s="".join(str(np.random.randint(2)) for _ in range(np.random.randint(1,10)))

print("The secret string is: " + s)
The secret string is: 101011

Circuit Definition

We now define the quantum circuit for Simon's algorithm:

  1. Apply Hadamard gates to the first -qubits.

  2. Query the oracle (i.e., the gate). In this example, the oracle is defined dynamically, based on our chosen value of . You can try experimenting with different values of (with differing lengths).

  3. Apply Hadamard gates to the first -qubits.

In [4]:
n = len(s)

circ = Circuit()

# Apply Hadamard gates to first n qubits
circ.h(range(n))

# Now apply the Oracle for f
circ.simons_oracle(s)

# Apply Hadamard gates to the first n qubits
circ.h(range(n))


print(circ)
T   : |0|     1     | 2 |3|4|5|6|
                                 
q0  : -H-C-----------C---C-C-C-H-
         |           |   | | |   
q1  : -H-|-C---------|-H-|-|-|---
         | |         |   | | |   
q2  : -H-|-|-C-------|-H-|-|-|---
         | | |       |   | | |   
q3  : -H-|-|-|-C-----|-H-|-|-|---
         | | | |     |   | | |   
q4  : -H-|-|-|-|-C---|-H-|-|-|---
         | | | | |   |   | | |   
q5  : -H-|-|-|-|-|-C-|-H-|-|-|---
         | | | | | | |   | | |   
q6  : ---X-|-|-|-|-|-X---|-|-|---
           | | | | |     | | |   
q7  : -----X-|-|-|-|-----|-|-|---
             | | | |     | | |   
q8  : -------X-|-|-|-----X-|-|---
               | | |       | |   
q9  : ---------X-|-|-------|-|---
                 | |       | |   
q10 : -----------X-|-------X-|---
                   |         |   
q11 : -------------X---------X---

T   : |0|     1     | 2 |3|4|5|6|

Now run the circuit

We need enough shots to obtain linearly independent bit strings in the output measurements. We have chosen 4n shots in the example below, just to be on the safe side.

In [5]:
task = device.run(circ, shots=4 * n)

Analyze the results

We can retrieve the measurement results on all qubits as follows:

In [6]:
result = task.result()

counts = result.measurement_counts
plt.bar(counts.keys(), counts.values())
plt.xlabel("bit strings")
plt.ylabel("counts")
plt.xticks(rotation=90)
plt.show()

Aggregate the results

The measurements are performed on all qubits, but we are only interested in the first qubits. Thus, we need to aggregate the results by ignoring the measurement outcomes on the last qubits:

In [7]:
new_results = {}
for bitstring, count in result.measurement_counts.items():
    # Only keep the outcomes on first n qubits
    trunc_bitstring = bitstring[:n]
    # Add the count to that of the of truncated bit string
    new_results[trunc_bitstring] = new_results.get(trunc_bitstring, 0) + count

plt.bar(new_results.keys(), new_results.values())
plt.xlabel("bit strings")
plt.ylabel("counts")
plt.xticks(rotation=70)
plt.show()

In practice, we only need the measurement results (i.e., the bit strings, not the counts) from the first qubits. These measurement outcomes correspond to bit strings that satisfy the equations:

With these linear equations in hand, we can use classical post-processing to solve for the unknown string .

Note that we may have too many bit strings in the above, since we ran the quantum task with shots just to be safe. In this case, not all of the bit strings will be linearly independent. Moreover, the all-zeros string may also be an outcome, but this bit string satisfies the equations above trivially, so we exclude it if needed.

At this stage, the quantum portion of Simon's algorithm is complete. Any remaining steps are just classical postprocessing, which we cover in the Appendix.

References

[1] Wikipedia: Simon's Problem

[2] Wikipedia: Birthday Problem

[3] Wikipedia: Computing a kernel by Gaussian elimination

[4] StackExchange: Sympy: Solving Matrices in a finite field


Appendix

Implementing an Oracle Function

In order to run the algorithm, we will need a unitary function that we can use as an oracle to query the function .

There are many possible ways of implementing a function with the desired property that . We will pick one implementation that is commonly used in example code, and we will try to give some intuition for why this oracle works.

Classical Intuition Behind the Function

Generating a function that is one-to-one is conceptually straightforward, as any such function of the bit strings will just be a permutation of the inputs. Generating a two-to-one function is a little trickier, though there are many ways to do it. The goal is to define a function that splits the inputs into two groups, such that one element from each group maps to the same output (i.e., must be in one group, while must be in the other group.)

We will implement one simple choice for , in which we define the split based on the value of one of the bits in the string. In this way, exactly half of the inputs will have that bit with value , while the other half will have that bit with value .

Our approach will be to choose a flag bit in the input bit strings that we will use to split the inputs. We then the input string with whenever the flag bit is . With this definition, half of the input strings will be untouched, while half of the strings will be 'ed with . Clearly, this function does nothing to the all-zeros string , since any choice of the flag bit will always be . Thus, we need to ensure that our definition also maps the string to the all-zeros string . In other words, we need to ensure that our flag bit is when the input string is . One way to ensure the function acts correctly on the input is to just define the flag bit to be the first bit in the string that is equal to . For example, if , we can choose the flag bit to be the second bit. Concretely: where is the bit of , and is the flag bit in .

Example for n=3:

We now revisit the example in the introduction. Suppose the secret string . Since the first appearance of in occurs at the second bit, we will use the second bit in the input strings as our flag bit. We take the of the input with whenever the flag bit in the input is 1. This definition results in the following truth table:

We leave it as an exercise to the reader to verify that this definition works for any input string size (i.e., for inputs , and that it is in fact two-to-one, rather than many-to-one. Note that the function defined in this way is not a general two-to-one function, but it is simple a choice that is easy to implement both classically and as a quantum circuit.

Quantum Implementation of

We now define the unitary using the @circuit.subroutine functionality of the Amazon Braket SDK. The following code was imported from the simons_utils.py module, and is shown below for reference.

In the quantum setting, we first copy the input register into some ancillary qubits: We then perform the quantum analog of , which means we apply an gate to the qubit whenever the bit of is . However, we only apply this gate when the flag qubit is also . Thus, our gate becomes a gate between the flag qubit on the input register, and the qubit on the output.

from braket.circuits import Circuit, circuit

@circuit.subroutine(register=True)
def simons_oracle(secret_s: str):
    """
    Quantum circuit implementing a particular oracle for Simon's problem. Details of this implementation are
    explained in the Simons Algorithm demo notebook.

    Args:
        secret_s (str): secret string we wish to find
    """
    # Find the index of the first 1 in s, to be used as the flag bit
    flag_bit=secret_s.find('1')
    
    n=len(secret_s)
    
    circ = Circuit()
    # First copy the first n qubits, so that |x>|0> -> |x>|x>
    for i in range(n):
        circ.cnot(i, i+n)
    
    # If flag_bit=-1, s is the all-zeros string, and we do nothing else.
    if flag_bit != -1:
        # Now apply the XOR with s whenever the flag bit is 1.
        for index,bit_value in enumerate(secret_s):
            
            if bit_value not in ['0','1']:
                raise Exception ('Incorrect char \'' + bit_value + '\' in secret string s:' + secret_s)
                
            # XOR with s whenever the flag bit is 1.
            # In terms of gates, XOR means we apply an X gate only whenever the corresponding bit in s is 1.
            # Applying this X only when the flag qubit is 1 means this is a CNOT gate.
            if(bit_value == '1'):
                circ.cnot(flag_bit,index+n)
    return circ

Implementing the Classical Post-Processing

We will now solve the system of linear equations above using Gaussian elimination. We first convert the results into matrix form, then we use sympy's Matrix.rref() method to transform the matrix into reduced row echelon form.

In [8]:
from sympy import Matrix
[notice] A new release of pip available: 22.3.1 -> 23.2.1
[notice] To update, run: pip install --upgrade pip

Generate a matrix from the bit string outputs. We first check that we have sufficiently many output strings to be able to solve the system of equations. If not: output and error and re-run the algorithm.

In [9]:
if len(new_results.keys()) < len(s):
    raise Exception(
        "System will be underdetermined. Minimum "
        + str(n)
        + " bistrings needed, but only "
        + str(len(new_results.keys()))
        + " returned. Please rerun Simon's algorithm.",
    )
string_list = [[int(c) for c in key] for key in new_results]

print("The result in matrix form is :")
for a in string_list:
    print(a)
The result in matrix form is :
[1, 0, 0, 0, 1, 0]
[1, 1, 0, 1, 1, 0]
[1, 1, 1, 1, 0, 0]
[1, 1, 1, 0, 0, 0]
[0, 0, 1, 1, 1, 0]
[1, 0, 1, 0, 1, 1]
[1, 0, 1, 1, 0, 0]
[1, 0, 0, 1, 1, 0]
[0, 1, 1, 1, 0, 1]
[0, 1, 1, 0, 0, 1]
[0, 0, 0, 1, 0, 0]
[1, 0, 1, 0, 0, 0]
[1, 1, 0, 0, 0, 1]
[1, 0, 0, 0, 0, 1]
[1, 1, 1, 1, 1, 1]

Now solve the system by finding the kernel of . We do this using Gaussian elimination on the augmented matrix to bring it to row echelon form. Converting the solution to numbers , we can then read off the solution from the last row of the reduced matrix. See [3] and [4] for more details.

In [10]:
M = Matrix(string_list).T

# Construct the agumented matrix
M_I = Matrix(np.hstack([M, np.eye(M.shape[0], dtype=int)]))

# Perform row reduction, working modulo 2. We use the iszerofunc property of rref
# to perform the Gaussian elimination over the finite field.
M_I_rref = M_I.rref(iszerofunc=lambda x: x % 2 == 0)

# In row reduced echelon form, we can end up with a solution outside of the finite field {0,1}.
# Thus, we need to revert the matrix back to this field by treating fractions as a modular inverse.
# Since the denominator will always be odd (i.e. 1 mod 2), it can be ignored.


# Helper function to treat fractions as modular inverse:
def mod2(x):
    return x.as_numer_denom()[0] % 2


# Apply our helper function to the matrix
M_I_final = M_I_rref[0].applyfunc(mod2)

# Extract the kernel of M from the remaining columns of the last row, when s is nonzero.
if all(value == 0 for value in M_I_final[-1, : M.shape[1]]):
    result_s = "".join(str(c) for c in M_I_final[-1, M.shape[1] :])

# Otherwise, the sub-matrix will be full rank, so just set s=0...0
else:
    result_s = "0" * M.shape[0]

# Check whether result_s is equal to initial s:
print("Secret string: " + s)
print("Result string: " + result_s)
if result_s == s:
    print("We found the correct answer.")
else:
    print("Error. The answer is wrong!")
Secret string: 101011
Result string: 101011
We found the correct answer.

Join the Discussion

Comments (0)

No comments yet. Be the first to share your thoughts!

Indexed by QCR Librarian

This entry was created automatically from publicly available records. QCR links to public sources and only stores repository content where the license permits redistribution.

Versions

v1 Latest
Jun 15, 2026
qcr:2606.16967.1

Cite all versions? Use the base QCR ID to always reference the latest version of this entry.

Tools used

Amazon Braket SDK

Keywords

simon
hidden-period
braket
query-complexity
exponential-speedup

You may also like5