"""The worked numbers of Chapter 5, asserted against the files of ``lecture_5/``. Every number checked here is printed in the notes. Where the chapter runs a simulation, the test asserts the analytic value the simulation estimates, with a tolerance wide enough for the sample size the file uses by default. """ from __future__ import annotations import importlib.util import math import os import subprocess import sys from pathlib import Path import pytest import torch from cis6270.nets import SequenceDenoiser ROOT = Path(__file__).resolve().parents[1] def load(name): """Import one lecture file by path, so that ``lecture_5`` needs no package marker.""" spec = importlib.util.spec_from_file_location(f"lecture5_{name}", ROOT / "lecture_5" / f"{name}.py") module = importlib.util.module_from_spec(spec) sys.path.insert(0, str(ROOT)) spec.loader.exec_module(module) return module dfm = load("dfm") velocity = load("velocity") corrector = load("corrector") kinetic = load("kinetic_optimal") edits = load("edit_flows") dirichlet = load("dirichlet") fisher = load("fisher") gumbel = load("gumbel") guidance = load("guidance") objectives = load("multi_objective") rectify = load("rectify") def close(values, expected, tolerance=5e-4): """Compare a list of numbers against the chapter's, entry by entry.""" assert len(values) == len(expected) for got, want in zip(values, expected): assert abs(float(got) - want) <= tolerance, (values, expected) # %% Section 5.2 and 5.3: a velocity, its two rules, and the master equation def test_worked_master_equation(): u, p, p_dot, arriving, leaving = velocity.worked_master_equation() assert velocity.obeys_the_two_rules(u) assert torch.allclose(u, velocity.set_diagonal(u)) close(p_dot.tolist(), [-0.1, -0.2, 0.7, -0.4]) assert abs(float(p_dot.sum())) < 1e-9 assert abs(arriving - 0.7) < 1e-9 and abs(leaving) < 1e-9 close((p + 0.1 * p_dot).tolist(), [0.09, 0.18, 0.37, 0.36]) def test_hazard_and_survival(): assert abs(velocity.hazard(0.5) - 2.0) < 1e-9 assert abs(velocity.hazard(0.9) - 10.0) < 1e-9 assert abs(velocity.survival(4000) - 0.1) < 2e-3 # S(t) = 1 - kappa_t. def test_continuous_and_discrete_side_by_side(): p1 = torch.tensor([0.1, 0.6, 0.2, 0.1]) moved, drawn, lam = velocity.side_by_side(2, p1, t=0.5, h=0.1) close(moved.tolist(), [0.02, 0.12, 0.84, 0.02]) close(drawn.tolist(), [0.02, 0.12, 0.84, 0.02]) assert abs(lam - 0.2) < 1e-9 # %% Sections 5.5 to 5.11: the mixture path, the velocity, the loss and the sampler def test_dfm_worked_numbers(): report = dfm.worked_numbers() assert report["mixture_draw"] == "ATGA" close(report["velocity_at_G"], [0.2, 1.2, -1.6, 0.2]) close(report["euler_step_at_G"], [0.02, 0.12, 0.84, 0.02]) assert abs(report["redraw_probability"] - 0.2) < 1e-9 assert abs(report["example_loss_nats"] - 4.780) < 1e-3 close(report["mask_posterior_C_G"], [0.625, 0.375]) assert report["mask_posterior_unchanged"] # Proposition 6 of Discrete Flow Matching. assert abs(report["posterior_entropy_nats"] - 0.6616) < 5e-4 assert abs(report["kl_gap_to_uniform"] - 0.0316) < 5e-4 def test_conditional_velocity_generates_the_conditional_path(): """Section 5.6.2: the two terms of the master equation on the mixture path.""" kappa, x0, x1 = 0.4, 0, 1 path = torch.zeros(4, dtype=torch.double) path[x0], path[x1] = 1.0 - kappa, kappa total = sum(path[z] * dfm.conditional_velocity(z, x1, kappa) for z in (x0, x1)) derivative = torch.zeros(4, dtype=torch.double) derivative[x1], derivative[x0] = 1.0, -1.0 # kappa_dot_t times the two indicators. assert torch.allclose(total, derivative, atol=1e-9) def test_corrupt_reaches_both_endpoints(): torch.manual_seed(0) process = dfm.DiscreteFlowMatching(4) x1 = torch.randint(0, 4, (64, 16)) assert torch.equal(process.corrupt(x1, torch.ones(64)), x1) # kappa_t = 1 gives the data. source = process.corrupt(x1, torch.zeros(64)) # kappa_t = 0 gives the source. assert float((source == x1).double().mean()) < 0.4 # A uniform draw agrees about a quarter of the time. halfway = process.corrupt(x1, torch.full((64,), 0.5)) assert 0.55 < float((halfway == x1).double().mean()) < 0.75 # Heads with probability one half. def test_two_sources(): assert torch.equal(dfm.DiscreteFlowMatching(4, mask=4).source((3, 5)), torch.full((3, 5), 4, dtype=torch.long)) # The all-mask source. uniform = dfm.DiscreteFlowMatching(4).source((200, 20)) assert int(uniform.min()) == 0 and int(uniform.max()) == 3 # Every base appears. # %% Sections 5.13 and 5.14: zero-flux velocities and the corrector def test_zero_flux_velocity_on_three_states(): p, u, v, with_u, with_both = corrector.three_state_example() assert torch.allclose(corrector.net_flux(v, p), torch.zeros(3), atol=1e-12) close(with_u.tolist(), [-0.2, -0.3, 0.5]) close(with_both.tolist(), [-0.2, -0.3, 0.5]) assert abs(float(-u[0, 0]) - 1.0) < 1e-12 and abs(float(-(u + v)[0, 0]) - 5.0) < 1e-12 def test_corrector_step_table(): """Section 5.14.3 at t = 0.4 with h = 0.01 and the denoiser (0.1, 0.6, 0.2, 0.1).""" p1 = torch.tensor([0.1, 0.6, 0.2, 0.1]) expected = { 0.0: ([0.00167, 0.01000, 0.00333, 0.00167], 0.98333), 2.0: ([0.00300, 0.01800, 0.00600, 0.00300], 0.97000), 10.0: ([0.00833, 0.05000, 0.01667, 0.00833], 0.91667), } for eta, (probabilities, stay) in expected.items(): bar_u = corrector.corrector_velocity(p1, corrector.MASK, 0.4, eta) close((0.01 * bar_u[:4]).tolist(), probabilities, tolerance=1e-5) assert abs(float(1.0 + 0.01 * bar_u[corrector.MASK]) - stay) < 1e-5 assert abs(corrector.masked_fraction_derivative(0.4, eta) + 1.0) < 1e-12 def test_corrector_rates_and_admissible_steps(): p1 = torch.tensor([0.1, 0.6, 0.2, 0.1]) bar_u = corrector.corrector_velocity(p1, corrector.MASK, 0.4, 2.0) assert abs(float(-bar_u[corrector.MASK]) - 3.0) < 1e-12 # (1 + 2 x 0.4)/(1 - 0.4). visible = torch.zeros(4) visible[1] = 1.0 assert abs(float(corrector.corrector_velocity(visible, 1, 0.4, 2.0)[corrector.MASK]) - 2.0) < 1e-12 close([corrector.admissible_step(0.9, eta) for eta in (0.0, 2.0, 10.0)], [0.1, 0.0357, 0.01], tolerance=1e-4) def test_jump_counts_are_one_plus_eta(): """Section 5.14.3: the masked fraction stays at 1 - t and the jumps come to 1 + eta.""" p1 = torch.tensor([0.25, 0.25, 0.25, 0.25]) for eta in (0.0, 2.0, 10.0): half, mean = corrector.simulate_jumps(p1, eta, positions=10_000, steps=300) assert abs(half - 0.5) < 0.03 assert abs(mean - (1.0 + eta)) < 0.25 def test_parallel_step_error(): assert abs(corrector.joint_reveal_rate(64) - 1.0 / 64) < 1e-12 assert abs(corrector.pair_survival(2.0) - 0.1353) < 5e-4 assert abs(corrector.pair_survival(10.0) - 4.54e-5) < 1e-7 measured = corrector.simulate_pair(0.0, samples=40_000, steps=64) assert abs(measured - 0.5 / 64) < 2e-3 # Half of the joint reveals land outside the data. # %% Section 5.16: kinetic-optimal paths def test_kinetic_optimal_recovers_the_mixture_velocity(): q = torch.tensor([0.1, 0.6, 0.2, 0.1]) p, p_dot = kinetic.mask_path(q, kappa_t=0.5) u = kinetic.kinetic_optimal(p, p_dot) assert abs(float(p_dot[kinetic.MASK] / p[kinetic.MASK]) + 2.0) < 1e-12 assert abs(float(p_dot[1] / p[1]) - 2.0) < 1e-12 assert abs(float(u[1, kinetic.MASK]) - 1.2) < 1e-12 # 0.30 times 4. assert abs(float(u[1, kinetic.MASK]) - kinetic.mixture_rate(0.5) * 0.6) < 1e-12 assert abs(float(u[1, 2])) < 1e-12 # No probability moves between two visible tokens. assert torch.allclose(u @ p, p_dot, atol=1e-12) def test_worked_metric_path_for_dna(): beta_t, beta_dot = kinetic.beta_schedule(0.5) assert abs(beta_t - 1.0) < 1e-12 and abs(beta_dot - 4.0) < 1e-12 p = kinetic.metric_path(2, beta_t) close(p.tolist(), [0.2245, 0.0826, 0.6103, 0.0826]) u = kinetic.metric_velocity(2, beta_t, beta_dot) assert abs(float(u[0, 1]) - 0.898) < 5e-4 # C to A. assert abs(float(u[2, 1]) - 4.882) < 5e-4 # C to G. assert abs(float(u[2, 0]) - 2.441) < 5e-4 # A to G. assert abs(float(-u[1, 1]) - 5.780) < 1e-3 # The total rate out of C. close((torch.eye(4)[1] + 0.05 * u[:, 1]).tolist(), [0.0449, 0.7110, 0.2441, 0.0]) close((u @ p).tolist(), [-0.3997, -0.4774, 1.3546, -0.4774]) close(kinetic.path_derivative(2, 0.5).tolist(), [-0.3997, -0.4774, 1.3546, -0.4774], 1e-3) def test_energy_of_two_schedules(): assert abs(kinetic.schedule_energy(*kinetic.LINEAR, 600, 1e-6) - 27.63) < 5e-3 assert abs(kinetic.schedule_energy(*kinetic.LINEAR, 600, 1e-9) - 41.45) < 5e-3 assert abs(kinetic.schedule_energy(*kinetic.GEODESIC, 600, 1e-6) - math.pi ** 2) < 1e-3 assert abs(kinetic.geodesic_schedule(0.25) - 0.1464) < 5e-4 assert abs(kinetic.geodesic_schedule(0.75) - 0.8536) < 5e-4 assert abs(kinetic.geodesic_schedule(0.5, 0.25) - 0.6667) < 5e-4 # %% Section 5.17: Edit Flows def test_alignment_and_column_wise_path(): z0 = [0, 1, 2, 3, edits.BLANK] # (A, C, G, T, eps). z1 = [0, edits.BLANK, 2, 0, 1] # (A, eps, G, A, C). assert edits.column_kinds(z0, z1) == ["none", "del", "none", "sub", "ins"] drawn = edits.draw_state(z0, z1, [True, False, True, True, True]) assert drawn == [0, 1, 2, 0, 1] # ACGAC, the draw of probability 0.5 ** 3 = 0.125. assert abs(edits.expected_length([0, 1, 2, 3], [0, 2, 0, 1], 0.5) - 4.0) < 1e-12 recovered = edits.align([0, 1, 2, 3], [0, 2, 0, 1]) assert len(recovered[0]) == len(recovered[1]) # Any alignment pads to a common width. def test_alignment_posterior_and_averaged_rates(): first = ([0, 1, edits.BLANK], [0, 0, 1]) # Substitute C with A, then insert C. second = ([0, edits.BLANK, 1], [0, 0, 1]) # Insert A, and leave the last column alone. for kappa_t, expected in ((0.25, 0.4286), (0.5, 1 / 3), (0.75, 0.2)): weights = edits.alignment_posterior([first, second], [0, 1], kappa_t) assert abs(weights[0] - expected) < 5e-4 posterior, rates = edits.averaged_rates([first, second], [0, 1], 0.5) assert abs(rates["sub(2,A)"] - 0.667) < 1e-3 assert abs(rates["ins(1,A)"] - 1.333) < 1e-3 assert abs(rates["ins(2,C)"] - 0.667) < 1e-3 assert abs(sum(rates.values()) - 2.667) < 1e-3 def test_edit_loss_and_sampler_step(): assert abs(edits.edit_loss(3.0, [0.8], kappa_t=0.5) - 3.446) < 1e-3 grid = [0.01 * i for i in range(1, 801)] assert abs(edits.loss_minimizer(2.0, grid) - 2.0) < 1e-6 intensities = {"ins": [0.2, 0.3, 0.4], "del": [0.1, 0.9, 0.2], "sub": [0.4, 0.3, 0.5]} insert, change = edits.step_probabilities(intensities, 0.05) close(insert, [0.010, 0.015, 0.020]) close(change, [0.025, 0.060, 0.035]) exact, first_order = edits.untouched_probability(intensities, 0.05) assert abs(exact - 0.8452) < 5e-4 and abs(first_order - 0.835) < 5e-4 assert abs(exact - first_order - 0.0102) < 5e-4 assert abs(edits.expected_length_change(intensities, 0.05) + 0.015) < 1e-9 assert edits.edit_rates(intensities)[0] == 11 # Three intensities and two token distributions. # %% Section 5.20: Dirichlet Flow Matching def test_dirichlet_path_and_speeds(): assert abs(dirichlet.dirichlet_density(torch.full((4,), 0.25), 0, 0.0) - 6.0) < 1e-9 assert dirichlet.straight_line_corners(torch.tensor([0.34, 0.33, 0.33, 0.0]), 0.3) == 3 assert dirichlet.straight_line_corners(torch.tensor([0.70, 0.30, 0.0, 0.0]), 0.6) == 1 closed = dirichlet.speed_two_letters(0.3, 1.0) assert abs(closed - 0.2580) < 5e-5 assert abs(float(dirichlet.speed(0.3, 1.0, K=2)) - closed) < 1e-6 x = torch.tensor([0.40, 0.30, 0.20, 0.10]) close(dirichlet.speed_vector(x, 1.0).tolist(), [0.232, 0.199, 0.159, 0.104], 1e-3) def test_dirichlet_velocity_and_posterior_average(): x = torch.tensor([0.40, 0.30, 0.20, 0.10]) toward_a = dirichlet.conditional_velocity(x, 0, 1.0) close(toward_a.tolist(), [0.139, -0.070, -0.046, -0.023], 1e-3) assert abs(float(toward_a.sum())) < 1e-12 p_hat = torch.tensor([0.60, 0.25, 0.10, 0.05]) v = dirichlet.marginal_velocity(x, p_hat, 1.0) close(v.tolist(), [0.0553, -0.0133, -0.0262, -0.0158], 2e-4) close((x + 0.1 * v).tolist(), [0.4055, 0.2987, 0.1974, 0.0984]) assert abs(float((x + 0.1 * v).sum()) - 1.0) < 1e-12 def test_dirichlet_continuity(): time_term, space_term = dirichlet.continuity_terms(torch.tensor([0.5, 0.3, 0.2]), 0, 0.8) assert abs(time_term - 0.6355) < 5e-4 assert abs(space_term + 0.6355) < 5e-4 assert abs(time_term + space_term) < 1e-5 # %% Section 5.21: Fisher-Flow def test_two_moves_under_two_metrics(): near = torch.tensor([0.5, 0.5]), torch.tensor([0.4, 0.6]) rare = torch.tensor([0.01, 0.99]), torch.tensor([0.001, 0.999]) assert abs(fisher.euclidean_distance(*near) - 0.1414) < 5e-4 assert abs(fisher.euclidean_distance(*rare) - 0.0127) < 5e-4 assert abs(fisher.fisher_rao_distance(*near) - 0.201) < 1e-3 assert abs(fisher.fisher_rao_distance(*rare) - 0.137) < 1e-3 orthogonal = fisher.fisher_rao_distance(torch.tensor([1.0, 0.0]), torch.tensor([0.0, 1.0])) assert abs(orthogonal - math.pi) < 1e-6 # Any two letters are the farthest apart. def test_sphere_and_geodesic(): p0 = torch.full((4,), 0.25) p1 = fisher.smooth(torch.eye(4)[0]) close(p1.tolist(), [0.91, 0.03, 0.03, 0.03]) x0, x1 = fisher.to_sphere(p0), fisher.to_sphere(p1) close(x1.tolist(), [0.954, 0.173, 0.173, 0.173], 1e-3) assert abs(float((x0 * x1).sum()) - 0.737) < 1e-3 assert abs(math.acos(float((x0 * x1).sum())) - 0.743) < 1e-3 assert abs(fisher.fisher_rao_distance(p0, p1) - 1.485) < 1e-3 mid = fisher.geodesic(x0, x1, torch.tensor(0.5)) close(mid.tolist(), [0.780, 0.361, 0.361, 0.361], 1e-3) close(fisher.to_simplex(mid).tolist(), [0.609, 0.130, 0.130, 0.130], 1e-3) remaining = fisher.log_map(mid, x1) close(remaining.tolist(), [0.2323, -0.1672, -0.1672, -0.1672], 1e-3) assert abs(float(remaining.norm()) - 0.3713) < 5e-4 assert abs(float(fisher.conditional_velocity(mid, x1, 0.5).norm()) - 0.743) < 1e-3 stepped = fisher.exp_map(mid, 0.1 * fisher.conditional_velocity(mid, x1, 0.5)) close(stepped.tolist(), [0.824, 0.327, 0.327, 0.327], 1e-3) close(fisher.to_simplex(stepped).tolist(), [0.680, 0.107, 0.107, 0.107], 1e-3) assert abs(float(stepped.norm()) - 1.0) < 1e-10 def test_tangent_projection(): x0, p1 = fisher.to_sphere(torch.full((4,), 0.25)), fisher.smooth(torch.eye(4)[0]) mid = fisher.geodesic(x0, fisher.to_sphere(p1), torch.tensor(0.5)) raw = torch.tensor([0.3, -0.2, 0.5, 0.1]) assert abs(float((mid * raw).sum()) - 0.378) < 1e-3 tangent = fisher.project(mid, raw) close(tangent.tolist(), [0.005, -0.337, 0.363, -0.037], 1e-3) assert abs(float((mid * tangent).sum())) < 1e-12 # %% Section 5.22: Gumbel-Softmax Flow Matching def test_gumbel_max_and_temperature(): pi = torch.tensor([0.5, 0.2, 0.2, 0.1]) torch.manual_seed(0) frequencies = gumbel.gumbel_max(pi, 200_000) close(frequencies.tolist(), pi.tolist(), tolerance=5e-3) close([gumbel.temperature(t) for t in (0.0, 0.5, 0.9, 1.0)], [10.0, 2.2313, 0.6721, 0.4979], 5e-4) def test_annealed_path_and_replicator(): logits = torch.tensor([0.0, -0.45, 1.40, -0.05]) expected = { 0.0: [0.2438, 0.2331, 0.2805, 0.2426], 0.5: [0.2142, 0.1751, 0.4012, 0.2095], 0.9: [0.0955, 0.0489, 0.7669, 0.0887], 1.0: [0.0528, 0.0214, 0.8781, 0.0477], } for t, values in expected.items(): close(gumbel.annealed_path(logits, t).tolist(), values) x = gumbel.annealed_path(logits, 0.5) assert abs(float((x * logits).sum()) - 0.4724) < 5e-4 noisy = gumbel.replicator_velocity(x, logits, 0.5) close(noisy.tolist(), [-0.136, -0.217, 0.500, -0.147], 1e-3) assert abs(float(noisy.sum())) < 1e-12 close(gumbel.inference_velocity(x, 2, 0.5).tolist(), [-0.116, -0.094, 0.323, -0.113], 1e-3) def test_gumbel_marginal_velocity(): late = torch.tensor([0.138, 0.084, 0.647, 0.131]) p_hat = torch.tensor([0.05, 0.10, 0.70, 0.15]) coefficients = (gumbel.LAMBDA / gumbel.temperature(0.8)) * late * p_hat close(coefficients.tolist(), [0.0228, 0.0278, 1.4976, 0.0650], 5e-4) v = gumbel.marginal_velocity(late, p_hat, 0.8) close(v.tolist(), [-0.200, -0.108, 0.454, -0.146], 1e-3) close((late + 0.01 * v).tolist(), [0.1360, 0.0829, 0.6515, 0.1295]) # %% Sections 5.24 to 5.29 and 5.33: guidance def test_guided_base(): p1 = torch.tensor([0.1, 0.2, 0.3, 0.4]) rates = p1 / (1.0 - 0.6) scores = torch.tensor([0.10, 0.35, 0.55, 0.25]) close(rates.tolist(), [0.25, 0.50, 0.75, 1.00]) assert abs(float(rates.sum()) - 2.50) < 1e-12 close((scores / 0.20).tolist(), [0.50, 1.75, 2.75, 1.25]) guided = guidance.tilt(rates, scores, 0.20) close(guided.tolist(), [0.125, 0.875, 2.0625, 1.250]) assert abs(float(guided.sum()) - 4.3125) < 1e-12 close((guided / guided.sum()).tolist(), [0.029, 0.203, 0.478, 0.290], 1e-3) close((p1 * scores).tolist(), [0.010, 0.070, 0.165, 0.100]) h = guidance.h_value(p1, scores) assert abs(h - 0.345) < 1e-12 assert abs(float(guidance.tilt(rates, scores, h).sum()) - 2.50) < 1e-12 assert guidance.reachable(100, 21) == 2001 def test_guidance_strength_and_classifier_free(): p1 = torch.tensor([0.1, 0.2, 0.3, 0.4]) rates, scores = p1 / 0.4, torch.tensor([0.10, 0.35, 0.55, 0.25]) harder = guidance.tilt(rates, scores, 0.20, gamma=2.0) close(harder.tolist(), [0.0625, 1.5312, 5.6719, 1.5625], 1e-3) assert abs(float(harder.sum()) - 8.828) < 1e-3 close((harder / harder.sum()).tolist(), [0.007, 0.173, 0.642, 0.177], 1e-3) mixed = guidance.classifier_free(guidance.tilt(rates, scores, 0.20), rates, gamma=2.0) assert torch.allclose(mixed, harder, atol=1e-12) def test_taylor_expansion(): gradient = torch.tensor([-2.10, -1.15, -0.75, -1.30, -1.61]) approximate = guidance.taylor_log_ratio(gradient, guidance.MASK)[:4] close(approximate.tolist(), [-0.49, 0.46, 0.86, 0.31]) exact = (torch.tensor([0.10, 0.35, 0.55, 0.25]) / 0.20).log() close(exact.tolist(), [-0.693, 0.560, 1.012, 0.223], 1e-3) rates = torch.tensor([0.25, 0.50, 0.75, 1.00]) * approximate.exp() close((rates / rates.sum()).tolist(), [0.038, 0.194, 0.434, 0.334], 1e-3) def test_guided_step_never_remasks_a_visible_position(): """The denoiser in the sampler is a distribution over the data tokens, the mask excluded.""" torch.manual_seed(0) model = SequenceDenoiser(guidance.V, 8, width=32, depth=1) prop = guidance.PropertyModel(8, width=32) xt = torch.randint(0, 4, (16, 8)) # Every position already holds a base. stepped = guidance.guided_step(model, prop, xt, torch.full((16,), 0.9), 0.5, gamma=1.0) assert int((stepped == guidance.MASK).sum()) == 0 # A revealed token never returns to the mask. # %% Sections 5.30 to 5.32: several objectives at once def test_pareto_front(): scores = torch.tensor([[0.8, 0.3], [0.6, 0.7], [0.5, 0.2], [0.6, 0.3]]) assert objectives.pareto_front(scores) == [0, 1] # The third and fourth are dominated. close((scores @ torch.tensor([0.6, 0.4]))[:2].tolist(), [0.60, 0.64]) close((scores @ torch.tensor([0.8, 0.2]))[:2].tolist(), [0.70, 0.62]) # The order reverses. def test_mogdfm_step(): changes = torch.tensor([[0.0, 0.0], [0.20, -0.05], [0.05, 0.15], [-0.10, 0.08]]) delta_s, ranks, direction = objectives.combined_score( changes, torch.tensor([1.2, 0.8]), torch.tensor([0.6, 0.4])) close(ranks.tolist(), [0.500, 0.700, 0.850, 0.450]) close(direction.tolist(), [0.0, 0.100, 0.090, -0.028]) close(delta_s.tolist(), [-1.511, 1.541, 2.297, -2.327], 1e-3) close(delta_s.exp().tolist(), [0.221, 4.667, 9.948, 0.098], 1e-2) base = torch.tensor([0.3, 0.3, 0.1]) / 0.3 # The pretrained rates out of A toward C, G and T. tilted = base * delta_s[1:].exp() close(tilted.tolist(), [4.667, 9.948, 0.033], 1e-2) angles, cosines = objectives.cone_angles(changes[1:], torch.tensor([0.6, 0.4])) close(cosines.tolist(), [0.6727, 0.7894, -0.3032], 5e-4) close(angles.tolist(), [47.7, 37.9, 107.7], 0.05) survivors = tilted[angles <= 60.0] close((survivors / survivors.sum()).tolist(), [0.319, 0.681], 1e-3) assert abs(objectives.update_cone(60.0, 1 / 3) - 64.1) < 0.05 def test_pcomole_rollout(): omega, reference = torch.tensor([0.5, 0.5]), torch.zeros(2) first = [torch.tensor(s) for s in ((0.8, 0.4), (0.6, 0.6), (0.9, 0.1), (0.5, 0.5))] value, rewards = objectives.rollout_value(first, [1, 1, 0, 1], omega, reference, 2.0) close(rewards, [1.682, 2.054, 0.0, 1.822], 1e-3) assert abs(value - 1.390) < 1e-3 second = [torch.tensor(s) for s in ((1.0, 0.0), (0.9, 0.1), (0.95, 0.05), (1.0, 0.1))] other, _ = objectives.rollout_value(second, [1, 1, 1, 1], omega, reference, 2.0) assert abs(other - 1.181) < 1e-3 products = torch.tensor([0.6 * value, 1.2 * other, 0.0]) close(products.tolist(), [0.834, 1.417, 0.0], 1e-3) close((products / products.sum()).tolist(), [0.371, 0.629, 0.0], 1e-3) # %% Sections 5.36 to 5.41: rectification and AReUReDi def test_total_correlation_on_two_bases(): source = torch.full((rectify.STATES,), 1.0 / rectify.STATES) q, support = rectify.complementary_pairs() coupling = q[None, :].repeat(rectify.STATES, 1) assert abs(rectify.total_correlation(coupling, source) - math.log(4)) < 1e-9 assert abs(rectify.hamming(coupling, source) - 1.5) < 1e-9 assert abs(rectify.one_step_success(coupling, source, support) - 0.25) < 1e-9 def test_redi_rounds(): source = torch.full((rectify.STATES,), 1.0 / rectify.STATES) q, support = rectify.complementary_pairs() coupling = q[None, :].repeat(rectify.STATES, 1) coupling = rectify.recouple(coupling, source, steps=300) assert abs(rectify.total_correlation(coupling, source) - 1.101) < 2e-3 assert abs(rectify.hamming(coupling, source) - 1.022) < 2e-3 assert abs(rectify.one_step_success(coupling, source, support) - 0.403) < 2e-3 columns = [a * 4 + b for a, b in support] close([float(coupling[0, c]) for c in columns], [0.400, 0.400, 0.100, 0.100], 2e-3) close([float(coupling[3, c]) for c in columns], [0.755, 0.082, 0.082, 0.082], 2e-3) coupling = rectify.recouple(coupling, source, steps=300) assert abs(rectify.total_correlation(coupling, source) - 0.843) < 2e-3 assert abs(rectify.one_step_success(coupling, source, support) - 0.525) < 2e-3 def test_areuredi_proposal_and_acceptance(): scores = torch.tensor([[0.60, 0.80], [0.70, 0.76], [0.50, 0.90], [0.64, 0.62]]) weights = torch.tensor([0.5, 0.5]) assert abs(rectify.anneal(11, 20) - 12.0) < 1e-12 tchebycheff = rectify.tchebycheff(scores, weights) close(tchebycheff.tolist(), [0.300, 0.350, 0.250, 0.310]) tilts = (12.0 * tchebycheff).exp() close(tilts.tolist(), [36.60, 66.69, 20.09, 41.26], 5e-2) model = torch.tensor([0.40, 0.25, 0.20, 0.15]) q_current, z_current, ratios = rectify.proposal(model, tilts, 0) close(ratios.tolist(), [1.000, 1.822, 0.549, 1.127], 1e-3) close(rectify.barker(ratios).tolist(), [0.500, 0.646, 0.354, 0.530], 1e-3) assert abs(z_current - 0.5118) < 5e-4 close(q_current.tolist(), [0.391, 0.315, 0.138, 0.155], 1e-3) _, z_proposed, _ = rectify.proposal(model, tilts, 1) assert abs(z_proposed - 0.3704) < 5e-4 assert abs(rectify.acceptance(z_current, z_proposed) - 1.0) < 1e-12 assert abs(rectify.acceptance(z_proposed, z_current) - 0.724) < 1e-3 def test_areuredi_concentration(): base = torch.tensor([0.50, 0.25, 0.15, 0.10]) objective = torch.tensor([0.20, 0.30, 0.45, 0.45]) tilted = rectify.tilted_distribution(base, objective, 20.0) close(tilted.tolist(), [0.0127, 0.0468, 0.5643, 0.3762], 1e-3) assert abs(float(tilted[2:].sum()) - 0.9405) < 1e-3 bound, gap = rectify.concentration_bound(base, objective, 20.0) assert abs(gap - 0.15) < 1e-9 assert abs(bound * float(base[1]) - 0.0498) < 5e-4 assert bound * float(base[1]) >= float(tilted[1]) sharper = rectify.tilted_distribution(base, objective, 60.0) assert abs(float(sharper[2:].sum()) - 0.9999) < 1e-3 assert abs(float(sharper[1]) - 1.234e-4) < 1e-6 # %% The command line of every file @pytest.mark.parametrize("script", [ "velocity", "kinetic_optimal", "rectify", "edit_flows", "multi_objective", ]) def test_script_runs(script): """The five files that need no training run end to end at the smallest grid.""" result = subprocess.run( [sys.executable, f"lecture_5/{script}.py", "--steps", "20", "--quiet"], cwd=ROOT, capture_output=True, text=True, env={**os.environ, "PYTHONPATH": str(ROOT)}, ) assert result.returncode == 0, result.stderr[-2000:] assert "nan" not in result.stdout.lower()