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

Add new differentiation method based on rewinding the tape [PR1] #1029

Closed
wants to merge 5 commits into from

Conversation

trbromley
Copy link
Contributor

To maintain small PRs, #1017 will be split into a few manageable PRs. This is the first one - it contains the main RewindTape and tests.

Coming later:

  • integration with QNode
  • associated tests
  • update to changelog

@trbromley trbromley self-assigned this Jan 22, 2021
@github-actions
Copy link
Contributor

Hello. You may have forgotten to update the changelog!
Please edit .github/CHANGELOG.md with:

  • A one-to-two sentence description of the change. You may include a small working example for new features.
  • A link back to this PR.
  • Your name (or GitHub username) in the contributors section.

Comment on lines +83 to +244

with RewindTape() as tape:
qml.RX(a, wires=0)
qml.expval(qml.PauliZ(0))

circuit_output = tape.execute(dev)
expected_output = np.cos(a)
assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0)

# circuit jacobians
circuit_jacobian = tape.jacobian(dev, method="analytic")
expected_jacobian = -np.sin(a)
assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0)

def test_multiple_rx_gradient(self, tol):
"""Tests that the gradient of multiple RX gates in a circuit yields the correct result."""
dev = qml.device("default.qubit", wires=3)
params = np.array([np.pi, np.pi / 2, np.pi / 3])

with RewindTape() as tape:
qml.RX(params[0], wires=0)
qml.RX(params[1], wires=1)
qml.RX(params[2], wires=2)

for idx in range(3):
qml.expval(qml.PauliZ(idx))

circuit_output = tape.execute(dev)
expected_output = np.cos(params)
assert np.allclose(circuit_output, expected_output, atol=tol, rtol=0)

# circuit jacobians
circuit_jacobian = tape.jacobian(dev, method="analytic")
expected_jacobian = -np.diag(np.sin(params))
assert np.allclose(circuit_jacobian, expected_jacobian, atol=tol, rtol=0)

qubit_ops = [getattr(qml, name) for name in qml.ops._qubit__ops__]
analytic_qubit_ops = {cls for cls in qubit_ops if cls.grad_method == "A"}
analytic_qubit_ops -= {
qml.CRot, # not supported for RewindTape
qml.PauliRot, # not supported in test
qml.MultiRZ, # not supported in test
qml.U1, # not supported on device
qml.U2, # not supported on device
qml.U3, # not supported on device
}

@pytest.mark.parametrize("obs", [qml.PauliX, qml.PauliY])
@pytest.mark.parametrize("op", analytic_qubit_ops)
def test_gradients(self, op, obs, mocker, tol, dev):
"""Tests that the gradients of circuits match between the
finite difference and analytic methods."""
args = np.linspace(0.2, 0.5, op.num_params)

with RewindTape() as tape:
qml.Hadamard(wires=0)
qml.RX(0.543, wires=0)
qml.CNOT(wires=[0, 1])

op(*args, wires=range(op.num_wires))

qml.Rot(1.3, -2.3, 0.5, wires=[0])
qml.RZ(-0.5, wires=0)
qml.RY(0.5, wires=1).inv()
qml.CNOT(wires=[0, 1])

qml.expval(obs(wires=0))
qml.expval(qml.PauliZ(wires=1))

tape.execute(dev)

tape.trainable_params = set(range(1, 1 + op.num_params))

grad_F = tape.jacobian(dev, method="numeric")

spy = mocker.spy(RewindTape, "_rewind_jacobian")
grad_A = tape.jacobian(dev, method="analytic")
spy.assert_called()
assert np.allclose(grad_A, grad_F, atol=tol, rtol=0)

def test_gradient_gate_with_multiple_parameters(self, tol, dev):
"""Tests that gates with multiple free parameters yield correct gradients."""
x, y, z = [0.5, 0.3, -0.7]

with RewindTape() as tape:
qml.RX(0.4, wires=[0])
qml.Rot(x, y, z, wires=[0])
qml.RY(-0.2, wires=[0])
qml.expval(qml.PauliZ(0))

tape.trainable_params = {1, 2, 3}

grad_A = tape.jacobian(dev, method="analytic")
grad_F = tape.jacobian(dev, method="numeric")

# gradient has the correct shape and every element is nonzero
assert grad_A.shape == (1, 3)
assert np.count_nonzero(grad_A) == 3
# the different methods agree
assert np.allclose(grad_A, grad_F, atol=tol, rtol=0)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Many of these are copied from the reversible tests, and edited accordingly.

@codecov
Copy link

codecov bot commented Jan 22, 2021

Codecov Report

Merging #1029 (c1c8e73) into master (2ae0263) will decrease coverage by 0.02%.
The diff coverage is 94.28%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1029      +/-   ##
==========================================
- Coverage   97.94%   97.91%   -0.03%     
==========================================
  Files         153      154       +1     
  Lines       11384    11441      +57     
==========================================
+ Hits        11150    11203      +53     
- Misses        234      238       +4     
Impacted Files Coverage Δ
pennylane/tape/__init__.py 100.00% <ø> (ø)
pennylane/tape/tapes/rewind.py 94.20% <94.20%> (ø)
pennylane/tape/tapes/__init__.py 100.00% <100.00%> (ø)
pennylane/numpy/tensor.py 93.75% <0.00%> (-0.85%) ⬇️
pennylane/_queuing.py 86.04% <0.00%> (-0.32%) ⬇️
pennylane/numpy/__init__.py 100.00% <0.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ae0263...ecb397c. Read the comment docs.

gradients of qubit operations using the rewind method of analytic differentiation.
This gradient method returns *exact* gradients, however requires use of a statevector simulator.
Simply create the tape, and then call the Jacobian method:

Copy link
Contributor

@albi3ro albi3ro Jan 25, 2021

Choose a reason for hiding this comment

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

Should we show how to create a tape as a rewind tape?

Suggested change
>>> dev = qml.Device('default.qubit', wires=[0])
>>> with qml.tape.RewindTape() as tape:
qml.RX(0.1, wires=0)


if not supported_device:
raise qml.QuantumFunctionError(
"The rewind gradient method is only supported on statevector-based devices"
Copy link
Contributor

Choose a reason for hiding this comment

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

The device could be a statevector device, yet just not use the same internal functions as default.qubit, the _apply_operation and _apply_unitary.

Maybe we should have one error for whether or not its a statevector device, and another for the internal functions. This would allow the error message to be more precise.

@trbromley
Copy link
Contributor Author

Closing this PR - after the benchmarking here we have decided to go with the approach in #1032.

@trbromley trbromley closed this Jan 25, 2021
@trbromley trbromley deleted the rewind_tape_pr1 branch January 25, 2021 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants