Skip to content

Commit 5123f31

Browse files
authored
Optimize PreparePureStateD (#3048)
This change introduces different structure for `Std.StatePreparation.PreparePureStateD` and underlying helper functions, which reduces the compile time by offloading more of the classical compute into static functions run on the full evaluator rather than handled inside the partial-evaluation layer. In local testing, this improved QIR compilation of a large state preparation on 16 qubits from 14 seconds to 4 seconds.
1 parent c9a9e6e commit 5123f31

File tree

1 file changed

+155
-29
lines changed

1 file changed

+155
-29
lines changed

library/std/src/Std/StatePreparation.qs

Lines changed: 155 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,35 @@ import
6363
/// # See Also
6464
/// - Std.StatePreparation.ApproximatelyPreparePureStateCP
6565
operation PreparePureStateD(coefficients : Double[], qubits : Qubit[]) : Unit is Adj + Ctl {
66-
let coefficientsAsComplexPolar = Mapped(a -> ComplexAsComplexPolar(Complex(a, 0.0)), coefficients);
67-
ApproximatelyPreparePureStateCP(0.0, coefficientsAsComplexPolar, qubits);
66+
let nQubits = Length(qubits);
67+
// Pad coefficients at tail length to a power of 2.
68+
let coefficientsPadded = Padded(-2^nQubits, 0.0, coefficients);
69+
let idxTarget = 0;
70+
71+
// Note we use the reversed qubits array to get the endianness ordering that we expect
72+
// when corresponding qubit state to state vector index.
73+
let qubits = Reversed(qubits);
74+
75+
// Since we know the coefficients are real, we can optimize the first round of adjoint approximate unpreparation by directly
76+
// computing the disentangling angles and the new coefficients on those doubles without producing intermediate complex numbers.
77+
78+
// For each 2D block, compute disentangling single-qubit rotation parameters
79+
let (disentanglingY, disentanglingZ, newCoefficients) = StatePreparationSBMComputeCoefficientsD(coefficientsPadded);
80+
81+
if nQubits == 1 {
82+
let (abs, arg) = newCoefficients[0]!;
83+
if (AbsD(arg) > 0.0) {
84+
Adjoint Exp([PauliI], -1.0 * arg, [qubits[idxTarget]]);
85+
}
86+
} elif (Any(c -> AbsComplexPolar(c) > 0.0, newCoefficients)) {
87+
// Some coefficients are outside tolerance
88+
let newControl = 2..(nQubits - 1);
89+
let newTarget = 1;
90+
Adjoint ApproximatelyUnprepareArbitraryState(0.0, newCoefficients, newControl, newTarget, qubits);
91+
}
92+
93+
Adjoint ApproximatelyMultiplexPauli(0.0, disentanglingY, PauliY, qubits[1...], qubits[0]);
94+
Adjoint ApproximatelyMultiplexPauli(0.0, disentanglingZ, PauliZ, qubits[1...], qubits[0]);
6895
}
6996

7097
/// # Summary
@@ -121,7 +148,7 @@ operation ApproximatelyPreparePureStateCP(
121148
) : Unit is Adj + Ctl {
122149

123150
let nQubits = Length(qubits);
124-
// pad coefficients at tail length to a power of 2.
151+
// Pad coefficients at tail length to a power of 2.
125152
let coefficientsPadded = Padded(-2^nQubits, ComplexPolar(0.0, 0.0), coefficients);
126153
let idxTarget = 0;
127154
// Determine what controls to apply
@@ -148,10 +175,9 @@ operation ApproximatelyUnprepareArbitraryState(
148175
) : Unit is Adj + Ctl {
149176

150177
// For each 2D block, compute disentangling single-qubit rotation parameters
151-
let (disentanglingY, disentanglingZ, newCoefficients) = StatePreparationSBMComputeCoefficients(coefficients);
178+
let (disentanglingY, disentanglingZ, newCoefficients) = StatePreparationSBMComputeCoefficientsCP(coefficients);
152179
if (AnyOutsideToleranceD(tolerance, disentanglingZ)) {
153180
ApproximatelyMultiplexPauli(tolerance, disentanglingZ, PauliZ, register[rngControl], register[idxTarget]);
154-
155181
}
156182
if (AnyOutsideToleranceD(tolerance, disentanglingY)) {
157183
ApproximatelyMultiplexPauli(tolerance, disentanglingY, PauliY, register[rngControl], register[idxTarget]);
@@ -226,7 +252,27 @@ operation ApproximatelyMultiplexPauli(
226252

227253
/// # Summary
228254
/// Implementation step of arbitrary state preparation procedure.
229-
function StatePreparationSBMComputeCoefficients(
255+
/// This version optimized for purely real coefficients represented by an array of doubles.
256+
function StatePreparationSBMComputeCoefficientsD(
257+
coefficients : Double[]
258+
) : (Double[], Double[], ComplexPolar[]) {
259+
mutable disentanglingZ = [];
260+
mutable disentanglingY = [];
261+
mutable newCoefficients = [];
262+
263+
for idxCoeff in 0..2..Length(coefficients) - 1 {
264+
let (rt, phi, theta) = BlockSphereCoordinatesD(coefficients[idxCoeff], coefficients[idxCoeff + 1]);
265+
set disentanglingZ += [0.5 * phi];
266+
set disentanglingY += [0.5 * theta];
267+
set newCoefficients += [rt];
268+
}
269+
270+
return (disentanglingY, disentanglingZ, newCoefficients);
271+
}
272+
273+
/// # Summary
274+
/// Implementation step of arbitrary state preparation procedure.
275+
function StatePreparationSBMComputeCoefficientsCP(
230276
coefficients : ComplexPolar[]
231277
) : (Double[], Double[], ComplexPolar[]) {
232278

@@ -235,7 +281,7 @@ function StatePreparationSBMComputeCoefficients(
235281
mutable newCoefficients = [];
236282

237283
for idxCoeff in 0..2..Length(coefficients) - 1 {
238-
let (rt, phi, theta) = BlochSphereCoordinates(coefficients[idxCoeff], coefficients[idxCoeff + 1]);
284+
let (rt, phi, theta) = BlochSphereCoordinatesCP(coefficients[idxCoeff], coefficients[idxCoeff + 1]);
239285
set disentanglingZ += [0.5 * phi];
240286
set disentanglingY += [0.5 * theta];
241287
set newCoefficients += [rt];
@@ -244,6 +290,36 @@ function StatePreparationSBMComputeCoefficients(
244290
return (disentanglingY, disentanglingZ, newCoefficients);
245291
}
246292

293+
/// # Summary
294+
/// Computes the Bloch sphere coordinates for a single-qubit state.
295+
///
296+
/// Given two doubles $a0, a1$ that represent the qubit state, computes coordinates
297+
/// on the Bloch sphere such that
298+
/// $a0 \ket{0} + a1 \ket{1} = r e^{it}(e^{-i \phi /2}\cos{(\theta/2)}\ket{0}+e^{i \phi /2}\sin{(\theta/2)}\ket{1})$.
299+
///
300+
/// # Input
301+
/// ## a0
302+
/// Double coefficient of state $\ket{0}$.
303+
/// ## a1
304+
/// Double coefficient of state $\ket{1}$.
305+
///
306+
/// # Output
307+
/// A tuple containing `(ComplexPolar(r, t), phi, theta)`.
308+
function BlockSphereCoordinatesD(
309+
a0 : Double,
310+
a1 : Double
311+
) : (ComplexPolar, Double, Double) {
312+
let abs0 = AbsD(a0);
313+
let abs1 = AbsD(a1);
314+
let arg0 = a0 < 0.0 ? PI() | 0.0;
315+
let arg1 = a1 < 0.0 ? PI() | 0.0;
316+
let r = Sqrt(abs0 * abs0 + abs1 * abs1);
317+
let t = 0.5 * (arg0 + arg1);
318+
let phi = arg1 - arg0;
319+
let theta = 2.0 * ArcTan2(abs1, abs0);
320+
return (ComplexPolar(r, t), phi, theta);
321+
}
322+
247323
/// # Summary
248324
/// Computes the Bloch sphere coordinates for a single-qubit state.
249325
///
@@ -259,7 +335,7 @@ function StatePreparationSBMComputeCoefficients(
259335
///
260336
/// # Output
261337
/// A tuple containing `(ComplexPolar(r, t), phi, theta)`.
262-
function BlochSphereCoordinates(
338+
function BlochSphereCoordinatesCP(
263339
a0 : ComplexPolar,
264340
a1 : ComplexPolar
265341
) : (ComplexPolar, Double, Double) {
@@ -321,31 +397,18 @@ operation ApproximatelyMultiplexZ(
321397
target : Qubit
322398
) : Unit is Adj + Ctl {
323399

324-
body (...) {
325-
// pad coefficients length at tail to a power of 2.
326-
let coefficientsPadded = Padded(-2^Length(control), 0.0, coefficients);
400+
body ... {
401+
let multiplexZParams = GenerateMultiplexZParams(coefficients, control, target);
402+
ApplyMultiplexZParams(multiplexZParams);
403+
}
327404

328-
if Length(coefficientsPadded) == 1 {
329-
// Termination case
330-
if AbsD(coefficientsPadded[0]) > tolerance {
331-
Exp([PauliZ], coefficientsPadded[0], [target]);
332-
}
333-
} else {
334-
// Compute new coefficients.
335-
let (coefficients0, coefficients1) = MultiplexZCoefficients(coefficientsPadded);
336-
ApproximatelyMultiplexZ(tolerance, coefficients0, Most(control), target);
337-
if AnyOutsideToleranceD(tolerance, coefficients1) {
338-
within {
339-
CNOT(Tail(control), target);
340-
} apply {
341-
ApproximatelyMultiplexZ(tolerance, coefficients1, Most(control), target);
342-
}
343-
}
344-
}
405+
adjoint ... {
406+
let adjMultiplexZParams = GenerateAdjMultiplexZParams(coefficients, control, target);
407+
Adjoint ApplyMultiplexZParams(adjMultiplexZParams);
345408
}
346409

347410
controlled (controlRegister, ...) {
348-
// pad coefficients length to a power of 2.
411+
// Pad coefficients length to a power of 2.
349412
let coefficientsPadded = Padded(2^(Length(control) + 1), 0.0, Padded(-2^Length(control), 0.0, coefficients));
350413
let (coefficients0, coefficients1) = MultiplexZCoefficients(coefficientsPadded);
351414
ApproximatelyMultiplexZ(tolerance, coefficients0, control, target);
@@ -357,8 +420,71 @@ operation ApproximatelyMultiplexZ(
357420
}
358421
}
359422
}
423+
424+
controlled adjoint invert;
425+
}
426+
427+
operation ApplyMultiplexZParams(params : (Double, Qubit[])[]) : Unit is Adj {
428+
for (angle, qs) in params {
429+
if Length(qs) == 2 {
430+
CNOT(qs[0], qs[1]);
431+
} elif AbsD(angle) > 0.0 {
432+
Exp([PauliZ], angle, qs);
433+
}
434+
}
435+
}
436+
// Provides the sequence of angles or entangling CNOTs to apply for the multiplex Z step of the state preparation procedure,
437+
// given a set of coefficients and control and target qubits.
438+
// In these sequences, there are always one or two qubits in the qubit array. If there is one qubit, it is the target of a Z rotation by the given angle.
439+
// If there are two qubits, they represent a CNOT with the first qubit as control and the second as target, and the angle is ignored.
440+
function GenerateMultiplexZParams(
441+
coefficients : Double[],
442+
control : Qubit[],
443+
target : Qubit
444+
) : (Double, Qubit[])[] {
445+
// Pad coefficients length at tail to a power of 2.
446+
let coefficientsPadded = Padded(-2^Length(control), 0.0, coefficients);
447+
448+
if Length(coefficientsPadded) == 1 {
449+
// Termination case
450+
[(coefficientsPadded[0], [target])]
451+
} else {
452+
// Compute new coefficients.
453+
let (coefficients0, coefficients1) = MultiplexZCoefficients(coefficientsPadded);
454+
mutable params = GenerateMultiplexZParams(coefficients0, Most(control), target);
455+
params += [(0.0, [Tail(control), target])] + GenerateMultiplexZParams(coefficients1, Most(control), target);
456+
params += [(0.0, [Tail(control), target])];
457+
params
458+
}
360459
}
361460

461+
// Provides the sequence of angles or entangling CNOTs to apply for the adjoint of the multiplex Z step of the state preparation procedure,
462+
// given a set of coefficients and control and target qubits.
463+
// Note that the adjoint sequence is NOT the reverse of the forward sequence due to the structure of the multiplex Z step, which applies
464+
// the disentangling rotations in between the two recursive calls.
465+
// See `GenerateMultiplexZParams` for more details on the structure of these sequences.
466+
function GenerateAdjMultiplexZParams(
467+
coefficients : Double[],
468+
control : Qubit[],
469+
target : Qubit
470+
) : (Double, Qubit[])[] {
471+
// Pad coefficients length at tail to a power of 2.
472+
let coefficientsPadded = Padded(-2^Length(control), 0.0, coefficients);
473+
474+
if Length(coefficientsPadded) == 1 {
475+
// Termination case
476+
[(coefficientsPadded[0], [target])]
477+
} else {
478+
// Compute new coefficients.
479+
let (coefficients0, coefficients1) = MultiplexZCoefficients(coefficientsPadded);
480+
mutable params = [(0.0, [Tail(control), target])] + GenerateAdjMultiplexZParams(coefficients1, Most(control), target);
481+
params += [(0.0, [Tail(control), target])];
482+
params += GenerateAdjMultiplexZParams(coefficients0, Most(control), target);
483+
params
484+
}
485+
}
486+
487+
362488
/// # Summary
363489
/// Implementation step of multiply-controlled Z rotations.
364490
function MultiplexZCoefficients(coefficients : Double[]) : (Double[], Double[]) {

0 commit comments

Comments
 (0)