Branch-Length Polynomial Fingerprint / fingerprint.py

✓✓ Beats tuned baseline

Raw ⬇ ZIP
 1import numpy as np
 2
 3
 4def polynomial_fingerprint(tree, x=1.0, y_points=None, eps=1e-12):
 5    """Evaluate the branch-length polynomial and return log-magnitude features.
 6
 7    ``tree`` is an immutable nested tuple: a node is a tuple of
 8    ``(positive_edge_length, child)`` pairs and a leaf is ``()``.  The
 9    recursion is order-independent because child contributions are multiplied.
10    For real points this returns M log|P(root; x, y_m)| values.  The small eps
11    only prevents log(0) in this finite-precision feature implementation.
12    """
13    if y_points is None:
14        raise ValueError("y_points must contain at least one evaluation point")
15    ys = np.asarray(y_points, dtype=float)
16    if ys.ndim != 1 or ys.size == 0:
17        raise ValueError("y_points must be a non-empty 1D array")
18
19    def rec(node):
20        if not node:
21            return np.full(ys.size, float(x))
22        out = np.ones(ys.size, dtype=float)
23        for weight, child in node:
24            if weight <= 0:
25                raise ValueError("edge lengths must be positive")
26            out *= weight * ys + rec(child)
27        return out
28
29    values = rec(tree)
30    return np.log(np.maximum(np.abs(values), eps))
31
32
33def polynomial_values(tree, x=1.0, y_points=None):
34    """Return raw real polynomial evaluations, useful for algebra checks."""
35    if y_points is None:
36        raise ValueError("y_points must contain at least one evaluation point")
37    ys = np.asarray(y_points, dtype=float)
38
39    def rec(node):
40        if not node:
41            return np.full(ys.size, float(x))
42        result = np.ones(ys.size)
43        for weight, child in node:
44            result *= weight * ys + rec(child)
45        return result
46
47    return rec(tree)