Use this file to discover all available pages before exploring further.
View on GitHub
Open this notebook in GitHub to run it yourself
This demonstration is based on the [1] paper.The notebook was written in collaboration with Prof. Di Fang, the first author of the paper.Time marching is a method for solving differential equations in time by integrating the solution vector through time in small discrete steps, where each timestep depends on previous timesteps.This paper applies an evolution matrix sequentially on the state and makes it evolve through time, as done in time-dependent Hamiltonian simulations.
Input: a system of linear homogenous linear equations (ODEs):
dtd∣ψ(t)⟩=A(t)∣ψ(t)⟩,∣ψ(0)⟩=∣ψ0⟩Note that A can vary in time. We assume that the matrix A is with bounded variation.The input model of A(t) is a series of time-dependent block-encodings, described next.
Output: a state that is proportional to the solution at time T, ∣ψ(T)⟩.
The algorithm divides the timeline into long timesteps and short timesteps. In each long timestep, some approximation of evolution of the short timesteps is done, such as the Truncated Dyson series [2] or Magnus series [3].These are applied as block-encodings on the state, where the following matrix is block-encoded in each long timestep:Ξl=Te∫tl−1tlA(t)dtThe problem is that when this block-encoding has some prefactor s (for example, because some LCU is block-encoding the integration), the prefactor of the entire simulation is amplified by s on each iteration.This means that the probability to sample the wanted block decreases exponentially with the number of long timesteps.This is the main pain-point that the algorithm in the paper resolves. In the case of Hamiltonian simulation, it is possible to wrap each timestep with oblivious amplitude amplification [4] (see oblivious amplitude amplification) and get rid of the prefactor. However, it is only possible in the case of a unitary block-encoding.The authors address this issue by using uniform singular amplitude amplification [5] instead, within the QSVT framework.
We choose an easy artificial example to demonstrate the algorithm.For simplicity, we choose A, which is easy to block-encode.The following matrix can be easily block-encoded using linear Pauli rotations:Aij(t)=cos(i+t)δijThe matrix is Hermitian and diagonal, and it helps us in several aspects:
The first-order Magnus expansion will be exact.
The QSVT and QET (quantum eigenvalue transform) will coincide, and we use it to exponentiate the block-encoding.
We simulate a 4x4 matrix using four timesteps, from t=0 to t=2:
The time-dependent block-encoding of A(t):(Inq⊗⟨0m∣⊗In)UA(t)(Inq⊗∣0m⟩⊗In)=i=0∑2nq−1∣i⟩⟨i∣αA((b−a)2nqi+a)For a given timeslice, we get this:Aij(t,a,b)=cos((b−a)2nqt+a+i)δijWe accomplish this easily with a sequence of two Pauli rotations:
from classiq import *SHORT_INTERVALS_TIME_SIZE = 2class TimeDependentBE(QStruct): index: QNum[DIM_SIZE] time: QNum[SHORT_INTERVALS_TIME_SIZE] block: QBit@qfuncdef block_encode_time_dependent_A(a: CReal, b: CReal, qbe: TimeDependentBE): # a factor 2 is applied on the slopes and offsets as RY rotates at half of the angle linear_pauli_rotations( [Pauli.Y], [(b - a) * 2 / (2**qbe.time.size)], [2 * a], qbe.time, qbe.block ) linear_pauli_rotations([Pauli.Y], [2], [0], qbe.index, qbe.block)
We want to find polynomials for cosh(ax) and sinh(ax), to combine them into eax.For pedagogical reasons, we work naively and create a polynomial approximation for each of the odd and even polynomials of Pcosh≈eacosh(ax) and Psinh≈easinh(ax).Combining them with LCU gives these results:P(x)≈2eaeax,which is a polynomial bounded by 21.We could choose Pcosh≈cosh(a)cosh(ax) and Psinh≈sinhasinh(ax).
Then, LCU with coefficients [cosh(a)+sinh(a)cosh(a),cosh(a)+sinh(a)sinh(a)] gives us this:P(x)≈eaeax,which is the best we can get, and doesn’t require amplification. We go with the first approach for demonstrating the singular value amplification.Getting rid of this redundant factor 2 can save us a multiplicative factor of O(2T) in the success probability.
We transform the polynomials to QSVT phases using the qsvt_phases function, and plug them into the qsvt_lcu function, which is optimized for implementing a linear combination of two QSVT sequences:
At the climax of the algorithm, we wrap the Magnus evolution in an amplification step.The prefactor of the exponential block-encoding is 2, so we want to approximate the function f(x)=2x in the interval [0,21].We use Classiq’s qsp_approximate to obtain the polynomial approximation. Our built-in routine is inspired by the paper’s approach: it solves a minimax problem to determine the Chebyshev coefficients on a subinterval of [−1,1], while ensuring the resulting polynomial remains bounded on the entire interval.
Lastly, we sequentially apply the block-encodings of each timeslice. To have a quantum variable that is ∣0⟩ when all the block encodings are applied to the state, we use a counter. A further amplitude amplification step is possible using the counter; however, we do not do it here.
import numpy as npclass FullBE(QStruct): time_slice: LongSliceBE counter: QNum[np.ceil(np.log2(NUM_LONG_SLICES + 1))]@qfuncdef long_time_integrator_step(a: CReal, b: CReal, qbe_full: FullBE): long_slice_evolution(a, b, qbe_full.time_slice) # if in block, decrement the counter control( long_slice_predicate(qbe_full.time_slice), lambda: inplace_add(-1, qbe_full.counter), )@qfuncdef long_time_integrator( T: CReal, num_slices: CInt, qbe_full: FullBE # start from time 0): qbe_full.counter ^= num_slices repeat( num_slices, lambda i: long_time_integrator_step( i * T / num_slices, (i + 1) * T / num_slices, qbe_full ), )@qfuncdef main(qbe: Output[FullBE]): allocate(qbe.size, qbe) # initial condition: uniform distribution hadamard_transform(qbe.time_slice.magnus.time_dependent.index) long_time_integrator(END_TIME, NUM_LONG_SLICES, qbe)prefereces = Preferences(optimization_level=0, timeout_seconds=500)execution_preferences = ExecutionPreferences(num_shots=1000000)qmod = create_model( main, preferences=prefereces, execution_preferences=execution_preferences,)qprog = synthesize(qmod)show(qprog)res = execute(qprog).get_sample_result()
Output:
Quantum program link: https://platform.classiq.io/circuit/3CJc4zlCs40jRs9m3cu8msnrDNw
def post_process_res_statevector(result): filtered_samples = [ s for s in result.parsed_state_vector if s.state["qbe"]["counter"] == 0 and np.abs(s.amplitude) > 1e-6 ] global_phase = np.exp(1j * np.angle(filtered_samples[0].amplitude)) amplitudes = np.zeros(2**DIM_SIZE) for sample in filtered_samples: index = sample.state["qbe"]["time_slice"]["magnus"]["time_dependent"]["index"] amplitudes[index] = np.real(sample.amplitude / global_phase) return amplitudesdef post_process_res_samples(result): filtered_samples = [s for s in result.parsed_counts if s.state["qbe"].counter == 0] probs = np.zeros(2**DIM_SIZE) for sample in filtered_samples: index = sample.state["qbe"].time_slice.magnus.time_dependent.index probs[index] += sample.shots / result.num_shots return np.sqrt(probs)
And indeed we can see the the naive amplitudes are order of magnitude smaller than the amplified case (this is exactly what we expect for 4 timesteps).