Skip to content

Latest commit

 

History

History
241 lines (190 loc) · 8.62 KB

QUICK_START.md

File metadata and controls

241 lines (190 loc) · 8.62 KB

Quick Start Guide

This guide walks through an implementation of Grover's algorithm using quantr 0.6.0. It's aimed at beginners in Rust and requires a little knowledge of the console. Moreover, it's assumed that Rust and Cargo are installed.

A good starting point to learn Rust is The Rust Book. Likewise, Qiskit offers a good explanation of Grover's. This guide does not attempt to explain either of these subjects in detail.

The complete code that is built from following this guide is available at the end. Or, the code can be found in examples/grovers.rs, and ran from the root directory with cargo run --example grovers.


Open the console, and create a new Rust project (known as a cargo package) by running

cargo new grovers_example

This will create a new directory called grovers_example containing the necessary files for a Rust project. Enter this directory.

Add the latest version of quantr as a dependency by running cargo add quantr on the console. This should add quantr below [dependecies] in you Cargo.toml file. Then, run cargo build. This will download the quantr crate from crates.io and make it accessible for your project.

Once quantr has been installed, open src/main.rs. This is where the subsequent code will be written to implement Grover's algorithm.

Add the following lines to the top of main.rs, and before fn main():

use quantr::{Circuit, Gate, Measurement, Printer};

These lines import the structs and enums that will be used throughout this guide.

Rust begins execution of the program by calling the main() function. So, the rest of the code will be inserted into this function. Let's first initialise a three qubit circuit:

let mut circuit: Circuit = Circuit::new(3).unwrap();

Grover's algorithm requires that the starting state is in a superposition of all basis states with equal amplitudes. This can be achieved by adding a Hadamard gate to each wire, that is to wires 0, 1 and 2:

circuit.add_repeating_gate(Gate::H, &[0, 1, 2]).unwrap();

The .unwrap() forces the program to quit if there is an error in adding these gates, such as adding a gate to a non-existent wire. There are no errors in this example, so the method .unwrap() does nothing.

Let's visualise the circuit that has been built so far (or at any other time throughout this guide) by adding the following:

let mut printer: Printer = Printer::new(&circuit);
printer.print_diagram();

This will print a circuit diagram to the console. This can be seen by running the program by entering cargo run while in the directory that cargo built.

The next step in the algorithm requires the oracle to be defined. This is a function built from unitary gates that flips the sign of the state that corresponds to a 'solution' of the searching function. In this example, the states |110> and |111> are chosen to be the solution states.

This is implemented by adding a controlled-z gate targeting the 0th wire, with its control node placed on the 1st wire; the first and second wire from top to bottom in a circuit diagram respectively:

circuit.add_gate(Gate::CZ(1), 0).unwrap();

The second argument of circuit.add_gate specifies which wire is the target, and the field of the variant Gate::CZ specifies the control wire.

With the solution states marked, these amplitudes are amplified so that solution states are more likely to be measured than other non-solution states. This can be achieved by adding:

circuit.add_repeating_gate(Gate::H, &[0, 1, 2]).unwrap()
    .add_repeating_gate(Gate::X, &[0, 1, 2]).unwrap();

// CC-Z gate
circuit.add_gate(Gate::H, 2).unwrap()
    .add_gate(Gate::Toffoli(0, 1), 2).unwrap()
    .add_gate(Gate::H, 2).unwrap();

circuit.add_repeating_gate(Gate::X, &[0, 1, 2]).unwrap()
    .add_repeating_gate(Gate::H, &[0, 1, 2]).unwrap();

This completes the construction of Grover's algorithm. To make sure that the gates are placed correctly, run the printing code as shown before. Once checked that it's correct, the circuit can be simulated by calling

let simulated_circuit: SimulatedCircuit = circuit.simulate();

This effectively attaches the |000> register to the circuit, resulting in a superposition that can be measured. This superposition, and other information about the simulated circuit, is stored in the simulated_circuit struct that was created upon calling the simulation. This SimulatedCircuit struct allows the circuit to be prepared and measured multiple times to collect a bin count of observed states. This bin count can be found and printed with

if let Measurement::Observable(bin_count) = simulated_circuit.measure_all(500) {
        println!("[Observable] Bin count of observed states.");
        for (state, count) in bin_count {
            println!("|{}> observed {} times", state, count);
        }
    } 

The above prints the number of times each state was observed over 500 measurements. In this situation, the amplitude amplification results in a superposition of two states: |110> and |111>.

Note that the above code is explicit in showing that the measurements are physically possible. This is to distinguish from other data that can be taken from circuit, such as the resulting superposition itself. In nature, this cannot be directly observed. However, it can still be useful to view this "theoretical" superposition. The superposition can be viewed explicitly with:

if let Measurement::NonObservable(output_super_position) = simulated_circuit.get_state()
    {
        println!("\n[Non-Observable] The amplitudes of each state in the final superposition.");
        for (state, amplitude) in output_super_position.into_iter() {
            println!("|{}> : {}", state, amplitude);
        }
    }

This completes the construction and measurement of a three qubit Grover's circuit. Other methods in Circuit (which include examples in their documentation) can add gates in other ways. Moreover, custom gates can be built, where examples can be found in examples/qft.rs and examples/custom_gate.rs.

To improve the readability of this code, the numerous unwrap() calls can be removed, requiring the main function declaration to be edited like so:

use ...;
use quantr::QuantrError;

fn main() -> Result<(), QuantrError> {
    ...; 
    Ok(()) 
}

An Ok(()) is returned on the last line; signalling that the program has exited without errors. Then, effectively all unwrap methods called after appending gates can be replaced with a ?. This can be seen explicitly in the example/grovers.rs folder.

The following is the completed code from this tutorial. This can be ran with cargo run --example grovers from the root directory.

use quantr::{Circuit, Gate, Measurement, Printer, QuantrError};

fn main() -> Result<(), QuantrError> {
    let mut circuit = Circuit::new(3)?;

    // Kick state into superposition of equal weights
    circuit.add_repeating_gate(Gate::H, &[0, 1, 2])?;

    // Oracle
    circuit.add_gate(Gate::CZ(1), 0)?;

    // Amplitude amplification
    circuit
        .add_repeating_gate(Gate::H, &[0, 1, 2])?
        .add_repeating_gate(Gate::X, &[0, 1, 2])?
        .add_gate(Gate::H, 2)?
        .add_gate(Gate::Toffoli(0, 1), 2)?
        .add_gate(Gate::H, 2)?
        .add_repeating_gate(Gate::X, &[0, 1, 2])?
        .add_repeating_gate(Gate::H, &[0, 1, 2])?;

    // Prints the circuit in UTF-8
    let mut printer = Printer::new(&circuit);
    printer.print_diagram();

    // Simulates the circuit
    let simulated_circuit = circuit.simulate();
    println!("");

    // Displays bin count of the resulting 500 repeat measurements of
    // superpositions. bin_count is a HashMap<ProductState, usize>.
    if let Measurement::Observable(bin_count) = simulated_circuit.measure_all(500) {
        println!("[Observable] Bin count of observed states.");
        for (state, count) in bin_count {
            println!("|{}> observed {} times", state, count);
        }
    } 

    // Returns the superpsoition that cannot be directly observed.
    if let Measurement::NonObservable(output_super_position) = simulated_circuit.get_state()
    {
        println!("\n[Non-Observable] The amplitudes of each state in the final superposition.");
        for (state, amplitude) in output_super_position.into_iter() {
            println!("|{}> : {}", state, amplitude);
        }
    }

    Ok(())
}