IA Utils — Adversarial Vector Attack¶
enhanced_dense_healing_hybrid decides, per step,
whether a change in a vector sequence looks like genuine dynamics or static noise --
this module asks how easy that decision is to fool. A gradient-based (PGD-style) stress
test crafts the minimal perturbation, within a fixed budget, that flips the
Phi-Trigger's decision either direction -- not random noise thrown at the problem and
hoping.
Step 1. Disguise a static point as genuine dynamics¶
import numpy as np
from ia_utils.adversarial_vector_attack import craft_adversarial_healing_perturbation
vettori = np.array([[1.0, 1.0]] * 10) + np.random.default_rng(0).normal(0, 0.001, (10, 2))
result = craft_adversarial_healing_perturbation(vettori, target_idx=5, direction='flip_to_dynamic')
result['success'], result['original_trigger_active'], result['final_trigger_active']
vettori is a nearly flat sequence -- real Phi-Trigger noise, not corrupted at all
(original_trigger_active=False, correctly seen as static). craft_adversarial_healing_perturbation(
vettori, target_idx, direction='flip_to_dynamic') searches for the smallest
perturbation (within an epsilon-ball, default epsilon=0.1) to step 5 that flips the
trigger on -- and finds one: final_trigger_active=True, success=True. This is the
evade direction: a real attacker's goal would be making corrupted data pass through
enhanced_dense_healing_hybrid unhealed by disguising it
as legitimate dynamics.
Step 2. The other direction: suppress a genuine trigger¶
vettori2 = np.array([[1.0, 1.0]] * 10) + np.random.default_rng(0).normal(0, 0.01, (10, 2))
result2 = craft_adversarial_healing_perturbation(vettori2, target_idx=3, direction='flip_to_static')
result2['success'], result2['original_trigger_active'], result2['final_trigger_active']
This time step 3 already genuinely triggers (original_trigger_active=True -- real
noise at this magnitude, not a hand-picked corruption). direction='flip_to_static'
finds the opposite perturbation: one that makes the Phi-Trigger wrongly see this real
trigger as noise (final_trigger_active=False) and median-replace it instead -- the
suppress direction, a real signal getting wiped out rather than a fake one getting
through. Not every target index is this exploitable within the epsilon budget (some
points genuinely resist the attack, success=False) -- which points are vulnerable
depends on the sequence's own local shape around them, not a fixed property of the
attack itself.
Details¶
Why this is possible at all: evaluate_phi_trigger (healing)
thresholds |v_dinamic| with a hard step (not differentiable at the boundary), but
v_dinamic itself is built entirely from JAX-differentiable operations
(calculate_phi_ab/calculate_vettore_dinamico) -- so a gradient-based search can
still find the minimal input change that pushes the pre-threshold value across the
boundary, even though the boundary decision itself isn't smooth.
Two real bugs found and fixed during this utility's own verification, not assumed
correct: the default step_size used to scale with the epsilon budget -- a
larger budget converged to a worse result (verified directly, non-monotonic in
epsilon), now a small fixed default independent of epsilon. And calculate_phi_ab's
[0,1] clip saturates for inputs whose semantic distance exceeds
MAX_SEMANTIC_DISTANCE, giving an exact-zero gradient -- a real property of the
formula, not a bug in the attack, but one that used to be silently misreported as "no
better point found" rather than detected and reported (perturbation_norm == 0,
success == False).
Adapted from IGME's chained-differentiable-attack idea (arXiv:2607.27465, "Efficient Chained Method Ensemble for Transferable Semantic Segmentation Attacks", He & Zhang), applied here to vector sequences instead of image segmentation.
adversarial_vector_attack ¶
Gradient-based adversarial stress-testing for enhanced_dense_healing_hybrid's Phi-Trigger decision -- a targeted, crafted perturbation instead of the random NaN/Inf corruption this healing pipeline is normally tested against, adapted from IGME's chained-differentiable-attack idea (arXiv:2607.27465, "IGME: Efficient Chained Method Ensemble for Transferable Semantic Segmentation Attacks") applied to vector sequences instead of image segmentation.
evaluate_phi_trigger (dense_evolution.healing) makes its keep-vs-median- replace decision by thresholding |v_dinamic| against NON_STATIC_THRESHOLD_A -- a hard step, not differentiable at the boundary. But v_dinamic itself (via calculate_phi_ab -> calculate_vettore_dinamico) is built entirely from norms, dot products, log, and clip -- all JAX-differentiable. This crafts a minimal perturbation to a single vector in the sequence, via gradient ascent/descent on |v_dinamic| (projected back into an epsilon L2-ball each step, the standard PGD pattern), that flips the trigger's decision at that point:
- "flip_to_dynamic": push an originally-static point (would be replaced by the local median) across the threshold so the trigger keeps it as-is instead -- the more security-relevant direction, since it represents a worst-case corruption crafted to evade the healer by looking like genuine motion, rather than obvious noise.
- "flip_to_static": push an originally-dynamic point (would be kept) across the threshold so the trigger discards it as noise instead -- the failure mode of genuine signal getting wrongly suppressed.
This does not attack the median-filter fallback itself, only the Phi-Trigger's keep-vs-replace decision -- see craft_adversarial_healing_ perturbation's own docstring for what "success" means precisely.
craft_adversarial_healing_perturbation ¶
craft_adversarial_healing_perturbation(
vettori: ndarray,
target_idx: int,
radius_baseline: Optional[int] = None,
epsilon: float = 0.1,
n_steps: int = 50,
step_size: Optional[float] = None,
direction: str = "flip_to_dynamic",
) -> dict
Crafts a minimal adversarial perturbation to vettori[target_idx], within an L2 epsilon-ball, that flips enhanced_dense_healing_hybrid's Phi-Trigger decision at that index -- a targeted stress test, not random noise.
Reproduces enhanced_dense_healing_hybrid's own per-step computation at target_idx exactly (same baseline_mean window, same adaptive radius default, same inter-point-gradient vector) so the crafted perturbation is faithful to what the real healing pipeline would actually see, not a simplified stand-in.
BUG FIX: step_size used to default to 2*epsilon/n_steps -- tying the per-step move size to the epsilon budget. Verified directly this makes LARGER epsilon give WORSE (higher final |v_dinamic|) results, not better: a bigger budget means a bigger step, which overshoots and oscillates around the minimum instead of converging to it, which is the opposite of what a bigger budget should ever do for a correct optimizer. step_size is now independent of epsilon (a small fixed default, tuned to v_dinamic's typical local scale) -- epsilon only bounds where the iterate is allowed to end up (via projection after each step), not how big each step is.
Args: vettori: array-like, shape (n_steps, dim), the (unperturbed) vector sequence. Not modified in place. target_idx: index to attack; must be >= 2 (the healing loop's own starting point) and < len(vettori). radius_baseline: same meaning as enhanced_dense_healing_hybrid's own parameter; None uses the same adaptive default. epsilon: L2-norm budget for the perturbation (the attack is projected back into this ball after every gradient step). n_steps: number of PGD-style gradient steps. step_size: per-step move size along the normalized gradient; None uses a small fixed default (0.02) independent of epsilon -- see the bug-fix note above for why. direction: "flip_to_dynamic" (evade -- make static-looking input pass through unhealed) or "flip_to_static" (suppress -- make dynamic-looking input get median-replaced instead).
Returns: dict with: perturbed_vettori: copy of vettori with vettori[target_idx] replaced by the crafted perturbation. success: bool, True only if the trigger decision actually flipped in the requested direction (a small epsilon or too few steps can fail to cross the threshold). original_trigger_active / final_trigger_active: bool, the Phi-Trigger's decision (True = dynamic/kept) before and after the perturbation. original_magnitude / final_magnitude: float, |v_dinamic| before and after. perturbation_norm: float, actual L2 norm of the applied perturbation (<= epsilon).
Source code in tools/ia_utils/adversarial_vector_attack.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
See also: ia_utils.vector_healing for the function
under test, and dense_evolution.healing for the underlying Phi-Trigger
primitives.