OverviewFilesCommitsIssuesDocsReleasesPeers

terrain-lookdev commits / 3962eee

terrain-lookdev: the instrument #46 §4 said the decision was waiting on
#46 §4 measured that terrain-diffusion and our erosion filter differ in roughness -- their beta steepens 4.18 -> 4.69 -> 5.10 across the octaves where ours is flat at 3.58 -> 3.44 -> 3.78 -- and said nothing whatever about which is better to look at. That is aesthetic, so the only instrument that can settle it is both images on screen at a matched scale. WHAT IS HERE erosion_render.cpp the map's surface on the GPU. 0.94 s for a 512^2 tile where dump_erosion_heightfield takes 13.5 minutes. server.py 2D and 3D, both generators side by side, knobs live. index.html the app. spectrum_probe.py the #46 §4 instrument, self-testing. compare_bands.py per-octave beta for both, same axes. dump_erosion_heightfield.cpp the CPU cross-check. NOTHING HERE REIMPLEMENTS THE TERRAIN erosion_render compiles erosion_filter.inc off disk and takes its wrapper from erosion_wrapper_glsl.h -- the same function test_erosion_parity.cpp calls -- and fills its parameters from erosion_wrapper_params_from_map(). The one piece it does own, the scan-and-bisect root finder in GLSL, is deliberately checked against the C++ one: --verify renders the nine columns the oracle publishes and compares. Worst delta 0.0042 m, which is the limit of the two decimals the oracle prints. That gate has already earned itself. The renderer's first run was 7.3 km out at every column, because it had hand-copied the map constants and MAP_FLOOR_Y moved -1000 -> -10900 in dc-terrain 156b2e4 the same morning. Nothing else would have caught it; the pictures looked like terrain. TWO BUGS THE HEADLESS SCREENSHOT CAUGHT - .busy{display:grid} beat the hidden attribute's UA display:none, so the "rendering..." overlay never went away and sat on top of every render. - small tiles did not scale to the pane. Both are the kind of thing that is invisible from the source and obvious in a picture, which is the argument for verifying a UI by looking at it. FIRST OBSERVATION, DEFAULT KNOBS, NOT YET A CONCLUSION Our filter produces texture; the diffusion model produces landscape. Ours is a dense uniform field of small gully loops with no drainage hierarchy -- small valleys do not feed larger ones -- and it holds that character from 11 km down. Widening to 57 km shows broad smooth swells with the same crinkle riding on top, unconnected to them. The diffusion output has coherent catchments at every scale examined. That is what flat beta and steepening beta look like, and it strengthens #46 §4b option 1: hand the large forms to diffusion because it is the one that makes them. Default knobs on both, two locations, one seed. The knobs exist precisely to move this and have not been swept yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3962eee7f2408f5a08b41b3edb7cf51d2040cd8f
by plankatron <noreply@anthropic.com> · 2026-08-31T16:16:44.000Z

Changed files (9) +3529 -0

README.md +80 -0
……@@ -0,0 +1,80 @@
1# terrain-lookdev
2
3Tooling for hyper-games #46 — comparing our erosion filter against
4terrain-diffusion, and looking at either while turning its knobs.
5
6Nothing here lives in a shared repo, and nothing here builds into any repo's
7`bin/`. Everything compiles standalone with `g++` into `/tmp`.
8
9## The rule this tooling exists to obey
10
11> Before believing a check, name the artifact it examined, and confirm it is
12> the one that ships. — hyper-games #44 §7
13
14So none of it reimplements the terrain. `erosion_render` compiles
15`proc-nodes/src/shared/erosion_filter.inc` off disk and gets its wrapper from
16`erosion_wrapper_glsl.h` — the same function `test_erosion_parity.cpp` calls —
17and it fills its parameters from `erosion_wrapper_params_from_map()` rather than
18copying constants. It hand-copied them once and was 7.3 km out within hours,
19because `MAP_FLOOR_Y` moved in dc-terrain `156b2e4` the same morning.
20
21## Files
22
23| file | what |
24|---|---|
25| `erosion_render.cpp` | the map's surface as a heightfield, on the GPU. 0.94 s for 512² where the CPU dumper takes 13.5 min. **Run `--verify` before believing any picture it makes.** |
26| `dump_erosion_heightfield.cpp` | the CPU equivalent. Slow, and kept because it goes through a different code path — it is the cross-check, not the workhorse. |
27| `spectrum_probe.py` | radially-averaged power spectrum. Self-tests first and refuses to print a terrain number until they pass. |
28| `compare_bands.py` | per-octave β for both generators on the same axes. #46 §4.3. |
29| `server.py` + `index.html` | the lookdev app: 2D and 3D, both generators side by side at matched scale. |
30
31## Build and run
32
33```bash
34# renderer — from the dc-terrain root, so the default --inc resolves
35cd ../dc-terrain
36g++ -O2 -std=c++17 -I ../proc-nodes/src/shared -I . \
37 -o /tmp/erosion_render ../terrain-lookdev/erosion_render.cpp -lvulkan
38/tmp/erosion_render --verify # MUST pass. 9 columns, worst delta < 0.01 m
39
40# the diffusion service (leave running)
41cd ../terrain-diffusion
42.venv/bin/python -m terrain_diffusion.inference.api \
43 xandergos/terrain-diffusion-30m --seed 1234 --no-compile \
44 --host 127.0.0.1 --port 8817
45
46# the app
47.venv/bin/python ../terrain-lookdev/server.py --port 8818
48```
49
50Then <http://127.0.0.1:8818>.
51
52Two traps in the diffusion service, both cost time once: `python -m
53terrain_diffusion` is broken — its `__main__` imports the training module, which
54imports `wandb` — so invoke `terrain_diffusion.inference.api` directly; and it
55blocks on an interactive prompt for WorldClim base data, so put
56`wc2.1_10m_bio_{1,4,12,15}.tif` in `terrain-diffusion/data/global/` first.
57
58## Using it
59
60Both panels are shaded by the same code, so a difference on screen is a
61difference in the terrain rather than in the renderer. The diffusion panel is
62always at its native 30 m/px: that API's `scale` knob is bilinear (#46 §4.1) and
63would only blur.
64
65- **2D / 3D** tabs. 3D is a different view of the same heightfield from the same
66 endpoint, so the tabs cannot disagree about the terrain.
67- Vertical exaggeration in 3D is an explicit multiplier on the true
68 height-to-width ratio, never an auto-fit — an auto-fit would flatter whichever
69 generator has less relief.
70- Any control can be set from the URL (`?mode=3d&mpp=30&en_gain=0.4&…`), which is
71 how the views get linked into an issue.
72- `voxel_size` band-limits the generator the way a coarse LOD does. At 30 m/px
73 leave it near 60, or you are looking at aliasing.
74
75## What it was built to answer
76
77#46 §4 measured that the two generators differ in roughness — diffusion's β
78steepens 4.18 → 5.10 across the octaves where ours is flat at 3.58 → 3.78 — and
79said nothing about which is better to look at. That is aesthetic, and this is the
80only instrument that can settle it.
__pycache__/spectrum_probe.cpython-312.pyc +0 -0
(no content changes — binary or rename)
compare_bands.py +88 -0
……@@ -0,0 +1,88 @@
1#!/usr/bin/env python3
2"""
3BAND COMPARISON — terrain-diffusion's spectrum against our erosion filter's,
4on the same physical axes.
5
6hyper-games #46 §3, probe B. Both fields are sampled over the same physical
7width at the same metres-per-pixel, so a given radial bin k is the same
8wavelength in both and the two spectra can be read against each other directly.
9
10The instrument is the same one, and its self-test runs first.
11"""
12import argparse, sys
13import numpy as np
14sys.path.insert(0, '/storage/projects/code/godot/terrain-lookdev')
15from spectrum_probe import fetch_tile, radial_power_spectrum, selftest
16
17
18def octave_table(name, field, mpp, kmin, kmax):
19 n = field.shape[0]
20 width = n * mpp
21 k, p = radial_power_spectrum(field)
22 rows = []
23 lo = kmin
24 while lo * 2 <= k.max():
25 hi = min(lo * 2, int(k.max()))
26 m = (k > lo) & (k <= hi) & (p > 0)
27 if m.sum() >= 3:
28 # local power-law slope inside this octave: the honest measure of
29 # "how rough is it HERE", with no extrapolation from a lower band.
30 s, _ = np.polyfit(np.log10(k[m]), np.log10(p[m]), 1)
31 rows.append((width / hi, width / lo, -s, m.sum()))
32 lo = hi
33 return k, p, rows
34
35
36def main():
37 ap = argparse.ArgumentParser()
38 ap.add_argument("--url", default="http://127.0.0.1:8817")
39 ap.add_argument("--erosion", default="/tmp/erosion-512.f32")
40 ap.add_argument("--n", type=int, default=512)
41 ap.add_argument("--mpp", type=float, default=30.0)
42 ap.add_argument("--i1", type=int, default=0)
43 ap.add_argument("--j1", type=int, default=2048)
44 args = ap.parse_args()
45
46 print("INSTRUMENT SELF-TEST")
47 if not selftest():
48 print(" FAILED — no number below is meaningful.")
49 return 2
50 print(" all pass\n")
51
52 n, mpp = args.n, args.mpp
53 print(f"Both fields: {n}x{n} px @ {mpp:g} m/px = {n*mpp/1000:.2f} km across, "
54 f"Nyquist lambda = {2*mpp:g} m\n")
55
56 ero = np.fromfile(args.erosion, dtype="<f4").reshape(n, n).astype(np.float64)
57 nan = int(np.isnan(ero).sum())
58 if nan:
59 print(f" NOTE: {nan} NaN cells in the erosion field; filled with the mean.")
60 ero = np.where(np.isnan(ero), np.nanmean(ero), ero)
61
62 dif, _ = fetch_tile(args.url, args.i1, args.j1, n, 1)
63
64 fields = [("terrain-diffusion (30m model, seed 1234)", dif),
65 ("our erosion filter (map_dc_erosion, band-limited)", ero)]
66
67 for label, f in fields:
68 k, p, rows = octave_table(label, f, mpp, 4, n // 2)
69 print(f"{label}")
70 print(f" relief: min {f.min():.0f} m max {f.max():.0f} m "
71 f"sd {f.std():.0f} m")
72 print(f" {'octave (wavelength)':>26} {'local beta':>11} {'bins':>6}")
73 for w_lo, w_hi, beta, nb in rows:
74 unit = (f"{w_lo/1000:5.2f}-{w_hi/1000:5.2f} km" if w_hi >= 1000
75 else f"{w_lo:6.0f}-{w_hi:6.0f} m ")
76 print(f" {unit:>26} {beta:11.2f} {nb:6d}")
77 print()
78
79 print("READING IT: beta is the local 2-D radial power-law exponent inside")
80 print("that octave alone -- no extrapolation. Higher beta = smoother, i.e.")
81 print("less energy at the fine end of the octave than the coarse end.")
82 print("A self-similar generator holds beta constant down the column.")
83 print("Subtract 1 for the radially-INTEGRATED convention some papers use.")
84 return 0
85
86
87if __name__ == "__main__":
88 sys.exit(main())
dump_erosion_heightfield.cpp +105 -0
……@@ -0,0 +1,105 @@
1// ============================================================================
2// EROSION HEIGHTFIELD DUMPER — our generator's surface, as a raw grid, so the
3// SAME spectrum probe can be run on it as on terrain-diffusion's output.
4//
5// WHY IT INCLUDES A TEST FILE
6//
7// The CPU wrapper (map_dc_erosion's terrain layers + erosion_node_eval + the
8// detail layer + the y-mapping) is not reachable outside a running engine;
9// test_erosion_parity.cpp already reconstructs it, with every constant
10// annotated by file and line, and that reconstruction is GUARDED green by the
11// parity oracle against the GPU. Copying it here would create a fifth copy of
12// the thing #45 spent a session reducing to one, and it would drift silently.
13//
14// So this file includes that translation unit and calls its `cpu_wrapper` and
15// `find_surface` directly. `main` is renamed out of the way. If the parity
16// oracle's reconstruction changes, this dumper changes with it -- which is the
17// property we want.
18//
19// Build (no engine, no godot-cpp, no scons, nothing into bin/):
20// g++ -O2 -std=c++17 -I ../proc-nodes/src/shared -I ../dc-terrain \
21// -o /tmp/dump_erosion dump_erosion_heightfield.cpp -lvulkan
22//
23// Run (from the dc-terrain root, so the default --inc path resolves):
24// /tmp/dump_erosion --n 512 --metres-per-px 30 --out /tmp/erosion.f32
25//
26// Output: N*N float32 little-endian surface heights in metres, row-major.
27// A column with no surface writes NAN and is counted in the summary.
28// ============================================================================
29#include <cstdio>
30#include <cstdlib>
31#include <cstring>
32#include <cmath>
33#include <vector>
34
35#define main parity_main_unused
36#include "tests/test_erosion_parity.cpp"
37#undef main
38
39int main(int argc, char** argv) {
40 int n = 512;
41 float mpp = 30.0f; // metres per pixel — match the diffusion tile
42 float ox = 0.0f, oz = 0.0f;
43 const char* out = "/tmp/erosion.f32";
44 float voxel = -1.0f; // default: band-limit to the sampling grid
45
46 for (int i = 1; i < argc; i++) {
47 if (!strcmp(argv[i], "--n") && i + 1 < argc) n = atoi(argv[++i]);
48 else if (!strcmp(argv[i], "--metres-per-px") && i+1<argc) mpp = (float)atof(argv[++i]);
49 else if (!strcmp(argv[i], "--ox") && i + 1 < argc) ox = (float)atof(argv[++i]);
50 else if (!strcmp(argv[i], "--oz") && i + 1 < argc) oz = (float)atof(argv[++i]);
51 else if (!strcmp(argv[i], "--out") && i + 1 < argc) out = argv[++i];
52 else if (!strcmp(argv[i], "--voxel") && i + 1 < argc) voxel = (float)atof(argv[++i]);
53 else { fprintf(stderr, "unknown arg %s\n", argv[i]); return 2; }
54 }
55
56 // BAND-LIMIT THE GENERATOR TO THE SAMPLING GRID. The erosion filter runs
57 // octaves down to the voxel size; sampling that on a 30 m grid would alias
58 // fine octaves into low bins and corrupt exactly the spectrum we are
59 // measuring. The filter already has Nyquist octave clamping keyed to
60 // MAP_VOXEL_SIZE (erosion_filter.inc nyquist_octaves), so setting it to
61 // 2*pixel makes the generator itself band-limited -- the same thing it does
62 // at a coarse LOD. Override with --voxel to sample it unclamped on purpose.
63 MAP_VOXEL_SIZE = (voxel > 0.0f) ? voxel : 2.0f * mpp;
64
65 printf("erosion heightfield: %dx%d px @ %g m/px = %.2f km across\n",
66 n, n, mpp, n * mpp / 1000.0f);
67 printf(" origin (%g, %g) m voxel size %g m detail scale %g\n",
68 ox, oz, MAP_VOXEL_SIZE, MAP_DETAIL_SCALE);
69
70 std::vector<float> h((size_t)n * n);
71 int misses = 0;
72 double lo = 1e30, hi = -1e30, sum = 0.0;
73
74 for (int j = 0; j < n; j++) {
75 for (int i = 0; i < n; i++) {
76 float x = ox + (float)i * mpp;
77 float z = oz + (float)j * mpp;
78 float y;
79 if (find_surface(cpu_wrapper, x, z, y)) {
80 h[(size_t)j * n + i] = y;
81 if (y < lo) lo = y;
82 if (y > hi) hi = y;
83 sum += y;
84 } else {
85 h[(size_t)j * n + i] = NAN;
86 misses++;
87 }
88 }
89 if ((j & 63) == 0) { printf("\r row %d/%d", j, n); fflush(stdout); }
90 }
91 printf("\r done \n");
92
93 long hits = (long)n * n - misses;
94 printf(" surface found in %ld of %d columns (%d misses)\n", hits, n * n, misses);
95 if (hits > 0)
96 printf(" height min %.1f m max %.1f m mean %.1f m\n",
97 lo, hi, sum / (double)hits);
98
99 FILE* f = fopen(out, "wb");
100 if (!f) { perror("fopen"); return 1; }
101 fwrite(h.data(), sizeof(float), h.size(), f);
102 fclose(f);
103 printf(" wrote %zu floats to %s\n", h.size(), out);
104 return misses == n * n ? 1 : 0;
105}
erosion_render.cpp +347 -0
……@@ -0,0 +1,347 @@
1// ============================================================================
2// EROSION RENDERER — the map's surface as a heightfield, on the GPU, from the
3// same erosion_filter.inc and the same wrapper text the parity oracle guards.
4//
5// WHY IT EXISTS
6//
7// The CPU dumper takes 13.5 minutes for a 512^2 tile. That is fine for a
8// measurement and useless for looking at something while turning a knob. This
9// does the same job on the GPU.
10//
11// WHAT IT SHARES, AND WHAT IT DOES NOT
12//
13// SHARED, and this is the point:
14// - erosion_filter.inc, read off disk at run time and compiled by glslc.
15// - the map WRAPPER, from proc-nodes/src/shared/erosion_wrapper_glsl.h --
16// the same function test_erosion_parity.cpp calls, which is why adding
17// this second GPU consumer did not create a third copy of the wrapper.
18// - the Vulkan setup, by including the oracle's translation unit.
19//
20// NOT SHARED:
21// - the root finder. find_surface() is C++ in the oracle; the scan-and-
22// bisect below is GLSL, because doing it per column inside the shader is
23// the whole speed win. Two implementations of a generic root finder is a
24// real duplication, but a much smaller one than the wrapper, and it
25// fails LOUDLY: --verify checks this renderer against the oracle's own
26// published surface heights, and those came from the C++ finder.
27//
28// VERIFY BEFORE BELIEVING A PICTURE (#45 section 5 -- instruments are layers)
29//
30// erosion_render --verify
31//
32// evaluates the nine columns the oracle prints and compares. The oracle's
33// values at MAP defaults are, from a green run:
34// (-10000,-10000) 5430.60 (0,0) -1059.81 (10000,10000) -5038.45
35// A picture from a renderer that has not passed --verify is decoration.
36//
37// Build (no engine, no godot-cpp, no scons, nothing into bin/):
38// cd dc-terrain
39// g++ -O2 -std=c++17 -I ../proc-nodes/src/shared -I . \
40// -o /tmp/erosion_render ../terrain-lookdev/erosion_render.cpp -lvulkan
41//
42// Run (from the dc-terrain root, so the default --inc path resolves):
43// /tmp/erosion_render --n 512 --mpp 30 --out /tmp/h.f32
44// /tmp/erosion_render --verify
45//
46// Output: N*N float32 LE surface heights in metres, row-major (z outer, x
47// inner). Columns with no surface are -3.0e38.
48// ============================================================================
49#include <cstdio>
50#include <cstdlib>
51#include <cstring>
52#include <cmath>
53#include <string>
54#include <vector>
55
56#define main parity_main_unused
57#include "tests/test_erosion_parity.cpp"
58#undef main
59
60static const float NO_SURFACE = -3.0e38f;
61
62struct PushC {
63 uint32_t w, h;
64 float ox, oz, mpp;
65 uint32_t row0;
66};
67
68// The root finder, in GLSL. Deliberately the same shape as find_surface() in
69// test_erosion_parity.cpp: a top-down scan for a sign change, then bisection
70// inside the bracket that found it. Same STEPS and same bisection count, so
71// --verify is comparing the FIELD and not two different search policies.
72static std::string build_render_shader(const std::string& inc,
73 const ErosionWrapperParams& wp,
74 int steps, int bisect) {
75 std::string s =
76 "#version 450\n"
77 "layout(local_size_x = 8, local_size_y = 8) in;\n"
78 "layout(set=0, binding=0) writeonly buffer OutH { float heights[]; };\n"
79 "layout(push_constant) uniform PC {\n"
80 " uint w; uint h; float ox; float oz; float mpp; uint row0;\n"
81 "} pc;\n"
82 "\n";
83 s += inc;
84 s += "\n";
85 s += erosion_wrapper_glsl(wp);
86
87 char tail[2048];
88 snprintf(tail, sizeof(tail),
89 "const int SCAN_STEPS = %d;\n"
90 "const int BISECT = %d;\n"
91 "void main() {\n"
92 " uint gx = gl_GlobalInvocationID.x;\n"
93 " uint gy = gl_GlobalInvocationID.y;\n"
94 " if (gx >= pc.w || gy >= pc.h) return;\n"
95 " float x = pc.ox + float(gx) * pc.mpp;\n"
96 " float z = pc.oz + float(pc.row0 + gy) * pc.mpp;\n"
97 " float lo = M_FLOOR_Y + 1.0;\n"
98 " float hi = M_CEIL_Y - 1.0;\n"
99 " float prev_y = hi;\n"
100 " float prev_d = gpu_wrapper(x, hi, z);\n"
101 " float found = %.9g;\n"
102 " for (int i = 1; i <= SCAN_STEPS; i++) {\n"
103 " float y = hi + (lo - hi) * float(i) / float(SCAN_STEPS);\n"
104 " float d = gpu_wrapper(x, y, z);\n"
105 " if ((prev_d < 0.0) != (d < 0.0)) {\n"
106 " float a = y, b = prev_y;\n"
107 " for (int k = 0; k < BISECT; k++) {\n"
108 " float m = 0.5 * (a + b);\n"
109 " if ((gpu_wrapper(x, m, z) < 0.0) == (prev_d < 0.0)) b = m;\n"
110 " else a = m;\n"
111 " }\n"
112 " found = 0.5 * (a + b);\n"
113 " break;\n"
114 " }\n"
115 " prev_y = y; prev_d = d;\n"
116 " }\n"
117 " heights[gy * pc.w + gx] = found;\n"
118 "}\n",
119 steps, bisect, (double)NO_SURFACE);
120 s += tail;
121 return s;
122}
123
124// ---------------------------------------------------------------------------
125struct Renderer {
126 Ctx c{};
127 VkShaderModule mod = VK_NULL_HANDLE;
128 VkDescriptorSetLayout dsl = VK_NULL_HANDLE;
129 VkPipelineLayout playout = VK_NULL_HANDLE;
130 VkPipeline pipe = VK_NULL_HANDLE;
131 VkDescriptorPool dpool = VK_NULL_HANDLE;
132 VkDescriptorSet dset = VK_NULL_HANDLE;
133
134 void build(const std::string& src) {
135 init_vulkan(c);
136 std::vector<uint32_t> spv = compile_glsl(src, "erosion render");
137
138 VkShaderModuleCreateInfo smi{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
139 smi.codeSize = spv.size() * 4; smi.pCode = spv.data();
140 VK(vkCreateShaderModule(c.dev, &smi, nullptr, &mod));
141
142 VkDescriptorSetLayoutBinding bind{};
143 bind.binding = 0;
144 bind.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
145 bind.descriptorCount = 1;
146 bind.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT;
147 VkDescriptorSetLayoutCreateInfo dli{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
148 dli.bindingCount = 1; dli.pBindings = &bind;
149 VK(vkCreateDescriptorSetLayout(c.dev, &dli, nullptr, &dsl));
150
151 VkPushConstantRange pcr{VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(PushC)};
152 VkPipelineLayoutCreateInfo pli{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
153 pli.setLayoutCount = 1; pli.pSetLayouts = &dsl;
154 pli.pushConstantRangeCount = 1; pli.pPushConstantRanges = &pcr;
155 VK(vkCreatePipelineLayout(c.dev, &pli, nullptr, &playout));
156
157 VkComputePipelineCreateInfo cpi{VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
158 cpi.stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
159 cpi.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
160 cpi.stage.module = mod; cpi.stage.pName = "main";
161 cpi.layout = playout;
162 VK(vkCreateComputePipelines(c.dev, VK_NULL_HANDLE, 1, &cpi, nullptr, &pipe));
163
164 VkDescriptorPoolSize ps{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1};
165 VkDescriptorPoolCreateInfo dpi{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
166 dpi.maxSets = 1; dpi.poolSizeCount = 1; dpi.pPoolSizes = &ps;
167 VK(vkCreateDescriptorPool(c.dev, &dpi, nullptr, &dpool));
168 VkDescriptorSetAllocateInfo dai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
169 dai.descriptorPool = dpool; dai.descriptorSetCount = 1; dai.pSetLayouts = &dsl;
170 VK(vkAllocateDescriptorSets(c.dev, &dai, &dset));
171 }
172
173 // One band of rows per submit. A single dispatch over a whole tile is
174 // hundreds of millions of erosion evaluations, and a compute submit that
175 // long on the DISPLAY gpu trips the driver's watchdog and takes the
176 // desktop with it. Banding keeps each submit short.
177 void render(float ox, float oz, uint32_t w, uint32_t h, float mpp,
178 uint32_t band, std::vector<float>& out) {
179 out.assign((size_t)w * h, NO_SURFACE);
180 Buf buf = make_host_buf(c, (size_t)w * band * sizeof(float));
181
182 VkDescriptorBufferInfo bi{buf.buf, 0, VK_WHOLE_SIZE};
183 VkWriteDescriptorSet wr{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
184 wr.dstSet = dset; wr.dstBinding = 0; wr.descriptorCount = 1;
185 wr.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
186 wr.pBufferInfo = &bi;
187 vkUpdateDescriptorSets(c.dev, 1, &wr, 0, nullptr);
188
189 for (uint32_t row0 = 0; row0 < h; row0 += band) {
190 uint32_t rows = (row0 + band <= h) ? band : (h - row0);
191 PushC pc{w, rows, ox, oz, mpp, row0};
192
193 VkCommandBufferAllocateInfo cbi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
194 cbi.commandPool = c.cmdpool; cbi.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
195 cbi.commandBufferCount = 1;
196 VkCommandBuffer cmd;
197 VK(vkAllocateCommandBuffers(c.dev, &cbi, &cmd));
198 VkCommandBufferBeginInfo cbb{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
199 cbb.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
200 VK(vkBeginCommandBuffer(cmd, &cbb));
201 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, pipe);
202 vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, playout,
203 0, 1, &dset, 0, nullptr);
204 vkCmdPushConstants(cmd, playout, VK_SHADER_STAGE_COMPUTE_BIT, 0,
205 sizeof(PushC), &pc);
206 vkCmdDispatch(cmd, (w + 7) / 8, (rows + 7) / 8, 1);
207 VK(vkEndCommandBuffer(cmd));
208 VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO};
209 si.commandBufferCount = 1; si.pCommandBuffers = &cmd;
210 VK(vkQueueSubmit(c.queue, 1, &si, VK_NULL_HANDLE));
211 VK(vkQueueWaitIdle(c.queue));
212 vkFreeCommandBuffers(c.dev, c.cmdpool, 1, &cmd);
213
214 memcpy(out.data() + (size_t)row0 * w, buf.mapped,
215 (size_t)rows * w * sizeof(float));
216 }
217 }
218};
219
220// ---------------------------------------------------------------------------
221static void set_param(ErosionWrapperParams& p, const char* k, const char* v) {
222 float f = (float)atof(v);
223 int i = atoi(v);
224 if (!strcmp(k, "floor_y")) p.floor_y = f;
225 else if (!strcmp(k, "ceiling_y")) p.ceiling_y = f;
226 else if (!strcmp(k, "terrain_height")) p.terrain_height = f;
227 else if (!strcmp(k, "mountain_scale")) p.mountain_scale = f;
228 else if (!strcmp(k, "mountain_height")) p.mountain_height = f;
229 else if (!strcmp(k, "mountain_gain")) p.mountain_gain = f;
230 else if (!strcmp(k, "mountain_variance")) p.mountain_variance = f;
231 else if (!strcmp(k, "mountain_octaves")) p.mountain_octaves = i;
232 else if (!strcmp(k, "relief_scale")) p.relief_scale = f;
233 else if (!strcmp(k, "relief_floor")) p.relief_floor = f;
234 else if (!strcmp(k, "detail_scale")) p.detail_scale = f;
235 else if (!strcmp(k, "detail_height")) p.detail_height = f;
236 else if (!strcmp(k, "detail_gain")) p.detail_gain = f;
237 else if (!strcmp(k, "detail_octaves")) p.detail_octaves = i;
238 else if (!strcmp(k, "noise_lacunarity")) p.noise_lacunarity = f;
239 else if (!strcmp(k, "voxel_size")) p.voxel_size = f;
240 else if (!strcmp(k, "gn_scale")) p.gn_scale = f;
241 else if (!strcmp(k, "gn_gain")) p.gn_gain = f;
242 else if (!strcmp(k, "gn_detail")) p.gn_detail = i;
243 else if (!strcmp(k, "en_height_scale")) p.en_height_scale = f;
244 else if (!strcmp(k, "en_strength")) p.en_strength = f;
245 else if (!strcmp(k, "en_cell_scale")) p.en_cell_scale = f;
246 else if (!strcmp(k, "en_gain")) p.en_gain = f;
247 else if (!strcmp(k, "en_octaves")) p.en_octaves = i;
248 else if (!strcmp(k, "en_erosion_scale")) p.en_erosion_scale = f;
249 else { fprintf(stderr, "unknown parameter '%s'\n", k); exit(2); }
250}
251
252int main(int argc, char** argv) {
253 const char* inc_path = "../proc-nodes/src/shared/erosion_filter.inc";
254 const char* out_path = "/tmp/h.f32";
255 int n = 512, steps = 512, bisect = 60, band = 32;
256 float mpp = 30.0f, ox = 0.0f, oz = 0.0f;
257 bool verify = false;
258
259 // NOT ErosionWrapperParams{} -- the struct's own defaults are a convenience
260 // and are not authoritative. The map's values come from the oracle's single
261 // mapping, so a constant that moves there moves here in the same commit.
262 // Hand-copying them instead is what put this renderer 7.3 km out on its
263 // first run, and the --verify gate is the only reason that was noticed.
264 ErosionWrapperParams wp = erosion_wrapper_params_from_map();
265
266 for (int i = 1; i < argc; i++) {
267 const char* a = argv[i];
268 if (!strcmp(a, "--inc") && i+1 < argc) inc_path = argv[++i];
269 else if (!strcmp(a, "--out") && i+1 < argc) out_path = argv[++i];
270 else if (!strcmp(a, "--n") && i+1 < argc) n = atoi(argv[++i]);
271 else if (!strcmp(a, "--mpp") && i+1 < argc) mpp = (float)atof(argv[++i]);
272 else if (!strcmp(a, "--ox") && i+1 < argc) ox = (float)atof(argv[++i]);
273 else if (!strcmp(a, "--oz") && i+1 < argc) oz = (float)atof(argv[++i]);
274 else if (!strcmp(a, "--steps") && i+1 < argc) steps = atoi(argv[++i]);
275 else if (!strcmp(a, "--bisect") && i+1 < argc) bisect = atoi(argv[++i]);
276 else if (!strcmp(a, "--band") && i+1 < argc) band = atoi(argv[++i]);
277 else if (!strcmp(a, "--verify")) verify = true;
278 else if (!strcmp(a, "--set") && i+2 < argc) { set_param(wp, argv[i+1], argv[i+2]); i += 2; }
279 else { fprintf(stderr, "unknown arg %s\n", a); return 2; }
280 }
281
282 std::string inc = read_file(inc_path);
283 std::string src = build_render_shader(inc, wp, steps, bisect);
284
285 Renderer r;
286 r.build(src);
287
288 if (verify) {
289 // The oracle's own columns, at MAP defaults, from a green run.
290 struct { float x, z, expect; } cols[] = {
291 {-10000.0f, -10000.0f, 5430.60f},
292 {-10000.0f, 0.0f, 4179.92f},
293 {-10000.0f, 10000.0f, -353.49f},
294 { 0.0f, -10000.0f, 4299.36f},
295 { 0.0f, 0.0f, -1059.81f},
296 { 0.0f, 10000.0f, -5454.78f},
297 { 10000.0f, -10000.0f, 215.53f},
298 { 10000.0f, 0.0f, -4136.03f},
299 { 10000.0f, 10000.0f, -5038.45f},
300 };
301 printf("VERIFY against test_erosion_parity's published columns\n");
302 printf(" scan steps %d, bisect %d (the oracle uses 512 / 60)\n\n", steps, bisect);
303 printf(" %11s %11s %13s %13s %12s\n", "x", "z", "oracle", "renderer", "delta");
304 double worst = 0.0;
305 for (auto& col : cols) {
306 std::vector<float> one;
307 r.render(col.x, col.z, 1, 1, 1.0f, 1, one);
308 double d = (double)one[0] - (double)col.expect;
309 if (fabs(d) > worst) worst = fabs(d);
310 printf(" %11.1f %11.1f %13.2f %13.2f %12.4f\n",
311 col.x, col.z, col.expect, one[0], d);
312 }
313 printf("\n worst |delta| : %.4f m\n", worst);
314 // The oracle prints 2 decimals, so agreement can only be shown to
315 // about half a centimetre. Anything above that is a real difference.
316 bool ok = worst < 0.01;
317 printf(" VERDICT: %s\n", ok ? "renderer MATCHES the oracle."
318 : "renderer DISAGREES with the oracle. Do not "
319 "trust any picture it makes.");
320 return ok ? 0 : 1;
321 }
322
323 printf("erosion render: %dx%d px @ %g m/px = %.2f km across\n",
324 n, n, mpp, n * mpp / 1000.0f);
325 printf(" origin (%g, %g) m voxel %g scan %d/%d band %d rows\n",
326 ox, oz, wp.voxel_size, steps, bisect, band);
327
328 std::vector<float> h;
329 r.render(ox, oz, (uint32_t)n, (uint32_t)n, mpp, (uint32_t)band, h);
330
331 long hits = 0; double lo = 1e30, hi = -1e30, sum = 0;
332 for (float v : h) {
333 if (v == NO_SURFACE) continue;
334 hits++; sum += v;
335 if (v < lo) lo = v;
336 if (v > hi) hi = v;
337 }
338 printf(" surface found in %ld of %d columns\n", hits, n * n);
339 if (hits) printf(" height min %.1f m max %.1f m mean %.1f m\n", lo, hi, sum / hits);
340
341 FILE* f = fopen(out_path, "wb");
342 if (!f) { perror("fopen"); return 1; }
343 fwrite(h.data(), sizeof(float), h.size(), f);
344 fclose(f);
345 printf(" wrote %zu floats to %s\n", h.size(), out_path);
346 return hits > 0 ? 0 : 1;
347}
index.html +480 -0
……@@ -0,0 +1,480 @@
1<!doctype html>
2<meta charset="utf-8">
3<title>Terrain Lookdev</title>
4<style>
5:root{
6 --bg:#12161a; --panel:#191f25; --edge:#28313a; --edge2:#3a4652;
7 --ink:#e2e8e6; --dim:#8c9aa1; --accent:#74c2cd; --warn:#d7a45c;
8 font-family:"IBM Plex Sans",system-ui,sans-serif;
9}
10*{box-sizing:border-box}
11body{margin:0;background:var(--bg);color:var(--ink);font-size:13px;height:100vh;
12 display:grid;grid-template-columns:290px 1fr;overflow:hidden}
13h1{font-size:13px;margin:0 0 2px;letter-spacing:.02em}
14.sub{color:var(--dim);font-size:11px;margin:0 0 14px;line-height:1.45}
15
16aside{background:var(--panel);border-right:1px solid var(--edge);
17 padding:14px;overflow-y:auto}
18fieldset{border:0;border-top:1px solid var(--edge);margin:14px 0 0;padding:12px 0 0}
19legend{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10px;
20 letter-spacing:.12em;text-transform:uppercase;color:var(--dim);padding:0}
21label{display:grid;grid-template-columns:1fr auto;gap:2px 8px;margin:9px 0;
22 font-size:11.5px;align-items:center}
23label .v{font-family:"IBM Plex Mono",ui-monospace,monospace;color:var(--accent);
24 font-size:11px;font-variant-numeric:tabular-nums}
25input[type=range]{grid-column:1/-1;width:100%;accent-color:var(--accent);height:16px}
26input[type=number],select{background:#0e1216;color:var(--ink);border:1px solid var(--edge2);
27 border-radius:3px;padding:3px 5px;font:inherit;font-size:11.5px;width:100%}
28button{background:#22303a;color:var(--ink);border:1px solid var(--edge2);border-radius:3px;
29 padding:6px 10px;font:inherit;font-size:11.5px;cursor:pointer}
30button:hover{border-color:var(--accent)}
31button.primary{background:var(--accent);color:#0d1417;border-color:var(--accent);font-weight:600}
32.row{display:flex;gap:6px;margin:8px 0}
33.row>*{flex:1}
34
35main{display:flex;flex-direction:column;overflow:hidden}
36.bar{display:flex;gap:14px;align-items:center;padding:9px 14px;
37 border-bottom:1px solid var(--edge);background:var(--panel);flex-wrap:wrap}
38.bar .grow{flex:1}
39.views{display:flex;gap:0;border:1px solid var(--edge2);border-radius:3px;overflow:hidden}
40.views button{border:0;border-radius:0;border-right:1px solid var(--edge2);padding:5px 11px}
41.views button:last-child{border-right:0}
42.views button[aria-pressed=true]{background:var(--accent);color:#0d1417;font-weight:600}
43
44.panes{flex:1;display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--edge);
45 overflow:hidden;min-height:0}
46.pane{background:#0d1114;display:flex;flex-direction:column;min-height:0;position:relative}
47.pane h2{margin:0;padding:7px 12px;font-size:11px;font-weight:600;letter-spacing:.02em;
48 border-bottom:1px solid var(--edge);display:flex;gap:10px;align-items:baseline}
49.pane h2 .tag{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:9.5px;
50 letter-spacing:.1em;text-transform:uppercase;color:var(--dim)}
51.imgwrap{flex:1;display:flex;align-items:center;justify-content:center;
52 overflow:hidden;padding:10px;min-height:0}
53.imgwrap img{width:100%;height:100%;object-fit:contain;image-rendering:pixelated;
54 border:1px solid var(--edge);display:block}
55.imgwrap canvas{width:100%;height:100%;border:1px solid var(--edge);display:block;
56 cursor:grab;touch-action:none}
57.imgwrap canvas:active{cursor:grabbing}
58.stats{font-family:"IBM Plex Mono",ui-monospace,monospace;font-size:10.5px;color:var(--dim);
59 padding:6px 12px;border-top:1px solid var(--edge);white-space:pre-wrap;
60 font-variant-numeric:tabular-nums;min-height:26px}
61.busy{position:absolute;inset:0;display:grid;place-items:center;background:#0d1114cc;
62 font-family:"IBM Plex Mono",monospace;font-size:11px;color:var(--accent)}
63/* display:grid above beats the hidden attribute's UA display:none, so the
64 overlay would never go away. State lives on the attribute; say so here. */
65.busy[hidden]{display:none}
66img[hidden],canvas[hidden]{display:none}
67.err{color:var(--warn)}
68.note{font-size:10.5px;color:var(--dim);line-height:1.5;margin-top:12px;
69 border-top:1px solid var(--edge);padding-top:10px}
70kbd{font-family:"IBM Plex Mono",monospace;background:#0e1216;border:1px solid var(--edge2);
71 border-radius:2px;padding:0 4px;font-size:10px}
72</style>
73
74<aside>
75 <h1>Terrain Lookdev</h1>
76 <p class="sub">hyper-games #46 §4 measured that these two differ in roughness.
77 It cannot say which is better to look at. That is what this is for.</p>
78
79 <div class="row">
80 <button class="primary" id="render">Render</button>
81 <button id="reset">Reset</button>
82 </div>
83 <label>Auto-render on change <input type="checkbox" id="auto" checked style="width:auto"></label>
84
85 <fieldset><legend>View</legend>
86 <label>Tile size (px)<span class="v" id="v_n">384</span>
87 <input type="range" id="n" min="128" max="768" step="64" value="384"></label>
88 <label>Metres per pixel<span class="v" id="v_mpp">30</span>
89 <input type="range" id="mpp" min="2" max="200" step="1" value="30"></label>
90 <label>Scan steps (quality)<span class="v" id="v_steps">256</span>
91 <input type="range" id="steps" min="64" max="768" step="32" value="256"></label>
92 <label>Erosion origin X (m)<input type="number" id="ox" value="0" step="1000"></label>
93 <label>Erosion origin Z (m)<input type="number" id="oz" value="0" step="1000"></label>
94 <label>Diffusion i1 (px)<input type="number" id="i1" value="0" step="256"></label>
95 <label>Diffusion j1 (px)<input type="number" id="j1" value="2048" step="256"></label>
96 <label id="exagrow" hidden>3D vertical exaggeration<span class="v" id="v_exag">1</span>
97 <input type="range" id="exag" min="0.2" max="6" step="0.1" value="1"></label>
98 </fieldset>
99
100 <fieldset><legend>Mountain layer</legend>
101 <div id="grp_mountain"></div>
102 </fieldset>
103 <fieldset><legend>Erosion filter</legend>
104 <div id="grp_erosion"></div>
105 </fieldset>
106 <fieldset><legend>Detail + base noise</legend>
107 <div id="grp_detail"></div>
108 </fieldset>
109
110 <p class="note">Both panels are shaded by the same code, so a difference on
111 screen is a difference in the terrain and not in the renderer.
112 Diffusion is always at its native 30&nbsp;m/px — its <code>scale</code> knob
113 is bilinear (#46 §4.1) and would only blur.
114 Drag on a panel to pan. <kbd>R</kbd> renders.</p>
115</aside>
116
117<main>
118 <div class="bar">
119 <div class="views" id="modes">
120 <button data-m="2d" aria-pressed="true">2D</button>
121 <button data-m="3d" aria-pressed="false">3D</button>
122 </div>
123 <div class="views" id="views">
124 <button data-v="shaded-height" aria-pressed="true">Shaded height</button>
125 <button data-v="shade" aria-pressed="false">Hillshade</button>
126 <button data-v="slope" aria-pressed="false">Slope</button>
127 <button data-v="height" aria-pressed="false">Height</button>
128 </div>
129 <div class="grow"></div>
130 <div class="stats" id="scale" style="border:0;padding:0"></div>
131 </div>
132
133 <div class="panes">
134 <section class="pane">
135 <h2>Our erosion filter <span class="tag">erosion_filter.inc · gpu</span></h2>
136 <div class="imgwrap"><img id="img_e" alt="erosion render">
137 <canvas id="cv_e" hidden></canvas></div>
138 <div class="stats" id="st_e">—</div>
139 <div class="busy" id="busy_e" hidden>rendering…</div>
140 </section>
141 <section class="pane">
142 <h2>terrain-diffusion <span class="tag">30 m model · seed 1234</span></h2>
143 <div class="imgwrap"><img id="img_d" alt="diffusion tile">
144 <canvas id="cv_d" hidden></canvas></div>
145 <div class="stats" id="st_d">—</div>
146 <div class="busy" id="busy_d" hidden>fetching…</div>
147 </section>
148 </div>
149</main>
150
151<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.160.0/three.min.js"></script>
152<script>
153const GROUPS = {
154 mountain: [["terrain_height",60000,1000,200000,500],["mountain_scale",1,0.05,8,0.05],
155 ["mountain_height",0.55,0,2,0.01],["mountain_gain",0.68,0.2,0.9,0.01],
156 ["mountain_variance",0,0,1,0.01],["mountain_octaves",8,1,12,1],
157 ["relief_scale",0.5,0,8,0.05],["relief_floor",0.08,0,1,0.01]],
158 erosion: [["en_strength",0.15,0,1.5,0.005],["en_erosion_scale",0.15,0.002,0.5,0.002],
159 ["en_cell_scale",0.7,0.05,4,0.01],["en_gain",0.5,0.2,0.9,0.01],
160 ["en_octaves",7,1,12,1],["en_height_scale",5,0.1,40,0.1]],
161 detail: [["detail_scale",600,0,5000,10],["detail_height",30,0,500,1],
162 ["detail_gain",0.5,0.2,0.9,0.01],["detail_octaves",4,1,10,1],
163 ["noise_lacunarity",1.87,1.2,3,0.01],["gn_scale",3,0.1,40,0.1],
164 ["gn_gain",0.1,0,0.9,0.01],["gn_detail",3,1,8,1],
165 ["voxel_size",1,0.25,512,0.25]]
166};
167const $ = id => document.getElementById(id);
168let view = "shaded-height";
169let mode = "2d";
170
171for (const [grp, items] of Object.entries(GROUPS)) {
172 const host = $("grp_" + grp);
173 for (const [key, def, lo, hi, step] of items) {
174 const l = document.createElement("label");
175 l.innerHTML = `${key.replace(/_/g," ")}<span class="v" id="v_${key}">${def}</span>
176 <input type="range" id="${key}" min="${lo}" max="${hi}" step="${step}" value="${def}">`;
177 host.appendChild(l);
178 }
179}
180
181function knobParams() {
182 const p = new URLSearchParams();
183 for (const items of Object.values(GROUPS))
184 for (const [key] of items) p.set(key, $(key).value);
185 return p;
186}
187
188function syncLabels() {
189 for (const id of ["n","mpp","steps","exag"]) $("v_"+id).textContent = $(id).value;
190 for (const items of Object.values(GROUPS))
191 for (const [key] of items) $("v_"+key).textContent = $(key).value;
192 const n = +$("n").value, mpp = +$("mpp").value;
193 $("scale").textContent =
194 `erosion ${(n*mpp/1000).toFixed(2)} km @ ${mpp} m/px · ` +
195 `diffusion ${(n*30/1000).toFixed(2)} km @ 30 m/px` +
196 (mpp !== 30 ? " · SCALES DIFFER" : "");
197 $("scale").className = "stats" + (mpp !== 30 ? " err" : "");
198}
199
200async function load(which, url, imgId, stId, busyId) {
201 $(busyId).hidden = false;
202 const t0 = performance.now();
203 try {
204 const r = await fetch(url);
205 if (!r.ok) throw new Error(await r.text());
206 const b = await r.blob();
207 const old = $(imgId).src;
208 $(imgId).src = URL.createObjectURL(b);
209 if (old.startsWith("blob:")) URL.revokeObjectURL(old);
210 const h = r.headers;
211 const ms = (performance.now() - t0).toFixed(0);
212 let s = `min ${h.get("X-Min")} max ${h.get("X-Max")} ` +
213 `mean ${h.get("X-Mean")} sd ${h.get("X-Sd")} m ${ms} ms`;
214 if (h.get("X-Climate")) s += `\n${h.get("X-Climate")}`;
215 $(stId).textContent = s;
216 $(stId).classList.remove("err");
217 } catch (e) {
218 $(stId).textContent = String(e.message || e);
219 $(stId).classList.add("err");
220 } finally {
221 $(busyId).hidden = true;
222 }
223}
224
225// ---------------------------------------------------------------------------
226// 3D. A different VIEW of the same heightfield the 2D panel shows -- same
227// endpoint, same render path -- so the two tabs can never disagree about the
228// terrain. Shading is a single directional light placed at the same azimuth
229// and altitude as the 2D hillshade (315 deg / 45 deg), for the same reason the
230// two panels share one shader: a difference on screen should be the terrain.
231// ---------------------------------------------------------------------------
232const V3 = {}; // per-pane viewer state
233
234function makeViewer(canvasId) {
235 const canvas = $(canvasId);
236 const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
237 renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
238 const scene = new THREE.Scene();
239 scene.background = new THREE.Color(0x0d1114);
240
241 const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
242 const sun = new THREE.DirectionalLight(0xfff2e0, 2.1);
243 // azimuth 315 (NW), altitude 45 -- matching hillshade() in server.py
244 sun.position.set(-0.707 * 0.707, 0.707, -0.707 * 0.707).normalize();
245 scene.add(sun);
246 scene.add(new THREE.HemisphereLight(0x93a8c4, 0x201a14, 0.55));
247
248 const st = {renderer, scene, camera, mesh: null,
249 yaw: -0.7, pitch: 0.62, dist: 2.4, dirty: true};
250 V3[canvasId] = st;
251
252 let down = false, sx = 0, sy = 0, y0 = 0, p0 = 0;
253 canvas.addEventListener("pointerdown", e => {
254 down = true; sx = e.clientX; sy = e.clientY; y0 = st.yaw; p0 = st.pitch;
255 canvas.setPointerCapture(e.pointerId);
256 });
257 canvas.addEventListener("pointermove", e => {
258 if (!down) return;
259 st.yaw = y0 - (e.clientX - sx) * 0.006;
260 st.pitch = Math.max(0.05, Math.min(1.5, p0 + (e.clientY - sy) * 0.005));
261 st.dirty = true;
262 });
263 canvas.addEventListener("pointerup", () => down = false);
264 canvas.addEventListener("wheel", e => {
265 e.preventDefault();
266 st.dist = Math.max(0.6, Math.min(8, st.dist * (1 + Math.sign(e.deltaY) * 0.12)));
267 st.dirty = true;
268 }, {passive: false});
269 return st;
270}
271
272// One shared frame loop. Only redraws panes marked dirty, so an idle 3D tab
273// costs nothing.
274function frame() {
275 for (const [id, st] of Object.entries(V3)) {
276 const cv = $(id);
277 if (cv.hidden || !st.mesh) continue;
278 const w = cv.clientWidth | 0, h = cv.clientHeight | 0;
279 if (!w || !h) continue;
280 if (cv.width !== w * st.renderer.getPixelRatio() ||
281 cv.height !== h * st.renderer.getPixelRatio()) {
282 st.renderer.setSize(w, h, false);
283 st.camera.aspect = w / h;
284 st.camera.updateProjectionMatrix();
285 st.dirty = true;
286 }
287 if (!st.dirty) continue;
288 const d = st.dist, cp = Math.cos(st.pitch);
289 st.camera.position.set(Math.sin(st.yaw) * cp * d, Math.sin(st.pitch) * d,
290 Math.cos(st.yaw) * cp * d);
291 st.camera.lookAt(0, 0, 0);
292 st.renderer.render(st.scene, st.camera);
293 st.dirty = false;
294 }
295 requestAnimationFrame(frame);
296}
297requestAnimationFrame(frame);
298
299function buildMesh(st, heights, n, mpp, lo, hi, exag) {
300 if (st.mesh) {
301 st.scene.remove(st.mesh);
302 st.mesh.geometry.dispose();
303 st.mesh.material.dispose();
304 }
305 // The tile is normalised to 1 unit wide, so the two panes stay comparable
306 // even when their metres-per-pixel differ. Height keeps its TRUE ratio to
307 // the ground extent -- exaggeration is an explicit, labelled multiplier and
308 // never a silent auto-fit, or the 3D view would flatter whichever generator
309 // has less relief.
310 const widthM = n * mpp;
311 const g = new THREE.PlaneGeometry(1, 1, n - 1, n - 1);
312 g.rotateX(-Math.PI / 2);
313 const pos = g.attributes.position;
314 const col = new Float32Array(n * n * 3);
315 const span = Math.max(hi - lo, 1e-6);
316 for (let i = 0; i < n * n; i++) {
317 const y = heights[i];
318 pos.array[i * 3 + 1] = (y / widthM) * exag;
319 const t = (y - lo) / span;
320 col[i * 3 + 0] = 0.20 + 0.75 * t;
321 col[i * 3 + 1] = 0.30 + 0.55 * t;
322 col[i * 3 + 2] = 0.38 + 0.22 * (1 - t);
323 }
324 // Recentre so orbiting is about the terrain, not about y = 0.
325 const mid = ((lo + hi) * 0.5 / widthM) * exag;
326 for (let i = 0; i < n * n; i++) pos.array[i * 3 + 1] -= mid;
327 pos.needsUpdate = true;
328 g.setAttribute("color", new THREE.BufferAttribute(col, 3));
329 g.computeVertexNormals();
330
331 const m = new THREE.MeshStandardMaterial({vertexColors: true, roughness: 0.95,
332 metalness: 0.0, flatShading: false});
333 st.mesh = new THREE.Mesh(g, m);
334 st.scene.add(st.mesh);
335 st.dirty = true;
336}
337
338async function load3d(url, canvasId, stId, busyId) {
339 $(busyId).hidden = false;
340 const t0 = performance.now();
341 try {
342 const r = await fetch(url);
343 if (!r.ok) throw new Error(await r.text());
344 const buf = await r.arrayBuffer();
345 const h = r.headers;
346 const n = +h.get("X-N"), mpp = +h.get("X-Mpp");
347 const lo = +h.get("X-Min"), hi = +h.get("X-Max");
348 const heights = new Float32Array(buf);
349 if (heights.length !== n * n) throw new Error(`expected ${n*n} floats, got ${heights.length}`);
350 const st = V3[canvasId] || makeViewer(canvasId);
351 buildMesh(st, heights, n, mpp, lo, hi, +$("exag").value);
352 const ms = (performance.now() - t0).toFixed(0);
353 let s = `min ${h.get("X-Min")} max ${h.get("X-Max")} mean ${h.get("X-Mean")} ` +
354 `sd ${h.get("X-Sd")} m ${n}x${n} @ ${mpp} m/px ${ms} ms`;
355 if (+h.get("X-Holes")) s += ` ${h.get("X-Holes")} holes filled`;
356 if (h.get("X-Climate")) s += `\n${h.get("X-Climate")}`;
357 $(stId).textContent = s;
358 $(stId).classList.remove("err");
359 } catch (e) {
360 $(stId).textContent = String(e.message || e);
361 $(stId).classList.add("err");
362 } finally {
363 $(busyId).hidden = true;
364 }
365}
366
367function erosionQuery() {
368 const p = knobParams();
369 p.set("n", $("n").value); p.set("mpp", $("mpp").value);
370 p.set("steps", $("steps").value);
371 p.set("ox", $("ox").value); p.set("oz", $("oz").value);
372 return p;
373}
374
375function render() {
376 syncLabels();
377 const p = erosionQuery();
378 const q = new URLSearchParams({n: $("n").value, i1: $("i1").value, j1: $("j1").value});
379
380 if (mode === "3d") {
381 load3d("/api/erosion.bin?" + p, "cv_e", "st_e", "busy_e");
382 load3d("/api/diffusion.bin?" + q, "cv_d", "st_d", "busy_d");
383 } else {
384 p.set("view", view); q.set("view", view);
385 load("e", "/api/erosion.png?" + p, "img_e", "st_e", "busy_e");
386 load("d", "/api/diffusion.png?" + q, "img_d", "st_d", "busy_d");
387 }
388}
389
390let timer = null;
391function schedule() {
392 syncLabels();
393 if (!$("auto").checked) return;
394 clearTimeout(timer);
395 timer = setTimeout(render, 250);
396}
397
398document.addEventListener("input", e => {
399 if (e.target.type === "range" || e.target.type === "number") schedule();
400});
401$("render").onclick = render;
402$("reset").onclick = () => {
403 for (const items of Object.values(GROUPS))
404 for (const [key, def] of items) $(key).value = def;
405 render();
406};
407$("views").onclick = e => {
408 const b = e.target.closest("button"); if (!b) return;
409 view = b.dataset.v;
410 for (const x of $("views").children) x.setAttribute("aria-pressed", x === b);
411 render();
412};
413$("modes").onclick = e => {
414 const b = e.target.closest("button"); if (!b) return;
415 mode = b.dataset.m;
416 for (const x of $("modes").children) x.setAttribute("aria-pressed", x === b);
417 const three = mode === "3d";
418 $("views").style.display = three ? "none" : "";
419 $("exagrow").hidden = !three;
420 for (const [img, cv] of [["img_e","cv_e"], ["img_d","cv_d"]]) {
421 $(img).hidden = three;
422 $(cv).hidden = !three;
423 }
424 if (three && !V3.cv_e) { makeViewer("cv_e"); makeViewer("cv_d"); }
425 render();
426};
427
428// Exaggeration only reshapes the existing mesh; no refetch.
429$("exag").addEventListener("input", () => {
430 $("v_exag").textContent = $("exag").value;
431 if (mode === "3d") render();
432});
433
434document.addEventListener("keydown", e => {
435 if (e.key === "r" && !/INPUT|SELECT/.test(document.activeElement.tagName)) render();
436});
437
438// Drag to pan. Erosion moves in metres, diffusion in its own pixels; both move
439// by the same number of PIXELS so the two views stay in step visually.
440for (const [imgId, xId, yId, unit] of
441 [["img_e","ox","oz",() => +$("mpp").value], ["img_d","i1","j1",() => 1]]) {
442 let sx = 0, sy = 0, x0 = 0, y0 = 0, down = false;
443 $(imgId).addEventListener("pointerdown", e => {
444 down = true; sx = e.clientX; sy = e.clientY;
445 x0 = +$(xId).value; y0 = +$(yId).value;
446 $(imgId).setPointerCapture(e.pointerId);
447 });
448 $(imgId).addEventListener("pointermove", e => {
449 if (!down) return;
450 const px = ($(imgId).naturalWidth || 1) / ($(imgId).clientWidth || 1);
451 const u = unit();
452 $(xId).value = Math.round(x0 - (e.clientX - sx) * px * u);
453 $(yId).value = Math.round(y0 - (e.clientY - sy) * px * u);
454 });
455 $(imgId).addEventListener("pointerup", () => { if (down) { down = false; render(); } });
456}
457
458// URL-driven initial state, so a view can be linked to and so headless
459// verification can reach the 3D tab without a click.
460// ?mode=3d&view=slope&ox=...&oz=...&i1=...&j1=...&n=...&mpp=... plus any knob
461(function fromUrl() {
462 const q = new URLSearchParams(location.search);
463 for (const [k, v] of q) {
464 if (k === "mode" || k === "view") continue;
465 const el = $(k);
466 if (el && (el.type === "range" || el.type === "number")) el.value = v;
467 }
468 if (q.get("view")) {
469 const b = [...$("views").children].find(x => x.dataset.v === q.get("view"));
470 if (b) { view = b.dataset.v;
471 for (const x of $("views").children) x.setAttribute("aria-pressed", x === b); }
472 }
473 if (q.get("mode") === "3d") {
474 const b = [...$("modes").children].find(x => x.dataset.m === "3d");
475 if (b) b.click(); // click() runs the full tab handler, incl. render()
476 return true;
477 }
478 return false;
479})() || (syncLabels(), render());
480</script>
server.py +323 -0
……@@ -0,0 +1,323 @@
1#!/usr/bin/env python3
2"""
3LOOKDEV SERVER — our erosion filter and terrain-diffusion, side by side, at a
4matched scale, with the knobs live.
5
6hyper-games #46. §4 measured that the two generators differ in roughness and
7said nothing about which is better to look at. That is the open question and it
8is aesthetic, so the only instrument that can answer it is a pair of eyes with
9both images at the same scale.
10
11 EROSION panel -> shells out to erosion_render, which compiles
12 erosion_filter.inc off disk and the wrapper from
13 erosion_wrapper_glsl.h. Nothing is reimplemented here.
14 DIFFUSION panel -> proxies the terrain_diffusion API.
15
16Both are shaded by the SAME code below, because otherwise you are comparing two
17renderers rather than two terrains.
18
19 python server.py --port 8818
20"""
21import argparse, hashlib, json, os, subprocess, sys, urllib.request
22from collections import OrderedDict
23
24import numpy as np
25from flask import Flask, Response, request, send_from_directory
26
27HERE = os.path.dirname(os.path.abspath(__file__))
28DC_TERRAIN = os.path.abspath(os.path.join(HERE, "..", "dc-terrain"))
29INC = os.path.abspath(os.path.join(HERE, "..", "proc-nodes", "src", "shared",
30 "erosion_filter.inc"))
31
32app = Flask(__name__, static_folder=None)
33
34# Every knob the renderer will accept, with its type and a sane range. The
35# server shells out, so this allowlist is also the injection boundary: a key
36# not in here never reaches argv, and a value is always re-emitted from a
37# parsed float/int, never passed through as text.
38KNOBS = {
39 "terrain_height": ("f", 1000.0, 200000.0),
40 "mountain_scale": ("f", 0.05, 20.0),
41 "mountain_height": ("f", 0.0, 2.0),
42 "mountain_gain": ("f", 0.2, 0.9),
43 "mountain_variance": ("f", 0.0, 1.0),
44 "mountain_octaves": ("i", 1, 12),
45 "relief_scale": ("f", 0.0, 8.0),
46 "relief_floor": ("f", 0.0, 1.0),
47 "detail_scale": ("f", 0.0, 5000.0),
48 "detail_height": ("f", 0.0, 500.0),
49 "detail_gain": ("f", 0.2, 0.9),
50 "detail_octaves": ("i", 1, 10),
51 "noise_lacunarity": ("f", 1.2, 3.0),
52 "voxel_size": ("f", 0.25, 512.0),
53 "gn_scale": ("f", 0.1, 40.0),
54 "gn_gain": ("f", 0.0, 0.9),
55 "gn_detail": ("i", 1, 8),
56 "en_height_scale": ("f", 0.1, 40.0),
57 "en_strength": ("f", 0.0, 1.5),
58 "en_cell_scale": ("f", 0.05, 4.0),
59 "en_gain": ("f", 0.2, 0.9),
60 "en_octaves": ("i", 1, 12),
61 "en_erosion_scale": ("f", 0.002, 0.5),
62}
63
64NO_SURFACE = -3.0e38
65_cache = OrderedDict()
66CACHE_MAX = 64
67
68
69def _clamp(v, lo, hi):
70 return lo if v < lo else (hi if v > hi else v)
71
72
73def parse_knobs(args):
74 """Allowlist + clamp. Nothing else is ever put on the command line."""
75 out = {}
76 for k, (kind, lo, hi) in KNOBS.items():
77 raw = args.get(k)
78 if raw is None or raw == "":
79 continue
80 try:
81 v = float(raw)
82 except ValueError:
83 raise ValueError(f"{k}: not a number")
84 if v != v or v in (float("inf"), float("-inf")):
85 raise ValueError(f"{k}: not finite")
86 v = _clamp(v, lo, hi)
87 out[k] = int(round(v)) if kind == "i" else v
88 return out
89
90
91def render_erosion(ox, oz, n, mpp, knobs, steps, bisect):
92 key = hashlib.sha1(json.dumps(
93 [ox, oz, n, mpp, sorted(knobs.items()), steps, bisect]).encode()).hexdigest()
94 if key in _cache:
95 _cache.move_to_end(key)
96 return _cache[key]
97
98 out = f"/tmp/lookdev-{key}.f32"
99 cmd = ["/tmp/erosion_render", "--inc", INC, "--out", out,
100 "--n", str(int(n)), "--mpp", repr(float(mpp)),
101 "--ox", repr(float(ox)), "--oz", repr(float(oz)),
102 "--steps", str(int(steps)), "--bisect", str(int(bisect))]
103 for k, v in knobs.items():
104 cmd += ["--set", k, repr(v)]
105 p = subprocess.run(cmd, cwd=DC_TERRAIN, capture_output=True, text=True, timeout=300)
106 if p.returncode != 0:
107 raise RuntimeError((p.stderr or p.stdout or "render failed").strip()[:500])
108
109 h = np.fromfile(out, dtype="<f4").reshape(int(n), int(n)).astype(np.float64)
110 os.unlink(out)
111 h = np.where(h <= NO_SURFACE / 2, np.nan, h)
112
113 _cache[key] = h
114 while len(_cache) > CACHE_MAX:
115 _cache.popitem(last=False)
116 return h
117
118
119def fetch_diffusion(url, i1, j1, n):
120 q = f"{url}/terrain?i1={int(i1)}&j1={int(j1)}&i2={int(i1)+int(n)}&j2={int(j1)+int(n)}&scale=1"
121 with urllib.request.urlopen(q, timeout=600) as r:
122 hh = int(r.headers["X-Height"]); ww = int(r.headers["X-Width"])
123 body = r.read()
124 nelev = hh * ww * 2
125 elev = np.frombuffer(body[:nelev], dtype="<i2").reshape(hh, ww).astype(np.float64)
126 climate = None
127 if len(body) > nelev:
128 climate = np.frombuffer(body[nelev:], dtype="<f4").reshape(hh, ww, 4)
129 return elev, climate
130
131
132# ---------------------------------------------------------------------------
133# Shading. ONE implementation, used for both panels — the whole point is that
134# a difference on screen is a difference in the terrain.
135# ---------------------------------------------------------------------------
136def hillshade(h, mpp, azimuth=315.0, altitude=45.0, exaggeration=1.0):
137 hh = np.nan_to_num(h, nan=float(np.nanmean(h)) if np.isfinite(h).any() else 0.0)
138 gy, gx = np.gradient(hh * exaggeration, mpp)
139 slope = np.arctan(np.hypot(gx, gy))
140 aspect = np.arctan2(-gy, gx)
141 az = np.radians(360.0 - azimuth + 90.0)
142 alt = np.radians(altitude)
143 shade = (np.sin(alt) * np.cos(slope) +
144 np.cos(alt) * np.sin(slope) * np.cos(az - aspect))
145 return np.clip(shade, 0.0, 1.0)
146
147
148def slope_map(h, mpp):
149 hh = np.nan_to_num(h, nan=float(np.nanmean(h)) if np.isfinite(h).any() else 0.0)
150 gy, gx = np.gradient(hh, mpp)
151 return np.degrees(np.arctan(np.hypot(gx, gy)))
152
153
154def to_png(rgb):
155 from PIL import Image
156 import io
157 im = Image.fromarray(rgb, mode="RGB")
158 buf = io.BytesIO()
159 im.save(buf, format="PNG", optimize=False, compress_level=1)
160 return buf.getvalue()
161
162
163def colourise(h, mpp, view, vmin=None, vmax=None):
164 """view: shade | height | slope | shaded-height"""
165 finite = h[np.isfinite(h)]
166 lo = float(vmin) if vmin is not None else (float(finite.min()) if finite.size else 0.0)
167 hi = float(vmax) if vmax is not None else (float(finite.max()) if finite.size else 1.0)
168 if hi <= lo:
169 hi = lo + 1.0
170
171 if view == "slope":
172 s = np.clip(slope_map(h, mpp) / 60.0, 0, 1)
173 rgb = np.stack([s, 1.0 - s * 0.65, 1.0 - s], axis=-1)
174 elif view == "height":
175 t = np.clip((h - lo) / (hi - lo), 0, 1)
176 t = np.nan_to_num(t)
177 rgb = np.stack([0.15 + 0.85 * t, 0.25 + 0.6 * t, 0.35 + 0.3 * (1 - t)], axis=-1)
178 else:
179 sh = hillshade(h, mpp)
180 if view == "shaded-height":
181 t = np.nan_to_num(np.clip((h - lo) / (hi - lo), 0, 1))
182 base = np.stack([0.20 + 0.75 * t, 0.30 + 0.55 * t,
183 0.38 + 0.22 * (1 - t)], axis=-1)
184 rgb = base * (0.35 + 0.65 * sh[..., None])
185 else:
186 rgb = np.repeat(sh[..., None], 3, axis=-1)
187
188 rgb = np.where(np.isfinite(h)[..., None], rgb, 0.0)
189 return to_png((np.clip(rgb, 0, 1) * 255).astype(np.uint8)), lo, hi
190
191
192def stats_headers(resp, h, lo, hi, extra=None):
193 finite = h[np.isfinite(h)]
194 resp.headers["X-Min"] = f"{finite.min():.1f}" if finite.size else "nan"
195 resp.headers["X-Max"] = f"{finite.max():.1f}" if finite.size else "nan"
196 resp.headers["X-Mean"] = f"{finite.mean():.1f}" if finite.size else "nan"
197 resp.headers["X-Sd"] = f"{finite.std():.1f}" if finite.size else "nan"
198 resp.headers["X-Ramp"] = f"{lo:.1f},{hi:.1f}"
199 if extra:
200 for k, v in extra.items():
201 resp.headers[k] = v
202 return resp
203
204
205# ---------------------------------------------------------------------------
206@app.get("/")
207def index():
208 return send_from_directory(HERE, "index.html")
209
210
211@app.get("/api/erosion.png")
212def api_erosion():
213 try:
214 a = request.args
215 n = int(_clamp(float(a.get("n", 384)), 64, 1024))
216 mpp = float(_clamp(float(a.get("mpp", 30)), 0.05, 20000))
217 ox = float(a.get("ox", 0)); oz = float(a.get("oz", 0))
218 steps = int(_clamp(float(a.get("steps", 256)), 32, 1024))
219 knobs = parse_knobs(a)
220 h = render_erosion(ox, oz, n, mpp, knobs, steps, 40)
221 png, lo, hi = colourise(h, mpp, a.get("view", "shaded-height"))
222 return stats_headers(Response(png, mimetype="image/png"), h, lo, hi,
223 {"X-Source": "erosion_render"})
224 except Exception as e:
225 return Response(str(e), status=400, mimetype="text/plain")
226
227
228@app.get("/api/diffusion.png")
229def api_diffusion():
230 try:
231 a = request.args
232 n = int(_clamp(float(a.get("n", 384)), 64, 1024))
233 i1 = int(float(a.get("i1", 0))); j1 = int(float(a.get("j1", 2048)))
234 h, climate = fetch_diffusion(app.config["TD_URL"], i1, j1, n)
235 png, lo, hi = colourise(h, 30.0, a.get("view", "shaded-height"))
236 extra = {"X-Source": "terrain-diffusion", "X-Mpp": "30"}
237 if climate is not None:
238 extra["X-Climate"] = (f"{climate[...,0].mean():.1f}C,"
239 f"{climate[...,1].mean():.0f},"
240 f"{climate[...,2].mean():.0f}mm,"
241 f"{climate[...,3].mean():.0f}%")
242 return stats_headers(Response(png, mimetype="image/png"), h, lo, hi, extra)
243 except Exception as e:
244 return Response(str(e), status=400, mimetype="text/plain")
245
246
247# ---------------------------------------------------------------------------
248# Raw heightfields, for the 3D tab. Same render path as the PNG endpoints --
249# the 3D view must be a different VIEW of the same field, never a different
250# field, or it stops being evidence about the thing on the left.
251# ---------------------------------------------------------------------------
252def _raw_response(h, mpp, extra=None):
253 finite = h[np.isfinite(h)]
254 lo = float(finite.min()) if finite.size else 0.0
255 hi = float(finite.max()) if finite.size else 1.0
256 # NaN does not survive a Float32Array cleanly in every browser path, so
257 # holes are sent as the floor and flagged by count.
258 filled = np.where(np.isfinite(h), h, lo).astype("<f4")
259 resp = Response(filled.tobytes(), mimetype="application/octet-stream")
260 resp.headers["X-N"] = str(h.shape[0])
261 resp.headers["X-Mpp"] = repr(float(mpp))
262 resp.headers["X-Min"] = f"{lo:.3f}"
263 resp.headers["X-Max"] = f"{hi:.3f}"
264 resp.headers["X-Mean"] = f"{finite.mean():.1f}" if finite.size else "nan"
265 resp.headers["X-Sd"] = f"{finite.std():.1f}" if finite.size else "nan"
266 resp.headers["X-Holes"] = str(int((~np.isfinite(h)).sum()))
267 if extra:
268 for k, v in extra.items():
269 resp.headers[k] = v
270 return resp
271
272
273@app.get("/api/erosion.bin")
274def api_erosion_bin():
275 try:
276 a = request.args
277 n = int(_clamp(float(a.get("n", 384)), 64, 1024))
278 mpp = float(_clamp(float(a.get("mpp", 30)), 0.05, 20000))
279 ox = float(a.get("ox", 0)); oz = float(a.get("oz", 0))
280 steps = int(_clamp(float(a.get("steps", 256)), 32, 1024))
281 h = render_erosion(ox, oz, n, mpp, parse_knobs(a), steps, 40)
282 return _raw_response(h, mpp, {"X-Source": "erosion_render"})
283 except Exception as e:
284 return Response(str(e), status=400, mimetype="text/plain")
285
286
287@app.get("/api/diffusion.bin")
288def api_diffusion_bin():
289 try:
290 a = request.args
291 n = int(_clamp(float(a.get("n", 384)), 64, 1024))
292 i1 = int(float(a.get("i1", 0))); j1 = int(float(a.get("j1", 2048)))
293 h, climate = fetch_diffusion(app.config["TD_URL"], i1, j1, n)
294 extra = {"X-Source": "terrain-diffusion"}
295 if climate is not None:
296 extra["X-Climate"] = (f"{climate[...,0].mean():.1f}C,"
297 f"{climate[...,1].mean():.0f},"
298 f"{climate[...,2].mean():.0f}mm,"
299 f"{climate[...,3].mean():.0f}%")
300 return _raw_response(h, 30.0, extra)
301 except Exception as e:
302 return Response(str(e), status=400, mimetype="text/plain")
303
304
305@app.get("/api/knobs")
306def api_knobs():
307 return Response(json.dumps({k: {"kind": v[0], "min": v[1], "max": v[2]}
308 for k, v in KNOBS.items()}),
309 mimetype="application/json")
310
311
312if __name__ == "__main__":
313 ap = argparse.ArgumentParser()
314 ap.add_argument("--port", type=int, default=8818)
315 ap.add_argument("--host", default="127.0.0.1")
316 ap.add_argument("--td-url", default="http://127.0.0.1:8817")
317 args = ap.parse_args()
318 if not os.path.exists("/tmp/erosion_render"):
319 sys.exit("build /tmp/erosion_render first — see erosion_render.cpp's header, "
320 "and run it with --verify before believing any picture it makes.")
321 app.config["TD_URL"] = args.td_url
322 print(f"lookdev on http://{args.host}:{args.port} (diffusion at {args.td_url})")
323 app.run(host=args.host, port=args.port, threaded=True)
spectra-mountain.csv +1790 -0
……@@ -0,0 +1,1790 @@
1scale,k,power
21,1,5.2687833258e+07
31,2,4.9792994088e+07
41,3,2.4994074186e+07
51,4,1.3073906787e+07
61,5,3.8223782999e+06
71,6,1.5221426949e+06
81,7,1.1774009313e+06
91,8,1.0481788303e+06
101,9,8.8397091917e+05
111,10,4.9330667115e+05
121,11,2.4753444462e+05
131,12,2.3414011349e+05
141,13,1.3515903262e+05
151,14,1.0027913770e+05
161,15,8.3561658310e+04
171,16,1.0234258655e+05
181,17,6.7220925381e+04
191,18,3.6412352785e+04
201,19,3.8097956857e+04
211,20,3.4141851727e+04
221,21,2.7769109860e+04
231,22,1.9993490346e+04
241,23,1.8149261896e+04
251,24,1.9175413147e+04
261,25,1.4034288300e+04
271,26,9.5102824381e+03
281,27,8.3137646973e+03
291,28,8.1425644040e+03
301,29,6.7442045927e+03
311,30,5.8814905915e+03
321,31,4.7546694406e+03
331,32,4.0229979156e+03
341,33,3.9487426108e+03
351,34,4.1570255193e+03
361,35,3.7199821331e+03
371,36,2.6562678364e+03
381,37,2.8272429221e+03
391,38,2.3850339022e+03
401,39,1.8118568494e+03
411,40,1.7575387586e+03
421,41,1.3962198347e+03
431,42,1.1561875305e+03
441,43,1.1117639269e+03
451,44,1.1071405852e+03
461,45,1.0362781210e+03
471,46,8.6217025882e+02
481,47,6.6687478556e+02
491,48,5.8467291901e+02
501,49,6.3405500638e+02
511,50,5.7214327263e+02
521,51,4.4839527442e+02
531,52,4.7666999581e+02
541,53,4.0777940116e+02
551,54,3.9244889453e+02
561,55,4.0144401461e+02
571,56,3.1566351086e+02
581,57,3.3433898004e+02
591,58,3.3609232811e+02
601,59,2.7267855555e+02
611,60,2.5126925300e+02
621,61,2.5824107656e+02
631,62,2.2593401362e+02
641,63,2.3576729224e+02
651,64,2.0198025354e+02
661,65,1.5516853902e+02
671,66,1.3398117366e+02
681,67,1.4050731068e+02
691,68,1.2561808461e+02
701,69,1.1484988780e+02
711,70,1.0666335412e+02
721,71,1.1114547981e+02
731,72,1.0678057710e+02
741,73,8.2328043430e+01
751,74,8.3427317503e+01
761,75,8.2349175262e+01
771,76,8.7829840748e+01
781,77,8.4571852159e+01
791,78,7.0124647557e+01
801,79,6.5373643130e+01
811,80,5.8169906416e+01
821,81,6.1301571861e+01
831,82,5.5534280142e+01
841,83,4.5679308632e+01
851,84,4.2513125405e+01
861,85,4.2915489843e+01
871,86,3.9262418571e+01
881,87,3.5789968299e+01
891,88,3.8544810429e+01
901,89,3.5704052761e+01
911,90,4.0646370237e+01
921,91,3.6495862290e+01
931,92,3.1642073968e+01
941,93,2.9887959528e+01
951,94,2.5482015399e+01
961,95,2.2125156043e+01
971,96,2.2614542629e+01
981,97,2.3867592979e+01
991,98,2.0453533046e+01
1001,99,2.0417273067e+01
1011,100,1.8191242102e+01
1021,101,1.7617264131e+01
1031,102,1.7742051416e+01
1041,103,1.8988704915e+01
1051,104,1.7278825237e+01
1061,105,1.4154413566e+01
1071,106,1.3173121949e+01
1081,107,1.3074501049e+01
1091,108,1.2766463483e+01
1101,109,1.2462716364e+01
1111,110,1.2002103033e+01
1121,111,1.1330812292e+01
1131,112,1.0774347534e+01
1141,113,1.1275903020e+01
1151,114,1.0145344722e+01
1161,115,9.1570601635e+00
1171,116,8.2444195096e+00
1181,117,8.2688823331e+00
1191,118,8.4460148617e+00
1201,119,7.6839847172e+00
1211,120,7.2306187968e+00
1221,121,6.4465775578e+00
1231,122,6.2603653766e+00
1241,123,6.6260113499e+00
1251,124,6.0190351255e+00
1261,125,5.9843175748e+00
1271,126,5.6740750529e+00
1281,127,5.0135757834e+00
1291,128,4.8644454596e+00
1301,129,4.8526750560e+00
1311,130,4.5620389406e+00
1321,131,4.4145968392e+00
1331,132,4.0426044754e+00
1341,133,3.9268719385e+00
1351,134,3.8868830014e+00
1361,135,3.6983613563e+00
1371,136,3.4705924557e+00
1381,137,3.1742219327e+00
1391,138,2.9746865547e+00
1401,139,2.9294360511e+00
1411,140,2.6058619013e+00
1421,141,2.7924095657e+00
1431,142,2.5458609160e+00
1441,143,2.2988727260e+00
1451,144,2.2752195413e+00
1461,145,2.3071075527e+00
1471,146,2.2551078163e+00
1481,147,1.9741853072e+00
1491,148,1.8820117796e+00
1501,149,1.6429593995e+00
1511,150,1.6530976801e+00
1521,151,1.6656089236e+00
1531,152,1.5299707318e+00
1541,153,1.4341580145e+00
1551,154,1.3485144415e+00
1561,155,1.3268882723e+00
1571,156,1.3831834555e+00
1581,157,1.2972350432e+00
1591,158,1.1967101713e+00
1601,159,1.1913516369e+00
1611,160,1.0938427935e+00
1621,161,1.0661608946e+00
1631,162,1.0889176596e+00
1641,163,1.0138837064e+00
1651,164,1.0496260983e+00
1661,165,9.9669911585e-01
1671,166,8.5780262940e-01
1681,167,7.8865817309e-01
1691,168,8.4656878721e-01
1701,169,8.8039326575e-01
1711,170,7.9223811849e-01
1721,171,7.4477128169e-01
1731,172,7.0646978254e-01
1741,173,6.9546165342e-01
1751,174,6.9947167385e-01
1761,175,6.5800173611e-01
1771,176,6.2984747056e-01
1781,177,6.4941050695e-01
1791,178,6.2412391717e-01
1801,179,6.0823983302e-01
1811,180,6.0524767207e-01
1821,181,6.0732426540e-01
1831,182,6.3569603332e-01
1841,183,5.1618500071e-01
1851,184,4.6161517919e-01
1861,185,4.8688682435e-01
1871,186,4.7564408414e-01
1881,187,4.7866245270e-01
1891,188,4.7029531967e-01
1901,189,4.7544067263e-01
1911,190,4.3724810498e-01
1921,191,4.2926133774e-01
1931,192,4.3560845052e-01
1941,193,4.2026994512e-01
1951,194,3.7676942401e-01
1961,195,3.6714027906e-01
1971,196,3.3901654094e-01
1981,197,3.6785441508e-01
1991,198,3.6950833294e-01
2001,199,3.3445071732e-01
2011,200,3.3068534266e-01
2021,201,3.1117622883e-01
2031,202,3.0180124879e-01
2041,203,2.8834712870e-01
2051,204,2.7279765269e-01
2061,205,2.7939084984e-01
2071,206,2.7225105441e-01
2081,207,2.7006778354e-01
2091,208,2.7420656439e-01
2101,209,2.5326310668e-01
2111,210,2.2829225551e-01
2121,211,2.1756406706e-01
2131,212,2.3979176884e-01
2141,213,2.5424816224e-01
2151,214,2.2468795794e-01
2161,215,2.1032978086e-01
2171,216,2.0988521801e-01
2181,217,1.9207200320e-01
2191,218,1.8761875015e-01
2201,219,1.8139671682e-01
2211,220,1.8088781967e-01
2221,221,1.7063957385e-01
2231,222,1.7281823930e-01
2241,223,1.7140492569e-01
2251,224,1.5976378874e-01
2261,225,1.5632892835e-01
2271,226,1.5384229373e-01
2281,227,1.6326986864e-01
2291,228,1.4819962601e-01
2301,229,1.5443740533e-01
2311,230,1.4351063695e-01
2321,231,1.3776671969e-01
2331,232,1.2795128482e-01
2341,233,1.2641826438e-01
2351,234,1.2691971036e-01
2361,235,1.2848041836e-01
2371,236,1.2979685283e-01
2381,237,1.3405718112e-01
2391,238,1.3051965589e-01
2401,239,1.2209519618e-01
2411,240,1.2137005773e-01
2421,241,1.1253233721e-01
2431,242,1.0531723682e-01
2441,243,1.0816322317e-01
2451,244,1.0929459925e-01
2461,245,1.1437495588e-01
2471,246,1.0866405815e-01
2481,247,1.1293691862e-01
2491,248,1.1170492018e-01
2501,249,1.0992064311e-01
2511,250,1.0175068551e-01
2521,251,9.9210369964e-02
2531,252,9.6174696952e-02
2541,253,9.2776193557e-02
2551,254,9.3449644023e-02
2561,255,9.1796255959e-02
2572,1,2.1166750661e+08
2582,2,1.9965013098e+08
2592,3,1.0003484704e+08
2602,4,5.2351756758e+07
2612,5,1.5301537449e+07
2622,6,6.0936100083e+06
2632,7,4.7122566132e+06
2642,8,4.1918494243e+06
2652,9,3.5335311491e+06
2662,10,1.9708962195e+06
2672,11,9.8828442631e+05
2682,12,9.3459774178e+05
2692,13,5.3902309248e+05
2702,14,3.9952900304e+05
2712,15,3.3234072791e+05
2722,16,4.0705367202e+05
2732,17,2.6701327262e+05
2742,18,1.4454576073e+05
2752,19,1.5112876821e+05
2762,20,1.3520619251e+05
2772,21,1.0985089396e+05
2782,22,7.9017104738e+04
2792,23,7.1614812368e+04
2802,24,7.5542533381e+04
2812,25,5.5254239990e+04
2822,26,3.7367321531e+04
2832,27,3.2619298705e+04
2842,28,3.1902034365e+04
2852,29,2.6372185781e+04
2862,30,2.2970852615e+04
2872,31,1.8536923535e+04
2882,32,1.5648613382e+04
2892,33,1.5331685794e+04
2902,34,1.6112757627e+04
2912,35,1.4394929759e+04
2922,36,1.0263388692e+04
2932,37,1.0890923897e+04
2942,38,9.1652598708e+03
2952,39,6.9507049682e+03
2962,40,6.7268721450e+03
2972,41,5.3275089853e+03
2982,42,4.3976603864e+03
2992,43,4.2231797605e+03
3002,44,4.1992166367e+03
3012,45,3.9226064030e+03
3022,46,3.2503349463e+03
3032,47,2.5071053558e+03
3042,48,2.1925360050e+03
3052,49,2.3715806373e+03
3062,50,2.1326575362e+03
3072,51,1.6681111325e+03
3082,52,1.7680891385e+03
3092,53,1.5072124487e+03
3102,54,1.4447243693e+03
3112,55,1.4726346392e+03
3122,56,1.1549215001e+03
3132,57,1.2184421611e+03
3142,58,1.2238107274e+03
3152,59,9.9075853696e+02
3162,60,9.0817443827e+02
3172,61,9.2757503323e+02
3182,62,8.1084500667e+02
3192,63,8.4318622020e+02
3202,64,7.1928933548e+02
3212,65,5.5081444478e+02
3222,66,4.7420688757e+02
3232,67,4.9575454669e+02
3242,68,4.4099841327e+02
3252,69,4.0164590367e+02
3262,70,3.7090583603e+02
3272,71,3.8518980932e+02
3282,72,3.6895678109e+02
3292,73,2.8262197159e+02
3302,74,2.8577945310e+02
3312,75,2.8120083249e+02
3322,76,2.9865563595e+02
3332,77,2.8593931811e+02
3342,78,2.3644081940e+02
3352,79,2.1916439580e+02
3362,80,1.9371415858e+02
3372,81,2.0302234417e+02
3382,82,1.8318956780e+02
3392,83,1.5005815018e+02
3402,84,1.3921790873e+02
3412,85,1.4017198091e+02
3422,86,1.2759732322e+02
3432,87,1.1553624192e+02
3442,88,1.2395079820e+02
3452,89,1.1416693798e+02
3462,90,1.2889687236e+02
3472,91,1.1479558217e+02
3482,92,9.9488953451e+01
3492,93,9.3317958092e+01
3502,94,7.9156590292e+01
3512,95,6.8698125448e+01
3522,96,6.9751602617e+01
3532,97,7.3039665469e+01
3542,98,6.2310738550e+01
3552,99,6.1794001959e+01
3562,100,5.4424632028e+01
3572,101,5.2488172004e+01
3582,102,5.2689814158e+01
3592,103,5.6016915792e+01
3602,104,5.0718406045e+01
3612,105,4.1078182701e+01
3622,106,3.8088832093e+01
3632,107,3.7813824960e+01
3642,108,3.6512776620e+01
3652,109,3.5401542607e+01
3662,110,3.3941266789e+01
3672,111,3.1730285565e+01
3682,112,2.9821707986e+01
3692,113,3.1046226097e+01
3702,114,2.8104029138e+01
3712,115,2.5000876138e+01
3722,116,2.2396637125e+01
3732,117,2.2232415741e+01
3742,118,2.2612317332e+01
3752,119,2.0470474723e+01
3762,120,1.9099773706e+01
3772,121,1.6853108298e+01
3782,122,1.6272054720e+01
3792,123,1.7058096499e+01
3802,124,1.5472950690e+01
3812,125,1.5403553380e+01
3822,126,1.4453581010e+01
3832,127,1.2609190201e+01
3842,128,1.2034511080e+01
3852,129,1.1970297420e+01
3862,130,1.1250772968e+01
3872,131,1.0728105845e+01
3882,132,9.7282845723e+00
3892,133,9.4179943151e+00
3902,134,9.3346488432e+00
3912,135,8.8295857024e+00
3922,136,8.0773773794e+00
3932,137,7.2925151974e+00
3942,138,6.7977696813e+00
3952,139,6.6367369399e+00
3962,140,5.8534850025e+00
3972,141,6.2257969102e+00
3982,142,5.6486971687e+00
3992,143,5.0391004791e+00
4002,144,4.9713459201e+00
4012,145,4.9811082466e+00
4022,146,4.8008138145e+00
4032,147,4.1887372143e+00
4042,148,4.0063145844e+00
4052,149,3.4164984293e+00
4062,150,3.4382777938e+00
4072,151,3.4658307601e+00
4082,152,3.1500350956e+00
4092,153,2.9234982410e+00
4102,154,2.7104361732e+00
4112,155,2.6303030154e+00
4122,156,2.6996310033e+00
4132,157,2.4901206381e+00
4142,158,2.2730482136e+00
4152,159,2.2689655956e+00
4162,160,2.0560610219e+00
4172,161,1.9817070751e+00
4182,162,2.0025138191e+00
4192,163,1.8770484263e+00
4202,164,1.9021785855e+00
4212,165,1.7865330706e+00
4222,166,1.5248138231e+00
4232,167,1.3862624160e+00
4242,168,1.4423262417e+00
4252,169,1.4964447765e+00
4262,170,1.3324949886e+00
4272,171,1.2638609925e+00
4282,172,1.1895081404e+00
4292,173,1.1545715858e+00
4302,174,1.1512565745e+00
4312,175,1.0686751319e+00
4322,176,1.0167130208e+00
4332,177,1.0301844032e+00
4342,178,9.7364401270e-01
4352,179,9.4296041670e-01
4362,180,9.4689342088e-01
4372,181,9.1724741490e-01
4382,182,9.4962846795e-01
4392,183,7.7459698834e-01
4402,184,6.7952452942e-01
4412,185,7.0413529560e-01
4422,186,6.8890538587e-01
4432,187,6.6710770608e-01
4442,188,6.7212112845e-01
4452,189,6.7815489839e-01
4462,190,6.0349342381e-01
4472,191,5.7776203687e-01
4482,192,5.7915741129e-01
4492,193,5.5227466710e-01
4502,194,4.7400673653e-01
4512,195,4.6713881543e-01
4522,196,4.3215900281e-01
4532,197,4.5746089728e-01
4542,198,4.4698837220e-01
4552,199,3.9383728443e-01
4562,200,3.9028905895e-01
4572,201,3.6455352268e-01
4582,202,3.4923710159e-01
4592,203,3.3607933243e-01
4602,204,3.1294757459e-01
4612,205,3.1905091970e-01
4622,206,3.0567875689e-01
4632,207,2.9285048404e-01
4642,208,2.9243226640e-01
4652,209,2.7286663901e-01
4662,210,2.3812255317e-01
4672,211,2.2470368028e-01
4682,212,2.4280166992e-01
4692,213,2.4986546853e-01
4702,214,2.1254742945e-01
4712,215,1.9933915603e-01
4722,216,1.9978405567e-01
4732,217,1.8421198728e-01
4742,218,1.8118760764e-01
4752,219,1.7543553892e-01
4762,220,1.7553604066e-01
4772,221,1.6215593273e-01
4782,222,1.5616074965e-01
4792,223,1.5783401215e-01
4802,224,1.4578227737e-01
4812,225,1.3859739617e-01
4822,226,1.3378781539e-01
4832,227,1.3594866134e-01
4842,228,1.1868764224e-01
4852,229,1.2660184320e-01
4862,230,1.1586191996e-01
4872,231,1.0798440196e-01
4882,232,1.0367983463e-01
4892,233,1.0265450487e-01
4902,234,1.0200490437e-01
4912,235,1.0080538032e-01
4922,236,9.6921190467e-02
4932,237,9.7637375188e-02
4942,238,1.0016940953e-01
4952,239,9.3430736715e-02
4962,240,8.5240878376e-02
4972,241,7.7028960356e-02
4982,242,7.0937331919e-02
4992,243,6.9616802923e-02
5002,244,7.6174869319e-02
5012,245,7.9457414530e-02
5022,246,6.8759904135e-02
5032,247,6.8978486042e-02
5042,248,6.7173378321e-02
5052,249,6.5340945649e-02
5062,250,6.2883919499e-02
5072,251,6.3748426001e-02
5082,252,5.9001676326e-02
5092,253,6.0395213626e-02
5102,254,5.9355367692e-02
5112,255,5.4500158346e-02
5122,256,5.1718965475e-02
5132,257,5.2215230984e-02
5142,258,4.9061080738e-02
5152,259,4.7318256433e-02
5162,260,4.7323267536e-02
5172,261,4.5747619399e-02
5182,262,4.8039840867e-02
5192,263,4.9807224594e-02
5202,264,4.8680920925e-02
5212,265,4.4984455688e-02
5222,266,4.4766922998e-02
5232,267,4.8585485921e-02
5242,268,4.6609465828e-02
5252,269,4.2740603576e-02
5262,270,4.3165717866e-02
5272,271,4.0791822952e-02
5282,272,4.0842391458e-02
5292,273,3.9466634141e-02
5302,274,3.9755914004e-02
5312,275,3.5402762676e-02
5322,276,3.7042969219e-02
5332,277,3.8554056112e-02
5342,278,4.1308534766e-02
5352,279,3.7472918863e-02
5362,280,3.4808034909e-02
5372,281,3.4550059621e-02
5382,282,3.3208044683e-02
5392,283,3.4952787751e-02
5402,284,3.3595796644e-02
5412,285,3.2172252964e-02
5422,286,3.2729763796e-02
5432,287,3.1329104366e-02
5442,288,3.3575765285e-02
5452,289,3.5448577491e-02
5462,290,3.3997431301e-02
5472,291,3.2930170654e-02
5482,292,3.2976022281e-02
5492,293,3.5132527650e-02
5502,294,3.5088106628e-02
5512,295,3.2785855301e-02
5522,296,3.0302161601e-02
5532,297,3.0400711133e-02
5542,298,3.2077132309e-02
5552,299,3.3464837798e-02
5562,300,3.5619135464e-02
5572,301,3.1483126782e-02
5582,302,2.7745855453e-02
5592,303,2.8398321111e-02
5602,304,2.9635044217e-02
5612,305,3.0842041736e-02
5622,306,3.2843008162e-02
5632,307,3.1011084174e-02
5642,308,3.1438384931e-02
5652,309,3.1197820159e-02
5662,310,3.0164917355e-02
5672,311,3.1534390969e-02
5682,312,2.9570248565e-02
5692,313,3.0209312279e-02
5702,314,3.3076318239e-02
5712,315,3.0284751612e-02
5722,316,2.8306866909e-02
5732,317,3.2842882270e-02
5742,318,3.0171094665e-02
5752,319,2.8259205782e-02
5762,320,3.0436313415e-02
5772,321,2.7993716174e-02
5782,322,2.7472081222e-02
5792,323,3.0850171124e-02
5802,324,3.0473955196e-02
5812,325,2.8617354506e-02
5822,326,2.8221787731e-02
5832,327,2.8565119382e-02
5842,328,3.1464711160e-02
5852,329,3.0352831780e-02
5862,330,2.9335153896e-02
5872,331,2.8882465215e-02
5882,332,3.1550298599e-02
5892,333,3.0365159720e-02
5902,334,2.8147231381e-02
5912,335,2.5886812773e-02
5922,336,2.6880814894e-02
5932,337,2.8177857914e-02
5942,338,2.7713262493e-02
5952,339,2.8198079711e-02
5962,340,2.7870888471e-02
5972,341,2.7172702968e-02
5982,342,2.5689449622e-02
5992,343,2.5676703819e-02
6002,344,2.6794589249e-02
6012,345,2.5674701906e-02
6022,346,2.5704760034e-02
6032,347,2.7069945536e-02
6042,348,2.6014199887e-02
6052,349,2.8265524347e-02
6062,350,2.9711618212e-02
6072,351,2.8050792098e-02
6082,352,2.7764322488e-02
6092,353,2.6992552784e-02
6102,354,2.5430685037e-02
6112,355,2.5634756364e-02
6122,356,2.4990882885e-02
6132,357,2.5323572414e-02
6142,358,2.6877895828e-02
6152,359,2.8100644264e-02
6162,360,2.6722160687e-02
6172,361,2.9133243715e-02
6182,362,2.8054182477e-02
6192,363,2.6844035354e-02
6202,364,2.5823786623e-02
6212,365,2.7933172379e-02
6222,366,2.8319625104e-02
6232,367,2.7837864497e-02
6242,368,2.7302581372e-02
6252,369,2.6336825275e-02
6262,370,2.8771423634e-02
6272,371,2.7254742795e-02
6282,372,2.5337675033e-02
6292,373,2.6433038904e-02
6302,374,2.6823446490e-02
6312,375,2.6783471305e-02
6322,376,2.7726577140e-02
6332,377,2.8575114858e-02
6342,378,2.7310870203e-02
6352,379,2.5317249497e-02
6362,380,2.4753216890e-02
6372,381,2.6012883428e-02
6382,382,2.7712538915e-02
6392,383,2.8232118792e-02
6402,384,2.7972044305e-02
6412,385,2.8202057330e-02
6422,386,2.6499561611e-02
6432,387,2.9658677827e-02
6442,388,2.7619496688e-02
6452,389,2.5937621728e-02
6462,390,2.6699641586e-02
6472,391,2.4727247534e-02
6482,392,2.5694193653e-02
6492,393,2.4458392686e-02
6502,394,2.7447700934e-02
6512,395,2.9188800935e-02
6522,396,2.4633424659e-02
6532,397,2.4959469548e-02
6542,398,2.4999526430e-02
6552,399,2.5372175861e-02
6562,400,2.4321316854e-02
6572,401,2.3875470538e-02
6582,402,2.4929390663e-02
6592,403,2.4587578760e-02
6602,404,2.6099280918e-02
6612,405,2.5924712812e-02
6622,406,2.4808739398e-02
6632,407,2.4839591277e-02
6642,408,2.4431588397e-02
6652,409,2.5074770561e-02
6662,410,2.4606007232e-02
6672,411,2.3429246557e-02
6682,412,2.3091252328e-02
6692,413,2.3925847921e-02
6702,414,2.3692456124e-02
6712,415,2.3666042278e-02
6722,416,2.2725566595e-02
6732,417,2.1207708222e-02
6742,418,2.2694509659e-02
6752,419,2.3407455362e-02
6762,420,2.2895790050e-02
6772,421,2.3393704871e-02
6782,422,2.3689799477e-02
6792,423,2.3804880768e-02
6802,424,2.0811193897e-02
6812,425,1.9788122244e-02
6822,426,1.9883499968e-02
6832,427,2.0719372616e-02
6842,428,1.9457503792e-02
6852,429,1.8559705983e-02
6862,430,2.0754765636e-02
6872,431,2.0853984730e-02
6882,432,1.8939436402e-02
6892,433,1.9195893277e-02
6902,434,2.0232873916e-02
6912,435,2.0352535461e-02
6922,436,1.9613599324e-02
6932,437,1.9996788509e-02
6942,438,2.0764963654e-02
6952,439,1.7847661082e-02
6962,440,1.8557645551e-02
6972,441,1.8416080372e-02
6982,442,1.7977264592e-02
6992,443,1.7295445534e-02
7002,444,1.6889339416e-02
7012,445,1.7620188919e-02
7022,446,1.7000626476e-02
7032,447,1.6607322258e-02
7042,448,1.7758860921e-02
7052,449,1.8099685281e-02
7062,450,1.7484058117e-02
7072,451,1.5708037902e-02
7082,452,1.6482884825e-02
7092,453,1.6638442843e-02
7102,454,1.5568421536e-02
7112,455,1.5413506424e-02
7122,456,1.5018994834e-02
7132,457,1.5304492354e-02
7142,458,1.3963315621e-02
7152,459,1.5215812161e-02
7162,460,1.5289013495e-02
7172,461,1.4331326052e-02
7182,462,1.5940948636e-02
7192,463,1.4925809782e-02
7202,464,1.3602279753e-02
7212,465,1.3966262765e-02
7222,466,1.4595484584e-02
7232,467,1.4365708459e-02
7242,468,1.3805158446e-02
7252,469,1.3472443740e-02
7262,470,1.3607450216e-02
7272,471,1.4051787042e-02
7282,472,1.3737747227e-02
7292,473,1.3338473671e-02
7302,474,1.2793768721e-02
7312,475,1.3604357985e-02
7322,476,1.3611627263e-02
7332,477,1.4769810798e-02
7342,478,1.3465458426e-02
7352,479,1.2908418232e-02
7362,480,1.3154417561e-02
7372,481,1.2557361016e-02
7382,482,1.1931601623e-02
7392,483,1.2204970812e-02
7402,484,1.2764471992e-02
7412,485,1.2962844931e-02
7422,486,1.3121416244e-02
7432,487,1.2693539821e-02
7442,488,1.2904801778e-02
7452,489,1.2166050483e-02
7462,490,1.2262270641e-02
7472,491,1.2010828554e-02
7482,492,1.1648179331e-02
7492,493,1.2259260451e-02
7502,494,1.2268628262e-02
7512,495,1.2409777856e-02
7522,496,1.1833814722e-02
7532,497,1.1173233881e-02
7542,498,1.1920641562e-02
7552,499,1.1943322300e-02
7562,500,1.1337419422e-02
7572,501,1.1822188890e-02
7582,502,1.1753550669e-02
7592,503,1.2147537357e-02
7602,504,1.1933785462e-02
7612,505,1.1911833896e-02
7622,506,1.1619371078e-02
7632,507,1.2048163361e-02
7642,508,1.2211042789e-02
7652,509,1.1675572368e-02
7662,510,1.0930087045e-02
7672,511,1.1431282353e-02
7684,1,8.4853048427e+08
7694,2,7.9960613877e+08
7704,3,4.0031692795e+08
7714,4,2.0957015415e+08
7724,5,6.1254905679e+07
7734,6,2.4400059959e+07
7744,7,1.8870345320e+07
7754,8,1.6784382939e+07
7764,9,1.4148484712e+07
7774,10,7.8916755600e+06
7784,11,3.9575976267e+06
7794,12,3.7437562795e+06
7804,13,2.1592686768e+06
7814,14,1.6005388834e+06
7824,15,1.3309473729e+06
7834,16,1.6307276212e+06
7844,17,1.0694861062e+06
7854,18,5.7914856320e+05
7864,19,6.0568642424e+05
7874,20,5.4176946754e+05
7884,21,4.4029035819e+05
7894,22,3.1671321525e+05
7904,23,2.8705000626e+05
7914,24,3.0281956071e+05
7924,25,2.2152237074e+05
7934,26,1.4988064393e+05
7944,27,1.3081378030e+05
7954,28,1.2796964364e+05
7964,29,1.0577161601e+05
7974,30,9.2151589193e+04
7984,31,7.4393141760e+04
7994,32,6.2802964473e+04
8004,33,6.1543581646e+04
8014,34,6.4702970299e+04
8024,35,5.7796580979e+04
8034,36,4.1204250714e+04
8044,37,4.3754202520e+04
8054,38,3.6827336016e+04
8064,39,2.7915914118e+04
8074,40,2.7025266623e+04
8084,41,2.1412766798e+04
8094,42,1.7682176003e+04
8104,43,1.6984014484e+04
8114,44,1.6883775557e+04
8124,45,1.5771176496e+04
8134,46,1.3077817147e+04
8144,47,1.0091093854e+04
8154,48,8.8257586866e+03
8164,49,9.5490698304e+03
8174,50,8.5912037274e+03
8184,51,6.7187284625e+03
8194,52,7.1218824072e+03
8204,53,6.0749720629e+03
8214,54,5.8270884591e+03
8224,55,5.9441781137e+03
8234,56,4.6588009450e+03
8244,57,4.9180581425e+03
8254,58,4.9381308076e+03
8264,59,3.9981387808e+03
8274,60,3.6687980002e+03
8284,61,3.7473114501e+03
8294,62,3.2736640443e+03
8304,63,3.4075266763e+03
8314,64,2.9075398938e+03
8324,65,2.2264210439e+03
8334,66,1.9180354256e+03
8344,67,2.0057380653e+03
8354,68,1.7857883092e+03
8364,69,1.6279373282e+03
8374,70,1.5038455357e+03
8384,71,1.5627518518e+03
8394,72,1.4975294868e+03
8404,73,1.1477325518e+03
8414,74,1.1592034097e+03
8424,75,1.1412547139e+03
8434,76,1.2122027527e+03
8444,77,1.1613621267e+03
8454,78,9.5886509891e+02
8464,79,8.9017513669e+02
8474,80,7.8808228020e+02
8484,81,8.2728328829e+02
8494,82,7.4688204244e+02
8504,83,6.1130327220e+02
8514,84,5.6717582248e+02
8524,85,5.7031125152e+02
8534,86,5.1905911969e+02
8544,87,4.6988432496e+02
8554,88,5.0467378531e+02
8564,89,4.6511863642e+02
8574,90,5.2577112138e+02
8584,91,4.6905695435e+02
8594,92,4.0642635228e+02
8604,93,3.8200578498e+02
8614,94,3.2404977590e+02
8624,95,2.8025576308e+02
8634,96,2.8497799181e+02
8644,97,2.9890091705e+02
8654,98,2.5513532050e+02
8664,99,2.5292252921e+02
8674,100,2.2306213973e+02
8684,101,2.1548522247e+02
8694,102,2.1656886873e+02
8704,103,2.3003911530e+02
8714,104,2.0826110459e+02
8724,105,1.6898063495e+02
8734,106,1.5693796339e+02
8744,107,1.5571626145e+02
8754,108,1.4991633145e+02
8764,109,1.4578930932e+02
8774,110,1.4006551813e+02
8784,111,1.3134803571e+02
8794,112,1.2341941987e+02
8804,113,1.2807004182e+02
8814,114,1.1549869642e+02
8824,115,1.0314709571e+02
8834,116,9.2822067237e+01
8844,117,9.2443235928e+01
8854,118,9.3434087036e+01
8864,119,8.4693793545e+01
8874,120,7.9297507424e+01
8884,121,6.9952279333e+01
8894,122,6.7762191526e+01
8904,123,7.1122217338e+01
8914,124,6.4460086281e+01
8924,125,6.3966000330e+01
8934,126,5.9977094872e+01
8944,127,5.2694682215e+01
8954,128,5.0178944407e+01
8964,129,4.9818200650e+01
8974,130,4.6859902101e+01
8984,131,4.4881669911e+01
8994,132,4.0782017491e+01
9004,133,3.9285110475e+01
9014,134,3.8793388526e+01
9024,135,3.6773925632e+01
9034,136,3.3914995970e+01
9044,137,3.0829205527e+01
9054,138,2.8607745394e+01
9064,139,2.7864754806e+01
9074,140,2.4770055340e+01
9084,141,2.6316306296e+01
9094,142,2.3862904270e+01
9104,143,2.1139466683e+01
9114,144,2.0951317337e+01
9124,145,2.1136046628e+01
9134,146,2.0376454970e+01
9144,147,1.7658408459e+01
9154,148,1.6856011745e+01
9164,149,1.4543431703e+01
9174,150,1.4631018058e+01
9184,151,1.4666505599e+01
9194,152,1.3333930337e+01
9204,153,1.2282396395e+01
9214,154,1.1537953370e+01
9224,155,1.1210180321e+01
9234,156,1.1387503453e+01
9244,157,1.0575740555e+01
9254,158,9.6976913699e+00
9264,159,9.6437844763e+00
9274,160,8.7114852109e+00
9284,161,8.3975006234e+00
9294,162,8.4518602039e+00
9304,163,7.9887242739e+00
9314,164,8.2424988191e+00
9324,165,7.7109157205e+00
9334,166,6.5203704245e+00
9344,167,5.9485498489e+00
9354,168,6.2398200717e+00
9364,169,6.4761019847e+00
9374,170,5.7679434956e+00
9384,171,5.4497367819e+00
9394,172,5.1109396458e+00
9404,173,4.9878840148e+00
9414,174,4.9407753818e+00
9424,175,4.5815082601e+00
9434,176,4.3610851080e+00
9444,177,4.4122558220e+00
9454,178,4.2271332125e+00
9464,179,4.0925955563e+00
9474,180,4.0515909876e+00
9484,181,3.9514934928e+00
9494,182,4.1055066479e+00
9504,183,3.3332811881e+00
9514,184,2.9641322211e+00
9524,185,3.0402786163e+00
9534,186,2.9438292595e+00
9544,187,2.9257917054e+00
9554,188,2.8973835447e+00
9564,189,2.9110510212e+00
9574,190,2.6131662939e+00
9584,191,2.5432733809e+00
9594,192,2.5152822278e+00
9604,193,2.3816322469e+00
9614,194,2.0440223664e+00
9624,195,2.0108650143e+00
9634,196,1.8570516508e+00
9644,197,1.9621578573e+00
9654,198,1.9416222596e+00
9664,199,1.7385390143e+00
9674,200,1.7359643002e+00
9684,201,1.6367820829e+00
9694,202,1.5110685281e+00
9704,203,1.4607468577e+00
9714,204,1.3435162901e+00
9724,205,1.3874271416e+00
9734,206,1.3549813059e+00
9744,207,1.3017354570e+00
9754,208,1.3121726672e+00
9764,209,1.1918856756e+00
9774,210,1.0318277697e+00
9784,211,9.8296168567e-01
9794,212,1.0792466546e+00
9804,213,1.1037738072e+00
9814,214,9.5486269644e-01
9824,215,8.6447595922e-01
9834,216,8.4268088492e-01
9844,217,7.9288689336e-01
9854,218,7.7252122393e-01
9864,219,7.4854413116e-01
9874,220,7.4753502601e-01
9884,221,6.8348967814e-01
9894,222,6.9298423832e-01
9904,223,6.8223978342e-01
9914,224,6.2272618688e-01
9924,225,5.9371642639e-01
9934,226,5.7540579055e-01
9944,227,5.9801631365e-01
9954,228,5.2374680041e-01
9964,229,5.3503542646e-01
9974,230,4.9542819695e-01
9984,231,4.6451444951e-01
9994,232,4.3435965439e-01
10004,233,4.3706717259e-01
10014,234,4.3347944059e-01
10024,235,4.2750045265e-01
10034,236,4.1580673320e-01
10044,237,4.1482159384e-01
10054,238,4.0163303293e-01
10064,239,3.7368978101e-01
10074,240,3.5412255960e-01
10084,241,3.2972592273e-01
10094,242,3.0397892270e-01
10104,243,2.9500239886e-01
10114,244,3.1184526703e-01
10124,245,3.2538618343e-01
10134,246,2.8510384273e-01
10144,247,2.9212993537e-01
10154,248,2.9659728999e-01
10164,249,2.9112699363e-01
10174,250,2.6242625514e-01
10184,251,2.5617054101e-01
10194,252,2.5085092129e-01
10204,253,2.4332254784e-01
10214,254,2.3149222287e-01
10224,255,2.1531942083e-01
10234,256,1.9850258000e-01
10244,257,1.9808156363e-01
10254,258,1.8423707181e-01
10264,259,1.9322558968e-01
10274,260,1.8510121328e-01
10284,261,1.8113515226e-01
10294,262,1.9251151922e-01
10304,263,2.0357788023e-01
10314,264,1.9350586510e-01
10324,265,1.8324996501e-01
10334,266,1.8258075407e-01
10344,267,1.8549012616e-01
10354,268,1.7781640460e-01
10364,269,1.6921271465e-01
10374,270,1.6365072929e-01
10384,271,1.5565727844e-01
10394,272,1.5333562587e-01
10404,273,1.5521345597e-01
10414,274,1.5702623274e-01
10424,275,1.4375905612e-01
10434,276,1.5636500729e-01
10444,277,1.5158802343e-01
10454,278,1.5981014400e-01
10464,279,1.4794625879e-01
10474,280,1.4165869243e-01
10484,281,1.3584856917e-01
10494,282,1.3343407431e-01
10504,283,1.3949191602e-01
10514,284,1.3149667696e-01
10524,285,1.2512731563e-01
10534,286,1.3137740373e-01
10544,287,1.2350517002e-01
10554,288,1.2956800046e-01
10564,289,1.3985529203e-01
10574,290,1.3926338474e-01
10584,291,1.3125982110e-01
10594,292,1.3463502230e-01
10604,293,1.4138281905e-01
10614,294,1.3522615857e-01
10624,295,1.2767154094e-01
10634,296,1.3136616839e-01
10644,297,1.3219938404e-01
10654,298,1.3117897925e-01
10664,299,1.2879817932e-01
10674,300,1.4252408696e-01
10684,301,1.3096281003e-01
10694,302,1.1914667140e-01
10704,303,1.1833711113e-01
10714,304,1.2230279776e-01
10724,305,1.2210515401e-01
10734,306,1.2776663054e-01
10744,307,1.2688858913e-01
10754,308,1.3511292602e-01
10764,309,1.2337543768e-01
10774,310,1.2044439982e-01
10784,311,1.2828932254e-01
10794,312,1.1854362913e-01
10804,313,1.1236404717e-01
10814,314,1.2017164469e-01
10824,315,1.1990352057e-01
10834,316,1.1444759221e-01
10844,317,1.2133803345e-01
10854,318,1.1298109591e-01
10864,319,1.1684457997e-01
10874,320,1.2541840901e-01
10884,321,1.2113273536e-01
10894,322,1.1560240203e-01
10904,323,1.2852914428e-01
10914,324,1.2449983257e-01
10924,325,1.1455026990e-01
10934,326,1.1715719265e-01
10944,327,1.1554492137e-01
10954,328,1.2852031449e-01
10964,329,1.2247935372e-01
10974,330,1.2260173101e-01
10984,331,1.2443686118e-01
10994,332,1.2855068928e-01
11004,333,1.2064726607e-01
11014,334,1.2221342865e-01
11024,335,1.1570163013e-01
11034,336,1.1314407280e-01
11044,337,1.1403667458e-01
11054,338,1.1508643939e-01
11064,339,1.1261728141e-01
11074,340,1.1220223505e-01
11084,341,1.0544879996e-01
11094,342,9.9007674776e-02
11104,343,1.0668579071e-01
11114,344,1.2027152935e-01
11124,345,1.1490555290e-01
11134,346,1.1338243958e-01
11144,347,1.1396814043e-01
11154,348,1.1617707164e-01
11164,349,1.2293559220e-01
11174,350,1.2548203979e-01
11184,351,1.2182780322e-01
11194,352,1.1654383698e-01
11204,353,1.1804692949e-01
11214,354,1.1827609387e-01
11224,355,1.1925159732e-01
11234,356,1.1633073608e-01
11244,357,1.2093420444e-01
11254,358,1.2655190542e-01
11264,359,1.2356264726e-01
11274,360,1.1928583453e-01
11284,361,1.2891880681e-01
11294,362,1.2252937978e-01
11304,363,1.1576336778e-01
11314,364,1.1248235143e-01
11324,365,1.2645391328e-01
11334,366,1.3622715378e-01
11344,367,1.2947826408e-01
11354,368,1.2449556811e-01
11364,369,1.2181614202e-01
11374,370,1.4885506985e-01
11384,371,1.3454786248e-01
11394,372,1.2540223133e-01
11404,373,1.2317631965e-01
11414,374,1.2992469307e-01
11424,375,1.3220684484e-01
11434,376,1.5063438043e-01
11444,377,1.5170516642e-01
11454,378,1.4760136947e-01
11464,379,1.3615624860e-01
11474,380,1.3211379128e-01
11484,381,1.3540380871e-01
11494,382,1.4799288074e-01
11504,383,1.5462017725e-01
11514,384,1.4614448812e-01
11524,385,1.5311919451e-01
11534,386,1.4348558291e-01
11544,387,1.6466829425e-01
11554,388,1.5650742741e-01
11564,389,1.4739171860e-01
11574,390,1.4070691287e-01
11584,391,1.2999011222e-01
11594,392,1.4367624091e-01
11604,393,1.3353175695e-01
11614,394,1.5574378080e-01
11624,395,1.7090538025e-01
11634,396,1.4605588000e-01
11644,397,1.5740805388e-01
11654,398,1.5654782514e-01
11664,399,1.5427118194e-01
11674,400,1.4493674421e-01
11684,401,1.3861507894e-01
11694,402,1.4819233158e-01
11704,403,1.3790975102e-01
11714,404,1.6092745928e-01
11724,405,1.6167387675e-01
11734,406,1.5713257968e-01
11744,407,1.6299402290e-01
11754,408,1.5932553409e-01
11764,409,1.7758512851e-01
11774,410,1.5826826473e-01
11784,411,1.4249542059e-01
11794,412,1.3706058487e-01
11804,413,1.4284256231e-01
11814,414,1.4133345112e-01
11824,415,1.4735780304e-01
11834,416,1.4291656499e-01
11844,417,1.3561507816e-01
11854,418,1.4448368580e-01
11864,419,1.4243086132e-01
11874,420,1.4750404623e-01
11884,421,1.5273857729e-01
11894,422,1.6880322802e-01
11904,423,1.7230754893e-01
11914,424,1.5957861351e-01
11924,425,1.5547604369e-01
11934,426,1.3831926879e-01
11944,427,1.3818037287e-01
11954,428,1.2509696847e-01
11964,429,1.1650446117e-01
11974,430,1.4857380186e-01
11984,431,1.5923978795e-01
11994,432,1.3646499278e-01
12004,433,1.3103644023e-01
12014,434,1.5433085905e-01
12024,435,1.5468324088e-01
12034,436,1.3930533602e-01
12044,437,1.3632216241e-01
12054,438,1.4891918687e-01
12064,439,1.2533477101e-01
12074,440,1.3022980339e-01
12084,441,1.3892393235e-01
12094,442,1.3528889336e-01
12104,443,1.2132263340e-01
12114,444,1.1223105715e-01
12124,445,1.2067818606e-01
12134,446,1.1055886767e-01
12144,447,1.0731782738e-01
12154,448,1.2238150436e-01
12164,449,1.3892252503e-01
12174,450,1.2948000335e-01
12184,451,1.2322807622e-01
12194,452,1.2827441588e-01
12204,453,1.2904605739e-01
12214,454,1.1685709932e-01
12224,455,1.2582243843e-01
12234,456,1.2033660112e-01
12244,457,1.1793111500e-01
12254,458,1.1147193372e-01
12264,459,1.1891554237e-01
12274,460,1.1667369427e-01
12284,461,1.0560863453e-01
12294,462,1.3254410600e-01
12304,463,1.2012513214e-01
12314,464,8.4612120958e-02
12324,465,9.5552334428e-02
12334,466,1.0453496610e-01
12344,467,1.0893590305e-01
12354,468,9.3557562193e-02
12364,469,9.5983847286e-02
12374,470,9.6039874579e-02
12384,471,9.3144445728e-02
12394,472,9.7948144410e-02
12404,473,1.0197503558e-01
12414,474,1.0491583461e-01
12424,475,9.0675014108e-02
12434,476,9.7183356272e-02
12444,477,1.1306437699e-01
12454,478,9.9941530495e-02
12464,479,9.3676932773e-02
12474,480,8.1143400480e-02
12484,481,7.0322182035e-02
12494,482,6.8192487786e-02
12504,483,7.0836280753e-02
12514,484,7.4707945641e-02
12524,485,7.6332534732e-02
12534,486,7.1285815449e-02
12544,487,7.4629568629e-02
12554,488,8.2669038130e-02
12564,489,6.6498300038e-02
12574,490,5.2496378625e-02
12584,491,5.2315886654e-02
12594,492,6.0887108650e-02
12604,493,6.2115281236e-02
12614,494,4.6759102643e-02
12624,495,4.8779049902e-02
12634,496,5.7222016741e-02
12644,497,4.4765027598e-02
12654,498,3.9545979833e-02
12664,499,3.2732356784e-02
12674,500,2.9626344531e-02
12684,501,3.1291029697e-02
12694,502,3.0815946740e-02
12704,503,3.5675800874e-02
12714,504,3.0719354503e-02
12724,505,2.3439843673e-02
12734,506,1.6505652456e-02
12744,507,1.6056859289e-02
12754,508,1.8100674077e-02
12764,509,1.5643826879e-02
12774,510,1.3106491226e-02
12784,511,1.2349250514e-02
12794,512,1.1715775128e-02
12804,513,1.2192772211e-02
12814,514,1.2144886779e-02
12824,515,1.4337259769e-02
12834,516,1.6432012002e-02
12844,517,1.5126511681e-02
12854,518,1.6380691534e-02
12864,519,2.2044263520e-02
12874,520,2.7090108298e-02
12884,521,2.9134256167e-02
12894,522,2.6389535579e-02
12904,523,2.7932703950e-02
12914,524,2.6453341273e-02
12924,525,2.8208368841e-02
12934,526,3.2141018938e-02
12944,527,3.5669125966e-02
12954,528,4.4784822221e-02
12964,529,3.9317834651e-02
12974,530,3.4385482618e-02
12984,531,4.3655124041e-02
12994,532,4.1344122500e-02
13004,533,3.4301894450e-02
13014,534,3.4942635447e-02
13024,535,4.3320909447e-02
13034,536,5.0988474093e-02
13044,537,4.9064383467e-02
13054,538,4.3165658169e-02
13064,539,4.9292828038e-02
13074,540,4.6785282399e-02
13084,541,4.3403940809e-02
13094,542,3.8169372768e-02
13104,543,3.7175718681e-02
13114,544,4.1323283404e-02
13124,545,4.8361760429e-02
13134,546,5.1012066796e-02
13144,547,5.4908224861e-02
13154,548,4.9478924582e-02
13164,549,4.6656873688e-02
13174,550,4.9833224461e-02
13184,551,4.6510684810e-02
13194,552,4.5098044441e-02
13204,553,4.3529749715e-02
13214,554,4.5223658293e-02
13224,555,4.2768765161e-02
13234,556,4.3937275908e-02
13244,557,4.3080528331e-02
13254,558,4.0160699384e-02
13264,559,3.7022188334e-02
13274,560,3.5880524351e-02
13284,561,4.9373023906e-02
13294,562,5.2018884591e-02
13304,563,4.3414987334e-02
13314,564,4.2235792918e-02
13324,565,4.0099365823e-02
13334,566,4.0147280884e-02
13344,567,4.2092120071e-02
13354,568,4.2108491691e-02
13364,569,4.1429208343e-02
13374,570,3.9550925507e-02
13384,571,4.2628705220e-02
13394,572,3.9616273784e-02
13404,573,3.8077927718e-02
13414,574,4.0489447412e-02
13424,575,4.0239746194e-02
13434,576,3.4218636597e-02
13444,577,3.1972218515e-02
13454,578,3.4613452112e-02
13464,579,3.3560135882e-02
13474,580,3.1436339772e-02
13484,581,3.1046720187e-02
13494,582,3.5339249842e-02
13504,583,3.6126641188e-02
13514,584,3.2385690343e-02
13524,585,3.3632317277e-02
13534,586,3.7119193993e-02
13544,587,3.5985224626e-02
13554,588,3.4042625620e-02
13564,589,3.3317686417e-02
13574,590,3.5258368798e-02
13584,591,3.0532787592e-02
13594,592,3.0847176128e-02
13604,593,3.1188331461e-02
13614,594,3.0071025434e-02
13624,595,2.9854930965e-02
13634,596,2.9875207071e-02
13644,597,2.8535075018e-02
13654,598,2.7960691470e-02
13664,599,3.0193752733e-02
13674,600,3.0660497760e-02
13684,601,3.0169830026e-02
13694,602,3.1781143114e-02
13704,603,2.9647039340e-02
13714,604,2.8793376746e-02
13724,605,2.9199719992e-02
13734,606,2.7027129405e-02
13744,607,2.5588631439e-02
13754,608,2.5525352330e-02
13764,609,2.5750615101e-02
13774,610,2.4576426451e-02
13784,611,2.6433109172e-02
13794,612,2.4861196318e-02
13804,613,2.5178765085e-02
13814,614,2.6369235602e-02
13824,615,2.7295611685e-02
13834,616,2.5393595295e-02
13844,617,2.4846997540e-02
13854,618,2.3523877568e-02
13864,619,2.3865716014e-02
13874,620,2.3607092693e-02
13884,621,2.3173079673e-02
13894,622,2.2870362064e-02
13904,623,2.2935186581e-02
13914,624,2.3251397515e-02
13924,625,2.2461881167e-02
13934,626,2.2734259666e-02
13944,627,2.0751992490e-02
13954,628,2.0881479932e-02
13964,629,2.2186319684e-02
13974,630,2.1381061897e-02
13984,631,1.9796686280e-02
13994,632,1.9247880543e-02
14004,633,1.9319138546e-02
14014,634,1.9837207342e-02
14024,635,2.0320120506e-02
14034,636,1.8708202072e-02
14044,637,1.9949554364e-02
14054,638,2.0390156239e-02
14064,639,2.0384205611e-02
14074,640,1.8841214942e-02
14084,641,1.9449888588e-02
14094,642,1.9641104665e-02
14104,643,1.8753032223e-02
14114,644,1.8165180176e-02
14124,645,1.7921483229e-02
14134,646,1.7972500389e-02
14144,647,1.8135798283e-02
14154,648,1.8193484569e-02
14164,649,1.6732986071e-02
14174,650,1.7293955510e-02
14184,651,1.6825977719e-02
14194,652,1.6432208950e-02
14204,653,1.7882645972e-02
14214,654,1.7027771416e-02
14224,655,1.6454664371e-02
14234,656,1.5268058866e-02
14244,657,1.4824277078e-02
14254,658,1.5292700971e-02
14264,659,1.5309325631e-02
14274,660,1.4661478172e-02
14284,661,1.5835390108e-02
14294,662,1.6267494482e-02
14304,663,1.5279071279e-02
14314,664,1.4641992271e-02
14324,665,1.4840627859e-02
14334,666,1.5221611952e-02
14344,667,1.4682409605e-02
14354,668,1.5157521564e-02
14364,669,1.5608609974e-02
14374,670,1.4754114742e-02
14384,671,1.4805825595e-02
14394,672,1.5472930245e-02
14404,673,1.4777227744e-02
14414,674,1.4750500922e-02
14424,675,1.4766873512e-02
14434,676,1.4697552788e-02
14444,677,1.4533999049e-02
14454,678,1.4150486481e-02
14464,679,1.4085005856e-02
14474,680,1.4329058082e-02
14484,681,1.3709496823e-02
14494,682,1.3783401299e-02
14504,683,1.3619479856e-02
14514,684,1.3693872088e-02
14524,685,1.3082639245e-02
14534,686,1.2962200776e-02
14544,687,1.2718909722e-02
14554,688,1.2849389781e-02
14564,689,1.2455668055e-02
14574,690,1.2500124749e-02
14584,691,1.3242194675e-02
14594,692,1.2926867300e-02
14604,693,1.3191132313e-02
14614,694,1.2235259107e-02
14624,695,1.2511094917e-02
14634,696,1.2499307627e-02
14644,697,1.2957796726e-02
14654,698,1.3370920183e-02
14664,699,1.3053124711e-02
14674,700,1.2418703401e-02
14684,701,1.3097691533e-02
14694,702,1.3250662297e-02
14704,703,1.3363229938e-02
14714,704,1.2820392163e-02
14724,705,1.2962340138e-02
14734,706,1.2844617424e-02
14744,707,1.2856173409e-02
14754,708,1.2241710397e-02
14764,709,1.1670855921e-02
14774,710,1.2110002791e-02
14784,711,1.2416555593e-02
14794,712,1.2534110102e-02
14804,713,1.1978701332e-02
14814,714,1.2030211201e-02
14824,715,1.1870684558e-02
14834,716,1.1545909669e-02
14844,717,1.2377153324e-02
14854,718,1.2599264661e-02
14864,719,1.2743746439e-02
14874,720,1.2158684551e-02
14884,721,1.2050572408e-02
14894,722,1.2284453418e-02
14904,723,1.2503904754e-02
14914,724,1.2861248383e-02
14924,725,1.2894452099e-02
14934,726,1.2501667746e-02
14944,727,1.2541907152e-02
14954,728,1.2111373581e-02
14964,729,1.2328531488e-02
14974,730,1.3208864573e-02
14984,731,1.2933043359e-02
14994,732,1.1955245898e-02
15004,733,1.1938925418e-02
15014,734,1.2121521542e-02
15024,735,1.2237616133e-02
15034,736,1.2424290979e-02
15044,737,1.2660531548e-02
15054,738,1.2740531747e-02
15064,739,1.2692464313e-02
15074,740,1.2604660756e-02
15084,741,1.2094232387e-02
15094,742,1.2109487120e-02
15104,743,1.2216045932e-02
15114,744,1.2061935563e-02
15124,745,1.1877216860e-02
15134,746,1.1911263455e-02
15144,747,1.2101043471e-02
15154,748,1.2273359374e-02
15164,749,1.1891691649e-02
15174,750,1.2282258237e-02
15184,751,1.2518553720e-02
15194,752,1.2147606252e-02
15204,753,1.1957464305e-02
15214,754,1.2161305419e-02
15224,755,1.1514901831e-02
15234,756,1.1756803728e-02
15244,757,1.2052379605e-02
15254,758,1.2006260360e-02
15264,759,1.2351259466e-02
15274,760,1.2399938642e-02
15284,761,1.2066846784e-02
15294,762,1.2065621923e-02
15304,763,1.2064770320e-02
15314,764,1.1780900367e-02
15324,765,1.1577488413e-02
15334,766,1.1916424970e-02
15344,767,1.2245708653e-02
15354,768,1.2171984030e-02
15364,769,1.1971697863e-02
15374,770,1.1571421091e-02
15384,771,1.1934978158e-02
15394,772,1.1889686863e-02
15404,773,1.2089811304e-02
15414,774,1.1968453218e-02
15424,775,1.2182572550e-02
15434,776,1.2107396322e-02
15444,777,1.2249139537e-02
15454,778,1.2174387585e-02
15464,779,1.2333183521e-02
15474,780,1.1708776451e-02
15484,781,1.1113482393e-02
15494,782,1.1517278740e-02
15504,783,1.1609946481e-02
15514,784,1.1527033729e-02
15524,785,1.1477508175e-02
15534,786,1.2413135700e-02
15544,787,1.2691469016e-02
15554,788,1.1837381836e-02
15564,789,1.2011118067e-02
15574,790,1.2154146760e-02
15584,791,1.1725814948e-02
15594,792,1.2214511057e-02
15604,793,1.2459886841e-02
15614,794,1.1968900519e-02
15624,795,1.1889993188e-02
15634,796,1.2158410877e-02
15644,797,1.1955944441e-02
15654,798,1.2007131757e-02
15664,799,1.2309650921e-02
15674,800,1.1633678689e-02
15684,801,1.1490816692e-02
15694,802,1.1889626880e-02
15704,803,1.2057644915e-02
15714,804,1.1655270436e-02
15724,805,1.1232683392e-02
15734,806,1.1727129672e-02
15744,807,1.2159075413e-02
15754,808,1.2140407134e-02
15764,809,1.2497465644e-02
15774,810,1.2694778412e-02
15784,811,1.2117974577e-02
15794,812,1.2324749939e-02
15804,813,1.2240001229e-02
15814,814,1.2269341269e-02
15824,815,1.1340618561e-02
15834,816,1.1398024104e-02
15844,817,1.2099339054e-02
15854,818,1.2037737593e-02
15864,819,1.1632917206e-02
15874,820,1.1607521821e-02
15884,821,1.2135129831e-02
15894,822,1.2070125037e-02
15904,823,1.1770510297e-02
15914,824,1.1988897696e-02
15924,825,1.2326459073e-02
15934,826,1.2220103129e-02
15944,827,1.1810311316e-02
15954,828,1.2038657336e-02
15964,829,1.2651884741e-02
15974,830,1.2268782380e-02
15984,831,1.1654235633e-02
15994,832,1.1475454405e-02
16004,833,1.2713530520e-02
16014,834,1.2485586980e-02
16024,835,1.1908763390e-02
16034,836,1.2051082839e-02
16044,837,1.2101963241e-02
16054,838,1.1684219609e-02
16064,839,1.1672237693e-02
16074,840,1.1739167339e-02
16084,841,1.2106088273e-02
16094,842,1.1885793378e-02
16104,843,1.1736726213e-02
16114,844,1.1838398200e-02
16124,845,1.2370411714e-02
16134,846,1.2169103355e-02
16144,847,1.2257939143e-02
16154,848,1.1773859302e-02
16164,849,1.1671826094e-02
16174,850,1.1848337464e-02
16184,851,1.1903929697e-02
16194,852,1.2309024012e-02
16204,853,1.1562406503e-02
16214,854,1.1853007156e-02
16224,855,1.2013282502e-02
16234,856,1.2262692011e-02
16244,857,1.2025637342e-02
16254,858,1.1721408136e-02
16264,859,1.1720114948e-02
16274,860,1.2127601570e-02
16284,861,1.2493618269e-02
16294,862,1.2387474453e-02
16304,863,1.2683601779e-02
16314,864,1.2495291915e-02
16324,865,1.2051295944e-02
16334,866,1.1946767542e-02
16344,867,1.2044966540e-02
16354,868,1.1874921683e-02
16364,869,1.1831712554e-02
16374,870,1.1966174047e-02
16384,871,1.2361409210e-02
16394,872,1.1802226435e-02
16404,873,1.1326668284e-02
16414,874,1.1855271752e-02
16424,875,1.2220130289e-02
16434,876,1.2190422612e-02
16444,877,1.2215031595e-02
16454,878,1.2307520494e-02
16464,879,1.1417835913e-02
16474,880,1.1867183279e-02
16484,881,1.2159596804e-02
16494,882,1.2379887936e-02
16504,883,1.2426964809e-02
16514,884,1.2497559597e-02
16524,885,1.2176647674e-02
16534,886,1.2272305591e-02
16544,887,1.2338651937e-02
16554,888,1.2030327242e-02
16564,889,1.1645677825e-02
16574,890,1.1468267542e-02
16584,891,1.1504761977e-02
16594,892,1.1766738266e-02
16604,893,1.2049132720e-02
16614,894,1.2371364233e-02
16624,895,1.2539544793e-02
16634,896,1.2216130501e-02
16644,897,1.2103025226e-02
16654,898,1.1515081757e-02
16664,899,1.2243695130e-02
16674,900,1.1999541697e-02
16684,901,1.1950781739e-02
16694,902,1.1771031283e-02
16704,903,1.2360275059e-02
16714,904,1.2630630571e-02
16724,905,1.2263978544e-02
16734,906,1.1521488882e-02
16744,907,1.1623790120e-02
16754,908,1.1600942150e-02
16764,909,1.1815807976e-02
16774,910,1.2168673170e-02
16784,911,1.1817163554e-02
16794,912,1.2005286695e-02
16804,913,1.2360930484e-02
16814,914,1.2378002665e-02
16824,915,1.2210055945e-02
16834,916,1.2359893436e-02
16844,917,1.2082780373e-02
16854,918,1.2012871964e-02
16864,919,1.1864462033e-02
16874,920,1.1815434142e-02
16884,921,1.2116039304e-02
16894,922,1.1792357270e-02
16904,923,1.2162142681e-02
16914,924,1.2083621684e-02
16924,925,1.1996117334e-02
16934,926,1.2194611165e-02
16944,927,1.1512122024e-02
16954,928,1.1323371724e-02
16964,929,1.1511579192e-02
16974,930,1.2093011714e-02
16984,931,1.2327629295e-02
16994,932,1.2295789683e-02
17004,933,1.1807477230e-02
17014,934,1.1804489261e-02
17024,935,1.1740872603e-02
17034,936,1.1740505265e-02
17044,937,1.1811183927e-02
17054,938,1.1309921547e-02
17064,939,1.2408857535e-02
17074,940,1.2743247366e-02
17084,941,1.2375062727e-02
17094,942,1.2232486003e-02
17104,943,1.1935603743e-02
17114,944,1.1967547304e-02
17124,945,1.1813159848e-02
17134,946,1.1908292699e-02
17144,947,1.1964120065e-02
17154,948,1.2408606077e-02
17164,949,1.1783475082e-02
17174,950,1.2282742241e-02
17184,951,1.2064106170e-02
17194,952,1.1617285407e-02
17204,953,1.1855528120e-02
17214,954,1.1998040768e-02
17224,955,1.2339363736e-02
17234,956,1.1696707116e-02
17244,957,1.2142379731e-02
17254,958,1.1740788841e-02
17264,959,1.1955450113e-02
17274,960,1.2111338829e-02
17284,961,1.2042820872e-02
17294,962,1.2014342301e-02
17304,963,1.1803992625e-02
17314,964,1.1245127269e-02
17324,965,1.1950241897e-02
17334,966,1.2191348034e-02
17344,967,1.2128977707e-02
17354,968,1.1472326439e-02
17364,969,1.1641403712e-02
17374,970,1.1926753426e-02
17384,971,1.1893717787e-02
17394,972,1.1835133417e-02
17404,973,1.1787567739e-02
17414,974,1.2125769323e-02
17424,975,1.2102172128e-02
17434,976,1.1650742912e-02
17444,977,1.1407346286e-02
17454,978,1.1456264525e-02
17464,979,1.1228826749e-02
17474,980,1.1342672895e-02
17484,981,1.1464701940e-02
17494,982,1.1380117968e-02
17504,983,1.1569835335e-02
17514,984,1.1715668377e-02
17524,985,1.1822589583e-02
17534,986,1.1094127334e-02
17544,987,1.1278715156e-02
17554,988,1.1498840959e-02
17564,989,1.1378299459e-02
17574,990,1.1371427600e-02
17584,991,1.1498856623e-02
17594,992,1.1736400968e-02
17604,993,1.1519346451e-02
17614,994,1.1634763814e-02
17624,995,1.1594505937e-02
17634,996,1.1827741545e-02
17644,997,1.2013900469e-02
17654,998,1.1972761675e-02
17664,999,1.1557043803e-02
17674,1000,1.1712791018e-02
17684,1001,1.1347747768e-02
17694,1002,1.1528811308e-02
17704,1003,1.1524953245e-02
17714,1004,1.1914997487e-02
17724,1005,1.1736986107e-02
17734,1006,1.1391134196e-02
17744,1007,1.1414629042e-02
17754,1008,1.1812000042e-02
17764,1009,1.1924803034e-02
17774,1010,1.1976769542e-02
17784,1011,1.1948932732e-02
17794,1012,1.1738695446e-02
17804,1013,1.1689225338e-02
17814,1014,1.1998664811e-02
17824,1015,1.2231890357e-02
17834,1016,1.1833448861e-02
17844,1017,1.1516570116e-02
17854,1018,1.1793588731e-02
17864,1019,1.2191044314e-02
17874,1020,1.1880566419e-02
17884,1021,1.1844599836e-02
17894,1022,1.2179639429e-02
17904,1023,1.2333189289e-02
spectrum_probe.py +316 -0
……@@ -0,0 +1,316 @@
1#!/usr/bin/env python3
2"""
3SPECTRUM PROBE — where does genuine detail stop in terrain-diffusion output?
4
5WHY THIS EXISTS
6
7hyper-games #46 §3. The band split between a learned coarse generator and our
8erosion filter is a decision about a wavelength, and it is only sound if we know
9the finest wavelength at which the model puts real energy.
10
11WHAT IT MEASURES
12
13The radially-averaged power spectrum of an elevation tile, P(k), fitted to a
14power law P(k) ~ k^-beta over a mid band, plus the residual of the top octave
15against that fit. A generator that produces genuine detail to its own Nyquist
16holds the power law all the way up. One that upsamples falls off a cliff.
17
18THE INSTRUMENT IS A LAYER TOO (#45 section 5)
19
20Every number below is worthless if the spectrum code is wrong, and a broken
21ruler reports confidently. So --selftest runs FIRST and must pass:
22
23 - white noise -> beta ~ 0 (flat spectrum), the null case
24 - a single sinusoid -> one sharp peak at the known bin, and nowhere else
25
26If either fails, no terrain number is printed at all.
27
28USAGE
29
30 python spectrum_probe.py --selftest
31 python spectrum_probe.py --url http://127.0.0.1:8817 --size 512 --scale 1
32 python spectrum_probe.py --url http://127.0.0.1:8817 --compare 1 2
33"""
34import argparse
35import sys
36
37import numpy as np
38
39
40# ---------------------------------------------------------------------------
41# The instrument.
42# ---------------------------------------------------------------------------
43
44def radial_power_spectrum(field):
45 """
46 Radially-averaged power spectrum of a 2-D real field.
47
48 Returns (k, power) where k is in cycles per field-width (so k=1 is one
49 cycle across the whole tile) and power is the mean |FFT|^2 in that
50 annulus. Bin k covers wavelength lambda = N / k pixels.
51
52 A Hann window is applied in both axes: the tile is a crop of an unbounded
53 field, so its edges do not match, and without a window that discontinuity
54 leaks broadband energy across every bin -- which would look exactly like
55 the "genuine high-frequency detail" this probe exists to detect.
56 """
57 a = np.asarray(field, dtype=np.float64)
58 if a.ndim != 2 or a.shape[0] != a.shape[1]:
59 raise ValueError(f"expected a square 2-D field, got {a.shape}")
60 n = a.shape[0]
61
62 # Remove mean and best-fit plane. A tilt is a k=0/k=1 feature and would
63 # otherwise dominate every low bin.
64 yy, xx = np.mgrid[0:n, 0:n].astype(np.float64)
65 A = np.column_stack([np.ones(n * n), xx.ravel(), yy.ravel()])
66 coef, *_ = np.linalg.lstsq(A, a.ravel(), rcond=None)
67 a = a - (A @ coef).reshape(n, n)
68
69 w = np.hanning(n)
70 a = a * w[:, None] * w[None, :]
71
72 f = np.fft.fftshift(np.fft.fft2(a))
73 power = (np.abs(f) ** 2) / (n * n)
74
75 cy = cx = n // 2
76 yy, xx = np.mgrid[0:n, 0:n]
77 r = np.sqrt((yy - cy) ** 2 + (xx - cx) ** 2)
78 rint = r.astype(np.int64)
79
80 nbins = n // 2
81 tot = np.bincount(rint.ravel(), weights=power.ravel(), minlength=nbins + 1)
82 cnt = np.bincount(rint.ravel(), minlength=nbins + 1)
83 with np.errstate(invalid="ignore", divide="ignore"):
84 radial = tot / np.maximum(cnt, 1)
85
86 k = np.arange(1, nbins) # drop k=0 (DC, removed anyway)
87 return k, radial[1:nbins]
88
89
90def fit_power_law(k, p, kmin, kmax):
91 """Least-squares fit of log10(P) = c - beta*log10(k) over [kmin, kmax]."""
92 m = (k >= kmin) & (k <= kmax) & (p > 0)
93 if m.sum() < 4:
94 raise ValueError(f"only {m.sum()} usable bins in [{kmin},{kmax}]")
95 lk, lp = np.log10(k[m]), np.log10(p[m])
96 slope, intercept = np.polyfit(lk, lp, 1)
97 pred = intercept + slope * lk
98 ss_res = float(np.sum((lp - pred) ** 2))
99 ss_tot = float(np.sum((lp - lp.mean()) ** 2))
100 r2 = 1.0 - ss_res / ss_tot if ss_tot > 0 else float("nan")
101 return -slope, intercept, r2
102
103
104def top_octave_residual(k, p, kmin, kmax):
105 """
106 How far the top octave sits below the power law fitted BELOW it, in dB.
107
108 Fit over [kmin, kmax], extrapolate into (kmax, nyquist], and report the
109 median dB shortfall. ~0 dB means the power law continues -- genuine
110 detail. A large negative number means the energy is not there.
111 """
112 beta, intercept, r2 = fit_power_law(k, p, kmin, kmax)
113 m = (k > kmax) & (p > 0)
114 if m.sum() < 3:
115 return beta, r2, float("nan"), 0
116 pred_log = intercept - beta * np.log10(k[m])
117 resid_db = 10.0 * (np.log10(p[m]) - pred_log)
118 return beta, r2, float(np.median(resid_db)), int(m.sum())
119
120
121# ---------------------------------------------------------------------------
122# Self-test. Nothing downstream runs unless this passes.
123# ---------------------------------------------------------------------------
124
125def selftest(n=512, verbose=True):
126 ok = True
127
128 # 1. White noise -> flat spectrum.
129 rng = np.random.default_rng(20260831)
130 k, p = radial_power_spectrum(rng.standard_normal((n, n)))
131 beta, _, r2 = fit_power_law(k, p, 8, n // 2 - 4)
132 passed = abs(beta) < 0.10
133 ok &= passed
134 if verbose:
135 print(f" [{'PASS' if passed else 'FAIL'}] white noise "
136 f"beta = {beta:+.4f} (expect 0.000 +/- 0.100)")
137
138 # 2. Single sinusoid at a known wavelength -> one peak, right bin.
139 lam = 32.0 # pixels per cycle
140 expect = int(round(n / lam)) # -> bin 16
141 xx = np.arange(n)
142 field = np.sin(2 * np.pi * xx / lam)[None, :] * np.ones((n, 1))
143 k, p = radial_power_spectrum(field)
144 peak = int(k[np.argmax(p)])
145 passed = peak == expect
146 ok &= passed
147 if verbose:
148 print(f" [{'PASS' if passed else 'FAIL'}] sinusoid lam={lam:.0f}px "
149 f"peak at k = {peak} (expect {expect})")
150
151 # 3a. Leakage check, on the UN-AVERAGED 2-D spectrum. A 1-D sinusoid puts
152 # all its energy in two bins; everything else is windowing leakage and
153 # must sit far below. This is the honest test of the FFT + window.
154 ff = np.fft.fftshift(np.fft.fft2(
155 (field - field.mean()) * np.hanning(n)[:, None] * np.hanning(n)[None, :]))
156 pw = np.abs(ff) ** 2
157 cy = cx = n // 2
158 yy2, xx2 = np.mgrid[0:n, 0:n]
159 near = (np.abs(xx2 - cx) <= expect + 2) & (np.abs(xx2 - cx) >= expect - 2) & \
160 (np.abs(yy2 - cy) <= 2)
161 far = ~near
162 ratio_db = 10 * np.log10(pw[near].max() / pw[far].max())
163 passed = ratio_db > 30
164 ok &= passed
165 if verbose:
166 print(f" [{'PASS' if passed else 'FAIL'}] leakage (2-D) "
167 f"peak is {ratio_db:.1f} dB above off-axis (expect > 30.0)")
168
169 # 3b. Purity through the RADIAL path, with an isotropic signal. Radial
170 # averaging divides an annulus's energy by its pixel count, so it
171 # dilutes a directional peak by ~10*log10(2*pi*r/2) -- about 17 dB at
172 # r=16 -- by construction, not by fault. The instrument is built for
173 # isotropic fields, so the check uses one: energy on a ring at k=16
174 # and nowhere else.
175 kx = np.fft.fftfreq(n) * n
176 KX, KY = np.meshgrid(kx, kx)
177 KR = np.sqrt(KX ** 2 + KY ** 2)
178 ring = (np.abs(KR - expect) < 0.5).astype(np.float64)
179 ph = rng.uniform(0, 2 * np.pi, (n, n))
180 field_iso = np.real(np.fft.ifft2(ring * np.exp(1j * ph)))
181 k, p = radial_power_spectrum(field_iso)
182 peak_iso = int(k[np.argmax(p)])
183 mask = np.abs(k - expect) > 2
184 ratio_db = 10 * np.log10(p.max() / p[mask].max())
185 passed = (peak_iso == expect) and ratio_db > 30
186 ok &= passed
187 if verbose:
188 print(f" [{'PASS' if passed else 'FAIL'}] ring k={expect} isotropic "
189 f"peak at k = {peak_iso}, {ratio_db:.1f} dB above next "
190 f"(expect {expect}, > 30.0)")
191
192 # 4. Known power law in -> same power law out.
193 for target in (1.0, 2.0):
194 f = np.fft.fftfreq(n) * n
195 kx, ky = np.meshgrid(f, f)
196 kr = np.sqrt(kx ** 2 + ky ** 2)
197 kr[0, 0] = 1.0
198 amp = kr ** (-target / 2.0)
199 ph = rng.uniform(0, 2 * np.pi, (n, n))
200 spec = amp * np.exp(1j * ph)
201 field = np.real(np.fft.ifft2(spec))
202 k, p = radial_power_spectrum(field)
203 beta, _, r2 = fit_power_law(k, p, 8, n // 4)
204 passed = abs(beta - target) < 0.20
205 ok &= passed
206 if verbose:
207 print(f" [{'PASS' if passed else 'FAIL'}] synthetic k^-{target:.0f} "
208 f"beta = {beta:.3f} (expect {target:.3f} +/- 0.200), R2 = {r2:.3f}")
209
210 return ok
211
212
213# ---------------------------------------------------------------------------
214# The terrain fetch.
215# ---------------------------------------------------------------------------
216
217def fetch_tile(url, i1, j1, size, scale, timeout=1800):
218 import urllib.request
219 q = (f"{url}/terrain?i1={i1}&j1={j1}&i2={i1 + size}&j2={j1 + size}"
220 f"&scale={scale}")
221 with urllib.request.urlopen(q, timeout=timeout) as r:
222 h = int(r.headers["X-Height"])
223 w = int(r.headers["X-Width"])
224 body = r.read()
225 nelev = h * w * 2
226 elev = np.frombuffer(body[:nelev], dtype="<i2").reshape(h, w).astype(np.float64)
227 climate = None
228 if len(body) > nelev:
229 climate = np.frombuffer(body[nelev:], dtype="<f4").reshape(h, w, 4)
230 return elev, climate
231
232
233def describe(name, elev, px_metres, kmin, kmax):
234 k, p = radial_power_spectrum(elev)
235 n = elev.shape[0]
236 beta, r2, resid_db, nbins = top_octave_residual(k, p, kmin, kmax)
237
238 print(f"\n {name}")
239 print(f" tile {n} x {n} px @ {px_metres:g} m/px "
240 f"= {n * px_metres / 1000:.1f} km across")
241 print(f" elevation min {elev.min():.0f} m max {elev.max():.0f} m "
242 f"mean {elev.mean():.0f} m sd {elev.std():.0f} m")
243 print(f" fit band k = {kmin}..{kmax} "
244 f"(lambda {n * px_metres / kmax:.0f} m .. {n * px_metres / kmin / 1000:.1f} km)")
245 print(f" power law P(k) ~ k^-{beta:.3f} R2 = {r2:.4f}")
246 print(f" above k={kmax} {resid_db:+.1f} dB vs the fit, "
247 f"median over {nbins} bins")
248 print(f" Nyquist lambda = {2 * px_metres:g} m")
249 return k, p, beta, resid_db
250
251
252def main():
253 ap = argparse.ArgumentParser()
254 ap.add_argument("--url", default="http://127.0.0.1:8817")
255 ap.add_argument("--size", type=int, default=512)
256 ap.add_argument("--i1", type=int, default=0)
257 ap.add_argument("--j1", type=int, default=0)
258 ap.add_argument("--native-metres", type=float, default=30.0,
259 help="metres per pixel at scale=1 for the loaded model")
260 ap.add_argument("--compare", type=int, nargs="*", default=[1],
261 help="scale factors to fetch and compare")
262 ap.add_argument("--selftest", action="store_true",
263 help="run the instrument checks and exit")
264 ap.add_argument("--csv", default=None, help="write spectra to this CSV")
265 args = ap.parse_args()
266
267 print("INSTRUMENT SELF-TEST")
268 if not selftest():
269 print("\n FAILED. The ruler is broken; no terrain number is meaningful.")
270 return 2
271 print(" all pass\n")
272 if args.selftest:
273 return 0
274
275 n = args.size
276 kmin, kmax = 6, n // 8 # fit below the top two octaves, extrapolate up
277
278 print(f"TERRAIN {args.url} region ({args.i1},{args.j1}) size {n}")
279 results = {}
280 for s in args.compare:
281 # Same WORLD region at every scale: coordinates are in target pixels.
282 # Same WORLD footprint at every scale: n native px wide. At scale s
283 # that is n*s target px starting at i1*s. Do NOT crop to n -- cropping
284 # would compare a smaller region, not a finer sampling of the same one.
285 elev, climate = fetch_tile(args.url, args.i1 * s, args.j1 * s, n * s, s)
286 elev_c = elev
287 px = args.native_metres / s
288 # kmin/kmax stay FIXED across scales. n*px is the same physical width
289 # for every scale, so a given k is the same physical wavelength -- the
290 # fit band is therefore identical and the residual above it asks
291 # exactly the question: does a finer scale add energy up there?
292 k, p, beta, resid = describe(
293 f"scale={s} ({px:g} m/px)", elev_c, px, kmin, kmax)
294 results[s] = (k, p, beta, resid)
295 if climate is not None:
296 c = climate
297 print(f" climate temp {c[...,0].mean():.1f} C "
298 f"t_season {c[...,1].mean():.0f} "
299 f"precip {c[...,2].mean():.0f} mm/yr "
300 f"p_cv {c[...,3].mean():.0f}%")
301
302 if args.csv:
303 import csv
304 with open(args.csv, "w", newline="") as fh:
305 wr = csv.writer(fh)
306 wr.writerow(["scale", "k", "power"])
307 for s, (k, p, _, _) in results.items():
308 for kk, pp in zip(k, p):
309 wr.writerow([s, int(kk), f"{pp:.10e}"])
310 print(f"\n spectra written to {args.csv}")
311
312 return 0
313
314
315if __name__ == "__main__":
316 sys.exit(main())