CIS6270 / tests /test_lecture_7.py
pranamanam's picture
Upload 87 files
0ba9d09 verified
Raw History Blame Contribute Delete
34.6 kB
"""Lecture 7: every number the chapter works by hand, asserted against the file that prints it.
Chapter 7 computes almost all of its results on paper, so these tests are the chapter itself read
back. The tolerances are the number of digits the chapter quotes.
"""
from __future__ import annotations
import importlib.util
import math
import sys
from pathlib import Path
import pytest
import torch
LECTURE = Path(__file__).resolve().parent.parent / "lecture_7"
def load(name: str):
"""Import one lecture file by path, since ``lecture_7`` is a folder of scripts, not a package."""
spec = importlib.util.spec_from_file_location(f"lecture_7_{name}", LECTURE / f"{name}.py")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
def tensor(*values):
return torch.tensor(values, dtype=torch.float64)
# %% Sections 7.1 to 7.6, optimal_transport.py
def test_four_points_and_two_pairings():
ot = load("optimal_transport")
pairings = ot.two_pairings()
assert pairings["arrows_ordered"] == pytest.approx(2.0)
assert pairings["arrows_crossed"] == pytest.approx(10.0)
assert pairings["mass_ordered"] == pytest.approx(1.0)
assert pairings["mass_crossed"] == pytest.approx(5.0)
assert pairings["mass_independent"] == pytest.approx(3.0)
assert pairings["C"].tolist() == [[1.0, 9.0], [1.0, 1.0]]
def test_two_by_two_plan_and_prices():
ot = load("optimal_transport")
assert ot.feasible_interval() == (0.0, 0.3)
C = ot.cost_matrix(ot.SOURCE_X, ot.TARGET_Y)
assert ot.cost_along_the_segment(0.0, C) == pytest.approx(5.8)
assert ot.cost_along_the_segment(0.3, C) == pytest.approx(3.4)
plan = ot.feasible_plan(0.3)
assert plan.tolist() == [[0.3, 0.3], [0.0, 0.4]]
assert ot.is_coupling(plan, ot.SOURCE_A, ot.TARGET_B)
f, g = ot.prices_from_loaded_routes(C, plan)
assert f.tolist() == [0.0, -8.0] and g.tolist() == [1.0, 9.0]
assert ot.slack(C, f, g).tolist() == [[0.0, 0.0], [8.0, 0.0]]
assert ot.dual_value(ot.SOURCE_A, f, ot.TARGET_B, g) == pytest.approx(3.4)
assert ot.plan_cost(C, plan) ** 0.5 == pytest.approx(1.844, abs=5e-4)
def test_no_crossing_and_three_points():
ot = load("optimal_transport")
expanded, collected = ot.crossing_difference(0.0, 2.0, 1.0, 3.0)
assert expanded == pytest.approx(collected) and collected == pytest.approx(-8.0)
x = tensor(4.0, 1.0, 6.0)
y = tensor(9.0, 2.0, 3.0)
costs = [c for c, _ in ot.monge_permutations(x.sort().values, y.sort().values)]
assert costs == pytest.approx([11 / 3, 17 / 3, 35 / 3, 45 / 3, 77 / 3, 81 / 3])
assert ot.wasserstein_on_the_line(x, y, 2.0, 3072) == pytest.approx(11 / 3)
def test_gaussian_closed_form():
ot = load("optimal_transport")
assert ot.gaussian_w2_squared(0.0, 1.0, 3.0, 2.0) == pytest.approx(10.0)
assert math.sqrt(10.0) == pytest.approx(3.162, abs=5e-4)
points = tensor(-1.0, 0.0, 1.0)
assert ot.brenier_map(points, 0.0, 1.0, 3.0, 2.0).tolist() == [1.0, 3.0, 5.0]
source = torch.special.ndtri(ot.quantile_levels(3072))
mapped = ot.brenier_map(source, 0.0, 1.0, 3.0, 2.0)
assert float(((mapped - source) ** 2).mean()) == pytest.approx(10.0, abs=1e-3)
# %% Sections 7.9 and 7.10, sinkhorn.py
def test_entropies_of_the_three_couplings():
sk = load("sinkhorn")
independent = sk.independent_coupling(sk.SOURCE_A, sk.TARGET_B)
assert independent.flatten().tolist() == pytest.approx([0.18, 0.42, 0.12, 0.28])
assert sk.entropy(sk.EXACT_PLAN) == pytest.approx(1.089, abs=5e-4)
assert sk.entropy(independent) == pytest.approx(1.284, abs=5e-4)
assert sk.entropy(sk.SOURCE_A) == pytest.approx(0.673, abs=5e-4)
assert sk.entropy(sk.TARGET_B) == pytest.approx(0.611, abs=5e-4)
assert sk.relative_entropy(sk.EXACT_PLAN, independent) == pytest.approx(0.195, abs=5e-4)
def test_sinkhorn_first_sweep_by_hand():
sk = load("sinkhorn")
K = sk.gibbs_kernel(sk.COST, 2.0)
assert K.flatten().tolist() == pytest.approx([0.606531, 0.011109, 0.606531, 0.606531],
abs=5e-7)
v = torch.ones(2, dtype=torch.float64)
assert (K @ v).tolist() == pytest.approx([0.617640, 1.213061], abs=5e-7)
u = sk.SOURCE_A / (K @ v)
assert u.tolist() == pytest.approx([0.971440, 0.329744], abs=5e-7)
assert (K.T @ u).tolist() == pytest.approx([0.789208, 0.210792], abs=5e-7)
v = sk.TARGET_B / (K.T @ u)
assert v.tolist() == pytest.approx([0.38013, 3.32081], abs=5e-6)
plan = sk.scaling_form(u, K, v)
assert plan.flatten().tolist() == pytest.approx([0.2240, 0.0358, 0.0760, 0.6642], abs=5e-5)
assert plan.sum(dim=0).tolist() == pytest.approx([0.3, 0.7])
assert plan.sum(dim=1).tolist() == pytest.approx([0.2598, 0.7402], abs=5e-5)
after = sk.project_rows(plan, sk.SOURCE_A)
assert after.sum(dim=0).tolist() == pytest.approx([0.558, 0.442], abs=5e-4)
def test_sinkhorn_convergence_and_epsilon():
sk = load("sinkhorn")
K = sk.gibbs_kernel(sk.COST, 2.0)
u, v, errors = sk.sinkhorn_sweeps(K, sk.SOURCE_A, sk.TARGET_B, 40)
assert errors[:5] == pytest.approx([0.680, 0.382, 0.155, 0.047, 0.012], abs=5e-4)
assert sk.sweeps_to_tolerance(K, sk.SOURCE_A, sk.TARGET_B) == 17
plan = sk.scaling_form(u, K, v)
assert plan.flatten().tolist() == pytest.approx(
[0.293122, 0.306878, 0.006878, 0.393122], abs=5e-7)
assert float((sk.COST * plan).sum()) == pytest.approx(3.45502, abs=5e-6)
assert sk.entropy(plan) == pytest.approx(1.124, abs=5e-4)
independent = sk.independent_coupling(sk.SOURCE_A, sk.TARGET_B)
assert sk.entropic_objective(plan, sk.COST, 2.0) == pytest.approx(1.208, abs=5e-4)
assert sk.entropic_objective(sk.EXACT_PLAN, sk.COST, 2.0) == pytest.approx(1.222, abs=5e-4)
assert sk.entropic_objective(independent, sk.COST, 2.0) == pytest.approx(1.792, abs=5e-4)
assert sk.relative_entropy(plan, independent) == pytest.approx(0.160, abs=5e-4)
f = sk.multipliers_from_scalings(u, 2.0)
g = sk.multipliers_from_scalings(v, 2.0)
assert f.tolist() == pytest.approx([3.766, -3.739], abs=5e-4)
assert g.tolist() == pytest.approx([-3.220, 4.872], abs=5e-4)
expected = {10.0: (4.0540165, 7), 2.0: (3.4550204, 17), 0.5: (3.4000004, 28),
0.1: (3.4000000, 80)}
for epsilon, (cost, sweeps) in expected.items():
kernel = sk.gibbs_kernel(sk.COST, epsilon)
su, sv, _ = sk.sinkhorn_sweeps(kernel, sk.SOURCE_A, sk.TARGET_B, 400)
assert float((sk.COST * sk.scaling_form(su, kernel, sv)).sum()) == pytest.approx(
cost, abs=5e-8)
assert sk.sweeps_to_tolerance(kernel, sk.SOURCE_A, sk.TARGET_B) == sweeps
# %% Sections 7.7 to 7.7.5, displacement.py
def test_one_particle_and_the_averaged_bound():
dp = load("displacement")
times = torch.linspace(0.0, 1.0, 1025, dtype=torch.float64)
assert dp.action_of_a_path(dp.steady_particle(times), times) == pytest.approx(4.0)
assert dp.action_of_a_path(dp.dawdling_particle(times), times) == pytest.approx(8.0)
x, y = tensor(0.0, 2.0), tensor(1.0, 3.0)
ordered = torch.tensor([[0.5, 0.0], [0.0, 0.5]], dtype=torch.float64)
crossed = torch.tensor([[0.0, 0.5], [0.5, 0.0]], dtype=torch.float64)
independent = torch.full((2, 2), 0.25, dtype=torch.float64)
assert dp.average_squared_displacement(x, y, ordered) == pytest.approx(1.0)
assert dp.average_squared_displacement(x, y, independent) == pytest.approx(3.0)
assert dp.average_squared_displacement(x, y, crossed) == pytest.approx(5.0)
def test_displacement_interpolation_and_its_velocity():
dp = load("displacement")
mean, std = dp.displacement_law(0.5)
assert (mean, std) == pytest.approx((1.5, 1.5))
assert dp.gaussian_w2(0.0, 1.0, mean, std) == pytest.approx(math.sqrt(2.5))
assert math.sqrt(2.5) == pytest.approx(1.581, abs=5e-4)
assert dp.gaussian_w2(0.0, 1.0, 3.0, 2.0) == pytest.approx(3.162, abs=5e-4)
grid = torch.linspace(-6.0, 9.0, 15001, dtype=torch.float64)
peak = float(grid[int(dp.mixture_density(grid).argmax())])
assert peak == pytest.approx(0.13, abs=5e-3)
half = torch.tensor(0.5, dtype=torch.float64)
assert float(dp.velocity_field(torch.tensor(1.5, dtype=torch.float64), half)) == pytest.approx(3.0)
assert float(dp.velocity_field(torch.tensor(3.0, dtype=torch.float64), half)) == pytest.approx(4.0)
for t in (0.0, 0.25, 0.5, 0.75, 1.0):
assert dp.action_density(t, 4096) == pytest.approx(10.0, abs=1e-3)
assert abs(dp.continuity_residual(1.5, 0.5)) < 1e-12
# %% Sections 7.8 and 7.8.1, ot_flow_matching.py
def test_batch_coupling_costs():
fm = load("ot_flow_matching")
x = torch.tensor([[0.0], [2.0]])
y = torch.tensor([[1.0], [3.0]])
C = fm.batch_cost_matrix(x, y)
assert C.flatten().tolist() == pytest.approx([1.0, 9.0, 1.0, 1.0])
assert float(C.mean()) == pytest.approx(3.0)
pairing = fm.optimal_pairing(x, y)
assert pairing.tolist() == [0, 1]
assert float(C[torch.arange(2), pairing].mean()) == pytest.approx(1.0)
assert fm.vertex_count(256, 256) == {"unknowns": 65536, "constraints": 512,
"loaded_routes": 511}
assert fm.conditional_velocity_energy(x, fm.couple(x, y, "optimal")) == pytest.approx(1.0)
def test_flow_matching_pipeline_runs():
fm = load("ot_flow_matching")
from cis6270.nets import VelocityMLP
torch.manual_seed(0)
model = VelocityMLP(dim=2, width=16)
x0 = torch.randn(8, 2)
x1 = torch.randn(8, 2)
assert float(fm.flow_matching_loss(model, x0, fm.couple(x0, x1, "optimal"))) > 0.0
samples, straightness = fm.integrate(model, x0, 5)
assert samples.shape == (8, 2) and straightness >= 0.0
# %% Sections 7.12 to 7.15.3, static_bridge.py
def test_eight_paths_and_the_chain_rule():
sb = load("static_bridge")
law = sb.path_law(sb.PI_0, [sb.A, sb.A])
assert law[(0, 0, 1)] == pytest.approx(0.105)
assert law[(0, 0, 0)] == pytest.approx(0.245)
assert law[(1, 1, 1)] == pytest.approx(0.180)
assert sum(law.values()) == pytest.approx(1.0)
table = torch.tensor([[0.5, 0.1], [0.1, 0.3]], dtype=torch.float64)
uniform = torch.full((2, 2), 0.25, dtype=torch.float64)
total, marginal, conditional = sb.chain_rule_terms(table, uniform)
assert total == pytest.approx(0.2180, abs=5e-5)
assert marginal == pytest.approx(0.0201, abs=5e-5)
assert conditional == pytest.approx(0.1979, abs=5e-5)
assert marginal + conditional == pytest.approx(total)
def test_proportional_fitting_on_the_chain():
sb = load("static_bridge")
R01 = sb.reference_endpoint_law(sb.PI_0, sb.A, 2)
assert R01.flatten().tolist() == pytest.approx([0.305, 0.195, 0.26, 0.24])
assert float((R01.sum(dim=0) - sb.PI_1).abs().sum()) == pytest.approx(0.73)
f, g, errors = sb.proportional_fitting(R01, sb.PI_0, sb.PI_1, 4)
assert errors == pytest.approx([3.933e-3, 2.098e-5, 1.119e-7, 5.966e-10], rel=1e-3)
assert f.tolist() == pytest.approx([1.072002, 0.937019], abs=5e-7)
assert g.tolist() == pytest.approx([0.350517, 1.843637], abs=5e-7)
plan = f[:, None] * R01 * g[None, :]
assert plan.flatten().tolist() == pytest.approx(
[0.114605, 0.385395, 0.085395, 0.414605], abs=5e-7)
assert sb.kl_vectors(plan.flatten(), R01.flatten()) == pytest.approx(0.2820, abs=5e-5)
independent = sb.PI_0[:, None] * sb.PI_1[None, :]
assert sb.kl_vectors(independent.flatten(), R01.flatten()) == pytest.approx(0.2847, abs=5e-5)
def test_h_transform_and_the_bridge_path_law():
sb = load("static_bridge")
R01 = sb.reference_endpoint_law(sb.PI_0, sb.A, 2)
f, g, _ = sb.proportional_fitting(R01, sb.PI_0, sb.PI_1, 40)
potentials = sb.backward_potential(sb.A, g, 2)
assert potentials[2].tolist() == pytest.approx([0.350517, 1.843637], abs=5e-7)
assert potentials[1].tolist() == pytest.approx([0.798453, 1.246389], abs=5e-7)
assert potentials[0].tolist() == pytest.approx([0.932834, 1.067215], abs=5e-7)
tilted = [sb.tilted_matrix(sb.A, potentials[k], potentials[k + 1]) for k in range(2)]
assert tilted[0].flatten().tolist() == pytest.approx(
[0.599160, 0.400840, 0.299266, 0.700734], abs=5e-7)
assert tilted[1].flatten().tolist() == pytest.approx(
[0.307297, 0.692703, 0.112490, 0.887510], abs=5e-7)
assert tilted[0].sum(dim=1).tolist() == pytest.approx([1.0, 1.0])
middle = sb.PI_0 @ tilted[0]
assert middle.tolist() == pytest.approx([0.449213, 0.550787], abs=5e-7)
assert (middle @ tilted[1]).tolist() == pytest.approx([0.2, 0.8])
assert (1.0 / potentials[0]).tolist() == pytest.approx(f.tolist(), abs=1e-9)
plan = f[:, None] * R01 * g[None, :]
bridge = sb.fill_with_reference_bridges(plan, sb.A)
reference = sb.path_law(sb.PI_0, [sb.A, sb.A])
assert bridge[(0, 0, 0)] == pytest.approx(0.092060, abs=5e-7)
assert bridge[(0, 0, 1)] == pytest.approx(0.207520, abs=5e-7)
assert bridge[(1, 1, 1)] == pytest.approx(0.310954, abs=5e-7)
ratios = {round(bridge[p] / reference[p], 6) for p in reference}
assert ratios == {0.375755, 1.976383, 0.328441, 1.727522}
assert sb.relative_entropy(bridge, reference) == pytest.approx(0.281961, abs=5e-7)
assert sum(v for k, v in bridge.items() if k[1] == 0) == pytest.approx(0.449213, abs=5e-7)
def test_brownian_reference_recovers_the_entropic_plan():
sb = load("static_bridge")
plan = sb.brownian_reference_plan(1.0)
assert plan.flatten().tolist() == pytest.approx([0.2931, 0.3069, 0.0069, 0.3931], abs=5e-5)
# %% Sections 7.16 to 7.18, dynamic_bridge.py
def test_constant_control_two_ways():
db = load("dynamic_bridge")
assert db.control_energy_entropy(2.0, 1.0) == pytest.approx(2.0)
assert db.gaussian_entropy_gap(2.0, 1.0) == pytest.approx(2.0)
generator = torch.Generator().manual_seed(0)
_, log_ratio = db.simulate_controlled(2.0, 200, 8192, 1.0, generator)
assert float(log_ratio.mean()) == pytest.approx(2.0, abs=0.1)
def test_the_bridge_potential_solves_both_equations():
db = load("dynamic_bridge")
value = db.hopf_cole(db.bridge_potential)
for x, t in ((-0.5, 0.2), (0.5, 0.5), (2.5, 0.8)):
assert abs(db.backward_residual(db.bridge_potential, x, t)) < 1e-12
assert abs(db.hjb_residual(value, x, t)) < 1e-12
drift = float(db.bridge_drift(torch.tensor(x, dtype=torch.float64),
torch.tensor(t, dtype=torch.float64)))
assert db.optimal_control(value, x, t) == pytest.approx(drift, abs=1e-10)
def test_brownian_bridge_numbers():
db = load("dynamic_bridge")
mean, variance = db.bridge_law(0.25)
assert mean == pytest.approx(0.5)
assert variance == pytest.approx(0.1875)
assert math.sqrt(variance) == pytest.approx(0.4330, abs=5e-5)
drift = float(db.bridge_drift(torch.tensor(0.6732, dtype=torch.float64),
torch.tensor(0.25, dtype=torch.float64)))
assert drift == pytest.approx(1.7691, abs=5e-5)
assert db.bridge_law(0.5)[1] == pytest.approx(0.25)
for epsilon, spread in ((0.25, 0.25), (1.0, 0.5), (4.0, 1.0)):
assert math.sqrt(db.bridge_law(0.5, epsilon=epsilon)[1]) == pytest.approx(spread)
# %% Section 7.19.1, dsb.py
def test_dsb_half_steps_on_the_chain():
dsb = load("dsb")
reference = dsb.reference_path_law(dsb.PI_0, dsb.A, 2)
assert dsb.marginal(reference, 1).tolist() == pytest.approx([0.55, 0.45])
assert dsb.marginal(reference, 2).tolist() == pytest.approx([0.565, 0.435])
B2 = dsb.reverse_step(reference, 1)
assert B2.flatten().tolist() == pytest.approx(
[0.681416, 0.318584, 0.379310, 0.620690], abs=5e-7)
assert float(dsb.reverse_step(reference, 0)[0, 0]) == pytest.approx(0.636364, abs=5e-7)
half = dsb.fix_terminal(reference, dsb.PI_1)
assert dsb.marginal(half, 2).tolist() == pytest.approx([0.2, 0.8])
assert dsb.marginal(half, 1).tolist() == pytest.approx([0.439731, 0.560269], abs=5e-7)
assert dsb.marginal(half, 0).tolist() == pytest.approx([0.466585, 0.533415], abs=5e-7)
assert float((dsb.marginal(half, 0) - dsb.PI_0).abs().sum()) == pytest.approx(0.0668, abs=5e-5)
full = dsb.fix_initial(half, dsb.PI_0)
assert dsb.one_step_conditional(full, 0).flatten().tolist() == pytest.approx(
[0.599738, 0.400262, 0.299771, 0.700229], abs=5e-7)
assert dsb.one_step_conditional(full, 1).flatten().tolist() == pytest.approx(
[0.309924, 0.690076, 0.113725, 0.886275], abs=5e-7)
assert dsb.marginal(full, 2).tolist() == pytest.approx([0.201967, 0.798033], abs=5e-7)
gap = max(float((dsb.one_step_conditional(full, 0) - dsb.TILTED_0).abs().max()),
float((dsb.one_step_conditional(full, 1) - dsb.TILTED_1).abs().max()))
assert gap < 0.003
_, errors, _ = dsb.proportional_fitting(reference, dsb.PI_0, dsb.PI_1, 4)
assert errors == pytest.approx([3.933e-3, 2.098e-5, 1.119e-7, 5.966e-10], rel=1e-3)
assert errors[0] / errors[1] == pytest.approx(187.5, abs=0.1)
# %% Section 7.19.2, dsbm.py
def test_markov_drift_at_one_point():
dm = load("dsbm")
assert dm.bridge_drift(3.0, 1.0, 0.5) == pytest.approx(4.0)
assert dm.bridge_drift(0.0, 1.0, 0.5) == pytest.approx(-2.0)
endpoints = tensor(3.0, 0.0)
weights = tensor(0.5, 0.5)
assert dm.markov_drift(endpoints, weights, 1.0, 0.5) == pytest.approx(1.0)
assert dm.least_squares_derivative(1.0, endpoints, weights, 1.0, 0.5) == pytest.approx(0.0)
assert dm.least_squares_derivative(0.0, endpoints, weights, 1.0, 0.5) == pytest.approx(-2.0)
def test_one_imf_round_on_the_chain():
dm = load("dsbm")
bridges = dm.reference_bridge_table(dm.A)
assert float(bridges[0, 0, 0]) == pytest.approx(0.803279, abs=5e-7)
assert float(bridges[0, 0, 1]) == pytest.approx(0.538462, abs=5e-7)
target = dm.bridge_coupling(dm.A, dm.PI_0, dm.PI_1)
independent = dm.PI_0[:, None] * dm.PI_1[None, :]
assert independent.flatten().tolist() == pytest.approx([0.1, 0.4, 0.1, 0.4])
assert float((independent - target).abs().sum()) == pytest.approx(0.0584, abs=5e-5)
coupling, gaps, first = dm.imf_rounds(independent, dm.A, 9, target)
assert first[0].flatten().tolist() == pytest.approx(
[0.591425, 0.408575, 0.307692, 0.692308], abs=5e-7)
assert first[1].flatten().tolist() == pytest.approx(
[0.298457, 0.701543, 0.119588, 0.880412], abs=5e-7)
projected = torch.diag(dm.PI_0) @ first[0] @ first[1]
assert projected.flatten().tolist() == pytest.approx(
[0.112688, 0.387312, 0.087312, 0.412688], abs=5e-7)
assert projected.sum(dim=0).tolist() == pytest.approx([0.2, 0.8])
assert projected.sum(dim=1).tolist() == pytest.approx([0.5, 0.5])
assert gaps[0] == pytest.approx(0.0077, abs=5e-5)
assert gaps[3] / gaps[4] == pytest.approx(7.37, abs=5e-3)
assert gaps[8] < 1e-9
# %% Section 7.19.3, sf2m.py
def test_the_two_regression_targets():
sm = load("sf2m")
x0, x1 = tensor(0.0)[0], tensor(2.0)[0]
z = tensor(0.4)[0]
for t_value, mean, sample, drift, flow, coefficient in (
(0.25, 0.5, 0.6732, 1.7691, 2.2309, 4 / 3),
(0.75, 1.5, 1.6732, 1.3072, 1.7691, -4 / 3),
):
t = torch.tensor(t_value, dtype=torch.float64)
assert float(sm.bridge_mean(x0, x1, t)) == pytest.approx(mean)
assert float(sm.bridge_std(t)) == pytest.approx(0.4330, abs=5e-5)
x = sm.bridge_sample(x0, x1, t, z)
assert float(x) == pytest.approx(sample, abs=5e-5)
assert float(sm.conditional_drift(x1, x, t)) == pytest.approx(drift, abs=5e-5)
assert float(sm.conditional_score(x0, x1, x, t)) == pytest.approx(-0.9238, abs=5e-5)
assert float(sm.flow_velocity(x0, x1, x, t)) == pytest.approx(flow, abs=5e-5)
assert float(sm.flow_velocity_collected(x0, x1, x, t)) == pytest.approx(flow, abs=5e-5)
assert float(sm.flow_coefficient(t)) == pytest.approx(coefficient)
assert float(sm.scaled_score(x0, x1, x, t)) == pytest.approx(-0.4, abs=5e-7)
def test_the_batch_plan_of_sf2m():
sm = load("sf2m")
x, y = tensor(0.0, 2.0), tensor(1.0, 3.0)
uniform = tensor(0.5, 0.5)
plan = sm.entropic_plan(x, y, uniform, uniform, 1.0)
assert plan.sum(dim=0).tolist() == pytest.approx([0.5, 0.5])
assert float(plan[0, 0] + plan[1, 1]) > float(plan[0, 1] + plan[1, 0])
# %% Sections 7.21 to 7.23.3, discrete_bridge.py
def test_rate_error_is_not_symmetric():
db = load("discrete_bridge")
assert db.scaling_cost(0.5) == pytest.approx(0.1534, abs=5e-5)
assert db.scaling_cost(2.0) == pytest.approx(0.3863, abs=5e-5)
assert float(db.rate_error(tensor(1.0)[0], tensor(1.0)[0])) == pytest.approx(0.0)
def test_two_state_continuous_time_bridge():
db = load("discrete_bridge")
assert torch.matrix_exp(db.Q).flatten().tolist() == pytest.approx(
[0.6328, 0.3672, 0.5507, 0.4493], abs=5e-5)
R01 = db.endpoint_law(db.Q, db.PI_0)
assert R01.flatten().tolist() == pytest.approx([0.3164, 0.1836, 0.2754, 0.2246], abs=5e-5)
assert R01.sum(dim=0).tolist() == pytest.approx([0.5918, 0.4082], abs=5e-5)
f, g, plan = db.sinkhorn(R01, db.PI_0, db.PI_1)
assert f.tolist() == pytest.approx([1.0716, 0.9373], abs=5e-5)
assert g.tolist() == pytest.approx([0.3349, 1.9643], abs=5e-5)
assert float(g[1] / g[0]) == pytest.approx(5.865, abs=5e-4)
assert plan.flatten().tolist() == pytest.approx([0.1136, 0.3864, 0.0864, 0.4136], abs=5e-5)
assert torch.matrix_exp(db.Q / 2).flatten().tolist() == pytest.approx(
[0.7146, 0.2854, 0.4281, 0.5719], abs=5e-5)
phi = db.backward_potential(db.Q, g, 0.5)
assert phi.tolist() == pytest.approx([0.7999, 1.2667], abs=5e-5)
tilted = db.tilted_generator(db.Q, phi)
assert tilted.flatten().tolist() == pytest.approx(
[-1.5836, 1.5836, 0.9472, -0.9472], abs=5e-5)
assert db.backward_potential(db.Q, g, 0.0).tolist() == pytest.approx([0.9331, 1.0669], abs=5e-5)
at_zero = db.tilted_generator(db.Q, db.backward_potential(db.Q, g, 0.0))
assert float(at_zero[0, 1]) == pytest.approx(1.1433, abs=5e-5)
assert float(at_zero[1, 0]) == pytest.approx(1.3120, abs=5e-5)
at_one = db.tilted_generator(db.Q, g)
assert float(at_one[0, 1]) == pytest.approx(5.8652, abs=5e-5)
assert float(at_one[1, 0]) == pytest.approx(0.2557, abs=5e-5)
marginal = db.bridge_marginal(db.Q, db.PI_0, f, g, 0.5)
assert marginal.tolist() == pytest.approx([0.4668, 0.5332], abs=5e-5)
assert db.bridge_marginal(db.Q, db.PI_0, f, g, 1.0).tolist() == pytest.approx([0.2, 0.8])
reverse = db.reverse_rates(tilted, marginal)
assert float(reverse[1, 0]) == pytest.approx(1.3862, abs=5e-5)
assert float(reverse[0, 1]) == pytest.approx(1.0821, abs=5e-5)
assert float(marginal[0] * tilted[0, 1]) == pytest.approx(
float(marginal[1] * reverse[1, 0]), abs=5e-5)
def test_euler_sampler_and_the_cost_computed_twice():
db = load("discrete_bridge")
R01 = db.endpoint_law(db.Q, db.PI_0)
f, g, plan = db.sinkhorn(R01, db.PI_0, db.PI_1)
errors = {}
for steps, expected in ((10, 0.0223), (20, 0.0106), (40, 0.0051), (100, 0.0021)):
terminal, leaving = db.euler_marginal(db.Q, db.PI_0, f, g, steps)
errors[steps] = float((terminal - db.PI_1).abs().sum() / 2)
assert errors[steps] == pytest.approx(expected, abs=1e-4)
if steps == 10:
assert leaving[:3] == pytest.approx([0.1143, 0.1187, 0.1245], abs=5e-5)
assert leaving[-1] == pytest.approx(0.3649, abs=5e-5)
assert db.relative_entropy(plan, R01) == pytest.approx(0.3235, abs=5e-5)
integral, at_half = db.path_cost(db.Q, db.PI_0, f, g)
assert at_half == pytest.approx(0.1300, abs=5e-5)
assert integral == pytest.approx(0.3235, abs=5e-5)
expected = {0.5: 0.3148, 1.0: 0.3235, 2.0: 0.3337, 4.0: 0.3348}
for lam, value in expected.items():
law = db.endpoint_law(lam * db.Q, db.PI_0)
_, _, scaled = db.sinkhorn(law, db.PI_0, db.PI_1)
assert db.relative_entropy(scaled, law) == pytest.approx(value, abs=5e-5)
assert db.relative_entropy(db.PI_1, db.STATIONARY) == pytest.approx(0.3348, abs=5e-5)
# %% Section 7.24, ddsbm.py
def test_ddsbm_single_bond():
dd = load("ddsbm")
kernel = dd.replacement_kernel(0.4, dd.PRIOR)
assert kernel.flatten().tolist() == pytest.approx(
[0.6, 0.2, 0.2, 0.2, 0.6, 0.2, 0.2, 0.2, 0.6])
reference = torch.diag(dd.BOND_0) @ kernel
assert reference.sum(dim=0).tolist() == pytest.approx([0.48, 0.28, 0.24])
assert (dd.BOND_1 / reference.sum(dim=0)).tolist() == pytest.approx(
[0.2083, 0.7143, 2.9167], abs=5e-5)
f, g, plan, errors = dd.sinkhorn(reference, dd.BOND_0, dd.BOND_1, 200)
assert f.tolist() == pytest.approx([1.1779, 0.9506, 0.5062], abs=5e-5)
assert g.tolist() == pytest.approx([0.1842, 0.6918, 3.0004], abs=5e-5)
assert errors[-1] < 1e-12
assert float(plan[0, 2]) == pytest.approx(0.495, abs=5e-4)
assert dd.held_mass(plan) == pytest.approx(0.261, abs=5e-4)
assert float((dd.BOND_0 * dd.BOND_1).sum()) == pytest.approx(0.18)
assert float(torch.minimum(dd.BOND_0, dd.BOND_1).sum()) == pytest.approx(0.4)
expected = {0.2: 0.2250, 0.4: 0.2612, 0.6: 0.2926, 0.8: 0.3248, 0.99: 0.3812}
for r, value in expected.items():
held = dd.sinkhorn(torch.diag(dd.BOND_0) @ dd.replacement_kernel(r, dd.PRIOR),
dd.BOND_0, dd.BOND_1, 200)[2]
assert dd.held_mass(held) == pytest.approx(value, abs=5e-5)
def test_ddsbm_pinned_rates_and_the_posterior_average():
dd = load("ddsbm")
beta = dd.resampling_rate(0.4)
assert beta == pytest.approx(0.9163, abs=5e-5)
rates = dd.replacement_generator(beta, dd.PRIOR)
assert float(rates[0, 1]) == pytest.approx(0.3054, abs=5e-5)
assert rates.sum(dim=1).abs().max() < 1e-12
assert dd.half_survival_time(0.4) == pytest.approx(math.log(0.8) / math.log(0.4))
assert dd.half_survival_time(0.4) == pytest.approx(0.2435, abs=5e-5)
pinned = dd.replacement_kernel(0.5, dd.PRIOR)
assert float(pinned[0, 0]) == pytest.approx(2 / 3, abs=5e-5)
assert float(pinned[0, 1]) == pytest.approx(1 / 6, abs=5e-5)
toward = dd.pinned_rate(rates, pinned, 0, 2, 2)
away = dd.pinned_rate(rates, pinned, 2, 0, 2)
between = dd.pinned_rate(rates, pinned, 0, 1, 2)
assert toward == pytest.approx(1.2217, abs=5e-5)
assert away == pytest.approx(0.0764, abs=5e-5)
assert between == pytest.approx(0.3054, abs=5e-5)
assert toward / away == pytest.approx(16.0, abs=5e-4)
assert dd.rate_error(toward, 1.0) == pytest.approx(0.0229, abs=5e-5)
posterior = 0.6 * toward + 0.4 * between
assert posterior == pytest.approx(0.8552, abs=5e-5)
rates_two = [toward, between]
weights = [0.6, 0.4]
assert dd.posterior_loss(rates_two, weights, posterior) == pytest.approx(0.1357, abs=5e-5)
assert dd.posterior_loss(rates_two, weights, 1.0) == pytest.approx(0.1467, abs=5e-5)
assert dd.posterior_loss(rates_two, weights, 0.5) == pytest.approx(0.2395, abs=5e-5)
def test_ddsbm_factorization_counts():
dd = load("ddsbm")
assert dd.feature_count(9) == (9, 36, 45)
assert dd.state_space_size(9, 5, 4) == pytest.approx(9.2e27, rel=5e-3)
assert dd.total_jump_rate(36, dd.resampling_rate(0.4), 4) == pytest.approx(24.74, abs=5e-3)
# %% Section 7.25, csbm.py
def test_csbm_references():
cs = load("csbm")
A = cs.uniform_reference(0.35, 2)
assert A.flatten().tolist() == pytest.approx([0.65, 0.35, 0.35, 0.65])
assert torch.linalg.matrix_power(A, 3).flatten().tolist() == pytest.approx(
[0.5135, 0.4865, 0.4865, 0.5135], abs=5e-5)
assert cs.uniform_reference(0.35, 5).sum(dim=1).tolist() == pytest.approx([1.0] * 5)
ordered = cs.ordered_reference(1.0, 5)
assert ordered[2].tolist() == pytest.approx(
[0.0545, 0.2442, 0.4026, 0.2442, 0.0545], abs=5e-5)
assert cs.transition_loss(tensor(0.3160, 0.6840), tensor(0.25, 0.75)) == pytest.approx(
0.0110, abs=5e-5)
def test_csbm_dimf_on_sixteen_paths():
cs = load("csbm")
A = cs.uniform_reference(cs.ALPHA, 2)
plan = cs.sinkhorn(torch.diag(cs.PI_0) @ torch.linalg.matrix_power(A, 3), cs.PI_0, cs.PI_1)
assert plan.flatten().tolist() == pytest.approx(
[0.10432, 0.39568, 0.09568, 0.40432], abs=5e-6)
assert float(cs.bridge_table(A, 3)[0, 0, 0, 0]) == pytest.approx(0.5348, abs=5e-5)
gap, start, target = cs.starting_gap(A, 3, cs.PI_0, cs.PI_1)
assert float(target[0, 0, 0, 0]) == pytest.approx(0.0558, abs=5e-5)
assert float(target[1, 1, 1, 1]) == pytest.approx(0.2162, abs=5e-5)
assert float(start[0, 0, 0, 0]) == pytest.approx(0.0535, abs=5e-5)
assert float(start[1, 1, 1, 1]) == pytest.approx(0.2139, abs=5e-5)
assert gap == pytest.approx(0.0173, abs=5e-5)
for axis, value in ((1, [0.4754, 0.5246]), (2, [0.4107, 0.5893])):
marginal = start.sum(dim=[d for d in range(4) if d != axis])
assert marginal.tolist() == pytest.approx(value, abs=5e-5)
gaps, conditionals = cs.d_imf(cs.PI_0[:, None] * cs.PI_1[None, :], A, 3, 5, target)
assert conditionals[0].flatten().tolist() == pytest.approx(
[0.6243, 0.3757, 0.3265, 0.6735], abs=5e-5)
assert conditionals[1].flatten().tolist() == pytest.approx(
[0.5626, 0.4374, 0.2729, 0.7271], abs=5e-5)
assert conditionals[2].flatten().tolist() == pytest.approx(
[0.3160, 0.6840, 0.1192, 0.8808], abs=5e-5)
assert gaps == pytest.approx([2.9e-4, 5.0e-6, 8.4e-8, 1.4e-9, 2.4e-11], rel=0.05)
assert gaps[0] / gaps[1] == pytest.approx(59.0, abs=1.0)
def test_csbm_mixing_and_length():
cs = load("csbm")
independent = cs.PI_0[:, None] * cs.PI_1[None, :]
expected = {0.2: (0.13589, 5.0), 0.4: (0.00512, 284.3)}
for alpha, (start, ratio) in expected.items():
A = cs.uniform_reference(alpha, 2)
gap, _, target = cs.starting_gap(A, 3, cs.PI_0, cs.PI_1)
assert gap == pytest.approx(start, abs=5e-5)
later, _ = cs.d_imf(independent, A, 3, 2, target)
assert later[0] / later[1] == pytest.approx(ratio, rel=0.01)
A = cs.uniform_reference(cs.ALPHA, 2)
for steps, (start, ratio) in {2: (0.0574, 7.3), 5: (0.00156, 4674.0)}.items():
gap, _, target = cs.starting_gap(A, steps, cs.PI_0, cs.PI_1)
assert gap == pytest.approx(start, abs=5e-5)
later, _ = cs.d_imf(independent, A, steps, 2, target)
assert later[0] / later[1] == pytest.approx(ratio, rel=0.01)
# %% Sections 7.26 and 7.27, branched.py
def test_two_branches_worked_out():
br = load("branched")
linear = lambda t: 0.3 * t
late = lambda t: 0.3 * t ** 2
assert float(br.branch_weights(linear, torch.tensor(0.5))) == pytest.approx(0.15)
assert float(br.branch_weights(linear, torch.tensor(1.0))) == pytest.approx(0.3)
near_zero = br.weighted_energy(2.0, lambda t: 1.0 - linear(t))
near_one = br.weighted_energy(-2.0, linear)
assert near_zero == pytest.approx(1.7, abs=5e-5)
assert near_one == pytest.approx(0.3, abs=5e-5)
assert near_zero + near_one == pytest.approx(2.0, abs=5e-5)
far_linear = near_zero + br.weighted_energy(-4.0, linear)
far_late = br.weighted_energy(2.0, lambda t: 1.0 - late(t)) + br.weighted_energy(-4.0, late)
assert far_linear == pytest.approx(2.9, abs=5e-5)
assert far_late == pytest.approx(2.6, abs=5e-5)
def test_the_interpolant():
br = load("branched")
grid = torch.tensor([0.0, 0.5, 1.0])
assert br.interpolant(0.0, 2.0, 1.0, grid).tolist() == pytest.approx([0.0, 1.25, 2.0])
assert br.interpolant_velocity(0.0, 2.0, 1.0, grid).tolist() == pytest.approx([3.0, 2.0, 1.0])
assert br.interpolant_energy(0.0, 2.0, 1.0) == pytest.approx(2.1667, abs=5e-5)
assert br.interpolant_energy(0.0, 2.0, 0.0) == pytest.approx(2.0, abs=5e-5)
def test_entangled_tilt_and_parameterization():
br = load("branched")
normalizer = 0.1 * 1.0 + 0.9 * 0.01
assert normalizer == pytest.approx(0.109)
tilted = 0.1 / normalizer
assert tilted == pytest.approx(0.917, abs=5e-4)
assert tilted / 0.1 == pytest.approx(9.17, abs=5e-3)
reward = (1.0 - tilted) * math.log(0.01)
assert reward == pytest.approx(-0.380, abs=5e-4)
assert reward - math.log(normalizer) == pytest.approx(1.836, abs=5e-4)
assert -math.log(normalizer) == pytest.approx(2.216, abs=5e-4)
assert br.NOISE == pytest.approx(1.4142, abs=5e-5)
step = (-0.3 + 0.5) * 0.01 - 1.0 * 0.8 * 0.01 + br.NOISE * math.sqrt(0.01) * 0.4
assert step == pytest.approx(0.0506, abs=5e-5)
assert 0.5 / br.NOISE == pytest.approx(0.3536, abs=5e-5)
assert 0.5 * (0.5 / br.NOISE) ** 2 * 0.01 == pytest.approx(0.000625, abs=5e-7)
force = br.compose_bias(torch.tensor([[0.5]]), torch.tensor([[[0.3, -0.4]]]),
torch.tensor([[[1.0, 0.0]]]))[0, 0]
assert force.tolist() == pytest.approx([0.5, -0.4])
assert float(force.norm()) == pytest.approx(0.640, abs=5e-4)
weights = torch.softmax(torch.tensor([-1.0, 0.0, 1.0]), dim=0)
assert weights.tolist() == pytest.approx([0.090, 0.245, 0.665], abs=5e-4)
def test_entangled_training_lowers_the_loss():
br = load("branched")
torch.manual_seed(0)
generator = torch.Generator().manual_seed(0)
force = br.BiasForce(width=16)
rollout = br.roll_out(force, 16, 8, torch.device("cpu"), generator)
weights = br.importance_weights(rollout)
assert float(weights.sum()) == pytest.approx(1.0)
assert br.force_check(rollout["positions"][-1]) < 1e-5
loss = br.cross_entropy_loss(force, rollout, weights)
assert torch.isfinite(loss)
# %% Every file runs end to end
@pytest.mark.parametrize("name,flags", [
("optimal_transport", ["--steps", "300"]),
("sinkhorn", ["--steps", "20"]),
("displacement", ["--steps", "64"]),
("ot_flow_matching", ["--steps", "3", "--samples", "64", "--sample-steps", "5"]),
("static_bridge", ["--steps", "3"]),
("dynamic_bridge", ["--steps", "20"]),
("dsb", ["--steps", "2"]),
("dsbm", ["--steps", "3"]),
("sf2m", ["--steps", "21"]),
("discrete_bridge", ["--steps", "4"]),
("ddsbm", ["--steps", "20"]),
("csbm", ["--steps", "3"]),
("branched", ["--steps", "2", "--samples", "16"]),
])
def test_the_script_runs(name, flags, monkeypatch, capsys):
module = load(name)
monkeypatch.setattr(sys, "argv", [name] + flags + ["--quiet"])
module.main()
assert capsys.readouterr().out.strip()