Repository: dwavesystems/dimod

Issue:
`as_samples` should handle empty samples
**Description**
`as_samples` fails for empty samples lists.

**Steps To Reproduce**
`dimod.sampleset.as_samples(([], ['a']))`

**Expected Behavior**
Should return an empty sampleset
```
>>> samples, labels = dimod.sampleset.as_samples(([], ['a']))
>>> samples
[]
>>> labels
['a']
```



Discussion:

Patch:
diff --git a/benchmarks/benchmarks/__init__.py b/benchmarks/benchmarks/__init__.py
index 8b137891..e69de29b 100644
--- a/benchmarks/benchmarks/__init__.py
+++ b/benchmarks/benchmarks/__init__.py
@@ -1,1 +0,0 @@
-
diff --git a/benchmarks/benchmarks/bench_as_samples.py b/benchmarks/benchmarks/bench_as_samples.py
new file mode 100644
index 00000000..99d19bfc
--- /dev/null
+++ b/benchmarks/benchmarks/bench_as_samples.py
@@ -0,0 +1,82 @@
+# Copyright 2019 D-Wave Systems Inc.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+# =============================================================================
+import itertools
+
+import numpy as np
+import dimod
+
+
+from benchmarks.common import Benchmark
+
+
+class NumpyArray(Benchmark):
+    def setup(self):
+        self.empty = np.asarray([], dtype=np.int8)
+        self.arr100x100 = np.ones((100, 100), dtype=np.int8)
+        self.labels100 = list(range(100))
+
+    def time_empty(self):
+        dimod.as_samples(self.empty)
+
+    def time_100x100(self):
+        dimod.as_samples(self.arr100x100)
+
+    def time_empty_labelled(self):
+        dimod.as_samples((self.empty, []))
+
+    def time_100x100_labelled(self):
+        dimod.as_samples((self.arr100x100, self.labels100))
+
+
+class Dicts(Benchmark):
+    def setup(self):
+        # 1000 variables
+        variables = list(''.join(letters) for letters in itertools.product('abcdefghij', repeat=3))
+
+        self.dict1000 = {v: 1 for v in variables}
+        self.dict1x1000 = [{v: 1 for v in variables}]
+        self.dict1000x1000 = [{v: 1 for v in variables} for _ in range(1000)]
+
+    def time_empty(self):
+        dimod.as_samples({})
+
+    def time_dict1000(self):
+        dimod.as_samples(self.dict1000)
+
+    def time_dict1x1000(self):
+        dimod.as_samples(self.dict1x1000)
+
+    def time_dict1000x1000(self):
+        dimod.as_samples(self.dict1000x1000)
+
+
+class Lists(Benchmark):
+    def setup(self):
+        self.list1000 = [1]*1000
+        self.list1x1000 = [[1]*1000]
+        self.list1000x1000 = [[1]*1000 for _ in range(1000)]
+
+    def time_empty(self):
+        dimod.as_samples([])
+
+    def time_list1000(self):
+        dimod.as_samples(self.list1000)
+
+    def time_list1x1000(self):
+        dimod.as_samples(self.list1x1000)
+
+    def time_list1000x1000(self):
+        dimod.as_samples(self.list1000x1000)
diff --git a/benchmarks/benchmarks/bench_binary_quadratic_model.py b/benchmarks/benchmarks/bench_binary_quadratic_model.py
index efe8f304..7602df69 100644
--- a/benchmarks/benchmarks/bench_binary_quadratic_model.py
+++ b/benchmarks/benchmarks/bench_binary_quadratic_model.py
@@ -19,9 +19,7 @@ import copy
 import numpy as np
 import dimod
 
-
-class Benchmark(object):
-    goal_time = 0.25
+from benchmarks.common import Benchmark
 
 
 class Construction(Benchmark):
diff --git a/benchmarks/benchmarks/common.py b/benchmarks/benchmarks/common.py
new file mode 100644
index 00000000..073497d4
--- /dev/null
+++ b/benchmarks/benchmarks/common.py
@@ -0,0 +1,19 @@
+# Copyright 2019 D-Wave Systems Inc.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+# =============================================================================
+
+
+class Benchmark(object):
+    repeat = (1, 1000, .5)
diff --git a/benchmarks/requirements.txt b/benchmarks/requirements.txt
index 751f0f87..b3e29585 100644
--- a/benchmarks/requirements.txt
+++ b/benchmarks/requirements.txt
@@ -1,2 +1,4 @@
+-r ../requirements.txt
+
 asv
 virtualenv
diff --git a/dimod/__init__.py b/dimod/__init__.py
index e90ffac4..4fd70e1b 100644
--- a/dimod/__init__.py
+++ b/dimod/__init__.py
@@ -48,7 +48,7 @@ import dimod.higherorder
 
 from dimod.package_info import __version__, __author__, __authoremail__, __description__
 
-from dimod.sampleset import as_samples, SampleSet, concatenate
+from dimod.sampleset import as_samples, concatenate, SampleSet
 
 from dimod.response import *
 import dimod.response
diff --git a/dimod/sampleset.py b/dimod/sampleset.py
index b6f5a4d7..d19f6142 100644
--- a/dimod/sampleset.py
+++ b/dimod/sampleset.py
@@ -13,16 +13,14 @@
 #    limitations under the License.
 #
 # ================================================================================================
-"""
-Samples can be provided in many forms, generally referred to as samples_like, an extension
-of NumPy's array_like_ to allow for arbitrary variable labels.
-
-.. _array_like: https://docs.scipy.org/doc/numpy/user/basics.creation.html#converting-python-array-like-objects-to-numpy-arrays
-
-"""
 import itertools
+import numbers
+
+try:
+    import collections.abc as abc
+except ImportError:
+    import collections as abc
 
-from collections import Iterable, Sized, Mapping, Iterator
 from collections import namedtuple
 
 import numpy as np
@@ -30,32 +28,39 @@ import numpy as np
 from numpy.lib import recfunctions
 
 from dimod.decorators import vartype_argument
-from dimod.utilities import resolve_label_conflict
 from dimod.vartypes import Vartype
 from dimod.views import SampleView
 from dimod.variables import Variables
 
-__all__ = 'SampleSet', 'as_samples'
+__all__ = 'as_samples', 'concatenate', 'SampleSet'
 
 
-def as_samples(samples_like, dtype=None):
+def as_samples(samples_like, dtype=None, copy=False, order='C'):
     """Convert a samples_like object to a NumPy array and list of labels.
 
     Args:
         samples_like (samples_like):
-            A collection of raw samples. `samples_like` is an extension of NumPy's array_like_
-            structure. See examples below.
+            A collection of raw samples. `samples_like` is an extension of
+            NumPy's array_like_ structure. See examples below.
 
         dtype (data-type, optional):
-            dtype for the returned samples array. If not provided, it is either derived from
-            `samples_like`, if that object has a dtype, or set to :class:`numpy.int8`.
+            dtype for the returned samples array. If not provided, it is either
+            derived from `samples_like`, if that object has a dtype, or set to
+            :class:`numpy.int8`.
+
+        copy (bool, optional, default=False):
+            If true, then samples_like is guaranteed to be copied, otherwise
+            it is only copied if necessary.
+
+        order ({'K', 'A', 'C', 'F'}, optional, default='C'):
+            Specify the memory layout of the array. See :func:`numpy.array`.
 
     Returns:
         tuple: A 2-tuple containing:
 
             :obj:`numpy.ndarray`: Samples.
 
-            list: Variable labels as a list.
+            list: Variable labels
 
     Examples:
         The following examples convert a variety of samples_like objects:
@@ -99,54 +104,74 @@ def as_samples(samples_like, dtype=None):
                 [0, 0],
                 [0, 0]], dtype=int8), ['in', 'out'])
 
-    .. _array_like:  https://docs.scipy.org/doc/numpy/user/basics.creation.html#converting-python-array-like-objects-to-numpy-arrays
+    .. _array_like: https://docs.scipy.org/doc/numpy/user/basics.creation.html
 
     """
-
-    if isinstance(samples_like, Iterator):
-        raise TypeError('samples_like cannot be an iterator')
-
     if isinstance(samples_like, tuple) and len(samples_like) == 2:
-        # (samples_like, labels)
         samples_like, labels = samples_like
 
-        samples, __ = as_samples(samples_like)
+        if not isinstance(labels, list) and labels is not None:
+            labels = list(labels)
+    else:
+        labels = None
 
-        labels = list(labels)  # coerce and/or shallow copy
+    if isinstance(samples_like, abc.Iterator):
+        # if we don't check this case we can get unexpected behaviour where an
+        # iterator can be depleted
+        raise TypeError('samples_like cannot be an iterator')
 
-        if len(labels) != samples.shape[1]:
-            raise ValueError("labels and samples_like dimensions do not match")
+    if isinstance(samples_like, abc.Mapping):
+        return as_samples(([samples_like], labels), dtype=dtype)
 
-        return samples, labels
+    if (isinstance(samples_like, list) and samples_like and
+            isinstance(samples_like[0], numbers.Number)):
+        # this is not actually necessary but it speeds up the
+        # samples_like = [1, 0, 1,...] case significantly
+        return as_samples(([samples_like], labels), dtype=dtype)
 
-    # if no ` is specified and the array_like doesn't already have a dtype, we default to int8
-    if dtype is None and not hasattr(samples_like, 'dtype'):
-        dtype = np.int8
+    if not isinstance(samples_like, np.ndarray):
+        if any(isinstance(sample, abc.Mapping) for sample in samples_like):
+            # go through samples-like, turning the dicts into lists
+            samples_like, old = list(samples_like), samples_like
 
-    if len(samples_like) == 0:
-        return np.empty((0, 0), dtype=dtype), []
-
-    if isinstance(samples_like, Mapping):
-        return as_samples([samples_like], dtype=dtype)
+            if labels is None:
+                first = samples_like[0]
+                if isinstance(first, abc.Mapping):
+                    labels = list(first)
+                else:
+                    labels = list(range(len(first)))
 
-    if isinstance(samples_like, Iterable) and all(isinstance(sample, Mapping) for sample in samples_like):
-        # list of dicts
-        return _samples_dicts_to_array(samples_like)
+            for idx, sample in enumerate(old):
+                if isinstance(sample, abc.Mapping):
+                    try:
+                        samples_like[idx] = [sample[v] for v in labels]
+                    except KeyError:
+                        raise ValueError("samples_like and labels do not match")
 
-    # anything else should be array_like, which covers ndarrays, lists of lists, etc
+    if dtype is None and not hasattr(samples_like, 'dtype'):
+        dtype = np.int8
 
-    try:
-        samples_like = np.asarray(samples_like, dtype=dtype)
-    except (ValueError, TypeError):
-        raise TypeError("unknown format for samples_like")
+    # samples-like should now be array-like
+    arr = np.array(samples_like, dtype=dtype, copy=copy, order=order)
 
-    # want 2D array
-    if samples_like.ndim == 1:
-        samples_like = np.expand_dims(samples_like, 0)
-    elif samples_like.ndim > 2:
-        ValueError("expected sample_like to be <= 2 dimensions")
+    if arr.ndim > 2:
+        raise ValueError("expected samples_like to be <= 2 dimensions")
+    if arr.ndim < 2:
+        if arr.size:
+            arr = np.atleast_2d(arr)
+        elif labels:  # is not None and len > 0
+            arr = arr.reshape((0, len(labels)))
+        else:
+            arr = arr.reshape((0, 0))
 
-    return samples_like, list(range(samples_like.shape[1]))
+    # ok we're basically done, just need to check against the labels
+    if labels is None:
+        return arr, list(range(arr.shape[1]))
+    elif len(labels) != arr.shape[1]:
+        print(arr, arr.shape, samples_like, labels, len(labels))
+        raise ValueError("samples_like and labels dimensions do not match")
+    else:
+        return arr, labels
 
 
 def concatenate(samplesets, defaults=None):
@@ -215,7 +240,7 @@ def _iter_records(samplesets, vartype, variables):
             yield samples.record
 
 
-class SampleSet(Iterable, Sized):
+class SampleSet(abc.Iterable, abc.Sized):
     """Samples and any other data returned by dimod samplers.
 
     Args:
@@ -946,30 +971,3 @@ class SampleSet(Iterable, Sized):
             df.loc[:, field] = self.record[field]
 
         return df
-
-
-def _samples_dicts_to_array(samples_dicts):
-    """Convert an iterable of samples where each sample is a dict to a numpy 2d array. Also
-    determines the labels is they are None.
-    """
-    itersamples = iter(samples_dicts)
-
-    first_sample = next(itersamples)
-
-    labels = list(first_sample)
-
-    num_variables = len(labels)
-
-    def _iter_samples():
-        yield np.fromiter((first_sample[v] for v in labels),
-                          count=num_variables, dtype=np.int8)
-
-        try:
-            for sample in itersamples:
-                yield np.fromiter((sample[v] for v in labels),
-                                  count=num_variables, dtype=np.int8)
-        except KeyError:
-            msg = ("Each dict in 'samples' must have the same keys.")
-            raise ValueError(msg)
-
-    return np.stack(list(_iter_samples())), labels
diff --git a/docs/reference/sampleset.rst b/docs/reference/sampleset.rst
index 74431514..d9705d80 100644
--- a/docs/reference/sampleset.rst
+++ b/docs/reference/sampleset.rst
@@ -4,12 +4,10 @@
 Samples
 =======
 
-This module is under development as a possible replacement for :obj:`.Response`.
-
 sample_like Objects
 ===================
 
-.. automodule:: dimod.sampleset
+.. currentmodule:: dimod
 
 .. autosummary::
    :toctree: generated/
@@ -22,7 +20,6 @@ SampleSet
 Class
 -----
 
-.. currentmodule:: dimod
 .. autoclass:: SampleSet
 
 Properties


Test patch:
diff --git a/tests/test_sampleset.py b/tests/test_sampleset.py
index 08218875..eefa64fd 100644
--- a/tests/test_sampleset.py
+++ b/tests/test_sampleset.py
@@ -16,6 +16,8 @@
 import unittest
 import json
 
+from collections import OrderedDict
+
 import numpy as np
 
 import dimod
@@ -28,6 +30,102 @@ except ImportError:
     _pandas = False
 
 
+class Test_as_samples(unittest.TestCase):
+
+    def test_copy_false(self):
+        samples_like = np.ones((5, 5))
+        labels = list('abcde')
+        arr, lab = dimod.as_samples((samples_like, labels))
+        np.testing.assert_array_equal(arr, np.ones((5, 5)))
+        self.assertEqual(lab, list('abcde'))
+        self.assertIs(labels, lab)
+        self.assertTrue(np.shares_memory(arr, samples_like))
+
+    def test_dict_with_inconsistent_labels(self):
+        with self.assertRaises(ValueError):
+            dimod.as_samples(({'a': -1}, 'b'))
+
+    def test_dict_with_labels(self):
+        arr, labels = dimod.as_samples(({'a': -1}, 'a'))
+        np.testing.assert_array_equal(arr, [[-1]])
+        self.assertEqual(labels, ['a'])
+
+    def test_empty_dict(self):
+        # one sample, no labels
+        arr, labels = dimod.as_samples({})
+        np.testing.assert_array_equal(arr, np.zeros((1, 0)))
+        self.assertEqual(labels, [])
+
+    def test_empty_list(self):
+        # no samples, no labels
+        arr, labels = dimod.as_samples([])
+        np.testing.assert_array_equal(arr, np.zeros((0, 0)))
+        self.assertEqual(labels, [])
+
+    def test_empty_list_labelled(self):
+        # no samples, no labels
+        arr, labels = dimod.as_samples(([], []))
+        np.testing.assert_array_equal(arr, np.zeros((0, 0)))
+        self.assertEqual(labels, [])
+
+        # no samples, 1 label
+        arr, labels = dimod.as_samples(([], ['a']))
+        np.testing.assert_array_equal(arr, np.zeros((0, 1)))
+        self.assertEqual(labels, ['a'])
+
+        # no samples, 2 labels
+        arr, labels = dimod.as_samples(([], ['a', 'b']))
+        np.testing.assert_array_equal(arr, np.zeros((0, 2)))
+        self.assertEqual(labels, ['a', 'b'])
+
+    def test_empty_ndarray(self):
+        arr, labels = dimod.as_samples(np.ones(0))
+        np.testing.assert_array_equal(arr, np.zeros((0, 0)))
+        self.assertEqual(labels, [])
+
+    def test_iterator(self):
+        with self.assertRaises(TypeError):
+            dimod.as_samples(([-1] for _ in range(10)))
+
+    def test_iterator_labelled(self):
+        with self.assertRaises(TypeError):
+            dimod.as_samples(([-1] for _ in range(10)), 'a')
+
+    def test_list_of_empty(self):
+        arr, labels = dimod.as_samples([[], [], []])
+        np.testing.assert_array_equal(arr, np.empty((3, 0)))
+        self.assertEqual(labels, [])
+
+        arr, labels = dimod.as_samples([{}, {}, {}])
+        np.testing.assert_array_equal(arr, np.empty((3, 0)))
+        self.assertEqual(labels, [])
+
+        arr, labels = dimod.as_samples(np.empty((3, 0)))
+        np.testing.assert_array_equal(arr, np.empty((3, 0)))
+        self.assertEqual(labels, [])
+
+    def test_mixed_sampletype(self):
+        s1 = [0, 1]
+        s2 = OrderedDict([(1, 0), (0, 1)])
+        s3 = OrderedDict([(0, 1), (1, 0)])
+
+        arr, labels = dimod.as_samples([s1, s2, s3])
+        np.testing.assert_array_equal(arr, [[0, 1], [1, 0], [1, 0]])
+        self.assertEqual(labels, [0, 1])
+
+    def test_ndarray(self):
+        arr, labels = dimod.as_samples(np.ones(5, dtype=np.int32))
+        np.testing.assert_array_equal(arr, np.ones((1, 5)))
+        self.assertEqual(labels, list(range(5)))
+        self.assertEqual(arr.dtype, np.int32)
+
+    def test_ndarray_labelled(self):
+        arr, labels = dimod.as_samples((np.ones(5, dtype=np.int32), 'abcde'))
+        np.testing.assert_array_equal(arr, np.ones((1, 5)))
+        self.assertEqual(labels, ['a', 'b', 'c', 'd', 'e'])
+        self.assertEqual(arr.dtype, np.int32)
+
+
 class TestSampleSet(unittest.TestCase):
     def test_from_samples(self):
 
@@ -119,8 +217,6 @@ class TestSampleSet(unittest.TestCase):
 
         self.assertEqual(len(dimod.SampleSet.from_samples([], dimod.SPIN, energy=[], a=1)), 0)
 
-        self.assertEqual(len(dimod.SampleSet.from_samples({}, dimod.SPIN, energy=[], a=1)), 0)
-
         self.assertEqual(len(dimod.SampleSet.from_samples(np.empty((0, 0)), dimod.SPIN, energy=[], a=1)), 0)
 
     def test_from_samples_with_aggregation(self):