use scirs2_core::ndarray::{ArrayD, ArrayViewD, Axis, Zip};
use crate::error::TlBackendError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UntilSemantics {
MaxMin,
ProbSumProduct,
}
impl UntilSemantics {
pub fn from_tag(tag: &str) -> Result<Self, TlBackendError> {
match tag {
"max" => Ok(UntilSemantics::MaxMin),
"prod" => Ok(UntilSemantics::ProbSumProduct),
other => Err(TlBackendError::invalid_operation(format!(
"Unknown UntilSemantics tag '{}'; expected 'max' or 'prod'",
other
))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TemporalBinaryForm {
Until,
WeakUntil,
Release,
StrongRelease,
}
impl TemporalBinaryForm {
pub fn from_op_word(word: &str) -> Result<Self, TlBackendError> {
match word {
"until" => Ok(Self::Until),
"weakuntil" => Ok(Self::WeakUntil),
"release" => Ok(Self::Release),
"strongrelease" => Ok(Self::StrongRelease),
other => Err(TlBackendError::invalid_operation(format!(
"Unknown temporal binary form '{}'; expected until/weakuntil/release/strongrelease",
other
))),
}
}
fn boundary_is_top(self) -> bool {
matches!(self, Self::WeakUntil | Self::Release)
}
fn outer_is_or(self) -> bool {
matches!(self, Self::Until | Self::WeakUntil)
}
}
pub(crate) enum TemporalOp {
Next { axis: usize },
Binary {
axis: usize,
sem: UntilSemantics,
form: TemporalBinaryForm,
},
}
fn parse_tag_axis(
remainder: &str,
op_prefix: &str,
) -> Result<(UntilSemantics, usize), TlBackendError> {
let mut parts = remainder.splitn(2, ':');
let tag = parts.next().ok_or_else(|| {
TlBackendError::invalid_operation(format!("{}: missing tag in '{}'", op_prefix, remainder))
})?;
let axis_str = parts.next().ok_or_else(|| {
TlBackendError::invalid_operation(format!("{}: missing axis in '{}'", op_prefix, remainder))
})?;
let sem = UntilSemantics::from_tag(tag)?;
let axis = axis_str.parse::<usize>().map_err(|_| {
TlBackendError::invalid_operation(format!(
"{}: invalid axis '{}' (must be a non-negative integer)",
op_prefix, axis_str
))
})?;
Ok((sem, axis))
}
pub(crate) fn parse_temporal_op(op: &str) -> Option<Result<TemporalOp, TlBackendError>> {
if !op.starts_with("temporal_") {
return None;
}
let rest = &op["temporal_".len()..];
if let Some(axis_str) = rest.strip_prefix("next:") {
let axis = match axis_str.parse::<usize>() {
Ok(a) => a,
Err(_) => {
return Some(Err(TlBackendError::invalid_operation(format!(
"temporal_next: invalid axis '{}' (must be a non-negative integer)",
axis_str
))))
}
};
return Some(Ok(TemporalOp::Next { axis }));
}
let binary_words = [
("until:", TemporalBinaryForm::Until),
("weakuntil:", TemporalBinaryForm::WeakUntil),
("release:", TemporalBinaryForm::Release),
("strongrelease:", TemporalBinaryForm::StrongRelease),
];
for (prefix, form) in &binary_words {
if let Some(remainder) = rest.strip_prefix(prefix) {
let op_name = format!("temporal_{}", prefix.trim_end_matches(':'));
return Some(parse_tag_axis(remainder, &op_name).map(|(sem, axis)| {
TemporalOp::Binary {
axis,
sem,
form: *form,
}
}));
}
}
Some(Err(TlBackendError::invalid_operation(format!(
"Unknown temporal operation '{}'; expected 'temporal_next:<axis>' or \
'temporal_<until|weakuntil|release|strongrelease>:<tag>:<axis>'",
op
))))
}
pub fn shift_next(x: &ArrayViewD<f64>, axis: usize) -> ArrayD<f64> {
let t = x.len_of(Axis(axis));
let mut out = ArrayD::zeros(x.raw_dim());
if t <= 1 {
return out;
}
for k in 0..(t - 1) {
let src = x.index_axis(Axis(axis), k + 1);
let mut dst = out.index_axis_mut(Axis(axis), k);
dst.assign(&src);
}
out
}
pub fn temporal_binary_scan(
a: &ArrayViewD<f64>,
b: &ArrayViewD<f64>,
axis: usize,
form: TemporalBinaryForm,
sem: UntilSemantics,
) -> ArrayD<f64> {
let t = a.len_of(Axis(axis));
let mut out = ArrayD::zeros(a.raw_dim());
if t == 0 {
return out;
}
let boundary_val = if form.boundary_is_top() {
1.0_f64
} else {
0.0_f64
};
let outer_or = form.outer_is_or();
let apply_outer = |b_v: f64, inner_val: f64| -> f64 {
match (outer_or, sem) {
(true, UntilSemantics::MaxMin) => b_v.max(inner_val),
(true, UntilSemantics::ProbSumProduct) => b_v + inner_val - b_v * inner_val,
(false, UntilSemantics::MaxMin) => b_v.min(inner_val),
(false, UntilSemantics::ProbSumProduct) => b_v * inner_val,
}
};
let apply_inner = |a_v: f64, u_next: f64| -> f64 {
match (outer_or, sem) {
(true, UntilSemantics::MaxMin) => a_v.min(u_next),
(true, UntilSemantics::ProbSumProduct) => a_v * u_next,
(false, UntilSemantics::MaxMin) => a_v.max(u_next),
(false, UntilSemantics::ProbSumProduct) => a_v + u_next - a_v * u_next,
}
};
{
let a_last = a.index_axis(Axis(axis), t - 1);
let b_last = b.index_axis(Axis(axis), t - 1);
let mut out_last = out.index_axis_mut(Axis(axis), t - 1);
Zip::from(&mut out_last)
.and(&b_last)
.and(&a_last)
.for_each(|o, &b_v, &a_v| {
let inner_val = apply_inner(a_v, boundary_val);
*o = apply_outer(b_v, inner_val);
});
}
for k in (0..t.saturating_sub(1)).rev() {
let a_k = a.index_axis(Axis(axis), k);
let b_k = b.index_axis(Axis(axis), k);
let u_next = out.index_axis(Axis(axis), k + 1).to_owned();
let new_slice = Zip::from(&b_k)
.and(&a_k)
.and(&u_next)
.map_collect(|&b_v, &a_v, &u_v| {
let inner_val = apply_inner(a_v, u_v);
apply_outer(b_v, inner_val)
});
let mut out_k = out.index_axis_mut(Axis(axis), k);
out_k.assign(&new_slice);
}
out
}
pub fn temporal_binary_scan_vjp(
a: &ArrayViewD<f64>,
b: &ArrayViewD<f64>,
grad_out: &ArrayViewD<f64>,
axis: usize,
form: TemporalBinaryForm,
sem: UntilSemantics,
) -> (ArrayD<f64>, ArrayD<f64>) {
let t = a.len_of(Axis(axis));
let mut grad_a = ArrayD::zeros(a.raw_dim());
let mut grad_b = ArrayD::zeros(b.raw_dim());
if t == 0 {
return (grad_a, grad_b);
}
let u = temporal_binary_scan(a, b, axis, form, sem);
let boundary_val = if form.boundary_is_top() {
1.0_f64
} else {
0.0_f64
};
let outer_or = form.outer_is_or();
let mut s = ArrayD::zeros(a.raw_dim());
s.assign(grad_out);
let local_partials = |b_v: f64, a_v: f64, u_next_v: f64| -> (f64, f64, f64, f64) {
match (outer_or, sem) {
(true, UntilSemantics::ProbSumProduct) => {
let i_v = a_v * u_next_v; (1.0 - i_v, 1.0 - b_v, u_next_v, a_v)
}
(false, UntilSemantics::ProbSumProduct) => {
let i_v = a_v + u_next_v - a_v * u_next_v; (i_v, b_v, 1.0 - u_next_v, 1.0 - a_v)
}
(true, UntilSemantics::MaxMin) => {
let i_v = a_v.min(u_next_v);
let d_outer_d_b = if b_v >= i_v { 1.0 } else { 0.0 };
let d_outer_d_i = if b_v < i_v { 1.0 } else { 0.0 };
let d_inner_d_a = if a_v <= u_next_v { 1.0 } else { 0.0 };
let d_inner_d_u = if u_next_v < a_v { 1.0 } else { 0.0 };
(d_outer_d_b, d_outer_d_i, d_inner_d_a, d_inner_d_u)
}
(false, UntilSemantics::MaxMin) => {
let i_v = a_v.max(u_next_v);
let d_outer_d_b = if b_v <= i_v { 1.0 } else { 0.0 };
let d_outer_d_i = if b_v > i_v { 1.0 } else { 0.0 };
let d_inner_d_a = if a_v >= u_next_v { 1.0 } else { 0.0 };
let d_inner_d_u = if u_next_v > a_v { 1.0 } else { 0.0 };
(d_outer_d_b, d_outer_d_i, d_inner_d_a, d_inner_d_u)
}
}
};
for k in 0..(t - 1) {
let a_k = a.index_axis(Axis(axis), k);
let b_k = b.index_axis(Axis(axis), k);
let u_next = u.index_axis(Axis(axis), k + 1);
let s_k = s.index_axis(Axis(axis), k).to_owned();
let da_k = Zip::from(&b_k)
.and(&a_k)
.and(&u_next)
.map_collect(|&b_v, &a_v, &u_v| {
let (_, d_outer_d_i, d_inner_d_a, _) = local_partials(b_v, a_v, u_v);
d_outer_d_i * d_inner_d_a
});
let db_k = Zip::from(&b_k)
.and(&a_k)
.and(&u_next)
.map_collect(|&b_v, &a_v, &u_v| {
let (d_outer_d_b, _, _, _) = local_partials(b_v, a_v, u_v);
d_outer_d_b
});
let ds_next = Zip::from(&b_k)
.and(&a_k)
.and(&u_next)
.map_collect(|&b_v, &a_v, &u_v| {
let (_, d_outer_d_i, _, d_inner_d_u) = local_partials(b_v, a_v, u_v);
d_outer_d_i * d_inner_d_u
});
{
let mut ga_k = grad_a.index_axis_mut(Axis(axis), k);
let contribution = Zip::from(&s_k)
.and(&da_k)
.map_collect(|&s_v, &da_v| s_v * da_v);
ga_k.zip_mut_with(&contribution, |acc, &v| *acc += v);
}
{
let mut gb_k = grad_b.index_axis_mut(Axis(axis), k);
let contribution = Zip::from(&s_k)
.and(&db_k)
.map_collect(|&s_v, &db_v| s_v * db_v);
gb_k.zip_mut_with(&contribution, |acc, &v| *acc += v);
}
{
let carry = Zip::from(&s_k)
.and(&ds_next)
.map_collect(|&s_v, &du_v| s_v * du_v);
let mut s_next_slice = s.index_axis_mut(Axis(axis), k + 1);
s_next_slice.zip_mut_with(&carry, |acc, &v| *acc += v);
}
}
{
let a_last = a.index_axis(Axis(axis), t - 1);
let b_last = b.index_axis(Axis(axis), t - 1);
let s_last = s.index_axis(Axis(axis), t - 1).to_owned();
let da_last = Zip::from(&b_last).and(&a_last).map_collect(|&b_v, &a_v| {
let (_, d_outer_d_i, d_inner_d_a, _) = local_partials(b_v, a_v, boundary_val);
d_outer_d_i * d_inner_d_a
});
let db_last = Zip::from(&b_last).and(&a_last).map_collect(|&b_v, &a_v| {
let (d_outer_d_b, _, _, _) = local_partials(b_v, a_v, boundary_val);
d_outer_d_b
});
{
let mut ga_last = grad_a.index_axis_mut(Axis(axis), t - 1);
let contribution = Zip::from(&s_last)
.and(&da_last)
.map_collect(|&s_v, &da_v| s_v * da_v);
ga_last.zip_mut_with(&contribution, |acc, &v| *acc += v);
}
{
let mut gb_last = grad_b.index_axis_mut(Axis(axis), t - 1);
let contribution = Zip::from(&s_last)
.and(&db_last)
.map_collect(|&s_v, &db_v| s_v * db_v);
gb_last.zip_mut_with(&contribution, |acc, &v| *acc += v);
}
}
(grad_a, grad_b)
}
pub fn until_scan(
a: &ArrayViewD<f64>,
b: &ArrayViewD<f64>,
axis: usize,
sem: UntilSemantics,
) -> ArrayD<f64> {
temporal_binary_scan(a, b, axis, TemporalBinaryForm::Until, sem)
}
pub fn until_scan_vjp(
a: &ArrayViewD<f64>,
b: &ArrayViewD<f64>,
grad_out: &ArrayViewD<f64>,
axis: usize,
sem: UntilSemantics,
) -> (ArrayD<f64>, ArrayD<f64>) {
temporal_binary_scan_vjp(a, b, grad_out, axis, TemporalBinaryForm::Until, sem)
}
pub fn shift_prev(grad_out: &ArrayViewD<f64>, axis: usize) -> ArrayD<f64> {
let t = grad_out.len_of(Axis(axis));
let mut grad_x = ArrayD::zeros(grad_out.raw_dim());
if t <= 1 {
return grad_x;
}
for k in 1..t {
let src = grad_out.index_axis(Axis(axis), k - 1);
let mut dst = grad_x.index_axis_mut(Axis(axis), k);
dst.assign(&src);
}
grad_x
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{arr1, arr2, Array};
fn vec_to_arrayd(data: &[f64]) -> ArrayD<f64> {
arr1(data).into_dyn()
}
fn assert_close(a: &ArrayD<f64>, b: &ArrayD<f64>, tol: f64, msg: &str) {
assert_eq!(
a.shape(),
b.shape(),
"{}: shape mismatch {:?} vs {:?}",
msg,
a.shape(),
b.shape()
);
for (av, bv) in a.iter().zip(b.iter()) {
assert!(
(av - bv).abs() < tol,
"{}: element differs by {} (got {}, expected {})",
msg,
(av - bv).abs(),
av,
bv
);
}
}
#[test]
fn test_shift_next_basic() {
let x = vec_to_arrayd(&[0.2, 0.8, 0.5]);
let out = shift_next(&x.view(), 0);
let expected = vec_to_arrayd(&[0.8, 0.5, 0.0]);
assert_close(&out, &expected, 1e-12, "shift_next basic");
}
#[test]
fn test_shift_next_t1_edge_case() {
let x = vec_to_arrayd(&[0.7]);
let out = shift_next(&x.view(), 0);
let expected = vec_to_arrayd(&[0.0]);
assert_close(&out, &expected, 1e-12, "shift_next T=1");
}
#[test]
fn test_until_scan_max_min() {
let a = vec_to_arrayd(&[0.9, 0.9, 0.9]);
let b = vec_to_arrayd(&[0.0, 0.0, 0.4]);
let u = until_scan(&a.view(), &b.view(), 0, UntilSemantics::MaxMin);
let expected = vec_to_arrayd(&[0.4, 0.4, 0.4]);
assert_close(&u, &expected, 1e-12, "until_scan MaxMin");
}
#[test]
fn test_until_scan_prob_sum_product() {
let a = vec_to_arrayd(&[0.5, 0.5]);
let b = vec_to_arrayd(&[0.3, 0.4]);
let u = until_scan(&a.view(), &b.view(), 0, UntilSemantics::ProbSumProduct);
let expected = vec_to_arrayd(&[0.44, 0.4]);
assert_close(&u, &expected, 1e-12, "until_scan ProbSumProduct");
}
#[test]
fn test_shift_next_rank2_axis1() {
let data = arr2(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
let x = data.into_dyn();
let out = shift_next(&x.view(), 1);
assert_eq!(out.shape(), x.shape(), "rank-2 output shape preserved");
let expected = arr2(&[[2.0, 3.0, 0.0], [5.0, 6.0, 0.0]]).into_dyn();
assert_close(&out, &expected, 1e-12, "shift_next rank-2 axis=1");
}
#[test]
fn test_until_scan_rank2_axis1() {
let a = Array::from_elem((2, 3), 0.9_f64).into_dyn();
let b_data = arr2(&[[0.0, 0.0, 0.4], [0.0, 0.0, 0.4]]);
let b = b_data.into_dyn();
let u = until_scan(&a.view(), &b.view(), 1, UntilSemantics::MaxMin);
assert_eq!(u.shape(), a.shape(), "until_scan rank-2 shape preserved");
let expected = Array::from_elem((2, 3), 0.4_f64).into_dyn();
assert_close(&u, &expected, 1e-12, "until_scan rank-2");
}
#[test]
fn test_shift_prev_inverse_of_shift_next() {
let x = vec_to_arrayd(&[0.1, 0.2, 0.3, 0.4]);
let shifted = shift_next(&x.view(), 0);
let grad_x = shift_prev(&shifted.view(), 0);
let expected = vec_to_arrayd(&[0.0, 0.2, 0.3, 0.4]);
assert_close(&grad_x, &expected, 1e-12, "shift_prev inverse");
}
#[test]
fn test_shift_next_vjp_finite_difference() {
let x0 = vec_to_arrayd(&[0.3, 0.7, 0.5, 0.2]);
let g_out = vec_to_arrayd(&[1.0, 2.0, 3.0, 4.0]);
let grad_analytic = shift_prev(&g_out.view(), 0);
let n = x0.len();
let eps = 1e-7;
let mut grad_fd = ArrayD::zeros(x0.raw_dim());
for j in 0..n {
let mut xp = x0.clone();
*xp.iter_mut().nth(j).expect("index in bounds") += eps;
let mut xm = x0.clone();
*xm.iter_mut().nth(j).expect("index in bounds") -= eps;
let out_p = shift_next(&xp.view(), 0);
let out_m = shift_next(&xm.view(), 0);
let diff = (&out_p - &out_m) / (2.0 * eps);
let val: f64 = g_out.iter().zip(diff.iter()).map(|(&g, &d)| g * d).sum();
if let Some(v) = grad_fd.iter_mut().nth(j) {
*v = val;
}
}
assert_close(
&grad_analytic,
&grad_fd,
1e-8,
"shift_next VJP finite-difference",
);
}
#[test]
fn test_until_scan_vjp_prob_sum_product_finite_difference() {
let a0 = vec_to_arrayd(&[0.4, 0.6, 0.8]);
let b0 = vec_to_arrayd(&[0.2, 0.5, 0.3]);
let g_out = vec_to_arrayd(&[1.0, 1.0, 1.0]);
let (grad_a_analytic, grad_b_analytic) = until_scan_vjp(
&a0.view(),
&b0.view(),
&g_out.view(),
0,
UntilSemantics::ProbSumProduct,
);
let n = a0.len();
let eps = 1e-5;
let tol = 1e-4;
let mut grad_a_fd = ArrayD::zeros(a0.raw_dim());
for j in 0..n {
let mut ap = a0.clone();
*ap.iter_mut().nth(j).expect("index in bounds") += eps;
let mut am = a0.clone();
*am.iter_mut().nth(j).expect("index in bounds") -= eps;
let u_p = until_scan(&ap.view(), &b0.view(), 0, UntilSemantics::ProbSumProduct);
let u_m = until_scan(&am.view(), &b0.view(), 0, UntilSemantics::ProbSumProduct);
let diff = (&u_p - &u_m) / (2.0 * eps);
let val: f64 = g_out.iter().zip(diff.iter()).map(|(&g, &d)| g * d).sum();
if let Some(v) = grad_a_fd.iter_mut().nth(j) {
*v = val;
}
}
let mut grad_b_fd = ArrayD::zeros(b0.raw_dim());
for j in 0..n {
let mut bp = b0.clone();
*bp.iter_mut().nth(j).expect("index in bounds") += eps;
let mut bm = b0.clone();
*bm.iter_mut().nth(j).expect("index in bounds") -= eps;
let u_p = until_scan(&a0.view(), &bp.view(), 0, UntilSemantics::ProbSumProduct);
let u_m = until_scan(&a0.view(), &bm.view(), 0, UntilSemantics::ProbSumProduct);
let diff = (&u_p - &u_m) / (2.0 * eps);
let val: f64 = g_out.iter().zip(diff.iter()).map(|(&g, &d)| g * d).sum();
if let Some(v) = grad_b_fd.iter_mut().nth(j) {
*v = val;
}
}
assert_close(
&grad_a_analytic,
&grad_a_fd,
tol,
"until_scan VJP grad_a finite-difference",
);
assert_close(
&grad_b_analytic,
&grad_b_fd,
tol,
"until_scan VJP grad_b finite-difference",
);
}
#[test]
fn test_weak_until_scan_maxmin_boundary_1() {
let a = vec_to_arrayd(&[1.0, 1.0, 1.0]);
let b = vec_to_arrayd(&[0.0, 0.0, 0.0]);
let u = temporal_binary_scan(
&a.view(),
&b.view(),
0,
TemporalBinaryForm::WeakUntil,
UntilSemantics::MaxMin,
);
let expected = vec_to_arrayd(&[1.0, 1.0, 1.0]);
assert_close(&u, &expected, 1e-12, "WeakUntil MaxMin boundary=1");
}
#[test]
fn test_release_scan_maxmin() {
let a = vec_to_arrayd(&[0.0, 0.0, 1.0]);
let b = vec_to_arrayd(&[1.0, 1.0, 1.0]);
let u = temporal_binary_scan(
&a.view(),
&b.view(),
0,
TemporalBinaryForm::Release,
UntilSemantics::MaxMin,
);
let expected = vec_to_arrayd(&[1.0, 1.0, 1.0]);
assert_close(&u, &expected, 1e-12, "Release MaxMin b holds always");
}
#[test]
fn test_strong_release_scan_maxmin() {
let a = vec_to_arrayd(&[0.0, 1.0, 0.0]);
let b = vec_to_arrayd(&[1.0, 0.0, 1.0]);
let u = temporal_binary_scan(
&a.view(),
&b.view(),
0,
TemporalBinaryForm::StrongRelease,
UntilSemantics::MaxMin,
);
let expected = vec_to_arrayd(&[0.0, 0.0, 0.0]);
assert_close(&u, &expected, 1e-12, "StrongRelease MaxMin hand-computed");
}
#[test]
fn test_weak_until_prob_sum_product() {
let a = vec_to_arrayd(&[0.5, 0.8]);
let b = vec_to_arrayd(&[0.2, 0.3]);
let u = temporal_binary_scan(
&a.view(),
&b.view(),
0,
TemporalBinaryForm::WeakUntil,
UntilSemantics::ProbSumProduct,
);
let u1 = 0.3_f64 + 0.8 - 0.3 * 0.8; let inner0 = 0.5_f64 * u1; let u0 = 0.2_f64 + inner0 - 0.2 * inner0; let expected = vec_to_arrayd(&[u0, u1]);
assert_close(
&u,
&expected,
1e-12,
"WeakUntil ProbSumProduct hand-computed",
);
}
#[test]
fn test_release_prob_sum_product() {
let a = vec_to_arrayd(&[0.4, 0.6]);
let b = vec_to_arrayd(&[0.7, 0.5]);
let u = temporal_binary_scan(
&a.view(),
&b.view(),
0,
TemporalBinaryForm::Release,
UntilSemantics::ProbSumProduct,
);
let u1 = 0.5_f64 * (0.6_f64 + 1.0 - 0.6);
let inner0 = 0.4_f64 + u1 - 0.4 * u1;
let u0 = 0.7_f64 * inner0;
let expected = vec_to_arrayd(&[u0, u1]);
assert_close(&u, &expected, 1e-12, "Release ProbSumProduct hand-computed");
}
#[test]
fn test_duality_release_vs_until() {
let a = vec_to_arrayd(&[0.0, 1.0, 0.0]);
let b = vec_to_arrayd(&[1.0, 1.0, 0.0]);
let release_u = temporal_binary_scan(
&a.view(),
&b.view(),
0,
TemporalBinaryForm::Release,
UntilSemantics::MaxMin,
);
let one_minus_a = a.mapv(|v| 1.0 - v);
let one_minus_b = b.mapv(|v| 1.0 - v);
let until_u = until_scan(
&one_minus_a.view(),
&one_minus_b.view(),
0,
UntilSemantics::MaxMin,
);
let dual = until_u.mapv(|v| 1.0 - v);
assert_close(
&release_u,
&dual,
1e-12,
"duality release vs until (Boolean MaxMin)",
);
}
#[test]
fn test_vjp_weak_until_finite_difference() {
let a0 = vec_to_arrayd(&[0.4, 0.6, 0.7]);
let b0 = vec_to_arrayd(&[0.2, 0.5, 0.3]);
let g_out = vec_to_arrayd(&[1.0, 0.5, -0.3]);
let sem = UntilSemantics::ProbSumProduct;
let form = TemporalBinaryForm::WeakUntil;
let (ga_analytic, gb_analytic) =
temporal_binary_scan_vjp(&a0.view(), &b0.view(), &g_out.view(), 0, form, sem);
let n = a0.len();
let eps = 1e-5;
let tol = 1e-4;
let mut ga_fd = ArrayD::zeros(a0.raw_dim());
for j in 0..n {
let mut ap = a0.clone();
*ap.iter_mut().nth(j).expect("j in bounds") += eps;
let mut am = a0.clone();
*am.iter_mut().nth(j).expect("j in bounds") -= eps;
let up = temporal_binary_scan(&ap.view(), &b0.view(), 0, form, sem);
let um = temporal_binary_scan(&am.view(), &b0.view(), 0, form, sem);
let diff = (&up - &um) / (2.0 * eps);
let val: f64 = g_out.iter().zip(diff.iter()).map(|(&g, &d)| g * d).sum();
if let Some(v) = ga_fd.iter_mut().nth(j) {
*v = val;
}
}
let mut gb_fd = ArrayD::zeros(b0.raw_dim());
for j in 0..n {
let mut bp = b0.clone();
*bp.iter_mut().nth(j).expect("j in bounds") += eps;
let mut bm = b0.clone();
*bm.iter_mut().nth(j).expect("j in bounds") -= eps;
let up = temporal_binary_scan(&a0.view(), &bp.view(), 0, form, sem);
let um = temporal_binary_scan(&a0.view(), &bm.view(), 0, form, sem);
let diff = (&up - &um) / (2.0 * eps);
let val: f64 = g_out.iter().zip(diff.iter()).map(|(&g, &d)| g * d).sum();
if let Some(v) = gb_fd.iter_mut().nth(j) {
*v = val;
}
}
assert_close(
&ga_analytic,
&ga_fd,
tol,
"WeakUntil VJP grad_a finite-diff",
);
assert_close(
&gb_analytic,
&gb_fd,
tol,
"WeakUntil VJP grad_b finite-diff",
);
}
#[test]
fn test_vjp_release_finite_difference() {
let a0 = vec_to_arrayd(&[0.3, 0.7, 0.5]);
let b0 = vec_to_arrayd(&[0.6, 0.4, 0.8]);
let g_out = vec_to_arrayd(&[0.5, -0.5, 1.0]);
let sem = UntilSemantics::ProbSumProduct;
let form = TemporalBinaryForm::Release;
let (ga_analytic, gb_analytic) =
temporal_binary_scan_vjp(&a0.view(), &b0.view(), &g_out.view(), 0, form, sem);
let n = a0.len();
let eps = 1e-5;
let tol = 1e-4;
let mut ga_fd = ArrayD::zeros(a0.raw_dim());
for j in 0..n {
let mut ap = a0.clone();
*ap.iter_mut().nth(j).expect("j in bounds") += eps;
let mut am = a0.clone();
*am.iter_mut().nth(j).expect("j in bounds") -= eps;
let up = temporal_binary_scan(&ap.view(), &b0.view(), 0, form, sem);
let um = temporal_binary_scan(&am.view(), &b0.view(), 0, form, sem);
let diff = (&up - &um) / (2.0 * eps);
let val: f64 = g_out.iter().zip(diff.iter()).map(|(&g, &d)| g * d).sum();
if let Some(v) = ga_fd.iter_mut().nth(j) {
*v = val;
}
}
let mut gb_fd = ArrayD::zeros(b0.raw_dim());
for j in 0..n {
let mut bp = b0.clone();
*bp.iter_mut().nth(j).expect("j in bounds") += eps;
let mut bm = b0.clone();
*bm.iter_mut().nth(j).expect("j in bounds") -= eps;
let up = temporal_binary_scan(&a0.view(), &bp.view(), 0, form, sem);
let um = temporal_binary_scan(&a0.view(), &bm.view(), 0, form, sem);
let diff = (&up - &um) / (2.0 * eps);
let val: f64 = g_out.iter().zip(diff.iter()).map(|(&g, &d)| g * d).sum();
if let Some(v) = gb_fd.iter_mut().nth(j) {
*v = val;
}
}
assert_close(&ga_analytic, &ga_fd, tol, "Release VJP grad_a finite-diff");
assert_close(&gb_analytic, &gb_fd, tol, "Release VJP grad_b finite-diff");
}
#[test]
fn test_vjp_strong_release_finite_difference() {
let a0 = vec_to_arrayd(&[0.5, 0.4, 0.6]);
let b0 = vec_to_arrayd(&[0.3, 0.7, 0.5]);
let g_out = vec_to_arrayd(&[-0.5, 1.0, 0.5]);
let sem = UntilSemantics::ProbSumProduct;
let form = TemporalBinaryForm::StrongRelease;
let (ga_analytic, gb_analytic) =
temporal_binary_scan_vjp(&a0.view(), &b0.view(), &g_out.view(), 0, form, sem);
let n = a0.len();
let eps = 1e-5;
let tol = 1e-4;
let mut ga_fd = ArrayD::zeros(a0.raw_dim());
for j in 0..n {
let mut ap = a0.clone();
*ap.iter_mut().nth(j).expect("j in bounds") += eps;
let mut am = a0.clone();
*am.iter_mut().nth(j).expect("j in bounds") -= eps;
let up = temporal_binary_scan(&ap.view(), &b0.view(), 0, form, sem);
let um = temporal_binary_scan(&am.view(), &b0.view(), 0, form, sem);
let diff = (&up - &um) / (2.0 * eps);
let val: f64 = g_out.iter().zip(diff.iter()).map(|(&g, &d)| g * d).sum();
if let Some(v) = ga_fd.iter_mut().nth(j) {
*v = val;
}
}
let mut gb_fd = ArrayD::zeros(b0.raw_dim());
for j in 0..n {
let mut bp = b0.clone();
*bp.iter_mut().nth(j).expect("j in bounds") += eps;
let mut bm = b0.clone();
*bm.iter_mut().nth(j).expect("j in bounds") -= eps;
let up = temporal_binary_scan(&a0.view(), &bp.view(), 0, form, sem);
let um = temporal_binary_scan(&a0.view(), &bm.view(), 0, form, sem);
let diff = (&up - &um) / (2.0 * eps);
let val: f64 = g_out.iter().zip(diff.iter()).map(|(&g, &d)| g * d).sum();
if let Some(v) = gb_fd.iter_mut().nth(j) {
*v = val;
}
}
assert_close(
&ga_analytic,
&ga_fd,
tol,
"StrongRelease VJP grad_a finite-diff",
);
assert_close(
&gb_analytic,
&gb_fd,
tol,
"StrongRelease VJP grad_b finite-diff",
);
}
#[test]
fn test_binary_scan_rank2_axis1() {
let a_data = arr2(&[[0.9, 0.9, 0.9], [0.5, 0.5, 0.5]]);
let b_data = arr2(&[[0.0, 0.0, 0.0], [0.3, 0.3, 0.3]]);
let a = a_data.into_dyn();
let b = b_data.into_dyn();
let u = temporal_binary_scan(
&a.view(),
&b.view(),
1,
TemporalBinaryForm::WeakUntil,
UntilSemantics::MaxMin,
);
assert_eq!(u.shape(), a.shape(), "rank-2 WeakUntil shape preserved");
let expected = arr2(&[[0.9, 0.9, 0.9], [0.5, 0.5, 0.5]]).into_dyn();
assert_close(&u, &expected, 1e-12, "WeakUntil rank-2 axis=1");
}
}