| … | … | @@ -96,9 +96,29 @@ ERODE_KNOBS = { |
| 96 | 96 | "e_rounding_input": ("f", 0.005, 4.0), |
| 97 | 97 | "e_offset": ("f", -2.0, 1.0), |
| 98 | 98 | "e_vscale": ("f", 0.0, 20000.0), |
| 99 | # Run the filter on a FINER grid than the model's. The model has nothing |
| 100 | # below its 60 m Nyquist, so upsampling adds no information -- it is a |
| 101 | # carrier. The filter is analytic and point-evaluable, so it genuinely |
| 102 | # generates detail there. Without this the erosion band is floored at 60 m |
| 103 | # and competes with the model instead of extending it. |
| 104 | "e_upsample": ("i", 1, 4), |
| 99 | 105 | } |
| 100 | 106 | |
| 101 | 107 | |
| 108 | def bilinear_up(h, f): |
| 109 | """Nearest-honest upsample: no information is claimed, only carried.""" |
| 110 | if f <= 1: |
| 111 | return h |
| 112 | n = h.shape[0] |
| 113 | t = np.linspace(0, n - 1, n * f) |
| 114 | i0 = np.clip(t.astype(int), 0, n - 2) |
| 115 | fr = (t - i0) |
| 116 | a = h[np.ix_(i0, i0)]; b = h[np.ix_(i0 + 1, i0)] |
| 117 | c = h[np.ix_(i0, i0 + 1)]; d = h[np.ix_(i0 + 1, i0 + 1)] |
| 118 | fy = fr[:, None]; fx = fr[None, :] |
| 119 | return a * (1 - fy) * (1 - fx) + b * fy * (1 - fx) + c * (1 - fy) * fx + d * fy * fx |
| 120 | |
| 121 | |
| 102 | 122 | def erode_field(h, mpp, args): |
| 103 | 123 | """Run erode_heightmap on an in-memory field. Returns (height, ridge).""" |
| 104 | 124 | import tempfile |
| … | … | @@ -120,6 +140,8 @@ def erode_field(h, mpp, args): |
| 120 | 140 | ("e_cell_scale", "--cell-scale"), |
| 121 | 141 | ("e_rounding_input", "--rounding-input"), |
| 122 | 142 | ("e_offset", "--offset"), ("e_vscale", "--vscale")): |
| 143 | if k == "e_upsample": |
| 144 | continue |
| 123 | 145 | v = gv(k) |
| 124 | 146 | if v is not None: |
| 125 | 147 | cmd += [flag, repr(v)] |
| … | … | @@ -307,10 +329,14 @@ def api_diffusion(): |
| 307 | 329 | i1 = int(float(a.get("i1", 0))); j1 = int(float(a.get("j1", 2048))) |
| 308 | 330 | h, climate = fetch_diffusion(app.config["TD_URL"], i1, j1, n, a.get("seed")) |
| 309 | 331 | extra = {"X-Source": "terrain-diffusion", "X-Mpp": "30"} |
| 332 | mpp_d = 30.0 |
| 310 | 333 | if a.get("erode") in ("1", "true", "on"): |
| 311 | | h, _ridge = erode_field(h, 30.0, a) |
| 334 | f = int(_clamp(float(a.get("e_upsample", 1)), 1, 4)) |
| 335 | h = bilinear_up(h, f); mpp_d = 30.0 / f |
| 336 | h, _ridge = erode_field(h, mpp_d, a) |
| 312 | 337 | extra["X-Source"] = "terrain-diffusion + erosion_filter" |
| 313 | | png, lo, hi = colourise(h, 30.0, a.get("view", "shaded-height")) |
| 338 | extra["X-Mpp"] = repr(mpp_d) |
| 339 | png, lo, hi = colourise(h, mpp_d, a.get("view", "shaded-height")) |
| 314 | 340 | if climate is not None: |
| 315 | 341 | extra["X-Climate"] = (f"{climate[...,0].mean():.1f}C," |
| 316 | 342 | f"{climate[...,1].mean():.0f}," |
| … | … | @@ -369,15 +395,18 @@ def api_diffusion_bin(): |
| 369 | 395 | i1 = int(float(a.get("i1", 0))); j1 = int(float(a.get("j1", 2048))) |
| 370 | 396 | h, climate = fetch_diffusion(app.config["TD_URL"], i1, j1, n, a.get("seed")) |
| 371 | 397 | extra = {"X-Source": "terrain-diffusion"} |
| 398 | mpp_d = 30.0 |
| 372 | 399 | if a.get("erode") in ("1", "true", "on"): |
| 373 | | h, _ridge = erode_field(h, 30.0, a) |
| 400 | f = int(_clamp(float(a.get("e_upsample", 1)), 1, 4)) |
| 401 | h = bilinear_up(h, f); mpp_d = 30.0 / f |
| 402 | h, _ridge = erode_field(h, mpp_d, a) |
| 374 | 403 | extra["X-Source"] = "terrain-diffusion + erosion_filter" |
| 375 | 404 | if climate is not None: |
| 376 | 405 | extra["X-Climate"] = (f"{climate[...,0].mean():.1f}C," |
| 377 | 406 | f"{climate[...,1].mean():.0f}," |
| 378 | 407 | f"{climate[...,2].mean():.0f}mm," |
| 379 | 408 | f"{climate[...,3].mean():.0f}%") |
| 380 | | return _raw_response(h, 30.0, extra) |
| 409 | return _raw_response(h, mpp_d, extra) |
| 381 | 410 | except Exception as e: |
| 382 | 411 | return Response(str(e), status=400, mimetype="text/plain") |
| 383 | 412 | |