File size: 3,722 Bytes
d58cfe6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""The cone's composition as a point in a 3-simplex: coordinates, camera and the mark.

Numpy only; nothing here needs gradio or a model.
"""
import re

import numpy as np
import pytest

from ulens import simplex, terms


def test_the_vertices_are_a_regular_tetrahedron_about_the_origin():
    V = simplex.VERT
    assert V.shape == (4, 3)
    np.testing.assert_allclose(V.sum(0), 0, atol=1e-6)                 # barycentre at 0
    np.testing.assert_allclose(np.linalg.norm(V, axis=1), 1, atol=1e-6)  # circumradius 1
    d = [np.linalg.norm(V[i] - V[j]) for i in range(4) for j in range(i + 1, 4)]
    np.testing.assert_allclose(d, d[0], atol=1e-6)                     # six equal edges
    assert len(terms.categories()) == 4                                # one vertex per axis


def test_shares_sum_to_one_and_a_zero_row_is_undefined():
    C = np.array([[1.0, 2.0, 3.0, 4.0], [0.5, 0.5, 0.5, 0.5], [0.0, 0.0, 0.0, 0.0]])
    W = simplex.shares(C)
    np.testing.assert_allclose(W[:2].sum(1), 1.0)
    np.testing.assert_allclose(W[0], [0.1, 0.2, 0.3, 0.4])
    assert np.isnan(W[2]).all()                                        # no composition at all
    assert (simplex.shares(np.full((5, 4), 2.0)) == 0.25).all()


def test_a_pure_one_axis_state_lands_on_its_vertex_and_the_centroid_at_the_origin():
    P = simplex.bary(np.eye(4))
    np.testing.assert_allclose(P, simplex.VERT, atol=1e-6)
    np.testing.assert_allclose(simplex.bary(np.full((1, 4), 0.25)), 0, atol=1e-6)
    # a state between two axes sits on their edge's midpoint
    np.testing.assert_allclose(simplex.bary(np.array([[0.5, 0.5, 0, 0]]))[0],
                               (simplex.VERT[0] + simplex.VERT[1]) / 2, atol=1e-6)


def test_boundary_fraction_is_zero_at_the_centre_and_one_on_the_surface():
    V = simplex.VERT
    assert simplex.boundary_fraction(np.zeros((1, 3)))[0] == pytest.approx(0.0)
    assert simplex.boundary_fraction(V)[0] == pytest.approx(1.0)                  # a vertex
    face = V[:3].mean(0)                                                          # a face centre
    assert simplex.boundary_fraction(face[None])[0] == pytest.approx(1.0)
    assert simplex.boundary_fraction((0.5 * face)[None])[0] == pytest.approx(0.5)
    assert simplex.boundary_fraction((1.5 * face)[None])[0] > 1.0                 # outside


def test_the_camera_sends_the_origin_to_the_screen_centre_and_keeps_the_paper_s_layout():
    xy = simplex.project(np.zeros((1, 3)))
    np.testing.assert_allclose(xy, 0, atol=1e-9)
    amb, inc, ce, gu = simplex.project(simplex.VERT)
    assert gu[1] > amb[1] > 0 > inc[1] > ce[1]          # general uncertainty on top
    assert inc[0] < amb[0] < 0 < gu[0] < ce[0]          # incompleteness left, conflict right
    # perspective, not orthographic: the near vertex (ambiguity) is pulled toward the eye
    orth = simplex.project(simplex.VERT, eye=tuple(1e6 * np.asarray(simplex.EYE)))
    assert abs(amb[1]) > abs(orth[0][1])


def test_the_mark_is_the_same_tetrahedron_under_the_same_camera():
    """`data/mark.svg` is generated from `mark_points`; this keeps it from drifting."""
    svg = simplex.mark_svg()
    assert svg.startswith("<svg") and svg.endswith("</svg>")
    assert 'viewBox="0 0 32 32"' in svg
    circles = re.findall(r'<circle cx="([\d.]+)" cy="([\d.]+)" r="([\d.]+)" fill="(#[0-9a-fA-F]{6})"',
                         svg)
    assert len(circles) == 4 and svg.count("<line ") == 6 and "<text" not in svg
    pts = np.array([[float(x), float(y)] for x, y, _r, _c in circles])
    np.testing.assert_allclose(pts, simplex.mark_points(), atol=0.01)
    assert (pts >= 0).all() and (pts <= 32).all()
    assert simplex.MARK_PATH.name == "mark.svg" and simplex.MARK_PATH.is_file()