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
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ dependencies = [
"pandas",
"qkernel4eo",
"qutip",
"scipy",
"scikit-learn"
"scikit-learn",
"scipy"
]


Expand All @@ -39,6 +39,7 @@ qkernel4eo = { git = "ssh://git@github.com/ICHEC/qkernel4eo.git" }

[dependency-groups]
dev = [
"ipykernel",
"pytest",
"ruff",
"ipykernel"
]
117 changes: 117 additions & 0 deletions tests/hamiltonian_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import numpy as np
import pytest
import qutip
from qat.core import Observable
from qkernel4eo.embeddings.chain_embedding import ChainEmbedding
from qkernel4eo.embeddings.radial_embedding import RadialEmbedding

from qkernel.hamiltonian import Hamiltonian, MyQLMHamiltonian, QuTiPHamiltonian

RNG = np.random.default_rng(42)
# 3 features: satisfies chain embedding constraint |-c_1 + n*c_2| = |-36+30| = 6 <= 38
# 2 samples: normalise_array needs at least 2 rows along axis=0 to avoid 0/0 = NaN
N_QBITS = 3
BANDS = RNG.random((2, N_QBITS))

QBITS_LIST = np.array(
[
RadialEmbedding(BANDS).embed()[0], # shape (N_QBITS, 2)
ChainEmbedding(BANDS).embed()[0], # shape (N_QBITS, 2)
]
) # stacked to shape (N, N_QBITS, 2) since Hamiltonian.__init__ reads qbits.shape[1]


@pytest.fixture
def myqlm():
return MyQLMHamiltonian(QBITS_LIST)


@pytest.fixture
def qutip_h():
return QuTiPHamiltonian(QBITS_LIST)


# --- Hamiltonian (abstract base) ---


def test_cannot_instantiate_abstract():
with pytest.raises(TypeError):
Hamiltonian(QBITS_LIST)


def test_nqbits_set_correctly(myqlm):
assert myqlm.nqbits == N_QBITS


def test_default_parameters(myqlm):
assert myqlm.omega == 2 * np.pi
assert myqlm.delta == 0
assert myqlm.c6 == 865723.02


def test_generate_hamiltonians_list_length(myqlm):
result = myqlm.generate_hamiltonians_list()
assert len(result) == len(QBITS_LIST)


# --- MyQLMHamiltonian ---


def test_myqlm_returns_three_terms(myqlm):
result = myqlm.generate_single_hamiltonian(QBITS_LIST[0])
assert len(result) == 3


def test_myqlm_coefficients(myqlm):
result = myqlm.generate_single_hamiltonian(QBITS_LIST[0])
assert result[0][0] == myqlm.omega
assert result[1][0] == -myqlm.delta
assert result[2][0] == myqlm.c6


def test_myqlm_terms_are_observables(myqlm):
result = myqlm.generate_single_hamiltonian(QBITS_LIST[0])
for _, obs in result:
assert isinstance(obs, Observable)


def test_myqlm_custom_parameters():
h = MyQLMHamiltonian(QBITS_LIST, omega=1.0, delta=0.5, c6=100.0)
result = h.generate_single_hamiltonian(QBITS_LIST[0])
assert result[0][0] == 1.0
assert result[1][0] == -0.5
assert result[2][0] == 100.0


# --- QuTiPHamiltonian ---


def test_qutip_returns_qobj(qutip_h):
result = qutip_h.generate_single_hamiltonian(QBITS_LIST[0])
assert isinstance(result, qutip.Qobj)


def test_qutip_hamiltonian_shape(qutip_h):
result = qutip_h.generate_single_hamiltonian(QBITS_LIST[0])
dim = 2**N_QBITS
assert result.shape == (dim, dim)


def test_qutip_hamiltonian_is_hermitian(qutip_h):
result = qutip_h.generate_single_hamiltonian(QBITS_LIST[0])
assert result.isherm


def test_qutip_op_single_qubit_shape(qutip_h):
op = qutip_h._qutip_op(0, qutip.sigmax())
assert op.shape == (2**N_QBITS, 2**N_QBITS)


def test_qutip_op_two_qubit_shape(qutip_h):
op = qutip_h._qutip_op(0, qutip.num(2), 1, qutip.num(2))
assert op.shape == (2**N_QBITS, 2**N_QBITS)


def test_qutip_generate_hamiltonians_list_length(qutip_h):
result = qutip_h.generate_hamiltonians_list()
assert len(result) == len(QBITS_LIST)
157 changes: 157 additions & 0 deletions tests/kernel_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import numpy as np
import pytest

from qkernel.kernel import (
Kernel,
exponential_distance,
exponential_jensen_shannon_distance,
)

RNG = np.random.default_rng(42)
N_TRAIN = 4
N_TEST = 2
N_STATES = 8 # 3 qubits → 2^3 states
N_QUBITS = 3


def make_probs(n, rng):
x = rng.random((n, N_STATES))
return x / x.sum(axis=1, keepdims=True)


P_TRAIN = make_probs(N_TRAIN, RNG)
P_TEST = make_probs(N_TEST, RNG)


@pytest.fixture
def kernel():
return Kernel(P_TRAIN, P_TEST, excitations=False, distance_fn="exp_js")


# --- exponential_jensen_shannon_distance ---


def test_exp_js_identical_distributions():
p = np.array([0.25, 0.25, 0.25, 0.25])
np.testing.assert_allclose(exponential_jensen_shannon_distance(p, p), 1.0)


def test_exp_js_different_distributions():
p = np.array([1.0, 0.0])
q = np.array([0.0, 1.0])
result = exponential_jensen_shannon_distance(p, q)
assert 0 < result < 1


def test_exp_js_mu_scaling():
p = np.array([0.5, 0.5])
q = np.array([1.0, 0.0])
r1 = exponential_jensen_shannon_distance(p, q, mu=1)
r2 = exponential_jensen_shannon_distance(p, q, mu=2)
assert r2 < r1


# --- exponential_distance ---


def test_exp_dist_identical_distributions():
p = np.array([0.25, 0.25, 0.25, 0.25])
np.testing.assert_allclose(exponential_distance(p, p), 1.0)


def test_exp_dist_known_value():
p = np.array([1.0, 0.0])
q = np.array([0.0, 1.0])
np.testing.assert_allclose(exponential_distance(p, q), np.exp(-2.0))


def test_exp_dist_mu_scaling():
p = np.array([0.5, 0.5])
q = np.array([1.0, 0.0])
r1 = exponential_distance(p, q, mu=1)
r2 = exponential_distance(p, q, mu=2)
assert r2 < r1


# --- Kernel.__init__ ---


def test_kernel_stores_distributions_without_excitations():
k = Kernel(P_TRAIN, P_TEST, excitations=False, distance_fn="exp_js")
np.testing.assert_array_equal(k.p_train, P_TRAIN)
np.testing.assert_array_equal(k.p_test, P_TEST)


def test_kernel_transforms_with_excitations():
k = Kernel(P_TRAIN, P_TEST, excitations=True, distance_fn="exp_js")
assert k.p_train.shape == (N_TRAIN, N_QUBITS + 1)
assert k.p_test.shape == (N_TEST, N_QUBITS + 1)


def test_kernel_distance_kwargs_defaults_to_empty(kernel):
assert kernel.distance_kwargs == {}


def test_kernel_n_train_n_test(kernel):
assert kernel.n_train == N_TRAIN
assert kernel.n_test == N_TEST


def test_kernel_exp_distance_fn():
k = Kernel(P_TRAIN, P_TEST, excitations=False, distance_fn="exp")
gram = k.compute_gram_train()
assert gram.shape == (N_TRAIN, N_TRAIN)


# --- compute_gram_train ---


def test_gram_train_shape(kernel):
assert kernel.compute_gram_train().shape == (N_TRAIN, N_TRAIN)


def test_gram_train_diagonal_is_ones(kernel):
gram = kernel.compute_gram_train()
np.testing.assert_allclose(np.diag(gram), 1.0)


def test_gram_train_is_symmetric(kernel):
gram = kernel.compute_gram_train()
np.testing.assert_allclose(gram, gram.T)


def test_gram_train_values_in_range(kernel):
gram = kernel.compute_gram_train()
assert np.all(gram > 0) and np.all(gram <= 1)


# --- compute_gram_test ---


def test_gram_test_shape(kernel):
assert kernel.compute_gram_test().shape == (N_TEST, N_TRAIN)


def test_gram_test_values_in_range(kernel):
gram = kernel.compute_gram_test()
assert np.all(gram > 0) and np.all(gram <= 1)


def test_gram_test_custom_mu():
k1 = Kernel(
P_TRAIN,
P_TEST,
excitations=False,
distance_fn="exp_js",
distance_kwargs={"mu": 1},
)
k2 = Kernel(
P_TRAIN,
P_TEST,
excitations=False,
distance_fn="exp_js",
distance_kwargs={"mu": 10},
)
gram1 = k1.compute_gram_test()
gram2 = k2.compute_gram_test()
assert gram2.mean() < gram1.mean()
63 changes: 63 additions & 0 deletions tests/simulator_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import numpy as np
import pytest
from qkernel4eo.embeddings.chain_embedding import ChainEmbedding
from qkernel4eo.embeddings.radial_embedding import RadialEmbedding

from qkernel.hamiltonian import QuTiPHamiltonian
from qkernel.simulator import QuTiPSimulator, Simulator

RNG = np.random.default_rng(42)
# 3 features: satisfies chain embedding constraint |-c_1 + n*c_2| = |-36+30| = 6 <= 38
# 2 samples: normalise_array needs at least 2 rows along axis=0 to avoid 0/0 = NaN
N_QBITS = 3
BANDS = RNG.random((2, N_QBITS))

QBITS_LIST = np.array(
[
RadialEmbedding(BANDS).embed()[0], # shape (N_QBITS, 2)
ChainEmbedding(BANDS).embed()[0], # shape (N_QBITS, 2)
]
) # stacked to shape (N, N_QBITS, 2) since Hamiltonian.__init__ reads qbits.shape[1]

HAMILTONIANS_LIST = QuTiPHamiltonian(QBITS_LIST).generate_hamiltonians_list()


@pytest.fixture
def simulator():
return QuTiPSimulator(HAMILTONIANS_LIST)


# --- Simulator (abstract base) ---


def test_cannot_instantiate_abstract():
with pytest.raises(TypeError):
Simulator(HAMILTONIANS_LIST)


# --- QuTiPSimulator ---


def test_evolve_hamiltonian_returns_array(simulator):
result = simulator._evolve_hamiltonian(HAMILTONIANS_LIST[0])
assert isinstance(result, np.ndarray)


def test_evolve_hamiltonian_shape(simulator):
result = simulator._evolve_hamiltonian(HAMILTONIANS_LIST[0])
assert result.flatten().shape == (2**N_QBITS,)


def test_get_probabilities_list_length(simulator):
result = simulator.get_probabilities_list()
assert len(result) == len(HAMILTONIANS_LIST)


def test_probabilities_are_nonnegative(simulator):
for probs in simulator.get_probabilities_list():
assert np.all(probs >= 0)


def test_probabilities_sum_to_one(simulator):
for probs in simulator.get_probabilities_list():
np.testing.assert_allclose(probs.sum(), 1.0, atol=1e-10)
Loading