def expectation(wavefn, operator, backend): """Returns the expectation value of an operator with the provided wavefunction using the provided backend""" exp = ExpectationFactory.build(operator=operator, backend=backend) composed_circuit = exp.convert(StateFn(operator, is_measurement=True)).compose( CircuitStateFn(wavefn)) sampler = CircuitSampler(backend) vals = sampler.convert(composed_circuit).eval() return np.real(vals)
def _hessian_states( self, state_op: StateFn, meas_op: Optional[OperatorBase] = None, target_params: Optional[Union[Tuple[ParameterExpression, ParameterExpression], List[Tuple[ParameterExpression, ParameterExpression]]]] = None ) -> OperatorBase: """Generate the operator states whose evaluation returns the Hessian (items). Args: state_op: The operator representing the quantum state for which we compute the Hessian. meas_op: The operator representing the observable for which we compute the gradient. target_params: The parameters we are computing the Hessian wrt: ω Returns: Operators which give the Hessian. If a parameter appears multiple times, one circuit is created per parameterized gates to compute the product rule. Raises: AquaError: If one of the circuits could not be constructed. TypeError: If ``operator`` is of unsupported type. """ state_qc = deepcopy(state_op.primitive) if isinstance(target_params, list) and isinstance( target_params[0], tuple): tuples_list = deepcopy(target_params) target_params = [] for tuples in tuples_list: if all([ param in state_qc._parameter_table.get_keys() for param in tuples ]): for param in tuples: if param not in target_params: target_params.append(param) elif isinstance(target_params, tuple): tuples_list = deepcopy([target_params]) target_params = [] for tuples in tuples_list: if all([ param in state_qc._parameter_table.get_keys() for param in tuples ]): for param in tuples: if param not in target_params: target_params.append(param) else: raise TypeError( 'Please define in the parameters for which the Hessian is evaluated either ' 'as parameter tuple or a list of parameter tuples') qr_add0 = QuantumRegister(1, 'work_qubit0') work_q0 = qr_add0[0] qr_add1 = QuantumRegister(1, 'work_qubit1') work_q1 = qr_add1[0] # create a copy of the original circuit with an additional working qubit register circuit = state_qc.copy() circuit.add_register(qr_add0, qr_add1) # Get the circuits needed to compute the Hessian hessian_ops = None for param_a, param_b in tuples_list: if param_a not in state_qc._parameter_table.get_keys() or param_b \ not in state_qc._parameter_table.get_keys(): hessian_op = ~Zero @ One else: param_gates_a = state_qc._parameter_table[param_a] param_gates_b = state_qc._parameter_table[param_b] for i, param_occurence_a in enumerate(param_gates_a): coeffs_a, gates_a = self._gate_gradient_dict( param_occurence_a[0])[param_occurence_a[1]] # apply Hadamard on working qubit self.insert_gate(circuit, param_occurence_a[0], HGate(), qubits=[work_q0]) self.insert_gate(circuit, param_occurence_a[0], HGate(), qubits=[work_q1]) for j, gate_to_insert_a in enumerate(gates_a): coeff_a = coeffs_a[j] hessian_circuit_temp = QuantumCircuit(*circuit.qregs) hessian_circuit_temp.data = circuit.data # Fix working qubit 0 phase sign = np.sign(coeff_a) is_complex = np.iscomplex(coeff_a) if sign == -1: if is_complex: self.insert_gate(hessian_circuit_temp, param_occurence_a[0], SdgGate(), qubits=[work_q0]) else: self.insert_gate(hessian_circuit_temp, param_occurence_a[0], ZGate(), qubits=[work_q0]) else: if is_complex: self.insert_gate(hessian_circuit_temp, param_occurence_a[0], SGate(), qubits=[work_q0]) # Insert controlled, intercepting gate - controlled by |1> if isinstance(param_occurence_a[0], UGate): if param_occurence_a[1] == 0: self.insert_gate( hessian_circuit_temp, param_occurence_a[0], RZGate(param_occurence_a[0].params[2])) self.insert_gate(hessian_circuit_temp, param_occurence_a[0], RXGate(np.pi / 2)) self.insert_gate(hessian_circuit_temp, param_occurence_a[0], gate_to_insert_a, additional_qubits=([work_q0], [])) self.insert_gate(hessian_circuit_temp, param_occurence_a[0], RXGate(-np.pi / 2)) self.insert_gate( hessian_circuit_temp, param_occurence_a[0], RZGate(-param_occurence_a[0].params[2])) elif param_occurence_a[1] == 1: self.insert_gate(hessian_circuit_temp, param_occurence_a[0], gate_to_insert_a, after=True, additional_qubits=([work_q0], [])) else: self.insert_gate(hessian_circuit_temp, param_occurence_a[0], gate_to_insert_a, additional_qubits=([work_q0], [])) else: self.insert_gate(hessian_circuit_temp, param_occurence_a[0], gate_to_insert_a, additional_qubits=([work_q0], [])) for m, param_occurence_b in enumerate(param_gates_b): coeffs_b, gates_b = self._gate_gradient_dict( param_occurence_b[0])[param_occurence_b[1]] for n, gate_to_insert_b in enumerate(gates_b): coeff_b = coeffs_b[n] # create a copy of the original circuit with the same registers hessian_circuit = QuantumCircuit( *hessian_circuit_temp.qregs) hessian_circuit.data = hessian_circuit_temp.data # Fix working qubit 1 phase sign = np.sign(coeff_b) is_complex = np.iscomplex(coeff_b) if sign == -1: if is_complex: self.insert_gate(hessian_circuit, param_occurence_b[0], SdgGate(), qubits=[work_q1]) else: self.insert_gate(hessian_circuit, param_occurence_b[0], ZGate(), qubits=[work_q1]) else: if is_complex: self.insert_gate(hessian_circuit, param_occurence_b[0], SGate(), qubits=[work_q1]) # Insert controlled, intercepting gate - controlled by |1> if isinstance(param_occurence_b[0], UGate): if param_occurence_b[1] == 0: self.insert_gate( hessian_circuit, param_occurence_b[0], RZGate(param_occurence_b[0]. params[2])) self.insert_gate( hessian_circuit, param_occurence_b[0], RXGate(np.pi / 2)) self.insert_gate( hessian_circuit, param_occurence_b[0], gate_to_insert_b, additional_qubits=([work_q1], [])) self.insert_gate( hessian_circuit, param_occurence_b[0], RXGate(-np.pi / 2)) self.insert_gate( hessian_circuit, param_occurence_b[0], RZGate(-param_occurence_b[0]. params[2])) elif param_occurence_b[1] == 1: self.insert_gate( hessian_circuit, param_occurence_b[0], gate_to_insert_b, after=True, additional_qubits=([work_q1], [])) else: self.insert_gate( hessian_circuit, param_occurence_b[0], gate_to_insert_b, additional_qubits=([work_q1], [])) else: self.insert_gate( hessian_circuit, param_occurence_b[0], gate_to_insert_b, additional_qubits=([work_q1], [])) hessian_circuit.h(work_q0) hessian_circuit.cz(work_q1, work_q0) hessian_circuit.h(work_q1) term = state_op.coeff * np.sqrt(np.abs(coeff_a) * np.abs(coeff_b)) \ * CircuitStateFn(hessian_circuit) # Chain Rule Parameter Expression gate_param_a = param_occurence_a[0].params[ param_occurence_a[1]] gate_param_b = param_occurence_b[0].params[ param_occurence_b[1]] if meas_op: meas = deepcopy(meas_op) if isinstance(gate_param_a, ParameterExpression): expr_grad = DerivativeBase.parameter_expression_grad( gate_param_a, param_a) meas *= expr_grad if isinstance(gate_param_b, ParameterExpression): expr_grad = DerivativeBase.parameter_expression_grad( gate_param_a, param_a) meas *= expr_grad term = meas @ term else: term = ListOp([term], combo_fn=partial( self._hess_combo_fn, state_op=state_op)) if isinstance(gate_param_a, ParameterExpression): expr_grad = DerivativeBase.parameter_expression_grad( gate_param_a, param_a) term *= expr_grad if isinstance(gate_param_b, ParameterExpression): expr_grad = DerivativeBase.parameter_expression_grad( gate_param_a, param_a) term *= expr_grad if i == 0 and j == 0 and m == 0 and n == 0: hessian_op = term else: # Product Rule hessian_op += term # Create a list of Hessian elements w.r.t. the given parameter tuples if len(tuples_list) == 1: return hessian_op else: if not hessian_ops: hessian_ops = [hessian_op] else: hessian_ops += [hessian_op] return ListOp(hessian_ops)
def _gradient_states( self, state_op: StateFn, meas_op: Optional[OperatorBase] = None, target_params: Optional[Union[ParameterExpression, ParameterVector, List[ParameterExpression]]] = None ) -> ListOp: """Generate the gradient states. Args: state_op: The operator representing the quantum state for which we compute the gradient. meas_op: The operator representing the observable for which we compute the gradient. target_params: The parameters we are taking the gradient wrt: ω Returns: ListOp of StateFns as quantum circuits which are the states w.r.t. which we compute the gradient. If a parameter appears multiple times, one circuit is created per parameterized gates to compute the product rule. Raises: AquaError: If one of the circuits could not be constructed. TypeError: If the operators is of unsupported type. """ state_qc = deepcopy(state_op.primitive) # Define the working qubit to realize the linear combination of unitaries qr_work = QuantumRegister(1, 'work_qubit_lin_comb_grad') work_q = qr_work[0] if not isinstance(target_params, (list, np.ndarray)): target_params = [target_params] if len(target_params) > 1: states = None additional_qubits: Tuple[List[Qubit], List[Qubit]] = ([work_q], []) for param in target_params: if param not in state_qc._parameter_table.get_keys(): op = ~Zero @ One else: param_gates = state_qc._parameter_table[param] for m, param_occurence in enumerate(param_gates): coeffs, gates = self._gate_gradient_dict( param_occurence[0])[param_occurence[1]] # construct the states for k, gate_to_insert in enumerate(gates): grad_state = QuantumCircuit(*state_qc.qregs, qr_work) grad_state.compose(state_qc, inplace=True) # apply Hadamard on work_q self.insert_gate(grad_state, param_occurence[0], HGate(), qubits=[work_q]) # Fix work_q phase coeff_i = coeffs[k] sign = np.sign(coeff_i) is_complex = np.iscomplex(coeff_i) if sign == -1: if is_complex: self.insert_gate(grad_state, param_occurence[0], SdgGate(), qubits=[work_q]) else: self.insert_gate(grad_state, param_occurence[0], ZGate(), qubits=[work_q]) else: if is_complex: self.insert_gate(grad_state, param_occurence[0], SGate(), qubits=[work_q]) # Insert controlled, intercepting gate - controlled by |0> if isinstance(param_occurence[0], UGate): if param_occurence[1] == 0: self.insert_gate( grad_state, param_occurence[0], RZGate(param_occurence[0].params[2])) self.insert_gate(grad_state, param_occurence[0], RXGate(np.pi / 2)) self.insert_gate( grad_state, param_occurence[0], gate_to_insert, additional_qubits=additional_qubits) self.insert_gate(grad_state, param_occurence[0], RXGate(-np.pi / 2)) self.insert_gate( grad_state, param_occurence[0], RZGate(-param_occurence[0].params[2])) elif param_occurence[1] == 1: self.insert_gate( grad_state, param_occurence[0], gate_to_insert, after=True, additional_qubits=additional_qubits) else: self.insert_gate( grad_state, param_occurence[0], gate_to_insert, additional_qubits=additional_qubits) else: self.insert_gate( grad_state, param_occurence[0], gate_to_insert, additional_qubits=additional_qubits) grad_state.h(work_q) state = np.sqrt( np.abs(coeff_i)) * state_op.coeff * CircuitStateFn( grad_state) # Chain Rule parameter expressions gate_param = param_occurence[0].params[ param_occurence[1]] if meas_op: if gate_param == param: state = meas_op @ state else: if isinstance(gate_param, ParameterExpression): expr_grad = DerivativeBase.parameter_expression_grad( gate_param, param) state = (expr_grad * meas_op) @ state else: state = ~Zero @ One else: if gate_param == param: state = ListOp([state], combo_fn=partial( self._grad_combo_fn, state_op=state_op)) else: if isinstance(gate_param, ParameterExpression): expr_grad = DerivativeBase.parameter_expression_grad( gate_param, param) state = expr_grad * ListOp( [state], combo_fn=partial(self._grad_combo_fn, state_op=state_op)) else: state = ~Zero @ One if m == 0 and k == 0: op = state else: # Product Rule op += state if len(target_params) > 1: if not states: states = [op] else: states += [op] else: return op if len(target_params) > 1: return ListOp(states) else: return op
def convert( self, operator: CircuitStateFn, params: Optional[Union[ParameterExpression, ParameterVector, List[ParameterExpression]]] = None, ) -> ListOp: r""" Args: operator: The operator corresponding to the quantum state :math:`|\psi(\omega)\rangle` for which we compute the QFI. params: The parameters :math:`\omega` with respect to which we are computing the QFI. Returns: A ``ListOp[ListOp]`` where the operator at position ``[k][l]`` corresponds to the matrix element :math:`k, l` of the QFI. Raises: AquaError: If one of the circuits could not be constructed. TypeError: If ``operator`` is an unsupported type. """ # QFI & phase fix observable qfi_observable = ~StateFn(4 * Z ^ (I ^ operator.num_qubits)) phase_fix_observable = ~StateFn((X + 1j * Y) ^ (I ^ operator.num_qubits)) # see https://arxiv.org/pdf/quant-ph/0108146.pdf # Check if the given operator corresponds to a quantum state given as a circuit. if not isinstance(operator, CircuitStateFn): raise TypeError( 'LinCombFull is only compatible with states that are given as CircuitStateFn' ) # If a single parameter is given wrap it into a list. if not isinstance(params, (list, np.ndarray)): params = [params] state_qc = operator.primitive # First, the operators are computed which can compensate for a potential phase-mismatch # between target and trained state, i.e.〈ψ|∂lψ〉 phase_fix_states = None # Add working qubit qr_work = QuantumRegister(1, 'work_qubit') work_q = qr_work[0] additional_qubits: Tuple[List[Qubit], List[Qubit]] = ([work_q], []) for param in params: # Get the gates of the given quantum state which are parameterized by param param_gates = state_qc._parameter_table[param] # Loop through the occurrences of param in the quantum state for m, param_occurence in enumerate(param_gates): # Get the coefficients and gates for the linear combination gradient for each # occurrence, see e.g. https://arxiv.org/abs/2006.06004 coeffs_i, gates_i = LinComb._gate_gradient_dict( param_occurence[0])[param_occurence[1]] # Construct the quantum states which are then evaluated for the respective QFI # element. for k, gate_to_insert_i in enumerate(gates_i): grad_state = state_qc.copy() grad_state.add_register(qr_work) # apply Hadamard on work_q LinComb.insert_gate(grad_state, param_occurence[0], HGate(), qubits=[work_q]) # Fix work_q phase such that the gradient is correct. coeff_i = coeffs_i[k] sign = np.sign(coeff_i) is_complex = np.iscomplex(coeff_i) if sign == -1: if is_complex: LinComb.insert_gate(grad_state, param_occurence[0], SdgGate(), qubits=[work_q]) else: LinComb.insert_gate(grad_state, param_occurence[0], ZGate(), qubits=[work_q]) else: if is_complex: LinComb.insert_gate(grad_state, param_occurence[0], SGate(), qubits=[work_q]) # Insert controlled, intercepting gate - controlled by |0> if isinstance(param_occurence[0], UGate): if param_occurence[1] == 0: LinComb.insert_gate( grad_state, param_occurence[0], RZGate(param_occurence[0].params[2])) LinComb.insert_gate(grad_state, param_occurence[0], RXGate(np.pi / 2)) LinComb.insert_gate( grad_state, param_occurence[0], gate_to_insert_i, additional_qubits=additional_qubits) LinComb.insert_gate(grad_state, param_occurence[0], RXGate(-np.pi / 2)) LinComb.insert_gate( grad_state, param_occurence[0], RZGate(-param_occurence[0].params[2])) elif param_occurence[1] == 1: LinComb.insert_gate( grad_state, param_occurence[0], gate_to_insert_i, after=True, additional_qubits=additional_qubits) else: LinComb.insert_gate( grad_state, param_occurence[0], gate_to_insert_i, additional_qubits=additional_qubits) else: LinComb.insert_gate( grad_state, param_occurence[0], gate_to_insert_i, additional_qubits=additional_qubits) # Remove unnecessary gates. grad_state = self.trim_circuit(grad_state, param_occurence[0]) # Apply the final Hadamard on the working qubit. grad_state.h(work_q) # Add the coefficient needed for the gradient as well as the original # coefficient of the given quantum state. state = np.sqrt(np.abs( coeff_i)) * operator.coeff * CircuitStateFn(grad_state) # Check if the gate parameter corresponding to param is a parameter expression gate_param = param_occurence[0].params[param_occurence[1]] if gate_param == param: state = phase_fix_observable @ state else: # If the gate parameter is a parameter expressions the chain rule needs # to be taken into account. if isinstance(gate_param, ParameterExpression): expr_grad = DerivativeBase.parameter_expression_grad( gate_param, param) state = (expr_grad * phase_fix_observable) @ state else: state *= 0 if m == 0 and k == 0: phase_fix_state = state else: # Take the product rule into account phase_fix_state += state # Create a list for the phase fix states if not phase_fix_states: phase_fix_states = [phase_fix_state] else: phase_fix_states += [phase_fix_state] # Get 4 * Re[〈∂kψ|∂lψ] qfi_operators = [] # Add a working qubit qr_work_qubit = QuantumRegister(1, 'work_qubit') work_qubit = qr_work_qubit[0] additional_qubits = ([work_qubit], []) # create a copy of the original circuit with an additional work_qubit register circuit = state_qc.copy() circuit.add_register(qr_work_qubit) # Apply a Hadamard on the working qubit LinComb.insert_gate(circuit, state_qc._parameter_table[params[0]][0][0], HGate(), qubits=[work_qubit]) # Get the circuits needed to compute〈∂iψ|∂jψ〉 for i, param_i in enumerate(params): # loop over parameters qfi_ops = None for j, param_j in enumerate(params): # Get the gates of the quantum state which are parameterized by param_i param_gates_i = state_qc._parameter_table[param_i] for m_i, param_occurence_i in enumerate(param_gates_i): coeffs_i, gates_i = LinComb._gate_gradient_dict( param_occurence_i[0])[param_occurence_i[1]] for k_i, gate_to_insert_i in enumerate(gates_i): coeff_i = coeffs_i[k_i] # Get the gates of the quantum state which are parameterized by param_j param_gates_j = state_qc._parameter_table[param_j] for m_j, param_occurence_j in enumerate(param_gates_j): coeffs_j, gates_j = \ LinComb._gate_gradient_dict(param_occurence_j[0])[ param_occurence_j[1]] for k_j, gate_to_insert_j in enumerate(gates_j): coeff_j = coeffs_j[k_j] # create a copy of the original circuit with the same registers qfi_circuit = QuantumCircuit(*circuit.qregs) qfi_circuit.data = circuit.data # Correct the phase of the working qubit according to coefficient i # and coefficient j sign = np.sign(np.conj(coeff_i) * coeff_j) is_complex = np.iscomplex( np.conj(coeff_i) * coeff_j) if sign == -1: if is_complex: LinComb.insert_gate( qfi_circuit, param_occurence_i[0], SdgGate(), qubits=[work_qubit]) else: LinComb.insert_gate( qfi_circuit, param_occurence_i[0], ZGate(), qubits=[work_qubit]) else: if is_complex: LinComb.insert_gate( qfi_circuit, param_occurence_i[0], SGate(), qubits=[work_qubit]) LinComb.insert_gate(qfi_circuit, param_occurence_i[0], XGate(), qubits=[work_qubit]) # Insert controlled, intercepting gate i - controlled by |1> if isinstance(param_occurence_i[0], UGate): if param_occurence_i[1] == 0: LinComb.insert_gate( qfi_circuit, param_occurence_i[0], RZGate(param_occurence_i[0]. params[2])) LinComb.insert_gate( qfi_circuit, param_occurence_i[0], RXGate(np.pi / 2)) LinComb.insert_gate( qfi_circuit, param_occurence_i[0], gate_to_insert_i, additional_qubits=additional_qubits ) LinComb.insert_gate( qfi_circuit, param_occurence_i[0], RXGate(-np.pi / 2)) LinComb.insert_gate( qfi_circuit, param_occurence_i[0], RZGate(-param_occurence_i[0]. params[2])) elif param_occurence_i[1] == 1: LinComb.insert_gate( qfi_circuit, param_occurence_i[0], gate_to_insert_i, after=True, additional_qubits=additional_qubits ) else: LinComb.insert_gate( qfi_circuit, param_occurence_i[0], gate_to_insert_i, additional_qubits=additional_qubits ) else: LinComb.insert_gate( qfi_circuit, param_occurence_i[0], gate_to_insert_i, additional_qubits=additional_qubits) LinComb.insert_gate(qfi_circuit, gate_to_insert_i, XGate(), qubits=[work_qubit], after=True) # Insert controlled, intercepting gate j - controlled by |0> if isinstance(param_occurence_j[0], UGate): if param_occurence_j[1] == 0: LinComb.insert_gate( qfi_circuit, param_occurence_j[0], RZGate(param_occurence_j[0]. params[2])) LinComb.insert_gate( qfi_circuit, param_occurence_j[0], RXGate(np.pi / 2)) LinComb.insert_gate( qfi_circuit, param_occurence_j[0], gate_to_insert_j, additional_qubits=additional_qubits ) LinComb.insert_gate( qfi_circuit, param_occurence_j[0], RXGate(-np.pi / 2)) LinComb.insert_gate( qfi_circuit, param_occurence_j[0], RZGate(-param_occurence_j[0]. params[2])) elif param_occurence_j[1] == 1: LinComb.insert_gate( qfi_circuit, param_occurence_j[0], gate_to_insert_j, after=True, additional_qubits=additional_qubits ) else: LinComb.insert_gate( qfi_circuit, param_occurence_j[0], gate_to_insert_j, additional_qubits=additional_qubits ) else: LinComb.insert_gate( qfi_circuit, param_occurence_j[0], gate_to_insert_j, additional_qubits=additional_qubits) # Remove redundant gates if j <= i: qfi_circuit = self.trim_circuit( qfi_circuit, param_occurence_i[0]) else: qfi_circuit = self.trim_circuit( qfi_circuit, param_occurence_j[0]) # Apply final Hadamard gate qfi_circuit.h(work_qubit) # Convert the quantum circuit into a CircuitStateFn and add the # coefficients i, j and the original operator coefficient term = np.sqrt( np.abs(coeff_i) * np.abs(coeff_j)) * operator.coeff term = term * CircuitStateFn(qfi_circuit) # Check if the gate parameters i and j are parameter expressions gate_param_i = param_occurence_i[0].params[ param_occurence_i[1]] gate_param_j = param_occurence_j[0].params[ param_occurence_j[1]] meas = deepcopy(qfi_observable) # If the gate parameter i is a parameter expression use the chain # rule. if isinstance(gate_param_i, ParameterExpression): expr_grad = DerivativeBase.parameter_expression_grad( gate_param_i, param_i) meas *= expr_grad # If the gate parameter j is a parameter expression use the chain # rule. if isinstance(gate_param_j, ParameterExpression): expr_grad = DerivativeBase.parameter_expression_grad( gate_param_j, param_j) meas *= expr_grad term = meas @ term if m_i == 0 and k_i == 0 and m_j == 0 and k_j == 0: qfi_op = term else: # Product Rule qfi_op += term # Compute −4 * Re(〈∂kψ|ψ〉〈ψ|∂lψ〉) def phase_fix_combo_fn(x): return 4 * (-0.5) * (x[0] * np.conjugate(x[1]) + x[1] * np.conjugate(x[0])) phase_fix = ListOp([phase_fix_states[i], phase_fix_states[j]], combo_fn=phase_fix_combo_fn) # Add the phase fix quantities to the entries of the QFI # Get 4 * Re[〈∂kψ|∂lψ〉−〈∂kψ|ψ〉〈ψ|∂lψ〉] if not qfi_ops: qfi_ops = [qfi_op + phase_fix] else: qfi_ops += [qfi_op + phase_fix] qfi_operators.append(ListOp(qfi_ops)) # Return the full QFI return ListOp(qfi_operators)