From 4158b3065f5d388d2d8aa8880fe4fa9b0f5a2182 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Wed, 27 Nov 2024 15:44:39 -0800 Subject: [PATCH 1/4] Convert asserts to hard validation --- hybrid/composers.py | 10 ++++++---- hybrid/core.py | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/hybrid/composers.py b/hybrid/composers.py index 2b92f49..4d8e55d 100644 --- a/hybrid/composers.py +++ b/hybrid/composers.py @@ -94,9 +94,10 @@ def next(self, states, **runopts): synthesis_en = thesis_en # input sanity check - # TODO: convert to hard input validation - assert len(thesis) == len(antithesis) - assert state_thesis.problem == state_antithesis.problem + if len(thesis) != len(antithesis): + raise ValueError("thesis-antithesis length mismatch") + if state_thesis.problem != state_antithesis.problem: + raise ValueError("thesis and antithesis refer to different problems") diff = {v for v in thesis if thesis[v] != antithesis[v]} @@ -116,7 +117,8 @@ def next(self, states, **runopts): synthesis_samples = SampleSet.from_samples_bqm(synthesis, bqm) # calculation sanity check - assert synthesis_samples.first.energy == synthesis_en + if synthesis_samples.first.energy != synthesis_en: + logger.error("Synthesis error: lowest energy sample is not on synthesis path.") return state_thesis.updated(samples=synthesis_samples) diff --git a/hybrid/core.py b/hybrid/core.py index ad1a1ba..fb0ee2b 100644 --- a/hybrid/core.py +++ b/hybrid/core.py @@ -663,9 +663,10 @@ def __init__(self, sampler, fields, **sample_kwargs): if not isinstance(sampler, dimod.Sampler): raise TypeError("'sampler' should be 'dimod.Sampler'") try: - assert len(tuple(fields)) == 2 + if len(tuple(fields)) != 2: + raise ValueError except: - raise ValueError("'fields' should be two-tuple with input/output state fields") + raise ValueError("'fields' should be a two-tuple with input/output state fields") self.sampler = sampler self.input, self.output = fields From 271b3292b26e1353543475893e5b464b11d85a1c Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Sat, 30 Nov 2024 11:34:37 -0800 Subject: [PATCH 2/4] Fix tests flaky due to noisy splat op `dimod.ExactSolver` will return samples in order of enumeration, not energy, and Splat composer with then just take the first sample, not necessarily the best sample. This will cause non-optimal subsample selection in the subsampler branch. --- tests/test_core.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 9ae38ee..fd3fba7 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -480,10 +480,15 @@ def test_runnable_composition(self): self.assertEqual(response.result().samples.record[0].energy, -3.0) def test_racing_workflow_with_oracle_subsolver(self): + class ExactSolver(dimod.ExactSolver): + """Exact solver that returns only the ground state.""" + def sample(self, bqm): + return super().sample(bqm).truncate(1) + workflow = hybrid.LoopUntilNoImprovement(hybrid.RacingBranches( hybrid.InterruptableTabuSampler(), hybrid.EnergyImpactDecomposer(size=1) - | HybridSubproblemRunnable(dimod.ExactSolver()) + | HybridSubproblemRunnable(ExactSolver()) | hybrid.SplatComposer() ) | hybrid.ArgMin(), convergence=3) state = State.from_sample(min_sample(self.bqm), self.bqm) @@ -497,7 +502,7 @@ class Sampler(dimod.ExactSolver): """Exact solver that fails if a sampling parameter is provided.""" parameters = {} def sample(self, bqm): - return super().sample(bqm) + return super().sample(bqm).truncate(1) workflow = hybrid.LoopUntilNoImprovement(hybrid.RacingBranches( hybrid.InterruptableTabuSampler(), From a99165aa7dcd1cbb119f40d7111f963975deec6d Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Sat, 30 Nov 2024 11:38:50 -0800 Subject: [PATCH 3/4] Simplify HybridRunnable tests --- tests/test_core.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index fd3fba7..fa13dd3 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -435,13 +435,14 @@ def test_validation(self): class TestHybridRunnable(unittest.TestCase): bqm = dimod.BinaryQuadraticModel({}, {'ab': 1, 'bc': 1, 'ca': -1}, 0, dimod.SPIN) init_state = State.from_sample(min_sample(bqm), bqm) + ground_energy = dimod.ExactSolver().sample(bqm).first.energy def test_generic(self): runnable = HybridRunnable(TabuSampler(), fields=('problem', 'samples')) response = runnable.run(self.init_state) self.assertIsInstance(response, concurrent.futures.Future) - self.assertEqual(response.result().samples.record[0].energy, -3.0) + self.assertEqual(response.result().samples.record[0].energy, self.ground_energy) def test_validation(self): with self.assertRaises(TypeError): @@ -462,7 +463,7 @@ def test_problem_sampler_runnable(self): response = runnable.run(self.init_state) self.assertIsInstance(response, concurrent.futures.Future) - self.assertEqual(response.result().samples.record[0].energy, -3.0) + self.assertEqual(response.result().samples.record[0].energy, self.ground_energy) def test_subproblem_sampler_runnable(self): runnable = HybridSubproblemRunnable(TabuSampler()) @@ -470,14 +471,14 @@ def test_subproblem_sampler_runnable(self): response = runnable.run(state) self.assertIsInstance(response, concurrent.futures.Future) - self.assertEqual(response.result().subsamples.record[0].energy, -3.0) + self.assertEqual(response.result().subsamples.record[0].energy, self.ground_energy) def test_runnable_composition(self): runnable = IdentityDecomposer() | HybridSubproblemRunnable(TabuSampler()) | IdentityComposer() response = runnable.run(self.init_state) self.assertIsInstance(response, concurrent.futures.Future) - self.assertEqual(response.result().samples.record[0].energy, -3.0) + self.assertEqual(response.result().samples.record[0].energy, self.ground_energy) def test_racing_workflow_with_oracle_subsolver(self): class ExactSolver(dimod.ExactSolver): @@ -491,11 +492,10 @@ def sample(self, bqm): | HybridSubproblemRunnable(ExactSolver()) | hybrid.SplatComposer() ) | hybrid.ArgMin(), convergence=3) - state = State.from_sample(min_sample(self.bqm), self.bqm) - response = workflow.run(state) + response = workflow.run(self.init_state) self.assertIsInstance(response, concurrent.futures.Future) - self.assertEqual(response.result().samples.record[0].energy, -3.0) + self.assertEqual(response.result().samples.record[0].energy, self.ground_energy) def test_sampling_parameters_filtering(self): class Sampler(dimod.ExactSolver): @@ -504,17 +504,15 @@ class Sampler(dimod.ExactSolver): def sample(self, bqm): return super().sample(bqm).truncate(1) - workflow = hybrid.LoopUntilNoImprovement(hybrid.RacingBranches( - hybrid.InterruptableTabuSampler(), - hybrid.EnergyImpactDecomposer(size=1) + workflow = ( + hybrid.IdentityDecomposer() | HybridSubproblemRunnable(Sampler()) - | hybrid.SplatComposer() - ) | hybrid.ArgMin(), convergence=3) - state = State.from_sample(min_sample(self.bqm), self.bqm) - response = workflow.run(state) + | hybrid.IdentityComposer() + ) + response = workflow.run(self.init_state) self.assertIsInstance(response, concurrent.futures.Future) - self.assertEqual(response.result().samples.record[0].energy, -3.0) + self.assertEqual(response.result().samples.record[0].energy, self.ground_energy) class TestLogging(unittest.TestCase): From 3f326400f642bea6bb06aa9ef55ababdd7112f53 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Sat, 30 Nov 2024 11:44:03 -0800 Subject: [PATCH 4/4] Actually test HybridSubproblemRunnable parameter filtering --- tests/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core.py b/tests/test_core.py index fa13dd3..1ba2bb6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -506,7 +506,7 @@ def sample(self, bqm): workflow = ( hybrid.IdentityDecomposer() - | HybridSubproblemRunnable(Sampler()) + | HybridSubproblemRunnable(Sampler(), unknown_sampler_argument=1) | hybrid.IdentityComposer() ) response = workflow.run(self.init_state)