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}