| """Full teaching samplers for simplex, rectification, and guided jump flows.""" |
| import math |
| import numpy as np |
| import torch |
| import torch.nn.functional as F |
| from lecture_core import (K, draw, gat_rates, rate_step, gat_sample, |
| dirichlet_velocity, dna_neighbors, mh_refine) |
| from common import project_simplex, objectives, soft_objectives, decode |
|
|
|
|
| @torch.no_grad() |
| def simplex_sample(model, method, batch, length, steps=100, a_max=8., |
| beta=1., tau_max=4., guidance=0.): |
| """Finite Euler discretization with explicit simplex/orthant projection. |
| |
| Projection is a numerical safeguard, not the exact continuous dynamics. |
| Gumbel has an approximate finite-temperature base and endpoint. |
| """ |
| base = torch.distributions.Dirichlet(torch.ones(K)) |
| z = base.sample((batch, length)) |
| corrections = 0 |
| if method == 'gumbel': |
| uniform = torch.rand(batch, length, K).clamp(1e-6, 1-1e-6) |
| g = -(-uniform.log()).log() |
| z = (g / (beta * tau_max)).softmax(-1) |
| y = z.sqrt() if method == 'fisher' else z |
| end = a_max if method == 'dirichlet' else 1. |
| dt = end / steps |
| for i in range(steps): |
| time = i * dt |
| t = torch.full((batch,), time) |
| if method == 'dirichlet': |
| posterior = model(y, t).softmax(-1) |
| velocity = dirichlet_velocity(y, posterior, time) |
| elif method == 'fisher': |
| raw = model(y, t) |
| velocity = raw - y * (raw * y).sum(-1, keepdim=True) |
| else: |
| raw = model(y, t) |
| velocity = raw - raw.mean(-1, keepdim=True) |
| if guidance: |
| |
| |
| with torch.enable_grad(): |
| state = y.detach().requires_grad_(True) |
| probability = state.square() if method == 'fisher' else state |
| score = soft_objectives(probability)[:, 0].sum() |
| gradient = torch.autograd.grad(score, state)[0] |
| if method == 'fisher': |
| gradient -= y * (gradient * y).sum(-1, keepdim=True) |
| else: |
| gradient -= gradient.mean(-1, keepdim=True) |
| velocity += guidance * gradient |
| candidate = y + dt * velocity |
| if not torch.isfinite(candidate).all(): |
| raise FloatingPointError('Nonfinite simplex integration state.') |
| corrections += int((candidate < 0).any(-1).sum()) |
| if method == 'fisher': |
| candidate = candidate.clamp_min(1e-6) |
| y = candidate / candidate.norm(dim=-1, keepdim=True) |
| else: |
| y = project_simplex(candidate) |
| prob = y.square() if method == 'fisher' else y |
| result = prob.argmax(-1) |
| return result, {'negative_coordinate_corrections': corrections, |
| 'max_probability_sum_error': float((prob.sum(-1)-1).abs().max()), |
| 'finite_endpoint': True, 'decode': 'argmax'} |
|
|
|
|
| def rectified_training_loss(model, clean): |
| source = torch.distributions.Dirichlet(torch.ones(K)).sample(clean.shape) |
| target = F.one_hot(clean, K).float() |
| t = torch.rand(len(clean)) |
| z = (1-t[:, None, None])*source + t[:, None, None]*target |
| raw = model(z, t) |
| prediction = raw - raw.mean(-1, keepdim=True) |
| return (prediction-(target-source)).square().sum(-1).mean() |
|
|
|
|
| @torch.no_grad() |
| def make_teacher_pairs(teacher, count, length, steps=60, chunk=64): |
| sources, targets = [], [] |
| for start in range(0, count, chunk): |
| batch = min(chunk, count-start) |
| source = torch.randint(K, (batch, length)) |
| target = gat_sample(teacher, batch, length, steps, source=source) |
| sources.append(source); targets.append(target) |
| return torch.cat(sources), torch.cat(targets) |
|
|
|
|
| def mog_multiplier(changes, preference, strength=1., angle=math.pi/3): |
| """Rank/direction score and acute-cone test; all objectives maximize.""" |
| from scipy.stats import rankdata |
| ranks = rankdata(changes, axis=0, method='average') / len(changes) |
| rank_score = ranks.mean(1) |
| norm = np.linalg.norm(changes, axis=1) |
| cosine = (changes @ preference) / np.maximum(norm*np.linalg.norm(preference), 1e-12) |
| direction = changes @ preference |
| |
| def standardize(x): |
| return (x-x.mean()) / max(float(x.std()), 1e-12) |
| score = standardize(rank_score) + standardize(direction) |
| keep = (norm > 0) & (cosine >= np.cos(angle)) |
| return strength * np.exp(np.clip(score, -20, 20)) * keep |
|
|
|
|
| @torch.no_grad() |
| def mog_sample(model, batch, length, steps=50, preference=(.7, .3), strength=1.): |
| """Evaluate all single-base edits, reweight rates, then use adaptive Euler. |
| |
| All-position scoring differs from the paper's random-position loop. |
| Rank normalization is per position. The adaptive cone stays acute; |
| no outside-cone fallback is used, so the local theorem remains valid. |
| """ |
| z = torch.randint(K, (batch, length)) |
| t = 0. |
| rejected = 0 |
| angle = math.pi/3 |
| ema = .5 |
| count = 0 |
| while t < 1-1e-6: |
| tb = torch.full((batch,), t) |
| rates = gat_rates(model(z, tb).softmax(-1), z, tb) |
| rejected_step = 0 |
| for b in range(batch): |
| for i in range(length): |
| candidates, destinations = [], [] |
| for a in range(K): |
| if a == int(z[b, i]): |
| continue |
| y = z[b].clone(); y[i] = a |
| candidates.append(y); destinations.append(a) |
| changes = (objectives(torch.stack(candidates)) - objectives(z[b:b+1])).numpy() |
| multiplier = mog_multiplier(changes, np.array(preference), strength, angle) |
| rejected_step += int(np.sum(multiplier == 0)) |
| for a, value in zip(destinations, multiplier): |
| rates[b, i, a] *= float(value) |
| rejected += rejected_step |
| rejection = rejected_step / (batch * length * (K-1)) |
| ema = .9 * ema + .1 * rejection |
| angle = float(np.clip(angle * np.exp(.02 * (ema-.5)), |
| np.deg2rad(10), np.deg2rad(89))) |
| max_exit = float(rates.sum(-1).max()) |
| h = min(1./steps, 1-t, .5/max(max_exit, 1e-8)) |
| z = rate_step(z, rates, h) |
| t += h |
| count += 1 |
| if count > 50000: |
| raise RuntimeError('MOG integration failed to advance.') |
| return z, {'filtered_candidates': rejected, 'adaptive_steps': count, |
| 'preference': list(preference), 'final_cone_angle_degrees': float(np.rad2deg(angle)), |
| 'guarantee': 'local weighted progress under the acute-cone assumptions'} |
|
|
|
|
| def reference_model(data, pseudocount=.5): |
| """A tractable positive product reference for exact MH ratios.""" |
| counts = F.one_hot(data, K).float().sum(0) + pseudocount |
| probability = counts / counts.sum(-1, keepdim=True) |
| def log_probability(sequence): |
| idx = torch.tensor(['ACGT'.index(c) for c in sequence]) |
| return float(probability.log()[torch.arange(len(idx)), idx].sum()) |
| return probability, log_probability |
|
|
|
|
| def refine_areuredi(tokens, data, steps=100, preference=(.5,.5), eta_max=8., seed=7): |
| """ReDi endpoint refinement with normalized proposals and exact MH ratios. |
| |
| The reference is explicitly fitted and evaluable; it is not a denoiser |
| joint density. Annealing is finite, so no global-optimum claim is made. |
| """ |
| from lecture_core import encode |
| _, log_ref = reference_model(data) |
| weight = np.array(preference) |
| def score(sequence): |
| values = objectives(encode([sequence]))[0].numpy() |
| return float(np.min(weight * values)) |
| rng = np.random.default_rng(seed) |
| strings = decode(tokens) |
| changes = 0 |
| for step in range(steps): |
| eta = eta_max * (step+1) / steps |
| for i, sequence in enumerate(strings): |
| new = mh_refine(sequence, score, log_ref, eta, rng) |
| changes += int(new != sequence) |
| strings[i] = new |
| return encode(strings), {'accepted_edits': changes, 'eta_final': eta_max, |
| 'reference': 'smoothed position-wise empirical DNA frequencies', |
| 'scope': 'finite annealing demonstration, not an equilibrium certificate'} |
|
|