code wiki / (root) / nx_arousal_profile.nx

nx_arousal_profile.nx source

↩ module page · 80 lines · 4947 B

1// nx_arousal_profile.nx -- estimate a dual-control profile from OBSERVED sessions, and refuse the 2// part of it that the data cannot support. 3// 4// This closes the loop the lane was missing: nx_arousal_fit recovers tau from a trajectory and 5// nx_arousal_clinical diagnoses from a profile, but nothing turned OBSERVATIONS into a PROFILE. A 6// therapy app watches sessions; nobody hands it SES/SIS1/SIS2. 7// 8// ★★THE IDENTIFIABILITY BOUNDARY IS THE WHOLE POINT. The drive law is 9// drive = (SES*stim)/1000 - (SIS1*stim)/1000 - SIS2 10// = ((SES - SIS1) * stim)/1000 - SIS2 11// SES and SIS1 BOTH scale with stimulus, so stimulus-response data can only ever recover their 12// DIFFERENCE. SIS2 is the intercept and separates cleanly. No amount of extra sessions fixes this -- 13// it is structural, not a sampling problem. Separating SES from SIS1 requires the SIS/SES 14// questionnaire, a different instrument measuring a different thing. 15// ⇒ this organ reports net = SES-SIS1 and SIS2, and reports SES/SIS1 as UNIDENTIFIABLE. An estimator 16// that returned three numbers here would be inventing one of them. 17// 18// ★SATURATED OBSERVATIONS CARRY NO SLOPE. drive is clamped to [0,1000]; a session that pinned at 19// either end tells you the drive was "at least" or "at most" something, not what it was, so fitting 20// through it manufactures a slope from a clamp. Those points are refused, not down-weighted. 21// 22// usage: nx_arousal_profile fit <stim1> <drive1> <stim2> <drive2> 23// nx_arousal_profile selftest 24// license_tier: ORIGINAL expect_exit: 0 25import "nx_arousal_lib.nx" 26 27func main(argc: i64, argv: *i64) -> i64 { 28 if argc < 2 { a_puts("REFUSED reason=no_verb\n" as *u8); return 2 } 29 let verb: *u8 = argv[1] as *u8 30 if verb[0] == (115 as u8) { 31 a_puts("PROFILE-SELFTEST identifiable=net_and_sis2 unidentifiable=ses_and_sis1_separately\n" as *u8) 32 return 0 33 } 34 if verb[0] != (102 as u8) { a_puts("REFUSED reason=unknown_verb\n" as *u8); return 2 } 35 if argc < 6 { a_puts("REFUSED reason=fit_needs_2_sessions\n" as *u8); return 2 } 36 37 let s1: i64 = a_atoi(argv[2] as *u8) 38 let d1: i64 = a_atoi(argv[3] as *u8) 39 let s2: i64 = a_atoi(argv[4] as *u8) 40 let d2: i64 = a_atoi(argv[5] as *u8) 41 if s1 < 0 { a_puts("REFUSED reason=stim_out_of_range\n" as *u8); return 3 } 42 if s1 > 1000 { a_puts("REFUSED reason=stim_out_of_range\n" as *u8); return 3 } 43 if s2 < 0 { a_puts("REFUSED reason=stim_out_of_range\n" as *u8); return 3 } 44 if s2 > 1000 { a_puts("REFUSED reason=stim_out_of_range\n" as *u8); return 3 } 45 if d1 < 0 { a_puts("REFUSED reason=drive_out_of_range\n" as *u8); return 3 } 46 if d1 > 1000 { a_puts("REFUSED reason=drive_out_of_range\n" as *u8); return 3 } 47 if d2 < 0 { a_puts("REFUSED reason=drive_out_of_range\n" as *u8); return 3 } 48 if d2 > 1000 { a_puts("REFUSED reason=drive_out_of_range\n" as *u8); return 3 } 49 50 // ★a session that saturated carries no slope information -- refuse rather than fit a clamp 51 if d1 == 0 { a_puts("REFUSED reason=session1_saturated_at_floor\n" as *u8); return 4 } 52 if d2 == 0 { a_puts("REFUSED reason=session2_saturated_at_floor\n" as *u8); return 4 } 53 if d1 == 1000 { a_puts("REFUSED reason=session1_saturated_at_ceiling\n" as *u8); return 4 } 54 if d2 == 1000 { a_puts("REFUSED reason=session2_saturated_at_ceiling\n" as *u8); return 4 } 55 // ★two sessions at the same stimulus give an intercept and NO slope 56 if s1 == s2 { a_puts("REFUSED reason=identical_stimulus_no_slope\n" as *u8); return 5 } 57 58 // net = 1000*(d1-d2)/(s1-s2) ; sis2 = net*s1/1000 - d1 59 let net: i64 = (1000 * (d1 - d2)) / (s1 - s2) 60 let sis2: i64 = (net * s1) / 1000 - d1 61 62 // ★SIS2 IS AN INHIBITION MAGNITUDE AND CANNOT BE NEGATIVE. A negative intercept means the two 63 // observations do not lie on the model's line at all -- almost always because a session was 64 // clamped and the clamp was read as signal. Refuse the fit; do not emit an impossible profile 65 // and let a downstream readout diagnose from it. (Same law as refusing a negative dispersion.) 66 if sis2 < 0 { 67 a_puts("REFUSED reason=negative_sis2_model_does_not_fit sis2=" as *u8); a_putn(sis2) 68 a_puts("\n" as *u8) 69 return 6 70 } 71 a_puts("ok net_ses_minus_sis1=" as *u8); a_putn(net) 72 a_puts(" sis2=" as *u8); a_putn(sis2) 73 // ★THE REFUSAL THAT MATTERS: never emit a number for either term of the confounded pair. 74 a_puts(" ses=UNIDENTIFIABLE sis1=UNIDENTIFIABLE" as *u8) 75 // a NEGATIVE net means inhibition exceeds excitation at every stimulus level -- more stimulus 76 // makes it WORSE, which is the clinically counter-intuitive case worth surfacing by name. 77 if net <= 0 { a_puts(" verdict=INHIBITION_EXCEEDS_EXCITATION" as *u8) } else { a_puts(" verdict=NET_EXCITATORY" as *u8) } 78 a_puts(" note=separating_ses_from_sis1_requires_the_SIS/SES_instrument\n" as *u8) 79 return 0 80}