code wiki / (root) / nx_river_quality.nx

nx_river_quality.nx source

↩ module page · 315 lines · 11898 B

1// nx_river_quality.nx -- per-kind grader for nx_river_carve paths. 2// 3// Reads a river polyline (control point array) and grades on 5 axes: 4// 5// 0. SINUOSITY_INDEX: actual polyline length / straight-line endpoint 6// distance. A perfectly straight river = 1.0 (low score; rivers 7// should meander); target [1.2, 2.0]. 8// 1. SEGMENT_CONSISTENCY: stddev of per-segment lengths. Wildly 9// varying segments suggest noise; uniform segments suggest natural 10// meander. 11// 2. MEANDER_BAND: mean perpendicular deviation from the start-end 12// baseline. A river that never deviates is a canal. 13// 3. ENDPOINT_DISPLACEMENT: ||end - start|| / sum_segment_lengths. 14// Closer to 0 = the river loops back to itself (bad); closer to 1 15// = straight line (also less interesting); target [0.4, 0.8]. 16// 4. NO_REVERSAL: count of segments where direction flips by > 90 deg 17// from the dominant flow direction; penalty for many reversals. 18// 19// EMITS LAYER_VERDICT (kind = NX_LAYER_KIND_RIVER). 20// 21// genealogy_id: leopold_wolman_1957 + genevaux_2013 + sinuosity_canon 22// lineage_id: nx_river_quality_5axis_v1 23 24// nx_safety_envelope: 25// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 26// sil_target: SIL1 27// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 28// verdict: NOT_YET_EVALUATED 29 30import "nx_syscalls.nx" 31import "nx_tier.nx" 32import "nx_layer_verdict.nx" 33 34const NX_RQ_Q: nx_int = 16384 35const NX_RQ_POINT_STRIDE: nx_int = 2 36 37const NX_RQ_AXIS_SINUOSITY: nx_int = 0 38const NX_RQ_AXIS_SEG_CONSISTENCY: nx_int = 1 39const NX_RQ_AXIS_MEANDER: nx_int = 2 40const NX_RQ_AXIS_ENDPOINT_DISP: nx_int = 3 41const NX_RQ_AXIS_NO_REVERSAL: nx_int = 4 42const NX_RQ_AXIS_COUNT: nx_int = 5 43 44// ===== Internal: integer sqrt approximation (Newton iteration) ===== 45func _rq_isqrt(n: nx_int) -> nx_int { 46 if n <= 0 { return 0 } 47 var x: nx_int = n 48 var y: nx_int = (x + 1) / 2 49 while y < x { 50 x = y 51 y = (x + n / x) / 2 52 } 53 return x 54} 55 56// ===== Axes ======================================================== 57func _rq_segment_total(points: *i64, n: nx_int) -> nx_int { 58 var sum: nx_int = 0 59 var i: nx_int = 0 60 while i < n - 1 { 61 let dx: nx_int = points[(i + 1) * NX_RQ_POINT_STRIDE ] - points[i * NX_RQ_POINT_STRIDE ] 62 let dy: nx_int = points[(i + 1) * NX_RQ_POINT_STRIDE + 1] - points[i * NX_RQ_POINT_STRIDE + 1] 63 sum = sum + _rq_isqrt(dx * dx + dy * dy) 64 i = i + 1 65 } 66 return sum 67} 68 69func _rq_straight_len(points: *i64, n: nx_int) -> nx_int { 70 if n < 2 { return 0 } 71 let dx: nx_int = points[(n - 1) * NX_RQ_POINT_STRIDE ] - points[0] 72 let dy: nx_int = points[(n - 1) * NX_RQ_POINT_STRIDE + 1] - points[1] 73 return _rq_isqrt(dx * dx + dy * dy) 74} 75 76func _rq_sinuosity_q14(points: *i64, n: nx_int) -> nx_int { 77 let q: nx_int = NX_RQ_Q 78 if n < 2 { return 0 } 79 let straight: nx_int = _rq_straight_len(points, n) 80 if straight <= 0 { return 0 } 81 let total: nx_int = _rq_segment_total(points, n) 82 let ratio_q: nx_int = (total * q) / straight 83 // Target band [1.2 Q, 2.0 Q]. 84 let lo: nx_int = q + q / 5 85 let hi: nx_int = 2 * q 86 var score: nx_int = 0 87 if ratio_q >= lo { 88 if ratio_q <= hi { score = q } 89 if ratio_q > hi { score = q - (ratio_q - hi) * q / hi } 90 } 91 if ratio_q < lo { 92 if lo > 0 { score = (ratio_q * q) / lo } 93 } 94 if score < 0 { score = 0 } 95 if score > q { score = q } 96 return score 97} 98 99func _rq_seg_consistency_q14(points: *i64, n: nx_int) -> nx_int { 100 if n < 3 { return 0 } 101 let q: nx_int = NX_RQ_Q 102 var sum: nx_int = 0 103 var count: nx_int = 0 104 var i: nx_int = 0 105 while i < n - 1 { 106 let dx: nx_int = points[(i + 1) * NX_RQ_POINT_STRIDE ] - points[i * NX_RQ_POINT_STRIDE ] 107 let dy: nx_int = points[(i + 1) * NX_RQ_POINT_STRIDE + 1] - points[i * NX_RQ_POINT_STRIDE + 1] 108 sum = sum + _rq_isqrt(dx * dx + dy * dy) 109 count = count + 1 110 i = i + 1 111 } 112 if count == 0 { return 0 } 113 let mean_len: nx_int = sum / count 114 if mean_len <= 0 { return 0 } 115 var dev_sum: nx_int = 0 116 var j: nx_int = 0 117 while j < n - 1 { 118 let dx: nx_int = points[(j + 1) * NX_RQ_POINT_STRIDE ] - points[j * NX_RQ_POINT_STRIDE ] 119 let dy: nx_int = points[(j + 1) * NX_RQ_POINT_STRIDE + 1] - points[j * NX_RQ_POINT_STRIDE + 1] 120 let len: nx_int = _rq_isqrt(dx * dx + dy * dy) 121 var d: nx_int = len - mean_len 122 if d < 0 { d = 0 - d } 123 dev_sum = dev_sum + d 124 j = j + 1 125 } 126 let mean_dev: nx_int = dev_sum / count 127 // CV = mean_dev / mean_len. Lower = more consistent. Target < 0.3. 128 let cv_q: nx_int = (mean_dev * q) / mean_len 129 var score: nx_int = q - cv_q 130 if score < 0 { score = 0 } 131 if score > q { score = q } 132 return score 133} 134 135func _rq_meander_q14(points: *i64, n: nx_int) -> nx_int { 136 if n < 3 { return 0 } 137 let q: nx_int = NX_RQ_Q 138 let x0: nx_int = points[0] 139 let y0: nx_int = points[1] 140 let xn: nx_int = points[(n - 1) * NX_RQ_POINT_STRIDE ] 141 let yn: nx_int = points[(n - 1) * NX_RQ_POINT_STRIDE + 1] 142 let bdx: nx_int = xn - x0 143 let bdy: nx_int = yn - y0 144 let blen_sq: nx_int = bdx * bdx + bdy * bdy 145 if blen_sq <= 0 { return 0 } 146 let blen: nx_int = _rq_isqrt(blen_sq) 147 if blen <= 0 { return 0 } 148 // For each interior point, perpendicular distance to baseline = 149 // |dx_to_point * baseline_dy - dy_to_point * baseline_dx| / blen. 150 var sum_perp: nx_int = 0 151 var i: nx_int = 1 152 while i < n - 1 { 153 let px: nx_int = points[i * NX_RQ_POINT_STRIDE ] - x0 154 let py: nx_int = points[i * NX_RQ_POINT_STRIDE + 1] - y0 155 var cross: nx_int = px * bdy - py * bdx 156 if cross < 0 { cross = 0 - cross } 157 sum_perp = sum_perp + cross / blen 158 i = i + 1 159 } 160 let mean_perp: nx_int = sum_perp / (n - 2) 161 // Target: mean_perp / blen = 0.1 to 0.3. 162 let target_lo_perp: nx_int = blen / 10 163 let target_hi_perp: nx_int = blen * 3 / 10 164 var score: nx_int = 0 165 if mean_perp >= target_lo_perp { 166 if mean_perp <= target_hi_perp { score = q } 167 if mean_perp > target_hi_perp { 168 score = q - ((mean_perp - target_hi_perp) * q) / target_hi_perp 169 } 170 } 171 if mean_perp < target_lo_perp { 172 if target_lo_perp > 0 { score = (mean_perp * q) / target_lo_perp } 173 } 174 if score < 0 { score = 0 } 175 if score > q { score = q } 176 return score 177} 178 179func _rq_endpoint_disp_q14(points: *i64, n: nx_int) -> nx_int { 180 if n < 2 { return 0 } 181 let q: nx_int = NX_RQ_Q 182 let total: nx_int = _rq_segment_total(points, n) 183 let straight: nx_int = _rq_straight_len(points, n) 184 if total <= 0 { return 0 } 185 let ratio_q: nx_int = (straight * q) / total 186 // Target [0.4Q, 0.8Q]; outside drops linearly to 0. 187 let lo: nx_int = q * 4 / 10 188 let hi: nx_int = q * 8 / 10 189 var score: nx_int = 0 190 if ratio_q >= lo { 191 if ratio_q <= hi { score = q } 192 if ratio_q > hi { 193 score = q - (ratio_q - hi) * q / (q - hi) 194 } 195 } 196 if ratio_q < lo { 197 if lo > 0 { score = (ratio_q * q) / lo } 198 } 199 if score < 0 { score = 0 } 200 if score > q { score = q } 201 return score 202} 203 204func _rq_no_reversal_q14(points: *i64, n: nx_int) -> nx_int { 205 if n < 3 { return 0 } 206 let q: nx_int = NX_RQ_Q 207 var n_reversals: nx_int = 0 208 var n_segments: nx_int = 0 209 // Dominant direction = endpoint - start. 210 let x0: nx_int = points[0] 211 let y0: nx_int = points[1] 212 let xn: nx_int = points[(n - 1) * NX_RQ_POINT_STRIDE ] 213 let yn: nx_int = points[(n - 1) * NX_RQ_POINT_STRIDE + 1] 214 let bdx: nx_int = xn - x0 215 let bdy: nx_int = yn - y0 216 var i: nx_int = 0 217 while i < n - 1 { 218 let sdx: nx_int = points[(i + 1) * NX_RQ_POINT_STRIDE ] - points[i * NX_RQ_POINT_STRIDE ] 219 let sdy: nx_int = points[(i + 1) * NX_RQ_POINT_STRIDE + 1] - points[i * NX_RQ_POINT_STRIDE + 1] 220 // Dot product with dominant direction. Negative = reversal. 221 let dot: nx_int = sdx * bdx + sdy * bdy 222 if dot < 0 { n_reversals = n_reversals + 1 } 223 n_segments = n_segments + 1 224 i = i + 1 225 } 226 if n_segments == 0 { return 0 } 227 // Score = 1 - reversal_ratio. Allow up to 10% reversals (oxbows etc.). 228 let rev_q: nx_int = (n_reversals * q) / n_segments 229 var score: nx_int = q - rev_q * 10 230 if score < 0 { score = 0 } 231 if score > q { score = q } 232 return score 233} 234 235// ===== Public: full river grader =================================== 236func nx_river_quality_grade( 237 points: *i64, n_points: nx_int, out_verdict: *i64 238) { 239 let sinuosity: nx_int = _rq_sinuosity_q14(points, n_points) 240 let consistency: nx_int = _rq_seg_consistency_q14(points, n_points) 241 let meander: nx_int = _rq_meander_q14(points, n_points) 242 let disp: nx_int = _rq_endpoint_disp_q14(points, n_points) 243 let no_reversal: nx_int = _rq_no_reversal_q14(points, n_points) 244 245 nx_layer_verdict_init(out_verdict, NX_LAYER_KIND_RIVER, 246 NX_RQ_AXIS_COUNT, NX_LAYER_REFINE_ADD_DETAIL) 247 out_verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_SINUOSITY] = sinuosity 248 out_verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_SEG_CONSISTENCY] = consistency 249 out_verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_MEANDER] = meander 250 out_verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_ENDPOINT_DISP] = disp 251 out_verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_NO_REVERSAL] = no_reversal 252 nx_layer_verdict_finalize(out_verdict) 253} 254 255// ===== Self-test ==================================================== 256func main() -> i64 { 257 let q: nx_int = NX_RQ_Q 258 let verdict: *i64 = (sys_mmap(NX_LV_STRIDE * NX_SIZEOF_NX_INT)) as *i64 259 260 // T1: A perfectly straight 10-point river -> sinuosity = 1.0, low 261 // score; meander = 0, low score; consistency = high. 262 let straight: *i64 = (sys_mmap(10 * NX_RQ_POINT_STRIDE * NX_SIZEOF_NX_INT)) as *i64 263 var i: nx_int = 0 264 while i < 10 { 265 straight[i * 2 ] = i * 10 266 straight[i * 2 + 1] = 0 267 i = i + 1 268 } 269 nx_river_quality_grade(straight, 10, verdict) 270 // Sinuosity: total/straight = 90/90 = 1.0 -> below 1.2 lower band -> below Q. 271 if verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_SINUOSITY] >= q { 272 return __syscall(93, 1, 0, 0, 0, 0, 0) 273 } 274 // Meander = 0 -> low score. 275 if verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_MEANDER] >= q / 4 { 276 return __syscall(93, 2, 0, 0, 0, 0, 0) 277 } 278 // Consistency should be high (uniform segments). 279 if verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_SEG_CONSISTENCY] < q * 9 / 10 { 280 return __syscall(93, 3, 0, 0, 0, 0, 0) 281 } 282 // No reversals. 283 if verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_NO_REVERSAL] != q { 284 return __syscall(93, 4, 0, 0, 0, 0, 0) 285 } 286 287 // T2: A meandering 10-point river with sinusoidal y-offset. 288 let meander: *i64 = (sys_mmap(10 * NX_RQ_POINT_STRIDE * NX_SIZEOF_NX_INT)) as *i64 289 var j: nx_int = 0 290 while j < 10 { 291 meander[j * 2 ] = j * 10 292 // Saw-tooth y to simulate meanders: 0, 5, -5, 5, -5, ... 293 if j % 2 == 0 { meander[j * 2 + 1] = 0 } 294 if j % 2 == 1 { 295 if (j / 2) % 2 == 0 { meander[j * 2 + 1] = 5 } 296 if (j / 2) % 2 == 1 { meander[j * 2 + 1] = 0 - 5 } 297 } 298 j = j + 1 299 } 300 nx_river_quality_grade(meander, 10, verdict) 301 // Meander should produce non-zero score. 302 if verdict[NX_LV_OFF_AXIS_0 + NX_RQ_AXIS_MEANDER] <= 0 { 303 return __syscall(93, 10, 0, 0, 0, 0, 0) 304 } 305 306 // T3: Verdict is well-formed. 307 if verdict[NX_LV_OFF_KIND] != NX_LAYER_KIND_RIVER { 308 return __syscall(93, 20, 0, 0, 0, 0, 0) 309 } 310 if nx_lv_grade_is_valid(verdict[NX_LV_OFF_GRADE]) != 1 { 311 return __syscall(93, 21, 0, 0, 0, 0, 0) 312 } 313 314 return 0 315}