FoxesThe Lanchester model is widely used to describe the dynamics of combat between two opposing forces.Originally formulated for military applications, this model can also be adapted to other contexts, such as ecological competition between two species or market competition between two businesses.The model uses linear differential equations to capture the interactions between the populations, making it suitable for scenarios where interaction terms are purely linear. In this notebook, we explore solving this modified Lanchester model using the Harrow-Hassidim-Lloyd (HHL) algorithm, a quantum algorithm designed for efficiently solving linear systems of equations.
Lanchester Model is described in this link.This differential equation describes the behavior of two forces, x and y:dtdx=ax+bxy+cy+ddtdy=ey+fxy+gx+hwhere the coefficients described in the next sections indicate the sensitivities of each side to the other and to itself.
Assuming b=0, f=0:xi+1=dt⋅axi+dt⋅cyi+dt⋅d+xi=(dt⋅a+1)xi+dt⋅cyi+dt⋅dyi+1=dt⋅eyi+dt⋅gxi+dt⋅h+yi=(dt⋅e+1)yi+dt⋅gxi+dt⋅h,where the dot product was added explicitly to emphasize that dt is a variable.The first sample is the initial condition:x0=X0y0=Y0Then, for any next point, we solve numerically using the previous point:−(dt⋅a+1)xi−dt⋅cyi+xi+1=dt⋅d−(dt⋅e+1)yi−dt⋅gxi+yi+1=dt⋅hThe system of linear equations describing the model looks like this:
import numpy as npdef diff_eq_model(a, b, c0, d, e, f, g, h, dt, N, x0, y0): assert b == 0, "model is currently unsupported" assert f == 0, "model is currently unsupported" A = np.identity(2 * N) for r in range(2 * N): if 1 <= r <= N - 1: c = r - 1 A[r][c] = -(dt * a + 1) c = N + r - 1 A[r][c] = -dt * c0 elif N + 1 <= r: c = r - 1 A[r][c] = -(dt * e + 1) c = r - 1- N A[r][c] = -dt * g b1 = np.ones((N, 1)) * dt * d b1[0] = x0 b2 = np.ones((N, 1)) * dt * h b2[0] = y0 b = np.concatenate([b1, b2]) return A, b
We choose specific values for the matrix, representing a predator-prey system. Here, (x) represents the prey population (e.g., rabbits), and (y) represents the predator population (e.g., foxes).The coefficients have the following meanings and specific values:
(a) and (e) define the rate of natural losses or birth (due to death, disease, etc.).
For example, (a=−0.01) (1% natural death rate for rabbits), (e=−0.02) (2 natural death rate for foxes).
(b) and (f) define the rate of losses due to environmental factors (affecting both species).
For example, (b=0), (f=0) (assuming no such environmental exposure for simplicity).
(c) and (g) are losses or gains due to interactions between species (prey hunted by predators and vice versa).
For example, (c=−0.1) (10 loss of rabbits due to predation), (g=0.2) (20 increase in predator population due to hunting prey).
(d) and (h) are gains due to migration.
For example, (d=0.4) (0.4K Rabbits/year migration rate for rabbits), (h=0.01) (0.01K Foxes/year migration rate for foxes).
N = 8 # N time stepsdt = 6 # Sample every dt yearsA, b = diff_eq_model( a=-0.01, b=0, c0=-0.1, d=0.4, e=-0.02, f=0, g=0.02, h=0.01, dt=dt, N=N, x0=30, y0=1)
import matplotlib.pyplot as pltplt.matshow(A)plt.title("Matrix A")plt.show()plt.matshow(b.transpose())plt.title("Vector b")plt.show()
Note that at some critical point, the rabbit population is so low that the fox population also starts to decrease.This is typical predator-prey model behavior.
Complete the matrix dimension to the closest 2n with an identity matrix.The vector b is completed with zeros.(A00I)(b0)=(x0).However, our matrix is already the right size.
If the eigenvalues of A are in the range [wmin,wmax], we can employ transformations to the exponentiated matrix and then undo them to extract the results:A~=(A−wminI)(1−2m1)wmax−wmin1.The eigenvalues of this matrix lie in the interval [0,1), and are related to the eigenvalues of the original matrix viaλ=(wmax+wmin)λ~[1/(1−2m1)]+wmin,with λ~ being an eigenvalue of A~ resulting from the QPE algorithm.This relation between eigenvalues is then used for the expression inserted into the eigenvalue inversion, via the AmplitudeLoading function.
def condition_number(A): w, _ = np.linalg.eig(A) return max(np.abs(w)) / min(np.abs(w))QPE_RESOLUTION_SIZE = 6assert QPE_RESOLUTION_SIZE > np.log2( condition_number(A)), "condition number is too big, and QPE resolution cannot hold all eigenvalues"w, v = np.linalg.eig(A_hermitian)w_max = np.max(w)w_min = np.min(w)mat_shift = -w_minmat_rescaling = (1 - 1 / 2**QPE_RESOLUTION_SIZE) / ( w_max - w_min) # assures eigenvalues in [0,1-1/2^QPE_SIZE]min_possible_w = ( w_max - w_min) / 2**QPE_RESOLUTION_SIZE # this is the minimal eigenvalue which can be resolved by the QPEA_rescaled = ( A_hermitian + mat_shift * np.identity(A_hermitian.shape[0])) * mat_rescaling# verifying that the matrix is symmetric and has eigenvalues in [0,1)if not np.allclose(A_rescaled, A_rescaled.T, rtol=1e-6, atol=1e-6): raise Exception("The matrix is not symmetric")w_rescaled, _ = np.linalg.eig(A_rescaled)for lam in w_rescaled: if lam < -1e-6 or lam >= 1: raise Exception("Eigenvalues are not in (0,1)")plt.matshow(A_rescaled)plt.title("Rescaled Matrix A")plt.show()
We create the circuit with depth limitation compared to the maximal width in a simulator (25).
We also set the preferences for synthesis and execution.
The analysis is explained in the swap test user guide.We compare the measured overlap with the exact overlap using the expected probability of measuring the state ∣0⟩, defined asα2=21(1+∣⟨ψ1∣ψ2⟩∣2).We extract the overlap ∣⟨ψ1∣ψ2⟩∣2=2P(qtest=∣0⟩)−1.The exact overlap is computed with the dot product of the two state vectors.Note that for the sake of this demonstration we execute this circuit 100,000 times to improve the precision of the probability estimate.This is usually not required.Since we are in the HHL context, we filter only the indicator==1 results.
execution_job_id = execute(qprog_hhl_swap)
result = execution_job_id.result_value()fidelity_basic = np.sqrt( result.counts_of_multiple_outputs(["indicator", "test"])[("1", "0")] * 2 / (result.counts_of_output("indicator")["1"]) - 1)print("Fidelity between basic HHL and classical solutions:", fidelity_basic)
Output:
Fidelity between basic HHL and classical solutions: 0.9526849928765404
In the IDE we can also check that among the indicator=1 results, the bar of test=0 is much higher than test=1.
We can also run the state vector of the HHL result and examine it without a swap test.Extracting the full information is exponentially hard, and used here just for educational purposes.Extension of this work can extract some information from the state vector, such as the last element, which is the most important. We can also pad the last solution with an extension, and therefore measure the last point with high probability.
We filter the results, then extract the amplitudes of the state vector and compare them to the classical solution. We filter only the indicator=1 and phase=0 results.
filtered_hhl_statevector = dict()for sample in result.parsed_state_vector: if sample.state["indicator"] == 1 and sample.state["rescaled_eig"] == 0: filtered_hhl_statevector[sample.state["time_index_and_group"]] = ( sample.amplitude )states = sorted(filtered_hhl_statevector)raw_qsol = np.array([filtered_hhl_statevector[s] for s in states])
Let’s compare the raw solution to the Hermitian matrix with the classical one.