File size: 3,749 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
75
76
77
78
79
80
81
82
83
"""The cone's composition as a point in a 3-simplex, and the tetrahedron that is the mark.

The four rectified components, normalised to sum to one, are barycentric coordinates in a
3-simplex: the positive orthant the cone lives in is exactly a regular tetrahedron whose four
vertices are the four axes, so a state's composition is drawn with nothing reduced or
projected away. Near a vertex, one axis carries the cone; at the barycentre all four
contribute equally. What the picture drops is the magnitude — how strongly the state pointed
— and only that.

Numpy only. The demo draws the view as inline SVG through `project`, and the same camera gives
the page its mark (`data/mark.svg`, the favicon and the eyebrow): four vertex dots in the axis
colours on six thin edges, nothing else.
"""
from __future__ import annotations

from importlib.resources import files

import numpy as np

#: a regular tetrahedron with its barycentre at the origin and circumradius 1. Row order is
#: the category order (`ambiguity, incompleteness, conflicting_evidence, general_uncertainty`).
VERT = np.array([[1, 1, 1], [1, -1, -1], [-1, 1, -1], [-1, -1, 1]], np.float32) / np.sqrt(3.0)

#: the paper's camera (its simplex-path figures): the eye, looking at the barycentre, z up.
EYE = (1.08, 1.87, 0.79)

#: the mark's file inside the package, so `pip install` ships it and `launch` can serve it
MARK_PATH = files("ulens.data") / "mark.svg"


def shares(C: np.ndarray) -> np.ndarray:
    """`[T, 4]` non-negative components as shares summing to one; a row of zeros is NaN."""
    C = np.asarray(C, np.float64)
    tot = C.sum(axis=-1, keepdims=True)
    with np.errstate(invalid="ignore", divide="ignore"):
        return np.where(tot > 0, C / tot, np.nan)


def bary(W: np.ndarray) -> np.ndarray:
    """`[T, 4]` shares to `[T, 3]` points inside the tetrahedron. Linear: a pure one-axis
    state is that axis's vertex and equal shares are the origin."""
    return np.asarray(W, np.float64) @ VERT.astype(np.float64)


def boundary_fraction(P: np.ndarray) -> np.ndarray:
    """How far out each `[T, 3]` point sits, as the fraction of the way from the barycentre
    to the tetrahedron's surface along its own direction: 0 at the centre, 1 on a face.
    The face opposite vertex `i` has outward normal `-VERT[i]` and sits at distance 1/3."""
    P = np.asarray(P, np.float64)
    d = -(P @ VERT.T.astype(np.float64))            # [T, 4] signed distance along each normal
    return np.max(d, axis=-1) * 3.0


def project(P: np.ndarray, eye=EYE) -> np.ndarray:
    """`[T, 3]` to `[T, 2]` screen coordinates (y up) under a perspective camera at `eye`
    looking at the origin with z up. The origin projects to (0, 0)."""
    e = np.asarray(eye, np.float64)
    d0 = np.linalg.norm(e)
    f = -e / d0                                      # forward
    r = np.cross(f, np.array([0.0, 0.0, 1.0]))
    r /= np.linalg.norm(r)                           # right
    u = np.cross(r, f)                               # up on screen
    rel = np.asarray(P, np.float64) - e
    depth = rel @ f
    return np.column_stack([(rel @ r) * d0 / depth, (rel @ u) * d0 / depth])


def mark_points(size: float = 32.0, margin: float = 4.6) -> np.ndarray:
    """The four vertices on a `size`-square canvas (y down), the projected frame fitted
    inside `margin`. This is where the dots of `data/mark.svg` are."""
    xy = project(VERT)
    lo, hi = xy.min(0), xy.max(0)
    s = (size - 2 * margin) / float((hi - lo).max())
    ctr = (lo + hi) / 2
    out = (xy - ctr) * s
    out[:, 1] *= -1
    return out + size / 2


def mark_svg() -> str:
    """The mark's markup, for inline use; the file itself is the favicon."""
    return MARK_PATH.read_text(encoding="utf-8").strip()