Mitiq error with GradientDescentOptimizer

Hi,

I’m running into an error with using the mitiq module for error mitigation. Just running the forward pass (ie. cost_loc(problem, w, local_hadamard_test)) seems to work perfectly fine, but when fed into the optimizer it results in the error below. The error looks the same as this post but I’ve tried @isaacdevlugt’s suggestions of explicitly specifying the kwargs + refactoring the QNode definition to be outside of the cost function, but I’m getting the same error.

Happy holidays!
Jeffrey

Error

{
	"name": "CircuitConversionError",
	"message": "Circuit could not be converted to an internal Mitiq circuit. This may be because the circuit contains custom gates or Pragmas (pyQuil). If you think this is a bug or that this circuit should be supported, you can open an issue at https://github.com/unitaryfund/mitiq. 

 Provided circuit has type <class 'pennylane.tape.qscript.QuantumScript'> and is:

<QuantumScript: wires=[3, 0, 1, 2], params=3>

Circuit types supported by Mitiq are 
{'cirq': 'Circuit', 'pyquil': 'Program', 'qiskit': 'QuantumCircuit', 'braket': 'Circuit', 'pennylane': 'QuantumTape'}.",
	"stack": "---------------------------------------------------------------------------
QasmException                             Traceback (most recent call last)
File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/mitiq/interface/conversions.py:123, in convert_to_mitiq(circuit)
    122 try:
--> 123     mitiq_circuit = conversion_function(circuit)
    124 except Exception:

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/mitiq/interface/mitiq_pennylane/conversions.py:61, in from_pennylane(tape)
     59 qasm = tape.to_openqasm(rotations=False, wires=wires, measure_all=False)
---> 61 return cirq_from_qasm(qasm)

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/mitiq/interface/mitiq_qiskit/conversions.py:299, in from_qasm(qasm)
    298 qasm = _remove_qasm_barriers(qasm)
--> 299 return circuit_from_qasm(qasm)

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/cirq/contrib/qasm_import/qasm.py:29, in circuit_from_qasm(qasm)
     20 \"\"\"Parses an OpenQASM string to `cirq.Circuit`.
     21 
     22 Args:
   (...)
     26     The parsed circuit
     27 \"\"\"
---> 29 return QasmParser().parse(qasm).circuit

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/cirq/contrib/qasm_import/_parser.py:538, in QasmParser.parse(self, qasm)
    537     self.lexer.input(self.qasm)
--> 538     self.parsedQasm = self.parser.parse(lexer=self.lexer)
    539 return self.parsedQasm

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/ply/yacc.py:333, in LRParser.parse(self, input, lexer, debug, tracking, tokenfunc)
    332 else:
--> 333     return self.parseopt_notrack(input, lexer, debug, tracking, tokenfunc)

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/ply/yacc.py:1201, in LRParser.parseopt_notrack(self, input, lexer, debug, tracking, tokenfunc)
   1200 self.state = state
-> 1201 tok = call_errorfunc(self.errorfunc, errtoken, self)
   1202 if self.errorok:
   1203     # User must have done some kind of panic
   1204     # mode recovery on their own.  The
   1205     # returned token is the next lookahead

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/ply/yacc.py:192, in call_errorfunc(errorfunc, token, parser)
    191 _restart = parser.restart
--> 192 r = errorfunc(token)
    193 try:

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/cirq/contrib/qasm_import/_parser.py:521, in QasmParser.p_error(self, p)
    519             raise QasmException('Unexpected end of file')
--> 521         raise QasmException(
    522             f\"\"\"Syntax error: '{p.value}'
    523 {self.debug_context(p)}
    524 at line {p.lineno}, column {self.find_column(p)}\"\"\"
    525         )

QasmException: Syntax error: 'ArrayBox'
...grad ArrayBox with value -1.8812601840080732) q[0];
ry(Autograd ArrayBox with value -0.29327952123155937) q[1];
ry(Autograd ArrayBox with value 0.3590991360570879) q[2];
h q[0];
h q[1];
cz q[3],q[0];
h q[0];
h q[1];
h q[3]
        ^
at line 6, column 13

During handling of the above exception, another exception occurred:

CircuitConversionError                    Traceback (most recent call last)
/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb セル 1 line 2
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=228'>229</a> start = time.time()
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=230'>231</a> for it in range(10):
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=231'>232</a>     # w, cost = opt.step_and_cost(cost_agg, w)
--> <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=232'>233</a>     w, cost = opt.step_and_cost(lambda w: cost_loc(problem, w, local_hadamard_test), w)
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=234'>235</a>     print(\"Step {:3d}       Cost_L = {:9.7f}\".format(it, cost), flush=True)
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=236'>237</a>     it += 1

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/optimize/gradient_descent.py:59, in GradientDescentOptimizer.step_and_cost(self, objective_fn, grad_fn, *args, **kwargs)
     39 def step_and_cost(self, objective_fn, *args, grad_fn=None, **kwargs):
     40     \"\"\"Update trainable arguments with one step of the optimizer and return the corresponding
     41     objective function value prior to the step.
     42 
   (...)
     56         If single arg is provided, list [array] is replaced by array.
     57     \"\"\"
---> 59     g, forward = self.compute_grad(objective_fn, args, kwargs, grad_fn=grad_fn)
     60     new_args = self.apply_grad(g, args)
     62     if forward is None:

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/optimize/gradient_descent.py:117, in GradientDescentOptimizer.compute_grad(objective_fn, args, kwargs, grad_fn)
     99 r\"\"\"Compute gradient of the objective function at the given point and return it along with
    100 the objective function forward pass (if available).
    101 
   (...)
    114     will not be evaluted and instead ``None`` will be returned.
    115 \"\"\"
    116 g = get_gradient(objective_fn) if grad_fn is None else grad_fn
--> 117 grad = g(*args, **kwargs)
    118 forward = getattr(g, \"forward\", None)
    120 num_trainable_args = sum(getattr(arg, \"requires_grad\", False) for arg in args)

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/_grad.py:118, in grad.__call__(self, *args, **kwargs)
    115     self._forward = self._fun(*args, **kwargs)
    116     return ()
--> 118 grad_value, ans = grad_fn(*args, **kwargs)  # pylint: disable=not-callable
    119 self._forward = ans
    121 return grad_value

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/autograd/wrap_util.py:20, in unary_to_nary.<locals>.nary_operator.<locals>.nary_f(*args, **kwargs)
     18 else:
     19     x = tuple(args[i] for i in argnum)
---> 20 return unary_operator(unary_f, x, *nary_op_args, **nary_op_kwargs)

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/_grad.py:136, in grad._grad_with_forward(fun, x)
    130 @staticmethod
    131 @unary_to_nary
    132 def _grad_with_forward(fun, x):
    133     \"\"\"This function is a replica of ``autograd.grad``, with the only
    134     difference being that it returns both the gradient *and* the forward pass
    135     value.\"\"\"
--> 136     vjp, ans = _make_vjp(fun, x)
    138     if not vspace(ans).size == 1:
    139         raise TypeError(
    140             \"Grad only applies to real scalar-output functions. \"
    141             \"Try jacobian, elementwise_grad or holomorphic_grad.\"
    142         )

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/autograd/core.py:10, in make_vjp(fun, x)
      8 def make_vjp(fun, x):
      9     start_node = VJPNode.new_root()
---> 10     end_value, end_node =  trace(start_node, fun, x)
     11     if end_node is None:
     12         def vjp(g): return vspace(x).zeros()

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/autograd/tracer.py:10, in trace(start_node, fun, x)
      8 with trace_stack.new_trace() as t:
      9     start_box = new_box(x, t, start_node)
---> 10     end_box = fun(start_box)
     11     if isbox(end_box) and end_box._trace == start_box._trace:
     12         return end_box._value, end_box._node

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/autograd/wrap_util.py:15, in unary_to_nary.<locals>.nary_operator.<locals>.nary_f.<locals>.unary_f(x)
     13 else:
     14     subargs = subvals(args, zip(argnum, x))
---> 15 return fun(*subargs, **kwargs)

/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb セル 1 line 2
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=228'>229</a> start = time.time()
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=230'>231</a> for it in range(10):
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=231'>232</a>     # w, cost = opt.step_and_cost(cost_agg, w)
--> <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=232'>233</a>     w, cost = opt.step_and_cost(lambda w: cost_loc(problem, w, local_hadamard_test), w)
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=234'>235</a>     print(\"Step {:3d}       Cost_L = {:9.7f}\".format(it, cost), flush=True)
    <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=236'>237</a>     it += 1

/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb セル 1 line 7
     <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=71'>72</a>     for lp in range(0, len(c)):
     <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=72'>73</a>         for j in range(0, n_qubits):
---> <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=73'>74</a>             mu_sum = mu_sum + c[l] * np.conj(c[lp]) * mu(weights, local_hadamard_test, problem, l, lp, j)
     <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=75'>76</a> mu_sum = abs(mu_sum)
     <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=77'>78</a> # Cost function C_L

/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb セル 1 line 4
     <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=44'>45</a> def mu(weights, local_hadamard_test, problem, l=None, lp=None, j=None):
     <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=45'>46</a>     \"\"\"Generates the coefficients to compute the \"local\" cost function C_L.\"\"\"
---> <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=47'>48</a>     mu_real = local_hadamard_test(weights, problem, l=l, lp=lp, j=j, part=\"Re\")
     <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=48'>49</a>     mu_imag = local_hadamard_test(weights, problem, l=l, lp=lp, j=j, part=\"Im\")
     <a href='vscode-notebook-cell:/Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/mitiq_integration.ipynb#W0sZmlsZQ%3D%3D?line=50'>51</a>     return mu_real + 1.0j * mu_imag

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/qnode.py:1027, in QNode.__call__(self, *args, **kwargs)
   1022         full_transform_program._set_all_argnums(
   1023             self, args, kwargs, argnums
   1024         )  # pylint: disable=protected-access
   1026 # pylint: disable=unexpected-keyword-arg
-> 1027 res = qml.execute(
   1028     (self._tape,),
   1029     device=self.device,
   1030     gradient_fn=self.gradient_fn,
   1031     interface=self.interface,
   1032     transform_program=full_transform_program,
   1033     config=config,
   1034     gradient_kwargs=self.gradient_kwargs,
   1035     override_shots=override_shots,
   1036     **self.execute_kwargs,
   1037 )
   1039 res = res[0]
   1041 # convert result to the interface in case the qfunc has no parameters

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/interfaces/execution.py:600, in execute(tapes, device, gradient_fn, interface, transform_program, config, grad_on_execution, gradient_kwargs, cache, cachesize, max_diff, override_shots, expand_fn, max_expansion, device_batch_transform)
    595     if not device_batch_transform:
    596         warnings.warn(
    597             \"device batch transforms cannot be turned off with the new device interface.\",
    598             UserWarning,
    599         )
--> 600     tapes, post_processing = transform_program(tapes)
    601 else:
    602     # TODO: Remove once old device are removed
    603     tapes, program_post_processing = transform_program(tapes)

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/transforms/core/transform_program.py:429, in TransformProgram.__call__(self, tapes)
    427 if self._argnums is not None and self._argnums[i] is not None:
    428     tape.trainable_params = self._argnums[i][j]
--> 429 new_tapes, fn = transform(tape, *targs, **tkwargs)
    430 execution_tapes.extend(new_tapes)
    432 fns.append(fn)

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/transforms/mitigate.py:512, in mitigate_with_zne(tape, scale_factors, folding, extrapolate, folding_kwargs, extrapolate_kwargs, reps_per_factor)
    509 tape = tape.expand(stop_at=lambda op: not isinstance(op, QuantumScript))
    510 script_removed = QuantumScript(tape.operations[tape.num_preps :])
--> 512 tapes = [
    513     [folding(script_removed, s, **folding_kwargs) for _ in range(reps_per_factor)]
    514     for s in scale_factors
    515 ]
    517 tapes = [tape_ for tapes_ in tapes for tape_ in tapes_]  # flattens nested list
    519 # if folding was a batch transform, ignore the processing function

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/transforms/mitigate.py:513, in <listcomp>(.0)
    509 tape = tape.expand(stop_at=lambda op: not isinstance(op, QuantumScript))
    510 script_removed = QuantumScript(tape.operations[tape.num_preps :])
    512 tapes = [
--> 513     [folding(script_removed, s, **folding_kwargs) for _ in range(reps_per_factor)]
    514     for s in scale_factors
    515 ]
    517 tapes = [tape_ for tapes_ in tapes for tape_ in tapes_]  # flattens nested list
    519 # if folding was a batch transform, ignore the processing function

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/pennylane/transforms/mitigate.py:513, in <listcomp>(.0)
    509 tape = tape.expand(stop_at=lambda op: not isinstance(op, QuantumScript))
    510 script_removed = QuantumScript(tape.operations[tape.num_preps :])
    512 tapes = [
--> 513     [folding(script_removed, s, **folding_kwargs) for _ in range(reps_per_factor)]
    514     for s in scale_factors
    515 ]
    517 tapes = [tape_ for tapes_ in tapes for tape_ in tapes_]  # flattens nested list
    519 # if folding was a batch transform, ignore the processing function

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/mitiq/interface/conversions.py:305, in accept_qprogram_and_validate.<locals>.new_function(circuit, *args, **kwargs)
    301     out_circuits = atomic_one_to_many_converter(cirq_circuit_modifier)(
    302         circuit, *args, **kwargs
    303     )
    304 else:
--> 305     out_circuit = atomic_converter(cirq_circuit_modifier)(
    306         circuit, *args, **kwargs
    307     )
    308     out_circuits = [out_circuit]
    310 circuits_to_return = []

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/mitiq/interface/conversions.py:221, in atomic_converter.<locals>.qprogram_modifier(circuit, *args, **kwargs)
    216 @wraps(cirq_circuit_modifier)
    217 def qprogram_modifier(
    218     circuit: QPROGRAM, *args: Any, **kwargs: Any
    219 ) -> QPROGRAM:
    220     # Convert to Mitiq representation.
--> 221     mitiq_circuit, input_circuit_type = convert_to_mitiq(circuit)
    223     # Modify the Cirq circuit.
    224     scaled_circuit = cirq_circuit_modifier(mitiq_circuit, *args, **kwargs)

File ~/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages/mitiq/interface/conversions.py:125, in convert_to_mitiq(circuit)
    123     mitiq_circuit = conversion_function(circuit)
    124 except Exception:
--> 125     raise CircuitConversionError(
    126         \"Circuit could not be converted to an internal Mitiq circuit. \"
    127         \"This may be because the circuit contains custom gates or Pragmas \"
    128         \"(pyQuil). If you think this is a bug or that this circuit should \"
    129         \"be supported, you can open an issue at \"
    130         \"https://github.com/unitaryfund/mitiq. \
\
 Provided circuit has \"
    131         f\"type {type(circuit)} and is:\
\
{circuit}\
\
Circuit types \"
    132         f\"supported by Mitiq are \
{SUPPORTED_PROGRAM_TYPES}.\"
    133     )
    135 return mitiq_circuit, input_circuit_type

CircuitConversionError: Circuit could not be converted to an internal Mitiq circuit. This may be because the circuit contains custom gates or Pragmas (pyQuil). If you think this is a bug or that this circuit should be supported, you can open an issue at https://github.com/unitaryfund/mitiq. 

 Provided circuit has type <class 'pennylane.tape.qscript.QuantumScript'> and is:

<QuantumScript: wires=[3, 0, 1, 2], params=3>

Circuit types supported by Mitiq are 
{'cirq': 'Circuit', 'pyquil': 'Program', 'qiskit': 'QuantumCircuit', 'braket': 'Circuit', 'pennylane': 'QuantumTape'}."
}

Here is my code:

import pennylane as qml
from pennylane import numpy as np
import time

def local_hadamard_test(weights, problem, l=None, lp=None, j=None, part=None):

    ancilla_idx = problem.get_n_qubits()

    # First Hadamard gate applied to the ancillary qubit.
    qml.Hadamard(wires=ancilla_idx)

    # For estimating the imaginary part of the coefficient "mu", we must add a "-i"
    # phase gate.
    if part == "Im" or part == "im":
        qml.PhaseShift(-np.pi / 2, wires=ancilla_idx)

    # Variational circuit generating a guess for the solution vector |x>
    problem.variational_block(weights)

    # Controlled application of the unitary component A_l of the problem matrix A.
    problem.CA(ancilla_idx, l)

    # Adjoint of the unitary U_b associated to the problem vector |b>.
    # In this specific example Adjoint(U_b) = U_b.
    problem.U_b()

    # Controlled Z operator at position j. If j = -1, apply the identity.
    if j != -1:
        qml.CZ(wires=[ancilla_idx, j])

    # Unitary U_b associated to the problem vector |b>.
    problem.U_b()

    # Controlled application of Adjoint(A_lp).
    # In this specific example Adjoint(A_lp) = A_lp.
    problem.CA(ancilla_idx, lp)

    # Second Hadamard gate applied to the ancillary qubit.
    qml.Hadamard(wires=ancilla_idx)

    # Expectation value of Z for the ancillary qubit.
    return qml.expval(qml.PauliZ(wires=ancilla_idx))

# Computes the mu coefficients
def mu(weights, local_hadamard_test, problem, l=None, lp=None, j=None):
    """Generates the coefficients to compute the "local" cost function C_L."""

    mu_real = local_hadamard_test(weights, problem, l=l, lp=lp, j=j, part="Re")
    mu_imag = local_hadamard_test(weights, problem, l=l, lp=lp, j=j, part="Im")

    return mu_real + 1.0j * mu_imag

def psi_norm(weights, c, local_hadamard_test, problem):
    """Returns the normalization constant <psi|psi>, where |psi> = A |x>."""
    norm = 0.0

    for l in range(0, len(c)):
        for lp in range(0, len(c)):
            norm = norm + c[l] * np.conj(c[lp]) * mu(weights, local_hadamard_test, problem, l, lp, -1)

    return abs(norm)

def cost_loc(problem, weights, local_hadamard_test):
    """Local version of the cost function. Tends to zero when A|x> is proportional to |b>."""

    c, _ = problem.get_coeffs()
    n_qubits = problem.get_n_qubits()
    
    mu_sum = 0.0

    for l in range(0, len(c)):
        for lp in range(0, len(c)):
            for j in range(0, n_qubits):
                mu_sum = mu_sum + c[l] * np.conj(c[lp]) * mu(weights, local_hadamard_test, problem, l, lp, j)

    mu_sum = abs(mu_sum)

    # Cost function C_L
    res = 0.5 - 0.5 * mu_sum / (n_qubits * psi_norm(weights, c, local_hadamard_test, problem))

    return res

# convert matrix A encoded as a string (eg. "IZZ") into qml code
def A_to_code (idx, ancilla_idx, terms, offset=0):

    if idx < 0:
        raise ValueError("Index of linear combination must be >= 0.")
    
    target_pauli = list(terms[idx])
    
    order_idx = offset

    for i in range(len(target_pauli)):
        if target_pauli[i] == 'I':
            order_idx += 1
            None
        if target_pauli[i] == 'X':
            qml.CNOT(wires = (ancilla_idx, order_idx))
            order_idx += 1
        if target_pauli[i] == 'Y':
            qml.CY(wires = (ancilla_idx, order_idx))
            order_idx += 1
        if target_pauli[i] == 'Z':
            qml.CZ(wires = (ancilla_idx, order_idx))
            order_idx += 1

import functools as ft

pauli_dict = {"I": qml.Identity.compute_matrix(), "X": qml.PauliX.compute_matrix(), "Y": qml.PauliY.compute_matrix(), "Z": qml.PauliZ.compute_matrix()}

def A_to_num (n_qubits: int, coefs: np.tensor, terms):
    """
    Given an array of coeffs c and an array of A_l formatted as a list of strings, return A
    @params
    coefs (eg. [1, 0.2, 0.2])
    terms (eg. ["III", "XZI", "XII"])

    returns an np.array
    """    
    if len(coefs) != len(terms):
        raise ValueError("Number of coefficients does not match number of terms.")
    
    if n_qubits <= 0:
        raise ValueError("Number of qubits is not a number greater than 0.")
    
    terms_len = len(terms)
    for i in range(terms_len):
        if len(terms[i]) != n_qubits:
            raise ValueError("Number of terms in each Pauli gate combination must be the same as number of qubits.")
        

    dim = 2**n_qubits
    mat = np.zeros((dim, dim), dtype=np.complex64)

    for (c, pauli) in zip(coefs, terms):
        pauli = [pauli_dict[key] for key in pauli]
        if pauli == ["I"]:
            mat += c * ft.reduce(np.kron, pauli)
        else:
            mat += c * ft.reduce(np.kron, pauli)
        
    return mat


# these classes encode the linear system we are trying to solve
from abc import ABC, abstractmethod
class Problem(ABC):
    def __init__(self, n_qubits, c, A_terms) -> None:
        super().__init__()
        self.n_qubits = n_qubits
        self.A_num = A_to_num(n_qubits, c, A_terms)
        self.A_terms = A_terms

        # normalize c
        self.c = np.array(c) / np.linalg.norm(self.A_num, ord=2)

        # Total number of qubits; here we add an ancillary qubit
        self.tot_qubits = self.n_qubits + 1
        # Index of ancillary qubit (Python lists are 0-indexed)
        self.ancilla_idx = self.n_qubits

    @abstractmethod
    def get_coeffs():
        """gets c, A_l"""
        pass
    
    @abstractmethod
    def get_n_qubits():
        """gets number of qubits of your problem"""
        pass

    @abstractmethod
    def U_b():
        """Unitary matrix rotating the ground state to the problem vector |b> = U_b |0>."""
        pass

    @abstractmethod
    def CA(idx):
        pass

    @abstractmethod
    def variational_block(weights):
        pass

class ToyProblem(Problem):
    def __init__(self, n_qubits):
        c = [1, 0.25]
        A_terms = ["III", "IIZ"]

        super().__init__(n_qubits, c, A_terms)

        self.param_shape = n_qubits

    def get_coeffs(self):
        return self.c, self.A_terms
    
    def get_n_qubits(self):
        return self.n_qubits
        

    def U_b(self):
        """Unitary matrix rotating the ground state to the problem vector |b> = U_b |0>."""
        [qml.Hadamard(wires=i) for i in [0,1]]
        
    def CA(self, ancilla_idx, idx, offset=0):
        A_to_code(idx, ancilla_idx=ancilla_idx, terms=self.A_terms, offset=offset)

    def variational_block(self, weights, offset=0):
        [qml.RY(phi=weights[i], wires=i+offset) for i in range(self.n_qubits)]

#############
n_qubits = 3
dev_mu = qml.device("default.qubit", wires=n_qubits+1)

from mitiq.zne.scaling import fold_global
from mitiq.zne.inference import RichardsonFactory
from pennylane.transforms import mitigate_with_zne

extrapolate = RichardsonFactory.extrapolate
scale_factors = [1, 2, 3]

local_hadamard_test = qml.QNode(local_hadamard_test, dev_mu, interface="autograd")
local_hadamard_test = mitigate_with_zne(local_hadamard_test, scale_factors, fold_global, extrapolate) # comment this line out to remove the error mitigation

problem = ToyProblem(3)
opt = qml.GradientDescentOptimizer(0.1)
w = np.random.randn(problem.param_shape, requires_grad=True)

start = time.time()

for it in range(10):
    # w, cost = opt.step_and_cost(cost_agg, w)
    w, cost = opt.step_and_cost(lambda w: cost_loc(problem, w, local_hadamard_test), w)

    print("Step {:3d}       Cost_L = {:9.7f}".format(it, cost), flush=True)

    it += 1

print(f"Training time: {time.time() - start}s")

qml.about():

Name: PennyLane
Version: 0.33.1
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /Users/bigsad/Downloads/Algorithm-Research/Student-Hub/Indy-Ng/.venv/lib/python3.11/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane-Lightning, PennyLane-qiskit

Platform info:           macOS-12.6-x86_64-i386-64bit
Python version:          3.11.6
Numpy version:           1.23.5
Scipy version:           1.10.1
Installed devices:
- default.gaussian (PennyLane-0.33.1)
- default.mixed (PennyLane-0.33.1)
- default.qubit (PennyLane-0.33.1)
- default.qubit.autograd (PennyLane-0.33.1)
- default.qubit.jax (PennyLane-0.33.1)
- default.qubit.legacy (PennyLane-0.33.1)
- default.qubit.tf (PennyLane-0.33.1)
- default.qubit.torch (PennyLane-0.33.1)
- default.qutrit (PennyLane-0.33.1)
- null.qubit (PennyLane-0.33.1)
- lightning.qubit (PennyLane-Lightning-0.33.1)
- qiskit.aer (PennyLane-qiskit-0.33.1)
- qiskit.basicaer (PennyLane-qiskit-0.33.1)
- qiskit.ibmq (PennyLane-qiskit-0.33.1)
- qiskit.ibmq.circuit_runner (PennyLane-qiskit-0.33.1)
- qiskit.ibmq.sampler (PennyLane-qiskit-0.33.1)
- qiskit.remote (PennyLane-qiskit-0.33.1)

Hi @jkwan314! The issue here could be that Mitiq’s error mitigation is not autodifferentiable; as a result, attempting to apply a gradient-based optimizer on a cost function that includes Mitiq as part of the workflow is likely going to error.

PennyLane provides an autodiff-compatible zero-noise extrapolation error mitigation transform, qml.transforms.mitigate_qith_zne(). You can also check out our differentiable error mitigation demo.

Thanks, replacing from mitiq.zne.scaling import fold_global
and from mitiq.zne.inference import RichardsonFactory with the pennylane.transforms equivalents fixed it for me!

2 Likes