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:
436
test/acceptance/lp_verify.py
Executable file
436
test/acceptance/lp_verify.py
Executable file
@@ -0,0 +1,436 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Independent pixel-by-pixel check of a Landscape Program (ЛП) run.
|
||||
|
||||
Re-implements every node of the ЛП graph in numpy — 00 z_integrated,
|
||||
01 u_integrated, 02 z_max, 03 u_max, 10 e_ecological_constraint,
|
||||
20 c_conflicts, 21 p_max, 30 func_zoning (V1–V3 with the run's thresholds and
|
||||
tie_priority), 31 c_max — from the 13 leaf input rasters the run actually used,
|
||||
and compares the result with all 15 output rasters the run published.
|
||||
|
||||
The program is READ-ONLY: it issues only HTTP GET requests, so it can be
|
||||
pointed at any stand, production included:
|
||||
|
||||
python lp_verify.py --base-url http://localhost:18080 --run <run-id>
|
||||
python lp_verify.py --base-url https://dssgis.dwh.kz --run <run-id>
|
||||
|
||||
Data flow:
|
||||
GET /landscape/runs/{id} run params, per-node inputs and outputs
|
||||
GET /datasets/{id}.cog every input (13) and output (15) raster
|
||||
(or /datasets/{id}/download for inputs with --inputs-from download)
|
||||
|
||||
The leaf inputs are taken from the run's node `inputs` (group = slot code), the
|
||||
outputs from the node `outputs`. Expected values are computed through the whole
|
||||
chain from the leaf inputs only (E from the re-computed Z/U, C and V from the
|
||||
re-computed E, ...), so a wrong intermediate output cannot hide a later error.
|
||||
The run's own chain references (node 10 reading node 00's output, ...) are
|
||||
checked for consistency as well.
|
||||
|
||||
Large rasters (production: 22219 x 14977) are processed in windows of
|
||||
--block-rows x --block-cols pixels (default 512 x 4096, aligned with the
|
||||
512-pixel COG tiles), so memory stays bounded (~0.5 GB).
|
||||
|
||||
Pixel rules (source of truth: internal/landscape/kernels.go):
|
||||
valid(v) 1 <= v <= 5 (NoData and anything else is invalid); class
|
||||
values are used as int16(v) (truncation)
|
||||
classify(x) float32 x: < 1 -> NoData, <= 1.8 -> 1, <= 2.6 -> 2,
|
||||
<= 3.4 -> 3, <= 4.2 -> 4, <= 5.0 -> 5, else NoData
|
||||
z_integrated all Z valid: classify(0.25*Z1 + 0.25*Z2 + 0.25*Z3 + 0.25*Z4)
|
||||
(float32, summed left to right)
|
||||
u_integrated water & soil & (crop | veg): classify(num/den),
|
||||
num = 0.25*water + 0.25*soil [+ 0.25*crop] [+ 0.25*veg],
|
||||
den = 0.5 [+ 0.25] [+ 0.25] (float32)
|
||||
z_max all Z valid: max; u_max: U domain as above, max of valid
|
||||
e Z_int & U_int valid: max(Z, U)
|
||||
c_conflicts P_j & E valid: min(P_j, E)
|
||||
p_max, c_max max over valid inputs; NoData when none is valid
|
||||
func_zoning E valid & any P_k valid; candidate k: P_k valid & rule(E, P_k)
|
||||
V1: E<=2 & S>=v1_min_s_e12 | E==3 & S>=v1_min_s_e3
|
||||
V2: E<=2 | E==3 & S>=v2_min_s_e3 | E==4 & S>=v2_min_s_e4
|
||||
V3: E<=4
|
||||
no candidate -> 1; else sector with max S (ties by
|
||||
tie_priority) + 1
|
||||
|
||||
Exit code: 0 when every output matches pixel for pixel, 1 on any mismatch,
|
||||
2 on a usage / data error.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
import rasterio
|
||||
from rasterio.windows import Window
|
||||
except ImportError: # pragma: no cover
|
||||
sys.exit("lp_verify.py needs numpy and rasterio: pip install numpy rasterio")
|
||||
|
||||
NODATA = np.int16(-9999)
|
||||
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",
|
||||
]
|
||||
Z = SLOTS[0:4]
|
||||
U = SLOTS[4:8] # water, soil, crop, veg
|
||||
P = SLOTS[8:13] # sector order 1..5
|
||||
C = ["lp-c-irrigation", "lp-c-snow", "lp-c-crop", "lp-c-pasture", "lp-c-tourism"]
|
||||
OUTPUTS = [
|
||||
"lp-z-integrated", "lp-u-integrated", "lp-z-max", "lp-u-max", "lp-e",
|
||||
*C, "lp-p-max", "lp-v1-conservative", "lp-v2-balanced", "lp-v3-development", "lp-c-max",
|
||||
]
|
||||
OUTPUT_NODE = {
|
||||
"lp-z-integrated": "00", "lp-u-integrated": "01", "lp-z-max": "02", "lp-u-max": "03", "lp-e": "10",
|
||||
**{c: "20" for c in C}, "lp-p-max": "21",
|
||||
"lp-v1-conservative": "30", "lp-v2-balanced": "30", "lp-v3-development": "30", "lp-c-max": "31",
|
||||
}
|
||||
DEFAULT_PARAMS = {"tie_priority": [1, 2, 3, 4, 5], "v1_min_s_e12": 3, "v1_min_s_e3": 4,
|
||||
"v2_min_s_e3": 3, "v2_min_s_e4": 5}
|
||||
|
||||
F32 = np.float32
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- pixel rules
|
||||
def valid(v):
|
||||
return (v >= 1) & (v <= 5)
|
||||
|
||||
|
||||
def cls(v, ok):
|
||||
"""int16(v) for valid pixels (Go truncation), 0 elsewhere."""
|
||||
return np.where(ok, np.trunc(np.where(ok, v, 0)), 0).astype(np.int16)
|
||||
|
||||
|
||||
def classify(x):
|
||||
x = x.astype(np.float32, copy=False)
|
||||
return np.select(
|
||||
[x < F32(1.0), x <= F32(1.8), x <= F32(2.6), x <= F32(3.4), x <= F32(4.2), x <= F32(5.0)],
|
||||
[NODATA, 1, 2, 3, 4, 5], default=NODATA).astype(np.int16)
|
||||
|
||||
|
||||
def quarter(v):
|
||||
# float32(float32(0.25) * float32(v)), as in the Go kernels
|
||||
return F32(0.25) * v.astype(np.float32)
|
||||
|
||||
|
||||
def z_integrated(z):
|
||||
ok = valid(z[0]) & valid(z[1]) & valid(z[2]) & valid(z[3])
|
||||
score = ((quarter(z[0]) + quarter(z[1])) + quarter(z[2])) + quarter(z[3])
|
||||
return np.where(ok, classify(score), NODATA).astype(np.int16)
|
||||
|
||||
|
||||
def u_domain(u):
|
||||
water, soil, crop, veg = u
|
||||
crop_ok, veg_ok = valid(crop), valid(veg)
|
||||
return valid(water) & valid(soil) & (crop_ok | veg_ok), crop_ok, veg_ok
|
||||
|
||||
|
||||
def u_integrated(u):
|
||||
water, soil, crop, veg = u
|
||||
ok, crop_ok, veg_ok = u_domain(u)
|
||||
num = quarter(water) + quarter(soil)
|
||||
den = np.full(num.shape, F32(0.5), dtype=np.float32)
|
||||
num = np.where(crop_ok, num + quarter(crop), num).astype(np.float32)
|
||||
den = np.where(crop_ok, den + F32(0.25), den).astype(np.float32)
|
||||
num = np.where(veg_ok, num + quarter(veg), num).astype(np.float32)
|
||||
den = np.where(veg_ok, den + F32(0.25), den).astype(np.float32)
|
||||
with np.errstate(invalid="ignore", divide="ignore"):
|
||||
score = (num / den).astype(np.float32)
|
||||
return np.where(ok, classify(score), NODATA).astype(np.int16)
|
||||
|
||||
|
||||
def z_max(z):
|
||||
ok = valid(z[0]) & valid(z[1]) & valid(z[2]) & valid(z[3])
|
||||
m = np.maximum.reduce([cls(v, ok) for v in z])
|
||||
return np.where(ok, m, NODATA).astype(np.int16)
|
||||
|
||||
|
||||
def u_max(u):
|
||||
water, soil, crop, veg = u
|
||||
ok, crop_ok, veg_ok = u_domain(u)
|
||||
m = np.maximum(cls(water, ok), cls(soil, ok))
|
||||
m = np.where(crop_ok, np.maximum(m, cls(crop, crop_ok)), m)
|
||||
m = np.where(veg_ok, np.maximum(m, cls(veg, veg_ok)), m)
|
||||
return np.where(ok, m, NODATA).astype(np.int16)
|
||||
|
||||
|
||||
def e_constraint(zi, ui):
|
||||
ok = valid(zi) & valid(ui)
|
||||
return np.where(ok, np.maximum(zi, ui), NODATA).astype(np.int16)
|
||||
|
||||
|
||||
def c_conflict(p, e):
|
||||
ok = valid(p) & valid(e)
|
||||
return np.where(ok, np.minimum(cls(p, ok), e), NODATA).astype(np.int16)
|
||||
|
||||
|
||||
def max_any_valid(layers):
|
||||
oks = [valid(v) for v in layers]
|
||||
m = np.maximum.reduce([cls(v, ok) for v, ok in zip(layers, oks)])
|
||||
anyok = np.logical_or.reduce(oks)
|
||||
return np.where(anyok, m, NODATA).astype(np.int16)
|
||||
|
||||
|
||||
def func_zoning(e, p, params):
|
||||
eok = valid(e)
|
||||
ev = np.where(eok, e, 0).astype(np.int16)
|
||||
base = [valid(v) for v in p]
|
||||
s = [cls(v, ok) for v, ok in zip(p, base)]
|
||||
any_base = np.logical_or.reduce(base)
|
||||
t = params
|
||||
rules = [
|
||||
lambda e_, s_: ((e_ <= 2) & (s_ >= t["v1_min_s_e12"])) | ((e_ == 3) & (s_ >= t["v1_min_s_e3"])),
|
||||
lambda e_, s_: (e_ <= 2) | ((e_ == 3) & (s_ >= t["v2_min_s_e3"])) | ((e_ == 4) & (s_ >= t["v2_min_s_e4"])),
|
||||
lambda e_, s_: e_ <= 4,
|
||||
]
|
||||
outs = []
|
||||
for rule in rules:
|
||||
cand = [b & rule(ev, sk) for b, sk in zip(base, s)]
|
||||
any_cand = np.logical_or.reduce(cand)
|
||||
max_s = np.maximum.reduce([np.where(ck, sk, 0) for ck, sk in zip(cand, s)]).astype(np.int16)
|
||||
sector = np.zeros(ev.shape, dtype=np.int16)
|
||||
done = np.zeros(ev.shape, dtype=bool)
|
||||
for code in t["tie_priority"]:
|
||||
k = code - 1
|
||||
hit = cand[k] & (s[k] == max_s) & ~done
|
||||
sector[hit] = code
|
||||
done |= hit
|
||||
v = np.where(any_cand, sector + 1, 1).astype(np.int16)
|
||||
outs.append(np.where(eok & any_base, v, NODATA).astype(np.int16))
|
||||
return outs
|
||||
|
||||
|
||||
def expected_block(inp, params):
|
||||
"""inp: dict slot -> float64 array. Returns dict output group -> int16."""
|
||||
z = [inp[g] for g in Z]
|
||||
u = [inp[g] for g in U]
|
||||
p = [inp[g] for g in P]
|
||||
out = {
|
||||
"lp-z-integrated": z_integrated(z),
|
||||
"lp-u-integrated": u_integrated(u),
|
||||
"lp-z-max": z_max(z),
|
||||
"lp-u-max": u_max(u),
|
||||
}
|
||||
e = e_constraint(out["lp-z-integrated"], out["lp-u-integrated"])
|
||||
out["lp-e"] = e
|
||||
for cg, pk in zip(C, p):
|
||||
out[cg] = c_conflict(pk, e)
|
||||
out["lp-p-max"] = max_any_valid(p)
|
||||
v1, v2, v3 = func_zoning(e, p, params)
|
||||
out["lp-v1-conservative"], out["lp-v2-balanced"], out["lp-v3-development"] = v1, v2, v3
|
||||
out["lp-c-max"] = max_any_valid([out[c] for c in C])
|
||||
return out
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- HTTP (GET)
|
||||
def http_get(url, timeout=120, retries=3):
|
||||
req = urllib.request.Request(url, method="GET", headers={"User-Agent": "lp_verify/1.0"})
|
||||
last = None
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
return urllib.request.urlopen(req, timeout=timeout)
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code < 500:
|
||||
raise
|
||||
last = e
|
||||
except (urllib.error.URLError, TimeoutError, ConnectionError) as e:
|
||||
last = e
|
||||
time.sleep(2 * (attempt + 1))
|
||||
raise last
|
||||
|
||||
|
||||
def get_json(url):
|
||||
with http_get(url) as r:
|
||||
return json.loads(r.read().decode("utf-8"))
|
||||
|
||||
|
||||
def download(url, path, wait=0):
|
||||
"""GET url into path (reused when already cached). A 409 means the COG is
|
||||
still being produced (conversion is asynchronous): retry for up to `wait`
|
||||
seconds."""
|
||||
if os.path.exists(path) and os.path.getsize(path) > 0:
|
||||
return "cached"
|
||||
tmp = path + ".part"
|
||||
deadline = time.time() + wait
|
||||
while True:
|
||||
try:
|
||||
r = http_get(url, timeout=600)
|
||||
break
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 409 or time.time() >= deadline:
|
||||
raise
|
||||
time.sleep(2)
|
||||
with r, open(tmp, "wb") as f:
|
||||
shutil.copyfileobj(r, f, length=4 << 20)
|
||||
os.replace(tmp, path)
|
||||
return "%.1f MB" % (os.path.getsize(path) / 1e6)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------- main
|
||||
def parse_points(s):
|
||||
pts = []
|
||||
for part in filter(None, (x.strip() for x in s.split(";"))):
|
||||
c, r = part.split(",")
|
||||
pts.append((int(c), int(r)))
|
||||
return pts
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--base-url", default=os.environ.get("BASE_URL", "http://localhost:18080"))
|
||||
ap.add_argument("--run", required=True, help="landscape run id")
|
||||
ap.add_argument("--cache", help="directory for downloaded rasters (kept; existing files are reused)")
|
||||
ap.add_argument("--inputs-from", choices=["cog", "download"], default="cog",
|
||||
help="read inputs from their COG (default) or the original upload (what the node read)")
|
||||
ap.add_argument("--block-rows", type=int, default=512)
|
||||
ap.add_argument("--block-cols", type=int, default=4096)
|
||||
ap.add_argument("--wait", type=int, default=180,
|
||||
help="seconds to keep retrying a COG that is not ready yet (HTTP 409), default 180")
|
||||
ap.add_argument("--points", help='spot-check pixels "col,row;col,row;..." and print their values')
|
||||
args = ap.parse_args()
|
||||
base = args.base_url.rstrip("/")
|
||||
|
||||
run = get_json("%s/landscape/runs/%s" % (base, args.run))
|
||||
params = dict(DEFAULT_PARAMS)
|
||||
raw_params = run.get("params") or {}
|
||||
for k in DEFAULT_PARAMS:
|
||||
if raw_params.get(k):
|
||||
params[k] = raw_params[k]
|
||||
print("run %s status=%s from_stage=%s" % (run["id"], run["status"], run.get("from_stage")))
|
||||
print("params %s" % json.dumps(params))
|
||||
nodes = sorted(run.get("nodes") or [], key=lambda n: n["node_code"])
|
||||
print("nodes %s" % " ".join("%s:%s" % (n["node_code"], n["status"]) for n in nodes))
|
||||
if run["status"] != "succeeded":
|
||||
print("ERROR: run is not succeeded; nothing to verify")
|
||||
return 2
|
||||
|
||||
errors = []
|
||||
slot_ids, out_ids = {}, {}
|
||||
for n in nodes:
|
||||
for ref in n.get("inputs") or []:
|
||||
g, did = ref["group"], ref["dataset_id"]
|
||||
if g in SLOTS:
|
||||
if slot_ids.setdefault(g, did) != did:
|
||||
errors.append("slot %s resolved to two datasets (%s, %s)" % (g, slot_ids[g], did))
|
||||
for ref in n.get("outputs") or []:
|
||||
out_ids[ref["group"]] = ref["dataset_id"]
|
||||
if OUTPUT_NODE.get(ref["group"]) != n["node_code"]:
|
||||
errors.append("output %s reported by node %s" % (ref["group"], n["node_code"]))
|
||||
# chain consistency: an intermediate input must be what this run produced
|
||||
for n in nodes:
|
||||
for ref in n.get("inputs") or []:
|
||||
g = ref["group"]
|
||||
if g in OUTPUTS and out_ids.get(g) != ref["dataset_id"]:
|
||||
errors.append("node %s read %s=%s but the run's output is %s" % (n["node_code"], g, ref["dataset_id"], out_ids.get(g)))
|
||||
missing = [g for g in SLOTS if g not in slot_ids] + [g for g in OUTPUTS if g not in out_ids]
|
||||
if missing:
|
||||
errors.append("run does not reference: %s" % ", ".join(missing))
|
||||
if errors:
|
||||
for e in errors:
|
||||
print("ERROR:", e)
|
||||
return 2
|
||||
|
||||
cache = args.cache or tempfile.mkdtemp(prefix="lp_verify_")
|
||||
os.makedirs(cache, exist_ok=True)
|
||||
files = {}
|
||||
t0 = time.time()
|
||||
for g in SLOTS + OUTPUTS:
|
||||
did = slot_ids.get(g) or out_ids[g]
|
||||
if g in SLOTS and args.inputs_from == "download":
|
||||
url, path = "%s/datasets/%s/download" % (base, did), os.path.join(cache, did + ".orig.tif")
|
||||
else:
|
||||
url, path = "%s/datasets/%s.cog" % (base, did), os.path.join(cache, did + ".cog.tif")
|
||||
try:
|
||||
info = download(url, path, args.wait)
|
||||
except urllib.error.HTTPError as e:
|
||||
print("ERROR: GET %s -> HTTP %d (output not ready or missing?)" % (url, e.code))
|
||||
return 2
|
||||
files[g] = path
|
||||
print("fetch %-22s %s %s" % (g, did, info))
|
||||
print("downloaded in %.1f s to %s" % (time.time() - t0, cache))
|
||||
|
||||
with rasterio.Env(GDAL_CACHEMAX=512):
|
||||
ds = {g: rasterio.open(files[g]) for g in SLOTS + OUTPUTS}
|
||||
ref = ds[SLOTS[0]]
|
||||
for g, d in ds.items():
|
||||
if (d.width, d.height) != (ref.width, ref.height) or not d.transform.almost_equals(ref.transform) or d.crs != ref.crs:
|
||||
errors.append("%s grid %dx%d %s differs from %s %dx%d %s" % (
|
||||
g, d.width, d.height, d.crs, SLOTS[0], ref.width, ref.height, ref.crs))
|
||||
for g in OUTPUTS:
|
||||
if ds[g].dtypes[0] != "int16" or ds[g].nodata != -9999:
|
||||
errors.append("output %s: dtype %s nodata %s (want int16, -9999)" % (g, ds[g].dtypes[0], ds[g].nodata))
|
||||
if errors:
|
||||
for e in errors:
|
||||
print("ERROR:", e)
|
||||
return 2
|
||||
W, H = ref.width, ref.height
|
||||
print("grid %dx%d %s pixel %.3f" % (W, H, ref.crs, ref.transform.a))
|
||||
|
||||
if args.points:
|
||||
return spot_check(ds, parse_points(args.points), params)
|
||||
|
||||
mism = {g: 0 for g in OUTPUTS}
|
||||
validn = {g: 0 for g in OUTPUTS}
|
||||
first = {}
|
||||
t0 = time.time()
|
||||
for r0 in range(0, H, args.block_rows):
|
||||
h = min(args.block_rows, H - r0)
|
||||
for c0 in range(0, W, args.block_cols):
|
||||
w = min(args.block_cols, W - c0)
|
||||
win = Window(c0, r0, w, h)
|
||||
inp = {g: ds[g].read(1, window=win, masked=False).astype(np.float64) for g in SLOTS}
|
||||
exp = expected_block(inp, params)
|
||||
for g in OUTPUTS:
|
||||
act = ds[g].read(1, window=win, masked=False)
|
||||
bad = exp[g] != act
|
||||
nbad = int(bad.sum())
|
||||
validn[g] += int((exp[g] != NODATA).sum())
|
||||
if nbad:
|
||||
mism[g] += nbad
|
||||
if g not in first:
|
||||
rr, cc = np.argwhere(bad)[0]
|
||||
first[g] = (c0 + int(cc), r0 + int(rr), int(exp[g][rr, cc]), int(act[rr, cc]))
|
||||
print("compared in %.1f s" % (time.time() - t0))
|
||||
for d in ds.values():
|
||||
d.close()
|
||||
|
||||
print()
|
||||
print("%-20s %-4s %-38s %12s %12s %10s" % ("output", "node", "dataset_id", "pixels", "valid(exp)", "mismatch"))
|
||||
total = 0
|
||||
for g in OUTPUTS:
|
||||
total += mism[g]
|
||||
line = "%-20s %-4s %-38s %12d %12d %10d" % (g, OUTPUT_NODE[g], out_ids[g], W * H, validn[g], mism[g])
|
||||
if g in first:
|
||||
line += " first at col=%d row=%d expected=%d actual=%d" % first[g]
|
||||
print(line)
|
||||
print()
|
||||
print("TOTAL mismatching pixels: %d -> %s" % (total, "PASS" if total == 0 else "FAIL"))
|
||||
return 0 if total == 0 else 1
|
||||
|
||||
|
||||
def spot_check(ds, points, params):
|
||||
bad = 0
|
||||
shown = ["lp-z-integrated", "lp-u-integrated", "lp-e", *C, "lp-v1-conservative", "lp-v2-balanced", "lp-v3-development"]
|
||||
for col, row in points:
|
||||
win = Window(col, row, 1, 1)
|
||||
inp = {g: ds[g].read(1, window=win, masked=False).astype(np.float64) for g in SLOTS}
|
||||
exp = expected_block(inp, params)
|
||||
vals = " ".join("%s=%d" % (g.replace("lp-", ""), int(inp[g][0, 0])) for g in SLOTS)
|
||||
print("point (%d,%d) inputs: %s" % (col, row, vals))
|
||||
for g in shown:
|
||||
act = int(ds[g].read(1, window=win, masked=False)[0, 0])
|
||||
e = int(exp[g][0, 0])
|
||||
ok = act == e
|
||||
bad += 0 if ok else 1
|
||||
print(" %-20s expected=%6d actual=%6d %s" % (g, e, act, "OK" if ok else "MISMATCH"))
|
||||
print("SPOT CHECK: %d points, %d mismatches -> %s" % (len(points), bad, "PASS" if bad == 0 else "FAIL"))
|
||||
return 0 if bad == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user