Skip to content

Install and first result

Install NWQLib, solve a small linear differential equation, check the answer against an independent reference, and see what the calculation costs. The same workflow applies to the other problems on the home page.

Install

NWQLib requires Python 3.12 or later. Install it from PyPI with the local Aer simulator:

python -m pip install "nwqlib[aer]"

Add an extra for each further capability you need, for example python -m pip install "nwqlib[aer,notebook,chemistry]":

Extra Use it for Packages it adds
aer Running circuits on the local Aer simulator Qiskit ≥ 2.5.2, Qiskit Aer ≥ 0.17.2
qiskit Building Qiskit circuits, passing Qiskit objects as input, exporting circuits, and the Qiskit-based kernels some methods choose Qiskit ≥ 2.5.2
notebook Running the example notebooks Jupyter, ipykernel, nbclient, Matplotlib
chemistry Building molecular Hamiltonians Qiskit, PySCF, OpenFermion
tensor Matrix-product-state (MPS) circuit state preparation Qiskit, plus scikit_tt installed as shown below
qasm Reading an exported OpenQASM 3 file back into a Qiskit circuit (Export OpenQASM) Qiskit, the OpenQASM 3 parser, the Qiskit QASM3 importer
ibm IBM Runtime Qiskit, qiskit-ibm-runtime ≥ 0.49.0
ionq IonQ Qiskit, qiskit-ionq ≥ 1.1.1, requests ≥ 2.34.2, urllib3 ≥ 2.8.0
nexus Quantinuum Nexus H2 Qiskit, qnexus ≥ 0.49.0, pytket ≥ 2.18.1, pytket-qiskit ≥ 0.78.0, selene-core ≥ 0.3.2
nwqec Logical Clifford+T compilation of small circuits (Estimate fault-tolerant resources) Qiskit, nwqec 0.1.2
qre QDK physical resource projection of a compiled circuit (Estimate fault-tolerant resources) qdk[qre] 1.32.3
dev Running the test suite (add qasm for the parser tests) pytest, pytest-xdist, Ruff, jsonschema
docs Building this documentation with MkDocs mkdocs-material ≥ 9.5, mkdocstrings[python] ≥ 0.25

The MPS route of tensor also needs scikit_tt, which is not on PyPI. Install it separately:

python -m pip install "scikit_tt @ git+https://github.com/PGelss/scikit_tt.git"

The stable test environment pins its scikit_tt commit in docs/ENVIRONMENT_LOCK.txt. To work on NWQLib itself, install an editable checkout as described in Set up, test and build.

Solve linear dynamics

This solves du/dt = -A u, starting from u(0) = (1, 0), at time 0.1:

from nwqlib import LinearDynamics, solve
from nwqlib.algorithms import LCHS

problem = LinearDynamics(
    A=[[0.4, 0.15], [0.05, 0.25]],
    initial_state=[1.0, 0.0],
    time=0.1,
)
result = solve(problem, method=LCHS())
print(result.solution)
[ 0.96006038-1.54102084e-12j -0.00513885+3.61167323e-13j]

The result is the physical solution vector u(0.1), including its scale and phase. It is not normalized to unit length.

LCHS, the linear combination of Hamiltonian simulation of An, Childs and Lin (ACL, arXiv:2312.03916v2, Eq. (6)), writes exp(-tA) for a matrix A with positive semidefinite Hermitian part as an integral over a kernel variable k of unitary evolutions, and approximates the integral by a quadrature sum. NWQLib shifts an A whose Hermitian part is not positive semidefinite and restores the resulting growth. Each quadrature node is one branch of that sum. SELECT is the circuit block that applies the branch whose index an address register holds (ACL Appendix A.3, Lemma 24, Eq. (178)). The default Aer circuit uses one system qubit and eight coefficient ancillas, which hold the address. It has 204 physical branches, padded to 256 address slots. The LCHS guide describes the construction.

Check the answer

Compute the same solution with SciPy's matrix exponential and compare:

import numpy as np
from scipy.linalg import expm

A = np.array([[0.4, 0.15], [0.05, 0.25]])
reference = expm(-0.1 * A) @ np.array([1.0, 0.0])
print(reference)
print(np.linalg.norm(result.solution - reference))
[ 0.96082565 -0.00484022]
0.0008214720329548587

The printed absolute L2 discrepancy, about .000821472, is that of the default finite LCHS approximation in this example. LCHS gives half of LCHS.approximation_tolerance, 0.01 by default, to the tail of the k integral that the cutoff drops and half to the k quadrature. Here the tail bound is .005 and the quadrature bound is about .00431140. These two component bounds do not bound the total physical-output error, which also depends on the preparation, evolution and numerical approximations. The LCHS guide describes these bounds, and Check accuracy and verify a result describes NWQLib's own checks.

Change the approximation

A smaller approximation_tolerance uses more quadrature nodes. Evaluate the finer finite sum classically:

refined = solve(
    problem,
    method=LCHS(approximation_tolerance=0.001),
    execution="classical",
)
print(refined.solution)
print(np.linalg.norm(refined.solution - reference))
[ 0.96084571+1.45318296e-17j -0.00488956-7.01766797e-19j]
5.3260455987202175e-05

For this input, the finer construction uses 396 nodes and has absolute L2 discrepancy about .0000532605. Its dense quantum circuit would need 512 address slots and ten total qubits, which exceeds the default max_dense_select_slots=256, so this comparison evaluates the finite sum classically. These two observations do not establish monotonic convergence for every input. Each branch of the dense SELECT is a classically computed matrix exponential. Larger systems need a supported structured construction with its own limit check.

See the cost before running

plan chooses the construction for this input and returns it, with its costs, as a Plan before any circuit exists. estimate reads the Plan and runs no circuit:

from nwqlib import estimate, plan

lchs_plan = plan(problem, method=LCHS())
resources = estimate(lchs_plan)
print(resources.quantity("logical_width", location="logical_device"))
operations = resources.quantity("operations")
print(operations.interpretation)
logical_width at logical_device: 9 count [exact]
  basis=selected_logical; lifecycle=planned; population=simultaneous live footprint
unavailable

Each quantity prints its value, its unit and a label (exact, upper_bound, estimate, conditional or unavailable), and on the second line what it counts. Here the circuit holds 9 qubits at the same time, an exact count. The operation count is unavailable because the blocks of this construction have no gate-count formula. An unavailable count carries its reason and is never treated as zero.

Planning computes the numerical data the Method needs and takes no measurements. Run the Plan with solve(lchs_plan), or pass it to prepare and submit to control preparation and submission yourself (Run on a backend). A Result keeps its Plan as result.plan. Estimate resources explains how counts from formulas differ from counts of a built circuit and from device predictions.

Save and load the result

Pass a path that does not exist yet. save creates that directory with the result's metadata and data files, and it raises FileExistsError when the path already exists, so a saved result is never overwritten:

from nwqlib import load_result

result.save("linear-dynamics-result")
restored = load_result("linear-dynamics-result")
print(restored.solution)
[ 0.96006038-1.54102084e-12j -0.00513885+3.61167323e-13j]

Next steps

  • Examples: the linear dynamics notebook (examples/lchs_linear_dynamics_intro.ipynb) applies LCHS to advection-diffusion. It separates product-formula error from quadrature error and compares kernels, quadratures, and exact and MPS state preparation by their errors and success probabilities, with compiled gate counts for the isolated state loaders.
  • Plan, compare and solve compares Lanczos and FixedGCIM for the same Eigenproblem.
  • Supply inputs covers dense, sparse and structured inputs.
  • Run on a backend and Continue an interrupted run cover pending work and continuation.
  • Save, load and reanalyze results covers saved results and reanalysis.
  • Use the command line lists methods and their parameters and inspects saved reports.
  • How NWQLib works explains Problem, Method, Plan, Run and Result.