Repository: dwavesystems/dwave-cloud-client

Issue:
Unstructured solvers do not update sampleset with general problem data
We should add `problem_id` and `problem_label` to `sampleset.info` returned.

Discussion:

Patch:
diff --git a/dwave/cloud/computation.py b/dwave/cloud/computation.py
index fd5e0e7..8c6000f 100644
--- a/dwave/cloud/computation.py
+++ b/dwave/cloud/computation.py
@@ -772,9 +772,19 @@ class Future(object):
     def wait_sampleset(self):
         """Blocking sampleset getter."""
 
+        # blocking result get
         result = self._load_result()
+
+        # common problem info: id/label
+        problem_info = dict(problem_id=self.id)
+        if self.label is not None:
+            problem_info.update(problem_label=self.label)
+
+        # sapi returned sampleset directly
         if 'sampleset' in result:
-            return result['sampleset']
+            sampleset = result['sampleset']
+            sampleset.info.update(problem_info)
+            return sampleset
 
         # construct sampleset from available data
         try:
@@ -792,10 +802,9 @@ class Future(object):
         vartype_from_problem_type = {'ising': 'SPIN', 'qubo': 'BINARY'}
         vartype = vartype_from_problem_type[self.problem_type]
 
-        # include timing and id/label in info
-        info = dict(timing=self.timing, problem_id=self.id)
-        if self.label is not None:
-            info.update(problem_label=self.label)
+        # for QPU jobs, info field is blank; add timing info
+        info = dict(timing=self.timing)
+        info.update(problem_info)
 
         sampleset = dimod.SampleSet.from_samples(
             (samples, variables), vartype=vartype,


Test patch:
diff --git a/tests/test_mock_submission.py b/tests/test_mock_submission.py
index c1da578..83bc2c9 100644
--- a/tests/test_mock_submission.py
+++ b/tests/test_mock_submission.py
@@ -69,7 +69,7 @@ def solver_data(id_, incomplete=False):
     return obj
 
 
-def complete_reply(id_, solver_name, answer=None, msg=None, label=""):
+def complete_reply(id_, solver_name, answer=None, msg=None, label=None):
     """Reply with solutions for the test problem."""
     response = {
         "status": "COMPLETED",
@@ -101,7 +101,7 @@ def complete_reply(id_, solver_name, answer=None, msg=None, label=""):
     return json.dumps(response)
 
 
-def complete_no_answer_reply(id_, solver_name, label=""):
+def complete_no_answer_reply(id_, solver_name, label=None):
     """A reply saying a problem is finished without providing the results."""
     return json.dumps({
         "status": "COMPLETED",
@@ -114,7 +114,7 @@ def complete_no_answer_reply(id_, solver_name, label=""):
     })
 
 
-def error_reply(id_, solver_name, error, label=""):
+def error_reply(id_, solver_name, error, label=None):
     """A reply saying an error has occurred."""
     return json.dumps({
         "status": "FAILED",
@@ -136,7 +136,7 @@ def immediate_error_reply(code, msg):
     })
 
 
-def cancel_reply(id_, solver_name, label=""):
+def cancel_reply(id_, solver_name, label=None):
     """A reply saying a problem was canceled."""
     return json.dumps({
         "status": "CANCELLED",
@@ -154,7 +154,7 @@ def datetime_in_future(seconds=0):
     return now + timedelta(seconds=seconds)
 
 
-def continue_reply(id_, solver_name, now=None, eta_min=None, eta_max=None, label=""):
+def continue_reply(id_, solver_name, now=None, eta_min=None, eta_max=None, label=None):
     """A reply saying a problem is still in the queue."""
 
     if not now:
diff --git a/tests/test_mock_unstructured_submission.py b/tests/test_mock_unstructured_submission.py
index b212311..c297286 100644
--- a/tests/test_mock_unstructured_submission.py
+++ b/tests/test_mock_unstructured_submission.py
@@ -39,7 +39,7 @@ def unstructured_solver_data(problem_type='bqm'):
         "description": "A test unstructured solver"
     }
 
-def complete_reply(sampleset, problem_type='bqm', label=""):
+def complete_reply(sampleset, id_="problem-id", type_='bqm', label=None):
     """Reply with the sampleset as a solution."""
 
     return json.dumps([{
@@ -51,8 +51,8 @@ def complete_reply(sampleset, problem_type='bqm', label=""):
             "format": "bq",
             "data": sampleset.to_serializable()
         },
-        "type": problem_type,
-        "id": "problem-id",
+        "type": type_,
+        "id": id_,
         "label": label
     }])
 
@@ -95,8 +95,9 @@ class TestUnstructuredSolver(unittest.TestCase):
 
                 # direct bqm sampling
                 ss = dimod.ExactSolver().sample(bqm)
+                ss.info.update(problem_id=mock_problem_id)
                 session.post = lambda path, _: choose_reply(
-                    path, {'problems/': complete_reply(ss)})
+                    path, {'problems/': complete_reply(ss, id_=mock_problem_id)})
 
                 fut = solver.sample_bqm(bqm)
                 numpy.testing.assert_array_equal(fut.sampleset, ss)
@@ -114,8 +115,9 @@ class TestUnstructuredSolver(unittest.TestCase):
                 # ising sampling
                 lin, quad, _ = bqm.to_ising()
                 ss = dimod.ExactSolver().sample_ising(lin, quad)
+                ss.info.update(problem_id=mock_problem_id)
                 session.post = lambda path, _: choose_reply(
-                    path, {'problems/': complete_reply(ss)})
+                    path, {'problems/': complete_reply(ss, id_=mock_problem_id)})
 
                 fut = solver.sample_ising(lin, quad)
                 numpy.testing.assert_array_equal(fut.sampleset, ss)
@@ -126,8 +128,9 @@ class TestUnstructuredSolver(unittest.TestCase):
                 # qubo sampling
                 qubo, _ = bqm.to_qubo()
                 ss = dimod.ExactSolver().sample_qubo(qubo)
+                ss.info.update(problem_id=mock_problem_id)
                 session.post = lambda path, _: choose_reply(
-                    path, {'problems/': complete_reply(ss)})
+                    path, {'problems/': complete_reply(ss, id_=mock_problem_id)})
 
                 fut = solver.sample_qubo(qubo)
                 numpy.testing.assert_array_equal(fut.sampleset, ss)
@@ -168,8 +171,9 @@ class TestUnstructuredSolver(unittest.TestCase):
 
                 # use bqm for mock response (for now)
                 ss = dimod.ExactSolver().sample(dimod.BQM.empty('SPIN'))
+                ss.info.update(problem_id=mock_problem_id)
                 session.post = lambda path, _: choose_reply(
-                    path, {'problems/': complete_reply(ss, problem_type=problem_type)})
+                    path, {'problems/': complete_reply(ss, id_=mock_problem_id, type_=problem_type)})
 
                 # verify decoding works
                 fut = solver.sample_dqm(dqm)
@@ -299,3 +303,41 @@ class TestProblemLabel(unittest.TestCase):
 
                             with self.assertRaises(self.PrimaryAssertionSatisfied):
                                 sample(*problem_args, label=label).result()
+
+    @parameterized.expand([
+        ("undefined", None),
+        ("empty", ""),
+        ("string", "text label")
+    ])
+    def test_label_is_received(self, name, label):
+        """Problem label is set from response in result/sampleset."""
+
+        # build a test problem
+        bqm = dimod.BQM.from_ising({}, {'ab': 1})
+
+        # use a global mocked session, so we can modify it on-fly
+        session = mock.Mock()
+
+        # upload is now part of submit, so we need to mock it
+        mock_problem_id = 'mock-problem-id'
+        def mock_upload(self, bqm):
+            return Present(result=mock_problem_id)
+
+        # construct a functional solver by mocking client and api response data
+        with mock.patch.multiple(Client, create_session=lambda self: session,
+                                 upload_problem_encoded=mock_upload):
+            with Client('endpoint', 'token') as client:
+                solver = BQMSolver(client, unstructured_solver_data())
+
+                # construct mock response
+                ss = dimod.ExactSolver().sample(bqm)
+                session.post = lambda path, _: choose_reply(
+                    path, {'problems/': complete_reply(ss, id_=mock_problem_id, label=label)})
+
+                # sample and verify label
+                fut = solver.sample_bqm(bqm, label=label)
+                info = fut.sampleset.info
+                self.assertIn('problem_id', info)
+                if label is not None:
+                    self.assertIn('problem_label', info)
+                    self.assertEqual(info['problem_label'], label)