1use std::collections::{BTreeSet, HashMap, HashSet};
26use std::sync::Arc;
27
28use super::nl_external::{EvalResult, ExternalArg, ExternalLibrary, ExternalResolver};
29use super::nl_reader::{BinOp, CmpOp, Expr, FuncallArg, UnaryOp};
30
31#[derive(Debug, Clone)]
35pub enum TapeOp {
36 Const(f64),
37 Var(usize),
38 Add(usize, usize),
39 Sub(usize, usize),
40 Mul(usize, usize),
41 Div(usize, usize),
42 Pow(usize, usize),
43 Neg(usize),
44 Abs(usize),
45 Sqrt(usize),
46 Exp(usize),
47 Log(usize),
48 Log10(usize),
49 Sin(usize),
50 Cos(usize),
51 Tan(usize),
52 Atan(usize),
53 Acos(usize),
54 Sinh(usize),
55 Cosh(usize),
56 Tanh(usize),
57 Asin(usize),
58 Acosh(usize),
59 Asinh(usize),
60 Atanh(usize),
61 Erf(usize),
69 XLogX(usize),
81 CEntropy(usize, usize),
90 Atan2(usize, usize),
93 Min(usize, usize),
98 Max(usize, usize),
101 Cmp(CmpOp, usize, usize),
105 And(usize, usize),
107 Or(usize, usize),
109 Not(usize),
111 Select(usize, usize, usize),
117 Funcall(Box<FuncallData>),
125}
126
127#[derive(Debug, Clone)]
132pub struct FuncallData {
133 pub lib: Arc<ExternalLibrary>,
134 pub name: String,
135 pub args: Vec<TapeFuncallArg>,
136}
137
138#[derive(Debug, Clone)]
142pub enum TapeFuncallArg {
143 Tape(usize),
144 Str(String),
145}
146
147#[inline]
163pub(crate) fn ln_ratio(a: f64, b: f64) -> f64 {
164 let q = a / b;
165 if q.is_finite() && q > 0.0 {
166 let t = (a - b) / b;
167 if t.abs() < 0.5 { t.ln_1p() } else { q.ln() }
168 } else {
169 a.ln() - b.ln()
170 }
171}
172
173#[inline]
185pub(crate) fn xlogx(a: f64) -> f64 {
186 if a == 0.0 { 0.0 } else { a * a.ln() }
187}
188
189#[inline]
197pub(crate) fn centropy(a: f64, b: f64) -> f64 {
198 if a == 0.0 { 0.0 } else { a * ln_ratio(a, b) }
199}
200
201#[inline]
203pub(crate) fn xlogx_d1(a: f64) -> f64 {
204 a.ln() + 1.0
205}
206
207#[inline]
212pub(crate) fn xlogx_d2(a: f64) -> f64 {
213 1.0 / a
214}
215
216#[inline]
218pub(crate) fn centropy_da(a: f64, b: f64) -> f64 {
219 ln_ratio(a, b) + 1.0
220}
221
222#[inline]
224pub(crate) fn centropy_db(a: f64, b: f64) -> f64 {
225 -(a / b)
226}
227
228#[inline]
230pub(crate) fn centropy_daa(a: f64) -> f64 {
231 1.0 / a
232}
233
234#[inline]
236pub(crate) fn centropy_dab(b: f64) -> f64 {
237 -1.0 / b
238}
239
240#[inline]
245pub(crate) fn centropy_dbb(a: f64, b: f64) -> f64 {
246 (a / b) / b
247}
248
249#[inline]
256pub(crate) fn erf(u: f64) -> f64 {
257 libm::erf(u)
258}
259
260#[inline]
265pub(crate) fn erf_d1(u: f64) -> f64 {
266 std::f64::consts::FRAC_2_SQRT_PI * (-u * u).exp()
267}
268
269#[inline]
280pub(crate) fn erf_d2(u: f64) -> f64 {
281 -2.0 * (u * erf_d1(u))
282}
283
284#[inline]
287fn cmp_holds(op: CmpOp, a: f64, b: f64) -> bool {
288 match op {
289 CmpOp::Lt => a < b,
290 CmpOp::Le => a <= b,
291 CmpOp::Eq => a == b,
292 CmpOp::Ge => a >= b,
293 CmpOp::Gt => a > b,
294 CmpOp::Ne => a != b,
295 }
296}
297
298fn funcall_to_ext_args<'a>(args: &'a [TapeFuncallArg], vals: &[f64]) -> Vec<ExternalArg<'a>> {
299 args.iter()
300 .map(|a| match a {
301 TapeFuncallArg::Tape(idx) => ExternalArg::Real(vals[*idx]),
302 TapeFuncallArg::Str(s) => ExternalArg::Str(s.as_str()),
303 })
304 .collect()
305}
306
307fn ext_eval_or_nan(
320 lib: &ExternalLibrary,
321 name: &str,
322 call_args: &[ExternalArg<'_>],
323 n_args: usize,
324 want_derivs: bool,
325 want_hes: bool,
326) -> EvalResult {
327 lib.eval(name, call_args, want_derivs, want_hes)
328 .unwrap_or_else(|_| EvalResult {
329 value: f64::NAN,
330 derivs: want_derivs.then(|| vec![f64::NAN; n_args]),
331 hessian: want_hes.then(|| vec![f64::NAN; n_args * (n_args + 1) / 2]),
332 })
333}
334
335#[derive(Debug, Clone)]
338pub struct Tape {
339 pub ops: Vec<TapeOp>,
340}
341
342impl Tape {
343 pub fn build(expr: &Expr) -> Self {
347 Self::build_with_externals(expr, &ExternalResolver::default())
348 }
349
350 pub fn build_with_externals(expr: &Expr, resolver: &ExternalResolver) -> Self {
355 let mut ops = Vec::new();
356 let mut cache: HashMap<*const Expr, usize> = HashMap::new();
357 build_recursive(expr, &mut ops, &mut cache, resolver);
358 Tape { ops }
359 }
360
361 pub fn forward(&self, x: &[f64]) -> Vec<f64> {
364 let mut vals: Vec<f64> = Vec::with_capacity(self.ops.len());
365 for op in &self.ops {
366 let v = match op {
367 TapeOp::Const(c) => *c,
368 TapeOp::Var(i) => x[*i],
369 TapeOp::Add(a, b) => vals[*a] + vals[*b],
370 TapeOp::Sub(a, b) => vals[*a] - vals[*b],
371 TapeOp::Mul(a, b) => vals[*a] * vals[*b],
372 TapeOp::Div(a, b) => vals[*a] / vals[*b],
373 TapeOp::Pow(a, b) => vals[*a].powf(vals[*b]),
374 TapeOp::Neg(a) => -vals[*a],
375 TapeOp::Abs(a) => vals[*a].abs(),
376 TapeOp::Sqrt(a) => vals[*a].sqrt(),
377 TapeOp::Exp(a) => vals[*a].exp(),
378 TapeOp::Log(a) => vals[*a].ln(),
379 TapeOp::Log10(a) => vals[*a].log10(),
380 TapeOp::Sin(a) => vals[*a].sin(),
381 TapeOp::Cos(a) => vals[*a].cos(),
382 TapeOp::Tan(a) => vals[*a].tan(),
383 TapeOp::Atan(a) => vals[*a].atan(),
384 TapeOp::Acos(a) => vals[*a].acos(),
385 TapeOp::Sinh(a) => vals[*a].sinh(),
386 TapeOp::Cosh(a) => vals[*a].cosh(),
387 TapeOp::Tanh(a) => vals[*a].tanh(),
388 TapeOp::Asin(a) => vals[*a].asin(),
389 TapeOp::Acosh(a) => vals[*a].acosh(),
390 TapeOp::Asinh(a) => vals[*a].asinh(),
391 TapeOp::Atanh(a) => vals[*a].atanh(),
392 TapeOp::Erf(a) => erf(vals[*a]),
393 TapeOp::XLogX(a) => xlogx(vals[*a]),
394 TapeOp::CEntropy(a, b) => centropy(vals[*a], vals[*b]),
395 TapeOp::Atan2(a, b) => vals[*a].atan2(vals[*b]),
396 TapeOp::Min(a, b) => vals[*a].min(vals[*b]),
397 TapeOp::Max(a, b) => vals[*a].max(vals[*b]),
398 TapeOp::Cmp(op, a, b) => f64::from(cmp_holds(*op, vals[*a], vals[*b])),
399 TapeOp::And(a, b) => f64::from(vals[*a] != 0.0 && vals[*b] != 0.0),
400 TapeOp::Or(a, b) => f64::from(vals[*a] != 0.0 || vals[*b] != 0.0),
401 TapeOp::Not(a) => f64::from(vals[*a] == 0.0),
402 TapeOp::Select(c, t, e) => {
403 if vals[*c] != 0.0 {
404 vals[*t]
405 } else {
406 vals[*e]
407 }
408 }
409 TapeOp::Funcall(fc) => {
410 let FuncallData { lib, name, args } = fc.as_ref();
411 let call_args = funcall_to_ext_args(args, &vals);
412 let res = ext_eval_or_nan(lib, name, &call_args, args.len(), false, false);
413 res.value
414 }
415 };
416 vals.push(v);
417 }
418 vals
419 }
420
421 pub fn eval(&self, x: &[f64]) -> f64 {
422 let vals = self.forward(x);
423 *vals.last().unwrap_or(&0.0)
424 }
425
426 pub fn gradient_seed(&self, x: &[f64], seed: f64, grad: &mut [f64]) {
431 if seed == 0.0 || self.ops.is_empty() {
432 return;
433 }
434 let vals = self.forward(x);
435 self.reverse(&vals, seed, grad);
436 }
437
438 pub fn gradient_seed_into(
450 &self,
451 x: &[f64],
452 seed: f64,
453 grad: &mut [f64],
454 vals: &mut [f64],
455 adj: &mut [f64],
456 ) {
457 if seed == 0.0 || self.ops.is_empty() {
458 return;
459 }
460 debug_assert!(vals.len() >= self.ops.len());
461 self.forward_into(x, vals);
462 self.reverse_into(vals, seed, grad, adj);
463 }
464
465 fn reverse(&self, vals: &[f64], seed: f64, grad: &mut [f64]) {
466 let n = self.ops.len();
467 let mut adj = vec![0.0f64; n];
468 self.reverse_into(vals, seed, grad, &mut adj);
469 }
470
471 fn reverse_into(&self, vals: &[f64], seed: f64, grad: &mut [f64], adj: &mut [f64]) {
476 let n = self.ops.len();
477 debug_assert!(adj.len() >= n);
478 adj[..n].fill(0.0);
479 adj[n - 1] = seed;
480
481 for i in (0..n).rev() {
482 let a = adj[i];
483 if a == 0.0 {
484 continue;
485 }
486 match &self.ops[i] {
487 TapeOp::Const(_) => {}
488 TapeOp::Var(j) => {
489 grad[*j] += a;
490 }
491 TapeOp::Add(l, r) => {
492 adj[*l] += a;
493 adj[*r] += a;
494 }
495 TapeOp::Sub(l, r) => {
496 adj[*l] += a;
497 adj[*r] -= a;
498 }
499 TapeOp::Mul(l, r) => {
500 adj[*l] += a * vals[*r];
501 adj[*r] += a * vals[*l];
502 }
503 TapeOp::Div(l, r) => {
504 let rv = vals[*r];
508 adj[*l] += a / rv;
509 adj[*r] -= a * vals[i] / rv;
510 }
511 TapeOp::Pow(l, r) => {
512 let lv = vals[*l];
513 let rv = vals[*r];
514 if rv != 0.0 {
515 adj[*l] += a * rv * lv.powf(rv - 1.0);
516 }
517 if lv > 0.0 {
518 adj[*r] += a * vals[i] * lv.ln();
519 }
520 }
521 TapeOp::Neg(j) => {
522 adj[*j] -= a;
523 }
524 TapeOp::Abs(j) => {
525 if vals[*j] >= 0.0 {
526 adj[*j] += a;
527 } else {
528 adj[*j] -= a;
529 }
530 }
531 TapeOp::Sqrt(j) => {
532 let sv = vals[i];
533 if sv > 0.0 {
534 adj[*j] += a * 0.5 / sv;
535 }
536 }
537 TapeOp::Exp(j) => {
538 adj[*j] += a * vals[i];
539 }
540 TapeOp::Log(j) => {
541 adj[*j] += a / vals[*j];
542 }
543 TapeOp::Log10(j) => {
544 adj[*j] += a / (vals[*j] * std::f64::consts::LN_10);
545 }
546 TapeOp::Sin(j) => {
547 adj[*j] += a * vals[*j].cos();
548 }
549 TapeOp::Cos(j) => {
550 adj[*j] -= a * vals[*j].sin();
551 }
552 TapeOp::Tan(j) => {
553 let t = vals[i];
554 adj[*j] += a * (1.0 + t * t);
555 }
556 TapeOp::Atan(j) => {
557 let u = vals[*j];
558 adj[*j] += a / (1.0 + u * u);
559 }
560 TapeOp::Acos(j) => {
561 let u = vals[*j];
562 adj[*j] -= a / (1.0 - u * u).sqrt();
563 }
564 TapeOp::Sinh(j) => {
565 adj[*j] += a * vals[*j].cosh();
566 }
567 TapeOp::Cosh(j) => {
568 adj[*j] += a * vals[*j].sinh();
569 }
570 TapeOp::Tanh(j) => {
571 let t = vals[i];
572 adj[*j] += a * (1.0 - t * t);
573 }
574 TapeOp::Asin(j) => {
575 let u = vals[*j];
576 adj[*j] += a / (1.0 - u * u).sqrt();
577 }
578 TapeOp::Acosh(j) => {
579 let u = vals[*j];
580 adj[*j] += a / (u * u - 1.0).sqrt();
581 }
582 TapeOp::Asinh(j) => {
583 let u = vals[*j];
584 adj[*j] += a / (u * u + 1.0).sqrt();
585 }
586 TapeOp::Atanh(j) => {
587 let u = vals[*j];
588 adj[*j] += a / (1.0 - u * u);
589 }
590 TapeOp::Erf(j) => {
591 adj[*j] += a * erf_d1(vals[*j]);
592 }
593 TapeOp::XLogX(j) => {
594 adj[*j] += a * xlogx_d1(vals[*j]);
595 }
596 TapeOp::CEntropy(l, r) => {
597 adj[*l] += a * centropy_da(vals[*l], vals[*r]);
598 adj[*r] += a * centropy_db(vals[*l], vals[*r]);
599 }
600 TapeOp::Atan2(l, r) => {
601 let y = vals[*l];
602 let x = vals[*r];
603 let d = y * y + x * x;
604 adj[*l] += a * (x / d);
605 adj[*r] += a * (-y / d);
606 }
607 TapeOp::Min(l, r) => {
611 if vals[*l] <= vals[*r] {
612 adj[*l] += a;
613 } else {
614 adj[*r] += a;
615 }
616 }
617 TapeOp::Max(l, r) => {
618 if vals[*l] >= vals[*r] {
619 adj[*l] += a;
620 } else {
621 adj[*r] += a;
622 }
623 }
624 TapeOp::Cmp(_, _, _) | TapeOp::And(_, _) | TapeOp::Or(_, _) | TapeOp::Not(_) => {}
627 TapeOp::Select(c, t, e) => {
630 if vals[*c] != 0.0 {
631 adj[*t] += a;
632 } else {
633 adj[*e] += a;
634 }
635 }
636 TapeOp::Funcall(fc) => {
637 let FuncallData { lib, name, args } = fc.as_ref();
638 let call_args = funcall_to_ext_args(args, vals);
639 let res = ext_eval_or_nan(lib, name, &call_args, args.len(), true, false);
640 let derivs = res.derivs.expect("want_derivs=true returns derivs");
641 let mut k = 0usize;
642 for arg in args {
643 if let TapeFuncallArg::Tape(idx) = arg {
644 adj[*idx] += a * derivs[k];
645 k += 1;
646 }
647 }
648 }
649 }
650 }
651 }
652
653 pub fn variables(&self) -> Vec<usize> {
655 let mut s: BTreeSet<usize> = BTreeSet::new();
656 for op in &self.ops {
657 if let TapeOp::Var(j) = op {
658 s.insert(*j);
659 }
660 }
661 s.into_iter().collect()
662 }
663
664 fn forward_tangent(&self, vals: &[f64], seed_var: usize, dot: &mut [f64]) {
669 let n = self.ops.len();
670 debug_assert_eq!(dot.len(), n);
671 for i in 0..n {
672 dot[i] = match &self.ops[i] {
673 TapeOp::Const(_) => 0.0,
674 TapeOp::Var(k) => {
675 if *k == seed_var {
676 1.0
677 } else {
678 0.0
679 }
680 }
681 TapeOp::Add(a, b) => dot[*a] + dot[*b],
682 TapeOp::Sub(a, b) => dot[*a] - dot[*b],
683 TapeOp::Mul(a, b) => dot[*a] * vals[*b] + vals[*a] * dot[*b],
684 TapeOp::Div(a, b) => {
685 (dot[*a] - vals[i] * dot[*b]) / vals[*b]
693 }
694 TapeOp::Pow(a, b) => {
695 let u = vals[*a];
696 let r = vals[*b];
697 let du = dot[*a];
698 let dr = dot[*b];
699 let mut result = 0.0;
700 if r != 0.0 {
705 result += r * u.powf(r - 1.0) * du;
706 }
707 if u > 0.0 {
708 result += vals[i] * u.ln() * dr;
709 }
710 result
711 }
712 TapeOp::Neg(a) => -dot[*a],
713 TapeOp::Abs(a) => {
714 if vals[*a] >= 0.0 {
715 dot[*a]
716 } else {
717 -dot[*a]
718 }
719 }
720 TapeOp::Sqrt(a) => {
721 let sv = vals[i];
722 if sv > 0.0 { dot[*a] * 0.5 / sv } else { 0.0 }
723 }
724 TapeOp::Exp(a) => dot[*a] * vals[i],
725 TapeOp::Log(a) => dot[*a] / vals[*a],
726 TapeOp::Log10(a) => dot[*a] / (vals[*a] * std::f64::consts::LN_10),
727 TapeOp::Sin(a) => dot[*a] * vals[*a].cos(),
728 TapeOp::Cos(a) => -dot[*a] * vals[*a].sin(),
729 TapeOp::Tan(a) => {
730 let t = vals[i];
731 dot[*a] * (1.0 + t * t)
732 }
733 TapeOp::Atan(a) => {
734 let u = vals[*a];
735 dot[*a] / (1.0 + u * u)
736 }
737 TapeOp::Acos(a) => {
738 let u = vals[*a];
739 -dot[*a] / (1.0 - u * u).sqrt()
740 }
741 TapeOp::Sinh(a) => dot[*a] * vals[*a].cosh(),
742 TapeOp::Cosh(a) => dot[*a] * vals[*a].sinh(),
743 TapeOp::Tanh(a) => {
744 let t = vals[i];
745 dot[*a] * (1.0 - t * t)
746 }
747 TapeOp::Asin(a) => {
748 let u = vals[*a];
749 dot[*a] / (1.0 - u * u).sqrt()
750 }
751 TapeOp::Acosh(a) => {
752 let u = vals[*a];
753 dot[*a] / (u * u - 1.0).sqrt()
754 }
755 TapeOp::Asinh(a) => {
756 let u = vals[*a];
757 dot[*a] / (u * u + 1.0).sqrt()
758 }
759 TapeOp::Atanh(a) => {
760 let u = vals[*a];
761 dot[*a] / (1.0 - u * u)
762 }
763 TapeOp::Erf(a) => erf_d1(vals[*a]) * dot[*a],
764 TapeOp::XLogX(a) => xlogx_d1(vals[*a]) * dot[*a],
765 TapeOp::CEntropy(a, b) => {
766 centropy_da(vals[*a], vals[*b]) * dot[*a]
767 + centropy_db(vals[*a], vals[*b]) * dot[*b]
768 }
769 TapeOp::Atan2(a, b) => {
770 let y = vals[*a];
771 let x = vals[*b];
772 let d = y * y + x * x;
773 (x * dot[*a] - y * dot[*b]) / d
774 }
775 TapeOp::Min(a, b) => {
777 if vals[*a] <= vals[*b] {
778 dot[*a]
779 } else {
780 dot[*b]
781 }
782 }
783 TapeOp::Max(a, b) => {
784 if vals[*a] >= vals[*b] {
785 dot[*a]
786 } else {
787 dot[*b]
788 }
789 }
790 TapeOp::Cmp(_, _, _) | TapeOp::And(_, _) | TapeOp::Or(_, _) | TapeOp::Not(_) => 0.0,
791 TapeOp::Select(c, t, e) => {
792 if vals[*c] != 0.0 {
793 dot[*t]
794 } else {
795 dot[*e]
796 }
797 }
798 TapeOp::Funcall(fc) => {
799 let FuncallData { lib, name, args } = fc.as_ref();
800 let call_args = funcall_to_ext_args(args, vals);
801 let res = ext_eval_or_nan(lib, name, &call_args, args.len(), true, false);
802 let derivs = res.derivs.expect("want_derivs=true returns derivs");
803 let mut acc = 0.0;
804 let mut k = 0usize;
805 for arg in args {
806 if let TapeFuncallArg::Tape(idx) = arg {
807 acc += derivs[k] * dot[*idx];
808 k += 1;
809 }
810 }
811 acc
812 }
813 };
814 }
815 }
816
817 pub fn forward_into(&self, x: &[f64], vals: &mut [f64]) {
821 let n = self.ops.len();
822 debug_assert!(vals.len() >= n);
823 for i in 0..n {
824 vals[i] = match &self.ops[i] {
825 TapeOp::Const(c) => *c,
826 TapeOp::Var(j) => x[*j],
827 TapeOp::Add(a, b) => vals[*a] + vals[*b],
828 TapeOp::Sub(a, b) => vals[*a] - vals[*b],
829 TapeOp::Mul(a, b) => vals[*a] * vals[*b],
830 TapeOp::Div(a, b) => vals[*a] / vals[*b],
831 TapeOp::Pow(a, b) => vals[*a].powf(vals[*b]),
832 TapeOp::Neg(a) => -vals[*a],
833 TapeOp::Abs(a) => vals[*a].abs(),
834 TapeOp::Sqrt(a) => vals[*a].sqrt(),
835 TapeOp::Exp(a) => vals[*a].exp(),
836 TapeOp::Log(a) => vals[*a].ln(),
837 TapeOp::Log10(a) => vals[*a].log10(),
838 TapeOp::Sin(a) => vals[*a].sin(),
839 TapeOp::Cos(a) => vals[*a].cos(),
840 TapeOp::Tan(a) => vals[*a].tan(),
841 TapeOp::Atan(a) => vals[*a].atan(),
842 TapeOp::Acos(a) => vals[*a].acos(),
843 TapeOp::Sinh(a) => vals[*a].sinh(),
844 TapeOp::Cosh(a) => vals[*a].cosh(),
845 TapeOp::Tanh(a) => vals[*a].tanh(),
846 TapeOp::Asin(a) => vals[*a].asin(),
847 TapeOp::Acosh(a) => vals[*a].acosh(),
848 TapeOp::Asinh(a) => vals[*a].asinh(),
849 TapeOp::Atanh(a) => vals[*a].atanh(),
850 TapeOp::Erf(a) => erf(vals[*a]),
851 TapeOp::XLogX(a) => xlogx(vals[*a]),
852 TapeOp::CEntropy(a, b) => centropy(vals[*a], vals[*b]),
853 TapeOp::Atan2(a, b) => vals[*a].atan2(vals[*b]),
854 TapeOp::Min(a, b) => vals[*a].min(vals[*b]),
855 TapeOp::Max(a, b) => vals[*a].max(vals[*b]),
856 TapeOp::Cmp(op, a, b) => f64::from(cmp_holds(*op, vals[*a], vals[*b])),
857 TapeOp::And(a, b) => f64::from(vals[*a] != 0.0 && vals[*b] != 0.0),
858 TapeOp::Or(a, b) => f64::from(vals[*a] != 0.0 || vals[*b] != 0.0),
859 TapeOp::Not(a) => f64::from(vals[*a] == 0.0),
860 TapeOp::Select(c, t, e) => {
861 if vals[*c] != 0.0 {
862 vals[*t]
863 } else {
864 vals[*e]
865 }
866 }
867 TapeOp::Funcall(fc) => {
868 let FuncallData { lib, name, args } = fc.as_ref();
869 let call_args = funcall_to_ext_args(args, &*vals);
870 let res = ext_eval_or_nan(lib, name, &call_args, args.len(), false, false);
871 res.value
872 }
873 };
874 }
875 }
876
877 pub fn eval_into(&self, x: &[f64], vals: &mut [f64]) -> f64 {
888 let n = self.ops.len();
889 if n == 0 {
890 return 0.0;
891 }
892 self.forward_into(x, vals);
893 vals[n - 1]
894 }
895
896 pub fn hessian_directional(
913 &self,
914 vals: &[f64],
915 seed: &[f64],
916 weight: f64,
917 out: &mut [f64],
918 dot: &mut [f64],
919 adj: &mut [f64],
920 adj_dot: &mut [f64],
921 ) {
922 let n = self.ops.len();
923 if n == 0 || weight == 0.0 {
924 return;
925 }
926 debug_assert!(vals.len() >= n);
927 debug_assert!(dot.len() >= n);
928 debug_assert!(adj.len() >= n);
929 debug_assert!(adj_dot.len() >= n);
930
931 for i in 0..n {
935 dot[i] = match &self.ops[i] {
936 TapeOp::Const(_) => 0.0,
937 TapeOp::Var(k) => seed[*k],
938 TapeOp::Add(a, b) => dot[*a] + dot[*b],
939 TapeOp::Sub(a, b) => dot[*a] - dot[*b],
940 TapeOp::Mul(a, b) => dot[*a] * vals[*b] + vals[*a] * dot[*b],
941 TapeOp::Div(a, b) => {
942 (dot[*a] - vals[i] * dot[*b]) / vals[*b]
950 }
951 TapeOp::Pow(a, b) => {
952 let u = vals[*a];
953 let r = vals[*b];
954 let du = dot[*a];
955 let dr = dot[*b];
956 let mut result = 0.0;
957 if r != 0.0 {
962 result += r * u.powf(r - 1.0) * du;
963 }
964 if u > 0.0 {
965 result += vals[i] * u.ln() * dr;
966 }
967 result
968 }
969 TapeOp::Neg(a) => -dot[*a],
970 TapeOp::Abs(a) => {
971 if vals[*a] >= 0.0 {
972 dot[*a]
973 } else {
974 -dot[*a]
975 }
976 }
977 TapeOp::Sqrt(a) => {
978 let sv = vals[i];
979 if sv > 0.0 { dot[*a] * 0.5 / sv } else { 0.0 }
980 }
981 TapeOp::Exp(a) => vals[i] * dot[*a],
982 TapeOp::Log(a) => dot[*a] / vals[*a],
983 TapeOp::Log10(a) => dot[*a] / (vals[*a] * std::f64::consts::LN_10),
984 TapeOp::Sin(a) => vals[*a].cos() * dot[*a],
985 TapeOp::Cos(a) => -vals[*a].sin() * dot[*a],
986 TapeOp::Tan(a) => {
987 let t = vals[i];
988 (1.0 + t * t) * dot[*a]
989 }
990 TapeOp::Atan(a) => {
991 let u = vals[*a];
992 dot[*a] / (1.0 + u * u)
993 }
994 TapeOp::Acos(a) => {
995 let u = vals[*a];
996 -dot[*a] / (1.0 - u * u).sqrt()
997 }
998 TapeOp::Sinh(a) => dot[*a] * vals[*a].cosh(),
999 TapeOp::Cosh(a) => dot[*a] * vals[*a].sinh(),
1000 TapeOp::Tanh(a) => {
1001 let t = vals[i];
1002 (1.0 - t * t) * dot[*a]
1003 }
1004 TapeOp::Asin(a) => {
1005 let u = vals[*a];
1006 dot[*a] / (1.0 - u * u).sqrt()
1007 }
1008 TapeOp::Acosh(a) => {
1009 let u = vals[*a];
1010 dot[*a] / (u * u - 1.0).sqrt()
1011 }
1012 TapeOp::Asinh(a) => {
1013 let u = vals[*a];
1014 dot[*a] / (u * u + 1.0).sqrt()
1015 }
1016 TapeOp::Atanh(a) => {
1017 let u = vals[*a];
1018 dot[*a] / (1.0 - u * u)
1019 }
1020 TapeOp::Erf(a) => erf_d1(vals[*a]) * dot[*a],
1021 TapeOp::XLogX(a) => xlogx_d1(vals[*a]) * dot[*a],
1022 TapeOp::CEntropy(a, b) => {
1023 centropy_da(vals[*a], vals[*b]) * dot[*a]
1024 + centropy_db(vals[*a], vals[*b]) * dot[*b]
1025 }
1026 TapeOp::Atan2(a, b) => {
1027 let y = vals[*a];
1028 let x = vals[*b];
1029 let d = y * y + x * x;
1030 (x * dot[*a] - y * dot[*b]) / d
1031 }
1032 TapeOp::Min(a, b) => {
1034 if vals[*a] <= vals[*b] {
1035 dot[*a]
1036 } else {
1037 dot[*b]
1038 }
1039 }
1040 TapeOp::Max(a, b) => {
1041 if vals[*a] >= vals[*b] {
1042 dot[*a]
1043 } else {
1044 dot[*b]
1045 }
1046 }
1047 TapeOp::Cmp(_, _, _) | TapeOp::And(_, _) | TapeOp::Or(_, _) | TapeOp::Not(_) => 0.0,
1048 TapeOp::Select(c, t, e) => {
1049 if vals[*c] != 0.0 {
1050 dot[*t]
1051 } else {
1052 dot[*e]
1053 }
1054 }
1055 TapeOp::Funcall(fc) => {
1056 let FuncallData { lib, name, args } = fc.as_ref();
1057 let call_args = funcall_to_ext_args(args, vals);
1058 let res = ext_eval_or_nan(lib, name, &call_args, args.len(), true, false);
1059 let derivs = res.derivs.expect("want_derivs=true returns derivs");
1060 let mut acc = 0.0;
1061 let mut k = 0usize;
1062 for arg in args {
1063 if let TapeFuncallArg::Tape(idx) = arg {
1064 acc += derivs[k] * dot[*idx];
1065 k += 1;
1066 }
1067 }
1068 acc
1069 }
1070 };
1071 }
1072
1073 for slot in adj.iter_mut().take(n) {
1077 *slot = 0.0;
1078 }
1079 for slot in adj_dot.iter_mut().take(n) {
1080 *slot = 0.0;
1081 }
1082 adj[n - 1] = 1.0;
1083
1084 for i in (0..n).rev() {
1085 let w = adj[i];
1086 let wd = adj_dot[i];
1087 if w == 0.0 && wd == 0.0 {
1088 continue;
1089 }
1090 match &self.ops[i] {
1091 TapeOp::Const(_) => {}
1092 TapeOp::Var(k) => {
1093 if wd != 0.0 {
1094 out[*k] += weight * wd;
1095 }
1096 }
1097 TapeOp::Add(a, b) => {
1098 adj[*a] += w;
1099 adj[*b] += w;
1100 adj_dot[*a] += wd;
1101 adj_dot[*b] += wd;
1102 }
1103 TapeOp::Sub(a, b) => {
1104 adj[*a] += w;
1105 adj[*b] -= w;
1106 adj_dot[*a] += wd;
1107 adj_dot[*b] -= wd;
1108 }
1109 TapeOp::Mul(a, b) => {
1110 adj[*a] += w * vals[*b];
1111 adj[*b] += w * vals[*a];
1112 adj_dot[*a] += wd * vals[*b] + w * dot[*b];
1113 adj_dot[*b] += wd * vals[*a] + w * dot[*a];
1114 }
1115 TapeOp::Div(a, b) => {
1116 let vb = vals[*b];
1123 let q = vals[i];
1124 let qd = dot[i];
1125 adj[*a] += w / vb;
1126 adj_dot[*a] += wd / vb - w * (dot[*b] / vb) / vb;
1127 adj[*b] -= w * q / vb;
1128 adj_dot[*b] += -(wd * q) / vb + (w / vb) * (-qd + q * (dot[*b] / vb));
1129 }
1130 TapeOp::Pow(a, b) => {
1131 let u = vals[*a];
1132 let r = vals[*b];
1133 let du = dot[*a];
1134 let dr = dot[*b];
1135 if r != 0.0 {
1136 if u != 0.0 {
1137 let p_a = r * u.powf(r - 1.0);
1138 adj[*a] += w * p_a;
1139 let mut dp_a = dr * u.powf(r - 1.0);
1140 if u > 0.0 {
1141 dp_a += r * u.powf(r - 1.0) * ((r - 1.0) * du / u + dr * u.ln());
1142 } else {
1143 dp_a += r * (r - 1.0) * u.powf(r - 2.0) * du;
1144 }
1145 adj_dot[*a] += wd * p_a + w * dp_a;
1146 } else if r >= 2.0 {
1147 let p_a = 0.0;
1148 adj[*a] += w * p_a;
1149 let dp_a = if r == 2.0 {
1150 2.0 * du
1151 } else {
1152 r * (r - 1.0) * (0.0_f64).powf(r - 2.0) * du
1153 };
1154 adj_dot[*a] += wd * p_a + w * dp_a;
1155 }
1156 }
1157 if u > 0.0 {
1158 let ln_u = u.ln();
1159 let p_b = vals[i] * ln_u;
1160 adj[*b] += w * p_b;
1161 let dur = vals[i] * (r * du / u + dr * ln_u);
1162 let dp_b = dur * ln_u + vals[i] * du / u;
1163 adj_dot[*b] += wd * p_b + w * dp_b;
1164 }
1165 }
1166 TapeOp::Neg(a) => {
1167 adj[*a] -= w;
1168 adj_dot[*a] -= wd;
1169 }
1170 TapeOp::Abs(a) => {
1171 let s = if vals[*a] >= 0.0 { 1.0 } else { -1.0 };
1172 adj[*a] += w * s;
1173 adj_dot[*a] += wd * s;
1174 }
1175 TapeOp::Sqrt(a) => {
1176 let sv = vals[i];
1177 if sv > 0.0 {
1178 let fp = 0.5 / sv;
1179 let fpp = -0.25 / (vals[*a] * sv);
1180 adj[*a] += w * fp;
1181 adj_dot[*a] += wd * fp + w * fpp * dot[*a];
1182 }
1183 }
1184 TapeOp::Exp(a) => {
1185 let ev = vals[i];
1186 adj[*a] += w * ev;
1187 adj_dot[*a] += wd * ev + w * ev * dot[*a];
1188 }
1189 TapeOp::Log(a) => {
1190 let u = vals[*a];
1191 adj[*a] += w / u;
1192 adj_dot[*a] += wd / u + w * (-1.0 / (u * u)) * dot[*a];
1193 }
1194 TapeOp::Log10(a) => {
1195 let u = vals[*a];
1196 let c = std::f64::consts::LN_10;
1197 adj[*a] += w / (u * c);
1198 adj_dot[*a] += wd / (u * c) + w * (-1.0 / (u * u * c)) * dot[*a];
1199 }
1200 TapeOp::Sin(a) => {
1201 let u = vals[*a];
1202 let cu = u.cos();
1203 adj[*a] += w * cu;
1204 adj_dot[*a] += wd * cu + w * (-u.sin()) * dot[*a];
1205 }
1206 TapeOp::Cos(a) => {
1207 let u = vals[*a];
1208 let su = u.sin();
1209 adj[*a] -= w * su;
1210 adj_dot[*a] += wd * (-su) + w * (-u.cos()) * dot[*a];
1211 }
1212 TapeOp::Tan(a) => {
1213 let t = vals[i];
1214 let gp = 1.0 + t * t;
1215 let gpp = 2.0 * t * gp;
1216 adj[*a] += w * gp;
1217 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1218 }
1219 TapeOp::Atan(a) => {
1220 let u = vals[*a];
1221 let d = 1.0 + u * u;
1222 let gp = 1.0 / d;
1223 let gpp = -2.0 * u / (d * d);
1224 adj[*a] += w * gp;
1225 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1226 }
1227 TapeOp::Acos(a) => {
1228 let u = vals[*a];
1229 let s = 1.0 - u * u;
1230 let r = s.sqrt();
1231 let gp = -1.0 / r;
1232 let gpp = -u / (s * r);
1233 adj[*a] += w * gp;
1234 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1235 }
1236 TapeOp::Sinh(a) => {
1237 let u = vals[*a];
1238 let gp = u.cosh();
1239 let gpp = u.sinh();
1240 adj[*a] += w * gp;
1241 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1242 }
1243 TapeOp::Cosh(a) => {
1244 let u = vals[*a];
1245 let gp = u.sinh();
1246 let gpp = u.cosh();
1247 adj[*a] += w * gp;
1248 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1249 }
1250 TapeOp::Tanh(a) => {
1251 let t = vals[i];
1252 let gp = 1.0 - t * t;
1253 let gpp = -2.0 * t * gp;
1254 adj[*a] += w * gp;
1255 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1256 }
1257 TapeOp::Asin(a) => {
1258 let u = vals[*a];
1259 let s = 1.0 - u * u;
1260 let r = s.sqrt();
1261 let gp = 1.0 / r;
1262 let gpp = u / (s * r);
1263 adj[*a] += w * gp;
1264 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1265 }
1266 TapeOp::Acosh(a) => {
1267 let u = vals[*a];
1268 let s = u * u - 1.0;
1269 let r = s.sqrt();
1270 let gp = 1.0 / r;
1271 let gpp = -u / (s * r);
1272 adj[*a] += w * gp;
1273 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1274 }
1275 TapeOp::Asinh(a) => {
1276 let u = vals[*a];
1277 let s = u * u + 1.0;
1278 let r = s.sqrt();
1279 let gp = 1.0 / r;
1280 let gpp = -u / (s * r);
1281 adj[*a] += w * gp;
1282 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1283 }
1284 TapeOp::Atanh(a) => {
1285 let u = vals[*a];
1286 let d = 1.0 - u * u;
1287 let gp = 1.0 / d;
1288 let gpp = 2.0 * u / (d * d);
1289 adj[*a] += w * gp;
1290 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1291 }
1292 TapeOp::Erf(a) => {
1293 let u = vals[*a];
1294 let gp = erf_d1(u);
1295 let gpp = erf_d2(u);
1296 adj[*a] += w * gp;
1297 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1298 }
1299 TapeOp::XLogX(a) => {
1300 let u = vals[*a];
1303 let gp = xlogx_d1(u);
1304 let gpp = xlogx_d2(u);
1305 adj[*a] += w * gp;
1306 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1307 }
1308 TapeOp::CEntropy(a, b) => {
1309 let ua = vals[*a];
1310 let ub = vals[*b];
1311 let fa = centropy_da(ua, ub);
1312 let fb = centropy_db(ua, ub);
1313 let faa = centropy_daa(ua);
1314 let fab = centropy_dab(ub);
1315 let fbb = centropy_dbb(ua, ub);
1316 adj[*a] += w * fa;
1317 adj[*b] += w * fb;
1318 adj_dot[*a] += wd * fa + w * (faa * dot[*a] + fab * dot[*b]);
1319 adj_dot[*b] += wd * fb + w * (fab * dot[*a] + fbb * dot[*b]);
1320 }
1321 TapeOp::Atan2(a, b) => {
1322 let y = vals[*a];
1323 let x = vals[*b];
1324 let d = y * y + x * x;
1325 let d2 = d * d;
1326 let fa = x / d;
1327 let fb = -y / d;
1328 let faa = -2.0 * y * x / d2;
1329 let fab = (y * y - x * x) / d2;
1330 let fbb = 2.0 * y * x / d2;
1331 adj[*a] += w * fa;
1332 adj[*b] += w * fb;
1333 adj_dot[*a] += wd * fa + w * (faa * dot[*a] + fab * dot[*b]);
1334 adj_dot[*b] += wd * fb + w * (fab * dot[*a] + fbb * dot[*b]);
1335 }
1336 TapeOp::Min(a, b) => {
1340 let br = if vals[*a] <= vals[*b] { *a } else { *b };
1341 adj[br] += w;
1342 adj_dot[br] += wd;
1343 }
1344 TapeOp::Max(a, b) => {
1345 let br = if vals[*a] >= vals[*b] { *a } else { *b };
1346 adj[br] += w;
1347 adj_dot[br] += wd;
1348 }
1349 TapeOp::Cmp(_, _, _) | TapeOp::And(_, _) | TapeOp::Or(_, _) | TapeOp::Not(_) => {}
1351 TapeOp::Select(c, t, e) => {
1354 let br = if vals[*c] != 0.0 { *t } else { *e };
1355 adj[br] += w;
1356 adj_dot[br] += wd;
1357 }
1358 TapeOp::Funcall(fc) => {
1359 let FuncallData { lib, name, args } = fc.as_ref();
1360 let call_args = funcall_to_ext_args(args, vals);
1361 let res = ext_eval_or_nan(lib, name, &call_args, args.len(), true, true);
1362 let derivs = res.derivs.expect("want_derivs=true returns derivs");
1363 let hes = res.hessian.expect("want_hes=true returns hessian");
1364 let real_tape: Vec<usize> = args
1365 .iter()
1366 .filter_map(|a| match a {
1367 TapeFuncallArg::Tape(t) => Some(*t),
1368 TapeFuncallArg::Str(_) => None,
1369 })
1370 .collect();
1371 for (k, &tk) in real_tape.iter().enumerate() {
1372 adj[tk] += w * derivs[k];
1373 let mut second_term = 0.0;
1374 for (l, &tl) in real_tape.iter().enumerate() {
1375 let (lo, hi) = if k <= l { (k, l) } else { (l, k) };
1376 let h_kl = hes[lo + hi * (hi + 1) / 2];
1377 second_term += h_kl * dot[tl];
1378 }
1379 adj_dot[tk] += wd * derivs[k] + w * second_term;
1380 }
1381 }
1382 }
1383 }
1384 }
1385
1386 pub fn hessian_accumulate(
1393 &self,
1394 x: &[f64],
1395 weight: f64,
1396 hess_map: &HashMap<(usize, usize), usize>,
1397 values: &mut [f64],
1398 ) {
1399 let n = self.ops.len();
1400 if n == 0 || weight == 0.0 {
1401 return;
1402 }
1403 let v = self.forward(x);
1404 let var_indices = self.variables();
1405
1406 let mut dot = vec![0.0f64; n];
1413 let mut adj = vec![0.0f64; n];
1414 let mut adj_dot = vec![0.0f64; n];
1415 for &j in &var_indices {
1416 self.forward_tangent(&v, j, &mut dot);
1417
1418 adj.fill(0.0);
1421 adj_dot.fill(0.0);
1422 adj[n - 1] = 1.0;
1423
1424 for i in (0..n).rev() {
1425 let w = adj[i];
1426 let wd = adj_dot[i];
1427 if w == 0.0 && wd == 0.0 {
1428 continue;
1429 }
1430 match &self.ops[i] {
1431 TapeOp::Const(_) => {}
1432 TapeOp::Var(k) => {
1433 if wd != 0.0 && *k >= j {
1436 if let Some(&pos) = hess_map.get(&(*k, j)) {
1437 values[pos] += weight * wd;
1438 }
1439 }
1440 }
1441 TapeOp::Add(a, b) => {
1442 adj[*a] += w;
1443 adj[*b] += w;
1444 adj_dot[*a] += wd;
1445 adj_dot[*b] += wd;
1446 }
1447 TapeOp::Sub(a, b) => {
1448 adj[*a] += w;
1449 adj[*b] -= w;
1450 adj_dot[*a] += wd;
1451 adj_dot[*b] -= wd;
1452 }
1453 TapeOp::Mul(a, b) => {
1454 adj[*a] += w * v[*b];
1455 adj[*b] += w * v[*a];
1456 adj_dot[*a] += wd * v[*b] + w * dot[*b];
1457 adj_dot[*b] += wd * v[*a] + w * dot[*a];
1458 }
1459 TapeOp::Div(a, b) => {
1460 let vb = v[*b];
1463 let q = v[i];
1464 let qd = dot[i];
1465 adj[*a] += w / vb;
1466 adj_dot[*a] += wd / vb - w * (dot[*b] / vb) / vb;
1467 adj[*b] -= w * q / vb;
1468 adj_dot[*b] += -(wd * q) / vb + (w / vb) * (-qd + q * (dot[*b] / vb));
1469 }
1470 TapeOp::Pow(a, b) => {
1471 let u = v[*a];
1472 let r = v[*b];
1473 let du = dot[*a];
1474 let dr = dot[*b];
1475 if r != 0.0 {
1476 if u != 0.0 {
1477 let p_a = r * u.powf(r - 1.0);
1478 adj[*a] += w * p_a;
1479 let mut dp_a = dr * u.powf(r - 1.0);
1480 if u > 0.0 {
1481 dp_a +=
1482 r * u.powf(r - 1.0) * ((r - 1.0) * du / u + dr * u.ln());
1483 } else {
1484 dp_a += r * (r - 1.0) * u.powf(r - 2.0) * du;
1485 }
1486 adj_dot[*a] += wd * p_a + w * dp_a;
1487 } else if r >= 2.0 {
1488 let p_a = 0.0;
1489 adj[*a] += w * p_a;
1490 let dp_a = if r == 2.0 {
1491 2.0 * du
1492 } else {
1493 r * (r - 1.0) * (0.0_f64).powf(r - 2.0) * du
1494 };
1495 adj_dot[*a] += wd * p_a + w * dp_a;
1496 }
1497 }
1498 if u > 0.0 {
1499 let ln_u = u.ln();
1500 let p_b = v[i] * ln_u;
1501 adj[*b] += w * p_b;
1502 let dur = v[i] * (r * du / u + dr * ln_u);
1503 let dp_b = dur * ln_u + v[i] * du / u;
1504 adj_dot[*b] += wd * p_b + w * dp_b;
1505 }
1506 }
1507 TapeOp::Neg(a) => {
1508 adj[*a] -= w;
1509 adj_dot[*a] -= wd;
1510 }
1511 TapeOp::Abs(a) => {
1512 let s = if v[*a] >= 0.0 { 1.0 } else { -1.0 };
1513 adj[*a] += w * s;
1514 adj_dot[*a] += wd * s;
1515 }
1516 TapeOp::Sqrt(a) => {
1517 let sv = v[i];
1518 if sv > 0.0 {
1519 let fp = 0.5 / sv;
1520 let fpp = -0.25 / (v[*a] * sv);
1521 adj[*a] += w * fp;
1522 adj_dot[*a] += wd * fp + w * fpp * dot[*a];
1523 }
1524 }
1525 TapeOp::Exp(a) => {
1526 let ev = v[i];
1527 adj[*a] += w * ev;
1528 adj_dot[*a] += wd * ev + w * ev * dot[*a];
1529 }
1530 TapeOp::Log(a) => {
1531 let u = v[*a];
1532 adj[*a] += w / u;
1533 adj_dot[*a] += wd / u + w * (-1.0 / (u * u)) * dot[*a];
1534 }
1535 TapeOp::Log10(a) => {
1536 let u = v[*a];
1537 let c = std::f64::consts::LN_10;
1538 adj[*a] += w / (u * c);
1539 adj_dot[*a] += wd / (u * c) + w * (-1.0 / (u * u * c)) * dot[*a];
1540 }
1541 TapeOp::Sin(a) => {
1542 let u = v[*a];
1543 let cu = u.cos();
1544 adj[*a] += w * cu;
1545 adj_dot[*a] += wd * cu + w * (-u.sin()) * dot[*a];
1546 }
1547 TapeOp::Cos(a) => {
1548 let u = v[*a];
1549 let su = u.sin();
1550 adj[*a] -= w * su;
1551 adj_dot[*a] += wd * (-su) + w * (-u.cos()) * dot[*a];
1552 }
1553 TapeOp::Tan(a) => {
1554 let t = v[i];
1555 let gp = 1.0 + t * t;
1556 let gpp = 2.0 * t * gp;
1557 adj[*a] += w * gp;
1558 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1559 }
1560 TapeOp::Atan(a) => {
1561 let u = v[*a];
1562 let d = 1.0 + u * u;
1563 let gp = 1.0 / d;
1564 let gpp = -2.0 * u / (d * d);
1565 adj[*a] += w * gp;
1566 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1567 }
1568 TapeOp::Acos(a) => {
1569 let u = v[*a];
1570 let s = 1.0 - u * u;
1571 let r = s.sqrt();
1572 let gp = -1.0 / r;
1573 let gpp = -u / (s * r);
1574 adj[*a] += w * gp;
1575 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1576 }
1577 TapeOp::Sinh(a) => {
1578 let u = v[*a];
1579 let gp = u.cosh();
1580 let gpp = u.sinh();
1581 adj[*a] += w * gp;
1582 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1583 }
1584 TapeOp::Cosh(a) => {
1585 let u = v[*a];
1586 let gp = u.sinh();
1587 let gpp = u.cosh();
1588 adj[*a] += w * gp;
1589 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1590 }
1591 TapeOp::Tanh(a) => {
1592 let t = v[i];
1593 let gp = 1.0 - t * t;
1594 let gpp = -2.0 * t * gp;
1595 adj[*a] += w * gp;
1596 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1597 }
1598 TapeOp::Asin(a) => {
1599 let u = v[*a];
1600 let s = 1.0 - u * u;
1601 let r = s.sqrt();
1602 let gp = 1.0 / r;
1603 let gpp = u / (s * r);
1604 adj[*a] += w * gp;
1605 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1606 }
1607 TapeOp::Acosh(a) => {
1608 let u = v[*a];
1609 let s = u * u - 1.0;
1610 let r = s.sqrt();
1611 let gp = 1.0 / r;
1612 let gpp = -u / (s * r);
1613 adj[*a] += w * gp;
1614 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1615 }
1616 TapeOp::Asinh(a) => {
1617 let u = v[*a];
1618 let s = u * u + 1.0;
1619 let r = s.sqrt();
1620 let gp = 1.0 / r;
1621 let gpp = -u / (s * r);
1622 adj[*a] += w * gp;
1623 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1624 }
1625 TapeOp::Atanh(a) => {
1626 let u = v[*a];
1627 let d = 1.0 - u * u;
1628 let gp = 1.0 / d;
1629 let gpp = 2.0 * u / (d * d);
1630 adj[*a] += w * gp;
1631 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1632 }
1633 TapeOp::Erf(a) => {
1634 let u = v[*a];
1635 let gp = erf_d1(u);
1636 let gpp = erf_d2(u);
1637 adj[*a] += w * gp;
1638 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1639 }
1640 TapeOp::XLogX(a) => {
1641 let u = v[*a];
1644 let gp = xlogx_d1(u);
1645 let gpp = xlogx_d2(u);
1646 adj[*a] += w * gp;
1647 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
1648 }
1649 TapeOp::CEntropy(a, b) => {
1650 let ua = v[*a];
1651 let ub = v[*b];
1652 let fa = centropy_da(ua, ub);
1653 let fb = centropy_db(ua, ub);
1654 let faa = centropy_daa(ua);
1655 let fab = centropy_dab(ub);
1656 let fbb = centropy_dbb(ua, ub);
1657 adj[*a] += w * fa;
1658 adj[*b] += w * fb;
1659 adj_dot[*a] += wd * fa + w * (faa * dot[*a] + fab * dot[*b]);
1660 adj_dot[*b] += wd * fb + w * (fab * dot[*a] + fbb * dot[*b]);
1661 }
1662 TapeOp::Atan2(a, b) => {
1663 let y = v[*a];
1664 let x = v[*b];
1665 let d = y * y + x * x;
1666 let d2 = d * d;
1667 let fa = x / d;
1668 let fb = -y / d;
1669 let faa = -2.0 * y * x / d2;
1670 let fab = (y * y - x * x) / d2;
1671 let fbb = 2.0 * y * x / d2;
1672 adj[*a] += w * fa;
1673 adj[*b] += w * fb;
1674 adj_dot[*a] += wd * fa + w * (faa * dot[*a] + fab * dot[*b]);
1675 adj_dot[*b] += wd * fb + w * (fab * dot[*a] + fbb * dot[*b]);
1676 }
1677 TapeOp::Min(a, b) => {
1681 let br = if v[*a] <= v[*b] { *a } else { *b };
1682 adj[br] += w;
1683 adj_dot[br] += wd;
1684 }
1685 TapeOp::Max(a, b) => {
1686 let br = if v[*a] >= v[*b] { *a } else { *b };
1687 adj[br] += w;
1688 adj_dot[br] += wd;
1689 }
1690 TapeOp::Cmp(_, _, _)
1692 | TapeOp::And(_, _)
1693 | TapeOp::Or(_, _)
1694 | TapeOp::Not(_) => {}
1695 TapeOp::Select(c, t, e) => {
1698 let br = if v[*c] != 0.0 { *t } else { *e };
1699 adj[br] += w;
1700 adj_dot[br] += wd;
1701 }
1702 TapeOp::Funcall(fc) => {
1703 let FuncallData { lib, name, args } = fc.as_ref();
1704 let call_args = funcall_to_ext_args(args, &v);
1705 let res = ext_eval_or_nan(lib, name, &call_args, args.len(), true, true);
1706 let derivs = res.derivs.expect("want_derivs=true returns derivs");
1707 let hes = res.hessian.expect("want_hes=true returns hessian");
1708 let real_tape: Vec<usize> = args
1709 .iter()
1710 .filter_map(|a| match a {
1711 TapeFuncallArg::Tape(t) => Some(*t),
1712 TapeFuncallArg::Str(_) => None,
1713 })
1714 .collect();
1715 for (k, &tk) in real_tape.iter().enumerate() {
1716 adj[tk] += w * derivs[k];
1717 let mut second_term = 0.0;
1718 for (l, &tl) in real_tape.iter().enumerate() {
1719 let (lo, hi) = if k <= l { (k, l) } else { (l, k) };
1720 let h_kl = hes[lo + hi * (hi + 1) / 2];
1721 second_term += h_kl * dot[tl];
1722 }
1723 adj_dot[tk] += wd * derivs[k] + w * second_term;
1724 }
1725 }
1726 }
1727 }
1728 }
1729 }
1730
1731 pub fn hessian_sparsity(&self) -> BTreeSet<(usize, usize)> {
1736 let n = self.ops.len();
1737 let mut var_sets: Vec<BTreeSet<usize>> = Vec::with_capacity(n);
1738 let mut pairs: BTreeSet<(usize, usize)> = BTreeSet::new();
1739
1740 let mut last_use: Vec<usize> = (0..n).collect();
1755 for (i, op) in self.ops.iter().enumerate() {
1756 for_each_input(op, |a| last_use[a] = i);
1757 }
1758 macro_rules! merge {
1762 ($a:expr, $b:expr, $i:expr) => {{
1763 let (a, b, i) = ($a, $b, $i);
1764 if a == b {
1765 if last_use[a] == i {
1766 std::mem::take(&mut var_sets[a])
1767 } else {
1768 var_sets[a].clone()
1769 }
1770 } else {
1771 let take_a = last_use[a] == i
1773 && (last_use[b] != i || var_sets[a].len() >= var_sets[b].len());
1774 if take_a {
1775 let mut s = std::mem::take(&mut var_sets[a]);
1776 s.extend(var_sets[b].iter().copied());
1777 s
1778 } else if last_use[b] == i {
1779 let mut s = std::mem::take(&mut var_sets[b]);
1780 s.extend(var_sets[a].iter().copied());
1781 s
1782 } else {
1783 var_sets[a].union(&var_sets[b]).copied().collect()
1784 }
1785 }
1786 }};
1787 }
1788 macro_rules! carry {
1790 ($a:expr, $i:expr) => {{
1791 let (a, i) = ($a, $i);
1792 if last_use[a] == i {
1793 std::mem::take(&mut var_sets[a])
1794 } else {
1795 var_sets[a].clone()
1796 }
1797 }};
1798 }
1799
1800 let emit_cross =
1801 |s1: &BTreeSet<usize>, s2: &BTreeSet<usize>, pairs: &mut BTreeSet<(usize, usize)>| {
1802 for &v1 in s1 {
1803 for &v2 in s2 {
1804 let (r, c) = if v1 >= v2 { (v1, v2) } else { (v2, v1) };
1805 pairs.insert((r, c));
1806 }
1807 }
1808 };
1809 let emit_self = |s: &BTreeSet<usize>, pairs: &mut BTreeSet<(usize, usize)>| {
1810 let vars: Vec<usize> = s.iter().copied().collect();
1811 for (ai, &vi) in vars.iter().enumerate() {
1812 for &vj in &vars[..=ai] {
1813 let (r, c) = if vi >= vj { (vi, vj) } else { (vj, vi) };
1814 pairs.insert((r, c));
1815 }
1816 }
1817 };
1818
1819 for (i, op) in self.ops.iter().enumerate() {
1820 let vset = match op {
1821 TapeOp::Const(_) => BTreeSet::new(),
1822 TapeOp::Var(j) => {
1823 let mut s = BTreeSet::new();
1824 s.insert(*j);
1825 s
1826 }
1827 TapeOp::Add(a, b) | TapeOp::Sub(a, b) => merge!(*a, *b, i),
1828 TapeOp::Neg(a) | TapeOp::Abs(a) => carry!(*a, i),
1829 TapeOp::Mul(a, b) => {
1830 emit_cross(&var_sets[*a], &var_sets[*b], &mut pairs);
1831 merge!(*a, *b, i)
1832 }
1833 TapeOp::Div(a, b) => {
1834 emit_cross(&var_sets[*a], &var_sets[*b], &mut pairs);
1835 emit_self(&var_sets[*b], &mut pairs);
1836 merge!(*a, *b, i)
1837 }
1838 TapeOp::Pow(a, b) => {
1839 let combined = merge!(*a, *b, i);
1840 emit_self(&combined, &mut pairs);
1841 combined
1842 }
1843 TapeOp::Sqrt(a)
1844 | TapeOp::Exp(a)
1845 | TapeOp::Log(a)
1846 | TapeOp::Log10(a)
1847 | TapeOp::Sin(a)
1848 | TapeOp::Cos(a)
1849 | TapeOp::Tan(a)
1850 | TapeOp::Atan(a)
1851 | TapeOp::Acos(a)
1852 | TapeOp::Sinh(a)
1853 | TapeOp::Cosh(a)
1854 | TapeOp::Tanh(a)
1855 | TapeOp::Asin(a)
1856 | TapeOp::Acosh(a)
1857 | TapeOp::Asinh(a)
1858 | TapeOp::Erf(a)
1859 | TapeOp::XLogX(a)
1860 | TapeOp::Atanh(a) => {
1861 emit_self(&var_sets[*a], &mut pairs);
1862 carry!(*a, i)
1863 }
1864 TapeOp::Atan2(a, b) | TapeOp::CEntropy(a, b) => {
1869 let combined = merge!(*a, *b, i);
1870 emit_self(&combined, &mut pairs);
1871 combined
1872 }
1873 TapeOp::Cmp(_, _, _) | TapeOp::And(_, _) | TapeOp::Or(_, _) | TapeOp::Not(_) => {
1879 BTreeSet::new()
1880 }
1881 TapeOp::Select(_c, t, e) => merge!(*t, *e, i),
1888 TapeOp::Min(a, b) | TapeOp::Max(a, b) => merge!(*a, *b, i),
1895 TapeOp::Funcall(fc) => {
1896 let args = &fc.args;
1897 let mut combined: BTreeSet<usize> = BTreeSet::new();
1898 for arg in args {
1899 if let TapeFuncallArg::Tape(t) = arg {
1900 for &vv in &var_sets[*t] {
1901 combined.insert(vv);
1902 }
1903 }
1904 }
1905 emit_self(&combined, &mut pairs);
1906 combined
1907 }
1908 };
1909 for_each_input(op, |a| {
1915 if last_use[a] == i {
1916 var_sets[a].clear();
1917 }
1918 });
1919 var_sets.push(vset);
1920 }
1921 pairs
1922 }
1923}
1924
1925fn for_each_input(op: &TapeOp, mut f: impl FnMut(usize)) {
1936 match op {
1937 TapeOp::Const(_) | TapeOp::Var(_) => {}
1938 TapeOp::Neg(a)
1939 | TapeOp::Abs(a)
1940 | TapeOp::Sqrt(a)
1941 | TapeOp::Exp(a)
1942 | TapeOp::Log(a)
1943 | TapeOp::Log10(a)
1944 | TapeOp::Sin(a)
1945 | TapeOp::Cos(a)
1946 | TapeOp::Tan(a)
1947 | TapeOp::Atan(a)
1948 | TapeOp::Acos(a)
1949 | TapeOp::Sinh(a)
1950 | TapeOp::Cosh(a)
1951 | TapeOp::Tanh(a)
1952 | TapeOp::Asin(a)
1953 | TapeOp::Acosh(a)
1954 | TapeOp::Asinh(a)
1955 | TapeOp::Atanh(a)
1956 | TapeOp::Erf(a)
1957 | TapeOp::XLogX(a)
1958 | TapeOp::Not(a) => f(*a),
1959 TapeOp::Add(a, b)
1960 | TapeOp::Sub(a, b)
1961 | TapeOp::Mul(a, b)
1962 | TapeOp::Div(a, b)
1963 | TapeOp::Pow(a, b)
1964 | TapeOp::CEntropy(a, b)
1965 | TapeOp::Atan2(a, b)
1966 | TapeOp::Min(a, b)
1967 | TapeOp::Max(a, b)
1968 | TapeOp::And(a, b)
1969 | TapeOp::Or(a, b)
1970 | TapeOp::Cmp(_, a, b) => {
1971 f(*a);
1972 f(*b);
1973 }
1974 TapeOp::Select(c, t, e) => {
1975 f(*c);
1976 f(*t);
1977 f(*e);
1978 }
1979 TapeOp::Funcall(fc) => {
1980 for arg in &fc.args {
1981 if let TapeFuncallArg::Tape(t) = arg {
1982 f(*t);
1983 }
1984 }
1985 }
1986 }
1987}
1988
1989fn build_recursive(
1990 expr: &Expr,
1991 ops: &mut Vec<TapeOp>,
1992 cache: &mut HashMap<*const Expr, usize>,
1993 resolver: &ExternalResolver,
1994) -> usize {
1995 match expr {
1996 Expr::Const(c) => {
1997 let idx = ops.len();
1998 ops.push(TapeOp::Const(*c));
1999 idx
2000 }
2001 Expr::Var(i) => {
2002 let idx = ops.len();
2003 ops.push(TapeOp::Var(*i));
2004 idx
2005 }
2006 Expr::Binary(op, a, b) => {
2007 if let BinOp::Pow = op {
2015 if let Some(c) = peek_const(b) {
2016 if let Some(idx) = try_emit_const_pow(a, c, ops, cache, resolver) {
2017 return idx;
2018 }
2019 }
2020 }
2021 let l = build_recursive(a, ops, cache, resolver);
2022 let r = build_recursive(b, ops, cache, resolver);
2023 let idx = ops.len();
2024 ops.push(match op {
2025 BinOp::Add => TapeOp::Add(l, r),
2026 BinOp::Sub => TapeOp::Sub(l, r),
2027 BinOp::Mul => TapeOp::Mul(l, r),
2028 BinOp::Div => TapeOp::Div(l, r),
2029 BinOp::Pow => TapeOp::Pow(l, r),
2030 BinOp::Atan2 => TapeOp::Atan2(l, r),
2031 BinOp::CEntropy => TapeOp::CEntropy(l, r),
2032 });
2033 idx
2034 }
2035 Expr::Unary(op, a) => {
2036 let v = build_recursive(a, ops, cache, resolver);
2037 let idx = ops.len();
2038 ops.push(match op {
2039 UnaryOp::Neg => TapeOp::Neg(v),
2040 UnaryOp::Sqrt => TapeOp::Sqrt(v),
2041 UnaryOp::Log => TapeOp::Log(v),
2042 UnaryOp::Log10 => TapeOp::Log10(v),
2043 UnaryOp::Exp => TapeOp::Exp(v),
2044 UnaryOp::Abs => TapeOp::Abs(v),
2045 UnaryOp::Sin => TapeOp::Sin(v),
2046 UnaryOp::Cos => TapeOp::Cos(v),
2047 UnaryOp::Tan => TapeOp::Tan(v),
2048 UnaryOp::Atan => TapeOp::Atan(v),
2049 UnaryOp::Acos => TapeOp::Acos(v),
2050 UnaryOp::Sinh => TapeOp::Sinh(v),
2051 UnaryOp::Cosh => TapeOp::Cosh(v),
2052 UnaryOp::Tanh => TapeOp::Tanh(v),
2053 UnaryOp::Asin => TapeOp::Asin(v),
2054 UnaryOp::Acosh => TapeOp::Acosh(v),
2055 UnaryOp::Asinh => TapeOp::Asinh(v),
2056 UnaryOp::Atanh => TapeOp::Atanh(v),
2057 UnaryOp::Erf => TapeOp::Erf(v),
2058 UnaryOp::XLogX => TapeOp::XLogX(v),
2059 });
2060 idx
2061 }
2062 Expr::Sum(args) => {
2063 if args.is_empty() {
2064 let idx = ops.len();
2065 ops.push(TapeOp::Const(0.0));
2066 return idx;
2067 }
2068 let mut acc = build_recursive(&args[0], ops, cache, resolver);
2069 for a in &args[1..] {
2070 let next = build_recursive(a, ops, cache, resolver);
2071 let idx = ops.len();
2072 ops.push(TapeOp::Add(acc, next));
2073 acc = idx;
2074 }
2075 acc
2076 }
2077 Expr::MinList(args) | Expr::MaxList(args) => {
2085 let is_min = matches!(expr, Expr::MinList(_));
2086 if args.is_empty() {
2087 let idx = ops.len();
2088 ops.push(TapeOp::Const(0.0));
2089 return idx;
2090 }
2091 let mut acc = build_recursive(&args[0], ops, cache, resolver);
2092 for a in &args[1..] {
2093 let next = build_recursive(a, ops, cache, resolver);
2094 let idx = ops.len();
2095 ops.push(if is_min {
2096 TapeOp::Min(acc, next)
2097 } else {
2098 TapeOp::Max(acc, next)
2099 });
2100 acc = idx;
2101 }
2102 acc
2103 }
2104 Expr::Cse(body) => {
2105 let key = Arc::as_ptr(body) as *const Expr;
2112 if let Some(&idx) = cache.get(&key) {
2113 idx
2114 } else {
2115 let idx = build_recursive(body, ops, cache, resolver);
2116 cache.insert(key, idx);
2117 idx
2118 }
2119 }
2120 Expr::Compare(op, a, b) => {
2121 let l = build_recursive(a, ops, cache, resolver);
2122 let r = build_recursive(b, ops, cache, resolver);
2123 let idx = ops.len();
2124 ops.push(TapeOp::Cmp(*op, l, r));
2125 idx
2126 }
2127 Expr::And(a, b) => {
2128 let l = build_recursive(a, ops, cache, resolver);
2129 let r = build_recursive(b, ops, cache, resolver);
2130 let idx = ops.len();
2131 ops.push(TapeOp::And(l, r));
2132 idx
2133 }
2134 Expr::Or(a, b) => {
2135 let l = build_recursive(a, ops, cache, resolver);
2136 let r = build_recursive(b, ops, cache, resolver);
2137 let idx = ops.len();
2138 ops.push(TapeOp::Or(l, r));
2139 idx
2140 }
2141 Expr::Not(a) => {
2142 let v = build_recursive(a, ops, cache, resolver);
2143 let idx = ops.len();
2144 ops.push(TapeOp::Not(v));
2145 idx
2146 }
2147 Expr::Cond { cond, then_, else_ } => {
2148 let c = build_recursive(cond, ops, cache, resolver);
2149 let t = build_recursive(then_, ops, cache, resolver);
2150 let e = build_recursive(else_, ops, cache, resolver);
2151 let idx = ops.len();
2152 ops.push(TapeOp::Select(c, t, e));
2153 idx
2154 }
2155 Expr::Funcall { id, args } => {
2156 let (lib, name) = resolver
2157 .funcs_by_id
2158 .get(id)
2159 .unwrap_or_else(|| panic!("unresolved AMPL funcall id {id}"));
2160 let tape_args: Vec<TapeFuncallArg> = args
2161 .iter()
2162 .map(|a| match a {
2163 FuncallArg::Real(e) => {
2164 TapeFuncallArg::Tape(build_recursive(e, ops, cache, resolver))
2165 }
2166 FuncallArg::Str(s) => TapeFuncallArg::Str(s.clone()),
2167 })
2168 .collect();
2169 let idx = ops.len();
2170 ops.push(TapeOp::Funcall(Box::new(FuncallData {
2171 lib: Arc::clone(lib),
2172 name: name.clone(),
2173 args: tape_args,
2174 })));
2175 idx
2176 }
2177 }
2178}
2179
2180fn peek_const(e: &Expr) -> Option<f64> {
2184 match e {
2185 Expr::Const(c) => Some(*c),
2186 Expr::Cse(body) => peek_const(body),
2187 _ => None,
2188 }
2189}
2190
2191fn try_emit_const_pow(
2199 base_expr: &Expr,
2200 c: f64,
2201 ops: &mut Vec<TapeOp>,
2202 cache: &mut HashMap<*const Expr, usize>,
2203 resolver: &ExternalResolver,
2204) -> Option<usize> {
2205 if c == 0.0 {
2206 let idx = ops.len();
2207 ops.push(TapeOp::Const(1.0));
2208 return Some(idx);
2209 }
2210 if c == 1.0 {
2211 return Some(build_recursive(base_expr, ops, cache, resolver));
2212 }
2213 if c == 0.5 {
2214 let b = build_recursive(base_expr, ops, cache, resolver);
2215 let idx = ops.len();
2216 ops.push(TapeOp::Sqrt(b));
2217 return Some(idx);
2218 }
2219 if c.is_finite() && c.fract() == 0.0 && c.abs() <= 8.0 {
2224 let n = c.abs() as u32;
2225 if n == 0 {
2226 let idx = ops.len();
2228 ops.push(TapeOp::Const(1.0));
2229 return Some(idx);
2230 }
2231 let b = build_recursive(base_expr, ops, cache, resolver);
2232 let pos = emit_int_pow(b, n, ops);
2233 if c < 0.0 {
2234 let one_idx = ops.len();
2237 ops.push(TapeOp::Const(1.0));
2238 let idx = ops.len();
2239 ops.push(TapeOp::Div(one_idx, pos));
2240 return Some(idx);
2241 }
2242 return Some(pos);
2243 }
2244 None
2245}
2246
2247fn emit_int_pow(base: usize, n: u32, ops: &mut Vec<TapeOp>) -> usize {
2251 debug_assert!(n >= 1);
2252 if n == 1 {
2253 return base;
2254 }
2255 let half = emit_int_pow(base, n / 2, ops);
2256 let squared = ops.len();
2257 ops.push(TapeOp::Mul(half, half));
2258 if n % 2 == 1 {
2259 let idx = ops.len();
2260 ops.push(TapeOp::Mul(squared, base));
2261 idx
2262 } else {
2263 squared
2264 }
2265}
2266
2267#[derive(Debug, Clone)]
2295pub enum SummandOp {
2296 Local(TapeOp),
2299 Shared(usize),
2303}
2304
2305#[derive(Debug, Clone)]
2306pub struct Summand {
2307 pub ops: Vec<SummandOp>,
2308 pub root_slot: usize,
2310 pub local_reach: Vec<usize>,
2312 pub prelude_reach: Vec<usize>,
2315 pub local_vars: Vec<usize>,
2317 pub prelude_vars: Vec<usize>,
2319 pub all_vars: Vec<usize>,
2326}
2327
2328#[derive(Debug, Clone)]
2329pub struct HybridTape {
2330 pub prelude: Vec<TapeOp>,
2335 pub summands: Vec<Summand>,
2336}
2337
2338impl HybridTape {
2339 pub fn build_multi(exprs: &[Expr]) -> Self {
2344 let mut cse_count: HashMap<*const Expr, usize> = HashMap::new();
2348 for e in exprs {
2349 let mut seen_in_root: HashSet<*const Expr> = HashSet::new();
2350 count_cse_appearances(e, &mut seen_in_root, &mut cse_count);
2351 }
2352
2353 let mut prelude: Vec<TapeOp> = Vec::new();
2358 let mut prelude_map: HashMap<*const Expr, usize> = HashMap::new();
2359 let mut summands: Vec<Summand> = Vec::with_capacity(exprs.len());
2360 for e in exprs {
2361 let mut local: Vec<SummandOp> = Vec::new();
2362 let mut local_cache: HashMap<*const Expr, usize> = HashMap::new();
2363 let root_slot = build_into_summand(
2364 e,
2365 &mut local,
2366 &mut local_cache,
2367 &mut prelude,
2368 &mut prelude_map,
2369 &cse_count,
2370 );
2371 summands.push(Summand {
2372 ops: local,
2373 root_slot,
2374 local_reach: Vec::new(),
2375 prelude_reach: Vec::new(),
2376 local_vars: Vec::new(),
2377 prelude_vars: Vec::new(),
2378 all_vars: Vec::new(),
2379 });
2380 }
2381
2382 let mut p_visited: Vec<u32> = vec![0; prelude.len()];
2386 let mut p_epoch: u32 = 0;
2387 let mut p_stack: Vec<usize> = Vec::new();
2388 for s in &mut summands {
2389 let (local_reach, shared_refs) = compute_local_reach(&s.ops, s.root_slot);
2390 s.local_reach = local_reach;
2391
2392 let mut lv: BTreeSet<usize> = BTreeSet::new();
2393 for &i in &s.local_reach {
2394 if let SummandOp::Local(TapeOp::Var(j)) = &s.ops[i] {
2395 lv.insert(*j);
2396 }
2397 }
2398 s.local_vars = lv.iter().copied().collect();
2399
2400 if !shared_refs.is_empty() {
2401 p_epoch += 1;
2402 let mut preach: Vec<usize> = Vec::new();
2403 for &start in &shared_refs {
2404 bfs_prelude(
2405 &prelude,
2406 start,
2407 &mut p_visited,
2408 p_epoch,
2409 &mut p_stack,
2410 &mut preach,
2411 );
2412 }
2413 preach.sort_unstable();
2414 s.prelude_vars = vars_in(&prelude, &preach);
2415 s.prelude_reach = preach;
2416 }
2417
2418 let mut av: BTreeSet<usize> = lv;
2419 for &v in &s.prelude_vars {
2420 av.insert(v);
2421 }
2422 s.all_vars = av.into_iter().collect();
2423 }
2424
2425 HybridTape { prelude, summands }
2426 }
2427
2428 pub fn n_prelude_ops(&self) -> usize {
2429 self.prelude.len()
2430 }
2431 pub fn n_summands(&self) -> usize {
2432 self.summands.len()
2433 }
2434 pub fn max_summand_ops(&self) -> usize {
2435 self.summands.iter().map(|s| s.ops.len()).max().unwrap_or(0)
2436 }
2437 pub fn total_local_ops(&self) -> usize {
2438 self.summands.iter().map(|s| s.ops.len()).sum()
2439 }
2440
2441 pub fn forward_prelude(&self, x: &[f64], prelude_vals: &mut [f64]) {
2444 debug_assert_eq!(prelude_vals.len(), self.prelude.len());
2445 for i in 0..self.prelude.len() {
2446 prelude_vals[i] = fwd_step(&self.prelude[i], x, prelude_vals);
2447 }
2448 }
2449
2450 pub fn forward_summand(
2453 &self,
2454 s: &Summand,
2455 x: &[f64],
2456 prelude_vals: &[f64],
2457 local_vals: &mut [f64],
2458 ) {
2459 debug_assert!(local_vals.len() >= s.ops.len());
2460 for i in 0..s.ops.len() {
2461 local_vals[i] = match &s.ops[i] {
2462 SummandOp::Local(op) => fwd_step(op, x, local_vals),
2463 SummandOp::Shared(k) => prelude_vals[*k],
2464 };
2465 }
2466 }
2467
2468 #[inline]
2470 pub fn root_value(&self, s: &Summand, local_vals: &[f64]) -> f64 {
2471 local_vals[s.root_slot]
2472 }
2473
2474 #[allow(clippy::too_many_arguments)]
2481 pub fn gradient_summand(
2482 &self,
2483 s: &Summand,
2484 prelude_vals: &[f64],
2485 local_vals: &[f64],
2486 seed: f64,
2487 grad: &mut [f64],
2488 local_adj: &mut [f64],
2489 prelude_adj: &mut [f64],
2490 ) {
2491 if seed == 0.0 || s.local_reach.is_empty() {
2492 return;
2493 }
2494 for &i in &s.local_reach {
2495 local_adj[i] = 0.0;
2496 }
2497 for &i in &s.prelude_reach {
2498 prelude_adj[i] = 0.0;
2499 }
2500 local_adj[s.root_slot] = seed;
2501 for &i in s.local_reach.iter().rev() {
2502 let a = local_adj[i];
2503 if a == 0.0 {
2504 continue;
2505 }
2506 match &s.ops[i] {
2507 SummandOp::Local(op) => rev_step(op, i, local_vals, local_adj, a, grad),
2508 SummandOp::Shared(k) => {
2509 prelude_adj[*k] += a;
2510 }
2511 }
2512 }
2513 for &i in s.prelude_reach.iter().rev() {
2514 let a = prelude_adj[i];
2515 if a == 0.0 {
2516 continue;
2517 }
2518 rev_step(&self.prelude[i], i, prelude_vals, prelude_adj, a, grad);
2519 }
2520 }
2521
2522 pub fn prelude_tangent(
2542 &self,
2543 prelude_vals: &[f64],
2544 seed: &[f64],
2545 reach: &[u32],
2546 prelude_dot: &mut [f64],
2547 ) {
2548 debug_assert!(prelude_dot.len() >= self.prelude.len());
2549 for &i in reach {
2550 let i = i as usize;
2551 prelude_dot[i] = fwd_dir_step(&self.prelude[i], seed, prelude_vals, prelude_dot, i);
2552 }
2553 }
2554
2555 #[allow(clippy::too_many_arguments)]
2583 pub fn hessian_summand_directional(
2584 &self,
2585 s: &Summand,
2586 local_vals: &[f64],
2587 prelude_dot: &[f64],
2588 seed: &[f64],
2589 weight: f64,
2590 out: &mut [f64],
2591 local_dot: &mut [f64],
2592 local_adj: &mut [f64],
2593 local_adj_dot: &mut [f64],
2594 prelude_adj: &mut [f64],
2595 prelude_adj_dot: &mut [f64],
2596 ) {
2597 if weight == 0.0 || s.local_reach.is_empty() {
2598 return;
2599 }
2600 for &i in &s.local_reach {
2601 local_adj[i] = 0.0;
2602 local_adj_dot[i] = 0.0;
2603 }
2604 for &i in &s.local_reach {
2608 local_dot[i] = match &s.ops[i] {
2609 SummandOp::Local(op) => fwd_dir_step(op, seed, local_vals, local_dot, i),
2610 SummandOp::Shared(k) => prelude_dot[*k],
2611 };
2612 }
2613 local_adj[s.root_slot] = 1.0;
2614 for &i in s.local_reach.iter().rev() {
2615 let w = local_adj[i];
2616 let wd = local_adj_dot[i];
2617 if w == 0.0 && wd == 0.0 {
2618 continue;
2619 }
2620 match &s.ops[i] {
2621 SummandOp::Local(op) => {
2622 ror_dir_step(
2623 op,
2624 i,
2625 local_vals,
2626 local_dot,
2627 local_adj,
2628 local_adj_dot,
2629 w,
2630 wd,
2631 weight,
2632 out,
2633 );
2634 }
2635 SummandOp::Shared(k) => {
2636 prelude_adj[*k] += weight * w;
2637 prelude_adj_dot[*k] += weight * wd;
2638 }
2639 }
2640 }
2641 }
2642
2643 pub fn prelude_reverse_directional(
2657 &self,
2658 prelude_vals: &[f64],
2659 prelude_dot: &[f64],
2660 reach: &[u32],
2661 out: &mut [f64],
2662 prelude_adj: &mut [f64],
2663 prelude_adj_dot: &mut [f64],
2664 ) {
2665 for &i in reach.iter().rev() {
2666 let i = i as usize;
2667 let w = prelude_adj[i];
2668 let wd = prelude_adj_dot[i];
2669 if w == 0.0 && wd == 0.0 {
2670 continue;
2671 }
2672 prelude_adj[i] = 0.0;
2673 prelude_adj_dot[i] = 0.0;
2674 ror_dir_step(
2675 &self.prelude[i],
2676 i,
2677 prelude_vals,
2678 prelude_dot,
2679 prelude_adj,
2680 prelude_adj_dot,
2681 w,
2682 wd,
2683 1.0,
2684 out,
2685 );
2686 }
2687 }
2688
2689 pub fn hessian_sparsity_all(&self) -> BTreeSet<(usize, usize)> {
2692 let mut pairs = hessian_sparsity_impl(&self.prelude);
2693
2694 let prelude_var_sets = compute_var_sets(&self.prelude);
2697
2698 for s in &self.summands {
2699 summand_sparsity(&s.ops, &prelude_var_sets, &mut pairs);
2700 }
2701 pairs
2702 }
2703}
2704
2705pub fn hybrid_supported(exprs: &[Expr]) -> bool {
2716 let mut stack: Vec<&Expr> = exprs.iter().collect();
2717 let mut seen_cse: HashSet<*const Expr> = HashSet::new();
2718 while let Some(e) = stack.pop() {
2719 match e {
2720 Expr::Const(_) | Expr::Var(_) => {}
2721 Expr::Binary(_, a, b) => {
2722 stack.push(a);
2723 stack.push(b);
2724 }
2725 Expr::Unary(_, a) => stack.push(a),
2726 Expr::Sum(args) => stack.extend(args.iter()),
2727 Expr::Cse(body) => {
2728 if seen_cse.insert(Arc::as_ptr(body)) {
2729 stack.push(body);
2730 }
2731 }
2732 Expr::Compare(..)
2733 | Expr::And(..)
2734 | Expr::Or(..)
2735 | Expr::Not(_)
2736 | Expr::Cond { .. }
2737 | Expr::MinList(_)
2738 | Expr::MaxList(_)
2739 | Expr::Funcall { .. } => return false,
2740 }
2741 }
2742 true
2743}
2744
2745fn cse_contains_funcall(expr: &Expr, built: &HashMap<*const Expr, usize>) -> bool {
2766 let mut stack: Vec<&Expr> = vec![expr];
2767 let mut seen: HashSet<*const Expr> = HashSet::new();
2768 while let Some(e) = stack.pop() {
2769 match e {
2770 Expr::Funcall { .. } => return true,
2771 Expr::Const(_) | Expr::Var(_) => {}
2772 Expr::Binary(_, a, b) | Expr::Compare(_, a, b) | Expr::And(a, b) | Expr::Or(a, b) => {
2773 stack.push(a);
2774 stack.push(b);
2775 }
2776 Expr::Unary(_, a) | Expr::Not(a) => stack.push(a),
2777 Expr::Sum(args) | Expr::MinList(args) | Expr::MaxList(args) => {
2778 stack.extend(args.iter())
2779 }
2780 Expr::Cond { cond, then_, else_ } => {
2781 stack.push(cond);
2782 stack.push(then_);
2783 stack.push(else_);
2784 }
2785 Expr::Cse(body) => {
2786 let key = Arc::as_ptr(body);
2787 if !built.contains_key(&key) && seen.insert(key) {
2788 stack.push(body);
2789 }
2790 }
2791 }
2792 }
2793 false
2794}
2795
2796fn count_cse_appearances(
2801 e: &Expr,
2802 seen_in_root: &mut HashSet<*const Expr>,
2803 counts: &mut HashMap<*const Expr, usize>,
2804) {
2805 match e {
2806 Expr::Const(_) | Expr::Var(_) => {}
2807 Expr::Binary(_, a, b) => {
2808 count_cse_appearances(a, seen_in_root, counts);
2809 count_cse_appearances(b, seen_in_root, counts);
2810 }
2811 Expr::Unary(_, a) => count_cse_appearances(a, seen_in_root, counts),
2812 Expr::Sum(args) | Expr::MinList(args) | Expr::MaxList(args) => {
2813 for a in args {
2814 count_cse_appearances(a, seen_in_root, counts);
2815 }
2816 }
2817 Expr::Compare(_, a, b) | Expr::And(a, b) | Expr::Or(a, b) => {
2818 count_cse_appearances(a, seen_in_root, counts);
2819 count_cse_appearances(b, seen_in_root, counts);
2820 }
2821 Expr::Not(a) => count_cse_appearances(a, seen_in_root, counts),
2822 Expr::Cond { cond, then_, else_ } => {
2823 count_cse_appearances(cond, seen_in_root, counts);
2824 count_cse_appearances(then_, seen_in_root, counts);
2825 count_cse_appearances(else_, seen_in_root, counts);
2826 }
2827 Expr::Cse(body) => {
2828 let key = Arc::as_ptr(body) as *const Expr;
2829 if seen_in_root.insert(key) {
2830 *counts.entry(key).or_insert(0) += 1;
2831 count_cse_appearances(body, seen_in_root, counts);
2832 }
2833 }
2834 Expr::Funcall { args, .. } => {
2835 for arg in args {
2836 if let FuncallArg::Real(e) = arg {
2837 count_cse_appearances(e, seen_in_root, counts);
2838 }
2839 }
2840 }
2841 }
2842}
2843
2844fn build_into_summand(
2850 expr: &Expr,
2851 local: &mut Vec<SummandOp>,
2852 local_cache: &mut HashMap<*const Expr, usize>,
2853 prelude: &mut Vec<TapeOp>,
2854 prelude_map: &mut HashMap<*const Expr, usize>,
2855 cse_count: &HashMap<*const Expr, usize>,
2856) -> usize {
2857 match expr {
2858 Expr::Const(c) => {
2859 let i = local.len();
2860 local.push(SummandOp::Local(TapeOp::Const(*c)));
2861 i
2862 }
2863 Expr::Var(j) => {
2864 let i = local.len();
2865 local.push(SummandOp::Local(TapeOp::Var(*j)));
2866 i
2867 }
2868 Expr::Binary(op, a, b) => {
2869 if let BinOp::Pow = op {
2870 if let Some(c) = peek_const(b) {
2871 if let Some(i) = try_emit_const_pow_summand(
2872 a,
2873 c,
2874 local,
2875 local_cache,
2876 prelude,
2877 prelude_map,
2878 cse_count,
2879 ) {
2880 return i;
2881 }
2882 }
2883 }
2884 let l = build_into_summand(a, local, local_cache, prelude, prelude_map, cse_count);
2885 let r = build_into_summand(b, local, local_cache, prelude, prelude_map, cse_count);
2886 let i = local.len();
2887 local.push(SummandOp::Local(match op {
2888 BinOp::Add => TapeOp::Add(l, r),
2889 BinOp::Sub => TapeOp::Sub(l, r),
2890 BinOp::Mul => TapeOp::Mul(l, r),
2891 BinOp::Div => TapeOp::Div(l, r),
2892 BinOp::Pow => TapeOp::Pow(l, r),
2893 BinOp::Atan2 => TapeOp::Atan2(l, r),
2894 BinOp::CEntropy => TapeOp::CEntropy(l, r),
2895 }));
2896 i
2897 }
2898 Expr::Unary(op, a) => {
2899 let v = build_into_summand(a, local, local_cache, prelude, prelude_map, cse_count);
2900 let i = local.len();
2901 local.push(SummandOp::Local(match op {
2902 UnaryOp::Neg => TapeOp::Neg(v),
2903 UnaryOp::Sqrt => TapeOp::Sqrt(v),
2904 UnaryOp::Log => TapeOp::Log(v),
2905 UnaryOp::Log10 => TapeOp::Log10(v),
2906 UnaryOp::Exp => TapeOp::Exp(v),
2907 UnaryOp::Abs => TapeOp::Abs(v),
2908 UnaryOp::Sin => TapeOp::Sin(v),
2909 UnaryOp::Cos => TapeOp::Cos(v),
2910 UnaryOp::Tan => TapeOp::Tan(v),
2911 UnaryOp::Atan => TapeOp::Atan(v),
2912 UnaryOp::Acos => TapeOp::Acos(v),
2913 UnaryOp::Sinh => TapeOp::Sinh(v),
2914 UnaryOp::Cosh => TapeOp::Cosh(v),
2915 UnaryOp::Tanh => TapeOp::Tanh(v),
2916 UnaryOp::Asin => TapeOp::Asin(v),
2917 UnaryOp::Acosh => TapeOp::Acosh(v),
2918 UnaryOp::Asinh => TapeOp::Asinh(v),
2919 UnaryOp::Atanh => TapeOp::Atanh(v),
2920 UnaryOp::Erf => TapeOp::Erf(v),
2921 UnaryOp::XLogX => TapeOp::XLogX(v),
2922 }));
2923 i
2924 }
2925 Expr::Sum(args) => {
2926 if args.is_empty() {
2927 let i = local.len();
2928 local.push(SummandOp::Local(TapeOp::Const(0.0)));
2929 return i;
2930 }
2931 let mut acc = build_into_summand(
2932 &args[0],
2933 local,
2934 local_cache,
2935 prelude,
2936 prelude_map,
2937 cse_count,
2938 );
2939 for a in &args[1..] {
2940 let nxt =
2941 build_into_summand(a, local, local_cache, prelude, prelude_map, cse_count);
2942 let i = local.len();
2943 local.push(SummandOp::Local(TapeOp::Add(acc, nxt)));
2944 acc = i;
2945 }
2946 acc
2947 }
2948 Expr::Cse(body) => {
2949 let key = Arc::as_ptr(body) as *const Expr;
2950 if let Some(&li) = local_cache.get(&key) {
2951 return li;
2952 }
2953 let promoted = cse_count.get(&key).copied().unwrap_or(0) >= 2;
2954 if promoted {
2955 if cse_contains_funcall(body, prelude_map) {
2962 panic!(
2963 "HybridTape: AMPL external function calls are not supported on the \
2964 hybrid (partial-separability) tape path. Build with \
2965 Tape::build_with_externals instead."
2966 );
2967 }
2968 let pslot =
2973 build_recursive(expr, prelude, prelude_map, &ExternalResolver::default());
2974 let li = local.len();
2975 local.push(SummandOp::Shared(pslot));
2976 local_cache.insert(key, li);
2977 li
2978 } else {
2979 let li =
2980 build_into_summand(body, local, local_cache, prelude, prelude_map, cse_count);
2981 local_cache.insert(key, li);
2982 li
2983 }
2984 }
2985 Expr::Compare(_, _, _)
2986 | Expr::And(_, _)
2987 | Expr::Or(_, _)
2988 | Expr::Not(_)
2989 | Expr::Cond { .. }
2990 | Expr::MinList(_)
2991 | Expr::MaxList(_) => {
2992 panic!(
2993 "HybridTape: conditional / logical / min-max opcodes (comparisons, \
2994 AND/OR/NOT, if-then-else, min/max lists) are not supported on the \
2995 hybrid (partial-separability) tape path. Build with \
2996 Tape::build_with_externals instead."
2997 );
2998 }
2999 Expr::Funcall { .. } => {
3000 panic!(
3001 "HybridTape: AMPL external function calls are not supported on the \
3002 hybrid (partial-separability) tape path. Build with Tape::build_with_externals \
3003 instead."
3004 );
3005 }
3006 }
3007}
3008
3009fn try_emit_const_pow_summand(
3012 base_expr: &Expr,
3013 c: f64,
3014 local: &mut Vec<SummandOp>,
3015 local_cache: &mut HashMap<*const Expr, usize>,
3016 prelude: &mut Vec<TapeOp>,
3017 prelude_map: &mut HashMap<*const Expr, usize>,
3018 cse_count: &HashMap<*const Expr, usize>,
3019) -> Option<usize> {
3020 if c == 0.0 {
3021 let i = local.len();
3022 local.push(SummandOp::Local(TapeOp::Const(1.0)));
3023 return Some(i);
3024 }
3025 if c == 1.0 {
3026 return Some(build_into_summand(
3027 base_expr,
3028 local,
3029 local_cache,
3030 prelude,
3031 prelude_map,
3032 cse_count,
3033 ));
3034 }
3035 if c == 0.5 {
3036 let b = build_into_summand(
3037 base_expr,
3038 local,
3039 local_cache,
3040 prelude,
3041 prelude_map,
3042 cse_count,
3043 );
3044 let i = local.len();
3045 local.push(SummandOp::Local(TapeOp::Sqrt(b)));
3046 return Some(i);
3047 }
3048 if c.is_finite() && c.fract() == 0.0 && c.abs() <= 8.0 {
3049 let n = c.abs() as u32;
3050 if n == 0 {
3051 let i = local.len();
3052 local.push(SummandOp::Local(TapeOp::Const(1.0)));
3053 return Some(i);
3054 }
3055 let b = build_into_summand(
3056 base_expr,
3057 local,
3058 local_cache,
3059 prelude,
3060 prelude_map,
3061 cse_count,
3062 );
3063 let pos = emit_int_pow_summand(b, n, local);
3064 if c < 0.0 {
3065 let one_idx = local.len();
3066 local.push(SummandOp::Local(TapeOp::Const(1.0)));
3067 let i = local.len();
3068 local.push(SummandOp::Local(TapeOp::Div(one_idx, pos)));
3069 return Some(i);
3070 }
3071 return Some(pos);
3072 }
3073 None
3074}
3075
3076fn emit_int_pow_summand(base: usize, n: u32, local: &mut Vec<SummandOp>) -> usize {
3077 debug_assert!(n >= 1);
3078 if n == 1 {
3079 return base;
3080 }
3081 let half = emit_int_pow_summand(base, n / 2, local);
3082 let squared = local.len();
3083 local.push(SummandOp::Local(TapeOp::Mul(half, half)));
3084 if n % 2 == 1 {
3085 let i = local.len();
3086 local.push(SummandOp::Local(TapeOp::Mul(squared, base)));
3087 i
3088 } else {
3089 squared
3090 }
3091}
3092
3093fn compute_local_reach(ops: &[SummandOp], root: usize) -> (Vec<usize>, Vec<usize>) {
3097 let mut visited = vec![false; ops.len()];
3098 let mut reach: Vec<usize> = Vec::new();
3099 let mut shared: BTreeSet<usize> = BTreeSet::new();
3100 let mut stack: Vec<usize> = Vec::with_capacity(16);
3101 visited[root] = true;
3102 reach.push(root);
3103 stack.push(root);
3104 while let Some(s) = stack.pop() {
3105 match &ops[s] {
3106 SummandOp::Local(op) => {
3107 let (a, b) = op_operands(op);
3108 if let Some(a) = a {
3109 if !visited[a] {
3110 visited[a] = true;
3111 reach.push(a);
3112 stack.push(a);
3113 }
3114 }
3115 if let Some(b) = b {
3116 if !visited[b] {
3117 visited[b] = true;
3118 reach.push(b);
3119 stack.push(b);
3120 }
3121 }
3122 }
3123 SummandOp::Shared(k) => {
3124 shared.insert(*k);
3125 }
3126 }
3127 }
3128 reach.sort_unstable();
3129 (reach, shared.into_iter().collect())
3130}
3131
3132fn bfs_prelude(
3136 prelude: &[TapeOp],
3137 start: usize,
3138 visited: &mut [u32],
3139 cur: u32,
3140 stack: &mut Vec<usize>,
3141 out: &mut Vec<usize>,
3142) {
3143 if visited[start] == cur {
3144 return;
3145 }
3146 visited[start] = cur;
3147 out.push(start);
3148 stack.push(start);
3149 while let Some(s) = stack.pop() {
3150 let (a, b) = op_operands(&prelude[s]);
3151 if let Some(a) = a {
3152 if visited[a] != cur {
3153 visited[a] = cur;
3154 out.push(a);
3155 stack.push(a);
3156 }
3157 }
3158 if let Some(b) = b {
3159 if visited[b] != cur {
3160 visited[b] = cur;
3161 out.push(b);
3162 stack.push(b);
3163 }
3164 }
3165 }
3166}
3167
3168fn compute_var_sets(ops: &[TapeOp]) -> Vec<BTreeSet<usize>> {
3172 let mut out: Vec<BTreeSet<usize>> = Vec::with_capacity(ops.len());
3173 for op in ops {
3174 let vs: BTreeSet<usize> = match op {
3175 TapeOp::Const(_) => BTreeSet::new(),
3176 TapeOp::Var(j) => {
3177 let mut s = BTreeSet::new();
3178 s.insert(*j);
3179 s
3180 }
3181 TapeOp::Add(a, b)
3182 | TapeOp::Sub(a, b)
3183 | TapeOp::Mul(a, b)
3184 | TapeOp::Div(a, b)
3185 | TapeOp::Pow(a, b)
3186 | TapeOp::Atan2(a, b)
3187 | TapeOp::CEntropy(a, b) => out[*a].union(&out[*b]).copied().collect(),
3188 TapeOp::Neg(a)
3189 | TapeOp::Abs(a)
3190 | TapeOp::Sqrt(a)
3191 | TapeOp::Exp(a)
3192 | TapeOp::Log(a)
3193 | TapeOp::Log10(a)
3194 | TapeOp::Sin(a)
3195 | TapeOp::Cos(a)
3196 | TapeOp::Tan(a)
3197 | TapeOp::Atan(a)
3198 | TapeOp::Acos(a)
3199 | TapeOp::Sinh(a)
3200 | TapeOp::Cosh(a)
3201 | TapeOp::Tanh(a)
3202 | TapeOp::Asin(a)
3203 | TapeOp::Acosh(a)
3204 | TapeOp::Asinh(a)
3205 | TapeOp::Erf(a)
3206 | TapeOp::XLogX(a)
3207 | TapeOp::Atanh(a) => out[*a].clone(),
3208 TapeOp::Cmp(_, _, _)
3209 | TapeOp::And(_, _)
3210 | TapeOp::Or(_, _)
3211 | TapeOp::Not(_)
3212 | TapeOp::Select(_, _, _)
3213 | TapeOp::Min(_, _)
3214 | TapeOp::Max(_, _) => unreachable!(
3215 "HybridTape prelude cannot contain conditional / logical / min-max \
3216 TapeOps; build_into_summand panics on those Expr variants."
3217 ),
3218 TapeOp::Funcall(_) => unreachable!(
3219 "HybridTape prelude cannot contain TapeOp::Funcall; \
3220 build_into_summand panics on Expr::Funcall."
3221 ),
3222 };
3223 out.push(vs);
3224 }
3225 out
3226}
3227
3228fn summand_sparsity(
3233 ops: &[SummandOp],
3234 prelude_var_sets: &[BTreeSet<usize>],
3235 pairs: &mut BTreeSet<(usize, usize)>,
3236) {
3237 let mut var_sets: Vec<BTreeSet<usize>> = Vec::with_capacity(ops.len());
3238 let emit_cross =
3239 |s1: &BTreeSet<usize>, s2: &BTreeSet<usize>, pairs: &mut BTreeSet<(usize, usize)>| {
3240 for &v1 in s1 {
3241 for &v2 in s2 {
3242 let (r, c) = if v1 >= v2 { (v1, v2) } else { (v2, v1) };
3243 pairs.insert((r, c));
3244 }
3245 }
3246 };
3247 let emit_self = |s: &BTreeSet<usize>, pairs: &mut BTreeSet<(usize, usize)>| {
3248 let vars: Vec<usize> = s.iter().copied().collect();
3249 for (ai, &vi) in vars.iter().enumerate() {
3250 for &vj in &vars[..=ai] {
3251 let (r, c) = if vi >= vj { (vi, vj) } else { (vj, vi) };
3252 pairs.insert((r, c));
3253 }
3254 }
3255 };
3256 for so in ops {
3257 let vset: BTreeSet<usize> = match so {
3258 SummandOp::Shared(k) => prelude_var_sets[*k].clone(),
3259 SummandOp::Local(op) => match op {
3260 TapeOp::Const(_) => BTreeSet::new(),
3261 TapeOp::Var(j) => {
3262 let mut s = BTreeSet::new();
3263 s.insert(*j);
3264 s
3265 }
3266 TapeOp::Add(a, b) | TapeOp::Sub(a, b) => {
3267 var_sets[*a].union(&var_sets[*b]).copied().collect()
3268 }
3269 TapeOp::Neg(a) | TapeOp::Abs(a) => var_sets[*a].clone(),
3270 TapeOp::Mul(a, b) => {
3271 emit_cross(&var_sets[*a], &var_sets[*b], pairs);
3272 var_sets[*a].union(&var_sets[*b]).copied().collect()
3273 }
3274 TapeOp::Div(a, b) => {
3275 emit_cross(&var_sets[*a], &var_sets[*b], pairs);
3276 emit_self(&var_sets[*b], pairs);
3277 var_sets[*a].union(&var_sets[*b]).copied().collect()
3278 }
3279 TapeOp::Pow(a, b) | TapeOp::Atan2(a, b) | TapeOp::CEntropy(a, b) => {
3280 let combined: BTreeSet<usize> =
3281 var_sets[*a].union(&var_sets[*b]).copied().collect();
3282 emit_self(&combined, pairs);
3283 combined
3284 }
3285 TapeOp::Sqrt(a)
3286 | TapeOp::Exp(a)
3287 | TapeOp::Log(a)
3288 | TapeOp::Log10(a)
3289 | TapeOp::Sin(a)
3290 | TapeOp::Cos(a)
3291 | TapeOp::Tan(a)
3292 | TapeOp::Atan(a)
3293 | TapeOp::Acos(a)
3294 | TapeOp::Sinh(a)
3295 | TapeOp::Cosh(a)
3296 | TapeOp::Tanh(a)
3297 | TapeOp::Asin(a)
3298 | TapeOp::Acosh(a)
3299 | TapeOp::Asinh(a)
3300 | TapeOp::Erf(a)
3301 | TapeOp::XLogX(a)
3302 | TapeOp::Atanh(a) => {
3303 emit_self(&var_sets[*a], pairs);
3304 var_sets[*a].clone()
3305 }
3306 TapeOp::Cmp(_, _, _)
3307 | TapeOp::And(_, _)
3308 | TapeOp::Or(_, _)
3309 | TapeOp::Not(_)
3310 | TapeOp::Select(_, _, _)
3311 | TapeOp::Min(_, _)
3312 | TapeOp::Max(_, _) => unreachable!(
3313 "HybridTape summand cannot contain conditional / logical / min-max \
3314 TapeOps; build_into_summand panics on those Expr variants."
3315 ),
3316 TapeOp::Funcall(_) => unreachable!(
3317 "HybridTape summand cannot contain TapeOp::Funcall; \
3318 build_into_summand panics on Expr::Funcall."
3319 ),
3320 },
3321 };
3322 var_sets.push(vset);
3323 }
3324}
3325
3326#[inline]
3329pub(crate) fn op_operands(op: &TapeOp) -> (Option<usize>, Option<usize>) {
3330 match op {
3331 TapeOp::Const(_) | TapeOp::Var(_) => (None, None),
3332 TapeOp::Add(a, b)
3333 | TapeOp::Sub(a, b)
3334 | TapeOp::Mul(a, b)
3335 | TapeOp::Div(a, b)
3336 | TapeOp::Pow(a, b)
3337 | TapeOp::Atan2(a, b)
3338 | TapeOp::CEntropy(a, b) => (Some(*a), Some(*b)),
3339 TapeOp::Neg(a)
3340 | TapeOp::Abs(a)
3341 | TapeOp::Sqrt(a)
3342 | TapeOp::Exp(a)
3343 | TapeOp::Log(a)
3344 | TapeOp::Log10(a)
3345 | TapeOp::Sin(a)
3346 | TapeOp::Cos(a)
3347 | TapeOp::Tan(a)
3348 | TapeOp::Atan(a)
3349 | TapeOp::Acos(a)
3350 | TapeOp::Sinh(a)
3351 | TapeOp::Cosh(a)
3352 | TapeOp::Tanh(a)
3353 | TapeOp::Asin(a)
3354 | TapeOp::Acosh(a)
3355 | TapeOp::Asinh(a)
3356 | TapeOp::Erf(a)
3357 | TapeOp::XLogX(a)
3358 | TapeOp::Atanh(a) => (Some(*a), None),
3359 TapeOp::Cmp(_, a, b) | TapeOp::And(a, b) | TapeOp::Or(a, b) => (Some(*a), Some(*b)),
3365 TapeOp::Not(a) => (Some(*a), None),
3366 TapeOp::Select(_, _, _) => unreachable!(
3367 "op_operands: TapeOp::Select has three operands and is unsupported on \
3368 the HybridTape path"
3369 ),
3370 TapeOp::Min(_, _) | TapeOp::Max(_, _) => unreachable!(
3371 "op_operands: TapeOp::Min/Max are unsupported on the HybridTape path \
3372 (build_into_summand rejects min/max lists)"
3373 ),
3374 TapeOp::Funcall(_) => unreachable!(
3382 "op_operands: TapeOp::Funcall is unsupported on the HybridTape path \
3383 (build_into_summand rejects external function calls)"
3384 ),
3385 }
3386}
3387
3388fn vars_in(ops: &[TapeOp], reach: &[usize]) -> Vec<usize> {
3389 let mut s: BTreeSet<usize> = BTreeSet::new();
3390 for &i in reach {
3391 if let TapeOp::Var(j) = &ops[i] {
3392 s.insert(*j);
3393 }
3394 }
3395 s.into_iter().collect()
3396}
3397
3398#[inline]
3401fn fwd_step(op: &TapeOp, x: &[f64], vals: &[f64]) -> f64 {
3402 match op {
3403 TapeOp::Const(c) => *c,
3404 TapeOp::Var(i) => x[*i],
3405 TapeOp::Add(a, b) => vals[*a] + vals[*b],
3406 TapeOp::Sub(a, b) => vals[*a] - vals[*b],
3407 TapeOp::Mul(a, b) => vals[*a] * vals[*b],
3408 TapeOp::Div(a, b) => vals[*a] / vals[*b],
3409 TapeOp::Pow(a, b) => vals[*a].powf(vals[*b]),
3410 TapeOp::Neg(a) => -vals[*a],
3411 TapeOp::Abs(a) => vals[*a].abs(),
3412 TapeOp::Sqrt(a) => vals[*a].sqrt(),
3413 TapeOp::Exp(a) => vals[*a].exp(),
3414 TapeOp::Log(a) => vals[*a].ln(),
3415 TapeOp::Log10(a) => vals[*a].log10(),
3416 TapeOp::Sin(a) => vals[*a].sin(),
3417 TapeOp::Cos(a) => vals[*a].cos(),
3418 TapeOp::Tan(a) => vals[*a].tan(),
3419 TapeOp::Atan(a) => vals[*a].atan(),
3420 TapeOp::Acos(a) => vals[*a].acos(),
3421 TapeOp::Sinh(a) => vals[*a].sinh(),
3422 TapeOp::Cosh(a) => vals[*a].cosh(),
3423 TapeOp::Tanh(a) => vals[*a].tanh(),
3424 TapeOp::Asin(a) => vals[*a].asin(),
3425 TapeOp::Acosh(a) => vals[*a].acosh(),
3426 TapeOp::Asinh(a) => vals[*a].asinh(),
3427 TapeOp::Atanh(a) => vals[*a].atanh(),
3428 TapeOp::Erf(a) => erf(vals[*a]),
3429 TapeOp::XLogX(a) => xlogx(vals[*a]),
3430 TapeOp::CEntropy(a, b) => centropy(vals[*a], vals[*b]),
3431 TapeOp::Atan2(a, b) => vals[*a].atan2(vals[*b]),
3432 TapeOp::Cmp(_, _, _)
3433 | TapeOp::And(_, _)
3434 | TapeOp::Or(_, _)
3435 | TapeOp::Not(_)
3436 | TapeOp::Select(_, _, _)
3437 | TapeOp::Min(_, _)
3438 | TapeOp::Max(_, _) => panic!(
3439 "GlobalTape free-function kernels do not implement conditional / logical \
3440 / min-max TapeOps; use the Tape (build_with_externals) interpreter path \
3441 instead."
3442 ),
3443 TapeOp::Funcall(fc) => {
3444 let FuncallData { lib, name, args } = fc.as_ref();
3445 let call_args = funcall_to_ext_args(args, vals);
3446 let res = lib
3447 .eval(name, &call_args, false, false)
3448 .unwrap_or_else(|e| panic!("external function '{name}' eval failed: {e}"));
3449 res.value
3450 }
3451 }
3452}
3453
3454#[inline]
3455fn rev_step(op: &TapeOp, i: usize, vals: &[f64], adj: &mut [f64], a: f64, grad: &mut [f64]) {
3456 match op {
3457 TapeOp::Const(_) => {}
3458 TapeOp::Var(j) => {
3459 grad[*j] += a;
3460 }
3461 TapeOp::Add(l, r) => {
3462 adj[*l] += a;
3463 adj[*r] += a;
3464 }
3465 TapeOp::Sub(l, r) => {
3466 adj[*l] += a;
3467 adj[*r] -= a;
3468 }
3469 TapeOp::Mul(l, r) => {
3470 adj[*l] += a * vals[*r];
3471 adj[*r] += a * vals[*l];
3472 }
3473 TapeOp::Div(l, r) => {
3474 let rv = vals[*r];
3476 adj[*l] += a / rv;
3477 adj[*r] -= a * vals[i] / rv;
3478 }
3479 TapeOp::Pow(l, r) => {
3480 let lv = vals[*l];
3481 let rv = vals[*r];
3482 if rv != 0.0 {
3483 adj[*l] += a * rv * lv.powf(rv - 1.0);
3484 }
3485 if lv > 0.0 {
3486 adj[*r] += a * vals[i] * lv.ln();
3487 }
3488 }
3489 TapeOp::Neg(j) => {
3490 adj[*j] -= a;
3491 }
3492 TapeOp::Abs(j) => {
3493 if vals[*j] >= 0.0 {
3494 adj[*j] += a;
3495 } else {
3496 adj[*j] -= a;
3497 }
3498 }
3499 TapeOp::Sqrt(j) => {
3500 let sv = vals[i];
3501 if sv > 0.0 {
3502 adj[*j] += a * 0.5 / sv;
3503 }
3504 }
3505 TapeOp::Exp(j) => {
3506 adj[*j] += a * vals[i];
3507 }
3508 TapeOp::Log(j) => {
3509 adj[*j] += a / vals[*j];
3510 }
3511 TapeOp::Log10(j) => {
3512 adj[*j] += a / (vals[*j] * std::f64::consts::LN_10);
3513 }
3514 TapeOp::Sin(j) => {
3515 adj[*j] += a * vals[*j].cos();
3516 }
3517 TapeOp::Cos(j) => {
3518 adj[*j] -= a * vals[*j].sin();
3519 }
3520 TapeOp::Tan(j) => {
3521 let t = vals[i];
3522 adj[*j] += a * (1.0 + t * t);
3523 }
3524 TapeOp::Atan(j) => {
3525 let u = vals[*j];
3526 adj[*j] += a / (1.0 + u * u);
3527 }
3528 TapeOp::Acos(j) => {
3529 let u = vals[*j];
3530 adj[*j] -= a / (1.0 - u * u).sqrt();
3531 }
3532 TapeOp::Sinh(j) => {
3533 adj[*j] += a * vals[*j].cosh();
3534 }
3535 TapeOp::Cosh(j) => {
3536 adj[*j] += a * vals[*j].sinh();
3537 }
3538 TapeOp::Tanh(j) => {
3539 let t = vals[i];
3540 adj[*j] += a * (1.0 - t * t);
3541 }
3542 TapeOp::Asin(j) => {
3543 let u = vals[*j];
3544 adj[*j] += a / (1.0 - u * u).sqrt();
3545 }
3546 TapeOp::Acosh(j) => {
3547 let u = vals[*j];
3548 adj[*j] += a / (u * u - 1.0).sqrt();
3549 }
3550 TapeOp::Asinh(j) => {
3551 let u = vals[*j];
3552 adj[*j] += a / (u * u + 1.0).sqrt();
3553 }
3554 TapeOp::Atanh(j) => {
3555 let u = vals[*j];
3556 adj[*j] += a / (1.0 - u * u);
3557 }
3558 TapeOp::Erf(j) => {
3559 adj[*j] += a * erf_d1(vals[*j]);
3560 }
3561 TapeOp::XLogX(j) => {
3562 adj[*j] += a * xlogx_d1(vals[*j]);
3563 }
3564 TapeOp::CEntropy(l, r) => {
3565 adj[*l] += a * centropy_da(vals[*l], vals[*r]);
3566 adj[*r] += a * centropy_db(vals[*l], vals[*r]);
3567 }
3568 TapeOp::Atan2(l, r) => {
3569 let y = vals[*l];
3570 let x = vals[*r];
3571 let d = y * y + x * x;
3572 adj[*l] += a * (x / d);
3573 adj[*r] += a * (-y / d);
3574 }
3575 TapeOp::Cmp(_, _, _)
3576 | TapeOp::And(_, _)
3577 | TapeOp::Or(_, _)
3578 | TapeOp::Not(_)
3579 | TapeOp::Select(_, _, _)
3580 | TapeOp::Min(_, _)
3581 | TapeOp::Max(_, _) => panic!(
3582 "GlobalTape free-function kernels do not implement conditional / logical \
3583 / min-max TapeOps; use the Tape (build_with_externals) interpreter path \
3584 instead."
3585 ),
3586 TapeOp::Funcall(fc) => {
3587 let FuncallData { lib, name, args } = fc.as_ref();
3588 let call_args = funcall_to_ext_args(args, vals);
3589 let res = lib
3590 .eval(name, &call_args, true, false)
3591 .unwrap_or_else(|e| panic!("external function '{name}' reverse eval failed: {e}"));
3592 let derivs = res.derivs.expect("want_derivs=true returns derivs");
3593 let mut k = 0usize;
3594 for arg in args {
3595 if let TapeFuncallArg::Tape(idx) = arg {
3596 adj[*idx] += a * derivs[k];
3597 k += 1;
3598 }
3599 }
3600 let _ = i;
3601 let _ = grad;
3602 }
3603 }
3604}
3605
3606#[inline]
3614fn fwd_dir_step(op: &TapeOp, seed: &[f64], vals: &[f64], dot: &[f64], i: usize) -> f64 {
3615 match op {
3616 TapeOp::Const(_) => 0.0,
3617 TapeOp::Var(k) => seed[*k],
3618 TapeOp::Add(a, b) => dot[*a] + dot[*b],
3619 TapeOp::Sub(a, b) => dot[*a] - dot[*b],
3620 TapeOp::Mul(a, b) => dot[*a] * vals[*b] + vals[*a] * dot[*b],
3621 TapeOp::Div(a, b) => {
3622 (dot[*a] - vals[i] * dot[*b]) / vals[*b]
3625 }
3626 TapeOp::Pow(a, b) => {
3627 let u = vals[*a];
3628 let r = vals[*b];
3629 let du = dot[*a];
3630 let dr = dot[*b];
3631 let mut result = 0.0;
3632 if r != 0.0 {
3637 result += r * u.powf(r - 1.0) * du;
3638 }
3639 if u > 0.0 {
3640 result += vals[i] * u.ln() * dr;
3641 }
3642 result
3643 }
3644 TapeOp::Neg(a) => -dot[*a],
3645 TapeOp::Abs(a) => {
3646 if vals[*a] >= 0.0 {
3647 dot[*a]
3648 } else {
3649 -dot[*a]
3650 }
3651 }
3652 TapeOp::Sqrt(a) => {
3653 let sv = vals[i];
3654 if sv > 0.0 { dot[*a] * 0.5 / sv } else { 0.0 }
3655 }
3656 TapeOp::Exp(a) => vals[i] * dot[*a],
3657 TapeOp::Log(a) => dot[*a] / vals[*a],
3658 TapeOp::Log10(a) => dot[*a] / (vals[*a] * std::f64::consts::LN_10),
3659 TapeOp::Sin(a) => vals[*a].cos() * dot[*a],
3660 TapeOp::Cos(a) => -vals[*a].sin() * dot[*a],
3661 TapeOp::Tan(a) => {
3662 let t = vals[i];
3663 (1.0 + t * t) * dot[*a]
3664 }
3665 TapeOp::Atan(a) => {
3666 let u = vals[*a];
3667 dot[*a] / (1.0 + u * u)
3668 }
3669 TapeOp::Acos(a) => {
3670 let u = vals[*a];
3671 -dot[*a] / (1.0 - u * u).sqrt()
3672 }
3673 TapeOp::Sinh(a) => dot[*a] * vals[*a].cosh(),
3674 TapeOp::Cosh(a) => dot[*a] * vals[*a].sinh(),
3675 TapeOp::Tanh(a) => {
3676 let t = vals[i];
3677 (1.0 - t * t) * dot[*a]
3678 }
3679 TapeOp::Asin(a) => {
3680 let u = vals[*a];
3681 dot[*a] / (1.0 - u * u).sqrt()
3682 }
3683 TapeOp::Acosh(a) => {
3684 let u = vals[*a];
3685 dot[*a] / (u * u - 1.0).sqrt()
3686 }
3687 TapeOp::Asinh(a) => {
3688 let u = vals[*a];
3689 dot[*a] / (u * u + 1.0).sqrt()
3690 }
3691 TapeOp::Atanh(a) => {
3692 let u = vals[*a];
3693 dot[*a] / (1.0 - u * u)
3694 }
3695 TapeOp::Erf(a) => erf_d1(vals[*a]) * dot[*a],
3696 TapeOp::XLogX(a) => xlogx_d1(vals[*a]) * dot[*a],
3697 TapeOp::CEntropy(a, b) => {
3698 centropy_da(vals[*a], vals[*b]) * dot[*a] + centropy_db(vals[*a], vals[*b]) * dot[*b]
3699 }
3700 TapeOp::Atan2(a, b) => {
3701 let y = vals[*a];
3702 let x = vals[*b];
3703 let d = y * y + x * x;
3704 (x * dot[*a] - y * dot[*b]) / d
3705 }
3706 TapeOp::Cmp(_, _, _)
3707 | TapeOp::And(_, _)
3708 | TapeOp::Or(_, _)
3709 | TapeOp::Not(_)
3710 | TapeOp::Select(_, _, _)
3711 | TapeOp::Min(_, _)
3712 | TapeOp::Max(_, _) => panic!(
3713 "GlobalTape free-function kernels do not implement conditional / logical \
3714 / min-max TapeOps; use the Tape (build_with_externals) interpreter path \
3715 instead."
3716 ),
3717 TapeOp::Funcall(fc) => {
3718 let FuncallData { lib, name, args } = fc.as_ref();
3719 let call_args = funcall_to_ext_args(args, vals);
3720 let res = lib
3721 .eval(name, &call_args, true, false)
3722 .unwrap_or_else(|e| panic!("external function '{name}' tangent eval failed: {e}"));
3723 let derivs = res.derivs.expect("want_derivs=true returns derivs");
3724 let mut acc = 0.0;
3725 let mut k = 0usize;
3726 for arg in args {
3727 if let TapeFuncallArg::Tape(idx) = arg {
3728 acc += derivs[k] * dot[*idx];
3729 k += 1;
3730 }
3731 }
3732 let _ = seed;
3733 acc
3734 }
3735 }
3736}
3737
3738#[allow(clippy::too_many_arguments)]
3747#[inline]
3748fn ror_dir_step(
3749 op: &TapeOp,
3750 i: usize,
3751 vals: &[f64],
3752 dot: &[f64],
3753 adj: &mut [f64],
3754 adj_dot: &mut [f64],
3755 w: f64,
3756 wd: f64,
3757 weight: f64,
3758 out: &mut [f64],
3759) {
3760 match op {
3761 TapeOp::Const(_) => {}
3762 TapeOp::Var(k) => {
3763 if wd != 0.0 {
3764 out[*k] += weight * wd;
3765 }
3766 }
3767 TapeOp::Add(a, b) => {
3768 adj[*a] += w;
3769 adj[*b] += w;
3770 adj_dot[*a] += wd;
3771 adj_dot[*b] += wd;
3772 }
3773 TapeOp::Sub(a, b) => {
3774 adj[*a] += w;
3775 adj[*b] -= w;
3776 adj_dot[*a] += wd;
3777 adj_dot[*b] -= wd;
3778 }
3779 TapeOp::Mul(a, b) => {
3780 adj[*a] += w * vals[*b];
3781 adj[*b] += w * vals[*a];
3782 adj_dot[*a] += wd * vals[*b] + w * dot[*b];
3783 adj_dot[*b] += wd * vals[*a] + w * dot[*a];
3784 }
3785 TapeOp::Div(a, b) => {
3786 let vb = vals[*b];
3789 let q = vals[i];
3790 let qd = dot[i];
3791 adj[*a] += w / vb;
3792 adj_dot[*a] += wd / vb - w * (dot[*b] / vb) / vb;
3793 adj[*b] -= w * q / vb;
3794 adj_dot[*b] += -(wd * q) / vb + (w / vb) * (-qd + q * (dot[*b] / vb));
3795 }
3796 TapeOp::Pow(a, b) => {
3797 let u = vals[*a];
3798 let r = vals[*b];
3799 let du = dot[*a];
3800 let dr = dot[*b];
3801 if r != 0.0 {
3802 if u != 0.0 {
3803 let p_a = r * u.powf(r - 1.0);
3804 adj[*a] += w * p_a;
3805 let mut dp_a = dr * u.powf(r - 1.0);
3806 if u > 0.0 {
3807 dp_a += r * u.powf(r - 1.0) * ((r - 1.0) * du / u + dr * u.ln());
3808 } else {
3809 dp_a += r * (r - 1.0) * u.powf(r - 2.0) * du;
3810 }
3811 adj_dot[*a] += wd * p_a + w * dp_a;
3812 } else if r >= 2.0 {
3813 let p_a = 0.0;
3814 adj[*a] += w * p_a;
3815 let dp_a = if r == 2.0 {
3816 2.0 * du
3817 } else {
3818 r * (r - 1.0) * (0.0_f64).powf(r - 2.0) * du
3819 };
3820 adj_dot[*a] += wd * p_a + w * dp_a;
3821 }
3822 }
3823 if u > 0.0 {
3824 let ln_u = u.ln();
3825 let p_b = vals[i] * ln_u;
3826 adj[*b] += w * p_b;
3827 let dur = vals[i] * (r * du / u + dr * ln_u);
3828 let dp_b = dur * ln_u + vals[i] * du / u;
3829 adj_dot[*b] += wd * p_b + w * dp_b;
3830 }
3831 }
3832 TapeOp::Neg(a) => {
3833 adj[*a] -= w;
3834 adj_dot[*a] -= wd;
3835 }
3836 TapeOp::Abs(a) => {
3837 let s = if vals[*a] >= 0.0 { 1.0 } else { -1.0 };
3838 adj[*a] += w * s;
3839 adj_dot[*a] += wd * s;
3840 }
3841 TapeOp::Sqrt(a) => {
3842 let sv = vals[i];
3843 if sv > 0.0 {
3844 let fp = 0.5 / sv;
3845 let fpp = -0.25 / (vals[*a] * sv);
3846 adj[*a] += w * fp;
3847 adj_dot[*a] += wd * fp + w * fpp * dot[*a];
3848 }
3849 }
3850 TapeOp::Exp(a) => {
3851 let ev = vals[i];
3852 adj[*a] += w * ev;
3853 adj_dot[*a] += wd * ev + w * ev * dot[*a];
3854 }
3855 TapeOp::Log(a) => {
3856 let u = vals[*a];
3857 adj[*a] += w / u;
3858 adj_dot[*a] += wd / u + w * (-1.0 / (u * u)) * dot[*a];
3859 }
3860 TapeOp::Log10(a) => {
3861 let u = vals[*a];
3862 let c = std::f64::consts::LN_10;
3863 adj[*a] += w / (u * c);
3864 adj_dot[*a] += wd / (u * c) + w * (-1.0 / (u * u * c)) * dot[*a];
3865 }
3866 TapeOp::Sin(a) => {
3867 let u = vals[*a];
3868 let cu = u.cos();
3869 adj[*a] += w * cu;
3870 adj_dot[*a] += wd * cu + w * (-u.sin()) * dot[*a];
3871 }
3872 TapeOp::Cos(a) => {
3873 let u = vals[*a];
3874 let su = u.sin();
3875 adj[*a] -= w * su;
3876 adj_dot[*a] += wd * (-su) + w * (-u.cos()) * dot[*a];
3877 }
3878 TapeOp::Tan(a) => {
3879 let t = vals[i];
3880 let gp = 1.0 + t * t;
3881 let gpp = 2.0 * t * gp;
3882 adj[*a] += w * gp;
3883 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3884 }
3885 TapeOp::Atan(a) => {
3886 let u = vals[*a];
3887 let d = 1.0 + u * u;
3888 let gp = 1.0 / d;
3889 let gpp = -2.0 * u / (d * d);
3890 adj[*a] += w * gp;
3891 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3892 }
3893 TapeOp::Acos(a) => {
3894 let u = vals[*a];
3895 let s = 1.0 - u * u;
3896 let r = s.sqrt();
3897 let gp = -1.0 / r;
3898 let gpp = -u / (s * r);
3899 adj[*a] += w * gp;
3900 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3901 }
3902 TapeOp::Sinh(a) => {
3903 let u = vals[*a];
3904 let gp = u.cosh();
3905 let gpp = vals[i]; adj[*a] += w * gp;
3907 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3908 }
3909 TapeOp::Cosh(a) => {
3910 let u = vals[*a];
3911 let gp = u.sinh();
3912 let gpp = vals[i]; adj[*a] += w * gp;
3914 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3915 }
3916 TapeOp::Tanh(a) => {
3917 let t = vals[i];
3918 let gp = 1.0 - t * t;
3919 let gpp = -2.0 * t * gp;
3920 adj[*a] += w * gp;
3921 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3922 }
3923 TapeOp::Asin(a) => {
3924 let u = vals[*a];
3925 let s = 1.0 - u * u;
3926 let r = s.sqrt();
3927 let gp = 1.0 / r;
3928 let gpp = u / (s * r);
3929 adj[*a] += w * gp;
3930 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3931 }
3932 TapeOp::Acosh(a) => {
3933 let u = vals[*a];
3934 let s = u * u - 1.0;
3935 let r = s.sqrt();
3936 let gp = 1.0 / r;
3937 let gpp = -u / (s * r);
3938 adj[*a] += w * gp;
3939 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3940 }
3941 TapeOp::Asinh(a) => {
3942 let u = vals[*a];
3943 let s = u * u + 1.0;
3944 let r = s.sqrt();
3945 let gp = 1.0 / r;
3946 let gpp = -u / (s * r);
3947 adj[*a] += w * gp;
3948 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3949 }
3950 TapeOp::Atanh(a) => {
3951 let u = vals[*a];
3952 let d = 1.0 - u * u;
3953 let gp = 1.0 / d;
3954 let gpp = 2.0 * u / (d * d);
3955 adj[*a] += w * gp;
3956 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3957 }
3958 TapeOp::Erf(a) => {
3959 let u = vals[*a];
3960 let gp = erf_d1(u);
3961 let gpp = erf_d2(u);
3962 adj[*a] += w * gp;
3963 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3964 }
3965 TapeOp::XLogX(a) => {
3966 let u = vals[*a];
3969 let gp = xlogx_d1(u);
3970 let gpp = xlogx_d2(u);
3971 adj[*a] += w * gp;
3972 adj_dot[*a] += wd * gp + w * gpp * dot[*a];
3973 }
3974 TapeOp::CEntropy(a, b) => {
3975 let ua = vals[*a];
3976 let ub = vals[*b];
3977 let fa = centropy_da(ua, ub);
3978 let fb = centropy_db(ua, ub);
3979 let faa = centropy_daa(ua);
3980 let fab = centropy_dab(ub);
3981 let fbb = centropy_dbb(ua, ub);
3982 adj[*a] += w * fa;
3983 adj[*b] += w * fb;
3984 adj_dot[*a] += wd * fa + w * (faa * dot[*a] + fab * dot[*b]);
3985 adj_dot[*b] += wd * fb + w * (fab * dot[*a] + fbb * dot[*b]);
3986 }
3987 TapeOp::Atan2(a, b) => {
3988 let y = vals[*a];
3989 let x = vals[*b];
3990 let d = y * y + x * x;
3991 let d2 = d * d;
3992 let fa = x / d;
3993 let fb = -y / d;
3994 let faa = -2.0 * x * y / d2;
3995 let fab = (y * y - x * x) / d2;
3996 let fbb = 2.0 * x * y / d2;
3997 adj[*a] += w * fa;
3998 adj[*b] += w * fb;
3999 adj_dot[*a] += wd * fa + w * (faa * dot[*a] + fab * dot[*b]);
4000 adj_dot[*b] += wd * fb + w * (fab * dot[*a] + fbb * dot[*b]);
4001 }
4002 TapeOp::Cmp(_, _, _)
4003 | TapeOp::And(_, _)
4004 | TapeOp::Or(_, _)
4005 | TapeOp::Not(_)
4006 | TapeOp::Select(_, _, _)
4007 | TapeOp::Min(_, _)
4008 | TapeOp::Max(_, _) => panic!(
4009 "GlobalTape free-function kernels do not implement conditional / logical \
4010 / min-max TapeOps; use the Tape (build_with_externals) interpreter path \
4011 instead."
4012 ),
4013 TapeOp::Funcall(fc) => {
4014 let FuncallData { lib, name, args } = fc.as_ref();
4015 let call_args = funcall_to_ext_args(args, vals);
4016 let res = lib.eval(name, &call_args, true, true).unwrap_or_else(|e| {
4017 panic!("external function '{name}' 2nd-order eval failed: {e}")
4018 });
4019 let derivs = res.derivs.expect("want_derivs=true returns derivs");
4020 let hes = res.hessian.expect("want_hes=true returns hessian");
4021 let real_tape: Vec<usize> = args
4022 .iter()
4023 .filter_map(|a| match a {
4024 TapeFuncallArg::Tape(t) => Some(*t),
4025 TapeFuncallArg::Str(_) => None,
4026 })
4027 .collect();
4028 for (k, &tk) in real_tape.iter().enumerate() {
4029 adj[tk] += w * derivs[k];
4030 let mut second_term = 0.0;
4031 for (l, &tl) in real_tape.iter().enumerate() {
4032 let (lo, hi) = if k <= l { (k, l) } else { (l, k) };
4033 let h_kl = hes[lo + hi * (hi + 1) / 2];
4034 second_term += h_kl * dot[tl];
4035 }
4036 adj_dot[tk] += wd * derivs[k] + w * second_term;
4037 }
4038 let _ = out;
4039 let _ = weight;
4040 let _ = i;
4041 }
4042 }
4043}
4044
4045fn hessian_sparsity_impl(ops: &[TapeOp]) -> BTreeSet<(usize, usize)> {
4049 let n = ops.len();
4050 let mut var_sets: Vec<BTreeSet<usize>> = Vec::with_capacity(n);
4051 let mut pairs: BTreeSet<(usize, usize)> = BTreeSet::new();
4052
4053 let emit_cross =
4054 |s1: &BTreeSet<usize>, s2: &BTreeSet<usize>, pairs: &mut BTreeSet<(usize, usize)>| {
4055 for &v1 in s1 {
4056 for &v2 in s2 {
4057 let (r, c) = if v1 >= v2 { (v1, v2) } else { (v2, v1) };
4058 pairs.insert((r, c));
4059 }
4060 }
4061 };
4062 let emit_self = |s: &BTreeSet<usize>, pairs: &mut BTreeSet<(usize, usize)>| {
4063 let vars: Vec<usize> = s.iter().copied().collect();
4064 for (ai, &vi) in vars.iter().enumerate() {
4065 for &vj in &vars[..=ai] {
4066 let (r, c) = if vi >= vj { (vi, vj) } else { (vj, vi) };
4067 pairs.insert((r, c));
4068 }
4069 }
4070 };
4071
4072 for op in ops {
4073 let vset = match op {
4074 TapeOp::Const(_) => BTreeSet::new(),
4075 TapeOp::Var(j) => {
4076 let mut s = BTreeSet::new();
4077 s.insert(*j);
4078 s
4079 }
4080 TapeOp::Add(a, b) | TapeOp::Sub(a, b) => {
4081 var_sets[*a].union(&var_sets[*b]).copied().collect()
4082 }
4083 TapeOp::Neg(a) | TapeOp::Abs(a) => var_sets[*a].clone(),
4084 TapeOp::Mul(a, b) => {
4085 emit_cross(&var_sets[*a], &var_sets[*b], &mut pairs);
4086 var_sets[*a].union(&var_sets[*b]).copied().collect()
4087 }
4088 TapeOp::Div(a, b) => {
4089 emit_cross(&var_sets[*a], &var_sets[*b], &mut pairs);
4090 emit_self(&var_sets[*b], &mut pairs);
4091 var_sets[*a].union(&var_sets[*b]).copied().collect()
4092 }
4093 TapeOp::Pow(a, b) | TapeOp::Atan2(a, b) | TapeOp::CEntropy(a, b) => {
4094 let combined: BTreeSet<usize> =
4095 var_sets[*a].union(&var_sets[*b]).copied().collect();
4096 emit_self(&combined, &mut pairs);
4097 combined
4098 }
4099 TapeOp::Sqrt(a)
4100 | TapeOp::Exp(a)
4101 | TapeOp::Log(a)
4102 | TapeOp::Log10(a)
4103 | TapeOp::Sin(a)
4104 | TapeOp::Cos(a)
4105 | TapeOp::Tan(a)
4106 | TapeOp::Atan(a)
4107 | TapeOp::Acos(a)
4108 | TapeOp::Sinh(a)
4109 | TapeOp::Cosh(a)
4110 | TapeOp::Tanh(a)
4111 | TapeOp::Asin(a)
4112 | TapeOp::Acosh(a)
4113 | TapeOp::Asinh(a)
4114 | TapeOp::Erf(a)
4115 | TapeOp::XLogX(a)
4116 | TapeOp::Atanh(a) => {
4117 emit_self(&var_sets[*a], &mut pairs);
4118 var_sets[*a].clone()
4119 }
4120 TapeOp::Funcall(fc) => {
4121 let args = &fc.args;
4122 let mut combined: BTreeSet<usize> = BTreeSet::new();
4123 for arg in args {
4124 if let TapeFuncallArg::Tape(t) = arg {
4125 for &vv in &var_sets[*t] {
4126 combined.insert(vv);
4127 }
4128 }
4129 }
4130 emit_self(&combined, &mut pairs);
4131 combined
4132 }
4133 TapeOp::Cmp(_, _, _) | TapeOp::And(_, _) | TapeOp::Or(_, _) | TapeOp::Not(_) => {
4134 BTreeSet::new()
4137 }
4138 TapeOp::Select(_, t, e) => {
4139 var_sets[*t].union(&var_sets[*e]).copied().collect()
4142 }
4143 TapeOp::Min(a, b) | TapeOp::Max(a, b) => {
4144 var_sets[*a].union(&var_sets[*b]).copied().collect()
4147 }
4148 };
4149 var_sets.push(vset);
4150 }
4151 pairs
4152}
4153
4154#[cfg(test)]
4155mod tests {
4156 use super::*;
4157
4158 fn cnst(c: f64) -> Expr {
4159 Expr::Const(c)
4160 }
4161 fn var(i: usize) -> Expr {
4162 Expr::Var(i)
4163 }
4164 fn add(a: Expr, b: Expr) -> Expr {
4165 Expr::Binary(BinOp::Add, Box::new(a), Box::new(b))
4166 }
4167 fn mul(a: Expr, b: Expr) -> Expr {
4168 Expr::Binary(BinOp::Mul, Box::new(a), Box::new(b))
4169 }
4170 fn pow(a: Expr, b: Expr) -> Expr {
4171 Expr::Binary(BinOp::Pow, Box::new(a), Box::new(b))
4172 }
4173 fn div(a: Expr, b: Expr) -> Expr {
4174 Expr::Binary(BinOp::Div, Box::new(a), Box::new(b))
4175 }
4176 fn unary(op: UnaryOp, a: Expr) -> Expr {
4177 Expr::Unary(op, Box::new(a))
4178 }
4179 fn cmp(op: CmpOp, a: Expr, b: Expr) -> Expr {
4180 Expr::Compare(op, Box::new(a), Box::new(b))
4181 }
4182 fn cond(c: Expr, t: Expr, e: Expr) -> Expr {
4183 Expr::Cond {
4184 cond: Box::new(c),
4185 then_: Box::new(t),
4186 else_: Box::new(e),
4187 }
4188 }
4189
4190 #[test]
4191 fn polynomial_eval_and_grad() {
4192 let e = add(
4194 mul(cnst(3.0), pow(var(0), cnst(2.0))),
4195 mul(cnst(2.0), var(1)),
4196 );
4197 let t = Tape::build(&e);
4198 assert!((t.eval(&[2.0, 3.0]) - 18.0).abs() < 1e-12);
4199 let mut g = vec![0.0; 2];
4200 t.gradient_seed(&[2.0, 3.0], 1.0, &mut g);
4201 assert!((g[0] - 12.0).abs() < 1e-12);
4203 assert!((g[1] - 2.0).abs() < 1e-12);
4204 }
4205
4206 #[test]
4207 fn cse_shared_body_evaluated_once() {
4208 let body = Arc::new(add(var(0), var(1)));
4210 let e = add(
4211 pow(Expr::Cse(body.clone()), cnst(2.0)),
4212 Expr::Cse(body.clone()),
4213 );
4214 let t = Tape::build(&e);
4215 let n_body_adds = t
4217 .ops
4218 .iter()
4219 .filter(|op| {
4220 matches!(op, TapeOp::Add(a, b) if {
4221 matches!(t.ops[*a], TapeOp::Var(0)) && matches!(t.ops[*b], TapeOp::Var(1))
4222 })
4223 })
4224 .count();
4225 assert_eq!(n_body_adds, 1, "CSE body should be emitted exactly once");
4226
4227 assert!((t.eval(&[1.0, 2.0]) - 12.0).abs() < 1e-12);
4229 let mut g = vec![0.0; 2];
4230 t.gradient_seed(&[1.0, 2.0], 1.0, &mut g);
4231 assert!((g[0] - 7.0).abs() < 1e-12);
4233 assert!((g[1] - 7.0).abs() < 1e-12);
4234 }
4235
4236 fn fd_check(tape: &Tape, x: &[f64], n: usize, tol: f64) {
4237 let vars = tape.variables();
4238 let mut hess_map: HashMap<(usize, usize), usize> = HashMap::new();
4239 let mut pairs = Vec::new();
4240 for (ai, &vi) in vars.iter().enumerate() {
4241 for &vj in &vars[..=ai] {
4242 let (r, c) = if vi >= vj { (vi, vj) } else { (vj, vi) };
4243 hess_map.entry((r, c)).or_insert_with(|| {
4244 let p = pairs.len();
4245 pairs.push((r, c));
4246 p
4247 });
4248 }
4249 }
4250 let nnz = pairs.len();
4251 let mut ad = vec![0.0; nnz];
4252 tape.hessian_accumulate(x, 1.0, &hess_map, &mut ad);
4253
4254 let mut fd = vec![0.0; nnz];
4255 let mut xp = x.to_vec();
4256 let mut gp = vec![0.0; n];
4257 let mut gm = vec![0.0; n];
4258 for &j in &vars {
4259 let h = (1e-7_f64).max(x[j].abs() * 1e-7);
4260 xp[j] = x[j] + h;
4261 gp.iter_mut().for_each(|v| *v = 0.0);
4262 tape.gradient_seed(&xp, 1.0, &mut gp);
4263 xp[j] = x[j] - h;
4264 gm.iter_mut().for_each(|v| *v = 0.0);
4265 tape.gradient_seed(&xp, 1.0, &mut gm);
4266 xp[j] = x[j];
4267 for &i in &vars {
4268 if i >= j {
4269 if let Some(&pos) = hess_map.get(&(i, j)) {
4270 fd[pos] = (gp[i] - gm[i]) / (2.0 * h);
4271 }
4272 }
4273 }
4274 }
4275 for (k, &(r, c)) in pairs.iter().enumerate() {
4276 let scale = fd[k].abs().max(1.0);
4277 assert!(
4278 (ad[k] - fd[k]).abs() / scale < tol,
4279 "H[{},{}]: AD={:.6e} FD={:.6e}",
4280 r,
4281 c,
4282 ad[k],
4283 fd[k]
4284 );
4285 }
4286 }
4287
4288 #[test]
4289 fn hessian_quadratic_matches_fd() {
4290 let e = add(
4292 add(
4293 mul(cnst(3.0), pow(var(0), cnst(2.0))),
4294 mul(cnst(2.0), mul(var(0), var(1))),
4295 ),
4296 pow(var(1), cnst(2.0)),
4297 );
4298 let t = Tape::build(&e);
4299 fd_check(&t, &[2.0, 3.0], 2, 1e-5);
4300 }
4301
4302 #[test]
4303 fn hessian_transcendental_matches_fd() {
4304 let e = Expr::Sum(vec![
4306 unary(UnaryOp::Exp, var(0)),
4307 unary(UnaryOp::Sin, var(1)),
4308 unary(UnaryOp::Log, var(0)),
4309 unary(UnaryOp::Sqrt, var(1)),
4310 mul(var(0), var(1)),
4311 ]);
4312 let t = Tape::build(&e);
4313 fd_check(&t, &[1.5, 2.0], 2, 1e-5);
4314 }
4315
4316 #[test]
4317 fn inverse_trig_grad_and_hessian_match_fd() {
4318 let e = Expr::Sum(vec![
4322 unary(UnaryOp::Tan, var(0)),
4323 unary(UnaryOp::Atan, var(1)),
4324 unary(UnaryOp::Acos, var(2)),
4325 mul(var(0), var(1)),
4326 ]);
4327 let t = Tape::build(&e);
4328 let x = [0.5, 1.3, 0.3];
4329
4330 let mut g = vec![0.0; 3];
4334 t.gradient_seed(&x, 1.0, &mut g);
4335 for j in 0..3 {
4336 let h = (1e-7_f64).max(x[j].abs() * 1e-7);
4337 let mut xp = x;
4338 let mut xm = x;
4339 xp[j] += h;
4340 xm[j] -= h;
4341 let fd = (t.eval(&xp) - t.eval(&xm)) / (2.0 * h);
4342 let scale = fd.abs().max(1.0);
4343 assert!(
4344 (g[j] - fd).abs() / scale < 1e-5,
4345 "grad[{j}]: AD={:.6e} FD={:.6e}",
4346 g[j],
4347 fd
4348 );
4349 }
4350
4351 fd_check(&t, &x, 3, 1e-5);
4353 }
4354
4355 fn grad_and_hess_match_fd(e: &Expr, x: &[f64], tol: f64) {
4358 let n = x.len();
4359 let t = Tape::build(e);
4360 let mut g = vec![0.0; n];
4361 t.gradient_seed(x, 1.0, &mut g);
4362 for j in 0..n {
4363 let h = (1e-7_f64).max(x[j].abs() * 1e-7);
4364 let mut xp = x.to_vec();
4365 let mut xm = x.to_vec();
4366 xp[j] += h;
4367 xm[j] -= h;
4368 let fd = (t.eval(&xp) - t.eval(&xm)) / (2.0 * h);
4369 let scale = fd.abs().max(1.0);
4370 assert!(
4371 (g[j] - fd).abs() / scale < tol,
4372 "grad[{j}]: AD={:.6e} FD={:.6e}",
4373 g[j],
4374 fd
4375 );
4376 }
4377 fd_check(&t, x, n, tol);
4378 }
4379
4380 #[test]
4381 fn hyperbolic_grad_and_hessian_match_fd() {
4382 let e = Expr::Sum(vec![
4385 unary(UnaryOp::Sinh, var(0)),
4386 unary(UnaryOp::Cosh, var(1)),
4387 unary(UnaryOp::Tanh, var(2)),
4388 unary(UnaryOp::Asinh, var(3)),
4389 mul(var(0), var(1)),
4390 mul(var(2), var(3)),
4391 ]);
4392 grad_and_hess_match_fd(&e, &[0.5, 0.7, 0.3, 1.1], 1e-5);
4393 }
4394
4395 #[test]
4396 fn restricted_inverse_grad_and_hessian_match_fd() {
4397 let e = Expr::Sum(vec![
4401 unary(UnaryOp::Asin, var(0)),
4402 unary(UnaryOp::Acosh, var(1)),
4403 unary(UnaryOp::Atanh, var(2)),
4404 mul(var(0), var(2)),
4405 ]);
4406 grad_and_hess_match_fd(&e, &[0.4, 1.8, 0.3], 1e-5);
4407 }
4408
4409 #[test]
4410 fn erf_value_matches_reference() {
4411 let t = Tape::build(&unary(UnaryOp::Erf, var(0)));
4416 for (x, want) in [
4417 (0.0, 0.0),
4418 (0.5, 0.520_499_877_813_046_5),
4419 (1.0, 0.842_700_792_949_714_9),
4420 (-1.0, -0.842_700_792_949_714_9),
4421 (2.0, 0.995_322_265_018_952_7),
4422 (3.0, 0.999_977_909_503_001_4),
4423 ] {
4424 let got = t.eval(&[x]);
4425 assert!(
4426 (got - want).abs() < 1e-15,
4427 "erf({x}): got {got:.17e}, want {want:.17e}"
4428 );
4429 }
4430 assert!((t.eval(&[-0.3]) + t.eval(&[0.3])).abs() < 1e-16);
4433 assert!((t.eval(&[10.0]) - 1.0).abs() < 1e-15);
4434 }
4435
4436 #[test]
4437 fn erf_second_derivative_stays_finite_at_extreme_magnitudes() {
4438 for u in [1e19, 1e150, 1e300, f64::MAX, f64::MAX / 2.0] {
4446 for signed in [u, -u] {
4447 let d2 = erf_d2(signed);
4448 assert!(
4449 d2.is_finite(),
4450 "erf_d2({signed:e}) = {d2} — must be finite (the limit is 0)"
4451 );
4452 }
4453 }
4454 let u = 0.7;
4456 let want = -2.0 * u * (2.0 / std::f64::consts::PI.sqrt()) * (-u * u).exp();
4457 assert!((erf_d2(u) - want).abs() < 1e-15, "{}", erf_d2(u));
4458 }
4459
4460 #[test]
4461 fn quotient_rule_keeps_representable_derivatives_at_extreme_denominators() {
4462 let t = Tape::build(&div(var(0), var(1)));
4473 let mut g = vec![0.0; 2];
4474 for b in [1e300_f64, 1e200, 1e160, -1e300, 1e-160, 1e-300] {
4475 g.iter_mut().for_each(|v| *v = 0.0);
4481 t.gradient_seed(&[b, b], 1.0, &mut g);
4482 assert_eq!(g[0], 1.0 / b, "d(a/b)/da at a=b={b:e}");
4483 assert_eq!(
4484 g[1],
4485 -1.0 / b,
4486 "d(a/b)/db at a=b={b:e} — b² is not representable"
4487 );
4488 }
4489
4490 let b = 1e155_f64;
4494 let mut hess_map: HashMap<(usize, usize), usize> = HashMap::new();
4495 hess_map.insert((0, 0), 0);
4496 hess_map.insert((1, 0), 1);
4497 hess_map.insert((1, 1), 2);
4498 let mut ad = vec![0.0; 3];
4499 t.hessian_accumulate(&[b, b], 1.0, &hess_map, &mut ad);
4500 let want = -(1.0 / b) / b;
4504 assert!(
4505 want != 0.0 && want.is_finite(),
4506 "test premise: -1/b² must be representable, got {want:e}"
4507 );
4508 assert!(
4509 (ad[1] - want).abs() <= 1e-10 * want.abs(),
4510 "d²(a/b)/da db at a=b={b:e}: got {:e}, want {want:e}",
4511 ad[1]
4512 );
4513 }
4514
4515 #[test]
4516 fn erf_grad_and_hessian_match_fd() {
4517 let e = Expr::Sum(vec![
4521 unary(UnaryOp::Erf, var(0)),
4522 unary(UnaryOp::Erf, mul(cnst(2.0), var(1))),
4523 mul(var(0), var(1)),
4524 ]);
4525 grad_and_hess_match_fd(&e, &[0.4, -0.7], 1e-5);
4526 }
4527
4528 #[test]
4529 fn erf_directional_hessian_matches_accumulated() {
4530 let e = Expr::Sum(vec![
4535 unary(UnaryOp::Erf, var(0)),
4536 unary(UnaryOp::Erf, mul(var(0), var(1))),
4537 ]);
4538 let tape = Tape::build(&e);
4539 let x = [0.6, -0.9];
4540 let n = x.len();
4541
4542 let pairs: Vec<(usize, usize)> = tape.hessian_sparsity().into_iter().collect();
4543 let hess_map: HashMap<(usize, usize), usize> =
4544 pairs.iter().enumerate().map(|(k, p)| (*p, k)).collect();
4545 let mut acc = vec![0.0; pairs.len()];
4546 tape.hessian_accumulate(&x, 1.0, &hess_map, &mut acc);
4547
4548 let ops = tape.ops.len();
4550 let mut vals = vec![0.0; ops];
4551 tape.forward_into(&x, &mut vals);
4552 for j in 0..n {
4553 let mut seed = vec![0.0; n];
4554 seed[j] = 1.0;
4555 let mut col = vec![0.0; n];
4556 let (mut dot, mut adj, mut adj_dot) = (vec![0.0; ops], vec![0.0; ops], vec![0.0; ops]);
4557 tape.hessian_directional(
4558 &vals,
4559 &seed,
4560 1.0,
4561 &mut col,
4562 &mut dot,
4563 &mut adj,
4564 &mut adj_dot,
4565 );
4566 for i in 0..n {
4567 let (r, c) = if i >= j { (i, j) } else { (j, i) };
4568 let want = hess_map.get(&(r, c)).map_or(0.0, |&k| acc[k]);
4569 assert!(
4570 (col[i] - want).abs() < 1e-12,
4571 "H[{i},{j}]: directional={:.6e} accumulated={want:.6e}",
4572 col[i]
4573 );
4574 }
4575 }
4576 }
4577
4578 fn centropy_expr(a: Expr, b: Expr) -> Expr {
4579 Expr::Binary(BinOp::CEntropy, Box::new(a), Box::new(b))
4580 }
4581
4582 fn hess2(e: &Expr, x: &[f64; 2]) -> (f64, f64, f64) {
4584 let tape = Tape::build(e);
4585 let mut hess_map: HashMap<(usize, usize), usize> = HashMap::new();
4586 hess_map.insert((0, 0), 0);
4587 hess_map.insert((1, 0), 1);
4588 hess_map.insert((1, 1), 2);
4589 let mut ad = vec![0.0; 3];
4590 tape.hessian_accumulate(x, 1.0, &hess_map, &mut ad);
4591 (ad[0], ad[1], ad[2])
4592 }
4593
4594 #[test]
4595 fn xlogx_and_centropy_grad_and_hessian_match_fd() {
4596 let e = Expr::Sum(vec![
4600 unary(UnaryOp::XLogX, var(0)),
4601 centropy_expr(var(0), var(1)),
4602 mul(var(0), var(1)),
4603 ]);
4604 grad_and_hess_match_fd(&e, &[1.7, 0.6], 1e-5);
4605 }
4606
4607 #[test]
4608 fn centropy_directional_hessian_matches_accumulated() {
4609 let e = Expr::Sum(vec![
4613 centropy_expr(var(0), var(1)),
4614 unary(UnaryOp::XLogX, mul(var(0), var(1))),
4615 ]);
4616 let tape = Tape::build(&e);
4617 let x = [1.3, 0.8];
4618 let n = x.len();
4619
4620 let pairs: Vec<(usize, usize)> = tape.hessian_sparsity().into_iter().collect();
4621 let hess_map: HashMap<(usize, usize), usize> =
4622 pairs.iter().enumerate().map(|(k, p)| (*p, k)).collect();
4623 let mut acc = vec![0.0; pairs.len()];
4624 tape.hessian_accumulate(&x, 1.0, &hess_map, &mut acc);
4625
4626 let ops = tape.ops.len();
4627 let mut vals = vec![0.0; ops];
4628 tape.forward_into(&x, &mut vals);
4629 let mut compared = 0usize;
4630 for j in 0..n {
4631 let mut seed = vec![0.0; n];
4632 seed[j] = 1.0;
4633 let mut col = vec![0.0; n];
4634 let (mut dot, mut adj, mut adj_dot) = (vec![0.0; ops], vec![0.0; ops], vec![0.0; ops]);
4635 tape.hessian_directional(
4636 &vals,
4637 &seed,
4638 1.0,
4639 &mut col,
4640 &mut dot,
4641 &mut adj,
4642 &mut adj_dot,
4643 );
4644 for i in 0..n {
4645 let (r, c) = if i >= j { (i, j) } else { (j, i) };
4646 let want = hess_map.get(&(r, c)).map_or(0.0, |&k| acc[k]);
4647 assert!(
4648 (col[i] - want).abs() < 1e-12,
4649 "H[{i},{j}]: directional={:.6e} accumulated={want:.6e}",
4650 col[i]
4651 );
4652 compared += 1;
4653 }
4654 }
4655 assert_eq!(compared, 4, "expected a full 2x2 comparison");
4658 assert!(
4659 acc.iter().any(|v| v.abs() > 1e-12),
4660 "every accumulated Hessian entry was zero — the arms under test never ran"
4661 );
4662 }
4663
4664 #[test]
4665 fn fused_entropy_ops_reach_derivatives_the_chain_rule_cannot() {
4666 let a = 1e-299_f64;
4672 let want = 1.0 / a; assert!(want.is_finite(), "test premise: 1/a must be representable");
4674
4675 let (fused, _, _) = hess2(&unary(UnaryOp::XLogX, var(0)), &[a, 0.0]);
4676 assert!(
4677 (fused - want).abs() <= 1e-12 * want,
4678 "xlogx''({a:e}): got {fused:e}, want {want:e}"
4679 );
4680
4681 let (decomposed, _, _) = hess2(&mul(var(0), unary(UnaryOp::Log, var(0))), &[a, 0.0]);
4686 assert!(
4687 !decomposed.is_finite(),
4688 "test premise: a*log(a) is supposed to lose this second derivative, \
4689 but it returned {decomposed:e} — if the Log/Mul arms now reach it, \
4690 re-derive whether the fused op is still needed"
4691 );
4692
4693 let (fused_aa, _, _) = hess2(¢ropy_expr(var(0), var(1)), &[a, 1.0]);
4695 assert!(
4696 (fused_aa - want).abs() <= 1e-12 * want,
4697 "centropy ∂²/∂a² at a={a:e}: got {fused_aa:e}, want {want:e}"
4698 );
4699 }
4700
4701 #[test]
4702 fn centropy_second_derivative_in_b_survives_a_squared_denominator() {
4703 let (a, b) = (1e300_f64, 1e200_f64);
4706 let want = (a / b) / b;
4707 assert!(
4711 want.is_finite() && want != 0.0 && a / (b * b) == 0.0,
4712 "test premise: a/b² representable ({want:e}) but a/(b*b) collapses"
4713 );
4714 let (_, _, h11) = hess2(¢ropy_expr(var(0), var(1)), &[a, b]);
4715 assert!(
4716 (h11 - want).abs() <= 1e-12 * want,
4717 "centropy ∂²/∂b² at ({a:e}, {b:e}): got {h11:e}, want {want:e}"
4718 );
4719 }
4720
4721 #[test]
4722 fn centropy_value_survives_an_out_of_range_ratio_and_the_zero_limit() {
4723 let t = |e: &Expr, x: &[f64]| Tape::build(e).eval(x);
4724 let ce = centropy_expr(var(0), var(1));
4725
4726 let want = 1e300 * (1e300_f64.ln() - 1e-300_f64.ln());
4730 let got = t(&ce, &[1e300, 1e-300]);
4731 assert!(
4732 got.is_finite() && (got - want).abs() <= 1e-12 * want,
4733 "centropy(1e300, 1e-300): got {got:e}, want {want:e}"
4734 );
4735 assert!(
4737 !t(
4738 &mul(var(0), unary(UnaryOp::Log, div(var(0), var(1)))),
4739 &[1e300, 1e-300]
4740 )
4741 .is_finite(),
4742 "test premise: a*log(a/b) is supposed to overflow here"
4743 );
4744
4745 let (a, b) = (1e10 + 1.0, 1e10_f64);
4752 assert_eq!(a - b, 1.0, "test premise: a - b must be exact here");
4753 let got = t(&ce, &[a, b]);
4754 assert!(
4755 (got - 1.00000000005).abs() <= 2.3e-16,
4756 "centropy(1e10+1, 1e10): got {got:.17e}, want 1.00000000005"
4757 );
4758 let naive = t(
4760 &mul(var(0), unary(UnaryOp::Log, div(var(0), var(1)))),
4761 &[a, b],
4762 );
4763 assert!(
4764 (naive - 1.00000000005).abs() > 1e-9,
4765 "test premise: a*log(a/b) is supposed to lose digits here, got {naive:.17e}"
4766 );
4767
4768 assert_eq!(t(&ce, &[0.0, 2.0]), 0.0);
4770 assert_eq!(t(&unary(UnaryOp::XLogX, var(0)), &[0.0]), 0.0);
4771 }
4772
4773 #[test]
4774 fn cond_does_not_leak_a_non_finite_from_its_inactive_branch() {
4775 let cond = Expr::Compare(CmpOp::Lt, Box::new(var(0)), Box::new(cnst(1e-300)));
4783 let e = Expr::Cond {
4784 cond: Box::new(cond),
4785 then_: Box::new(mul(var(0), cnst(1e-300f64.ln()))),
4786 else_: Box::new(unary(UnaryOp::XLogX, var(0))),
4787 };
4788 let t = Tape::build(&e);
4789 let x = [-1.0_f64];
4790
4791 assert!(
4794 Tape::build(&unary(UnaryOp::XLogX, var(0)))
4795 .eval(&x)
4796 .is_nan(),
4797 "test premise: xlogx(-1) must be NaN"
4798 );
4799
4800 assert_eq!(t.eval(&x), -1e-300_f64.ln());
4801
4802 let mut g = vec![0.0; 1];
4803 t.gradient_seed(&x, 1.0, &mut g);
4804 assert_eq!(g[0], 1e-300_f64.ln(), "gradient leaked the inactive branch");
4805
4806 let mut hess_map: HashMap<(usize, usize), usize> = HashMap::new();
4807 hess_map.insert((0, 0), 0);
4808 let mut acc = vec![0.0; 1];
4809 t.hessian_accumulate(&x, 1.0, &hess_map, &mut acc);
4810 assert_eq!(acc[0], 0.0, "hessian_accumulate leaked the inactive branch");
4811
4812 let ops = t.ops.len();
4813 let mut vals = vec![0.0; ops];
4814 t.forward_into(&x, &mut vals);
4815 let mut col = vec![0.0; 1];
4816 let (mut dot, mut adj, mut adj_dot) = (vec![0.0; ops], vec![0.0; ops], vec![0.0; ops]);
4817 t.hessian_directional(
4818 &vals,
4819 &[1.0],
4820 1.0,
4821 &mut col,
4822 &mut dot,
4823 &mut adj,
4824 &mut adj_dot,
4825 );
4826 assert_eq!(
4827 col[0], 0.0,
4828 "hessian_directional leaked the inactive branch"
4829 );
4830 }
4831
4832 #[test]
4833 fn atan2_grad_and_hessian_match_fd() {
4834 let atan2 = |a: Expr, b: Expr| Expr::Binary(BinOp::Atan2, Box::new(a), Box::new(b));
4836 let e = Expr::Sum(vec![atan2(var(0), var(1)), mul(var(0), var(1))]);
4837 grad_and_hess_match_fd(&e, &[1.2, 0.7], 1e-5);
4838 }
4839
4840 #[test]
4841 fn minmax_grad_and_hessian_match_fd() {
4842 let e = Expr::Sum(vec![
4849 Expr::MinList(vec![var(0), var(1), var(2)]),
4850 Expr::MaxList(vec![var(1), var(2)]),
4851 mul(var(0), var(2)),
4852 ]);
4853 grad_and_hess_match_fd(&e, &[0.5, 3.0, 2.0], 1e-5);
4854 }
4855
4856 #[test]
4857 fn minmax_value_and_active_operand() {
4858 let e = Expr::Sum(vec![
4861 Expr::MinList(vec![var(0), var(1)]),
4862 Expr::MaxList(vec![var(0), var(1)]),
4863 ]);
4864 let t = Tape::build(&e);
4865 let x = [1.3, -0.4];
4867 assert!((t.eval(&x) - (x[0] + x[1])).abs() < 1e-12);
4868 let mut g = vec![0.0; 2];
4869 t.gradient_seed(&x, 1.0, &mut g);
4870 assert!((g[0] - 1.0).abs() < 1e-12, "g0={}", g[0]);
4873 assert!((g[1] - 1.0).abs() < 1e-12, "g1={}", g[1]);
4874 }
4875
4876 #[test]
4877 fn hessian_division_matches_fd() {
4878 let e = add(div(var(0), var(1)), unary(UnaryOp::Cos, var(0)));
4880 let t = Tape::build(&e);
4881 fd_check(&t, &[0.5, 1.2], 2, 1e-5);
4882 }
4883
4884 #[test]
4885 fn conditional_value_grad_hessian_active_branch() {
4886 let e = cond(
4890 cmp(CmpOp::Ge, var(0), cnst(1.0)),
4891 mul(var(0), var(1)),
4892 pow(var(1), cnst(2.0)),
4893 );
4894 let t = Tape::build(&e);
4895
4896 let x = [2.0, 5.0];
4898 assert!((t.eval(&x) - 10.0).abs() < 1e-12);
4899 let mut g = vec![0.0; 2];
4900 t.gradient_seed(&x, 1.0, &mut g);
4901 assert!((g[0] - 5.0).abs() < 1e-10);
4903 assert!((g[1] - 2.0).abs() < 1e-10);
4904 fd_check(&t, &x, 2, 1e-5);
4906
4907 let x2 = [0.0, 5.0];
4909 assert!((t.eval(&x2) - 25.0).abs() < 1e-12);
4910 let mut g2 = vec![0.0; 2];
4911 t.gradient_seed(&x2, 1.0, &mut g2);
4912 assert!(g2[0].abs() < 1e-10);
4913 assert!((g2[1] - 10.0).abs() < 1e-10);
4914 fd_check(&t, &x2, 2, 1e-5);
4915 }
4916
4917 #[test]
4918 fn comparison_and_logical_have_zero_derivative() {
4919 let lt = cmp(CmpOp::Lt, var(0), var(1));
4923 let and = Expr::And(
4924 Box::new(cmp(CmpOp::Gt, var(0), cnst(0.0))),
4925 Box::new(cmp(CmpOp::Gt, var(1), cnst(0.0))),
4926 );
4927 let notc = Expr::Not(Box::new(cmp(CmpOp::Eq, var(0), var(1))));
4928 let e = add(add(lt, and), notc);
4929 let t = Tape::build(&e);
4930
4931 let x = [1.0, 2.0];
4932 assert!((t.eval(&x) - 3.0).abs() < 1e-12);
4934 let mut g = vec![0.0; 2];
4935 t.gradient_seed(&x, 1.0, &mut g);
4936 assert!(g[0].abs() < 1e-12, "d/dx0 should be 0, got {}", g[0]);
4937 assert!(g[1].abs() < 1e-12, "d/dx1 should be 0, got {}", g[1]);
4938 }
4939
4940 #[test]
4941 fn logical_or_value() {
4942 let e = Expr::Or(
4944 Box::new(cmp(CmpOp::Gt, var(0), cnst(0.0))),
4945 Box::new(cmp(CmpOp::Gt, var(1), cnst(0.0))),
4946 );
4947 let t = Tape::build(&e);
4948 assert!((t.eval(&[-1.0, 3.0]) - 1.0).abs() < 1e-12);
4949 assert!((t.eval(&[-1.0, -3.0]) - 0.0).abs() < 1e-12);
4950 }
4951
4952 fn directional_matches_accumulate(tape: &Tape, x: &[f64], n: usize) {
4957 let vars = tape.variables();
4958 let mut hess_map: HashMap<(usize, usize), usize> = HashMap::new();
4959 let mut pairs = Vec::new();
4960 for (ai, &vi) in vars.iter().enumerate() {
4961 for &vj in &vars[..=ai] {
4962 let (r, c) = if vi >= vj { (vi, vj) } else { (vj, vi) };
4963 hess_map.entry((r, c)).or_insert_with(|| {
4964 let p = pairs.len();
4965 pairs.push((r, c));
4966 p
4967 });
4968 }
4969 }
4970 let nnz = pairs.len();
4971 let mut ad = vec![0.0; nnz];
4972 tape.hessian_accumulate(x, 1.0, &hess_map, &mut ad);
4973
4974 let nops = tape.ops.len();
4975 let mut vals = vec![0.0; nops];
4976 tape.forward_into(x, &mut vals);
4977 let mut dot = vec![0.0; nops];
4978 let mut adj = vec![0.0; nops];
4979 let mut adj_dot = vec![0.0; nops];
4980
4981 for &j in &vars {
4982 let mut seed = vec![0.0; n];
4983 seed[j] = 1.0;
4984 let mut col = vec![0.0; n];
4985 tape.hessian_directional(
4986 &vals,
4987 &seed,
4988 1.0,
4989 &mut col,
4990 &mut dot,
4991 &mut adj,
4992 &mut adj_dot,
4993 );
4994 for &i in &vars {
4995 let (r, c) = if i >= j { (i, j) } else { (j, i) };
4996 let expect = ad[hess_map[&(r, c)]];
4997 assert!(
4998 (col[i] - expect).abs() < 1e-10,
4999 "directional H[{i},{j}] = {} vs accumulate {}",
5000 col[i],
5001 expect
5002 );
5003 }
5004 }
5005 }
5006
5007 #[test]
5008 fn directional_quadratic_matches_accumulate() {
5009 let e = add(
5011 add(
5012 mul(cnst(3.0), pow(var(0), cnst(2.0))),
5013 mul(mul(cnst(2.0), var(0)), var(1)),
5014 ),
5015 pow(var(1), cnst(2.0)),
5016 );
5017 let t = Tape::build(&e);
5018 directional_matches_accumulate(&t, &[0.5, -0.3], 2);
5019 }
5020
5021 #[test]
5022 fn directional_transcendental_matches_accumulate() {
5023 let e = Expr::Sum(vec![
5024 unary(UnaryOp::Exp, var(0)),
5025 unary(UnaryOp::Sin, var(1)),
5026 unary(UnaryOp::Log, var(0)),
5027 unary(UnaryOp::Sqrt, var(1)),
5028 mul(var(0), var(1)),
5029 ]);
5030 let t = Tape::build(&e);
5031 directional_matches_accumulate(&t, &[1.5, 2.0], 2);
5032 }
5033
5034 #[test]
5035 fn directional_with_division_matches_accumulate() {
5036 let e = add(div(var(0), var(1)), unary(UnaryOp::Cos, var(0)));
5037 let t = Tape::build(&e);
5038 directional_matches_accumulate(&t, &[0.5, 1.2], 2);
5039 }
5040
5041 #[test]
5042 fn hessian_sparsity_separable() {
5043 let e = add(unary(UnaryOp::Sin, var(0)), mul(var(1), var(2)));
5045 let t = Tape::build(&e);
5046 let s = t.hessian_sparsity();
5047 assert!(s.contains(&(0, 0)));
5048 assert!(s.contains(&(2, 1)));
5049 assert!(!s.contains(&(1, 0)));
5050 assert!(!s.contains(&(2, 0)));
5051 }
5052
5053 fn count_op<F: Fn(&TapeOp) -> bool>(t: &Tape, pred: F) -> usize {
5054 t.ops.iter().filter(|o| pred(o)).count()
5055 }
5056
5057 #[test]
5058 fn pow_zero_const_folds_to_one() {
5059 let e = pow(var(0), cnst(0.0));
5061 let t = Tape::build(&e);
5062 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 0);
5063 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Var(_))), 0);
5064 assert!((t.eval(&[7.0]) - 1.0).abs() < 1e-12);
5065 }
5066
5067 #[test]
5068 fn pow_one_passes_through() {
5069 let e = pow(var(0), cnst(1.0));
5071 let t = Tape::build(&e);
5072 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 0);
5073 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Const(_))), 0);
5074 assert!((t.eval(&[3.5]) - 3.5).abs() < 1e-12);
5075 }
5076
5077 #[test]
5078 fn pow_half_lowers_to_sqrt() {
5079 let e = pow(var(0), cnst(0.5));
5080 let t = Tape::build(&e);
5081 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 0);
5082 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Sqrt(_))), 1);
5083 assert!((t.eval(&[16.0]) - 4.0).abs() < 1e-12);
5084 }
5085
5086 #[test]
5087 fn pow_two_lowers_to_single_mul() {
5088 let e = pow(var(0), cnst(2.0));
5089 let t = Tape::build(&e);
5090 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 0);
5091 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Mul(..))), 1);
5092 assert!((t.eval(&[3.0]) - 9.0).abs() < 1e-12);
5093 }
5094
5095 #[test]
5096 fn pow_three_lowers_to_two_muls() {
5097 let e = pow(var(0), cnst(3.0));
5098 let t = Tape::build(&e);
5099 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 0);
5100 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Mul(..))), 2);
5101 assert!((t.eval(&[2.0]) - 8.0).abs() < 1e-12);
5102 }
5103
5104 #[test]
5105 fn pow_eight_lowers_to_three_muls() {
5106 let e = pow(var(0), cnst(8.0));
5108 let t = Tape::build(&e);
5109 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 0);
5110 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Mul(..))), 3);
5111 assert!((t.eval(&[2.0]) - 256.0).abs() < 1e-12);
5112 }
5113
5114 #[test]
5115 fn pow_negative_two_lowers_to_div() {
5116 let e = pow(var(0), cnst(-2.0));
5118 let t = Tape::build(&e);
5119 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 0);
5120 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Div(..))), 1);
5121 assert!((t.eval(&[4.0]) - (1.0 / 16.0)).abs() < 1e-12);
5122 }
5123
5124 #[test]
5125 fn pow_large_const_stays_generic() {
5126 let e = pow(var(0), cnst(9.0));
5128 let t = Tape::build(&e);
5129 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 1);
5130 }
5131
5132 #[test]
5133 fn pow_non_integer_const_stays_generic() {
5134 let e = pow(var(0), cnst(1.5));
5136 let t = Tape::build(&e);
5137 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 1);
5138 }
5139
5140 #[test]
5141 fn pow_const_through_cse_const() {
5142 let two = Arc::new(cnst(2.0));
5144 let e = Expr::Binary(BinOp::Pow, Box::new(var(0)), Box::new(Expr::Cse(two)));
5145 let t = Tape::build(&e);
5146 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Pow(..))), 0);
5147 assert_eq!(count_op(&t, |o| matches!(o, TapeOp::Mul(..))), 1);
5148 }
5149
5150 #[test]
5151 fn hessian_pow_three_matches_fd() {
5152 let e = add(mul(cnst(5.0), pow(var(0), cnst(3.0))), mul(var(0), var(1)));
5154 let t = Tape::build(&e);
5155 fd_check(&t, &[1.7, 0.8], 2, 1e-5);
5156 }
5157
5158 #[test]
5159 fn hessian_pow_negative_matches_fd() {
5160 let e = add(pow(var(0), cnst(-2.0)), pow(var(1), cnst(2.0)));
5162 let t = Tape::build(&e);
5163 fd_check(&t, &[1.3, 2.4], 2, 1e-5);
5164 }
5165
5166 #[test]
5167 fn hessian_pow_half_matches_fd() {
5168 let e = add(pow(var(0), cnst(0.5)), mul(var(0), var(1)));
5170 let t = Tape::build(&e);
5171 fd_check(&t, &[2.5, 1.1], 2, 1e-5);
5172 }
5173
5174 #[test]
5175 fn hessian_sparsity_through_cse() {
5176 let body = Arc::new(add(var(0), var(1)));
5179 let e = add(
5180 pow(Expr::Cse(body.clone()), cnst(2.0)),
5181 Expr::Cse(body.clone()),
5182 );
5183 let t = Tape::build(&e);
5184 let s = t.hessian_sparsity();
5185 assert!(s.contains(&(0, 0)));
5186 assert!(s.contains(&(1, 0)));
5187 assert!(s.contains(&(1, 1)));
5188 assert_eq!(s.len(), 3);
5189 }
5190
5191 #[test]
5192 fn pow_forward_tangent_matches_reverse_gradient_at_base_zero() {
5193 let e = pow(var(0), var(1));
5202 let t = Tape::build(&e);
5203 assert!(
5206 t.ops.iter().any(|op| matches!(op, TapeOp::Pow(_, _))),
5207 "expected a Pow op in the tape; got {:?}",
5208 t.ops
5209 );
5210 let x = [0.0, 1.0];
5211 let n = t.ops.len();
5212
5213 let mut grad = vec![0.0; 2];
5215 t.gradient_seed(&x, 1.0, &mut grad);
5216
5217 let vals = t.forward(&x);
5219 let mut dot = vec![0.0; n];
5220 t.forward_tangent(&vals, 0, &mut dot);
5221 let fwd_dfx0 = dot[n - 1];
5222
5223 assert!(
5224 (grad[0] - 1.0).abs() < 1e-12,
5225 "reverse gradient df/dx0 at base 0 should be 1, got {}",
5226 grad[0]
5227 );
5228 assert!(
5229 (fwd_dfx0 - grad[0]).abs() < 1e-12,
5230 "forward tangent df/dx0 = {fwd_dfx0} must match reverse gradient {} at base 0",
5231 grad[0]
5232 );
5233 }
5234
5235 #[test]
5236 #[should_panic(expected = "external function calls are not supported on the")]
5237 fn hybrid_promoted_cse_with_funcall_reports_clear_message() {
5238 let body = Arc::new(Expr::Funcall {
5246 id: 0,
5247 args: vec![FuncallArg::Real(var(0))],
5248 });
5249 let exprs = vec![
5250 add(Expr::Cse(body.clone()), cnst(1.0)),
5251 add(Expr::Cse(body.clone()), cnst(2.0)),
5252 ];
5253 HybridTape::build_multi(&exprs);
5254 }
5255
5256 fn shooting_chain_roots(k: usize, leaf: Expr) -> Vec<Expr> {
5261 let step = |e: &Expr, i: usize| {
5262 let sq = mul(e.clone(), e.clone());
5263 mul(cnst(0.1), add(var(i), mul(cnst(-1.0), sq)))
5264 };
5265 let mut e = leaf;
5266 for i in 0..k - 1 {
5267 e = Expr::Cse(Arc::new(add(e.clone(), step(&e, i))));
5268 }
5269 let last = step(&e, k - 1);
5270 vec![e, last]
5271 }
5272
5273 #[test]
5278 fn hybrid_build_of_a_shared_square_chain_is_linear() {
5279 let k = 64;
5280 let roots = shooting_chain_roots(k, cnst(0.5));
5281 let tape = HybridTape::build_multi(&roots);
5282 let x: Vec<f64> = (0..k).map(|i| 0.01 * i as f64).collect();
5284 let flat = Tape::build(&add(roots[0].clone(), roots[1].clone())).eval(&x);
5285 let mut prelude = vec![0.0; tape.n_prelude_ops()];
5286 tape.forward_prelude(&x, &mut prelude);
5287 let mut local = vec![0.0; tape.max_summand_ops()];
5288 let mut hybrid = 0.0;
5289 for s in &tape.summands {
5290 tape.forward_summand(s, &x, &prelude, &mut local);
5291 hybrid += tape.root_value(s, &local);
5292 }
5293 assert!(
5294 (hybrid - flat).abs() <= 1e-12 * flat.abs().max(1.0),
5295 "{hybrid} vs {flat}"
5296 );
5297 }
5298
5299 #[test]
5303 #[should_panic(expected = "external function calls are not supported on the")]
5304 fn hybrid_funcall_under_a_shared_square_chain_still_reports() {
5305 let leaf = Expr::Funcall {
5306 id: 0,
5307 args: vec![FuncallArg::Real(var(0))],
5308 };
5309 HybridTape::build_multi(&shooting_chain_roots(64, leaf));
5310 }
5311
5312 #[test]
5323 fn directional_hybrid_hessian_matches_flat_directional() {
5324 let body = Arc::new(unary(UnaryOp::Exp, mul(cnst(0.5), add(var(0), var(1)))));
5327 let exprs = vec![
5328 pow(Expr::Cse(body.clone()), cnst(2.0)),
5329 mul(Expr::Cse(body.clone()), var(2)),
5330 add(
5331 unary(UnaryOp::Sin, Expr::Cse(body.clone())),
5332 pow(var(2), cnst(2.0)),
5333 ),
5334 ];
5335 let weights = [1.25, -0.75, 2.5];
5336 let x = [0.3, -0.1, 0.7];
5337 let n = 3;
5338
5339 let hybrid = HybridTape::build_multi(&exprs);
5340 assert!(
5341 hybrid.n_prelude_ops() > 0,
5342 "a CSE shared by 3 roots must be promoted into the prelude"
5343 );
5344
5345 let seeds = [[1.0, 0.0, 1.0], [0.0, 1.0, 0.0]];
5347 for seed in &seeds {
5348 let mut flat_out = vec![0.0; n];
5349 for (e, &wt) in exprs.iter().zip(&weights) {
5350 let t = Tape::build(e);
5351 let vals = t.forward(&x);
5352 let m = t.ops.len();
5353 let (mut dot, mut adj, mut adj_dot) = (vec![0.0; m], vec![0.0; m], vec![0.0; m]);
5354 t.hessian_directional(
5355 &vals,
5356 seed,
5357 wt,
5358 &mut flat_out,
5359 &mut dot,
5360 &mut adj,
5361 &mut adj_dot,
5362 );
5363 }
5364
5365 let mut hyb_out = vec![0.0; n];
5366 let np = hybrid.n_prelude_ops();
5367 let ml = hybrid.max_summand_ops();
5368 let mut prelude_vals = vec![0.0; np];
5369 let mut prelude_dot = vec![0.0; np];
5370 let mut prelude_adj = vec![0.0; np];
5371 let mut prelude_adj_dot = vec![0.0; np];
5372 let (mut local_dot, mut local_adj, mut local_adj_dot) =
5373 (vec![0.0; ml], vec![0.0; ml], vec![0.0; ml]);
5374 let creach: Vec<u32> = {
5380 let mut u: BTreeSet<u32> = BTreeSet::new();
5381 for s in &hybrid.summands {
5382 u.extend(s.prelude_reach.iter().map(|&p| p as u32));
5383 }
5384 u.into_iter().collect()
5385 };
5386 hybrid.forward_prelude(&x, &mut prelude_vals);
5387 hybrid.prelude_tangent(&prelude_vals, seed, &creach, &mut prelude_dot);
5388 for (s, &wt) in hybrid.summands.iter().zip(&weights) {
5389 let mut local_vals = vec![0.0; s.ops.len()];
5390 hybrid.forward_summand(s, &x, &prelude_vals, &mut local_vals);
5391 hybrid.hessian_summand_directional(
5392 s,
5393 &local_vals,
5394 &prelude_dot,
5395 seed,
5396 wt,
5397 &mut hyb_out,
5398 &mut local_dot,
5399 &mut local_adj,
5400 &mut local_adj_dot,
5401 &mut prelude_adj,
5402 &mut prelude_adj_dot,
5403 );
5404 }
5405 hybrid.prelude_reverse_directional(
5406 &prelude_vals,
5407 &prelude_dot,
5408 &creach,
5409 &mut hyb_out,
5410 &mut prelude_adj,
5411 &mut prelude_adj_dot,
5412 );
5413
5414 for k in 0..n {
5415 let scale = flat_out[k].abs().max(1.0);
5416 assert!(
5417 (hyb_out[k] - flat_out[k]).abs() <= 1e-13 * scale,
5418 "seed {seed:?} entry {k}: hybrid {} vs flat {}",
5419 hyb_out[k],
5420 flat_out[k]
5421 );
5422 }
5423 assert!(
5424 hyb_out.iter().any(|v| *v != 0.0),
5425 "an all-zero H·s is no comparison"
5426 );
5427 assert!(prelude_adj.iter().all(|v| *v == 0.0));
5430 assert!(prelude_adj_dot.iter().all(|v| *v == 0.0));
5431 }
5432 }
5433}