Repository: dwavesystems/dimod

Issue:
bqm.normalize does not incorporate ignored variables/interactions for scalar
normalize method of BinaryQuadraticModel should incorporate ignored variables/interactions when determining the scalar for scaling. 

Discussion:
conta877: closes #375

Patch:
diff --git a/dimod/binary_quadratic_model.py b/dimod/binary_quadratic_model.py
index 77f60957..86003d9a 100644
--- a/dimod/binary_quadratic_model.py
+++ b/dimod/binary_quadratic_model.py
@@ -946,6 +946,16 @@ class BinaryQuadraticModel(abc.Sized, abc.Container, abc.Iterable):
                 return 0, 0
             return min(iterable), max(iterable)
 
+        if ignored_variables is None:
+            ignored_variables = set()
+        elif not isinstance(ignored_variables, abc.Container):
+            ignored_variables = set(ignored_variables)
+
+        if ignored_interactions is None:
+            ignored_interactions = set()
+        elif not isinstance(ignored_interactions, abc.Container):
+            ignored_interactions = set(ignored_interactions)
+
         if quadratic_range is None:
             linear_range, quadratic_range = bias_range, bias_range
         else:
@@ -954,8 +964,13 @@ class BinaryQuadraticModel(abc.Sized, abc.Container, abc.Iterable):
         lin_range, quad_range = map(parse_range, (linear_range,
                                                   quadratic_range))
 
-        lin_min, lin_max = min_and_max(self.linear.values())
-        quad_min, quad_max = min_and_max(self.quadratic.values())
+        lin_min, lin_max = min_and_max([v for k, v in self.linear.items()
+                                    if k not in ignored_variables])
+        quad_min, quad_max = min_and_max([v for (a,b),
+                                                v in self.quadratic.items()
+                                          if ((a, b) not in ignored_interactions
+                                              and (b, a) not in
+                                              ignored_interactions)])
 
         inv_scalar = max(lin_min / lin_range[0], lin_max / lin_range[1],
                          quad_min / quad_range[0], quad_max / quad_range[1])
diff --git a/dimod/reference/composites/scalecomposite.py b/dimod/reference/composites/scalecomposite.py
index 10fcc4a5..a2f49e28 100644
--- a/dimod/reference/composites/scalecomposite.py
+++ b/dimod/reference/composites/scalecomposite.py
@@ -1,4 +1,4 @@
-# Copyright 2018 D-Wave Systems Inc.
+# 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.
@@ -14,8 +14,8 @@
 #
 # ================================================================================================
 """
-A composite that fixes the variables provided and removes them from the
-bqm object before sending to its child sampler.
+A composite that scales problem variables as directed. if scalar is not given
+calculates it based on quadratic and bias ranges.
 
 See `Ocean Glossary <https://docs.ocean.dwavesys.com/en/latest/glossary.html>`_ for explanations
 of technical terms in descriptions of Ocean tools.
@@ -72,12 +72,12 @@ class ScaleComposite(ComposedSampler):
     @property
     def parameters(self):
         param = self.child.parameters.copy()
-        param['scalar'] = []
-        param['bias_range'] = []
-        param['quadratic_range'] = []
-        param['ignored_variables'] = []
-        param['ignored_interactions'] = []
-        param['ignore_offset'] = []
+        param.update({'scalar': [],
+                      'bias_range': [],
+                      'quadratic_range': [],
+                      'ignored_variables': [],
+                      'ignored_interactions': [],
+                      'ignore_offset': []})
         return param
 
     @property
@@ -124,22 +124,17 @@ class ScaleComposite(ComposedSampler):
 
         """
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=ignored_interactions,
-            ignored_variables=ignored_variables)
+            ignored_variables, ignored_interactions)
 
         child = self.child
-        bqm_copy = _scaled_bqm(bqm, scalar=scalar,
-                               bias_range=bias_range,
-                               quadratic_range=quadratic_range,
-                               ignored_variables=ignored_variables,
-                               ignored_interactions=ignored_interactions,
-                               ignore_offset=ignore_offset)
+        bqm_copy = _scaled_bqm(bqm, scalar, bias_range, quadratic_range,
+                               ignored_variables, ignored_interactions,
+                               ignore_offset)
         response = child.sample(bqm_copy, **parameters)
 
         return _scale_back_response(bqm, response, bqm_copy.info['scalar'],
-                                    ignored_variables=ignored_variables,
-                                    ignored_interactions=ignored_interactions,
-                                    ignore_offset=ignore_offset)
+                                    ignored_variables, ignored_interactions,
+                                    ignore_offset)
 
     def sample_ising(self, h, J, offset=0, scalar=None,
                      bias_range=1, quadratic_range=None,
@@ -198,17 +193,14 @@ class ScaleComposite(ComposedSampler):
         # handle HUBO
 
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=ignored_interactions,
-            ignored_variables=ignored_variables)
+            ignored_variables,
+            ignored_interactions)
 
         child = self.child
-        h_sc, J_sc, offset_sc = _scaled_hubo(h, J, offset=offset,
-                                             scalar=scalar,
-                                             bias_range=bias_range,
-                                             quadratic_range=quadratic_range,
-                                             ignored_variables=ignored_variables,
-                                             ignored_interactions=ignored_interactions,
-                                             ignore_offset=ignore_offset)
+        h_sc, J_sc, offset_sc = _scaled_hubo(h, J, offset, scalar, bias_range,
+                                             quadratic_range, ignored_variables,
+                                             ignored_interactions,
+                                             ignore_offset)
         response = child.sample_ising(h_sc, J_sc, offset=offset_sc,
                                       **parameters)
 
@@ -218,8 +210,8 @@ class ScaleComposite(ComposedSampler):
         return response
 
 
-def _scale_back_response(bqm, response, scalar, ignored_interactions=None,
-                         ignored_variables=None, ignore_offset=None):
+def _scale_back_response(bqm, response, scalar, ignored_interactions,
+                         ignored_variables, ignore_offset):
     """Helper function to scale back the response of sample method"""
     if len(ignored_interactions) + len(
             ignored_variables) + ignore_offset == 0:
@@ -230,7 +222,7 @@ def _scale_back_response(bqm, response, scalar, ignored_interactions=None,
     return response
 
 
-def _check_params(ignored_interactions=None, ignored_variables=None):
+def _check_params(ignored_variables, ignored_interactions):
     """Helper for sample methods"""
 
     if ignored_variables is None:
@@ -246,8 +238,8 @@ def _check_params(ignored_interactions=None, ignored_variables=None):
     return ignored_variables, ignored_interactions
 
 
-def _calc_norm_coeff(h, J, bias_range, quadratic_range, ignored_variables=None,
-                     ignored_interactions=None):
+def _calc_norm_coeff(h, J, bias_range, quadratic_range, ignored_variables,
+                     ignored_interactions):
     """Helper function to calculate normalization coefficient"""
 
     if ignored_variables is None or ignored_interactions is None:
@@ -286,17 +278,16 @@ def _calc_norm_coeff(h, J, bias_range, quadratic_range, ignored_variables=None,
         return 1.
 
 
-def _scaled_bqm(bqm, scalar=None, bias_range=1, quadratic_range=None,
-                ignored_variables=None, ignored_interactions=None,
-                ignore_offset=False):
+def _scaled_bqm(bqm, scalar, bias_range, quadratic_range,
+                ignored_variables, ignored_interactions,
+                ignore_offset):
     """Helper function of sample for scaling"""
 
     bqm_copy = bqm.copy()
     if scalar is None:
         scalar = _calc_norm_coeff(bqm_copy.linear, bqm_copy.quadratic,
                                   bias_range, quadratic_range,
-                                  ignored_variables=ignored_variables,
-                                  ignored_interactions=ignored_interactions)
+                                  ignored_variables, ignored_interactions)
 
     bqm_copy.scale(scalar, ignored_variables=ignored_variables,
                    ignored_interactions=ignored_interactions,
@@ -305,18 +296,16 @@ def _scaled_bqm(bqm, scalar=None, bias_range=1, quadratic_range=None,
     return bqm_copy
 
 
-def _scaled_hubo(h, j, offset=0, scalar=None, bias_range=1,
-                 quadratic_range=None,
-                 ignored_variables=None,
-                 ignored_interactions=None,
-                 ignore_offset=False):
+def _scaled_hubo(h, j, offset, scalar, bias_range,
+                 quadratic_range,
+                 ignored_variables,
+                 ignored_interactions,
+                 ignore_offset):
     """Helper function of sample_ising for scaling"""
 
     if scalar is None:
         scalar = _calc_norm_coeff(h, j, bias_range, quadratic_range,
-                                  ignored_variables=ignored_variables,
-                                  ignored_interactions=ignored_interactions
-                                  )
+                                  ignored_variables, ignored_interactions)
     h_sc = dict(h)
     j_sc = dict(j)
     offset_sc = offset


Test patch:
diff --git a/dimod/testing/asserts.py b/dimod/testing/asserts.py
index b28d929f..7a723c11 100644
--- a/dimod/testing/asserts.py
+++ b/dimod/testing/asserts.py
@@ -194,3 +194,61 @@ def assert_response_energies(response, bqm, precision=7):
                 assert v in sample, "bqm contains a variable not in sample"
 
         assert round(bqm.energy(sample) - energy, precision) == 0
+
+
+def assert_bqm_almost_equal(actual, desired, places=7,
+                            ignore_zero_interactions=False):
+    """Test if two bqm have almost equal biases.
+
+    Args:
+        actual (:obj:`.BinaryQuadraticModel`)
+
+        desired (:obj:`.BinaryQuadraticModel`)
+
+        places (int, optional, default=7):
+            Bias equality is computed as :code:`round(b0 - b1, places) == 0`
+
+        ignore_zero_interactions (bool, optional, default=False):
+            If true, interactions with 0 bias are ignored.
+
+    """
+    assert isinstance(actual, dimod.BinaryQuadraticModel), "not a binary quadratic model"
+    assert isinstance(desired, dimod.BinaryQuadraticModel), "not a binary quadratic model"
+
+    # vartype should match
+    assert actual.vartype is desired.vartype, "unlike vartype"
+
+    # variables should match
+    variables_diff = set(actual).symmetric_difference(desired)
+    if variables_diff:
+        v = variables_diff.pop()
+        msg = "{!r} is not a shared variable".format(v)
+        raise AssertionError(msg)
+
+    # offset
+    if round(actual.offset - desired.offset, places):
+        msg = 'offsets {} != {}'.format(desired.offset, actual.offset)
+        raise AssertionError(msg)
+
+    # linear biases - we already checked variables
+    for v, bias in desired.linear.items():
+        if round(bias - actual.linear[v], places):
+            msg = 'linear bias associated with {!r} does not match, {!r} != {!r}'
+            raise AssertionError(msg.format(v, bias, actual.linear[v]))
+
+    default = 0 if ignore_zero_interactions else None
+
+    for inter, bias in actual.quadratic.items():
+        other_bias = desired.quadratic.get(inter, default)
+        if other_bias is None:
+            raise AssertionError('{!r} is not a shared interaction'.format(inter))
+        if round(bias - other_bias, places):
+            msg = 'quadratic bias associated with {!r} does not match, {!r} != {!r}'
+            raise AssertionError(msg.format(inter, bias, other_bias))
+    for inter, bias in desired.quadratic.items():
+        other_bias = actual.quadratic.get(inter, default)
+        if other_bias is None:
+            raise AssertionError('{!r} is not a shared interaction'.format(inter))
+        if round(bias - other_bias, places):
+            msg = 'quadratic bias associated with {!r} does not match, {!r} != {!r}'
+            raise AssertionError(msg.format(inter, bias, other_bias))
diff --git a/docs/reference/testing.rst b/docs/reference/testing.rst
index bd6f5185..1f58ccae 100644
--- a/docs/reference/testing.rst
+++ b/docs/reference/testing.rst
@@ -12,6 +12,7 @@ API Asserts
 .. autosummary::
    :toctree: generated/
 
+   assert_bqm_almost_equal
    assert_composite_api
    assert_sampler_api
    assert_structured_api
diff --git a/tests/test_binary_quadratic_model.py b/tests/test_binary_quadratic_model.py
index 3def6ab9..1d65fbc2 100644
--- a/tests/test_binary_quadratic_model.py
+++ b/tests/test_binary_quadratic_model.py
@@ -771,6 +771,13 @@ class TestBinaryQuadraticModel(unittest.TestCase):
         self.assertAlmostEqual(bqm.offset, 1.)
         self.assertConsistentBQM(bqm)
 
+        bqm = dimod.BinaryQuadraticModel({0: -2, 1: 2}, {(0, 1): -5}, 1.,
+                                         dimod.SPIN)
+        bqm.normalize(0.5, ignored_interactions=[(0, 1)])
+        self.assertAlmostEqual(bqm.linear, {0: -.5, 1: .5})
+        self.assertAlmostEqual(bqm.quadratic, {(0, 1): -5})
+        self.assertAlmostEqual(bqm.offset, 0.25)
+        self.assertConsistentBQM(bqm)
 
     def test_scale_exclusions(self):
         bqm = dimod.BinaryQuadraticModel({0: -2, 1: 2}, {(0, 1): -1}, 1., dimod.SPIN)
diff --git a/tests/test_scalecomposite.py b/tests/test_scalecomposite.py
index 4921ef2e..754a6ae2 100644
--- a/tests/test_scalecomposite.py
+++ b/tests/test_scalecomposite.py
@@ -1,4 +1,4 @@
-# Copyright 2018 D-Wave Systems Inc.
+# 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.
@@ -14,15 +14,6 @@
 #
 # ================================================================================================
 
-# class ScalingChecker(dimod.Sampler):
-#     def __init__(self, originalbqm, scalar):
-#         self.bqm = originalbqm.copy()
-#         self.bqm.scale(scalar)
-#     def sample(self, bqm):
-#         assert self.bqm == bqm
-#         return ExactSolver().sample(bqm)
-#     def sample_ising(self,h, J):
-
 import unittest
 
 import dimod.testing as dtest
@@ -87,12 +78,14 @@ class TestScaleComposite(unittest.TestCase):
         linear = {'a': -4.0, 'b': -4.0}
         quadratic = {('a', 'b'): 3.2}
 
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic, offset=5.0)
-        bqm_new = _scaled_bqm(bqm,
-                              bias_range=2,
-                              ignored_interactions=ignored_interactions,
-                              ignored_variables=ignored_variables)
+        scalar = None
+        quadratic_range = None
+        ignore_offset = False
+        bqm_new = _scaled_bqm(bqm, scalar, 2, quadratic_range,
+                              ignored_variables, ignored_interactions,
+                              ignore_offset)
 
         sc = 2.
         hsc = {k: v / sc if k not in ignored_variables else v for
@@ -102,13 +95,11 @@ class TestScaleComposite(unittest.TestCase):
         bqm_scaled = BinaryQuadraticModel.from_ising(hsc, Jsc, offset=5.0 / 2.)
         self.assertEqual(bqm_scaled, bqm_new)
 
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic, offset=5.0)
-        bqm_new = _scaled_bqm(bqm,
-                              bias_range=2,
-                              ignored_interactions=ignored_interactions,
-                              ignored_variables=ignored_variables,
-                              ignore_offset=True)
+        bqm_new = _scaled_bqm(bqm, scalar, 2, quadratic_range,
+                              ignored_variables, ignored_interactions,
+                              True)
 
         sc = 2.
         hsc = {k: v / sc if k not in ignored_variables else v for
@@ -118,10 +109,9 @@ class TestScaleComposite(unittest.TestCase):
         bqm_scaled = BinaryQuadraticModel.from_ising(hsc, Jsc, offset=5.0)
         self.assertEqual(bqm_scaled, bqm_new)
 
-        bqm_new = _scaled_bqm(bqm,
-                              quadratic_range=(-1, 0.4),
-                              ignored_interactions=ignored_interactions,
-                              ignored_variables=ignored_variables)
+        bqm_new = _scaled_bqm(bqm, scalar, 1, (-1, 0.4),
+                              ignored_variables, ignored_interactions,
+                              ignore_offset)
 
         sc = 3.2 / 0.4
         hsc = {k: v / sc if k not in ignored_variables else v for
@@ -135,13 +125,12 @@ class TestScaleComposite(unittest.TestCase):
         ignored_variables = ['a', 'b']
         ignored_interactions = None
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=ignored_interactions,
-            ignored_variables=ignored_variables)
+            ignored_variables, ignored_interactions)
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic)
-        bqm_new = _scaled_bqm(bqm,
-                              bias_range=(-2, 2),
-                              ignored_interactions=ignored_interactions,
-                              ignored_variables=ignored_variables)
+
+        bqm_new = _scaled_bqm(bqm, scalar, (2, 2), quadratic_range,
+                              ignored_variables, ignored_interactions,
+                              ignore_offset)
 
         sc = 3.2 / 2.
         hsc = {k: v / sc if k not in ignored_variables else v for
@@ -155,13 +144,12 @@ class TestScaleComposite(unittest.TestCase):
         ignored_variables = None
         ignored_interactions = [('a', 'b')]
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=ignored_interactions,
-            ignored_variables=ignored_variables)
+            ignored_variables, ignored_interactions)
+
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic)
-        bqm_new = _scaled_bqm(bqm,
-                              quadratic_range=0.5,
-                              ignored_interactions=ignored_interactions,
-                              ignored_variables=ignored_variables)
+        bqm_new = _scaled_bqm(bqm, scalar, 1, 0.5,
+                              ignored_variables, ignored_interactions,
+                              ignore_offset)
 
         sc = 4.
         hsc = {k: v / sc if k not in ignored_variables else v for
@@ -179,13 +167,11 @@ class TestScaleComposite(unittest.TestCase):
         ignored_variables = None
         ignored_interactions = None
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=ignored_interactions,
-            ignored_variables=ignored_variables)
+            ignored_variables, ignored_interactions)
 
-        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset=offset,
-                                             bias_range=2,
-                                             ignored_interactions=ignored_interactions,
-                                             ignored_variables=ignored_variables)
+        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset,
+                                             None, 2, None, ignored_variables,
+                                             ignored_interactions, False)
 
         sc = 2.
         hsc = {k: v / sc if k not in ignored_variables else v for
@@ -196,11 +182,9 @@ class TestScaleComposite(unittest.TestCase):
         self.assertEqual(Jsc, jnew)
         self.assertEqual(offsetnew, offset / sc)
 
-        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset=offset,
-                                             bias_range=2,
-                                             ignored_interactions=ignored_interactions,
-                                             ignored_variables=ignored_variables,
-                                             ignore_offset=True)
+        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset,
+                                             None, 2, None, ignored_variables,
+                                             ignored_interactions, True)
 
         sc = 2.
         hsc = {k: v / sc if k not in ignored_variables else v for
@@ -211,10 +195,10 @@ class TestScaleComposite(unittest.TestCase):
         self.assertEqual(Jsc, jnew)
         self.assertEqual(offsetnew, offset)
 
-        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset=offset,
-                                             quadratic_range=(-1, 0.4),
-                                             ignored_interactions=ignored_interactions,
-                                             ignored_variables=ignored_variables)
+        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset,
+                                             None, 1, (-1, 0.4),
+                                             ignored_variables,
+                                             ignored_interactions, False)
 
         sc = 3.2 / 0.4
         hsc = {k: v / sc if k not in ignored_variables else v for
@@ -228,13 +212,12 @@ class TestScaleComposite(unittest.TestCase):
         ignored_variables = ['a', 'b']
         ignored_interactions = None
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=ignored_interactions,
-            ignored_variables=ignored_variables)
+            ignored_variables, ignored_interactions)
 
-        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset=offset,
-                                             bias_range=(-2, 2),
-                                             ignored_interactions=ignored_interactions,
-                                             ignored_variables=ignored_variables)
+        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset,
+                                             None, (-2, 2), None,
+                                             ignored_variables,
+                                             ignored_interactions, False)
         sc = 3.2 / 2.
         hsc = {k: v / sc if k not in ignored_variables else v for
                k, v in linear.items()}
@@ -247,13 +230,11 @@ class TestScaleComposite(unittest.TestCase):
         ignored_variables = None
         ignored_interactions = [('a', 'b', 'c')]
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=ignored_interactions,
-            ignored_variables=ignored_variables)
+            ignored_variables, ignored_interactions)
 
-        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset=offset,
-                                             quadratic_range=0.5,
-                                             ignored_interactions=ignored_interactions,
-                                             ignored_variables=ignored_variables)
+        hnew, jnew, offsetnew = _scaled_hubo(linear, quadratic, offset,
+                                             None, 1, 0.5, ignored_variables,
+                                             ignored_interactions, False)
 
         sc = 4.
         hsc = {k: v / sc if k not in ignored_variables else v for
@@ -269,7 +250,7 @@ class TestScaleComposite(unittest.TestCase):
         quadratic = {('a', 'b', 'c'): 3.2}
         offset = 5
 
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
 
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
@@ -290,7 +271,7 @@ class TestScaleComposite(unittest.TestCase):
         linear = {'a': -4.0, 'b': -4.0, 'c': -4.0}
         quadratic = {('a', 'b', 'c'): 3.2}
         offset = 5
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                penalty_strength=5.,
@@ -309,7 +290,7 @@ class TestScaleComposite(unittest.TestCase):
         linear = {'a': -4.0, 'b': -4.0, 'c': -4.0}
         quadratic = {('a', 'b', 'c'): 3.2}
         offset = 5
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
 
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
@@ -345,7 +326,7 @@ class TestScaleComposite(unittest.TestCase):
         linear = {'a': -4.0, 'b': -4.0, 'c': -4.0}
         quadratic = {('a', 'b', 'c'): 3.2}
         offset = 5
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                penalty_strength=5.,
@@ -366,7 +347,7 @@ class TestScaleComposite(unittest.TestCase):
         linear = {'a': -4.0, 'b': -4.0}
         quadratic = {('a', 'b'): 3.2}
         offset = 5
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic, offset=offset)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables)
@@ -379,8 +360,7 @@ class TestScaleComposite(unittest.TestCase):
     def test_sample_bias_range(self):
         linear = {'a': -4.0, 'b': -4.0}
         quadratic = {('a', 'b'): 3.2}
-
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
@@ -395,7 +375,7 @@ class TestScaleComposite(unittest.TestCase):
         linear = {'a': -4.0, 'b': -4.0}
         quadratic = {('a', 'b'): 3.2}
         offset = 5
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic, offset=offset)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
@@ -419,7 +399,7 @@ class TestScaleComposite(unittest.TestCase):
         linear = {'a': -4.0, 'b': -4.0}
         quadratic = {('a', 'b'): 3.2}
         offset = 5
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic, offset=offset)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
@@ -435,7 +415,7 @@ class TestScaleComposite(unittest.TestCase):
         linear = {'a': -4.0, 'b': -4.0}
         quadratic = {('a', 'b'): 3.2}
         offset = 5
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables)
         sampler = ScaleComposite(ScalingChecker(ExactSolver(), h=linear,
@@ -450,7 +430,7 @@ class TestScaleComposite(unittest.TestCase):
         quadratic = {('a', 'b'): 3.2}
         offset = 5
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=[('a', 'b')])
+            None, [('a', 'b')])
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                scalar=0.5
@@ -469,7 +449,7 @@ class TestScaleComposite(unittest.TestCase):
         quadratic = {('a', 'b'): 3.2}
         offset = 5
 
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                ignore_offset=True,
@@ -488,7 +468,7 @@ class TestScaleComposite(unittest.TestCase):
         offset = 5
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic, offset=offset)
 
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                ignore_offset=True,
@@ -505,7 +485,7 @@ class TestScaleComposite(unittest.TestCase):
         quadratic = {('a', 'b', 'c'): 3.2}
         offset = 5
 
-        ignored_variables, ignored_interactions = _check_params()
+        ignored_variables, ignored_interactions = _check_params(None, None)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                ignore_offset=True,
@@ -524,7 +504,7 @@ class TestScaleComposite(unittest.TestCase):
         quadratic = {('a', 'b'): 3.2}
         offset = 5
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=[('a', 'b')])
+            None, [('a', 'b')])
 
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
@@ -545,7 +525,7 @@ class TestScaleComposite(unittest.TestCase):
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic, offset=offset)
 
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=[('a', 'b')])
+            None, [('a', 'b')])
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                ignore_offset=True,
@@ -562,7 +542,7 @@ class TestScaleComposite(unittest.TestCase):
         quadratic = {('a', 'b', 'c'): 3.2}
         offset = 5
         ignored_variables, ignored_interactions = _check_params(
-            ignored_interactions=[('a', 'b', 'c')])
+            None, [('a', 'b', 'c')])
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                ignore_offset=True,
@@ -581,7 +561,7 @@ class TestScaleComposite(unittest.TestCase):
         quadratic = {('a', 'b'): 3.2}
         offset = 5
         ignored_variables, ignored_interactions = _check_params(
-            ignored_variables=['a'])
+            ['a'], None)
 
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
@@ -601,7 +581,7 @@ class TestScaleComposite(unittest.TestCase):
         offset = 5
         bqm = BinaryQuadraticModel.from_ising(linear, quadratic, offset=offset)
         ignored_variables, ignored_interactions = _check_params(
-            ignored_variables=['a'])
+            ['a'], None)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                ignore_offset=True,
@@ -618,7 +598,7 @@ class TestScaleComposite(unittest.TestCase):
         quadratic = {('a', 'b', 'c'): 3.2}
         offset = 5
         ignored_variables, ignored_interactions = _check_params(
-            ignored_variables=['a'])
+            ['a'], None)
         comp_parameters = dict(ignored_interactions=ignored_interactions,
                                ignored_variables=ignored_variables,
                                ignore_offset=True,
diff --git a/tests/test_testing.py b/tests/test_testing.py
new file mode 100644
index 00000000..2947944f
--- /dev/null
+++ b/tests/test_testing.py
@@ -0,0 +1,70 @@
+# 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 unittest
+
+import dimod
+
+
+class Test_assert_almost_equal_bqm(unittest.TestCase):
+    def test_empty(self):
+        bqm0 = dimod.BinaryQuadraticModel.empty(dimod.SPIN)
+        bqm1 = dimod.BinaryQuadraticModel.empty(dimod.SPIN)
+        dimod.testing.assert_bqm_almost_equal(bqm0, bqm1)
+
+    def test_self_empty(self):
+        bqm = dimod.BinaryQuadraticModel.empty(dimod.SPIN)
+        dimod.testing.assert_bqm_almost_equal(bqm, bqm)
+
+    def test_unlike_variables(self):
+        bqm0 = dimod.BinaryQuadraticModel.from_ising({'a': -1}, {})
+        bqm1 = dimod.BinaryQuadraticModel.from_ising({'b': -1}, {})
+        with self.assertRaises(AssertionError):
+            dimod.testing.assert_bqm_almost_equal(bqm0, bqm1)
+
+    def test_unlike_offset(self):
+        bqm0 = dimod.BinaryQuadraticModel.from_ising({'a': -1}, {}, 1.1)
+        bqm1 = dimod.BinaryQuadraticModel.from_ising({'a': -1}, {}, 1.2)
+        with self.assertRaises(AssertionError):
+            dimod.testing.assert_bqm_almost_equal(bqm0, bqm1)
+        dimod.testing.assert_bqm_almost_equal(bqm0, bqm1, places=0)
+
+    def test_unlike_linear(self):
+        bqm0 = dimod.BinaryQuadraticModel.from_ising({'a': -1}, {})
+        bqm1 = dimod.BinaryQuadraticModel.from_ising({'a': -1.1}, {})
+        with self.assertRaises(AssertionError):
+            dimod.testing.assert_bqm_almost_equal(bqm0, bqm1)
+        dimod.testing.assert_bqm_almost_equal(bqm0, bqm1, places=0)
+
+    def test_unlike_interactions(self):
+        bqm0 = dimod.BinaryQuadraticModel.from_ising({}, {'ab': -1})
+        bqm1 = dimod.BinaryQuadraticModel.from_ising({}, {'ab': -1.1})
+        with self.assertRaises(AssertionError):
+            dimod.testing.assert_bqm_almost_equal(bqm0, bqm1)
+        dimod.testing.assert_bqm_almost_equal(bqm0, bqm1, places=0)
+
+    def test_ignore_zero_interactions(self):
+        h = {'a': 0, 'b': 0, 'c': 0, 'd': 0}
+        J0 = {'ab': 0, 'bc': -1}
+        J1 = {'cb': -1, 'cd': 0}
+
+        bqm0 = dimod.BinaryQuadraticModel.from_ising(h, J0)
+        bqm1 = dimod.BinaryQuadraticModel.from_ising(h, J1)
+        with self.assertRaises(AssertionError):
+            dimod.testing.assert_bqm_almost_equal(bqm0, bqm1)
+        dimod.testing.assert_bqm_almost_equal(bqm0, bqm1, ignore_zero_interactions=True)
+        with self.assertRaises(AssertionError):
+            dimod.testing.assert_bqm_almost_equal(bqm1, bqm0)
+        dimod.testing.assert_bqm_almost_equal(bqm1, bqm0, ignore_zero_interactions=True)