File size: 12,585 Bytes
2903bd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
"""
Tests for curve visualization module.
"""

import numpy as np
import pytest

from ptpd_calibration.core.models import CurveData
from ptpd_calibration.core.types import CurveType
from ptpd_calibration.curves.visualization import (
    CurveVisualizer,
    CurveStatistics,
    CurveComparisonResult,
    VisualizationConfig,
    PlotStyle,
    ColorScheme,
)


class TestVisualizationConfig:
    """Tests for VisualizationConfig."""

    def test_default_config(self):
        """Test default configuration values."""
        config = VisualizationConfig()

        assert config.figure_width == 10.0
        assert config.figure_height == 6.0
        assert config.dpi == 100
        assert config.line_width == 2.0
        assert config.show_grid is True
        assert config.show_legend is True
        assert config.color_scheme == ColorScheme.PLATINUM

    def test_custom_config(self):
        """Test custom configuration values."""
        config = VisualizationConfig(
            figure_width=12.0,
            figure_height=8.0,
            dpi=150,
            color_scheme=ColorScheme.ACCESSIBLE,
            show_grid=False,
        )

        assert config.figure_width == 12.0
        assert config.figure_height == 8.0
        assert config.dpi == 150
        assert config.color_scheme == ColorScheme.ACCESSIBLE
        assert config.show_grid is False

    def test_get_color_palette_platinum(self):
        """Test platinum color palette generation."""
        config = VisualizationConfig(color_scheme=ColorScheme.PLATINUM)
        colors = config.get_color_palette(5)

        assert len(colors) == 5
        assert all(isinstance(c, str) for c in colors)
        assert all(c.startswith("#") for c in colors)

    def test_get_color_palette_accessible(self):
        """Test accessible color palette generation."""
        config = VisualizationConfig(color_scheme=ColorScheme.ACCESSIBLE)
        colors = config.get_color_palette(8)

        assert len(colors) == 8
        # Accessible palette should have high contrast colors
        assert "#0072B2" in colors  # Blue

    def test_get_color_palette_custom(self):
        """Test custom color palette."""
        custom_colors = ["#FF0000", "#00FF00", "#0000FF"]
        config = VisualizationConfig(custom_colors=custom_colors)
        colors = config.get_color_palette(3)

        assert colors == custom_colors

    def test_get_color_palette_wrapping(self):
        """Test color palette wrapping for large requests."""
        config = VisualizationConfig(color_scheme=ColorScheme.MONOCHROME)
        colors = config.get_color_palette(20)

        assert len(colors) == 20


class TestCurveStatistics:
    """Tests for CurveStatistics."""

    def test_to_dict(self):
        """Test statistics dictionary conversion."""
        stats = CurveStatistics(
            name="Test Curve",
            num_points=256,
            input_min=0.0,
            input_max=1.0,
            output_min=0.0,
            output_max=1.0,
            gamma=1.0,
            midpoint_value=0.5,
            is_monotonic=True,
            max_slope=1.2,
            min_slope=0.8,
            average_slope=1.0,
            linearity_error=0.01,
        )

        d = stats.to_dict()

        assert d["name"] == "Test Curve"
        assert d["num_points"] == 256
        assert d["gamma"] == 1.0
        assert d["is_monotonic"] is True
        assert "input_range" in d
        assert "output_range" in d


class TestCurveComparisonResult:
    """Tests for CurveComparisonResult."""

    def test_to_dict(self):
        """Test comparison result dictionary conversion."""
        result = CurveComparisonResult(
            curve_names=["Curve A", "Curve B"],
            max_difference=0.05,
            average_difference=0.02,
            rms_difference=0.03,
            correlation=0.98,
        )

        d = result.to_dict()

        assert d["curves_compared"] == ["Curve A", "Curve B"]
        assert d["max_difference"] == 0.05
        assert d["correlation"] == 0.98


class TestCurveVisualizer:
    """Tests for CurveVisualizer."""

    @pytest.fixture
    def sample_curve(self):
        """Create a sample curve for testing."""
        inputs = list(np.linspace(0, 1, 256))
        outputs = list(np.linspace(0, 1, 256) ** 0.9)
        return CurveData(
            name="Test Curve",
            input_values=inputs,
            output_values=outputs,
            curve_type=CurveType.LINEAR,
        )

    @pytest.fixture
    def linear_curve(self):
        """Create a perfectly linear curve."""
        inputs = list(np.linspace(0, 1, 100))
        return CurveData(
            name="Linear Curve",
            input_values=inputs,
            output_values=inputs.copy(),
        )

    @pytest.fixture
    def gamma_curve(self):
        """Create a gamma curve."""
        inputs = list(np.linspace(0, 1, 100))
        outputs = list(np.array(inputs) ** 2.2)
        return CurveData(
            name="Gamma 2.2 Curve",
            input_values=inputs,
            output_values=outputs,
        )

    @pytest.fixture
    def visualizer(self):
        """Create a visualizer instance."""
        return CurveVisualizer()

    def test_init_default_config(self):
        """Test visualizer initialization with default config."""
        viz = CurveVisualizer()
        assert viz.config is not None
        assert isinstance(viz.config, VisualizationConfig)

    def test_init_custom_config(self):
        """Test visualizer initialization with custom config."""
        config = VisualizationConfig(dpi=200, show_grid=False)
        viz = CurveVisualizer(config)

        assert viz.config.dpi == 200
        assert viz.config.show_grid is False

    def test_compute_statistics(self, visualizer, sample_curve):
        """Test statistics computation."""
        stats = visualizer.compute_statistics(sample_curve)

        assert stats.name == "Test Curve"
        assert stats.num_points == 256
        assert stats.input_min == pytest.approx(0.0)
        assert stats.input_max == pytest.approx(1.0)
        assert stats.output_min == pytest.approx(0.0, abs=0.01)
        assert stats.output_max == pytest.approx(1.0, abs=0.01)
        assert stats.is_monotonic is True

    def test_compute_statistics_linear(self, visualizer, linear_curve):
        """Test statistics for linear curve."""
        stats = visualizer.compute_statistics(linear_curve)

        assert stats.gamma == pytest.approx(1.0, abs=0.1)
        assert stats.midpoint_value == pytest.approx(0.5, abs=0.1)
        assert stats.linearity_error < 0.01
        assert stats.average_slope == pytest.approx(1.0, abs=0.1)

    def test_compute_statistics_gamma(self, visualizer, gamma_curve):
        """Test statistics for gamma curve."""
        stats = visualizer.compute_statistics(gamma_curve)

        # Gamma 2.2 curve should have gamma close to 2.2
        assert stats.gamma > 1.5
        assert stats.midpoint_value < 0.5  # Darker midtones
        assert stats.is_monotonic is True

    def test_compare_curves_same(self, visualizer, linear_curve):
        """Test comparison of identical curves."""
        result = visualizer.compare_curves([linear_curve, linear_curve])

        assert result.max_difference == pytest.approx(0.0)
        assert result.average_difference == pytest.approx(0.0)
        assert result.correlation == pytest.approx(1.0)

    def test_compare_curves_different(self, visualizer, linear_curve, gamma_curve):
        """Test comparison of different curves."""
        result = visualizer.compare_curves([linear_curve, gamma_curve])

        assert result.max_difference > 0
        assert result.average_difference > 0
        assert result.correlation < 1.0
        assert result.difference_curve is not None

    def test_compare_curves_insufficient(self, visualizer, linear_curve):
        """Test comparison with insufficient curves."""
        with pytest.raises(ValueError, match="At least 2 curves"):
            visualizer.compare_curves([linear_curve])

    def test_plot_single_curve(self, visualizer, sample_curve):
        """Test single curve plotting."""
        fig = visualizer.plot_single_curve(sample_curve)

        assert fig is not None
        # Check that figure was created
        assert hasattr(fig, "axes")

    def test_plot_single_curve_with_options(self, visualizer, sample_curve):
        """Test single curve plotting with options."""
        fig = visualizer.plot_single_curve(
            sample_curve,
            title="Custom Title",
            style=PlotStyle.LINE_MARKERS,
            color="#FF0000",
            show_stats=True,
        )

        assert fig is not None

    def test_plot_multiple_curves(self, visualizer, linear_curve, gamma_curve):
        """Test multiple curve plotting."""
        fig = visualizer.plot_multiple_curves(
            [linear_curve, gamma_curve],
            title="Comparison",
        )

        assert fig is not None

    def test_plot_multiple_curves_with_difference(self, visualizer, linear_curve, gamma_curve):
        """Test multiple curve plotting with difference."""
        fig = visualizer.plot_multiple_curves(
            [linear_curve, gamma_curve],
            show_difference=True,
        )

        assert fig is not None

    def test_plot_multiple_curves_empty(self, visualizer):
        """Test plotting with no curves."""
        with pytest.raises(ValueError, match="No curves provided"):
            visualizer.plot_multiple_curves([])

    def test_plot_with_statistics(self, visualizer, linear_curve, gamma_curve):
        """Test plotting with statistics panel."""
        fig = visualizer.plot_with_statistics(
            [linear_curve, gamma_curve],
            title="Analysis",
        )

        assert fig is not None

    def test_plot_histogram(self, visualizer, sample_curve):
        """Test histogram plotting."""
        fig = visualizer.plot_histogram(sample_curve, bins=50)

        assert fig is not None

    def test_plot_slope_analysis(self, visualizer, sample_curve):
        """Test slope analysis plotting."""
        fig = visualizer.plot_slope_analysis(sample_curve)

        assert fig is not None

    def test_figure_to_bytes(self, visualizer, sample_curve):
        """Test figure to bytes conversion."""
        fig = visualizer.plot_single_curve(sample_curve)
        bytes_data = visualizer.figure_to_bytes(fig, format="png")

        assert isinstance(bytes_data, bytes)
        assert len(bytes_data) > 0
        # PNG magic bytes
        assert bytes_data[:8] == b'\x89PNG\r\n\x1a\n'

    def test_save_figure(self, visualizer, sample_curve, tmp_path):
        """Test figure saving."""
        fig = visualizer.plot_single_curve(sample_curve)
        output_path = tmp_path / "test_curve.png"

        result_path = visualizer.save_figure(fig, output_path)

        assert result_path == output_path
        assert output_path.exists()
        assert output_path.stat().st_size > 0


class TestPlotStyles:
    """Tests for different plot styles."""

    @pytest.fixture
    def sample_curve(self):
        """Create sample curve."""
        inputs = list(np.linspace(0, 1, 50))
        outputs = list(np.array(inputs) ** 0.9)
        return CurveData(
            name="Test",
            input_values=inputs,
            output_values=outputs,
        )

    @pytest.fixture
    def visualizer(self):
        """Create visualizer."""
        return CurveVisualizer()

    @pytest.mark.parametrize("style", [
        PlotStyle.LINE,
        PlotStyle.LINE_MARKERS,
        PlotStyle.SCATTER,
        PlotStyle.AREA,
        PlotStyle.STEP,
    ])
    def test_all_plot_styles(self, visualizer, sample_curve, style):
        """Test all plot styles work without error."""
        fig = visualizer.plot_single_curve(sample_curve, style=style)
        assert fig is not None


class TestColorSchemes:
    """Tests for color schemes."""

    @pytest.mark.parametrize("scheme", [
        ColorScheme.PLATINUM,
        ColorScheme.MONOCHROME,
        ColorScheme.VIBRANT,
        ColorScheme.PASTEL,
        ColorScheme.ACCESSIBLE,
    ])
    def test_all_color_schemes(self, scheme):
        """Test all color schemes provide valid colors."""
        config = VisualizationConfig(color_scheme=scheme)
        colors = config.get_color_palette(5)

        assert len(colors) == 5
        for color in colors:
            assert isinstance(color, str)
            assert color.startswith("#")
            assert len(color) == 7  # #RRGGBB format