Example #1
0
    def convert(
        self,
        operator: OperatorBase,
        params: Optional[Union[Tuple[ParameterExpression, ParameterExpression],
                               List[Tuple[ParameterExpression,
                                          ParameterExpression]],
                               List[ParameterExpression],
                               ParameterVector]] = None
    ) -> OperatorBase:
        """
        Args:
            operator: The operator for which we compute the Hessian
            params: The parameters we are computing the Hessian with respect to
                    Either give directly the tuples/list of tuples for which the second order
                    derivative is to be computed or give a list of parameters to build the
                    full Hessian for those parameters.

        Returns:
            OperatorBase: An operator whose evaluation yields the Hessian

        Raises:
            ValueError: If `params` is not set.
        """
        # if input is a tuple instead of a list, wrap it into a list
        if params is None:
            raise ValueError("No parameters were provided to differentiate")

        if isinstance(params, (ParameterVector, list)):
            # Case: a list of parameters were given, compute the Hessian for all param pairs
            if all(isinstance(param, ParameterExpression) for param in params):
                return ListOp([
                    ListOp([self.convert(operator, (p0, p1)) for p1 in params])
                    for p0 in params
                ])
            # Case: a list was given containing tuples of parameter pairs.
            # Compute the Hessian entries corresponding to these pairs of parameters.
            elif all(isinstance(param, tuple) for param in params):
                return ListOp([
                    self.convert(operator, param_pair) for param_pair in params
                ])

        expec_op = PauliExpectation(
            group_paulis=False).convert(operator).reduce()
        cleaned_op = self._factor_coeffs_out_of_composed_op(expec_op)
        return self.get_hessian(cleaned_op, params)
Example #2
0
    def convert(self,
                operator: CircuitStateFn,
                params: Optional[Union[ParameterExpression, ParameterVector,
                                       List[ParameterExpression]]] = None
                ) -> ListOp:
        r"""
        Args:
            operator: The operator corresponding to the quantum state \|ψ(ω)〉for which we compute
                the QFI
            params: The parameters we are computing the QFI wrt: ω

        Returns:
            ListOp[ListOp] where the operator at position k,l corresponds to QFI_kl
        """
        expec_op = PauliExpectation(group_paulis=False).convert(operator).reduce()
        cleaned_op = self._factor_coeffs_out_of_composed_op(expec_op)

        return self.qfi_method.convert(cleaned_op, params)
Example #3
0
    def _block_diag_approx(
        self,
        operator: Union[CircuitOp, 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:
            NotImplementedError: If a circuit is found such that one parameter controls multiple
                gates, or one gate contains multiple parameters.
            AquaError: If there are more than one parameter.

        """

        circuit = operator.primitive
        # Partition the circuit into layers, and build the circuits to prepare $\psi_i$
        layers = _partition_circuit(circuit)
        if layers[-1].num_parameters == 0:
            layers.pop(-1)

        block_params = [list(layer.parameters) for layer in layers]
        # Remove any parameters found which are not in params
        block_params = [[param for param in block if param in params]
                        for block in block_params]

        # Determine the permutation needed to ensure that the final
        # operator is consistent with the ordering of the input parameters
        perm = [
            params.index(param) for block in block_params for param in block
        ]

        psis = [CircuitOp(layer) for layer in layers]
        for i, psi in enumerate(psis):
            if i == 0:
                continue
            psis[i] = psi @ psis[i - 1]

        # Get generators
        # TODO: make this work for other types of rotations
        # NOTE: This assumes that each parameter only affects one rotation.
        # we need to think more about what happens if multiple rotations
        # are controlled with a single parameter.

        generators = _get_generators(params, circuit)

        blocks = []

        # Psi_i = layer_i @ layer_i-1 @ ... @ layer_0 @ Zero
        for k, psi_i in enumerate(psis):
            params = block_params[k]
            block = np.zeros((len(params), len(params))).tolist()

            # calculate all single-operator terms <psi_i|generator_i|psi_i>
            single_terms = np.zeros(len(params)).tolist()
            for i, p_i in enumerate(params):
                generator = generators[p_i]
                psi_gen_i = ~StateFn(generator) @ psi_i @ Zero
                psi_gen_i = PauliExpectation().convert(psi_gen_i)
                single_terms[i] = psi_gen_i

            def get_parameter_expression(circuit, param):
                if len(circuit._parameter_table[param]) > 1:
                    raise NotImplementedError(
                        "OverlapDiag does not yet support multiple "
                        "gates parameterized by a single parameter. For such "
                        "circuits use LinCombFull")
                gate = circuit._parameter_table[param][0][0]
                if len(gate.params) > 1:
                    raise AquaError(
                        "OverlapDiag cannot yet support gates with more than one "
                        "parameter.")

                param_value = gate.params[0]
                return param_value

            # Calculate all double-operator terms <psi_i|generator_j @ generator_i|psi_i>
            # and build composite operators for each matrix entry
            for i, p_i in enumerate(params):
                generator_i = generators[p_i]
                param_expr_i = get_parameter_expression(circuit, p_i)

                for j, p_j in enumerate(params):
                    if i == j:
                        block[i][i] = ListOp([single_terms[i]],
                                             combo_fn=lambda x: 1 - x[0]**2)
                        if isinstance(param_expr_i,
                                      ParameterExpression) and not isinstance(
                                          param_expr_i, Parameter):
                            expr_grad_i = DerivativeBase.parameter_expression_grad(
                                param_expr_i, p_i)
                            block[i][j] *= (expr_grad_i) * (expr_grad_i)
                        continue

                    generator_j = generators[p_j]
                    generator = ~generator_j @ generator_i
                    param_expr_j = get_parameter_expression(circuit, p_j)

                    psi_gen_ij = ~StateFn(generator) @ psi_i @ Zero
                    psi_gen_ij = PauliExpectation().convert(psi_gen_ij)
                    cross_term = ListOp([single_terms[i], single_terms[j]],
                                        combo_fn=np.prod)
                    block[i][j] = psi_gen_ij - cross_term

                    # pylint: disable=unidiomatic-typecheck
                    if type(param_expr_i) == ParameterExpression:
                        expr_grad_i = DerivativeBase.parameter_expression_grad(
                            param_expr_i, p_i)
                        block[i][j] *= expr_grad_i
                    if type(param_expr_j) == ParameterExpression:
                        expr_grad_j = DerivativeBase.parameter_expression_grad(
                            param_expr_j, p_j)
                        block[i][j] *= expr_grad_j

            wrapped_block = ListOp([ListOp(row) for row in block])
            blocks.append(wrapped_block)

        block_diagonal_qfi = ListOp(
            oplist=blocks,
            combo_fn=lambda x: np.real(block_diag(*x))[:, perm][perm, :])
        return block_diagonal_qfi
Example #4
0
    def _diagonal_approx(
        self,
        operator: Union[CircuitOp, CircuitStateFn],
        params: Union[ParameterExpression, ParameterVector, List] = None
    ) -> OperatorBase:
        """
        Args:
            operator: The operator corresponding to the quantum state |ψ(ω)〉for which we compute
                the QFI
            params: The parameters we are computing the QFI wrt: ω

        Returns:
            ListOp where the operator at position k corresponds to QFI_k,k

        Raises:
            NotImplementedError: If a circuit is found such that one parameter controls multiple
                                 gates, or one gate contains multiple parameters.
            TypeError: If a circuit is found that includes more than one parameter as they are
                       currently not supported for the overlap diagonal QFI method.

        """

        if not isinstance(operator, CircuitStateFn):
            raise NotImplementedError('operator must be a CircuitStateFn')

        circuit = operator.primitive

        # Partition the circuit into layers, and build the circuits to prepare $\psi_i$
        layers = _partition_circuit(circuit)
        if layers[-1].num_parameters == 0:
            layers.pop(-1)

        psis = [CircuitOp(layer) for layer in layers]
        for i, psi in enumerate(psis):
            if i == 0:
                continue
            psis[i] = psi @ psis[i - 1]

        # TODO: make this work for other types of rotations
        # NOTE: This assumes that each parameter only affects one rotation.
        # we need to think more about what happens if multiple rotations
        # are controlled with a single parameter.
        generators = _get_generators(params, circuit)

        diag = []
        for param in params:
            if len(circuit._parameter_table[param]) > 1:
                raise NotImplementedError(
                    "OverlapDiag does not yet support multiple "
                    "gates parameterized by a single parameter. For such "
                    "circuits use LinCombFull")

            gate = circuit._parameter_table[param][0][0]

            if len(gate.params) != 1:
                raise TypeError(
                    "OverlapDiag cannot yet support gates with more than one "
                    "parameter.")

            param_value = gate.params[0]
            generator = generators[param]
            meas_op = ~StateFn(generator)

            # get appropriate psi_i
            psi = [(psi) for psi in psis
                   if param in psi.primitive.parameters][0]

            op = meas_op @ psi @ Zero
            if type(param_value) == ParameterExpression:  # pylint: disable=unidiomatic-typecheck
                expr_grad = DerivativeBase.parameter_expression_grad(
                    param_value, param)
                op *= expr_grad
            rotated_op = PauliExpectation().convert(op)
            diag.append(rotated_op)

        grad_op = ListOp(
            diag, combo_fn=lambda x: np.diag(np.real([1 - y**2 for y in x])))
        return grad_op