Skip to content

Updating Dimod Sampler#73

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

Updating Dimod Sampler#73
anahitamansouri wants to merge 5 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 on lines +144 to +155
self.assertTupleEqual(samples.shape, (2, 1, 4))
samples = samples.squeeze()

# Check clamped values unchanged
mask = ~torch.isnan(x)
self.assertTrue(torch.all(samples[mask] == x[mask]))

# Check free variables are ±1
free_mask = torch.isnan(x)
free_values = samples[free_mask]
self.assertTrue(torch.all(torch.isin(free_values, torch.tensor([-1.0, 1.0]))),
"Free variables should be sampled as ±1")

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.

Should these be separate subtests? They're currently part of a subtest but only the second test is appropriate for that heading

@anahitamansouri anahitamansouri Jun 11, 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.

I agree. I wanted to be concise :) but it makes sense to have multiple subtests.


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)

full_read[:, idx] = torch.from_numpy(sample_array[:, var_to_idx[node]]).to(device=device, dtype=torch.float)
results.append(full_read)
# Stack to get (batch_size, num_reads, n_nodes)
samples = torch.stack(results, dim=0)

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.

Unlikely but certainly possible uncaught error here: results have tensors of different sample size (e.g., num_reads) and so the stack function will fail. Probably want to check all elements in result have same shape.

@anahitamansouri anahitamansouri Jun 18, 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.

I thought this would not happen because I am appending tensors of shape(num_reads, num_nodes) to results and then stack them. So, they would have the same shape. I will add this check anyways.

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.

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