import numpy as np def polynomial_fingerprint(tree, x=1.0, y_points=None, eps=1e-12): """Evaluate the branch-length polynomial and return log-magnitude features. ``tree`` is an immutable nested tuple: a node is a tuple of ``(positive_edge_length, child)`` pairs and a leaf is ``()``. The recursion is order-independent because child contributions are multiplied. For real points this returns M log|P(root; x, y_m)| values. The small eps only prevents log(0) in this finite-precision feature implementation. """ if y_points is None: raise ValueError("y_points must contain at least one evaluation point") ys = np.asarray(y_points, dtype=float) if ys.ndim != 1 or ys.size == 0: raise ValueError("y_points must be a non-empty 1D array") def rec(node): if not node: return np.full(ys.size, float(x)) out = np.ones(ys.size, dtype=float) for weight, child in node: if weight <= 0: raise ValueError("edge lengths must be positive") out *= weight * ys + rec(child) return out values = rec(tree) return np.log(np.maximum(np.abs(values), eps)) def polynomial_values(tree, x=1.0, y_points=None): """Return raw real polynomial evaluations, useful for algebra checks.""" if y_points is None: raise ValueError("y_points must contain at least one evaluation point") ys = np.asarray(y_points, dtype=float) def rec(node): if not node: return np.full(ys.size, float(x)) result = np.ones(ys.size) for weight, child in node: result *= weight * ys + rec(child) return result return rec(tree)