Skip to content

Commit 0f6cb09

Browse files
Merge pull request #389 from sQUlearn/develop
sQUlearn version 0.11.2
2 parents da3469d + c62c650 commit 0f6cb09

3 files changed

Lines changed: 129 additions & 31 deletions

File tree

src/squlearn/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .util import Executor
44
from . import observables, encoding_circuit, kernel, optimizers, qnn, util
55

6-
__version__ = "0.11.1"
6+
__version__ = "0.11.2"
77

88
__all__ = [
99
"Executor",

src/squlearn/kernel/lowlevel_kernel/fidelity_kernel_statevector.py

Lines changed: 70 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -74,47 +74,19 @@ def __init__(
7474
circuit = transpile(enc_circ, target=qiskit_pennylane_target, optimization_level=0)
7575
self._pennylane_circuit = PennyLaneCircuit(circuit, "state")
7676

77-
@lru_cache(maxsize=self._cache_size)
78-
def pennylane_circuit_executor(*args, **kwargs):
79-
args_numpy = [np.array(arg) for arg in args]
80-
return self._executor.pennylane_execute(
81-
self._pennylane_circuit, *args_numpy, **kwargs
82-
)
83-
84-
self._cached_execution = pennylane_circuit_executor
85-
8677
elif self._executor.quantum_framework == "qulacs":
8778

8879
enc_circ = self._encoding_circuit.get_circuit(x, self._parameter_vector)
8980
self._qulacs_circuit = QulacsCircuit(enc_circ, None)
9081

91-
@lru_cache(maxsize=self._cache_size)
92-
def qulacs_circuit_executor(*args):
93-
args_numpy = [np.array(arg) for arg in args]
94-
if len(args_numpy) == 0:
95-
return self._executor.qulacs_execute(
96-
qulacs_evaluate_statevector, self._qulacs_circuit
97-
)
98-
elif len(args_numpy) == 1:
99-
return self._executor.qulacs_execute(
100-
qulacs_evaluate_statevector, self._qulacs_circuit, x=args_numpy[0]
101-
)
102-
elif len(args_numpy) == 2:
103-
return self._executor.qulacs_execute(
104-
qulacs_evaluate_statevector,
105-
self._qulacs_circuit,
106-
p=args_numpy[0],
107-
x=args_numpy[1],
108-
)
109-
110-
self._cached_execution = qulacs_circuit_executor
111-
11282
else:
11383
raise RuntimeError(
11484
"Quantum framework not supported for FidelityKernelStatevector: "
11585
f"{self._executor.quantum_framework}"
11686
)
11787

88+
self._build_cached_execution()
89+
11890
else:
11991

12092
# Mode 2 for shot based: calculate the |0> probabilities
@@ -144,6 +116,74 @@ def qulacs_circuit_executor(*args):
144116
f"{self._executor.quantum_framework}"
145117
)
146118

119+
def _build_cached_execution(self) -> None:
120+
"""(Re)create the ``lru_cache``-wrapped circuit executor.
121+
122+
Only statevector kernels use a cached executor (shot-based kernels do
123+
not). The closure is rebuilt from the already-constructed circuit
124+
object rather than from the executor's framework, so it survives being
125+
deserialized onto a different backend executor (see
126+
:meth:`__getstate__`/:meth:`__setstate__`).
127+
"""
128+
if not self._executor.is_statevector:
129+
return
130+
131+
if getattr(self, "_qulacs_circuit", None) is not None:
132+
133+
@lru_cache(maxsize=self._cache_size)
134+
def qulacs_circuit_executor(*args):
135+
args_numpy = [np.array(arg) for arg in args]
136+
if len(args_numpy) == 0:
137+
return self._executor.qulacs_execute(
138+
qulacs_evaluate_statevector, self._qulacs_circuit
139+
)
140+
elif len(args_numpy) == 1:
141+
return self._executor.qulacs_execute(
142+
qulacs_evaluate_statevector, self._qulacs_circuit, x=args_numpy[0]
143+
)
144+
elif len(args_numpy) == 2:
145+
return self._executor.qulacs_execute(
146+
qulacs_evaluate_statevector,
147+
self._qulacs_circuit,
148+
p=args_numpy[0],
149+
x=args_numpy[1],
150+
)
151+
152+
self._cached_execution = qulacs_circuit_executor
153+
154+
elif getattr(self, "_pennylane_circuit", None) is not None:
155+
156+
@lru_cache(maxsize=self._cache_size)
157+
def pennylane_circuit_executor(*args, **kwargs):
158+
args_numpy = [np.array(arg) for arg in args]
159+
return self._executor.pennylane_execute(
160+
self._pennylane_circuit, *args_numpy, **kwargs
161+
)
162+
163+
self._cached_execution = pennylane_circuit_executor
164+
165+
def __getstate__(self) -> dict:
166+
"""Return a picklable copy of the kernel's state.
167+
168+
``self._cached_execution`` is a local closure wrapped in
169+
:func:`functools.lru_cache`. Such objects are not picklable by
170+
reference: pickle/cloudpickle serialize them via the wrapped function's
171+
``<locals>`` qualname
172+
(``FidelityKernelStatevector.__init__.<locals>.qulacs_circuit_executor``),
173+
which cannot be resolved on load -> ``AttributeError: Can't get local
174+
object ...``. It is a pure memoization cache, so we drop it here and
175+
rebuild it in :meth:`__setstate__`; every other attribute (including the
176+
circuit objects) is preserved unchanged.
177+
"""
178+
state = self.__dict__.copy()
179+
state.pop("_cached_execution", None)
180+
return state
181+
182+
def __setstate__(self, state: dict) -> None:
183+
"""Restore the kernel and rebuild the dropped executor closure."""
184+
self.__dict__.update(state)
185+
self._build_cached_execution()
186+
147187
@property
148188
def num_parameters(self) -> int:
149189
"""Returns the number of trainable parameters."""

tests/kernel/lowlevel_kernel/test_fidelity_kernel_statevector.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import pickle
12
from unittest.mock import MagicMock
23
import numpy as np
34
import pytest
5+
from squlearn.encoding_circuit import ChebyshevPQC
46
from squlearn.kernel.lowlevel_kernel.fidelity_kernel_statevector import FidelityKernelStatevector
7+
from squlearn.util import Executor
58

69

710
def make_executor(is_statevector=True, framework="pennylane", shots=None):
@@ -298,3 +301,58 @@ def test_evaluate_kernel_sv_qulacs_with_parameter_vector_missing_parameters_rais
298301
x = np.array([[0.1]])
299302
with pytest.raises(ValueError):
300303
k.evaluate_kernel_sv(x, x)
304+
305+
306+
def _make_statevector_kernel(framework):
307+
executor = Executor(framework)
308+
encoding_circuit = ChebyshevPQC(num_qubits=2, num_features=2, num_layers=1)
309+
kernel = FidelityKernelStatevector(
310+
encoding_circuit=encoding_circuit, executor=executor, num_features=2
311+
)
312+
rng = np.random.default_rng(0)
313+
if kernel.num_parameters > 0:
314+
kernel.assign_training_parameters(rng.random(kernel.num_parameters))
315+
return kernel
316+
317+
318+
def test_pickle_roundtrip_preserves_kernel_qulacs():
319+
"""A qulacs statevector kernel must survive stdlib pickling.
320+
321+
``_cached_execution`` is an ``lru_cache``-wrapped local closure that cannot
322+
be pickled by reference (the original ``AttributeError: Can't get local
323+
object ...``); ``__getstate__``/``__setstate__`` drop and rebuild it. The
324+
pennylane circuit holds a sympy lambda that stdlib pickle cannot handle
325+
regardless, so that framework is covered by the dill-based ModelPickler
326+
serialization tests and by ``test_getstate_setstate_rebuilds_executor``.
327+
"""
328+
kernel = _make_statevector_kernel("qulacs")
329+
x = np.random.default_rng(0).random((4, 2))
330+
expected = kernel.evaluate(x, x)
331+
332+
restored = pickle.loads(pickle.dumps(kernel))
333+
334+
assert "_cached_execution" in vars(restored)
335+
np.testing.assert_allclose(restored.evaluate(x, x), expected)
336+
337+
338+
@pytest.mark.parametrize("framework", ["qulacs", "pennylane"])
339+
def test_getstate_setstate_rebuilds_executor(framework):
340+
"""__getstate__ drops the unpicklable closure; __setstate__ rebuilds it.
341+
342+
Exercises the mechanism directly (no pickling), so it covers pennylane too,
343+
and confirms the rebuilt kernel reproduces the original kernel matrix.
344+
"""
345+
kernel = _make_statevector_kernel(framework)
346+
x = np.random.default_rng(0).random((4, 2))
347+
expected = kernel.evaluate(x, x)
348+
349+
state = kernel.__getstate__()
350+
assert "_cached_execution" not in state
351+
352+
restored = FidelityKernelStatevector.__new__(FidelityKernelStatevector)
353+
restored.__setstate__(state)
354+
355+
assert "_cached_execution" in vars(restored)
356+
np.testing.assert_allclose(restored.evaluate(x, x), expected)
357+
if kernel._parameters is not None:
358+
np.testing.assert_allclose(restored._parameters, kernel._parameters)

0 commit comments

Comments
 (0)