#![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>,
pa: Option<crate::popanneal::Outcome>,
bb: Option<crate::branch::Outcome>,
sampler_state: Vec<i8>,
beta: f64,
seed: u64,
sweeps_done: u64,
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, ledger: Ledger::default(), gpu: None, ground: None, cert: None, tb: None, pa: None, bb: 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_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(0.0, |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(0.0, |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,
}
}
#[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_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"
);
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),
};
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()),
};
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_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)
}
#[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),
("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");
}
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 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_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());
}
}