diff --git a/.pylintdict b/.pylintdict index d1d3d6af9..d46596d97 100644 --- a/.pylintdict +++ b/.pylintdict @@ -55,6 +55,7 @@ borujeni boyer brassard broyden +brisbane callables cambridge cancelled @@ -66,6 +67,8 @@ centroid chernoff choi chuang +cincio +cîrstoiu clbit clbits clopper @@ -166,14 +169,18 @@ fae failsafe farhi farrokh +fermionic fi fidelities fidelity fidelityquantumkernel filippo +filip +fitzpatrick fletcher fm fmin +fock formatter fourier frac @@ -221,6 +228,7 @@ hermitian hessians hilbert hoc +holmes homebrew hopkins hoyer @@ -230,6 +238,7 @@ hubbard hyperparameter hyperparameters hyperplanes +ibm idx im imag @@ -242,6 +251,7 @@ instantiations interatomic interdependencies ints +iosue iprint iqft isaac @@ -255,12 +265,15 @@ izaac izz jac jacobian +jhp johnson jm jonathan jones +jordan july jupyter +jw kandala kernelized killoran @@ -284,6 +297,7 @@ leq lin linalg loglik +longterm loglikelihood lov lr @@ -329,6 +343,7 @@ msg multiclass multinomial multioutput +muñoz mxd mypy nabla @@ -358,6 +373,7 @@ nones nonlocal nosignatures np +npj ns num numpy @@ -401,6 +417,7 @@ pedro pegasos peruzzo pixelated +pj platt polyfit postprocess @@ -455,6 +472,7 @@ quantile quantumcircuit qubit qubits +ramo rangle raymond rbf @@ -501,11 +519,13 @@ scipy sdg seealso semidefinite +sep serializable serializablemodelmixin shalev shanno shende +shortterm shwartz sigmoid sima @@ -516,6 +536,7 @@ slsqp sobol softmax soloviev +sornborger spall sparsearray spedalieri @@ -582,6 +603,7 @@ transpiling trotterization trotterized tunable +uncertainities uncompiled uncompress uncompute @@ -590,6 +612,7 @@ univariate uno unscaled unsymmetric +usecase utf utils varadarajan @@ -598,6 +621,7 @@ vatan vec vectorized veeravalli +vff vicente vicentini vigo @@ -611,6 +635,7 @@ vx vy vz wavefunction +wigner wikipedia wilhelm williams diff --git a/qiskit_machine_learning/datasets/__init__.py b/qiskit_machine_learning/datasets/__init__.py index 2e198db3f..3e45e9366 100644 --- a/qiskit_machine_learning/datasets/__init__.py +++ b/qiskit_machine_learning/datasets/__init__.py @@ -1,6 +1,6 @@ # This code is part of a Qiskit project. # -# (C) Copyright IBM 2019, 2024. +# (C) Copyright IBM 2019, 2025. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -14,23 +14,24 @@ Datasets (:mod:`qiskit_machine_learning.datasets`) ================================================== -A set of sample datasets to test machine learning algorithms. +A collection of synthetic datasets used to test and benchmark machine-learning +algorithms implemented in Qiskit Machine Learning. .. currentmodule:: qiskit_machine_learning.datasets -Datasets --------- +Synthetic dataset generators +---------------------------- .. autosummary:: :toctree: ../stubs/ :nosignatures: ad_hoc_data + h_molecule_evolution_data """ from .ad_hoc import ad_hoc_data +from .h_molecule_evolution import h_molecule_evolution_data -__all__ = [ - "ad_hoc_data", -] +__all__ = ["ad_hoc_data", "h_molecule_evolution_data"] diff --git a/qiskit_machine_learning/datasets/h_molecule_evolution.py b/qiskit_machine_learning/datasets/h_molecule_evolution.py new file mode 100644 index 000000000..e2778c37f --- /dev/null +++ b/qiskit_machine_learning/datasets/h_molecule_evolution.py @@ -0,0 +1,352 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +H Molecule Evolution +""" + +from __future__ import annotations + +import warnings +import os +import pickle as pkl + +import numpy as np + +from qiskit import QuantumCircuit, transpile +from qiskit.circuit import Parameter +from qiskit.quantum_info import Statevector +from qiskit.circuit.library import PauliEvolutionGate +from qiskit.synthesis import SuzukiTrotter + +from qiskit_ibm_runtime import QiskitRuntimeService + +from qiskit_aer import AerSimulator +from qiskit_aer.noise import NoiseModel, depolarizing_error + +from scipy.linalg import expm + + +# pylint: disable=too-many-positional-arguments +def h_molecule_evolution_data( + delta_t: float, + train_end: int, + test_start: int, + test_end: int, + molecule: str = "H2", + noise_mode: str = "reduced", + formatting: str = "ndarray", +) -> ( + tuple[Statevector, np.ndarray, list[Statevector], np.ndarray, list[Statevector]] + | tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray] +): + r""" + + Generates a dataset based on the time-evolution of Hydrogen molecules that can be used + to benchmark Variational Fast Forward (VFF) pipelines such as those discussed in [1]. + The dataset generator gives the user the Hartree-Fock (HF) state of a Hydrogen molecule's + spin orbital occupancy, a few noisy short-term evolutions of this state over time for + training the VFF and exact long-term evolutions of this state for comparing with the + long-term inferences made by the pipeline. + + + The Fermionic Hamiltonian for the Hydrogen Molecule is first obtained using Quantum + Chemistry calculations. This is then mapped to Qubits in Pauli form using the + Jordan-Wigner Mapping. Thus each qubit state represents the occupancy state of + one spin orbital each. The HF state :math:`\ket{\psi_{HF}}` is obtained by setting a + :math:`\ket{1}` state for the lowest energy orbitals and :math:`\ket{0}` for higher. + + + For generating the short-term evolutions with realistic noise models that will be + incurred by a Quantum Computer if it were to simulate short-term evolution terms, + the unitary operator for the evolution is first transpiled into a circuit with + :class:`~qiskit.synthesis.SuzukiTrotter` and :class:`~qiskit.circuit.library.PauliEvolutionGate`. + That is, suppose :math:`U` represents the noisy circuit's effect on a given state to simulate + the evolution through a time step of :math:`\Delta T` (``delta_t`` given by the user), then + + + .. math:: + U \approx e^{- j H \Delta T} + + + Where the approximate sign signifies that there is noise added by the noisy simulation and + also the approximate nature of transpiling with a trotterized hamiltonian. Now, the + short-term evolution terms are generate until ``train_end`` such evolutions. Suppose we + denote ``train_end`` as N. Then + + + .. math:: + \text{y_train} = + \left[\ket{\psi_{HF}}, U \ket{\psi_{HF}}, ...U^N \ket{\psi_{HF}}\right] + + + Long-term evolution for testing as numerically generated from the exact Hamiltonian without + the uncertainities introduced by noise and trotterization. Suppose ``test_start`` is denoted + as P and ``test_end`` as Q. Then + + + .. math:: + \text{y_test} = + \left[e^{-jHP\Delta T} \ket{\psi_{HF}}...e^{-jHQ\Delta T} \ket{\psi_{HF}}\right] + + + The choice of noise added in simulation is determined by ``noise_mode``, which can also + fetch calibration data from IBM runtimes. ``formatting`` parameter can be used to get + the data as numpy arrays or as list of statevectors as per the usecase. + + + **References:** + + [1] Filip M-A, Muñoz Ramo D, Fitzpatrick N. *Variational Phase Estimation + with Variational Fast Forwarding*. Quantum. 2024 Mar;8:1278. + `arXiv:2211.16097 `_ + + [2] Cîrstoiu C, Holmes Z, Iosue J, Cincio L, Coles PJ, Sornborger A. + *Variational fast forwarding for quantum simulation beyond the coherence time*. + npj Quantum Information. 2020 Sep;6(1):82. + `arXiv:1910.04292 `_ + + Parameters: + delta_t : Time step per evolution term (in atomic units). 1 a.u. = 2.42e-17 s + train_end : Generate short term evolutions up until :math:`U ^ \text{train_end}` + test_start : Generate long term evolution terms from :math:`U ^ \text{test_start}` + test_end : Generate long term evolution terms until :math:`U ^ \text{test_end}` + molecule : Decides which molecule is being simulation. The options are: + + * ``"H2"``: A linear H2 molecule at 0.735 A bond-length + * ``"H3"``: H3 molecule at an equilateral triangle of side 0.9 A + + Default is ``"H2"``. + noise_mode: The noise model used in the simulation of noisy short term evolutions + Choices are: + + * ``"noiseless"``: Which will generate no noise + * ``"reduced"``: Uses a low noise profile + * One of the IBM runtimes such as "ibm_brisbane". + + Default is ``"reduced"``. The available runtime backends can be found using + :class:`qiskit_ibm_runtime.QiskitRuntimeService.backends` + formatting: The format in which datapoints are given. + Choices are: + + * ``"ndarray"``: gives a numpy array of shape (n_points, 2**n_qubits, 1) + * ``"statevector"``: gives a python list of Statevector objects + + Default is ``"ndarray"``. + + Returns: + Tuple + containing the following: + + * **Hartree-Fock State** : ``np.ndarray`` | ``qiskit.quantum_info.Statevector`` + * **training_timestamps** : ``np.ndarray`` + * **training_states** : ``np.ndarray`` | ``qiskit.quantum_info.Statevector`` + * **testing_timestamps** : ``np.ndarray`` + * **testing_states** : ``np.ndarray`` | ``qiskit.quantum_info.Statevector`` + + """ + + # Errors and Warnings + if delta_t <= 0: + raise ValueError("delta_t must be positive (atomic-units of time).") + + if not isinstance(train_end, int) or train_end < 1: + raise ValueError("train_end must be a positive integer.") + + if not isinstance(test_start, int) or test_start < 1: + raise ValueError("test_start must be a positive integer.") + + if not isinstance(test_end, int) or test_end <= test_start: + raise ValueError("test_end must be an integer greater than test_start.") + + if molecule not in {"H2", "H3"}: # H6 disabled for now + raise ValueError("molecule must be 'H2' or 'H3'; 'H6' is temporarily unsupported.") + + if formatting not in {"ndarray", "statevector"}: + raise ValueError("formatting must be 'ndarray' or 'statevector'.") + + if test_start <= train_end: + warnings.warn("Training and testing ranges overlap; this can cause data leakage.") + + backend = None + if noise_mode not in {"reduced", "noiseless"}: + try: + service = QiskitRuntimeService() + # real, operational, ≥4-qubit devices + backends = service.backends(min_num_qubits=4, operational=True, simulator=False) + backend_names = [b.name for b in backends] # list-comprehension + allowed_modes = backend_names + ["reduced", "noiseless"] + + if noise_mode not in allowed_modes: + raise ValueError( + f"'{noise_mode}' is not available. " f"Choose from {allowed_modes}" + ) + + backend = service.backend(noise_mode) + except Exception as exc: + raise RuntimeError( + "Unable to fetch IBM backends; check your internet connection " + "and IBM Quantum account configuration." + ) from exc + + # Electron Occupancy + occupancy = {"H2": 2, "H3": 2, "H6": 6} + num_occupancy = occupancy[molecule] + + # Noise Models for Training Data + simulator = _noise_simulator(noise_mode, backend) + + # Import Hamiltonian and Unitary Evolution Circuit + qc, time, hamiltonian = _evolution_circuit(molecule) + qc_evo = qc.assign_parameters({time: delta_t}) + + # Get Hartree Fock State + psi_hf = _initial_state(hamiltonian, num_occupancy) + + # Time stamps for Train & Test + idx_train, idx_test = np.arange(0, train_end + 1), np.arange(test_start, test_end + 1) + x_train, x_test = delta_t * idx_train, delta_t * idx_test + + # Noisy Shortterm Evolutions + y_train = _simulate_shortterm(psi_hf, qc_evo, simulator, train_end) + + # Ideal Longterm Evolutions + y_test = _ideal_longterm(psi_hf, hamiltonian, x_test) + + if formatting == "ndarray": + y_train = _to_np(y_train) + y_test = _to_np(y_test) + psi_hf = psi_hf.probabilities() + + return (psi_hf, x_train, y_train, x_test, y_test) + + +def _evolution_circuit(molecule): + """Get the parametrized circuit for evolution after Trotterization. + Returns: + - QuantumCircuit (for training set) + - Parameter Object "t" (for training set) + - Original Hamiltonian (for testing set)""" + + spo = _hamiltonian_import(molecule) + + time = Parameter("time") + trotterizer = SuzukiTrotter(order=2, reps=1) + u_evolution = PauliEvolutionGate(spo, time=time, synthesis=trotterizer) + + n_qubits = spo.num_qubits + qc = QuantumCircuit(n_qubits) + qc.append(u_evolution, range(n_qubits)) + + qc_flat = qc.decompose() + + return qc_flat, time, spo + + +def _hamiltonian_import(molecule): + """Import Hamiltonian from Hamiltonians folder""" + + dir_path = os.path.dirname(__file__) + filename = os.path.join(dir_path, f"hamiltonians/h_molecule_hamiltonians/{molecule}.bin") + + with open(filename, "rb") as ham_file: + spo = pkl.load(ham_file) + + return spo + + +def _initial_state(hamiltonian, num_occupancy): + """Sets a realistic initial state + + JW map automatically keeps orbitals in ascending order of energy""" + + n_qubits = hamiltonian.num_qubits + + bitstring = ["1"] * num_occupancy + ["0"] * (n_qubits - num_occupancy) + + occupation_label = "".join(bitstring) + + return Statevector.from_label(occupation_label) + + +def _noise_simulator(noise_mode, backend): + """Returns a Noisy/Noiseless AerSimulator object""" + + if noise_mode == "noiseless": + noise_model = None + + elif noise_mode == "reduced": + single_qubit_error = depolarizing_error(0.001, 1) + two_qubit_error = depolarizing_error(0.01, 2) + noise_model = NoiseModel() + noise_model.add_all_qubit_quantum_error(single_qubit_error, ["u1", "u2", "u3"]) + noise_model.add_all_qubit_quantum_error(two_qubit_error, ["cx"]) + + # If the given Model is an IBM location + else: + noise_model = NoiseModel.from_backend(backend) + + simulator = AerSimulator(noise_model=noise_model) + return simulator + + +def _simulate_shortterm(psi_hf, qc_evo, simulator, train_end): + """Simulates short-term dynamics using a noisy simulator.""" + + y_train = [ + psi_hf, + ] + psi = psi_hf.copy() + + for _ in range(train_end): + + # Create a new quantum circuit for each step + qc = QuantumCircuit(psi.num_qubits) + + # psi persists after each step + qc.initialize(psi.data, qc.qubits) + qc.append(qc_evo, qc.qubits) + + qc.save_statevector() + qc_resolved = transpile(qc, simulator) + + # Execute the circuit on the noisy simulator + job = simulator.run(qc_resolved) + result = job.result() + + # Update the statevector with the result + psi = Statevector(result.get_statevector(qc)) + y_train.append(psi.copy()) + + return y_train + + +def _ideal_longterm(psi_hf, hamiltonian, timestamps): + """ + Return the list of statevectors exp(-i H t_k) @ psi_hf + for every t_k in `times`, using an exact matrix exponential. + """ + h_dense = hamiltonian.to_matrix() + y_test = [] + + for t_k in timestamps: + u_t = expm(-1j * h_dense * t_k) + psi_t = Statevector(u_t @ psi_hf.data) + y_test.append(psi_t) + + return y_test + + +def _to_np(states): + """Convert list[Statevector] to ndarray""" + dim = len(states[0]) + return np.stack([sv.data for sv in states], axis=0).reshape(len(states), dim, 1) diff --git a/qiskit_machine_learning/datasets/hamiltonians/create_h_molecules.py b/qiskit_machine_learning/datasets/hamiltonians/create_h_molecules.py new file mode 100644 index 000000000..bfd2a74a1 --- /dev/null +++ b/qiskit_machine_learning/datasets/hamiltonians/create_h_molecules.py @@ -0,0 +1,264 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +H Atom Pauli Forms (Developer-Only) +""" + +import numpy as np +import pickle as pkl +import os +import itertools + +from qiskit.quantum_info import SparsePauliOp + +""" +- H2: Usually found at a `0.735 Å` equilibrium bond distance +- H3+: Usually found both in an equilateral triangle configuration with `0.9 Å` between each pair +- H6: Usually modelled as a linear chain of 6 atoms with bond lengths `1 Å` between each pair +""" +_molecules = { + "H2": {"atom": "H 0 0 0; H 0 0 0.735", "basis": "sto-3g", "charge": 0, "spin": 0}, + "H3": { + "atom": "H 0 0 -0.45; H 0 0 0.45; H 0 0.78 0;", + "basis": "sto-3g", + "charge": 1, + "spin": 0, + }, + "H6": { + "atom": "H 0 0 0; H 0 0 1; H 0 0 2; H 0 0 3; H 0 0 4; H 0 0 5;", + "basis": "sto-3g", + "charge": 0, + "spin": 0, + }, +} + +# Pauli multiplication table +_mul_table = { + ("I", "I"): (1, "I"), + ("I", "X"): (1, "X"), + ("I", "Y"): (1, "Y"), + ("I", "Z"): (1, "Z"), + ("X", "I"): (1, "X"), + ("X", "X"): (1, "I"), + ("X", "Y"): (1j, "Z"), + ("X", "Z"): (-1j, "Y"), + ("Y", "I"): (1, "Y"), + ("Y", "X"): (-1j, "Z"), + ("Y", "Y"): (1, "I"), + ("Y", "Z"): (1j, "X"), + ("Z", "I"): (1, "Z"), + ("Z", "X"): (1j, "Y"), + ("Z", "Y"): (-1j, "X"), + ("Z", "Z"): (1, "I"), +} + + +def _a_p(p, n): + """JW Substituion for Annihilation Operator""" + z_str, t_str = ["Z"] * p, ["I"] * (n - p - 1) + return {tuple(z_str + ["X"] + t_str): 0.5, tuple(z_str + ["Y"] + t_str): 0.5j} + + +def _a_p_dag(p, n): + """JW Substitution for Creation Operator""" + z_str, t_str = ["Z"] * p, ["I"] * (n - p - 1) + return {tuple(z_str + ["X"] + t_str): 0.5, tuple(z_str + ["Y"] + t_str): -0.5j} + + +def _n_p(p, n): + """JW Substitution for 1-Body Diagonal Entries""" + id_str = ("I",) * n + z_str = tuple("Z" if i == p else "I" for i in range(n)) + return {id_str: 0.5, z_str: -0.5} + + +def _z_string(idxs, n): + """Helper for Z string generation""" + return {tuple("Z" if i in idxs else "I" for i in range(n)): 1.0} + + +def _mul_strs(s1, s2): + """Multiply Pauli Strings of form (Q0_op, Q1_op, Q2_op...)""" + phase, res = 1, [] + for a, b in zip(s1, s2): + ph, op = _mul_table[(a, b)] + phase *= ph + res.append(op) + return phase, tuple(res) + + +def _mul_pauli(t1, t2): + """Multiply Pauli Summation Dicts of form {Pauli String: Coefficient}""" + res = {} + for s1, c1 in t1.items(): + for s2, c2 in t2.items(): + ph, s = _mul_strs(s1, s2) + res[s] = res.get(s, 0) + ph * c1 * c2 + if abs(res[s]) == 0: + del res[s] + return res + + +def _add_pauli(H, t, coef=1.0): + """Add Pauli Summation Dicts of form {Pauli String: Coefficient}""" + for s, c in t.items(): + H[s] = H.get(s, 0) + coef * c + if abs(H[s]) == 0: + del H[s] + + +def _JW_map(E_nuc, h_so, g_so, eps=1e-15): + """Jordan Wigner mapping of each Spin Orbital to a Qubit""" + n = h_so.shape[0] + H = {("I",) * n: E_nuc} + + for p in range(n): + _add_pauli(H, _n_p(p, n), h_so[p, p]) + + for p in range(n): + for q in range(p + 1, n): + c = 0.5 * (h_so[p, q] + np.conj(h_so[q, p])) + if abs(c) < eps: + continue + _add_pauli(H, _mul_pauli(_a_p_dag(p, n), _a_p(q, n)), c) + _add_pauli(H, _mul_pauli(_a_p_dag(q, n), _a_p(p, n)), np.conj(c)) + + for p, q, r, s in itertools.product(range(n), repeat=4): + g = 0.5 * g_so[p, q, r, s] + if abs(g) < eps: + continue + uniq = len({p, q, r, s}) + + if uniq == 4: + term1 = _mul_pauli( + _mul_pauli(_a_p_dag(p, n), _a_p_dag(q, n)), _mul_pauli(_a_p(r, n), _a_p(s, n)) + ) + term2 = _mul_pauli( + _mul_pauli(_a_p_dag(s, n), _a_p_dag(r, n)), _mul_pauli(_a_p(q, n), _a_p(p, n)) + ) + _add_pauli(H, term1, g) + _add_pauli(H, term2, np.conj(g)) + elif uniq == 3: + term = _mul_pauli( + _mul_pauli(_a_p_dag(p, n), _a_p_dag(q, n)), _mul_pauli(_a_p(r, n), _a_p(s, n)) + ) + _add_pauli(H, term, g) + _add_pauli(H, _mul_pauli(_z_string([p], n), term), -g) + _add_pauli(H, _mul_pauli(_z_string([q], n), term), -g) + elif uniq == 2 and p != q and r == s: + coeff = 0.25 * g + z_p = _z_string([p], n) + z_q = _z_string([q], n) + z_pq = _z_string([p, q], n) + _add_pauli(H, {("I",) * n: -coeff}) + _add_pauli(H, z_p, coeff) + _add_pauli(H, z_q, coeff) + _add_pauli(H, z_pq, -coeff) + + return {k: v for k, v in H.items() if abs(v) > eps} + + +def _save_H_atom_pauli_forms(): + r"""Generates and saves + + This is a Developer-only module. The results of this code has already been + cached in the repository. This has been included in the repository for + ready availability of the generation process for future developments. + + Running this needs installing PySCF which is not included as a + requirement in requirements-dev.txt. If you are using a Windows + environment, it is recommended that you proceed with this module + on a different operating system and drop-in import the results. + + This saves H Atom Hamiltonians in Pauli form for further usage + by other dataset generators + + """ + try: + from pyscf import gto, scf, ao2mo + except ModuleNotFoundError: + raise ModuleNotFoundError( + """This Developer-Only Module requires PySCF. Please install it with + pip install --prefer-binary pyscf. If you are using a Windows + environment, it is recommended that you proceed with this module + on a different operating system and drop-in import the results""" + ) + + dir_path = os.path.dirname(__file__) + + for label, params in _molecules.items(): + + # PySCF Simulations + mol = gto.M(**params) + mf = scf.RHF(mol).run(conv_tol=1e-12) + + # E_nuc, 1-body and 2-body Integrals + E_nuc = mol.energy_nuc() + h_ao = mf.get_hcore() + g_ao = mol.intor("int2e") + + # Converting to Spatial Orbitals + C = mf.mo_coeff + h_mo = C.T @ h_ao @ C + g_mo8 = ao2mo.kernel(mol, C) + g_mo = ao2mo.restore(1, g_mo8, C.shape[1]) + + # Converting to Spin Orbitals + eps = 1e-18 + n_mo = h_mo.shape[0] + n_so = 2 * n_mo + + h_so = np.zeros((n_so, n_so), dtype=complex) + g_so = np.zeros((n_so, n_so, n_so, n_so), dtype=complex) + + # Single Body Terms + for p in range(n_mo): + for q in range(n_mo): + val = h_mo[p, q] + if abs(val) < eps: + continue + h_so[2 * p, 2 * q] = val + h_so[2 * p + 1, 2 * q + 1] = val + + # Two Body Terms + for p, q, r, s in itertools.product(range(n_mo), repeat=4): + val = g_mo[p, q, r, s] + if abs(val) < eps: + continue + g_so[2 * p, 2 * q, 2 * r, 2 * s] = val + g_so[2 * p + 1, 2 * q + 1, 2 * r + 1, 2 * s + 1] = val + g_so[2 * p, 2 * q + 1, 2 * r, 2 * s + 1] = val + g_so[2 * p + 1, 2 * q, 2 * r + 1, 2 * s] = val + + # Jordan Wigner Transform uses O(n) depth. + JW_H = _JW_map(E_nuc, h_so, g_so) + # Alternatively Bravyi-Kaetev Transform can give O(logn) + + # Convert to SparsePauliOp + + # We used qubits in (0,1...) indexing in pauli strings while qiskit + # uses (...1,0). Hence we reverse here + pauli_list = [("".join(reversed(k)), v) for k, v in JW_H.items()] + spo = SparsePauliOp.from_list(pauli_list) + + fname = f"h_molecule_hamiltonians/{label}.bin" + finalpath = os.path.join(dir_path, fname) + + with open(finalpath, "wb") as f: + pkl.dump(JW_H, f) + + print("Hamiltonians saved.") + + +if __name__ == "__main__": + _save_H_atom_pauli_forms() diff --git a/qiskit_machine_learning/datasets/hamiltonians/h_molecule_hamiltonians/H2.bin b/qiskit_machine_learning/datasets/hamiltonians/h_molecule_hamiltonians/H2.bin new file mode 100644 index 000000000..1a492b56c Binary files /dev/null and b/qiskit_machine_learning/datasets/hamiltonians/h_molecule_hamiltonians/H2.bin differ diff --git a/qiskit_machine_learning/datasets/hamiltonians/h_molecule_hamiltonians/H3.bin b/qiskit_machine_learning/datasets/hamiltonians/h_molecule_hamiltonians/H3.bin new file mode 100644 index 000000000..e1ee6b286 Binary files /dev/null and b/qiskit_machine_learning/datasets/hamiltonians/h_molecule_hamiltonians/H3.bin differ diff --git a/qiskit_machine_learning/datasets/hamiltonians/h_molecule_hamiltonians/H6.bin b/qiskit_machine_learning/datasets/hamiltonians/h_molecule_hamiltonians/H6.bin new file mode 100644 index 000000000..96111f4af Binary files /dev/null and b/qiskit_machine_learning/datasets/hamiltonians/h_molecule_hamiltonians/H6.bin differ diff --git a/releasenotes/notes/add_variational_fast_forwarding_dataset-06bbf48921ee645c.yaml b/releasenotes/notes/add_variational_fast_forwarding_dataset-06bbf48921ee645c.yaml new file mode 100644 index 000000000..de5909fde --- /dev/null +++ b/releasenotes/notes/add_variational_fast_forwarding_dataset-06bbf48921ee645c.yaml @@ -0,0 +1,21 @@ +features: + - | + The :func:`~qiskit_machine_learning.datasets.h_molecule_evolution_data` function has + been added. This dataset generator simulates time evolution of Hydrogen-based molecules + (H₂, H₃) under their electronic Hamiltonians using quantum simulation. It supports + realistic noise models from IBM Quantum backends and can be used to benchmark + variational fast-forwarding algorithms. + + Example of a 4-qubit H₂ dataset in noiseless statevector format is below. + + .. code-block:: python + + h_molecule_evolution_data( + delta_t=1.0, + train_end=5, + test_start=10, + test_end=15, + molecule="H2", + noise_mode="noiseless", + formatting="statevector" + ) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6a56691fc..85e181931 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,9 +12,9 @@ sphinx-design>=0.4.0 sphinxcontrib-spelling jupyter-sphinx discover -qiskit-aer>=0.11.2 mypy>=0.981 mypy-extensions>=0.4.3 nbsphinx qiskit_sphinx_theme~=1.16.0 +qiskit-aer>=0.11.2 qiskit-ibm-runtime>=0.21 diff --git a/requirements.txt b/requirements.txt index 811a007a0..fc0ef10fb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ scipy>=1.4 scikit-learn>=1.2 setuptools>=40.1 dill>=0.3.4 +qiskit-aer>=0.11.2 +qiskit-ibm-runtime>=0.21 diff --git a/test/datasets/test_h_molecule_evolution_data.py b/test/datasets/test_h_molecule_evolution_data.py new file mode 100644 index 000000000..86b643459 --- /dev/null +++ b/test/datasets/test_h_molecule_evolution_data.py @@ -0,0 +1,129 @@ +# This code is part of a Qiskit project. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" Test H Molecule Evolution Data """ + +from test import QiskitMachineLearningTestCase + +import unittest +import numpy as np +from ddt import ddt, unpack, idata + +from qiskit.quantum_info import Statevector +from qiskit.providers.exceptions import QiskitBackendNotFoundError + +from qiskit_ibm_runtime import QiskitRuntimeService +from qiskit_ibm_runtime.exceptions import IBMInputValueError +from qiskit_ibm_runtime.accounts.exceptions import AccountNotFoundError + +from qiskit_machine_learning.datasets import h_molecule_evolution_data + + +@ddt +class TestHMoleculeEvolution(QiskitMachineLearningTestCase): + """H Molecule Evolution Tests""" + + @idata([("H2", 4), ("H3", 6)]) + @unpack + def test_default_params(self, molecule, n_qubits): + """Checking for right shapes and labels""" + psi_hf, x_train, y_train, x_test, y_test = h_molecule_evolution_data( + delta_t=1.0, train_end=2, test_start=4, test_end=6, molecule=molecule + ) + + np.testing.assert_array_equal(psi_hf.shape, (2**n_qubits,)) + np.testing.assert_array_equal(x_train.shape, (3,)) + np.testing.assert_array_equal(x_test.shape, (3,)) + np.testing.assert_array_equal(y_train.shape, (3, 2**n_qubits, 1)) + np.testing.assert_array_equal(y_test.shape, (3, 2**n_qubits, 1)) + + @idata([("H2",), ("H3",)]) + @unpack + def test_statevector_formatting_noiseless(self, molecule): + """Check if output values are normalized qiskit.circuit_info.Statevector objects""" + psi_hf, x_tr, y_tr, x_te, y_te = h_molecule_evolution_data( + 1.0, + 1, + 3, + 4, + molecule=molecule, + formatting="statevector", + noise_mode="noiseless", + ) + self.assertIsInstance(psi_hf, Statevector) + self.assertTrue(all(isinstance(sv, Statevector) for sv in y_tr)) + self.assertTrue(all(isinstance(sv, Statevector) for sv in y_te)) + self.assertAlmostEqual(psi_hf.probabilities().sum(), 1.0, places=7) + for sv in y_tr[:2] + y_te[:2]: + self.assertAlmostEqual(sv.probabilities().sum(), 1.0, places=7) + np.testing.assert_array_equal(x_tr.shape, (2,)) + np.testing.assert_array_equal(x_te.shape, (2,)) + self.assertEqual(len(y_tr), 2) + self.assertEqual(len(y_te), 2) + + def test_connecting_to_runtime(self): + """Fetches the best runtime and connects to its noise model""" + try: + service = QiskitRuntimeService() + backend = service.backends(min_num_qubits=4, operational=True, simulator=False)[0] + except (IBMInputValueError, QiskitBackendNotFoundError, AccountNotFoundError): + self.skipTest("IBMQ account or internet unavailable") + psi_hf, _, y_tr, _, y_te = h_molecule_evolution_data( + 1.0, + 1, + 2, + 3, + molecule="H2", + noise_mode=backend.name, + formatting="ndarray", + ) + np.testing.assert_array_equal(psi_hf.shape, (2**4,)) + self.assertEqual(y_tr.shape[-1], 1) + self.assertEqual(y_te.shape[-1], 1) + + def test_error_raises(self): + """Check if parameter errors are handled""" + valid = dict( + delta_t=0.1, + train_end=5, + test_start=6, + test_end=10, + molecule="H2", + noise_mode="noiseless", + formatting="ndarray", + ) + + with self.assertRaises(ValueError): # bad delta_t + h_molecule_evolution_data(**{**valid, "delta_t": 0}) + + with self.assertRaises(ValueError): # bad train_end + h_molecule_evolution_data(**{**valid, "train_end": 0}) + + with self.assertRaises(ValueError): # bad test_start + h_molecule_evolution_data(**{**valid, "test_start": 0}) + + with self.assertRaises(ValueError): # test_end ≤ test_start + h_molecule_evolution_data(**{**valid, "test_end": 5}) + + with self.assertRaises(ValueError): # unsupported molecule + h_molecule_evolution_data(**{**valid, "molecule": "H6"}) + + with self.assertRaises(ValueError): # bad formatting + h_molecule_evolution_data(**{**valid, "formatting": "json"}) + + # invalid backend name – ValueError _or_ RuntimeError depending on connectivity + with self.assertRaises((ValueError, RuntimeError)): + h_molecule_evolution_data(**{**valid, "noise_mode": "bad_backend"}) + + +if __name__ == "__main__": + unittest.main()