Repository: dwavesystems/dwave-system (language: python)

Issue:
Inconsistent `SampleSet` interface across Leap hybrid samplers
BQM sampler, CQM sampler, as well as the `sample_*` methods on `dwave.cloud.Solver`s return a `SampleSet` with `wait_id()`:

```python
>>> import dimod
>>> from dwave.system import LeapHybridBQMSampler
>>> 
>>> bqm = dimod.BQM.from_qubo({})
>>> 
>>> sampler = LeapHybridBQMSampler()
>>> ss = sampler.sample(bqm)
>>> 
>>> ss.wait_id()
'a59...dc95'
```

```python
>>> import dimod
>>> from dwave.system import LeapHybridCQMSampler
>>> 
>>> cqm = dimod.CQM.from_bqm(dimod.BQM.from_qubo({'ab': 1}))
>>> 
>>> sampler = LeapHybridCQMSampler()
>>> ss = sampler.sample_cqm(cqm)
>>> 
>>> ss.wait_id()
'54b...3b1'
```

But the DQM sampler doesn't:
```python
>>> import dimod
>>> from dwave.system import LeapHybridDQMSampler
>>> 
>>> dqm = dimod.DQM()
>>> dqm.add_variable(1)
0
>>> 
>>> sampler = LeapHybridDQMSampler()
>>> ss = sampler.sample_dqm(dqm)
>>> 
>>> ss.wait_id()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'SampleSet' object has no attribute 'wait_id'
```

**Root cause**: `.wait_id` is set in the `Future.sampleset` property (cloud-client), and since both BQM and CQM samplers defer sampleset contruction to the cloud-client, `wait_id` is available. DQM sampler, on the other hand strips and re-attaches labels, so it constructs the sampleset via `@dimod.decorators.nonblocking_sample_method` (that doesn't add the `wait_id` method).

**Proposed solution**:
- extend `dimod.SampleSet` with `wait_id` during construction in `SampleSet.from_future()`
  - done in https://github.com/dwavesystems/dimod/pull/1393
- stop setting `wait_id` in the cloud-client
  - done in https://github.com/dwavesystems/dwave-cloud-client/pull/674

It's backwards-compatible, fixes the issue at hand, and will even work with older clients.


Discussion / hints:
> extend dimod.SampleSet with wait_id during construction in SampleSet.from_future()

At least in principal, `SampleSet.from_future()` is meant to use with any `Future`-like object, the notion of an `id` is specific to the cloud-client. On the other hand, this is the only place we use `from_future()` so far as I know after years of being more general in principal. So probably not too much of a restriction.

Fix patch:
diff --git a/requirements.txt b/requirements.txt
index a21f38d0..bc0a3e7f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-dimod==0.12.17
+dimod==0.12.18
 dwave-optimization==0.4.0
 dwave-preprocessing==0.6.7
 dwave-cloud-client==0.12.0
diff --git a/setup.py b/setup.py
index ad7e9950..f701582f 100644
--- a/setup.py
+++ b/setup.py
@@ -24,7 +24,7 @@
 exec(open(os.path.join(".", "dwave", "system", "package_info.py")).read())
 
 
-install_requires = ['dimod>=0.12.7,<0.14.0',
+install_requires = ['dimod>=0.12.18,<0.14.0',
                     'dwave-optimization>=0.1.0,<0.6',
                     'dwave-cloud-client>=0.12.0,<0.14.0',
                     'dwave-networkx>=0.8.10',


Test patch:
diff --git a/tests/qpu/test_leaphybrid_common.py b/tests/qpu/test_leaphybrid_common.py
index 0b425638..25cbe30c 100644
--- a/tests/qpu/test_leaphybrid_common.py
+++ b/tests/qpu/test_leaphybrid_common.py
@@ -19,6 +19,7 @@
 
 from parameterized import parameterized_class
 
+import dimod
 from dwave.cloud.exceptions import ConfigFileError, SolverNotFoundError
 from dwave.cloud.testing import isolated_environ
 from dwave.system import LeapHybridSampler, LeapHybridDQMSampler, LeapHybridCQMSampler
@@ -83,3 +84,29 @@ def test_old(self):
             self.assertIn(self.problem_type, sampler.solver.supported_problem_types)
 
         sampler.client.close()
+
+
+@parameterized_class(
+    ("sampler_cls", "sample_meth", "problem_gen"), [
+        (LeapHybridSampler, "sample", lambda self: dimod.BQM.from_qubo({})),
+        (LeapHybridDQMSampler, "sample_dqm", lambda self: dimod.DQM.from_numpy_vectors([0], [0], ([], [], []))),
+        (LeapHybridCQMSampler, "sample_cqm", lambda self: dimod.CQM.from_bqm(dimod.BQM.from_qubo({'ab': 1}))),
+    ])
+@unittest.skipIf(os.getenv('SKIP_INT_TESTS'), "Skipping integration test.")
+class TestSamplesetInterface(unittest.TestCase):
+
+    def test_wait_id_availability(self):
+        # verify https://github.com/dwavesystems/dwave-system/issues/540 is fixed
+
+        sampler = self.sampler_cls()
+        problem = self.problem_gen()
+        ss = getattr(sampler, self.sample_meth)(problem)
+
+        with self.subTest("sampleset.wait_id() exists"):
+            pid = ss.wait_id()
+            self.assertIsInstance(pid, str)
+
+        with self.subTest("sampleset.wait_id() exists post-resolve"):
+            ss.resolve()
+            pid_post = ss.wait_id()
+            self.assertEqual(pid, pid_post)