Skip to main contentIBM Quantum Documentation Preview
This is a preview build of IBM Quantum® documentation. Refer to quantum.cloud.ibm.com/docs for the official documentation.

Model a flowing non-viscous fluid using QUICK-PDE

Note

Qiskit Functions are an experimental feature available only to IBM Quantum® Premium Plan, Flex Plan, and On-Prem (via IBM Quantum Platform API) Plan users. They are in preview release status and subject to change.

Usage estimate: 50 minutes on a Heron r2 processor. (NOTE: This is an estimate only. Your runtime might vary.)

Note that the execution time of this function is generally greater than 20 minutes, so you might want to split this tutorial in two sections: the first one, in which you read through it and launch the jobs, and the second one a few hours later (giving ample time for the jobs to complete), to work with the results of the jobs.


Background

This tutorial seeks to teach on an introductory level how to use the QUICK-PDE function to solve complex multi-physics problems on 156Q Heron R2 QPUs by using ColibriTD's H-DES (Hybrid Differential Equation Solver). The underlying algorithm is described in the H-DES paper. Note that this solver can also solve non-linear equations.

Multi-physics problems - including fluids dynamics, heat diffusion, and material deformation, to name a few - can be ubiquitously described by Partial Differential Equations (PDEs).

Such problems are highly relevant for various industries and constitute an important branch of applied mathematics. However, solving non-linear multivariate coupled PDEs with classical tools remains challenging due to the requirement of an exponentially large amount of resources.

This function is appropriate for equations with increasing complexity and variables, and is the first step to unlock possibilities that were once considered intractable. To fully describe a problem modeled by PDEs, it is necessary to know the initial and boundary conditions. These can strongly change the solution of the PDE and the path to find their solution.

This tutorial teaches you how to:

  1. Define the parameters of the initial condition function.
  2. Adjust the qubit number (used to encode the function of the differential equation), depth, and shot number.
  3. Run QUICK-PDE to solve the underlying differential equation.

Requirements

Before starting this tutorial, be sure you have the following installed:

  • Qiskit SDK v2.0 or later (pip install qiskit)
  • Qiskit Functions Catalog (pip install qiskit-ibm-catalog)
  • Matplotlib (pip install matplotlib)
  • Access to the QUICK-PDE function. Fill out the form to request access.

Setup

Authenticate using your API key and select the function as follows:

import numpy as np
import matplotlib.pyplot as plt
from qiskit_ibm_catalog import QiskitFunctionsCatalog
 
catalog = QiskitFunctionsCatalog(
    channel="ibm_quantum_platform",
    instance="INSTANCE_CRN",
    token="YOUR_API_KEY",  # Use the 44-character API_KEY you created and saved from the IBM Quantum Platform Home dashboard
)
 
quick = catalog.load("colibritd/quick-pde")

Step 1: Set properties of the problem to solve

This tutorial covers the user experience from two perspectives: the physical problem determined by the initial conditions, and the algorithmic component in solving a fluid dynamics example on a quantum computer.

Computational Fluid Dynamics (CFD) has a broad range of applications, and thus it is important to study and solve the underlying PDEs. An important family of PDEs are the Navier-Stokes equations, which are a system of nonlinear partial differential equations describing the motion of fluids. They are highly relevant for scientific problems and engineering applications.

Under certain conditions, the Navier-Stokes equations reduce to Burgers' equation, a convection–diffusion equation describing phenomena occurring in fluid dynamics, gas dynamics, and nonlinear acoustics, to name a few, by modeling dissipative systems.

The one-dimensional version of the equation depends on two variables: tR0t \in \mathbb{R}_{\geq 0} modeling the temporal dimension, xRx \in \mathbb{R} representing the spatial dimension. The general form of the equation is called the viscous Burgers' equation and reads:

ut+uux=ν2u2x,\frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} = \nu \frac{\partial^2 u}{\partial^2 x},

where u(x,t)u(x,t) is the fluid speed field at a given position xx and time tt, and ν\nu is the viscosity of the fluid. Viscosity is an important property of a fluid that measures its rate-dependent resistance to motion or deformation, and thus it plays a crucial role in the determination of the dynamics of a fluid. When the viscosity of the fluid is null (ν\nu = 0), the equation becomes a conservation equation that can develop discontinuities (shock waves), due to the lack of its internal resistance. In this case, the equation is called the inviscid Burgers' equation and is a special case of nonlinear wave equation.

Strictly speaking, inviscid flows do not occur in nature, but when modeling aerodynamic flow, due to the infinitesimal effect of transport, using an inviscid description of the problem can be useful. Surprisingly, more than 70% of aerodynamic theory deals with inviscid flows.

This tutorial uses the inviscid Burgers' equation as a CFD example to solve on IBM® QPUs using QUICK-PDE, given by the equation:

ut+uux=0\frac{\partial u}{\partial t} + u\frac{\partial u}{\partial x} = 0

The initial condition for this problem is set to a linear function: u(t=0,x)=ax+b, with a,bRu(t=0,x) = ax + b,\text{ with }a,b\in\mathbb{R} where aa and bb are arbitrary constants influencing the shape of the solution. You can adjust aa and bb and see how they influence the solving process and the solution.

job = quick.run(
    use_case="cfd",
    physical_parameters={"a": 1.0, "b": 1.0},
)
print(job.result())

Output:

{'functions': {'u': array([[1.        , 0.96112378, 0.9230742 , 0.88616096, 0.85058445,
        0.81644741, 0.78376878, 0.75249908, 0.72253689, 0.69374562,
        0.66597013, 0.63905258, 0.61284684, 0.58723093, 0.56211691,
        0.53745752, 0.51324915, 0.48953036, 0.46637547, 0.44388257,
        0.4221554 , 0.40127848, 0.38128488, 0.36211604, 0.34357308,
        0.32525895, 0.30651089, 0.28632252, 0.26325504, 0.23533692],
       [1.2375    , 1.19267729, 1.14850734, 1.10544526, 1.06382155,
        1.02385326, 0.98565757, 0.94926734, 0.91464784, 0.88171402,
        0.85034771, 0.82041411, 0.79177677, 0.76431068, 0.73791248,
        0.71250742, 0.68805224, 0.66453346, 0.64196021, 0.62035121,
        0.59971506, 0.5800232 , 0.56117499, 0.54295419, 0.52497612,
        0.50662498, 0.48698059, 0.4647339 , 0.43809065, 0.40466247],
       [1.475     , 1.4242308 , 1.37394048, 1.32472956, 1.27705866,
        1.23125911, 1.18754636, 1.1460356 , 1.10675879, 1.06968242,
        1.03472529, 1.00177563, 0.9707067 , 0.94139043, 0.91370806,
        0.88755732, 0.86285533, 0.83953655, 0.81754494, 0.79681986,
        0.77727473, 0.75876792, 0.74106511, 0.72379234, 0.70637915,
        0.687991  , 0.66745028, 0.64314527, 0.61292625, 0.57398802],
       [1.7125    , 1.65578431, 1.59937362, 1.54401386, 1.49029576,
        1.43866495, 1.38943515, 1.34280386, 1.29886974, 1.25765082,
        1.21910288, 1.18313715, 1.14963664, 1.11847019, 1.08950364,
        1.06260722, 1.03765842, 1.01453964, 0.99312968, 0.97328851,
        0.95483439, 0.93751264, 0.92095522, 0.90463049, 0.88778219,
        0.86935702, 0.84791997, 0.82155665, 0.78776186, 0.74331358],
       [1.95      , 1.88733782, 1.82480676, 1.76329816, 1.70353287,
        1.6460708 , 1.59132394, 1.53957212, 1.49098069, 1.44561922,
        1.40348046, 1.36449867, 1.32856657, 1.29554994, 1.26529921,
        1.23765712, 1.21246152, 1.18954273, 1.16871442, 1.14975716,
        1.13239406, 1.11625736, 1.10084533, 1.08546864, 1.06918523,
        1.05072304, 1.02838966, 0.99996803, 0.96259746, 0.91263913]])}, 'samples': {'t': array([0.        , 0.03275862, 0.06551724, 0.09827586, 0.13103448,
       0.1637931 , 0.19655172, 0.22931034, 0.26206897, 0.29482759,
       0.32758621, 0.36034483, 0.39310345, 0.42586207, 0.45862069,
       0.49137931, 0.52413793, 0.55689655, 0.58965517, 0.62241379,
       0.65517241, 0.68793103, 0.72068966, 0.75344828, 0.7862069 ,
       0.81896552, 0.85172414, 0.88448276, 0.91724138, 0.95      ]), 'x': array([0.    , 0.2375, 0.475 , 0.7125, 0.95  ])}}

Step 2 (if needed): Optimize problem for quantum hardware execution

By default, the solver uses physically-informed parameters, which are initial circuit parameters for a given qubit number and depth from which our solver will start.

The shots are also part of the parameters with a default value, since fine-tuning them is important.

Depending on the configuration you're trying to solve, the algorithm's parameters to achieve satisfactory solutions might need to be adapted; for example, it can require more or fewer qubits per variable tt and xx, depending on aa and bb. The following adjusts the number of qubits per function per variable, the depth per function, and the number of shots.

You can also see how to specify the backend and the execution mode.

In addition, physically-informed parameters might steer the optimization process in a wrong direction; in that case, you can disable it by setting the initialization strategy to "RANDOM".

job_2 = quick.run(
    use_case="cfd",
    physical_parameters={"a": 0.5, "b": 0.25},
    nb_qubits={"u": {"t": 2, "x": 1}},
    depth={"u": 3},
    shots=[500, 2500, 5000, 10000],
    initialization="RANDOM",
    backend="ibm_kingston",
    mode="session",
)
print(job_2.result())

Output:

{'functions': {'u': array([[0.25      , 0.24856543, 0.24687708, 0.2449444 , 0.24277686,
        0.24038389, 0.23777496, 0.23495952, 0.23194702, 0.22874691,
        0.22536866, 0.22182171, 0.21811551, 0.21425952, 0.2102632 ,
        0.20613599, 0.20188736, 0.19752675, 0.19306361, 0.18850741,
        0.18386759, 0.1791536 , 0.17437491, 0.16954096, 0.16466122,
        0.15974512, 0.15480213, 0.1498417 , 0.14487328, 0.13990632],
       [0.36875   , 0.36681313, 0.36457201, 0.36203594, 0.35921422,
        0.35611615, 0.35275103, 0.34912817, 0.34525687, 0.34114643,
        0.33680614, 0.33224532, 0.32747327, 0.32249928, 0.31733266,
        0.31198271, 0.30645873, 0.30077002, 0.29492589, 0.28893564,
        0.28280857, 0.27655397, 0.27018116, 0.26369944, 0.2571181 ,
        0.25044645, 0.24369378, 0.23686941, 0.22998264, 0.22304275],
       [0.4875    , 0.48506084, 0.48226695, 0.47912748, 0.47565158,
        0.47184841, 0.46772711, 0.46329683, 0.45856672, 0.45354594,
        0.44824363, 0.44266894, 0.43683103, 0.43073904, 0.42440212,
        0.41782942, 0.4110301 , 0.4040133 , 0.39678818, 0.38936388,
        0.38174955, 0.37395435, 0.36598742, 0.35785791, 0.34957498,
        0.34114777, 0.33258544, 0.32389713, 0.315092  , 0.30617919],
       [0.60625   , 0.60330854, 0.59996188, 0.59621902, 0.59208895,
        0.58758067, 0.58270318, 0.57746549, 0.57187658, 0.56594545,
        0.55968112, 0.55309256, 0.54618879, 0.53897879, 0.53147158,
        0.52367614, 0.51560147, 0.50725658, 0.49865046, 0.48979211,
        0.48069053, 0.47135472, 0.46179367, 0.45201638, 0.44203186,
        0.4318491 , 0.42147709, 0.41092485, 0.40020136, 0.38931562],
       [0.725     , 0.72155625, 0.71765682, 0.71331056, 0.70852631,
        0.70331293, 0.69767926, 0.69163414, 0.68518643, 0.67834497,
        0.6711186 , 0.66351618, 0.65554655, 0.64721855, 0.63854104,
        0.62952285, 0.62017284, 0.61049986, 0.60051274, 0.59022035,
        0.57963151, 0.56875509, 0.55759992, 0.54617486, 0.53448874,
        0.52255042, 0.51036875, 0.49795257, 0.48531072, 0.47245205]])}, 'samples': {'t': array([0.        , 0.03275862, 0.06551724, 0.09827586, 0.13103448,
       0.1637931 , 0.19655172, 0.22931034, 0.26206897, 0.29482759,
       0.32758621, 0.36034483, 0.39310345, 0.42586207, 0.45862069,
       0.49137931, 0.52413793, 0.55689655, 0.58965517, 0.62241379,
       0.65517241, 0.68793103, 0.72068966, 0.75344828, 0.7862069 ,
       0.81896552, 0.85172414, 0.88448276, 0.91724138, 0.95      ]), 'x': array([0.    , 0.2375, 0.475 , 0.7125, 0.95  ])}}

Step 3: Compare the algorithm performances

You can compare the convergence process of our solution (HDES) of job_2 to the performance of a physics-informed neural networks (PINN) algorithm and solver (see the paper and the associated GitHub repository).

In the example of job_2's output (quantum-based approach), only 13 parameters (12 circuit parameters plus 1 scaling parameter) are optimized with the classical solver. The convergence process is as follows:

optimizers:
   CMA: {'ftarget': np.float64(0.1), 'verb_disp': 10, 'maxiter': 100}
   CMA: {'ftarget': np.float64(0.005), 'verb_disp': 10, 'maxiter': 20}
   CMA: {'ftarget': np.float64(0.0025), 'verb_disp': 10, 'maxiter': 30}
   CMA: {'ftarget': np.float64(0.0005), 'verb_disp': 10, 'maxiter': 10}

500 shots
================== CMA =================
option:  {'ftarget': np.float64(0.1), 'verb_disp': 10, 'maxiter': 100}
0/100, loss: 0.02456641

1000 shots
================== CMA =================
option:  {'ftarget': np.float64(0.005), 'verb_disp': 10, 'maxiter': 20}
0/20, loss: 0.03641833
1/20, loss: 0.02461719
2/20, loss: 0.0283689
3/20, loss: 0.009898383
4/20, loss: 0.04454522
5/20, loss: 0.007019971
6/20, loss: 0.00811147
7/20, loss: 0.01592619
8/20, loss: 0.00764708
9/20, loss: 0.01401516
10/20, loss: 0.01767467
11/20, loss: 0.01220387

5000 shots
================== CMA =================
option:  {'ftarget': np.float64(0.0025), 'verb_disp': 10, 'maxiter': 30}
0/30, loss: 0.01024792
1/30, loss: 0.004343748
2/30, loss: 0.01450951
3/30, loss: 0.008591284
4/30, loss: 0.00266414
5/30, loss: 0.007923613
6/30, loss: 0.02023853
7/30, loss: 0.01031438
8/30, loss: 0.009513116
9/30, loss: 0.008132266
10/30, loss: 0.005787766
11/30, loss: 0.00390582

10000 shots
================== CMA =================
option:  {'ftarget': np.float64(0.0005), 'verb_disp': 10, 'maxiter': 10}
0/10, loss: 0.002386168
1/10, loss: 0.004024823
2/10, loss: 0.001311999
3/10, loss: 0.003433991
4/10, loss: 0.002339664
5/10, loss: 0.002978438
6/10, loss: 0.005458391
7/10, loss: 0.002026701
8/10, loss: 0.00207467
9/10, loss: 0.001947627
final_loss: 0.00151994463476429

That means a loss below 0.0015 can be reached after 28 iterations, and with optimizing only a few classical parameters.

Now we can compare the same to the PINN solution with the default configuration suggested by the paper using a gradient-based optimizer. The equivalent of our circuit with 13 parameters to be optimized is the neural network, which requires at least eight layers of 20 neurons, and thus involves optimizing 3021 parameters. Then, the target loss is reached at Step 315, loss: 0.0014988397.

Graph showing PINN data compared with the HDES-Qiskit function.

Now, since we want to do a fair comparison, we should use the same optimizer in both cases. The lowest number of iterations we found for 12 layers of 20 neurons = 4701 parameters:

(10_w,20)-aCMA-ES (mu_w=5.9,w_1=27%) in dimension 4701 (seed=351961)
Iterat #Fevals   function value  axis ratio  sigma  min&max std  t[m:s]
    1     20 5.398521572351456e-02 1.0e+00 9.98e-03  1e-02  1e-02 0:02.3
    2     40 5.444650724530220e-02 1.0e+00 9.97e-03  1e-02  1e-02 0:05.1
    3     60 4.447407275438309e-02 1.0e+00 9.95e-03  1e-02  1e-02 0:08.2
    4     80 2.068969979882240e-02 1.0e+00 9.94e-03  1e-02  1e-02 0:11.7
    6    120 1.028892211616039e-02 1.0e+00 9.91e-03  1e-02  1e-02 0:20.1
    7    140 5.140972323715687e-03 1.0e+00 9.90e-03  1e-02  1e-02 0:25.4
    9    180 3.811701666563749e-03 1.0e+00 9.87e-03  1e-02  1e-02 0:37.4
   10    200 3.189878538250923e-03 1.0e+00 9.85e-03  1e-02  1e-02 0:44.2
   12    240 2.547040116041899e-03 1.0e+00 9.83e-03  1e-02  1e-02 0:59.7
   14    280 2.166548743844032e-03 1.0e+00 9.80e-03  1e-02  1e-02 1:18.0
   15    300 1.783065614290535e-03 1.0e+00 9.79e-03  1e-02  1e-02 1:28.4
   16    320 2.045844215899706e-03 1.0e+00 9.78e-03  1e-02  1e-02 1:39.8
Stopping early: loss 0.001405 <= target 0.0015
CMA-ES finished. Best loss: 0.001404788694344461

You can do the same with your data from job_2, and plot a comparison to the PINN solution.

# check the loss function and compare between the two approaches
print(job_2.logs())

Step 4: Use the result

With your solution, you can now choose what to do with it. The following demonstrates how to plot the result.

solution = job.result()
 
# Plot the solution of the second simulation job_2
_ = plt.figure()
ax = plt.axes(projection="3d")
 
# plot the solution using the 3d plotting capabilities of pyplot
t, x = np.meshgrid(solution["samples"]["t"], solution["samples"]["x"])
ax.plot_surface(
    t,
    x,
    solution["functions"]["u"],
    edgecolor="royalblue",
    lw=0.25,
    rstride=26,
    cstride=26,
    alpha=0.3,
)
ax.scatter(t, x, solution, marker=".")
ax.set(xlabel="t", ylabel="x", zlabel="u(t,x)")
 
plt.show()

Output:

Output of the previous code cell

Notice the difference of initial condition for the second run and its effect on the result:

solution_2 = job_2.result()
 
# Plot the solution of the second simulation job_2
_ = plt.figure()
ax = plt.axes(projection="3d")
 
# plot the solution using the 3d plotting capabilities of pyplot
t, x = np.meshgrid(solution_2["samples"]["t"], solution_2["samples"]["x"])
ax.plot_surface(
    t,
    x,
    solution_2["functions"]["u"],
    edgecolor="royalblue",
    lw=0.25,
    rstride=26,
    cstride=26,
    alpha=0.3,
)
ax.scatter(t, x, solution_2, marker=".")
ax.set(xlabel="t", ylabel="x", zlabel="u(t,x)")
 
plt.show()

Output:

Output of the previous code cell

Tutorial survey

Please take a minute to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience:

Link to survey