Skip to content

Commit

Permalink
added _rearrange_results
Browse files Browse the repository at this point in the history
  • Loading branch information
a-matsuo committed Feb 1, 2023
1 parent c9e0a42 commit 9eb1744
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 90 deletions.
131 changes: 98 additions & 33 deletions qiskit/algorithms/gradients/base_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ def __init__(
estimator: BaseEstimator,
options: Options | None = None,
derivative_type: DerivativeType = DerivativeType.REAL,
unique_parameters: bool = None,
supported_gates: Sequence[str] = None,
):
r"""
Args:
Expand All @@ -68,13 +70,21 @@ def __init__(
Defaults to ``DerivativeType.REAL``, as this yields e.g. the commonly-used energy
gradient and this type is the only supported type for function-level schemes like
finite difference.
unique_parameters: Whether to assign unique parameters to each circuit. If ``True``, the
gradient circuits will have unique parameters, which can be useful for gradients
with the chain rule, e.g. parameter shift. If ``False``, the gradient circuits will
have the same parameters as the original circuits, which can be useful for e.g.
finite difference. Defaults to ``True``.
supported_gates: The list of gates that the gradient supports. Other gates will be
decomposed into supported gates. If ``None``, the gradient supports all gates.
"""
self._estimator: BaseEstimator = estimator
self._default_options = Options()
if options is not None:
self._default_options.update_options(**options)
self._derivative_type = derivative_type

self._unique_parameters = unique_parameters
self._supported_gates = supported_gates
self._gradient_circuit_cache: dict[QuantumCircuit, GradientCircuit] = {}

@property
Expand Down Expand Up @@ -125,32 +135,60 @@ def run(
# Allow a single observable to be passed in.
observables = (observables,)

if parameters is None:
# If parameters is None, we calculate the gradients of all parameters in each circuit.
parameter_sets = [set(circuit.parameters) for circuit in circuits]
else:
# If parameters is not None, we calculate the gradients of the specified parameters.
# None in parameters means that the gradients of all parameters in the corresponding
# circuit are calculated.
parameter_sets = [
set(parameters_) if parameters_ is not None else set(circuits[i].parameters)
for i, parameters_ in enumerate(parameters)
]
# Validate the arguments.
self._validate_arguments(circuits, observables, parameter_values, parameter_sets)
self._validate_arguments(circuits, observables, parameter_values, parameters)
# The priority of run option is as follows:
# options in ``run`` method > gradient's default options > primitive's default setting.
opts = copy(self._default_options)
opts.update_options(**options)
# Run the job.
job = AlgorithmJob(
self._run, circuits, observables, parameter_values, parameter_sets, **opts.__dict__
self._run, circuits, observables, parameter_values, parameters, **opts.__dict__
)
job.submit()
return job

@abstractmethod
def _run(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
parameter_values: Sequence[Sequence[float]],
parameters: Sequence[Sequence[Parameter] | None] | None = None,
**options,
) -> EstimatorGradientResult:
"""Run the job of the estimator gradient on the given circuits."""
if parameters is None:
# If parameters is None, we calculate the gradients of all parameters in each circuit.
differentiated_parameters = [circuit.parameters.data for circuit in circuits]
parameter_sets = [set(parameters_) for parameters_ in differentiated_parameters]
else:
# If parameters is not None, we calculate the gradients of the specified parameters.
# None in parameters means that the gradients of all parameters in the corresponding
# circuit are calculated.
differentiated_parameters = [
parameters_ if parameters_ is not None else circuit.parameters.data
for circuit, parameters_ in zip(circuits, parameters)
]
parameter_sets = [set(parameters_) for parameters_ in differentiated_parameters]

if self._unique_parameters:
# If unique_parameters is True, we assign unique parameters to each circuit.
g_circuits, g_parameter_values, g_parameter_sets = self._preprocess(
circuits, parameter_values, parameter_sets, self._supported_gates
)
unique_results = self._calc(
g_circuits, observables, g_parameter_values, g_parameter_sets, **options
)
results = self._postprocess(unique_results, circuits, parameter_values, parameter_sets)
else:
# If unique_parameters is False, we use the same parameters in the gradient circuits.
results = self._calc(circuits, observables, parameter_values, parameter_sets, **options)

# Return the final results after rearranging the results.
return self._rearrange_results(results, differentiated_parameters)

@abstractmethod
def _calc(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
Expand Down Expand Up @@ -261,17 +299,43 @@ def _postprocess(
* results.gradients[idx][g_parameter_indices[g_parameter]]
)
gradients.append(gradient)
metadata.append([{"parameters": parameter_indices}])
results.metadata[idx]["parameters"] = parameter_indices
metadata.append(results.metadata[idx])
return EstimatorGradientResult(
gradients=gradients, metadata=metadata, options=results.options
)

@staticmethod
def _rearrange_results(
results: EstimatorGradientResult,
parameters: Sequence[Sequence[Parameter]],
) -> EstimatorGradientResult:
"""Rearrange the results to match the order of the input parameters."""
rearranged_gradients, rearranged_metadata = [], []
for idx, parameter in enumerate(parameters):
gradient = np.zeros(len(parameter))
metadata = [{"parameters": parameter}]
if (
"derivative_type" in results.metadata[idx]
and results.metadata[idx]["derivative_type"] == DerivativeType.COMPLEX
):
# If the derivative type is complex, cast the gradient to complex.
gradient = gradient.astype("complex")
# Rearrange the gradient
for i, param in enumerate(results.metadata[idx]["parameters"]):
gradient[parameter.index(param)] = results.gradients[idx][i]
rearranged_gradients.append(gradient)
rearranged_metadata.append(metadata)
return EstimatorGradientResult(
gradients=rearranged_gradients, metadata=rearranged_metadata, options=results.options
)

@staticmethod
def _validate_arguments(
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
parameter_values: Sequence[Sequence[float]],
parameter_sets: Sequence[set[Parameter]],
parameters: Sequence[Sequence[Parameter]],
) -> None:
"""Validate the arguments of the ``run`` method.
Expand All @@ -292,18 +356,6 @@ def _validate_arguments(
f"the number of parameter value sets ({len(parameter_values)})."
)

if len(circuits) != len(observables):
raise ValueError(
f"The number of circuits ({len(circuits)}) does not match "
f"the number of observables ({len(observables)})."
)

if len(circuits) != len(parameter_sets):
raise ValueError(
f"The number of circuits ({len(circuits)}) does not match "
f"the number of the specified parameter sets ({len(parameter_sets)})."
)

for i, (circuit, parameter_value) in enumerate(zip(circuits, parameter_values)):
if not circuit.num_parameters:
raise ValueError(f"The {i}-th circuit is not parameterised.")
Expand All @@ -313,6 +365,12 @@ def _validate_arguments(
f"the number of parameters ({circuit.num_parameters}) for the {i}-th circuit."
)

if len(circuits) != len(observables):
raise ValueError(
f"The number of circuits ({len(circuits)}) does not match "
f"the number of observables ({len(observables)})."
)

for i, (circuit, observable) in enumerate(zip(circuits, observables)):
if circuit.num_qubits != observable.num_qubits:
raise ValueError(
Expand All @@ -321,13 +379,20 @@ def _validate_arguments(
f"({observable.num_qubits})."
)

for i, (circuit, parameter_set) in enumerate(zip(circuits, parameter_sets)):
if not set(parameter_set).issubset(circuit.parameters):
if parameters is not None:
if len(circuits) != len(parameters):
raise ValueError(
f"The {i}-th parameter set contains parameters not present in the "
f"{i}-th circuit."
f"The number of circuits ({len(circuits)}) does not match "
f"the number of the specified parameter sets ({len(parameters)})."
)

for i, (circuit, parameter) in enumerate(zip(circuits, parameters)):
if parameter is not None and not set(parameter).issubset(circuit.parameters):
raise ValueError(
f"The {i}-th parameters contains parameters not present in the "
f"{i}-th circuit."
)

@property
def options(self) -> Options:
"""Return the union of estimator options setting and gradient default options,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def __init__(
self._method = method
super().__init__(estimator, options)

def _run(
def _calc(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
Expand Down
44 changes: 25 additions & 19 deletions qiskit/algorithms/gradients/lin_comb_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,31 +88,37 @@ def __init__(
Higher priority setting overrides lower priority setting.
"""
self._lin_comb_cache = {}
super().__init__(estimator, options, derivative_type=derivative_type)
super().__init__(
estimator,
options,
derivative_type=derivative_type,
unique_parameters=True,
supported_gates=self.SUPPORTED_GATES,
)

@BaseEstimatorGradient.derivative_type.setter
def derivative_type(self, derivative_type: DerivativeType) -> None:
"""Set the derivative type."""
self._derivative_type = derivative_type

def _run(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
parameter_values: Sequence[Sequence[float]],
parameter_sets: Sequence[set[Parameter]],
**options,
) -> EstimatorGradientResult:
"""Compute the estimator gradients on the given circuits."""
g_circuits, g_parameter_values, g_parameter_sets = self._preprocess(
circuits, parameter_values, parameter_sets, self.SUPPORTED_GATES
)
results = self._run_unique(
g_circuits, observables, g_parameter_values, g_parameter_sets, **options
)
return self._postprocess(results, circuits, parameter_values, parameter_sets)

def _run_unique(
# def _run(
# self,
# circuits: Sequence[QuantumCircuit],
# observables: Sequence[BaseOperator | PauliSumOp],
# parameter_values: Sequence[Sequence[float]],
# parameter_sets: Sequence[set[Parameter]],
# **options,
# ) -> EstimatorGradientResult:
# """Compute the estimator gradients on the given circuits."""
# g_circuits, g_parameter_values, g_parameter_sets = self._preprocess(
# circuits, parameter_values, parameter_sets, self.SUPPORTED_GATES
# )
# results = self._run_unique(
# g_circuits, observables, g_parameter_values, g_parameter_sets, **options
# )
# return self._postprocess(results, circuits, parameter_values, parameter_sets)

def _calc(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
Expand Down
46 changes: 29 additions & 17 deletions qiskit/algorithms/gradients/param_shift_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

from qiskit.circuit import Parameter, QuantumCircuit
from qiskit.opflow import PauliSumOp
from qiskit.primitives import BaseEstimator
from qiskit.providers import Options
from qiskit.quantum_info.operators.base_operator import BaseOperator

from .base_estimator_gradient import BaseEstimatorGradient
Expand Down Expand Up @@ -55,24 +57,34 @@ class ParamShiftEstimatorGradient(BaseEstimatorGradient):
"rzx",
]

def _run(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
parameter_values: Sequence[Sequence[float]],
parameter_sets: Sequence[set[Parameter]],
**options,
) -> EstimatorGradientResult:
"""Compute the gradients of the expectation values by the parameter shift rule."""
g_circuits, g_parameter_values, g_parameter_sets = self._preprocess(
circuits, parameter_values, parameter_sets, self.SUPPORTED_GATES
)
results = self._run_unique(
g_circuits, observables, g_parameter_values, g_parameter_sets, **options
)
return self._postprocess(results, circuits, parameter_values, parameter_sets)
def __init__(self, estimator: BaseEstimator, options: Options | None = None):
"""
Args:
estimator: The estimator to compute the expectation values.
epsilon: The epsilon value for the parameter shift rule.
"""
super().__init__(estimator, options, unique_parameters=True, supported_gates=self.SUPPORTED_GATES)



# def _run(
# self,
# circuits: Sequence[QuantumCircuit],
# observables: Sequence[BaseOperator | PauliSumOp],
# parameter_values: Sequence[Sequence[float]],
# parameter_sets: Sequence[set[Parameter]],
# **options,
# ) -> EstimatorGradientResult:
# """Compute the gradients of the expectation values by the parameter shift rule."""
# g_circuits, g_parameter_values, g_parameter_sets = self._preprocess(
# circuits, parameter_values, parameter_sets, self.SUPPORTED_GATES
# )
# results = self._run_unique(
# g_circuits, observables, g_parameter_values, g_parameter_sets, **options
# )
# return self._postprocess(results, circuits, parameter_values, parameter_sets)

def _run_unique(
def _calc(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
Expand Down
38 changes: 19 additions & 19 deletions qiskit/algorithms/gradients/reverse_gradient/reverse_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,31 +65,31 @@ def __init__(self, derivative_type: DerivativeType = DerivativeType.REAL):
of the gradient is returned.
"""
dummy_estimator = Estimator() # this is required by the base class, but not used
super().__init__(dummy_estimator, derivative_type=derivative_type)
super().__init__(dummy_estimator, derivative_type=derivative_type, unique_parameters=True, supported_gates=self.SUPPORTED_GATES)

@BaseEstimatorGradient.derivative_type.setter
def derivative_type(self, derivative_type: DerivativeType) -> None:
"""Set the derivative type."""
self._derivative_type = derivative_type

def _run(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
parameter_values: Sequence[Sequence[float]],
parameter_sets: Sequence[set[Parameter]],
**options,
) -> EstimatorGradientResult:
"""Compute the gradients of the expectation values by the parameter shift rule."""
g_circuits, g_parameter_values, g_parameter_sets = self._preprocess(
circuits, parameter_values, parameter_sets, self.SUPPORTED_GATES
)
results = self._run_unique(
g_circuits, observables, g_parameter_values, g_parameter_sets, **options
)
return self._postprocess(results, circuits, parameter_values, parameter_sets)

def _run_unique(
# def _calc(
# self,
# circuits: Sequence[QuantumCircuit],
# observables: Sequence[BaseOperator | PauliSumOp],
# parameter_values: Sequence[Sequence[float]],
# parameter_sets: Sequence[set[Parameter]],
# **options,
# ) -> EstimatorGradientResult:
# """Compute the gradients of the expectation values by the parameter shift rule."""
# g_circuits, g_parameter_values, g_parameter_sets = self._preprocess(
# circuits, parameter_values, parameter_sets, self.SUPPORTED_GATES
# )
# results = self._run_unique(
# g_circuits, observables, g_parameter_values, g_parameter_sets, **options
# )
# return self._postprocess(results, circuits, parameter_values, parameter_sets)

def _calc(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
Expand Down
2 changes: 1 addition & 1 deletion qiskit/algorithms/gradients/spsa_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(

super().__init__(estimator, options)

def _run(
def _calc(
self,
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
Expand Down

0 comments on commit 9eb1744

Please sign in to comment.