Skip to content

Updating Dimod Sampler - #73

Open
anahitamansouri wants to merge 6 commits into
dwavesystems:mainfrom
anahitamansouri:feature/dimod-conditional-sampling
Open

Updating Dimod Sampler#73
anahitamansouri wants to merge 6 commits into
dwavesystems:mainfrom
anahitamansouri:feature/dimod-conditional-sampling

Conversation

@anahitamansouri

Copy link
Copy Markdown
Collaborator

This PR adds:

  • Conditional sampling feature for Dimod sampler.
  • Tests for this feature.

@anahitamansouri anahitamansouri self-assigned this Apr 7, 2026
@anahitamansouri anahitamansouri added the enhancement New feature or request label Apr 7, 2026

@kevinchern kevinchern left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a quick first pass over the main function.

Comment thread dwave/plugins/torch/samplers/dimod_sampler.py
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread tests/test_samplers/test_dimod_sampler.py
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread tests/test_samplers/test_dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
Comment thread dwave/plugins/torch/samplers/dimod_sampler.py Outdated
"""
if x is not None:
raise NotImplementedError("Support for conditional sampling has not been implemented.")
device = self._grbm._linear.device

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might not be an issue, but accessing hidden attributes (as in GRBM._linear here) likely indicates that it either shouldn't be hidden or accessed in a different way (e.g., GRBM.linear if that exists).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted. I also use _linear inconsistently and should use the public attribute

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend changing all other similar occurrences in this PR, e.g., self._sampler_params

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated access to private attributes of external objects ( GRBM._linear and GRBM._node_to_idx) but self._sampler_params is an internal attribute of DimodSampler, so I kept that unchanged.

@kevinchern kevinchern left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lookin' goooood! Left some minor comments

Comment on lines +101 to +106
torch.Tensor:
- If ``x is None``:
tensor of shape ``(num_reads, n_nodes)``.

- If ``x`` is provided:
tensor of shape ``(batch_size, num_reads, n_nodes)``.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thisac want to double check with you whether there's a conventional format for docstrings where return value shapes are input-dependent

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just write it as a single paragraph (no specific formatting). As long as it renders nicely in the docs, should be OK. Bullets are fine, but not sure how the indentations after each bullet are rendered.

raise ValueError(f"x must have shape (batch_size, {n_nodes})")

mask = ~torch.isnan(x)
if not torch.all(torch.isin(x[mask], torch.tensor([-1, 1], device=x.device))):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use consistently device or x.device

if x is not None:
raise NotImplementedError("Support for conditional sampling has not been implemented.")
device = self._grbm._linear.device
nodes = self._grbm.nodes

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is only ever used once; no need to define this variable here

Comment on lines +157 to +170
for v in bqm.linear:
if bqm.linear[v] > ub:
bqm.set_linear(v, ub)
elif bqm.linear[v] < lb:
bqm.set_linear(v, lb)

# Clip quadratic biases
if self._quadratic_range is not None:
lb, ub = self._quadratic_range
for u, v, bias in bqm.iter_quadratic():
if bias > ub:
bqm.set_quadratic(u, v, ub)
elif bias < lb:
bqm.set_quadratic(u, v, lb)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do the two for-loops differ? Here it's for v in bqm.linear and the next uses iter_quadratic

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bqm.linear is a mapping from variable to bias, while bqm.iter_quadratic() gives (u, v, bias) triples. Are you suggesting to change the linear loop to bqm.linear.items() (style of the iteration) or do you think the loops can be merged? Could you clarify this? :)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I meant why not use bqm.iter_linear()?

elif bias < lb:
bqm.set_quadratic(u, v, lb)

# Sample one configuration per input

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is dated

Comment thread tests/test_samplers/test_dimod_sampler.py

free_values = samples[:, i, :][free_mask]
self.assertTrue(torch.all(torch.isin(free_values, torch.tensor([-1.0, 1.0]))),
f"Free variables should be sampled as ±1 in read {i}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f"Free variables should be sampled as ±1 in read {i}")
f"Free variables should be sampled as ±1 in read {i}")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should all subtests be separated by a blank line?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea.

x,
msg=f"Fully clamped inputs should be preserved in read {i}"
)
with self.subTest("Conditional sampling supports mixed fully-clamped and partially-clamped rows."):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
with self.subTest("Conditional sampling supports mixed fully-clamped and partially-clamped rows."):
with self.subTest("Conditional sampling supports mixed fully clamped and partially clamped rows."):


# Sample one configuration per input
sample_kwargs = dict(self._sampler_params)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(empty space characters; use formatter)

Comment thread dwave/plugins/torch/samplers/dimod_sampler.py
Comment on lines +101 to +106
torch.Tensor:
- If ``x is None``:
tensor of shape ``(num_reads, n_nodes)``.

- If ``x`` is provided:
tensor of shape ``(batch_size, num_reads, n_nodes)``.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just write it as a single paragraph (no specific formatting). As long as it renders nicely in the docs, should be OK. Bullets are fine, but not sure how the indentations after each bullet are rendered.

samples = torch.stack(results, dim=0)
return samples

def _sampleset_to_tensor(self, sample_set: SampleSet, device: Optional[torch.device] = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems close to identical to utils.sampleset_to_tensor(). Why is this added here and used instead of the one in utils?

@anahitamansouri anahitamansouri Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you follow our discussion above, you can see that Kevin mentionedutils.sampleset_to_tensor should bot be used here because that function is only safe when contained in GRBM class.
Here, I defined another function that rely on grbm._index_to_node and grbm._node_to_index order. So, there is a subtle difference between these functions.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something, but it's not clear to me why this is different to

def _sampleset_to_tensor(...)
    ordered_vars = [v for v, _ in sorted(self._grbm.node_to_idx.items(), key=lambda x: x[1])]
    return utils.sampleset_to_tensor(ordered_vars, sample_set)

Seems to me still much better to use what we already have defined in utils.

We should not use sampleset_to_tensor because that function is only safe when contained in GRBM class.

@kevinchern Not sure I understand what this means or why. If it's very specifically just for GRBM classes, but we have similar needs in other places, we should probably either generalize it or move it into the GRBM scope.

Here, we should rely on grbm._index_to_node and grbm._node_to_index etc. to guarantee correct mapping between indices and nodes

@kevinchern Isn't that what we're doing when passing in ordered_vars?

@anahitamansouri anahitamansouri Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The difference is that in this new function we follow the grbm.node_to_idx order whereas in the one in utils we allow any ordered_vars and in the older version in DimodSampler we passed the grbm.nodes as ordered_vars. The idea is that grbm.nodes could have a different order from grbm.node_to_idx and we want to make sure we follow grbm.node_to_idx order.

@kevinchern kevinchern left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Can merge after @thisac approves!

@thisac thisac left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple of tests should be added for coverage. Otherwise looks good to me.

Happy to approve as soon as these have been addressed!

from __future__ import annotations

from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Optional

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but I'd recommend using ... | None instead of Optional[...] below.

Suggested change
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any

import dimod

from dwave.plugins.torch.models.boltzmann_machine import GraphRestrictedBoltzmannMachine
from dimod import SampleSet

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but since you're already importing dimod and also use e.g., dimod.Sampler for typing, it might be a bit cleaner to just use dimod.SampleSet instead of importing SampleSet again here.

Suggested change
from dimod import SampleSet

Comment on lines +120 to +125
if x.shape[1] != n_nodes:
raise ValueError(f"x must have shape (batch_size, {n_nodes})")

mask = ~torch.isnan(x)
if not torch.all(torch.isin(x[mask], torch.tensor([-1, 1], device=device))):
raise ValueError("x must contain only ±1 or NaN")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of these cases are tested.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +150 to +166
# Clip linear biases for remaining free variables
if self._linear_range is not None:
lb, ub = self._linear_range
for v, bias in bqm.iter_linear():
if bias > ub:
bqm.set_linear(v, ub)
elif bias < lb:
bqm.set_linear(v, lb)

# Clip quadratic biases
if self._quadratic_range is not None:
lb, ub = self._quadratic_range
for u, v, bias in bqm.iter_quadratic():
if bias > ub:
bqm.set_quadratic(u, v, ub)
elif bias < lb:
bqm.set_quadratic(u, v, lb)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem like there's any test coverage for either of these either. Could you check whether there are any tests that jump into these blocks?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this suggestion. I added the test for the linear biases as requested. For the quadratic ones, I noticed that the quadratic clipping is already applied in to_ising() before constructing the BQM. Unlike linear biases, conditioning cannot make quadratic biases exceed their specified range; therefore, the conditional quadratic clipping loop will not encounter out-of-range quadratic biases in the current implementation. This makes the block redundant, so I will remove it.


reference_shape = results[0].shape
if not all(result.shape == reference_shape for result in results):
raise ValueError(f"Expected all samples to have shape {reference_shape}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also missing test coverage.

@anahitamansouri anahitamansouri Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. This check is intended to guard against inconsistent sample shapes returned by a sampler. Under normal execution, I cannot trigger this error. And this is why I added a small fake sampler that intentionally returns a different number of reads for different conditional rows. This simulates a sampler returning inconsistent sample sizes and verifies that DimodSampler raises the expected ValueError instead of failing later during stacking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants