#![allow(missing_docs)]
use ferrotherm::graph::{Graph, GraphBuilder};
use ferrotherm::rng::Pcg;
use ferrotherm::{branch, embed, ising, tempering};
const SEEDS: u64 = 24;
const NV: usize = 12;
fn logical(seed: u64) -> Graph {
let mut rng = Pcg::new(seed, 0x0C4A_1234);
let mut b = GraphBuilder::new(NV);
for i in 0..NV {
for j in (i + 1)..NV {
b.couple(i, j, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
}
}
b.build()
}
fn main() {
let hardware = ising::chimera(8, 8, 4, 1.0);
println!("Chain strength in the embedding layer, against a proved optimum\n");
println!(
"Logical: complete graph on {NV} variables, +/-1 couplings -- degree {} against Chimera's\n\
6. Hardware: C_8,8,4, {} sites. {SEEDS} seeds, each\n\
scored against the optimum BRANCH AND BOUND PROVED for that instance.\n",
NV - 1,
hardware.n
);
let multiples = [0.25f64, 0.5, 1.0, 2.0, 3.0, 4.0, 6.0, 8.0, 16.0];
println!(
"{:>10} {:>10} {:>9} {:>10} {:>9}",
"chain x", "strength", "broken", "gap", "found"
);
struct Case {
logical: Graph,
emb: embed::Embedding,
optimum: f64,
worst: f64,
}
let mut cases = Vec::new();
for seed in 0..SEEDS {
let g = logical(seed);
let Some(emb) = embed::embed(&g, &hardware, seed) else {
continue;
};
let proved = branch::solve(&g, &branch::Params { max_nodes: 40_000_000, ..Default::default() });
if !proved.proved_optimal {
continue;
}
cases.push(Case {
worst: embed::worst_coefficient(&g),
logical: g,
emb,
optimum: proved.energy,
});
}
if cases.is_empty() {
println!("no instance both embedded and proved; nothing to report");
return;
}
let longest = cases
.iter()
.flat_map(|c| c.emb.chains.iter().map(std::vec::Vec::len))
.max()
.unwrap_or(0);
println!(
" {} of {SEEDS} instances both embedded and proved. Longest chain {longest} sites.\n",
cases.len()
);
for &mult in &multiples {
let (mut broken_frac, mut gap, mut found) = (0.0, 0.0, 0usize);
let mut strength = 0.0;
for (ci, c) in cases.iter().enumerate() {
let s = mult * c.worst;
strength = s;
let emb = embed::apply_with(&c.logical, &hardware, &c.emb, s);
let ladder: Vec<(f64, usize)> =
tempering::geometric_ladder(0.05, 6.0, 120).into_iter().map(|b| (b, 60)).collect();
let (state, _) = tempering::anneal(&emb.graph, &ladder, ci as u64 ^ 0xC4A1, None);
let (values, broke) = embed::unembed(&c.emb, &state);
broken_frac += broke.len() as f64 / NV as f64;
let e = c.logical.energy(&values);
gap += e - c.optimum;
if (e - c.optimum).abs() < 1e-9 {
found += 1;
}
}
let n = cases.len() as f64;
println!(
"{mult:>10.2} {strength:>10.2} {:>8.1}% {:>10.2} {found:>5}/{}",
broken_frac / n * 100.0,
gap / n,
cases.len()
);
}
println!(
"\nTHE DEFAULT WAS WRONG, AND THIS IS THE MEASUREMENT THAT MOVED IT. Two is the standard\n\
first guess in the literature and it was this crate's default until this table existed. At\n\
two, a tenth of chains BREAK. Four is the first multiple that breaks none, and it ties the\n\
best gap and the best hit rate. DEFAULT_CHAIN_MULTIPLE is now four."
);
println!(
"\nAND BOTH FAILURE MODES ARE HERE, WHICH IS THE WHOLE POINT OF THE SWEEP. They are not\n\
symmetric. Too weak ANNOUNCES ITSELF -- the broken column runs 69.8%, 58.0%, 32.6% as the\n\
chains give way. Too strong is SILENT: at sixteen nothing breaks at all, every chain holds,\n\
no warning fires anywhere, and the answer is nine times further from the optimum than at\n\
four. A caller watching only for broken chains would read the worst row in this table as\n\
the safest one."
);
println!(
"\nTHIS COULD NOT BE MEASURED UNTIL THE PLACER WAS REPAIRED, and that is worth saying\n\
plainly because the previous version of this file said the opposite conclusion honestly and\n\
was still wrong. It ran at SIX logical variables, because the placer could not embed any\n\
clique past K_7. Six variables on a degree-6 machine barely needs chains, so the rigidity\n\
half never appeared, the sweep looked flat above the default, and the file correctly\n\
reported that it had not exhibited the failure rather than that the failure was absent.\n\
Twelve variables with chains of eighteen sites exhibit it immediately."
);
println!(
"\nREAD THE BROKEN COLUMN FIRST. A row with broken chains has not answered the question at\n\
all: a broken chain means one logical variable held two values, and the majority vote that\n\
resolves it is a coin toss wearing a number. The 1x row is exactly that -- gap 0.00 with\n\
10.4% of chains broken -- and reading its energy alone would call it a success."
);
println!(
"\nTHE FAILURE AT THE FAR END IS SILENT, WHICH IS WHY THIS NEEDED A PROOF RATHER THAN A\n\
BASELINE. A chain strength far above the model breaks nothing and reports clean; every\n\
chain holds, no warning fires, and the answer is simply worse. Without an oracle there is\n\
nothing in the output to tell that from a hard instance. This is the same shape as the\n\
reduction penalty in `src/hubo.rs`, and the same reason that comparison needed a swept\n\
beta ladder before it meant anything."
);
println!(
"\nWHAT THIS DOES NOT ESTABLISH. One logical family, one hardware graph, one annealing\n\
schedule, {} instances. A model whose coefficients span orders of magnitude has a\n\
different `worst_coefficient` story entirely, since the scale a chain must outrank is then\n\
set by one outlier. `apply_with` exists so that case can be handled; this table calibrates\n\
the default, and is not a law.",
cases.len()
);
}