#![allow(clippy::not_unsafe_ptr_arg_deref)]
use crate::device::z1_grid;
use crate::gibbs::Sampler;
use crate::graph::Graph;
use crate::ising::{lattice2d, onsager_m};
use crate::ledger::{Ledger, Z1_SPICE};
pub struct Sim {
graph: Box<Graph>,
gpu: Option<crate::wgsl::GpuModel>,
ground: Option<f64>,
cert: Option<crate::certify::Certificate>,
tb: Option<crate::tabu::Outcome>,
bl: Option<crate::bls::Outcome>,
pc: Option<Result<crate::planarcut::Outcome, String>>,
tor: Option<crate::planarcut::SurfaceBound>,
gw: Option<crate::sdp::Rounding>,
ic: Option<crate::icm::Outcome>,
pa: Option<crate::popanneal::Outcome>,
bb: Option<crate::branch::Outcome>,
hf: Option<crate::hfs::Outcome>,
sampler_state: Vec<i8>,
beta: f64,
seed: u64,
sweeps_done: u64,
threads_used: u32,
ledger: Ledger,
}
impl Sim {
fn new(graph: Graph, beta: f64, seed: u64) -> *mut Sim {
let g = Box::new(graph);
let sampler = Sampler::new(&g, beta, seed);
Box::into_raw(Box::new(Sim { sampler_state: sampler.s.clone(), graph: g, beta, seed, sweeps_done: 0,
threads_used: 0, ledger: Ledger::default(), gpu: None, ground: None, cert: None, tb: None, bl: None, pc: None, tor: None, gw: None, ic: None, pa: None, bb: None, hf: None }))
}
}
#[no_mangle]
pub extern "C" fn ft_ising2d_new(l: u32, j: f64, beta: f64, seed: u64) -> *mut Sim {
Sim::new(lattice2d(l as usize, j), beta, seed)
}
#[no_mangle]
pub extern "C" fn ft_ommx_read(
bytes: *const u8,
len: u32,
beta: f64,
seed: u64,
constant_out: *mut f64,
) -> *mut Sim {
if bytes.is_null() {
set_ommx_error("no bytes were given");
return core::ptr::null_mut();
}
let raw = unsafe { core::slice::from_raw_parts(bytes, len as usize) };
match crate::ommx::import(raw) {
Ok((g, constant)) => {
set_ommx_error("");
if !constant_out.is_null() {
unsafe { *constant_out = constant };
}
Sim::new(g, beta, seed)
}
Err(e) => {
set_ommx_error(&e.to_string());
core::ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn ft_ommx_error(buf: *mut u8, cap: u32) -> u32 {
OMMX_ERROR.with(|e| {
let e = e.borrow();
let b = e.as_bytes();
if buf.is_null() {
return b.len() as u32;
}
let n = b.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(b.as_ptr(), buf, n) };
n as u32
})
}
thread_local! {
static OMMX_ERROR: core::cell::RefCell<String> = const { core::cell::RefCell::new(String::new()) };
}
fn set_ommx_error(s: &str) {
OMMX_ERROR.with(|e| *e.borrow_mut() = s.to_string());
}
#[no_mangle]
pub extern "C" fn ft_z1_new(w: u32, h: u32, j: f64, hb: f64, beta: f64, seed: u64) -> *mut Sim {
Sim::new(z1_grid(w as usize, h as usize, j, hb), beta, seed)
}
#[no_mangle]
pub extern "C" fn ft_sweep(sim: *mut Sim, n: u32) -> u64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return 0 };
let mut smp = Sampler::new(&s.graph, s.beta, s.seed ^ s.sweeps_done.wrapping_mul(0x9E3779B97F4A7C15));
smp.s.copy_from_slice(&s.sampler_state);
for _ in 0..n {
smp.sweep(Some(&mut s.ledger));
}
s.sampler_state.copy_from_slice(&smp.s);
s.sweeps_done += n as u64;
s.sweeps_done
}
#[no_mangle]
pub extern "C" fn ft_hardware_threads() -> u32 {
#[cfg(not(target_arch = "wasm32"))]
{
std::thread::available_parallelism().map_or(1, |n| n.get() as u32)
}
#[cfg(target_arch = "wasm32")]
{
1
}
}
#[no_mangle]
pub extern "C" fn ft_sweep_par(sim: *mut Sim, n: u32, threads: u32) -> u64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return 0 };
let threads = if threads == 0 { ft_hardware_threads() } else { threads }.max(1) as usize;
let mut smp = Sampler::new(
&s.graph,
s.beta,
s.seed ^ s.sweeps_done.wrapping_mul(0x9E3779B97F4A7C15),
);
smp.s.copy_from_slice(&s.sampler_state);
smp.sweeps_par(n as usize, threads, Some(&mut s.ledger));
s.sampler_state.copy_from_slice(&smp.s);
s.threads_used = smp.threads_used() as u32;
s.sweeps_done += n as u64;
s.sweeps_done
}
#[no_mangle]
pub extern "C" fn ft_threads_used(sim: *const Sim) -> u32 {
unsafe { sim.as_ref() }.map_or(0, |s| s.threads_used)
}
#[no_mangle]
pub extern "C" fn ft_set_beta(sim: *mut Sim, beta: f64) {
if let Some(s) = unsafe { sim.as_mut() } {
s.beta = beta;
}
}
#[no_mangle]
pub extern "C" fn ft_len(sim: *const Sim) -> u32 {
unsafe { sim.as_ref() }.map_or(0, |s| s.graph.n as u32)
}
#[no_mangle]
pub extern "C" fn ft_spins(sim: *const Sim) -> *const i8 {
unsafe { sim.as_ref() }.map_or(std::ptr::null(), |s| s.sampler_state.as_ptr())
}
#[no_mangle]
pub extern "C" fn ft_magnetization(sim: *const Sim) -> f64 {
unsafe { sim.as_ref() }.map_or(f64::NAN, |s| {
s.sampler_state.iter().map(|&v| v as i64).sum::<i64>() as f64 / s.graph.n as f64
})
}
#[no_mangle]
pub extern "C" fn ft_energy(sim: *const Sim) -> f64 {
unsafe { sim.as_ref() }.map_or(f64::NAN, |s| s.graph.energy(&s.sampler_state))
}
#[no_mangle]
pub extern "C" fn ft_ledger_joules_z1(sim: *const Sim) -> f64 {
unsafe { sim.as_ref() }.map_or(0.0, |s| s.ledger.joules(&Z1_SPICE).unwrap_or(f64::NAN))
}
#[no_mangle]
pub extern "C" fn ft_onsager(beta: f64) -> f64 {
onsager_m(beta)
}
#[no_mangle]
pub extern "C" fn ft_free(sim: *mut Sim) {
if !sim.is_null() {
drop(unsafe { Box::from_raw(sim) });
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ffi_roundtrip_matches_onsager() {
let sim = ft_ising2d_new(32, 1.0, 0.6, 42);
assert_eq!(ft_len(sim), 1024);
ft_sweep(sim, 2000);
let mut acc = 0.0;
let reads = 200;
for _ in 0..reads {
ft_sweep(sim, 10);
acc += ft_magnetization(sim).abs();
}
let m = acc / reads as f64;
let exact = ft_onsager(0.6);
assert!((m - exact).abs() < 0.02, "FFI |M| {m} vs Onsager {exact}");
assert!(ft_ledger_joules_z1(sim) > 0.0);
assert!(!ft_spins(sim).is_null());
ft_free(sim);
}
}
use crate::graph::GraphBuilder;
use crate::tempering::{anneal, geometric_ladder};
#[no_mangle]
pub extern "C" fn ft_builder_new(n: u32) -> *mut GraphBuilder {
if n == 0 {
return core::ptr::null_mut();
}
Box::into_raw(Box::new(GraphBuilder::new(n as usize)))
}
#[no_mangle]
pub extern "C" fn ft_builder_couple(b: *mut GraphBuilder, i: u32, j: u32, w: f64) -> u32 {
let Some(b) = (unsafe { b.as_mut() }) else { return 0 };
if i == j || !w.is_finite() || i as usize >= b.n() || j as usize >= b.n() {
return 0;
}
b.couple(i as usize, j as usize, w);
1
}
#[no_mangle]
pub extern "C" fn ft_builder_bias(b: *mut GraphBuilder, i: u32, h: f64) -> u32 {
let Some(bb) = (unsafe { b.as_mut() }) else { return 0 };
if !h.is_finite() || i as usize >= bb.n() {
return 0;
}
bb.bias(i as usize, h);
1
}
#[no_mangle]
pub extern "C" fn ft_builder_build(b: *mut GraphBuilder, beta: f64, seed: u64) -> *mut Sim {
if b.is_null() {
return core::ptr::null_mut();
}
let b = unsafe { Box::from_raw(b) };
Sim::new(b.build(), beta, seed)
}
#[no_mangle]
pub extern "C" fn ft_builder_free(b: *mut GraphBuilder) {
if !b.is_null() {
drop(unsafe { Box::from_raw(b) });
}
}
#[no_mangle]
pub extern "C" fn ft_anneal(
sim: *mut Sim,
beta_min: f64,
beta_max: f64,
stages: u32,
sweeps_per_stage: u32,
) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
if !(beta_min > 0.0 && beta_max > beta_min) || stages < 2 || sweeps_per_stage == 0 {
return f64::NAN;
}
let ladder = geometric_ladder(beta_min, beta_max, stages as usize);
let schedule: Vec<(f64, usize)> =
ladder.iter().map(|&b| (b, sweeps_per_stage as usize)).collect();
let seed = s.seed ^ s.sweeps_done.wrapping_mul(0x9E37_79B9_7F4A_7C15);
let (best, e) = anneal(&s.graph, &schedule, seed, Some(&mut s.ledger));
s.sampler_state.copy_from_slice(&best);
s.sweeps_done += (stages as u64) * (sweeps_per_stage as u64);
s.beta = beta_max;
e
}
#[no_mangle]
pub extern "C" fn ft_nodes(sim: *const Sim) -> u32 {
match unsafe { sim.as_ref() } {
Some(s) => s.graph.n as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_ledger_updates(sim: *const Sim) -> u64 {
match unsafe { sim.as_ref() } {
Some(s) => s.ledger.samples,
None => 0,
}
}
#[cfg(test)]
mod builder_tests {
use super::*;
#[test]
fn builds_and_samples_an_arbitrary_graph() {
let b = ft_builder_new(4);
assert!(!b.is_null());
assert_eq!(ft_builder_couple(b, 0, 1, 1.0), 1);
assert_eq!(ft_builder_couple(b, 1, 2, 1.0), 1);
assert_eq!(ft_builder_bias(b, 0, 0.5), 1);
let sim = ft_builder_build(b, 1.0, 7);
assert_eq!(ft_nodes(sim), 4);
ft_sweep(sim, 50);
assert!(ft_energy(sim).is_finite());
assert!(ft_ledger_updates(sim) >= 200);
ft_free(sim);
}
#[test]
fn rejects_bad_edges_without_crashing() {
let b = ft_builder_new(3);
assert_eq!(ft_builder_couple(b, 0, 9, 1.0), 0, "out of range");
assert_eq!(ft_builder_couple(b, 1, 1, 1.0), 0, "self coupling");
assert_eq!(ft_builder_couple(b, 0, 1, f64::NAN), 0, "non-finite");
assert_eq!(ft_builder_bias(b, 7, 1.0), 0, "out of range");
ft_builder_free(b);
assert_eq!(ft_builder_couple(core::ptr::null_mut(), 0, 1, 1.0), 0);
assert_eq!(ft_nodes(core::ptr::null()), 0);
assert!(ft_anneal(core::ptr::null_mut(), 0.1, 1.0, 4, 4).is_nan());
}
#[test]
fn anneal_finds_the_frustrated_optimum() {
let b = ft_builder_new(5);
for i in 0..5u32 {
ft_builder_couple(b, i, (i + 1) % 5, -1.0);
}
let sim = ft_builder_build(b, 0.1, 1);
let e = ft_anneal(sim, 0.05, 6.0, 40, 30);
assert_eq!(e, -3.0, "frustrated 5-cycle optimum");
assert_eq!(ft_energy(sim), -3.0, "sim must hold the best state");
ft_free(sim);
}
}
use crate::wgsl::{sweep_shader, GpuModel};
fn ensure_gpu(s: &mut Sim) -> &GpuModel {
if s.gpu.is_none() {
s.gpu = Some(GpuModel::from_graph(&s.graph));
}
s.gpu.as_ref().unwrap()
}
#[no_mangle]
pub extern "C" fn ft_gpu_k(sim: *mut Sim) -> u32 {
match unsafe { sim.as_mut() } {
Some(s) => ensure_gpu(s).k,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_gpu_nbr(sim: *mut Sim) -> *const u32 {
match unsafe { sim.as_mut() } {
Some(s) => ensure_gpu(s).nbr.as_ptr(),
None => core::ptr::null(),
}
}
#[no_mangle]
pub extern "C" fn ft_gpu_w(sim: *mut Sim) -> *const f32 {
match unsafe { sim.as_mut() } {
Some(s) => ensure_gpu(s).w.as_ptr(),
None => core::ptr::null(),
}
}
#[no_mangle]
pub extern "C" fn ft_gpu_h(sim: *mut Sim) -> *const f32 {
match unsafe { sim.as_mut() } {
Some(s) => ensure_gpu(s).h.as_ptr(),
None => core::ptr::null(),
}
}
#[no_mangle]
pub extern "C" fn ft_gpu_classes(sim: *mut Sim) -> u32 {
match unsafe { sim.as_mut() } {
Some(s) => ensure_gpu(s).classes.len() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_gpu_class_len(sim: *mut Sim, c: u32) -> u32 {
match unsafe { sim.as_mut() } {
Some(s) => ensure_gpu(s).classes.get(c as usize).map_or(0, |v| v.len() as u32),
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_gpu_class_ptr(sim: *mut Sim, c: u32) -> *const u32 {
match unsafe { sim.as_mut() } {
Some(s) => ensure_gpu(s).classes.get(c as usize).map_or(core::ptr::null(), |v| v.as_ptr()),
None => core::ptr::null(),
}
}
#[no_mangle]
pub extern "C" fn ft_set_spins(sim: *mut Sim, ptr: *const i8, len: u32) -> u32 {
let Some(s) = (unsafe { sim.as_mut() }) else { return 0 };
if ptr.is_null() || len as usize != s.sampler_state.len() {
return 0;
}
let src = unsafe { core::slice::from_raw_parts(ptr, len as usize) };
if src.iter().any(|&v| v != 1 && v != -1) {
return 0; }
s.sampler_state.copy_from_slice(src);
1
}
#[no_mangle]
pub extern "C" fn ft_shader() -> *const u8 {
shader_bytes().as_ptr()
}
#[no_mangle]
pub extern "C" fn ft_shader_len() -> u32 {
shader_bytes().len() as u32
}
fn shader_bytes() -> &'static [u8] {
use std::sync::OnceLock;
static SRC: OnceLock<String> = OnceLock::new();
SRC.get_or_init(sweep_shader).as_bytes()
}
#[cfg(test)]
mod gpu_tests {
use super::*;
#[test]
fn the_gpu_view_matches_the_graph() {
let sim = ft_ising2d_new(8, 1.0, 0.44, 1);
assert_eq!(ft_gpu_k(sim), 4, "a square lattice has degree 4");
assert_eq!(ft_gpu_classes(sim), 2, "a bipartite lattice has two colours");
let total: u32 = (0..ft_gpu_classes(sim)).map(|c| ft_gpu_class_len(sim, c)).sum();
assert_eq!(total, ft_len(sim), "every node belongs to exactly one class");
assert!(!ft_gpu_nbr(sim).is_null() && !ft_gpu_w(sim).is_null());
ft_free(sim);
}
#[test]
fn the_shader_crosses_the_boundary_intact() {
let len = ft_shader_len() as usize;
let src = unsafe { core::slice::from_raw_parts(ft_shader(), len) };
let s = core::str::from_utf8(src).expect("the shader must be valid UTF-8");
assert!(s.contains("@compute"), "not a compute shader");
assert!(s.contains("1.0 / (1.0 + exp(-2.0 * P.ctl.x * f))"), "the update must survive");
}
#[test]
fn a_state_can_be_read_back_in() {
let sim = ft_ising2d_new(4, 1.0, 1.0, 1);
let n = ft_len(sim) as usize;
let up = vec![1i8; n];
assert_eq!(ft_set_spins(sim, up.as_ptr(), n as u32), 1);
assert_eq!(ft_energy(sim), -2.0 * n as f64, "all aligned on a degree-4 lattice");
let bad = vec![0i8; n];
assert_eq!(ft_set_spins(sim, bad.as_ptr(), n as u32), 0);
assert_eq!(ft_set_spins(sim, up.as_ptr(), 3), 0, "wrong length");
ft_free(sim);
}
#[test]
fn null_handles_stay_inert() {
assert_eq!(ft_gpu_k(core::ptr::null_mut()), 0);
assert_eq!(ft_gpu_classes(core::ptr::null_mut()), 0);
assert!(ft_gpu_nbr(core::ptr::null_mut()).is_null());
assert_eq!(ft_set_spins(core::ptr::null_mut(), core::ptr::null(), 0), 0);
}
}
#[no_mangle]
pub extern "C" fn ft_field(sim: *const Sim, i: u32) -> f64 {
match unsafe { sim.as_ref() } {
Some(s) if (i as usize) < s.graph.n => s.graph.field(i as usize, &s.sampler_state),
_ => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_planted_frustrated(l: u32, loops: u32, seed: u64, beta: f64) -> *mut Sim {
if l < 3 || loops == 0 {
return core::ptr::null_mut();
}
let p = crate::planted::frustrated_loops(l as usize, loops as usize, seed);
let sim = Sim::new(p.graph, beta, seed);
if let Some(s) = unsafe { sim.as_mut() } {
s.ground = Some(p.ground_energy);
}
sim
}
#[no_mangle]
pub extern "C" fn ft_planted_wishart(n: u32, alpha: f64, seed: u64, beta: f64) -> *mut Sim {
if n < 3 || !alpha.is_finite() || !(alpha > 0.0) {
return core::ptr::null_mut();
}
let p = crate::planted::wishart(n as usize, alpha, seed);
let sim = Sim::new(p.graph, beta, seed);
if let Some(s) = unsafe { sim.as_mut() } {
s.ground = Some(p.ground_energy);
}
sim
}
#[no_mangle]
pub extern "C" fn ft_ground_energy(sim: *const Sim) -> f64 {
match unsafe { sim.as_ref() } {
Some(s) => s.ground.unwrap_or(f64::NAN),
None => f64::NAN,
}
}
#[cfg(test)]
mod planted_ffi_tests {
use super::*;
#[test]
fn a_planted_instance_carries_its_optimum() {
let sim = ft_planted_frustrated(6, 40, 3, 1.0);
assert!(!sim.is_null());
let known = ft_ground_energy(sim);
assert_eq!(known, -80.0, "40 plaquettes contribute -2 each");
let e = ft_anneal(sim, 0.05, 6.0, 80, 40);
assert!(e >= known - 1e-9, "nothing can beat the planted optimum");
ft_free(sim);
}
#[test]
fn a_wishart_instance_is_dense_and_carries_its_optimum() {
let sim = ft_planted_wishart(24, 0.5, 1, 1.0);
assert!(ft_ground_energy(sim).is_finite());
assert_eq!(ft_gpu_k(sim), 23, "dense: every spin couples to every other");
ft_free(sim);
}
#[test]
fn an_ordinary_simulation_has_no_known_optimum() {
let sim = ft_ising2d_new(8, 1.0, 0.44, 1);
assert!(ft_ground_energy(sim).is_nan(), "only planted instances know their optimum");
ft_free(sim);
assert!(ft_planted_frustrated(2, 1, 0, 1.0).is_null(), "too small to have plaquettes");
}
}
#[no_mangle]
pub extern "C" fn ft_certify(sim: *mut Sim, draws: u32, thin: u32) -> u32 {
let Some(s) = (unsafe { sim.as_mut() }) else { return 0 };
if draws < 16 {
return 0; }
let mut smp = Sampler::new(&s.graph, s.beta, s.seed ^ s.sweeps_done.wrapping_mul(0x9E37_79B9_7F4A_7C15));
smp.s.copy_from_slice(&s.sampler_state);
let mut samples = Vec::with_capacity(draws as usize);
let mut trace = Vec::with_capacity(draws as usize);
for _ in 0..draws {
for _ in 0..thin.max(1) {
smp.sweep(Some(&mut s.ledger));
}
samples.push(smp.s.clone());
trace.push(s.graph.energy(&smp.s));
}
s.sampler_state.copy_from_slice(&smp.s);
s.sweeps_done += draws as u64 * thin.max(1) as u64;
s.cert = Some(crate::certify::certify(&s.graph, s.beta, &samples, &trace));
1
}
macro_rules! cert_field {
($name:ident, $f:expr) => {
#[no_mangle]
pub extern "C" fn $name(sim: *const Sim) -> f64 {
match unsafe { sim.as_ref() }.and_then(|s| s.cert.as_ref()) {
Some(c) => $f(c),
None => f64::NAN,
}
}
};
}
cert_field!(ft_cert_beta_eff, |c: &crate::certify::Certificate| c.beta_eff);
cert_field!(ft_cert_beta_lo, |c: &crate::certify::Certificate| c.beta_ci.0);
cert_field!(ft_cert_beta_hi, |c: &crate::certify::Certificate| c.beta_ci.1);
cert_field!(ft_cert_tau, |c: &crate::certify::Certificate| c.tau_int);
cert_field!(ft_cert_ess, |c: &crate::certify::Certificate| c.ess);
cert_field!(ft_cert_tv, |c: &crate::certify::Certificate| c.tv_exact.unwrap_or(f64::NAN));
cert_field!(ft_cert_floor, |c: &crate::certify::Certificate| c.noise_floor.unwrap_or(f64::NAN));
#[no_mangle]
pub extern "C" fn ft_cert_passed(sim: *const Sim) -> u32 {
match unsafe { sim.as_ref() }.and_then(|s| s.cert.as_ref()) {
Some(c) if c.passed() => 1,
_ => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_cert_findings(sim: *const Sim) -> u32 {
match unsafe { sim.as_ref() }.and_then(|s| s.cert.as_ref()) {
Some(c) => c.findings.len() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_cert_finding(sim: *const Sim, i: u32, buf: *mut u8, cap: u32) -> u32 {
let Some(c) = unsafe { sim.as_ref() }.and_then(|s| s.cert.as_ref()) else { return 0 };
let Some(f) = c.findings.get(i as usize) else { return 0 };
let text = f.to_string();
let bytes = text.as_bytes();
if buf.is_null() {
return bytes.len() as u32;
}
let n = bytes.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, n) };
n as u32
}
#[no_mangle]
pub extern "C" fn ft_exact_ground(sim: *const Sim, max_width: u32) -> f64 {
let Some(s) = (unsafe { sim.as_ref() }) else { return f64::NAN };
crate::exact::Elimination { max_width: max_width as usize }
.ground_state(&s.graph)
.ok()
.and_then(|e| e.ground_energy)
.unwrap_or(f64::NAN)
}
#[no_mangle]
pub extern "C" fn ft_exact_log_z(sim: *const Sim, beta: f64, max_width: u32) -> f64 {
let Some(s) = (unsafe { sim.as_ref() }) else { return f64::NAN };
crate::exact::Elimination { max_width: max_width as usize }
.log_partition(&s.graph, beta)
.ok()
.and_then(|e| e.log_z)
.unwrap_or(f64::NAN)
}
#[no_mangle]
pub extern "C" fn ft_exact_width(sim: *const Sim) -> u32 {
match unsafe { sim.as_ref() } {
Some(s) => crate::exact::Elimination::default().width(&s.graph) as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_exact_ground_state(
sim: *const Sim,
max_width: u32,
out: *mut i8,
len: u32,
) -> u32 {
let Some(s) = (unsafe { sim.as_ref() }) else { return 0 };
if out.is_null() || len as usize != s.graph.n {
return 0;
}
let el = crate::exact::Elimination { max_width: max_width as usize };
match el.ground_state(&s.graph) {
Ok(e) => match e.ground_state {
Some(st) => {
unsafe { core::ptr::copy_nonoverlapping(st.as_ptr(), out, st.len()) };
1
}
None => 0,
},
Err(_) => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_exact_marginals(
sim: *const Sim,
beta: f64,
max_width: u32,
out: *mut f64,
len: u32,
) -> u32 {
let Some(s) = (unsafe { sim.as_ref() }) else { return 0 };
if out.is_null() || len as usize != s.graph.n || !beta.is_finite() {
return 0;
}
let el = crate::exact::Elimination { max_width: max_width as usize };
match el.marginals(&s.graph, beta) {
Ok(m) => {
unsafe { core::ptr::copy_nonoverlapping(m.as_ptr(), out, m.len()) };
1
}
Err(_) => 0,
}
}
#[cfg(test)]
mod exact_marginal_ffi {
use super::*;
#[test]
fn the_marginals_are_a_referee_a_sampler_can_be_checked_against() {
let b = ft_builder_new(5);
for i in 0..5u32 {
assert_eq!(ft_builder_couple(b, i, (i + 1) % 5, -1.0), 1);
}
assert_eq!(ft_builder_bias(b, 0, 0.4), 1);
let sim = ft_builder_build(b, 0.7, 11);
let n = ft_len(sim) as usize;
let mut m = vec![0.0f64; n];
assert_eq!(ft_exact_marginals(sim, 0.7, 24, m.as_mut_ptr(), n as u32), 1);
assert!(m.iter().all(|p| (0.0..=1.0).contains(p)), "{m:?}");
assert!(m[0] > 0.5, "a positive field must favour +1: {}", m[0]);
ft_sweep(sim, 2000);
let draws = 20_000;
let mut up = vec![0u64; n];
for _ in 0..draws {
ft_sweep(sim, 1);
let st = unsafe { core::slice::from_raw_parts(ft_spins(sim), n) };
for i in 0..n {
if st[i] == 1 {
up[i] += 1;
}
}
}
for i in 0..n {
let got = up[i] as f64 / draws as f64;
assert!((got - m[i]).abs() < 0.03, "node {i}: sampled {got:.4} vs exact {:.4}", m[i]);
}
ft_free(sim);
}
#[test]
fn a_wrong_length_or_a_too_wide_graph_is_refused_rather_than_partly_written() {
let sim = ft_ising2d_new(4, 1.0, 0.5, 1);
let n = ft_len(sim) as usize;
let mut m = vec![0.0f64; n];
assert_eq!(ft_exact_marginals(sim, 0.5, 24, m.as_mut_ptr(), (n - 1) as u32), 0);
assert_eq!(ft_exact_marginals(sim, 0.5, 24, core::ptr::null_mut(), n as u32), 0);
assert_eq!(ft_exact_marginals(sim, f64::NAN, 24, m.as_mut_ptr(), n as u32), 0);
assert_eq!(ft_exact_marginals(sim, 0.5, 0, m.as_mut_ptr(), n as u32), 0);
assert!(m.iter().all(|&x| x == 0.0), "a refusal must not write");
assert_eq!(ft_exact_marginals(core::ptr::null(), 0.5, 24, m.as_mut_ptr(), n as u32), 0);
ft_free(sim);
}
}
#[cfg(test)]
mod exact_state_ffi {
use super::*;
#[test]
fn the_recovered_state_attains_the_energy() {
let sim = ft_planted_frustrated(4, 12, 3, 1.0);
let n = ft_len(sim) as usize;
let mut out = vec![0i8; n];
assert_eq!(ft_exact_ground_state(sim, 20, out.as_mut_ptr(), n as u32), 1);
assert!(out.iter().all(|&v| v == 1 || v == -1));
assert_eq!(ft_set_spins(sim, out.as_ptr(), n as u32), 1);
let e = ft_energy(sim);
assert!((e - ft_exact_ground(sim, 20)).abs() < 1e-9, "state {e} vs energy");
assert!((e - ft_ground_energy(sim)).abs() < 1e-9, "and it is the planted optimum");
ft_free(sim);
}
#[test]
fn a_wrong_length_is_refused() {
let sim = ft_ising2d_new(4, 1.0, 1.0, 1);
let mut out = vec![0i8; 3];
assert_eq!(ft_exact_ground_state(sim, 20, out.as_mut_ptr(), 3), 0);
assert_eq!(ft_exact_ground_state(sim, 20, core::ptr::null_mut(), 16), 0);
ft_free(sim);
}
}
use crate::model::{Compiled, Constraint, Expr, Lit, Model, Sense, Solution};
pub struct ModelHandle {
model: Model,
compiled: Option<Compiled>,
solution: Option<Solution>,
last_error: String,
lits: Vec<Lit>,
cert: Option<crate::certify::Certificate>,
}
#[no_mangle]
pub extern "C" fn ft_model_new() -> *mut ModelHandle {
Box::into_raw(Box::new(ModelHandle {
model: Model::new(),
compiled: None,
solution: None,
last_error: String::new(),
lits: Vec::new(),
cert: None,
}))
}
#[no_mangle]
pub extern "C" fn ft_model_free(m: *mut ModelHandle) {
if !m.is_null() {
drop(unsafe { Box::from_raw(m) });
}
}
#[no_mangle]
pub extern "C" fn ft_model_categorical(m: *mut ModelHandle, k: u32) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return u32::MAX };
if k < 2 {
return u32::MAX;
}
let n = h.model.len();
h.model.categorical(&format!("v{n}"), k as usize);
n as u32
}
#[no_mangle]
pub extern "C" fn ft_model_integer(m: *mut ModelHandle, lo: i64, hi: i64) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return u32::MAX };
if hi <= lo {
return u32::MAX;
}
let n = h.model.len();
h.model.integer(&format!("v{n}"), lo, hi);
n as u32
}
#[no_mangle]
pub extern "C" fn ft_model_binary(m: *mut ModelHandle) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return u32::MAX };
let n = h.model.len();
h.model.binary(&format!("v{n}"));
n as u32
}
fn var_of(h: &ModelHandle, i: u32) -> Option<crate::model::Var> {
(( i as usize) < h.model.len()).then(|| h.model.var_at(i as usize))
}
#[no_mangle]
pub extern "C" fn ft_model_not_equal(m: *mut ModelHandle, a: u32, b: u32) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
match (var_of(h, a), var_of(h, b)) {
(Some(x), Some(y)) if a != b => {
h.model.constrain(Constraint::NotEqual(x, y));
h.last_error.clear();
1
}
_ => {
h.last_error = if a == b {
format!("'not_equal' needs two DIFFERENT variables; both arguments are variable {a}")
} else {
format!(
"'not_equal' names variable {}, which is not declared; {} exist",
if var_of(h, a).is_none() { a } else { b },
h.model.len()
)
};
0
}
}
}
#[no_mangle]
pub extern "C" fn ft_model_equal(m: *mut ModelHandle, a: u32, b: u32) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
match (var_of(h, a), var_of(h, b)) {
(Some(x), Some(y)) if a != b => {
h.model.constrain(Constraint::Equal(x, y));
h.last_error.clear();
1
}
_ => {
h.last_error = if a == b {
format!("'equal' needs two DIFFERENT variables; both arguments are variable {a}")
} else {
format!(
"'equal' names variable {}, which is not declared; {} exist",
if var_of(h, a).is_none() { a } else { b },
h.model.len()
)
};
0
}
}
}
#[no_mangle]
pub extern "C" fn ft_model_fix(m: *mut ModelHandle, v: u32, value: i64) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
match var_of(h, v) {
Some(x) if check_value(h, x, value) => {
h.model.constrain(Constraint::Fix(x, value));
h.last_error.clear();
1
}
_ => {
if var_of(h, v).is_none() {
h.last_error = format!(
"'fix' names variable {v}, which is not declared; {} exist",
h.model.len()
);
}
0
}
}
}
#[no_mangle]
pub extern "C" fn ft_model_objective_term(
m: *mut ModelHandle,
maximize: u32,
coeff: f64,
v: u32,
value: i64,
) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
let Some(x) = var_of(h, v) else { return 0 };
if !coeff.is_finite() || !check_value(h, x, value) {
return 0;
}
let sense = if maximize != 0 { Sense::Maximize } else { Sense::Minimize };
h.model.objective(sense, Expr::lit(coeff, Lit::Is(x, value)));
1
}
#[no_mangle]
pub extern "C" fn ft_model_objective_pair(
m: *mut ModelHandle,
maximize: u32,
coeff: f64,
a: u32,
av: i64,
b: u32,
bv: i64,
) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
let (Some(x), Some(y)) = (var_of(h, a), var_of(h, b)) else { return 0 };
if !coeff.is_finite() || a == b || !check_value(h, x, av) || !check_value(h, y, bv) {
return 0;
}
let sense = if maximize != 0 { Sense::Maximize } else { Sense::Minimize };
h.model.objective(sense, Expr::pair(coeff, Lit::Is(x, av), Lit::Is(y, bv)));
1
}
#[no_mangle]
pub extern "C" fn ft_model_objective_product(
m: *mut ModelHandle,
maximize: u32,
coeff: f64,
) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
let lits = core::mem::take(&mut h.lits);
if lits.is_empty() {
h.last_error = "an objective term needs at least one literal".into();
return 0;
}
if !coeff.is_finite() {
h.last_error = format!("an objective coefficient must be a real number, not {coeff}");
return 0;
}
let sense = if maximize != 0 { Sense::Maximize } else { Sense::Minimize };
h.model.objective(sense, Expr::product(coeff, &lits));
1
}
#[no_mangle]
pub extern "C" fn ft_model_compile(m: *mut ModelHandle) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
match h.model.compile() {
Ok(c) => {
let n = c.spins() as u32;
h.compiled = Some(c);
h.last_error.clear();
n
}
Err(e) => {
h.last_error = e.to_string();
h.compiled = None;
0
}
}
}
#[no_mangle]
pub extern "C" fn ft_model_solve(m: *mut ModelHandle, tries: u32) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
let Some(c) = h.compiled.as_ref() else { return 0 };
h.solution = Some(c.solve_best_of(tries.max(1) as u64));
1
}
#[no_mangle]
pub extern "C" fn ft_model_solve_with(
m: *mut ModelHandle,
tries: u32,
beta0: f64,
beta1: f64,
stages: u32,
sweeps: u32,
) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
let Some(c) = h.compiled.as_ref() else { return 0 };
if beta0.is_nan() || beta1.is_nan() {
return 0;
}
let (dlo, dhi, dn, dw) = crate::model::Compiled::DEFAULT_LADDER;
let lo = if beta0 > 0.0 { beta0 } else { dlo };
let hi = if beta1 > 0.0 { beta1 } else { dhi };
if !lo.is_finite() || !hi.is_finite() || hi <= lo {
return 0;
}
let n = if stages > 0 { stages as usize } else { dn };
let w = if sweeps > 0 { sweeps as usize } else { dw };
let sched = crate::schedule::Schedule::geometric(lo, hi, n, w);
h.solution = Some(c.solve_best_with(&sched, tries.max(1) as u64));
1
}
#[no_mangle]
pub extern "C" fn ft_model_value(m: *const ModelHandle, v: u32) -> i64 {
let Some(h) = (unsafe { m.as_ref() }) else { return i64::MIN };
let Some(s) = h.solution.as_ref() else { return i64::MIN };
if (v as usize) >= h.model.len() {
return i64::MIN;
}
let name = h.model.name_of(h.model.var_at(v as usize));
s.get(name).unwrap_or(i64::MIN)
}
#[no_mangle]
pub extern "C" fn ft_model_feasible(m: *const ModelHandle) -> u32 {
match unsafe { m.as_ref() }.and_then(|h| h.solution.as_ref()) {
Some(s) if s.feasible() => 1,
_ => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_model_ommx(m: *const ModelHandle, buf: *mut u8, cap: u32) -> u32 {
let Some(h) = (unsafe { m.as_ref() }) else { return 0 };
let Some(c) = h.compiled.as_ref() else { return 0 };
let e = crate::ommx::export(&c.graph);
if buf.is_null() {
return e.bytes.len() as u32;
}
let n = e.bytes.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(e.bytes.as_ptr(), buf, n) };
n as u32
}
#[no_mangle]
pub extern "C" fn ft_model_ommx_constant(m: *const ModelHandle) -> f64 {
match unsafe { m.as_ref() }.and_then(|h| h.compiled.as_ref()) {
Some(c) => crate::ommx::export(&c.graph).constant,
None => 0.0,
}
}
#[no_mangle]
pub extern "C" fn ft_model_caveats(m: *const ModelHandle) -> u32 {
match unsafe { m.as_ref() }.and_then(|h| h.compiled.as_ref()) {
Some(c) => c.caveats.len() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_model_solve_by(m: *mut ModelHandle, method: u32, effort: u64) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
let Some(c) = h.compiled.as_ref() else {
h.last_error = "compile the model before solving it".into();
return 0;
};
let meth = match method {
0 => crate::model::Method::Anneal,
1 => crate::model::Method::Tabu {
iterations: if effort == 0 { 50_000 } else { effort as usize },
},
2 => crate::model::Method::Breakout {
iterations: if effort == 0 { 50_000 } else { effort as usize },
},
3 => crate::model::Method::Branch {
max_nodes: if effort == 0 { 20_000_000 } else { effort },
},
other => {
h.last_error =
format!("unknown method {other}; 0 anneal, 1 tabu, 2 breakout, 3 branch");
return 0;
}
};
let sol = c.solve_by(meth, 1);
h.solution = Some(sol);
h.last_error.clear();
1
}
#[no_mangle]
pub extern "C" fn ft_model_proved(m: *const ModelHandle) -> u32 {
u32::from(
unsafe { m.as_ref() }
.and_then(|h| h.solution.as_ref())
.is_some_and(|s| s.proved_optimal),
)
}
#[no_mangle]
pub extern "C" fn ft_model_objective(m: *const ModelHandle) -> f64 {
unsafe { m.as_ref() }
.and_then(|h| h.solution.as_ref())
.and_then(|s| s.objective)
.unwrap_or(f64::NAN)
}
#[no_mangle]
pub extern "C" fn ft_model_has_objective(m: *const ModelHandle) -> u32 {
u32::from(
unsafe { m.as_ref() }
.and_then(|h| h.solution.as_ref())
.is_some_and(|s| s.objective.is_some()),
)
}
#[no_mangle]
pub extern "C" fn ft_model_caveat(
m: *const ModelHandle,
i: u32,
buf: *mut u8,
cap: u32,
) -> u32 {
let Some(h) = (unsafe { m.as_ref() }) else { return 0 };
let Some(c) = h.compiled.as_ref() else { return 0 };
let Some(text) = c.caveats.get(i as usize) else { return 0 };
let b = text.as_bytes();
if buf.is_null() {
return b.len() as u32;
}
let n = b.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(b.as_ptr(), buf, n) };
n as u32
}
#[no_mangle]
pub extern "C" fn ft_model_ancillas(m: *const ModelHandle) -> u32 {
match unsafe { m.as_ref() }.and_then(|h| h.compiled.as_ref()) {
Some(c) => c.ancillas as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_model_violations(m: *const ModelHandle) -> u32 {
match unsafe { m.as_ref() }.and_then(|h| h.solution.as_ref()) {
Some(s) => s.violated.len() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_model_violation(
m: *const ModelHandle,
i: u32,
buf: *mut u8,
cap: u32,
) -> u32 {
let Some(s) = unsafe { m.as_ref() }.and_then(|h| h.solution.as_ref()) else { return 0 };
let Some(v) = s.violated.get(i as usize) else { return 0 };
let b = v.detail.as_bytes();
if buf.is_null() {
return b.len() as u32;
}
let n = b.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(b.as_ptr(), buf, n) };
n as u32
}
#[no_mangle]
pub extern "C" fn ft_model_violation_amount(m: *const ModelHandle, i: u32) -> f64 {
match unsafe { m.as_ref() }.and_then(|h| h.solution.as_ref()) {
Some(s) => s.violated.get(i as usize).map(|v| v.amount).unwrap_or(f64::NAN),
None => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_model_energy(m: *const ModelHandle) -> f64 {
match unsafe { m.as_ref() }.and_then(|h| h.solution.as_ref()) {
Some(s) => s.energy,
None => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_model_penalty(m: *const ModelHandle) -> f64 {
match unsafe { m.as_ref() } {
Some(h) => h.model.effective_penalty(),
None => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_model_error(m: *const ModelHandle, buf: *mut u8, cap: u32) -> u32 {
let Some(h) = (unsafe { m.as_ref() }) else { return 0 };
let b = h.last_error.as_bytes();
if buf.is_null() {
return b.len() as u32;
}
let n = b.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(b.as_ptr(), buf, n) };
n as u32
}
#[no_mangle]
pub extern "C" fn ft_model_ftp(m: *const ModelHandle, buf: *mut u8, cap: u32) -> u32 {
let Some(c) = unsafe { m.as_ref() }.and_then(|h| h.compiled.as_ref()) else { return 0 };
let text = c.program.to_ftp();
let b = text.as_bytes();
if buf.is_null() {
return b.len() as u32;
}
let n = b.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(b.as_ptr(), buf, n) };
n as u32
}
#[cfg(test)]
mod model_ffi_tests {
use super::*;
fn text(m: *const ModelHandle, f: unsafe extern "C" fn(*const ModelHandle, *mut u8, u32) -> u32) -> String {
let need = unsafe { f(m, core::ptr::null_mut(), 0) } as usize;
let mut buf = vec![0u8; need];
let got = unsafe { f(m, buf.as_mut_ptr(), need as u32) } as usize;
String::from_utf8_lossy(&buf[..got]).into_owned()
}
#[test]
fn a_colouring_model_goes_through_the_boundary() {
let m = ft_model_new();
let a = ft_model_categorical(m, 3);
let b = ft_model_categorical(m, 3);
let c = ft_model_categorical(m, 3);
assert_eq!((a, b, c), (0, 1, 2));
assert_eq!(ft_model_not_equal(m, a, b), 1);
assert_eq!(ft_model_not_equal(m, b, c), 1);
assert_eq!(ft_model_not_equal(m, a, c), 1);
assert_eq!(ft_model_compile(m), 9, "three one-hot variables of three values");
assert_eq!(ft_model_solve(m, 12), 1);
assert_eq!(ft_model_feasible(m), 1);
let (va, vb, vc) = (ft_model_value(m, a), ft_model_value(m, b), ft_model_value(m, c));
assert!(va != vb && vb != vc && va != vc, "a triangle needs three colours: {va} {vb} {vc}");
ft_model_free(m);
}
#[test]
fn a_compile_error_crosses_as_text() {
let m = ft_model_new();
assert_eq!(ft_model_compile(m), 0, "a model with nothing in it");
let e = text(m, ft_model_error);
assert!(e.contains("no variables"), "{e}");
ft_model_free(m);
}
#[test]
fn the_compiled_program_comes_back_as_ftp() {
let m = ft_model_new();
let a = ft_model_categorical(m, 3);
let b = ft_model_categorical(m, 3);
ft_model_not_equal(m, a, b);
ft_model_compile(m);
let ftp = text(m, ft_model_ftp);
assert!(ftp.starts_with("ftp 1"));
assert!(ftp.contains("encode 0 3 onehot"), "the layout travels with it: {ftp}");
assert!(crate::ftp::Program::from_ftp(&ftp).is_ok());
ft_model_free(m);
}
#[test]
fn malformed_calls_are_inert() {
let m = ft_model_new();
assert_eq!(ft_model_categorical(m, 1), u32::MAX, "k below 2 is a constant");
assert_eq!(ft_model_integer(m, 5, 5), u32::MAX, "an empty range");
assert_eq!(ft_model_not_equal(m, 0, 0), 0, "a variable differs from nothing but itself");
assert_eq!(ft_model_value(m, 0), i64::MIN, "no solution yet");
assert_eq!(ft_model_categorical(core::ptr::null_mut(), 3), u32::MAX);
assert_eq!(ft_model_solve(core::ptr::null_mut(), 1), 0);
ft_model_free(m);
ft_model_free(core::ptr::null_mut());
}
}
#[no_mangle]
pub extern "C" fn ft_scratch(len: u32) -> *mut u8 {
use std::cell::RefCell;
thread_local! {
static BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
BUF.with(|b| {
let mut b = b.borrow_mut();
if b.len() < len as usize {
b.resize(len as usize, 0);
}
b.as_mut_ptr()
})
}
#[cfg(test)]
mod scratch_tests {
use super::*;
#[test]
fn the_scratch_buffer_grows_and_is_writable() {
let p = ft_scratch(16);
assert!(!p.is_null());
unsafe { core::ptr::write_bytes(p, 0xAB, 16) };
let big = ft_scratch(4096);
assert!(!big.is_null());
unsafe { core::ptr::write_bytes(big, 0x01, 4096) };
}
#[test]
fn text_round_trips_through_the_scratch_protocol() {
let m = ft_model_new();
let x = ft_model_categorical(m, 3);
assert_eq!(ft_model_objective_term(m, 0, 1.0, x, 99), 0, "99 is not one of three values");
let need = ft_model_error(m, core::ptr::null_mut(), 0);
assert!(need > 0);
let buf = ft_scratch(need);
let got = ft_model_error(m, buf, need);
let s = unsafe { core::slice::from_raw_parts(buf, got as usize) };
assert!(core::str::from_utf8(s).unwrap().contains("not one of them"));
ft_model_free(m);
}
}
#[no_mangle]
pub extern "C" fn ft_model_cardinality(
m: *mut ModelHandle,
count: u32,
k: u32,
value: i64,
a: u32,
b: u32,
c: u32,
d: u32,
) -> u32 {
counting(m, count, k, value, [a, b, c, d], |lits, k| Constraint::Cardinality { lits, k })
}
#[no_mangle]
pub extern "C" fn ft_model_at_most(
m: *mut ModelHandle,
count: u32,
k: u32,
value: i64,
a: u32,
b: u32,
c: u32,
d: u32,
) -> u32 {
counting(m, count, k, value, [a, b, c, d], |lits, k| Constraint::AtMost { lits, k })
}
#[no_mangle]
pub extern "C" fn ft_model_at_least(
m: *mut ModelHandle,
count: u32,
k: u32,
value: i64,
a: u32,
b: u32,
c: u32,
d: u32,
) -> u32 {
counting(m, count, k, value, [a, b, c, d], |lits, k| Constraint::AtLeast { lits, k })
}
#[no_mangle]
pub extern "C" fn ft_model_categorical_as(m: *mut ModelHandle, k: u32, encoding: u32) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return u32::MAX };
let Some(enc) = encoding_of(encoding, h) else { return u32::MAX };
if k < 2 {
return u32::MAX;
}
let n = h.model.len();
h.model.categorical_as(&format!("v{n}"), k as usize, enc);
n as u32
}
#[no_mangle]
pub extern "C" fn ft_model_integer_as(
m: *mut ModelHandle,
lo: i64,
hi: i64,
encoding: u32,
) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return u32::MAX };
let Some(enc) = encoding_of(encoding, h) else { return u32::MAX };
if hi <= lo {
return u32::MAX;
}
let n = h.model.len();
h.model.integer_as(&format!("v{n}"), lo, hi, enc);
n as u32
}
fn encoding_of(code: u32, h: &mut ModelHandle) -> Option<crate::encode::Encoding> {
use crate::encode::Encoding;
match code {
0 => Some(Encoding::OneHot),
1 => Some(Encoding::Binary),
2 => Some(Encoding::DomainWall),
other => {
h.last_error =
format!("unknown encoding {other}; 0 one-hot, 1 binary, 2 domain-wall");
None
}
}
}
#[no_mangle]
pub extern "C" fn ft_model_lits_clear(m: *mut ModelHandle) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
h.lits.clear();
1
}
#[no_mangle]
pub extern "C" fn ft_model_var(m: *mut ModelHandle, var: u32) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
if var as usize >= h.model.len() {
h.last_error = format!("no variable {var}; {} declared", h.model.len());
return 0;
}
let v = h.model.var_at(var as usize);
let Some(value) = h.model.domain_of(v).values().next() else {
h.last_error = format!("variable {var} has an empty domain");
return 0;
};
h.lits.push(Lit::Is(v, value));
1
}
#[no_mangle]
pub extern "C" fn ft_model_lit(m: *mut ModelHandle, var: u32, value: i64) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
match var_of(h, var) {
Some(x) if check_value(h, x, value) => {
h.lits.push(Lit::Is(x, value));
1
}
_ => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_model_lits(m: *const ModelHandle) -> u32 {
match unsafe { m.as_ref() } {
Some(h) => h.lits.len() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_model_close(m: *mut ModelHandle, kind: u32, k: u32) -> u32 {
close_counting(m, kind, k, None)
}
fn close_counting(m: *mut ModelHandle, kind: u32, k: u32, soft: Option<f64>) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
if let Some(w) = soft {
if !(w > 0.0) || !w.is_finite() {
h.last_error = format!("a soft constraint needs a positive price, not {w}");
h.lits.clear();
return 0;
}
}
let lits = core::mem::take(&mut h.lits);
if lits.len() < 2 {
h.last_error = format!(
"a counting constraint needs at least two literals; {} were given",
lits.len()
);
return 0;
}
if kind <= 2 && k as usize > lits.len() {
h.last_error = format!(
"k is {k} and only {} literals were given, so the constraint cannot be met",
lits.len()
);
return 0;
}
let c = match kind {
0 => Constraint::Cardinality { lits, k: k as usize },
1 => Constraint::AtMost { lits, k: k as usize },
2 => Constraint::AtLeast { lits, k: k as usize },
3 => Constraint::ExactlyOne(lits),
4 => Constraint::AtMostOne(lits),
5 => {
let mut vars: Vec<crate::model::Var> = Vec::new();
for l in &lits {
if let crate::model::Lit::Is(v, _) = l {
if !vars.contains(v) {
vars.push(*v);
}
}
}
Constraint::AllDifferent(vars)
}
other => {
h.last_error = format!(
"unknown counting kind {other}; 0 exactly, 1 at-most, 2 at-least, \
3 exactly-one, 4 at-most-one, 5 all-different"
);
return 0;
}
};
match soft {
Some(w) => h.model.soft(c, w),
None => h.model.constrain(c),
};
1
}
#[no_mangle]
pub extern "C" fn ft_model_close_soft(
m: *mut ModelHandle,
kind: u32,
k: u32,
weight: f64,
) -> u32 {
close_counting(m, kind, k, Some(weight))
}
#[no_mangle]
pub extern "C" fn ft_model_soften_last(m: *mut ModelHandle, weight: f64) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
if !(weight > 0.0) || !weight.is_finite() {
h.last_error = format!("a soft constraint needs a positive price, not {weight}");
return 0;
}
if !h.model.soften_last(weight) {
h.last_error = "there is no constraint to soften yet".into();
return 0;
}
1
}
#[no_mangle]
pub extern "C" fn ft_model_soft_cost(m: *const ModelHandle) -> f64 {
match unsafe { m.as_ref() }.and_then(|h| h.solution.as_ref()) {
Some(s) => s.soft_cost(),
None => 0.0,
}
}
#[no_mangle]
pub extern "C" fn ft_model_violation_is_hard(m: *const ModelHandle, i: u32) -> u32 {
match unsafe { m.as_ref() }.and_then(|h| h.solution.as_ref()) {
Some(s) => s.violated.get(i as usize).map(|v| v.hard as u32).unwrap_or(1),
None => 1,
}
}
#[no_mangle]
pub extern "C" fn ft_model_fixed_penalty(m: *mut ModelHandle, p: f64) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
if !p.is_finite() || p <= 0.0 {
h.last_error = format!("a penalty must be a positive number, not {p}");
return 0;
}
h.model.fixed_penalty(p);
1
}
#[no_mangle]
pub extern "C" fn ft_model_name(m: *mut ModelHandle, v: u32, name: *const u8, len: u32) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
let Some(x) = var_of(h, v) else { return 0 };
if name.is_null() {
return 0;
}
let bytes = unsafe { core::slice::from_raw_parts(name, len as usize) };
let Ok(s) = core::str::from_utf8(bytes) else {
h.last_error = "a variable name must be UTF-8".into();
return 0;
};
let clash = (0..h.model.len())
.map(|i| h.model.var_at(i))
.any(|v| v != x && h.model.name_of(v) == s);
if clash {
h.last_error = format!("'{s}' is already the name of another variable");
return 0;
}
h.model.rename(x, s);
1
}
fn check_value(h: &mut ModelHandle, var: crate::model::Var, value: i64) -> bool {
let d = h.model.domain_of(var);
if d.index_of(value).is_some() {
return true;
}
h.last_error = format!(
"'{}' takes {}; {value} is not one of them",
h.model.name_of(var),
d.describe()
);
false
}
fn counting(
m: *mut ModelHandle,
count: u32,
k: u32,
value: i64,
vars: [u32; 4],
build: impl FnOnce(Vec<Lit>, usize) -> Constraint,
) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
let mut lits = Vec::new();
for v in vars.iter().take(count.min(4) as usize) {
match var_of(h, *v) {
Some(x) if check_value(h, x, value) => lits.push(Lit::Is(x, value)),
_ => return 0,
}
}
if lits.len() < 2 || k as usize > lits.len() {
return 0;
}
h.model.constrain(build(lits, k as usize));
1
}
#[cfg(test)]
mod cardinality_ffi {
use super::*;
#[test]
fn exactly_k_crosses_the_boundary() {
let m = ft_model_new();
let v: Vec<u32> = (0..4).map(|_| ft_model_binary(m)).collect();
assert_eq!(ft_model_cardinality(m, 4, 2, 1, v[0], v[1], v[2], v[3]), 1);
assert!(ft_model_compile(m) > 0);
ft_model_solve(m, 24);
assert_eq!(ft_model_feasible(m), 1);
let on = v.iter().filter(|&&i| ft_model_value(m, i) == 1).count();
assert_eq!(on, 2, "exactly two should be on");
ft_model_free(m);
}
#[test]
fn an_encoding_can_be_chosen_and_costs_what_it_says() {
let spins_for = |enc: u32| {
let m = ft_model_new();
let v = ft_model_categorical_as(m, 8, enc);
assert_ne!(v, u32::MAX, "encoding {enc} should be accepted");
let n = ft_model_compile(m);
ft_model_free(m);
n
};
assert_eq!(spins_for(0), 8, "one-hot: one spin per value");
assert_eq!(spins_for(2), 7, "domain-wall: one fewer");
assert_eq!(spins_for(1), 3, "binary: log2 of the domain, and the cheapest by far");
for enc in [0u32, 2] {
let m = ft_model_new();
let v = ft_model_categorical_as(m, 8, enc);
assert_eq!(ft_model_fix(m, v, 3), 1);
assert!(ft_model_compile(m) > 0, "encoding {enc} must work in a constraint");
assert_eq!(ft_model_solve(m, 16), 1);
assert_eq!(ft_model_value(m, v), 3, "encoding {enc} decodes to what it was fixed to");
ft_model_free(m);
}
let m = ft_model_new();
assert_eq!(ft_model_categorical_as(m, 8, 9), u32::MAX, "an unknown encoding is refused");
let mut buf = [0u8; 256];
let n = ft_model_error(m, buf.as_mut_ptr(), buf.len() as u32) as usize;
let e = core::str::from_utf8(&buf[..n]).unwrap();
assert!(e.contains("domain-wall"), "and lists the ones it knows: {e}");
ft_model_free(m);
}
#[test]
fn a_binary_encoded_variable_cannot_appear_in_an_objective() {
let m = ft_model_new();
let v = ft_model_categorical_as(m, 8, 1);
ft_model_objective_term(m, 1, 1.0, v, 3);
assert_eq!(ft_model_compile(m), 0, "a binary variable in an objective must not compile");
let mut buf = [0u8; 512];
let n = ft_model_error(m, buf.as_mut_ptr(), buf.len() as u32) as usize;
let e = core::str::from_utf8(&buf[..n]).unwrap();
assert!(e.contains("OneHot") || e.contains("one-hot"), "{e}");
ft_model_free(m);
}
#[test]
fn a_soft_constraint_crosses_the_c_abi_as_a_price() {
let run = |price: f64| {
let m = ft_model_new();
let a = ft_model_categorical(m, 2);
let b = ft_model_categorical(m, 2);
ft_model_not_equal(m, a, b);
assert_eq!(ft_model_soften_last(m, price), 1);
ft_model_objective_term(m, 1, 5.0, a, 0);
ft_model_objective_term(m, 1, 5.0, b, 0);
assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_solve(m, 24), 1);
let out = (
ft_model_value(m, a),
ft_model_value(m, b),
ft_model_feasible(m),
ft_model_soft_cost(m),
ft_model_violations(m),
);
ft_model_free(m);
out
};
let (a, b, feasible, cost, n) = run(1.0);
assert_eq!((a, b), (0, 0), "a cheap clash is worth having");
assert_eq!(feasible, 1, "and a soft violation is not an infeasible answer");
assert_eq!(cost, 1.0);
assert_eq!(n, 1, "it is still reported");
let (a, b, _, cost, _) = run(50.0);
assert_ne!(a, b, "a dear one is not");
assert_eq!(cost, 0.0);
}
#[test]
fn hard_and_soft_are_distinguishable_over_the_abi() {
let m = ft_model_new();
let a = ft_model_categorical(m, 2);
let b = ft_model_categorical(m, 2);
ft_model_not_equal(m, a, b);
ft_model_fixed_penalty(m, 1.0); ft_model_objective_term(m, 1, 40.0, a, 0);
ft_model_objective_term(m, 1, 40.0, b, 0);
assert!(ft_model_compile(m) > 0);
ft_model_solve(m, 16);
assert_eq!(ft_model_feasible(m), 0, "a broken hard constraint is infeasible");
assert_eq!(ft_model_violation_is_hard(m, 0), 1);
assert_eq!(ft_model_soft_cost(m), 0.0, "a hard constraint has no price");
ft_model_free(m);
}
#[test]
fn a_soft_counting_constraint_and_a_bad_price_are_both_handled() {
let m = ft_model_new();
let v: Vec<u32> = (0..4).map(|_| ft_model_binary(m)).collect();
for &i in &v {
ft_model_lit(m, i, 1);
ft_model_objective_term(m, 1, 4.0, i, 1);
}
assert_eq!(ft_model_close_soft(m, 1, 2, 1.0), 1);
assert!(ft_model_compile(m) > 0);
ft_model_solve(m, 24);
assert_eq!(v.iter().filter(|&&i| ft_model_value(m, i) == 1).count(), 4, "all four taken");
assert_eq!(ft_model_feasible(m), 1, "and the answer is still an answer");
assert_eq!(ft_model_violation_is_hard(m, 0), 0, "the violation is a traded preference");
assert!(ft_model_soft_cost(m) > 0.0);
ft_model_free(m);
let m = ft_model_new();
let a = ft_model_binary(m);
let b = ft_model_binary(m);
ft_model_lit(m, a, 1);
ft_model_lit(m, b, 1);
assert_eq!(ft_model_close_soft(m, 1, 1, 0.0), 0, "a price must be positive");
assert_eq!(ft_model_lits(m), 0, "and a refused constraint clears the list");
assert_eq!(ft_model_soften_last(m, 1.0), 0, "with nothing to soften");
ft_model_free(m);
}
#[test]
fn a_higher_order_objective_term_crosses_the_c_abi() {
let m = ft_model_new();
let v: Vec<u32> = (0..3).map(|_| ft_model_categorical(m, 3)).collect();
for &i in &v {
assert_eq!(ft_model_lit(m, i, 2), 1);
}
assert_eq!(ft_model_objective_product(m, 1, 9.0), 1);
assert_eq!(ft_model_lits(m), 0, "closing clears the list");
let spins = ft_model_compile(m);
assert!(spins > 9, "three categoricals are 9 spins; the ancilla makes it more: {spins}");
assert_eq!(ft_model_solve(m, 24), 1);
for &i in &v {
assert_eq!(ft_model_value(m, i), 2, "the reward is only paid when all three hold");
}
ft_model_free(m);
}
#[test]
fn an_objective_product_refuses_what_it_cannot_mean() {
let m = ft_model_new();
let x = ft_model_categorical(m, 3);
assert_eq!(ft_model_objective_product(m, 1, 1.0), 0, "no literals is not a term");
ft_model_lit(m, x, 1);
assert_eq!(ft_model_objective_product(m, 1, f64::NAN), 0, "NaN is not a coefficient");
assert_eq!(ft_model_lits(m), 0, "and a refused term does not bleed into the next");
ft_model_free(m);
}
#[test]
fn a_counting_constraint_can_be_any_length_and_name_different_values() {
let m = ft_model_new();
let v: Vec<u32> = (0..9).map(|_| ft_model_binary(m)).collect();
for &i in &v {
assert_eq!(ft_model_lit(m, i, 1), 1);
ft_model_objective_term(m, 1, 1.0, i, 1); }
assert_eq!(ft_model_lits(m), 9);
assert_eq!(ft_model_close(m, 1, 2), 1, "at most 2 of nine");
assert_eq!(ft_model_lits(m), 0, "closing clears the list");
assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_solve(m, 24), 1);
assert_eq!(ft_model_feasible(m), 1);
assert_eq!(v.iter().filter(|&&i| ft_model_value(m, i) == 1).count(), 2);
ft_model_free(m);
let m = ft_model_new();
let a = ft_model_categorical(m, 4);
let b = ft_model_integer(m, 10, 20);
ft_model_lit(m, a, 3);
ft_model_lit(m, b, 17);
assert_eq!(ft_model_close(m, 0, 2), 1, "exactly both");
assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_solve(m, 16), 1);
assert_eq!((ft_model_value(m, a), ft_model_value(m, b)), (3, 17));
ft_model_free(m);
}
#[test]
fn exactly_one_and_at_most_one_are_reachable() {
for (kind, want) in [(3u32, 1usize), (4u32, 0usize)] {
let m = ft_model_new();
let v: Vec<u32> = (0..5).map(|_| ft_model_binary(m)).collect();
for &i in &v {
ft_model_lit(m, i, 1);
ft_model_objective_term(m, 0, 1.0, i, 1);
}
assert_eq!(ft_model_close(m, kind, 0), 1);
assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_solve(m, 24), 1);
let on = v.iter().filter(|&&i| ft_model_value(m, i) == 1).count();
assert_eq!(on, want, "kind {kind}");
assert_eq!(ft_model_feasible(m), 1);
ft_model_free(m);
}
}
#[test]
fn a_refused_counting_constraint_does_not_bleed_into_the_next() {
let m = ft_model_new();
let a = ft_model_binary(m);
let b = ft_model_binary(m);
ft_model_lit(m, a, 1);
assert_eq!(ft_model_close(m, 1, 1), 0, "one literal is not a counting constraint");
assert_eq!(ft_model_lits(m), 0, "and the list is cleared even so");
ft_model_lit(m, a, 1);
ft_model_lit(m, b, 1);
assert_eq!(ft_model_close(m, 0, 5), 0, "k cannot exceed the literal count");
assert_eq!(ft_model_lits(m), 0);
assert_eq!(ft_model_close(m, 9, 1), 0, "and an unknown kind is refused by name");
assert_eq!(ft_model_lit(m, 99, 1), 0, "no such variable");
let t = ft_model_integer(m, 10, 20);
assert_eq!(ft_model_lit(m, t, 3), 0, "3 is not a temperature in 10..=20");
ft_model_free(m);
}
#[test]
fn a_penalty_can_be_raised_when_a_constraint_loses() {
let build = |p: f64| {
let m = ft_model_new();
let a = ft_model_categorical(m, 3);
let b = ft_model_categorical(m, 3);
ft_model_not_equal(m, a, b);
ft_model_objective_term(m, 1, 40.0, a, 1);
ft_model_objective_term(m, 1, 40.0, b, 1);
if p > 0.0 {
assert_eq!(ft_model_fixed_penalty(m, p), 1);
}
assert!(ft_model_compile(m) > 0);
ft_model_solve(m, 16);
let out = (ft_model_feasible(m), ft_model_value(m, a), ft_model_value(m, b));
ft_model_free(m);
out
};
let (_, a, b) = build(1.0);
assert_eq!((a, b), (1, 1), "a penalty of 1 against a weight of 40 loses, as it should");
let (feasible, a, b) = build(200.0);
assert_eq!(feasible, 1);
assert_ne!(a, b, "a raised penalty wins the constraint back");
let m = ft_model_new();
assert_eq!(ft_model_fixed_penalty(m, 0.0), 0);
assert_eq!(ft_model_fixed_penalty(m, -1.0), 0);
assert_eq!(ft_model_fixed_penalty(m, f64::NAN), 0);
ft_model_free(m);
}
#[test]
fn objective_terms_accumulate_and_a_later_sense_does_not_rewrite_earlier_ones() {
let m = ft_model_new();
let v: Vec<u32> = (0..4).map(|_| ft_model_binary(m)).collect();
for &i in &v[..3] {
assert_eq!(ft_model_objective_term(m, 1, 1.0, i, 1), 1); }
assert_eq!(ft_model_objective_term(m, 0, 1.0, v[3], 1), 1); assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_solve(m, 16), 1);
let on: Vec<usize> = (0..4).filter(|&i| ft_model_value(m, v[i]) == 1).collect();
assert_eq!(on, vec![0, 1, 2], "three rewarded, one penalised, and no flipping");
ft_model_free(m);
}
#[test]
fn every_objective_term_survives_to_the_answer() {
let m = ft_model_new();
let x = ft_model_categorical(m, 4);
for value in 1..4i64 {
assert_eq!(ft_model_objective_term(m, 1, value as f64, x, value), 1);
}
assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_solve(m, 16), 1);
assert_eq!(ft_model_value(m, x), 3);
ft_model_free(m);
}
#[test]
fn a_name_already_taken_is_refused_at_the_call() {
let m = ft_model_new();
let a = ft_model_binary(m);
let b = ft_model_binary(m);
let n = "shift";
assert_eq!(ft_model_name(m, a, n.as_ptr(), n.len() as u32), 1);
assert_eq!(ft_model_name(m, b, n.as_ptr(), n.len() as u32), 0, "already taken");
let mut buf = [0u8; 256];
let k = ft_model_error(m, buf.as_mut_ptr(), buf.len() as u32) as usize;
let e = core::str::from_utf8(&buf[..k]).unwrap();
assert!(e.contains("'shift' is already"), "{e}");
assert_eq!(ft_model_name(m, a, n.as_ptr(), n.len() as u32), 1);
ft_model_free(m);
}
#[test]
fn a_renamed_variable_can_still_be_read_back() {
let m = ft_model_new();
let x = ft_model_categorical(m, 3);
let n = "west";
assert_eq!(ft_model_name(m, x, n.as_ptr(), n.len() as u32), 1);
ft_model_fix(m, x, 2);
assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_solve(m, 4), 1);
assert_eq!(ft_model_value(m, x), 2, "a renamed variable still reads back by index");
assert_eq!(ft_model_value(m, 99), i64::MIN, "and an index that does not exist does not");
ft_model_free(m);
}
#[test]
fn a_name_pushed_down_shows_up_in_the_error() {
let m = ft_model_new();
let t = ft_model_integer(m, 10, 20);
let n = "temperature";
assert_eq!(ft_model_name(m, t, n.as_ptr(), n.len() as u32), 1);
assert_eq!(ft_model_fix(m, t, 3), 0);
let mut buf = [0u8; 256];
let n = ft_model_error(m, buf.as_mut_ptr(), buf.len() as u32) as usize;
let e = core::str::from_utf8(&buf[..n]).unwrap();
assert!(e.contains("'temperature'"), "should name the variable the caller knows: {e}");
assert!(!e.contains("v0"), "and not the handle they never saw: {e}");
ft_model_free(m);
}
#[test]
fn a_value_outside_the_domain_is_refused_at_the_call_that_wrote_it() {
let m = ft_model_new();
let t = ft_model_integer(m, 10, 20);
assert_eq!(ft_model_fix(m, t, 3), 0, "3 is a slot, not a temperature in 10..=20");
let mut buf = [0u8; 256];
let n = ft_model_error(m, buf.as_mut_ptr(), buf.len() as u32) as usize;
let e = core::str::from_utf8(&buf[..n]).unwrap();
assert!(e.contains("10..=20") && e.contains("3 is not"), "{e}");
assert_eq!(ft_model_fix(m, t, 13), 1, "13 is one");
assert_eq!(ft_model_objective_term(m, 1, 1.0, t, 99), 0, "and 99 is not");
ft_model_free(m);
}
#[test]
fn a_caller_supplied_ladder_is_used_and_a_bad_one_refused() {
let m = ft_model_new();
let a = ft_model_categorical(m, 3);
let b = ft_model_categorical(m, 3);
ft_model_not_equal(m, a, b);
assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_solve_with(m, 4, 0.05, 6.0, 60, 20), 1);
assert_eq!(ft_model_feasible(m), 1);
assert_ne!(ft_model_value(m, a), ft_model_value(m, b));
assert_eq!(ft_model_solve_with(m, 4, 0.0, 0.0, 0, 0), 1);
assert_eq!(ft_model_feasible(m), 1);
assert_eq!(ft_model_solve_with(m, 4, 8.0, 0.05, 60, 20), 0, "hot-to-cold only");
assert_eq!(ft_model_solve_with(m, 4, f64::NAN, 6.0, 60, 20), 0, "NaN is not a temperature");
ft_model_free(m);
}
#[test]
fn ffi_inequalities_bound_without_forcing() {
let m = ft_model_new();
let v: Vec<u32> = (0..4).map(|_| ft_model_binary(m)).collect();
assert_eq!(ft_model_at_most(m, 4, 2, 1, v[0], v[1], v[2], v[3]), 1);
for &i in &v {
ft_model_objective_term(m, 1, 1.0, i, 1);
}
assert!(ft_model_compile(m) > 0);
ft_model_solve(m, 24);
assert_eq!(ft_model_feasible(m), 1);
let on = v.iter().filter(|&&i| ft_model_value(m, i) == 1).count();
assert_eq!(on, 2, "the ceiling binds against a reward pushing past it");
ft_model_free(m);
let m = ft_model_new();
let v: Vec<u32> = (0..4).map(|_| ft_model_binary(m)).collect();
assert_eq!(ft_model_at_least(m, 4, 3, 1, v[0], v[1], v[2], v[3]), 1);
for &i in &v {
ft_model_objective_term(m, 0, 1.0, i, 1);
}
assert!(ft_model_compile(m) > 0);
ft_model_solve(m, 24);
let on = v.iter().filter(|&&i| ft_model_value(m, i) == 1).count();
assert_eq!(on, 3, "the floor holds against a reward pushing below it");
ft_model_free(m);
}
#[test]
fn a_degenerate_cardinality_is_refused() {
let m = ft_model_new();
let a = ft_model_binary(m);
let b = ft_model_binary(m);
assert_eq!(ft_model_cardinality(m, 1, 1, 1, a, u32::MAX, u32::MAX, u32::MAX), 0,
"one variable is not a cardinality constraint");
assert_eq!(ft_model_cardinality(m, 2, 5, 1, a, b, u32::MAX, u32::MAX), 0,
"k cannot exceed the number of variables");
ft_model_free(m);
}
}
#[no_mangle]
pub extern "C" fn ft_model_certify(m: *mut ModelHandle, beta: f64, draws: u32, thin: u32) -> u32 {
let Some(h) = (unsafe { m.as_mut() }) else { return 0 };
let Some(c) = h.compiled.as_ref() else { return 0 };
if draws < 16 || !(beta > 0.0) {
return 0;
}
let g = &c.graph;
let mut smp = Sampler::new(g, beta, 1);
smp.sweeps(200, None);
let mut samples = Vec::with_capacity(draws as usize);
let mut trace = Vec::with_capacity(draws as usize);
for _ in 0..draws {
smp.sweeps(thin.max(1) as usize, None);
samples.push(smp.s.clone());
trace.push(g.energy(&smp.s));
}
h.cert = Some(crate::certify::certify(g, beta, &samples, &trace));
1
}
macro_rules! model_cert_field {
($name:ident, $f:expr) => {
#[no_mangle]
pub extern "C" fn $name(m: *const ModelHandle) -> f64 {
match unsafe { m.as_ref() }.and_then(|h| h.cert.as_ref()) {
Some(c) => $f(c),
None => f64::NAN,
}
}
};
}
model_cert_field!(ft_model_cert_beta, |c: &crate::certify::Certificate| c.beta_eff);
model_cert_field!(ft_model_cert_ess, |c: &crate::certify::Certificate| c.ess);
model_cert_field!(ft_model_cert_tau, |c: &crate::certify::Certificate| c.tau_int);
model_cert_field!(ft_model_cert_tv, |c: &crate::certify::Certificate| c
.tv_exact
.unwrap_or(f64::NAN));
model_cert_field!(ft_model_cert_floor, |c: &crate::certify::Certificate| c
.noise_floor
.unwrap_or(f64::NAN));
#[no_mangle]
pub extern "C" fn ft_model_cert_findings(m: *const ModelHandle) -> u32 {
match unsafe { m.as_ref() }.and_then(|h| h.cert.as_ref()) {
Some(c) => c.findings.len() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_model_cert_finding(
m: *const ModelHandle,
i: u32,
buf: *mut u8,
cap: u32,
) -> u32 {
let Some(c) = unsafe { m.as_ref() }.and_then(|h| h.cert.as_ref()) else { return 0 };
let Some(fnd) = c.findings.get(i as usize) else { return 0 };
let text = fnd.to_string();
let b = text.as_bytes();
if buf.is_null() {
return b.len() as u32;
}
let n = b.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(b.as_ptr(), buf, n) };
n as u32
}
#[cfg(test)]
mod model_cert_tests {
use super::*;
#[test]
fn a_compiled_model_can_be_certified() {
let m = ft_model_new();
let a = ft_model_categorical(m, 3);
let b = ft_model_categorical(m, 3);
ft_model_not_equal(m, a, b);
assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_certify(m, 0.5, 800, 4), 1);
let beta = ft_model_cert_beta(m);
assert!((beta - 0.5).abs() < 0.15, "beta_eff {beta} should be near the 0.5 asked for");
assert!(ft_model_cert_ess(m) > 0.0);
ft_model_free(m);
}
#[test]
fn certifying_before_compiling_is_refused() {
let m = ft_model_new();
ft_model_categorical(m, 3);
assert_eq!(ft_model_certify(m, 0.5, 800, 1), 0, "nothing compiled yet");
assert_eq!(ft_model_certify(m, 0.5, 4, 1), 0, "and 4 draws certifies nothing");
assert!(ft_model_cert_beta(m).is_nan());
ft_model_free(m);
}
}
#[no_mangle]
pub extern "C" fn ft_tabu(sim: *mut Sim, iterations: u32, tenure: u32, restart_after: u32) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
let p = crate::tabu::Params {
iterations: iterations.max(1) as usize,
tenure: tenure as usize,
restart_after: (restart_after > 0).then_some(restart_after as usize),
start: Some(s.sampler_state.clone()),
};
let out = crate::tabu::search_metered(&s.graph, &p, s.seed, Some(&mut s.ledger));
if out.state.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(&out.state);
}
let e = out.energy;
s.tb = Some(out);
e
}
#[no_mangle]
pub extern "C" fn ft_tabu_iterations(sim: *const Sim) -> u64 {
unsafe { sim.as_ref() }.and_then(|s| s.tb.as_ref()).map_or(0, |o| o.iterations_run as u64)
}
#[no_mangle]
pub extern "C" fn ft_popanneal(
sim: *mut Sim,
population: u32,
sweeps: u32,
beta_max: f64,
stages: u32,
) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
if !beta_max.is_finite() || beta_max < 0.0 {
return f64::NAN;
}
let p = crate::popanneal::Params::linear_from_zero(
population.max(1) as usize,
sweeps.max(1) as usize,
beta_max,
stages.max(1) as usize,
);
let out = crate::popanneal::run(&s.graph, &p, s.seed);
if out.state.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(&out.state);
}
let e = out.energy;
s.pa = Some(out);
e
}
#[no_mangle]
pub extern "C" fn ft_popanneal_ln_z(sim: *const Sim) -> f64 {
match unsafe { sim.as_ref() }.and_then(|s| s.pa.as_ref()) {
Some(o) if o.ln_z_is_absolute => o.ln_z,
_ => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_popanneal_rho(sim: *const Sim) -> f64 {
match unsafe { sim.as_ref() }.and_then(|s| s.pa.as_ref()) {
Some(o) => o.rho_max,
None => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_branch(sim: *mut Sim, max_nodes: u64) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
let p = crate::branch::Params {
max_nodes: max_nodes.max(1),
incumbent: Some(s.sampler_state.clone()),
..crate::branch::Params::default()
};
let out = crate::branch::solve(&s.graph, &p);
if out.state.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(&out.state);
}
let e = out.energy;
s.bb = Some(out);
e
}
#[no_mangle]
pub extern "C" fn ft_branch_proved(sim: *const Sim) -> u32 {
match unsafe { sim.as_ref() }.and_then(|s| s.bb.as_ref()) {
Some(o) => u32::from(o.proved_optimal),
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_branch_nodes(sim: *const Sim) -> u64 {
unsafe { sim.as_ref() }.and_then(|s| s.bb.as_ref()).map_or(0, |o| o.nodes)
}
#[no_mangle]
pub extern "C" fn ft_toroidal_bound(sim: *mut Sim, scale: f64) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
s.tor = None;
let Some(emb) = crate::planar::torus_grid_of(&s.graph) else { return f64::NAN };
let p = crate::planarcut::Params { scale };
match crate::planarcut::bound_on_surface(&s.graph, &emb, &p) {
Ok(b) => {
if let Some(st) = &b.state {
if st.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(st);
}
}
let c = b.cut;
s.tor = Some(b);
c
}
Err(_) => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_toroidal_attained(sim: *const Sim) -> u32 {
match unsafe { sim.as_ref() }.and_then(|s| s.tor.as_ref()) {
Some(b) => u32::from(b.attained),
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_gw_round(sim: *mut Sim, hyperplanes: u32, seed: u64) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
let r = crate::sdp::goemans_williamson(&s.graph, &crate::sdp::Params::default(), seed, hyperplanes.max(1) as usize);
if r.state.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(&r.state);
}
let c = r.cut;
s.gw = Some(r);
c
}
#[no_mangle]
pub extern "C" fn ft_gw_guaranteed(sim: *const Sim) -> u32 {
match unsafe { sim.as_ref() }.and_then(|s| s.gw.as_ref()) {
Some(r) => u32::from(r.guaranteed),
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_icm(sim: *mut Sim, rungs: u32, rounds: u32, beta_min: f64, beta_max: f64) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
if !(beta_min > 0.0 && beta_max > beta_min) {
return f64::NAN;
}
let p = crate::icm::Params {
betas: crate::tempering::geometric_ladder(beta_min, beta_max, rungs.max(2) as usize),
rounds: rounds.max(1) as usize,
sweeps_per_round: 1,
swap_every: 1,
icm_every: 1,
};
match crate::icm::run_metered(&s.graph, &p, s.seed, Some(&mut s.ledger)) {
Ok(o) => {
if o.state.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(&o.state);
}
let e = o.energy;
s.ic = Some(o);
e
}
Err(_) => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_icm_moves(sim: *const Sim) -> u64 {
unsafe { sim.as_ref() }.and_then(|s| s.ic.as_ref()).map_or(0, |o| o.icm_moves as u64)
}
#[no_mangle]
pub extern "C" fn ft_sqa(
sim: *mut Sim,
trotter: u32,
beta: f64,
gamma_max: f64,
gamma_min: f64,
steps: u32,
) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
if !(beta > 0.0 && gamma_max > 0.0 && gamma_min >= 0.0 && gamma_max >= gamma_min) {
return f64::NAN;
}
let p = crate::sqa::Params {
trotter: trotter.max(1) as usize,
beta,
gamma_max,
gamma_min,
steps: steps.max(1) as usize,
sweeps_per_step: 1,
};
let o = crate::sqa::run_metered(&s.graph, &p, s.seed, Some(&mut s.ledger));
if o.state.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(&o.state);
}
o.energy
}
#[no_mangle]
pub extern "C" fn ft_bls(sim: *mut Sim, iterations: u32) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
let p = crate::bls::Params {
iterations: iterations.max(1) as usize,
start: Some(s.sampler_state.clone()),
..crate::bls::Params::default()
};
let out = crate::bls::search_metered(&s.graph, &p, s.seed, Some(&mut s.ledger));
if out.state.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(&out.state);
}
let e = out.energy;
s.bl = Some(out);
e
}
#[no_mangle]
pub extern "C" fn ft_hfs(sim: *mut Sim, steps: u32, block: u32) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
let p = crate::hfs::Params {
steps: steps.max(1) as usize,
block: if block == 0 { crate::hfs::Params::default().block } else { block as usize },
..crate::hfs::Params::default()
};
let out = crate::hfs::run_from(&s.graph, s.sampler_state.clone(), &p, s.seed);
if out.state.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(&out.state);
}
let e = out.energy;
s.hf = Some(out);
e
}
#[no_mangle]
pub extern "C" fn ft_hfs_moves(sim: *const Sim) -> u64 {
unsafe { sim.as_ref() }.and_then(|s| s.hf.as_ref()).map_or(0, |o| o.moves as u64)
}
#[no_mangle]
pub extern "C" fn ft_hfs_improving(sim: *const Sim) -> u64 {
unsafe { sim.as_ref() }.and_then(|s| s.hf.as_ref()).map_or(0, |o| o.improving as u64)
}
#[no_mangle]
pub extern "C" fn ft_bls_descents(sim: *const Sim) -> u64 {
unsafe { sim.as_ref() }.and_then(|s| s.bl.as_ref()).map_or(0, |o| o.descents as u64)
}
#[no_mangle]
pub extern "C" fn ft_bls_iterations(sim: *const Sim) -> u64 {
unsafe { sim.as_ref() }.and_then(|s| s.bl.as_ref()).map_or(0, |o| o.iterations_run as u64)
}
#[no_mangle]
pub extern "C" fn ft_bls_max_jump(sim: *const Sim) -> u32 {
unsafe { sim.as_ref() }.and_then(|s| s.bl.as_ref()).map_or(0, |o| o.max_jump as u32)
}
#[no_mangle]
pub extern "C" fn ft_planar_cut(sim: *mut Sim, scale: f64) -> f64 {
let Some(s) = (unsafe { sim.as_mut() }) else { return f64::NAN };
let p = crate::planarcut::Params { scale };
match crate::planarcut::solve(&s.graph, &p) {
Ok(o) => {
if o.state.len() == s.sampler_state.len() {
s.sampler_state.copy_from_slice(&o.state);
}
let c = o.cut;
s.pc = Some(Ok(o));
c
}
Err(e) => {
s.pc = Some(Err(e.to_string()));
f64::NAN
}
}
}
#[no_mangle]
pub extern "C" fn ft_planar_faces(sim: *const Sim) -> u64 {
match unsafe { sim.as_ref() }.and_then(|s| s.pc.as_ref()) {
Some(Ok(o)) => o.faces as u64,
_ => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_planar_odd_faces(sim: *const Sim) -> u64 {
match unsafe { sim.as_ref() }.and_then(|s| s.pc.as_ref()) {
Some(Ok(o)) => o.odd_faces as u64,
_ => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_planar_error(sim: *const Sim, buf: *mut u8, cap: u32) -> u32 {
let msg = match unsafe { sim.as_ref() }.and_then(|s| s.pc.as_ref()) {
Some(Err(e)) => e.as_str(),
_ => "",
};
let bytes = msg.as_bytes();
if buf.is_null() {
return bytes.len() as u32;
}
let n = bytes.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(bytes.as_ptr(), buf, n) };
n as u32
}
#[no_mangle]
pub extern "C" fn ft_bound_decoupled(sim: *const Sim) -> f64 {
unsafe { sim.as_ref() }.map_or(f64::NAN, |s| crate::bound::decoupled(&s.graph).value)
}
#[no_mangle]
pub extern "C" fn ft_bound_forest(sim: *const Sim, rounds: u32) -> f64 {
unsafe { sim.as_ref() }
.map_or(f64::NAN, |s| crate::bound::forest(&s.graph, rounds as usize).value)
}
#[no_mangle]
pub extern "C" fn ft_bound_odd_cycle(sim: *const Sim, max_len: u32) -> f64 {
unsafe { sim.as_ref() }
.map_or(f64::NAN, |s| crate::bound::odd_cycle(&s.graph, max_len as usize).value)
}
#[no_mangle]
pub extern "C" fn ft_bound_sdp(sim: *const Sim, sweeps: u32, seed: u64) -> f64 {
let Some(s) = (unsafe { sim.as_ref() }) else { return f64::NAN };
let p = crate::sdp::Params { sweeps: sweeps.max(1) as usize, ..crate::sdp::Params::default() };
let (_, cert) = crate::sdp::certified(&s.graph, &p, seed);
cert.verify(&s.graph).unwrap_or(f64::NAN)
}
use crate::hubo::{Hubo, Outcome as HuboOutcome, Params as HuboParams};
pub struct HuboHandle {
hubo: Hubo,
out: Option<HuboOutcome>,
state: Vec<i8>,
ledger: Ledger,
last_error: String,
vars: Vec<u32>,
}
#[no_mangle]
pub extern "C" fn ft_hubo_new(n: u32) -> *mut HuboHandle {
if n == 0 {
return core::ptr::null_mut();
}
Box::into_raw(Box::new(HuboHandle {
hubo: Hubo::new(n as usize),
out: None,
state: vec![1; n as usize],
ledger: Ledger::default(),
last_error: String::new(),
vars: Vec::new(),
}))
}
#[no_mangle]
pub extern "C" fn ft_hubo_free(h: *mut HuboHandle) {
if !h.is_null() {
drop(unsafe { Box::from_raw(h) });
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_from_sim(sim: *const Sim) -> *mut HuboHandle {
let Some(s) = (unsafe { sim.as_ref() }) else { return core::ptr::null_mut() };
let hubo = Hubo::from_graph(&s.graph);
Box::into_raw(Box::new(HuboHandle {
hubo,
out: None,
state: s.sampler_state.clone(),
ledger: Ledger::default(),
last_error: String::new(),
vars: Vec::new(),
}))
}
#[no_mangle]
pub extern "C" fn ft_hubo_vars_clear(h: *mut HuboHandle) -> u32 {
let Some(hh) = (unsafe { h.as_mut() }) else { return 0 };
hh.vars.clear();
1
}
#[no_mangle]
pub extern "C" fn ft_hubo_var(h: *mut HuboHandle, var: u32) -> u32 {
let Some(hh) = (unsafe { h.as_mut() }) else { return 0 };
if var as usize >= hh.hubo.len() {
hh.last_error = format!("no variable {var}; {} declared", hh.hubo.len());
return 0;
}
if hh.vars.contains(&var) {
hh.last_error =
format!("variable {var} is already in this term; s*s = 1, so a repeat would change its order");
return 0;
}
hh.vars.push(var);
hh.last_error.clear();
1
}
#[no_mangle]
pub extern "C" fn ft_hubo_vars(h: *const HuboHandle) -> u32 {
match unsafe { h.as_ref() } {
Some(hh) => hh.vars.len() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_add(h: *mut HuboHandle, weight: f64) -> u32 {
let Some(hh) = (unsafe { h.as_mut() }) else { return 0 };
let vars: Vec<usize> = core::mem::take(&mut hh.vars).iter().map(|&v| v as usize).collect();
match hh.hubo.add(&vars, weight) {
Ok(()) => {
hh.last_error.clear();
1
}
Err(e) => {
hh.last_error = e.to_string();
0
}
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_term(
h: *mut HuboHandle,
count: u32,
weight: f64,
a: u32,
b: u32,
c: u32,
d: u32,
) -> u32 {
let Some(hh) = (unsafe { h.as_mut() }) else { return 0 };
if count == 0 || count > 4 {
hh.last_error = format!("this form takes one to four variables, not {count}");
return 0;
}
hh.vars.clear();
for &v in [a, b, c, d].iter().take(count as usize) {
if ft_hubo_var(h, v) == 0 {
let Some(hh) = (unsafe { h.as_mut() }) else { return 0 };
hh.vars.clear();
return 0;
}
}
ft_hubo_add(h, weight)
}
#[no_mangle]
pub extern "C" fn ft_hubo_len(h: *const HuboHandle) -> u32 {
match unsafe { h.as_ref() } {
Some(hh) => hh.hubo.len() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_terms(h: *const HuboHandle) -> u32 {
match unsafe { h.as_ref() } {
Some(hh) => hh.hubo.terms() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_max_arity(h: *const HuboHandle) -> u32 {
match unsafe { h.as_ref() } {
Some(hh) => hh.hubo.max_arity() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_ancillas_avoided(h: *const HuboHandle) -> u32 {
match unsafe { h.as_ref() } {
Some(hh) => hh.hubo.ancillas_avoided() as u32,
None => 0,
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_anneal(
h: *mut HuboHandle,
beta_min: f64,
beta_max: f64,
stages: u32,
sweeps_per_stage: u32,
seed: u64,
) -> f64 {
let Some(hh) = (unsafe { h.as_mut() }) else { return f64::NAN };
if !beta_min.is_finite() || !beta_max.is_finite() || beta_min < 0.0 || beta_max < 0.0 {
hh.last_error =
format!("a beta ladder needs two finite non-negative numbers, not {beta_min} and {beta_max}");
return f64::NAN;
}
let d = HuboParams::default();
let p = HuboParams {
beta_min: if beta_min > 0.0 { beta_min } else { d.beta_min },
beta_max: if beta_max > 0.0 { beta_max } else { d.beta_max },
stages: if stages > 0 { stages as usize } else { d.stages },
sweeps_per_stage: if sweeps_per_stage > 0 { sweeps_per_stage as usize } else { d.sweeps_per_stage },
};
if p.beta_max <= p.beta_min {
hh.last_error =
format!("beta_max must exceed beta_min; got {} and {}", p.beta_max, p.beta_min);
return f64::NAN;
}
let out = crate::hubo::anneal_metered(&hh.hubo, &p, seed, Some(&mut hh.ledger));
hh.state.clear();
hh.state.extend_from_slice(&out.state);
let e = out.energy;
hh.out = Some(out);
hh.last_error.clear();
e
}
#[no_mangle]
pub extern "C" fn ft_hubo_spins(h: *const HuboHandle) -> *const i8 {
match unsafe { h.as_ref() } {
Some(hh) if !hh.state.is_empty() => hh.state.as_ptr(),
_ => core::ptr::null(),
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_read(h: *const HuboHandle, out: *mut i8, len: u32) -> u32 {
let Some(hh) = (unsafe { h.as_ref() }) else { return 0 };
if out.is_null() || len as usize != hh.state.len() {
return 0;
}
unsafe { core::ptr::copy_nonoverlapping(hh.state.as_ptr(), out, hh.state.len()) };
1
}
#[no_mangle]
pub extern "C" fn ft_hubo_set_spins(h: *mut HuboHandle, ptr: *const i8, len: u32) -> u32 {
let Some(hh) = (unsafe { h.as_mut() }) else { return 0 };
if ptr.is_null() || len as usize != hh.hubo.len() {
hh.last_error =
format!("this model has {} spins; {len} were offered", hh.hubo.len());
return 0;
}
let src = unsafe { core::slice::from_raw_parts(ptr, len as usize) };
if let Some(bad) = src.iter().position(|&v| v != -1 && v != 1) {
hh.last_error = format!("spin {bad} is {}, and a spin is -1 or +1", src[bad]);
return 0;
}
hh.state.clear();
hh.state.extend_from_slice(src);
hh.last_error.clear();
1
}
#[no_mangle]
pub extern "C" fn ft_hubo_energy(h: *const HuboHandle) -> f64 {
match unsafe { h.as_ref() } {
Some(hh) if hh.state.len() == hh.hubo.len() => hh.hubo.energy(&hh.state),
_ => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_delta(h: *const HuboHandle, i: u32) -> f64 {
match unsafe { h.as_ref() } {
Some(hh) if (i as usize) < hh.hubo.len() && hh.state.len() == hh.hubo.len() => {
hh.hubo.delta(&hh.state, i as usize)
}
_ => f64::NAN,
}
}
#[no_mangle]
pub extern "C" fn ft_hubo_proposals(h: *const HuboHandle) -> u64 {
unsafe { h.as_ref() }.and_then(|hh| hh.out.as_ref()).map_or(0, |o| o.proposals)
}
#[no_mangle]
pub extern "C" fn ft_hubo_accepted(h: *const HuboHandle) -> u64 {
unsafe { h.as_ref() }.and_then(|hh| hh.out.as_ref()).map_or(0, |o| o.accepted)
}
#[no_mangle]
pub extern "C" fn ft_hubo_joules_z1(h: *const HuboHandle) -> f64 {
unsafe { h.as_ref() }.map_or(f64::NAN, |hh| hh.ledger.joules(&Z1_SPICE).unwrap_or(f64::NAN))
}
#[no_mangle]
pub extern "C" fn ft_hubo_error(h: *const HuboHandle, buf: *mut u8, cap: u32) -> u32 {
let Some(hh) = (unsafe { h.as_ref() }) else { return 0 };
let b = hh.last_error.as_bytes();
if buf.is_null() {
return b.len() as u32;
}
let n = b.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(b.as_ptr(), buf, n) };
n as u32
}
#[cfg(test)]
mod solver_ffi_tests {
use super::*;
#[test]
fn every_solver_leaves_the_state_its_energy_belongs_to() {
for (name, run) in [
("tabu", (|s: *mut Sim| ft_tabu(s, 4_000, 0, 1_000)) as fn(*mut Sim) -> f64),
("bls", |s: *mut Sim| ft_bls(s, 4_000)),
("popanneal", |s: *mut Sim| ft_popanneal(s, 64, 2, 6.0, 20)),
("branch", |s: *mut Sim| ft_branch(s, 2_000_000)),
] {
let sim = ft_planted_frustrated(4, 8, 7, 1.0);
assert!(!sim.is_null());
let e = run(sim);
assert!(e.is_finite(), "{name} returned {e}");
if name == "tabu" {
assert_eq!(ft_tabu_iterations(sim), 4_000, "the whole budget, not a truncated run");
}
if name == "bls" {
assert_eq!(ft_bls_iterations(sim), 4_000, "the whole budget, not a truncated run");
assert!(ft_bls_descents(sim) > 0, "a search with no descents is not a search");
assert!(ft_bls_max_jump(sim) >= 1);
}
assert!(
(ft_energy(sim) - e).abs() < 1e-9,
"{name}: returned {e}, the state it left has {}",
ft_energy(sim)
);
let known = ft_ground_energy(sim);
assert!(e >= known - 1e-9, "{name} beat the planted optimum {known} with {e}");
ft_free(sim);
}
}
#[test]
fn the_proof_flag_crosses_the_boundary_and_can_say_no() {
let sim = ft_planted_frustrated(3, 4, 1, 1.0);
let e = ft_branch(sim, 5_000_000);
assert_eq!(ft_branch_proved(sim), 1, "a 9-spin tree fits in five million nodes");
assert!(ft_branch_nodes(sim) > 0);
assert!((e - ft_ground_energy(sim)).abs() < 1e-9, "a proved minimum IS the planted optimum");
ft_free(sim);
let hard = ft_planted_wishart(40, 0.5, 5, 1.0);
ft_branch(hard, 200);
assert_eq!(ft_branch_proved(hard), 0, "200 nodes cannot exhaust a dense 40-spin tree");
assert!(ft_branch_nodes(hard) <= 201);
ft_free(hard);
}
#[test]
fn a_ferromagnet_is_proved_at_once_because_the_cheap_bound_is_exact_there() {
let sim = ft_z1_new(8, 8, 0.5, 0.1, 1.0, 3);
let e = ft_branch(sim, 100_000);
assert_eq!(ft_branch_proved(sim), 1);
assert!(ft_branch_nodes(sim) < 300, "took {} nodes", ft_branch_nodes(sim));
let d = ft_bound_decoupled(sim);
assert!((e - d).abs() < 1e-9, "ground {e} should equal the decoupled bound {d}");
ft_free(sim);
}
#[test]
fn population_annealing_hands_over_its_free_energy_and_its_warning() {
let sim = ft_z1_new(4, 4, 0.4, 0.0, 1.0, 5);
assert!(ft_popanneal_ln_z(sim).is_nan(), "no run yet, so no free energy");
assert!(ft_popanneal_rho(sim).is_nan());
ft_popanneal(sim, 128, 2, 3.0, 25);
let ln_z = ft_popanneal_ln_z(sim);
let floor = 16.0 * core::f64::consts::LN_2;
assert!(ln_z >= floor - 1e-9, "ln Z {ln_z} below n ln 2 = {floor}");
let rho = ft_popanneal_rho(sim);
assert!((1.0..=128.0).contains(&rho), "rho {rho} outside [1, population]");
ft_free(sim);
}
#[test]
fn no_bound_crossing_this_boundary_exceeds_a_known_optimum() {
let sim = ft_planted_frustrated(4, 12, 11, 1.0);
let known = ft_ground_energy(sim);
assert!(known.is_finite());
let bounds = [
("decoupled", ft_bound_decoupled(sim)),
("forest", ft_bound_forest(sim, 20)),
("odd_cycle", ft_bound_odd_cycle(sim, 6)),
("sdp", ft_bound_sdp(sim, 100, 1)),
];
for (name, v) in bounds {
assert!(v.is_finite(), "{name} returned {v}");
assert!(v <= known + 1e-9, "{name} bound {v} EXCEEDS the planted optimum {known}");
}
assert!(
bounds[3].1 >= bounds[0].1 - 1e-9,
"sdp {} is worse than decoupled {}",
bounds[3].1,
bounds[0].1
);
ft_free(sim);
}
#[test]
fn the_planar_solver_and_its_four_refusals_cross_the_boundary() {
let b = ft_builder_new(16);
for y in 0..4u32 {
for x in 0..4u32 {
let i = y * 4 + x;
if x + 1 < 4 {
ft_builder_couple(b, i, i + 1, -1.0);
}
if y + 1 < 4 {
ft_builder_couple(b, i, i + 4, -1.0);
}
}
}
let sim = ft_builder_build(b, 1.0, 1);
assert_eq!(ft_planar_cut(sim, 1.0), 24.0);
assert_eq!(ft_planar_faces(sim), 10);
assert_eq!(ft_planar_odd_faces(sim), 0);
assert_eq!(ft_planar_error(sim, core::ptr::null_mut(), 0), 0, "no error on success");
assert_eq!(ft_energy(sim), -24.0);
ft_free(sim);
let b = ft_builder_new(16);
let signs = [-1.0f64, -1.0, 1.0, -1.0, 1.0];
let mut k = 0usize;
for y in 0..4u32 {
for x in 0..4u32 {
let i = y * 4 + x;
if x + 1 < 4 {
ft_builder_couple(b, i, i + 1, signs[k % signs.len()]);
k += 1;
}
if y + 1 < 4 {
ft_builder_couple(b, i, i + 4, signs[k % signs.len()]);
k += 1;
}
}
}
let frus = ft_builder_build(b, 1.0, 1);
let c = ft_planar_cut(frus, 1.0);
assert!(c.is_finite() && c < 24.0, "a frustrated grid cannot cut every edge: {c}");
assert!(ft_planar_odd_faces(frus) > 0, "frustration makes face degrees odd");
ft_free(frus);
let torus = ft_ising2d_new(4, 1.0, 1.0, 1);
assert!(ft_planar_cut(torus, 1.0).is_nan());
let need = ft_planar_error(torus, core::ptr::null_mut(), 0);
assert!(need > 0, "a refusal must carry a reason");
let mut buf = vec![0u8; need as usize];
let got = ft_planar_error(torus, buf.as_mut_ptr(), need);
let msg = String::from_utf8_lossy(&buf[..got as usize]).to_string();
assert!(msg.contains("not planar"), "{msg}");
ft_free(torus);
}
#[test]
fn the_toroidal_bound_crosses_and_is_never_beaten() {
let torus = ft_ising2d_new(6, -1.0, 1.0, 3);
let bound = ft_toroidal_bound(torus, 1.0);
assert!(bound.is_finite(), "a periodic lattice IS a toroidal grid");
assert!(ft_planar_cut(torus, 1.0).is_nan(), "and it is not planar");
assert_eq!(bound, 72.0);
assert_eq!(ft_toroidal_attained(torus), 1, "a bound that is achieved says so");
ft_free(torus);
let hard = ft_ising2d_new(5, 1.0, 1.0, 3);
let b = ft_toroidal_bound(hard, 1.0);
assert!(b.is_finite());
let e = ft_bls(hard, 200_000);
let cut = (-50.0 - e) / 2.0;
assert!(cut <= b + 1e-9, "breakout local search reached {cut}, above the bound {b}");
ft_free(hard);
let b2 = ft_builder_new(9);
for y in 0..3u32 {
for x in 0..3u32 {
let i = y * 3 + x;
if x + 1 < 3 {
ft_builder_couple(b2, i, i + 1, -1.0);
}
if y + 1 < 3 {
ft_builder_couple(b2, i, i + 3, -1.0);
}
}
}
let planar = ft_builder_build(b2, 1.0, 1);
assert!(ft_toroidal_bound(planar, 1.0).is_nan(), "an open grid is not a torus");
ft_free(planar);
}
#[test]
fn the_closed_gaps_reach_this_boundary_and_report_their_own_caveats() {
let anti = ft_ising2d_new(6, -1.0, 1.0, 3);
let cut = ft_gw_round(anti, 64, 5);
assert!(cut.is_finite());
assert_eq!(ft_gw_guaranteed(anti), 1, "an antiferromagnet is inside the hypothesis");
assert_eq!(cut, 72.0);
assert_eq!(ft_energy(anti), -72.0, "the state left behind is the one that cut them");
ft_free(anti);
let ferro = ft_ising2d_new(6, 1.0, 1.0, 3);
assert!(ft_gw_round(ferro, 16, 5).is_finite());
assert_eq!(ft_gw_guaranteed(ferro), 0, "positive couplings are outside the theorem");
let e = ft_icm(ferro, 8, 200, 0.1, 4.0);
assert!((e + 72.0).abs() < 1e-9, "icm reached {e}");
assert!(ft_icm_moves(ferro) > 0, "the cluster move never fired");
let q = ft_sqa(ferro, 4, 10.0, 3.0, 0.05, 200);
assert!((q + 72.0).abs() < 1e-9, "sqa reached {q}");
ft_free(ferro);
let b = ft_builder_new(6);
for i in 0..6u32 {
ft_builder_couple(b, i, (i + 1) % 6, 1.0);
}
ft_builder_bias(b, 2, 0.5);
let fielded = ft_builder_build(b, 1.0, 1);
assert!(ft_icm(fielded, 4, 50, 0.1, 4.0).is_nan(), "a field is not isoenergetic");
ft_free(fielded);
}
#[test]
fn a_null_handle_returns_rather_than_dereferencing() {
let n: *mut Sim = core::ptr::null_mut();
assert!(ft_tabu(n, 10, 0, 0).is_nan());
assert!(ft_bls(n, 10).is_nan());
assert!(ft_planar_cut(n, 1.0).is_nan());
assert!(ft_toroidal_bound(n, 1.0).is_nan());
assert!(ft_gw_round(n, 8, 1).is_nan());
assert_eq!(ft_gw_guaranteed(n), 0);
assert!(ft_icm(n, 8, 10, 0.1, 4.0).is_nan());
assert_eq!(ft_icm_moves(n), 0);
assert!(ft_sqa(n, 4, 1.0, 3.0, 0.05, 10).is_nan());
assert_eq!(ft_toroidal_attained(n), 0);
assert_eq!(ft_planar_faces(n), 0);
assert_eq!(ft_planar_odd_faces(n), 0);
assert_eq!(ft_planar_error(n, core::ptr::null_mut(), 0), 0);
assert_eq!(ft_bls_descents(n), 0);
assert_eq!(ft_bls_iterations(n), 0);
assert_eq!(ft_bls_max_jump(n), 0);
assert!(ft_popanneal(n, 8, 1, 1.0, 4).is_nan());
assert!(ft_branch(n, 10).is_nan());
assert!(ft_bound_decoupled(n).is_nan());
assert!(ft_bound_forest(n, 4).is_nan());
assert!(ft_bound_odd_cycle(n, 6).is_nan());
assert!(ft_bound_sdp(n, 10, 0).is_nan());
assert_eq!(ft_branch_proved(n), 0);
assert_eq!(ft_branch_nodes(n), 0);
assert!(ft_popanneal_ln_z(n).is_nan());
assert!(ft_popanneal_rho(n).is_nan());
}
}
#[cfg(test)]
mod hubo_ffi_tests {
use super::*;
fn model(n: u32, terms: &[(&[u32], f64)]) -> *mut HuboHandle {
let h = ft_hubo_new(n);
assert!(!h.is_null());
for (vars, w) in terms {
assert_eq!(ft_hubo_vars_clear(h), 1);
for &v in *vars {
assert_eq!(ft_hubo_var(h, v), 1, "variable {v}");
}
assert_eq!(ft_hubo_add(h, *w), 1, "term {vars:?}");
}
h
}
#[test]
fn the_module_doc_example_solves_through_the_abi() {
let h = model(3, &[(&[0, 1, 2], 1.0)]);
let e = ft_hubo_anneal(h, 0.0, 0.0, 0, 0, 7);
assert_eq!(e, -1.0, "the three-body parity term");
let mut out = [0i8; 3];
assert_eq!(ft_hubo_read(h, out.as_mut_ptr(), 3), 1);
assert_eq!(out[0] as i32 * out[1] as i32 * out[2] as i32, 1, "{out:?}");
assert!((ft_hubo_energy(h) - e).abs() < 1e-9, "{} against {e}", ft_hubo_energy(h));
assert_eq!(ft_hubo_terms(h), 1);
assert_eq!(ft_hubo_max_arity(h), 3);
assert_eq!(ft_hubo_ancillas_avoided(h), 1, "one substitution for one 3-body term");
assert!(ft_hubo_proposals(h) > 0, "a run that proposed nothing is not a run");
ft_hubo_free(h);
}
#[test]
fn a_refused_variable_does_not_bleed_into_the_next_term() {
let h = ft_hubo_new(4);
assert_eq!(ft_hubo_var(h, 0), 1);
assert_eq!(ft_hubo_var(h, 9), 0, "out of range");
assert_eq!(ft_hubo_vars(h), 1, "the good one is still pending; only the bad one was refused");
assert_eq!(ft_hubo_var(h, 0), 0, "a repeat, because s*s = 1 changes the order silently");
let need = ft_hubo_error(h, core::ptr::null_mut(), 0);
let mut buf = vec![0u8; need as usize];
ft_hubo_error(h, buf.as_mut_ptr(), need);
let msg = String::from_utf8(buf).unwrap();
assert!(msg.contains("already in this term"), "{msg}");
assert_eq!(ft_hubo_add(h, f64::NAN), 0, "a non-finite weight poisons every energy");
assert_eq!(ft_hubo_vars(h), 0, "cleared even though the add failed");
assert_eq!(ft_hubo_terms(h), 0, "nothing malformed was recorded");
ft_hubo_free(h);
}
#[test]
fn the_positional_form_matches_the_list_form() {
let a = model(4, &[(&[0, 1, 2], 1.5), (&[1, 2, 3], -2.0)]);
let b = ft_hubo_new(4);
assert_eq!(ft_hubo_term(b, 3, 1.5, 0, 1, 2, u32::MAX), 1);
assert_eq!(ft_hubo_term(b, 3, -2.0, 1, 2, 3, u32::MAX), 1);
let state: [i8; 4] = [1, -1, 1, -1];
assert_eq!(ft_hubo_set_spins(a, state.as_ptr(), 4), 1);
assert_eq!(ft_hubo_set_spins(b, state.as_ptr(), 4), 1);
assert_eq!(ft_hubo_energy(a), ft_hubo_energy(b), "two ways to say one model");
for i in 0..4 {
assert_eq!(ft_hubo_delta(a, i), ft_hubo_delta(b, i), "flip {i}");
}
assert_eq!(ft_hubo_term(b, 5, 1.0, 0, 1, 2, 3), 0);
assert_eq!(ft_hubo_term(b, 0, 1.0, 0, 1, 2, 3), 0);
assert_eq!(ft_hubo_vars(b), 0, "a refused positional term leaves nothing pending");
ft_hubo_free(a);
ft_hubo_free(b);
}
#[test]
fn a_lifted_graph_scores_exactly_as_the_pairwise_path_does() {
let b = ft_builder_new(6);
assert!(!b.is_null());
for i in 0..5u32 {
assert_eq!(ft_builder_couple(b, i, i + 1, if i % 2 == 0 { 1.0 } else { -1.0 }), 1);
}
assert_eq!(ft_builder_bias(b, 0, 0.5), 1);
let sim = ft_builder_build(b, 0.9, 11);
assert!(!sim.is_null());
ft_sweep(sim, 20);
let h = ft_hubo_from_sim(sim);
assert!(!h.is_null());
assert_eq!(ft_hubo_len(h), 6);
assert_eq!(ft_hubo_max_arity(h), 2, "a lifted pairwise graph is still pairwise");
assert_eq!(ft_hubo_ancillas_avoided(h), 0, "nothing wider than two needs a substitution");
let pairwise = ft_energy(sim);
let native = ft_hubo_energy(h);
assert!((pairwise - native).abs() < 1e-9, "{pairwise} against {native}");
let mut state = vec![0i8; 6];
assert_eq!(ft_hubo_read(h, state.as_mut_ptr(), 6), 1);
for i in 0..6usize {
let before = ft_hubo_energy(h);
let d = ft_hubo_delta(h, i as u32);
state[i] = -state[i];
assert_eq!(ft_hubo_set_spins(h, state.as_ptr(), 6), 1);
let after = ft_hubo_energy(h);
assert!((after - before - d).abs() < 1e-9, "flip {i}: {d} against {}", after - before);
state[i] = -state[i];
assert_eq!(ft_hubo_set_spins(h, state.as_ptr(), 6), 1);
}
ft_hubo_free(h);
ft_free(sim);
}
#[test]
fn a_bad_ladder_is_refused_by_name_and_a_nan_is_not_read_as_a_default() {
let h = model(3, &[(&[0, 1, 2], 1.0)]);
assert!(ft_hubo_anneal(h, 8.0, 0.05, 10, 10, 1).is_nan(), "backwards");
assert!(ft_hubo_anneal(h, f64::NAN, 8.0, 10, 10, 1).is_nan(), "NaN is not a zero");
assert!(ft_hubo_anneal(h, -1.0, 8.0, 10, 10, 1).is_nan(), "negative");
assert_eq!(ft_hubo_anneal(h, 0.0, 0.0, 0, 0, 1), -1.0);
ft_hubo_free(h);
}
#[test]
fn a_state_is_refused_whole_or_taken_whole() {
let h = model(3, &[(&[0, 1, 2], 1.0)]);
let good: [i8; 3] = [1, 1, 1];
assert_eq!(ft_hubo_set_spins(h, good.as_ptr(), 3), 1);
assert_eq!(ft_hubo_energy(h), -1.0);
let bad: [i8; 3] = [1, 0, 1];
assert_eq!(ft_hubo_set_spins(h, bad.as_ptr(), 3), 0, "0 is not a spin");
assert_eq!(ft_hubo_energy(h), -1.0, "the refused write changed nothing");
assert_eq!(ft_hubo_set_spins(h, good.as_ptr(), 2), 0, "wrong length");
assert_eq!(ft_hubo_read(h, core::ptr::null_mut(), 3), 0);
ft_hubo_free(h);
}
#[test]
fn every_call_is_inert_on_a_null_handle() {
let n: *mut HuboHandle = core::ptr::null_mut();
ft_hubo_free(n);
assert!(ft_hubo_from_sim(core::ptr::null()).is_null());
assert_eq!(ft_hubo_vars_clear(n), 0);
assert_eq!(ft_hubo_var(n, 0), 0);
assert_eq!(ft_hubo_vars(n), 0);
assert_eq!(ft_hubo_add(n, 1.0), 0);
assert_eq!(ft_hubo_term(n, 2, 1.0, 0, 1, u32::MAX, u32::MAX), 0);
assert_eq!(ft_hubo_len(n), 0);
assert_eq!(ft_hubo_terms(n), 0);
assert_eq!(ft_hubo_max_arity(n), 0);
assert_eq!(ft_hubo_ancillas_avoided(n), 0);
assert!(ft_hubo_anneal(n, 0.05, 8.0, 10, 10, 1).is_nan());
assert!(ft_hubo_spins(n).is_null());
assert_eq!(ft_hubo_read(n, core::ptr::null_mut(), 0), 0);
assert_eq!(ft_hubo_set_spins(n, core::ptr::null(), 0), 0);
assert!(ft_hubo_energy(n).is_nan());
assert!(ft_hubo_delta(n, 0).is_nan());
assert_eq!(ft_hubo_proposals(n), 0);
assert_eq!(ft_hubo_accepted(n), 0);
assert!(ft_hubo_joules_z1(n).is_nan());
assert_eq!(ft_hubo_error(n, core::ptr::null_mut(), 0), 0);
assert!(ft_hubo_new(0).is_null(), "a model with no variables can hold no term");
}
}
#[cfg(test)]
mod parallel_sweep_tests {
use super::*;
fn lattice(l: u32, beta: f64, seed: u64) -> *mut Sim {
let s = ft_ising2d_new(l, 1.0, beta, seed);
assert!(!s.is_null());
s
}
#[test]
fn a_parallel_sweep_reproduces_bit_for_bit_at_a_fixed_thread_count() {
let a = lattice(16, 0.5, 0xABC);
let b = lattice(16, 0.5, 0xABC);
assert_eq!(ft_sweep_par(a, 40, 4), 40);
assert_eq!(ft_sweep_par(b, 40, 4), 40);
let (na, nb) = (ft_len(a) as usize, ft_len(b) as usize);
let sa = unsafe { core::slice::from_raw_parts(ft_spins(a), na) };
let sb = unsafe { core::slice::from_raw_parts(ft_spins(b), nb) };
assert_eq!(sa, sb, "same (seed, threads) must reproduce bit-identically");
ft_free(a);
ft_free(b);
}
#[test]
fn the_thread_count_is_part_of_the_run_and_the_abi_says_which_ran() {
let a = lattice(16, 0.5, 0xABC);
let b = lattice(16, 0.5, 0xABC);
ft_sweep_par(a, 40, 1);
ft_sweep_par(b, 40, 4);
let n = ft_len(a) as usize;
let sa = unsafe { core::slice::from_raw_parts(ft_spins(a), n) }.to_vec();
let sb = unsafe { core::slice::from_raw_parts(ft_spins(b), n) }.to_vec();
assert_ne!(sa, sb, "one thread and four are different paths, not the same one");
assert_eq!(ft_threads_used(a), 1);
assert!(ft_threads_used(b) >= 1, "the ABI reports what RAN, not what was asked");
ft_free(a);
ft_free(b);
}
#[test]
fn threads_used_reports_the_chunks_that_ran_not_the_number_asked_for() {
let b = ft_builder_new(5);
for i in 0..5u32 {
assert_eq!(ft_builder_couple(b, i, (i + 1) % 5, -1.0), 1);
}
let s = ft_builder_build(b, 0.5, 1);
assert!(!s.is_null());
ft_sweep_par(s, 5, 4);
let used = ft_threads_used(s);
assert!(used <= 4, "cannot use more threads than were asked for: {used}");
assert!(used <= 3, "5 nodes over 2 colour classes cannot occupy 4 threads: {used}");
ft_free(s);
let b2 = ft_builder_new(400);
for i in 0..400u32 {
assert_eq!(ft_builder_couple(b2, i, (i + 1) % 400, -1.0), 1);
}
let s2 = ft_builder_build(b2, 0.5, 1);
ft_sweep_par(s2, 2, 4);
assert_eq!(ft_threads_used(s2), 4, "200 nodes per class split four ways is four threads");
ft_free(s2);
}
#[test]
fn zero_threads_asks_the_machine_and_the_answer_is_at_least_one() {
assert!(ft_hardware_threads() >= 1, "a machine has at least one thread");
let s = lattice(12, 0.4, 5);
assert_eq!(ft_threads_used(s), 0, "nothing parallel has run yet");
ft_sweep_par(s, 10, 0);
assert!(ft_threads_used(s) >= 1, "0 means ask the machine, not run on nothing");
ft_free(s);
}
#[test]
fn the_parallel_path_samples_the_same_physics_as_the_serial_one() {
let beta = 0.6;
let want = ft_onsager(beta);
for threads in [1u32, 4] {
let s = lattice(48, beta, 0x9A7);
let up = vec![1i8; ft_len(s) as usize];
assert_eq!(ft_set_spins(s, up.as_ptr(), up.len() as u32), 1);
ft_sweep_par(s, 2000, threads);
let mut acc = 0.0;
for _ in 0..400 {
ft_sweep_par(s, 1, threads);
acc += ft_magnetization(s).abs();
}
let m = acc / 400.0;
assert!((m - want).abs() < 0.02, "threads={threads}: |M| {m:.4} vs Onsager {want:.4}");
ft_free(s);
}
}
#[test]
fn every_parallel_call_is_inert_on_a_null_handle() {
let n: *mut Sim = core::ptr::null_mut();
assert_eq!(ft_sweep_par(n, 10, 4), 0);
assert_eq!(ft_threads_used(core::ptr::null()), 0);
}
}
#[cfg(test)]
mod hfs_ffi {
use super::*;
#[test]
fn a_block_descent_composes_after_annealing_and_never_undoes_it() {
let sim = ft_planted_frustrated(6, 40, 3, 1.0);
assert!(!sim.is_null());
ft_anneal(sim, 0.05, 4.0, 60, 40);
let after_anneal = ft_energy(sim);
let e = ft_hfs(sim, 200, 32);
assert!(e <= after_anneal + 1e-9, "a descent cannot rise: {after_anneal} -> {e}");
assert!((ft_energy(sim) - e).abs() < 1e-9);
assert!(ft_hfs_moves(sim) > 0, "a run that made no move is not a run");
assert!(ft_hfs_improving(sim) <= ft_hfs_moves(sim));
ft_free(sim);
}
#[test]
fn block_moves_reach_lower_energy_than_the_same_budget_of_sweeps() {
let (mut better, mut worse) = (0, 0);
for seed in 0..8u64 {
let a = ft_planted_frustrated(6, 40, 3 + seed, 1.0);
let b = ft_planted_frustrated(6, 40, 3 + seed, 1.0);
ft_anneal(a, 0.05, 4.0, 40, 20);
ft_anneal(b, 0.05, 4.0, 40, 20);
let hfs = ft_hfs(a, 150, 32);
let swept = {
ft_sweep(b, 150 * 32);
ft_energy(b)
};
if hfs < swept - 1e-9 {
better += 1;
} else if hfs > swept + 1e-9 {
worse += 1;
}
ft_free(a);
ft_free(b);
}
assert!(better >= worse, "block moves reached lower {better} times, higher {worse}");
}
#[test]
fn every_hfs_call_is_inert_on_a_null_handle() {
let n: *mut Sim = core::ptr::null_mut();
assert!(ft_hfs(n, 10, 8).is_nan());
assert_eq!(ft_hfs_moves(core::ptr::null()), 0);
assert_eq!(ft_hfs_improving(core::ptr::null()), 0);
}
}
#[cfg(test)]
mod warm_start_ffi {
use super::*;
#[test]
fn tabu_and_breakout_build_on_the_state_they_are_given() {
for solver in 0..2 {
let sim = ft_planted_frustrated(6, 40, 7, 1.0);
assert!(!sim.is_null());
ft_anneal(sim, 0.05, 4.0, 60, 40);
let annealed = ft_energy(sim);
let after = if solver == 0 {
ft_tabu(sim, 5_000, 0, 0)
} else {
ft_bls(sim, 5_000)
};
assert!(
after <= annealed + 1e-9,
"solver {solver}: handed {annealed}, returned {after} -- the start was discarded"
);
ft_free(sim);
}
}
}
#[cfg(test)]
mod model_method_ffi {
use super::*;
#[test]
fn the_model_layer_can_prove_an_answer_through_the_abi() {
let m = ft_model_new();
let a = ft_model_categorical(m, 3);
let b = ft_model_categorical(m, 3);
assert_eq!(ft_model_not_equal(m, a, b), 1);
assert_eq!(ft_model_objective_term(m, 1, 5.0, a, 1), 1);
assert_eq!(ft_model_objective_term(m, 1, 4.0, b, 2), 1);
assert!(ft_model_compile(m) > 0);
assert_eq!(ft_model_solve_by(m, 0, 0), 1);
assert_eq!(ft_model_proved(m), 0, "an anneal proves nothing");
assert_eq!(ft_model_solve_by(m, 3, 5_000_000), 1);
assert_eq!(ft_model_proved(m), 1, "the tree is tiny");
assert_eq!(ft_model_feasible(m), 1);
assert!((ft_model_objective(m) - 9.0).abs() < 1e-9, "{}", ft_model_objective(m));
ft_model_free(m);
}
#[test]
fn every_method_runs_and_an_unknown_one_is_refused_by_name() {
let m = ft_model_new();
let v = ft_model_categorical(m, 3);
assert_eq!(ft_model_fix(m, v, 1), 1);
assert!(ft_model_compile(m) > 0);
for method in 0..4u32 {
assert_eq!(ft_model_solve_by(m, method, 2_000), 1, "method {method}");
assert_eq!(ft_model_feasible(m), 1, "method {method}");
}
assert_eq!(ft_model_solve_by(m, 9, 0), 0);
let need = ft_model_error(m, core::ptr::null_mut(), 0);
let mut buf = vec![0u8; need as usize];
ft_model_error(m, buf.as_mut_ptr(), need);
let msg = String::from_utf8(buf).unwrap();
assert!(msg.contains("unknown method 9"), "{msg}");
ft_model_free(m);
}
#[test]
fn solving_before_compiling_is_refused_with_the_reason() {
let m = ft_model_new();
let _ = ft_model_categorical(m, 3);
assert_eq!(ft_model_solve_by(m, 3, 0), 0, "nothing has been compiled");
let need = ft_model_error(m, core::ptr::null_mut(), 0);
let mut buf = vec![0u8; need as usize];
ft_model_error(m, buf.as_mut_ptr(), need);
assert!(String::from_utf8(buf).unwrap().contains("compile the model"));
ft_model_free(m);
let n: *mut ModelHandle = core::ptr::null_mut();
assert_eq!(ft_model_solve_by(n, 0, 0), 0);
assert_eq!(ft_model_proved(core::ptr::null()), 0);
}
}
thread_local! {
static EBM_ERROR: core::cell::RefCell<String> = const { core::cell::RefCell::new(String::new()) };
}
fn set_ebm_error(s: &str) {
EBM_ERROR.with(|e| *e.borrow_mut() = s.to_string());
}
#[no_mangle]
pub extern "C" fn ft_ebm_error(buf: *mut u8, cap: u32) -> u32 {
EBM_ERROR.with(|e| {
let e = e.borrow();
let b = e.as_bytes();
if buf.is_null() {
return b.len() as u32;
}
let n = b.len().min(cap as usize);
unsafe { core::ptr::copy_nonoverlapping(b.as_ptr(), buf, n) };
n as u32
})
}
#[no_mangle]
pub extern "C" fn ft_ebm_rbm(visible: u32, hidden: u32, beta: f64, seed: u64) -> *mut Sim {
set_ebm_error("");
if visible == 0 {
set_ebm_error("an RBM needs at least one visible unit");
return core::ptr::null_mut();
}
Sim::new(crate::ebm::rbm(visible as usize, hidden as usize), beta, seed)
}
#[no_mangle]
pub extern "C" fn ft_ebm_dbm(
visible: u32,
layers: *const u32,
n_layers: u32,
beta: f64,
seed: u64,
) -> *mut Sim {
set_ebm_error("");
if visible == 0 {
set_ebm_error("a Boltzmann machine needs at least one visible unit");
return core::ptr::null_mut();
}
if layers.is_null() || n_layers == 0 {
set_ebm_error("no hidden layers were given");
return core::ptr::null_mut();
}
let widths: Vec<usize> = unsafe { core::slice::from_raw_parts(layers, n_layers as usize) }
.iter()
.map(|&w| w as usize)
.collect();
Sim::new(crate::ebm::dbm(visible as usize, &widths), beta, seed)
}
#[no_mangle]
#[allow(clippy::too_many_arguments)]
pub extern "C" fn ft_ebm_train(
sim: *mut Sim,
visible: u32,
rows: *const i8,
n_rows: u32,
epochs: u32,
k: u32,
positive_sweeps: u32,
learning_rate: f64,
batch: u32,
seed: u64,
) -> u32 {
set_ebm_error("");
let Some(s) = (unsafe { sim.as_mut() }) else {
set_ebm_error("no simulation was given");
return 0;
};
let Some(data) = read_dataset(visible, rows, n_rows) else { return 0 };
let d = crate::ebm::Params::default();
let p = crate::ebm::Params {
epochs: if epochs == 0 { d.epochs } else { epochs as usize },
k: if k == 0 { d.k } else { k as usize },
positive_sweeps: if positive_sweeps == 0 {
d.positive_sweeps
} else {
positive_sweeps as usize
},
learning_rate: if learning_rate == 0.0 { d.learning_rate } else { learning_rate },
batch: if batch == 0 { d.batch } else { batch as usize },
};
match crate::ebm::train(&s.graph, &data, &p, seed) {
Ok(t) => {
*s.graph = t.graph;
s.gpu = None;
s.cert = None;
s.tb = None;
s.bl = None;
s.pc = None;
s.tor = None;
s.gw = None;
s.ic = None;
s.pa = None;
s.bb = None;
s.hf = None;
s.ground = None;
1
}
Err(e) => {
set_ebm_error(&e.to_string());
0
}
}
}
#[no_mangle]
pub extern "C" fn ft_ebm_log_likelihood(
sim: *mut Sim,
visible: u32,
rows: *const i8,
n_rows: u32,
) -> f64 {
set_ebm_error("");
let Some(s) = (unsafe { sim.as_ref() }) else {
set_ebm_error("no simulation was given");
return f64::NAN;
};
let Some(data) = read_dataset(visible, rows, n_rows) else { return f64::NAN };
match crate::ebm::exact_log_likelihood(&s.graph, &data) {
Ok(v) => v,
Err(e) => {
set_ebm_error(&e.to_string());
f64::NAN
}
}
}
#[no_mangle]
pub extern "C" fn ft_ebm_bars_and_stripes(side: u32, out: *mut i8, cap: u32) -> u32 {
set_ebm_error("");
if side == 0 || side > 8 {
set_ebm_error("side must be between 1 and 8");
return 0;
}
let d = crate::ebm::bars_and_stripes(side as usize);
let need = d.rows.len() * d.visible;
if out.is_null() {
return d.rows.len() as u32;
}
if (cap as usize) < need {
set_ebm_error("the buffer is too small for every row");
return 0;
}
let dst = unsafe { core::slice::from_raw_parts_mut(out, need) };
for (r, row) in d.rows.iter().enumerate() {
dst[r * d.visible..(r + 1) * d.visible].copy_from_slice(row);
}
d.rows.len() as u32
}
fn read_dataset(visible: u32, rows: *const i8, n_rows: u32) -> Option<crate::ebm::Dataset> {
if rows.is_null() {
set_ebm_error("no data rows were given");
return None;
}
if visible == 0 || n_rows == 0 {
set_ebm_error("a dataset needs at least one row and one visible unit");
return None;
}
let flat =
unsafe { core::slice::from_raw_parts(rows, n_rows as usize * visible as usize) };
Some(crate::ebm::Dataset {
visible: visible as usize,
rows: flat.chunks(visible as usize).map(|c| c.to_vec()).collect(),
})
}
#[cfg(test)]
mod ebm_ffi_tests {
use super::*;
#[test]
fn fitting_through_the_abi_learns_and_drops_what_it_invalidates() {
let n_rows = ft_ebm_bars_and_stripes(2, core::ptr::null_mut(), 0);
assert_eq!(n_rows, 6, "2x2 bars and stripes is 2*2^2 - 2 rows");
let mut rows = vec![0i8; n_rows as usize * 4];
assert_eq!(ft_ebm_bars_and_stripes(2, rows.as_mut_ptr(), rows.len() as u32), n_rows);
assert!(rows.iter().all(|&v| v == 1 || v == -1));
let sim = ft_ebm_rbm(4, 4, 1.0, 11);
assert!(!sim.is_null());
assert_eq!(ft_len(sim), 8);
let before = ft_ebm_log_likelihood(sim, 4, rows.as_ptr(), n_rows);
assert!((before - (-4.0 * 2f64.ln())).abs() < 1e-9, "{before}");
ft_sweep(sim, 50);
assert!(ft_tabu(sim, 2000, 0, 0).is_finite());
assert!(unsafe { sim.as_ref() }.unwrap().tb.is_some());
assert_eq!(ft_ebm_train(sim, 4, rows.as_ptr(), n_rows, 600, 10, 5, 0.05, 6, 3), 1);
let after = ft_ebm_log_likelihood(sim, 4, rows.as_ptr(), n_rows);
assert!(after > before + 0.05, "training must help: {before:.4} -> {after:.4}");
assert!(after < 0.0, "a log-likelihood is negative: {after}");
let s = unsafe { sim.as_ref() }.unwrap();
assert!(s.tb.is_none(), "the fit must drop results about the old weights");
assert!(s.cert.is_none() && s.gpu.is_none() && s.ground.is_none());
assert_eq!(s.sampler_state.len(), 8);
assert!(ft_tabu(sim, 2000, 0, 0).is_finite());
ft_free(sim);
}
#[test]
fn a_refusal_says_why_in_the_callers_terms() {
let read = || {
let n = ft_ebm_error(core::ptr::null_mut(), 0) as usize;
let mut b = vec![0u8; n];
let got = ft_ebm_error(b.as_mut_ptr(), n as u32) as usize;
String::from_utf8_lossy(&b[..got]).to_string()
};
assert!(ft_ebm_rbm(0, 4, 1.0, 1).is_null());
assert!(read().contains("visible"));
let sim = ft_ebm_rbm(4, 2, 1.0, 1);
let rows = [1i8, -1, 1, -1];
assert_eq!(ft_ebm_train(sim, 4, core::ptr::null(), 1, 10, 1, 1, 0.05, 1, 1), 0);
assert!(read().contains("no data rows"));
assert_eq!(ft_ebm_train(core::ptr::null_mut(), 4, rows.as_ptr(), 1, 10, 1, 1, 0.05, 1, 1), 0);
assert!(read().contains("simulation"));
assert_eq!(ft_ebm_train(sim, 4, rows.as_ptr(), 1, 10, 1, 1, 0.05, 1, 1), 1);
assert_eq!(read(), "");
ft_free(sim);
let big = ft_ebm_rbm(20, 8, 1.0, 1);
let wide = [1i8; 20];
assert!(ft_ebm_log_likelihood(big, 20, wide.as_ptr(), 1).is_nan());
assert!(read().contains("28"), "the message names the size it refused: {}", read());
ft_free(big);
assert!(ft_ebm_dbm(4, core::ptr::null(), 0, 1.0, 1).is_null());
assert!(read().contains("hidden layers"));
let layers = [3u32, 3];
let deep = ft_ebm_dbm(4, layers.as_ptr(), 2, 1.0, 1);
assert_eq!(ft_len(deep), 10);
ft_free(deep);
}
}