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
 =============
 

SpinTransform should propagate Structured inheritance
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)

pip install -e .[all]
parse_log_pytest
requirements.txt
pytest coverage codecov
apt-get update apt-get install -y libboost-dev
3.6
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
charset-normalizer==2.0.12
codecov==2.1.13
coverage==6.2
Cython==3.0.12
decorator==5.1.1
-e git+https://github.com/dwavesystems/dimod.git@1f6e29b84db61ffd337abc14e982ed022b3391b5#egg=dimod
idna==3.10
importlib-metadata==4.8.3
iniconfig==1.1.1
jsonschema==2.6.0
mock==2.0.0
networkx==2.0
numpy==1.15.0
packaging==21.3
pandas==0.22.0
pbr==6.1.1
pluggy==1.0.0
py==1.11.0
pymongo==3.7.1
pyparsing==3.1.4
pytest==7.0.1
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.27.1
simplejson==3.16.0
six==1.11.0
tomli==1.2.3
typing_extensions==4.1.1
urllib3==1.26.20
zipp==3.6.0

name: dimod
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - charset-normalizer==2.0.12
      - codecov==2.1.13
      - coverage==6.2
      - cython==3.0.12
      - decorator==5.1.1
      - idna==3.10
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - jsonschema==2.6.0
      - mock==2.0.0
      - networkx==2.0
      - numpy==1.15.0
      - packaging==21.3
      - pandas==0.22.0
      - pbr==6.1.1
      - pluggy==1.0.0
      - py==1.11.0
      - pymongo==3.7.1
      - pyparsing==3.1.4
      - pytest==7.0.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.27.1
      - simplejson==3.16.0
      - six==1.11.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
prefix: /opt/conda/envs/dimod

tests/test_utilities.py::TestChildStructureDFS::test_composed_sampler tests/test_utilities.py::TestChildStructureDFS::test_sampler
tests/test_utilities.py::TestIsingEnergy::test_trivial tests/test_utilities.py::TestIsingEnergy::test_typical tests/test_utilities.py::TestQuboEnergy::test_trivial tests/test_utilities.py::TestQuboEnergy::test_typical tests/test_utilities.py::TestIsingToQubo::test_energy tests/test_utilities.py::TestIsingToQubo::test_j_diag tests/test_utilities.py::TestIsingToQubo::test_no_zeros tests/test_utilities.py::TestIsingToQubo::test_offset_propogation tests/test_utilities.py::TestIsingToQubo::test_trivial tests/test_utilities.py::TestIsingToQubo::test_typical tests/test_utilities.py::TestQuboToIsing::test_no_zeros tests/test_utilities.py::TestQuboToIsing::test_trivial tests/test_utilities.py::TestQuboToIsing::test_typical tests/test_utilities.py::TestUtilitiesIntegration::test_start_from_binary tests/test_utilities.py::TestUtilitiesIntegration::test_start_from_spin
docs/reference/utilities.rst dimod/core/structured.py dimod/utilities.py
docs/reference/utilities.rst dimod/core/structured.py dimod/utilities.py