logicaffeine_compile/codegen_ana/
dynamics.rs1use logicaffeine_base::numeric::{rational_from_f64_exact, Rational};
21use logicaffeine_proof::relax_replay::{IsingClaim, MinimizeClaim, RelaxClaim};
22
23use super::cell::{ASrc, AnalogCell};
24use super::target::AnalogTarget;
25use super::AnaLowerError;
26
27#[derive(Clone, Debug, PartialEq)]
31pub struct DynSystem {
32 pub n: usize,
33 pub force: Vec<AnalogCell>,
36 pub init: Vec<f64>,
37}
38
39pub fn gradient_flow_system(a: &[Vec<f64>], b: &[f64]) -> DynSystem {
42 let n = a.len();
43 let force = (0..n)
44 .map(|i| {
45 let mut inputs: Vec<ASrc> = (0..n).map(ASrc::In).collect();
46 let mut weights: Vec<f64> = a[i].iter().map(|w| -w).collect();
47 inputs.push(ASrc::Const(1.0));
48 weights.push(b[i]);
49 AnalogCell::Sum { inputs, weights }
50 })
51 .collect();
52 DynSystem {
53 n,
54 force,
55 init: vec![0.0; n],
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq)]
64pub struct SettleOpts {
65 pub steps: usize,
66 pub rate: f64,
67 pub eps_required: f64,
68 pub noise_amp: f64,
69 pub seed: u64,
70}
71
72#[derive(Clone, Debug, PartialEq)]
74pub struct RelaxOutcome {
75 pub x_hat: Vec<f64>,
77 pub steps: usize,
78 pub claim: RelaxClaim,
80}
81
82pub fn solve_by_settling(
84 a: &[Vec<f64>],
85 b: &[f64],
86 target: &AnalogTarget,
87 opts: &SettleOpts,
88) -> Result<RelaxOutcome, AnaLowerError> {
89 let aq = exact_matrix(a)?;
90 let n = aq.len();
91 if b.len() != n {
92 return Err(AnaLowerError::ConvergenceUnprovable {
93 reason: format!("shape mismatch: A is {n}×{n} but b has {} entries", b.len()),
94 });
95 }
96 admit(&aq)?;
97
98 let sys = gradient_flow_system(a, b);
100 let mut x = sys.init.clone();
101 let mut noise_state = opts.seed ^ 0x5E77_1E5A_1755_A17E;
102 for _ in 0..opts.steps {
103 let forces: Vec<f64> = sys.force.iter().map(|cell| cell.eval(&x, &[])).collect();
104 for i in 0..n {
105 x[i] += opts.rate * forces[i];
106 if opts.noise_amp > 0.0 {
107 x[i] += opts.noise_amp * centered_uniform(&mut noise_state);
108 }
109 }
110 }
111
112 let half_span = target.half_span_v();
115 let adc_lsb = 2.0 * half_span / f64::powi(2.0, i32::from(target.adc_bits));
116 let x_hat: Vec<f64> = x.iter().map(|v| (v / adc_lsb).round() * adc_lsb).collect();
117
118 let bq = exact_vec(b)?;
120 let xq = exact_vec(&x_hat)?;
121 let mut linf = Rational::zero();
122 for i in 0..n {
123 let mut acc = bq[i].negated();
124 for j in 0..n {
125 acc = acc.add(&aq[i][j].mul(&xq[j]));
126 }
127 let mag = acc.abs();
128 if mag > linf {
129 linf = mag;
130 }
131 }
132 let linf_f = linf.to_f64();
133 if !(linf_f <= opts.eps_required) {
134 return Err(AnaLowerError::ResidualExceedsBudget {
135 linf: linf_f,
136 required: opts.eps_required,
137 });
138 }
139 Ok(RelaxOutcome {
140 x_hat,
141 steps: opts.steps,
142 claim: RelaxClaim {
143 a: aq,
144 b: bq,
145 x_hat: xq,
146 eps: linf,
147 },
148 })
149}
150
151#[derive(Clone, Debug, PartialEq)]
153pub struct MinimizeOutcome {
154 pub x_hat: Vec<f64>,
155 pub steps: usize,
156 pub claim: MinimizeClaim,
158 pub subopt: f64,
160}
161
162pub fn minimize_by_settling(
168 q: &[Vec<f64>],
169 b: &[f64],
170 target: &AnalogTarget,
171 opts: &SettleOpts,
172) -> Result<MinimizeOutcome, AnaLowerError> {
173 let qq = exact_matrix(q)?;
174 let n = qq.len();
175 if b.len() != n {
176 return Err(AnaLowerError::ConvergenceUnprovable {
177 reason: format!("shape mismatch: Q is {n}×{n} but b has {} entries", b.len()),
178 });
179 }
180 admit(&qq)?;
181 let mut mu: Option<Rational> = None;
184 for i in 0..n {
185 let off = (0..n)
186 .filter(|j| *j != i)
187 .fold(Rational::zero(), |acc, j| acc.add(&qq[i][j].abs()));
188 let row = qq[i][i].sub(&off);
189 mu = Some(match mu {
190 None => row,
191 Some(m) => {
192 if row < m {
193 row
194 } else {
195 m
196 }
197 }
198 });
199 }
200 let mu = mu.expect("n > 0");
201 if !mu.is_positive() {
202 return Err(AnaLowerError::ConvergenceUnprovable {
203 reason: "no certified spectral floor: Q is not strictly diagonally dominant, so \
204 the Gershgorin bound gives no positive μ for the suboptimality theorem"
205 .to_string(),
206 });
207 }
208
209 let neg_b: Vec<f64> = b.iter().map(|x| -x).collect();
211 let sys = gradient_flow_system(q, &neg_b);
212 let mut x = sys.init.clone();
213 let mut noise_state = opts.seed ^ 0x5E77_1E5A_1755_A17E;
214 for _ in 0..opts.steps {
215 let forces: Vec<f64> = sys.force.iter().map(|cell| cell.eval(&x, &[])).collect();
216 for i in 0..n {
217 x[i] += opts.rate * forces[i];
218 if opts.noise_amp > 0.0 {
219 x[i] += opts.noise_amp * centered_uniform(&mut noise_state);
220 }
221 }
222 }
223 let half_span = target.half_span_v();
224 let adc_lsb = 2.0 * half_span / f64::powi(2.0, i32::from(target.adc_bits));
225 let x_hat: Vec<f64> = x.iter().map(|v| (v / adc_lsb).round() * adc_lsb).collect();
226
227 let bq = exact_vec(b)?;
229 let xq = exact_vec(&x_hat)?;
230 let mut norm2 = Rational::zero();
231 for i in 0..n {
232 let mut g = bq[i].clone();
233 for j in 0..n {
234 g = g.add(&qq[i][j].mul(&xq[j]));
235 }
236 norm2 = norm2.add(&g.mul(&g));
237 }
238 let subopt = norm2.div(&Rational::from_i64(2).mul(&mu)).expect("mu > 0");
239 let subopt_f = subopt.to_f64();
240 if !(subopt_f <= opts.eps_required) {
241 return Err(AnaLowerError::ResidualExceedsBudget {
242 linf: subopt_f,
243 required: opts.eps_required,
244 });
245 }
246 Ok(MinimizeOutcome {
247 x_hat,
248 steps: opts.steps,
249 subopt: subopt_f,
250 claim: MinimizeClaim {
251 q: qq,
252 b: bq,
253 x_hat: xq,
254 mu,
255 subopt,
256 },
257 })
258}
259
260#[derive(Clone, Debug, PartialEq)]
264pub struct IsingOutcome {
265 pub spins: Vec<i8>,
266 pub steps: usize,
267 pub claim: IsingClaim,
268 pub energy: f64,
270 pub flip_stable: bool,
271}
272
273pub fn ising_by_soft_spin(
282 j: &[Vec<f64>],
283 h: &[f64],
284 opts: &SettleOpts,
285 beta_max: f64,
286) -> Result<IsingOutcome, AnaLowerError> {
287 let jq = exact_matrix(j)?;
288 let n = jq.len();
289 if h.len() != n {
290 return Err(AnaLowerError::ConvergenceUnprovable {
291 reason: format!("shape mismatch: J is {n}×{n} but h has {} entries", h.len()),
292 });
293 }
294 for i in 0..n {
295 if !jq[i][i].is_zero() {
296 return Err(AnaLowerError::ConvergenceUnprovable {
297 reason: format!("nonzero self-coupling J[{i}][{i}] — not an Ising instance"),
298 });
299 }
300 for k in (i + 1)..n {
301 if jq[i][k] != jq[k][i] {
302 return Err(AnaLowerError::ConvergenceUnprovable {
303 reason: format!("asymmetric coupling at ({i},{k})"),
304 });
305 }
306 }
307 }
308
309 let mut x = vec![0.0_f64; n];
311 let mut noise_state = opts.seed ^ 0x1516_C0DE_5EED_0151;
312 for step in 0..opts.steps {
313 let progress = step as f64 / opts.steps.max(1) as f64;
314 let beta = beta_max * (0.1 + 0.9 * progress);
315 let amp = opts.noise_amp * (1.0 - progress);
316 for i in 0..n {
317 let mut field = h[i];
318 for k in 0..n {
319 field += j[i][k] * x[k];
320 }
321 let dx = -x[i] + (beta * field).tanh();
322 x[i] += opts.rate * dx;
323 if amp > 0.0 {
324 x[i] += amp * centered_uniform(&mut noise_state);
325 }
326 x[i] = x[i].clamp(-1.0, 1.0);
327 }
328 }
329 let mut spins: Vec<i8> = x.iter().map(|v| if *v < 0.0 { -1 } else { 1 }).collect();
330
331 let hq = exact_vec(h)?;
334 loop {
335 let mut flipped = false;
336 for i in 0..n {
337 let mut field = hq[i].clone();
338 for k in 0..n {
339 if k != i {
340 field = field.add(&jq[i][k].mul(&Rational::from_i64(spins[k] as i64)));
341 }
342 }
343 let delta = Rational::from_i64(2 * spins[i] as i64).mul(&field);
344 if delta.is_negative() {
345 spins[i] = -spins[i];
346 flipped = true;
347 }
348 }
349 if !flipped {
350 break;
351 }
352 }
353
354 let s_of = |i: usize| Rational::from_i64(spins[i] as i64);
356 let mut energy = Rational::zero();
357 for i in 0..n {
358 for k in (i + 1)..n {
359 energy = energy.sub(&jq[i][k].mul(&s_of(i)).mul(&s_of(k)));
360 }
361 energy = energy.sub(&hq[i].mul(&s_of(i)));
362 }
363 let energy_f = energy.to_f64();
364 let claim = IsingClaim {
365 j: jq,
366 h: hq,
367 spins: spins.clone(),
368 energy,
369 flip_stable: true,
370 };
371 Ok(IsingOutcome {
372 spins,
373 steps: opts.steps,
374 claim,
375 energy: energy_f,
376 flip_stable: true,
377 })
378}
379
380pub fn with_penalty(
385 q: &[Vec<f64>],
386 b: &[f64],
387 g: &[f64],
388 c: f64,
389 lambda: f64,
390) -> (Vec<Vec<f64>>, Vec<f64>) {
391 let n = q.len();
392 let mut q2: Vec<Vec<f64>> = q.iter().map(|r| r.to_vec()).collect();
393 let mut b2 = b.to_vec();
394 for i in 0..n {
395 for j in 0..n {
396 q2[i][j] += 2.0 * lambda * g[i] * g[j];
397 }
398 b2[i] -= 2.0 * lambda * c * g[i];
399 }
400 (q2, b2)
401}
402
403fn admit(a: &[Vec<Rational>]) -> Result<(), AnaLowerError> {
408 let n = a.len();
409 let refuse = |reason: String| Err(AnaLowerError::ConvergenceUnprovable { reason });
410 if n == 0 {
411 return refuse("empty system".to_string());
412 }
413 for i in 0..n {
414 for j in (i + 1)..n {
415 if a[i][j] != a[j][i] {
416 return refuse(format!(
417 "A is not symmetric at ({i},{j}) — gradient flow solves ∇(½xᵀAx − bᵀx) = 0, \
418 which is only A·x = b for symmetric A"
419 ));
420 }
421 }
422 }
423 let dominant = (0..n).all(|i| {
424 let off: Rational = (0..n)
425 .filter(|j| *j != i)
426 .fold(Rational::zero(), |acc, j| acc.add(&a[i][j].abs()));
427 a[i][i].is_positive() && a[i][i] > off
428 });
429 if dominant {
430 return Ok(());
431 }
432 if (1..=n).all(|k| leading_minor_det(a, k).is_positive()) {
433 return Ok(());
434 }
435 refuse(
436 "A is symmetric but neither strictly diagonally dominant (positive diagonal) nor \
437 Sylvester-positive (all leading principal minors > 0) — convergence unproven"
438 .to_string(),
439 )
440}
441
442fn leading_minor_det(a: &[Vec<Rational>], k: usize) -> Rational {
444 let mut m: Vec<Vec<Rational>> = (0..k).map(|i| a[i][..k].to_vec()).collect();
445 let mut det = Rational::one();
446 for col in 0..k {
447 let Some(p) = (col..k).find(|r| !m[*r][col].is_zero()) else {
448 return Rational::zero();
449 };
450 if p != col {
451 m.swap(p, col);
452 det = det.negated();
453 }
454 det = det.mul(&m[col][col]);
455 let pivot = m[col][col].clone();
456 for r in (col + 1)..k {
457 let factor = m[r][col].div(&pivot).expect("pivot is nonzero");
458 for c in col..k {
459 let sub = factor.mul(&m[col][c]);
460 m[r][c] = m[r][c].sub(&sub);
461 }
462 }
463 }
464 det
465}
466
467fn exact_matrix(a: &[Vec<f64>]) -> Result<Vec<Vec<Rational>>, AnaLowerError> {
468 let n = a.len();
469 a.iter()
470 .map(|row| {
471 if row.len() != n {
472 return Err(AnaLowerError::ConvergenceUnprovable {
473 reason: format!(
474 "ragged matrix: a row has {} entries in an n={n} system",
475 row.len()
476 ),
477 });
478 }
479 exact_vec(row)
480 })
481 .collect()
482}
483
484fn exact_vec(v: &[f64]) -> Result<Vec<Rational>, AnaLowerError> {
485 v.iter()
486 .map(|x| {
487 rational_from_f64_exact(*x).ok_or_else(|| AnaLowerError::ConvergenceUnprovable {
488 reason: format!("non-finite entry {x}"),
489 })
490 })
491 .collect()
492}
493
494fn centered_uniform(state: &mut u64) -> f64 {
496 *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
497 let mut z = *state;
498 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
499 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
500 z ^= z >> 31;
501 ((z >> 11) as f64 + 0.5) / (1u64 << 52) as f64 - 1.0
502}