File size: 6,793 Bytes
79ae023
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
"""Backend abstraction: parity, quantization monotonicity, XFCVDEPTH,
LUT approximations, latency model, registry."""

import numpy as np
import pytest

from sensorflow.vitis.backend import (
    BACKENDS, DeviceConfig, PipelineConfig, backend_status, get_backend,
    make_bayer_mosaic,
)


@pytest.fixture(scope="module")
def img():
    rng = np.random.default_rng(3)
    base = rng.random((96, 128, 3)).astype(np.float32)
    return base


@pytest.fixture(scope="module")
def gray(img):
    return img.mean(axis=2).astype(np.float32)


def _cfg(w, i=6, **kw):
    return PipelineConfig(precision={"default": (w, i)}, **kw)


class TestParityAndMonotonicity:
    def test_high_bitwidth_parity_within_quantization_bound(self, img):
        ref = get_backend("reference")
        vit = get_backend("vitis_emulated", _cfg(16))
        r = ref.gaussian_filter(img, 1.5)
        v = vit.gaussian_filter(img, 1.5)
        # ap_fixed<16,6> has 10 fractional bits -> lsb ~ 1e-3; allow a few lsb
        # of accumulated error through the filter.
        assert float(np.abs(r - v).mean()) < 4 * 2.0 ** -10

    def test_divergence_grows_monotonically_as_bits_shrink(self, img):
        ref = get_backend("reference")
        target = ref.gaussian_filter(img, 1.5)
        errs = []
        for w in (16, 12, 10, 8):
            vit = get_backend("vitis_emulated", _cfg(w))
            errs.append(float(np.abs(vit.gaussian_filter(img, 1.5) - target).mean()))
        assert errs == sorted(errs), f"not monotone: {errs}"
        assert errs[-1] > 10 * errs[0]

    def test_ops_run_on_both_backends(self, img, gray):
        raw = make_bayer_mosaic(img)
        for name in ("reference", "vitis_emulated"):
            be = get_backend(name, _cfg(16))
            assert be.demosaic(raw).shape == img.shape
            assert be.resize(img, 48, 64).shape == (48, 64, 3)
            assert be.resize(gray, 48, 64, "area").shape == (48, 64)
            assert be.crop(img, 10, 10, 20, 30).shape == (20, 30, 3)
            yuv = be.rgb_to_yuv(img)
            back = be.yuv_to_rgb(yuv)
            assert float(np.abs(back - img).mean()) < 0.05
            assert be.bad_pixel_correction(gray).shape == gray.shape
            assert be.hdr_tone_map(img).max() <= 1.0
            assert be.gain_exposure(img, 2.0).max() <= 1.0
            assert be.lens_distortion(gray, k1=0.2).shape == gray.shape
            assert be.median_filter(gray, 3).shape == gray.shape
            merged = be.hdr_merge([img * 0.3, img], [0.5, 1.0])
            assert merged.shape == img.shape


class TestStreamingDepthConstraint:
    def test_no_artifacts_when_depth_sufficient(self, gray):
        wide = get_backend("vitis_emulated", _cfg(16, max_line_buffer_depth=4096))
        exact = get_backend("vitis_emulated",
                            _cfg(16, max_line_buffer_depth=gray.shape[1]))
        a = wide.gaussian_filter(gray, 2.0)
        b = exact.gaussian_filter(gray, 2.0)
        assert np.array_equal(a, b)

    def test_localized_boundary_artifacts_only_when_exceeded(self, gray):
        depth = 48
        full = get_backend("vitis_emulated", _cfg(16, max_line_buffer_depth=4096))
        narrow = get_backend("vitis_emulated",
                             _cfg(16, max_line_buffer_depth=depth))
        a = full.gaussian_filter(gray, 2.0)
        b = narrow.gaussian_filter(gray, 2.0)
        col_err = np.abs(a - b).mean(axis=0)
        radius = 8  # scipy gaussian kernel truncates at 4*sigma = 8 px
        seam_cols = [c for s in range(depth, gray.shape[1], depth)
                     for c in range(max(0, s - radius),
                                    min(gray.shape[1], s + radius))]
        interior = [c for c in range(gray.shape[1]) if c not in set(seam_cols)]
        assert col_err[seam_cols].max() > 1e-3, "expected seam artifacts"
        assert col_err[interior].max() < 1e-6, "artifacts leaked beyond seams"


class TestHlsApproximations:
    def test_lut_recip_bounded_relative_error(self):
        cfg = _cfg(24, i=8, lut_bits=8)
        from sensorflow.vitis.backend import _FixedNumerics
        num = _FixedNumerics(cfg)
        x = np.linspace(0.01, 50.0, 5000).astype(np.float32)
        rel = np.abs(num.recip(x) - 1.0 / x) * x
        assert float(rel.max()) < 2.0 ** -7  # ~2x lsb of a 256-entry LUT

    def test_lut_sqrt_bounded_relative_error(self):
        cfg = _cfg(24, i=8, lut_bits=8)
        from sensorflow.vitis.backend import _FixedNumerics
        num = _FixedNumerics(cfg)
        x = np.linspace(0.01, 50.0, 5000).astype(np.float32)
        rel = np.abs(num.sqrt(x) - np.sqrt(x)) / np.sqrt(x)
        assert float(rel.max()) < 2.0 ** -8

    def test_lut_toggle_changes_output(self, img):
        on = get_backend("vitis_emulated", _cfg(20, i=6, use_lut_approx=True))
        off = get_backend("vitis_emulated", _cfg(20, i=6, use_lut_approx=False))
        a = on.hdr_tone_map(img)
        b = off.hdr_tone_map(img)
        assert not np.array_equal(a, b)


class TestLatencyModelAndRegistry:
    def test_modeled_latency_deterministic_and_labeled(self, gray):
        be = get_backend("vitis_emulated", _cfg(12))
        be.gaussian_filter(gray, 1.0)
        rep = be.profile_report()[-1]
        assert rep["modeled"]["modeled_not_measured"] is True
        assert rep["modeled"]["latency_ms"] > 0
        be2 = get_backend("vitis_emulated", _cfg(12))
        be2.gaussian_filter(gray, 1.0)
        assert be2.profile_report()[-1]["modeled"]["latency_ms"] == \
            rep["modeled"]["latency_ms"]

    def test_aie_placement_depends_on_device(self, gray):
        versal = get_backend("vitis_emulated", PipelineConfig(
            device=DeviceConfig(name="versal-ai-edge")))
        zynq = get_backend("vitis_emulated", PipelineConfig(
            device=DeviceConfig(name="zynq-ultrascale")))
        versal.gaussian_filter(gray, 1.0)
        zynq.gaussian_filter(gray, 1.0)
        assert versal.profile_report()[-1]["modeled"]["placement"] == "AIE"
        assert zynq.profile_report()[-1]["modeled"]["placement"] == "PL"

    def test_reference_backend_reports_no_modeled_numbers(self, gray):
        ref = get_backend("reference")
        ref.gaussian_filter(gray, 1.0)
        assert "modeled" not in ref.profile_report()[-1]

    def test_registry_and_hw_stub(self):
        assert set(BACKENDS) == {"reference", "vitis_emulated", "vitis_hw"}
        with pytest.raises(NotImplementedError):
            get_backend("vitis_hw")
        with pytest.raises(ValueError):
            get_backend("nonsense")
        status = backend_status()
        assert status["hardware_present"] is False
        emu = next(b for b in status["backends"] if b["name"] == "vitis_emulated")
        assert emu["emulated"] is True and "modeled" in emu["description"]