Files
gis/test/acceptance/lp_gen.py
Bakhtiyar Issakhmetov ca87d6961b test(acceptance): reproducible Stage 8 acceptance package
Add test/acceptance/ so the customer's reviewers can re-run what the
Stage 8 documents claim on an isolated stand built from this branch:

- compose.acceptance.yml + stand.sh: compose project gis-acceptance
  (app 18080, postgres 15432, minio 19000/19001, rabbitmq mgmt 15672),
  POSTGIS_PLATFORM / MINIO_IMAGE knobs for arm64 and amd64 hosts.
- functional.sh: the 89 test-stand checks of Приложение 7 section 5
  (F01-F38, N01-N38, L02-L05, L07-L15), own prefixed test data,
  PostGIS/MinIO confirmation via the stand containers, full cleanup.
- lp_gen.py / lp_run.sh / lp_verify.py: synthetic ЛП inputs, a run from
  stage 0, and an independent numpy re-implementation of every node that
  compares all 15 outputs pixel by pixel; read-only (GET) mode works
  against any stand, in row/column windows for 22219x14977 rasters.
- load.sh + seed.sh: scenarios S1-S6 of Приложение 3 with hey 0.1.5
  running inside the stand network; CSV/JSON summaries, criteria K1-K4.
- scan.sh: govulncheck (source and binary), gosec, trivy fs/image,
  golangci-lint; fails on any High/Critical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:37:28 +05:00

112 lines
4.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generates the synthetic input rasters of the Landscape Program (ЛП) test.
Writes 13 grid-aligned GeoTIFFs, one per input slot (file name = slot code),
plus one deliberately misaligned raster for the negative test (L13):
<out>/lp-z-biodiversity.tif ... <out>/lp-p-tourism.tif (13 files)
<out>/misaligned-lp-u-veg.tif (half size, 2x pixel)
Every input raster: int16, EPSG:32642 (UTM 42N), 100 m pixels, default
3000 x 2000 pixels, classes 1..5 from smooth trigonometric patterns (a
different pattern per slot, so ties and every class combination occur), and a
NoData (-9999) frame 40 pixels wide. Extra edge cases exercise the pixel rules:
* lp-u-crop: a 200 x 200 NoData block — U is computed from water, soil and
veg only (the crop/veg union rule of u_integrated / u_max);
* lp-u-veg: a 100 x 100 NoData block overlapping half of the crop block —
where both crop and veg are missing, U must be NoData;
* lp-p-snow: a 120 x 120 block of 0 and a 60 x 60 block of 6 — values that
are not NoData but outside 1..5, hence invalid (the "valid iff 1 <= v <= 5"
rule).
Output is deterministic (no random numbers), so the same command always
produces byte-identical pixel values.
python lp_gen.py --out /tmp/lp-inputs [--width 3000 --height 2000]
"""
import argparse
import os
import sys
import numpy as np
import rasterio
from rasterio.transform import from_origin
SLOTS = [
"lp-z-biodiversity", "lp-z-lcr", "lp-z-soil-protection", "lp-z-wr",
"lp-u-surf-water", "lp-u-soil", "lp-u-crop", "lp-u-veg",
"lp-p-irrigation", "lp-p-snow", "lp-p-crop", "lp-p-pasture", "lp-p-tourism",
]
NODATA = -9999
CRS = "EPSG:32642"
ORIGIN_X, ORIGIN_Y = 300000.0, 6100000.0 # UTM 42N, North Kazakhstan region
PIXEL = 100.0
FRAME = 40
def pattern(k, width, height):
"""Smooth class field 1..5 for slot index k."""
y, x = np.mgrid[0:height, 0:width].astype(np.float64)
fx = 2 * np.pi * (1.0 + 0.37 * k) / width
fy = 2 * np.pi * (0.8 + 0.29 * ((k * 5) % 7)) / height
v = (np.sin(fx * x + 0.9 * k) * np.cos(fy * y + 0.4 * k)
+ 0.5 * np.sin(fx * 2.3 * x + fy * 1.7 * y + k))
v = (v + 1.5) / 3.0 # ~0..1
cls = np.floor(np.clip(v, 0, 0.999999) * 5).astype(np.int16) + 1
return cls
def block(arr, r0, c0, h, w, value):
arr[r0:r0 + h, c0:c0 + w] = value
def write(path, arr, transform):
profile = dict(driver="GTiff", width=arr.shape[1], height=arr.shape[0], count=1,
dtype="int16", crs=CRS, transform=transform, nodata=NODATA,
tiled=True, blockxsize=256, blockysize=256, compress="DEFLATE")
with rasterio.open(path, "w", **profile) as dst:
dst.write(arr, 1)
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--out", required=True, help="output directory")
ap.add_argument("--width", type=int, default=3000)
ap.add_argument("--height", type=int, default=2000)
args = ap.parse_args()
w, h = args.width, args.height
if w < 400 or h < 400:
sys.exit("width/height must be >= 400")
os.makedirs(args.out, exist_ok=True)
transform = from_origin(ORIGIN_X, ORIGIN_Y, PIXEL, PIXEL)
for k, slot in enumerate(SLOTS):
arr = pattern(k, w, h)
arr[:FRAME, :] = NODATA
arr[-FRAME:, :] = NODATA
arr[:, :FRAME] = NODATA
arr[:, -FRAME:] = NODATA
cr, cc = h // 2 - 100, w // 2 - 100 # centre block anchor
if slot == "lp-u-crop":
block(arr, cr, cc, 200, 200, NODATA)
if slot == "lp-u-veg":
block(arr, cr + 50, cc + 150, 100, 100, NODATA)
if slot == "lp-p-snow":
block(arr, 300, 300, 120, 120, 0)
block(arr, 300, 500, 60, 60, 6)
write(os.path.join(args.out, slot + ".tif"), arr, transform)
print("wrote", slot + ".tif")
# Misaligned raster for L13: half the size, double pixel => same footprint,
# different grid.
mis = pattern(7, w // 2, h // 2)
write(os.path.join(args.out, "misaligned-lp-u-veg.tif"), mis,
from_origin(ORIGIN_X, ORIGIN_Y, PIXEL * 2, PIXEL * 2))
print("wrote misaligned-lp-u-veg.tif (%dx%d, %d m)" % (w // 2, h // 2, int(PIXEL * 2)))
return 0
if __name__ == "__main__":
sys.exit(main())