Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HT gradient - additional modes #7046

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 146 additions & 12 deletions pennylane/gradients/hadamard_gradient.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
argnum=None,
aux_wire=None,
device_wires=None,
mode: str = "standard",
) -> tuple[QuantumScriptBatch, PostprocessingFn]:
"""Expand function to be applied before hadamard gradient."""
batch, postprocessing = qml.devices.preprocess.decompose(
Expand Down Expand Up @@ -94,6 +95,7 @@
argnum=None,
aux_wire=None,
device_wires=None,
mode: str = "standard",
) -> tuple[QuantumScriptBatch, PostprocessingFn]:
r"""Transform a circuit to compute the Hadamard test gradient of all gates
with respect to their inputs.
Expand All @@ -109,6 +111,9 @@
the original circuit and ``device_wires``.
device_wires (pennylane.wires.Wires): Wires of the device that are going to be used for the
gradient. Facilitates finding a default for ``aux_wire`` if ``aux_wire`` is ``None``.
mode (str): Specifies the gradient computation mode. Accepted values are
``"standard"``, ``"reversed"``, ``"direct"``, and ``"reversed_direct"``. The default
is ``"standard"``.

Returns:
qnode (QNode) or tuple[List[QuantumTape], function]:
Expand Down Expand Up @@ -270,7 +275,17 @@
Array([-0.3875172 , -0.18884787, -0.38355704], dtype=float64)
"""

transform_name = "Hadamard test"
modes = {
"standard": ("Hadamard test", _hadamard_test),
"reversed": ("Reversed hadamard test", _reversed_hadamard_test),
"direct": ("Direct hadamard test", _direct_hadamard_test),
"reversed_direct": ("Reversed direct hadamard test", _reversed_direct_hadamard_test),
}
try:
transform_name, gradient_method = modes[mode]
except KeyError as exc:
raise ValueError(f"Invalid mode: {mode}") from exc

Check notice on line 288 in pennylane/gradients/hadamard_gradient.py

View check run for this annotation

codefactor.io / CodeFactor

pennylane/gradients/hadamard_gradient.py#L288

Trailing whitespace (trailing-whitespace)
assert_no_state_returns(tape.measurements, transform_name)
assert_no_variance(tape.measurements, transform_name)
assert_no_trainable_tape_batching(tape, transform_name)
Expand Down Expand Up @@ -303,7 +318,7 @@
else:
# can dispatch between different algorithms here in the future
# hadamard test, direct hadamard test, reversed, reversed direct, and flexible
batch, new_coeffs = _hadamard_test(tape, trainable_param_idx, aux_wire)
batch, new_coeffs = gradient_method(tape, trainable_param_idx, aux_wire)
g_tapes += batch
coeffs += new_coeffs
generators_per_parameter.append(len(batch))
Expand Down Expand Up @@ -335,28 +350,147 @@
new_batch.append(new_tape)
return new_batch, sub_coeffs

def _direct_hadamard_test(tape, trainable_param_idx, aux_wire) -> tuple[list, list]:

trainable_op, idx, _ = tape.get_operation(trainable_param_idx)

ops_to_trainable_op = tape.operations[: idx + 1]
ops_after_trainable_op = tape.operations[idx + 1 :]

# Get a generator and coefficients
sub_coeffs, generators = _get_pauli_generators(trainable_op)

measurements = tape.measurements

new_batch = []
new_coeffs = []
for idx, gen in enumerate(generators):
pos_rot = [qml.evolve(gen, np.pi/4)]
neg_rot = [qml.evolve(gen, -np.pi/4)]
pos_ops = ops_to_trainable_op + pos_rot + ops_after_trainable_op
neg_ops = ops_to_trainable_op + neg_rot + ops_after_trainable_op

pos_tape = qml.tape.QuantumScript(pos_ops, measurements, shots=tape.shots)
neg_tape = qml.tape.QuantumScript(neg_ops, measurements, shots=tape.shots)
new_batch.append(pos_tape)
new_batch.append(neg_tape)
new_coeffs.append(-1/2 * sub_coeffs[idx])
new_coeffs.append(1/2 * sub_coeffs[idx])
return new_batch, new_coeffs

def _reversed_hadamard_test(tape, trainable_param_idx, aux_wire) -> tuple[list, list]:

trainable_op, idx, _ = tape.get_operation(trainable_param_idx)

ops_before_trainable_op = tape.operations[:]
ops_after_trainable_op = [qml.adjoint(op) for op in reversed(tape.operations[idx + 1:])]

# Get a generator and coefficients
sub_coeffs, generators = _get_pauli_generators(trainable_op)

# Create measurement with gate generators
# With type pennylane.measurements.expval.ExpectationMP
mp = qml.expval(
qml.Hamiltonian(
coeffs=sub_coeffs,
observables=generators,
# grouping_type="commuting",
)
)
Comment on lines +388 to +399
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we are using the generator as a meausrement, do we still need to represent the observable as a linear combination of unitaries (paulis)?

Or could we technically just do:

mp = qml.expval(trainable_op.generator() @ qml.Y(aux_wire))

This might be a bit more general, and leaves interpreting the new observable up to the device.

measurements = [_new_measurement(mp, aux_wire, tape.wires)]

# Get the observable from tape measurement
# Assume there's only one observable in the tape ################ processing function aggregation
coeffs, observables = _get_pauli_terms(tape.measurements[0].obs)
coeffs = [-c for c in coeffs]

new_batch = []
for obs in observables:
ctrl_obs = [qml.ctrl(obs, control=aux_wire)]
hadamard = [qml.Hadamard(wires=aux_wire)]
ops = ops_before_trainable_op + hadamard + ctrl_obs + hadamard + ops_after_trainable_op

new_tape = qml.tape.QuantumScript(ops, measurements, shots=tape.shots)
new_batch.append(new_tape)
return new_batch, coeffs

def _reversed_direct_hadamard_test(tape, trainable_param_idx, aux_wire) -> tuple[list, list]:

trainable_op, idx, _ = tape.get_operation(trainable_param_idx)

ops_before_trainable_op = tape.operations[:]
ops_after_trainable_op = [qml.adjoint(op) for op in reversed(tape.operations[idx + 1:])]

sub_coeffs, generators = _get_pauli_generators(trainable_op)

# Create measurement with gate generators
measurements = [
qml.expval(
qml.Hamiltonian(
coeffs=sub_coeffs,
observables=generators,
# grouping_type="commuting",
)
)
]

# Get the observable from tape measurement
# Assume there's only one observable in the tape ################ processing function aggregation
coeffs, observables = _get_pauli_terms(tape.measurements[0].obs)

new_batch = []
new_coeffs = []
for idx, obs in enumerate(observables):
pos_rot = [qml.evolve(obs, np.pi/4)]
neg_rot = [qml.evolve(obs, -np.pi/4)]
pos_ops = ops_before_trainable_op + pos_rot + ops_after_trainable_op
neg_ops = ops_before_trainable_op + neg_rot + ops_after_trainable_op

pos_tape = qml.tape.QuantumScript(pos_ops, measurements, shots=tape.shots)
neg_tape = qml.tape.QuantumScript(neg_ops, measurements, shots=tape.shots)
new_batch.append(pos_tape)
new_batch.append(neg_tape)
new_coeffs.append(1/2 * coeffs[idx])
new_coeffs.append(-1/2 * coeffs[idx])
return new_batch, new_coeffs

def _new_measurement(mp, aux_wire, all_wires: qml.wires.Wires):
obs = mp.obs or qml.prod(*(qml.Z(w) for w in mp.wires or all_wires))
new_obs = qml.simplify(obs @ qml.Y(aux_wire))
return type(mp)(obs=new_obs)

def _get_pauli_terms(op):
"""Extract the Pauli terms (generators) and their coefficients for an operator.

If the operator has no pre-computed pauli_rep, the function computes the matrix
and performs a Pauli decomposition.

Parameters:
op: The operator (e.g., a Hamiltonian) for which to extract the Pauli terms.

Returns:
The Pauli terms (generators) and their coefficients.
"""
if op.pauli_rep is None:
mat = qml.matrix(op, wire_order=op.wires)
pauli_rep = qml.pauli_decompose(mat, wire_order=op.wires, pauli=True)
else:
pauli_rep = op.pauli_rep

# Remove identity term if present.
id_pw = qml.pauli.PauliWord({})
if id_pw in pauli_rep:
del pauli_rep[qml.pauli.PauliWord({})]

Check notice on line 484 in pennylane/gradients/hadamard_gradient.py

View check run for this annotation

codefactor.io / CodeFactor

pennylane/gradients/hadamard_gradient.py#L484

Trailing whitespace (trailing-whitespace)
# qml.PauliZ has no defined terms() behavior
return pauli_rep.operation().terms() if isinstance(pauli_rep.operation(), qml.ops.op_math.Sum) else (1 * pauli_rep.operation()).terms()

def _get_pauli_generators(trainable_op):
"""From a trainable operation, extract the unitary generators and their coefficients.
Any operator with a generator is supported.
"""
generator = trainable_op.generator()
if generator.pauli_rep is None:
mat = qml.matrix(generator, wire_order=generator.wires)
pauli_rep = qml.pauli_decompose(mat, wire_order=generator.wires, pauli=True)
else:
pauli_rep = generator.pauli_rep
id_pw = qml.pauli.PauliWord({})
if id_pw in pauli_rep:
del pauli_rep[qml.pauli.PauliWord({})] # remove identity term
sum_op = pauli_rep.operation()
return sum_op.terms()
return _get_pauli_terms(generator)


def _postprocess_probs(res, measurement, tape):
Expand Down
Loading