xref: /aosp_15_r20/external/fonttools/Tests/varLib/models_test.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.varLib.models import (
2    normalizeLocation,
3    supportScalar,
4    VariationModel,
5    VariationModelError,
6)
7import pytest
8
9
10def test_normalizeLocation():
11    axes = {"wght": (100, 400, 900)}
12    assert normalizeLocation({"wght": 400}, axes) == {"wght": 0.0}
13    assert normalizeLocation({"wght": 100}, axes) == {"wght": -1.0}
14    assert normalizeLocation({"wght": 900}, axes) == {"wght": 1.0}
15    assert normalizeLocation({"wght": 650}, axes) == {"wght": 0.5}
16    assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0}
17    assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0}
18
19    axes = {"wght": (0, 0, 1000)}
20    assert normalizeLocation({"wght": 0}, axes) == {"wght": 0.0}
21    assert normalizeLocation({"wght": -1}, axes) == {"wght": 0.0}
22    assert normalizeLocation({"wght": 1000}, axes) == {"wght": 1.0}
23    assert normalizeLocation({"wght": 500}, axes) == {"wght": 0.5}
24    assert normalizeLocation({"wght": 1001}, axes) == {"wght": 1.0}
25
26    axes = {"wght": (0, 1000, 1000)}
27    assert normalizeLocation({"wght": 0}, axes) == {"wght": -1.0}
28    assert normalizeLocation({"wght": -1}, axes) == {"wght": -1.0}
29    assert normalizeLocation({"wght": 500}, axes) == {"wght": -0.5}
30    assert normalizeLocation({"wght": 1000}, axes) == {"wght": 0.0}
31    assert normalizeLocation({"wght": 1001}, axes) == {"wght": 0.0}
32
33
34@pytest.mark.parametrize(
35    "axes, location, expected",
36    [
37        # lower != default != upper
38        ({"wght": (100, 400, 900)}, {"wght": 1000}, {"wght": 1.2}),
39        ({"wght": (100, 400, 900)}, {"wght": 900}, {"wght": 1.0}),
40        ({"wght": (100, 400, 900)}, {"wght": 650}, {"wght": 0.5}),
41        ({"wght": (100, 400, 900)}, {"wght": 400}, {"wght": 0.0}),
42        ({"wght": (100, 400, 900)}, {"wght": 250}, {"wght": -0.5}),
43        ({"wght": (100, 400, 900)}, {"wght": 100}, {"wght": -1.0}),
44        ({"wght": (100, 400, 900)}, {"wght": 25}, {"wght": -1.25}),
45        # lower == default != upper
46        (
47            {"wght": (400, 400, 900), "wdth": (100, 100, 150)},
48            {"wght": 1000, "wdth": 200},
49            {"wght": 1.2, "wdth": 2.0},
50        ),
51        (
52            {"wght": (400, 400, 900), "wdth": (100, 100, 150)},
53            {"wght": 25, "wdth": 25},
54            {"wght": -0.75, "wdth": -1.5},
55        ),
56        # lower != default == upper
57        (
58            {"wght": (100, 400, 400), "wdth": (50, 100, 100)},
59            {"wght": 700, "wdth": 150},
60            {"wght": 1.0, "wdth": 1.0},
61        ),
62        (
63            {"wght": (100, 400, 400), "wdth": (50, 100, 100)},
64            {"wght": -50, "wdth": 25},
65            {"wght": -1.5, "wdth": -1.5},
66        ),
67        # degenerate case with lower == default == upper, normalized location always 0
68        ({"wght": (400, 400, 400)}, {"wght": 100}, {"wght": 0.0}),
69        ({"wght": (400, 400, 400)}, {"wght": 400}, {"wght": 0.0}),
70        ({"wght": (400, 400, 400)}, {"wght": 700}, {"wght": 0.0}),
71    ],
72)
73def test_normalizeLocation_extrapolate(axes, location, expected):
74    assert normalizeLocation(location, axes, extrapolate=True) == expected
75
76
77def test_supportScalar():
78    assert supportScalar({}, {}) == 1.0
79    assert supportScalar({"wght": 0.2}, {}) == 1.0
80    assert supportScalar({"wght": 0.2}, {"wght": (0, 2, 3)}) == 0.1
81    assert supportScalar({"wght": 2.5}, {"wght": (0, 2, 4)}) == 0.75
82    assert supportScalar({"wght": 3}, {"wght": (0, 2, 2)}) == 0.0
83    assert (
84        supportScalar(
85            {"wght": 3},
86            {"wght": (0, 2, 2)},
87            extrapolate=True,
88            axisRanges={"wght": (0, 2)},
89        )
90        == 1.5
91    )
92    assert (
93        supportScalar(
94            {"wght": -1},
95            {"wght": (0, 2, 2)},
96            extrapolate=True,
97            axisRanges={"wght": (0, 2)},
98        )
99        == -0.5
100    )
101    assert (
102        supportScalar(
103            {"wght": 3},
104            {"wght": (0, 1, 2)},
105            extrapolate=True,
106            axisRanges={"wght": (0, 2)},
107        )
108        == -1.0
109    )
110    assert (
111        supportScalar(
112            {"wght": -1},
113            {"wght": (0, 1, 2)},
114            extrapolate=True,
115            axisRanges={"wght": (0, 2)},
116        )
117        == -1.0
118    )
119    assert (
120        supportScalar(
121            {"wght": 2},
122            {"wght": (0, 0.75, 1)},
123            extrapolate=True,
124            axisRanges={"wght": (0, 1)},
125        )
126        == -4.0
127    )
128    with pytest.raises(TypeError):
129        supportScalar(
130            {"wght": 2}, {"wght": (0, 0.75, 1)}, extrapolate=True, axisRanges=None
131        )
132
133
134def test_model_extrapolate():
135    locations = [{}, {"a": 1}, {"b": 1}, {"a": 1, "b": 1}]
136    model = VariationModel(locations, extrapolate=True)
137    masterValues = [100, 200, 300, 400]
138    testLocsAndValues = [
139        ({"a": -1, "b": -1}, -200),
140        ({"a": -1, "b": 0}, 0),
141        ({"a": -1, "b": 1}, 200),
142        ({"a": -1, "b": 2}, 400),
143        ({"a": 0, "b": -1}, -100),
144        ({"a": 0, "b": 0}, 100),
145        ({"a": 0, "b": 1}, 300),
146        ({"a": 0, "b": 2}, 500),
147        ({"a": 1, "b": -1}, 0),
148        ({"a": 1, "b": 0}, 200),
149        ({"a": 1, "b": 1}, 400),
150        ({"a": 1, "b": 2}, 600),
151        ({"a": 2, "b": -1}, 100),
152        ({"a": 2, "b": 0}, 300),
153        ({"a": 2, "b": 1}, 500),
154        ({"a": 2, "b": 2}, 700),
155    ]
156    for loc, expectedValue in testLocsAndValues:
157        assert expectedValue == model.interpolateFromMasters(loc, masterValues)
158
159
160@pytest.mark.parametrize(
161    "numLocations, numSamples",
162    [
163        pytest.param(127, 509, marks=pytest.mark.slow),
164        (31, 251),
165    ],
166)
167def test_modeling_error(numLocations, numSamples):
168    # https://github.com/fonttools/fonttools/issues/2213
169    locations = [{"axis": float(i) / numLocations} for i in range(numLocations)]
170    masterValues = [100.0 if i else 0.0 for i in range(numLocations)]
171
172    model = VariationModel(locations)
173
174    for i in range(numSamples):
175        loc = {"axis": float(i) / numSamples}
176        scalars = model.getScalars(loc)
177
178        deltas_float = model.getDeltas(masterValues)
179        deltas_round = model.getDeltas(masterValues, round=round)
180
181        expected = model.interpolateFromDeltasAndScalars(deltas_float, scalars)
182        actual = model.interpolateFromDeltasAndScalars(deltas_round, scalars)
183
184        err = abs(actual - expected)
185        assert err <= 0.5, (i, err)
186
187        # This is how NOT to round deltas.
188        # deltas_late_round = [round(d) for d in deltas_float]
189        # bad = model.interpolateFromDeltasAndScalars(deltas_late_round, scalars)
190        # err_bad = abs(bad - expected)
191        # if err != err_bad:
192        #    print("{:d}	{:.2}	{:.2}".format(i, err, err_bad))
193
194
195locationsA = [{}, {"wght": 1}, {"wdth": 1}]
196locationsB = [{}, {"wght": 1}, {"wdth": 1}, {"wght": 1, "wdth": 1}]
197locationsC = [
198    {},
199    {"wght": 0.5},
200    {"wght": 1},
201    {"wdth": 1},
202    {"wght": 1, "wdth": 1},
203]
204
205
206class VariationModelTest(object):
207    @pytest.mark.parametrize(
208        "locations, axisOrder, sortedLocs, supports, deltaWeights",
209        [
210            (
211                [
212                    {"wght": 0.55, "wdth": 0.0},
213                    {"wght": -0.55, "wdth": 0.0},
214                    {"wght": -1.0, "wdth": 0.0},
215                    {"wght": 0.0, "wdth": 1.0},
216                    {"wght": 0.66, "wdth": 1.0},
217                    {"wght": 0.66, "wdth": 0.66},
218                    {"wght": 0.0, "wdth": 0.0},
219                    {"wght": 1.0, "wdth": 1.0},
220                    {"wght": 1.0, "wdth": 0.0},
221                ],
222                ["wght"],
223                [
224                    {},
225                    {"wght": -0.55},
226                    {"wght": -1.0},
227                    {"wght": 0.55},
228                    {"wght": 1.0},
229                    {"wdth": 1.0},
230                    {"wdth": 1.0, "wght": 1.0},
231                    {"wdth": 1.0, "wght": 0.66},
232                    {"wdth": 0.66, "wght": 0.66},
233                ],
234                [
235                    {},
236                    {"wght": (-1.0, -0.55, 0)},
237                    {"wght": (-1.0, -1.0, -0.55)},
238                    {"wght": (0, 0.55, 1.0)},
239                    {"wght": (0.55, 1.0, 1.0)},
240                    {"wdth": (0, 1.0, 1.0)},
241                    {"wdth": (0, 1.0, 1.0), "wght": (0, 1.0, 1.0)},
242                    {"wdth": (0, 1.0, 1.0), "wght": (0, 0.66, 1.0)},
243                    {"wdth": (0, 0.66, 1.0), "wght": (0, 0.66, 1.0)},
244                ],
245                [
246                    {},
247                    {0: 1.0},
248                    {0: 1.0},
249                    {0: 1.0},
250                    {0: 1.0},
251                    {0: 1.0},
252                    {0: 1.0, 4: 1.0, 5: 1.0},
253                    {
254                        0: 1.0,
255                        3: 0.7555555555555555,
256                        4: 0.24444444444444444,
257                        5: 1.0,
258                        6: 0.66,
259                    },
260                    {
261                        0: 1.0,
262                        3: 0.7555555555555555,
263                        4: 0.24444444444444444,
264                        5: 0.66,
265                        6: 0.43560000000000004,
266                        7: 0.66,
267                    },
268                ],
269            ),
270            (
271                [
272                    {},
273                    {"bar": 0.5},
274                    {"bar": 1.0},
275                    {"foo": 1.0},
276                    {"bar": 0.5, "foo": 1.0},
277                    {"bar": 1.0, "foo": 1.0},
278                ],
279                None,
280                [
281                    {},
282                    {"bar": 0.5},
283                    {"bar": 1.0},
284                    {"foo": 1.0},
285                    {"bar": 0.5, "foo": 1.0},
286                    {"bar": 1.0, "foo": 1.0},
287                ],
288                [
289                    {},
290                    {"bar": (0, 0.5, 1.0)},
291                    {"bar": (0.5, 1.0, 1.0)},
292                    {"foo": (0, 1.0, 1.0)},
293                    {"bar": (0, 0.5, 1.0), "foo": (0, 1.0, 1.0)},
294                    {"bar": (0.5, 1.0, 1.0), "foo": (0, 1.0, 1.0)},
295                ],
296                [
297                    {},
298                    {0: 1.0},
299                    {0: 1.0},
300                    {0: 1.0},
301                    {0: 1.0, 1: 1.0, 3: 1.0},
302                    {0: 1.0, 2: 1.0, 3: 1.0},
303                ],
304            ),
305            (
306                [
307                    {},
308                    {"foo": 0.25},
309                    {"foo": 0.5},
310                    {"foo": 0.75},
311                    {"foo": 1.0},
312                    {"bar": 0.25},
313                    {"bar": 0.75},
314                    {"bar": 1.0},
315                ],
316                None,
317                [
318                    {},
319                    {"bar": 0.25},
320                    {"bar": 0.75},
321                    {"bar": 1.0},
322                    {"foo": 0.25},
323                    {"foo": 0.5},
324                    {"foo": 0.75},
325                    {"foo": 1.0},
326                ],
327                [
328                    {},
329                    {"bar": (0.0, 0.25, 1.0)},
330                    {"bar": (0.25, 0.75, 1.0)},
331                    {"bar": (0.75, 1.0, 1.0)},
332                    {"foo": (0.0, 0.25, 1.0)},
333                    {"foo": (0.25, 0.5, 1.0)},
334                    {"foo": (0.5, 0.75, 1.0)},
335                    {"foo": (0.75, 1.0, 1.0)},
336                ],
337                [
338                    {},
339                    {0: 1.0},
340                    {0: 1.0, 1: 0.3333333333333333},
341                    {0: 1.0},
342                    {0: 1.0},
343                    {0: 1.0, 4: 0.6666666666666666},
344                    {0: 1.0, 4: 0.3333333333333333, 5: 0.5},
345                    {0: 1.0},
346                ],
347            ),
348            (
349                [
350                    {},
351                    {"foo": 0.25},
352                    {"foo": 0.5},
353                    {"foo": 0.75},
354                    {"foo": 1.0},
355                    {"bar": 0.25},
356                    {"bar": 0.75},
357                    {"bar": 1.0},
358                ],
359                None,
360                [
361                    {},
362                    {"bar": 0.25},
363                    {"bar": 0.75},
364                    {"bar": 1.0},
365                    {"foo": 0.25},
366                    {"foo": 0.5},
367                    {"foo": 0.75},
368                    {"foo": 1.0},
369                ],
370                [
371                    {},
372                    {"bar": (0, 0.25, 1.0)},
373                    {"bar": (0.25, 0.75, 1.0)},
374                    {"bar": (0.75, 1.0, 1.0)},
375                    {"foo": (0, 0.25, 1.0)},
376                    {"foo": (0.25, 0.5, 1.0)},
377                    {"foo": (0.5, 0.75, 1.0)},
378                    {"foo": (0.75, 1.0, 1.0)},
379                ],
380                [
381                    {},
382                    {0: 1.0},
383                    {0: 1.0, 1: 0.3333333333333333},
384                    {0: 1.0},
385                    {0: 1.0},
386                    {0: 1.0, 4: 0.6666666666666666},
387                    {0: 1.0, 4: 0.3333333333333333, 5: 0.5},
388                    {0: 1.0},
389                ],
390            ),
391        ],
392    )
393    def test_init(self, locations, axisOrder, sortedLocs, supports, deltaWeights):
394        model = VariationModel(locations, axisOrder=axisOrder)
395
396        assert model.locations == sortedLocs
397        assert model.supports == supports
398        assert model.deltaWeights == deltaWeights
399
400    def test_init_duplicate_locations(self):
401        with pytest.raises(VariationModelError, match="Locations must be unique."):
402            VariationModel(
403                [
404                    {"foo": 0.0, "bar": 0.0},
405                    {"foo": 1.0, "bar": 1.0},
406                    {"bar": 1.0, "foo": 1.0},
407                ]
408            )
409
410    @pytest.mark.parametrize(
411        "locations, axisOrder, masterValues, instanceLocation, expectedValue, masterScalars",
412        [
413            (
414                [
415                    {},
416                    {"axis_A": 1.0},
417                    {"axis_B": 1.0},
418                    {"axis_A": 1.0, "axis_B": 1.0},
419                    {"axis_A": 0.5, "axis_B": 1.0},
420                    {"axis_A": 1.0, "axis_B": 0.5},
421                ],
422                ["axis_A", "axis_B"],
423                [
424                    0,
425                    10,
426                    20,
427                    70,
428                    50,
429                    60,
430                ],
431                {
432                    "axis_A": 0.5,
433                    "axis_B": 0.5,
434                },
435                37.5,
436                [0.25, 0.0, 0.0, -0.25, 0.5, 0.5],
437            ),
438        ],
439    )
440    def test_interpolation(
441        self,
442        locations,
443        axisOrder,
444        masterValues,
445        instanceLocation,
446        expectedValue,
447        masterScalars,
448    ):
449        model = VariationModel(locations, axisOrder=axisOrder)
450
451        interpolatedValue = model.interpolateFromMasters(instanceLocation, masterValues)
452        assert interpolatedValue == expectedValue
453
454        assert masterScalars == model.getMasterScalars(instanceLocation)
455
456        assert model.interpolateFromValuesAndScalars(
457            masterValues, masterScalars
458        ) == pytest.approx(interpolatedValue)
459
460    @pytest.mark.parametrize(
461        "masterLocations, location, expected",
462        [
463            (locationsA, {"wght": 0, "wdth": 0}, [1, 0, 0]),
464            (
465                locationsA,
466                {"wght": 0.5, "wdth": 0},
467                [0.5, 0.5, 0],
468            ),
469            (locationsA, {"wght": 1, "wdth": 0}, [0, 1, 0]),
470            (
471                locationsA,
472                {"wght": 0, "wdth": 0.5},
473                [0.5, 0, 0.5],
474            ),
475            (locationsA, {"wght": 0, "wdth": 1}, [0, 0, 1]),
476            (locationsA, {"wght": 1, "wdth": 1}, [-1, 1, 1]),
477            (
478                locationsA,
479                {"wght": 0.5, "wdth": 0.5},
480                [0, 0.5, 0.5],
481            ),
482            (
483                locationsA,
484                {"wght": 0.75, "wdth": 0.75},
485                [-0.5, 0.75, 0.75],
486            ),
487            (
488                locationsB,
489                {"wght": 1, "wdth": 1},
490                [0, 0, 0, 1],
491            ),
492            (
493                locationsB,
494                {"wght": 0.5, "wdth": 0},
495                [0.5, 0.5, 0, 0],
496            ),
497            (
498                locationsB,
499                {"wght": 1, "wdth": 0.5},
500                [0, 0.5, 0, 0.5],
501            ),
502            (
503                locationsB,
504                {"wght": 0.5, "wdth": 0.5},
505                [0.25, 0.25, 0.25, 0.25],
506            ),
507            (
508                locationsC,
509                {"wght": 0.5, "wdth": 0},
510                [0, 1, 0, 0, 0],
511            ),
512            (
513                locationsC,
514                {"wght": 0.25, "wdth": 0},
515                [0.5, 0.5, 0, 0, 0],
516            ),
517            (
518                locationsC,
519                {"wght": 0.75, "wdth": 0},
520                [0, 0.5, 0.5, 0, 0],
521            ),
522            (
523                locationsC,
524                {"wght": 0.5, "wdth": 1},
525                [-0.5, 1, -0.5, 0.5, 0.5],
526            ),
527            (
528                locationsC,
529                {"wght": 0.75, "wdth": 1},
530                [-0.25, 0.5, -0.25, 0.25, 0.75],
531            ),
532        ],
533    )
534    def test_getMasterScalars(self, masterLocations, location, expected):
535        model = VariationModel(masterLocations)
536        assert model.getMasterScalars(location) == expected
537