Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .pylintdict
Original file line number Diff line number Diff line change
Expand Up @@ -676,3 +676,36 @@ zz
φ_ij
Δ
π
annni
antiphase
antiferromagnetic
canonicalize
diagonalization
eigsh
ferromagnetic
haldane
hamiltonian
hamiltonians
heisenberg
kappa
lattice
paramagnetic
pauli
paulis
spt
topological
trivial
atol
ddt
eigh
geq
idata
namespace
rng
simulable
zxz
simulatable
bermejo
ceil
doctest
eq
4 changes: 3 additions & 1 deletion qiskit_machine_learning/datasets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@

ad_hoc_data
entanglement_concentration_data
phase_of_matter_data
"""

from .ad_hoc import ad_hoc_data
from .entanglement_concentration import entanglement_concentration_data
from .phase_of_matter import phase_of_matter_data

__all__ = ["ad_hoc_data", "entanglement_concentration_data"]
__all__ = ["ad_hoc_data", "entanglement_concentration_data", "phase_of_matter_data"]
39 changes: 39 additions & 0 deletions qiskit_machine_learning/datasets/phase_of_matter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2019, 2026.
# (C) Copyright UKRI-STFC (Hartree Centre) 2024, 2026.
#
# 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.

"""
Phase of Matter dataset (:mod:`phase_of_matter`)

Quantum Phase of Matter classification dataset generator.

Each supported model lives in its own module:

* :mod:`._heisenberg` — Bond-alternating XXX Heisenberg chain
* :mod:`._haldane` — Haldane chain
* :mod:`._annni` — Axial Next-Nearest-Neighbor Ising (ANNNI) model
* :mod:`._cluster` — Cluster Hamiltonian

The :func:`phase_of_matter_data` function is the single public entry point.

.. currentmodule:: phase_of_matter

.. autosummary::
:toctree: ../stubs/
:nosignatures:

phase_of_matter_data
"""

from .phase_of_matter import phase_of_matter_data

__all__ = ["phase_of_matter_data"]
105 changes: 105 additions & 0 deletions qiskit_machine_learning/datasets/phase_of_matter/_annni.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2019, 2026.
# (C) Copyright UKRI-STFC (Hartree Centre) 2024, 2026.
#
# 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.

"""Axial Next-Nearest-Neighbor Ising (ANNNI) Hamiltonian and phase sampler.

Reference: Bermejo et al., arXiv:2408.12739, eq. (8).
"""

from __future__ import annotations

import numpy as np
from qiskit.quantum_info import SparsePauliOp

from ._base import pauli_term

#: Ordered list of phase labels for the ANNNI model.
PHASE_LABELS: list[str] = ["ferromagnetic", "paramagnetic", "floating", "antiphase"]


def build_hamiltonian(n: int, kappa: float, h: float, j1: float = 1.0) -> SparsePauliOp:
r"""ANNNI Hamiltonian (Paper eq. 8).

.. math::

H = -J_1 \sum_{i=1}^{n-1} X_i X_{i+1}
- J_2 \sum_{i=1}^{n-2} X_i X_{i+2}
- B \sum_{i=1}^{n} Z_i

with :math:`J_2 = -\kappa J_1` and :math:`B = h J_1`.

Phase diagram (see Fig. 5 in the reference, axes :math:`\kappa` vs
:math:`h` with :math:`J_1 = 1`):

* **ferromagnetic** (I) -- small :math:`\kappa`, small :math:`h`
* **paramagnetic** (II) -- small :math:`\kappa`, large :math:`h`
* **floating** (III) -- large :math:`\kappa`, moderate :math:`h`
* **antiphase** (IV) -- large :math:`\kappa`, small :math:`h`

Args:
n: Number of lattice sites (qubits).
kappa: Dimensionless ratio :math:`\kappa = -J_2 / J_1`.
h: Dimensionless ratio :math:`h = B / J_1`.
j1: Overall energy scale (default 1.0).

Returns:
SparsePauliOp for the Hamiltonian on *n* qubits.
"""
j2 = -kappa * j1
b = h * j1
terms: list[SparsePauliOp] = []
for i in range(n - 1):
terms.append(-j1 * pauli_term([("X", i), ("X", i + 1)], n))
for i in range(n - 2):
terms.append(-j2 * pauli_term([("X", i), ("X", i + 2)], n))
for i in range(n):
terms.append(-b * pauli_term([("Z", i)], n))
return SparsePauliOp.sum(terms).simplify()


def sample_parameters(n_samples: int, rng: np.random.Generator) -> list[tuple[dict, str]]:
"""Sample coupling parameters uniformly from the interior of each phase.

Sampling regions (see Fig. 5 in the reference) are placed well inside
each phase to avoid mislabeled points near boundaries.

Args:
n_samples: Number of samples to draw *per class*.
rng: NumPy random Generator instance.

Returns:
List of ``(params_dict, phase_label)`` tuples. The list contains
*n_samples* entries for each phase in :data:`PHASE_LABELS`, in order.
"""
samples: list[tuple[dict, str]] = []
# ferromagnetic (I): kappa in (0, 0.3), h in (0, 0.25)
ks = rng.uniform(0.0, 0.3, size=n_samples)
hs = rng.uniform(0.0, 0.25, size=n_samples)
for k, hv in zip(ks, hs):
samples.append(({"kappa": float(k), "h": float(hv)}, "ferromagnetic"))
# paramagnetic (II): kappa in (0, 0.45), h in (0.9, 1.5)
ks = rng.uniform(0.0, 0.45, size=n_samples)
hs = rng.uniform(0.9, 1.5, size=n_samples)
for k, hv in zip(ks, hs):
samples.append(({"kappa": float(k), "h": float(hv)}, "paramagnetic"))
# floating (III): kappa in (0.55, 0.9), h in (0.25, 0.65)
ks = rng.uniform(0.55, 0.9, size=n_samples)
hs = rng.uniform(0.25, 0.65, size=n_samples)
for k, hv in zip(ks, hs):
samples.append(({"kappa": float(k), "h": float(hv)}, "floating"))
# antiphase (IV): kappa in (0.55, 0.9), h in (0, 0.1)
ks = rng.uniform(0.55, 0.9, size=n_samples)
hs = rng.uniform(0.0, 0.1, size=n_samples)
for k, hv in zip(ks, hs):
samples.append(({"kappa": float(k), "h": float(hv)}, "antiphase"))
return samples
120 changes: 120 additions & 0 deletions qiskit_machine_learning/datasets/phase_of_matter/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2019, 2026.
# (C) Copyright UKRI-STFC (Hartree Centre) 2024, 2026.
#
# 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.

"""Shared utilities for Phase of Matter dataset generators."""

from __future__ import annotations

import numpy as np
import scipy.sparse
import scipy.sparse.linalg
from qiskit.quantum_info import SparsePauliOp, Statevector


def pauli_term(op_list: list[tuple[str, int]], n: int) -> SparsePauliOp:
"""Build a single n-qubit Pauli term from a list of (pauli_char, site) pairs.

Sites not listed are identity. Uses Qiskit's little-endian convention:
site 0 is the rightmost character in the Pauli string.

Args:
op_list: List of (Pauli character, qubit site index) pairs.
n: Total number of qubits.

Returns:
SparsePauliOp representing the term.
"""
chars = ["I"] * n
for pauli_char, site in op_list:
chars[site] = pauli_char
return SparsePauliOp("".join(reversed(chars)))


def _canonicalize_phase(vec: np.ndarray) -> np.ndarray:
"""Fix the global phase so that the leading large-magnitude element is real positive.

Eigenvectors are defined only up to a global complex phase; this
phase-fixing makes repeated calls to ``eigsh`` return numerically
identical arrays for the same Hamiltonian.
"""
threshold = 1e-10 * np.max(np.abs(vec))
for val in vec:
if abs(val) > threshold:
return vec * (np.conj(val) / abs(val))
return vec


def get_ground_state_exact(hamiltonian: SparsePauliOp) -> np.ndarray:
"""Return the ground-state vector via sparse exact diagonalization.

Uses ``scipy.sparse.linalg.eigsh`` with ``which='SA'`` (smallest algebraic
eigenvalue). Practical limit: n <= 16 qubits (2^16 x 2^16 matrix).

The returned vector is phase-fixed so that repeated calls for the
same Hamiltonian yield identical arrays.

Args:
hamiltonian: Hamiltonian as a SparsePauliOp.

Returns:
Complex numpy array of shape ``(2**n,)`` -- the normalized ground state.
"""
mat = hamiltonian.to_matrix(sparse=True).astype(complex)
_, vecs = scipy.sparse.linalg.eigsh(mat, k=1, which="SA")
return _canonicalize_phase(vecs[:, 0])


def get_ground_state_vqe(
hamiltonian: SparsePauliOp,
backend, # pylint: disable=unused-argument
) -> Statevector:
"""Approximate the ground state via VQE using qiskit primitives.

.. warning::

VQE is provided for hardware-experiment workflows only. For reliable
phase labels, use the default exact diagonalization (``backend=None``).
VQE approximations near phase boundaries may produce incorrect labels.

Uses an ``EfficientSU2`` ansatz (1 repetition) with COBYLA optimization via
``StatevectorEstimator`` from ``qiskit.primitives``. The ``backend``
argument is accepted for API consistency and future hardware integration;
the current implementation uses ``StatevectorEstimator`` unconditionally.

Args:
hamiltonian: Hamiltonian as a SparsePauliOp.
backend (object): Reserved for future hardware integration. Currently unused;
pass any non-``None`` value to activate this pathway.

Returns:
Qiskit ``Statevector`` of the approximate ground state.
"""
# Deferred imports so qiskit-aer is only required when VQE is used.
from qiskit.circuit.library import EfficientSU2 # pylint: disable=import-outside-toplevel
from qiskit.primitives import StatevectorEstimator # pylint: disable=import-outside-toplevel
from scipy.optimize import minimize # pylint: disable=import-outside-toplevel

n = hamiltonian.num_qubits
ansatz = EfficientSU2(n, reps=1, entanglement="linear")
num_params = ansatz.num_parameters
estimator = StatevectorEstimator()

def cost(params: np.ndarray) -> float:
"""Evaluate energy expectation value for given parameters."""
pub = (ansatz, [hamiltonian], [params])
return float(estimator.run([pub]).result()[0].data.evs[0])

rng = np.random.default_rng(0)
x0 = rng.uniform(-np.pi, np.pi, num_params)
result = minimize(cost, x0, method="COBYLA", options={"maxiter": 1000, "rhobeg": 0.5})
return Statevector(ansatz.assign_parameters(result.x))
Loading
Loading