code wiki / (root) / nx_biome_classifier.nx

nx_biome_classifier.nx source

↩ module page · 336 lines · 14512 B

1// nx_biome_classifier.nx -- Whittaker / Holdridge biome classifier. 2// 3// Cross-cutting Substrate C primitive per 4// nxc2/docs/NISHI_GAME_ENGINE_ROADMAP.md. The foundational LOGIC 5// layer for guided / god procgen -- where pure noise samples 6// uniformly, this primitive routes climate + elevation through a 7// biome decision table so downstream generators (vegetation density, 8// species spawn, settlement growth) get to place features WHERE 9// THEY MAKE SENSE. 10// 11// "Forest near a river, desert where it's dry" -- this is the 12// primitive that says so. 13// 14// Inputs (all Q14): 15// temperature_c_q14: mean annual temperature in degrees C 16// rainfall_mm_q14: mean annual rainfall in mm 17// elevation_m_q14: elevation above sea level in metres 18// 19// Output: sealed-enum biome kind (13 values). Use the 20// classifier-driven downstream primitive `nx_growth_field` (queued) 21// to spawn the actual vegetation / ores / settlements per biome. 22// 23// Classification scheme: Whittaker 1975 simplified 2D (temp x rain) 24// + Holdridge altitudinal adjustment. Reasonable cuts at standard 25// climate boundaries (frost line at 0 C; treeline at ~3000 m; 26// tropical/temperate cutoff at 18 C). Per cardinal 27// `feedback-no-third-party-trust-native-or-nothing`, classification 28// thresholds are derived from public-domain climate science, never 29// copied from a proprietary engine. 30// 31// Loss audit: classification is BAND-DECISION; no continuous 32// information lost beyond Q14-quantised inputs. Downstream 33// generators read the sealed-enum verdict and apply their own 34// continuous biases (e.g. forest density tapers within the 35// TEMPERATE_FOREST band toward the band's edges). 36// 37// genealogy_id: whittaker_1975_community_ecology + holdridge_1947_life_zones 38// lineage_id: nx_biome_classifier_whittaker_holdridge_2d_q14 39// 40// nx_safety_envelope: 41// intended_use: "2D climate -> biome classifier (Whittaker 42// 1975 / Holdridge 1947). Substrate procgen 43// Substrate-C cross-cutting verdict that 44// per-kind generators consult for density" 45// sil_target: SIL1 46// asil_target: QM 47// dal_target: NONE 48// evidence: [Whittaker_1975_canonical_basis, 49// Holdridge_1947_canonical_basis, 50// sealed_13_biome_enum_complete, 51// Q14_fixed_point_deterministic] 52// hazard_register: [bug-tape-biome-discontinuity-at-boundary, 53// bug-tape-input-out-of-canonical-range] 54// residual_risk: "Temperature + precipitation are caller- 55// supplied; substrate clamps to Whittaker 56// 2D ranges before classification. Edge- 57// smoothing at biome boundaries is queued 58// per cardinal feedback-kind-specific- 59// generators-not-broad-noise." 60// verdict: NOT_YET_EVALUATED 61 62import "nx_syscalls.nx" 63import "nx_tier.nx" 64const NX_MAGIC_2048: i64 = 2048 65const NX_MAGIC_6144: i64 = 6144 66const NX_MAGIC_1024: i64 = 1024 67const NX_MAGIC_5120: i64 = 5120 68const NX_MAGIC_10240: i64 = 10240 69const NX_MAGIC_12288: i64 = 12288 70const NX_MAGIC_3072: i64 = 3072 71const NX_MAGIC_13312: i64 = 13312 72const NX_MAGIC_16384: i64 = 16384 73const NX_MAGIC_1200: i64 = 1200 74const NX_MAGIC_2500: i64 = 2500 75const NX_MAGIC_1800: i64 = 1800 76const NX_MAGIC_3000: i64 = 3000 77const NX_MAGIC_3500: i64 = 3500 78const NX_MAGIC_1500: i64 = 1500 79 80// ===== Q14 constants ================================================ 81const NX_BIOME_Q: nx_int = 16384 82 83// Thresholds named (no magic numbers per cardinal rule 11). Q14 84// values: 1 degree C = 16384, 1 mm = 16384, 1 metre = 16384. 85const NX_BIOME_T_ARCTIC: nx_int = -163840 // -10 C (Q14) 86const NX_BIOME_T_FREEZE: nx_int = 0 // 0 C 87const NX_BIOME_T_BOREAL_TOP: nx_int = 81920 // 5 C 88const NX_BIOME_T_TROPICAL: nx_int = 294912 // 18 C 89const NX_BIOME_T_SAVANNA_MIN: nx_int = 327680 // 20 C 90 91const NX_BIOME_P_HYPER_ARID: nx_int = 1638400 // 100 mm 92const NX_BIOME_P_SEMI_ARID: nx_int = 4096000 // 250 mm 93const NX_BIOME_P_DRY: nx_int = 6553600 // 400 mm 94const NX_BIOME_P_MODERATE: nx_int = 9830400 // 600 mm 95const NX_BIOME_P_WET: nx_int = 24576000 // 1500 mm 96const NX_BIOME_P_VERY_WET: nx_int = 32768000 // 2000 mm 97const NX_BIOME_P_EXTREME_WET: nx_int = 40960000 // 2500 mm 98 99const NX_BIOME_E_ALPINE: nx_int = 49152000 // 3000 m (treeline) 100const NX_BIOME_E_HIGH_MOUNTAIN: nx_int = 65536000 // 4000 m 101 102// ===== Sealed-enum biome kinds (13 values) ========================= 103// IDs ordered from cold/dry to warm/wet so downstream code can 104// approximate "more lush" by sorting on id. Refinement: add a Q14 105// "lushness" score primitive on top of this if needed. 106const NX_BIOME_ARCTIC_ICE: nx_int = 0 107const NX_BIOME_TUNDRA: nx_int = 1 108const NX_BIOME_TAIGA: nx_int = 2 109const NX_BIOME_COLD_DESERT: nx_int = 3 110const NX_BIOME_TEMPERATE_GRASSLAND: nx_int = 4 111const NX_BIOME_TEMPERATE_FOREST: nx_int = 5 112const NX_BIOME_TEMPERATE_RAINFOREST: nx_int = 6 113const NX_BIOME_CHAPARRAL: nx_int = 7 114const NX_BIOME_DESERT: nx_int = 8 115const NX_BIOME_SAVANNA: nx_int = 9 116const NX_BIOME_TROPICAL_FOREST: nx_int = 10 117const NX_BIOME_TROPICAL_RAINFOREST: nx_int = 11 118const NX_BIOME_ALPINE: nx_int = 12 119 120const NX_BIOME_COUNT: nx_int = 13 121 122// ===== Validity predicate ========================================== 123func nx_biome_is_valid(b: nx_int) -> nx_int { 124 if b == NX_BIOME_ARCTIC_ICE { return 1 } 125 if b == NX_BIOME_TUNDRA { return 1 } 126 if b == NX_BIOME_TAIGA { return 1 } 127 if b == NX_BIOME_COLD_DESERT { return 1 } 128 if b == NX_BIOME_TEMPERATE_GRASSLAND { return 1 } 129 if b == NX_BIOME_TEMPERATE_FOREST { return 1 } 130 if b == NX_BIOME_TEMPERATE_RAINFOREST { return 1 } 131 if b == NX_BIOME_CHAPARRAL { return 1 } 132 if b == NX_BIOME_DESERT { return 1 } 133 if b == NX_BIOME_SAVANNA { return 1 } 134 if b == NX_BIOME_TROPICAL_FOREST { return 1 } 135 if b == NX_BIOME_TROPICAL_RAINFOREST { return 1 } 136 if b == NX_BIOME_ALPINE { return 1 } 137 return 0 138} 139 140// ===== Biome lushness ============================================= 141// Q14 score in [0, 16384] -- 0 is barren (arctic ice / desert), 16384 142// is maximally lush (tropical rainforest). Downstream generators 143// use this to size their feature density. 144func nx_biome_lushness_q14(b: nx_int) -> nx_int { 145 if b == NX_BIOME_ARCTIC_ICE { return 0 } 146 if b == NX_BIOME_TUNDRA { return NX_MAGIC_2048 } 147 if b == NX_BIOME_TAIGA { return NX_MAGIC_6144 } 148 if b == NX_BIOME_COLD_DESERT { return NX_MAGIC_1024 } 149 if b == NX_BIOME_TEMPERATE_GRASSLAND { return NX_MAGIC_5120 } 150 if b == NX_BIOME_TEMPERATE_FOREST { return NX_MAGIC_10240 } 151 if b == NX_BIOME_TEMPERATE_RAINFOREST { return NX_MAGIC_12288 } 152 if b == NX_BIOME_CHAPARRAL { return NX_MAGIC_3072 } 153 if b == NX_BIOME_DESERT { return 512 } 154 if b == NX_BIOME_SAVANNA { return NX_MAGIC_6144 } 155 if b == NX_BIOME_TROPICAL_FOREST { return NX_MAGIC_13312 } 156 if b == NX_BIOME_TROPICAL_RAINFOREST { return NX_MAGIC_16384 } 157 if b == NX_BIOME_ALPINE { return NX_MAGIC_3072 } 158 return 0 159} 160 161// ===== Main classifier ============================================== 162// Returns one of 13 NX_BIOME_* sealed-enum values per 163// (temperature, rainfall, elevation). Decision tree applies in 164// order: elevation override -> arctic -> tundra -> boreal/cold-desert 165// -> temperate (forest type by rainfall) -> tropical (forest type by 166// rainfall). No interpolation -- this is a band classifier. 167// 168// Threshold rationale: 169// - Arctic ice below -10 C (mean annual): permafrost too deep for soil. 170// - Tundra -10..0 C: short growing season but life present. 171// - Treeline at ~3000 m elevation regardless of temp; ALPINE. 172// - Boreal/taiga 0..5 C with enough moisture; otherwise cold desert. 173// - Temperate 5..18 C: forest type by rainfall. 174// - Tropical > 18 C: rainforest if very wet, dry-forest -> savanna -> 175// desert as rainfall decreases. 176func nx_biome_classify_q14( 177 temperature_c_q14: nx_int, 178 rainfall_mm_q14: nx_int, 179 elevation_m_q14: nx_int 180) -> nx_int { 181 // Alpine elevation override -- above treeline only if not arctic. 182 if elevation_m_q14 >= NX_BIOME_E_ALPINE { 183 if temperature_c_q14 >= NX_BIOME_T_ARCTIC { 184 return NX_BIOME_ALPINE 185 } 186 } 187 188 // Arctic / polar 189 if temperature_c_q14 < NX_BIOME_T_ARCTIC { 190 return NX_BIOME_ARCTIC_ICE 191 } 192 if temperature_c_q14 < NX_BIOME_T_FREEZE { 193 return NX_BIOME_TUNDRA 194 } 195 196 // Boreal band (0 .. 5 C). 197 if temperature_c_q14 < NX_BIOME_T_BOREAL_TOP { 198 if rainfall_mm_q14 >= NX_BIOME_P_HYPER_ARID { 199 return NX_BIOME_TAIGA 200 } 201 return NX_BIOME_COLD_DESERT 202 } 203 204 // Temperate band (5 .. 18 C). 205 if temperature_c_q14 < NX_BIOME_T_TROPICAL { 206 if rainfall_mm_q14 >= NX_BIOME_P_VERY_WET { 207 return NX_BIOME_TEMPERATE_RAINFOREST 208 } 209 if rainfall_mm_q14 >= NX_BIOME_P_MODERATE { 210 return NX_BIOME_TEMPERATE_FOREST 211 } 212 if rainfall_mm_q14 >= NX_BIOME_P_SEMI_ARID { 213 return NX_BIOME_TEMPERATE_GRASSLAND 214 } 215 return NX_BIOME_CHAPARRAL 216 } 217 218 // Tropical band (>= 18 C). 219 if rainfall_mm_q14 >= NX_BIOME_P_EXTREME_WET { 220 return NX_BIOME_TROPICAL_RAINFOREST 221 } 222 if rainfall_mm_q14 >= NX_BIOME_P_WET { 223 return NX_BIOME_TROPICAL_FOREST 224 } 225 if rainfall_mm_q14 >= NX_BIOME_P_SEMI_ARID { 226 return NX_BIOME_SAVANNA 227 } 228 return NX_BIOME_DESERT 229} 230 231// ===== Self-test ==================================================== 232func main() -> i64 { 233 // T1: Arctic at -20 C, anything for rain/elev. 234 let q: nx_int = NX_BIOME_Q 235 if nx_biome_classify_q14(0 - 20 * q, 500 * q, 0) != NX_BIOME_ARCTIC_ICE { 236 return __syscall(93, 1, 0, 0, 0, 0, 0) 237 } 238 239 // T2: Tundra at -5 C. 240 if nx_biome_classify_q14(0 - 5 * q, 500 * q, 0) != NX_BIOME_TUNDRA { 241 return __syscall(93, 2, 0, 0, 0, 0, 0) 242 } 243 244 // T3: Taiga at 3 C with moderate rain. 245 if nx_biome_classify_q14(3 * q, 500 * q, 0) != NX_BIOME_TAIGA { 246 return __syscall(93, 3, 0, 0, 0, 0, 0) 247 } 248 249 // T4: Cold desert at 3 C with very little rain. 250 if nx_biome_classify_q14(3 * q, 50 * q, 0) != NX_BIOME_COLD_DESERT { 251 return __syscall(93, 4, 0, 0, 0, 0, 0) 252 } 253 254 // T5: Temperate forest at 12 C, 1200 mm. 255 if nx_biome_classify_q14(12 * q, NX_MAGIC_1200 * q, 100 * q) != NX_BIOME_TEMPERATE_FOREST { 256 return __syscall(93, 5, 0, 0, 0, 0, 0) 257 } 258 259 // T6: Temperate rainforest at 12 C, 2500 mm. 260 if nx_biome_classify_q14(12 * q, NX_MAGIC_2500 * q, 100 * q) != NX_BIOME_TEMPERATE_RAINFOREST { 261 return __syscall(93, 6, 0, 0, 0, 0, 0) 262 } 263 264 // T7: Temperate grassland at 15 C, 350 mm. 265 if nx_biome_classify_q14(15 * q, 350 * q, 200 * q) != NX_BIOME_TEMPERATE_GRASSLAND { 266 return __syscall(93, 7, 0, 0, 0, 0, 0) 267 } 268 269 // T8: Chaparral at 16 C, 150 mm. 270 if nx_biome_classify_q14(16 * q, 150 * q, 200 * q) != NX_BIOME_CHAPARRAL { 271 return __syscall(93, 8, 0, 0, 0, 0, 0) 272 } 273 274 // T9: Savanna at 25 C, 800 mm. 275 if nx_biome_classify_q14(25 * q, 800 * q, 100 * q) != NX_BIOME_SAVANNA { 276 return __syscall(93, 9, 0, 0, 0, 0, 0) 277 } 278 279 // T10: Tropical forest at 24 C, 1800 mm. 280 if nx_biome_classify_q14(24 * q, NX_MAGIC_1800 * q, 100 * q) != NX_BIOME_TROPICAL_FOREST { 281 return __syscall(93, 10, 0, 0, 0, 0, 0) 282 } 283 284 // T11: Tropical rainforest at 27 C, 3000 mm. 285 if nx_biome_classify_q14(27 * q, NX_MAGIC_3000 * q, 50 * q) != NX_BIOME_TROPICAL_RAINFOREST { 286 return __syscall(93, 11, 0, 0, 0, 0, 0) 287 } 288 289 // T12: Desert at 35 C, 50 mm. 290 if nx_biome_classify_q14(35 * q, 50 * q, 200 * q) != NX_BIOME_DESERT { 291 return __syscall(93, 12, 0, 0, 0, 0, 0) 292 } 293 294 // T13: Alpine override -- 25 C real temp at 3500 m elevation 295 // (anomalous but the cutoff is structural). The classifier MUST 296 // return ALPINE when above treeline regardless of warmer mean. 297 if nx_biome_classify_q14(25 * q, 1000 * q, NX_MAGIC_3500 * q) != NX_BIOME_ALPINE { 298 return __syscall(93, 13, 0, 0, 0, 0, 0) 299 } 300 // But arctic still wins over alpine: -15 C at 3500 m is ICE. 301 if nx_biome_classify_q14(0 - 15 * q, 100 * q, NX_MAGIC_3500 * q) != NX_BIOME_ARCTIC_ICE { 302 return __syscall(93, 14, 0, 0, 0, 0, 0) 303 } 304 305 // T14: Causal example -- the user's "forest near a river" case. 306 // Two locations, same Z but different rainfall (river-driven). 307 // 12 C, 200 mm (away from river) -> CHAPARRAL. 308 // 12 C, 1500 mm (river floodplain) -> TEMPERATE_FOREST. 309 let away: nx_int = nx_biome_classify_q14(12 * q, 200 * q, 100 * q) 310 let near: nx_int = nx_biome_classify_q14(12 * q, NX_MAGIC_1500 * q, 100 * q) 311 if away == near { return __syscall(93, 30, 0, 0, 0, 0, 0) } 312 if away != NX_BIOME_CHAPARRAL { return __syscall(93, 31, 0, 0, 0, 0, 0) } 313 if near != NX_BIOME_TEMPERATE_FOREST { return __syscall(93, 32, 0, 0, 0, 0, 0) } 314 // Lushness ordering: forest > chaparral. 315 if nx_biome_lushness_q14(near) <= nx_biome_lushness_q14(away) { 316 return __syscall(93, 33, 0, 0, 0, 0, 0) 317 } 318 319 // T15: Validity predicate. 320 if nx_biome_is_valid(NX_BIOME_TUNDRA) != 1 { return __syscall(93, 40, 0, 0, 0, 0, 0) } 321 if nx_biome_is_valid(NX_BIOME_TROPICAL_RAINFOREST) != 1 { return __syscall(93, 41, 0, 0, 0, 0, 0) } 322 if nx_biome_is_valid(0 - 1) != 0 { return __syscall(93, 42, 0, 0, 0, 0, 0) } 323 if nx_biome_is_valid(13) != 0 { return __syscall(93, 43, 0, 0, 0, 0, 0) } 324 if nx_biome_is_valid(99) != 0 { return __syscall(93, 44, 0, 0, 0, 0, 0) } 325 326 // T16: Lushness for every biome falls in [0, Q]. 327 var bb: nx_int = 0 328 while bb < NX_BIOME_COUNT { 329 let v: nx_int = nx_biome_lushness_q14(bb) 330 if v < 0 { return __syscall(93, 50, 0, 0, 0, 0, 0) } 331 if v > NX_BIOME_Q { return __syscall(93, 51, 0, 0, 0, 0, 0) } 332 bb = bb + 1 333 } 334 335 return 0 336}