Skip to content

Commit

Permalink
Table lookup with select networks (#864)
Browse files Browse the repository at this point in the history
This adds an operation `Select` in a new namespace
`Microsoft.Quantum.Unstable.TableLookup`. It implements a table lookup
based on a recursive implementation of the select network algorithm (see
references inside the doc string for more details).

The behavior of this operation is similar to
[MultiplexOperations](https://learn.microsoft.com/en-us/qsharp/api/qsharp/microsoft.quantum.canon.multiplexoperations),
with the following differences:

- It uses an optimized implementation
- It has a name more commonly used in the quantum algorithms community
- It writes bit strings rather than general unitaries

---------

Co-authored-by: Dmitry Vasilevsky <[email protected]>
  • Loading branch information
msoeken and Dmitry Vasilevsky authored Dec 7, 2023
1 parent 8be9143 commit 2b224bb
Show file tree
Hide file tree
Showing 5 changed files with 400 additions and 0 deletions.
4 changes: 4 additions & 0 deletions compiler/qsc_frontend/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,10 @@ pub fn std(store: &PackageStore, target: TargetProfile) -> CompileUnit {
"unstable_arithmetic_internal.qs".into(),
include_str!("../../../library/std/unstable_arithmetic_internal.qs").into(),
),
(
"unstable_table_lookup.qs".into(),
include_str!("../../../library/std/unstable_table_lookup.qs").into(),
),
(
"re.qs".into(),
include_str!("../../../library/std/re.qs").into(),
Expand Down
277 changes: 277 additions & 0 deletions library/std/unstable_table_lookup.qs
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.Quantum.Unstable.TableLookup {
open Microsoft.Quantum.Arithmetic;
open Microsoft.Quantum.Unstable.Arithmetic;
open Microsoft.Quantum.Arrays;
open Microsoft.Quantum.Convert;
open Microsoft.Quantum.Diagnostics;
open Microsoft.Quantum.Math;
open Microsoft.Quantum.Measurement;
open Microsoft.Quantum.ResourceEstimation;

/// # Summary
/// Performs table lookup using a SELECT network
///
/// # Description
/// Assuming a zero-initialized `target` register, this operation will
/// initialize it with the bitstrings in `data` at indices according to the
/// computational values of the `address` register.
///
/// # Input
/// ## data
/// The classical table lookup data which is prepared in `target` with
/// respect to the state in `address`. The length of data must be less than
/// 2ⁿ, where 𝑛 is the length of `address`. Each entry in data must have
/// the same length that must be equal to the length of `target`.
/// ## address
/// Address register
/// ## target
/// Zero-initialized target register
///
/// # Remarks
/// The implementation of the SELECT network is based on unary encoding as
/// presented in [1]. The recursive implementation of that algorithm is
/// presented in [3]. The adjoint variant is optimized using a
/// measurement-based unlookup operation [3]. The controlled adjoint variant
/// is not optimized using this technique.
///
/// # References
/// [1] [arXiv:1805.03662](https://arxiv.org/abs/1805.03662)
/// "Encoding Electronic Spectra in Quantum Circuits with Linear T
/// Complexity"
/// [2] [arXiv:1905.07682](https://arxiv.org/abs/1905.07682)
/// "Windowed arithmetic"
/// [3] [arXiv:2211.01133](https://arxiv.org/abs/2211.01133)
/// "Space-time optimized table lookup"
@Config(Full)
operation Select(
data : Bool[][],
address : Qubit[],
target : Qubit[]
) : Unit is Adj + Ctl {
body (...) {
let (N, n) = DimensionsForSelect(data, address);

if N == 1 { // base case
WriteMemoryContents(Head(data), target);
} else {
let (most, tail) = MostAndTail(address[...n - 1]);
let parts = Partitioned([2^(n - 1)], data);

within {
X(tail);
} apply {
SinglyControlledSelect(tail, parts[0], most, target);
}

SinglyControlledSelect(tail, parts[1], most, target);
}
}
adjoint (...) {
Unlookup(Select, data, address, target);
}

controlled (ctls, ...) {
let numCtls = Length(ctls);

if numCtls == 0 {
Select(data, address, target);
} elif numCtls == 1 {
SinglyControlledSelect(ctls[0], data, address, target);
} else {
use andChainTarget = Qubit();
let andChain = MakeAndChain(ctls, andChainTarget);
use helper = Qubit[andChain::NGarbageQubits];

within {
andChain::Apply(helper);
} apply {
SinglyControlledSelect(andChainTarget, data, address, target);
}
}
}

controlled adjoint (ctls, ...) {
Controlled Select(ctls, (data, address, target));
}
}

@Config(Full)
internal operation SinglyControlledSelect(
ctl : Qubit,
data : Bool[][],
address : Qubit[],
target : Qubit[]
) : Unit {
let (N, n) = DimensionsForSelect(data, address);

if BeginEstimateCaching("Microsoft.Quantum.Unstable.TableLookup.SinglyControlledSelect", N) {
if N == 1 { // base case
Controlled WriteMemoryContents([ctl], (Head(data), target));
} else {
use helper = Qubit();

let (most, tail) = MostAndTail(address[...n - 1]);
let parts = Partitioned([2^(n - 1)], data);

within {
X(tail);
} apply {
ApplyAndAssuming0Target(ctl, tail, helper);
}

SinglyControlledSelect(helper, parts[0], most, target);

CNOT(ctl, helper);

SinglyControlledSelect(helper, parts[1], most, target);

Adjoint ApplyAndAssuming0Target(ctl, tail, helper);
}

EndEstimateCaching();
}
}

internal function DimensionsForSelect(
data : Bool[][],
address : Qubit[]
) : (Int, Int) {
let N = Length(data);
Fact(N > 0, "data cannot be empty");

let n = Ceiling(Lg(IntAsDouble(N)));
Fact(
Length(address) >= n,
$"address register is too small, requires at least {n} qubits");

return (N, n);
}

internal operation WriteMemoryContents(
value : Bool[],
target : Qubit[]
) : Unit is Adj + Ctl {
Fact(
Length(value) == Length(target),
"number of data bits must equal number of target qubits");

ApplyPauliFromBitString(PauliX, true, value, target);
}

/// # References
/// - [arXiv:1905.07682](https://arxiv.org/abs/1905.07682)
/// "Windowed arithmetic"
@Config(Full)
internal operation Unlookup(
lookup : (Bool[][], Qubit[], Qubit[]) => Unit,
data : Bool[][],
select : Qubit[],
target : Qubit[]
) : Unit {
let numBits = Length(target);
let numAddressBits = Length(select);

let l = MinI(Floor(Lg(IntAsDouble(numBits))), numAddressBits - 1);
Fact(
l < numAddressBits,
$"l = {l} must be smaller than {numAddressBits}");

let res = Mapped(r -> r == One, ForEach(MResetX, target));

let dataFixup = Chunks(2^l, Padded(-2^numAddressBits, false,
Mapped(MustBeFixed(res, _), data)));

let numAddressBitsFixup = numAddressBits - l;

let selectParts = Partitioned([l], select);
let targetFixup = target[...2^l - 1];

within {
EncodeUnary(selectParts[0], targetFixup);
ApplyToEachA(H, targetFixup);
} apply {
lookup(dataFixup, selectParts[1], targetFixup);
}
}

// Checks whether specific bitstring `data` must be fixed for a given
// measurement result `result`.
//
// Returns true if the number of indices for which both result and data are
// `true` is odd.
internal function MustBeFixed(result : Bool[], data : Bool[]) : Bool {
mutable state = false;
for i in IndexRange(result) {
set state = state != (result[i] and data[i]);
}
state
}

// Computes unary encoding of value in `input` into `target`
//
// Assumptions:
// - `target` is zero-initialized
// - length of `input` is n
// - length of `target` is 2^n
internal operation EncodeUnary(
input : Qubit[],
target : Qubit[]
) : Unit is Adj {
Fact(
Length(target) == 2^Length(input),
$"target register should be of length {2^Length(input)}, but is {Length(target)}"
);

X(Head(target));

for i in IndexRange(input) {
if i == 0 {
CNOT(input[i], target[1]);
CNOT(target[1], target[0]);
} else {
// targets are the first and second 2^i qubits of the target register
let split = Partitioned([2^i, 2^i], target);
for j in IndexRange(split[0]) {
ApplyAndAssuming0Target(input[i], split[0][j], split[1][j]);
CNOT(split[1][j], split[0][j]);
}
}
}

}

internal newtype AndChain = (
NGarbageQubits: Int,
Apply: Qubit[] => Unit is Adj
);

internal function MakeAndChain(ctls : Qubit[], target : Qubit) : AndChain {
AndChain(
MaxI(Length(ctls) - 2, 0),
helper => AndChainOperation(ctls, helper, target)
)
}

internal operation AndChainOperation(ctls : Qubit[], helper : Qubit[], target : Qubit) : Unit is Adj {
let n = Length(ctls);

Fact(Length(helper) == MaxI(n - 2, 0), "Invalid number of helper qubits");

if n == 0 {
X(target);
} elif n == 1 {
CNOT(ctls[0], target);
} else {
let ctls1 = ctls[0..0] + helper;
let ctls2 = ctls[1...];
let tgts = helper + [target];

for idx in IndexRange(tgts) {
ApplyAndAssuming0Target(ctls1[idx], ctls2[idx], tgts[idx]);
}
}
}
}
2 changes: 2 additions & 0 deletions library/tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ mod test_math;
#[cfg(test)]
mod test_measurement;
#[cfg(test)]
mod test_table_lookup;
#[cfg(test)]
mod tests;

use qsc::{
Expand Down
63 changes: 63 additions & 0 deletions library/tests/src/resources/select.qs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
namespace Test {
open Microsoft.Quantum.Arithmetic;
open Microsoft.Quantum.Arrays;
open Microsoft.Quantum.Convert;
open Microsoft.Quantum.Diagnostics;
open Microsoft.Quantum.Measurement;
open Microsoft.Quantum.Random;
open Microsoft.Quantum.Unstable.TableLookup;

internal operation TestSelect(addressBits : Int, dataBits : Int) : Unit {
use addressRegister = Qubit[addressBits];
use temporaryRegister = Qubit[dataBits];
use dataRegister = Qubit[dataBits];

let data = DrawMany(_ => DrawMany(_ => (DrawRandomInt(0, 1) == 1), dataBits, 0), 2^addressBits, 0);

for (index, expected) in Enumerated(data) {
ApplyXorInPlace(index, addressRegister);

// a temporary register is not necessary normally, but we want to
// test the optimized adjoint operation as well.
within {
Select(data, addressRegister, temporaryRegister);
} apply {
ApplyToEach(CNOT, Zipped(temporaryRegister, dataRegister));
}

Fact(Mapped(ResultAsBool, MResetEachZ(dataRegister)) == expected, $"Invalid data result for address {index}");
Fact(MeasureInteger(addressRegister) == index, $"Invalid address result for address {index}");
}
}

internal operation TestSelectFuzz(rounds : Int) : Unit {
for _ in 1..rounds {
let addressBits = DrawRandomInt(2, 6);
let dataBits = 10;
let numData = DrawRandomInt(2^(addressBits - 1) + 1, 2^addressBits - 1);

let data = DrawMany(_ => DrawMany(_ => (DrawRandomInt(0, 1) == 1), dataBits, 0), numData, 0);

use addressRegister = Qubit[addressBits];
use temporaryRegister = Qubit[dataBits];
use dataRegister = Qubit[dataBits];

for _ in 1..5 {
let index = DrawRandomInt(0, numData - 1);

ApplyXorInPlace(index, addressRegister);

// a temporary register is not necessary normally, but we want to
// test the optimized adjoint operation as well.
within {
Select(data, addressRegister, temporaryRegister);
} apply {
ApplyToEach(CNOT, Zipped(temporaryRegister, dataRegister));
}

Fact(Mapped(ResultAsBool, MResetEachZ(dataRegister)) == data[index], $"Invalid data result for address {index} (addressBits = {addressBits}, dataBits = {dataBits})");
Fact(MeasureInteger(addressRegister) == index, $"Invalid address result for address {index} (addressBits = {addressBits}, dataBits = {dataBits})");
}
}
}
}
Loading

0 comments on commit 2b224bb

Please sign in to comment.