Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor RawFeatureVector to fix compositions #21

Merged
merged 6 commits into from
Mar 29, 2021
Merged
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
133 changes: 55 additions & 78 deletions qiskit_machine_learning/circuit/library/raw_feature_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@

"""The raw feature vector circuit."""

from typing import Set, List, Optional
from typing import Optional
import numpy as np
from qiskit.circuit import QuantumRegister, ParameterVector, ParameterExpression, Gate
from qiskit.exceptions import QiskitError
from qiskit.circuit import (
QuantumRegister, QuantumCircuit, ParameterVector, Instruction
)
from qiskit.circuit.library import BlueprintCircuit


class RawFeatureVector(BlueprintCircuit):
"""The raw feature vector circuit.

This circuit acts as parameterized initialization for statevectors with ``feature_dimension``
dimensions, thus with ``log2(feature_dimension)`` qubits. As long as there are free parameters,
this circuit holds a placeholder instruction and can not be decomposed.
Once all parameters are bound, the placeholder is replaced by a state initialization and can
be unrolled.
dimensions, thus with ``log2(feature_dimension)`` qubits. The circuit contains a
placeholder instruction that can only be synthesized/defined when all parameters are bound.

In ML, this circuit can be used to load the training data into qubit amplitudes. It does not
apply an kernel transformation. (Therefore, it is a "raw" feature vector.)
Expand All @@ -41,11 +42,11 @@ class RawFeatureVector(BlueprintCircuit):

print(circuit.draw(output='text'))
# prints:
# ┌──────┐
# q_0: ┤0 ├
# │ Raw
# q_1: ┤1 ├
# └──────┘
# ┌───────────────────────────────────────────────
# q_0: ┤0
# │ PARAMETERIZEDINITIALIZE(x[0],x[1],x[2],x[3])
# q_1: ┤1
# └───────────────────────────────────────────────

print(circuit.ordered_parameters)
# prints: [Parameter(p[0]), Parameter(p[1]), Parameter(p[2]), Parameter(p[3])]
Expand All @@ -55,11 +56,11 @@ class RawFeatureVector(BlueprintCircuit):
bound = circuit.assign_parameters(state)
print(bound.draw())
# prints:
# ┌──────────────────────────────────┐
# q_0: ┤0 ├
# │ initialize(0.70711,0,0,0.70711) │
# q_1: ┤1 ├
# └──────────────────────────────────┘
# ┌───────────────────────────────────────────────
# q_0: ┤0
# │ PARAMETERIZEDINITIALIZE(0.70711,0,0,0.70711) │
# q_1: ┤1
# └───────────────────────────────────────────────

"""

Expand All @@ -71,26 +72,31 @@ def __init__(self, feature_dimension: Optional[int]) -> None:
"""
super().__init__()

self._num_qubits = None # type: int
self._ordered_parameters = None # type: List[ParameterExpression]
self._num_qubits = None
self._ordered_parameters = ParameterVector('x')

if feature_dimension:
self.feature_dimension = feature_dimension

def _build(self):
super()._build()

# if the parameters are fully specified, use the initialize instruction
if len(self.parameters) == 0:
self.initialize(self._ordered_parameters, self.qubits) # pylint: disable=no-member
placeholder = ParameterizedInitialize(self._ordered_parameters[:])
self.append(placeholder, self.qubits)

# otherwise get a gate that acts as placeholder
else:
placeholder = Gate('Raw', self.num_qubits, self._ordered_parameters[:], label='Raw')
self.append(placeholder, self.qubits)
def _unsorted_parameters(self):
if self.data is None:
self._build()
return super()._unsorted_parameters()

def _check_configuration(self, raise_on_failure=True):
pass
if isinstance(self._ordered_parameters, ParameterVector):
self._ordered_parameters.resize(self.feature_dimension)
elif len(self._ordered_parameters) != self.feature_dimension:
if raise_on_failure:
raise ValueError('Mismatching number of parameters and feature dimension.')
return False
return True

@property
def num_qubits(self) -> int:
Expand All @@ -112,7 +118,6 @@ def num_qubits(self, num_qubits: int) -> None:
# invalidate the circuit
self._invalidate()
self._num_qubits = num_qubits
self._ordered_parameters = list(ParameterVector('p', length=self.feature_dimension))
self.add_register(QuantumRegister(self.num_qubits, 'q'))

@property
Expand Down Expand Up @@ -144,59 +149,31 @@ def feature_dimension(self, feature_dimension: int) -> None:

def _invalidate(self):
super()._invalidate()
self._ordered_parameters = None
self._num_qubits = None

@property
def parameters(self) -> Set[ParameterExpression]:
"""Return the free parameters in the RawFeatureVector.

Returns:
A set of the free parameters.
"""
return set(self.ordered_parameters)
class ParameterizedInitialize(Instruction):
"""A normalized parameterized initialize instruction."""

@property
def ordered_parameters(self) -> List[ParameterExpression]:
"""Return the free parameters in the RawFeatureVector.
def __init__(self, amplitudes):
num_qubits = np.log2(len(amplitudes))
if int(num_qubits) != num_qubits:
raise ValueError('feature_dimension must be a power of 2!')

Returns:
A list of the free parameters.
"""
return list(param for param in self._ordered_parameters
if isinstance(param, ParameterExpression))

def bind_parameters(self, values): # pylint: disable=arguments-differ
"""Bind parameters."""
if not isinstance(values, dict):
values = dict(zip(self.ordered_parameters, values))
return super().bind_parameters(values)

def assign_parameters(self, parameters, inplace=False): # pylint: disable=arguments-differ
"""Call the initialize instruction."""
if not isinstance(parameters, dict):
parameters = dict(zip(self.ordered_parameters, parameters))

if inplace:
dest = self
else:
dest = RawFeatureVector(2 ** self.num_qubits)
dest._build()
dest._ordered_parameters = self._ordered_parameters.copy()

# update the parameter list
for i, param in enumerate(dest._ordered_parameters):
if param in parameters.keys():
dest._ordered_parameters[i] = parameters[param]

# if fully bound call the initialize instruction
if len(dest.parameters) == 0:
dest._data = [] # wipe the current data
parameters = dest._ordered_parameters / np.linalg.norm(dest._ordered_parameters)
dest.initialize(parameters, dest.qubits) # pylint: disable=no-member

# else update the placeholder
else:
dest.data[0][0].params = dest._ordered_parameters

return None if inplace else dest
super().__init__('ParameterizedInitialize', int(num_qubits), 0, amplitudes)

def _define(self):
# cast ParameterExpressions that are fully bound to numbers
cleaned_params = []
for param in self.params:
if len(param.parameters) == 0:
cleaned_params.append(complex(param))
else:
raise QiskitError('Cannot define a ParameterizedInitialize with unbound parameters')

# normalize
normalized = np.array(cleaned_params) / np.linalg.norm(cleaned_params)

circuit = QuantumCircuit(self.num_qubits)
circuit.initialize(normalized, range(self.num_qubits))
self.definition = circuit
23 changes: 18 additions & 5 deletions test/circuit/library/test_raw_feature_vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@

import numpy as np
from qiskit import transpile, Aer
from qiskit.exceptions import QiskitError
from qiskit.algorithms.optimizers import COBYLA
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import EfficientSU2
from qiskit.algorithms.optimizers import COBYLA
from qiskit.exceptions import QiskitError
from qiskit.quantum_info import Statevector
from qiskit_machine_learning.circuit.library import RawFeatureVector
from qiskit_machine_learning.algorithms import VQC
from qiskit_machine_learning.datasets import wine
Expand Down Expand Up @@ -57,13 +58,13 @@ def test_fully_bound(self):
ref = QuantumCircuit(3)
ref.initialize(params, ref.qubits)

self.assertEqual(bound, ref)
self.assertEqual(bound.decompose(), ref)

def test_partially_bound(self):
"""Test partially binding the circuit works."""

circuit = RawFeatureVector(4)
params = circuit.ordered_parameters
params = circuit.parameters

with self.subTest('single numeric value'):
circuit.assign_parameters({params[0]: 0.2}, inplace=True)
Expand All @@ -77,7 +78,7 @@ def test_partially_bound(self):
bound = circuit.assign_parameters({params[2]: 0.4, params[3]: 0.8})
ref = QuantumCircuit(2)
ref.initialize([0.2, 0.4, 0.4, 0.8], ref.qubits)
self.assertEqual(bound, ref)
self.assertEqual(bound.decompose(), ref)

def test_usage_in_vqc(self):
"""Test using the circuit the a single VQC iteration works."""
Expand All @@ -97,6 +98,18 @@ def test_usage_in_vqc(self):
result = vqc.run(backend)
self.assertTrue(result['eval_count'] > 0)

def test_bind_after_composition(self):
"""Test binding the parameters after the circuit was composed onto a larger one."""
circuit = QuantumCircuit(2)
circuit.h([0, 1])

raw = RawFeatureVector(4)
circuit.append(raw, [0, 1])

bound = circuit.bind_parameters([1, 0, 0, 0])

self.assertTrue(Statevector.from_label('00').equiv(bound))


if __name__ == '__main__':
unittest.main()