code wiki / (root) / nx_ansur_regress.nx

nx_ansur_regress.nx source

↩ module page · 348 lines · 14802 B

1// nx_ansur_regress.nx -- DOES HUMAN PROPORTION CHANGE WITH STATURE? Regress a landmark ratio on 2// stature over the ANSUR II sample and let the SLOPE answer. 3// 4// WHY. nx_skelgen stores every segment as PER-MILLE OF STATURE, and its own docstring states the 5// consequence as a feature: "so the same rules generate a child, an adult or a giant". Measured, 6// femur/stature is 0.2411 at 1750 mm and 0.2407/0.2411 at 1400/1900 -- CONSTANT BY CONSTRUCTION. 7// It is not emergent; it is the storage format. A rig that only scales is a doll at every size. 8// The fix is mechanical because ANSUR measures nx_skelgen's OWN landmarks: 9// acromialheight/stature <-> BP_ACROMION = 818 per-mille 10// trochanterionheight/stature <-> BP_TROCH = 530 per-mille 11// Regress (landmark_height / stature) ON stature. A NON-ZERO SLOPE IS THE MISSING PROPORTION 12// CHANGE, and the slope itself is the correction term. 13// 14// nx_ansur_regress <ansur.csv> <landmark_column_name> 15// 16// COLUMNS ARE RESOLVED BY NAME FROM THE HEADER ROW AT RUNTIME, never by a hardcoded index: ANSUR's 17// columns are alphabetical and the male and female files must both parse, so an index literal would 18// be a magic number that silently reads the wrong measure if either file differs. 19// 20// THE THREE TRAPS, QUOTED FROM THE PUBLISHER, ALL OF WHICH BITE THIS COMPUTATION: 21// 1. Units are millimetres EXCEPT MASS, WHICH IS HECTOGRAMS DESPITE ITS COLUMN BEING NAMED 22// weightkg -- a 10x error waiting. This organ touches no mass column, and refuses any column 23// whose name contains "weight" so the trap cannot be walked into later. 24// 2. The trailing Heightin / Weightlbs columns are SELF-REPORTED and DISAGREE with the official 25// measures. This organ uses `stature`, never Heightin, and refuses those names by name. 26// 3. The sample is military including reservists and the publisher states it "is still not an 27// approximation of the US Civilian population". THAT BOUND TRAVELS WITH EVERY NUMBER THIS 28// ORGAN PRINTS, so it is printed WITH them rather than left in a doc nobody reads. 29// 30// ARITHMETIC. Integer-only, and mean-centred ON PURPOSE: the raw cross-products (n*Sxy - Sx*Sy) 31// reach ~5.7e12 and multiplying by a precision scale would approach the i64 ceiling. Centring first 32// keeps every term small (dx ~ +/-100, dy ~ +/-30), so the scaled slope cannot overflow. Two passes, 33// no floats -- NishiLang has no float type and this needs none. 34// license_tier: ORIGINAL expect_exit: 0 35import "nx_syscalls.nx" 36 37const AR_E6: i64 = 1000000 38const AR_PERMIL: i64 = 1000 39const AR_COMMA: i64 = 44 40const AR_NL: i64 = 10 41const AR_CR: i64 = 13 42const AR_D0: i64 = 48 43const AR_D9: i64 = 57 44const AR_DOT: i64 = 46 45const AR_MINUS: i64 = 45 46// plausibility band for a human stature in mm. NOT a filter on the data -- a COLUMN-SHIFT DETECTOR: 47// if the parsed stature column stops looking like statures, the header mapping is wrong and every 48// number below it would be confident nonsense. 49const AR_STAT_LO: i64 = 1000 50const AR_STAT_HI: i64 = 2200 51 52func ar_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 53func ar_num(v: i64) -> i64 { 54 let b: *u8 = sys_mmap(32) as *u8 55 var x: i64 = v 56 var ng: i64 = 0 57 if x < 0 { ng = 1; x = 0 - x } 58 var i: i64 = 31 59 if x == 0 { b[i] = AR_D0 as u8; i = i - 1 } 60 while x > 0 { b[i] = (AR_D0 + x % 10) as u8; x = x / 10; i = i - 1 } 61 if ng == 1 { b[i] = AR_MINUS as u8; i = i - 1 } 62 sys_write(1, ((b as i64) + i + 1) as *u8, 31 - i) 63 return 0 64} 65func ar_kv(k: *u8, v: i64) -> i64 { ar_puts(k); ar_num(v); ar_puts("\n" as *u8); return 0 } 66func ar_refuse(r: *u8) -> i64 { ar_puts("ANSUR-REGRESS REFUSED: " as *u8); ar_puts(r); ar_puts("\n" as *u8); return 0 } 67func ar_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 68 69// does the buffer region [a,b) equal the C string s ? 70func ar_eq(buf: *u8, a: i64, b: i64, s: *u8) -> i64 { 71 let n: i64 = ar_slen(s) 72 if b - a != n { return 0 } 73 var i: i64 = 0 74 while i < n { if buf[a+i] != s[i] { return 0 } i = i + 1 } 75 return 1 76} 77// does the buffer region contain the C string s as a substring (for the refused-name guards) 78func ar_contains(buf: *u8, a: i64, b: i64, s: *u8) -> i64 { 79 let n: i64 = ar_slen(s) 80 if n == 0 { return 0 } 81 var i: i64 = a 82 while i + n <= b { 83 var k: i64 = 0 84 var same: i64 = 1 85 while k < n { if buf[i+k] != s[k] { same = 0 } k = k + 1 } 86 if same == 1 { return 1 } 87 i = i + 1 88 } 89 return 0 90} 91// parse an unsigned integer from [a,b); a decimal point truncates (ANSUR heights are whole mm) 92func ar_parse(buf: *u8, a: i64, b: i64) -> i64 { 93 var v: i64 = 0 94 var got: i64 = 0 95 var i: i64 = a 96 var go: i64 = 1 97 while go == 1 { 98 if i >= b { go = 0 } 99 if go == 1 { 100 let c: i64 = buf[i] as i64 101 if c == AR_DOT { go = 0 } else { 102 var isd: i64 = 0 103 if c >= AR_D0 { if c <= AR_D9 { isd = 1 } } 104 if isd == 1 { v = v*10 + (c - AR_D0); got = 1; i = i + 1 } else { go = 0 } 105 } 106 } 107 } 108 if got == 0 { return 0 - 1 } 109 return v 110} 111// end of the current line starting at p 112func ar_eol(buf: *u8, n: i64, p: i64) -> i64 { 113 var i: i64 = p 114 var go: i64 = 1 115 while go == 1 { 116 if i >= n { go = 0 } 117 if go == 1 { if buf[i] == (AR_NL as u8) { go = 0 } else { i = i + 1 } } 118 } 119 return i 120} 121// index of field `want` within the line [p,e), or -1 122func ar_field_bounds(buf: *u8, p: i64, e: i64, want: i64, sa: *i64, sb: *i64) -> i64 { 123 var f: i64 = 0 124 var a: i64 = p 125 var i: i64 = p 126 var go: i64 = 1 127 while go == 1 { 128 if i >= e { 129 if f == want { sa[0] = a; sb[0] = i; return 1 } 130 go = 0 131 } 132 if go == 1 { 133 if buf[i] == (AR_COMMA as u8) { 134 if f == want { sa[0] = a; sb[0] = i; return 1 } 135 f = f + 1 136 a = i + 1 137 } 138 i = i + 1 139 } 140 } 141 return 0 142} 143// resolve a column index BY NAME from the header line 144func ar_col_by_name(buf: *u8, p: i64, e: i64, name: *u8) -> i64 { 145 var f: i64 = 0 146 var a: i64 = p 147 var i: i64 = p 148 var go: i64 = 1 149 while go == 1 { 150 if i >= e { 151 var b2: i64 = i 152 if b2 > a { if buf[b2-1] == (AR_CR as u8) { b2 = b2 - 1 } } 153 if ar_eq(buf, a, b2, name) == 1 { return f } 154 go = 0 155 } 156 if go == 1 { 157 if buf[i] == (AR_COMMA as u8) { 158 if ar_eq(buf, a, i, name) == 1 { return f } 159 f = f + 1 160 a = i + 1 161 } 162 i = i + 1 163 } 164 } 165 return 0 - 1 166} 167 168// MEAN VERB (2026-09-02, the faceanat scale derivation): `nx_ansur_regress <csv> <column> mean` prints the 169// column's mean, n, min and max in the file's own units (mm) and exits -- a POPULATION reading for the 170// aesthetic canon, never a regression. Same header-resolved column, same trap guards, and the publisher's 171// population bound is printed WITH the number rather than left in a doc nobody reads. Rows whose value 172// does not parse are COUNTED and reported, never silently dropped into the denominator. 173func ar_mean(buf: *u8, n: i64, h_end: i64, c_land: i64, lname: *u8) -> i64 { 174 let sa: *i64 = sys_mmap(16) as *i64 175 let sb: *i64 = sys_mmap(16) as *i64 176 var rows: i64 = 0 177 var bad: i64 = 0 178 var sum: i64 = 0 179 var lo: i64 = 0 180 var hi: i64 = 0 181 var p: i64 = h_end + 1 182 while p < n { 183 let e: i64 = ar_eol(buf, n, p) 184 if e > p { 185 var lv: i64 = 0 - 1 186 if ar_field_bounds(buf, p, e, c_land, sa, sb) == 1 { lv = ar_parse(buf, sa[0], sb[0]) } 187 if lv > 0 { 188 if rows == 0 { lo = lv; hi = lv } 189 if lv < lo { lo = lv } 190 if lv > hi { hi = lv } 191 sum = sum + lv 192 rows = rows + 1 193 } else { bad = bad + 1 } 194 } 195 p = e + 1 196 } 197 ar_puts("verb=mean column=" as *u8); ar_puts(lname); ar_puts("\n" as *u8) 198 ar_kv("rows_used=" as *u8, rows) 199 ar_kv("rows_unparsable=" as *u8, bad) 200 if rows < 1 { ar_refuse("no usable rows for that column -- no mean to publish" as *u8); return 6 } 201 ar_kv("mean_mm=" as *u8, sum / rows) 202 ar_kv("mean_mm_x10=" as *u8, (sum * 10) / rows) 203 ar_kv("min_mm=" as *u8, lo) 204 ar_kv("max_mm=" as *u8, hi) 205 ar_puts("POPULATION BOUND, TRAVELS WITH EVERY NUMBER ABOVE: ANSUR II is a military sample including reservists; the publisher states it is still not an approximation of the US Civilian population.\n" as *u8) 206 return 0 207} 208 209func main(argc: i64, argv: *i64) -> i64 { 210 if argc < 3 { 211 ar_puts("usage: nx_ansur_regress <ansur.csv> <landmark_column_name>\n" as *u8) 212 return 3 213 } 214 let path: *u8 = argv[1] as *u8 215 let lname: *u8 = argv[2] as *u8 216 217 // TRAP GUARDS -- refuse the publisher's known-bad columns BY NAME so the trap cannot be walked 218 // into by a future caller rather than merely documented. 219 let ln: i64 = ar_slen(lname) 220 if ar_contains(lname, 0, ln, "weight" as *u8) == 1 { 221 ar_refuse("mass columns are HECTOGRAMS despite being named weightkg -- this organ does not touch mass" as *u8) 222 return 4 223 } 224 if ar_contains(lname, 0, ln, "Heightin" as *u8) == 1 { 225 ar_refuse("Heightin is SELF-REPORTED and disagrees with the official measures -- use stature" as *u8) 226 return 4 227 } 228 229 let lnp: *i64 = sys_mmap(16) as *i64 230 lnp[0] = 0 231 let buf: *u8 = sys_read_file(path, lnp) 232 if (buf as i64) == 0 { ar_refuse("cannot read the ANSUR csv" as *u8); return 4 } 233 let n: i64 = lnp[0] 234 ar_kv("bytes=" as *u8, n) 235 236 let h_end: i64 = ar_eol(buf, n, 0) 237 let c_stat: i64 = ar_col_by_name(buf, 0, h_end, "stature" as *u8) 238 let c_land: i64 = ar_col_by_name(buf, 0, h_end, lname) 239 ar_puts("landmark_column="); ar_puts(lname); ar_puts("\n" as *u8) 240 ar_kv("col_stature=" as *u8, c_stat) 241 ar_kv("col_landmark=" as *u8, c_land) 242 if c_land < 0 { ar_refuse("named landmark column not found in the header" as *u8); return 5 } 243 // the mean verb needs only the landmark column; it runs BEFORE the stature requirement below 244 if argc >= 4 { let vb: *u8 = argv[3] as *u8; if ar_eq(vb, 0, ar_slen(vb), "mean" as *u8) == 1 { return ar_mean(buf, n, h_end, c_land, lname) } } 245 if c_stat < 0 { ar_refuse("no column named stature in the header" as *u8); return 5 } 246 247 // ---- pass 1: collect, and DETECT COLUMN SHIFT rather than trusting the mapping ---- 248 let cap: i64 = 8192 249 let xs: *i64 = sys_mmap(cap*8) as *i64 250 let ys: *i64 = sys_mmap(cap*8) as *i64 251 let sa: *i64 = sys_mmap(16) as *i64 252 let sb: *i64 = sys_mmap(16) as *i64 253 var rows: i64 = 0 254 var bad: i64 = 0 255 var implausible: i64 = 0 256 var p: i64 = h_end + 1 257 var over: i64 = 0 258 while p < n { 259 let e: i64 = ar_eol(buf, n, p) 260 if e > p { 261 var okrow: i64 = 0 262 var sv: i64 = 0 - 1 263 var lv: i64 = 0 - 1 264 if ar_field_bounds(buf, p, e, c_stat, sa, sb) == 1 { sv = ar_parse(buf, sa[0], sb[0]) } 265 if ar_field_bounds(buf, p, e, c_land, sa, sb) == 1 { lv = ar_parse(buf, sa[0], sb[0]) } 266 if sv > 0 { if lv > 0 { okrow = 1 } } 267 if okrow == 1 { 268 var plaus: i64 = 0 269 if sv >= AR_STAT_LO { if sv <= AR_STAT_HI { plaus = 1 } } 270 if plaus == 0 { implausible = implausible + 1 } else { 271 if rows < cap { 272 xs[rows] = sv 273 ys[rows] = (lv * AR_PERMIL) / sv 274 rows = rows + 1 275 } else { over = over + 1 } 276 } 277 } else { bad = bad + 1 } 278 } 279 p = e + 1 280 } 281 ar_kv("rows_used=" as *u8, rows) 282 ar_kv("rows_unparsable=" as *u8, bad) 283 ar_kv("rows_stature_outside_plausible_band=" as *u8, implausible) 284 ar_kv("rows_dropped_over_capacity=" as *u8, over) 285 if over > 0 { 286 ar_refuse("row capacity exceeded -- refusing to publish a regression over a SILENTLY TRUNCATED sample" as *u8) 287 return 6 288 } 289 if implausible > 0 { 290 ar_refuse("stature column carries values outside 1000-2200mm -- the header mapping is wrong and every derived number would be confident nonsense" as *u8) 291 return 6 292 } 293 if rows < 2 { ar_refuse("fewer than two usable rows -- a slope over one point is undefined" as *u8); return 6 } 294 295 // ---- pass 2: mean-centred least squares (integer, overflow-safe by construction) ---- 296 var sx: i64 = 0 297 var sy: i64 = 0 298 var i: i64 = 0 299 while i < rows { sx = sx + xs[i]; sy = sy + ys[i]; i = i + 1 } 300 let mx: i64 = sx / rows 301 let my: i64 = sy / rows 302 var sxy: i64 = 0 303 var sxx: i64 = 0 304 var lo: i64 = xs[0] 305 var hi: i64 = xs[0] 306 i = 0 307 while i < rows { 308 let dx: i64 = xs[i] - mx 309 let dy: i64 = ys[i] - my 310 sxy = sxy + dx*dy 311 sxx = sxx + dx*dx 312 if xs[i] < lo { lo = xs[i] } 313 if xs[i] > hi { hi = xs[i] } 314 i = i + 1 315 } 316 ar_kv("mean_stature_mm=" as *u8, mx) 317 ar_kv("mean_ratio_permil=" as *u8, my) 318 ar_kv("stature_min_mm=" as *u8, lo) 319 ar_kv("stature_max_mm=" as *u8, hi) 320 if sxx <= 0 { ar_refuse("zero variance in stature -- slope undefined" as *u8); return 6 } 321 322 // slope in MILLIONTHS of a per-mille per millimetre 323 let slope_e6: i64 = (sxy * AR_E6) / sxx 324 ar_kv("slope_permil_per_mm_e6=" as *u8, slope_e6) 325 326 // the number that matters: how much the ratio moves across the OBSERVED stature range 327 let span: i64 = hi - lo 328 let d_e6: i64 = slope_e6 * span 329 ar_kv("stature_span_mm=" as *u8, span) 330 ar_kv("delta_ratio_permil_e6_over_span=" as *u8, d_e6) 331 ar_kv("delta_ratio_permil_over_span=" as *u8, d_e6 / AR_E6) 332 333 // predicted ratio at each end of the observed range -- the DOLL TEST in one line 334 let r_lo_e6: i64 = my * AR_E6 + slope_e6 * (lo - mx) 335 let r_hi_e6: i64 = my * AR_E6 + slope_e6 * (hi - mx) 336 ar_kv("ratio_at_stature_min_permil_e6=" as *u8, r_lo_e6) 337 ar_kv("ratio_at_stature_max_permil_e6=" as *u8, r_hi_e6) 338 339 var verdict: i64 = 0 340 if slope_e6 != 0 { verdict = 1 } 341 if verdict == 1 { 342 ar_puts("VERDICT=PROPORTION-CHANGES-WITH-STATURE (non-zero slope: per-mille-of-stature storage CANNOT express this)\n" as *u8) 343 } else { 344 ar_puts("VERDICT=PROPORTION-CONSTANT (zero slope: pure scaling would be anthropometrically correct)\n" as *u8) 345 } 346 ar_puts("POPULATION BOUND, TRAVELS WITH EVERY NUMBER ABOVE: ANSUR II is a military sample including reservists; the publisher states it is still not an approximation of the US Civilian population.\n" as *u8) 347 return 0 348}