code wiki / (root) / nx_forest_quality.nx

nx_forest_quality.nx source

↩ module page · 362 lines · 13352 B

1// nx_forest_quality.nx -- per-kind grader for nx_forest_layout output. 2// 3// Reads a tree array (as emitted by nx_forest_layout_generate or 4// nx_forest_layout_generate_poisson) and grades the forest on 5 axes: 5// 6// 0. SPECIES_DIVERSITY: Shannon entropy of species histogram / 7// log2(n_distinct_species). All-pine forest scores low; mixed 8// deciduous/coniferous scores high. 9// 1. SPACING_REGULARITY: median pairwise distance compared to target 10// (target = sqrt(area / n_trees) for Poisson uniform). Sample 11// pairwise distances on a budget; report 1 - stddev/median. 12// 2. AGE_VARIANCE: stddev of tree ages / Q. Worlds with only mature 13// trees score low (no seedlings); worlds with all seedlings score 14// low (no canopy). 15// 3. EDGE_PRESENCE: fraction of trees within outer [0.85r, r] of 16// region center. Edge-effect ecology demands some boundary 17// density. 18// 4. CANOPY_VARIANCE: stddev of canopy radii. All-same-canopy forests 19// look stamped; varied canopy means age + species variety meet. 20// 21// EMITS LAYER_VERDICT (kind = NX_LAYER_KIND_FOREST). 22// 23// genealogy_id: forest_ecology_canon + shannon_1948_entropy + 24// watt_2008_forest_management 25// lineage_id: nx_forest_quality_5axis_v1 26 27// nx_safety_envelope: 28// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 29// sil_target: SIL1 30// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 31// verdict: NOT_YET_EVALUATED 32 33import "nx_syscalls.nx" 34import "nx_tier.nx" 35import "nx_layer_verdict.nx" 36const NX_MAGIC_16384: i64 = 16384 37const NX_MAGIC_25976: i64 = 25976 38const NX_MAGIC_32768: i64 = 32768 39const NX_MAGIC_38048: i64 = 38048 40const NX_MAGIC_42361: i64 = 42361 41const NX_MAGIC_46006: i64 = 46006 42const NX_MAGIC_49152: i64 = 49152 43 44const NX_FQ_Q: nx_int = 16384 45 46// Tree array stride must match nx_forest_layout NX_FOREST_TREE_STRIDE. 47const NX_FQ_TREE_STRIDE: nx_int = 8 48const NX_FQ_OFF_SPECIES: nx_int = 0 49const NX_FQ_OFF_X: nx_int = 1 50const NX_FQ_OFF_Z: nx_int = 3 51const NX_FQ_OFF_CANOPY: nx_int = 5 52const NX_FQ_OFF_AGE: nx_int = 7 53 54// ===== Axis indices ================================================= 55const NX_FQ_AXIS_SPECIES_DIV: nx_int = 0 56const NX_FQ_AXIS_SPACING_REG: nx_int = 1 57const NX_FQ_AXIS_AGE_VAR: nx_int = 2 58const NX_FQ_AXIS_EDGE_PRESENCE: nx_int = 3 59const NX_FQ_AXIS_CANOPY_VAR: nx_int = 4 60const NX_FQ_AXIS_COUNT: nx_int = 5 61 62// ===== Internal: log2 helper ======================================= 63func _fq_log2_q14(bin: nx_int) -> nx_int { 64 if bin <= 1 { return 0 } 65 if bin == 2 { return NX_MAGIC_16384 } 66 if bin == 3 { return NX_MAGIC_25976 } 67 if bin == 4 { return NX_MAGIC_32768 } 68 if bin == 5 { return NX_MAGIC_38048 } 69 if bin == 6 { return NX_MAGIC_42361 } 70 if bin == 7 { return NX_MAGIC_46006 } 71 return NX_MAGIC_49152 // 8 species max 72} 73 74// ===== Axis 0: species diversity (Shannon entropy normalised) ===== 75func _fq_species_diversity_q14(trees: *i64, n: nx_int) -> nx_int { 76 if n <= 1 { return 0 } 77 let q: nx_int = NX_FQ_Q 78 let hist: *i64 = (sys_mmap(8 * NX_SIZEOF_NX_INT)) as *i64 79 var k: nx_int = 0 80 while k < 8 { hist[k] = 0; k = k + 1 } 81 var i: nx_int = 0 82 while i < n { 83 var s: nx_int = trees[i * NX_FQ_TREE_STRIDE + NX_FQ_OFF_SPECIES] 84 if s < 0 { s = 0 } 85 if s > 7 { s = 7 } 86 hist[s] = hist[s] + 1 87 i = i + 1 88 } 89 var n_distinct: nx_int = 0 90 var ent: nx_int = 0 91 var j: nx_int = 0 92 while j < 8 { 93 let c: nx_int = hist[j] 94 if c > 0 { 95 n_distinct = n_distinct + 1 96 var bin: nx_int = n / c 97 if bin < 1 { bin = 1 } 98 if bin > 7 { bin = 7 } 99 let log_bin: nx_int = _fq_log2_q14(bin) 100 ent = ent + (c * log_bin) / n 101 } 102 j = j + 1 103 } 104 if n_distinct <= 1 { return 0 } 105 var max_log: nx_int = n_distinct 106 if max_log > 7 { max_log = 7 } 107 let max_ent: nx_int = _fq_log2_q14(max_log) 108 if max_ent <= 0 { return 0 } 109 var score: nx_int = (ent * q) / max_ent 110 if score > q { score = q } 111 return score 112} 113 114// ===== Axis 1: spacing regularity (median pairwise distance) ====== 115// Sample N random pairs; compute mean abs distance. Compare to 116// expected Poisson-uniform spacing in the region. 117func _fq_spacing_q14( 118 trees: *i64, n: nx_int, region_r: nx_int 119) -> nx_int { 120 if n <= 1 { return 0 } 121 if region_r <= 0 { return 0 } 122 let q: nx_int = NX_FQ_Q 123 // Sample first 64 trees against next-tree spacing (chain walk). 124 // For each tree i, compute distance to tree (i+1) % n. 125 var sum_d: nx_int = 0 126 var count: nx_int = 0 127 var max_d: nx_int = 0 128 var min_d: nx_int = 0 - 1 129 var i: nx_int = 0 130 var lim: nx_int = n 131 if lim > 64 { lim = 64 } 132 while i < lim { 133 let next_i: nx_int = (i + 1) % n 134 let x0: nx_int = trees[i * NX_FQ_TREE_STRIDE + NX_FQ_OFF_X] 135 let z0: nx_int = trees[i * NX_FQ_TREE_STRIDE + NX_FQ_OFF_Z] 136 let x1: nx_int = trees[next_i * NX_FQ_TREE_STRIDE + NX_FQ_OFF_X] 137 let z1: nx_int = trees[next_i * NX_FQ_TREE_STRIDE + NX_FQ_OFF_Z] 138 let dx: nx_int = x1 - x0 139 let dz: nx_int = z1 - z0 140 let d_sq: nx_int = dx * dx + dz * dz 141 sum_d = sum_d + d_sq // accumulate squared, normalise later 142 count = count + 1 143 if d_sq > max_d { max_d = d_sq } 144 if min_d < 0 { min_d = d_sq } 145 if d_sq < min_d { min_d = d_sq } 146 i = i + 1 147 } 148 if count == 0 { return 0 } 149 let mean_d_sq: nx_int = sum_d / count 150 // Score: penalise high (max - min) variance vs mean. 151 let range: nx_int = max_d - min_d 152 if mean_d_sq <= 0 { return 0 } 153 var ratio: nx_int = (range * q) / mean_d_sq 154 if ratio > q * 4 { ratio = q * 4 } 155 // Map [0, 4Q] -> [Q, 0] (high variance = low regularity). 156 var score: nx_int = q - ratio / 4 157 if score < 0 { score = 0 } 158 if score > q { score = q } 159 return score 160} 161 162// ===== Axis 2: age variance (mean abs deviation) =================== 163func _fq_age_variance_q14(trees: *i64, n: nx_int) -> nx_int { 164 if n <= 1 { return 0 } 165 let q: nx_int = NX_FQ_Q 166 var sum_age: nx_int = 0 167 var i: nx_int = 0 168 while i < n { 169 sum_age = sum_age + trees[i * NX_FQ_TREE_STRIDE + NX_FQ_OFF_AGE] 170 i = i + 1 171 } 172 let mean_age: nx_int = sum_age / n 173 var dev_sum: nx_int = 0 174 var j: nx_int = 0 175 while j < n { 176 var d: nx_int = trees[j * NX_FQ_TREE_STRIDE + NX_FQ_OFF_AGE] - mean_age 177 if d < 0 { d = 0 - d } 178 dev_sum = dev_sum + d 179 j = j + 1 180 } 181 let mean_dev: nx_int = dev_sum / n 182 // Target: mean_dev = q / 4 for healthy spread. 183 let target: nx_int = q / 4 184 if target <= 0 { return 0 } 185 var score: nx_int = (mean_dev * q) / target 186 if score > q { score = q } 187 if score < 0 { score = 0 } 188 return score 189} 190 191// ===== Axis 3: edge presence ======================================= 192func _fq_edge_presence_q14( 193 trees: *i64, n: nx_int, cx: nx_int, cz: nx_int, region_r: nx_int 194) -> nx_int { 195 if n <= 0 { return 0 } 196 if region_r <= 0 { return 0 } 197 let q: nx_int = NX_FQ_Q 198 let inner_r: nx_int = (region_r * 85) / 100 199 let inner_sq: nx_int = inner_r * inner_r 200 let outer_sq: nx_int = region_r * region_r 201 var n_edge: nx_int = 0 202 var i: nx_int = 0 203 while i < n { 204 let tx: nx_int = trees[i * NX_FQ_TREE_STRIDE + NX_FQ_OFF_X] 205 let tz: nx_int = trees[i * NX_FQ_TREE_STRIDE + NX_FQ_OFF_Z] 206 let dx: nx_int = tx - cx 207 let dz: nx_int = tz - cz 208 let d_sq: nx_int = dx * dx + dz * dz 209 if d_sq >= inner_sq { 210 if d_sq <= outer_sq { 211 n_edge = n_edge + 1 212 } 213 } 214 i = i + 1 215 } 216 // Target: 15-35% of trees in edge band. 217 let target_lo: nx_int = (n * 15) / 100 218 let target_hi: nx_int = (n * 35) / 100 219 var score: nx_int = 0 220 if n_edge >= target_lo { 221 if n_edge <= target_hi { score = q } 222 if n_edge > target_hi { 223 score = q - ((n_edge - target_hi) * q) / n 224 } 225 } 226 if n_edge < target_lo { 227 if target_lo > 0 { score = (n_edge * q) / target_lo } 228 } 229 if score < 0 { score = 0 } 230 if score > q { score = q } 231 return score 232} 233 234// ===== Axis 4: canopy variance ===================================== 235func _fq_canopy_variance_q14(trees: *i64, n: nx_int) -> nx_int { 236 if n <= 1 { return 0 } 237 let q: nx_int = NX_FQ_Q 238 var sum_c: nx_int = 0 239 var i: nx_int = 0 240 while i < n { 241 sum_c = sum_c + trees[i * NX_FQ_TREE_STRIDE + NX_FQ_OFF_CANOPY] 242 i = i + 1 243 } 244 let mean_c: nx_int = sum_c / n 245 if mean_c <= 0 { return 0 } 246 var dev_sum: nx_int = 0 247 var j: nx_int = 0 248 while j < n { 249 var d: nx_int = trees[j * NX_FQ_TREE_STRIDE + NX_FQ_OFF_CANOPY] - mean_c 250 if d < 0 { d = 0 - d } 251 dev_sum = dev_sum + d 252 j = j + 1 253 } 254 let mean_dev: nx_int = dev_sum / n 255 // CV = mean_dev / mean_c. Target: 0.2 to 0.5. 256 let cv_q: nx_int = (mean_dev * q) / mean_c 257 let target_lo: nx_int = q / 5 // 0.2 258 let target_hi: nx_int = q / 2 // 0.5 259 var score: nx_int = 0 260 if cv_q >= target_lo { 261 if cv_q <= target_hi { score = q } 262 if cv_q > target_hi { score = q - (cv_q - target_hi) * q / target_hi } 263 } 264 if cv_q < target_lo { 265 if target_lo > 0 { score = (cv_q * q) / target_lo } 266 } 267 if score < 0 { score = 0 } 268 if score > q { score = q } 269 return score 270} 271 272// ===== Public: full forest grader ================================== 273func nx_forest_quality_grade( 274 trees: *i64, n_trees: nx_int, 275 cx_q14: nx_int, cz_q14: nx_int, region_r_q14: nx_int, 276 out_verdict: *i64 277) { 278 let species_div: nx_int = _fq_species_diversity_q14(trees, n_trees) 279 let spacing: nx_int = _fq_spacing_q14(trees, n_trees, region_r_q14) 280 let age_var: nx_int = _fq_age_variance_q14(trees, n_trees) 281 let edge: nx_int = _fq_edge_presence_q14(trees, n_trees, cx_q14, cz_q14, region_r_q14) 282 let canopy_var: nx_int = _fq_canopy_variance_q14(trees, n_trees) 283 284 nx_layer_verdict_init(out_verdict, NX_LAYER_KIND_FOREST, 285 NX_FQ_AXIS_COUNT, NX_LAYER_REFINE_MORE_VARIETY) 286 out_verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_SPECIES_DIV] = species_div 287 out_verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_SPACING_REG] = spacing 288 out_verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_AGE_VAR] = age_var 289 out_verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_EDGE_PRESENCE] = edge 290 out_verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_CANOPY_VAR] = canopy_var 291 nx_layer_verdict_finalize(out_verdict) 292} 293 294// ===== Self-test ==================================================== 295func main() -> i64 { 296 let q: nx_int = NX_FQ_Q 297 let verdict: *i64 = (sys_mmap(NX_LV_STRIDE * NX_SIZEOF_NX_INT)) as *i64 298 299 // Build a synthetic tree array: 24 trees with 4 distinct species, 300 // varied ages + canopies, spread across a region of radius 100q. 301 let n: nx_int = 24 302 let trees: *i64 = (sys_mmap(n * NX_FQ_TREE_STRIDE * NX_SIZEOF_NX_INT)) as *i64 303 var i: nx_int = 0 304 while i < n { 305 let base: nx_int = i * NX_FQ_TREE_STRIDE 306 trees[base + 0] = i % 4 // species cycling 0..3 307 trees[base + 1] = (i * 11) % 80 // x in [0, 80] q-units 308 trees[base + 2] = 100 // ground 309 trees[base + 3] = (i * 13) % 80 // z 310 trees[base + 4] = 10 // height 311 trees[base + 5] = 2 + (i % 5) // canopy varies 2..6 312 trees[base + 6] = 1 // trunk 313 trees[base + 7] = (i * q) / n // age varies 0..Q 314 i = i + 1 315 } 316 nx_forest_quality_grade(trees, n, 0, 0, 100 * q, verdict) 317 // Species diversity should be high (4 distinct). 318 if verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_SPECIES_DIV] < q * 8 / 10 { 319 return __syscall(93, 1, 0, 0, 0, 0, 0) 320 } 321 // Age variance should be substantial. 322 if verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_AGE_VAR] <= 0 { 323 return __syscall(93, 2, 0, 0, 0, 0, 0) 324 } 325 // Canopy variance: canopies 2..6 around mean 4 -> CV = 1/4=0.25. 326 // In our target band (0.2-0.5) -> should be high. 327 if verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_CANOPY_VAR] < q * 8 / 10 { 328 return __syscall(93, 3, 0, 0, 0, 0, 0) 329 } 330 331 // T2: All-one-species forest -> diversity = 0. 332 var j: nx_int = 0 333 while j < n { 334 trees[j * NX_FQ_TREE_STRIDE + NX_FQ_OFF_SPECIES] = 0 335 j = j + 1 336 } 337 nx_forest_quality_grade(trees, n, 0, 0, 100 * q, verdict) 338 if verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_SPECIES_DIV] != 0 { 339 return __syscall(93, 10, 0, 0, 0, 0, 0) 340 } 341 342 // T3: All-same-age -> age variance = 0. 343 var k: nx_int = 0 344 while k < n { 345 trees[k * NX_FQ_TREE_STRIDE + NX_FQ_OFF_AGE] = q / 2 346 k = k + 1 347 } 348 nx_forest_quality_grade(trees, n, 0, 0, 100 * q, verdict) 349 if verdict[NX_LV_OFF_AXIS_0 + NX_FQ_AXIS_AGE_VAR] != 0 { 350 return __syscall(93, 20, 0, 0, 0, 0, 0) 351 } 352 353 // T4: Verdict is well-formed. 354 if nx_lv_grade_is_valid(verdict[NX_LV_OFF_GRADE]) != 1 { 355 return __syscall(93, 30, 0, 0, 0, 0, 0) 356 } 357 if verdict[NX_LV_OFF_KIND] != NX_LAYER_KIND_FOREST { 358 return __syscall(93, 31, 0, 0, 0, 0, 0) 359 } 360 361 return 0 362}