From 5693a074b3222291d350aebd4257e70633a5cf6f Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Sat, 16 May 2026 06:02:15 -0700 Subject: [PATCH 1/2] geotiff: golden corpus phase 2.4 - dtype variants (#1930) Eight single-band fixtures, one per dtype: int8, uint8, int16, uint16, int32, uint32, float32, float64. They share the manifest defaults (stripped, no compression, EPSG:4326); only dtype, pixel pattern, and seed vary. Integer fixtures use a new `noise_with_corners` pixel pattern that stamps the dtype min/max sentinels into the four corner pixels of every band. Float fixtures use plain `noise`; a NaN-in-pixels variant is deferred to Phase 2 PR 6 (nodata). Files are 20x20 so the float64 fixture (3566 bytes) stays under the 4 kB corpus ceiling. --- .../golden_corpus/fixtures/dtype_float32.tif | Bin 0 -> 1966 bytes .../golden_corpus/fixtures/dtype_float64.tif | Bin 0 -> 3566 bytes .../golden_corpus/fixtures/dtype_int16.tif | Bin 0 -> 1166 bytes .../golden_corpus/fixtures/dtype_int32.tif | Bin 0 -> 1966 bytes .../golden_corpus/fixtures/dtype_int8.tif | Bin 0 -> 766 bytes .../golden_corpus/fixtures/dtype_uint16.tif | Bin 0 -> 1166 bytes .../golden_corpus/fixtures/dtype_uint32.tif | Bin 0 -> 1966 bytes .../golden_corpus/fixtures/dtype_uint8.tif | Bin 0 -> 766 bytes .../geotiff/tests/golden_corpus/generate.py | 23 ++- .../geotiff/tests/golden_corpus/manifest.yaml | 103 +++++++++++ .../test_golden_corpus_dtype_variants_1930.py | 167 ++++++++++++++++++ 11 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_float32.tif create mode 100644 xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_float64.tif create mode 100644 xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int16.tif create mode 100644 xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int32.tif create mode 100644 xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int8.tif create mode 100644 xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint16.tif create mode 100644 xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint32.tif create mode 100644 xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint8.tif create mode 100644 xrspatial/geotiff/tests/test_golden_corpus_dtype_variants_1930.py diff --git a/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_float32.tif b/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_float32.tif new file mode 100644 index 0000000000000000000000000000000000000000..99d1595f7d3c9a59b8e7649ffe56934f886e01f5 GIT binary patch literal 1966 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 R#+Gm3V3$;^OOo~|{sZ#lZ_5Ax literal 0 HcmV?d00001 diff --git a/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int16.tif b/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int16.tif new file mode 100644 index 0000000000000000000000000000000000000000..b92ee5326ca6477dc9a508cd5d545ba06e2f807a GIT binary patch literal 1166 zcmebD)MDUZU|-owRWL9^*&uanNNhny7O>ttpoA!rxEPcT zGE*F?MuC}uK?X@pFqF*%RKwTI!vK_KI0wW{Ej$ctK>8UFuWx5&Pyo_mK=U`W^Duza zh5^|d+nK|r!Sln@zr0IZ_H%>ky85$qdw1_l;jSO9GUIvq#@ zEdd4;f`v&9 z14F}(gNy%ZXm&ctSgK$DE>TwT(qzJR)+Jn~Ph)mGPMj;vsm-1j^1nW!LfY)m)U+^* zU+Lx>87D0YO53qw=dBAY&+nfTNX(00dPqEUeU^!`^2c*?&Lv%`5_jDoT7BZPc@Mwi z^P-9Q$p`CpJkZ!&wygV3^^5k`6+Jg=o(E|zVa(vZze4JIr>J>w-crw}wpUhOcj=Up zzHxKj#FNY1E_i>x=~-y;u3NgnJf(K^eCL^hfzCHJ{a~}yy>6WxGAoHaFLvRiLkmyn ze+!Q0uXebq9aNZd@9*a+4BBhN`EI;P7UcNdeO>ynUnY8qTcKz1qh=GoD^mVvms)DnU!QZVeC~NJ)^qBU8-CT#G-2=i#A|eQ zl7fbdUEP&pCNE9F3D-9Cap5_T@xXXQ>O<)Q)TUUTSGG z>~-={jDNwW;45OsAyj;jEdZTECfJGS}Ev!%xlCCLR{xOGQ%?;in?Is0at Q{LY%M|CFQfyct6S0C#h(g#Z8m literal 0 HcmV?d00001 diff --git a/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int32.tif b/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int32.tif new file mode 100644 index 0000000000000000000000000000000000000000..582726caed54889abe417fd35869aef7e9e03335 GIT binary patch literal 1966 zcmai#c{r4N8^@nPlOvj8jI1H@N+ah!>xhA6{mj0oALd571zuKs(U>$;!s=Xc-V=l(5!{2pUtC4d(I04M-}xB(D{ zggF8Ffw_)YhLh($;sYP}2NneJ9DeJ|DZqcoiyZMm&r?SpJum=B{@}4ZVlIy7B#s~8 z=ICoq#KrRi94kQML^Ylpkm0BZr+*EeA2{$@bGR1Ib+~#D4j(-Gm1}>Rxgd4_(f?)t z-`lb4&=G5>_m2~FxSQhuz{5EU&TO2ebCfd)=cqsc4GQ9|#dE3v_;8#m_Y-do{-A#1 zPyry(2b&{R7I@H4@0rz2eVgl+GFK2$2km2BkQ%<10+WZ#lfvYtcgW~tSiXoVwZuwL zt}?r7@1DACB;RD@;8;Lq5s_$nX+w!=1!lv^f$OPL7qEsfoQ~CC)pKz@ClfZC9g{ul z#!ZlDy-#&V-@0czVS@1gIraJq$sv9-Rvd~oWX>C1^k{;6Hh#SV1eC@m?3SX=2x6Zs zejv&9@RSbL8%0mWW)0G|S9VvZS~rU7P>s6(D*O1w^wEfW@JF4^-SHEcDRfR%>s;@d z$f0)36Y~3fxjM@d*!edf|6H}p-6(YQQrRI?&t`_ zG$_x??bNMr4Sb6150DOk+r%X`Bhtb|zvXq%yD>OuGm?}6BElm4r8=CVzvHr9YEQ|+ zOHxAi=uy;rE@f(#=eY&jQ^V^_j+<6~u^#RwAkvp?zTuwgH2j6*rFaBMR3aMJo-Go@ z6rU!myNYT0&rRkJdbu$CQYm@jdBAUcJAEV^qw0c;Ug=gXu=KEj7 z)}DR}svtI>oIYzkb4ks14%t*AnGtT-o_{CxZZy8X=JZA*O(7o#vbC2H7M9mxm|gv{ zf`}T`(-=2SF83*s=sv4rt!gdhzv^@)y)Kg|&NC_?;1$2d1(<$A8s~`#-Tie~Gc&Cj zs>is6J@;x$Zh;=oCytx9a5r-5@jO9DdVHeC8&whw?dilm^rop8+G~5IwU89(>|?Up zR80E^QGdVp(gklDD`r@=!8Z^j`RnZXZg44czIDq3UhF!(S1tGXCzsJ$$4^6ALAIhp zb2F-`kHyzDi$1#>Ja5j~o^oXdFIexdh&(ZwVWn*C7aBf1yTG2XaGc$80thyf5iQ z0spav>B*DdT&!`W$_C$To75^6gsnf?ZSZ!tuWCW`=Ox~sE;Ttvb7oQXvmji_J8gl@ z&N0H_%{+ZXKK~-dI|=K}a0+$|0iKfyt++4iqZZ@oR9v=PXP?oUQNOCpJs;tG*C^Ai z-uT_tOZAsX!?Mb*?lD&QSo!j5*KAOdYLK3y(qkE1l&GxW!1WI>!pd6)20O$oWZ8?l zyHA3Lk9qMU?DOsD7Pq0FABfH=Y!0*k1Ltc&MBuVcrK3n=iX*$rkaGRb@}h;sDpnQq zwWqQ`j;(ocCoH&AsM1%y2z;J^W;%9cNT)?ZC|xZAVjlCar-U5n^f)xEOk2MxsUlH7 zv){oXp}aE1<5d8g7Kfy5>w*n}E{`ItB2!N11*%XA(pGGVIwx7GA@#^|XFxp+qcS1^ zxg`}UzVNBVF^Z8~vQhZRLm5j;m(J0EDQ&ThhF&QyEpgkl9rxsc14-T+XydE89DRnJ67NT2>fdx#R+UbuY~T`vWXz)%pHJC~RE*T-Q+ z15{$LB~kix+rAJ*SL0%pM)iscMVl+;4s67@MSv^F@It1xMLJ#X(uH^kcpmAz?XXOh z68@vW*urCqpXpz1_q8rreBw7mViI0?)gWhFdRtG4ksk_!6zR(2ry8C%_nEb#YP=|;}qIN-d*x&Vn%QUr$>4Q6Zt8@$ErV&bZ%lbP(A(@7z{MvwW6C514 zdhQ{t%?5L`SaINsTHu@Q7{r~(b}>ydTiK5Y*S&UXNg$hwxa$2-QQKF;`*~@?!N23b DI|)&D literal 0 HcmV?d00001 diff --git a/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int8.tif b/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_int8.tif new file mode 100644 index 0000000000000000000000000000000000000000..054a56c1828edec118f1d8ee64e3cdb2f64731f4 GIT binary patch literal 766 zcmebD)MDUZU|-qGR53%@Aa!g=Y(YjAu--hNgea1@7?ce% zQyi*h0#J<%lA2&Bn+d3fubGE|8AzW4;-(fJ1~wr542ajaGczavX)&PQ4edM(AhlsY z_QrN5u?imfep;>Q|9E@&<>L42Ga}-9l0gbWS10y%yU>465$x`9;{$tQp3>D zx*=-H7KK^M*Db1YKKO6jos5a~M~dC&Bt5(``F`b-hfjL$ESP@mx%t+h-5;2Io_ZL( zke~H__DWTq=M}c?a}ROc;bqI#2)MvkYO#@N;Umq%npaeQ@|3wTya}3Rt?$)-#JlA> z_qK_9-<}VhyQ?uZgB@#(x2;r)?e=qaLq*U8FTfphd>PdeIvxwKc2O+_|B3vQ>_w@#4j- z4Z2EkjSBFe zzfrdM>)A>Ey?06$EUe{xe?7L^LFE4Q{KqPPgKi!*Y&)2fG3`RdYyO470iPs{tG3N~ zwx~ix@k!LbRJnpe{*$FUKLjgWyywr(TIjp=P~e6|yLKu6Hm^Vb`sP#a>9!s%9WA}r L1>M-se`x>!s#Nqo literal 0 HcmV?d00001 diff --git a/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint16.tif b/xrspatial/geotiff/tests/golden_corpus/fixtures/dtype_uint16.tif new file mode 100644 index 0000000000000000000000000000000000000000..c4113d50098eb26f45b9aa750867d74081c7d441 GIT binary patch literal 1166 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 H_H_&ZZTRRy literal 0 HcmV?d00001 diff --git a/xrspatial/geotiff/tests/golden_corpus/generate.py b/xrspatial/geotiff/tests/golden_corpus/generate.py index d2da36088..9902f5c67 100644 --- a/xrspatial/geotiff/tests/golden_corpus/generate.py +++ b/xrspatial/geotiff/tests/golden_corpus/generate.py @@ -80,7 +80,7 @@ "none", "deflate", "lzw", "lerc", "jpeg", "packbits", "zstd", } ALLOWED_PHOTOMETRIC = {"minisblack", "miniswhite", "rgb", "ycbcr"} -ALLOWED_PATTERN = {"ramp", "checker", "noise", "uniform"} +ALLOWED_PATTERN = {"ramp", "checker", "noise", "uniform", "noise_with_corners"} ALLOWED_PREDICTOR = {1, 2, 3} @@ -291,7 +291,7 @@ def _make_pixels(entry: dict[str, Any]) -> np.ndarray: cols = np.arange(w)[None, :] single = ((rows // 8 + cols // 8) % 2).astype(np.float64) flat = np.broadcast_to(single, (bands, h, w)).reshape(-1).astype(np.float64) - elif pattern == "noise": + elif pattern in ("noise", "noise_with_corners"): rng = np.random.default_rng(int(entry.get("pixel_seed", 0))) flat = rng.random(n) elif pattern == "uniform": @@ -304,13 +304,28 @@ def _make_pixels(entry: dict[str, Any]) -> np.ndarray: # Map [0, 1) for noise / [0, n) for ramp into the dtype range. if pattern in ("ramp", "checker", "uniform"): arr = flat % (info.max - info.min + 1) + info.min - else: # noise + else: # noise / noise_with_corners arr = flat * (info.max - info.min) + info.min arr = arr.astype(dtype) else: arr = flat.astype(dtype) - return arr.reshape(bands, h, w) + arr = arr.reshape(bands, h, w) + + # ``noise_with_corners`` plants the dtype's min and max sentinels in the + # four corner pixels of every band so dtype-edge handling gets exercised + # by the corpus. Floats keep noise as-is; a NaN sentinel is a separate + # property tracked by Phase 2 PR 6 (nodata). + if pattern == "noise_with_corners" and dtype.kind in ("i", "u"): + info = np.iinfo(dtype) + lo = dtype.type(info.min) + hi = dtype.type(info.max) + arr[:, 0, 0] = lo + arr[:, 0, -1] = hi + arr[:, -1, 0] = hi + arr[:, -1, -1] = lo + + return arr def _resolve_crs(crs_spec: dict[str, Any] | None): diff --git a/xrspatial/geotiff/tests/golden_corpus/manifest.yaml b/xrspatial/geotiff/tests/golden_corpus/manifest.yaml index b4e2ce697..f2a9160af 100644 --- a/xrspatial/geotiff/tests/golden_corpus/manifest.yaml +++ b/xrspatial/geotiff/tests/golden_corpus/manifest.yaml @@ -64,6 +64,13 @@ # "checker" block checker for compression # "noise" seeded numpy.random (seed below) # "uniform" single constant value +# "noise_with_corners" +# seeded noise (same as "noise") +# with the four corner pixels of +# each band forced to the dtype's +# min/max sentinels. Integer-only +# corner stamping; floats keep +# noise as-is. # pixel_seed: int Seed for "noise" patterns. Default 0. # pixel_value: number Constant for "uniform" patterns. # @@ -204,3 +211,99 @@ fixtures: pixel_pattern: noise pixel_seed: 1930 tags: [fast, tiled, big_endian, layout_endian_matrix] + + # --------------------------------------------------------------------- + # Phase 2 PR 4 -- dtype variants (#1930). + # + # Eight single-band fixtures, one per supported dtype. All entries + # inherit the manifest defaults (stripped layout, blocksize 64, + # compression=none, minisblack, EPSG:4326, fixed transform) and override + # only ``dtype``, ``pixel_pattern``, ``pixel_seed``, and pixel dimensions + # (20 x 20 -- chosen so the float64 file stays under the 4 kB ceiling). + # Integer dtypes use the ``noise_with_corners`` pattern, which + # plants the dtype's min and max sentinels in the four corner pixels so + # readers exercise the dtype-edge values. Float dtypes use plain + # ``noise``; a NaN-in-pixels variant is the job of Phase 2 PR 6 (nodata). + # Each fixture uses a unique ``pixel_seed`` so dtypes do not share bit + # patterns once written. + # --------------------------------------------------------------------- + - id: dtype_int8 + description: >- + Single-band int8 stripped uncompressed; corner pixels carry the int8 + min/max sentinels. + width: 20 + height: 20 + dtype: int8 + pixel_pattern: noise_with_corners + pixel_seed: 1001 + tags: [fast, dtype, int8] + - id: dtype_uint8 + description: >- + Single-band uint8 stripped uncompressed; corner pixels carry the + uint8 min/max sentinels. + width: 20 + height: 20 + dtype: uint8 + pixel_pattern: noise_with_corners + pixel_seed: 1002 + tags: [fast, dtype, uint8] + - id: dtype_int16 + description: >- + Single-band int16 stripped uncompressed; corner pixels carry the + int16 min/max sentinels. + width: 20 + height: 20 + dtype: int16 + pixel_pattern: noise_with_corners + pixel_seed: 1003 + tags: [fast, dtype, int16] + - id: dtype_uint16 + description: >- + Single-band uint16 stripped uncompressed; corner pixels carry the + uint16 min/max sentinels. + width: 20 + height: 20 + dtype: uint16 + pixel_pattern: noise_with_corners + pixel_seed: 1004 + tags: [fast, dtype, uint16] + - id: dtype_int32 + description: >- + Single-band int32 stripped uncompressed; corner pixels carry the + int32 min/max sentinels. + width: 20 + height: 20 + dtype: int32 + pixel_pattern: noise_with_corners + pixel_seed: 1005 + tags: [fast, dtype, int32] + - id: dtype_uint32 + description: >- + Single-band uint32 stripped uncompressed; corner pixels carry the + uint32 min/max sentinels. + width: 20 + height: 20 + dtype: uint32 + pixel_pattern: noise_with_corners + pixel_seed: 1006 + tags: [fast, dtype, uint32] + - id: dtype_float32 + description: >- + Single-band float32 stripped uncompressed. NaN-in-pixels handling is + deferred to Phase 2 PR 6 (nodata). + width: 20 + height: 20 + dtype: float32 + pixel_pattern: noise + pixel_seed: 1007 + tags: [fast, dtype, float32] + - id: dtype_float64 + description: >- + Single-band float64 stripped uncompressed. NaN-in-pixels handling is + deferred to Phase 2 PR 6 (nodata). + width: 20 + height: 20 + dtype: float64 + pixel_pattern: noise + pixel_seed: 1008 + tags: [fast, dtype, float64] diff --git a/xrspatial/geotiff/tests/test_golden_corpus_dtype_variants_1930.py b/xrspatial/geotiff/tests/test_golden_corpus_dtype_variants_1930.py new file mode 100644 index 000000000..ee96507b8 --- /dev/null +++ b/xrspatial/geotiff/tests/test_golden_corpus_dtype_variants_1930.py @@ -0,0 +1,167 @@ +"""Smoke test for Phase 2 PR 4 dtype fixtures (issue #1930). + +This test pins the eight dtype fixtures added in Phase 2 PR 4: +``dtype_int8``, ``dtype_uint8``, ``dtype_int16``, ``dtype_uint16``, +``dtype_int32``, ``dtype_uint32``, ``dtype_float32``, ``dtype_float64``. + +It checks three things per fixture: + +* the ``.tif`` file is present on disk under + ``golden_corpus/fixtures/`` (i.e. the generator committed output); +* ``rasterio.open`` reports the dtype the manifest declared; +* the oracle (`compare_to_oracle`) accepts a candidate DataArray built + straight from the rasterio read. This is a trivial identity check -- + no backend wiring lives here (that is Phase 3) -- but it confirms the + oracle understands every dtype the corpus now ships. + +For the integer dtypes the test also verifies that the four corner +pixels carry the dtype's min / max sentinels, since that is the whole +point of the ``noise_with_corners`` pixel pattern this PR adds. +""" +from __future__ import annotations + +import importlib +import pathlib + +import numpy as np +import pytest + +pytest.importorskip("yaml") +rasterio = pytest.importorskip("rasterio") + +import xarray as xr # noqa: E402 + +from xrspatial.geotiff.tests.golden_corpus._oracle import ( # noqa: E402 + compare_to_oracle, +) + +generate = importlib.import_module( + "xrspatial.geotiff.tests.golden_corpus.generate" +) + + +FIXTURES_DIR = ( + pathlib.Path(generate.__file__).resolve().parent / "fixtures" +) + +DTYPE_IDS = ( + ("dtype_int8", "int8"), + ("dtype_uint8", "uint8"), + ("dtype_int16", "int16"), + ("dtype_uint16", "uint16"), + ("dtype_int32", "int32"), + ("dtype_uint32", "uint32"), + ("dtype_float32", "float32"), + ("dtype_float64", "float64"), +) + + +def _candidate_from_rasterio(src) -> xr.DataArray: + """Build a DataArray that mirrors what a parity-correct reader would emit. + + The oracle compares attrs (transform / crs / nodata / dtype) and the + pixel array. This helper round-trips the rasterio read into the same + shape, so the oracle's accept/reject decision rests entirely on + whether it can handle the dtype -- which is what this smoke test is + pinning. + """ + pixels = src.read(1) + transform = src.transform + attrs: dict = { + "transform": ( + transform.a, transform.b, transform.c, + transform.d, transform.e, transform.f, + ), + } + crs = src.crs + if crs is not None: + epsg = crs.to_epsg() + if epsg is not None: + attrs["crs"] = epsg + else: + attrs["crs_wkt"] = crs.to_wkt() + if src.nodata is not None: + attrs["nodata"] = src.nodata + width = pixels.shape[-1] + height = pixels.shape[-2] + x = transform.c + (np.arange(width) + 0.5) * transform.a + y = transform.f + (np.arange(height) + 0.5) * transform.e + return xr.DataArray( + pixels, + dims=("y", "x"), + coords={"y": y, "x": x}, + attrs=attrs, + ) + + +@pytest.mark.parametrize("fixture_id, expected_dtype", DTYPE_IDS) +def test_dtype_fixture_exists_and_dtype_matches( + fixture_id: str, expected_dtype: str, +) -> None: + """The generator must have written the file with the manifest's dtype.""" + path = FIXTURES_DIR / f"{fixture_id}.tif" + assert path.exists(), ( + f"missing fixture {path}; rerun " + "`python -m xrspatial.geotiff.tests.golden_corpus.generate`" + ) + # 4 kB hard cap per PR 4 plan; the corpus must stay tiny in git. + assert path.stat().st_size < 4 * 1024, ( + f"{path.name} is {path.stat().st_size} bytes; " + "dtype fixtures must stay under 4 kB" + ) + with rasterio.open(path) as src: + assert src.dtypes[0] == expected_dtype, ( + f"{fixture_id}: rasterio dtype {src.dtypes[0]!r} != " + f"expected {expected_dtype!r}" + ) + + +@pytest.mark.parametrize("fixture_id, expected_dtype", DTYPE_IDS) +def test_oracle_accepts_dtype_fixture( + fixture_id: str, expected_dtype: str, +) -> None: + """The oracle accepts a rasterio-read DataArray for every dtype. + + This is the dtype-level smoke check the plan asks for: it does not + test any xrspatial backend (Phase 3) -- it confirms the oracle + understands every dtype the corpus now ships. + """ + path = FIXTURES_DIR / f"{fixture_id}.tif" + with rasterio.open(path) as src: + cand = _candidate_from_rasterio(src) + assert np.dtype(cand.dtype) == np.dtype(expected_dtype) + compare_to_oracle(path, cand) + + +@pytest.mark.parametrize( + "fixture_id, expected_dtype", + [(fid, dt) for fid, dt in DTYPE_IDS if not dt.startswith("float")], +) +def test_int_dtype_fixture_has_corner_sentinels( + fixture_id: str, expected_dtype: str, +) -> None: + """Integer fixtures plant min/max sentinels in the four corner pixels. + + Mirrors the ``noise_with_corners`` contract added to ``generate.py`` + in this PR. If somebody ever changes the corner-stamping logic this + test will tell us. + """ + info = np.iinfo(np.dtype(expected_dtype)) + path = FIXTURES_DIR / f"{fixture_id}.tif" + with rasterio.open(path) as src: + pixels = src.read(1) + assert pixels[0, 0] == info.min, fixture_id + assert pixels[0, -1] == info.max, fixture_id + assert pixels[-1, 0] == info.max, fixture_id + assert pixels[-1, -1] == info.min, fixture_id + + +def test_all_eight_dtype_fixtures_in_manifest() -> None: + """The eight ids are present in the manifest with the expected dtypes.""" + manifest = generate.load_manifest() + resolved = {e["id"]: e for e in generate.validate(manifest)} + for fid, dt in DTYPE_IDS: + assert fid in resolved, f"manifest missing {fid}" + assert resolved[fid]["dtype"] == dt, ( + f"{fid}: manifest dtype {resolved[fid]['dtype']!r} != {dt!r}" + ) From 3b47d2ad452eff760f21b45743e7d5fa80370bc7 Mon Sep 17 00:00:00 2001 From: Brendan Collins Date: Sat, 16 May 2026 06:03:55 -0700 Subject: [PATCH 2/2] geotiff: review fixes for phase 2.4 dtype variants - Update pixel_seed schema doc to mention noise_with_corners. - Reject 1x1 rasters with noise_with_corners in the validator; without the guard, all four corner stamps land on the same pixel and the pattern silently degrades to a constant. - Add a unit test for the new validator guard. --- .../geotiff/tests/golden_corpus/generate.py | 10 +++++++++ .../geotiff/tests/golden_corpus/manifest.yaml | 3 ++- .../test_golden_corpus_dtype_variants_1930.py | 22 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/xrspatial/geotiff/tests/golden_corpus/generate.py b/xrspatial/geotiff/tests/golden_corpus/generate.py index 9902f5c67..e70e9d4c4 100644 --- a/xrspatial/geotiff/tests/golden_corpus/generate.py +++ b/xrspatial/geotiff/tests/golden_corpus/generate.py @@ -164,6 +164,16 @@ def _validate_one(entry: dict[str, Any], seen_ids: set[str]) -> None: f"{fid}: pixel_pattern must be one of {sorted(ALLOWED_PATTERN)}" ) + # noise_with_corners needs at least 2x2 so the four corners are + # distinct pixels; otherwise the corner stamping silently collapses. + if entry["pixel_pattern"] == "noise_with_corners": + if entry.get("width", 0) < 2 or entry.get("height", 0) < 2: + raise ManifestError( + f"{fid}: pixel_pattern 'noise_with_corners' requires " + f"width >= 2 and height >= 2, got " + f"{entry.get('width')}x{entry.get('height')}" + ) + # dtype must be a recognised numpy dtype. try: np.dtype(entry["dtype"]) diff --git a/xrspatial/geotiff/tests/golden_corpus/manifest.yaml b/xrspatial/geotiff/tests/golden_corpus/manifest.yaml index f2a9160af..72f234077 100644 --- a/xrspatial/geotiff/tests/golden_corpus/manifest.yaml +++ b/xrspatial/geotiff/tests/golden_corpus/manifest.yaml @@ -71,7 +71,8 @@ # min/max sentinels. Integer-only # corner stamping; floats keep # noise as-is. -# pixel_seed: int Seed for "noise" patterns. Default 0. +# pixel_seed: int Seed for "noise" and "noise_with_corners" +# patterns. Default 0. # pixel_value: number Constant for "uniform" patterns. # # tags: list Optional. Free-form tags used to filter the diff --git a/xrspatial/geotiff/tests/test_golden_corpus_dtype_variants_1930.py b/xrspatial/geotiff/tests/test_golden_corpus_dtype_variants_1930.py index ee96507b8..2c530d3bf 100644 --- a/xrspatial/geotiff/tests/test_golden_corpus_dtype_variants_1930.py +++ b/xrspatial/geotiff/tests/test_golden_corpus_dtype_variants_1930.py @@ -156,6 +156,28 @@ def test_int_dtype_fixture_has_corner_sentinels( assert pixels[-1, -1] == info.min, fixture_id +def test_noise_with_corners_rejects_tiny_rasters() -> None: + """``noise_with_corners`` needs >=2x2 so the four corners are distinct. + + The validator should refuse a 1x1 fixture with that pattern instead of + silently collapsing all four corner stamps into the same pixel. + """ + manifest = generate.load_manifest() + defaults = manifest.get("defaults") or {} + entry = dict(defaults) + entry.update({ + "id": "tiny_corner_bad", + "description": "1x1 noise_with_corners must be rejected.", + "width": 1, + "height": 1, + "dtype": "uint8", + "pixel_pattern": "noise_with_corners", + }) + bad = {"version": 1, "defaults": {}, "fixtures": [entry]} + with pytest.raises(generate.ManifestError, match="noise_with_corners"): + generate.validate(bad) + + def test_all_eight_dtype_fixtures_in_manifest() -> None: """The eight ids are present in the manifest with the expected dtypes.""" manifest = generate.load_manifest()