WNTR-Quantum
  • Home
  • Installation
  • QNR Simulation
  • QUBO Simulation
  • QUBO Network design
  • Previous
  • Next
  • QNR Simulation
    • Set up water network model
    • Solve model classically using EpanetSimulator
    • Solve model using the classical Cholesky solver from QuantumEpanetSimulator
    • Check that the classical results are equivalent
    • Solve water network with QuantumEpanetSimulator and VQLS

QNR Simulation¶

We illustrate here how to use the Variational Quantum Linear Solver as part of the Newton Raphson to simulate the water pressure and flow of a small water network using wntr-quantum.

Set up water network model¶

In this example, we test our quantum solvers into a slightly larger network as contained in Net2Loops.inp. Let's start by setting up the model:

In [1]:
Copied!
import os
from pathlib import Path
import wntr
import wntr_quantum

# Define the directory path
epanet_tmp_dir = Path("/Users/murilo/scratch_dir/.epanet_quantum")

# check if the directory exists
if not epanet_tmp_dir.exists():
    epanet_tmp_dir.mkdir(parents=True, exist_ok=True)

# set the environment variables
os.environ["EPANET_TMP"] = str(epanet_tmp_dir)
os.environ["EPANET_QUANTUM"] = "/Users/murilo/Documents/NLeSC_Projects/Vitens/EPANET"

# set up network model
inp_file = "networks/Net2Loops_modified.inp"  # reproduces the values in https://doi.org/10.3390/w14060851
wn = wntr.network.WaterNetworkModel(inp_file)

# plot network
wntr.graphics.plot_network(wn, title=wn.name, node_labels=True)

# explicitly set Hazen-Williams head loss formulas and pressure dependent demand mode
wn.options.hydraulic.headloss = "H-W"  # 'D-W'
wn.options.hydraulic.demand_model = "PDD"  # 'PDD'

# required pressure: the pressure above which the consumer should receive the desired demand
wn.options.hydraulic.required_pressure = 30.0  # m
# minimum pressure: this is the pressure below which the consumer cannot receive any water
wn.options.hydraulic.minimum_pressure = 0.0  # m

# set time duration of the simulation (in seconds)
wn.options.time.duration = 0

# print options
dict(wn.options.hydraulic)
dict(wn.options.time)
import os from pathlib import Path import wntr import wntr_quantum # Define the directory path epanet_tmp_dir = Path("/Users/murilo/scratch_dir/.epanet_quantum") # check if the directory exists if not epanet_tmp_dir.exists(): epanet_tmp_dir.mkdir(parents=True, exist_ok=True) # set the environment variables os.environ["EPANET_TMP"] = str(epanet_tmp_dir) os.environ["EPANET_QUANTUM"] = "/Users/murilo/Documents/NLeSC_Projects/Vitens/EPANET" # set up network model inp_file = "networks/Net2Loops_modified.inp" # reproduces the values in https://doi.org/10.3390/w14060851 wn = wntr.network.WaterNetworkModel(inp_file) # plot network wntr.graphics.plot_network(wn, title=wn.name, node_labels=True) # explicitly set Hazen-Williams head loss formulas and pressure dependent demand mode wn.options.hydraulic.headloss = "H-W" # 'D-W' wn.options.hydraulic.demand_model = "PDD" # 'PDD' # required pressure: the pressure above which the consumer should receive the desired demand wn.options.hydraulic.required_pressure = 30.0 # m # minimum pressure: this is the pressure below which the consumer cannot receive any water wn.options.hydraulic.minimum_pressure = 0.0 # m # set time duration of the simulation (in seconds) wn.options.time.duration = 0 # print options dict(wn.options.hydraulic) dict(wn.options.time)
No description has been provided for this image
Out[1]:
{'duration': 0.0,
 'hydraulic_timestep': 3600,
 'quality_timestep': 300,
 'rule_timestep': 360,
 'pattern_timestep': 7200,
 'pattern_start': 0.0,
 'report_timestep': 3600,
 'report_start': 0.0,
 'start_clocktime': 0.0,
 'statistic': 'NONE',
 'pattern_interpolation': False}

Solve model classically using EpanetSimulator¶

In [2]:
Copied!
# define classical solver using EpanetSimulator
sim = wntr.sim.EpanetSimulator(wn)

# run the classical EPANET simulation
results_original_epanet = sim.run_sim()

# plot network
wntr.graphics.plot_network(
    wn,
    node_attribute=results_original_epanet.node["pressure"].iloc[0],
    link_attribute=results_original_epanet.link["flowrate"].iloc[0],
    node_colorbar_label="Pressure (m)",
    link_colorbar_label="Flow (liters/sec)",
    node_size=100,
    node_labels=True,
    link_labels=True,
    node_alpha=0.5,
)

results_original_epanet.node["pressure"], results_original_epanet.link["velocity"]
# define classical solver using EpanetSimulator sim = wntr.sim.EpanetSimulator(wn) # run the classical EPANET simulation results_original_epanet = sim.run_sim() # plot network wntr.graphics.plot_network( wn, node_attribute=results_original_epanet.node["pressure"].iloc[0], link_attribute=results_original_epanet.link["flowrate"].iloc[0], node_colorbar_label="Pressure (m)", link_colorbar_label="Flow (liters/sec)", node_size=100, node_labels=True, link_labels=True, node_alpha=0.5, ) results_original_epanet.node["pressure"], results_original_epanet.link["velocity"]
No description has been provided for this image
Out[2]:
(name         2          3         4          5         6          7  \
 0     53.24765  30.466589  43.45023  33.808372  30.44595  30.553503   
 
 name             1  
 0     4.394531e-07  ,
 name         1         2         3         4         5        6         7  \
 0     1.894879  1.846526  1.462826  1.115641  1.136156  1.09944  1.298481   
 
 name         8  
 0     0.315217  )

Solve model using the classical Cholesky solver from QuantumEpanetSimulator¶

We now solve the same problem using the classical Epanet simulator. Note that, by default, QuantumEpanetSimulator uses a classical CholeskySolver by default to iteratively solve the linear problem.

In [3]:
Copied!
# define classical solver using QuantumEpanetSimulator
sim = wntr_quantum.sim.QuantumEpanetSimulator(wn)

# run the EPANET quantum simulation using classical Cholesky
results_epanet = sim.run_sim()

# remember to set up EPANET Quantum environment variables!
epanet_path = os.environ["EPANET_QUANTUM"]
epanet_tmp = os.environ["EPANET_TMP"]

# check paths
print(f"Your EPANET quantum path: {epanet_path}")
print(f"Your EPANET temp dir: {epanet_tmp}\n")

# load EPANET A and b matrices from temp
epanet_A, epanet_b = wntr_quantum.sim.epanet.load_epanet_matrix()

# set the size of the Jacobian (A matrix)
epanet_A_dim = epanet_A.todense().shape[0]
print(f"Size of the Jacobian in EPANET simulator: {epanet_A_dim}")
print(f"Size of the b vector in EPANET simulator: {epanet_b.shape[0]}")

# save number of nodes and pipes
n_nodes = (len(results_epanet.node["pressure"].iloc[0]),)
n_pipes = len(results_epanet.link["flowrate"].iloc[0])

results_epanet.node["pressure"], results_epanet.link["velocity"]
# define classical solver using QuantumEpanetSimulator sim = wntr_quantum.sim.QuantumEpanetSimulator(wn) # run the EPANET quantum simulation using classical Cholesky results_epanet = sim.run_sim() # remember to set up EPANET Quantum environment variables! epanet_path = os.environ["EPANET_QUANTUM"] epanet_tmp = os.environ["EPANET_TMP"] # check paths print(f"Your EPANET quantum path: {epanet_path}") print(f"Your EPANET temp dir: {epanet_tmp}\n") # load EPANET A and b matrices from temp epanet_A, epanet_b = wntr_quantum.sim.epanet.load_epanet_matrix() # set the size of the Jacobian (A matrix) epanet_A_dim = epanet_A.todense().shape[0] print(f"Size of the Jacobian in EPANET simulator: {epanet_A_dim}") print(f"Size of the b vector in EPANET simulator: {epanet_b.shape[0]}") # save number of nodes and pipes n_nodes = (len(results_epanet.node["pressure"].iloc[0]),) n_pipes = len(results_epanet.link["flowrate"].iloc[0]) results_epanet.node["pressure"], results_epanet.link["velocity"]
Your EPANET quantum path: /Users/murilo/Documents/NLeSC_Projects/Vitens/EPANET
Your EPANET temp dir: /Users/murilo/scratch_dir/.epanet_quantum

Size of the Jacobian in EPANET simulator: 6
Size of the b vector in EPANET simulator: 6
Out[3]:
(name          2          3          4          5         6          7  \
 0     53.247612  30.466719  43.450195  33.808483  30.44593  30.553486   
 
 name             1  
 0     4.394531e-07  ,
 name         1         2         3         4         5         6         7  \
 0     1.894883  1.846514  1.462829  1.115635  1.136152  1.099438  1.298483   
 
 name         8  
 0     0.315222  )

Check that the classical results are equivalent¶

In [4]:
Copied!
import pandas.testing as pdt

pdt.assert_frame_equal(
    results_original_epanet.node["pressure"], results_epanet.node["pressure"]
)
pdt.assert_frame_equal(
    results_original_epanet.link["flowrate"], results_epanet.link["flowrate"]
)
# pdt.assert_frame_equal(results_original_epanet.link["velocity"], results_epanet.link["velocity"])
import pandas.testing as pdt pdt.assert_frame_equal( results_original_epanet.node["pressure"], results_epanet.node["pressure"] ) pdt.assert_frame_equal( results_original_epanet.link["flowrate"], results_epanet.link["flowrate"] ) # pdt.assert_frame_equal(results_original_epanet.link["velocity"], results_epanet.link["velocity"])

Solve water network with QuantumEpanetSimulator and VQLS¶

We now solve the model using VQLS. In this example, we are preconditioning the initial linear system using diagonal scaling and also using a mix of two classical optimizers.

In [ ]:
Copied!
import numpy as np

from qiskit.circuit.library import RealAmplitudes
from qiskit.primitives import Estimator
from qiskit_algorithms import optimizers as opt
from quantum_newton_raphson.vqls_solver import VQLS_SOLVER
from utils import compare_results

n_qubits = int(np.ceil(np.log2(epanet_A_dim)))

qc = RealAmplitudes(n_qubits, reps=3, entanglement="full")
estimator = Estimator()

linear_solver = VQLS_SOLVER(
    estimator=estimator,
    ansatz=qc,
    optimizer=[opt.COBYLA(maxiter=1000, disp=True), opt.CG(maxiter=500, disp=True)],
    matrix_decomposition="symmetric",
    verbose=True,
    preconditioner="diagonal_scaling",
    reorder=True,
)

sim = wntr_quantum.sim.QuantumEpanetSimulator(wn, linear_solver=linear_solver)
results_vqls = sim.run_sim(linear_solver=linear_solver)

results_classical, results_quantum = compare_results(results_epanet, results_vqls)

results_vqls.node["pressure"], results_vqls.link["velocity"]
import numpy as np from qiskit.circuit.library import RealAmplitudes from qiskit.primitives import Estimator from qiskit_algorithms import optimizers as opt from quantum_newton_raphson.vqls_solver import VQLS_SOLVER from utils import compare_results n_qubits = int(np.ceil(np.log2(epanet_A_dim))) qc = RealAmplitudes(n_qubits, reps=3, entanglement="full") estimator = Estimator() linear_solver = VQLS_SOLVER( estimator=estimator, ansatz=qc, optimizer=[opt.COBYLA(maxiter=1000, disp=True), opt.CG(maxiter=500, disp=True)], matrix_decomposition="symmetric", verbose=True, preconditioner="diagonal_scaling", reorder=True, ) sim = wntr_quantum.sim.QuantumEpanetSimulator(wn, linear_solver=linear_solver) results_vqls = sim.run_sim(linear_solver=linear_solver) results_classical, results_quantum = compare_results(results_epanet, results_vqls) results_vqls.node["pressure"], results_vqls.link["velocity"]
VQLS Iteration 1000 Cost 6.643e-03
   Return from subroutine COBYLA because the MAXFUN limit has been reached.

   NFVALS = 1000   F = 6.643069E-03    MAXCV = 0.000000E+00
   X =-6.003301E-01  -1.100831E+00  -1.251520E+00  -1.103777E+00   1.332641E-01
       2.956498E+00   8.709108E-01   1.550173E+00   7.461350E-01   2.731111E-01
      -2.069924E+00   1.162553E+00
Optimization terminated successfully.
         Current function value: 0.000000
         Iterations: 74
         Function evaluations: 1599
         Gradient evaluations: 123
VQLS Iteration 1000 Cost 1.396e-02
   Return from subroutine COBYLA because the MAXFUN limit has been reached.

   NFVALS = 1000   F = 1.395057E-02    MAXCV = 0.000000E+00
   X = 4.137658E-01  -1.559580E+00   7.397351E-01   1.718300E+00   2.247472E+00
      -2.605908E+00  -5.912157E-01  -1.401045E+00   2.133813E+00   4.532001E+00
       1.376001E+00   2.235611E-01
Optimization terminated successfully.
         Current function value: 0.000000
         Iterations: 60
         Function evaluations: 1456
         Gradient evaluations: 112
VQLS Iteration 1000 Cost 2.472e-03
   Return from subroutine COBYLA because the MAXFUN limit has been reached.

   NFVALS = 1000   F = 2.472077E-03    MAXCV = 0.000000E+00
   X = 3.857522E+00  -1.265118E+00   4.256407E+00  -1.239055E+00  -1.682544E+00
      -2.869380E+00  -4.414322E-01   4.111229E+00   3.386487E+00  -3.059907E+00
       2.906477E+00   2.793646E+00
Optimization terminated successfully.
         Current function value: 0.000000
         Iterations: 51
         Function evaluations: 1196
         Gradient evaluations: 92
Quantum result 0.31111666560173035 within 0.008334552381341634% of classical result 0.31109073758125305
Quantum result 0.09354956448078156 within 0.016220675077726884% of classical result 0.09356474131345749
Quantum result 0.189796581864357 within 0.021948702387887445% of classical result 0.1897549331188202
Quantum result 0.009042594581842422 within 0.02503129426341868% of classical result 0.009044858627021313
Quantum result 0.14740082621574402 within 0.014701052975458082% of classical result 0.14737915992736816
Quantum result 0.05571400374174118 within 0.007790323802495752% of classical result 0.05570966377854347
Quantum result 0.06578942388296127 within 0.009217611617040038% of classical result 0.06579548865556717
Quantum result -0.00015969578817021102 within 0.019068349538275086% of classical result -0.000159726245328784
Quantum result 53.2465705871582 within 0.001955791657896612% of classical result 53.24761199951172
Quantum result 30.46950912475586 within 0.00915901406938471% of classical result 30.466718673706055
Quantum result 43.44719696044922 within 0.006900664149416603% of classical result 43.4501953125
Quantum result 33.812408447265625 within 0.011610469100186043% of classical result 33.8084831237793
Quantum result 30.44211769104004 within 0.012523151228295821% of classical result 30.44593048095703
Quantum result 30.54896354675293 within 0.014801334379934725% of classical result 30.553485870361328
Quantum result 4.39453117451194e-07 within 0.0% of classical result 4.39453117451194e-07
Out[ ]:
(name          2          3          4          5          6          7  \
 0     53.246571  30.469509  43.447197  33.812408  30.442118  30.548964   
 
 name             1  
 0     4.394531e-07  ,
 name         1         2        3         4         5         6         7  \
 0     1.895041  1.846214  1.46315  1.115356  1.136319  1.099524  1.298364   
 
 name         8  
 0     0.315162  )
In [7]:
Copied!
import matplotlib.pyplot as plt

for i, result in enumerate(results_vqls.linear_solver_results):
    plt.semilogy(result.logger.values, label=f"iter # {i}")
    plt.legend()
import matplotlib.pyplot as plt for i, result in enumerate(results_vqls.linear_solver_results): plt.semilogy(result.logger.values, label=f"iter # {i}") plt.legend()
No description has been provided for this image

Plot Network model with absolute percent errors¶

To compare the results, we plot the network together with the absolute percent errors (with respect to the classical results) for the predicted pressures and flow rates.

In [ ]:
Copied!
from utils import get_ape_from_pd_series

wntr.graphics.plot_network(
    wn,
    node_attribute=get_ape_from_pd_series(
        results_vqls.node["pressure"].iloc[0], results_epanet.node["pressure"].iloc[0]
    ),
    link_attribute=get_ape_from_pd_series(
        results_vqls.link["flowrate"].iloc[0],
        results_epanet.link["flowrate"].iloc[0],
    ),
    node_colorbar_label="Pressure %",
    link_colorbar_label="Flows %",
    node_size=150,
    title=f"{inp_file}: Absolute Percent Error",
    node_labels=True,
    link_labels=True,
    node_alpha=0.5,
)
from utils import get_ape_from_pd_series wntr.graphics.plot_network( wn, node_attribute=get_ape_from_pd_series( results_vqls.node["pressure"].iloc[0], results_epanet.node["pressure"].iloc[0] ), link_attribute=get_ape_from_pd_series( results_vqls.link["flowrate"].iloc[0], results_epanet.link["flowrate"].iloc[0], ), node_colorbar_label="Pressure %", link_colorbar_label="Flows %", node_size=150, title=f"{inp_file}: Absolute Percent Error", node_labels=True, link_labels=True, node_alpha=0.5, )
No description has been provided for this image
Out[ ]:
<Axes: title={'center': 'networks/Net2Loops_modified.inp: Absolute Percent Error'}>

Plot pressures and flow rates¶

Let's check graphically the equivalence of the results.

In [9]:
Copied!
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes

fig, ax = plt.subplots()
ax.scatter(
    results_classical[:n_pipes],
    results_quantum[:n_pipes],
    label="Flow rates",
    color="blue",
    marker="o",
)
ax.scatter(
    results_classical[n_pipes:],
    results_quantum[n_pipes:],
    label="Pressures",
    color="red",
    marker="s",
    facecolors="none",
)
ax.axline((0, 0), slope=1, linestyle="--", color="gray", label="")
ax.set_xlabel("Classical results")
ax.set_ylabel("Quantum results")
ax.legend()

# inset plot
ax_inset = inset_axes(ax, width="35%", height="35%", loc="lower right")
ax_inset.scatter(
    results_classical[:n_pipes], results_quantum[:n_pipes], color="blue", marker="o"
)
ax_inset.scatter(
    results_classical[n_pipes:],
    results_quantum[n_pipes:],
    color="red",
    marker="s",
    facecolors="none",
)
ax_inset.axline((0, 0), slope=1, linestyle="--", color="gray")
ax_inset.set_xlim(-0.25, 0.5)
ax_inset.set_ylim(-0.25, 0.5)
ax_inset.set_xticks([-0.25, 0, 0.5])
ax_inset.set_yticks([-0.25, 0, 0.5])
# plt.legend()
plt.show()

results_classical, results_quantum
import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1.inset_locator import inset_axes fig, ax = plt.subplots() ax.scatter( results_classical[:n_pipes], results_quantum[:n_pipes], label="Flow rates", color="blue", marker="o", ) ax.scatter( results_classical[n_pipes:], results_quantum[n_pipes:], label="Pressures", color="red", marker="s", facecolors="none", ) ax.axline((0, 0), slope=1, linestyle="--", color="gray", label="") ax.set_xlabel("Classical results") ax.set_ylabel("Quantum results") ax.legend() # inset plot ax_inset = inset_axes(ax, width="35%", height="35%", loc="lower right") ax_inset.scatter( results_classical[:n_pipes], results_quantum[:n_pipes], color="blue", marker="o" ) ax_inset.scatter( results_classical[n_pipes:], results_quantum[n_pipes:], color="red", marker="s", facecolors="none", ) ax_inset.axline((0, 0), slope=1, linestyle="--", color="gray") ax_inset.set_xlim(-0.25, 0.5) ax_inset.set_ylim(-0.25, 0.5) ax_inset.set_xticks([-0.25, 0, 0.5]) ax_inset.set_yticks([-0.25, 0, 0.5]) # plt.legend() plt.show() results_classical, results_quantum
No description has been provided for this image
Out[9]:
([0.31109074,
  0.09356474,
  0.18975493,
  0.009044859,
  0.14737916,
  0.055709664,
  0.06579549,
  -0.00015972625,
  53.247612,
  30.466719,
  43.450195,
  33.808483,
  30.44593,
  30.553486,
  4.3945312e-07],
 [0.31111667,
  0.093549564,
  0.18979658,
  0.009042595,
  0.14740083,
  0.055714004,
  0.065789424,
  -0.00015969579,
  53.24657,
  30.46951,
  43.447197,
  33.81241,
  30.442118,
  30.548964,
  4.3945312e-07])

Documentation built with MkDocs.

Keyboard Shortcuts

Keys Action
? Open this help
n Next page
p Previous page
s Search