Repository: dwavesystems/dimod

Issue:
SpinTransform should propagate Structured inheritance

Discussion:

Patch:
diff --git a/dimod/core/structured.py b/dimod/core/structured.py
index 86ca0560..5a7acbc2 100644
--- a/dimod/core/structured.py
+++ b/dimod/core/structured.py
@@ -117,7 +117,7 @@ class Structured:
     def structure(self):
         """Structure of the structured sampler formatted as a
         :class:`~collections.namedtuple`, `Structure(nodelist, edgelist, adjacency)`,
-        where the 3-tuple values are the :attr:`.nodelist` and :attr:`.edgelist`
-        properties and :meth:`.adjacency` method.
+        where the 3-tuple values are the :attr:`.nodelist`, :attr:`.edgelist`
+        and :attr:`.adjacency` attributes.
         """
         return _Structure(self.nodelist, self.edgelist, self.adjacency)
diff --git a/dimod/utilities.py b/dimod/utilities.py
index 69905925..7f1acd22 100644
--- a/dimod/utilities.py
+++ b/dimod/utilities.py
@@ -20,7 +20,12 @@ import itertools
 
 from six import iteritems, itervalues
 
-__all__ = ['ising_energy', 'qubo_energy', 'ising_to_qubo', 'qubo_to_ising']
+__all__ = ['ising_energy',
+           'qubo_energy',
+           'ising_to_qubo',
+           'qubo_to_ising',
+           'child_structure_dfs',
+           ]
 
 
 def ising_energy(sample, h, J, offset=0.0):
@@ -340,3 +345,48 @@ def resolve_label_conflict(mapping, old_labels=None, new_labels=None):
             # don't need to add it to intermediate_to_new because it is a self-label
 
     return old_to_intermediate, intermediate_to_new
+
+
+def child_structure_dfs(sampler, seen=None):
+    """Find the structure of a composed sampler by a depth first search on the
+    children.
+
+    Args:
+        sampler (:obj:`.Sampler`):
+            A :class:`.Structured` sampler or a composed sampler with at least
+            one structured child.
+
+        seen (set, optional, default=False):
+            A set of the ids of the child samplers that have already been
+            checked.
+
+    Returns:
+        :class:`~collections.namedtuple`: A named tuple of the form
+        `Structure(nodelist, edgelist, adjacency)`, where the 3-tuple values
+        are the :attr:`.Structured.nodelist`, :attr:`.Structured.edgelist`
+        and :attr:`.Structured.adjacency` attributes of the first structured
+        sampler encountered.
+
+    """
+    seen = set() if seen is None else seen
+
+    if sampler not in seen:
+        try:
+            return sampler.structure
+        except AttributeError:
+            # hasattr just tries to access anyway...
+            pass
+
+    seen.add(sampler)
+
+    for child in sampler.children:
+        if child in seen:
+            continue
+
+        try:
+            return child_structure_dfs(child, seen=seen)
+        except ValueError:
+            # tree has no child samplers
+            pass
+
+    raise ValueError("no structured sampler found")
diff --git a/docs/reference/utilities.rst b/docs/reference/utilities.rst
index c2cb7143..6f178101 100644
--- a/docs/reference/utilities.rst
+++ b/docs/reference/utilities.rst
@@ -33,6 +33,17 @@ Decorators
    vartype_argument
 
 
+Graph-like
+==========
+
+.. currentmodule:: dimod.utilities
+
+.. autosummary::
+   :toctree: generated/
+
+   child_structure_dfs
+
+
 Serialization
 =============
 


Test patch:
diff --git a/tests/test_utilities.py b/tests/test_utilities.py
index 36386ba0..c175b83c 100644
--- a/tests/test_utilities.py
+++ b/tests/test_utilities.py
@@ -20,6 +20,8 @@ import unittest
 from itertools import groupby
 import itertools
 
+import dimod
+
 from dimod import ising_to_qubo, qubo_to_ising, ising_energy, qubo_energy
 
 
@@ -217,3 +219,39 @@ def normalized_matrix(mat):
     smat = sorted(((sorted(k), v) for k, v in mat.items()), key=key_fn)
     return dict((tuple(k), s) for k, g in groupby(smat, key=key_fn) for s in
                 [sum(v for _, v in g)] if s != 0)
+
+
+class TestChildStructureDFS(unittest.TestCase):
+    def test_sampler(self):
+        # not a composed sampler
+
+        nodelist = list(range(5))
+        edgelist = list(itertools.combinations(nodelist, 2))
+
+        class Dummy(dimod.Structured):
+            @property
+            def nodelist(self):
+                return nodelist
+
+            @property
+            def edgelist(self):
+                return edgelist
+
+        sampler = Dummy()
+
+        structure = dimod.child_structure_dfs(sampler)
+        self.assertEqual(structure.nodelist, nodelist)
+        self.assertEqual(structure.edgelist, edgelist)
+
+    def test_composed_sampler(self):
+        nodelist = list(range(5))
+        edgelist = list(itertools.combinations(nodelist, 2))
+
+        structured_sampler = dimod.StructureComposite(dimod.NullSampler(),
+                                                      nodelist, edgelist)
+
+        sampler = dimod.TrackingComposite(structured_sampler)
+
+        structure = dimod.child_structure_dfs(sampler)
+        self.assertEqual(structure.nodelist, nodelist)
+        self.assertEqual(structure.edgelist, edgelist)