code wiki / (root) / nx_patch_attribution.nx

nx_patch_attribution.nx source

↩ module page · 332 lines · 12177 B

1// nx_patch_attribution.nx -- the COMPOST primitive. 2// 3// User: "did this patch improve things if so then lets find out why 4// and test and integrate like how we are using a lora right now 5// that improves our elder ai realism image gen but i couldnt tell 6// you why and neither can the system". 7// 8// THE answer. Given a population of "before-patch" samples and a 9// population of "after-patch" samples, each with per-axis 10// measurements, this primitive reports: 11// 12// - WHICH measurement axes the patch significantly moved 13// - HOW MUCH each axis moved (Cohen's d effect size) 14// - WHETHER the movement is statistically significant after 15// family-wise alpha correction (Bonferroni) 16// 17// The substrate's bits-up answer to the user's concrete pain: 18// don't ship LoRAs nobody understands; ship a decomposition that 19// turns the LoRA's effect into measurable named-axis changes that 20// can then be COMPOSTED into substrate architecture per the 21// patches-as-manure cardinal. 22// 23// ===== Workflow =================================================== 24// 25// 1. Run N generations WITHOUT the patch; collect per-axis 26// measurements via caller's measurement_fn. Call this 27// sample population B (before). 28// 2. Run N generations WITH the patch; collect same measurements. 29// Call this population A (after). 30// 3. nx_patch_attribute(B_data, A_data, n_axes, n_samples) 31// 4. Result lists per-axis: mean_before, mean_after, effect_size, 32// significant? Tells caller EXACTLY what the patch is doing. 33// 5. Caller then chooses: is this a useful axis movement? If 34// yes: ship a substrate-side primitive that achieves the 35// same axis movement without the LoRA. COMPOSTED. 36// 37// ===== Statistical methods ======================================= 38// 39// Cohen's d (Cohen 1988): standardised mean difference. 40// d = (mean_A - mean_B) / pooled_std 41// pooled_std = sqrt((var_A + var_B) / 2) 42// Interpretation (Cohen 1988 rules of thumb): 43// |d| < 0.2: negligible effect 44// 0.2-0.5: small effect 45// 0.5-0.8: medium effect 46// |d| > 0.8: large effect 47// 48// Welch t-test (Welch 1947): unequal-variance two-sample test. 49// t = (mean_A - mean_B) / sqrt(var_A/n_A + var_B/n_B) 50// Significance: |t| > critical_t for given alpha + dof. 51// 52// Bonferroni correction: divide alpha by n_axes for family-wise 53// alpha control. Critical t table from nx_multivariate. 54// 55// Per the bits-up + bounded-loop cardinals. 56// 57// genealogy_id: cohen_1988_effect_size + welch_1947_t_test + 58// bonferroni_1936_inequality + 59// hinton_2015_distillation_attribution 60// lineage_id: substrate_patch_attribution_v1 61 62// nx_safety_envelope: 63// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 64// sil_target: SIL1 65// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 66// verdict: NOT_YET_EVALUATED 67 68import "nx_syscalls.nx" 69import "nx_tier.nx" 70import "nx_loop.nx" 71import "nx_isqrt.nx" 72import "nx_multivariate.nx" 73 74const NX_PA_Q10: nx_int = 1024 75 76// ===== Effect-size sealed enum ==================================== 77// 78// Cohen's d magnitude classification. Cleaner-API than passing raw 79// d back; caller can branch on this directly. 80 81const NX_PA_EFFECT_NEGLIGIBLE: nx_int = 0 // |d| < 0.2 82const NX_PA_EFFECT_SMALL: nx_int = 1 // 0.2 <= |d| < 0.5 83const NX_PA_EFFECT_MEDIUM: nx_int = 2 // 0.5 <= |d| < 0.8 84const NX_PA_EFFECT_LARGE: nx_int = 3 // |d| >= 0.8 85const NX_PA_EFFECT_N: nx_int = 4 86 87func nx_pa_effect_is_valid(e: nx_int) -> nx_int { 88 if e < 0 { return 0 } 89 if e >= NX_PA_EFFECT_N { return 0 } 90 return 1 91} 92 93// ===== Patch-attribution verdict ================================== 94 95const NX_PA_OK: nx_int = 0 96const NX_PA_ERR_BAD_DIMS: nx_int = 1 97const NX_PA_ERR_INSUFFICIENT: nx_int = 2 98const NX_PA_N_VERDICTS: nx_int = 3 99 100func nx_pa_verdict_is_valid(v: nx_int) -> nx_int { 101 if v < 0 { return 0 } 102 if v >= NX_PA_N_VERDICTS { return 0 } 103 return 1 104} 105 106// ===== Result envelope ============================================ 107 108struct NxPatchAttributionResult { 109 overall_verdict: nx_int, // NX_PA_OK / ERR 110 n_axes: nx_int, 111 n_samples_b: nx_int, 112 n_samples_a: nx_int, 113 means_b: *i64, // [n_axes] 114 means_a: *i64, 115 cohens_d_q10: *i64, // [n_axes] effect size Q10 (signed) 116 effect_class: *i64, // [n_axes] NX_PA_EFFECT_* 117 t_stat_q10: *i64, // [n_axes] Welch t-statistic 118 significant: *i64, // [n_axes] 1 = significant after Bonferroni, 0 else 119 n_significant: nx_int // count of significant axes 120} 121 122const NX_PA_RESULT_BYTES: nx_int = 88 123 124// ===== Helper: classify Cohen's d effect size ===================== 125 126func _pa_classify_d(d_q10: i64) -> nx_int { 127 var abs_d: i64 = d_q10 128 if abs_d < 0 { abs_d = 0 - abs_d } 129 // Thresholds in Q10: 130 // 0.2 Q10 = 205 131 // 0.5 Q10 = 512 132 // 0.8 Q10 = 819 133 if abs_d < 205 { return NX_PA_EFFECT_NEGLIGIBLE } 134 if abs_d < 512 { return NX_PA_EFFECT_SMALL } 135 if abs_d < 819 { return NX_PA_EFFECT_MEDIUM } 136 return NX_PA_EFFECT_LARGE 137} 138 139// ===== Main attribute entrypoint ================================== 140// 141// samples_b: [n_samples_b * n_axes] per-axis measurements pre-patch 142// samples_a: [n_samples_a * n_axes] per-axis measurements with patch 143// n_axes: measurement dimensions 144// n_samples_b / n_samples_a: sample counts per population 145// 146// Returns *NxPatchAttributionResult populated with per-axis stats. 147 148func nx_patch_attribute( 149 samples_b: *i64, n_samples_b: nx_int, 150 samples_a: *i64, n_samples_a: nx_int, 151 n_axes: nx_int) -> *NxPatchAttributionResult { 152 153 let r: *NxPatchAttributionResult = sys_mmap(NX_PA_RESULT_BYTES) as *NxPatchAttributionResult 154 r.overall_verdict = NX_PA_ERR_BAD_DIMS 155 r.n_axes = n_axes 156 r.n_samples_b = n_samples_b 157 r.n_samples_a = n_samples_a 158 r.n_significant = 0 159 160 if n_axes <= 0 { return r } 161 if n_samples_b < NX_MC_MIN_N { r.overall_verdict = NX_PA_ERR_INSUFFICIENT; return r } 162 if n_samples_a < NX_MC_MIN_N { r.overall_verdict = NX_PA_ERR_INSUFFICIENT; return r } 163 164 r.means_b = sys_mmap(n_axes * 8) as *i64 165 r.means_a = sys_mmap(n_axes * 8) as *i64 166 r.cohens_d_q10 = sys_mmap(n_axes * 8) as *i64 167 r.effect_class = sys_mmap(n_axes * 8) as *i64 168 r.t_stat_q10 = sys_mmap(n_axes * 8) as *i64 169 r.significant = sys_mmap(n_axes * 8) as *i64 170 171 let crit_t: nx_int = _mv_bonferroni_critical_t_q10(n_axes) 172 173 var ax: nx_int = 0 174 var ax_iter: nx_int = 0 175 var ax_verdict: nx_int = NX_LOOP_RUNNING 176 let AX_BUDGET: nx_int = n_axes 177 var n_sig: nx_int = 0 178 while ax_verdict == NX_LOOP_RUNNING && ax_iter < AX_BUDGET { 179 // Per-axis stats: mean + sample variance for B and A. 180 var sum_b: i64 = 0 181 var i: nx_int = 0 182 while i < n_samples_b { sum_b = sum_b + samples_b[i * n_axes + ax]; i = i + 1 } 183 let mean_b: i64 = sum_b / n_samples_b 184 var sumsq_b: i64 = 0 185 var j: nx_int = 0 186 while j < n_samples_b { 187 let d: i64 = samples_b[j * n_axes + ax] - mean_b 188 sumsq_b = sumsq_b + d * d 189 j = j + 1 190 } 191 let var_b: i64 = sumsq_b / (n_samples_b - 1) 192 193 var sum_a: i64 = 0 194 var k: nx_int = 0 195 while k < n_samples_a { sum_a = sum_a + samples_a[k * n_axes + ax]; k = k + 1 } 196 let mean_a: i64 = sum_a / n_samples_a 197 var sumsq_a: i64 = 0 198 var m: nx_int = 0 199 while m < n_samples_a { 200 let d: i64 = samples_a[m * n_axes + ax] - mean_a 201 sumsq_a = sumsq_a + d * d 202 m = m + 1 203 } 204 let var_a: i64 = sumsq_a / (n_samples_a - 1) 205 206 r.means_b[ax] = mean_b 207 r.means_a[ax] = mean_a 208 209 // Cohen's d: (mean_a - mean_b) / pooled_std 210 // pooled_std = sqrt((var_b + var_a) / 2) 211 let pooled_var: i64 = (var_b + var_a) / 2 212 let pooled_std: i64 = nx_isqrt_q10(pooled_var + 1) 213 var d_q10: i64 = 0 214 if pooled_std > 0 { 215 d_q10 = ((mean_a - mean_b) * NX_PA_Q10) / pooled_std 216 } 217 r.cohens_d_q10[ax] = d_q10 218 r.effect_class[ax] = _pa_classify_d(d_q10) 219 220 // Welch t-test: t = (mean_a - mean_b) / sqrt(var_b/n_b + var_a/n_a) 221 // se^2 in raw units; sqrt -> Q10 via nx_isqrt_q10(... * Q10). 222 let se_sq: i64 = var_b / n_samples_b + var_a / n_samples_a 223 let se: i64 = nx_isqrt_q10(se_sq * NX_PA_Q10 + 1) 224 var diff: i64 = mean_a - mean_b 225 var abs_diff: i64 = diff 226 if abs_diff < 0 { abs_diff = 0 - abs_diff } 227 var t_stat: i64 = 0 228 if se > 0 { 229 t_stat = (abs_diff * NX_PA_Q10) / se 230 } 231 r.t_stat_q10[ax] = t_stat 232 233 // Significance (Bonferroni-corrected). 234 if t_stat >= crit_t { 235 r.significant[ax] = 1 236 n_sig = n_sig + 1 237 } 238 if t_stat < crit_t { 239 r.significant[ax] = 0 240 } 241 242 ax = ax + 1 243 ax_iter = ax_iter + 1 244 } 245 r.n_significant = n_sig 246 r.overall_verdict = NX_PA_OK 247 return r 248} 249 250// ===== Self-test ================================================== 251// 252// 3 measurement axes, 100 samples each population. 253// Axis 0: B mean ~100, A mean ~100 (NO effect; patch didn't move) 254// Axis 1: B mean ~100, A mean ~150 (LARGE effect) 255// Axis 2: B mean ~100, A mean ~110 (SMALL effect) 256// 257// Expected: 258// axis 0: NEGLIGIBLE, not significant 259// axis 1: LARGE, significant 260// axis 2: SMALL, may or may not be significant depending on 261// variance (low variance -> significant) 262 263func main() -> i64 { 264 let N: nx_int = 100 265 let A: nx_int = 3 266 let buf_b: *i64 = sys_mmap(N * A * 8) as *i64 267 let buf_a: *i64 = sys_mmap(N * A * 8) as *i64 268 269 let prng: *i64 = sys_mmap(8) as *i64 270 nx_prng_init(prng, 0xdeadbeef) 271 272 // Fill samples. 273 var i: nx_int = 0 274 while i < N { 275 // B population. 276 let r0_b: i64 = nx_prng_range(prng, 20) + 90 // uniform [90, 109] 277 let r1_b: i64 = nx_prng_range(prng, 20) + 90 278 let r2_b: i64 = nx_prng_range(prng, 20) + 90 279 buf_b[i * A + 0] = r0_b 280 buf_b[i * A + 1] = r1_b 281 buf_b[i * A + 2] = r2_b 282 283 // A population. 284 let r0_a: i64 = nx_prng_range(prng, 20) + 90 // same as B (no effect) 285 let r1_a: i64 = nx_prng_range(prng, 20) + 140 // shifted up by 50 (large effect) 286 let r2_a: i64 = nx_prng_range(prng, 20) + 100 // shifted up by 10 (small effect) 287 buf_a[i * A + 0] = r0_a 288 buf_a[i * A + 1] = r1_a 289 buf_a[i * A + 2] = r2_a 290 291 i = i + 1 292 } 293 294 let r: *NxPatchAttributionResult = nx_patch_attribute( 295 buf_b, N, buf_a, N, A) 296 if r.overall_verdict != NX_PA_OK { return 10 } 297 298 // Axis 0: no movement, NEGLIGIBLE. 299 if r.effect_class[0] != NX_PA_EFFECT_NEGLIGIBLE { return 20 } 300 if r.significant[0] != 0 { return 21 } 301 302 // Axis 1: large movement. 303 if r.effect_class[1] != NX_PA_EFFECT_LARGE { return 30 } 304 if r.significant[1] != 1 { return 31 } 305 // d should be ~ +50 / ~5 std ~ 10.0 -> Q10 = ~10000. 306 // Actual will depend on uniform-distribution variance ~ (range^2/12). 307 if r.cohens_d_q10[1] < 5000 { return 32 } // certainly large 308 309 // Axis 2: small movement (~10 mean shift on similar variance); 310 // may or may not be significant depending on n; with N=100 likely 311 // significant. Just check effect_class is SMALL or MEDIUM. 312 if r.effect_class[2] != NX_PA_EFFECT_SMALL { 313 if r.effect_class[2] != NX_PA_EFFECT_MEDIUM { return 40 } 314 } 315 316 // At least 1 significant axis (axis 1). 317 if r.n_significant < 1 { return 50 } 318 319 // Verdict gates. 320 var vi: nx_int = 0 321 while vi < NX_PA_N_VERDICTS { 322 if nx_pa_verdict_is_valid(vi) != 1 { return 60 + vi } 323 vi = vi + 1 324 } 325 var ei: nx_int = 0 326 while ei < NX_PA_EFFECT_N { 327 if nx_pa_effect_is_valid(ei) != 1 { return 70 + ei } 328 ei = ei + 1 329 } 330 331 return 0 332}