Skip to content

Commit bc03508

Browse files
authored
Fix TQATrainer returning incorrect optimised QAOA angles (#35)
2 parents 9e5ffce + fa0c641 commit bc03508

File tree

4 files changed

+179
-38
lines changed

4 files changed

+179
-38
lines changed

README.md

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -156,23 +156,24 @@ This repository is still in development: new functionality is being added and th
156156

157157
## Version tracking
158158

159-
| Version | Added functionality | Pull request |
160-
|---------|----------------------------------------|--------------|
161-
| 1 | Track system information | #8 |
162-
| 2 | Add history mix-in | #11 |
163-
| 3 | Add fidelity bounds for MPS | #6 |
164-
| 4 | Add QAOA angles functions | #14 |
165-
| 5 | Switch to qaoa_ansatz | #16 |
166-
| 6 | Add Pauli Propagation | #15 |
167-
| 7 | Add problem class in train | #17 |
168-
| 8 | Improve train tests | #22 |
169-
| 9 | Add SAT map pre-processing | #19 |
170-
| 10 | Add recursive transition states trainer| #20 |
171-
| 11 | Bug fix in train pre-processing data | #24 |
172-
| 12 | More data in result saving in train.py | #26 |
173-
| 13 | Create PPEvaluator from configs | #25 |
174-
| 14 | Custom ansatz operator to state vector | #29 |
175-
| 15 | Remove python 3.9 support | #31 |
159+
| Version | Added functionality | Pull request |
160+
|---------|--------------------------------------------------------------|--------------|
161+
| 1 | Track system information | #8 |
162+
| 2 | Add history mix-in | #11 |
163+
| 3 | Add fidelity bounds for MPS | #6 |
164+
| 4 | Add QAOA angles functions | #14 |
165+
| 5 | Switch to qaoa_ansatz | #16 |
166+
| 6 | Add Pauli Propagation | #15 |
167+
| 7 | Add problem class in train | #17 |
168+
| 8 | Improve train tests | #22 |
169+
| 9 | Add SAT map pre-processing | #19 |
170+
| 10 | Add recursive transition states trainer | #20 |
171+
| 11 | Bug fix in train pre-processing data | #24 |
172+
| 12 | More data in result saving in train.py | #26 |
173+
| 13 | Create PPEvaluator from configs | #25 |
174+
| 14 | Custom ansatz operator to state vector | #29 |
175+
| 15 | Remove python 3.9 support | #31 |
176+
| 16 | Fix TQATrainer qaoa_angles_function and returned ParamResult | #35 |
176177

177178
## IBM Public Repository Disclosure
178179

qaoa_training_pipeline/training/param_result.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def __init__(
4747
"system": platform.system(),
4848
"processor": platform.processor(),
4949
"platform": platform.platform(),
50-
"qaoa_training_pipeline_version": 15,
50+
"qaoa_training_pipeline_version": 16,
5151
}
5252

5353
# Convert, e.g., np.float to float

qaoa_training_pipeline/training/tqa_trainer.py

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,61 @@
1010

1111
from time import time
1212
from typing import Any, Dict, Optional
13+
1314
import matplotlib.pyplot as plt
1415
import numpy as np
15-
1616
from qiskit import QuantumCircuit
1717
from qiskit.quantum_info import SparsePauliOp
1818
from scipy.optimize import minimize
1919

2020
from qaoa_training_pipeline.evaluation import EVALUATORS
2121
from qaoa_training_pipeline.evaluation.base_evaluator import BaseEvaluator
22-
from qaoa_training_pipeline.training.history_mixin import HistoryMixin
2322
from qaoa_training_pipeline.training.base_trainer import BaseTrainer
23+
from qaoa_training_pipeline.training.functions import BaseAnglesFunction
24+
from qaoa_training_pipeline.training.history_mixin import HistoryMixin
2425
from qaoa_training_pipeline.training.param_result import ParamResult
2526

2627

28+
class TQATrainerFunction(BaseAnglesFunction):
29+
"""Wrapper function around TQATrainer.tqa_schedule.
30+
31+
This function allows for a default ``rep`` value to be set, which is passed
32+
to ``tqa_schedule``. If no value has been set yet, and no value is provided
33+
to :meth:`__call__`, then an error is raised.
34+
"""
35+
36+
def __init__(self, tqa_schedule_method: callable, reps: int | None = None) -> None:
37+
"""Create an instance of a TQATrainer QAOA angles function.
38+
39+
Args:
40+
tqa_schedule_method: The :meth:`TQATrainer.tqa_schedule` method for
41+
an instance of :class:`TQATrainer`.
42+
reps: The default reps to use if not overridden by
43+
:meth:`TQATrainer.train`. If None and ``reps`` is not provided
44+
in :meth:`__call__`, an error is raised. Defaults to None.
45+
"""
46+
super().__init__()
47+
self._tqa_schedule = tqa_schedule_method
48+
self.reps = reps
49+
50+
# pylint: disable=unused-argument
51+
def __call__(self, x: list, reps: int | None = None) -> list:
52+
if reps is None:
53+
reps = self.reps
54+
if reps is None:
55+
raise ValueError(
56+
f"reps must be provided to {self.__class__.__name__}(reps=...) or "
57+
+ "set with trainer.train(..., reps=...)"
58+
)
59+
return self._tqa_schedule(reps=reps, dt=x[0])
60+
61+
# pylint: disable=unused-argument
62+
@classmethod
63+
def from_config(cls, config: dict) -> None:
64+
"""Create a TQATrainer from a config dictionary."""
65+
raise RuntimeError(f"{cls.__name__} cannot be constructed from a config.")
66+
67+
2768
class TQATrainer(BaseTrainer, HistoryMixin):
2869
"""Trotterized Quantum Annealing parameter generation.
2970
@@ -33,13 +74,24 @@ class TQATrainer(BaseTrainer, HistoryMixin):
3374
be used as an initial point generator which is independent of problem instance and
3475
does not do any optimization. However, we can also use this class to perform a
3576
SciPy optimization of the end point of the TQA schedule.
77+
78+
79+
.. note::
80+
81+
:attr:`qaoa_angles_function` for :class:`TQATrainer` requires knowledge
82+
of the number of repetitions. ``reps`` can be provided by calling
83+
``qaoa_angles_function(params, reps=reps)``. If no value for ``reps`` is
84+
provided, the value for the most recent call to :meth:`train` will be
85+
used. If :meth:`train` has not been called yet for the instance of
86+
:class:`TQATrainer`, then an error is raised.
3687
"""
3788

3889
def __init__(
3990
self,
4091
evaluator: Optional[BaseEvaluator] = None,
4192
minimize_args: Optional[Dict[str, Any]] = None,
4293
energy_minimization: bool = False,
94+
initial_dt: float = 0.75,
4395
) -> None:
4496
"""Initialize an instance.
4597
@@ -51,11 +103,18 @@ def __init__(
51103
energy_minimization: Allows us to switch between minimizing the energy or maximizing
52104
the energy. The default and assumed convention in this repository is to
53105
maximize the energy.
106+
initial_dt: Initial dt if not provided to :meth:`train`. Defaults to
107+
``0.75``.
54108
"""
55-
BaseTrainer.__init__(self, evaluator)
109+
BaseTrainer.__init__(
110+
self,
111+
evaluator,
112+
qaoa_angles_function=TQATrainerFunction(self.tqa_schedule, reps=None),
113+
)
56114
HistoryMixin.__init__(self)
57115

58116
self._minimize_args = {"method": "COBYLA", "options": {"maxiter": 20, "rhobeg": 0.1}}
117+
self.initial_dt = initial_dt
59118

60119
minimize_args = minimize_args or {}
61120
self._minimize_args.update(minimize_args)
@@ -76,9 +135,17 @@ def train(
76135
mixer: Optional[QuantumCircuit] = None,
77136
initial_state: Optional[QuantumCircuit] = None,
78137
ansatz_circuit: Optional[QuantumCircuit] = None,
138+
initial_dt: float | None = None,
79139
) -> ParamResult:
80140
"""Train the QAOA parameters."""
81141
self.reset_history()
142+
# Set the reps attribute on the angles function, if it supports one.
143+
# This allow us to override it later with a function that doesn't
144+
# require reps. We set reps here so that ParamResult.from_scipy_result
145+
# correctly populates "optimized_qaoa_angles" and so we can call
146+
# `trainer.qaoa_angles_function()` in scripts.
147+
if hasattr(self._qaoa_angles_function, "reps"):
148+
self._qaoa_angles_function.reps = reps
82149

83150
def _energy(x):
84151
"""Optimize the energy by minimizing the negative energy.
@@ -105,21 +172,15 @@ def _energy(x):
105172

106173
start = time()
107174

175+
initial_dt = initial_dt or self.initial_dt
108176
if self.evaluator is None:
109-
tqa_dt = 0.75
110-
param_result = ParamResult(
111-
self.tqa_schedule(reps, dt=tqa_dt), time() - start, self, None
112-
)
177+
param_result = ParamResult([initial_dt], time() - start, self, None)
113178
else:
114-
params0 = [0.75]
179+
params0 = [initial_dt]
115180
result = minimize(_energy, params0, **self._minimize_args)
116181
param_result = ParamResult.from_scipy_result(
117182
result, params0, time() - start, self._sign, self
118183
)
119-
param_result["optimized_params"] = self.tqa_schedule(
120-
reps, dt=param_result["optimized_params"]
121-
)
122-
123184
param_result.add_history(self)
124185

125186
return param_result

test/training/test_tqa.py

Lines changed: 87 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,40 +8,116 @@
88

99
"""Classes to test the TQA trainer."""
1010

11-
from test import TrainingPipelineTestCase
12-
1311
from qiskit.quantum_info import SparsePauliOp
1412

1513
from qaoa_training_pipeline.evaluation.mps_evaluator import MPSEvaluator
14+
from qaoa_training_pipeline.training.param_result import ParamResult
1615
from qaoa_training_pipeline.training.tqa_trainer import TQATrainer
1716

17+
# Disable import order for this line. Python has a stdlib test module, but this
18+
# is our own one. Therefore, it is imported with third-party libraries.
19+
from test import TrainingPipelineTestCase # pylint: disable=wrong-import-order
20+
1821

1922
class TestTQA(TrainingPipelineTestCase):
2023
"""Class to test the TQA trainer."""
2124

2225
def test_no_optim(self):
2326
""" "Test that we can run without doing any optimization."""
24-
result = TQATrainer().train(None, 3)
25-
26-
self.assertListEqual(result["optimized_params"], [0.875, 0.625, 0.375, 0.125, 0.375, 0.625])
27+
reps = 3
28+
trainer = TQATrainer()
29+
30+
with self.assertRaises(
31+
ValueError,
32+
msg="Calling qaoa_angles_function without reps=... "
33+
+ "on untrained TQATrainer should raise an error.",
34+
):
35+
_ = trainer.qaoa_angles_function([0.2])
36+
37+
self.assertTrue(
38+
len(trainer.qaoa_angles_function([0.2], reps=reps)) == 2 * reps,
39+
msg="Calling qaoa_angles_function with reps=... on untrained "
40+
+ "TQATrainer should return list of angles.",
41+
)
42+
43+
result = trainer.train(None, reps)
44+
45+
self.assertListEqual(
46+
result["optimized_qaoa_angles"],
47+
[0.875, 0.625, 0.375, 0.125, 0.375, 0.625],
48+
msg="Number of QAOA angles is not as expected.",
49+
)
50+
self.assertListEqual(
51+
result["optimized_params"],
52+
[0.75],
53+
msg="Optimized params with default argument should be [0.75]",
54+
)
2755

2856
# Check that history is not present.
2957
self.assertTrue(len(result["energy_history"]) == 0)
3058
self.assertTrue(len(result["parameter_history"]) == 0)
3159
self.assertTrue(len(result["energy_evaluation_time"]) == 0)
60+
# Double check that the default number of reps for qaoa_angles_function
61+
# is the same as the most recent run.
62+
self.assertTrue(
63+
len(trainer.qaoa_angles_function(result["optimized_params"])) == 2 * reps,
64+
msg="Calling qaoa_angles_function without reps=... "
65+
+ "on trained TQATrainer should return list of angles.",
66+
)
67+
68+
result = trainer.train(None, reps + 1)
69+
self.assertTrue(
70+
len(trainer.qaoa_angles_function(result["optimized_params"])) == 2 * (reps + 1),
71+
msg="Calling qaoa_angles_function without reps=... "
72+
+ "on trained TQATrainer should return list of angles.",
73+
)
3274

3375
def test_optim(self):
3476
"""Test that we can optimize the dt of the TQA schedule."""
3577
evaluator = MPSEvaluator()
3678

79+
reps = 4
3780
trainer = TQATrainer(evaluator)
3881

82+
with self.assertRaises(
83+
ValueError,
84+
msg="Calling qaoa_angles_function without reps=... "
85+
+ "on untrained TQATrainer should raise an error.",
86+
):
87+
_ = trainer.qaoa_angles_function([0.2])
88+
89+
self.assertTrue(
90+
len(trainer.qaoa_angles_function([0.2], reps=reps)) == 2 * reps,
91+
msg="Calling qaoa_angles_function with reps=... "
92+
+ "on untrained TQATrainer should return list of angles.",
93+
)
94+
3995
cost_op = SparsePauliOp.from_list([("ZIIZ", -1), ("IZIZ", -1), ("IIZZ", -1)])
4096

41-
result = trainer.train(cost_op, reps=4)
97+
result: ParamResult = trainer.train(cost_op, reps=reps)
4298

4399
self.assertEqual(result["success"], "True")
44-
self.assertEqual(len(result["optimized_params"]), 8)
100+
self.assertEqual(
101+
len(result["optimized_params"]),
102+
1,
103+
msg="There is only one parameter, dt, for TQATrainer.",
104+
)
105+
self.assertEqual(
106+
len(result["optimized_qaoa_angles"]),
107+
2 * reps,
108+
msg="Number of QAOA angles is not as expected.",
109+
)
110+
self.assertTrue(
111+
len(trainer.qaoa_angles_function(result["optimized_params"])) == 2 * reps,
112+
msg="Calling qaoa_angles_function without reps=... "
113+
+ "on trained TQATrainer should return list of angles.",
114+
)
115+
self.assertListEqual(
116+
result["optimized_qaoa_angles"],
117+
trainer.qaoa_angles_function(result["optimized_params"]),
118+
msg="Calling qaoa_angles_function without reps=... "
119+
+ "on trained TQATrainer should return the same angles.",
120+
)
45121

46122
# Check that history is present.
47123
self.assertTrue(len(result["energy_history"]) > 0)
@@ -55,7 +131,10 @@ def test_from_config(self):
55131
trainer = TQATrainer.from_config(config)
56132
self.assertIsNone(trainer.evaluator)
57133

58-
config = {"evaluator": "MPSEvaluator", "evaluator_init": {"bond_dim_circuit": 2}}
134+
config = {
135+
"evaluator": "MPSEvaluator",
136+
"evaluator_init": {"bond_dim_circuit": 2},
137+
}
59138

60139
trainer = TQATrainer.from_config(config)
61140
self.assertTrue(isinstance(trainer.evaluator, MPSEvaluator))

0 commit comments

Comments
 (0)