Source code for qailab.gradient.gradient_calculation

  1"""Gradient and partial derivative calculation methods for parameterized quantum circuits."""
  2from typing import Literal
  3
  4import numpy as np
  5
  6from qiskit import QuantumCircuit
  7from qiskit.circuit import Parameter
  8from qiskit_machine_learning.gradients import (
  9    BaseSamplerGradient,
 10    LinCombSamplerGradient,
 11    SPSASamplerGradient,
 12    ParamShiftSamplerGradient,
 13)
 14
 15from quantum_launcher.base.base import Backend
 16from quantum_launcher.routines.qiskit_routines import QiskitBackend
 17
 18
 19def _param_grads_to_jacobian(grads, num_possible_values) -> np.ndarray:
 20    grads_list = [[g.get(i, 0) for i in range(num_possible_values)] for g in grads]
 21    return np.array(grads_list)
 22
 23
[docs] 24def calculate_jacobian( 25 circuit: QuantumCircuit, 26 set_params: dict[Parameter, int | float], 27 backend: Backend, 28 method: Literal['param_shift', 'spsa', 'lin_comb'] = 'param_shift', 29 shots: int = 1024 30) -> np.ndarray: 31 """ 32 For each parameter calculate partial derivatives w.r.t to each output value (possible measurement). 33 34 Args: 35 circuit (QuantumCircuit): Circuit to sample. 36 set_params (dict[Parameter, int | float]): Parameters for which to calculate derivatives and their current values. 37 backend (Backend): Backend to use. 38 method (Literal['param_shift', 'spsa', 'lin_comb'], optional): 39 Gradient algorithm to use. Defaults to 'param_shift'. 40 shots (int): How many shots to use for Sampler. 41 42 Raises: 43 ValueError: For unsupported method or backend. 44 45 Returns: 46 np.ndarray: `len(set_params) x 2^measured_qubits` matrix of partial derivatives. 47 """ 48 if not isinstance(backend, QiskitBackend): 49 raise ValueError("Only qiskit backends are supported.") 50 51 num_possible_values = 2**circuit.num_clbits 52 53 set_params_amp = {k: v for k, v in set_params.items() if 'Amp_Encoder' in k.name} 54 set_params_compatible = {k: v for k, v in set_params.items() if 'Amp_Encoder' not in k.name} 55 56 grads_amp = calculate_gradients_incompatible(circuit.assign_parameters(set_params_compatible), set_params_amp, backend, shots, 0.01) 57 grads_compat = calculate_gradients_compatible(circuit.assign_parameters(set_params_amp), set_params_compatible, backend, method, shots) 58 59 grads_amp = dict(zip(set_params_amp.keys(), grads_amp)) 60 grads_compat = dict(zip(set_params_compatible.keys(), grads_compat)) 61 62 grads = grads_amp | grads_compat 63 64 return _param_grads_to_jacobian([grads[k] for k in set_params.keys()], num_possible_values)
65 66
[docs] 67def calculate_gradients_compatible( 68 circuit: QuantumCircuit, 69 set_params: dict[Parameter, int | float], 70 backend: QiskitBackend, 71 method: Literal['param_shift', 'spsa', 'lin_comb'] = 'param_shift', 72 shots: int = 1024 73): 74 """ 75 Calculate gradients for parameters in compatible gates. 76 77 Args: 78 circuit (QuantumCircuit): Circuit to sample. 79 set_params (dict[Parameter, int | float]): Parameters for which to calculate derivatives and their current values. 80 backend (QiskitBackend): Backend to use. 81 method (Literal['param_shift', 'spsa', 'lin_comb'], optional): 82 Gradient algorithm to use. Defaults to 'param_shift'. 83 shots (int): How many shots to use for Sampler. 84 85 Raises: 86 ValueError: For unsupported method or backend. 87 88 Returns: 89 np.ndarray: `len(set_params) x 2^measured_qubits` matrix of partial derivatives. 90 """ 91 gradient_type = { 92 'param_shift': ParamShiftSamplerGradient, 93 'spsa': lambda sampler: SPSASamplerGradient(sampler, epsilon=0.001, batch_size=10), 94 'lin_comb': LinCombSamplerGradient, 95 }.get(method, None) 96 97 if gradient_type is None: 98 raise ValueError(f"Unsupported method {method}") 99 100 gradient_calc: BaseSamplerGradient = gradient_type(backend.samplerV1) 101 if len(set_params) == 0: 102 return [] 103 params, values = zip(*list(set_params.items())) 104 105 grads = gradient_calc.run([circuit], [values], [params], shots=shots).result().gradients[0] 106 return grads
107 108
[docs] 109def calculate_gradients_incompatible( 110 circuit: QuantumCircuit, 111 set_params: dict[Parameter, int | float], 112 backend: QiskitBackend, 113 shots: int = 1024, 114 epsilon: float = 0.01 115): 116 """ 117 Calculate gradients for parameters in incompatible gates, e.g. amplitude encoding parameters. 118 Calculation is done using the standard derivative formula (f(x+h) - f(x))/h 119 120 Args: 121 circuit (QuantumCircuit): Circuit to sample. 122 set_params (dict[Parameter, int | float]): Parameters for which to calculate derivatives and their current values. 123 backend (QiskitBackend): Backend to use. 124 shots (int): How many shots to use for Sampler. 125 epsilon (float): How much to shift the parameter. 126 127 Raises: 128 ValueError: For unsupported method or backend. 129 130 Returns: 131 np.ndarray: `len(set_params) x 2^measured_qubits` matrix of partial derivatives. 132 """ 133 initial_distribution = backend.samplerV1.run(circuit.assign_parameters(set_params), shots=shots).result().quasi_dists[0] 134 shifted_results = [] 135 for param in set_params: 136 distribution = backend.samplerV1.run( 137 circuit.assign_parameters( 138 set_params | {param: set_params[param] + epsilon} 139 ), 140 shots=shots).result().quasi_dists[0] 141 142 for k in initial_distribution: 143 distribution[k] = distribution.get(k, 0) - initial_distribution[k] 144 145 for k in distribution: 146 distribution[k] /= epsilon 147 148 shifted_results.append(distribution) 149 return shifted_results