Skip to content

Commit

Permalink
Rearrange the gradient result based on the order of the input paramet…
Browse files Browse the repository at this point in the history
…ers (Qiskit#9503)

* finite diff and spsa estimator gradient

* for param shift, lin_comb, and reverse gradients

* reverse estimator gradient

* implemented the new feature

* fix lint

* fix test

* fix and add reno

* fix

* fix style
  • Loading branch information
a-matsuo authored Mar 28, 2023
1 parent 7e09125 commit 67eaa4b
Show file tree
Hide file tree
Showing 20 changed files with 575 additions and 427 deletions.
98 changes: 45 additions & 53 deletions qiskit/algorithms/gradients/base_estimator_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
DerivativeType,
GradientCircuit,
_assign_unique_parameters,
_make_gradient_parameter_set,
_make_gradient_parameters,
_make_gradient_parameter_values,
)

Expand Down Expand Up @@ -103,7 +103,8 @@ def run(
parameters: The sequence of parameters to calculate only the gradients of
the specified parameters. Each sequence of parameters corresponds to a circuit in
``circuits``. Defaults to None, which means that the gradients of all parameters in
each circuit are calculated.
each circuit are calculated. None in the sequence means that the gradients of all
parameters in the corresponding circuit are calculated.
options: Primitive backend runtime options used for circuit execution.
The order of priority is: options in ``run`` method > gradient's
default options > primitive's default setting.
Expand All @@ -127,24 +128,24 @@ def run(

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]
parameters = [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)
parameters = [
params if params is not None else circuits[i].parameters
for i, params 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
Expand All @@ -155,7 +156,7 @@ def _run(
circuits: Sequence[QuantumCircuit],
observables: Sequence[BaseOperator | PauliSumOp],
parameter_values: Sequence[Sequence[float]],
parameter_sets: Sequence[set[Parameter]],
parameters: Sequence[Sequence[Parameter]],
**options,
) -> EstimatorGradientResult:
"""Compute the estimator gradients on the given circuits."""
Expand All @@ -165,17 +166,17 @@ def _preprocess(
self,
circuits: Sequence[QuantumCircuit],
parameter_values: Sequence[Sequence[float]],
parameter_sets: Sequence[set[Parameter]],
parameters: Sequence[Sequence[Parameter]],
supported_gates: Sequence[str],
) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[set[Parameter]]]:
) -> tuple[Sequence[QuantumCircuit], Sequence[Sequence[float]], Sequence[Sequence[Parameter]]]:
"""Preprocess the gradient. This makes a gradient circuit for each circuit. The gradient
circuit is a transpiled circuit by using the supported gates, and has unique parameters.
``parameter_values`` and ``parameters`` are also updated to match the gradient circuit.
Args:
circuits: The list of quantum circuits to compute the gradients.
parameter_values: The list of parameter values to be bound to the circuit.
parameter_sets: The sequence of parameters to calculate only the gradients of the specified
parameters: The sequence of parameters to calculate only the gradients of the specified
parameters.
supported_gates: The supported gates used to transpile the circuit.
Expand All @@ -184,10 +185,8 @@ def _preprocess(
parameter_values and parameters are updated to match the gradient circuit.
"""
translator = TranslateParameterizedGates(supported_gates)
g_circuits, g_parameter_values, g_parameter_sets = [], [], []
for circuit, parameter_value_, parameter_set in zip(
circuits, parameter_values, parameter_sets
):
g_circuits, g_parameter_values, g_parameters = [], [], []
for circuit, parameter_value_, parameters_ in zip(circuits, parameter_values, parameters):
circuit_key = _circuit_key(circuit)
if circuit_key not in self._gradient_circuit_cache:
unrolled = translator(circuit)
Expand All @@ -197,15 +196,15 @@ def _preprocess(
g_parameter_values.append(
_make_gradient_parameter_values(circuit, gradient_circuit, parameter_value_)
)
g_parameter_sets.append(_make_gradient_parameter_set(gradient_circuit, parameter_set))
return g_circuits, g_parameter_values, g_parameter_sets
g_parameters.append(_make_gradient_parameters(gradient_circuit, parameters_))
return g_circuits, g_parameter_values, g_parameters

def _postprocess(
self,
results: EstimatorGradientResult,
circuits: Sequence[QuantumCircuit],
parameter_values: Sequence[Sequence[float]],
parameter_sets: Sequence[set[Parameter]],
parameters: Sequence[Sequence[Parameter]],
) -> EstimatorGradientResult:
"""Postprocess the gradients. This method computes the gradient of the original circuits
by applying the chain rule to the gradient of the circuits with unique parameters.
Expand All @@ -214,36 +213,30 @@ def _postprocess(
results: The computed gradients for the circuits with unique parameters.
circuits: The list of original circuits submitted for gradient computation.
parameter_values: The list of parameter values to be bound to the circuits.
parameter_sets: An optional subset of parameters with respect to which the gradients should
be calculated.
parameters: The sequence of parameters to calculate only the gradients of the specified
parameters.
Returns:
The gradients of the original circuits.
"""
gradients, metadata = [], []
for idx, (circuit, parameter_values_, parameter_set) in enumerate(
zip(circuits, parameter_values, parameter_sets)
for idx, (circuit, parameter_values_, parameters_) in enumerate(
zip(circuits, parameter_values, parameters)
):
gradient = np.zeros(len(parameter_set))
gradient = np.zeros(len(parameters_))
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")
gradient_circuit = self._gradient_circuit_cache[_circuit_key(circuit)]
g_parameter_set = _make_gradient_parameter_set(gradient_circuit, parameter_set)
g_parameters = _make_gradient_parameters(gradient_circuit, parameters_)
# Make a map from the gradient parameter to the respective index in the gradient.
parameter_indices = [param for param in circuit.parameters if param in parameter_set]
g_parameter_indices = [
param
for param in gradient_circuit.gradient_circuit.parameters
if param in g_parameter_set
]
g_parameter_indices = {param: i for i, param in enumerate(g_parameter_indices)}
g_parameter_indices = {param: i for i, param in enumerate(g_parameters)}
# Compute the original gradient from the gradient of the gradient circuit
# by using the chain rule.
for i, parameter in enumerate(parameter_indices):
for i, parameter in enumerate(parameters_):
for g_parameter, coeff in gradient_circuit.parameter_map[parameter]:
# Compute the coefficient
if isinstance(coeff, ParameterExpression):
Expand All @@ -261,7 +254,7 @@ def _postprocess(
* results.gradients[idx][g_parameter_indices[g_parameter]]
)
gradients.append(gradient)
metadata.append([{"parameters": parameter_indices}])
metadata.append({"parameters": parameters_})
return EstimatorGradientResult(
gradients=gradients, metadata=metadata, options=results.options
)
Expand All @@ -271,17 +264,16 @@ 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.
Args:
circuits: The list of quantum circuits to compute the gradients.
observables: The list of observables.
parameter_values: The list of parameter values to be bound to the circuit.
parameter_sets: The Sequence of parameter sets to calculate only the gradients of
the specified parameters. Each set of parameters corresponds to a circuit in
``circuits``.
parameters: The sequence of parameters to calculate only the gradients of the specified
parameters.
Raises:
ValueError: Invalid arguments are given.
Expand All @@ -292,18 +284,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 +293,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,10 +307,16 @@ 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 len(circuits) != len(parameters):
raise ValueError(
f"The number of circuits ({len(circuits)}) does not match "
f"the number of the list of specified parameters ({len(parameters)})."
)

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

Expand Down
Loading

0 comments on commit 67eaa4b

Please sign in to comment.