Branch-Length Polynomial Fingerprint / fingerprint.py
Beats tuned baseline
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)