use crate::dataflow_fixpoint::reachability_closure_into;
use vyre_primitives::graph::do_calculus::{
do_intervention_delete_incoming_cpu_into, do_rule2_reverse_incoming_cpu_into,
do_rule3_subgraph_cpu_into,
};
#[derive(Debug, Default)]
pub struct DoCalculusImpactScratch {
surgically_modified_adj: Vec<u32>,
closure: Vec<u32>,
scratch: Vec<u32>,
impact_mask: Vec<u32>,
reduced_adjacency: Vec<u32>,
kept_indices: Vec<u32>,
}
impl DoCalculusImpactScratch {
#[must_use]
pub fn impact_mask(&self) -> &[u32] {
&self.impact_mask
}
#[must_use]
pub fn reduced_adjacency(&self) -> &[u32] {
&self.reduced_adjacency
}
#[must_use]
pub fn kept_indices(&self) -> &[u32] {
&self.kept_indices
}
}
#[must_use]
pub fn predict_impact(adj: &[u32], intervention_mask: &[u32], n: u32) -> Vec<u32> {
use crate::observability::{bump, do_calculus_change_impact_calls};
bump(&do_calculus_change_impact_calls);
if n == 0 {
return Vec::new();
}
let mut scratch = DoCalculusImpactScratch::default();
predict_impact_with_scratch(adj, intervention_mask, n, &mut scratch);
scratch.impact_mask
}
pub fn predict_impact_with_scratch(
adj: &[u32],
intervention_mask: &[u32],
n: u32,
scratch: &mut DoCalculusImpactScratch,
) {
predict_impact_into(
adj,
intervention_mask,
n,
&mut scratch.surgically_modified_adj,
&mut scratch.closure,
&mut scratch.scratch,
&mut scratch.impact_mask,
);
}
pub fn predict_impact_into(
adj: &[u32],
intervention_mask: &[u32],
n: u32,
surgically_modified_adj: &mut Vec<u32>,
closure: &mut Vec<u32>,
scratch: &mut Vec<u32>,
impact_mask: &mut Vec<u32>,
) {
if n == 0 {
impact_mask.clear();
return;
}
do_intervention_delete_incoming_cpu_into(adj, intervention_mask, n, surgically_modified_adj);
reachability_closure_into(surgically_modified_adj, n, n, closure, scratch);
let n_us = n as usize;
impact_mask.clear();
impact_mask.resize(n_us, 0);
for i in 0..n_us {
if intervention_mask[i] != 0 {
impact_mask[i] = 1; for j in 0..n_us {
if closure[i * n_us + j] != 0 {
impact_mask[j] = 1;
}
}
}
}
}
#[must_use]
pub fn impact_subgraph(adj: &[u32], intervention_mask: &[u32], n: u32) -> (Vec<u32>, Vec<u32>) {
use crate::observability::{bump, do_calculus_change_impact_calls};
bump(&do_calculus_change_impact_calls);
if n == 0 {
return (Vec::new(), Vec::new());
}
let mut scratch = DoCalculusImpactScratch::default();
impact_subgraph_with_scratch(adj, intervention_mask, n, &mut scratch);
(scratch.reduced_adjacency, scratch.kept_indices)
}
pub fn impact_subgraph_with_scratch(
adj: &[u32],
intervention_mask: &[u32],
n: u32,
scratch: &mut DoCalculusImpactScratch,
) {
predict_impact_with_scratch(adj, intervention_mask, n, scratch);
do_rule3_subgraph_cpu_into(
adj,
&scratch.impact_mask,
n,
&mut scratch.reduced_adjacency,
&mut scratch.kept_indices,
);
}
#[must_use]
pub fn predict_impact_observation_form(adj: &[u32], observation_mask: &[u32], n: u32) -> Vec<u32> {
use crate::observability::{bump, do_calculus_change_impact_calls};
bump(&do_calculus_change_impact_calls);
if n == 0 {
return Vec::new();
}
let mut scratch = DoCalculusImpactScratch::default();
predict_impact_observation_form_with_scratch(adj, observation_mask, n, &mut scratch);
scratch.impact_mask
}
pub fn predict_impact_observation_form_with_scratch(
adj: &[u32],
observation_mask: &[u32],
n: u32,
scratch: &mut DoCalculusImpactScratch,
) {
predict_impact_observation_form_into(
adj,
observation_mask,
n,
&mut scratch.surgically_modified_adj,
&mut scratch.closure,
&mut scratch.scratch,
&mut scratch.impact_mask,
);
}
pub fn predict_impact_observation_form_into(
adj: &[u32],
observation_mask: &[u32],
n: u32,
reversed_adj: &mut Vec<u32>,
closure: &mut Vec<u32>,
scratch: &mut Vec<u32>,
impact_mask: &mut Vec<u32>,
) {
if n == 0 {
impact_mask.clear();
return;
}
let n_us = n as usize;
do_rule2_reverse_incoming_cpu_into(adj, observation_mask, n, reversed_adj);
reachability_closure_into(reversed_adj, n, n, closure, scratch);
impact_mask.clear();
impact_mask.resize(n_us, 0);
for i in 0..n_us {
if observation_mask[i] != 0 {
impact_mask[i] = 1;
for j in 0..n_us {
if closure[i * n_us + j] != 0 {
impact_mask[j] = 1;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn chain_impact() {
let adj = vec![0, 1, 0, 0, 0, 1, 0, 0, 0];
let mask = vec![1, 0, 0];
let impact = predict_impact(&adj, &mask, 3);
assert_eq!(impact, vec![1, 1, 1]);
}
#[test]
fn impact_scratch_reuses_matrix_buffers() {
let adj = vec![0, 1, 0, 0, 0, 1, 0, 0, 0];
let mask = vec![1, 0, 0];
let mut scratch = DoCalculusImpactScratch::default();
predict_impact_with_scratch(&adj, &mask, 3, &mut scratch);
let modified_capacity = scratch.surgically_modified_adj.capacity();
let closure_capacity = scratch.closure.capacity();
let temp_capacity = scratch.scratch.capacity();
let mask_capacity = scratch.impact_mask.capacity();
assert_eq!(scratch.impact_mask(), &[1, 1, 1]);
predict_impact_with_scratch(&adj, &[0, 1, 0], 3, &mut scratch);
assert_eq!(
scratch.surgically_modified_adj.capacity(),
modified_capacity
);
assert_eq!(scratch.closure.capacity(), closure_capacity);
assert_eq!(scratch.scratch.capacity(), temp_capacity);
assert_eq!(scratch.impact_mask.capacity(), mask_capacity);
assert_eq!(scratch.impact_mask(), &[0, 1, 1]);
}
#[test]
fn middle_chain_impact() {
let adj = vec![0, 1, 0, 0, 0, 1, 0, 0, 0];
let mask = vec![0, 1, 0];
let impact = predict_impact(&adj, &mask, 3);
assert_eq!(impact, vec![0, 1, 1]);
}
#[test]
fn branched_impact() {
let adj = vec![0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0];
let mask = vec![0, 0, 1, 0];
let impact = predict_impact(&adj, &mask, 4);
assert_eq!(impact, vec![0, 0, 1, 1]);
}
#[test]
fn disjoint_impact() {
let adj = vec![0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0];
let mask = vec![1, 0, 0, 0];
let impact = predict_impact(&adj, &mask, 4);
assert_eq!(impact, vec![1, 1, 0, 0]);
}
#[test]
fn cycle_impact() {
let adj = vec![0, 1, 0, 1, 0, 1, 0, 0, 0];
let mask = vec![1, 0, 0];
let impact = predict_impact(&adj, &mask, 3);
assert_eq!(impact, vec![1, 1, 1]);
}
#[test]
fn empty_graph() {
let impact = predict_impact(&[], &[], 0);
assert!(impact.is_empty());
}
#[test]
fn impact_subgraph_chain_extracts_downstream() {
let adj = vec![0, 1, 0, 0, 0, 1, 0, 0, 0];
let mask = vec![1, 0, 0];
let (reduced, kept) = impact_subgraph(&adj, &mask, 3);
assert_eq!(kept, vec![0, 1, 2]);
assert_eq!(reduced, adj);
}
#[test]
fn impact_subgraph_branch_compresses_unimpacted_rows() {
let adj = vec![0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0];
let mask = vec![1, 0, 0, 0];
let (reduced, kept) = impact_subgraph(&adj, &mask, 4);
assert_eq!(kept, vec![0, 1]);
assert_eq!(reduced, vec![0, 1, 0, 0]);
}
#[test]
fn impact_subgraph_scratch_reuses_reduction_buffers() {
let adj = vec![0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0];
let mut scratch = DoCalculusImpactScratch::default();
impact_subgraph_with_scratch(&adj, &[1, 0, 0, 0], 4, &mut scratch);
let reduced_capacity = scratch.reduced_adjacency.capacity();
let kept_capacity = scratch.kept_indices.capacity();
assert_eq!(scratch.kept_indices(), &[0, 1]);
assert_eq!(scratch.reduced_adjacency(), &[0, 1, 0, 0]);
impact_subgraph_with_scratch(&adj, &[0, 0, 1, 0], 4, &mut scratch);
assert_eq!(scratch.reduced_adjacency.capacity(), reduced_capacity);
assert_eq!(scratch.kept_indices.capacity(), kept_capacity);
assert_eq!(scratch.kept_indices(), &[2, 3]);
assert_eq!(scratch.reduced_adjacency(), &[0, 1, 0, 0]);
}
#[test]
fn impact_subgraph_empty_intervention_empty_subgraph() {
let adj = vec![0, 1, 0, 0];
let mask = vec![0, 0];
let (reduced, kept) = impact_subgraph(&adj, &mask, 2);
assert!(reduced.is_empty());
assert!(kept.is_empty());
}
#[test]
fn impact_subgraph_empty_graph() {
let (r, k) = impact_subgraph(&[], &[], 0);
assert!(r.is_empty());
assert!(k.is_empty());
}
#[test]
fn impact_subgraph_size_invariant_holds_under_partial_impact() {
let adj = vec![
0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, ];
let mask = vec![0, 1, 0, 0, 0];
let (reduced, kept) = impact_subgraph(&adj, &mask, 5);
assert_eq!(reduced.len(), kept.len() * kept.len());
assert_eq!(kept, vec![1, 2]);
assert_eq!(reduced, vec![0, 1, 0, 0]);
}
#[test]
fn impact_subgraph_adversarial_leaf_intervention_keeps_only_leaf() {
let adj = vec![0, 1, 0, 0, 0, 1, 0, 0, 0];
let mask = vec![0, 0, 1];
let (reduced, kept) = impact_subgraph(&adj, &mask, 3);
assert_eq!(kept, vec![2]);
assert_eq!(reduced, vec![0]);
}
#[test]
fn impact_subgraph_adversarial_dense_must_drop_unkept_edges() {
let adj = vec![
0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, ];
let mask = vec![1, 0, 0, 0];
let (reduced, kept) = impact_subgraph(&adj, &mask, 4);
assert_eq!(kept, vec![0, 1, 2]);
assert_eq!(
reduced,
vec![
0, 1, 1, 1, 0, 1, 1, 1, 0, ]
);
}
#[test]
fn observation_form_dag_observed_self_only() {
let adj = vec![0, 1, 0, 0, 0, 1, 0, 0, 0];
let mask = vec![1, 0, 0];
let observed = predict_impact_observation_form(&adj, &mask, 3);
let intervened = predict_impact(&adj, &mask, 3);
assert_eq!(observed, intervened);
}
#[test]
fn observation_form_scratch_reuses_buffers() {
let adj = vec![0, 1, 0, 0, 0, 1, 0, 0, 0];
let mut scratch = DoCalculusImpactScratch::default();
predict_impact_observation_form_with_scratch(&adj, &[1, 0, 0], 3, &mut scratch);
let reversed_capacity = scratch.surgically_modified_adj.capacity();
let closure_capacity = scratch.closure.capacity();
assert_eq!(scratch.impact_mask(), &[1, 1, 1]);
predict_impact_observation_form_with_scratch(&adj, &[0, 1, 0], 3, &mut scratch);
assert_eq!(
scratch.surgically_modified_adj.capacity(),
reversed_capacity
);
assert_eq!(scratch.closure.capacity(), closure_capacity);
assert_eq!(scratch.impact_mask(), &[1, 1, 1]);
}
#[test]
fn observation_form_marks_observed_node() {
let adj = vec![0, 1, 0, 0];
let mask = vec![0, 1];
let impact = predict_impact_observation_form(&adj, &mask, 2);
assert_eq!(impact[1], 1, "observed node must be in impact set");
}
#[test]
fn observation_form_walks_reversed_feedback_edge() {
let adj = vec![0, 1, 0, 1, 0, 1, 0, 0, 0];
let mask = vec![1, 0, 0];
let impact = predict_impact_observation_form(&adj, &mask, 3);
assert_eq!(impact, vec![1, 1, 1]);
}
#[test]
fn observation_form_empty_mask_yields_empty() {
let adj = vec![0, 1, 0, 0];
let mask = vec![0, 0];
let impact = predict_impact_observation_form(&adj, &mask, 2);
assert_eq!(impact, vec![0, 0]);
}
#[test]
fn observation_form_empty_graph() {
assert!(predict_impact_observation_form(&[], &[], 0).is_empty());
}
}