"""Abstract quantum-like perception model and shared operations."""
from abc import ABC, abstractmethod
from collections.abc import Generator, Sequence
from typing import TypeAlias
import numpy as np
from qrobot.backends import QiskitBackend, QuantumBackend
Scalar: TypeAlias = float | int
TargetVector: TypeAlias = Sequence[Scalar] | Scalar
[docs]
class Model(ABC):
"""Base class for quantum-like perception models.
A model encodes an ``n``-dimensional sequence over ``tau`` samples in a
quantum circuit. Subclasses define the input-to-rotation mapping, query
transformation, and decoding rule.
Parameters
----------
n : int
Number of input dimensions. Each dimension is represented by one qubit.
tau : int
Number of samples encoded in one temporal window.
Attributes
----------
n : int
Model's dimension.
tau : int
Number of samples of the temporal window.
circ : object
Backend-specific circuit containing the encoded window.
"""
def __init__(self, n: int, tau: int, backend: QuantumBackend | None = None) -> None:
"""Create an empty ``n``-qubit model for ``tau`` samples."""
# Check the argument n
if isinstance(n, int):
if n > 0:
self.n = n
else:
raise ValueError("n must be greater than 0!")
else:
raise TypeError("n must be an integer!")
# Check the argument tau
if isinstance(tau, int):
if tau > 0:
self.tau = tau
else:
raise ValueError("tau must be greater than 0!")
else:
raise TypeError("tau must be an integer!")
self.backend = backend or QiskitBackend()
self.circ = self.backend.create_circuit(n)
def __iter__(self) -> Generator[tuple[str, object], None, None]:
"""Yield the model configuration as key-value pairs."""
yield "model", self.__class__.__name__
yield "n", self.n
yield "tau", self.tau
def __repr__(self) -> str:
"""Return a compact representation of the model configuration."""
out_str = "["
for key, value in dict(self).items():
out_str += f"{key}: {value}, "
return out_str[:-2] + "]"
def _dim_index_check(self, dim: int) -> int:
"""Validate and return an input-dimension index.
Raises
------
TypeError
`dim` is not an integer `int`
ValueError
``dim`` is negative.
IndexError
``dim`` is greater than or equal to ``n``.
Returns
-------
int
The dimension index `dim`
"""
if not isinstance(dim, int):
raise TypeError("dim must be an integer!")
if dim < 0:
raise ValueError("dim must be greater or equal to 0!")
if dim >= self.n:
raise IndexError(f"dim is greater than the model dimension n={self.n}!")
return dim
@staticmethod
def _scalar_input_check(scalar_input: Scalar) -> float:
"""Validate and normalize one scalar model input.
Raises
------
TypeError:
``scalar_input`` is neither an ``int`` nor a ``float``.
ValueError
`scalar_input` is not between 0 and 1 inclusive
Returns
-------
float
The `scalar_input`
"""
if not isinstance(scalar_input, (float, int)):
raise TypeError(f"input must be a scalar number, not a {type(scalar_input)}!")
if scalar_input > 1 or scalar_input < 0:
raise ValueError("scalar_input must be between 0 and 1 inclusive!")
return float(scalar_input)
def _target_vector_check(self, target_vector: TargetVector) -> list[float]:
"""Validate a query target and return it as a list of floats.
A scalar target is accepted for a one-dimensional model. Vector
targets must contain exactly one normalized value per model dimension.
Raises
------
TypeError
`target_vector` elements are not all integers or floats
ValueError
`target_vector` dimension does not match model's dimension `n`
ValueError
A ``target_vector`` element is outside the interval ``[0, 1]``.
Returns
-------
list
The `target_vector`
"""
# Use the same validation path for scalar and vector targets.
if isinstance(target_vector, (float, int)):
target_vector = [target_vector]
else:
target_vector = list(target_vector)
# Dimensionality check on the vector
if len(target_vector) != self.n:
raise ValueError(f"target_vector must be a {self.n}-dimensional vector!")
for element in target_vector:
if not isinstance(element, (float, int)):
raise TypeError("target_vector elements must be all integers or floats!")
if element > 1 or element < 0:
raise ValueError("target_vector elements must be all between 0 and 1 inclusive!")
return [float(element) for element in target_vector]
[docs]
def clear(self) -> None:
"""Re-initialize the model with an empty circuit."""
self.circ = self.backend.create_circuit(self.n)
[docs]
@abstractmethod
def encode(self, scalar_input: Scalar, dim: int) -> float:
"""Encode one normalized input in the qubit for ``dim``.
Example
-------
To encode a `sequence` of input vectors, given `tau` and `n`::
for t in range(model.tau): # loop through time
for dim in range(model.n): # loop through dimensions
model.encode(sequence[t][dim], dim)
"""
[docs]
def measure(self, shots: int = 1) -> dict[str, int]:
"""Measure the qubits using the configured backend.
Parameters
----------
shots : int
Number of times to repeat the measurement shot
Returns
-------
dict
State occurrences counts in the form {"state": count}
"""
return self.backend.sample_counts(self.circ, shots)
[docs]
@abstractmethod
def decode(self) -> str:
"""Measure and decode the model state as a basis-state label."""
[docs]
@abstractmethod
def query(self, target_vector: TargetVector) -> None:
r"""Change basis so ``target_vector`` maps to state \|00...0>."""
[docs]
def get_statevector(self) -> np.ndarray:
"""Return the simulated state vector of the model.
Returns
-------
numpy.ndarray
Model's state vector.
"""
return self.backend.statevector(self.circ)
[docs]
def get_density_matrix(self) -> np.ndarray:
"""Return the simulated density matrix of the model.
Returns
-------
numpy.ndarray
Model's density matrix.
"""
statevector = self.get_statevector()
return np.outer(statevector, statevector.conjugate())
[docs]
def print_circuit(self) -> None:
"""Print the quantum circuit on which the model is implemented."""
print(self.circ)
[docs]
def plot_state_mat(self) -> None:
"""Plot the real parts of the state vector and density matrix.
Example
-------
To plot a perfectly balanced superposition of states::
model = Model(n, tau) # change Model with the desired child class
for t in range(0,model.tau): # loop through time
for dim in range(model.n): # loop through dimensions
model.encode(.5, dim)
model.plot_state_mat()
Raises
------
OverflowError
If the dimension of the model is 6 or greater, plotting fails
due to the high number of basis states.
"""
if self.n >= 6: # avoid matrices too big to be useful
raise OverflowError(
f"n={self.n} means {np.power(2, self.n)} states"
+ "(too much for a reasonable plot)!"
)
# Plotting dependencies are loaded at the presentation boundary.
try:
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
except ImportError as exc:
raise ImportError(
"plot_state_mat() requires the 'model-visualization' extra. "
"Install it with 'poetry install --extras model-visualization'."
) from exc
fig = plt.figure(figsize=(15, 4))
# Plot the vector state
axis = fig.add_subplot(121)
state = pd.DataFrame(self.get_statevector().real)
axis = sns.heatmap(
state,
annot=True,
linewidths=0.5,
xticklabels="",
ax=axis,
cmap="coolwarm",
vmin=-1,
vmax=1,
fmt=".5g",
)
axis.set_title("State vector (real part)")
# Plot the density matrix
axis = fig.add_subplot(122)
matrix = pd.DataFrame(self.get_density_matrix().real)
axis = sns.heatmap(
matrix,
annot=True,
linewidths=0.5,
ax=axis,
cmap="coolwarm",
vmin=-1,
vmax=1,
fmt=".5g",
)
axis.set_title("Density Matrix (real part)")