#![cfg(feature = "bytecode")]
use approx::assert_relative_eq;
use echidna::opcode::OpCode;
use echidna::record;
use num_traits::Float;
#[test]
fn forward_nonsmooth_detects_abs_kink() {
let (mut tape, _) = record(|x| x[0].abs(), &[0.0]);
let info = tape.forward_nonsmooth(&[0.0]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].opcode, echidna::opcode::OpCode::Abs);
assert_relative_eq!(info.kinks[0].switching_value, 0.0);
assert_eq!(info.kinks[0].branch, 1); }
#[test]
fn forward_nonsmooth_detects_abs_negative() {
let (mut tape, _) = record(|x| x[0].abs(), &[-3.0]);
let info = tape.forward_nonsmooth(&[-3.0]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].branch, -1); assert_relative_eq!(info.kinks[0].switching_value, -3.0);
}
#[test]
fn forward_nonsmooth_detects_max_kink() {
let (mut tape, _) = record(|x| x[0].max(x[1]), &[1.0, 1.0]);
let info = tape.forward_nonsmooth(&[1.0, 1.0]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].opcode, echidna::opcode::OpCode::Max);
assert_relative_eq!(info.kinks[0].switching_value, 0.0); assert_eq!(info.kinks[0].branch, 1); }
#[test]
fn forward_nonsmooth_detects_min_kink() {
let (mut tape, _) = record(|x| x[0].min(x[1]), &[2.0, 2.0]);
let info = tape.forward_nonsmooth(&[2.0, 2.0]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].opcode, echidna::opcode::OpCode::Min);
assert_relative_eq!(info.kinks[0].switching_value, 0.0);
assert_eq!(info.kinks[0].branch, 1); }
#[test]
fn forward_nonsmooth_smooth_function() {
let (mut tape, _) = record(|x| x[0] * x[0] + x[0].sin(), &[1.0]);
let info = tape.forward_nonsmooth(&[1.0]);
assert!(info.kinks.is_empty());
assert!(info.is_smooth(1e-10));
}
#[test]
fn forward_nonsmooth_multiple_kinks() {
let (mut tape, _) = record(
|x| x[0].abs() + x[0].max(x[1]) + x[0].min(x[1]),
&[0.0, 0.0],
);
let info = tape.forward_nonsmooth(&[0.0, 0.0]);
assert_eq!(info.kinks.len(), 3); }
#[test]
fn nonsmooth_signature_consistency() {
let (mut tape, _) = record(|x| x[0].abs() + x[0].max(x[1]), &[1.0, 2.0]);
let info1 = tape.forward_nonsmooth(&[1.0, 2.0]);
let info2 = tape.forward_nonsmooth(&[1.0, 2.0]);
assert_eq!(info1.signature(), info2.signature());
}
#[test]
fn active_kinks_tolerance() {
let (mut tape, _) = record(|x| x[0].abs(), &[1e-6]);
let info = tape.forward_nonsmooth(&[1e-6]);
assert_eq!(info.active_kinks(1e-8).len(), 0);
assert!(info.is_smooth(1e-8));
assert_eq!(info.active_kinks(1e-4).len(), 1);
assert!(!info.is_smooth(1e-4));
}
#[test]
fn jacobian_limiting_abs_positive() {
let (mut tape, _) = record(|x| x[0].abs(), &[0.0]);
let info = tape.forward_nonsmooth(&[0.0]);
let abs_idx = info.kinks[0].tape_index;
let jac = tape.jacobian_limiting(&[0.0], &[(abs_idx, 1)]);
assert_relative_eq!(jac[0][0], 1.0, max_relative = 1e-12);
}
#[test]
fn jacobian_limiting_abs_negative() {
let (mut tape, _) = record(|x| x[0].abs(), &[0.0]);
let info = tape.forward_nonsmooth(&[0.0]);
let abs_idx = info.kinks[0].tape_index;
let jac = tape.jacobian_limiting(&[0.0], &[(abs_idx, -1)]);
assert_relative_eq!(jac[0][0], -1.0, max_relative = 1e-12);
}
#[test]
fn jacobian_limiting_max_branches() {
let (mut tape, _) = record(|x| x[0].max(x[1]), &[1.0, 1.0]);
let info = tape.forward_nonsmooth(&[1.0, 1.0]);
let max_idx = info.kinks[0].tape_index;
let jac_a = tape.jacobian_limiting(&[1.0, 1.0], &[(max_idx, 1)]);
assert_relative_eq!(jac_a[0][0], 1.0, max_relative = 1e-12);
assert_relative_eq!(jac_a[0][1], 0.0, max_relative = 1e-12);
let jac_b = tape.jacobian_limiting(&[1.0, 1.0], &[(max_idx, -1)]);
assert_relative_eq!(jac_b[0][0], 0.0, max_relative = 1e-12);
assert_relative_eq!(jac_b[0][1], 1.0, max_relative = 1e-12);
}
#[test]
fn jacobian_limiting_matches_standard_smooth() {
let (mut tape, _) = record(|x| x[0] * x[0] + x[1], &[3.0, 4.0]);
let jac_std = tape.jacobian(&[3.0, 4.0]);
let jac_lim = tape.jacobian_limiting(&[3.0, 4.0], &[]);
assert_relative_eq!(jac_std[0][0], jac_lim[0][0], max_relative = 1e-12);
assert_relative_eq!(jac_std[0][1], jac_lim[0][1], max_relative = 1e-12);
}
#[test]
fn clarke_single_kink() {
let (mut tape, _) = record(|x| x[0].abs(), &[0.0]);
let (info, jacobians) = tape.clarke_jacobian(&[0.0], 1e-8, None).unwrap();
assert_eq!(info.kinks.len(), 1);
assert_eq!(jacobians.len(), 2);
let mut derivs: Vec<f64> = jacobians.iter().map(|j| j[0][0]).collect();
derivs.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_relative_eq!(derivs[0], -1.0, max_relative = 1e-12);
assert_relative_eq!(derivs[1], 1.0, max_relative = 1e-12);
}
#[test]
fn clarke_two_kinks() {
let (mut tape, _) = record(|x| x[0].abs() + x[0].max(x[1]), &[0.0, 0.0]);
let (info, jacobians) = tape.clarke_jacobian(&[0.0, 0.0], 1e-8, None).unwrap();
assert_eq!(info.active_kinks(1e-8).len(), 2);
assert_eq!(jacobians.len(), 4); }
#[test]
fn clarke_smooth_single_jacobian() {
let (mut tape, _) = record(|x| x[0].abs(), &[5.0]);
let (info, jacobians) = tape.clarke_jacobian(&[5.0], 1e-8, None).unwrap();
assert!(info.is_smooth(1e-8));
assert_eq!(jacobians.len(), 1);
assert_relative_eq!(jacobians[0][0][0], 1.0, max_relative = 1e-12);
}
#[test]
fn clarke_too_many_kinks_error() {
let (mut tape, _) = record(|x| x[0].abs() + x[0].abs() + x[0].abs(), &[0.0]);
let result = tape.clarke_jacobian(&[0.0], 1e-8, Some(2));
assert!(result.is_err());
match result.unwrap_err() {
echidna::ClarkeError::TooManyKinks { count, limit } => {
assert_eq!(count, 3);
assert_eq!(limit, 2);
}
}
}
#[test]
fn forward_nonsmooth_detects_signum_kink() {
let (mut tape, _) = record(|x| x[0].signum(), &[1e-12]);
let info = tape.forward_nonsmooth(&[1e-12]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].opcode, OpCode::Signum);
assert_relative_eq!(info.kinks[0].switching_value, 1e-12, max_relative = 1e-6);
assert_eq!(info.kinks[0].branch, 1); }
#[test]
fn forward_nonsmooth_detects_signum_away() {
let (mut tape, _) = record(|x| x[0].signum(), &[5.0]);
let info = tape.forward_nonsmooth(&[5.0]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].opcode, OpCode::Signum);
assert_relative_eq!(info.kinks[0].switching_value, 5.0);
assert_eq!(info.active_kinks(0.1).len(), 0);
}
#[test]
fn forward_nonsmooth_detects_floor_kink() {
let (mut tape, _) = record(|x| x[0].floor(), &[2.0001]);
let info = tape.forward_nonsmooth(&[2.0001]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].opcode, OpCode::Floor);
assert_relative_eq!(info.kinks[0].switching_value, 0.0001, max_relative = 1e-6);
assert_eq!(info.active_kinks(0.01).len(), 1);
}
#[test]
fn forward_nonsmooth_detects_floor_away() {
let (mut tape, _) = record(|x| x[0].floor(), &[2.7]);
let info = tape.forward_nonsmooth(&[2.7]);
assert_eq!(info.kinks.len(), 1);
assert_relative_eq!(info.kinks[0].switching_value, -0.3, max_relative = 1e-6);
assert_eq!(info.active_kinks(0.1).len(), 0);
}
#[test]
fn forward_nonsmooth_detects_ceil_kink() {
let (mut tape, _) = record(|x| x[0].ceil(), &[2.9999]);
let info = tape.forward_nonsmooth(&[2.9999]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].opcode, OpCode::Ceil);
assert_relative_eq!(info.kinks[0].switching_value, -0.0001, max_relative = 1e-3);
assert_eq!(info.active_kinks(0.01).len(), 1);
}
#[test]
fn forward_nonsmooth_detects_round_trunc() {
let (mut tape, _) = record(|x| x[0].trunc(), &[4.001]);
let info = tape.forward_nonsmooth(&[4.001]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].opcode, OpCode::Trunc);
assert_eq!(info.active_kinks(0.01).len(), 1);
let (mut tape, _) = record(|x| x[0].round(), &[3.501]);
let info = tape.forward_nonsmooth(&[3.501]);
assert_eq!(info.kinks.len(), 1);
assert_eq!(info.kinks[0].opcode, OpCode::Round);
assert_eq!(info.active_kinks(0.01).len(), 1);
let (mut tape, _) = record(|x| x[0].round() + x[0].trunc(), &[4.001]);
let info = tape.forward_nonsmooth(&[4.001]);
assert_eq!(info.kinks.len(), 2);
assert_eq!(info.kinks[0].opcode, OpCode::Round);
assert_eq!(info.kinks[1].opcode, OpCode::Trunc);
assert_eq!(info.active_kinks(0.01).len(), 1);
}
#[test]
fn clarke_filters_trivial_kinks() {
let (mut tape, _) = record(|x| x[0].abs() + x[0].signum(), &[0.0]);
let (info, jacobians) = tape.clarke_jacobian(&[0.0], 1e-8, None).unwrap();
assert_eq!(info.kinks.len(), 2);
assert_eq!(jacobians.len(), 2); }
#[test]
fn forced_partials_step_functions_zero() {
use echidna::opcode::forced_reverse_partials;
let ops = [
OpCode::Signum,
OpCode::Floor,
OpCode::Ceil,
OpCode::Round,
OpCode::Trunc,
];
for op in ops {
let (da, db) = forced_reverse_partials(op, 1.5_f64, 0.0, 1.0, 1);
assert_eq!(da, 0.0, "expected zero da for {:?} with sign +1", op);
assert_eq!(db, 0.0, "expected zero db for {:?} with sign +1", op);
let (da, db) = forced_reverse_partials(op, 1.5, 0.0, 1.0, -1);
assert_eq!(da, 0.0, "expected zero da for {:?} with sign -1", op);
assert_eq!(db, 0.0, "expected zero db for {:?} with sign -1", op);
}
}
#[test]
fn signum_records_to_tape() {
use num_traits::Signed;
let (tape, val) = record(
|x| {
Signed::signum(&x[0])
},
&[3.0],
);
assert_eq!(val, 1.0);
assert!(
tape.opcodes_slice().contains(&OpCode::Signum),
"expected OpCode::Signum in tape, got: {:?}",
tape.opcodes_slice()
);
}
#[test]
fn regression_24_nan_switching_value_is_not_smooth() {
use echidna::{KinkEntry, NonsmoothInfo};
let info = NonsmoothInfo {
kinks: vec![KinkEntry {
tape_index: 0,
opcode: OpCode::Abs,
switching_value: f64::NAN,
branch: 1,
}],
};
assert!(
!info.is_smooth(0.1),
"NaN switching value should mean not smooth"
);
assert!(
!info.active_kinks(0.1).is_empty(),
"NaN switching value should appear in active_kinks"
);
}
#[test]
fn regression_25_fract_is_nonsmooth() {
use echidna::opcode::is_nonsmooth;
assert!(
is_nonsmooth(OpCode::Fract),
"OpCode::Fract should be nonsmooth"
);
}