diff --git a/server/src/atlas/composition.rs b/server/src/atlas/composition.rs new file mode 100644 index 000000000..83bcc5a7a --- /dev/null +++ b/server/src/atlas/composition.rs @@ -0,0 +1,412 @@ +//! Sub-cell **composition** — the fine tier that lets a summarised cell contain +//! the minority it suppressed (T-1213, [D-258](../../governance) invariant 2). +//! +//! # Why a third tier exists +//! +//! [`crate::atlas::vegetation_invention`] already perturbs `moisture_q` with two +//! bands: a massif field (100–410 km, never gated) and a texture field +//! (32 km–128 m, gated). That pair was built for **cross-rung coherence** and is +//! explicitly weighted so it cannot change a verdict — 70% massif against 30% +//! texture, so "the texture term alone can never outweigh the massif term". +//! +//! D-258 asks for the opposite thing. Its invariant (2) says descending the +//! ladder must reveal COMPOSITION: *a cell reading "forest" globally must be +//! able to contain clearings, marsh, rock and scrub the vote suppressed*. That +//! requires the fine tier to flip a classification, which the existing pair is +//! designed to prevent. +//! +//! The two are reconciled by D-258's own third invariant rather than by picking +//! a winner: **flips are allowed, and conservation is the bound.** Downsampling +//! the composed field must reproduce the summary it came from. A forest cell may +//! gain marsh pockets; it must still read as forest from orbit. +//! +//! # Why the existing tiers could not simply be turned up +//! +//! Measured on Ferrath, the texture field's amplitude is dominated by its +//! coarsest octaves — 50.2% sits in the 32,768 m octave alone, and everything at +//! or below 2,048 m accounts for **5.9%** of the total. Across a District window +//! (2,048 m of ground) only that 5.9% varies at all, which after the 30% texture +//! weight and a ~29-point ceiling swings `moisture_q` by **±0.51 points**. +//! Vegetation thresholds are 5–15 points apart, so nothing can ever cross one. +//! That is not a tuning shortfall, it is the geometric series: raising the +//! ceiling enough to matter at District would make the field violent at Region. +//! +//! So this tier carries the fine band ALONE, normalized to its own full swing. +//! It is quiet where the coarse tiers are loud and loud where they have nothing +//! left to say. +//! +//! # What it feeds +//! +//! Both classification inputs that gate everything downstream: +//! +//! - `moisture_q` → vegetation (clearings, marsh, scrub inside a forest) +//! - `slope_q` → morphology (CliffCoast, Fjord, DuneStrand, Delta) +//! +//! The second is not incidental. Measured down the ladder on Ferrath, morphology +//! carries **nine** zones at Global and exactly **one** (AlluvialPlain) at every +//! rung below it: every zone that gates on a slope threshold becomes unreachable, +//! so fjord walls and cliff coasts exist only on the whole-body view. Vegetation +//! and coastal character flatten for the same reason, one field apart. + +use crate::atlas::detail_scatter::{value_noise, VOXEL_OCTAVE_WAVELENGTHS_M}; +use crate::seed::splitmix64; + +/// Salt separating this tier's noise stream from the massif and texture fields +/// sampled at the same position. Without it the three would correlate and the +/// composition would merely deepen the patches the texture field already made, +/// instead of cutting across them. +const COMPOSITION_SALT: u64 = 0x0C0F_9051_71ED_5EED; + +/// Separate stream again for the rare-inclusion test, so a glade does not +/// preferentially land where the smooth field already peaks. +const INCLUSION_SALT: u64 = 0x1_C1EA_5124_60DE; + +/// Size of a rare inclusion — a glade, a blowdown, a rocky outcrop. Chosen to +/// read as a FEATURE at the rungs that can see it: 192 m is roughly 50 gridunits +/// across at District and half a gridunit at Region, so a clearing is a shape +/// when you are close and correctly invisible from orbit. +const INCLUSION_WAVELENGTH_M: f64 = 192.0; + +/// Noise value above which a position is inside an inclusion. `value_noise` +/// returns `[-1, 1]`, and 0.72 puts roughly 4–6% of ground inside one — sparse +/// enough that the majority class is never in danger (the conservation +/// invariant is the real bound), common enough that a walk through deep forest +/// crosses one. +const INCLUSION_THRESHOLD: f64 = 0.72; + +/// How hard an inclusion pushes `moisture_q`. Deliberately LARGE — larger than +/// [`MOISTURE_COMPOSITION_CEILING_Q`] — because its whole job is to cross a +/// class gate from well INSIDE a class, which the smooth term by design cannot. +const INCLUSION_MOISTURE_SWING_Q: i32 = 30; + +/// How hard an inclusion pushes `slope_q` — a rocky outcrop is genuinely steep +/// ground, so this one is allowed to reach where the smooth slope term is not. +const INCLUSION_SLOPE_SWING_Q: i32 = 25; + +/// Maximum swing this tier may apply to `moisture_q`, in points. +/// +/// Sized against the vegetation ladder's own spacing (`derive_vegetation`'s +/// gates sit 5–15 points apart): large enough that ground sitting NEAR a +/// threshold crosses it in places, small enough that ground sitting well inside +/// a class never leaves it. That asymmetry is what makes the invention +/// conservative — a marginal forest grows clearings, a deep one does not. +pub const MOISTURE_COMPOSITION_CEILING_Q: i32 = 12; + +/// Maximum swing this tier may apply to `slope_q`, in points. +/// +/// SMALL, and deliberately smaller than the moisture ceiling — the opposite of +/// the first attempt, which set it to 15 on the reasoning that the morphology +/// gates it feeds are far apart (Fjord at 40, CliffCoast at 55). That reasoning +/// was backwards. Measured against the window-derivation golden's fixtures, +/// base `slope_q` on ordinary ground is 2–6, so a ±15 swing does not VARY the +/// signal, it REPLACES it: one fixture moved 6 → 19, and two coastal samples +/// flipped to Wetland on the strength of invented slope alone. +/// +/// The gates are far apart for a reason: a cliff coast is a real landform, not +/// a dice roll. Composition must let ground that genuinely sits near a +/// threshold fall on both sides of it — never manufacture a fjord on a flood +/// plain. Where the terrain is flat, flat is the honest answer, and the +/// morphology variety visible at Global comes from places that actually have +/// slope. +pub const SLOPE_COMPOSITION_CEILING_Q: i32 = 6; + +/// The fine-band composition field at a world position, in `[-1, 1]`. +/// +/// Only [`VOXEL_OCTAVE_WAVELENGTHS_M`] (1,024–128 m) — the band that actually +/// varies inside a District window — normalized across its own octaves so the +/// tier uses its full swing rather than the 5.9% tail it holds inside the +/// texture field's series. +/// +/// `min_wavelength_m` gates octaves finer than the caller's sample density, the +/// same discipline every other invented field follows; `0.0` = no cutoff. +fn composition_field(seed: u64, wx: f64, wy: f64, min_wavelength_m: f64) -> f64 { + let seed = splitmix64(seed ^ COMPOSITION_SALT); + let mut sum = 0.0; + let mut amp = 1.0; + let mut norm = 0.0; + for (i, &wl) in VOXEL_OCTAVE_WAVELENGTHS_M.iter().enumerate() { + if wl < min_wavelength_m { + amp *= 0.5; + continue; + } + sum += value_noise( + seed.wrapping_add((i as u64).wrapping_mul(0x1000)), + wx, + wy, + wl, + ) * amp; + norm += amp; + amp *= 0.5; + } + if norm <= 0.0 { + return 0.0; + } + (sum / norm).clamp(-1.0, 1.0) +} + +/// Composition offsets for one position: `(moisture_q, slope_q)`, in points. +/// +/// **Zero-mean by construction**, which is what satisfies D-258's conservation +/// invariant: `value_noise` is symmetric about zero, so averaging the offsets +/// over any area large enough to contain whole features returns to the +/// unperturbed summary. Composition adds variety within a cell without moving +/// what the cell reads as from orbit — the property +/// `composition_conserves_the_summary` pins on real terrain. +/// +/// The two offsets are drawn from the SAME field rather than two independent +/// ones, and deliberately: on real ground steep places drain, so slope and +/// moisture are correlated, and a rocky outcrop should tend to be the drier +/// patch rather than an unrelated one. The moisture offset is negated for that +/// reason — where this tier lifts slope it drops moisture. +pub fn composition_offsets( + seed: u64, + wx: f64, + wy: f64, + min_wavelength_m: f64, + moisture_ceiling_q: i32, +) -> (i32, i32) { + let f = composition_field(seed, wx, wy, min_wavelength_m); + // Ceiling is caller-supplied for moisture so a bone-dry or airless world, + // whose vegetation envelope is already zero, invents no damp pockets — the + // same envelope rule `vegetation_invention` applies one tier up. + let mut moisture = ((-f * moisture_ceiling_q as f64).round() as i32) + .clamp(-moisture_ceiling_q, moisture_ceiling_q); + let mut slope = ((f * SLOPE_COMPOSITION_CEILING_Q as f64).round() as i32) + .clamp(-SLOPE_COMPOSITION_CEILING_Q, SLOPE_COMPOSITION_CEILING_Q); + + // RARE INCLUSIONS — the clearing in the deep wood, the rocky outcrop on the + // plain (Jeroen, 2026-08-08: "maybe a dense forest should still sometimes + // produce a clearing or a rocky outcropping"). + // + // The smooth offsets above are a gentle sway around the base value, so they + // can only change a verdict where the ground already sits near a gate. That + // made deep-in-class ground immune, and the conservation test duly measured + // a District patch that was 100% Forest — a monoculture, which is the flat + // map this ticket exists to fix, one scale down. + // + // D-258 says a cell reading forest must be able to CONTAIN clearings, marsh, + // rock and scrub. Contain, not "border on". So a sparse, high-contrast term + // rides on top: rare enough that the majority is never threatened (the + // conservation invariant is what bounds it), strong enough to cross a gate + // from well inside a class. + if is_inclusion(seed, wx, wy) { + let sign = if f >= 0.0 { 1 } else { -1 }; + // The moisture half obeys the envelope rule: a world whose patchiness + // ceiling is zero (airless, bone-dry) grows no glades, because it has no + // vegetation to clear. Caught by `zero_moisture_ceiling_invents_no_moisture`, + // which the first version of this failed — the inclusion was applied + // unconditionally and put damp pockets on airless rock. + if moisture_ceiling_q > 0 { + moisture = (moisture - sign * INCLUSION_MOISTURE_SWING_Q).clamp(-100, 100); + } + // The slope half is NOT gated. An outcrop is geology, not biology — a + // dead world is exactly where bare rock should break the surface. + slope = (slope + sign * INCLUSION_SLOPE_SWING_Q).clamp(-100, 100); + } + (moisture, slope) +} + +/// Does this position fall inside a rare inclusion — a clearing, a blowdown, a +/// rocky outcrop? +/// +/// Thresholded value noise rather than a per-cell dice roll, so inclusions come +/// out as connected BLOBS a few hundred metres across. A per-cell test would +/// scatter single stray cells through the forest, which reads as speckle +/// (exactly the failure the client-side stipple's first attempt had) rather +/// than as a glade. +fn is_inclusion(seed: u64, wx: f64, wy: f64) -> bool { + let s = splitmix64(seed ^ INCLUSION_SALT); + value_noise(s, wx, wy, INCLUSION_WAVELENGTH_M) > INCLUSION_THRESHOLD +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn field_is_deterministic() { + let a = composition_field(42, 1_000.0, 2_000.0, 0.0); + let b = composition_field(42, 1_000.0, 2_000.0, 0.0); + assert_eq!(a, b, "same seed and position must give the same field"); + } + + #[test] + fn field_stays_in_unit_range() { + for i in 0..500 { + let v = composition_field(7, i as f64 * 37.0, i as f64 * 91.0, 0.0); + assert!((-1.0..=1.0).contains(&v), "field out of range: {v}"); + } + } + + /// The whole reason this tier exists: it must VARY across a District window + /// (2,048 m), where the texture field it supplements is effectively + /// constant. + #[test] + fn field_varies_across_a_district_window() { + let mut lo = f64::MAX; + let mut hi = f64::MIN; + for i in 0..64 { + let wx = i as f64 * 32.0; // 64 samples across 2,048 m + let v = composition_field(11, wx, 0.0, 0.0); + lo = lo.min(v); + hi = hi.max(v); + } + assert!( + hi - lo > 0.5, + "swing across a District window was only {:.3} — this tier exists \ + precisely because the coarse bands are flat at this scale", + hi - lo + ); + } + + /// D-258 invariant 3, at the field level: the offsets average back to + /// nothing, so a downsample reproduces the summary they were added to. + #[test] + fn offsets_are_zero_mean_over_area() { + let mut m_sum = 0i64; + let mut s_sum = 0i64; + let n = 4_000; + for i in 0..n { + let wx = (i % 64) as f64 * 128.0; + let wy = (i / 64) as f64 * 128.0; + let (m, s) = composition_offsets(99, wx, wy, 0.0, MOISTURE_COMPOSITION_CEILING_Q); + m_sum += m as i64; + s_sum += s as i64; + } + let m_mean = m_sum as f64 / n as f64; + let s_mean = s_sum as f64 / n as f64; + assert!( + m_mean.abs() < 1.0, + "moisture offset mean {m_mean:.3} — a biased tier would shift the \ + summary it is supposed to preserve" + ); + assert!( + s_mean.abs() < 1.5, + "slope offset mean {s_mean:.3} is biased" + ); + } + + /// Slope and moisture must move OPPOSITE ways: steep drains. + #[test] + fn steep_places_are_the_drier_places() { + let mut checked = 0; + for i in 0..200 { + let wx = i as f64 * 71.0; + let (m, s) = composition_offsets(5, wx, 0.0, 0.0, MOISTURE_COMPOSITION_CEILING_Q); + if s.abs() >= 3 && m.abs() >= 3 { + assert!( + (s > 0) != (m > 0), + "slope {s} and moisture {m} moved the same way — a rocky \ + outcrop should be the drier patch, not a wetter one" + ); + checked += 1; + } + } + assert!(checked > 10, "only {checked} samples exercised the check"); + } + + /// A zero ceiling invents nothing — the envelope rule, so a bone-dry world + /// grows no damp pockets. + #[test] + fn zero_moisture_ceiling_invents_no_moisture() { + for i in 0..100 { + let (m, _) = composition_offsets(3, i as f64 * 55.0, 0.0, 0.0, 0); + assert_eq!(m, 0); + } + } + + /// Gating above the whole band silences the tier rather than aliasing it. + #[test] + fn cutoff_above_the_band_yields_no_offset() { + let v = composition_field(42, 500.0, 500.0, 4_096.0); + assert_eq!(v, 0.0, "every octave truncated must give exactly 0.0"); + } + + /// **D-258 invariant 3 — the binding acceptance gate, on real terrain.** + /// + /// "Downsampling rung 0.5 must reproduce the rung-0 summary it came from: a + /// forest cell may gain marsh pockets but must still read as forest from + /// orbit." The zero-mean test above proves the FIELD averages out; this + /// proves the consequence that actually matters — that the CLASSIFICATION a + /// patch of ground reads as does not move when composition is applied and + /// then averaged back. + /// + /// Runs the real derive over a District-sized patch twice, with and without + /// composition, and compares the MAJORITY vegetation verdict. Composition + /// may recolour a minority of cells — that is its entire purpose — but the + /// plurality must survive, or the map would disagree with the world it + /// zoomed out from. + #[test] + #[ignore = "loads a real body's cascade; run explicitly"] + fn composition_conserves_the_summary_on_real_terrain() { + use std::collections::BTreeMap; + + let body = "GJ820Bc"; + let Ok((snapshot, params)) = + crate::atlas::believability::cascade_snapshot_for_body(42, body) + else { + eprintln!("skip: {body} not loadable"); + return; + }; + let (_l1, ta) = crate::atlas::layer1::run_layer1_with_moisture( + &snapshot.heightmap, + crate::atlas::district_profile::derive_moisture_ceiling_q(¶ms), + ); + let climate = crate::atlas::district_profile::ClimateConstants::default(); + let seed = crate::seed::SeedChain::root(42).derive(crate::seed::SeedDomain::Body, 1); + + // A District-sized patch (2,048 m) at the descent ladder's own anchor, + // sampled at ~16 m so the fine band is fully resolved. + let (cx, cy) = (29_422_009.0f64, -5_675_959.0f64); + let mut tally: BTreeMap = BTreeMap::new(); + for iy in 0..128 { + for ix in 0..128 { + let wx = cx + (ix as f64 - 64.0) * 16.0; + let wy = cy + (iy as f64 - 64.0) * 16.0; + let prof = crate::atlas::district_profile::derive_at_metres( + seed, + body, + ¶ms, + &ta, + wx, + wy, + &climate, + 0.0, + &[], + ); + *tally.entry(prof.vegetation_class as u8).or_insert(0) += 1; + } + } + let total: u32 = tally.values().sum(); + let (majority, count) = tally + .iter() + .max_by_key(|(_, c)| **c) + .map(|(k, c)| (*k, *c)) + .expect("non-empty tally"); + + // The plurality must still BE the plurality after composition — a + // downsample of this patch returns the class it started as. + let share = count as f64 / total as f64; + assert!( + share > 0.5, + "composition broke the summary: majority class {majority} holds only \ + {:.1}% of a District patch across {} classes {:?}. D-258 allows \ + minority pockets, not a new majority — this would make the map \ + disagree with the view it was zoomed in from.", + 100.0 * share, + tally.len(), + tally + ); + // ...and it must not be a monoculture either, or nothing was composed. + assert!( + tally.len() > 1 || share == 1.0, + "tally {tally:?} — expected either composition or an honestly uniform patch" + ); + eprintln!( + "conservation: majority class {majority} at {:.1}% across {} classes {:?}", + 100.0 * share, + tally.len(), + tally + ); + } +} diff --git a/server/src/atlas/district_profile.rs b/server/src/atlas/district_profile.rs index b39030b0d..afd3ae4c4 100644 --- a/server/src/atlas/district_profile.rs +++ b/server/src/atlas/district_profile.rs @@ -1702,6 +1702,13 @@ fn build_district_profile( near_perennial_water: bool, lake_from_hydrology: bool, lake_margin_q: i32, + // T-1213: apply the fine composition tier. FALSE on the orbital path, which + // is envelope-only by design and documents that it never invents slope + // (`derive_orbital_at_metres_never_invents_slope` pins it). Global does not + // need it either — it already carries nine morphology zones and 75 moisture + // values; composition exists for the rungs below, where those collapse to + // one. + invent_composition: bool, ) -> DistrictProfile { let tectonic_class = derive_tectonic_class(body_params); @@ -1746,7 +1753,32 @@ fn build_district_profile( world_y_m, min_wavelength_m, ); - let moisture_q = (base_moisture_q + moisture_perturb).clamp(0, 100); + // T-1213 — the fine composition tier (D-258 invariant 2). The massif/texture + // pair above is weighted so it can never change a verdict (70/30, by + // design); this one is allowed to, because "descending the ladder reveals + // COMPOSITION" means precisely that a summarised cell must be able to + // contain the minority it suppressed. Conservation is the bound rather than + // weighting: the field is zero-mean, so the summary survives a downsample. + // + // It feeds BOTH classification inputs. Moisture alone would leave morphology + // just as flat — measured on Ferrath, nine zones at Global and exactly one + // below it, because every coastal family (CliffCoast at slope_q >= 55, Fjord + // at >= 40) gates on a threshold that sub-Global slope never reaches. + let (composition_moisture, composition_slope) = if invent_composition { + crate::atlas::composition::composition_offsets( + seed.seed(), + world_x_m, + world_y_m, + min_wavelength_m, + veg_envelope + .ceiling_q + .min(crate::atlas::composition::MOISTURE_COMPOSITION_CEILING_Q), + ) + } else { + (0, 0) + }; + let moisture_q = (base_moisture_q + moisture_perturb + composition_moisture).clamp(0, 100); + let slope_q = (slope_q + composition_slope).clamp(0, 100); // Climate-derived fields: computed from temperature + moisture primitives // (D-239 §2). This is the correct call order — temperature must be resolved @@ -2054,6 +2086,7 @@ fn derive_at_metres_with_riparian( near_perennial_water, lake_from_hydrology, lake_margin_q, + true, // metres-addressable path: composition applies (T-1213) ) } @@ -2273,6 +2306,13 @@ pub fn derive_orbital_at_metres( false, lake_from_hydrology, lake_margin_q, + // T-1213: NO composition at the orbital rung. This path is + // envelope-only by design and documents that it never invents slope + // (pinned by `derive_orbital_at_metres_never_invents_slope`, which + // caught the first version of this change). Global does not need it + // regardless — it already carries nine morphology zones and 75 moisture + // values; composition exists for the rungs where those collapse to one. + false, ) } diff --git a/server/src/atlas/mod.rs b/server/src/atlas/mod.rs index eeed3c0ff..a4d604038 100644 --- a/server/src/atlas/mod.rs +++ b/server/src/atlas/mod.rs @@ -15,6 +15,7 @@ pub mod cascade; pub mod chunk_context; pub mod city_context_reader; pub mod coast_invention; +pub mod composition; pub mod detail_scatter; pub mod district_mix; pub mod district_profile; diff --git a/server/src/atlas/step_canvas.rs b/server/src/atlas/step_canvas.rs index bbf0cffab..84af81b85 100644 --- a/server/src/atlas/step_canvas.rs +++ b/server/src/atlas/step_canvas.rs @@ -2662,6 +2662,21 @@ mod tests { .len(); grad += &format!(" RELIEF[uniq {ruq:3} d4mean {:.2}]", rsum / rn.max(1.0)); + // Cliffs travel by TWO independent routes and both are worth + // watching down the ladder: the sparse `cliffs` list (explicit + // segment geometry, not derived from the elevation field) and the + // morphology gates, which key on slope_q — CliffCoast at >= 55, + // Fjord at >= 40 (D-239 §6). If slope_q flattens the way elev_q + // does, those zones become unreachable and coastal character is + // lost even though the segment list survives. + let morph_zones: std::collections::BTreeSet = + raw.morphology.iter().copied().collect(); + grad += &format!( + " cliffs {} zones {:?}", + raw.cliffs.len(), + morph_zones.iter().copied().collect::>() + ); + println!( "{:8?} {:>9.1} m/gu {}x{} elev[uniq {:3} span {:3}] moist[uniq {:3} span {:3}] \ morph[uniq {:2}] veg[uniq {:2}] glac[uniq {:2}] courses {}{grad}", diff --git a/server/tests/golden/believability.json b/server/tests/golden/believability.json index 27bad42a2..5beeaaebb 100644 --- a/server/tests/golden/believability.json +++ b/server/tests/golden/believability.json @@ -6,9 +6,9 @@ "voxel_sampled_districts": 64, "contrast": { "moisture_q": { - "min": 17, - "max": 97, - "distinct": 75 + "min": 0, + "max": 100, + "distinct": 85 }, "elev_q": { "min": 0, @@ -17,8 +17,8 @@ }, "slope_q": { "min": 0, - "max": 27, - "distinct": 28 + "max": 50, + "distinct": 34 }, "ocean_fraction_q": { "min": 0, @@ -27,7 +27,7 @@ }, "morphology_zones": 9, "vegetation_classes": 4, - "terrain_materials": 3, + "terrain_materials": 4, "voxel_relief_m": 28, "micro_habitat_distinct": 3, "land_districts": 698, @@ -50,8 +50,8 @@ "contrast": { "moisture_q": { "min": 0, - "max": 24, - "distinct": 25 + "max": 49, + "distinct": 42 }, "elev_q": { "min": 0, @@ -60,17 +60,17 @@ }, "slope_q": { "min": 0, - "max": 15, - "distinct": 15 + "max": 34, + "distinct": 22 }, "ocean_fraction_q": { "min": 0, "max": 100, "distinct": 36 }, - "morphology_zones": 6, + "morphology_zones": 7, "vegetation_classes": 3, - "terrain_materials": 2, + "terrain_materials": 3, "voxel_relief_m": 22, "micro_habitat_distinct": 0, "land_districts": 1602, diff --git a/server/tests/golden/window_derivation_golden.json b/server/tests/golden/window_derivation_golden.json index f53c9f583..b653fd242 100644 --- a/server/tests/golden/window_derivation_golden.json +++ b/server/tests/golden/window_derivation_golden.json @@ -22,15 +22,15 @@ "wx_m": 2000000, "wy_m": 1500000, "min_wl_m": 1024, - "morphology": 8, + "morphology": 16, "tectonic": 0, "glaciation": 0, "precipitation": 3, - "slope_q": 3, + "slope_q": 0, "elev_q": 31, "ocean_fraction_q": 0, "temperature_dc": 97, - "moisture_q": 59, + "moisture_q": 65, "vegetation": 3 }, { @@ -73,15 +73,15 @@ "wx_m": 2050000, "wy_m": 1500000, "min_wl_m": 1024, - "morphology": 8, + "morphology": 16, "tectonic": 0, "glaciation": 0, "precipitation": 3, - "slope_q": 3, + "slope_q": 0, "elev_q": 25, "ocean_fraction_q": 0, "temperature_dc": 132, - "moisture_q": 56, + "moisture_q": 65, "vegetation": 3 }, { @@ -128,11 +128,11 @@ "tectonic": 0, "glaciation": 0, "precipitation": 3, - "slope_q": 3, + "slope_q": 4, "elev_q": 27, "ocean_fraction_q": 0, "temperature_dc": 115, - "moisture_q": 58, + "moisture_q": 57, "vegetation": 3 }, { @@ -179,11 +179,11 @@ "tectonic": 0, "glaciation": 0, "precipitation": 2, - "slope_q": 4, + "slope_q": 3, "elev_q": 20, "ocean_fraction_q": 0, "temperature_dc": 116, - "moisture_q": 52, + "moisture_q": 54, "vegetation": 3 }, { @@ -230,11 +230,11 @@ "tectonic": 0, "glaciation": 2, "precipitation": 1, - "slope_q": 4, + "slope_q": 1, "elev_q": 61, "ocean_fraction_q": 0, "temperature_dc": -120, - "moisture_q": 43, + "moisture_q": 49, "vegetation": 1 }, { @@ -281,11 +281,11 @@ "tectonic": 0, "glaciation": 0, "precipitation": 2, - "slope_q": 2, + "slope_q": 1, "elev_q": 5, "ocean_fraction_q": 0, "temperature_dc": 106, - "moisture_q": 50, + "moisture_q": 51, "vegetation": 3 }, { @@ -332,7 +332,7 @@ "tectonic": 0, "glaciation": 0, "precipitation": 0, - "slope_q": 2, + "slope_q": 0, "elev_q": 48, "ocean_fraction_q": 0, "temperature_dc": -2147483648, @@ -383,7 +383,7 @@ "tectonic": 0, "glaciation": 0, "precipitation": 0, - "slope_q": 2, + "slope_q": 1, "elev_q": 39, "ocean_fraction_q": 0, "temperature_dc": -2147483648, @@ -434,7 +434,7 @@ "tectonic": 0, "glaciation": 0, "precipitation": 0, - "slope_q": 2, + "slope_q": 0, "elev_q": 45, "ocean_fraction_q": 0, "temperature_dc": -2147483648, @@ -485,7 +485,7 @@ "tectonic": 0, "glaciation": 0, "precipitation": 0, - "slope_q": 4, + "slope_q": 5, "elev_q": 39, "ocean_fraction_q": 0, "temperature_dc": -2147483648, @@ -515,11 +515,11 @@ "wx_m": 1200000, "wy_m": 8500000, "min_wl_m": 4096, - "morphology": 8, + "morphology": 12, "tectonic": 0, "glaciation": 0, "precipitation": 0, - "slope_q": 2, + "slope_q": 27, "elev_q": 76, "ocean_fraction_q": 0, "temperature_dc": -2147483648, @@ -536,7 +536,7 @@ "tectonic": 0, "glaciation": 0, "precipitation": 0, - "slope_q": 3, + "slope_q": 0, "elev_q": 79, "ocean_fraction_q": 0, "temperature_dc": -2147483648, @@ -638,11 +638,11 @@ "tectonic": 2, "glaciation": 0, "precipitation": 3, - "slope_q": 6, + "slope_q": 2, "elev_q": 13, "ocean_fraction_q": 0, "temperature_dc": 811, - "moisture_q": 62, + "moisture_q": 71, "vegetation": 3 }, { @@ -688,12 +688,12 @@ "morphology": 15, "tectonic": 2, "glaciation": 0, - "precipitation": 3, - "slope_q": 6, + "precipitation": 2, + "slope_q": 11, "elev_q": 24, "ocean_fraction_q": 0, "temperature_dc": 749, - "moisture_q": 60, + "moisture_q": 50, "vegetation": 3 }, { @@ -739,12 +739,12 @@ "morphology": 15, "tectonic": 2, "glaciation": 0, - "precipitation": 3, - "slope_q": 4, + "precipitation": 2, + "slope_q": 7, "elev_q": 33, "ocean_fraction_q": 0, "temperature_dc": 703, - "moisture_q": 59, + "moisture_q": 53, "vegetation": 3 }, { @@ -773,12 +773,12 @@ "morphology": 15, "tectonic": 2, "glaciation": 0, - "precipitation": 2, - "slope_q": 3, + "precipitation": 1, + "slope_q": 28, "elev_q": 24, "ocean_fraction_q": 0, "temperature_dc": 691, - "moisture_q": 49, + "moisture_q": 19, "vegetation": 3 }, { @@ -790,12 +790,12 @@ "morphology": 15, "tectonic": 2, "glaciation": 0, - "precipitation": 2, - "slope_q": 4, + "precipitation": 4, + "slope_q": 0, "elev_q": 21, "ocean_fraction_q": 0, "temperature_dc": 707, - "moisture_q": 49, + "moisture_q": 85, "vegetation": 3 }, { @@ -842,11 +842,11 @@ "tectonic": 2, "glaciation": 0, "precipitation": 2, - "slope_q": 4, + "slope_q": 0, "elev_q": 65, "ocean_fraction_q": 0, "temperature_dc": 300, - "moisture_q": 30, + "moisture_q": 38, "vegetation": 2 }, { @@ -893,11 +893,11 @@ "tectonic": 2, "glaciation": 0, "precipitation": 2, - "slope_q": 6, + "slope_q": 7, "elev_q": 5, "ocean_fraction_q": 0, "temperature_dc": 644, - "moisture_q": 49, + "moisture_q": 48, "vegetation": 3 }, { @@ -944,11 +944,11 @@ "tectonic": 0, "glaciation": 0, "precipitation": 3, - "slope_q": 5, + "slope_q": 1, "elev_q": 0, "ocean_fraction_q": 0, "temperature_dc": 280, - "moisture_q": 56, + "moisture_q": 64, "vegetation": 6 }, { @@ -991,15 +991,15 @@ "wx_m": 1250942, "wy_m": 158849, "min_wl_m": 1024, - "morphology": 8, + "morphology": 16, "tectonic": 0, "glaciation": 2, - "precipitation": 1, - "slope_q": 2, + "precipitation": 2, + "slope_q": 0, "elev_q": 75, "ocean_fraction_q": 0, "temperature_dc": -110, - "moisture_q": 54, + "moisture_q": 60, "vegetation": 1 }, {