Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ python3 -m pip install -e '.[qiskit]'
python3 -m pip install -e '.[dwave]'
```

The default path requires no paid APIs, no IBM Quantum credentials, and no D-Wave credentials.
The default path requires no paid APIs, no IBM Quantum credentials, and no D-Wave credentials. Installing `.[dwave]` enables local `dimod.BinaryQuadraticModel` construction and energy parity reports; it does not submit to D-Wave cloud services.

## Tests And Audit

Expand Down
6 changes: 4 additions & 2 deletions docs/dwave_mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,17 @@ The v0.2 parity harness checks:

These values must agree for every assignment of each small parity fixture. Larger export-only models are validated and serialized without invoking the bounded exhaustive solver.

When Ocean is installed, Noetheris also constructs a `dimod.BinaryQuadraticModel` locally. This does not require D-Wave credentials. The BQM summary records variable count, interaction count, vartype, and `to_qubo` offset.
When Ocean is installed, Noetheris also constructs a `dimod.BinaryQuadraticModel` locally. This does not require D-Wave credentials. The BQM report records vartype, variable count, interaction count, offset, `to_qubo` offset, checked assignments, Noetheris energy, Ocean energy, and agreement status.

If Ocean is absent, the report states that `dimod` is unavailable and leaves BQM data empty. This is a supported default path, not a degraded verification path: Noetheris replay and certificate checks remain local.

The executable local example is:

```bash
python3 examples/dwave_ocean_exchange.py
```

It emits the canonical exchange payload, optional local Ocean parity data, the exact local reference assignment, and a replay result that verifies hashes and energy.
It emits the canonical exchange payload, optional local Ocean BQM parity data, the exact local reference assignment, and a replay result that verifies hashes and energy.

## Solver Boundary

Expand Down
39 changes: 6 additions & 33 deletions examples/dwave_ocean_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,15 @@
import json
from pathlib import Path
import sys
from typing import Any

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "python"))

from noetheris.backends import export_qubo_to_dwave
from noetheris.backends import export_qubo_to_dwave, ocean_bqm_parity_report
from noetheris.ir import StructuralSystem
from noetheris.qubo import compile_system, replay_external_solution, solve_exact


def _ocean_energy_check(exchange: dict[str, Any], assignment: dict[str, bool]) -> dict[str, Any]:
try:
import dimod # type: ignore
except Exception as exc:
return {"available": False, "reason": exc.__class__.__name__}

bqm = dimod.BinaryQuadraticModel(
{
item["variable"]: float(item["coefficient"])
for item in exchange["linear_terms"]
},
{
(item["left"], item["right"]): float(item["coefficient"])
for item in exchange["quadratic_terms"]
},
float(exchange["offset"]),
dimod.BINARY,
)
ocean_assignment = {variable: int(value) for variable, value in assignment.items()}
return {
"available": True,
"bqm_class": "dimod.BinaryQuadraticModel",
"vartype": str(bqm.vartype),
"num_variables": len(bqm.variables),
"num_interactions": len(bqm.quadratic),
"energy": float(bqm.energy(ocean_assignment)),
}


def main() -> None:
system = StructuralSystem.from_json_file(
ROOT / "examples" / "structural_ir" / "consensus_safety_ir.json"
Expand All @@ -65,7 +35,10 @@ def main() -> None:
"embedding": None,
},
)
ocean_check = _ocean_energy_check(exported["exchange"], solution.assignment)
ocean_bqm_report = ocean_bqm_parity_report(
compiled.model,
assignments=(solution.assignment,),
)
print(
json.dumps(
{
Expand All @@ -76,7 +49,7 @@ def main() -> None:
"exchange": exported["exchange"],
"dwave_status": exported["status"],
"bqm_summary": exported["bqm_summary"],
"ocean_energy_check": ocean_check,
"ocean_bqm_report": ocean_bqm_report,
"local_solution": {
"assignment": solution.assignment,
"energy": solution.energy,
Expand Down
2 changes: 2 additions & 0 deletions python/noetheris/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from noetheris.backends.dwave import (
dwave_status,
export_qubo_to_dwave,
ocean_bqm_parity_report,
qubo_exchange_payload,
replay_external_sample,
)
Expand All @@ -16,6 +17,7 @@
"export_bool_expr_to_qiskit",
"export_oracle_to_qiskit",
"export_qubo_to_dwave",
"ocean_bqm_parity_report",
"qubo_exchange_payload",
"qasm_like_export",
"qiskit_status",
Expand Down
122 changes: 94 additions & 28 deletions python/noetheris/backends/dwave.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Any
from typing import Any, Mapping

from noetheris.certificates import stable_problem_hash
from noetheris.qubo import QuboModel
Expand All @@ -16,38 +16,14 @@ def dwave_status() -> dict[str, Any]:

def export_qubo_to_dwave(model: QuboModel) -> dict[str, Any]:
model.validate()
canonical = model.canonicalized()
status = dwave_status()
payload = qubo_exchange_payload(model)
bqm_summary: dict[str, Any] | None = None
if status["available"]:
try:
import dimod # type: ignore

bqm = dimod.BinaryQuadraticModel(
dict(canonical.linear),
{(term.left, term.right): term.coefficient for term in canonical.quadratic},
canonical.constant,
dimod.BINARY,
)
qubo, offset = bqm.to_qubo()
bqm_summary = {
"class": "dimod.BinaryQuadraticModel",
"vartype": str(bqm.vartype),
"num_variables": len(bqm.variables),
"num_interactions": len(bqm.quadratic),
"to_qubo_terms": len(qubo),
"to_qubo_offset": float(offset),
}
except Exception as exc:
bqm_summary = {
"class": "dimod.BinaryQuadraticModel",
"export_error": exc.__class__.__name__,
}
bqm_report = ocean_bqm_parity_report(model, assignments=())
return {
"status": status,
"exchange": payload,
"bqm_summary": bqm_summary,
"bqm_summary": bqm_report["bqm_summary"],
"ocean_bqm_report": bqm_report,
"credential_required": False,
"external_solver_policy": "solver samples are untrusted until local energy replay succeeds",
}
Expand Down Expand Up @@ -81,6 +57,96 @@ def qubo_exchange_payload(model: QuboModel) -> dict[str, Any]:
return {**payload, "model_hash": stable_problem_hash(payload)}


def ocean_bqm_parity_report(
model: QuboModel,
*,
assignments: tuple[Mapping[str, bool | int], ...],
) -> dict[str, Any]:
model.validate()
try:
import dimod # type: ignore
except Exception as exc:
return {
"available": False,
"reason": exc.__class__.__name__,
"credential_required": False,
"bqm_summary": None,
"assignment_reports": [],
"energy_agreement": None,
"policy": "Ocean is optional; missing dimod does not affect local Noetheris replay",
}

canonical = model.canonicalized()
try:
linear_biases = {
variable: canonical.linear.get(variable, 0.0)
for variable in canonical.variables
}
bqm = dimod.BinaryQuadraticModel(
linear_biases,
{(term.left, term.right): term.coefficient for term in canonical.quadratic},
canonical.constant,
dimod.BINARY,
)
qubo, offset = bqm.to_qubo()
bqm_summary = {
"class": "dimod.BinaryQuadraticModel",
"vartype": str(bqm.vartype),
"num_variables": len(bqm.variables),
"num_interactions": len(bqm.quadratic),
"offset": float(getattr(bqm, "offset", canonical.constant)),
"to_qubo_terms": len(qubo),
"to_qubo_offset": float(offset),
}
assignment_reports = [
_ocean_assignment_report(model, bqm, assignment)
for assignment in assignments
]
except Exception as exc:
return {
"available": True,
"credential_required": False,
"bqm_summary": {
"class": "dimod.BinaryQuadraticModel",
"export_error": exc.__class__.__name__,
},
"assignment_reports": [],
"energy_agreement": False,
"policy": "local BQM construction failed before any external solver boundary",
}
return {
"available": True,
"credential_required": False,
"bqm_summary": bqm_summary,
"assignment_reports": assignment_reports,
"energy_agreement": (
all(item["agreement"] for item in assignment_reports)
if assignment_reports
else None
),
"policy": "local dimod BQM construction only; no D-Wave credentials or sampler calls",
}


def _ocean_assignment_report(
model: QuboModel, bqm: Any, assignment: Mapping[str, bool | int]
) -> dict[str, Any]:
normalized = {variable: bool(value) for variable, value in assignment.items()}
noetheris_energy = model.evaluate(normalized)
ocean_assignment = {
variable: int(normalized[variable]) for variable in model.variables
}
ocean_energy = float(bqm.energy(ocean_assignment))
difference = ocean_energy - noetheris_energy
return {
"assignment": {variable: normalized[variable] for variable in model.variables},
"noetheris_energy": noetheris_energy,
"ocean_energy": ocean_energy,
"difference": difference,
"agreement": abs(difference) <= 1e-9,
}


def replay_external_sample(
model: QuboModel,
sample: dict[str, bool | int],
Expand Down
10 changes: 7 additions & 3 deletions tests/test_external_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,13 @@ def test_dwave_ocean_exchange_example_replays_locally() -> None:
assert payload["replay"]["status"] == "verified"
assert payload["replay"]["energy_recomputed"] is True
assert payload["local_solution"]["energy"] == payload["replay"]["energy"]
ocean_check = payload["ocean_energy_check"]
if ocean_check["available"]:
assert ocean_check["energy"] == payload["local_solution"]["energy"]
ocean_report = payload["ocean_bqm_report"]
assert ocean_report["credential_required"] is False
if ocean_report["available"]:
assert ocean_report["energy_agreement"] is True
assert ocean_report["assignment_reports"][0]["ocean_energy"] == payload["local_solution"]["energy"]
else:
assert ocean_report["bqm_summary"] is None


def test_qiskit_oracle_export_example_has_exact_local_semantics() -> None:
Expand Down
91 changes: 90 additions & 1 deletion tests/test_optional_integrations.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from __future__ import annotations

from types import SimpleNamespace

from noetheris.annealing import export_to_dimod_bqm
from noetheris.backends import (
export_bool_expr_to_qiskit,
export_oracle_to_qiskit,
export_qubo_to_dwave,
ocean_bqm_parity_report,
qubo_exchange_payload,
replay_external_sample,
)
from noetheris.circuits import AND, BooleanOracle, BoolExpr
from noetheris.qubo import QuboModel
from noetheris.qubo import QuadraticTerm, QuboModel


def test_dwave_ocean_export_gracefully_reports_availability() -> None:
Expand All @@ -33,6 +36,7 @@ def test_backend_wrappers_never_require_credentials() -> None:
dwave_payload = export_qubo_to_dwave(model)
assert dwave_payload["credential_required"] is False
assert dwave_payload["exchange"]["vartype"] == "BINARY"
assert dwave_payload["ocean_bqm_report"]["credential_required"] is False
assert dwave_payload["external_solver_policy"].startswith("solver samples are untrusted")
oracle = BooleanOracle(("x",), lambda bits: bits[0], name="identity")
qiskit_payload = export_oracle_to_qiskit(oracle)
Expand All @@ -51,6 +55,91 @@ def test_dwave_exchange_payload_and_replay_are_local() -> None:
assert replay["embedding_metadata"]["provided"] is False


def test_ocean_bqm_parity_report_handles_optional_dependency() -> None:
model = QuboModel(variables=["x"], linear={"x": -1.0})
report = ocean_bqm_parity_report(model, assignments=({"x": True},))
assert report["credential_required"] is False
if report["available"]:
assert report["bqm_summary"]["class"] == "dimod.BinaryQuadraticModel"
assert report["assignment_reports"][0]["agreement"] is True
else:
assert report["reason"] == "ModuleNotFoundError"
assert report["bqm_summary"] is None
assert report["energy_agreement"] is None


def test_ocean_bqm_parity_report_verifies_local_bqm_fields(monkeypatch) -> None:
class LocalBqm:
def __init__(self, linear, quadratic, offset, vartype):
self.linear = dict(linear)
self.quadratic = dict(quadratic)
self.offset = float(offset)
self.vartype = vartype
variables = set(self.linear)
for left, right in self.quadratic:
variables.add(left)
variables.add(right)
self.variables = tuple(sorted(variables))

def to_qubo(self):
terms = {
(variable, variable): coefficient
for variable, coefficient in self.linear.items()
if coefficient != 0.0
}
terms.update(self.quadratic)
return terms, self.offset

def energy(self, assignment):
value = self.offset
for variable, coefficient in self.linear.items():
if assignment[variable]:
value += coefficient
for (left, right), coefficient in self.quadratic.items():
if assignment[left] and assignment[right]:
value += coefficient
return value

local_dimod = SimpleNamespace(
__version__="local-test",
BINARY="BINARY",
BinaryQuadraticModel=LocalBqm,
)
monkeypatch.setitem(__import__("sys").modules, "dimod", local_dimod)
model = QuboModel(
variables=["a", "b", "c"],
linear={"a": -1.0},
quadratic=[
QuadraticTerm("b", "a", 2.0),
QuadraticTerm("a", "a", 3.0),
],
constant=0.25,
)
report = ocean_bqm_parity_report(
model,
assignments=(
{"a": True, "b": False, "c": False},
{"a": True, "b": True, "c": True},
),
)
assert report["available"] is True
assert report["credential_required"] is False
assert report["energy_agreement"] is True
assert report["bqm_summary"] == {
"class": "dimod.BinaryQuadraticModel",
"vartype": "BINARY",
"num_variables": 3,
"num_interactions": 1,
"offset": 0.25,
"to_qubo_terms": 2,
"to_qubo_offset": 0.25,
}
assert report["assignment_reports"][0]["noetheris_energy"] == 2.25
assert report["assignment_reports"][0]["ocean_energy"] == 2.25
assert report["assignment_reports"][1]["noetheris_energy"] == 4.25
assert report["assignment_reports"][1]["ocean_energy"] == 4.25


def test_dwave_exchange_handles_large_and_comma_named_models() -> None:
variables = [f"x,{idx}" for idx in range(25)]
model = QuboModel(variables=variables, linear={variables[0]: -1.0})
Expand Down