Skip to Content

Quantum Machine Learning

Overview

The unitarylab_algorithms.quantum_machine_learning package provides variational and generative quantum algorithms suitable for near-term quantum devices (the NISQ era). These algorithms use classical-quantum hybrid optimization loops to train parameterized quantum circuits.

AlgorithmClassTask
VQEVQEAlgorithmGround-state energy estimation
VQCVQCAlgorithmQuantum classifier (Iris dataset)
QAOAQAOAAlgorithmCombinatorial optimization (Max-Cut problem)
QCBMQCBMAlgorithmGenerative modeling of probability distributions (Bars-and-Stripes)
CVQNNCVQNNAlgorithmContinuous-variable quantum neural network classification
Fermi-Hubbard VQEFermiHubbardVQEAlgorithmGround-state solver for the one-dimensional open Fermi-Hubbard model

Return Values and Output

All algorithms return status, circuit_path, plot, and circuit, plus their own result fields. The content of plot varies by algorithm: VQE/VQC/CVQNN each have 1 entry, QAOA has 2, QCBM has 3, and Fermi-Hubbard VQE includes a convergence plot and a parameter .npy file. Optimization quality should be judged by combining result fields such as energy, loss, accuracy, and optimizer status.


Variational Quantum Eigensolver (VQE)

Background

VQE estimates the ground-state energy of a Hermitian Hamiltonian by minimizing the expectation value over a parameterized ansatz state. This algorithm is widely used in quantum chemistry and materials simulation.

The algorithm alternates between:

  1. Evaluating on the quantum circuit
  2. Updating the parameters using COBYLA (scipy.optimize.minimize)

The ansatz is a Ry-Rz ring-entangling circuit: each layer first applies Ry+Rz rotations to every qubit, then entangles adjacent qubits (including the first-last wraparound) with a ring of CNOT gates.

Import

from unitarylab_algorithms import VQEAlgorithm

.run() Parameters

def run(self, n=2, layers=2, max_iter=150, seed=7, hamiltonian=None, normalize=True, backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]
ParameterTypeDefaultDescription
nint2Number of qubits (only effective when hamiltonian=None)
layersint2Number of variational layers in the ansatz
max_iterint150Maximum number of COBYLA iterations
seedint7Random seed for random Hamiltonian generation and initial parameter sampling
hamiltoniannp.ndarray | NoneNoneCustom Hermitian Hamiltonian; when None, a random Hermitian matrix is generated internally
normalizeboolTrueWhether to normalize by the spectral norm when generating a random Hamiltonian (only affects the random-Hamiltonian branch)

_validate_hamiltonian() checks that the matrix is square, has a dimension that is a power of 2, and satisfies ; otherwise it raises ValueError.

Return Value

{ "status": "ok", "circuit_path": "<path to VQE_Circuit.svg.svg>", "plot": [{"format": "svg", "filename": "<path to VQE_Convergence.svg>"}], "circuit": <Circuit object>, "Exact Energy": ..., "VQE Energy": ..., "Absolute Error": ..., "Optimizer Message": "...", "Quantum Comp Time": ..., }

Example

import numpy as np from unitarylab_algorithms import VQEAlgorithm # Use a custom Hamiltonian H = np.array([[1, 0.5], [0.5, -1]]) algo = VQEAlgorithm() result = algo.run(n=2, layers=2, max_iter=150, hamiltonian=H) print(result['status'], result['VQE Energy'], result['Absolute Error'])

Quick Demo

from unitarylab_algorithms.quantum_machine_learning.vqe.algorithm import test test(n=2, layers=2, max_iter=150)

Notes

  • When a custom Hamiltonian is supplied, n is silently overridden: if hamiltonian is not None, the code first validates it and derives the true number of qubits from the matrix dimension, then executes n = num_qubits; the n argument passed by the user is ignored without any error or warning—
  • The normalize parameter is only used when hamiltonian=None (the internal random-generation branch); if a custom Hamiltonian is supplied, normalize has no effect whatsoever.
  • The description field in parameters.json still states “find the ground-state energy of the 2-qubit Ising Hamiltonian H = Z0Z1 - 0.5X0 - 0.5X1,” but the actual default behavior of run() is to generate a random Hermitian matrix (determined by seed), not this fixed Ising Hamiltonian—the parameters.json description is out of date and should not be relied on as a reference for the default behavior.
  • UI default value does not match the Python default value: the default value of layers in parameters.json is 3, while the default in the run() signature is layers=2; submitting an empty form via the Web UI and calling VQEAlgorithm().run() directly will not produce the same number of layers.
  • status is always 'ok' (see the package-level note), so you should use Optimizer Message instead to judge whether COBYLA actually converged.

Variational Quantum Classifier (VQC)

Background

VQC maps classical features into a quantum state via angle encoding, then performs 3-class classification using a trainable ansatz plus Pauli-Z expectation-value observables. This implementation is fixed to the Iris dataset (4 features, 3 classes); gradients are computed parameter-by-parameter via a manually implemented Parameter Shift Rule (not automatic differentiation), and training uses CrossEntropyLoss (applied to expectation-value logits scaled by 10).

Import

from unitarylab_algorithms import VQCAlgorithm

.run() Parameters

def run(self, layers=3, epochs=20, lr=0.05, batch_size=16, backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]
ParameterTypeDefaultDescription
layersint3Number of variational layers
epochsint20Number of training epochs (number of passes over the full training set)
lrfloat0.05Adam optimizer learning rate
batch_sizeint16Mini-batch size

The number of qubits is fixed at 4 (corresponding to the 4 features of the Iris dataset) and is not exposed as a parameter.

Return Value

{ "status": "ok", "circuit_path": "<path to vqc_algorithm_circuit.svg>", "plot": [{"format": "svg", "filename": "<path to VQC_Metrics.svg>"}], "circuit": <Circuit object>, "Final Loss": ..., "Final Accuracy": ..., "Quantal Computation Time (s)": ..., }

Example

from unitarylab_algorithms import VQCAlgorithm algo = VQCAlgorithm() result = algo.run(layers=3, epochs=20, lr=0.05, batch_size=16) print(result['status'], result['Final Accuracy'])

Quick Demo

from unitarylab_algorithms.quantum_machine_learning.vqc.algorithm import test test(layers=3, epochs=20, lr=0.05, batch_size=16)

Notes

  • The output key in the returned dictionary contains a spelling issue: the actual output key is named "Quantal Computation Time (s)" (it should be “Quantum”), and this is the exact spelling as it appears in the source code. When retrieving this field by name, callers must use this exact (misspelled) string, not the semantically “correct” "Quantum Computation Time (s)".
  • The number of qubits is fixed at 4 (determined by the number of features in the Iris dataset) and cannot be adjusted for other datasets via parameters.
  • _load_iris_data() prefers to use sklearn.datasets.load_iris; if scikit-learn is not installed in the environment, it falls back to a fixed copy of the Iris data embedded in the source file (120 training / 30 test samples), so the algorithm can still run even without the scikit-learn dependency—but in the fallback path, the train/test split ratio cannot be customized (the embedded data uses a fixed 80/20 split).
  • The parameter-shift gradient is computed via a nested for loop that performs forward/backward shifted evaluations for each individual component of theta (a total of circuit executions per parameter update), and the computational cost grows linearly as the number of layers and batch size increase.
  • __init__ unconditionally calls torch.manual_seed(42) and np.random.seed(42), and does not expose a seed parameter. Along the normal path of “instantiate, then immediately call run(),” both the initial parameters and the dataset split are reproducible—although train_test_split is not explicitly passed a random_state, it uses the NumPy global random state that was just reset to 42; if other code consumes that global random state after instantiation but before calling run(), the dataset split may still change. Thus the current implementation relies on the global random state—reproducible, but not isolated—and users have no way to adjust the seed externally.
  • The default value of layers in parameters.json is 5, while the default in the run() signature is layers=3—again a case of the UI default and the Python default not matching.
  • status is always 'ok' (see the package-level note).

Quantum Approximate Optimization Algorithm (QAOA)

Background

QAOA is a variational algorithm for combinatorial optimization problems. It alternates between applying a problem Hamiltonian (composed of a term for each edge of the graph) that encodes the objective function, and a mixing layer implemented via rotations, executing layers rounds in total. This implementation solves the Max-Cut problem on a graph defined by an edge list, optimizing via COBYLA.

Import

from unitarylab_algorithms import QAOAAlgorithm

.run() Parameters

def run(self, edges=None, n=6, layers=4, max_iter=100, backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]
ParameterTypeDefaultDescription
edgesList[Tuple[int,int]] | NoneNoneEdge list of the graph; when None, the built-in default graph [(0,1),(1,2),(2,3),(3,0),(0,4),(1,5)] is used
nint6Number of qubits (number of graph vertices)
layersint4Number of QAOA evolution layers
max_iterint100Maximum number of COBYLA iterations

Return Value

{ "status": "ok", "circuit_path": "<path to QAOA_Circuit.svg>", "plot": [ {"format": "svg", "filename": "<path to QAOA_Convergence.svg>"}, {"format": "svg", "filename": "<path to MaxCut_Solution.svg>"}, ], "circuit": <Circuit object>, "Optimal bitstring": "...", "Max-Cut Value": ..., "Optimized Energy": ..., "Quantum Computation Time": ..., }

Example

from unitarylab_algorithms import QAOAAlgorithm edges = [(0, 1), (1, 2), (2, 3), (3, 0), (0, 4), (1, 5)] algo = QAOAAlgorithm() result = algo.run(edges=edges, n=6, layers=4, max_iter=100) print(result['status'], result['Optimal bitstring'], result['Max-Cut Value'])

Quick Demo

from unitarylab_algorithms.quantum_machine_learning.qaoa.algorithm import test test()

Notes

  • QAOAAlgorithm.__init__ unconditionally calls np.random.seed(42) and torch.manual_seed(42), and does not expose a seed parameter; the initial parameters initial_params = np.random.uniform(0, np.pi, 2*layers) depend on this global seed.
  • The plot field contains 2 entries (the convergence curve plot + the Max-Cut result plot); callers iterating over plot should not take only the first element.
  • The module-level test() function itself defaults to max_iter=60, which is inconsistent with the .run() method signature’s default of max_iter=100; while the if __name__ == "__main__": block at the bottom of the file explicitly uses max_iter=100. The three default values are not unified, so check the default actually used based on which entry point you call (.run() or test()).
  • status is always 'ok' (see the package-level note), so quality should be judged by comparing Optimized Energy against the known exact Max-Cut value of the graph.

Quantum Circuit Born Machine (QCBM)

Background

QCBM uses a parameterized quantum circuit as a generative model, training the Born probability distribution to fit a target distribution. The target distribution in this implementation is fixed to be the Bars-and-Stripes (BAS) distribution, computed internally by _get_bas_dist(n) (which derives the rows × cols grid closest to square based on , enumerates binary patterns where all rows are identical or all columns are identical as “valid” states, and assigns them uniform nonzero probability); arbitrary target distributions supplied by the user are not supported. The loss function is the KL divergence (torch.sum(target * log((target+eps)/(curr+eps)))), and gradients are computed via the parameter-shift rule.

Import

from unitarylab_algorithms import QCBMAlgorithm

.run() Parameters

def run(self, n=4, layers=4, epochs=40, lr=0.1, backend='torch', device='cpu', dtype=np.complex128) -> Dict[str, Any]
ParameterTypeDefaultDescription
nint4Number of qubits
layersint4Variational circuit depth
epochsint40Number of training iterations
lrfloat0.1Adam optimizer learning rate

_validate_run_params() explicitly validates that n/layers/epochs are positive integers and lr is positive, raising ValueError otherwise—this is one of the few algorithms in this package that performs genuine input validation at the Python level.

Return Value

{ "status": "ok", "circuit_path": "<path to qcbm_algorithm_circuit.svg>", "plot": [ {"format": "svg", "filename": "<path to QCBM_Loss.svg>"}, {"format": "svg", "filename": "<path to QCBM_Distribution.svg>"}, {"format": "svg", "filename": "<path to QCBM_Samples.svg>"}, ], "circuit": <Circuit object>, "Final KL Loss": ..., "Quantum Computation Time": ..., }

Example

from unitarylab_algorithms import QCBMAlgorithm algo = QCBMAlgorithm() result = algo.run(n=4, layers=4, epochs=40, lr=0.1) print(result['status'], result['Final KL Loss'])

Quick Demo

from unitarylab_algorithms.quantum_machine_learning.qcbm.algorithm import test test(n=4, layers=4, epochs=40, lr=0.1)

Notes

  • The target distribution is fixed to the BAS (Bars-and-Stripes) distribution, generated from n via _get_bas_dist(n); supplying a custom target distribution is not supported.
  • The loss function uses the KL divergence (see the formula above).
  • The plot field contains 3 entries (the loss curve, the distribution comparison plot, and the sample grid plot).
  • QCBMAlgorithm.__init__ unconditionally calls torch.manual_seed(42) / np.random.seed(42), and does not expose a seed parameter.
  • status is always 'ok' (see the package-level note), but this algorithm is one of the few in this package that performs genuine validation of its parameters at the entry point (_validate_run_params)—invalid inputs will directly raise ValueError rather than silently producing an erroneous result.

Continuous-Variable Quantum Neural Network (CVQNN)

Background

CVQNN operates under the continuous-variable (CV) quantum computing paradigm, using optical modes rather than qubits. Within a truncated Fock space (of dimension cutoff), it constructs a variational circuit using displacement, squeezing, beamsplitter, rotation, and Kerr nonlinearity operators (all implemented via torch.matrix_exp), trained for binary classification (MSELoss, with labels mapped to ).

Import

from unitarylab_algorithms import CVQNNAlgorithm

.run() Parameters

def run(self, x_train: np.ndarray, y_train: np.ndarray, n_layers: int = 2, cutoff: int = 6, epochs: int = 40, lr: float = 0.05) -> Dict[str, Any]
ParameterTypeDefaultDescription
x_trainnp.ndarray— (required)Input feature array, shape (N, 2)
y_trainnp.ndarray— (required)Label array (0/1)
n_layersint2Number of variational CV layers
cutoffint6Fock space truncation dimension
epochsint40Number of training epochs
lrfloat0.05Adam optimizer learning rate

Note: unlike the other algorithms in this package, CVQNNAlgorithm.run() has no backend/device/dtype parameters—the CV simulation is entirely implemented by a custom CVSimulator (a dense matrix exponential based on torch.matrix_exp) and does not go through the Circuit.execute() backend dispatch.

Return Value

{ "status": "ok", "circuit_path": "<path to cvqnn_algorithm_circuit.svg>", "plot": [{"format": "svg", "filename": "<path to CVQNN_Metrics.svg>"}], "circuit": <Circuit object>, "Final Loss": ..., "Final Accuracy": ..., "Total Computation Time (s)": ..., }

Example

import numpy as np from unitarylab_algorithms import CVQNNAlgorithm # Generate a simple 2D dataset x_train = np.random.randn(50, 2) y_train = (x_train[:, 0] + x_train[:, 1] > 0).astype(int) algo = CVQNNAlgorithm() result = algo.run(x_train=x_train, y_train=y_train, n_layers=2, cutoff=6, epochs=30) print(result['status'], result['Final Accuracy'])

Quick Demo

from unitarylab_algorithms.quantum_machine_learning.cvqnn.algorithm import test test(n_layers=2, cutoff=6, epochs=30, lr=0.05)

Notes

  • The saved circuit diagram does not reflect the trained gate parameters: the Circuit drawn by save_circuit() comes from _build_circuit(), which builds the topology only using gates such as qc.ry(0.0, ...)/qc.rx(0.0, ...)/qc.rz(0.0, ...) whose placeholder angles are always 0, purely to display the structure. The continuous-variable parameters actually involved in training and inference (sq_r, disp_r, rot_theta, kerr_k, bs_theta) exist in a separate CVClassifier/CVSimulator (based on raw torch tensor operations), and are never written back to the exported circuit diagram. Therefore the SVG corresponding to circuit_path can only convey the circuit structure, not the actual trained parameter values.
  • Increasing cutoff improves approximation accuracy, but the memory footprint (a cutoff × cutoff dense matrix exponential) grows accordingly; going beyond 12 is not recommended on standard hardware (the UI upper limit in parameters.json is also 12).
  • The CV framework models optical quantum systems and is fundamentally different from qubit-based circuits; operators such as x_op and n_op are defined in the truncated Fock basis, and too small a cutoff will introduce truncation error.
  • CVQNNAlgorithm.__init__ unconditionally calls torch.manual_seed(42), np.random.seed(42), and sets torch.set_default_dtype(torch.float64), and does not expose a seed parameter.
  • The module-level test() function itself defaults to epochs=30, inconsistent with the .run() method signature’s default of epochs=40; the dataset inside test() is generated preferentially via sklearn.datasets.make_moons, falling back to a fixed copy embedded in the source code (40 samples) if unavailable.
  • status is always 'ok' (see the package-level note).

Fermi-Hubbard Model VQE (Fermi-Hubbard VQE)

FermiHubbardVQEAlgorithm is located in fermi_hubbard_vqe/algorithm.py.

Background

This algorithm targets the one-dimensional open-boundary Fermi-Hubbard model (L sites, nearest-neighbor hopping coefficient t, on-site interaction strength U, Zeeman magnetic field coefficient B):

  1. Constructs the fermionic Hamiltonian and its Pauli-string expression after Jordan-Wigner transformation via the unitarylab.library.fermi_hubbard.fermi_hubbard_pauli module;
  2. Uses pauli_ground_state() to perform dense exact diagonalization, obtaining the exact ground-state energy as a reference;
  3. Internally reuses VQEAlgorithm (see the “Variational Quantum Eigensolver” section above) to run VQE optimization, obtaining an approximate ground-state energy for this Pauli Hamiltonian;
  4. Optionally measures the total spin magnetic moment on the optimized circuit.

Endianness Adaptation

The qubit numbering convention used by the Pauli expressions generated by fermi_hubbard_pauli is the opposite of the UnitaryLab circuit simulator’s convention, in which the lowest-index qubit (q0) is the least significant bit. Therefore, before handing the Pauli Hamiltonian matrix to VQEAlgorithm, the source code calls an internal _bit_reverse_hamiltonian() to apply a bit-reversal permutation to the matrix’s rows and columns, and performs the following self-checks (raising an exception, rather than silently producing an incorrect result, if any check fails):

  • Round-trip consistency: applying the reversal twice should restore the original matrix, with an error of < 1e-12 (otherwise raises ValueError);
  • Spectral consistency: the eigenvalue spectrum of the matrix before and after reversal should match, with an error of < 1e-10 (otherwise raises ValueError);
  • Environment self-check _check_unitarylab_environment(): on the first call within a process, executes Circuit(2).x(0) and checks that the final state’s peak indeed falls at index 1 (i.e., verifying that the assumption that q0 is the least significant bit actually holds), raising RuntimeError if the bit-ordering convention of the current UnitaryLab version has changed.

Import

from unitarylab_algorithms import FermiHubbardVQEAlgorithm

.run() Parameters

def run(self, params: Dict[str, Any] | str | None = None, *, L: int = 2, t: float = 1.0, U: float = 4.0, B: float = 1.5, layers: int = 5, max_iter: int = 1000, seed: int = 7, measure_shots: int = 10000, backend: str = "torch", device: str = "cpu", dtype = np.complex128) -> Dict[str, Any]
ParameterTypeDefaultDescription
paramsdict | str | NoneNoneOptional: pass the keyword parameters below all at once as a dict or JSON string (see the description below), overriding the corresponding keyword defaults
Lint2Number of one-dimensional sites (open boundary)
tfloat1.0Nearest-neighbor hopping coefficient
Ufloat4.0On-site interaction strength
Bfloat1.5Zeeman magnetic field coefficient
layersint5Number of variational layers in the VQE ansatz
max_iterint1000Maximum number of COBYLA iterations
seedint7Random seed for VQE initial parameters
measure_shotsint10000Number of shots used to measure the total spin magnetic moment on the optimized circuit; set to 0 to skip this measurement stage

Other than params, all remaining parameters are keyword-only (after the * in the signature). If params is supplied, its keys override the corresponding keyword defaults; layers/max_iter/measure_shots also each accept the alias keys vqe_layers/vqe_max_iter/measurement_shots.

Return Value

{ "status": "ok", "circuit_path": "<path to Fermi_Hubbard_VQE_Circuit.svg>", "plot": [ {"format": "svg", "filename": "<path to Fermi_Hubbard_VQE_Convergence.svg>"}, {"format": "npy", "filename": "<path to Fermi_Hubbard_VQE_Parameters.npy>"}, ], "circuit": <Circuit object>, "Exact Energy": ..., "VQE Energy": ..., "Absolute Error": ..., "Circuit Energy": ..., "Number of Qubits": ..., "Optimizer Evaluations": ..., "Optimizer Converged": ..., "Optimizer Message": "...", "VQE Runtime": ..., "Total Runtime": ..., "Qubit Mapping": ..., "Fermionic Hamiltonian": "...", "Pauli Hamiltonian": "...", # Appended only when measure_shots > 0: "Measured Magnetic Moment": ..., "Magnetic Moment Standard Errors": ..., "Measurement Total Shots": ..., }

Example

from unitarylab_algorithms import FermiHubbardVQEAlgorithm algo = FermiHubbardVQEAlgorithm() result = algo.run(L=2, t=1.0, U=4.0, B=1.5, layers=5, max_iter=1000, measure_shots=10000) print(result['status'], result['VQE Energy'], result['Exact Energy'])

The parameters can also be passed as a single dictionary as a whole (equivalent to passing them individually as keywords when params=None):

algo = FermiHubbardVQEAlgorithm() result = algo.run(params={"L": 3, "U": 6.0, "vqe_layers": 6})

Quick Demo

from unitarylab_algorithms.quantum_machine_learning.fermi_hubbard_vqe.algorithm import test test(L=2, t=1.0, U=4.0, B=1.5, layers=5, max_iter=1000, seed=7, measurement_shots=10000)

Notes

  • Best-observed parameter tracking: internally, the source code uses _TrackingVQEAlgorithm (a subclass of VQEAlgorithm) that overrides _expectation(), recording the energy history and the parameter combination with the lowest energy so far (best_energy/best_parameters) on every COBYLA objective-function evaluation, because the final iteration point at which COBYLA converges is not necessarily the historical optimum—the returned VQE Energy/Circuit Energy come from this “historical best,” not from the optimizer’s last iteration.
  • Multiple correctness self-checks, raising an exception on failure: besides the endianness self-check, run_pauli_vqe() also checks: whether the internal VQE call reports status == 'ok' (otherwise RuntimeError); whether zero objective-function evaluations occurred (otherwise RuntimeError); whether best_parameters exists and its energy is finite (otherwise RuntimeError); whether the exact energy reported by VQE matches the spectrum of the Pauli Hamiltonian (error < 1e-10, otherwise ValueError); whether the energy recomputed on the optimized circuit matches the recorded best energy (error < 1e-8, otherwise RuntimeError); and whether the best energy violates the variational lower bound (< exact energy - 1e-8 triggers ValueError, suggesting a possible endianness issue). Therefore, once this algorithm returns normally (without raising an exception), its numerical consistency is better guaranteed than that of the other algorithms in this package; but this also means it is more prone than the other algorithms to directly raising an exception and terminating on abnormal input or environment conditions, rather than returning a result dict with status: 'failed'.
  • params and individual keyword arguments can be mixed, but keys in params always take priority in overriding the corresponding keyword defaults (they will not override other keywords you explicitly passed under a different name than the one in params).
  • The second entry of the plot field is a .npy file (a NumPy array of the optimized VQE parameters), not an image; frontend rendering or download logic needs to branch on the format field, and should not assume that everything in the plot list is a directly displayable image.
  • When measure_shots=0, the magnetic-moment measurement stage is skipped, and the returned dictionary will not include the three measurement-related keys such as Measured Magnetic Moment—callers should check whether these keys exist rather than assuming they always appear.

Version Reference

from unitarylab_algorithms import ( VQEAlgorithm, VQCAlgorithm, QAOAAlgorithm, QCBMAlgorithm, CVQNNAlgorithm, FermiHubbardVQEAlgorithm, )
Last updated on