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}