Skip to content

Commit a554067

Browse files
committed
Merge branch 'allowablepixels'
2 parents 05a132c + 47b986d commit a554067

File tree

3 files changed

+66
-6
lines changed

3 files changed

+66
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- `openeo.testing.io.TestDataLoader`: unit test utility to compactly load (and optionally preprocess) tests data (text/JSON/...)
1313
- `openeo.Connection`: automatically retry API requests on `429 Too Many Requests` HTTP errors, with appropriate delay if possible ([#441](https://github.com/Open-EO/openeo-python-client/issues/441))
14+
- Introduced `pixel_tolerance` argument in `openeo.testing.results` helpers to specify the ignorable fraction of significantly differing pixels. ([#776](https://github.com/Open-EO/openeo-python-client/issues/776))
1415

1516
### Changed
1617

openeo/testing/results.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@
2020

2121
_DEFAULT_RTOL = 1e-6
2222
_DEFAULT_ATOL = 1e-6
23+
_DEFAULT_PIXELTOL = 0.0
2324

2425
# https://paulbourke.net/dataformats/asciiart
2526
DEFAULT_GRAYSCALE_70_CHARACTERS = r"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "[::-1]
2627
DEFAULT_GRAYSCALE_10_CHARACTERS = " .:-=+*#%@"
2728

29+
2830
def _load_xarray_netcdf(path: Union[str, Path], **kwargs) -> xarray.Dataset:
2931
"""
3032
Load a netCDF file as Xarray Dataset
@@ -194,6 +196,7 @@ def _compare_xarray_dataarray(
194196
*,
195197
rtol: float = _DEFAULT_RTOL,
196198
atol: float = _DEFAULT_ATOL,
199+
pixel_tolerance: float = _DEFAULT_PIXELTOL,
197200
name: str = None,
198201
) -> List[str]:
199202
"""
@@ -230,9 +233,19 @@ def _compare_xarray_dataarray(
230233
issues.append(f"Shape mismatch: {actual.shape} != {expected.shape}")
231234
compatible = len(issues) == 0
232235
try:
233-
xarray.testing.assert_allclose(a=actual, b=expected, rtol=rtol, atol=atol)
236+
if pixel_tolerance and compatible:
237+
threshold = abs(expected * rtol) + atol
238+
bad_pixels = abs(actual * 1.0 - expected * 1.0) > threshold
239+
percentage_bad_pixels = bad_pixels.mean().item() * 100
240+
assert (
241+
percentage_bad_pixels <= pixel_tolerance
242+
), f"Fraction significantly differing pixels: {percentage_bad_pixels}% > {pixel_tolerance}%"
243+
xarray.testing.assert_allclose(
244+
a=actual.where(~bad_pixels), b=expected.where(~bad_pixels), rtol=rtol, atol=atol
245+
)
246+
else:
247+
xarray.testing.assert_allclose(a=actual, b=expected, rtol=rtol, atol=atol)
234248
except AssertionError as e:
235-
# TODO: message of `assert_allclose` is typically multiline, split it again or make it one line?
236249
issues.append(str(e).strip())
237250
if compatible and {"x", "y"} <= set(expected.dims):
238251
issues.extend(
@@ -266,12 +279,14 @@ def assert_xarray_dataarray_allclose(
266279
if issues:
267280
raise AssertionError("\n".join(issues))
268281

282+
269283
def _compare_xarray_datasets(
270284
actual: Union[xarray.Dataset, str, Path],
271285
expected: Union[xarray.Dataset, str, Path],
272286
*,
273287
rtol: float = _DEFAULT_RTOL,
274288
atol: float = _DEFAULT_ATOL,
289+
pixel_tolerance: float = _DEFAULT_PIXELTOL,
275290
) -> List[str]:
276291
"""
277292
Compare two xarray ``DataSet``s with tolerance and report mismatch issues (as strings)
@@ -290,7 +305,9 @@ def _compare_xarray_datasets(
290305
all_issues.append(f"Xarray DataSet variables mismatch: {actual_vars} != {expected_vars}")
291306
for var in expected_vars.intersection(actual_vars):
292307
_log.debug(f"_compare_xarray_datasets: comparing variable {var!r}")
293-
issues = _compare_xarray_dataarray(actual[var], expected[var], rtol=rtol, atol=atol, name=var)
308+
issues = _compare_xarray_dataarray(
309+
actual[var], expected[var], rtol=rtol, atol=atol, pixel_tolerance=pixel_tolerance, name=var
310+
)
294311
if issues:
295312
all_issues.append(f"Issues for variable {var!r}:")
296313
all_issues.extend(issues)
@@ -387,6 +404,7 @@ def _compare_job_results(
387404
*,
388405
rtol: float = _DEFAULT_RTOL,
389406
atol: float = _DEFAULT_ATOL,
407+
pixel_tolerance: float = _DEFAULT_PIXELTOL,
390408
tmp_path: Optional[Path] = None,
391409
) -> List[str]:
392410
"""
@@ -415,12 +433,16 @@ def _compare_job_results(
415433
all_issues.append(f"Issues for metadata file {filename!r}:")
416434
all_issues.extend(issues)
417435
elif expected_path.suffix.lower() in {".nc", ".netcdf"}:
418-
issues = _compare_xarray_datasets(actual=actual_path, expected=expected_path, rtol=rtol, atol=atol)
436+
issues = _compare_xarray_datasets(
437+
actual=actual_path, expected=expected_path, rtol=rtol, atol=atol, pixel_tolerance=pixel_tolerance
438+
)
419439
if issues:
420440
all_issues.append(f"Issues for file {filename!r}:")
421441
all_issues.extend(issues)
422442
elif expected_path.suffix.lower() in {".tif", ".tiff", ".gtiff", ".geotiff"}:
423-
issues = _compare_xarray_dataarray(actual=actual_path, expected=expected_path, rtol=rtol, atol=atol)
443+
issues = _compare_xarray_dataarray(
444+
actual=actual_path, expected=expected_path, rtol=rtol, atol=atol, pixel_tolerance=pixel_tolerance
445+
)
424446
if issues:
425447
all_issues.append(f"Issues for file {filename!r}:")
426448
all_issues.extend(issues)
@@ -463,6 +485,7 @@ def assert_job_results_allclose(
463485
*,
464486
rtol: float = _DEFAULT_RTOL,
465487
atol: float = _DEFAULT_ATOL,
488+
pixel_tolerance: float = _DEFAULT_PIXELTOL,
466489
tmp_path: Optional[Path] = None,
467490
):
468491
"""
@@ -474,6 +497,8 @@ def assert_job_results_allclose(
474497
:py:meth:`~openeo.rest.job.JobResults` object or path to directory with downloaded assets.
475498
:param rtol: relative tolerance
476499
:param atol: absolute tolerance
500+
:param pixel_tolerance: maximum fraction of pixels (in percent)
501+
that is allowed to be significantly different (considering ``atol`` and ``rtol``)
477502
:param tmp_path: root temp path to download results if needed.
478503
It's recommended to pass pytest's `tmp_path` fixture here
479504
:raises AssertionError: if not equal within the given tolerance
@@ -483,6 +508,8 @@ def assert_job_results_allclose(
483508
.. warning::
484509
This function is experimental and subject to change.
485510
"""
486-
issues = _compare_job_results(actual, expected, rtol=rtol, atol=atol, tmp_path=tmp_path)
511+
issues = _compare_job_results(
512+
actual, expected, rtol=rtol, atol=atol, pixel_tolerance=pixel_tolerance, tmp_path=tmp_path
513+
)
487514
if issues:
488515
raise AssertionError("\n".join(issues))

tests/testing/test_results.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,38 @@ def test_simple_atol(self, actual, atol, expected_issues):
151151
expected = xarray.DataArray([1, 2, 3])
152152
assert _compare_xarray_dataarray(actual=actual, expected=expected, rtol=0, atol=atol) == expected_issues
153153

154+
@pytest.mark.parametrize(
155+
["actual", "atol", "pixel_tolerance", "expected_issues"],
156+
[
157+
(xarray.DataArray([1, 2, 300, 4, 5]), 1e-4, 21.0, []),
158+
(xarray.DataArray([1, 2, 3.00001, 4.1, 5]), 1e-4, 21, []),
159+
(
160+
xarray.DataArray([1, 2, 3.001, 4.1, 5]),
161+
1e-4,
162+
21,
163+
[
164+
dirty_equals.IsStr(
165+
regex=r"Fraction significantly differing pixels: 40.0% > 21%",
166+
)
167+
],
168+
),
169+
(
170+
xarray.DataArray([1, 2, 3.001, 4, 5]),
171+
1e-4,
172+
0,
173+
[dirty_equals.IsStr(regex=r"Left and right DataArray objects are not close.*", regex_flags=re.DOTALL)],
174+
),
175+
],
176+
)
177+
def test_simple_pixel_tolerance(self, actual, atol, pixel_tolerance, expected_issues):
178+
expected = xarray.DataArray([1, 2, 3, 4, 5])
179+
assert (
180+
_compare_xarray_dataarray(
181+
actual=actual, expected=expected, atol=atol, rtol=0, pixel_tolerance=pixel_tolerance
182+
)
183+
== expected_issues
184+
)
185+
154186

155187
def test_nan_handling(self):
156188
expected = xarray.DataArray([1, 2, numpy.nan, 4, float("nan")])

0 commit comments

Comments
 (0)