"""Exact graph statistics and candidate-level molecular measurements.""" import numpy as np from .exact import endpoint_distribution, expected_cost, solve def graph_metrics(graph, forward, log_rewards, temperature): """Return exact endpoint TV, joint KL and mean cost on the stored DAG.""" ref = solve(graph, log_rewards, temperature) p = endpoint_distribution(graph, forward) mass = np.zeros(len(graph.nodes)) mass[graph.root_index] = 1.0 joint_kl = 0.0 for u in graph.order: for i in graph.outgoing[u]: f = mass[u] * forward[i] if f > 0: joint_kl += f * np.log(forward[i] / ref.forward[i]) mass[graph.node_index[graph.edges[i].target]] += f endpoint_kl = sum(p[y] * np.log(p[y] / ref.target[y]) for y in p if p[y] > 0) return { "endpoint_tv": 0.5 * sum(abs(p[y] - ref.target[y]) for y in p), "joint_kl": float(joint_kl), "conditional_kl": max(0.0, float(joint_kl - endpoint_kl)), "mean_cost": expected_cost(graph, forward), "conditional_free_energy_gap": temperature * max(0.0, float(joint_kl - endpoint_kl)), } def hypervolume_2d(points): """Dominated area for utility points clipped to [0,1]^2, reference (0,0).""" a = np.clip(np.asarray(points, dtype=float), 0, 1) if a.size == 0: return 0.0 a = a[np.argsort(-a[:, 0])] area = 0.0 height = 0.0 for x, y in a: if y > height: area += x * (y - height) height = y return float(area)