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}