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>
This commit is contained in:
111
test/acceptance/lp_gen.py
Executable file
111
test/acceptance/lp_gen.py
Executable file
@@ -0,0 +1,111 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user