Skip to content

Quantum Annealing Bridge

Public module: sc_neurocore.bridges.quantum_annealing Tier: Research bridge Compatibility surface: 24 historical exports Optional runtimes: SC-NeuroCore Rust engine, dimod, and D-Wave Ocean

The bridge compiles stochastic-computing (SC) network structures into validated Ising and QUBO models. It provides deterministic classical solving, optional native acceleration, optional D-Wave submission, analysis, decomposition, transformations, and model export.

This is research software. It does not establish quantum advantage, certify a minor embedding, validate a physical QPU, or claim parity with unrelated quantum-circuit modules in sc_neurocore.quantum.

Architecture

The former 1,910-line implementation is now a 98-line compatibility façade and nine responsibility modules. Historical imports and pickle identities remain at sc_neurocore.bridges.quantum_annealing.

Module Responsibility Lines
quantum_annealing.py Stable exports, optional-backend observables, pickle identity 98
annealing_backends.py Optional native, dimod, and D-Wave adapters 139
annealing_models.py Validated Ising/QUBO value objects and backend selection 276
annealing_compilers.py SC adjacency, bitstream, and pruning compilers 235
annealing_solvers.py Python/native simulated annealing and D-Wave adapter 267
annealing_analysis.py Landscape, embedding, sample, and TTS analysis 317
annealing_hardware.py Hardware-capacity estimates and chain resolution 203
annealing_transforms.py Schedules, gauges, and SC precision encodings 245
annealing_io.py Deterministic export and text visualisation 118
annealing_decomposition.py Overlapping partitioning and reconstruction 173

The responsibility graph is acyclic. No responsibility module imports the façade, and an architecture test enforces the dependency direction and size limits.

Public surface

All 24 names are available from both the historical module and sc_neurocore.bridges.

Group Public names
Models ProblemType, QubitSpec, CouplerSpec, IsingModel, QUBOModel
Compilers SCToIsing, SCToQUBO, SCBitstreamQUBO
Solvers SimulatedAnnealer, DWaveInterface
Analysis EnergyLandscape, EmbeddingAnalyzer, SampleAggregator, TTSAnalyzer
Hardware and decomposition HardwareGraph, ChainBreakResolver, ProblemDecomposer
Transformations AnnealingSchedule, GaugeTransform, SCPrecisionEncoder
Export export_ising_json, export_qubo_json, export_bqm, visualize_ising

Quick start

Python
import numpy as np

from sc_neurocore.bridges.quantum_annealing import (
    SCToIsing,
    SimulatedAnnealer,
)

adjacency = np.array(
    [
        [0.0, 0.8, -0.2],
        [0.8, 0.0, 0.4],
        [-0.2, 0.4, 0.0],
    ],
    dtype=np.float64,
)
model = SCToIsing().compile(adjacency, node_labels=["a", "b", "c"])
result = SimulatedAnnealer(
    n_sweeps=200,
    seed=42,
    backend="python",
).solve_ising(model, num_reads=10)

print(result["best_spins"], result["best_energy"])

Choose backend="python" when repeatable Python-path evidence is required. Choose backend="rust" to require the native engine; a missing native backend then raises a stable RuntimeError. backend="auto" may select Rust according to the thresholds documented below.

Model contracts

IsingModel uses

Text Only
E(s) = offset + Σᵢ hᵢsᵢ + Σᵢ<ⱼ Jᵢⱼsᵢsⱼ,  sᵢ ∈ {-1, +1}

and QUBOModel uses

Text Only
E(x) = offset + Σᵢ≤ⱼ Qᵢⱼxᵢxⱼ,  xᵢ ∈ {0, 1}.

Both models canonicalise pair indices, combine reversed duplicates, remove zero-valued terms, infer n_qubits from terms when it is omitted, and reject non-finite values, invalid indices, out-of-bounds labels, and duplicate label values. Missing variables preserve the historical defaults: +1 for an Ising spin and 0 for a QUBO bit.

QUBOModel.to_ising() uses x = (s + 1) / 2 and is exactly energy-equivalent. In particular, a diagonal term Qᵢᵢxᵢ contributes Qᵢᵢ / 2 to both hᵢ and the offset. Off-diagonal terms contribute Qᵢⱼ / 4 to the coupling, both incident fields, and the offset. Exhaustive tests compare every assignment for small models.

Compilers

SC adjacency compilers

SCToIsing.compile() and SCToQUBO.compile() accept a finite, non-empty, square matrix and optional unique labels. Directed pairs are averaged before a single canonical coupling is emitted.

  • SCToIsing maps positive averaged weights to ferromagnetic (negative) couplings and applies the configured field and coupling scales.
  • SCToQUBO uses negative absolute column sums on the diagonal and scaled averaged off-diagonal weights.

Both reject malformed matrices, non-finite values, mismatched biases, and invalid names or labels.

SC bitstream QUBOs

SCBitstreamQUBO.weight_optimization() encodes ||target - candidate_weights @ x||² for binary x. n_bits selects the first candidate columns and must be a positive integer no greater than the available column count; it is never silently clamped.

SCBitstreamQUBO.pruning() creates one variable for every undirected candidate edge found in either adjacency direction. It averages the two directed importance scores and applies a quadratic penalty for selecting exactly max_connections. Impossible cardinalities fail before a model is returned.

SC precision encoding

SCPrecisionEncoder supports binary, unary (thermometer), and one-hot representations of finite values clipped to [0, 1].

Encoding Levels Qubits per value
binary 2**n_bits n_bits
unary n_bits + 1 n_bits
one_hot n_bits n_bits

Array encoding requires a finite, non-empty one-dimensional array and assigns non-overlapping global qubit indices. Decoding rejects invalid indices, values, and multiple active one-hot bits.

Solver and backend behaviour

Simulated annealing

SimulatedAnnealer implements seeded single-spin Metropolis sweeps. The Python and Rust paths return the same mapping-shaped contract:

  • best_spins: dict[int, int]
  • best_energy: float
  • energies: one finite value per returned sample
  • samples: list[dict[int, int]]
  • n_sweeps, num_reads, and backend

solve_qubo() converts through the exact Ising mapping and returns the corresponding bit-valued contract. Native responses are validated for shape, spin domain, energy finiteness, and aligned sample counts before they cross the public boundary. The configured seed is forwarded to Rust.

Backend dispatch is explicit:

Caller auto native threshold Explicit behaviour
IsingModel.energy() Rust available and more than 20 qubits python stays Python; rust requires Rust
SimulatedAnnealer.solve_ising() Rust available and more than 10 qubits python stays Python; rust requires Rust
EnergyLandscape.analyze() Rust available and more than 100 samples python stays Python; rust requires Rust

D-Wave adapter

DWaveInterface.available means only that dimod and the required Ocean SDK classes are importable. It does not prove credentials, network access, solver entitlement, or QPU health.

When those imports are absent, solve_ising() uses a bounded local simulated annealing fallback with at most 20 reads and labels the result simulated_annealing_fallback. When the SDK is installed, sampler construction, authentication, provider, and submission failures propagate; they are not misreported as local success. A successful QPU result is checked for a valid spin mapping and finite best energy.

Install the optional local annealing dependencies with:

Bash
python -m pip install 'sc-neurocore[annealing]'

Real QPU access additionally requires a compatible Ocean installation and provider credentials outside this repository.

Analysis, hardware, and decomposition

  • EnergyLandscape exhaustively enumerates models up to 20 qubits. Larger models use a deterministic configurable random sample unless samples are supplied by the caller.
  • EmbeddingAnalyzer reports graph density, degree, and a Pegasus-oriented chain-size estimate. It is a capacity estimate, not a minor embedding.
  • SampleAggregator validates aligned samples and energies, deduplicates spin patterns, and reports histogram and Boltzmann-weighted summaries.
  • TTSAnalyzer validates probabilities, times, energy samples, and solver payloads before computing the standard cumulative-success estimate.
  • HardwareGraph models idealised Chimera, Pegasus, and Zephyr capacity. Its degree-based can_embed() result is not hardware placement evidence.
  • ChainBreakResolver validates non-overlapping physical chains and resolves by majority vote or local energy minimisation. The latter requires a model.
  • AnnealingSchedule rejects non-finite or non-positive timings and invalid anneal fractions for linear, pause-and-quench, and reverse schedules.
  • GaugeTransform produces deterministic energy-equivalent Python models and validates samples when returning them to the original spin frame.
  • ProblemDecomposer creates deterministic graph-aware partitions with up to the configured overlap from connected assigned neighbours. Submodels use local indices, while reconstruction retains an exact local-to-global index map; it does not infer identity from optional labels.

Decomposition is a heuristic orchestration strategy. It does not guarantee a global optimum or a favourable embedding.

Export behaviour

export_ising_json() and export_qubo_json() write sorted, deterministic UTF-8 JSON through a same-directory temporary file, flush and synchronise it, and atomically replace the destination. A failed write removes the temporary file. export_bqm() returns a dimod spin BQM when available and None otherwise. visualize_ising() returns a stable human-readable text representation.

Polyglot authority

The Python bridge is the maintained orchestration authority. The maintained native implementation is engine/src/quantum.rs, exposed through the PyO3 engine package and covered by 12 focused Rust tests.

Earlier generated files under the Rust safety registry, Go services, Julia bridges, and Mojo kernels were not alternate implementations: the Rust safety file returned constants, the Go functions were empty, and the Julia and Mojo files did not build. They have been removed together with their registry entries. No Go, Julia, Mojo, or generated-Rust parity or performance claim is made for this bridge.

The maintained native comparison harness remains available:

Bash
python benchmarks/bench_quantum_annealing_rust_vs_python.py

Its results are machine- and load-specific; rerun it in the target release environment before publishing a native speed claim.

Modularisation benchmark

benchmarks/bench_quantum_annealing_modularisation.py compares the committed single-file parent (9308910a5d863ebfb338244b43d10f73f25cfbc6) with the modular candidate. It uses 30 measured child processes after five warm-ups, alternates variant order, pins each child with taskset, records raw samples and load context, and binds both variants to source digests.

Metric Parent median Modular median Delta
Cold import 383.554 ms 286.231 ms -25.37%
Eight-node compile 0.0389 ms 0.0776 ms +99.65%
Python solve 1.8522 ms 1.5434 ms -16.67%
Child wall time 569.268 ms 437.154 ms -23.21%
Maximum RSS 41,326 KiB 39,888 KiB -3.48%

The candidate source digest is 9d5152b02377c5358a7eb678ab9b35633fbe40deefd6c0a7a8b7996978023d1a. The workstation load average rose from 7.46 to 11.53 during capture, and CPU 0 was affinity-pinned but not exclusively isolated. These measurements are local regression diagnostics, not release throughput or hardware claims. Repeat on reserved isolated cores before promotion.

Reproduce the committed evidence with:

Bash
python benchmarks/bench_quantum_annealing_modularisation.py \
  --baseline-root /path/to/clean-parent \
  --candidate-root . \
  --output benchmarks/results/local_python_quantum_annealing.json

Verification

The focused cohort is split by responsibility instead of rebuilding a test GodFile. Its largest file is 445 lines. The linked cohort passes 250 tests with one optional neal parity test skipped when that extra is absent. Exact-file coverage passes 216 tests and records 100% of 1,040 statements and 442 branches with no misses or partial branches.

Bash
PYTHONPATH=src:. python -m pytest \
  tests/test_bridges/test_quantum_annealing_value_specs.py \
  tests/test_bridges/test_quantum_annealing_ising_model.py \
  tests/test_bridges/test_quantum_annealing_qubo_model.py \
  tests/test_bridges/test_quantum_annealing_network_compilers.py \
  tests/test_bridges/test_quantum_annealing_bitstream_qubo.py \
  tests/test_bridges/test_quantum_annealing_solvers_backends.py \
  tests/test_bridges/test_quantum_annealing_energy_landscape.py \
  tests/test_bridges/test_quantum_annealing_embedding_analysis.py \
  tests/test_bridges/test_quantum_annealing_sample_aggregation.py \
  tests/test_bridges/test_quantum_annealing_time_to_solution.py \
  tests/test_bridges/test_quantum_annealing_hardware_graph.py \
  tests/test_bridges/test_quantum_annealing_chain_break_resolution.py \
  tests/test_bridges/test_quantum_annealing_transforms_io.py \
  tests/test_bridges/test_quantum_annealing_decomposition_architecture.py \
  tests/test_bridges/test_quantum_annealing_neal_parity.py \
  tests/test_bench_quantum_annealing_modularisation.py

cargo test --manifest-path engine/Cargo.toml quantum --lib

Coverage includes exact QUBO-to-Ising energy equivalence, backend selection and malformed native returns, QPU fallback/error boundaries, compiler objectives, schedule and hardware validation, atomic export cleanup, exact decomposition index reconstruction, historical import and pickle identities, the architecture DAG, and benchmark schema/source binding.

Limitations

  • No real QPU submission is exercised by the dependency-light test cohort.
  • available checks imports, not credentials or provider health.
  • Hardware and embedding results are conservative estimates, not placement proofs.
  • Problem decomposition is heuristic and offers no optimum guarantee.
  • Optional neal parity requires sc-neurocore[annealing].
  • Benchmark evidence does not demonstrate quantum speedup or production throughput.

Auto-rendered API

sc_neurocore.bridges.quantum_annealing

Compatibility façade for modular quantum-annealing tooling.

The bridge compiles SC network structures into validated Ising/QUBO models, runs classical or optional native/D-Wave solvers, and exposes bounded analysis, embedding, transformation, decomposition, and export responsibilities.

ProblemType

Bases: Enum

Quantum optimization problem type.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
47
48
49
50
51
class ProblemType(Enum):
    """Quantum optimization problem type."""

    ISING = "ising"
    QUBO = "qubo"

QubitSpec dataclass

Specification for one logical qubit.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@dataclass
class QubitSpec:
    """Specification for one logical qubit."""

    index: int
    label: str
    bias: float = 0.0

    def __post_init__(self) -> None:
        """Validate the qubit index, label, and bias."""
        self.index = _require_index("index", self.index)
        if not isinstance(self.label, str) or not self.label.strip():
            raise ValueError("label must be a non-empty string")
        self.bias = _require_finite("bias", self.bias)

__post_init__()

Validate the qubit index, label, and bias.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
62
63
64
65
66
67
def __post_init__(self) -> None:
    """Validate the qubit index, label, and bias."""
    self.index = _require_index("index", self.index)
    if not isinstance(self.label, str) or not self.label.strip():
        raise ValueError("label must be a non-empty string")
    self.bias = _require_finite("bias", self.bias)

CouplerSpec dataclass

Specification for one logical Ising/QUBO coupling.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@dataclass
class CouplerSpec:
    """Specification for one logical Ising/QUBO coupling."""

    qubit_a: int
    qubit_b: int
    strength: float = 0.0

    def __post_init__(self) -> None:
        """Validate distinct endpoints and a finite strength."""
        self.qubit_a = _require_index("qubit_a", self.qubit_a)
        self.qubit_b = _require_index("qubit_b", self.qubit_b)
        if self.qubit_a == self.qubit_b:
            raise ValueError("coupler endpoints must be distinct")
        if self.qubit_a > self.qubit_b:
            self.qubit_a, self.qubit_b = self.qubit_b, self.qubit_a
        self.strength = _require_finite("strength", self.strength)

__post_init__()

Validate distinct endpoints and a finite strength.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
78
79
80
81
82
83
84
85
86
def __post_init__(self) -> None:
    """Validate distinct endpoints and a finite strength."""
    self.qubit_a = _require_index("qubit_a", self.qubit_a)
    self.qubit_b = _require_index("qubit_b", self.qubit_b)
    if self.qubit_a == self.qubit_b:
        raise ValueError("coupler endpoints must be distinct")
    if self.qubit_a > self.qubit_b:
        self.qubit_a, self.qubit_b = self.qubit_b, self.qubit_a
    self.strength = _require_finite("strength", self.strength)

IsingModel dataclass

Ising spin-glass model H = Σhᵢsᵢ + ΣJᵢⱼsᵢsⱼ.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
@dataclass
class IsingModel:
    """Ising spin-glass model ``H = Σhᵢsᵢ + ΣJᵢⱼsᵢsⱼ``."""

    h: dict[int, float] = field(default_factory=dict)
    J: dict[tuple[int, int], float] = field(default_factory=dict)
    offset: float = 0.0
    qubit_labels: dict[int, str] = field(default_factory=dict)
    n_qubits: int = 0
    source: str = ""

    def __post_init__(self) -> None:
        """Normalize canonical couplings and validate model bounds."""
        normalized_h: dict[int, float] = {}
        highest_index = -1
        for raw_index, raw_bias in self.h.items():
            index = _require_index("h index", raw_index)
            normalized_h[index] = _require_finite(f"h[{index}]", raw_bias)
            highest_index = max(highest_index, index)

        normalized_j: dict[tuple[int, int], float] = {}
        for raw_pair, raw_strength in self.J.items():
            if not isinstance(raw_pair, tuple) or len(raw_pair) != 2:
                raise ValueError("J keys must be two-index tuples")
            first = _require_index("J endpoint", raw_pair[0])
            second = _require_index("J endpoint", raw_pair[1])
            if first == second:
                raise ValueError("Ising couplings must connect distinct qubits")
            pair = (min(first, second), max(first, second))
            strength = _require_finite(f"J[{pair}]", raw_strength)
            normalized_j[pair] = normalized_j.get(pair, 0.0) + strength
            highest_index = max(highest_index, *pair)

        normalized_labels: dict[int, str] = {}
        for raw_index, label in self.qubit_labels.items():
            index = _require_index("qubit label index", raw_index)
            if not isinstance(label, str) or not label.strip():
                raise ValueError("qubit labels must be non-empty strings")
            normalized_labels[index] = label
            highest_index = max(highest_index, index)
        if len(set(normalized_labels.values())) != len(normalized_labels):
            raise ValueError("qubit labels must be unique")

        if isinstance(self.n_qubits, bool) or not isinstance(self.n_qubits, int):
            raise ValueError("n_qubits must be a non-negative integer")
        if self.n_qubits < 0:
            raise ValueError("n_qubits must be a non-negative integer")
        if self.n_qubits == 0 and highest_index >= 0:
            self.n_qubits = highest_index + 1
        if highest_index >= self.n_qubits:
            raise ValueError("model indices must be smaller than n_qubits")
        if not isinstance(self.source, str):
            raise ValueError("source must be a string")

        self.h = normalized_h
        self.J = {pair: value for pair, value in normalized_j.items() if value != 0.0}
        self.qubit_labels = normalized_labels
        self.offset = _require_finite("offset", self.offset)

    def energy(
        self,
        spins: Mapping[int, int],
        *,
        backend: BackendChoice = "auto",
    ) -> float:
        """Compute energy for a partial spin assignment.

        Missing spins retain the historical ``+1`` default. An explicit Rust
        request fails when the native engine is unavailable; ``auto`` uses it
        only for models larger than 20 qubits.
        """
        selected = validate_backend_choice(backend)
        for index, spin in spins.items():
            _require_index("spin index", index)
            if spin not in {-1, 1}:
                raise ValueError("spin values must be -1 or +1")

        use_rust = selected == "rust" or (
            selected == "auto" and backends.HAS_RUST_QA and self.n_qubits > 20
        )
        if use_rust:
            kernel = backends.require_rust_energy()
            h_indices = list(self.h)
            j_pairs = list(self.J)
            return float(
                kernel(
                    h_indices,
                    [self.h[index] for index in h_indices],
                    [pair[0] for pair in j_pairs],
                    [pair[1] for pair in j_pairs],
                    [self.J[pair] for pair in j_pairs],
                    [spins.get(index, 1) for index in range(self.n_qubits)],
                    self.offset,
                )
            )

        energy = self.offset
        for index, bias in self.h.items():
            energy += bias * spins.get(index, 1)
        for (first, second), strength in self.J.items():
            energy += strength * spins.get(first, 1) * spins.get(second, 1)
        return energy

__post_init__()

Normalize canonical couplings and validate model bounds.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def __post_init__(self) -> None:
    """Normalize canonical couplings and validate model bounds."""
    normalized_h: dict[int, float] = {}
    highest_index = -1
    for raw_index, raw_bias in self.h.items():
        index = _require_index("h index", raw_index)
        normalized_h[index] = _require_finite(f"h[{index}]", raw_bias)
        highest_index = max(highest_index, index)

    normalized_j: dict[tuple[int, int], float] = {}
    for raw_pair, raw_strength in self.J.items():
        if not isinstance(raw_pair, tuple) or len(raw_pair) != 2:
            raise ValueError("J keys must be two-index tuples")
        first = _require_index("J endpoint", raw_pair[0])
        second = _require_index("J endpoint", raw_pair[1])
        if first == second:
            raise ValueError("Ising couplings must connect distinct qubits")
        pair = (min(first, second), max(first, second))
        strength = _require_finite(f"J[{pair}]", raw_strength)
        normalized_j[pair] = normalized_j.get(pair, 0.0) + strength
        highest_index = max(highest_index, *pair)

    normalized_labels: dict[int, str] = {}
    for raw_index, label in self.qubit_labels.items():
        index = _require_index("qubit label index", raw_index)
        if not isinstance(label, str) or not label.strip():
            raise ValueError("qubit labels must be non-empty strings")
        normalized_labels[index] = label
        highest_index = max(highest_index, index)
    if len(set(normalized_labels.values())) != len(normalized_labels):
        raise ValueError("qubit labels must be unique")

    if isinstance(self.n_qubits, bool) or not isinstance(self.n_qubits, int):
        raise ValueError("n_qubits must be a non-negative integer")
    if self.n_qubits < 0:
        raise ValueError("n_qubits must be a non-negative integer")
    if self.n_qubits == 0 and highest_index >= 0:
        self.n_qubits = highest_index + 1
    if highest_index >= self.n_qubits:
        raise ValueError("model indices must be smaller than n_qubits")
    if not isinstance(self.source, str):
        raise ValueError("source must be a string")

    self.h = normalized_h
    self.J = {pair: value for pair, value in normalized_j.items() if value != 0.0}
    self.qubit_labels = normalized_labels
    self.offset = _require_finite("offset", self.offset)

energy(spins, *, backend='auto')

Compute energy for a partial spin assignment.

Missing spins retain the historical +1 default. An explicit Rust request fails when the native engine is unavailable; auto uses it only for models larger than 20 qubits.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def energy(
    self,
    spins: Mapping[int, int],
    *,
    backend: BackendChoice = "auto",
) -> float:
    """Compute energy for a partial spin assignment.

    Missing spins retain the historical ``+1`` default. An explicit Rust
    request fails when the native engine is unavailable; ``auto`` uses it
    only for models larger than 20 qubits.
    """
    selected = validate_backend_choice(backend)
    for index, spin in spins.items():
        _require_index("spin index", index)
        if spin not in {-1, 1}:
            raise ValueError("spin values must be -1 or +1")

    use_rust = selected == "rust" or (
        selected == "auto" and backends.HAS_RUST_QA and self.n_qubits > 20
    )
    if use_rust:
        kernel = backends.require_rust_energy()
        h_indices = list(self.h)
        j_pairs = list(self.J)
        return float(
            kernel(
                h_indices,
                [self.h[index] for index in h_indices],
                [pair[0] for pair in j_pairs],
                [pair[1] for pair in j_pairs],
                [self.J[pair] for pair in j_pairs],
                [spins.get(index, 1) for index in range(self.n_qubits)],
                self.offset,
            )
        )

    energy = self.offset
    for index, bias in self.h.items():
        energy += bias * spins.get(index, 1)
    for (first, second), strength in self.J.items():
        energy += strength * spins.get(first, 1) * spins.get(second, 1)
    return energy

QUBOModel dataclass

Quadratic unconstrained binary optimization model xᵀQx.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
@dataclass
class QUBOModel:
    """Quadratic unconstrained binary optimization model ``xᵀQx``."""

    Q: dict[tuple[int, int], float] = field(default_factory=dict)
    offset: float = 0.0
    qubit_labels: dict[int, str] = field(default_factory=dict)
    n_qubits: int = 0
    source: str = ""

    def __post_init__(self) -> None:
        """Normalize matrix keys and validate model bounds."""
        normalized_q: dict[tuple[int, int], float] = {}
        highest_index = -1
        for raw_pair, raw_value in self.Q.items():
            if not isinstance(raw_pair, tuple) or len(raw_pair) != 2:
                raise ValueError("Q keys must be two-index tuples")
            first = _require_index("Q index", raw_pair[0])
            second = _require_index("Q index", raw_pair[1])
            pair = (min(first, second), max(first, second))
            value = _require_finite(f"Q[{pair}]", raw_value)
            normalized_q[pair] = normalized_q.get(pair, 0.0) + value
            highest_index = max(highest_index, *pair)

        normalized_labels: dict[int, str] = {}
        for raw_index, label in self.qubit_labels.items():
            index = _require_index("qubit label index", raw_index)
            if not isinstance(label, str) or not label.strip():
                raise ValueError("qubit labels must be non-empty strings")
            normalized_labels[index] = label
            highest_index = max(highest_index, index)
        if len(set(normalized_labels.values())) != len(normalized_labels):
            raise ValueError("qubit labels must be unique")

        if isinstance(self.n_qubits, bool) or not isinstance(self.n_qubits, int):
            raise ValueError("n_qubits must be a non-negative integer")
        if self.n_qubits < 0:
            raise ValueError("n_qubits must be a non-negative integer")
        if self.n_qubits == 0 and highest_index >= 0:
            self.n_qubits = highest_index + 1
        if highest_index >= self.n_qubits:
            raise ValueError("model indices must be smaller than n_qubits")
        if not isinstance(self.source, str):
            raise ValueError("source must be a string")

        self.Q = {pair: value for pair, value in normalized_q.items() if value != 0.0}
        self.qubit_labels = normalized_labels
        self.offset = _require_finite("offset", self.offset)

    def energy(self, bits: Mapping[int, int]) -> float:
        """Compute QUBO energy for a partial binary assignment."""
        for index, bit in bits.items():
            _require_index("bit index", index)
            if bit not in {0, 1}:
                raise ValueError("bit values must be 0 or 1")
        energy = self.offset
        for (first, second), coefficient in self.Q.items():
            energy += coefficient * bits.get(first, 0) * bits.get(second, 0)
        return energy

    def to_ising(self) -> IsingModel:
        """Convert QUBO to an exactly energy-equivalent Ising model."""
        h: dict[int, float] = {}
        couplings: dict[tuple[int, int], float] = {}
        offset = self.offset

        for (first, second), coefficient in self.Q.items():
            if first == second:
                h[first] = h.get(first, 0.0) + coefficient / 2.0
                offset += coefficient / 2.0
                continue
            couplings[(first, second)] = couplings.get((first, second), 0.0) + coefficient / 4.0
            h[first] = h.get(first, 0.0) + coefficient / 4.0
            h[second] = h.get(second, 0.0) + coefficient / 4.0
            offset += coefficient / 4.0

        return IsingModel(
            h=h,
            J=couplings,
            offset=offset,
            qubit_labels=dict(self.qubit_labels),
            n_qubits=self.n_qubits,
            source=f"{self.source} (QUBO→Ising)",
        )

__post_init__()

Normalize matrix keys and validate model bounds.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def __post_init__(self) -> None:
    """Normalize matrix keys and validate model bounds."""
    normalized_q: dict[tuple[int, int], float] = {}
    highest_index = -1
    for raw_pair, raw_value in self.Q.items():
        if not isinstance(raw_pair, tuple) or len(raw_pair) != 2:
            raise ValueError("Q keys must be two-index tuples")
        first = _require_index("Q index", raw_pair[0])
        second = _require_index("Q index", raw_pair[1])
        pair = (min(first, second), max(first, second))
        value = _require_finite(f"Q[{pair}]", raw_value)
        normalized_q[pair] = normalized_q.get(pair, 0.0) + value
        highest_index = max(highest_index, *pair)

    normalized_labels: dict[int, str] = {}
    for raw_index, label in self.qubit_labels.items():
        index = _require_index("qubit label index", raw_index)
        if not isinstance(label, str) or not label.strip():
            raise ValueError("qubit labels must be non-empty strings")
        normalized_labels[index] = label
        highest_index = max(highest_index, index)
    if len(set(normalized_labels.values())) != len(normalized_labels):
        raise ValueError("qubit labels must be unique")

    if isinstance(self.n_qubits, bool) or not isinstance(self.n_qubits, int):
        raise ValueError("n_qubits must be a non-negative integer")
    if self.n_qubits < 0:
        raise ValueError("n_qubits must be a non-negative integer")
    if self.n_qubits == 0 and highest_index >= 0:
        self.n_qubits = highest_index + 1
    if highest_index >= self.n_qubits:
        raise ValueError("model indices must be smaller than n_qubits")
    if not isinstance(self.source, str):
        raise ValueError("source must be a string")

    self.Q = {pair: value for pair, value in normalized_q.items() if value != 0.0}
    self.qubit_labels = normalized_labels
    self.offset = _require_finite("offset", self.offset)

energy(bits)

Compute QUBO energy for a partial binary assignment.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
242
243
244
245
246
247
248
249
250
251
def energy(self, bits: Mapping[int, int]) -> float:
    """Compute QUBO energy for a partial binary assignment."""
    for index, bit in bits.items():
        _require_index("bit index", index)
        if bit not in {0, 1}:
            raise ValueError("bit values must be 0 or 1")
    energy = self.offset
    for (first, second), coefficient in self.Q.items():
        energy += coefficient * bits.get(first, 0) * bits.get(second, 0)
    return energy

to_ising()

Convert QUBO to an exactly energy-equivalent Ising model.

Source code in src/sc_neurocore/bridges/annealing_models.py
Python
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def to_ising(self) -> IsingModel:
    """Convert QUBO to an exactly energy-equivalent Ising model."""
    h: dict[int, float] = {}
    couplings: dict[tuple[int, int], float] = {}
    offset = self.offset

    for (first, second), coefficient in self.Q.items():
        if first == second:
            h[first] = h.get(first, 0.0) + coefficient / 2.0
            offset += coefficient / 2.0
            continue
        couplings[(first, second)] = couplings.get((first, second), 0.0) + coefficient / 4.0
        h[first] = h.get(first, 0.0) + coefficient / 4.0
        h[second] = h.get(second, 0.0) + coefficient / 4.0
        offset += coefficient / 4.0

    return IsingModel(
        h=h,
        J=couplings,
        offset=offset,
        qubit_labels=dict(self.qubit_labels),
        n_qubits=self.n_qubits,
        source=f"{self.source} (QUBO→Ising)",
    )

SCToIsing

Compile an SC adjacency matrix into an Ising model.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
class SCToIsing:
    """Compile an SC adjacency matrix into an Ising model."""

    def __init__(
        self,
        coupling_scale: float = 1.0,
        field_scale: float = 0.1,
    ) -> None:
        """Configure finite coupling and local-field scales."""
        self._coupling_scale = _finite_scalar("coupling_scale", coupling_scale)
        self._field_scale = _finite_scalar("field_scale", field_scale)

    def compile(
        self,
        adjacency: np.ndarray[Any, Any],
        node_labels: Sequence[str] | None = None,
        biases: np.ndarray[Any, Any] | None = None,
        name: str = "sc_ising",
    ) -> IsingModel:
        """Compile a finite square weight matrix.

        Directed pairs are averaged. Positive weights become ferromagnetic
        couplings and negative weights become antiferromagnetic couplings.
        """
        matrix = _square_matrix("adjacency", adjacency)
        size = matrix.shape[0]
        labels = _labels(node_labels, size)
        if not isinstance(name, str) or not name.strip():
            raise ValueError("name must be a non-empty string")

        if biases is None:
            bias_array = np.zeros(size, dtype=np.float64)
        else:
            bias_array = np.asarray(biases, dtype=np.float64)
            if bias_array.shape != (size,):
                raise ValueError("biases must contain exactly one value per node")
            if not bool(np.all(np.isfinite(bias_array))):
                raise ValueError("biases must contain only finite values")

        fields = {index: float(bias_array[index]) * self._field_scale for index in range(size)}
        couplings: dict[tuple[int, int], float] = {}
        for first in range(size):
            for second in range(first + 1, size):
                weight = float(matrix[first, second] + matrix[second, first]) / 2.0
                if abs(weight) > _ZERO_TOLERANCE:
                    couplings[(first, second)] = -weight * self._coupling_scale

        return IsingModel(
            h=fields,
            J=couplings,
            qubit_labels={index: label for index, label in enumerate(labels)},
            n_qubits=size,
            source=name,
        )

__init__(coupling_scale=1.0, field_scale=0.1)

Configure finite coupling and local-field scales.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
62
63
64
65
66
67
68
69
def __init__(
    self,
    coupling_scale: float = 1.0,
    field_scale: float = 0.1,
) -> None:
    """Configure finite coupling and local-field scales."""
    self._coupling_scale = _finite_scalar("coupling_scale", coupling_scale)
    self._field_scale = _finite_scalar("field_scale", field_scale)

compile(adjacency, node_labels=None, biases=None, name='sc_ising')

Compile a finite square weight matrix.

Directed pairs are averaged. Positive weights become ferromagnetic couplings and negative weights become antiferromagnetic couplings.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def compile(
    self,
    adjacency: np.ndarray[Any, Any],
    node_labels: Sequence[str] | None = None,
    biases: np.ndarray[Any, Any] | None = None,
    name: str = "sc_ising",
) -> IsingModel:
    """Compile a finite square weight matrix.

    Directed pairs are averaged. Positive weights become ferromagnetic
    couplings and negative weights become antiferromagnetic couplings.
    """
    matrix = _square_matrix("adjacency", adjacency)
    size = matrix.shape[0]
    labels = _labels(node_labels, size)
    if not isinstance(name, str) or not name.strip():
        raise ValueError("name must be a non-empty string")

    if biases is None:
        bias_array = np.zeros(size, dtype=np.float64)
    else:
        bias_array = np.asarray(biases, dtype=np.float64)
        if bias_array.shape != (size,):
            raise ValueError("biases must contain exactly one value per node")
        if not bool(np.all(np.isfinite(bias_array))):
            raise ValueError("biases must contain only finite values")

    fields = {index: float(bias_array[index]) * self._field_scale for index in range(size)}
    couplings: dict[tuple[int, int], float] = {}
    for first in range(size):
        for second in range(first + 1, size):
            weight = float(matrix[first, second] + matrix[second, first]) / 2.0
            if abs(weight) > _ZERO_TOLERANCE:
                couplings[(first, second)] = -weight * self._coupling_scale

    return IsingModel(
        h=fields,
        J=couplings,
        qubit_labels={index: label for index, label in enumerate(labels)},
        n_qubits=size,
        source=name,
    )

SCToQUBO

Compile an SC adjacency matrix into a QUBO model.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
class SCToQUBO:
    """Compile an SC adjacency matrix into a QUBO model."""

    def __init__(self, penalty: float = 2.0) -> None:
        """Configure a positive constraint penalty."""
        self._penalty = _finite_scalar("penalty", penalty, positive=True)

    def compile(
        self,
        adjacency: np.ndarray[Any, Any],
        node_labels: Sequence[str] | None = None,
        name: str = "sc_qubo",
    ) -> QUBOModel:
        """Compile a finite square weight matrix into canonical QUBO terms."""
        matrix = _square_matrix("adjacency", adjacency)
        size = matrix.shape[0]
        labels = _labels(node_labels, size)
        if not isinstance(name, str) or not name.strip():
            raise ValueError("name must be a non-empty string")

        q_matrix: dict[tuple[int, int], float] = {}
        for index in range(size):
            q_matrix[(index, index)] = -float(np.sum(np.abs(matrix[:, index])))
            for second in range(index + 1, size):
                weight = float(matrix[index, second] + matrix[second, index]) / 2.0
                if abs(weight) > _ZERO_TOLERANCE:
                    q_matrix[(index, second)] = weight * self._penalty

        return QUBOModel(
            Q=q_matrix,
            qubit_labels={index: label for index, label in enumerate(labels)},
            n_qubits=size,
            source=name,
        )

__init__(penalty=2.0)

Configure a positive constraint penalty.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
118
119
120
def __init__(self, penalty: float = 2.0) -> None:
    """Configure a positive constraint penalty."""
    self._penalty = _finite_scalar("penalty", penalty, positive=True)

compile(adjacency, node_labels=None, name='sc_qubo')

Compile a finite square weight matrix into canonical QUBO terms.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def compile(
    self,
    adjacency: np.ndarray[Any, Any],
    node_labels: Sequence[str] | None = None,
    name: str = "sc_qubo",
) -> QUBOModel:
    """Compile a finite square weight matrix into canonical QUBO terms."""
    matrix = _square_matrix("adjacency", adjacency)
    size = matrix.shape[0]
    labels = _labels(node_labels, size)
    if not isinstance(name, str) or not name.strip():
        raise ValueError("name must be a non-empty string")

    q_matrix: dict[tuple[int, int], float] = {}
    for index in range(size):
        q_matrix[(index, index)] = -float(np.sum(np.abs(matrix[:, index])))
        for second in range(index + 1, size):
            weight = float(matrix[index, second] + matrix[second, index]) / 2.0
            if abs(weight) > _ZERO_TOLERANCE:
                q_matrix[(index, second)] = weight * self._penalty

    return QUBOModel(
        Q=q_matrix,
        qubit_labels={index: label for index, label in enumerate(labels)},
        n_qubits=size,
        source=name,
    )

SCBitstreamQUBO

Build QUBOs for SC weight selection and connection pruning.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
class SCBitstreamQUBO:
    """Build QUBOs for SC weight selection and connection pruning."""

    def __init__(self, penalty: float = 5.0) -> None:
        """Configure a positive constraint penalty."""
        self._penalty = _finite_scalar("penalty", penalty, positive=True)

    def weight_optimization(
        self,
        target_output: np.ndarray[Any, Any],
        candidate_weights: np.ndarray[Any, Any],
        n_bits: int = 8,
    ) -> QUBOModel:
        """Encode ``||target - candidate_weights @ x||²`` for binary ``x``."""
        target = np.asarray(target_output, dtype=np.float64)
        weights = np.asarray(candidate_weights, dtype=np.float64)
        if target.ndim != 1 or target.size == 0:
            raise ValueError("target_output must be a non-empty one-dimensional array")
        if weights.ndim != 2 or weights.shape[0] != target.shape[0] or weights.shape[1] == 0:
            raise ValueError("candidate_weights must be a non-empty matrix with one row per target")
        if not bool(np.all(np.isfinite(target))) or not bool(np.all(np.isfinite(weights))):
            raise ValueError("weight optimization inputs must contain only finite values")
        if isinstance(n_bits, bool) or not isinstance(n_bits, int) or n_bits <= 0:
            raise ValueError("n_bits must be a positive integer")
        if n_bits > weights.shape[1]:
            raise ValueError("n_bits cannot exceed the number of candidate columns")

        selected_weights = weights[:, :n_bits]
        gram = selected_weights.T @ selected_weights
        correlation = selected_weights.T @ target
        q_matrix: dict[tuple[int, int], float] = {}
        for first in range(n_bits):
            q_matrix[(first, first)] = float(gram[first, first] - 2.0 * correlation[first])
            for second in range(first + 1, n_bits):
                value = float(gram[first, second] + gram[second, first])
                if abs(value) > _ZERO_TOLERANCE:
                    q_matrix[(first, second)] = value

        return QUBOModel(
            Q=q_matrix,
            offset=float(target @ target),
            n_qubits=n_bits,
            source="sc_weight_optimization",
        )

    def pruning(
        self,
        adjacency: np.ndarray[Any, Any],
        importance_scores: np.ndarray[Any, Any],
        max_connections: int,
    ) -> QUBOModel:
        """Select exactly ``max_connections`` undirected candidate edges."""
        matrix = _square_matrix("adjacency", adjacency)
        importance = np.asarray(importance_scores, dtype=np.float64)
        if importance.shape != matrix.shape or not bool(np.all(np.isfinite(importance))):
            raise ValueError("importance_scores must be finite and match adjacency")
        if isinstance(max_connections, bool) or not isinstance(max_connections, int):
            raise ValueError("max_connections must be an integer")

        edges: list[tuple[int, int]] = []
        for first in range(matrix.shape[0]):
            for second in range(first + 1, matrix.shape[0]):
                if max(abs(matrix[first, second]), abs(matrix[second, first])) > _ZERO_TOLERANCE:
                    edges.append((first, second))
        if max_connections < 0 or max_connections > len(edges):
            raise ValueError("max_connections must be between zero and the candidate edge count")

        q_matrix: dict[tuple[int, int], float] = {}
        for edge_index, (first, second) in enumerate(edges):
            symmetric_importance = (
                float(importance[first, second] + importance[second, first]) / 2.0
            )
            q_matrix[(edge_index, edge_index)] = -symmetric_importance

        for first in range(len(edges)):
            q_matrix[(first, first)] += self._penalty * (1 - 2 * max_connections)
            for second in range(first + 1, len(edges)):
                q_matrix[(first, second)] = 2 * self._penalty

        return QUBOModel(
            Q=q_matrix,
            offset=self._penalty * max_connections**2,
            n_qubits=len(edges),
            source="sc_pruning",
        )

__init__(penalty=5.0)

Configure a positive constraint penalty.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
154
155
156
def __init__(self, penalty: float = 5.0) -> None:
    """Configure a positive constraint penalty."""
    self._penalty = _finite_scalar("penalty", penalty, positive=True)

weight_optimization(target_output, candidate_weights, n_bits=8)

Encode ||target - candidate_weights @ x||² for binary x.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
def weight_optimization(
    self,
    target_output: np.ndarray[Any, Any],
    candidate_weights: np.ndarray[Any, Any],
    n_bits: int = 8,
) -> QUBOModel:
    """Encode ``||target - candidate_weights @ x||²`` for binary ``x``."""
    target = np.asarray(target_output, dtype=np.float64)
    weights = np.asarray(candidate_weights, dtype=np.float64)
    if target.ndim != 1 or target.size == 0:
        raise ValueError("target_output must be a non-empty one-dimensional array")
    if weights.ndim != 2 or weights.shape[0] != target.shape[0] or weights.shape[1] == 0:
        raise ValueError("candidate_weights must be a non-empty matrix with one row per target")
    if not bool(np.all(np.isfinite(target))) or not bool(np.all(np.isfinite(weights))):
        raise ValueError("weight optimization inputs must contain only finite values")
    if isinstance(n_bits, bool) or not isinstance(n_bits, int) or n_bits <= 0:
        raise ValueError("n_bits must be a positive integer")
    if n_bits > weights.shape[1]:
        raise ValueError("n_bits cannot exceed the number of candidate columns")

    selected_weights = weights[:, :n_bits]
    gram = selected_weights.T @ selected_weights
    correlation = selected_weights.T @ target
    q_matrix: dict[tuple[int, int], float] = {}
    for first in range(n_bits):
        q_matrix[(first, first)] = float(gram[first, first] - 2.0 * correlation[first])
        for second in range(first + 1, n_bits):
            value = float(gram[first, second] + gram[second, first])
            if abs(value) > _ZERO_TOLERANCE:
                q_matrix[(first, second)] = value

    return QUBOModel(
        Q=q_matrix,
        offset=float(target @ target),
        n_qubits=n_bits,
        source="sc_weight_optimization",
    )

pruning(adjacency, importance_scores, max_connections)

Select exactly max_connections undirected candidate edges.

Source code in src/sc_neurocore/bridges/annealing_compilers.py
Python
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def pruning(
    self,
    adjacency: np.ndarray[Any, Any],
    importance_scores: np.ndarray[Any, Any],
    max_connections: int,
) -> QUBOModel:
    """Select exactly ``max_connections`` undirected candidate edges."""
    matrix = _square_matrix("adjacency", adjacency)
    importance = np.asarray(importance_scores, dtype=np.float64)
    if importance.shape != matrix.shape or not bool(np.all(np.isfinite(importance))):
        raise ValueError("importance_scores must be finite and match adjacency")
    if isinstance(max_connections, bool) or not isinstance(max_connections, int):
        raise ValueError("max_connections must be an integer")

    edges: list[tuple[int, int]] = []
    for first in range(matrix.shape[0]):
        for second in range(first + 1, matrix.shape[0]):
            if max(abs(matrix[first, second]), abs(matrix[second, first])) > _ZERO_TOLERANCE:
                edges.append((first, second))
    if max_connections < 0 or max_connections > len(edges):
        raise ValueError("max_connections must be between zero and the candidate edge count")

    q_matrix: dict[tuple[int, int], float] = {}
    for edge_index, (first, second) in enumerate(edges):
        symmetric_importance = (
            float(importance[first, second] + importance[second, first]) / 2.0
        )
        q_matrix[(edge_index, edge_index)] = -symmetric_importance

    for first in range(len(edges)):
        q_matrix[(first, first)] += self._penalty * (1 - 2 * max_connections)
        for second in range(first + 1, len(edges)):
            q_matrix[(first, second)] = 2 * self._penalty

    return QUBOModel(
        Q=q_matrix,
        offset=self._penalty * max_connections**2,
        n_qubits=len(edges),
        source="sc_pruning",
    )

SimulatedAnnealer

Metropolis simulated annealer with explicit backend selection.

Source code in src/sc_neurocore/bridges/annealing_solvers.py
Python
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
class SimulatedAnnealer:
    """Metropolis simulated annealer with explicit backend selection."""

    def __init__(
        self,
        n_sweeps: int = 1000,
        beta_start: float = 0.1,
        beta_end: float = 10.0,
        seed: int = 42,
        *,
        backend: BackendChoice = "auto",
    ) -> None:
        """Configure a deterministic annealer."""
        self._n_sweeps = _positive_int("n_sweeps", n_sweeps)
        self._beta_start = _positive_float("beta_start", beta_start)
        self._beta_end = _positive_float("beta_end", beta_end)
        if self._beta_end < self._beta_start:
            raise ValueError("beta_end must be greater than or equal to beta_start")
        if isinstance(seed, bool) or not isinstance(seed, int):
            raise ValueError("seed must be an integer")
        self._seed = seed
        self._backend = validate_backend_choice(backend)
        self._rng = np.random.default_rng(seed)

    def solve_ising(self, model: IsingModel, num_reads: int = 10) -> dict[str, Any]:
        """Solve an Ising model and preserve a stable sample contract."""
        if not isinstance(model, IsingModel):
            raise ValueError("model must be an IsingModel")
        if model.n_qubits <= 0:
            raise ValueError("model must contain at least one qubit")
        reads = _positive_int("num_reads", num_reads)
        use_rust = self._backend == "rust" or (
            self._backend == "auto" and backends.HAS_RUST_QA and model.n_qubits > 10
        )
        if use_rust:
            return self._solve_ising_rust(model, reads)
        return self._solve_ising_python(model, reads)

    def _solve_ising_rust(self, model: IsingModel, num_reads: int) -> dict[str, Any]:
        """Execute and validate the native solver result."""
        h_indices = list(model.h)
        j_pairs = list(model.J)
        raw = backends.require_rust_annealer()(
            h_indices,
            [model.h[index] for index in h_indices],
            [pair[0] for pair in j_pairs],
            [pair[1] for pair in j_pairs],
            [model.J[pair] for pair in j_pairs],
            model.n_qubits,
            model.offset,
            self._n_sweeps,
            num_reads,
            self._beta_start,
            self._beta_end,
            self._seed,
        )
        best_vector = _spin_sequence("best_spins", raw.get("best_spins"), model.n_qubits)

        raw_samples = raw.get("samples", [])
        if isinstance(raw_samples, (str, bytes)) or not isinstance(raw_samples, Sequence):
            raise RuntimeError("native solver returned invalid samples")
        samples: list[dict[int, int]] = []
        for sample_index, sample in enumerate(raw_samples):
            vector = _spin_sequence(f"samples[{sample_index}]", sample, model.n_qubits)
            samples.append(dict(enumerate(vector)))

        raw_energies = raw.get("energies", [])
        if isinstance(raw_energies, (str, bytes)) or not isinstance(raw_energies, Sequence):
            raise RuntimeError("native solver returned invalid energies")
        energies = [
            _finite_float(f"energies[{index}]", energy) for index, energy in enumerate(raw_energies)
        ]
        if samples and len(samples) != len(energies):
            raise RuntimeError("native solver returned mismatched samples and energies")

        return {
            "best_spins": dict(enumerate(best_vector)),
            "best_energy": _finite_float("best_energy", raw.get("best_energy")),
            "energies": energies,
            "samples": samples,
            "n_sweeps": self._n_sweeps,
            "num_reads": num_reads,
            "backend": "rust",
        }

    def _solve_ising_python(self, model: IsingModel, num_reads: int) -> dict[str, Any]:
        """Execute the deterministic pure-Python Metropolis solver."""
        best_energy = float("inf")
        best_spins: dict[int, int] = {}
        all_energies: list[float] = []
        all_samples: list[dict[int, int]] = []

        for _ in range(num_reads):
            spins = {index: int(self._rng.choice((-1, 1))) for index in range(model.n_qubits)}
            energy = model.energy(spins, backend="python")
            for sweep in range(self._n_sweeps):
                exponent = sweep / max(self._n_sweeps - 1, 1)
                beta = self._beta_start * (self._beta_end / self._beta_start) ** exponent
                for qubit in range(model.n_qubits):
                    local_field = model.h.get(qubit, 0.0)
                    for (first, second), strength in model.J.items():
                        if first == qubit:
                            local_field += strength * spins.get(second, 1)
                        elif second == qubit:
                            local_field += strength * spins.get(first, 1)
                    delta_energy = -2.0 * spins[qubit] * local_field
                    if delta_energy < 0.0 or self._rng.random() < math.exp(-beta * delta_energy):
                        spins[qubit] *= -1
                        energy += delta_energy

            all_energies.append(energy)
            all_samples.append(dict(spins))
            if energy < best_energy:
                best_energy = energy
                best_spins = dict(spins)

        return {
            "best_spins": best_spins,
            "best_energy": best_energy,
            "energies": all_energies,
            "samples": all_samples,
            "n_sweeps": self._n_sweeps,
            "num_reads": num_reads,
            "backend": "python",
        }

    def solve_qubo(self, model: QUBOModel, num_reads: int = 10) -> dict[str, Any]:
        """Convert a QUBO to Ising, solve it, and map samples back to bits."""
        if not isinstance(model, QUBOModel):
            raise ValueError("model must be a QUBOModel")
        result = self.solve_ising(model.to_ising(), num_reads=num_reads)
        best_bits = {index: (spin + 1) // 2 for index, spin in result["best_spins"].items()}
        bit_samples = [
            {index: (spin + 1) // 2 for index, spin in sample.items()}
            for sample in result["samples"]
        ]
        return {
            "best_bits": best_bits,
            "best_energy": model.energy(best_bits),
            "energies": [model.energy(sample) for sample in bit_samples],
            "samples": bit_samples,
            "n_sweeps": self._n_sweeps,
            "num_reads": num_reads,
            "backend": result["backend"],
        }

__init__(n_sweeps=1000, beta_start=0.1, beta_end=10.0, seed=42, *, backend='auto')

Configure a deterministic annealer.

Source code in src/sc_neurocore/bridges/annealing_solvers.py
Python
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def __init__(
    self,
    n_sweeps: int = 1000,
    beta_start: float = 0.1,
    beta_end: float = 10.0,
    seed: int = 42,
    *,
    backend: BackendChoice = "auto",
) -> None:
    """Configure a deterministic annealer."""
    self._n_sweeps = _positive_int("n_sweeps", n_sweeps)
    self._beta_start = _positive_float("beta_start", beta_start)
    self._beta_end = _positive_float("beta_end", beta_end)
    if self._beta_end < self._beta_start:
        raise ValueError("beta_end must be greater than or equal to beta_start")
    if isinstance(seed, bool) or not isinstance(seed, int):
        raise ValueError("seed must be an integer")
    self._seed = seed
    self._backend = validate_backend_choice(backend)
    self._rng = np.random.default_rng(seed)

solve_ising(model, num_reads=10)

Solve an Ising model and preserve a stable sample contract.

Source code in src/sc_neurocore/bridges/annealing_solvers.py
Python
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def solve_ising(self, model: IsingModel, num_reads: int = 10) -> dict[str, Any]:
    """Solve an Ising model and preserve a stable sample contract."""
    if not isinstance(model, IsingModel):
        raise ValueError("model must be an IsingModel")
    if model.n_qubits <= 0:
        raise ValueError("model must contain at least one qubit")
    reads = _positive_int("num_reads", num_reads)
    use_rust = self._backend == "rust" or (
        self._backend == "auto" and backends.HAS_RUST_QA and model.n_qubits > 10
    )
    if use_rust:
        return self._solve_ising_rust(model, reads)
    return self._solve_ising_python(model, reads)

solve_qubo(model, num_reads=10)

Convert a QUBO to Ising, solve it, and map samples back to bits.

Source code in src/sc_neurocore/bridges/annealing_solvers.py
Python
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def solve_qubo(self, model: QUBOModel, num_reads: int = 10) -> dict[str, Any]:
    """Convert a QUBO to Ising, solve it, and map samples back to bits."""
    if not isinstance(model, QUBOModel):
        raise ValueError("model must be a QUBOModel")
    result = self.solve_ising(model.to_ising(), num_reads=num_reads)
    best_bits = {index: (spin + 1) // 2 for index, spin in result["best_spins"].items()}
    bit_samples = [
        {index: (spin + 1) // 2 for index, spin in sample.items()}
        for sample in result["samples"]
    ]
    return {
        "best_bits": best_bits,
        "best_energy": model.energy(best_bits),
        "energies": [model.energy(sample) for sample in bit_samples],
        "samples": bit_samples,
        "n_sweeps": self._n_sweeps,
        "num_reads": num_reads,
        "backend": result["backend"],
    }

DWaveInterface

Submit validated Ising models to D-Wave or use a local fallback.

Source code in src/sc_neurocore/bridges/annealing_solvers.py
Python
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
class DWaveInterface:
    """Submit validated Ising models to D-Wave or use a local fallback."""

    def __init__(
        self,
        chain_strength: float = _DEFAULT_CHAIN_STRENGTH,
        num_reads: int = _DEFAULT_NUM_READS,
        annealing_time_us: float = _DEFAULT_ANNEALING_TIME_US,
    ) -> None:
        """Configure QPU sampling parameters."""
        self._chain_strength = _positive_float("chain_strength", chain_strength)
        self._num_reads = _positive_int("num_reads", num_reads)
        self._annealing_time_us = _positive_float("annealing_time_us", annealing_time_us)

    @property
    def available(self) -> bool:
        """Return whether both Ocean SDK components are importable."""
        return backends.HAS_DWAVE and backends.HAS_DIMOD

    def solve_ising(self, model: IsingModel) -> dict[str, Any]:
        """Submit to a QPU, or run a bounded local fallback when unavailable."""
        if not isinstance(model, IsingModel) or model.n_qubits <= 0:
            raise ValueError("model must be a non-empty IsingModel")
        if not self.available:
            result = SimulatedAnnealer().solve_ising(model, num_reads=min(self._num_reads, 20))
            result["backend"] = "simulated_annealing_fallback"
            return result

        dimod_module, sampler_type, composite_type = backends.require_dwave_components()
        bqm = dimod_module.BinaryQuadraticModel(model.h, model.J, model.offset, "SPIN")
        sampler = composite_type(sampler_type())
        response = sampler.sample(
            bqm,
            num_reads=self._num_reads,
            chain_strength=self._chain_strength,
            annealing_time=self._annealing_time_us,
        )
        best = getattr(response, "first", None)
        sample = getattr(best, "sample", None)
        energy = getattr(best, "energy", None)
        if not isinstance(sample, Mapping):
            raise RuntimeError("D-Wave response did not contain a best sample")
        best_spins = {int(index): int(spin) for index, spin in sample.items()}
        model.energy(best_spins, backend="python")
        info = getattr(response, "info", {})
        timing = info.get("timing", {}) if isinstance(info, Mapping) else {}
        return {
            "best_spins": best_spins,
            "best_energy": _finite_float("best_energy", energy),
            "num_reads": self._num_reads,
            "backend": "dwave_qpu",
            "timing": timing,
        }

available property

Return whether both Ocean SDK components are importable.

__init__(chain_strength=_DEFAULT_CHAIN_STRENGTH, num_reads=_DEFAULT_NUM_READS, annealing_time_us=_DEFAULT_ANNEALING_TIME_US)

Configure QPU sampling parameters.

Source code in src/sc_neurocore/bridges/annealing_solvers.py
Python
218
219
220
221
222
223
224
225
226
227
def __init__(
    self,
    chain_strength: float = _DEFAULT_CHAIN_STRENGTH,
    num_reads: int = _DEFAULT_NUM_READS,
    annealing_time_us: float = _DEFAULT_ANNEALING_TIME_US,
) -> None:
    """Configure QPU sampling parameters."""
    self._chain_strength = _positive_float("chain_strength", chain_strength)
    self._num_reads = _positive_int("num_reads", num_reads)
    self._annealing_time_us = _positive_float("annealing_time_us", annealing_time_us)

solve_ising(model)

Submit to a QPU, or run a bounded local fallback when unavailable.

Source code in src/sc_neurocore/bridges/annealing_solvers.py
Python
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
def solve_ising(self, model: IsingModel) -> dict[str, Any]:
    """Submit to a QPU, or run a bounded local fallback when unavailable."""
    if not isinstance(model, IsingModel) or model.n_qubits <= 0:
        raise ValueError("model must be a non-empty IsingModel")
    if not self.available:
        result = SimulatedAnnealer().solve_ising(model, num_reads=min(self._num_reads, 20))
        result["backend"] = "simulated_annealing_fallback"
        return result

    dimod_module, sampler_type, composite_type = backends.require_dwave_components()
    bqm = dimod_module.BinaryQuadraticModel(model.h, model.J, model.offset, "SPIN")
    sampler = composite_type(sampler_type())
    response = sampler.sample(
        bqm,
        num_reads=self._num_reads,
        chain_strength=self._chain_strength,
        annealing_time=self._annealing_time_us,
    )
    best = getattr(response, "first", None)
    sample = getattr(best, "sample", None)
    energy = getattr(best, "energy", None)
    if not isinstance(sample, Mapping):
        raise RuntimeError("D-Wave response did not contain a best sample")
    best_spins = {int(index): int(spin) for index, spin in sample.items()}
    model.energy(best_spins, backend="python")
    info = getattr(response, "info", {})
    timing = info.get("timing", {}) if isinstance(info, Mapping) else {}
    return {
        "best_spins": best_spins,
        "best_energy": _finite_float("best_energy", energy),
        "num_reads": self._num_reads,
        "backend": "dwave_qpu",
        "timing": timing,
    }

EnergyLandscape

Compute energy statistics and spectral gaps for an Ising model.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
class EnergyLandscape:
    """Compute energy statistics and spectral gaps for an Ising model."""

    def __init__(
        self,
        *,
        backend: BackendChoice = "auto",
        random_sample_count: int = 10_000,
        seed: int = 42,
    ) -> None:
        """Configure deterministic large-model sampling and backend choice."""
        self._backend = validate_backend_choice(backend)
        if (
            isinstance(random_sample_count, bool)
            or not isinstance(random_sample_count, int)
            or random_sample_count <= 0
        ):
            raise ValueError("random_sample_count must be a positive integer")
        if isinstance(seed, bool) or not isinstance(seed, int):
            raise ValueError("seed must be an integer")
        self._random_sample_count = random_sample_count
        self._seed = seed

    def analyze(
        self,
        model: IsingModel,
        samples: Sequence[Mapping[int, int]] | None = None,
    ) -> dict[str, Any]:
        """Analyze exhaustive, supplied, or deterministic random samples."""
        if not isinstance(model, IsingModel) or model.n_qubits <= 0:
            raise ValueError("model must be a non-empty IsingModel")
        if samples is None:
            if model.n_qubits <= 20:
                normalized_samples = self._enumerate_all(model.n_qubits)
            else:
                rng = np.random.default_rng(self._seed)
                normalized_samples = [
                    {index: int(rng.choice((-1, 1))) for index in range(model.n_qubits)}
                    for _ in range(self._random_sample_count)
                ]
        else:
            if isinstance(samples, (str, bytes)):
                raise ValueError("samples must be a sequence of spin mappings")
            normalized_samples = [_validated_sample(sample) for sample in samples]
            if not normalized_samples:
                raise ValueError("samples must not be empty")

        use_rust = self._backend == "rust" or (
            self._backend == "auto" and backends.HAS_RUST_QA and len(normalized_samples) > 100
        )
        if use_rust:
            h_indices = list(model.h)
            j_pairs = list(model.J)
            raw_energies = backends.require_rust_batch_energy()(
                h_indices,
                [model.h[index] for index in h_indices],
                [pair[0] for pair in j_pairs],
                [pair[1] for pair in j_pairs],
                [model.J[pair] for pair in j_pairs],
                [
                    [sample.get(index, 1) for index in range(model.n_qubits)]
                    for sample in normalized_samples
                ],
                model.offset,
            )
            energies = [
                _finite(f"energies[{index}]", value) for index, value in enumerate(raw_energies)
            ]
            if len(energies) != len(normalized_samples):
                raise RuntimeError("native batch backend returned the wrong energy count")
        else:
            energies = [model.energy(sample, backend="python") for sample in normalized_samples]

        unique_energies = sorted(set(energies))
        minimum = unique_energies[0]
        spectral_gap = unique_energies[1] - unique_energies[0] if len(unique_energies) > 1 else 0.0
        return {
            "min_energy": minimum,
            "max_energy": max(energies),
            "mean_energy": float(np.mean(energies)),
            "std_energy": float(np.std(energies)),
            "spectral_gap": spectral_gap,
            "degeneracy": energies.count(minimum),
            "n_unique_energies": len(unique_energies),
            "n_samples": len(normalized_samples),
        }

    @staticmethod
    def _enumerate_all(n_qubits: int) -> list[dict[int, int]]:
        """Enumerate every configuration for at most 20 qubits."""
        if isinstance(n_qubits, bool) or not isinstance(n_qubits, int) or not 0 <= n_qubits <= 20:
            raise ValueError("n_qubits must be an integer between zero and 20")
        return [
            {index: 1 if (bits >> index) & 1 else -1 for index in range(n_qubits)}
            for bits in range(2**n_qubits)
        ]

__init__(*, backend='auto', random_sample_count=10000, seed=42)

Configure deterministic large-model sampling and backend choice.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def __init__(
    self,
    *,
    backend: BackendChoice = "auto",
    random_sample_count: int = 10_000,
    seed: int = 42,
) -> None:
    """Configure deterministic large-model sampling and backend choice."""
    self._backend = validate_backend_choice(backend)
    if (
        isinstance(random_sample_count, bool)
        or not isinstance(random_sample_count, int)
        or random_sample_count <= 0
    ):
        raise ValueError("random_sample_count must be a positive integer")
    if isinstance(seed, bool) or not isinstance(seed, int):
        raise ValueError("seed must be an integer")
    self._random_sample_count = random_sample_count
    self._seed = seed

analyze(model, samples=None)

Analyze exhaustive, supplied, or deterministic random samples.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def analyze(
    self,
    model: IsingModel,
    samples: Sequence[Mapping[int, int]] | None = None,
) -> dict[str, Any]:
    """Analyze exhaustive, supplied, or deterministic random samples."""
    if not isinstance(model, IsingModel) or model.n_qubits <= 0:
        raise ValueError("model must be a non-empty IsingModel")
    if samples is None:
        if model.n_qubits <= 20:
            normalized_samples = self._enumerate_all(model.n_qubits)
        else:
            rng = np.random.default_rng(self._seed)
            normalized_samples = [
                {index: int(rng.choice((-1, 1))) for index in range(model.n_qubits)}
                for _ in range(self._random_sample_count)
            ]
    else:
        if isinstance(samples, (str, bytes)):
            raise ValueError("samples must be a sequence of spin mappings")
        normalized_samples = [_validated_sample(sample) for sample in samples]
        if not normalized_samples:
            raise ValueError("samples must not be empty")

    use_rust = self._backend == "rust" or (
        self._backend == "auto" and backends.HAS_RUST_QA and len(normalized_samples) > 100
    )
    if use_rust:
        h_indices = list(model.h)
        j_pairs = list(model.J)
        raw_energies = backends.require_rust_batch_energy()(
            h_indices,
            [model.h[index] for index in h_indices],
            [pair[0] for pair in j_pairs],
            [pair[1] for pair in j_pairs],
            [model.J[pair] for pair in j_pairs],
            [
                [sample.get(index, 1) for index in range(model.n_qubits)]
                for sample in normalized_samples
            ],
            model.offset,
        )
        energies = [
            _finite(f"energies[{index}]", value) for index, value in enumerate(raw_energies)
        ]
        if len(energies) != len(normalized_samples):
            raise RuntimeError("native batch backend returned the wrong energy count")
    else:
        energies = [model.energy(sample, backend="python") for sample in normalized_samples]

    unique_energies = sorted(set(energies))
    minimum = unique_energies[0]
    spectral_gap = unique_energies[1] - unique_energies[0] if len(unique_energies) > 1 else 0.0
    return {
        "min_energy": minimum,
        "max_energy": max(energies),
        "mean_energy": float(np.mean(energies)),
        "std_energy": float(np.std(energies)),
        "spectral_gap": spectral_gap,
        "degeneracy": energies.count(minimum),
        "n_unique_energies": len(unique_energies),
        "n_samples": len(normalized_samples),
    }

EmbeddingAnalyzer

Estimate logical-to-physical embedding requirements.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
class EmbeddingAnalyzer:
    """Estimate logical-to-physical embedding requirements."""

    def analyze(self, model: IsingModel) -> dict[str, Any]:
        """Return graph density, degree, and Pegasus chain estimates."""
        if not isinstance(model, IsingModel) or model.n_qubits <= 0:
            raise ValueError("model must be a non-empty IsingModel")
        size = model.n_qubits
        coupling_count = len(model.J)
        max_possible = size * (size - 1) // 2
        degree = {index: 0 for index in range(size)}
        for first, second in model.J:
            degree[first] += 1
            degree[second] += 1
        max_degree = max(degree.values())
        mean_degree = sum(degree.values()) / size
        chain_length = max(1, math.ceil(max_degree / 15))
        estimated_physical = size * chain_length
        return {
            "n_logical_qubits": size,
            "n_couplers": coupling_count,
            "density": coupling_count / max(max_possible, 1),
            "max_degree": max_degree,
            "mean_degree": float(mean_degree),
            "min_chain_estimate": chain_length,
            "estimated_physical_qubits": estimated_physical,
            "pegasus_compatible": estimated_physical <= 5000,
        }

analyze(model)

Return graph density, degree, and Pegasus chain estimates.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def analyze(self, model: IsingModel) -> dict[str, Any]:
    """Return graph density, degree, and Pegasus chain estimates."""
    if not isinstance(model, IsingModel) or model.n_qubits <= 0:
        raise ValueError("model must be a non-empty IsingModel")
    size = model.n_qubits
    coupling_count = len(model.J)
    max_possible = size * (size - 1) // 2
    degree = {index: 0 for index in range(size)}
    for first, second in model.J:
        degree[first] += 1
        degree[second] += 1
    max_degree = max(degree.values())
    mean_degree = sum(degree.values()) / size
    chain_length = max(1, math.ceil(max_degree / 15))
    estimated_physical = size * chain_length
    return {
        "n_logical_qubits": size,
        "n_couplers": coupling_count,
        "density": coupling_count / max(max_possible, 1),
        "max_degree": max_degree,
        "mean_degree": float(mean_degree),
        "min_chain_estimate": chain_length,
        "estimated_physical_qubits": estimated_physical,
        "pegasus_compatible": estimated_physical <= 5000,
    }

SampleAggregator

Deduplicate samples and compute energy-distribution statistics.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
class SampleAggregator:
    """Deduplicate samples and compute energy-distribution statistics."""

    def aggregate(
        self,
        samples: Sequence[Mapping[int, int]],
        energies: Sequence[float],
        temperature: float = 1.0,
    ) -> dict[str, Any]:
        """Aggregate an aligned sample and energy sequence."""
        if isinstance(samples, (str, bytes)) or isinstance(energies, (str, bytes)):
            raise ValueError("samples and energies must be sequences")
        if len(samples) != len(energies):
            raise ValueError("samples and energies must have equal lengths")
        if not samples:
            return {"unique_samples": 0, "best": {}, "histogram": {}}
        thermal = _finite("temperature", temperature)
        if thermal <= 0.0:
            raise ValueError("temperature must be greater than zero")

        normalized_samples = [_validated_sample(sample, spins_only=False) for sample in samples]
        normalized_energies = [
            _finite(f"energies[{index}]", value) for index, value in enumerate(energies)
        ]
        paired = sorted(zip(normalized_energies, normalized_samples), key=lambda item: item[0])
        best_energy, best_sample = paired[0]
        unique_samples = len({tuple(sorted(sample.items())) for _, sample in paired})

        energy_array = np.asarray(normalized_energies, dtype=np.float64)
        bin_count = max(min(20, len(set(normalized_energies))), 1)
        counts, bin_edges = np.histogram(energy_array, bins=bin_count)
        shifted_weights = np.exp(-(energy_array - min(normalized_energies)) / thermal)
        partition = float(np.sum(shifted_weights))
        ground_count = sum(1 for energy in normalized_energies if abs(energy - best_energy) < 1e-10)
        return {
            "unique_samples": unique_samples,
            "total_samples": len(normalized_samples),
            "best_sample": best_sample,
            "best_energy": best_energy,
            "mean_energy": float(np.mean(energy_array)),
            "std_energy": float(np.std(energy_array)),
            "boltzmann_avg_energy": float(np.sum(shifted_weights * energy_array) / partition),
            "success_probability": ground_count / len(normalized_energies),
            "gs_degeneracy": ground_count,
            "histogram": {
                "counts": counts.tolist(),
                "bin_edges": bin_edges.tolist(),
            },
        }

aggregate(samples, energies, temperature=1.0)

Aggregate an aligned sample and energy sequence.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def aggregate(
    self,
    samples: Sequence[Mapping[int, int]],
    energies: Sequence[float],
    temperature: float = 1.0,
) -> dict[str, Any]:
    """Aggregate an aligned sample and energy sequence."""
    if isinstance(samples, (str, bytes)) or isinstance(energies, (str, bytes)):
        raise ValueError("samples and energies must be sequences")
    if len(samples) != len(energies):
        raise ValueError("samples and energies must have equal lengths")
    if not samples:
        return {"unique_samples": 0, "best": {}, "histogram": {}}
    thermal = _finite("temperature", temperature)
    if thermal <= 0.0:
        raise ValueError("temperature must be greater than zero")

    normalized_samples = [_validated_sample(sample, spins_only=False) for sample in samples]
    normalized_energies = [
        _finite(f"energies[{index}]", value) for index, value in enumerate(energies)
    ]
    paired = sorted(zip(normalized_energies, normalized_samples), key=lambda item: item[0])
    best_energy, best_sample = paired[0]
    unique_samples = len({tuple(sorted(sample.items())) for _, sample in paired})

    energy_array = np.asarray(normalized_energies, dtype=np.float64)
    bin_count = max(min(20, len(set(normalized_energies))), 1)
    counts, bin_edges = np.histogram(energy_array, bins=bin_count)
    shifted_weights = np.exp(-(energy_array - min(normalized_energies)) / thermal)
    partition = float(np.sum(shifted_weights))
    ground_count = sum(1 for energy in normalized_energies if abs(energy - best_energy) < 1e-10)
    return {
        "unique_samples": unique_samples,
        "total_samples": len(normalized_samples),
        "best_sample": best_sample,
        "best_energy": best_energy,
        "mean_energy": float(np.mean(energy_array)),
        "std_energy": float(np.std(energy_array)),
        "boltzmann_avg_energy": float(np.sum(shifted_weights * energy_array) / partition),
        "success_probability": ground_count / len(normalized_energies),
        "gs_degeneracy": ground_count,
        "histogram": {
            "counts": counts.tolist(),
            "bin_edges": bin_edges.tolist(),
        },
    }

TTSAnalyzer

Compute time-to-solution from single-run success probability.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
class TTSAnalyzer:
    """Compute time-to-solution from single-run success probability."""

    def compute(
        self,
        p_success: float,
        t_anneal_us: float,
        p_target: float = 0.99,
    ) -> dict[str, float]:
        """Compute the standard cumulative-success TTS metric."""
        success = _finite("p_success", p_success)
        target = _finite("p_target", p_target)
        anneal_time = _finite("t_anneal_us", t_anneal_us)
        if not 0.0 <= success <= 1.0:
            raise ValueError("p_success must be between zero and one")
        if not 0.0 < target < 1.0:
            raise ValueError("p_target must be strictly between zero and one")
        if anneal_time <= 0.0:
            raise ValueError("t_anneal_us must be greater than zero")
        if success == 0.0:
            return {
                "tts_us": float("inf"),
                "tts_ms": float("inf"),
                "n_runs_needed": float("inf"),
                "p_success": 0.0,
                "p_target": target,
            }
        if success == 1.0:
            return {
                "tts_us": anneal_time,
                "tts_ms": anneal_time / 1000.0,
                "n_runs_needed": 1.0,
                "p_success": 1.0,
                "p_target": target,
            }

        run_count = math.log1p(-target) / math.log1p(-success)
        tts = anneal_time * run_count
        return {
            "tts_us": tts,
            "tts_ms": tts / 1000.0,
            "n_runs_needed": run_count,
            "p_success": success,
            "p_target": target,
        }

    def from_samples(
        self,
        energies: Sequence[float],
        ground_state_energy: float,
        t_anneal_us: float = 20.0,
        tolerance: float = 1e-6,
        p_target: float = 0.99,
    ) -> dict[str, float]:
        """Estimate single-run success from observed energies."""
        if isinstance(energies, (str, bytes)):
            raise ValueError("energies must be a sequence")
        ground = _finite("ground_state_energy", ground_state_energy)
        threshold = _finite("tolerance", tolerance)
        if threshold <= 0.0:
            raise ValueError("tolerance must be greater than zero")
        normalized = [
            _finite(f"energies[{index}]", energy) for index, energy in enumerate(energies)
        ]
        ground_count = sum(1 for energy in normalized if abs(energy - ground) < threshold)
        success = ground_count / len(normalized) if normalized else 0.0
        return self.compute(success, t_anneal_us, p_target)

    def compare_solvers(
        self,
        results: Mapping[str, Mapping[str, Any]],
        ground_state_energy: float,
        tolerance: float = 1e-6,
    ) -> dict[str, dict[str, float]]:
        """Compute comparable TTS rows for named solver outputs."""
        comparison: dict[str, dict[str, float]] = {}
        for name, data in results.items():
            if not isinstance(name, str) or not name:
                raise ValueError("solver names must be non-empty strings")
            raw_energies = data.get("energies")
            if isinstance(raw_energies, (str, bytes)) or not isinstance(raw_energies, Sequence):
                raise ValueError(f"solver {name!r} must provide an energy sequence")
            comparison[name] = self.from_samples(
                raw_energies,
                ground_state_energy,
                t_anneal_us=_finite("t_anneal_us", data.get("t_anneal_us", 20.0)),
                tolerance=tolerance,
            )
        return comparison

compute(p_success, t_anneal_us, p_target=0.99)

Compute the standard cumulative-success TTS metric.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def compute(
    self,
    p_success: float,
    t_anneal_us: float,
    p_target: float = 0.99,
) -> dict[str, float]:
    """Compute the standard cumulative-success TTS metric."""
    success = _finite("p_success", p_success)
    target = _finite("p_target", p_target)
    anneal_time = _finite("t_anneal_us", t_anneal_us)
    if not 0.0 <= success <= 1.0:
        raise ValueError("p_success must be between zero and one")
    if not 0.0 < target < 1.0:
        raise ValueError("p_target must be strictly between zero and one")
    if anneal_time <= 0.0:
        raise ValueError("t_anneal_us must be greater than zero")
    if success == 0.0:
        return {
            "tts_us": float("inf"),
            "tts_ms": float("inf"),
            "n_runs_needed": float("inf"),
            "p_success": 0.0,
            "p_target": target,
        }
    if success == 1.0:
        return {
            "tts_us": anneal_time,
            "tts_ms": anneal_time / 1000.0,
            "n_runs_needed": 1.0,
            "p_success": 1.0,
            "p_target": target,
        }

    run_count = math.log1p(-target) / math.log1p(-success)
    tts = anneal_time * run_count
    return {
        "tts_us": tts,
        "tts_ms": tts / 1000.0,
        "n_runs_needed": run_count,
        "p_success": success,
        "p_target": target,
    }

from_samples(energies, ground_state_energy, t_anneal_us=20.0, tolerance=1e-06, p_target=0.99)

Estimate single-run success from observed energies.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
def from_samples(
    self,
    energies: Sequence[float],
    ground_state_energy: float,
    t_anneal_us: float = 20.0,
    tolerance: float = 1e-6,
    p_target: float = 0.99,
) -> dict[str, float]:
    """Estimate single-run success from observed energies."""
    if isinstance(energies, (str, bytes)):
        raise ValueError("energies must be a sequence")
    ground = _finite("ground_state_energy", ground_state_energy)
    threshold = _finite("tolerance", tolerance)
    if threshold <= 0.0:
        raise ValueError("tolerance must be greater than zero")
    normalized = [
        _finite(f"energies[{index}]", energy) for index, energy in enumerate(energies)
    ]
    ground_count = sum(1 for energy in normalized if abs(energy - ground) < threshold)
    success = ground_count / len(normalized) if normalized else 0.0
    return self.compute(success, t_anneal_us, p_target)

compare_solvers(results, ground_state_energy, tolerance=1e-06)

Compute comparable TTS rows for named solver outputs.

Source code in src/sc_neurocore/bridges/annealing_analysis.py
Python
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
def compare_solvers(
    self,
    results: Mapping[str, Mapping[str, Any]],
    ground_state_energy: float,
    tolerance: float = 1e-6,
) -> dict[str, dict[str, float]]:
    """Compute comparable TTS rows for named solver outputs."""
    comparison: dict[str, dict[str, float]] = {}
    for name, data in results.items():
        if not isinstance(name, str) or not name:
            raise ValueError("solver names must be non-empty strings")
        raw_energies = data.get("energies")
        if isinstance(raw_energies, (str, bytes)) or not isinstance(raw_energies, Sequence):
            raise ValueError(f"solver {name!r} must provide an energy sequence")
        comparison[name] = self.from_samples(
            raw_energies,
            ground_state_energy,
            t_anneal_us=_finite("t_anneal_us", data.get("t_anneal_us", 20.0)),
            tolerance=tolerance,
        )
    return comparison

HardwareGraph

Capacity model for Chimera, Pegasus, and Zephyr topologies.

Source code in src/sc_neurocore/bridges/annealing_hardware.py
Python
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class HardwareGraph:
    """Capacity model for Chimera, Pegasus, and Zephyr topologies."""

    _TOPOLOGIES: ClassVar[dict[str, dict[str, int]]] = {
        "chimera": {"connectivity": 6, "base_qubits_per_cell": 8},
        "pegasus": {"connectivity": 15, "base_qubits_per_cell": 24},
        "zephyr": {"connectivity": 20, "base_qubits_per_cell": 48},
    }

    def __init__(self, topology: str = "pegasus", size: int = 16) -> None:
        """Select a topology and a valid positive size parameter."""
        if topology not in self._TOPOLOGIES:
            raise ValueError(f"Unknown topology: {topology}")
        if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
            raise ValueError("size must be a positive integer")
        if topology == "pegasus" and size < 2:
            raise ValueError("Pegasus size must be at least two")
        self._topology = topology
        self._size = size
        self._props = self._TOPOLOGIES[topology]

    @property
    def n_physical_qubits(self) -> int:
        """Return the idealized physical-qubit capacity."""
        if self._topology == "chimera":
            return self._size * self._size * 8
        if self._topology == "pegasus":
            return 24 * self._size * (self._size - 1)
        return 48 * self._size * self._size

    @property
    def connectivity(self) -> int:
        """Return the idealized per-qubit connectivity."""
        return self._props["connectivity"]

    def can_embed(self, model: IsingModel) -> dict[str, Any]:
        """Return a conservative degree-based capacity estimate."""
        if not isinstance(model, IsingModel) or model.n_qubits <= 0:
            raise ValueError("model must be a non-empty IsingModel")
        degree = {index: 0 for index in range(model.n_qubits)}
        for first, second in model.J:
            degree[first] += 1
            degree[second] += 1
        max_degree = max(degree.values())
        chain_length = max(1, math.ceil(max_degree / self.connectivity))
        physical_needed = model.n_qubits * chain_length
        return {
            "embeddable": physical_needed <= self.n_physical_qubits,
            "topology": self._topology,
            "size": self._size,
            "n_logical": model.n_qubits,
            "n_couplers": len(model.J),
            "max_degree": max_degree,
            "chain_length_estimate": chain_length,
            "n_physical_available": self.n_physical_qubits,
            "estimated_physical_needed": physical_needed,
            "utilization_pct": physical_needed / self.n_physical_qubits * 100.0,
        }

n_physical_qubits property

Return the idealized physical-qubit capacity.

connectivity property

Return the idealized per-qubit connectivity.

__init__(topology='pegasus', size=16)

Select a topology and a valid positive size parameter.

Source code in src/sc_neurocore/bridges/annealing_hardware.py
Python
73
74
75
76
77
78
79
80
81
82
83
def __init__(self, topology: str = "pegasus", size: int = 16) -> None:
    """Select a topology and a valid positive size parameter."""
    if topology not in self._TOPOLOGIES:
        raise ValueError(f"Unknown topology: {topology}")
    if isinstance(size, bool) or not isinstance(size, int) or size <= 0:
        raise ValueError("size must be a positive integer")
    if topology == "pegasus" and size < 2:
        raise ValueError("Pegasus size must be at least two")
    self._topology = topology
    self._size = size
    self._props = self._TOPOLOGIES[topology]

can_embed(model)

Return a conservative degree-based capacity estimate.

Source code in src/sc_neurocore/bridges/annealing_hardware.py
Python
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def can_embed(self, model: IsingModel) -> dict[str, Any]:
    """Return a conservative degree-based capacity estimate."""
    if not isinstance(model, IsingModel) or model.n_qubits <= 0:
        raise ValueError("model must be a non-empty IsingModel")
    degree = {index: 0 for index in range(model.n_qubits)}
    for first, second in model.J:
        degree[first] += 1
        degree[second] += 1
    max_degree = max(degree.values())
    chain_length = max(1, math.ceil(max_degree / self.connectivity))
    physical_needed = model.n_qubits * chain_length
    return {
        "embeddable": physical_needed <= self.n_physical_qubits,
        "topology": self._topology,
        "size": self._size,
        "n_logical": model.n_qubits,
        "n_couplers": len(model.J),
        "max_degree": max_degree,
        "chain_length_estimate": chain_length,
        "n_physical_available": self.n_physical_qubits,
        "estimated_physical_needed": physical_needed,
        "utilization_pct": physical_needed / self.n_physical_qubits * 100.0,
    }

ChainBreakResolver

Resolve embedded-chain disagreement by vote or local energy search.

Source code in src/sc_neurocore/bridges/annealing_hardware.py
Python
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
class ChainBreakResolver:
    """Resolve embedded-chain disagreement by vote or local energy search."""

    def __init__(self, method: str = "majority_vote") -> None:
        """Select a supported deterministic resolution method."""
        if method not in {"majority_vote", "minimize_energy"}:
            raise ValueError(f"Unknown method: {method}")
        self._method = method

    def resolve(
        self,
        physical_samples: Sequence[Mapping[int, int]],
        chains: Mapping[int, Sequence[int]],
        model: IsingModel | None = None,
    ) -> list[dict[int, int]]:
        """Map physical samples to logical samples and optionally refine them."""
        normalized_samples = _validated_physical_samples(physical_samples)
        normalized_chains = _validated_chains(chains)
        if self._method == "minimize_energy" and model is None:
            raise ValueError("model is required for minimize_energy resolution")
        if model is not None:
            if not isinstance(model, IsingModel):
                raise ValueError("model must be an IsingModel")
            if any(logical >= model.n_qubits for logical in normalized_chains):
                raise ValueError("chain logical indices must fit within model.n_qubits")

        resolved: list[dict[int, int]] = []
        for sample in normalized_samples:
            logical = {
                logical_index: (
                    1
                    if sum(sample.get(physical_index, 1) for physical_index in physical) >= 0
                    else -1
                )
                for logical_index, physical in normalized_chains.items()
            }
            if self._method == "minimize_energy" and model is not None:
                energy = model.energy(logical, backend="python")
                for logical_index in logical:
                    candidate = dict(logical)
                    candidate[logical_index] *= -1
                    candidate_energy = model.energy(candidate, backend="python")
                    if candidate_energy < energy:
                        logical[logical_index] *= -1
                        energy = candidate_energy
            resolved.append(logical)
        return resolved

    def analyze_breaks(
        self,
        physical_samples: Sequence[Mapping[int, int]],
        chains: Mapping[int, Sequence[int]],
    ) -> dict[str, Any]:
        """Measure per-chain and aggregate break rates."""
        normalized_samples = _validated_physical_samples(physical_samples)
        normalized_chains = _validated_chains(chains)
        total_breaks = 0
        breakable_chain_count = 0
        per_chain: dict[int, float] = {}
        for logical_index, physical in normalized_chains.items():
            if len(physical) == 1:
                per_chain[logical_index] = 0.0
                continue
            breaks = sum(
                1
                for sample in normalized_samples
                if len({sample.get(index, 1) for index in physical}) > 1
            )
            per_chain[logical_index] = (
                breaks / len(normalized_samples) if normalized_samples else 0.0
            )
            total_breaks += breaks
            breakable_chain_count += 1
        opportunity_count = breakable_chain_count * len(normalized_samples)
        return {
            "total_breaks": total_breaks,
            "break_rate": total_breaks / opportunity_count if opportunity_count else 0.0,
            "per_chain": per_chain,
            "n_chains": len(normalized_chains),
        }

__init__(method='majority_vote')

Select a supported deterministic resolution method.

Source code in src/sc_neurocore/bridges/annealing_hardware.py
Python
127
128
129
130
131
def __init__(self, method: str = "majority_vote") -> None:
    """Select a supported deterministic resolution method."""
    if method not in {"majority_vote", "minimize_energy"}:
        raise ValueError(f"Unknown method: {method}")
    self._method = method

resolve(physical_samples, chains, model=None)

Map physical samples to logical samples and optionally refine them.

Source code in src/sc_neurocore/bridges/annealing_hardware.py
Python
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def resolve(
    self,
    physical_samples: Sequence[Mapping[int, int]],
    chains: Mapping[int, Sequence[int]],
    model: IsingModel | None = None,
) -> list[dict[int, int]]:
    """Map physical samples to logical samples and optionally refine them."""
    normalized_samples = _validated_physical_samples(physical_samples)
    normalized_chains = _validated_chains(chains)
    if self._method == "minimize_energy" and model is None:
        raise ValueError("model is required for minimize_energy resolution")
    if model is not None:
        if not isinstance(model, IsingModel):
            raise ValueError("model must be an IsingModel")
        if any(logical >= model.n_qubits for logical in normalized_chains):
            raise ValueError("chain logical indices must fit within model.n_qubits")

    resolved: list[dict[int, int]] = []
    for sample in normalized_samples:
        logical = {
            logical_index: (
                1
                if sum(sample.get(physical_index, 1) for physical_index in physical) >= 0
                else -1
            )
            for logical_index, physical in normalized_chains.items()
        }
        if self._method == "minimize_energy" and model is not None:
            energy = model.energy(logical, backend="python")
            for logical_index in logical:
                candidate = dict(logical)
                candidate[logical_index] *= -1
                candidate_energy = model.energy(candidate, backend="python")
                if candidate_energy < energy:
                    logical[logical_index] *= -1
                    energy = candidate_energy
        resolved.append(logical)
    return resolved

analyze_breaks(physical_samples, chains)

Measure per-chain and aggregate break rates.

Source code in src/sc_neurocore/bridges/annealing_hardware.py
Python
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def analyze_breaks(
    self,
    physical_samples: Sequence[Mapping[int, int]],
    chains: Mapping[int, Sequence[int]],
) -> dict[str, Any]:
    """Measure per-chain and aggregate break rates."""
    normalized_samples = _validated_physical_samples(physical_samples)
    normalized_chains = _validated_chains(chains)
    total_breaks = 0
    breakable_chain_count = 0
    per_chain: dict[int, float] = {}
    for logical_index, physical in normalized_chains.items():
        if len(physical) == 1:
            per_chain[logical_index] = 0.0
            continue
        breaks = sum(
            1
            for sample in normalized_samples
            if len({sample.get(index, 1) for index in physical}) > 1
        )
        per_chain[logical_index] = (
            breaks / len(normalized_samples) if normalized_samples else 0.0
        )
        total_breaks += breaks
        breakable_chain_count += 1
    opportunity_count = breakable_chain_count * len(normalized_samples)
    return {
        "total_breaks": total_breaks,
        "break_rate": total_breaks / opportunity_count if opportunity_count else 0.0,
        "per_chain": per_chain,
        "n_chains": len(normalized_chains),
    }

ProblemDecomposer

Partition large Ising graphs into bounded overlapping subproblems.

Source code in src/sc_neurocore/bridges/annealing_decomposition.py
Python
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
class ProblemDecomposer:
    """Partition large Ising graphs into bounded overlapping subproblems."""

    def __init__(
        self,
        max_subproblem_size: int = 64,
        overlap: int = 4,
        n_iterations: int = 10,
    ) -> None:
        """Configure maximum size, real overlap, and merge iterations."""
        self._max_size = _positive_int("max_subproblem_size", max_subproblem_size)
        if isinstance(overlap, bool) or not isinstance(overlap, int) or overlap < 0:
            raise ValueError("overlap must be a non-negative integer")
        if overlap >= self._max_size:
            raise ValueError("overlap must be smaller than max_subproblem_size")
        self._overlap = overlap
        self._n_iterations = _positive_int("n_iterations", n_iterations)

    def _partition_indices(self, model: IsingModel) -> list[list[int]]:
        """Return deterministic graph-aware partitions in global indices."""
        if model.n_qubits <= self._max_size:
            return [list(range(model.n_qubits))]

        neighbors: dict[int, dict[int, float]] = {index: {} for index in range(model.n_qubits)}
        for (first, second), strength in model.J.items():
            magnitude = abs(strength)
            neighbors[first][second] = magnitude
            neighbors[second][first] = magnitude

        remaining = set(range(model.n_qubits))
        assigned: set[int] = set()
        partitions: list[list[int]] = []
        while remaining:
            seed = min(remaining)
            overlap_candidates = sorted(
                (
                    (neighbors[seed].get(index, 0.0), index)
                    for index in assigned
                    if index in neighbors[seed]
                ),
                key=lambda item: (-item[0], item[1]),
            )
            shared = [index for _, index in overlap_candidates[: self._overlap]]
            partition = [*shared, seed]
            remaining.remove(seed)
            assigned.add(seed)

            while len(partition) < self._max_size and remaining:
                scored: list[tuple[float, int]] = []
                for candidate in remaining:
                    score = max(
                        (neighbors[candidate].get(member, 0.0) for member in partition),
                        default=0.0,
                    )
                    if score > 0.0:
                        scored.append((score, candidate))
                next_qubit = (
                    min(remaining)
                    if not scored
                    else min(scored, key=lambda item: (-item[0], item[1]))[1]
                )
                partition.append(next_qubit)
                remaining.remove(next_qubit)
                assigned.add(next_qubit)
            partitions.append(partition)
        return partitions

    @staticmethod
    def _submodel(model: IsingModel, indices: list[int], part_index: int) -> IsingModel:
        """Build one local-indexed model from global indices."""
        local_index = {global_index: index for index, global_index in enumerate(indices)}
        index_set = set(indices)
        couplings: dict[tuple[int, int], float] = {}
        for (first, second), strength in model.J.items():
            if first in index_set and second in index_set:
                local_first = local_index[first]
                local_second = local_index[second]
                couplings[(min(local_first, local_second), max(local_first, local_second))] = (
                    strength
                )
        return IsingModel(
            h={
                local_index[global_index]: model.h.get(global_index, 0.0)
                for global_index in indices
            },
            J=couplings,
            qubit_labels={
                local_index[global_index]: model.qubit_labels.get(global_index, f"q{global_index}")
                for global_index in indices
            },
            n_qubits=len(indices),
            source=f"{model.source}_part{part_index}",
        )

    def decompose(self, model: IsingModel) -> list[IsingModel]:
        """Return bounded submodels; small inputs retain object identity."""
        if not isinstance(model, IsingModel) or model.n_qubits <= 0:
            raise ValueError("model must be a non-empty IsingModel")
        if model.n_qubits <= self._max_size:
            return [model]
        return [
            self._submodel(model, indices, part_index)
            for part_index, indices in enumerate(self._partition_indices(model))
        ]

    def solve_decomposed(
        self,
        model: IsingModel,
        solver: SimulatedAnnealer | None = None,
    ) -> dict[str, Any]:
        """Solve submodels iteratively and reconstruct by exact global index."""
        if not isinstance(model, IsingModel) or model.n_qubits <= 0:
            raise ValueError("model must be a non-empty IsingModel")
        active_solver = solver or SimulatedAnnealer(n_sweeps=1000, seed=42)
        if not isinstance(active_solver, SimulatedAnnealer):
            raise ValueError("solver must be a SimulatedAnnealer")

        partitions = self._partition_indices(model)
        submodels = [
            model
            if len(partitions) == 1 and partitions[0] == list(range(model.n_qubits))
            else self._submodel(model, indices, part_index)
            for part_index, indices in enumerate(partitions)
        ]
        global_spins = {index: 1 for index in range(model.n_qubits)}
        for _ in range(self._n_iterations):
            for indices, submodel in zip(partitions, submodels):
                result = active_solver.solve_ising(submodel, num_reads=5)
                best_spins = result.get("best_spins")
                if not isinstance(best_spins, Mapping):
                    raise RuntimeError("subproblem solver returned no best_spins mapping")
                for local_index, spin in best_spins.items():
                    if (
                        isinstance(local_index, bool)
                        or not isinstance(local_index, int)
                        or not 0 <= local_index < len(indices)
                        or spin not in {-1, 1}
                    ):
                        raise RuntimeError("subproblem solver returned an invalid spin mapping")
                    global_spins[indices[local_index]] = int(spin)

        return {
            "best_spins": global_spins,
            "best_energy": model.energy(global_spins, backend="python"),
            "n_partitions": len(partitions),
            "n_iterations": self._n_iterations,
        }

__init__(max_subproblem_size=64, overlap=4, n_iterations=10)

Configure maximum size, real overlap, and merge iterations.

Source code in src/sc_neurocore/bridges/annealing_decomposition.py
Python
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __init__(
    self,
    max_subproblem_size: int = 64,
    overlap: int = 4,
    n_iterations: int = 10,
) -> None:
    """Configure maximum size, real overlap, and merge iterations."""
    self._max_size = _positive_int("max_subproblem_size", max_subproblem_size)
    if isinstance(overlap, bool) or not isinstance(overlap, int) or overlap < 0:
        raise ValueError("overlap must be a non-negative integer")
    if overlap >= self._max_size:
        raise ValueError("overlap must be smaller than max_subproblem_size")
    self._overlap = overlap
    self._n_iterations = _positive_int("n_iterations", n_iterations)

decompose(model)

Return bounded submodels; small inputs retain object identity.

Source code in src/sc_neurocore/bridges/annealing_decomposition.py
Python
121
122
123
124
125
126
127
128
129
130
def decompose(self, model: IsingModel) -> list[IsingModel]:
    """Return bounded submodels; small inputs retain object identity."""
    if not isinstance(model, IsingModel) or model.n_qubits <= 0:
        raise ValueError("model must be a non-empty IsingModel")
    if model.n_qubits <= self._max_size:
        return [model]
    return [
        self._submodel(model, indices, part_index)
        for part_index, indices in enumerate(self._partition_indices(model))
    ]

solve_decomposed(model, solver=None)

Solve submodels iteratively and reconstruct by exact global index.

Source code in src/sc_neurocore/bridges/annealing_decomposition.py
Python
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def solve_decomposed(
    self,
    model: IsingModel,
    solver: SimulatedAnnealer | None = None,
) -> dict[str, Any]:
    """Solve submodels iteratively and reconstruct by exact global index."""
    if not isinstance(model, IsingModel) or model.n_qubits <= 0:
        raise ValueError("model must be a non-empty IsingModel")
    active_solver = solver or SimulatedAnnealer(n_sweeps=1000, seed=42)
    if not isinstance(active_solver, SimulatedAnnealer):
        raise ValueError("solver must be a SimulatedAnnealer")

    partitions = self._partition_indices(model)
    submodels = [
        model
        if len(partitions) == 1 and partitions[0] == list(range(model.n_qubits))
        else self._submodel(model, indices, part_index)
        for part_index, indices in enumerate(partitions)
    ]
    global_spins = {index: 1 for index in range(model.n_qubits)}
    for _ in range(self._n_iterations):
        for indices, submodel in zip(partitions, submodels):
            result = active_solver.solve_ising(submodel, num_reads=5)
            best_spins = result.get("best_spins")
            if not isinstance(best_spins, Mapping):
                raise RuntimeError("subproblem solver returned no best_spins mapping")
            for local_index, spin in best_spins.items():
                if (
                    isinstance(local_index, bool)
                    or not isinstance(local_index, int)
                    or not 0 <= local_index < len(indices)
                    or spin not in {-1, 1}
                ):
                    raise RuntimeError("subproblem solver returned an invalid spin mapping")
                global_spins[indices[local_index]] = int(spin)

    return {
        "best_spins": global_spins,
        "best_energy": model.energy(global_spins, backend="python"),
        "n_partitions": len(partitions),
        "n_iterations": self._n_iterations,
    }

AnnealingSchedule

Build validated linear, pause, and reverse annealing schedules.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
class AnnealingSchedule:
    """Build validated linear, pause, and reverse annealing schedules."""

    def __init__(self) -> None:
        """Create an empty schedule."""
        self._points: list[tuple[float, float]] = []

    def linear(self, duration_us: float = 20.0) -> AnnealingSchedule:
        """Configure a standard linear anneal from zero to one."""
        duration = _positive_duration("duration_us", duration_us)
        self._points = [(0.0, 0.0), (duration, 1.0)]
        return self

    def pause_and_quench(
        self,
        ramp_time_us: float = 5.0,
        pause_at_s: float = 0.4,
        pause_duration_us: float = 50.0,
        quench_time_us: float = 1.0,
    ) -> AnnealingSchedule:
        """Ramp, hold at an intermediate fraction, then quench."""
        ramp = _positive_duration("ramp_time_us", ramp_time_us)
        pause = _positive_duration("pause_duration_us", pause_duration_us)
        quench = _positive_duration("quench_time_us", quench_time_us)
        fraction = _anneal_fraction("pause_at_s", pause_at_s)
        if fraction in {0.0, 1.0}:
            raise ValueError("pause_at_s must be strictly between zero and one")
        self._points = [
            (0.0, 0.0),
            (ramp, fraction),
            (ramp + pause, fraction),
            (ramp + pause + quench, 1.0),
        ]
        return self

    def reverse(
        self,
        initial_s: float = 1.0,
        reverse_to_s: float = 0.3,
        ramp_time_us: float = 5.0,
        hold_time_us: float = 10.0,
        forward_time_us: float = 5.0,
    ) -> AnnealingSchedule:
        """Configure a reverse anneal followed by a forward return."""
        initial = _anneal_fraction("initial_s", initial_s)
        reverse_to = _anneal_fraction("reverse_to_s", reverse_to_s)
        if reverse_to >= initial:
            raise ValueError("reverse_to_s must be smaller than initial_s")
        ramp = _positive_duration("ramp_time_us", ramp_time_us)
        hold = _positive_duration("hold_time_us", hold_time_us)
        forward = _positive_duration("forward_time_us", forward_time_us)
        self._points = [
            (0.0, initial),
            (ramp, reverse_to),
            (ramp + hold, reverse_to),
            (ramp + hold + forward, 1.0),
        ]
        return self

    @property
    def points(self) -> list[tuple[float, float]]:
        """Return a defensive copy of the schedule points."""
        return list(self._points)

    @property
    def total_time_us(self) -> float:
        """Return zero for an empty schedule or its final timestamp."""
        return self._points[-1][0] if self._points else 0.0

    def to_dict(self) -> dict[str, Any]:
        """Return a D-Wave-compatible schedule payload."""
        return {
            "schedule": list(self._points),
            "total_time_us": self.total_time_us,
            "n_points": len(self._points),
        }

points property

Return a defensive copy of the schedule points.

total_time_us property

Return zero for an empty schedule or its final timestamp.

__init__()

Create an empty schedule.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
49
50
51
def __init__(self) -> None:
    """Create an empty schedule."""
    self._points: list[tuple[float, float]] = []

linear(duration_us=20.0)

Configure a standard linear anneal from zero to one.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
53
54
55
56
57
def linear(self, duration_us: float = 20.0) -> AnnealingSchedule:
    """Configure a standard linear anneal from zero to one."""
    duration = _positive_duration("duration_us", duration_us)
    self._points = [(0.0, 0.0), (duration, 1.0)]
    return self

pause_and_quench(ramp_time_us=5.0, pause_at_s=0.4, pause_duration_us=50.0, quench_time_us=1.0)

Ramp, hold at an intermediate fraction, then quench.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def pause_and_quench(
    self,
    ramp_time_us: float = 5.0,
    pause_at_s: float = 0.4,
    pause_duration_us: float = 50.0,
    quench_time_us: float = 1.0,
) -> AnnealingSchedule:
    """Ramp, hold at an intermediate fraction, then quench."""
    ramp = _positive_duration("ramp_time_us", ramp_time_us)
    pause = _positive_duration("pause_duration_us", pause_duration_us)
    quench = _positive_duration("quench_time_us", quench_time_us)
    fraction = _anneal_fraction("pause_at_s", pause_at_s)
    if fraction in {0.0, 1.0}:
        raise ValueError("pause_at_s must be strictly between zero and one")
    self._points = [
        (0.0, 0.0),
        (ramp, fraction),
        (ramp + pause, fraction),
        (ramp + pause + quench, 1.0),
    ]
    return self

reverse(initial_s=1.0, reverse_to_s=0.3, ramp_time_us=5.0, hold_time_us=10.0, forward_time_us=5.0)

Configure a reverse anneal followed by a forward return.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def reverse(
    self,
    initial_s: float = 1.0,
    reverse_to_s: float = 0.3,
    ramp_time_us: float = 5.0,
    hold_time_us: float = 10.0,
    forward_time_us: float = 5.0,
) -> AnnealingSchedule:
    """Configure a reverse anneal followed by a forward return."""
    initial = _anneal_fraction("initial_s", initial_s)
    reverse_to = _anneal_fraction("reverse_to_s", reverse_to_s)
    if reverse_to >= initial:
        raise ValueError("reverse_to_s must be smaller than initial_s")
    ramp = _positive_duration("ramp_time_us", ramp_time_us)
    hold = _positive_duration("hold_time_us", hold_time_us)
    forward = _positive_duration("forward_time_us", forward_time_us)
    self._points = [
        (0.0, initial),
        (ramp, reverse_to),
        (ramp + hold, reverse_to),
        (ramp + hold + forward, 1.0),
    ]
    return self

to_dict()

Return a D-Wave-compatible schedule payload.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
115
116
117
118
119
120
121
def to_dict(self) -> dict[str, Any]:
    """Return a D-Wave-compatible schedule payload."""
    return {
        "schedule": list(self._points),
        "total_time_us": self.total_time_us,
        "n_points": len(self._points),
    }

GaugeTransform

Generate deterministic random spin-reversal transformations.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
class GaugeTransform:
    """Generate deterministic random spin-reversal transformations."""

    def __init__(self, n_gauges: int = 10, seed: int = 42) -> None:
        """Configure a positive transform count and deterministic seed."""
        if isinstance(n_gauges, bool) or not isinstance(n_gauges, int) or n_gauges <= 0:
            raise ValueError("n_gauges must be a positive integer")
        if isinstance(seed, bool) or not isinstance(seed, int):
            raise ValueError("seed must be an integer")
        self._n_gauges = n_gauges
        self._rng = np.random.default_rng(seed)

    def transform(self, model: IsingModel) -> list[IsingModel]:
        """Return energy-equivalent gauge-transformed model copies."""
        if not isinstance(model, IsingModel) or model.n_qubits <= 0:
            raise ValueError("model must be a non-empty IsingModel")
        transforms: list[IsingModel] = []
        for gauge_index in range(self._n_gauges):
            gauge = {index: int(self._rng.choice((-1, 1))) for index in range(model.n_qubits)}
            transforms.append(
                IsingModel(
                    h={index: gauge[index] * bias for index, bias in model.h.items()},
                    J={
                        pair: gauge[pair[0]] * gauge[pair[1]] * strength
                        for pair, strength in model.J.items()
                    },
                    offset=model.offset,
                    qubit_labels=dict(model.qubit_labels),
                    n_qubits=model.n_qubits,
                    source=f"{model.source}_gauge{gauge_index}",
                )
            )
        return transforms

    def untransform_sample(
        self,
        sample: Mapping[int, int],
        gauge: Mapping[int, int],
    ) -> dict[int, int]:
        """Return a transformed sample to the original spin frame."""
        for mapping_name, mapping in (("sample", sample), ("gauge", gauge)):
            for index, spin in mapping.items():
                if isinstance(index, bool) or not isinstance(index, int) or index < 0:
                    raise ValueError(f"{mapping_name} indices must be non-negative integers")
                if spin not in {-1, 1}:
                    raise ValueError(f"{mapping_name} values must be -1 or +1")
        return {index: spin * gauge.get(index, 1) for index, spin in sample.items()}

__init__(n_gauges=10, seed=42)

Configure a positive transform count and deterministic seed.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
127
128
129
130
131
132
133
134
def __init__(self, n_gauges: int = 10, seed: int = 42) -> None:
    """Configure a positive transform count and deterministic seed."""
    if isinstance(n_gauges, bool) or not isinstance(n_gauges, int) or n_gauges <= 0:
        raise ValueError("n_gauges must be a positive integer")
    if isinstance(seed, bool) or not isinstance(seed, int):
        raise ValueError("seed must be an integer")
    self._n_gauges = n_gauges
    self._rng = np.random.default_rng(seed)

transform(model)

Return energy-equivalent gauge-transformed model copies.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def transform(self, model: IsingModel) -> list[IsingModel]:
    """Return energy-equivalent gauge-transformed model copies."""
    if not isinstance(model, IsingModel) or model.n_qubits <= 0:
        raise ValueError("model must be a non-empty IsingModel")
    transforms: list[IsingModel] = []
    for gauge_index in range(self._n_gauges):
        gauge = {index: int(self._rng.choice((-1, 1))) for index in range(model.n_qubits)}
        transforms.append(
            IsingModel(
                h={index: gauge[index] * bias for index, bias in model.h.items()},
                J={
                    pair: gauge[pair[0]] * gauge[pair[1]] * strength
                    for pair, strength in model.J.items()
                },
                offset=model.offset,
                qubit_labels=dict(model.qubit_labels),
                n_qubits=model.n_qubits,
                source=f"{model.source}_gauge{gauge_index}",
            )
        )
    return transforms

untransform_sample(sample, gauge)

Return a transformed sample to the original spin frame.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
158
159
160
161
162
163
164
165
166
167
168
169
170
def untransform_sample(
    self,
    sample: Mapping[int, int],
    gauge: Mapping[int, int],
) -> dict[int, int]:
    """Return a transformed sample to the original spin frame."""
    for mapping_name, mapping in (("sample", sample), ("gauge", gauge)):
        for index, spin in mapping.items():
            if isinstance(index, bool) or not isinstance(index, int) or index < 0:
                raise ValueError(f"{mapping_name} indices must be non-negative integers")
            if spin not in {-1, 1}:
                raise ValueError(f"{mapping_name} values must be -1 or +1")
    return {index: spin * gauge.get(index, 1) for index, spin in sample.items()}

SCPrecisionEncoder

Encode unit-interval SC values as binary, unary, or one-hot qubits.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
class SCPrecisionEncoder:
    """Encode unit-interval SC values as binary, unary, or one-hot qubits."""

    def __init__(self, encoding: str = "binary", n_bits: int = 8) -> None:
        """Select a supported encoding and positive qubit count."""
        if encoding not in {"binary", "unary", "one_hot"}:
            raise ValueError(f"Unknown encoding: {encoding}")
        if isinstance(n_bits, bool) or not isinstance(n_bits, int) or n_bits <= 0:
            raise ValueError("n_bits must be a positive integer")
        self._encoding = encoding
        self._n_bits = n_bits

    @property
    def n_levels(self) -> int:
        """Return the number of representable precision levels."""
        if self._encoding == "binary":
            return int(2**self._n_bits)
        if self._encoding == "unary":
            return self._n_bits + 1
        return self._n_bits

    def encode(self, sc_value: float) -> dict[int, int]:
        """Encode one finite SC value after clipping it to ``[0, 1]``."""
        value = _finite("sc_value", sc_value)
        clipped = max(0.0, min(1.0, value))
        if self._encoding == "binary":
            level = int(round(clipped * (2**self._n_bits - 1)))
            return {index: (level >> index) & 1 for index in range(self._n_bits)}
        if self._encoding == "unary":
            one_count = int(round(clipped * self._n_bits))
            return {index: 1 if index < one_count else 0 for index in range(self._n_bits)}
        level = int(round(clipped * (self._n_bits - 1)))
        return {index: 1 if index == level else 0 for index in range(self._n_bits)}

    def decode(self, qubits: Mapping[int, int]) -> float:
        """Decode a validated partial binary qubit mapping."""
        for index, bit in qubits.items():
            if (
                isinstance(index, bool)
                or not isinstance(index, int)
                or not 0 <= index < self._n_bits
            ):
                raise ValueError("qubit indices must fit within the configured encoding")
            if bit not in {0, 1}:
                raise ValueError("qubit values must be binary")
        if self._encoding == "binary":
            level = sum(qubits.get(index, 0) << index for index in range(self._n_bits))
            return float(level / (2**self._n_bits - 1))
        if self._encoding == "unary":
            return sum(qubits.get(index, 0) for index in range(self._n_bits)) / self._n_bits
        active = [index for index in range(self._n_bits) if qubits.get(index, 0) == 1]
        if len(active) > 1:
            raise ValueError("one_hot decoding accepts at most one active qubit")
        return active[0] / max(self._n_bits - 1, 1) if active else 0.0

    def qubits_needed(self, n_sc_values: int) -> int:
        """Return the total qubits required for a non-negative value count."""
        if isinstance(n_sc_values, bool) or not isinstance(n_sc_values, int) or n_sc_values < 0:
            raise ValueError("n_sc_values must be a non-negative integer")
        return n_sc_values * self._n_bits

    def encode_array(self, values: np.ndarray[Any, Any]) -> dict[int, int]:
        """Encode a non-empty one-dimensional array into global qubit indices."""
        array = np.asarray(values, dtype=np.float64)
        if array.ndim != 1 or array.size == 0:
            raise ValueError("values must be a non-empty one-dimensional array")
        if not bool(np.all(np.isfinite(array))):
            raise ValueError("values must contain only finite numbers")
        result: dict[int, int] = {}
        for value_index, value in enumerate(array):
            for local_index, bit in self.encode(float(value)).items():
                result[value_index * self._n_bits + local_index] = bit
        return result

n_levels property

Return the number of representable precision levels.

__init__(encoding='binary', n_bits=8)

Select a supported encoding and positive qubit count.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
176
177
178
179
180
181
182
183
def __init__(self, encoding: str = "binary", n_bits: int = 8) -> None:
    """Select a supported encoding and positive qubit count."""
    if encoding not in {"binary", "unary", "one_hot"}:
        raise ValueError(f"Unknown encoding: {encoding}")
    if isinstance(n_bits, bool) or not isinstance(n_bits, int) or n_bits <= 0:
        raise ValueError("n_bits must be a positive integer")
    self._encoding = encoding
    self._n_bits = n_bits

encode(sc_value)

Encode one finite SC value after clipping it to [0, 1].

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
194
195
196
197
198
199
200
201
202
203
204
205
def encode(self, sc_value: float) -> dict[int, int]:
    """Encode one finite SC value after clipping it to ``[0, 1]``."""
    value = _finite("sc_value", sc_value)
    clipped = max(0.0, min(1.0, value))
    if self._encoding == "binary":
        level = int(round(clipped * (2**self._n_bits - 1)))
        return {index: (level >> index) & 1 for index in range(self._n_bits)}
    if self._encoding == "unary":
        one_count = int(round(clipped * self._n_bits))
        return {index: 1 if index < one_count else 0 for index in range(self._n_bits)}
    level = int(round(clipped * (self._n_bits - 1)))
    return {index: 1 if index == level else 0 for index in range(self._n_bits)}

decode(qubits)

Decode a validated partial binary qubit mapping.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def decode(self, qubits: Mapping[int, int]) -> float:
    """Decode a validated partial binary qubit mapping."""
    for index, bit in qubits.items():
        if (
            isinstance(index, bool)
            or not isinstance(index, int)
            or not 0 <= index < self._n_bits
        ):
            raise ValueError("qubit indices must fit within the configured encoding")
        if bit not in {0, 1}:
            raise ValueError("qubit values must be binary")
    if self._encoding == "binary":
        level = sum(qubits.get(index, 0) << index for index in range(self._n_bits))
        return float(level / (2**self._n_bits - 1))
    if self._encoding == "unary":
        return sum(qubits.get(index, 0) for index in range(self._n_bits)) / self._n_bits
    active = [index for index in range(self._n_bits) if qubits.get(index, 0) == 1]
    if len(active) > 1:
        raise ValueError("one_hot decoding accepts at most one active qubit")
    return active[0] / max(self._n_bits - 1, 1) if active else 0.0

qubits_needed(n_sc_values)

Return the total qubits required for a non-negative value count.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
228
229
230
231
232
def qubits_needed(self, n_sc_values: int) -> int:
    """Return the total qubits required for a non-negative value count."""
    if isinstance(n_sc_values, bool) or not isinstance(n_sc_values, int) or n_sc_values < 0:
        raise ValueError("n_sc_values must be a non-negative integer")
    return n_sc_values * self._n_bits

encode_array(values)

Encode a non-empty one-dimensional array into global qubit indices.

Source code in src/sc_neurocore/bridges/annealing_transforms.py
Python
234
235
236
237
238
239
240
241
242
243
244
245
def encode_array(self, values: np.ndarray[Any, Any]) -> dict[int, int]:
    """Encode a non-empty one-dimensional array into global qubit indices."""
    array = np.asarray(values, dtype=np.float64)
    if array.ndim != 1 or array.size == 0:
        raise ValueError("values must be a non-empty one-dimensional array")
    if not bool(np.all(np.isfinite(array))):
        raise ValueError("values must contain only finite numbers")
    result: dict[int, int] = {}
    for value_index, value in enumerate(array):
        for local_index, bit in self.encode(float(value)).items():
            result[value_index * self._n_bits + local_index] = bit
    return result

export_ising_json(model, path)

Write a canonical JSON representation of an Ising model.

Source code in src/sc_neurocore/bridges/annealing_io.py
Python
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def export_ising_json(model: IsingModel, path: str | Path) -> None:
    """Write a canonical JSON representation of an Ising model."""
    if not isinstance(model, IsingModel):
        raise ValueError("model must be an IsingModel")
    _atomic_json_write(
        path,
        {
            "type": "ising",
            "n_qubits": model.n_qubits,
            "source": model.source,
            "offset": model.offset,
            "h": {str(index): value for index, value in model.h.items()},
            "J": {f"{first},{second}": value for (first, second), value in model.J.items()},
            "qubit_labels": {str(index): label for index, label in model.qubit_labels.items()},
        },
    )

export_qubo_json(model, path)

Write a canonical JSON representation of a QUBO model.

Source code in src/sc_neurocore/bridges/annealing_io.py
Python
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def export_qubo_json(model: QUBOModel, path: str | Path) -> None:
    """Write a canonical JSON representation of a QUBO model."""
    if not isinstance(model, QUBOModel):
        raise ValueError("model must be a QUBOModel")
    _atomic_json_write(
        path,
        {
            "type": "qubo",
            "n_qubits": model.n_qubits,
            "source": model.source,
            "offset": model.offset,
            "Q": {f"{first},{second}": value for (first, second), value in model.Q.items()},
            "qubit_labels": {str(index): label for index, label in model.qubit_labels.items()},
        },
    )

export_bqm(model)

Return a dimod spin BQM, or None when dimod is unavailable.

Source code in src/sc_neurocore/bridges/annealing_io.py
Python
85
86
87
88
89
def export_bqm(model: IsingModel) -> Any | None:
    """Return a dimod spin BQM, or ``None`` when dimod is unavailable."""
    if not isinstance(model, IsingModel):
        raise ValueError("model must be an IsingModel")
    return backends.build_spin_bqm(model.h, model.J, model.offset)

visualize_ising(model)

Return a stable Unicode text rendering of fields and couplings.

Source code in src/sc_neurocore/bridges/annealing_io.py
Python
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def visualize_ising(model: IsingModel) -> str:
    """Return a stable Unicode text rendering of fields and couplings."""
    if not isinstance(model, IsingModel):
        raise ValueError("model must be an IsingModel")
    lines = [
        f"┌{'=' * 50}┐",
        f"│ Ising Model: {model.source:<34} │",
        f"│ Qubits: {model.n_qubits:<4}  Couplers: {len(model.J):<5}          │",
        f"│ Offset: {model.offset:<40.4f} │",
        f"└{'=' * 50}┘",
        "",
        "  Biases (h):",
    ]
    for index in sorted(model.h):
        label = model.qubit_labels.get(index, f"q{index}")
        bar = "█" * min(int(abs(model.h[index]) * 20), 20)
        sign = "+" if model.h[index] >= 0.0 else "-"
        lines.append(f"    {label:>8}: {sign}{bar:<20} ({model.h[index]:+.4f})")

    lines.extend(("", "  Couplings (J):"))
    for first, second in sorted(model.J):
        first_label = model.qubit_labels.get(first, f"q{first}")
        second_label = model.qubit_labels.get(second, f"q{second}")
        strength = model.J[(first, second)]
        kind = "ferro" if strength < 0.0 else "anti"
        lines.append(f"    {first_label:>8} ─── {second_label:<8}: {strength:+.4f} [{kind}]")
    return "\n".join(lines)