Delete tests
Browse files- tests/conftest.py +0 -23
- tests/test_lecture_1.py +0 -554
- tests/test_lecture_2.py +0 -235
- tests/test_lecture_3.py +0 -445
- tests/test_lecture_4.py +0 -535
- tests/test_lecture_5.py +0 -550
- tests/test_lecture_6.py +0 -398
- tests/test_lecture_7.py +0 -709
tests/conftest.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
"""Shared setup for the test suite.
|
| 2 |
-
|
| 3 |
-
Several lecture files print five decimal places of an arithmetic result, which float32 cannot
|
| 4 |
-
separate, so those files raise the default precision when they are imported. A test may import a
|
| 5 |
-
file that does not, so the suite raises the precision here for every test alike.
|
| 6 |
-
|
| 7 |
-
This runs at module scope, not in a fixture, because pytest imports a conftest before it
|
| 8 |
-
imports or collects anything else, and a module-scoped fixture in a test file would otherwise
|
| 9 |
-
build its values first, in the lower precision.
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
import pytest
|
| 13 |
-
import torch
|
| 14 |
-
|
| 15 |
-
torch.set_default_dtype(torch.float64) # Match what every script does in main().
|
| 16 |
-
torch.set_num_threads(1) # These networks are small; one thread is the fastest here.
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
@pytest.fixture(scope="session", autouse=True)
|
| 20 |
-
def _restore_defaults():
|
| 21 |
-
"""Put the process back the way it was once the session ends."""
|
| 22 |
-
yield
|
| 23 |
-
torch.set_default_dtype(torch.float32)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_lecture_1.py
DELETED
|
@@ -1,554 +0,0 @@
|
|
| 1 |
-
"""Chapter 1's hand-computed numbers, asserted against the six Lecture 1 scripts.
|
| 2 |
-
|
| 3 |
-
Every row below is a value Chapter 1 works out by hand, paired with the label under which one of
|
| 4 |
-
the Lecture 1 scripts prints its own computation of it. The tolerance on each row is the rounding the
|
| 5 |
-
chapter itself prints: a value given to three decimals is checked to 5e-4, one given to five
|
| 6 |
-
decimals to 5e-6, and a value the chapter states exactly is checked to machine precision. A value
|
| 7 |
-
the chapter predicts and a script measures by sampling has a test of its own, with a band wide
|
| 8 |
-
enough for the Monte Carlo error at the sample size the test uses.
|
| 9 |
-
|
| 10 |
-
cd cis6270 && PYTHONPATH=. python -m pytest tests/test_lecture_1.py -q
|
| 11 |
-
"""
|
| 12 |
-
|
| 13 |
-
import math
|
| 14 |
-
import subprocess
|
| 15 |
-
import sys
|
| 16 |
-
from pathlib import Path
|
| 17 |
-
|
| 18 |
-
import pytest
|
| 19 |
-
|
| 20 |
-
from cis6270.runner import set_seed
|
| 21 |
-
from lecture_1 import calculus, fields, information, linear_algebra, optimization, probability
|
| 22 |
-
|
| 23 |
-
SEED = 0 # The seed every fixture sets before running a script's sections.
|
| 24 |
-
SAMPLES = 4000 # Monte Carlo draws and random competitors, fixed so the printed labels are fixed.
|
| 25 |
-
GRID = 1000 # Points in the numerical checks of calculus.py and fields.py.
|
| 26 |
-
STEPS = 600 # Gradient-descent steps, chain steps, Euler steps and estimator repeats.
|
| 27 |
-
EXACT = 1e-12 # The tolerance for a value the chapter states exactly.
|
| 28 |
-
ROOT = Path(__file__).resolve().parents[1] # The repository root, for the command-line check.
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
# %% 01. Probability, Sections 1.3 to 1.11
|
| 32 |
-
@pytest.fixture(scope="module")
|
| 33 |
-
def probability_worked() -> dict:
|
| 34 |
-
"""Run every worked example of probability.py and return the values it computed."""
|
| 35 |
-
set_seed(SEED)
|
| 36 |
-
probability.WORKED.clear()
|
| 37 |
-
probability.two_token_distribution(quiet=True)
|
| 38 |
-
probability.continuous_densities(quiet=True)
|
| 39 |
-
probability.expected_adenine(quiet=True)
|
| 40 |
-
probability.monte_carlo(SAMPLES, STEPS, quiet=True)
|
| 41 |
-
probability.worked_variance(quiet=True)
|
| 42 |
-
probability.peptide_covariance(quiet=True)
|
| 43 |
-
probability.noising_covariance(quiet=True)
|
| 44 |
-
probability.joint_marginal_conditional(quiet=True)
|
| 45 |
-
probability.bayes(quiet=True)
|
| 46 |
-
probability.independence(quiet=True)
|
| 47 |
-
return dict(probability.WORKED)
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
PROBABILITY_ROWS = [
|
| 51 |
-
# Section 1.3.1, the two-token distribution and the event that the first token is A.
|
| 52 |
-
("sum_x p_X(x)", 1.0, EXACT),
|
| 53 |
-
("P(x_1 = A) = p(AA) + p(AC)", 0.5, EXACT),
|
| 54 |
-
# Section 1.4, densities whose height exceeds one.
|
| 55 |
-
("uniform[0, 0.1] density height", 10.0, EXACT),
|
| 56 |
-
("P(0 <= X <= 0.02)", 0.2, EXACT),
|
| 57 |
-
("peak of N(0, 0.1^2)", 3.989, 5e-4),
|
| 58 |
-
# Sections 1.5.1 and 1.5.3, the expected adenine count and the Monte Carlo error scale.
|
| 59 |
-
("E[h(X)], expected A count", 1.1, EXACT),
|
| 60 |
-
("expected A count in 1000 draws", 1100.0, EXACT),
|
| 61 |
-
("E[h(X)^2]", 1.9, EXACT),
|
| 62 |
-
("Var(h(X))", 0.69, EXACT),
|
| 63 |
-
("sd of a 10-sample average", 0.263, 5e-4),
|
| 64 |
-
("sd of a 1000-sample average", 0.0263, 5e-5),
|
| 65 |
-
# Section 1.6.2, the variance of the values 0, 1, 2 by both routes.
|
| 66 |
-
("E[X]", 1.1, EXACT),
|
| 67 |
-
("E[X^2]", 1.7, EXACT),
|
| 68 |
-
("Var(X) from the two moments", 0.49, EXACT),
|
| 69 |
-
("Var(X) from the definition", 0.49, EXACT),
|
| 70 |
-
("sigma_X", 0.7, EXACT),
|
| 71 |
-
# Sections 1.7.1 and 1.7.2, the three peptides and their covariance matrix.
|
| 72 |
-
("mu_X", 2.0, EXACT),
|
| 73 |
-
("mu_Y", 4.0, EXACT),
|
| 74 |
-
("Cov(X, Y)", 5.0 / 3.0, EXACT),
|
| 75 |
-
("Var(X) = Sigma_11", 2.0 / 3.0, EXACT),
|
| 76 |
-
("Var(Y) = Sigma_22", 14.0 / 3.0, EXACT),
|
| 77 |
-
("sigma_X across the peptides", 0.8165, 5e-5),
|
| 78 |
-
("sigma_Y across the peptides", 2.160, 5e-4),
|
| 79 |
-
("rho_XY", 0.9449, 5e-5),
|
| 80 |
-
("Sigma symmetry, Sigma_21 - Sigma_12", 0.0, EXACT),
|
| 81 |
-
# Section 1.8, the noising equation at alpha_t = 0.6 and sigma_t = 0.8.
|
| 82 |
-
("alpha_t^2 + sigma_t^2", 1.0, EXACT),
|
| 83 |
-
("Cov(X_t)_11 with Cov(X_0) = I", 1.0, EXACT),
|
| 84 |
-
("Cov(X_t)_11 with diag(4, 1)", 2.08, EXACT),
|
| 85 |
-
("Cov(X_t)_22 with diag(4, 1)", 1.00, EXACT),
|
| 86 |
-
# Sections 1.9.1 to 1.9.3, the motif-and-binding table with its marginals and conditionals.
|
| 87 |
-
("sum over the four cells", 1.0, EXACT),
|
| 88 |
-
("p(X = 1), motif present", 0.60, EXACT),
|
| 89 |
-
("p(X = 0), motif absent", 0.40, EXACT),
|
| 90 |
-
("p(Y = 1), high binding", 0.44, EXACT),
|
| 91 |
-
("p(Y = 0), low binding", 0.56, EXACT),
|
| 92 |
-
("p(Y = 1 | X = 1)", 0.6, EXACT),
|
| 93 |
-
("p(Y = 0 | X = 1)", 0.4, EXACT),
|
| 94 |
-
("p(Y = 1 | X = 0)", 0.2, EXACT),
|
| 95 |
-
("p(Y = 0 | X = 0)", 0.8, EXACT),
|
| 96 |
-
# Section 1.10, Bayes' rule and the law of total probability.
|
| 97 |
-
("evidence p(Y = 1)", 0.44, EXACT),
|
| 98 |
-
("posterior p(X = 1 | Y = 1)", 0.818, 5e-4),
|
| 99 |
-
("likelihood ratio 0.6 / 0.2", 3.0, EXACT),
|
| 100 |
-
# Section 1.11, the failure of independence on the same table.
|
| 101 |
-
("p(X = 1) p(Y = 1)", 0.264, EXACT),
|
| 102 |
-
("p(X = 1, Y = 1)", 0.36, EXACT),
|
| 103 |
-
("association missed by factorizing", 0.096, EXACT),
|
| 104 |
-
]
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
@pytest.mark.parametrize("label, chapter, tolerance", PROBABILITY_ROWS)
|
| 108 |
-
def test_probability(probability_worked, label, chapter, tolerance):
|
| 109 |
-
assert probability_worked[label] == pytest.approx(chapter, abs=tolerance)
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def test_monte_carlo_lands_inside_its_own_error_bar():
|
| 113 |
-
"""Section 1.5.3: the sample average lands within four standard deviations of E[h(X)] = 1.1."""
|
| 114 |
-
set_seed(SEED)
|
| 115 |
-
estimates = probability.monte_carlo(SAMPLES, STEPS, quiet=True)
|
| 116 |
-
size, value, error = estimates["ladder"][-1] # The last rung, at the largest N the ladder reached.
|
| 117 |
-
assert 10 * size > SAMPLES >= size
|
| 118 |
-
assert abs(value - estimates["exact"]) < 4.0 * error
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
def test_the_measured_spread_of_ten_sample_averages_matches_0_263(probability_worked):
|
| 122 |
-
"""Section 1.5.3: repeating the ten-sample average measures the 0.263 the chapter predicts.
|
| 123 |
-
|
| 124 |
-
The spread of a sample standard deviation over 600 rounds is a few percent, so the band is
|
| 125 |
-
ten percent of the predicted value, which is looser than the rounding of the printed digits.
|
| 126 |
-
"""
|
| 127 |
-
predicted = math.sqrt(0.69 / 10.0)
|
| 128 |
-
assert probability_worked[f"sd of {STEPS} ten-sample averages"] == pytest.approx(
|
| 129 |
-
predicted, rel=0.10)
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
def test_the_same_seed_gives_the_same_monte_carlo_ladder():
|
| 133 |
-
"""Rule 8 of the conventions: a run is reproducible from its seed alone."""
|
| 134 |
-
set_seed(SEED)
|
| 135 |
-
first = probability.monte_carlo(1000, 20, quiet=True)["ladder"]
|
| 136 |
-
set_seed(SEED)
|
| 137 |
-
second = probability.monte_carlo(1000, 20, quiet=True)["ladder"]
|
| 138 |
-
assert first == second
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
# %% 02. Linear algebra, Sections 1.12 to 1.18
|
| 142 |
-
@pytest.fixture(scope="module")
|
| 143 |
-
def linear_algebra_worked() -> dict:
|
| 144 |
-
"""Run every worked example of linear_algebra.py and return the values it computed."""
|
| 145 |
-
set_seed(SEED)
|
| 146 |
-
linear_algebra.WORKED.clear()
|
| 147 |
-
linear_algebra.simplex(quiet=True)
|
| 148 |
-
linear_algebra.softmax(quiet=True)
|
| 149 |
-
linear_algebra.one_transition(quiet=True)
|
| 150 |
-
spectrum = linear_algebra.eigen(quiet=True)
|
| 151 |
-
linear_algebra.powers(spectrum["values"], spectrum["vectors"], quiet=True)
|
| 152 |
-
linear_algebra.diagonalization(spectrum["values"], spectrum["vectors"], quiet=True)
|
| 153 |
-
linear_algebra.stationary(10, quiet=True)
|
| 154 |
-
linear_algebra.matrix_exponential(spectrum["values"], spectrum["vectors"], quiet=True)
|
| 155 |
-
return dict(linear_algebra.WORKED)
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
LINEAR_ALGEBRA_ROWS = [
|
| 159 |
-
# Sections 1.12.2 and 1.12.3, expectation as a dot product and a mixture of two points.
|
| 160 |
-
("E[h(X)] = p^T h", 1.3, EXACT),
|
| 161 |
-
("mixture r_1 at lambda = 0.5", 0.30, EXACT),
|
| 162 |
-
("mixture r_2 at lambda = 0.5", 0.20, EXACT),
|
| 163 |
-
("mixture r_3 at lambda = 0.5", 0.50, EXACT),
|
| 164 |
-
("sum_i r_i, still a distribution", 1.0, EXACT),
|
| 165 |
-
# Section 1.13.1, the softmax of z = (2, 1, 0).
|
| 166 |
-
("e^{z_1}", 7.389, 5e-4),
|
| 167 |
-
("e^{z_2}", 2.718, 5e-4),
|
| 168 |
-
("e^{z_3}", 1.000, EXACT),
|
| 169 |
-
("partition function sum_j e^{z_j}", 11.107, 5e-4),
|
| 170 |
-
("softmax(z)_1", 0.665, 5e-4),
|
| 171 |
-
("softmax(z)_2", 0.245, 5e-4),
|
| 172 |
-
("softmax(z)_3", 0.090, 5e-4),
|
| 173 |
-
("sum_i softmax(z)_i", 1.0, EXACT),
|
| 174 |
-
("shift invariance, largest change", 0.0, 1e-12),
|
| 175 |
-
# Section 1.15.1, one step of the two-state chain from an even split.
|
| 176 |
-
("p_{t+1}(1)", 0.55, EXACT),
|
| 177 |
-
("p_{t+1}(2)", 0.45, EXACT),
|
| 178 |
-
# Sections 1.16 and 1.16.1, the eigenvalues and eigenvectors of A = [[2, 1], [1, 2]].
|
| 179 |
-
("trace A, lambda_1 + lambda_2", 4.0, EXACT),
|
| 180 |
-
("det A, lambda_1 lambda_2", 3.0, EXACT),
|
| 181 |
-
("lambda_1", 3.0, 1e-12),
|
| 182 |
-
("lambda_2", 1.0, 1e-12),
|
| 183 |
-
("v_1 second entry over first", 1.0, 1e-12),
|
| 184 |
-
("v_2 second entry over first", -1.0, 1e-12),
|
| 185 |
-
("largest |A v - lambda v|", 0.0, 1e-12),
|
| 186 |
-
# Section 1.16.2, powers along an eigendirection from x_0 = (3, 1).
|
| 187 |
-
("c_1", 2.0, 1e-12),
|
| 188 |
-
("c_2", 1.0, 1e-12),
|
| 189 |
-
("lambda_1^10", 59049.0, 1e-6),
|
| 190 |
-
("x_2 first entry, eigen route", 19.0, 1e-10),
|
| 191 |
-
("x_2 second entry, eigen route", 17.0, 1e-10),
|
| 192 |
-
("x_10 first entry", 118099.0, 1e-6),
|
| 193 |
-
("x_10 second entry", 118097.0, 1e-6),
|
| 194 |
-
# Sections 1.16.3 and 1.16.4, diagonalization and the dominant eigenvalue.
|
| 195 |
-
("V^{-1}_11", 0.5, 1e-12),
|
| 196 |
-
("largest |V Lambda V^{-1} - A|", 0.0, 1e-12),
|
| 197 |
-
("A^3_11 through the eigenvalues", 14.0, 1e-10),
|
| 198 |
-
("A^3_12 through the eigenvalues", 13.0, 1e-10),
|
| 199 |
-
("dominant coefficient 2^10", 1024.0, EXACT),
|
| 200 |
-
("decaying coefficient 0.5^10", 0.000977, 5e-7),
|
| 201 |
-
("ratio of the two, in millions", 1.05, 5e-3),
|
| 202 |
-
# Section 1.17, the stationary distribution and the rate of approach.
|
| 203 |
-
("p*_1", 0.6, 1e-10),
|
| 204 |
-
("p*_2", 0.4, 1e-10),
|
| 205 |
-
("largest |P^T p* - p*|", 0.0, 1e-10),
|
| 206 |
-
("trace P", 1.5, EXACT),
|
| 207 |
-
("det P", 0.5, EXACT),
|
| 208 |
-
("second eigenvalue, the rate", 0.5, 1e-12),
|
| 209 |
-
("p_1(1)", 0.55, EXACT),
|
| 210 |
-
("p_2(1)", 0.575, EXACT),
|
| 211 |
-
("p_3(1)", 0.5875, EXACT),
|
| 212 |
-
("p_10(1)", 0.59990, 5e-6),
|
| 213 |
-
# Section 1.18.3, the four-term series against the eigenvalue formula at t = 0.1.
|
| 214 |
-
("four-term series, entry 11", 1.22733, 5e-6),
|
| 215 |
-
("four-term series, entry 12", 0.12217, 5e-6),
|
| 216 |
-
("eigenvalue route, entry 11", 1.22751, 5e-6),
|
| 217 |
-
("eigenvalue route, entry 12", 0.12234, 5e-6),
|
| 218 |
-
("largest gap between the routes", 1.8e-4, 5e-6),
|
| 219 |
-
("first discarded term, entry 11", 1.71e-4, 5e-7),
|
| 220 |
-
("first discarded term, entry 12", 1.67e-4, 5e-7),
|
| 221 |
-
("x_1 first entry, 2e^3 + e", 42.889, 5e-4),
|
| 222 |
-
("x_1 second entry, 2e^3 - e", 37.453, 5e-4),
|
| 223 |
-
# Sections 1.18.4 and 1.18.5, stability and the covariance read through its eigenvalues.
|
| 224 |
-
("decay e^{-2t} at t = 1", 0.1353, 5e-5),
|
| 225 |
-
("decay e^{-2t} at t = 3", 0.002479, 5e-7),
|
| 226 |
-
("sqrt(lambda_1), Gaussian long axis", 1.732, 5e-4),
|
| 227 |
-
]
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
@pytest.mark.parametrize("label, chapter, tolerance", LINEAR_ALGEBRA_ROWS)
|
| 231 |
-
def test_linear_algebra(linear_algebra_worked, label, chapter, tolerance):
|
| 232 |
-
assert linear_algebra_worked[label] == pytest.approx(chapter, abs=tolerance)
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
def test_the_chain_halves_its_deviation_at_every_step():
|
| 236 |
-
"""Section 1.17: the deviation from p* lies along the second eigenvector, so d_n = 0.5^n d_0."""
|
| 237 |
-
chain = linear_algebra.stationary(12, quiet=True)
|
| 238 |
-
for step, point in enumerate(chain["history"], start=1):
|
| 239 |
-
deviation = float((point - chain["p_star"]).abs().max())
|
| 240 |
-
assert deviation == pytest.approx(0.1 * 0.5 ** step, abs=1e-12)
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
# %% 03. Calculus, Sections 1.22 to 1.30
|
| 244 |
-
@pytest.fixture(scope="module")
|
| 245 |
-
def calculus_worked() -> dict:
|
| 246 |
-
"""Run every worked example of calculus.py and return the values it computed."""
|
| 247 |
-
set_seed(SEED)
|
| 248 |
-
calculus.WORKED.clear()
|
| 249 |
-
calculus.jacobian(quiet=True)
|
| 250 |
-
calculus.determinant(quiet=True)
|
| 251 |
-
calculus.change_of_variables(GRID, quiet=True)
|
| 252 |
-
calculus.composition(quiet=True)
|
| 253 |
-
calculus.branches(GRID, quiet=True)
|
| 254 |
-
calculus.gradient(quiet=True)
|
| 255 |
-
calculus.divergences(quiet=True)
|
| 256 |
-
calculus.continuity(GRID, STEPS, quiet=True)
|
| 257 |
-
return dict(calculus.WORKED)
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
CALCULUS_ROWS = [
|
| 261 |
-
# Sections 1.22.2 and 1.22.3, the Jacobian at (1, 0) and the displacement it predicts.
|
| 262 |
-
("J_phi(1, 0)_11", 2.0, EXACT),
|
| 263 |
-
("J_phi(1, 0)_21 = cos(1)", 0.5403, 5e-5),
|
| 264 |
-
("largest |autograd - closed form|", 0.0, EXACT),
|
| 265 |
-
("predicted change, first output", 0.0, 1e-12),
|
| 266 |
-
("predicted change, second output", -0.0546, 5e-5),
|
| 267 |
-
("exact change, first output", 0.0001, 5e-6),
|
| 268 |
-
("exact change, second output", -0.05464, 5e-6),
|
| 269 |
-
# Section 1.23, the determinant as the local volume factor.
|
| 270 |
-
("det A for diag(2, 3)", 6.0, EXACT),
|
| 271 |
-
("area of the image rectangle", 6.0, EXACT),
|
| 272 |
-
("det of the collapsing map", 0.0, EXACT),
|
| 273 |
-
("det J_phi at x_1 = 2", 4.0, 1e-12),
|
| 274 |
-
("det J_phi at x_1 = 0.25", 0.5, 1e-12),
|
| 275 |
-
# Sections 1.24.1 and 1.24.2, change of variables in one and in two dimensions.
|
| 276 |
-
("preimage of [1.4, 2.0], lower end", 0.2, EXACT),
|
| 277 |
-
("preimage of [1.4, 2.0], upper end", 0.5, EXACT),
|
| 278 |
-
("P(X in the preimage)", 0.3, EXACT),
|
| 279 |
-
("p_Y on [1, 3] after Y = 2X + 1", 0.5, EXACT),
|
| 280 |
-
("integral of p_Y over [1, 3]", 1.0, EXACT),
|
| 281 |
-
("p_Y inside the image of A", 1.0 / 6.0, EXACT),
|
| 282 |
-
("density times area for A", 1.0, EXACT),
|
| 283 |
-
("p_Y(y_1 = 1) for the x_1^2 map", 0.5, EXACT),
|
| 284 |
-
("largest |p_Y |det J| - p_X|", 0.0, 1e-12),
|
| 285 |
-
("total mass after the x_1^2 map", 1.0, 1e-12),
|
| 286 |
-
# Section 1.25, the composition and its log-determinant.
|
| 287 |
-
("det J_psi", 6.0, 1e-12),
|
| 288 |
-
("chained Jacobian, entry 11", 8.0, 1e-12),
|
| 289 |
-
("det of the composition", 24.0, 1e-10),
|
| 290 |
-
("log 4", 1.3863, 5e-5),
|
| 291 |
-
("log 6", 1.7918, 5e-5),
|
| 292 |
-
("log 4 + log 6 = log 24", 3.1781, 5e-5),
|
| 293 |
-
("log p_Y from a unit base density", -3.1781, 5e-5),
|
| 294 |
-
# Section 1.26, the two inverse branches of the squaring map.
|
| 295 |
-
("p_Y(0.25), two branches at ±0.5", 1.0, 1e-12),
|
| 296 |
-
("p_Y(0.04), two branches at ±0.2", 2.5, 1e-12),
|
| 297 |
-
("integral of p_Y over [0.01, 1]", 0.9, 5e-5),
|
| 298 |
-
# Sections 1.27.1 and 1.27.2, the gradient and the directional derivative.
|
| 299 |
-
("grad f(1, 2), first entry", 2.0, EXACT),
|
| 300 |
-
("grad f(1, 2), second entry", 8.0, EXACT),
|
| 301 |
-
("|grad f(1, 2)| = sqrt(68)", math.sqrt(68.0), 1e-12),
|
| 302 |
-
("steepest direction, first entry", 0.243, 5e-4),
|
| 303 |
-
("steepest direction, second entry", 0.970, 5e-4),
|
| 304 |
-
("D_u f along (1, 0)", 2.0, EXACT),
|
| 305 |
-
("D_u f along (0, 1)", 8.0, EXACT),
|
| 306 |
-
# Section 1.29, the divergence of the three fields.
|
| 307 |
-
("div of (-x_1, -2 x_2)", -3.0, EXACT),
|
| 308 |
-
("div of (2 x_1, -x_2)", 1.0, EXACT),
|
| 309 |
-
("div of (-x_2, x_1)", 0.0, EXACT),
|
| 310 |
-
("area factor e^{div t} at t = 0.5", 1.649, 5e-4),
|
| 311 |
-
# Section 1.30.2, the worked flow v(x) = x on a uniform source.
|
| 312 |
-
("largest continuity residual", 0.0, 1e-6),
|
| 313 |
-
("log p along the trajectory at t = 1", -1.0, 1e-12),
|
| 314 |
-
("p_t(x_t) at t = 1", 0.3679, 5e-5),
|
| 315 |
-
("density at t = log 2", 0.5, EXACT),
|
| 316 |
-
("mass at t = log 2", 1.0, EXACT),
|
| 317 |
-
("mass at t = 1", 1.000, 5e-4),
|
| 318 |
-
]
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
@pytest.mark.parametrize("label, chapter, tolerance", CALCULUS_ROWS)
|
| 322 |
-
def test_calculus(calculus_worked, label, chapter, tolerance):
|
| 323 |
-
assert calculus_worked[label] == pytest.approx(chapter, abs=tolerance)
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
def test_the_euler_walk_approaches_the_exact_trajectory(calculus_worked):
|
| 327 |
-
"""Section 1.30.2: x_t = x_0 e^t, so a first-order walk closes on 0.5e as the step shrinks."""
|
| 328 |
-
coarse = calculus.continuity(GRID, 50, quiet=True)["x_t"]
|
| 329 |
-
fine = calculus.continuity(GRID, 4 * 50, quiet=True)["x_t"]
|
| 330 |
-
assert abs(fine - 0.5 * math.e) < abs(coarse - 0.5 * math.e) / 3.0
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
# %% 04. Information, Sections 1.33 to 1.35
|
| 334 |
-
@pytest.fixture(scope="module")
|
| 335 |
-
def information_worked() -> dict:
|
| 336 |
-
"""Run every worked example of information.py and return the values it computed."""
|
| 337 |
-
set_seed(SEED)
|
| 338 |
-
information.WORKED.clear()
|
| 339 |
-
information.information(quiet=True)
|
| 340 |
-
information.entropies(quiet=True)
|
| 341 |
-
information.cross_entropies(quiet=True)
|
| 342 |
-
information.kl(SAMPLES, quiet=True)
|
| 343 |
-
information.monte_carlo(SAMPLES, STEPS, quiet=True)
|
| 344 |
-
information.maximum_likelihood(quiet=True)
|
| 345 |
-
return dict(information.WORKED)
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
INFORMATION_ROWS = [
|
| 349 |
-
# Section 1.33.1, the information content of a common and of a rare outcome.
|
| 350 |
-
("I(x) at p(x) = 0.5", 0.693, 5e-4),
|
| 351 |
-
("I(x) at p(x) = 0.01", 4.605, 5e-4),
|
| 352 |
-
("I(x) at p(x) = 1", 0.0, EXACT),
|
| 353 |
-
("ratio of the two surprises", 6.6, 5e-2),
|
| 354 |
-
# Section 1.33.2, entropy on a skewed, a balanced and a uniform distribution.
|
| 355 |
-
("H((0.9, 0.1))", 0.325, 5e-4),
|
| 356 |
-
("H((0.5, 0.5)) = log 2", 0.693, 5e-4),
|
| 357 |
-
("H of a point mass", 0.0, EXACT),
|
| 358 |
-
("H of the uniform on 4 states", 1.386, 5e-4),
|
| 359 |
-
# Section 1.33.3, the cross-entropy of p = (0.8, 0.2) scored by q = (0.6, 0.4).
|
| 360 |
-
("H(p, q)", 0.592, 5e-4),
|
| 361 |
-
("H(p)", 0.500, 5e-4),
|
| 362 |
-
("excess cost H(p, q) - H(p)", 0.092, 5e-4),
|
| 363 |
-
("H(p, p) - H(p)", 0.0, EXACT),
|
| 364 |
-
# Section 1.34, the divergence, its split, its nonnegativity and its asymmetry.
|
| 365 |
-
("KL(p || q)", 0.0915, 5e-5),
|
| 366 |
-
("KL(q || p)", 0.1046, 5e-5),
|
| 367 |
-
("H(p) + KL(p || q) = H(p, q)", 0.592, 5e-4),
|
| 368 |
-
("KL(p || p)", 0.0, EXACT),
|
| 369 |
-
("reverse over forward, percent", 14.0, 0.5),
|
| 370 |
-
("largest log t - (t - 1)", 0.0, 1e-12),
|
| 371 |
-
(f"largest KL breach over {SAMPLES} pairs", 0.0, EXACT),
|
| 372 |
-
# Section 1.35, the empirical loss on ten DNA bases and its gap to the uniform model.
|
| 373 |
-
("term for A, 0.4 log(1/0.4)", 0.36652, 5e-6),
|
| 374 |
-
("term for C, 0.3 log(1/0.3)", 0.36119, 5e-6),
|
| 375 |
-
("term for G, 0.2 log(1/0.2)", 0.32189, 5e-6),
|
| 376 |
-
("term for T, 0.1 log(1/0.1)", 0.23026, 5e-6),
|
| 377 |
-
("loss at the frequencies", 1.280, 5e-4),
|
| 378 |
-
("loss under the uniform model", 1.386, 5e-4),
|
| 379 |
-
("gap between the two losses", 0.1064, 5e-5),
|
| 380 |
-
("KL(p_hat || uniform)", 0.1064, 5e-5),
|
| 381 |
-
]
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
@pytest.mark.parametrize("label, chapter, tolerance", INFORMATION_ROWS)
|
| 385 |
-
def test_information(information_worked, label, chapter, tolerance):
|
| 386 |
-
assert information_worked[label] == pytest.approx(chapter, abs=tolerance)
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
def test_cross_entropy_by_sampling_lands_inside_its_error_bar():
|
| 390 |
-
"""Section 1.33.3 estimated by the Monte Carlo of Section 1.5.3, to within four error bars."""
|
| 391 |
-
set_seed(SEED)
|
| 392 |
-
estimate = information.monte_carlo(SAMPLES, STEPS, quiet=True)
|
| 393 |
-
assert abs(estimate["estimate"] - estimate["exact"]) < 4.0 * estimate["error"]
|
| 394 |
-
assert estimate["measured"] == pytest.approx(estimate["predicted"], rel=0.15)
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
# %% 05. Optimization, Sections 1.36 to 1.38
|
| 398 |
-
@pytest.fixture(scope="module")
|
| 399 |
-
def optimization_worked() -> dict:
|
| 400 |
-
"""Run every worked example of optimization.py and return the values it computed."""
|
| 401 |
-
set_seed(SEED)
|
| 402 |
-
optimization.WORKED.clear()
|
| 403 |
-
optimization.unconstrained(STEPS, quiet=True)
|
| 404 |
-
optimization.equality_constraint(quiet=True)
|
| 405 |
-
optimization.normalization(quiet=True)
|
| 406 |
-
optimization.maximum_entropy(SAMPLES, quiet=True)
|
| 407 |
-
optimization.kkt(quiet=True)
|
| 408 |
-
optimization.over_the_simplex(STEPS, quiet=True)
|
| 409 |
-
return dict(optimization.WORKED)
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
OPTIMIZATION_ROWS = [
|
| 413 |
-
# Section 1.36, three steps of gradient descent on (theta - 3)^2 at eta = 0.1.
|
| 414 |
-
("theta_1", 0.600, 5e-4),
|
| 415 |
-
("theta_2", 1.080, 5e-4),
|
| 416 |
-
("theta_3", 1.464, 5e-4),
|
| 417 |
-
(f"theta_{STEPS} from the closed form", 3.0, 1e-12),
|
| 418 |
-
("gradient at the minimizer theta = 3", 0.0, EXACT),
|
| 419 |
-
# Section 1.37.1, the squared distance to the origin along x_1 + x_2 = 1.
|
| 420 |
-
("x_1 at the constrained minimum", 0.5, 1e-12),
|
| 421 |
-
("x_2 at the constrained minimum", 0.5, 1e-12),
|
| 422 |
-
("lambda, the equality multiplier", -1.0, 1e-12),
|
| 423 |
-
("f at the minimum", 0.5, 1e-12),
|
| 424 |
-
("f at the endpoint (1, 0)", 1.0, EXACT),
|
| 425 |
-
("largest |grad f + lambda grad g|", 0.0, 1e-12),
|
| 426 |
-
# Section 1.37.2, normalization alone, and the likelihood minimizer it produces.
|
| 427 |
-
("uniform p_i at K = 4", 0.25, EXACT),
|
| 428 |
-
("max entropy under normalization", 1.386, 5e-4),
|
| 429 |
-
("p_hat(A) = n(A) / N", 0.4, EXACT),
|
| 430 |
-
("p_hat(T) = n(T) / N", 0.1, EXACT),
|
| 431 |
-
("minimum empirical loss", 1.280, 5e-4),
|
| 432 |
-
("loss drop from moving off p_hat", 0.0, EXACT),
|
| 433 |
-
# Section 1.37.3, maximum entropy under a GC-content target of 0.6.
|
| 434 |
-
("e^lambda", 1.5, 1e-12),
|
| 435 |
-
("lambda = log 1.5", 0.4055, 5e-5),
|
| 436 |
-
("partition function Z", 5.0, 1e-12),
|
| 437 |
-
("p*(A)", 0.2, 1e-12),
|
| 438 |
-
("p*(C)", 0.3, 1e-12),
|
| 439 |
-
("GC content of p*", 0.6, 1e-12),
|
| 440 |
-
("H(p*)", 1.366, 5e-4),
|
| 441 |
-
("H of the unconstrained uniform", 1.386, 5e-4),
|
| 442 |
-
("entropy cost of the constraint", 0.0201, 5e-5),
|
| 443 |
-
(f"H above H(p*) over {SAMPLES} draws", 0.0, EXACT),
|
| 444 |
-
# Section 1.38, the bounded quadratic and its four KKT conditions.
|
| 445 |
-
("x* at the bound", 1.0, EXACT),
|
| 446 |
-
("lambda, the inequality multiplier", 4.0, EXACT),
|
| 447 |
-
("g(x*) = x* - 1", 0.0, EXACT),
|
| 448 |
-
("complementary slackness lambda g", 0.0, EXACT),
|
| 449 |
-
("stationarity 2(x* - 3) + lambda", 0.0, EXACT),
|
| 450 |
-
("f at the unconstrained minimum", 0.0, EXACT),
|
| 451 |
-
("f under the bound", 4.0, EXACT),
|
| 452 |
-
("value drop for epsilon = 0.001", 0.004, 5e-6),
|
| 453 |
-
# Section 1.38.1, the same likelihood problem solved on logits through the softmax.
|
| 454 |
-
("sum_i p_i at the end", 1.0, 1e-12),
|
| 455 |
-
("entries at or below zero", 0.0, EXACT),
|
| 456 |
-
("p(A) after the descent", 0.4, 5e-4),
|
| 457 |
-
("p(T) after the descent", 0.1, 5e-4),
|
| 458 |
-
("loss after the descent", 1.280, 5e-4),
|
| 459 |
-
("largest |p - p_hat|", 0.0, 5e-4),
|
| 460 |
-
]
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
@pytest.mark.parametrize("label, chapter, tolerance", OPTIMIZATION_ROWS)
|
| 464 |
-
def test_optimization(optimization_worked, label, chapter, tolerance):
|
| 465 |
-
assert optimization_worked[label] == pytest.approx(chapter, abs=tolerance)
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
def test_the_softmax_descent_reaches_the_multiplier_solution():
|
| 469 |
-
"""Section 1.38.1: the unconstrained route on logits reaches what Section 1.37.2 derived."""
|
| 470 |
-
set_seed(SEED)
|
| 471 |
-
fitted = optimization.over_the_simplex(2000, quiet=True)
|
| 472 |
-
frequencies = optimization.BASE_COUNTS / optimization.BASE_COUNTS.sum()
|
| 473 |
-
assert fitted["distance"] < 1e-5
|
| 474 |
-
assert fitted["losses"][-1] == pytest.approx(
|
| 475 |
-
float(-(frequencies * frequencies.log()).sum()), abs=1e-9)
|
| 476 |
-
assert fitted["losses"][0] > fitted["losses"][-1]
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
# %% 06. Fields, Sections 1.31 and 1.32
|
| 480 |
-
@pytest.fixture(scope="module")
|
| 481 |
-
def fields_worked() -> dict:
|
| 482 |
-
"""Run every worked example of fields.py and return the values it computed."""
|
| 483 |
-
set_seed(SEED)
|
| 484 |
-
fields.WORKED.clear()
|
| 485 |
-
fields.scores(quiet=True)
|
| 486 |
-
fields.curvatures(quiet=True)
|
| 487 |
-
fields.heat(GRID, quiet=True)
|
| 488 |
-
fields.fokker_planck(GRID, quiet=True)
|
| 489 |
-
fields.euler_maruyama(SAMPLES, STEPS, quiet=True)
|
| 490 |
-
return dict(fields.WORKED)
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
FIELDS_ROWS = [
|
| 494 |
-
# Section 1.31, the score of the standard Gaussian and of N(1, 4).
|
| 495 |
-
("s(x) at x = 3", -3.0, EXACT),
|
| 496 |
-
("s(x) at x = -2", 2.0, EXACT),
|
| 497 |
-
("s(x) at the mode x = 0", 0.0, EXACT),
|
| 498 |
-
("largest |s(x) + x|", 0.0, EXACT),
|
| 499 |
-
("largest change from dropping Z", 0.0, EXACT),
|
| 500 |
-
("s(5) under N(1, 4)", -1.0, EXACT),
|
| 501 |
-
# Section 1.32, the two curvatures of the bowl and the cancelling pair of the saddle.
|
| 502 |
-
("d^2 f / d x_1^2", 2.0, EXACT),
|
| 503 |
-
("d^2 f / d x_2^2", 6.0, EXACT),
|
| 504 |
-
("Laplacian of x_1^2 + 3 x_2^2", 8.0, EXACT),
|
| 505 |
-
("Laplacian from the four neighbors", 8.0, 5e-6),
|
| 506 |
-
("Laplacian of the saddle", 0.0, EXACT),
|
| 507 |
-
# Section 1.32.1, the heat equation on the widening Gaussian p_t = N(0, t) at D = 1/2.
|
| 508 |
-
("p_1(0) = 1 / sqrt(2 pi)", 0.39894, 5e-6),
|
| 509 |
-
("d p_t(0) / d t at t = 1", -0.19947, 5e-6),
|
| 510 |
-
("Laplacian of p_1 at 0", -0.39894, 5e-6),
|
| 511 |
-
("-p_1(0) / sigma^2 at sigma^2 = 1", -0.39894, 5e-6),
|
| 512 |
-
("D times the Laplacian of p_1 at 0", -0.19947, 5e-6),
|
| 513 |
-
("largest heat-equation residual", 0.0, 1e-6),
|
| 514 |
-
# Section 1.32.2, the Fokker-Planck equation and the two equations it reduces to.
|
| 515 |
-
("center of p_t at t = 1", 0.5, EXACT),
|
| 516 |
-
("largest residual, drift and noise", 0.0, 1e-6),
|
| 517 |
-
("largest residual at sigma_t = 0", 0.0, 1e-6),
|
| 518 |
-
]
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
@pytest.mark.parametrize("label, chapter, tolerance", FIELDS_ROWS)
|
| 522 |
-
def test_fields(fields_worked, label, chapter, tolerance):
|
| 523 |
-
assert fields_worked[label] == pytest.approx(chapter, abs=tolerance)
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
def test_the_heat_equation_matches_the_time_derivative_at_the_center(fields_worked):
|
| 527 |
-
"""Section 1.32.1: D times the Laplacian at the origin equals d p_t(0)/dt to every digit."""
|
| 528 |
-
assert fields_worked["D times the Laplacian of p_1 at 0"] == pytest.approx(
|
| 529 |
-
fields_worked["d p_t(0) / d t at t = 1"], abs=EXACT)
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
def test_the_simulated_walk_tracks_the_density_it_solves():
|
| 533 |
-
"""Section 1.32.2: dX_t = b dt + sigma dW_t has density N(bt, t), so the variance tracks t."""
|
| 534 |
-
paths = 20000 # Enough paths that four standard errors is a tight band on both moments.
|
| 535 |
-
set_seed(SEED)
|
| 536 |
-
walk = fields.euler_maruyama(paths, 200, quiet=True)
|
| 537 |
-
assert abs(walk["mean"] - 0.5) < 4.0 * math.sqrt(1.0 / paths)
|
| 538 |
-
assert abs(walk["variance"] - 1.0) < 4.0 * math.sqrt(2.0 / paths)
|
| 539 |
-
assert walk["variance_gap"] < 0.05
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
# %% 07. Every script runs from the command line
|
| 543 |
-
@pytest.mark.parametrize("script", ["probability", "linear_algebra", "calculus", "information",
|
| 544 |
-
"optimization", "fields"])
|
| 545 |
-
def test_the_script_runs_clean(script):
|
| 546 |
-
"""Rule 7 of the conventions: --steps 20 --quiet finishes in a few seconds on a CPU."""
|
| 547 |
-
finished = subprocess.run(
|
| 548 |
-
[sys.executable, f"lecture_1/{script}.py", "--steps", "20", "--quiet"],
|
| 549 |
-
cwd=ROOT, env={"PYTHONPATH": str(ROOT), "PATH": "/usr/bin:/bin"},
|
| 550 |
-
capture_output=True, text=True, timeout=120,
|
| 551 |
-
)
|
| 552 |
-
assert finished.returncode == 0, finished.stderr
|
| 553 |
-
assert finished.stderr == ""
|
| 554 |
-
assert "Sections" in finished.stdout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_lecture_2.py
DELETED
|
@@ -1,235 +0,0 @@
|
|
| 1 |
-
"""The worked numbers of Chapter 2, asserted against the Lecture 2 scripts.
|
| 2 |
-
|
| 3 |
-
Every figure here is quoted in the chapter: the Euler landings at one, two and four steps, the flux
|
| 4 |
-
balance on [-1, 1], both sides of the continuity equation at x = 2, the single endpoint pair, the
|
| 5 |
-
average at the crossing, the nine-unit gap between the two objectives, the table of three
|
| 6 |
-
interpolants at t = 0.25, and the parameter count of the MNIST U-Net. Nothing in this file trains.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import math
|
| 10 |
-
import sys
|
| 11 |
-
from pathlib import Path
|
| 12 |
-
|
| 13 |
-
import pytest
|
| 14 |
-
import torch
|
| 15 |
-
|
| 16 |
-
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # Run from a fresh clone.
|
| 17 |
-
|
| 18 |
-
from lecture_2 import flow_matching_2d as fm2d # noqa: E402
|
| 19 |
-
from lecture_2 import interpolants as interp # noqa: E402
|
| 20 |
-
from lecture_2 import velocity_fields as vf # noqa: E402
|
| 21 |
-
|
| 22 |
-
LOG2 = math.log(2.0)
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
# %% 01. Velocity fields, trajectories and the flow map (Sections 2.2 to 2.4)
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def test_trajectories_match_the_chapter():
|
| 29 |
-
report = vf.trajectories()
|
| 30 |
-
assert report["constant_field_trajectory"] == [1.0, 1.5, 2.0, 2.5, 3.0]
|
| 31 |
-
assert report["expansion_x_half_from_two"] == pytest.approx(2.828, abs=1e-3)
|
| 32 |
-
assert report["expansion_endpoints"] == [-2.0, 0.0, 2.0]
|
| 33 |
-
assert report["expansion_outer_spacing"] == pytest.approx(4.0)
|
| 34 |
-
assert report["contraction_endpoints"] == [-0.5, 0.0, 0.5]
|
| 35 |
-
assert report["contraction_outer_spacing"] == pytest.approx(1.0)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def test_rotation_preserves_the_radius():
|
| 39 |
-
report = vf.trajectories()
|
| 40 |
-
assert report["rotation_quarter_turn"] == [0.0, 1.0]
|
| 41 |
-
assert report["rotation_half_turn"] == [-1.0, 0.0]
|
| 42 |
-
assert report["rotation_radius"] == pytest.approx(1.0, abs=1e-6)
|
| 43 |
-
assert report["rotation_squared_norm_rate"] == pytest.approx(0.0, abs=1e-6)
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def test_flow_map_is_the_identity_at_zero_and_doubles_at_one():
|
| 47 |
-
report = vf.flow_map_report()
|
| 48 |
-
assert report["flow_map_identity_error"] == pytest.approx(0.0, abs=1e-6)
|
| 49 |
-
assert report["flow_map_jacobian_det"] == pytest.approx(2.0, abs=1e-6)
|
| 50 |
-
assert report["trajectory_gap_minimum"] == pytest.approx(2.0, abs=1e-6)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# %% 02. Euler's method (Section 2.5)
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def test_four_euler_steps_land_at_the_chapter_value():
|
| 57 |
-
landing = vf.euler(lambda x, t: x, torch.tensor(1.0), 4) # dx/dt = x from x_0 = 1.
|
| 58 |
-
assert float(landing) == pytest.approx(2.44140625, abs=1e-6)
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def test_euler_ladder_errors_and_ratios():
|
| 62 |
-
report = vf.euler_ladder(4)
|
| 63 |
-
assert report["euler_step_counts"] == [1, 2, 4]
|
| 64 |
-
assert report["euler_landings"] == [2.0, 2.25, pytest.approx(2.4414, abs=1e-4)]
|
| 65 |
-
assert report["euler_errors"] == [pytest.approx(0.7183, abs=1e-4),
|
| 66 |
-
pytest.approx(0.4683, abs=1e-4),
|
| 67 |
-
pytest.approx(0.2769, abs=1e-4)]
|
| 68 |
-
assert report["euler_error_halving_ratios"] == [pytest.approx(1.53, abs=5e-3),
|
| 69 |
-
pytest.approx(1.69, abs=5e-3)]
|
| 70 |
-
assert report["euler_relative_error_four_steps"] == pytest.approx(0.1019, abs=1e-4)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def test_the_euler_error_closes_like_one_over_the_step_count():
|
| 74 |
-
report = vf.euler_ladder(256) # The error times the step count settles at e/2 = 1.359.
|
| 75 |
-
scaled = report["euler_error_times_steps"]
|
| 76 |
-
assert scaled[-1] == pytest.approx(math.e / 2.0, abs=0.02)
|
| 77 |
-
assert report["euler_error_halving_ratios"][-1] == pytest.approx(2.0, abs=0.02)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
# %% 03. The pushforward, the flux and the continuity equation (Sections 2.6 and 2.7)
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def test_the_expansion_field_doubles_the_standard_deviation():
|
| 84 |
-
report = vf.pushforward_moments(4096, seed=0)
|
| 85 |
-
assert report["pushforward_mean"] == pytest.approx(0.0, abs=0.05)
|
| 86 |
-
assert report["pushforward_variance"] == pytest.approx(4.0, abs=0.2)
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def test_flux_balance_on_the_unit_interval():
|
| 90 |
-
report = vf.flux_balance()
|
| 91 |
-
assert report["density_at_one"] == pytest.approx(0.2420, abs=1e-4)
|
| 92 |
-
assert report["flux_at_minus_one"] == pytest.approx(-0.1677, abs=1e-4)
|
| 93 |
-
assert report["flux_at_one"] == pytest.approx(0.1677, abs=1e-4)
|
| 94 |
-
assert report["flux_balance"] == pytest.approx(-0.3354, abs=1e-4)
|
| 95 |
-
assert report["enclosed_mass"] == pytest.approx(0.6827, abs=1e-4)
|
| 96 |
-
# The balance and the derivative of the enclosed mass agree to four figures.
|
| 97 |
-
assert report["enclosed_mass_rate"] == pytest.approx(report["flux_balance"], abs=1e-4)
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def test_both_sides_of_the_continuity_equation_at_x_equals_two():
|
| 101 |
-
report = vf.continuity_check()
|
| 102 |
-
assert report["density_at_two"] == pytest.approx(0.05399, abs=1e-5)
|
| 103 |
-
assert report["density_gradient_at_two"] == pytest.approx(-0.10798, abs=1e-5)
|
| 104 |
-
assert report["velocity_divergence"] == pytest.approx(LOG2, abs=1e-6)
|
| 105 |
-
assert report["continuity_time_derivative"] == pytest.approx(0.11227, abs=1e-5)
|
| 106 |
-
assert report["continuity_minus_flux_divergence"] == pytest.approx(0.11227, abs=1e-5)
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def test_log_density_along_a_trajectory_falls_by_log_two():
|
| 110 |
-
report = vf.log_density_along_trajectory()
|
| 111 |
-
assert report["log_density_rate"] == pytest.approx(-LOG2)
|
| 112 |
-
assert report["log_density_change"] == pytest.approx(-LOG2, abs=1e-6)
|
| 113 |
-
assert report["log_density_accumulated"] == pytest.approx(-LOG2)
|
| 114 |
-
assert report["prior_density_at_origin"] == pytest.approx(0.3989, abs=1e-4)
|
| 115 |
-
assert report["final_density_at_origin"] == pytest.approx(0.1995, abs=1e-4)
|
| 116 |
-
assert report["density_ratio_at_origin"] == pytest.approx(2.0, abs=1e-6)
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
# %% 04. The conditional path and the crossing (Sections 2.12 and 2.13)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def test_the_single_endpoint_pair():
|
| 123 |
-
report = fm2d.worked_pair()
|
| 124 |
-
assert report["worked_pair_path"] == [-2.0, -0.75, 0.5, 1.75, 3.0]
|
| 125 |
-
assert report["worked_pair_velocity"] == pytest.approx(5.0)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
def test_the_average_at_the_crossing():
|
| 129 |
-
report = fm2d.crossing_average()
|
| 130 |
-
assert report["crossing_state"] == pytest.approx(2.0)
|
| 131 |
-
assert report["crossing_velocities"] == [4.0, -2.0]
|
| 132 |
-
assert report["crossing_average_velocity"] == pytest.approx(1.0)
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def test_the_two_losses_differ_by_nine_at_the_crossing():
|
| 136 |
-
report = fm2d.crossing_gap()
|
| 137 |
-
assert report["crossing_conditional_loss"] == [10.0, 9.0, 10.0]
|
| 138 |
-
assert report["crossing_marginal_loss"] == [1.0, 0.0, 1.0]
|
| 139 |
-
assert report["crossing_constant_gap"] == [9.0, 9.0, 9.0]
|
| 140 |
-
assert report["crossing_target_variance"] == pytest.approx(9.0)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
# %% 05. The marginal velocity and the equivalence (Sections 2.13 and 2.15)
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
def test_the_marginal_velocity_at_the_two_ends():
|
| 147 |
-
x = torch.randn(64, 2)
|
| 148 |
-
# v_0(x) = E[X_1] - x = -x, and v_1(x) = x - E[X_0] = x, since the coupling is independent.
|
| 149 |
-
assert torch.allclose(fm2d.marginal_velocity(x, torch.zeros(64)), -x, atol=1e-5)
|
| 150 |
-
assert torch.allclose(fm2d.marginal_velocity(x, torch.ones(64)), x, atol=1e-5)
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
def test_the_marginal_velocity_transports_the_prior_onto_the_data():
|
| 154 |
-
from cis6270.metrics import mean_log_density, mode_report
|
| 155 |
-
|
| 156 |
-
torch.manual_seed(0)
|
| 157 |
-
x_t = torch.randn(512, 2) # X_0 ~ p_0.
|
| 158 |
-
for step in range(50): # Euler on the exact field, with no network anywhere.
|
| 159 |
-
t = torch.full((512,), step / 50.0)
|
| 160 |
-
x_t = x_t + fm2d.marginal_velocity(x_t, t) / 50.0
|
| 161 |
-
assert mode_report(x_t)["modes_found"] == 4
|
| 162 |
-
assert mean_log_density(x_t) > -3.0 # The data themselves measure -2.14.
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
def test_the_losses_decompose_exactly_on_any_batch():
|
| 166 |
-
from cis6270.nets import VelocityMLP
|
| 167 |
-
|
| 168 |
-
torch.manual_seed(0)
|
| 169 |
-
model = VelocityMLP(dim=2, width=32)
|
| 170 |
-
x_0, x_1 = torch.randn(256, 2), torch.randn(256, 2) + 2.0
|
| 171 |
-
t = torch.rand(256)
|
| 172 |
-
loss_cfm, loss_fm, gap, cross = fm2d.objectives(model, x_0, x_1, t)
|
| 173 |
-
# Expanding the square gives L_CFM = L_FM + cross + gap term by term, on every batch.
|
| 174 |
-
assert float(loss_cfm) == pytest.approx(float(loss_fm + cross + gap), rel=1e-5)
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
def test_the_two_objectives_share_a_gradient():
|
| 178 |
-
from cis6270.data import gaussian_mixture_2d
|
| 179 |
-
from cis6270.nets import VelocityMLP
|
| 180 |
-
|
| 181 |
-
torch.manual_seed(0)
|
| 182 |
-
model = VelocityMLP(dim=2, width=64)
|
| 183 |
-
data = gaussian_mixture_2d(4096, seed=0)
|
| 184 |
-
report = fm2d.equivalence_check(model, data, 8192)
|
| 185 |
-
assert report["loss_cfm"] == pytest.approx(report["loss_fm_plus_gap"], abs=0.3)
|
| 186 |
-
assert abs(report["loss_cross_term"]) < 0.3 # Zero in expectation, small on a large batch.
|
| 187 |
-
assert report["gradient_cosine"] > 0.99 # The residual is the batch's own sampling error.
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
# %% 06. The three interpolants (Section 2.18)
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
def test_every_schedule_reaches_the_right_endpoints():
|
| 194 |
-
for name in interp.SCHEDULES:
|
| 195 |
-
assert interp.endpoint_and_table(name)[f"{name}_endpoint_error"] == pytest.approx(0.0,
|
| 196 |
-
abs=1e-6)
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
def test_the_table_of_three_interpolants_at_one_quarter():
|
| 200 |
-
expected = { # Table 2.1, at X_0 = (2, 0), X_1 = (0, 2) and t = 0.25.
|
| 201 |
-
"linear": ([1.50, 0.50], [-2.0, 2.0]),
|
| 202 |
-
"quadratic": ([1.875, 0.125], [-1.0, 1.0]),
|
| 203 |
-
"trigonometric": ([1.848, 0.765], [-1.202, 2.902]),
|
| 204 |
-
}
|
| 205 |
-
for name, (position, velocity) in expected.items():
|
| 206 |
-
report = interp.endpoint_and_table(name)
|
| 207 |
-
assert report[f"{name}_position_at_quarter"] == pytest.approx(position, abs=1e-3)
|
| 208 |
-
assert report[f"{name}_velocity_at_quarter"] == pytest.approx(velocity, abs=1e-3)
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
def test_only_the_trigonometric_schedule_keeps_identity_covariance():
|
| 212 |
-
trig = interp.covariance_check("trigonometric", 8192, seed=0)
|
| 213 |
-
assert trig["trigonometric_coefficient_squares"] == [1.0, 1.0, 1.0, 1.0, 1.0]
|
| 214 |
-
assert trig["trigonometric_covariance_deviation"] < 0.05
|
| 215 |
-
linear = interp.covariance_check("linear", 8192, seed=0)
|
| 216 |
-
assert linear["linear_coefficient_squares"] == [1.0, 0.625, 0.5, 0.625, 1.0]
|
| 217 |
-
assert linear["linear_covariance_deviation"] > 0.4 # Cov(X_{1/2}) is half the identity.
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
def test_the_quadratic_schedule_has_covered_a_sixteenth_at_one_quarter():
|
| 221 |
-
# beta_{1/4} = (1/4)^2 = 0.0625, so the state has covered 6.25 percent of the segment.
|
| 222 |
-
_, beta, _, _ = interp.quadratic(torch.tensor([0.25]))
|
| 223 |
-
assert float(beta) == pytest.approx(0.0625)
|
| 224 |
-
_, beta_half, _, _ = interp.quadratic(torch.tensor([0.5]))
|
| 225 |
-
assert float(beta_half) == pytest.approx(0.25) # A quarter of the distance at the midpoint.
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
# %% 07. The MNIST U-Net (Section 2.20)
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
def test_the_unet_has_the_parameter_count_the_chapter_quotes():
|
| 232 |
-
from lecture_2 import flow_matching_mnist as mnist
|
| 233 |
-
|
| 234 |
-
model = mnist.FlowUNet() # The default base width of 32, so the widths are 32, 64 and 128.
|
| 235 |
-
assert sum(p.numel() for p in model.parameters()) == 471_265
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_lecture_3.py
DELETED
|
@@ -1,445 +0,0 @@
|
|
| 1 |
-
"""Lecture 3: the numbers Chapter 3 works by hand, and the invariants the code must keep.
|
| 2 |
-
|
| 3 |
-
Every assertion here is either a quantity the chapter computes in the text or a property the
|
| 4 |
-
derivation guarantees. Nothing trains, so the whole file runs in a few seconds.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
import math
|
| 8 |
-
|
| 9 |
-
import pytest
|
| 10 |
-
import torch
|
| 11 |
-
|
| 12 |
-
from cis6270.metrics import mode_report
|
| 13 |
-
from lecture_3 import (
|
| 14 |
-
ddpm, forward_diffusion, guidance, latent_diffusion, samplers, score_matching, sde,
|
| 15 |
-
)
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
# %% The forward process, Sections 3.2 through 3.10
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def test_euler_maruyama_step_matches_the_worked_numbers():
|
| 22 |
-
landed, drift, noise = sde.euler_maruyama_step(2.0, -1.0, 0.5, 0.04, 1.5)
|
| 23 |
-
assert landed == pytest.approx(2.11)
|
| 24 |
-
assert (drift, noise) == pytest.approx((-0.04, 0.15))
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def test_three_draws_average_above_the_drift_prediction():
|
| 28 |
-
endpoints = [sde.euler_maruyama_step(2.0, -1.0, 0.5, 0.04, e)[0] for e in (1.5, -0.5, 0.2)]
|
| 29 |
-
assert endpoints == pytest.approx([2.11, 1.91, 1.98])
|
| 30 |
-
assert sum(endpoints) / 3.0 == pytest.approx(2.00, abs=1e-6) # The drift alone reaches 1.96.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def test_increment_scales_as_the_square_root_of_the_step():
|
| 34 |
-
assert sde.increment_scale(0.04)["std"] == pytest.approx(0.2)
|
| 35 |
-
assert sde.increment_scale(0.02)["std"] == pytest.approx(0.1414, abs=1e-4)
|
| 36 |
-
assert sde.increment_scale(0.04, -1.0, 0.5)["noise_over_drift"] == pytest.approx(2.5)
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def test_ornstein_uhlenbeck_law_at_log_two():
|
| 40 |
-
law = sde.ornstein_uhlenbeck_law(2.0, 1.0, math.sqrt(2.0), math.log(2.0))
|
| 41 |
-
assert law["mean"] == pytest.approx(1.0)
|
| 42 |
-
assert law["variance"] == pytest.approx(0.75)
|
| 43 |
-
assert law["std"] == pytest.approx(0.8660, abs=1e-4)
|
| 44 |
-
assert law["stationary_variance"] == pytest.approx(1.0)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def test_ito_recovers_the_variance_and_the_chain_rule_alone_does_not():
|
| 48 |
-
generator = torch.Generator().manual_seed(0)
|
| 49 |
-
check = sde.check_ito_on_square(8192, 200, 0.25, generator)
|
| 50 |
-
assert check["ito_mean"] == pytest.approx(0.25, abs=0.02)
|
| 51 |
-
assert check["realized_mean_square"] == pytest.approx(0.25, abs=0.02)
|
| 52 |
-
assert abs(check["no_curvature_mean"]) < 0.02 # The curvature term carries all the growth.
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def test_fokker_planck_holds_on_the_spreading_gaussian():
|
| 56 |
-
grid = torch.linspace(-3.0, 3.0, 41)
|
| 57 |
-
for t in (0.0, 0.5, 2.0):
|
| 58 |
-
residual = sde.fokker_planck_residual(grid, torch.full_like(grid, t))
|
| 59 |
-
assert float(residual.abs().max()) < 1e-6
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def test_peak_of_the_gaussian_falls_at_the_rate_the_equation_gives():
|
| 63 |
-
peak = sde.peak_extrapolation(0.1)
|
| 64 |
-
assert peak["p_0"] == pytest.approx(0.3989, abs=1e-4)
|
| 65 |
-
assert peak["rate"] == pytest.approx(-0.1995, abs=1e-4)
|
| 66 |
-
assert peak["linear"] == pytest.approx(0.3790, abs=1e-4)
|
| 67 |
-
assert peak["exact"] == pytest.approx(0.3804, abs=1e-4)
|
| 68 |
-
assert peak["gap"] == pytest.approx(0.0014, abs=1e-4)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
# %% Score matching, Sections 3.14 through 3.17
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def test_posterior_average_at_the_running_observation():
|
| 75 |
-
report = score_matching.posterior_report(7.4, (10.0, 9.0), (0.3, 0.7), epsilon_theta=-0.8)
|
| 76 |
-
assert report["weights"] == pytest.approx([0.2156, 0.7844], abs=1e-4)
|
| 77 |
-
assert report["clean_mean"] == pytest.approx(9.2156, abs=1e-4)
|
| 78 |
-
assert report["conditional_scores"] == pytest.approx([5 / 3, -5 / 9], abs=1e-4)
|
| 79 |
-
assert report["marginal"] == pytest.approx([-0.0765, -0.0765], abs=1e-4)
|
| 80 |
-
assert report["residual"] == pytest.approx(0.835, abs=1e-3)
|
| 81 |
-
assert report["score_hat"] == pytest.approx(4 / 3, abs=1e-4)
|
| 82 |
-
assert report["x0"] == pytest.approx([9.85, 9.85, 9.2156], abs=1e-4)
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def test_one_parameter_fit_is_minimized_at_the_inverse_variance():
|
| 86 |
-
assert score_matching.one_parameter_loss(0.5, 1.0) == pytest.approx(-0.375)
|
| 87 |
-
assert score_matching.one_parameter_loss(1.0, 1.0) == pytest.approx(-0.5)
|
| 88 |
-
assert score_matching.one_parameter_loss(0.25, 4.0) == pytest.approx(-0.125)
|
| 89 |
-
grid = [0.05 * i for i in range(1, 41)]
|
| 90 |
-
best = min(grid, key=lambda a: score_matching.one_parameter_loss(a, 4.0))
|
| 91 |
-
assert best == pytest.approx(0.25) # a = 1 / v with v = 4.
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def test_the_three_objectives_share_one_minimizer():
|
| 95 |
-
generator = torch.Generator().manual_seed(1)
|
| 96 |
-
clean = score_matching.sample_clean(32768, generator)
|
| 97 |
-
epsilon = torch.randn(clean.shape, generator=generator)
|
| 98 |
-
found = score_matching.minimizers(clean, epsilon)
|
| 99 |
-
assert found["explicit"] == pytest.approx(1.0)
|
| 100 |
-
assert found["implicit"] == pytest.approx(1.0, abs=0.03)
|
| 101 |
-
assert found["denoising"] == pytest.approx(1.0, abs=0.03)
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def test_integration_by_parts_holds_on_the_exact_field():
|
| 105 |
-
generator = torch.Generator().manual_seed(2)
|
| 106 |
-
clean = score_matching.sample_clean(32768, generator)
|
| 107 |
-
points = score_matching.A_T * clean + score_matching.SIGMA_T * torch.randn(
|
| 108 |
-
clean.shape, generator=generator,
|
| 109 |
-
)
|
| 110 |
-
identity = score_matching.integration_by_parts_gap(score_matching.exact_score, points)
|
| 111 |
-
assert identity["gap"] < 0.05 * abs(identity["left"])
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def test_hutchinson_estimates_the_trace_the_exact_loop_computes():
|
| 115 |
-
generator = torch.Generator().manual_seed(3)
|
| 116 |
-
means = torch.randn(2, score_matching.TRACE_DIM, generator=generator) * 1.5
|
| 117 |
-
points = torch.randn(8, score_matching.TRACE_DIM, generator=generator)
|
| 118 |
-
|
| 119 |
-
def field(z):
|
| 120 |
-
return score_matching.high_dimensional_score(z, means, 1.0)
|
| 121 |
-
|
| 122 |
-
exact, passes = score_matching.exact_trace(field, points)
|
| 123 |
-
assert passes == score_matching.TRACE_DIM # One backward pass per coordinate.
|
| 124 |
-
estimate, draws = score_matching.hutchinson_trace(field, points, 256, generator)
|
| 125 |
-
error = float(draws.std(dim=0).mean()) / math.sqrt(256)
|
| 126 |
-
assert float((estimate - exact).mean().abs()) < 4.0 * error
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
# %% The schedule, the corruption and DDPM, Sections 3.11, 3.12, 3.19 and 3.24 through 3.28
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
def test_schedule_matches_the_continuous_curve():
|
| 133 |
-
assert float(forward_diffusion.alphas_cumprod[207]) == pytest.approx(0.6402, abs=1e-4)
|
| 134 |
-
assert float(forward_diffusion.a[207]) == pytest.approx(0.8001, abs=1e-4)
|
| 135 |
-
assert float(forward_diffusion.sigma[207]) == pytest.approx(0.5999, abs=1e-4)
|
| 136 |
-
assert forward_diffusion.continuous_signal(0.207) == pytest.approx(0.7997, abs=1e-4)
|
| 137 |
-
assert float(forward_diffusion.alphas_cumprod[1000]) == pytest.approx(4.036e-5, rel=1e-3)
|
| 138 |
-
assert float(forward_diffusion.a[1000]) == pytest.approx(0.0064, abs=1e-4)
|
| 139 |
-
assert forward_diffusion.continuous_signal(1.0) == pytest.approx(0.0066, abs=1e-4)
|
| 140 |
-
assert float(forward_diffusion.a[500]) == pytest.approx(0.2803, abs=1e-4)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
def test_every_copy_of_the_schedule_agrees():
|
| 144 |
-
"""No lecture file imports another, so each rebuilds the schedule. The copies must match."""
|
| 145 |
-
for name in ("betas", "alphas", "alphas_cumprod"):
|
| 146 |
-
for other in (ddpm, latent_diffusion):
|
| 147 |
-
assert torch.equal(getattr(forward_diffusion, name), getattr(other, name)), name
|
| 148 |
-
assert torch.equal(ddpm.posterior_variance[1:], latent_diffusion.posterior_variance[1:])
|
| 149 |
-
# The samplers read the same path off the continuous curve, which agrees at a fine grid.
|
| 150 |
-
for grid in (samplers.make_schedule(1000), guidance.schedule_tensors(1000)):
|
| 151 |
-
assert float(grid["alphas_cumprod"][207].sqrt()) == pytest.approx(0.8, abs=5e-3)
|
| 152 |
-
assert torch.allclose(grid["alphas_cumprod"], ddpm.alphas_cumprod, atol=1e-3)
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
def test_noise_levels_of_the_continuous_schedule():
|
| 156 |
-
for signal, time, rate in ((0.98, 0.0589, 1.2721), (0.8, 0.2068, 4.2157),
|
| 157 |
-
(0.35, 0.4544, 9.1420), (0.04, 0.7994, 16.0073)):
|
| 158 |
-
located = forward_diffusion.level_time(signal)
|
| 159 |
-
assert located["t"] == pytest.approx(time, abs=1e-4)
|
| 160 |
-
assert located["beta_t"] == pytest.approx(rate, abs=1e-3)
|
| 161 |
-
assert located["sigma_t"] == pytest.approx(math.sqrt(1.0 - signal ** 2), abs=1e-9)
|
| 162 |
-
assert forward_diffusion.level_time(0.8)["sigma_t"] == pytest.approx(0.6, abs=1e-4)
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
def test_dropped_elbo_weight_at_step_five_hundred():
|
| 166 |
-
weight = ddpm.elbo_weight(
|
| 167 |
-
ddpm.betas[500], ddpm.posterior_variance[500],
|
| 168 |
-
ddpm.alphas[500], ddpm.alphas_cumprod[500],
|
| 169 |
-
)
|
| 170 |
-
assert float(ddpm.posterior_variance[500]) == pytest.approx(0.01003, abs=1e-5)
|
| 171 |
-
assert float(weight) == pytest.approx(0.005508, abs=1e-6)
|
| 172 |
-
assert ddpm.elbo_weight(0.2, 1 / 9, 0.8, 0.64) == pytest.approx(0.625, abs=1e-6)
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
def test_one_pixel_through_the_forward_code():
|
| 176 |
-
a_k = float(forward_diffusion.a[207])
|
| 177 |
-
sigma_k = float(forward_diffusion.sigma[207])
|
| 178 |
-
assert a_k * 0.5 + sigma_k * (-1.0) == pytest.approx(-0.1998, abs=1e-4)
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
def test_q_sample_uses_the_supplied_noise_exactly():
|
| 182 |
-
x_0 = torch.randn(4, 1, 28, 28)
|
| 183 |
-
noise = torch.randn_like(x_0)
|
| 184 |
-
k = torch.tensor([1, 207, 500, 1000])
|
| 185 |
-
for module in (forward_diffusion, ddpm):
|
| 186 |
-
x_k, returned = module.q_sample(x_0, k, noise)
|
| 187 |
-
abar = module.alphas_cumprod[k].view(-1, 1, 1, 1)
|
| 188 |
-
assert torch.allclose(x_k, abar.sqrt() * x_0 + (1 - abar).sqrt() * noise, atol=1e-6)
|
| 189 |
-
assert returned is noise
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
def test_corruption_law_matches_the_gaussian_it_is_drawn_from():
|
| 193 |
-
generator = torch.Generator().manual_seed(0)
|
| 194 |
-
x_0 = torch.randn(1, 1, 28, 28, generator=generator)
|
| 195 |
-
levels = [1, 207, 500, 1000]
|
| 196 |
-
law = forward_diffusion.measure_law(x_0, levels, 1024, generator)
|
| 197 |
-
assert law["mean_gap"] < 0.05 # Centered on a_k x_0, to one over the square root of 1024.
|
| 198 |
-
assert law["variance_gap"] < 0.05 # Spread sigma_k^2.
|
| 199 |
-
# Four times the draws halves the gap, which a systematic error in a coefficient would not.
|
| 200 |
-
finer = forward_diffusion.measure_law(x_0, levels, 4096, generator)
|
| 201 |
-
assert finer["mean_gap"] < 0.6 * law["mean_gap"]
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
def test_worked_training_batch_at_abar_point_eight():
|
| 205 |
-
batch = forward_diffusion.worked_batch(
|
| 206 |
-
0.8, (2.0, -1.0, 0.5), (0.5, -0.2, 1.0), (0.4, -0.1, 0.8),
|
| 207 |
-
)
|
| 208 |
-
assert batch["a_k"] == pytest.approx(0.8944, abs=1e-4)
|
| 209 |
-
assert batch["sigma_k"] == pytest.approx(0.4472, abs=1e-4)
|
| 210 |
-
assert batch["x_k"] == pytest.approx([2.0125, -0.9839, 0.8944], abs=1e-3)
|
| 211 |
-
assert batch["loss"] == pytest.approx(0.02, abs=1e-6)
|
| 212 |
-
assert batch["score_targets"] == pytest.approx([-1.118, 0.447, -2.236], abs=1e-3)
|
| 213 |
-
assert batch["x_0_hat"] == pytest.approx([2.05, -1.05, 0.60], abs=1e-2)
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
def test_two_step_reverse_transition_agrees_in_both_forms():
|
| 217 |
-
step = ddpm.two_step_transition(0.2, 7.4, -1.0, 0.3)
|
| 218 |
-
assert step["x_0_hat"] == pytest.approx(10.0, abs=1e-6)
|
| 219 |
-
assert step["mean_from_noise"] == pytest.approx(8.646, abs=1e-3)
|
| 220 |
-
assert step["mean_from_clean"] == pytest.approx(step["mean_from_noise"], abs=1e-6)
|
| 221 |
-
assert step["weights"] == pytest.approx([0.4969, 0.4969], abs=1e-4)
|
| 222 |
-
assert step["variance"] == pytest.approx(1 / 9)
|
| 223 |
-
assert step["x_{k-1}"] == pytest.approx(8.746, abs=1e-3)
|
| 224 |
-
assert step["elbo_weight"] == pytest.approx(0.625, abs=1e-6)
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
def test_unet_keeps_the_image_shape_and_the_last_reverse_step_adds_no_noise():
|
| 228 |
-
torch.manual_seed(0)
|
| 229 |
-
model = ddpm.UNet(in_channels=1, base_channels=8, time_embed_dim=32)
|
| 230 |
-
x = torch.randn(2, 1, 28, 28)
|
| 231 |
-
k = torch.tensor([1, 1])
|
| 232 |
-
assert model(x, k).shape == x.shape
|
| 233 |
-
first, second = ddpm.p_sample(model, x, k), ddpm.p_sample(model, x, k)
|
| 234 |
-
assert torch.allclose(first, second) # keep = 0 at k = 1, so the step is the mean.
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
# %% The three samplers, Sections 3.22, 3.26, 3.32, 3.34 and 3.35
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
def test_one_backward_step_under_both_dynamics():
|
| 241 |
-
step = samplers.one_step_comparison(1.2, 2.0, 0.5, 0.8, 0.1)
|
| 242 |
-
assert step["score"] == pytest.approx(-1.6)
|
| 243 |
-
assert step["velocity"] == pytest.approx(0.4)
|
| 244 |
-
assert step["ode_next"] == pytest.approx(1.16)
|
| 245 |
-
assert step["sde_drift"] == pytest.approx(2.0)
|
| 246 |
-
assert step["sde_mean"] == pytest.approx(1.0)
|
| 247 |
-
assert step["sde_noise_std"] == pytest.approx(0.4472, abs=1e-4)
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
def test_langevin_step_from_the_worked_state():
|
| 251 |
-
assert samplers.langevin_step(2.0, -2.0, 0.01, 0.5) == pytest.approx(2.0507, abs=1e-4)
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
def test_schedule_stays_a_valid_discretization_at_any_grid_size():
|
| 255 |
-
for num_steps in (20, 200, 1000):
|
| 256 |
-
schedule = samplers.make_schedule(num_steps)
|
| 257 |
-
assert float(schedule["betas"][1:].max()) < 1.0
|
| 258 |
-
assert float(schedule["alphas_cumprod"][0]) == pytest.approx(1.0)
|
| 259 |
-
assert float(schedule["alphas_cumprod"][num_steps]) < 1e-4 # Terminal law is Gaussian.
|
| 260 |
-
thousand = samplers.make_schedule(1000)
|
| 261 |
-
assert float(thousand["alphas_cumprod"][207].sqrt()) == pytest.approx(0.8, abs=5e-3)
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
def test_every_sampler_finds_all_four_modes():
|
| 265 |
-
budget = 60
|
| 266 |
-
for name, points in (
|
| 267 |
-
("ancestral", samplers.ancestral_sample(
|
| 268 |
-
samplers.ExactScore(), samplers.make_schedule(budget), 512,
|
| 269 |
-
torch.Generator().manual_seed(0))),
|
| 270 |
-
("flow", samplers.probability_flow_sample(
|
| 271 |
-
samplers.ExactScore(), 512, budget, torch.Generator().manual_seed(0))),
|
| 272 |
-
("langevin", samplers.annealed_langevin_sample(
|
| 273 |
-
samplers.ExactScore(), 512, [0.7 - 0.69 * i / 19 for i in range(20)], 3, 0.15,
|
| 274 |
-
torch.Generator().manual_seed(0))),
|
| 275 |
-
):
|
| 276 |
-
measured = mode_report(points)
|
| 277 |
-
assert measured["modes_found"] == 4, name
|
| 278 |
-
assert measured["mode_entropy"] > 1.30, name # log 4 = 1.386 when the modes are even.
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
def test_each_sampler_reports_the_evaluations_it_spent():
|
| 282 |
-
score = samplers.ExactScore()
|
| 283 |
-
samplers.probability_flow_sample(score, 64, 37, torch.Generator().manual_seed(0))
|
| 284 |
-
assert score.calls == 37
|
| 285 |
-
score = samplers.ExactScore()
|
| 286 |
-
samplers.annealed_langevin_sample(score, 64, [0.5, 0.2, 0.05], 4, 0.1,
|
| 287 |
-
torch.Generator().manual_seed(0))
|
| 288 |
-
assert score.calls == 12 # Three levels, four iterations each.
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
# %% Guidance, Sections 3.37 through 3.45
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
def test_worked_guided_score():
|
| 295 |
-
score, gradient = torch.tensor([[-1.0, -0.1]]), torch.tensor([[1.3, 1.1]])
|
| 296 |
-
at_one = guidance.guided_score(score, gradient, 1.0)[0]
|
| 297 |
-
at_two = guidance.guided_score(score, gradient, 2.0)[0]
|
| 298 |
-
assert at_one.tolist() == pytest.approx([0.3, 1.0], abs=1e-6)
|
| 299 |
-
assert float(at_one.norm()) == pytest.approx(1.0440, abs=1e-4)
|
| 300 |
-
assert at_two.tolist() == pytest.approx([1.6, 2.1], abs=1e-6)
|
| 301 |
-
assert float(at_two.norm()) == pytest.approx(2.6401, abs=1e-4)
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
def test_classifier_free_step_and_the_score_it_implies():
|
| 305 |
-
eps = guidance.classifier_free_field(torch.tensor(0.50), torch.tensor(0.10), 2.0)
|
| 306 |
-
assert float(eps) == pytest.approx(-0.30, abs=1e-6)
|
| 307 |
-
assert float(-eps / 0.5) == pytest.approx(0.6, abs=1e-6)
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
def test_guided_backward_step_agrees_in_both_parameterizations():
|
| 311 |
-
guided_eps = float(guidance.guided_noise(torch.tensor(0.8), torch.tensor(0.6), 2.0, 0.5))
|
| 312 |
-
assert guided_eps == pytest.approx(0.2, abs=1e-6)
|
| 313 |
-
velocity = -0.5 * 2.0 * (1.2 + (-guided_eps / 0.5)) # The collected form of the velocity.
|
| 314 |
-
assert velocity == pytest.approx(-0.8, abs=1e-6)
|
| 315 |
-
assert 1.2 - 0.1 * velocity == pytest.approx(1.28, abs=1e-6)
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
def test_classifier_gradient_equals_the_difference_of_the_two_fields():
|
| 319 |
-
points = torch.randn(64, 2, generator=torch.Generator().manual_seed(0))
|
| 320 |
-
gradient = guidance.classifier_gradient(points, 0.8, 0)
|
| 321 |
-
difference = (guidance.conditional_score(points, 0.8, 0)
|
| 322 |
-
- guidance.unconditional_score(points, 0.8))
|
| 323 |
-
assert torch.allclose(gradient, difference, atol=1e-6)
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
def test_weights_are_projected_onto_the_simplex():
|
| 327 |
-
weights = guidance.normalize_weights((-1.0, 3.0, 1.0), torch.device("cpu"))
|
| 328 |
-
assert float(weights.sum()) == pytest.approx(1.0)
|
| 329 |
-
assert float(weights[0]) == 0.0 # A negative weight is clamped away.
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
def test_scalarized_rewards_reverse_the_ranking_with_the_weights():
|
| 333 |
-
first, second = (1.2, 0.2), (0.5, 1.1)
|
| 334 |
-
assert guidance.scalarize(first, (0.7, 0.3)) == pytest.approx(0.90, abs=1e-4)
|
| 335 |
-
assert guidance.scalarize(second, (0.7, 0.3)) == pytest.approx(0.68, abs=1e-4)
|
| 336 |
-
assert guidance.scalarize(first, (0.3, 0.7)) == pytest.approx(0.50, abs=1e-4)
|
| 337 |
-
assert guidance.scalarize(second, (0.3, 0.7)) == pytest.approx(0.92, abs=1e-4)
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
def test_competing_gradients_cancel_at_the_stated_weights():
|
| 341 |
-
combined = 0.6 * torch.tensor([1.0, 0.8]) + 0.4 * torch.tensor([0.5, -1.2])
|
| 342 |
-
assert combined.tolist() == pytest.approx([0.80, 0.0], abs=1e-6)
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
def test_selection_weights_and_the_guidance_schedule():
|
| 346 |
-
weights = (2.0 * torch.tensor([1.2, 0.8, 0.3])).softmax(0)
|
| 347 |
-
assert weights.tolist() == pytest.approx([0.6193, 0.2783, 0.1024], abs=1e-4)
|
| 348 |
-
assert guidance.schedule_value(0.6) == pytest.approx(0.96)
|
| 349 |
-
assert guidance.schedule_value(0.0) == 0.0 and guidance.schedule_value(1.0) == 0.0
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
def test_guidance_moves_mass_onto_the_target_mode():
|
| 353 |
-
schedule = guidance.schedule_tensors(60)
|
| 354 |
-
fractions = []
|
| 355 |
-
for w in (0.0, 0.25, 1.0):
|
| 356 |
-
points = guidance.sample_diffusion(
|
| 357 |
-
schedule, 512, guidance.TARGET_LABEL, w=w,
|
| 358 |
-
generator=torch.Generator().manual_seed(0),
|
| 359 |
-
)
|
| 360 |
-
fractions.append(mode_report(points)["mode_fractions"][guidance.TARGET_LABEL])
|
| 361 |
-
assert fractions[0] < 0.35 # Unconditional, so about a quarter.
|
| 362 |
-
assert fractions[0] < fractions[1] < fractions[2]
|
| 363 |
-
assert fractions[2] > 0.9
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
def test_reward_guidance_prefers_the_right_hand_modes():
|
| 367 |
-
schedule = guidance.schedule_tensors(60)
|
| 368 |
-
points = guidance.sample_diffusion(
|
| 369 |
-
schedule, 512, guidance.TARGET_LABEL, eta=2.0, lambdas=(1.0, 0.0),
|
| 370 |
-
generator=torch.Generator().manual_seed(0),
|
| 371 |
-
)
|
| 372 |
-
fractions = mode_report(points)["mode_fractions"]
|
| 373 |
-
assert fractions[0] + fractions[1] > 0.9 # The two modes at x = +2.
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
# %% Latent diffusion, Sections 3.21, 3.29 and 3.46
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
def test_latent_coordinate_corrupted_and_carried_back():
|
| 380 |
-
abar = float(latent_diffusion.alphas_cumprod[500])
|
| 381 |
-
a_500, sigma_500 = math.sqrt(abar), math.sqrt(1.0 - abar)
|
| 382 |
-
assert (a_500, sigma_500) == pytest.approx((0.2803, 0.9599), abs=1e-4)
|
| 383 |
-
z_500 = a_500 * 1.2 + sigma_500 * 0.4
|
| 384 |
-
assert z_500 == pytest.approx(0.7204, abs=1e-4)
|
| 385 |
-
eps_hat = (z_500 - a_500 * 1.05) / sigma_500
|
| 386 |
-
assert eps_hat == pytest.approx(0.4438, abs=1e-4)
|
| 387 |
-
beta_k, alpha_k = float(latent_diffusion.betas[500]), float(latent_diffusion.alphas[500])
|
| 388 |
-
mean = (z_500 - beta_k / sigma_500 * eps_hat) / math.sqrt(alpha_k)
|
| 389 |
-
assert mean == pytest.approx(0.7193, abs=1e-4)
|
| 390 |
-
variance = float(latent_diffusion.posterior_variance[500])
|
| 391 |
-
assert math.sqrt(variance) == pytest.approx(0.1002, abs=1e-4)
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
def test_masked_corruption_leaves_padding_at_zero():
|
| 395 |
-
z0 = torch.randn(3, 8, 16)
|
| 396 |
-
mask = torch.ones(3, 8)
|
| 397 |
-
mask[:, 5:] = 0.0 # The last three positions are padding.
|
| 398 |
-
z_k, noise = latent_diffusion.q_sample_latent(z0, torch.tensor([1, 500, 1000]), mask)
|
| 399 |
-
assert torch.count_nonzero(z_k[:, 5:, :]) == 0
|
| 400 |
-
assert noise.shape == z0.shape
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
def test_latent_loss_counts_valid_residues_only():
|
| 404 |
-
z0 = torch.zeros(2, 4, 3)
|
| 405 |
-
z0_hat = torch.ones(2, 4, 3) # Unit error at every coordinate.
|
| 406 |
-
mask = torch.tensor([[1.0, 1.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]])
|
| 407 |
-
assert float(latent_diffusion.latent_loss(z0_hat, z0, mask)) == pytest.approx(1.0)
|
| 408 |
-
z0_hat[:, 2:, :] = 100.0 # A large error, but only on padding.
|
| 409 |
-
assert float(latent_diffusion.latent_loss(z0_hat, z0, mask)) == pytest.approx(1.0)
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
def test_denoiser_returns_a_latent_of_the_same_shape():
|
| 413 |
-
torch.manual_seed(0)
|
| 414 |
-
model = latent_diffusion.LatentDenoiser(embed_dim=16, num_layers=1, num_heads=2, max_len=8)
|
| 415 |
-
z_k = torch.randn(2, 8, 16)
|
| 416 |
-
mask = torch.ones(2, 8)
|
| 417 |
-
assert model(z_k, torch.tensor([7, 900]), mask).shape == z_k.shape
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
def test_composition_proxies_on_a_known_peptide():
|
| 421 |
-
charge, polar = latent_diffusion.composition_proxies("KRDE")
|
| 422 |
-
assert charge == pytest.approx(0.0) # Two positive and two negative over four residues.
|
| 423 |
-
assert polar == pytest.approx(1.0) # All four are in the polar set.
|
| 424 |
-
assert latent_diffusion.composition_proxies("AAAA") == pytest.approx((0.0, 0.0))
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
def test_enforce_polar_reaches_the_minimum_at_the_cheapest_positions():
|
| 428 |
-
torch.manual_seed(0)
|
| 429 |
-
logits = torch.randn(2, 12, 20)
|
| 430 |
-
logits[:, :, latent_diffusion.POLAR_COLUMNS] -= 5.0 # Make the argmax non-polar everywhere.
|
| 431 |
-
choices = logits.argmax(dim=-1)
|
| 432 |
-
sequences = latent_diffusion.enforce_polar(logits, choices, 5)
|
| 433 |
-
for sequence in sequences:
|
| 434 |
-
assert len(sequence) == 12
|
| 435 |
-
assert sum(a in latent_diffusion.POLAR_RESIDUES for a in sequence) >= 5
|
| 436 |
-
unchanged = latent_diffusion.enforce_polar(logits, choices, 0) # Nothing is missing.
|
| 437 |
-
assert unchanged == ["".join(latent_diffusion.AMINO_ACIDS[int(i)] for i in row)
|
| 438 |
-
for row in choices]
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
def test_the_cheapest_two_substitutions_cost_what_the_notes_says():
|
| 442 |
-
costs = torch.tensor([0.9, 1.4, 2.6, 3.1, 3.3, 4.0, 4.2, 5.5, 6.1])
|
| 443 |
-
cheapest, _ = costs.topk(2, largest=False)
|
| 444 |
-
assert cheapest.tolist() == pytest.approx([0.9, 1.4])
|
| 445 |
-
assert float(cheapest.sum()) == pytest.approx(2.3, abs=1e-5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_lecture_4.py
DELETED
|
@@ -1,535 +0,0 @@
|
|
| 1 |
-
"""Lecture 4: the worked numbers of Chapter 4, asserted against the files that print them.
|
| 2 |
-
|
| 3 |
-
Every assertion here is a number the chapter computes by hand. The tests import the lecture files
|
| 4 |
-
directly, so they check the same functions the scripts run, and none of them trains anything.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
import importlib.util
|
| 10 |
-
import math
|
| 11 |
-
import sys
|
| 12 |
-
from pathlib import Path
|
| 13 |
-
|
| 14 |
-
import pytest
|
| 15 |
-
import torch
|
| 16 |
-
|
| 17 |
-
LECTURE = Path(__file__).resolve().parents[1] / "lecture_4"
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def load(name: str):
|
| 21 |
-
"""Import one lecture file by path, since lecture_4 is a folder of scripts and not a package."""
|
| 22 |
-
spec = importlib.util.spec_from_file_location(f"lecture_4_{name}", LECTURE / f"{name}.py")
|
| 23 |
-
module = importlib.util.module_from_spec(spec)
|
| 24 |
-
sys.modules[spec.name] = module
|
| 25 |
-
spec.loader.exec_module(module)
|
| 26 |
-
return module
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
ctmc = load("ctmc")
|
| 30 |
-
mdlm = load("mdlm")
|
| 31 |
-
uniform = load("uniform")
|
| 32 |
-
d3pm = load("d3pm")
|
| 33 |
-
sedd = load("sedd")
|
| 34 |
-
block = load("block")
|
| 35 |
-
guidance = load("guidance")
|
| 36 |
-
planner = load("planner")
|
| 37 |
-
search = load("search")
|
| 38 |
-
simplex = load("simplex")
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
# %% 01. ctmc: rates, the master equation, the exponential and the reversal
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def test_two_rules_and_the_dna_row():
|
| 45 |
-
"""A jumps to C at rate 2 and to G and T at rate 1, so its diagonal entry is -4."""
|
| 46 |
-
assert ctmc.obeys_the_two_rules(ctmc.DNA_RATES)
|
| 47 |
-
assert float(ctmc.DNA_RATES[0, 0]) == pytest.approx(-4.0)
|
| 48 |
-
assert float(ctmc.exit_rates(ctmc.DNA_RATES)[0]) == pytest.approx(4.0)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def test_short_interval_row_and_the_step_bound():
|
| 52 |
-
"""P_{t,t+0.1}(A, .) = (0.6, 0.2, 0.1, 0.1), and the step must satisfy h <= 0.25."""
|
| 53 |
-
row = ctmc.short_interval_row(ctmc.DNA_RATES, 0, 0.1)
|
| 54 |
-
assert [round(float(v), 4) for v in row] == [0.6, 0.2, 0.1, 0.1]
|
| 55 |
-
assert math.exp(-0.4) == pytest.approx(0.6703, abs=5e-5)
|
| 56 |
-
assert ctmc.largest_valid_step(ctmc.DNA_RATES) == pytest.approx(0.25)
|
| 57 |
-
assert float(ctmc.short_interval_row(ctmc.DNA_RATES, 0, 0.3)[0]) == pytest.approx(-0.2)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def test_waiting_time_from_the_chapters_uniform():
|
| 61 |
-
"""Inverting S(s) = exp(-4s) at u = 0.42 gives a wait of 0.2169."""
|
| 62 |
-
assert -math.log(0.42) / 4.0 == pytest.approx(0.2169, abs=5e-5)
|
| 63 |
-
assert math.log(2.0) / 4.0 == pytest.approx(0.1733, abs=5e-5)
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def test_master_equation_on_four_dna_probabilities():
|
| 67 |
-
"""p_dot = (0.6, 0.2, -0.2, -0.6) and one Euler step gives (0.16, 0.22, 0.28, 0.34)."""
|
| 68 |
-
derivative = ctmc.master_equation(ctmc.START, ctmc.UNIT_RATES)
|
| 69 |
-
assert [round(float(v), 4) for v in derivative] == [0.6, 0.2, -0.2, -0.6]
|
| 70 |
-
arriving, leaving = ctmc.arrivals_and_departures(ctmc.START, ctmc.UNIT_RATES)
|
| 71 |
-
assert float(arriving[0]) == pytest.approx(0.9)
|
| 72 |
-
assert float(leaving[0]) == pytest.approx(0.3)
|
| 73 |
-
stepped = ctmc.START + 0.1 * derivative
|
| 74 |
-
assert [round(float(v), 4) for v in stepped] == [0.16, 0.22, 0.28, 0.34]
|
| 75 |
-
assert float(derivative.sum()) == pytest.approx(0.0, abs=1e-12)
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def test_exact_evolution_matches_the_matrix_exponential():
|
| 79 |
-
"""1/4 + (p_0 - 1/4) exp(-4t) agrees with exp(tR) at t = 0.1 and at t = 1."""
|
| 80 |
-
for t, expected in ((0.1, [0.1495, 0.2165, 0.2835, 0.3505]),
|
| 81 |
-
(1.0, [0.2473, 0.2491, 0.2509, 0.2527])):
|
| 82 |
-
exact = ctmc.exact_evolution(ctmc.START, t)
|
| 83 |
-
assert [round(float(v), 4) for v in exact] == expected
|
| 84 |
-
through_matrix = ctmc.START @ ctmc.transition_matrix(ctmc.UNIT_RATES, t)
|
| 85 |
-
assert float((exact - through_matrix).abs().max()) == pytest.approx(0.0, abs=1e-12)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def test_the_matrix_exponential_worked_for_dna():
|
| 89 |
-
"""At tau = log 2 the uniform kernel is 0.625 on the diagonal and 0.125 elsewhere."""
|
| 90 |
-
pi = torch.full((4,), 0.25, dtype=torch.float64)
|
| 91 |
-
tau = math.log(2.0)
|
| 92 |
-
kernel = ctmc.transition_matrix(ctmc.r_pi(pi), tau)
|
| 93 |
-
assert float(kernel[0, 0]) == pytest.approx(0.625, abs=1e-12)
|
| 94 |
-
assert float(kernel[0, 1]) == pytest.approx(0.125, abs=1e-12)
|
| 95 |
-
assert float(kernel.sum(dim=1).min()) == pytest.approx(1.0)
|
| 96 |
-
closed = ctmc.pi_kernel(pi, math.exp(-tau))
|
| 97 |
-
assert float((kernel - closed).abs().max()) == pytest.approx(0.0, abs=1e-12)
|
| 98 |
-
euler = {n: float(ctmc.euler_product(ctmc.r_pi(pi), tau, n)[0, 0]) for n in (1, 2, 10)}
|
| 99 |
-
assert [round(euler[n], 3) for n in (1, 2, 10)] == [0.480, 0.570, 0.616]
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def test_masking_kernel_is_absorbing():
|
| 103 |
-
"""At alpha = 1/2 the masking kernel keeps each base with 0.5 and sends 0.5 to the mask."""
|
| 104 |
-
mask_pi = torch.zeros(5, dtype=torch.float64)
|
| 105 |
-
mask_pi[4] = 1.0
|
| 106 |
-
kernel = ctmc.pi_kernel(mask_pi, 0.5)
|
| 107 |
-
assert float(kernel[0, 0]) == pytest.approx(0.5)
|
| 108 |
-
assert float(kernel[0, 4]) == pytest.approx(0.5)
|
| 109 |
-
assert float(kernel[4, 4]) == pytest.approx(1.0)
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def test_composition_eigenvalues_and_detailed_balance():
|
| 113 |
-
"""Intervals compose, R_pi has eigenvalues 0 and -1, and the cycle breaks detailed balance."""
|
| 114 |
-
whole = ctmc.transition_matrix(ctmc.UNIT_RATES, 0.7)
|
| 115 |
-
halves = ctmc.transition_matrix(ctmc.UNIT_RATES, 0.3) @ ctmc.transition_matrix(
|
| 116 |
-
ctmc.UNIT_RATES, 0.4)
|
| 117 |
-
assert float((whole - halves).abs().max()) == pytest.approx(0.0, abs=1e-12)
|
| 118 |
-
pi = torch.full((4,), 0.25, dtype=torch.float64)
|
| 119 |
-
eigenvalues = sorted(round(float(v.real), 6) for v in torch.linalg.eigvals(ctmc.r_pi(pi)))
|
| 120 |
-
assert eigenvalues == [-1.0, -1.0, -1.0, 0.0]
|
| 121 |
-
assert float((pi @ ctmc.r_pi(pi)).abs().max()) == pytest.approx(0.0, abs=1e-12)
|
| 122 |
-
assert ctmc.detailed_balance_gap(ctmc.r_pi(pi), pi) == pytest.approx(0.0, abs=1e-12)
|
| 123 |
-
cycle_pi = torch.full((3,), 1.0 / 3.0, dtype=torch.float64)
|
| 124 |
-
assert float((cycle_pi @ ctmc.CYCLE).abs().max()) == pytest.approx(0.0, abs=1e-12)
|
| 125 |
-
assert ctmc.detailed_balance_gap(ctmc.CYCLE, cycle_pi) == pytest.approx(1.0 / 3.0)
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
def test_time_reversal_retraces_the_marginals():
|
| 129 |
-
"""With p_t = (0.5, 0.25, 0.125, 0.125) the reverse rate out of C toward A is 2."""
|
| 130 |
-
marginal = torch.tensor([0.5, 0.25, 0.125, 0.125], dtype=torch.float64)
|
| 131 |
-
backward = ctmc.reverse_rates(ctmc.UNIT_RATES, marginal)
|
| 132 |
-
assert float(backward[1, 0]) == pytest.approx(2.0)
|
| 133 |
-
assert float(backward[1, 2]) == pytest.approx(0.5)
|
| 134 |
-
assert float(ctmc.exit_rates(backward)[1]) == pytest.approx(3.0)
|
| 135 |
-
net = marginal @ backward + marginal @ ctmc.UNIT_RATES
|
| 136 |
-
assert float(net.abs().max()) == pytest.approx(0.0, abs=1e-12)
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
# %% 02. mdlm: the corruption, the loss and the sampler
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
def test_reverse_step_worked():
|
| 143 |
-
"""At alpha_s = 0.7 and alpha_t = 0.4 the reveal probability is 0.5."""
|
| 144 |
-
assert mdlm.reveal_probability(0.7, 0.4) == pytest.approx(0.5)
|
| 145 |
-
law = mdlm.reverse_step_distribution(0.7, 0.4, [0.6, 0.4, 0.0, 0.0])
|
| 146 |
-
assert [round(v, 4) for v in law] == [0.3, 0.2, 0.0, 0.0, 0.5]
|
| 147 |
-
assert sum(law) == pytest.approx(1.0)
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
def test_one_term_of_the_loss_worked():
|
| 151 |
-
"""w(0.5) = 2 and the masked cross-entropy of 1.04982 give a weighted term of 2.09964."""
|
| 152 |
-
assert mdlm.reveal_rate(0.5) == pytest.approx(2.0)
|
| 153 |
-
masked = -math.log(0.7) - math.log(0.5)
|
| 154 |
-
assert masked == pytest.approx(1.04982, abs=5e-6)
|
| 155 |
-
assert 2.0 * masked == pytest.approx(2.09964, abs=5e-6)
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
def test_the_rate_error_collapses_to_the_cross_entropy():
|
| 159 |
-
"""At a masked position the rate errors sum to w(t) times the negative log probability."""
|
| 160 |
-
rates = [2.0 * d for d in (0.2, 0.6, 0.1, 0.1)]
|
| 161 |
-
assert [round(v, 4) for v in rates] == [0.4, 1.2, 0.2, 0.2]
|
| 162 |
-
total = mdlm.rate_error(2.0, rates[1]) + sum(
|
| 163 |
-
mdlm.rate_error(0.0, r) for i, r in enumerate(rates) if i != 1)
|
| 164 |
-
assert total == pytest.approx(2.0 * -math.log(0.6), abs=1e-9)
|
| 165 |
-
assert total == pytest.approx(1.0217, abs=5e-5)
|
| 166 |
-
assert 2.0 * -math.log(0.4) == pytest.approx(1.8326, abs=5e-5)
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
def test_corruption_and_sampling_shapes():
|
| 170 |
-
"""The corruption keeps or masks, and the sampler returns clean DNA with no mask left."""
|
| 171 |
-
torch.manual_seed(0)
|
| 172 |
-
x0 = torch.randint(0, 4, (8, 6))
|
| 173 |
-
zt = mdlm.corrupt(x0, torch.full((8, 1), 0.5))
|
| 174 |
-
assert bool(((zt == x0) | (zt == mdlm.MASK)).all())
|
| 175 |
-
kept = mdlm.corrupt(x0, torch.zeros(8, 1)).eq(x0).to(torch.get_default_dtype())
|
| 176 |
-
assert float(kept.mean()) == 1.0
|
| 177 |
-
assert int(mdlm.corrupt(x0, torch.ones(8, 1)).eq(mdlm.MASK).all())
|
| 178 |
-
model = _tiny_denoiser(5, 6)
|
| 179 |
-
drawn = mdlm.sample(model, 4, 6, steps=4)
|
| 180 |
-
assert drawn.shape == (4, 6) and int(drawn.max()) < 4
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
def _tiny_denoiser(vocab: int, length: int, num_classes: int = 0):
|
| 184 |
-
"""A very small SequenceDenoiser, so the sampler tests cost nothing."""
|
| 185 |
-
from cis6270.nets import SequenceDenoiser
|
| 186 |
-
|
| 187 |
-
torch.manual_seed(0)
|
| 188 |
-
return SequenceDenoiser(vocab, length, width=16, depth=1, heads=2, num_classes=num_classes)
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
# %% 03. uniform: the posterior average of 2, and the rate error that keeps its staying term
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
def test_the_uniform_kernel_and_the_clean_factor():
|
| 195 |
-
"""At alpha_t = 1/2 the kernel is 0.625 at the clean base and 0.125 elsewhere."""
|
| 196 |
-
assert float(uniform.kernel(0.5)[0, 0]) == pytest.approx(0.625)
|
| 197 |
-
assert float(uniform.kernel(0.5)[0, 1]) == pytest.approx(0.125)
|
| 198 |
-
assert uniform.clean_factor(0.5) == pytest.approx(5.0)
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
def test_the_worked_posterior_average_is_two():
|
| 202 |
-
"""The chapter's posterior average of the conditional reverse rates on one base comes out 2."""
|
| 203 |
-
prior = torch.tensor([0.75, 0.25, 0.0, 0.0], dtype=torch.float64)
|
| 204 |
-
posterior = uniform.posterior_over_clean(0.5, 1, prior)
|
| 205 |
-
assert [round(float(v), 4) for v in posterior] == [0.375, 0.625, 0.0, 0.0]
|
| 206 |
-
assert float(uniform.conditional_rates(0.5, current=1, clean=0)[0]) == pytest.approx(5.0)
|
| 207 |
-
assert float(uniform.conditional_rates(0.5, current=1, clean=1)[0]) == pytest.approx(0.2)
|
| 208 |
-
assert uniform.marginal_reverse_rate(0.5, 1, 0, prior) == pytest.approx(2.0)
|
| 209 |
-
assert uniform.marginal_reverse_rate(0.5, 1, 2, prior) == pytest.approx(0.5)
|
| 210 |
-
marginal = prior @ uniform.kernel(0.5)
|
| 211 |
-
assert [round(float(v), 4) for v in marginal] == [0.5, 0.25, 0.125, 0.125]
|
| 212 |
-
assert float(marginal[0] / marginal[1]) == pytest.approx(2.0)
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
def test_the_rate_error_on_one_base():
|
| 216 |
-
"""A denoiser of (0.5, 0.3, 0.1, 0.1) gives rates 2.76 and 1.16 and a rate error of 0.754."""
|
| 217 |
-
denoiser = torch.tensor([[[0.5, 0.3, 0.1, 0.1]]], dtype=torch.float64)
|
| 218 |
-
current = torch.tensor([[1]])
|
| 219 |
-
alpha = torch.tensor([[0.5]], dtype=torch.float64)
|
| 220 |
-
rates = uniform.model_rates(denoiser, uniform.ratio_table(current, alpha))[0, 0]
|
| 221 |
-
rates = rates / (uniform.BETA / 4) # Quote the rates in the chapter's unit forward rate.
|
| 222 |
-
assert [round(float(v), 4) for v in rates] == [2.76, 1.0, 1.16, 1.16]
|
| 223 |
-
target = uniform.conditional_rates(0.5, current=1, clean=0)
|
| 224 |
-
error = float(uniform.rate_error(target[[0, 2, 3]], rates[[0, 2, 3]]).sum())
|
| 225 |
-
assert error == pytest.approx(0.754, abs=5e-4)
|
| 226 |
-
assert float(uniform.rate_error(torch.tensor(5.0), torch.tensor(2.76))) == pytest.approx(
|
| 227 |
-
0.731, abs=5e-4)
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
def test_the_rate_error_is_minimized_at_the_posterior_average():
|
| 231 |
-
"""0.375 l(5, B) + 0.625 l(0.2, B) is smallest at B = 2, where it equals 1.430."""
|
| 232 |
-
def mixture(b: float) -> float:
|
| 233 |
-
return float(0.375 * uniform.rate_error(torch.tensor(5.0), torch.tensor(b))
|
| 234 |
-
+ 0.625 * uniform.rate_error(torch.tensor(0.2), torch.tensor(b)))
|
| 235 |
-
|
| 236 |
-
assert mixture(2.0) == pytest.approx(1.430, abs=5e-4)
|
| 237 |
-
assert mixture(5.0) == pytest.approx(2.598, abs=5e-4)
|
| 238 |
-
assert mixture(0.2) == pytest.approx(4.235, abs=5e-4)
|
| 239 |
-
assert mixture(2.0) < min(mixture(1.9), mixture(2.1))
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
# %% 04. d3pm: the finite step
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
def test_cumulative_transition_matrix():
|
| 246 |
-
"""With beta_1 = 0.2 and beta_2 = 0.3 the product is 0.67 on the diagonal and 0.11 off it."""
|
| 247 |
-
product = d3pm.one_step(0.2) @ d3pm.one_step(0.3)
|
| 248 |
-
assert float(product[0, 0]) == pytest.approx(0.67, abs=5e-3)
|
| 249 |
-
assert float(product[0, 1]) == pytest.approx(0.11, abs=5e-3)
|
| 250 |
-
assert float((product - d3pm.cumulative(0.56)).abs().max()) == pytest.approx(0.0, abs=1e-6)
|
| 251 |
-
assert (1.0 - 1.0 / 1000) ** 1000 == pytest.approx(0.3677, abs=5e-5)
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
def test_reversing_one_finite_step():
|
| 255 |
-
"""Observing C under a clean A gives (0.5795, 0.3523, 0.0341, 0.0341) at the earlier step."""
|
| 256 |
-
first, second = d3pm.one_step(0.2), d3pm.one_step(0.3)
|
| 257 |
-
clean_a = torch.tensor([[[1.0, 0.0, 0.0, 0.0]]])
|
| 258 |
-
posterior = d3pm.exact_posterior(torch.tensor([[1]]), clean_a, second[None], first[None])[0, 0]
|
| 259 |
-
assert [round(float(v), 4) for v in posterior] == [0.5795, 0.3523, 0.0341, 0.0341]
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
def test_rate_matrices_chosen_for_the_data():
|
| 263 |
-
"""The Gaussian row is 0.4088 to a neighbour, and exp(0.3 R) has first row (0.6747, ...)."""
|
| 264 |
-
row = d3pm.gaussian_row()
|
| 265 |
-
assert float(row[1]) == pytest.approx(0.4088, abs=5e-5)
|
| 266 |
-
assert float(row[0]) == pytest.approx(0.0912, abs=5e-5)
|
| 267 |
-
adjacency = torch.tensor([[0.0, 1.0, 0.5, 0.0], [1.0, 0.0, 1.0, 0.0],
|
| 268 |
-
[0.5, 1.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.0]])
|
| 269 |
-
R = d3pm.graph_rates(adjacency)
|
| 270 |
-
assert [round(float(v), 4) for v in R[0]] == [-1.5, 1.0, 0.5, 0.0]
|
| 271 |
-
kernel = torch.matrix_exp(0.3 * R)
|
| 272 |
-
assert [round(float(v), 4) for v in kernel[0]] == [0.6747, 0.1964, 0.1122, 0.0167]
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
# %% 05. sedd: score entropy
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
def test_score_entropy_at_three_predictions():
|
| 279 |
-
"""With the target ratio 2, a prediction of 1.8 costs 0.0107 nats and 1.0 costs 0.3863."""
|
| 280 |
-
target = torch.tensor(2.0)
|
| 281 |
-
assert float(sedd.score_entropy(target, torch.tensor(1.8))) == pytest.approx(0.0107, abs=5e-5)
|
| 282 |
-
assert float(sedd.score_entropy(target, torch.tensor(1.0))) == pytest.approx(0.3863, abs=5e-5)
|
| 283 |
-
assert float(sedd.score_entropy(target, torch.tensor(3.0))) == pytest.approx(0.1891, abs=5e-5)
|
| 284 |
-
assert float(sedd.score_entropy(target, target)) == pytest.approx(0.0, abs=1e-6)
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
def test_the_target_ratio_from_the_denoiser_route():
|
| 288 |
-
"""0.375(5) + 0.625(0.2) is 2, and the kernel ratio reproduces it at a clean A."""
|
| 289 |
-
assert 0.375 * 5.0 + 0.625 * 0.2 == pytest.approx(2.0)
|
| 290 |
-
x0 = torch.tensor([[0]]) # A clean A.
|
| 291 |
-
z = torch.tensor([[1]]) # Observed as a C.
|
| 292 |
-
ratio = sedd.kernel_ratio(x0, z, torch.tensor([[0.5]]))
|
| 293 |
-
assert float(ratio[0, 0, 0]) == pytest.approx(5.0)
|
| 294 |
-
assert float(ratio[0, 0, 2]) == pytest.approx(1.0)
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
# %% 06. block: the dial between diffusion and autoregression
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
def test_the_worked_block_bound():
|
| 301 |
-
"""ACGTAC in blocks of two at t = 0.5 gives 1.8326, 0.7134 and 0.2107, summing to 2.7567."""
|
| 302 |
-
weight = 2.0
|
| 303 |
-
contributions = [weight * (-math.log(0.5) - math.log(0.8)),
|
| 304 |
-
weight * -math.log(0.7), weight * -math.log(0.9)]
|
| 305 |
-
assert [round(v, 4) for v in contributions] == [1.8326, 0.7133, 0.2107]
|
| 306 |
-
assert sum(contributions) == pytest.approx(2.7567, abs=5e-5)
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
def test_the_one_position_identity():
|
| 310 |
-
"""At blocks of one position the time integral of w(t)(1 - alpha_t) is exactly one."""
|
| 311 |
-
assert block.one_position_integral() == pytest.approx(1.0, abs=1e-9)
|
| 312 |
-
generator = torch.Generator().manual_seed(0)
|
| 313 |
-
estimate = block.one_position_estimate(0.7, 40000, generator)
|
| 314 |
-
assert estimate == pytest.approx(-math.log(0.7), abs=0.05)
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
def test_block_input_keeps_the_prefix_and_hides_the_future():
|
| 318 |
-
"""The prefix stays clean, the current block is corrupted, and the suffix is all mask."""
|
| 319 |
-
x0 = torch.arange(8).remainder(4)[None, :].repeat(2, 1) # [2, 8].
|
| 320 |
-
z = block.block_input(x0, torch.tensor([1, 1]), 2, torch.full((2, 1), 1.0))
|
| 321 |
-
assert bool((z[:, :2] == x0[:, :2]).all()) # The prefix is clean.
|
| 322 |
-
assert bool((z[:, 2:4] == block.MASK).all()) # The block is fully masked at t = 1.
|
| 323 |
-
assert bool((z[:, 4:] == block.MASK).all()) # The future is hidden.
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
# %% 07. guidance: the h-transform and the worked guided jump
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
def test_h_transform_keeps_a_rate_matrix():
|
| 330 |
-
"""Reweighting by h(y)/h(x) leaves the row sums at zero, and a constant h changes nothing."""
|
| 331 |
-
R = torch.ones(4, 4) - torch.diag(torch.full((4,), 4.0))
|
| 332 |
-
transformed = guidance.h_transform(R, torch.tensor([2.0, 1.0, 0.5, 0.5]))
|
| 333 |
-
assert float(transformed.sum(dim=1).abs().max()) == pytest.approx(0.0, abs=1e-6)
|
| 334 |
-
constant = guidance.h_transform(R, torch.full((4,), 3.0))
|
| 335 |
-
assert float((constant - R).abs().max()) == pytest.approx(0.0, abs=1e-6)
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
def test_the_reverse_process_as_an_h_transform():
|
| 339 |
-
"""h_t = p_t/pi is (2, 1, 0.5, 0.5), and the rates out of C come back as 2, 0.5 and 0.5."""
|
| 340 |
-
pi = torch.full((4,), 0.25)
|
| 341 |
-
values = guidance.value_from_marginal(torch.tensor([0.5, 0.25, 0.125, 0.125]), pi)
|
| 342 |
-
assert [round(float(v), 4) for v in values] == [2.0, 1.0, 0.5, 0.5]
|
| 343 |
-
R = torch.ones(4, 4) - torch.diag(torch.full((4,), 4.0))
|
| 344 |
-
reverse = guidance.h_transform(R, values)
|
| 345 |
-
assert float(reverse[1, 0]) == pytest.approx(2.0, abs=1e-5)
|
| 346 |
-
assert float(reverse[1, 2]) == pytest.approx(0.5, abs=1e-5)
|
| 347 |
-
clean_a = guidance.value_from_marginal(torch.tensor([0.625, 0.125, 0.125, 0.125]), pi)
|
| 348 |
-
assert float(clean_a[0] / clean_a[1]) == pytest.approx(5.0)
|
| 349 |
-
assert float(clean_a[1] / clean_a[0]) == pytest.approx(0.2)
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
def test_the_worked_guided_jump():
|
| 353 |
-
"""The classifier turns the rates 2, 0.5, 0.5 into 1, 1.5, 0.5, and gamma = 2 sharpens them."""
|
| 354 |
-
unguided = torch.tensor([2.0, 0.5, 0.5])
|
| 355 |
-
ratio = torch.tensor([0.1, 0.6, 0.2]) / 0.2
|
| 356 |
-
guided = unguided * ratio
|
| 357 |
-
assert [round(float(v), 4) for v in guided] == [1.0, 1.5, 0.5]
|
| 358 |
-
assert float(guided.sum()) == pytest.approx(3.0)
|
| 359 |
-
shares = guided / guided.sum()
|
| 360 |
-
assert [float(v) for v in shares] == pytest.approx([1 / 3, 1 / 2, 1 / 6], abs=1e-6)
|
| 361 |
-
sharper = unguided * ratio ** 2
|
| 362 |
-
assert [round(float(v), 4) for v in sharper] == [0.5, 4.5, 0.5]
|
| 363 |
-
assert float(sharper.sum()) == pytest.approx(5.5)
|
| 364 |
-
assert float((sharper / sharper.sum())[1]) == pytest.approx(0.8182, abs=5e-5)
|
| 365 |
-
assert 1.0 / float(sharper.sum()) == pytest.approx(0.1818, abs=5e-5)
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
def test_exact_and_gradient_ratios_agree_at_first_order():
|
| 369 |
-
"""Both estimates of p(c | z^{i <- a})/p(c | z) have the right shape and a positive value."""
|
| 370 |
-
torch.manual_seed(0)
|
| 371 |
-
classifier = guidance.NoisyClassifier(6, width=16)
|
| 372 |
-
z = torch.tensor([[0, guidance.MASK, 2, guidance.MASK, 1, 3]])
|
| 373 |
-
t = torch.tensor([0.5])
|
| 374 |
-
with torch.no_grad():
|
| 375 |
-
exact = guidance.exact_ratio(classifier, z, t)
|
| 376 |
-
approximate = guidance.gradient_ratio(classifier, z, t)
|
| 377 |
-
assert exact.shape == approximate.shape == (1, 6, 4)
|
| 378 |
-
assert bool((exact > 0).all()) and bool((approximate > 0).all())
|
| 379 |
-
# The ratio at the token already in place is one under both estimates.
|
| 380 |
-
assert float(exact[0, 0, 0]) == pytest.approx(1.0, abs=1e-5)
|
| 381 |
-
assert float(approximate[0, 0, 0]) == pytest.approx(1.0, abs=1e-5)
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
# %% 08. planner: the order of the reveals and the weights it puts in the loss
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
def test_the_worked_planner_selection():
|
| 388 |
-
"""Scores (0.42, 0.91, 0.55, 0.78) at the masked positions reveal 3 and 6, and remask 5."""
|
| 389 |
-
scores = torch.tensor([[0.42, 0.88, 0.91, 0.55, 0.31, 0.78]])
|
| 390 |
-
masked = torch.tensor([[True, False, True, True, False, True]])
|
| 391 |
-
revealed = sorted(int(i) + 1 for i in planner.topk_where(scores, masked, 2).nonzero()[:, 1])
|
| 392 |
-
returned = sorted(int(i) + 1 for i in planner.topk_where(-scores, ~masked, 1).nonzero()[:, 1])
|
| 393 |
-
assert revealed == [3, 6]
|
| 394 |
-
assert returned == [5]
|
| 395 |
-
assert int(planner.topk_where(scores, masked, 0).sum()) == 0
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
def test_the_corrector_fluxes_match():
|
| 399 |
-
"""alpha_t beta(t) and (1 - alpha_t) w(t) both equal -alpha_dot, which is one here."""
|
| 400 |
-
for t in (0.5, 0.25, 0.75):
|
| 401 |
-
forward, reverse = planner.corrector_fluxes(t)
|
| 402 |
-
assert forward == pytest.approx(1.0)
|
| 403 |
-
assert reverse == pytest.approx(1.0)
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
def test_the_worked_papl_weights():
|
| 407 |
-
"""A planner distribution of (0.10, 0.50, 0.15, 0.25) turns 2.9 nats into 2.12."""
|
| 408 |
-
rho = torch.tensor([[0.10, 0.50, 0.15, 0.25]])
|
| 409 |
-
cross_entropies = torch.tensor([[1.2, 0.3, 0.9, 0.5]])
|
| 410 |
-
lam = 4.0 * rho
|
| 411 |
-
assert [round(float(v), 4) for v in lam[0]] == [0.4, 2.0, 0.6, 1.0]
|
| 412 |
-
assert float(lam.sum()) == pytest.approx(4.0)
|
| 413 |
-
assert float(cross_entropies.sum()) == pytest.approx(2.9)
|
| 414 |
-
assert float((lam * cross_entropies).sum()) == pytest.approx(2.12)
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
def test_planner_weights_average_to_one_and_reduce_to_mdlm():
|
| 418 |
-
"""lambda_i averages to one over the masked positions, and strength zero gives every weight one."""
|
| 419 |
-
score = torch.tensor([[0.9, 0.1, 0.5, 0.2]])
|
| 420 |
-
masked = torch.tensor([[True, True, False, True]])
|
| 421 |
-
weights = planner.planner_weights(score, masked, 1.0)
|
| 422 |
-
assert float((weights * masked).sum()) == pytest.approx(float(masked.sum()), abs=1e-5)
|
| 423 |
-
plain = planner.planner_weights(score, masked, 0.0)
|
| 424 |
-
assert float((plain - 1.0).abs().max()) == pytest.approx(0.0, abs=1e-6)
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
# %% 09. simplex: the Dirichlet belief and the thinning representation
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
def test_the_worked_dirichlet_parameters():
|
| 431 |
-
"""At alpha_t = 1/2 the parameters are (5, 1, 1, 1) and at alpha_s = 0.8 they are (17, 1, 1, 1)."""
|
| 432 |
-
assert simplex.concentration(0.5) == pytest.approx(8.0)
|
| 433 |
-
assert simplex.concentration(0.8) == pytest.approx(20.0)
|
| 434 |
-
mean_t = simplex.belief_mean(0.5, 0)
|
| 435 |
-
assert [round(float(v), 4) for v in mean_t] == [0.625, 0.125, 0.125, 0.125]
|
| 436 |
-
assert [round(float(v), 4) for v in simplex.belief_mean(0.8, 0)] == [0.85, 0.05, 0.05, 0.05]
|
| 437 |
-
at_t = simplex.dirichlet_parameters(0.5, 0)
|
| 438 |
-
at_s = simplex.dirichlet_parameters(0.8, 0)
|
| 439 |
-
assert [round(float(v), 4) for v in at_t] == [5.0, 1.0, 1.0, 1.0]
|
| 440 |
-
assert [round(float(v), 4) for v in at_s] == [17.0, 1.0, 1.0, 1.0]
|
| 441 |
-
total = at_t.sum()
|
| 442 |
-
deviation = (at_t * (total - at_t) / (total ** 2 * (total + 1))).sqrt()
|
| 443 |
-
assert float(deviation[0]) == pytest.approx(0.161, abs=5e-4)
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
def test_the_increment_sits_on_the_clean_coordinate():
|
| 447 |
-
"""The increment is (12, 0, 0, 0), so the innovation is the one-hot vector of the clean base."""
|
| 448 |
-
step = simplex.increment(0.5, 0.8, 0)
|
| 449 |
-
assert [round(float(v), 4) for v in step] == [12.0, 0.0, 0.0, 0.0]
|
| 450 |
-
first, second = simplex.thinning_law(0.5, 0.8)
|
| 451 |
-
assert (round(first, 4), round(second, 4)) == (8.0, 12.0)
|
| 452 |
-
weight = first / (first + second)
|
| 453 |
-
assert weight == pytest.approx(0.4)
|
| 454 |
-
mixed = weight * simplex.belief_mean(0.5, 0) + (1.0 - weight) * torch.eye(4)[0]
|
| 455 |
-
assert [round(float(v), 4) for v in mixed] == [0.85, 0.05, 0.05, 0.05]
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
def test_the_thinning_representation_reproduces_the_dirichlet():
|
| 459 |
-
"""W V + (1 - W) onehot(A) has the mean and the spread of Dir(17, 1, 1, 1)."""
|
| 460 |
-
torch.manual_seed(0)
|
| 461 |
-
at_t, at_s = simplex.dirichlet_parameters(0.5, 0), simplex.dirichlet_parameters(0.8, 0)
|
| 462 |
-
first, second = simplex.thinning_law(0.5, 0.8)
|
| 463 |
-
draws = 200_000
|
| 464 |
-
thinned = torch.distributions.Dirichlet(at_t).sample((draws,))
|
| 465 |
-
weight = torch.distributions.Beta(torch.tensor(first), torch.tensor(second)).sample((draws,))
|
| 466 |
-
drawn = weight[:, None] * thinned + (1.0 - weight[:, None]) * torch.eye(4)[0]
|
| 467 |
-
total = at_s.sum()
|
| 468 |
-
exact = (at_s * (total - at_s) / (total ** 2 * (total + 1))).sqrt()
|
| 469 |
-
assert [round(float(v), 4) for v in exact] == [0.0779, 0.0476, 0.0476, 0.0476]
|
| 470 |
-
assert float(drawn.mean(0)[0]) == pytest.approx(0.85, abs=2e-3)
|
| 471 |
-
assert float(drawn.std(0)[0]) == pytest.approx(float(exact[0]), abs=2e-3)
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
def test_the_parameter_field_and_the_reverse_step():
|
| 475 |
-
"""The batched parameters match the per-position ones, and one step stays on the simplex."""
|
| 476 |
-
x0 = torch.tensor([[0, 1], [2, 3]])
|
| 477 |
-
field = simplex.parameter_field(x0, torch.full((2, 1), 0.5))
|
| 478 |
-
assert [round(float(v), 4) for v in field[0, 0]] == [5.0, 1.0, 1.0, 1.0]
|
| 479 |
-
assert [round(float(v), 4) for v in field[1, 1]] == [1.0, 1.0, 1.0, 5.0]
|
| 480 |
-
torch.manual_seed(0)
|
| 481 |
-
belief = simplex.corrupt(x0, torch.full((2, 1), 0.5))
|
| 482 |
-
assert belief.shape == (2, 2, 4)
|
| 483 |
-
assert float(belief.sum(-1).sub(1.0).abs().max()) == pytest.approx(0.0, abs=1e-6)
|
| 484 |
-
moved = simplex.reverse_step(belief, x0, 0.5, 0.8)
|
| 485 |
-
assert float(moved.sum(-1).sub(1.0).abs().max()) == pytest.approx(0.0, abs=1e-6)
|
| 486 |
-
assert bool((moved >= 0).all())
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
def test_a_one_hot_belief_recovers_the_token_denoiser():
|
| 490 |
-
"""The only architectural change is at the input, so one-hot beliefs give identical logits."""
|
| 491 |
-
torch.manual_seed(0)
|
| 492 |
-
model = simplex.BeliefDenoiser(6, width=16)
|
| 493 |
-
tokens = torch.tensor([[0, 1, 2, 3, 0, 1]])
|
| 494 |
-
t = torch.tensor([0.4])
|
| 495 |
-
one_hot = torch.nn.functional.one_hot(tokens, 4).to(torch.get_default_dtype())
|
| 496 |
-
with torch.no_grad():
|
| 497 |
-
through_belief = model(one_hot, t)
|
| 498 |
-
through_tokens = model.inner(tokens, t)
|
| 499 |
-
assert float((through_belief - through_tokens).abs().max()) == pytest.approx(0.0, abs=1e-6)
|
| 500 |
-
drawn = simplex.sample(model, 3, 6, steps=4)
|
| 501 |
-
assert drawn.shape == (3, 6) and int(drawn.max()) < 4
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
# %% 10. search: rollouts, the archive and the two objectives
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
def test_the_worked_value_estimate():
|
| 508 |
-
"""Rollouts averaging 0.35 and 0.75 reweight a rate of 1.2 to 2.571."""
|
| 509 |
-
parent = float(torch.tensor([0.1, 0.8, 0.3, 0.2]).mean())
|
| 510 |
-
child = float(torch.tensor([0.7, 0.9, 0.6, 0.8]).mean())
|
| 511 |
-
assert parent == pytest.approx(0.35)
|
| 512 |
-
assert child == pytest.approx(0.75)
|
| 513 |
-
assert child / parent == pytest.approx(2.143, abs=5e-4)
|
| 514 |
-
assert 1.2 * child / parent == pytest.approx(2.571, abs=5e-4)
|
| 515 |
-
assert 0.2 / parent == pytest.approx(0.571, abs=5e-4)
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
def test_the_worked_pareto_archive():
|
| 519 |
-
"""P3 at (0.4, 0.2) is dominated by P1 at (0.8, 0.3), and P1 and P2 both stay."""
|
| 520 |
-
archive = search.ParetoArchive()
|
| 521 |
-
for name, scores in (((1,), (0.8, 0.3)), ((2,), (0.5, 0.7)), ((3,), (0.4, 0.2))):
|
| 522 |
-
archive.add(name, scores)
|
| 523 |
-
assert sorted(q for q, _ in archive.entries) == [(1,), (2,)]
|
| 524 |
-
archive.add((4,), (0.9, 0.9)) # A candidate that dominates both of them.
|
| 525 |
-
assert [q for q, _ in archive.entries] == [(4,)]
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
def test_the_two_objectives():
|
| 529 |
-
"""A GC-free sequence scores zero on the first axis, and the motif scores one on the second."""
|
| 530 |
-
tokens = torch.tensor([[0, 1, 2, 3, 0, 0], [0, 0, 3, 3, 0, 3]])
|
| 531 |
-
scores = search.objectives(tokens)
|
| 532 |
-
assert float(scores[0, 0]) == pytest.approx(2 / 6)
|
| 533 |
-
assert float(scores[0, 1]) == pytest.approx(1.0) # ACGT sits at the start.
|
| 534 |
-
assert float(scores[1, 0]) == pytest.approx(0.0)
|
| 535 |
-
assert float(scores[1, 1]) < 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_lecture_5.py
DELETED
|
@@ -1,550 +0,0 @@
|
|
| 1 |
-
"""The worked numbers of Chapter 5, asserted against the files of ``lecture_5/``.
|
| 2 |
-
|
| 3 |
-
Every number checked here is printed in the notes. Where the chapter runs a simulation, the test
|
| 4 |
-
asserts the analytic value the simulation estimates, with a tolerance wide enough for the sample
|
| 5 |
-
size the file uses by default.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import importlib.util
|
| 11 |
-
import math
|
| 12 |
-
import os
|
| 13 |
-
import subprocess
|
| 14 |
-
import sys
|
| 15 |
-
from pathlib import Path
|
| 16 |
-
|
| 17 |
-
import pytest
|
| 18 |
-
import torch
|
| 19 |
-
|
| 20 |
-
from cis6270.nets import SequenceDenoiser
|
| 21 |
-
|
| 22 |
-
ROOT = Path(__file__).resolve().parents[1]
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def load(name):
|
| 26 |
-
"""Import one lecture file by path, so that ``lecture_5`` needs no package marker."""
|
| 27 |
-
spec = importlib.util.spec_from_file_location(f"lecture5_{name}", ROOT / "lecture_5" / f"{name}.py")
|
| 28 |
-
module = importlib.util.module_from_spec(spec)
|
| 29 |
-
sys.path.insert(0, str(ROOT))
|
| 30 |
-
spec.loader.exec_module(module)
|
| 31 |
-
return module
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
dfm = load("dfm")
|
| 35 |
-
velocity = load("velocity")
|
| 36 |
-
corrector = load("corrector")
|
| 37 |
-
kinetic = load("kinetic_optimal")
|
| 38 |
-
edits = load("edit_flows")
|
| 39 |
-
dirichlet = load("dirichlet")
|
| 40 |
-
fisher = load("fisher")
|
| 41 |
-
gumbel = load("gumbel")
|
| 42 |
-
guidance = load("guidance")
|
| 43 |
-
objectives = load("multi_objective")
|
| 44 |
-
rectify = load("rectify")
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def close(values, expected, tolerance=5e-4):
|
| 48 |
-
"""Compare a list of numbers against the chapter's, entry by entry."""
|
| 49 |
-
assert len(values) == len(expected)
|
| 50 |
-
for got, want in zip(values, expected):
|
| 51 |
-
assert abs(float(got) - want) <= tolerance, (values, expected)
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
# %% Section 5.2 and 5.3: a velocity, its two rules, and the master equation
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def test_worked_master_equation():
|
| 58 |
-
u, p, p_dot, arriving, leaving = velocity.worked_master_equation()
|
| 59 |
-
assert velocity.obeys_the_two_rules(u)
|
| 60 |
-
assert torch.allclose(u, velocity.set_diagonal(u))
|
| 61 |
-
close(p_dot.tolist(), [-0.1, -0.2, 0.7, -0.4])
|
| 62 |
-
assert abs(float(p_dot.sum())) < 1e-9
|
| 63 |
-
assert abs(arriving - 0.7) < 1e-9 and abs(leaving) < 1e-9
|
| 64 |
-
close((p + 0.1 * p_dot).tolist(), [0.09, 0.18, 0.37, 0.36])
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def test_hazard_and_survival():
|
| 68 |
-
assert abs(velocity.hazard(0.5) - 2.0) < 1e-9
|
| 69 |
-
assert abs(velocity.hazard(0.9) - 10.0) < 1e-9
|
| 70 |
-
assert abs(velocity.survival(4000) - 0.1) < 2e-3 # S(t) = 1 - kappa_t.
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def test_continuous_and_discrete_side_by_side():
|
| 74 |
-
p1 = torch.tensor([0.1, 0.6, 0.2, 0.1])
|
| 75 |
-
moved, drawn, lam = velocity.side_by_side(2, p1, t=0.5, h=0.1)
|
| 76 |
-
close(moved.tolist(), [0.02, 0.12, 0.84, 0.02])
|
| 77 |
-
close(drawn.tolist(), [0.02, 0.12, 0.84, 0.02])
|
| 78 |
-
assert abs(lam - 0.2) < 1e-9
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
# %% Sections 5.5 to 5.11: the mixture path, the velocity, the loss and the sampler
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def test_dfm_worked_numbers():
|
| 85 |
-
report = dfm.worked_numbers()
|
| 86 |
-
assert report["mixture_draw"] == "ATGA"
|
| 87 |
-
close(report["velocity_at_G"], [0.2, 1.2, -1.6, 0.2])
|
| 88 |
-
close(report["euler_step_at_G"], [0.02, 0.12, 0.84, 0.02])
|
| 89 |
-
assert abs(report["redraw_probability"] - 0.2) < 1e-9
|
| 90 |
-
assert abs(report["example_loss_nats"] - 4.780) < 1e-3
|
| 91 |
-
close(report["mask_posterior_C_G"], [0.625, 0.375])
|
| 92 |
-
assert report["mask_posterior_unchanged"] # Proposition 6 of Discrete Flow Matching.
|
| 93 |
-
assert abs(report["posterior_entropy_nats"] - 0.6616) < 5e-4
|
| 94 |
-
assert abs(report["kl_gap_to_uniform"] - 0.0316) < 5e-4
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def test_conditional_velocity_generates_the_conditional_path():
|
| 98 |
-
"""Section 5.6.2: the two terms of the master equation on the mixture path."""
|
| 99 |
-
kappa, x0, x1 = 0.4, 0, 1
|
| 100 |
-
path = torch.zeros(4, dtype=torch.double)
|
| 101 |
-
path[x0], path[x1] = 1.0 - kappa, kappa
|
| 102 |
-
total = sum(path[z] * dfm.conditional_velocity(z, x1, kappa) for z in (x0, x1))
|
| 103 |
-
derivative = torch.zeros(4, dtype=torch.double)
|
| 104 |
-
derivative[x1], derivative[x0] = 1.0, -1.0 # kappa_dot_t times the two indicators.
|
| 105 |
-
assert torch.allclose(total, derivative, atol=1e-9)
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def test_corrupt_reaches_both_endpoints():
|
| 109 |
-
torch.manual_seed(0)
|
| 110 |
-
process = dfm.DiscreteFlowMatching(4)
|
| 111 |
-
x1 = torch.randint(0, 4, (64, 16))
|
| 112 |
-
assert torch.equal(process.corrupt(x1, torch.ones(64)), x1) # kappa_t = 1 gives the data.
|
| 113 |
-
source = process.corrupt(x1, torch.zeros(64)) # kappa_t = 0 gives the source.
|
| 114 |
-
assert float((source == x1).double().mean()) < 0.4 # A uniform draw agrees about a quarter of the time.
|
| 115 |
-
halfway = process.corrupt(x1, torch.full((64,), 0.5))
|
| 116 |
-
assert 0.55 < float((halfway == x1).double().mean()) < 0.75 # Heads with probability one half.
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def test_two_sources():
|
| 120 |
-
assert torch.equal(dfm.DiscreteFlowMatching(4, mask=4).source((3, 5)),
|
| 121 |
-
torch.full((3, 5), 4, dtype=torch.long)) # The all-mask source.
|
| 122 |
-
uniform = dfm.DiscreteFlowMatching(4).source((200, 20))
|
| 123 |
-
assert int(uniform.min()) == 0 and int(uniform.max()) == 3 # Every base appears.
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
# %% Sections 5.13 and 5.14: zero-flux velocities and the corrector
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def test_zero_flux_velocity_on_three_states():
|
| 130 |
-
p, u, v, with_u, with_both = corrector.three_state_example()
|
| 131 |
-
assert torch.allclose(corrector.net_flux(v, p), torch.zeros(3), atol=1e-12)
|
| 132 |
-
close(with_u.tolist(), [-0.2, -0.3, 0.5])
|
| 133 |
-
close(with_both.tolist(), [-0.2, -0.3, 0.5])
|
| 134 |
-
assert abs(float(-u[0, 0]) - 1.0) < 1e-12 and abs(float(-(u + v)[0, 0]) - 5.0) < 1e-12
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
def test_corrector_step_table():
|
| 138 |
-
"""Section 5.14.3 at t = 0.4 with h = 0.01 and the denoiser (0.1, 0.6, 0.2, 0.1)."""
|
| 139 |
-
p1 = torch.tensor([0.1, 0.6, 0.2, 0.1])
|
| 140 |
-
expected = {
|
| 141 |
-
0.0: ([0.00167, 0.01000, 0.00333, 0.00167], 0.98333),
|
| 142 |
-
2.0: ([0.00300, 0.01800, 0.00600, 0.00300], 0.97000),
|
| 143 |
-
10.0: ([0.00833, 0.05000, 0.01667, 0.00833], 0.91667),
|
| 144 |
-
}
|
| 145 |
-
for eta, (probabilities, stay) in expected.items():
|
| 146 |
-
bar_u = corrector.corrector_velocity(p1, corrector.MASK, 0.4, eta)
|
| 147 |
-
close((0.01 * bar_u[:4]).tolist(), probabilities, tolerance=1e-5)
|
| 148 |
-
assert abs(float(1.0 + 0.01 * bar_u[corrector.MASK]) - stay) < 1e-5
|
| 149 |
-
assert abs(corrector.masked_fraction_derivative(0.4, eta) + 1.0) < 1e-12
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
def test_corrector_rates_and_admissible_steps():
|
| 153 |
-
p1 = torch.tensor([0.1, 0.6, 0.2, 0.1])
|
| 154 |
-
bar_u = corrector.corrector_velocity(p1, corrector.MASK, 0.4, 2.0)
|
| 155 |
-
assert abs(float(-bar_u[corrector.MASK]) - 3.0) < 1e-12 # (1 + 2 x 0.4)/(1 - 0.4).
|
| 156 |
-
visible = torch.zeros(4)
|
| 157 |
-
visible[1] = 1.0
|
| 158 |
-
assert abs(float(corrector.corrector_velocity(visible, 1, 0.4, 2.0)[corrector.MASK]) - 2.0) < 1e-12
|
| 159 |
-
close([corrector.admissible_step(0.9, eta) for eta in (0.0, 2.0, 10.0)],
|
| 160 |
-
[0.1, 0.0357, 0.01], tolerance=1e-4)
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
def test_jump_counts_are_one_plus_eta():
|
| 164 |
-
"""Section 5.14.3: the masked fraction stays at 1 - t and the jumps come to 1 + eta."""
|
| 165 |
-
p1 = torch.tensor([0.25, 0.25, 0.25, 0.25])
|
| 166 |
-
for eta in (0.0, 2.0, 10.0):
|
| 167 |
-
half, mean = corrector.simulate_jumps(p1, eta, positions=10_000, steps=300)
|
| 168 |
-
assert abs(half - 0.5) < 0.03
|
| 169 |
-
assert abs(mean - (1.0 + eta)) < 0.25
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
def test_parallel_step_error():
|
| 173 |
-
assert abs(corrector.joint_reveal_rate(64) - 1.0 / 64) < 1e-12
|
| 174 |
-
assert abs(corrector.pair_survival(2.0) - 0.1353) < 5e-4
|
| 175 |
-
assert abs(corrector.pair_survival(10.0) - 4.54e-5) < 1e-7
|
| 176 |
-
measured = corrector.simulate_pair(0.0, samples=40_000, steps=64)
|
| 177 |
-
assert abs(measured - 0.5 / 64) < 2e-3 # Half of the joint reveals land outside the data.
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
# %% Section 5.16: kinetic-optimal paths
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
def test_kinetic_optimal_recovers_the_mixture_velocity():
|
| 184 |
-
q = torch.tensor([0.1, 0.6, 0.2, 0.1])
|
| 185 |
-
p, p_dot = kinetic.mask_path(q, kappa_t=0.5)
|
| 186 |
-
u = kinetic.kinetic_optimal(p, p_dot)
|
| 187 |
-
assert abs(float(p_dot[kinetic.MASK] / p[kinetic.MASK]) + 2.0) < 1e-12
|
| 188 |
-
assert abs(float(p_dot[1] / p[1]) - 2.0) < 1e-12
|
| 189 |
-
assert abs(float(u[1, kinetic.MASK]) - 1.2) < 1e-12 # 0.30 times 4.
|
| 190 |
-
assert abs(float(u[1, kinetic.MASK]) - kinetic.mixture_rate(0.5) * 0.6) < 1e-12
|
| 191 |
-
assert abs(float(u[1, 2])) < 1e-12 # No probability moves between two visible tokens.
|
| 192 |
-
assert torch.allclose(u @ p, p_dot, atol=1e-12)
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
def test_worked_metric_path_for_dna():
|
| 196 |
-
beta_t, beta_dot = kinetic.beta_schedule(0.5)
|
| 197 |
-
assert abs(beta_t - 1.0) < 1e-12 and abs(beta_dot - 4.0) < 1e-12
|
| 198 |
-
p = kinetic.metric_path(2, beta_t)
|
| 199 |
-
close(p.tolist(), [0.2245, 0.0826, 0.6103, 0.0826])
|
| 200 |
-
u = kinetic.metric_velocity(2, beta_t, beta_dot)
|
| 201 |
-
assert abs(float(u[0, 1]) - 0.898) < 5e-4 # C to A.
|
| 202 |
-
assert abs(float(u[2, 1]) - 4.882) < 5e-4 # C to G.
|
| 203 |
-
assert abs(float(u[2, 0]) - 2.441) < 5e-4 # A to G.
|
| 204 |
-
assert abs(float(-u[1, 1]) - 5.780) < 1e-3 # The total rate out of C.
|
| 205 |
-
close((torch.eye(4)[1] + 0.05 * u[:, 1]).tolist(), [0.0449, 0.7110, 0.2441, 0.0])
|
| 206 |
-
close((u @ p).tolist(), [-0.3997, -0.4774, 1.3546, -0.4774])
|
| 207 |
-
close(kinetic.path_derivative(2, 0.5).tolist(), [-0.3997, -0.4774, 1.3546, -0.4774], 1e-3)
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
def test_energy_of_two_schedules():
|
| 211 |
-
assert abs(kinetic.schedule_energy(*kinetic.LINEAR, 600, 1e-6) - 27.63) < 5e-3
|
| 212 |
-
assert abs(kinetic.schedule_energy(*kinetic.LINEAR, 600, 1e-9) - 41.45) < 5e-3
|
| 213 |
-
assert abs(kinetic.schedule_energy(*kinetic.GEODESIC, 600, 1e-6) - math.pi ** 2) < 1e-3
|
| 214 |
-
assert abs(kinetic.geodesic_schedule(0.25) - 0.1464) < 5e-4
|
| 215 |
-
assert abs(kinetic.geodesic_schedule(0.75) - 0.8536) < 5e-4
|
| 216 |
-
assert abs(kinetic.geodesic_schedule(0.5, 0.25) - 0.6667) < 5e-4
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
# %% Section 5.17: Edit Flows
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
def test_alignment_and_column_wise_path():
|
| 223 |
-
z0 = [0, 1, 2, 3, edits.BLANK] # (A, C, G, T, eps).
|
| 224 |
-
z1 = [0, edits.BLANK, 2, 0, 1] # (A, eps, G, A, C).
|
| 225 |
-
assert edits.column_kinds(z0, z1) == ["none", "del", "none", "sub", "ins"]
|
| 226 |
-
drawn = edits.draw_state(z0, z1, [True, False, True, True, True])
|
| 227 |
-
assert drawn == [0, 1, 2, 0, 1] # ACGAC, the draw of probability 0.5 ** 3 = 0.125.
|
| 228 |
-
assert abs(edits.expected_length([0, 1, 2, 3], [0, 2, 0, 1], 0.5) - 4.0) < 1e-12
|
| 229 |
-
recovered = edits.align([0, 1, 2, 3], [0, 2, 0, 1])
|
| 230 |
-
assert len(recovered[0]) == len(recovered[1]) # Any alignment pads to a common width.
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
def test_alignment_posterior_and_averaged_rates():
|
| 234 |
-
first = ([0, 1, edits.BLANK], [0, 0, 1]) # Substitute C with A, then insert C.
|
| 235 |
-
second = ([0, edits.BLANK, 1], [0, 0, 1]) # Insert A, and leave the last column alone.
|
| 236 |
-
for kappa_t, expected in ((0.25, 0.4286), (0.5, 1 / 3), (0.75, 0.2)):
|
| 237 |
-
weights = edits.alignment_posterior([first, second], [0, 1], kappa_t)
|
| 238 |
-
assert abs(weights[0] - expected) < 5e-4
|
| 239 |
-
posterior, rates = edits.averaged_rates([first, second], [0, 1], 0.5)
|
| 240 |
-
assert abs(rates["sub(2,A)"] - 0.667) < 1e-3
|
| 241 |
-
assert abs(rates["ins(1,A)"] - 1.333) < 1e-3
|
| 242 |
-
assert abs(rates["ins(2,C)"] - 0.667) < 1e-3
|
| 243 |
-
assert abs(sum(rates.values()) - 2.667) < 1e-3
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
def test_edit_loss_and_sampler_step():
|
| 247 |
-
assert abs(edits.edit_loss(3.0, [0.8], kappa_t=0.5) - 3.446) < 1e-3
|
| 248 |
-
grid = [0.01 * i for i in range(1, 801)]
|
| 249 |
-
assert abs(edits.loss_minimizer(2.0, grid) - 2.0) < 1e-6
|
| 250 |
-
intensities = {"ins": [0.2, 0.3, 0.4], "del": [0.1, 0.9, 0.2], "sub": [0.4, 0.3, 0.5]}
|
| 251 |
-
insert, change = edits.step_probabilities(intensities, 0.05)
|
| 252 |
-
close(insert, [0.010, 0.015, 0.020])
|
| 253 |
-
close(change, [0.025, 0.060, 0.035])
|
| 254 |
-
exact, first_order = edits.untouched_probability(intensities, 0.05)
|
| 255 |
-
assert abs(exact - 0.8452) < 5e-4 and abs(first_order - 0.835) < 5e-4
|
| 256 |
-
assert abs(exact - first_order - 0.0102) < 5e-4
|
| 257 |
-
assert abs(edits.expected_length_change(intensities, 0.05) + 0.015) < 1e-9
|
| 258 |
-
assert edits.edit_rates(intensities)[0] == 11 # Three intensities and two token distributions.
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
# %% Section 5.20: Dirichlet Flow Matching
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
def test_dirichlet_path_and_speeds():
|
| 265 |
-
assert abs(dirichlet.dirichlet_density(torch.full((4,), 0.25), 0, 0.0) - 6.0) < 1e-9
|
| 266 |
-
assert dirichlet.straight_line_corners(torch.tensor([0.34, 0.33, 0.33, 0.0]), 0.3) == 3
|
| 267 |
-
assert dirichlet.straight_line_corners(torch.tensor([0.70, 0.30, 0.0, 0.0]), 0.6) == 1
|
| 268 |
-
closed = dirichlet.speed_two_letters(0.3, 1.0)
|
| 269 |
-
assert abs(closed - 0.2580) < 5e-5
|
| 270 |
-
assert abs(float(dirichlet.speed(0.3, 1.0, K=2)) - closed) < 1e-6
|
| 271 |
-
x = torch.tensor([0.40, 0.30, 0.20, 0.10])
|
| 272 |
-
close(dirichlet.speed_vector(x, 1.0).tolist(), [0.232, 0.199, 0.159, 0.104], 1e-3)
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
def test_dirichlet_velocity_and_posterior_average():
|
| 276 |
-
x = torch.tensor([0.40, 0.30, 0.20, 0.10])
|
| 277 |
-
toward_a = dirichlet.conditional_velocity(x, 0, 1.0)
|
| 278 |
-
close(toward_a.tolist(), [0.139, -0.070, -0.046, -0.023], 1e-3)
|
| 279 |
-
assert abs(float(toward_a.sum())) < 1e-12
|
| 280 |
-
p_hat = torch.tensor([0.60, 0.25, 0.10, 0.05])
|
| 281 |
-
v = dirichlet.marginal_velocity(x, p_hat, 1.0)
|
| 282 |
-
close(v.tolist(), [0.0553, -0.0133, -0.0262, -0.0158], 2e-4)
|
| 283 |
-
close((x + 0.1 * v).tolist(), [0.4055, 0.2987, 0.1974, 0.0984])
|
| 284 |
-
assert abs(float((x + 0.1 * v).sum()) - 1.0) < 1e-12
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
def test_dirichlet_continuity():
|
| 288 |
-
time_term, space_term = dirichlet.continuity_terms(torch.tensor([0.5, 0.3, 0.2]), 0, 0.8)
|
| 289 |
-
assert abs(time_term - 0.6355) < 5e-4
|
| 290 |
-
assert abs(space_term + 0.6355) < 5e-4
|
| 291 |
-
assert abs(time_term + space_term) < 1e-5
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
# %% Section 5.21: Fisher-Flow
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
def test_two_moves_under_two_metrics():
|
| 298 |
-
near = torch.tensor([0.5, 0.5]), torch.tensor([0.4, 0.6])
|
| 299 |
-
rare = torch.tensor([0.01, 0.99]), torch.tensor([0.001, 0.999])
|
| 300 |
-
assert abs(fisher.euclidean_distance(*near) - 0.1414) < 5e-4
|
| 301 |
-
assert abs(fisher.euclidean_distance(*rare) - 0.0127) < 5e-4
|
| 302 |
-
assert abs(fisher.fisher_rao_distance(*near) - 0.201) < 1e-3
|
| 303 |
-
assert abs(fisher.fisher_rao_distance(*rare) - 0.137) < 1e-3
|
| 304 |
-
orthogonal = fisher.fisher_rao_distance(torch.tensor([1.0, 0.0]), torch.tensor([0.0, 1.0]))
|
| 305 |
-
assert abs(orthogonal - math.pi) < 1e-6 # Any two letters are the farthest apart.
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
def test_sphere_and_geodesic():
|
| 309 |
-
p0 = torch.full((4,), 0.25)
|
| 310 |
-
p1 = fisher.smooth(torch.eye(4)[0])
|
| 311 |
-
close(p1.tolist(), [0.91, 0.03, 0.03, 0.03])
|
| 312 |
-
x0, x1 = fisher.to_sphere(p0), fisher.to_sphere(p1)
|
| 313 |
-
close(x1.tolist(), [0.954, 0.173, 0.173, 0.173], 1e-3)
|
| 314 |
-
assert abs(float((x0 * x1).sum()) - 0.737) < 1e-3
|
| 315 |
-
assert abs(math.acos(float((x0 * x1).sum())) - 0.743) < 1e-3
|
| 316 |
-
assert abs(fisher.fisher_rao_distance(p0, p1) - 1.485) < 1e-3
|
| 317 |
-
mid = fisher.geodesic(x0, x1, torch.tensor(0.5))
|
| 318 |
-
close(mid.tolist(), [0.780, 0.361, 0.361, 0.361], 1e-3)
|
| 319 |
-
close(fisher.to_simplex(mid).tolist(), [0.609, 0.130, 0.130, 0.130], 1e-3)
|
| 320 |
-
remaining = fisher.log_map(mid, x1)
|
| 321 |
-
close(remaining.tolist(), [0.2323, -0.1672, -0.1672, -0.1672], 1e-3)
|
| 322 |
-
assert abs(float(remaining.norm()) - 0.3713) < 5e-4
|
| 323 |
-
assert abs(float(fisher.conditional_velocity(mid, x1, 0.5).norm()) - 0.743) < 1e-3
|
| 324 |
-
stepped = fisher.exp_map(mid, 0.1 * fisher.conditional_velocity(mid, x1, 0.5))
|
| 325 |
-
close(stepped.tolist(), [0.824, 0.327, 0.327, 0.327], 1e-3)
|
| 326 |
-
close(fisher.to_simplex(stepped).tolist(), [0.680, 0.107, 0.107, 0.107], 1e-3)
|
| 327 |
-
assert abs(float(stepped.norm()) - 1.0) < 1e-10
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
def test_tangent_projection():
|
| 331 |
-
x0, p1 = fisher.to_sphere(torch.full((4,), 0.25)), fisher.smooth(torch.eye(4)[0])
|
| 332 |
-
mid = fisher.geodesic(x0, fisher.to_sphere(p1), torch.tensor(0.5))
|
| 333 |
-
raw = torch.tensor([0.3, -0.2, 0.5, 0.1])
|
| 334 |
-
assert abs(float((mid * raw).sum()) - 0.378) < 1e-3
|
| 335 |
-
tangent = fisher.project(mid, raw)
|
| 336 |
-
close(tangent.tolist(), [0.005, -0.337, 0.363, -0.037], 1e-3)
|
| 337 |
-
assert abs(float((mid * tangent).sum())) < 1e-12
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
# %% Section 5.22: Gumbel-Softmax Flow Matching
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
def test_gumbel_max_and_temperature():
|
| 344 |
-
pi = torch.tensor([0.5, 0.2, 0.2, 0.1])
|
| 345 |
-
torch.manual_seed(0)
|
| 346 |
-
frequencies = gumbel.gumbel_max(pi, 200_000)
|
| 347 |
-
close(frequencies.tolist(), pi.tolist(), tolerance=5e-3)
|
| 348 |
-
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)
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
def test_annealed_path_and_replicator():
|
| 352 |
-
logits = torch.tensor([0.0, -0.45, 1.40, -0.05])
|
| 353 |
-
expected = {
|
| 354 |
-
0.0: [0.2438, 0.2331, 0.2805, 0.2426],
|
| 355 |
-
0.5: [0.2142, 0.1751, 0.4012, 0.2095],
|
| 356 |
-
0.9: [0.0955, 0.0489, 0.7669, 0.0887],
|
| 357 |
-
1.0: [0.0528, 0.0214, 0.8781, 0.0477],
|
| 358 |
-
}
|
| 359 |
-
for t, values in expected.items():
|
| 360 |
-
close(gumbel.annealed_path(logits, t).tolist(), values)
|
| 361 |
-
x = gumbel.annealed_path(logits, 0.5)
|
| 362 |
-
assert abs(float((x * logits).sum()) - 0.4724) < 5e-4
|
| 363 |
-
noisy = gumbel.replicator_velocity(x, logits, 0.5)
|
| 364 |
-
close(noisy.tolist(), [-0.136, -0.217, 0.500, -0.147], 1e-3)
|
| 365 |
-
assert abs(float(noisy.sum())) < 1e-12
|
| 366 |
-
close(gumbel.inference_velocity(x, 2, 0.5).tolist(), [-0.116, -0.094, 0.323, -0.113], 1e-3)
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
def test_gumbel_marginal_velocity():
|
| 370 |
-
late = torch.tensor([0.138, 0.084, 0.647, 0.131])
|
| 371 |
-
p_hat = torch.tensor([0.05, 0.10, 0.70, 0.15])
|
| 372 |
-
coefficients = (gumbel.LAMBDA / gumbel.temperature(0.8)) * late * p_hat
|
| 373 |
-
close(coefficients.tolist(), [0.0228, 0.0278, 1.4976, 0.0650], 5e-4)
|
| 374 |
-
v = gumbel.marginal_velocity(late, p_hat, 0.8)
|
| 375 |
-
close(v.tolist(), [-0.200, -0.108, 0.454, -0.146], 1e-3)
|
| 376 |
-
close((late + 0.01 * v).tolist(), [0.1360, 0.0829, 0.6515, 0.1295])
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
# %% Sections 5.24 to 5.29 and 5.33: guidance
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
def test_guided_base():
|
| 383 |
-
p1 = torch.tensor([0.1, 0.2, 0.3, 0.4])
|
| 384 |
-
rates = p1 / (1.0 - 0.6)
|
| 385 |
-
scores = torch.tensor([0.10, 0.35, 0.55, 0.25])
|
| 386 |
-
close(rates.tolist(), [0.25, 0.50, 0.75, 1.00])
|
| 387 |
-
assert abs(float(rates.sum()) - 2.50) < 1e-12
|
| 388 |
-
close((scores / 0.20).tolist(), [0.50, 1.75, 2.75, 1.25])
|
| 389 |
-
guided = guidance.tilt(rates, scores, 0.20)
|
| 390 |
-
close(guided.tolist(), [0.125, 0.875, 2.0625, 1.250])
|
| 391 |
-
assert abs(float(guided.sum()) - 4.3125) < 1e-12
|
| 392 |
-
close((guided / guided.sum()).tolist(), [0.029, 0.203, 0.478, 0.290], 1e-3)
|
| 393 |
-
close((p1 * scores).tolist(), [0.010, 0.070, 0.165, 0.100])
|
| 394 |
-
h = guidance.h_value(p1, scores)
|
| 395 |
-
assert abs(h - 0.345) < 1e-12
|
| 396 |
-
assert abs(float(guidance.tilt(rates, scores, h).sum()) - 2.50) < 1e-12
|
| 397 |
-
assert guidance.reachable(100, 21) == 2001
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
def test_guidance_strength_and_classifier_free():
|
| 401 |
-
p1 = torch.tensor([0.1, 0.2, 0.3, 0.4])
|
| 402 |
-
rates, scores = p1 / 0.4, torch.tensor([0.10, 0.35, 0.55, 0.25])
|
| 403 |
-
harder = guidance.tilt(rates, scores, 0.20, gamma=2.0)
|
| 404 |
-
close(harder.tolist(), [0.0625, 1.5312, 5.6719, 1.5625], 1e-3)
|
| 405 |
-
assert abs(float(harder.sum()) - 8.828) < 1e-3
|
| 406 |
-
close((harder / harder.sum()).tolist(), [0.007, 0.173, 0.642, 0.177], 1e-3)
|
| 407 |
-
mixed = guidance.classifier_free(guidance.tilt(rates, scores, 0.20), rates, gamma=2.0)
|
| 408 |
-
assert torch.allclose(mixed, harder, atol=1e-12)
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
def test_taylor_expansion():
|
| 412 |
-
gradient = torch.tensor([-2.10, -1.15, -0.75, -1.30, -1.61])
|
| 413 |
-
approximate = guidance.taylor_log_ratio(gradient, guidance.MASK)[:4]
|
| 414 |
-
close(approximate.tolist(), [-0.49, 0.46, 0.86, 0.31])
|
| 415 |
-
exact = (torch.tensor([0.10, 0.35, 0.55, 0.25]) / 0.20).log()
|
| 416 |
-
close(exact.tolist(), [-0.693, 0.560, 1.012, 0.223], 1e-3)
|
| 417 |
-
rates = torch.tensor([0.25, 0.50, 0.75, 1.00]) * approximate.exp()
|
| 418 |
-
close((rates / rates.sum()).tolist(), [0.038, 0.194, 0.434, 0.334], 1e-3)
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
def test_guided_step_never_remasks_a_visible_position():
|
| 422 |
-
"""The denoiser in the sampler is a distribution over the data tokens, which exclude the mask."""
|
| 423 |
-
torch.manual_seed(0)
|
| 424 |
-
model = SequenceDenoiser(guidance.V, 8, width=32, depth=1)
|
| 425 |
-
prop = guidance.PropertyModel(8, width=32)
|
| 426 |
-
xt = torch.randint(0, 4, (16, 8)) # Every position already holds a base.
|
| 427 |
-
stepped = guidance.guided_step(model, prop, xt, torch.full((16,), 0.9), 0.5, gamma=1.0)
|
| 428 |
-
assert int((stepped == guidance.MASK).sum()) == 0 # A revealed token never returns to the mask.
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
# %% Sections 5.30 to 5.32: several objectives at once
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
def test_pareto_front():
|
| 435 |
-
scores = torch.tensor([[0.8, 0.3], [0.6, 0.7], [0.5, 0.2], [0.6, 0.3]])
|
| 436 |
-
assert objectives.pareto_front(scores) == [0, 1] # The third and fourth are dominated.
|
| 437 |
-
close((scores @ torch.tensor([0.6, 0.4]))[:2].tolist(), [0.60, 0.64])
|
| 438 |
-
close((scores @ torch.tensor([0.8, 0.2]))[:2].tolist(), [0.70, 0.62]) # The order reverses.
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
def test_mogdfm_step():
|
| 442 |
-
changes = torch.tensor([[0.0, 0.0], [0.20, -0.05], [0.05, 0.15], [-0.10, 0.08]])
|
| 443 |
-
delta_s, ranks, direction = objectives.combined_score(
|
| 444 |
-
changes, torch.tensor([1.2, 0.8]), torch.tensor([0.6, 0.4]))
|
| 445 |
-
close(ranks.tolist(), [0.500, 0.700, 0.850, 0.450])
|
| 446 |
-
close(direction.tolist(), [0.0, 0.100, 0.090, -0.028])
|
| 447 |
-
close(delta_s.tolist(), [-1.511, 1.541, 2.297, -2.327], 1e-3)
|
| 448 |
-
close(delta_s.exp().tolist(), [0.221, 4.667, 9.948, 0.098], 1e-2)
|
| 449 |
-
base = torch.tensor([0.3, 0.3, 0.1]) / 0.3 # The pretrained rates out of A toward C, G and T.
|
| 450 |
-
tilted = base * delta_s[1:].exp()
|
| 451 |
-
close(tilted.tolist(), [4.667, 9.948, 0.033], 1e-2)
|
| 452 |
-
angles, cosines = objectives.cone_angles(changes[1:], torch.tensor([0.6, 0.4]))
|
| 453 |
-
close(cosines.tolist(), [0.6727, 0.7894, -0.3032], 5e-4)
|
| 454 |
-
close(angles.tolist(), [47.7, 37.9, 107.7], 0.05)
|
| 455 |
-
survivors = tilted[angles <= 60.0]
|
| 456 |
-
close((survivors / survivors.sum()).tolist(), [0.319, 0.681], 1e-3)
|
| 457 |
-
assert abs(objectives.update_cone(60.0, 1 / 3) - 64.1) < 0.05
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
def test_pcomole_rollout():
|
| 461 |
-
omega, reference = torch.tensor([0.5, 0.5]), torch.zeros(2)
|
| 462 |
-
first = [torch.tensor(s) for s in ((0.8, 0.4), (0.6, 0.6), (0.9, 0.1), (0.5, 0.5))]
|
| 463 |
-
value, rewards = objectives.rollout_value(first, [1, 1, 0, 1], omega, reference, 2.0)
|
| 464 |
-
close(rewards, [1.682, 2.054, 0.0, 1.822], 1e-3)
|
| 465 |
-
assert abs(value - 1.390) < 1e-3
|
| 466 |
-
second = [torch.tensor(s) for s in ((1.0, 0.0), (0.9, 0.1), (0.95, 0.05), (1.0, 0.1))]
|
| 467 |
-
other, _ = objectives.rollout_value(second, [1, 1, 1, 1], omega, reference, 2.0)
|
| 468 |
-
assert abs(other - 1.181) < 1e-3
|
| 469 |
-
products = torch.tensor([0.6 * value, 1.2 * other, 0.0])
|
| 470 |
-
close(products.tolist(), [0.834, 1.417, 0.0], 1e-3)
|
| 471 |
-
close((products / products.sum()).tolist(), [0.371, 0.629, 0.0], 1e-3)
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
# %% Sections 5.36 to 5.41: rectification and AReUReDi
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
def test_total_correlation_on_two_bases():
|
| 478 |
-
source = torch.full((rectify.STATES,), 1.0 / rectify.STATES)
|
| 479 |
-
q, support = rectify.complementary_pairs()
|
| 480 |
-
coupling = q[None, :].repeat(rectify.STATES, 1)
|
| 481 |
-
assert abs(rectify.total_correlation(coupling, source) - math.log(4)) < 1e-9
|
| 482 |
-
assert abs(rectify.hamming(coupling, source) - 1.5) < 1e-9
|
| 483 |
-
assert abs(rectify.one_step_success(coupling, source, support) - 0.25) < 1e-9
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
def test_redi_rounds():
|
| 487 |
-
source = torch.full((rectify.STATES,), 1.0 / rectify.STATES)
|
| 488 |
-
q, support = rectify.complementary_pairs()
|
| 489 |
-
coupling = q[None, :].repeat(rectify.STATES, 1)
|
| 490 |
-
coupling = rectify.recouple(coupling, source, steps=300)
|
| 491 |
-
assert abs(rectify.total_correlation(coupling, source) - 1.101) < 2e-3
|
| 492 |
-
assert abs(rectify.hamming(coupling, source) - 1.022) < 2e-3
|
| 493 |
-
assert abs(rectify.one_step_success(coupling, source, support) - 0.403) < 2e-3
|
| 494 |
-
columns = [a * 4 + b for a, b in support]
|
| 495 |
-
close([float(coupling[0, c]) for c in columns], [0.400, 0.400, 0.100, 0.100], 2e-3)
|
| 496 |
-
close([float(coupling[3, c]) for c in columns], [0.755, 0.082, 0.082, 0.082], 2e-3)
|
| 497 |
-
coupling = rectify.recouple(coupling, source, steps=300)
|
| 498 |
-
assert abs(rectify.total_correlation(coupling, source) - 0.843) < 2e-3
|
| 499 |
-
assert abs(rectify.one_step_success(coupling, source, support) - 0.525) < 2e-3
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
def test_areuredi_proposal_and_acceptance():
|
| 503 |
-
scores = torch.tensor([[0.60, 0.80], [0.70, 0.76], [0.50, 0.90], [0.64, 0.62]])
|
| 504 |
-
weights = torch.tensor([0.5, 0.5])
|
| 505 |
-
assert abs(rectify.anneal(11, 20) - 12.0) < 1e-12
|
| 506 |
-
tchebycheff = rectify.tchebycheff(scores, weights)
|
| 507 |
-
close(tchebycheff.tolist(), [0.300, 0.350, 0.250, 0.310])
|
| 508 |
-
tilts = (12.0 * tchebycheff).exp()
|
| 509 |
-
close(tilts.tolist(), [36.60, 66.69, 20.09, 41.26], 5e-2)
|
| 510 |
-
model = torch.tensor([0.40, 0.25, 0.20, 0.15])
|
| 511 |
-
q_current, z_current, ratios = rectify.proposal(model, tilts, 0)
|
| 512 |
-
close(ratios.tolist(), [1.000, 1.822, 0.549, 1.127], 1e-3)
|
| 513 |
-
close(rectify.barker(ratios).tolist(), [0.500, 0.646, 0.354, 0.530], 1e-3)
|
| 514 |
-
assert abs(z_current - 0.5118) < 5e-4
|
| 515 |
-
close(q_current.tolist(), [0.391, 0.315, 0.138, 0.155], 1e-3)
|
| 516 |
-
_, z_proposed, _ = rectify.proposal(model, tilts, 1)
|
| 517 |
-
assert abs(z_proposed - 0.3704) < 5e-4
|
| 518 |
-
assert abs(rectify.acceptance(z_current, z_proposed) - 1.0) < 1e-12
|
| 519 |
-
assert abs(rectify.acceptance(z_proposed, z_current) - 0.724) < 1e-3
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
def test_areuredi_concentration():
|
| 523 |
-
base = torch.tensor([0.50, 0.25, 0.15, 0.10])
|
| 524 |
-
objective = torch.tensor([0.20, 0.30, 0.45, 0.45])
|
| 525 |
-
tilted = rectify.tilted_distribution(base, objective, 20.0)
|
| 526 |
-
close(tilted.tolist(), [0.0127, 0.0468, 0.5643, 0.3762], 1e-3)
|
| 527 |
-
assert abs(float(tilted[2:].sum()) - 0.9405) < 1e-3
|
| 528 |
-
bound, gap = rectify.concentration_bound(base, objective, 20.0)
|
| 529 |
-
assert abs(gap - 0.15) < 1e-9
|
| 530 |
-
assert abs(bound * float(base[1]) - 0.0498) < 5e-4
|
| 531 |
-
assert bound * float(base[1]) >= float(tilted[1])
|
| 532 |
-
sharper = rectify.tilted_distribution(base, objective, 60.0)
|
| 533 |
-
assert abs(float(sharper[2:].sum()) - 0.9999) < 1e-3
|
| 534 |
-
assert abs(float(sharper[1]) - 1.234e-4) < 1e-6
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
# %% The command line of every file
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
@pytest.mark.parametrize("script", [
|
| 541 |
-
"velocity", "kinetic_optimal", "rectify", "edit_flows", "multi_objective",
|
| 542 |
-
])
|
| 543 |
-
def test_script_runs(script):
|
| 544 |
-
"""The five files that need no training run end to end at the smallest grid."""
|
| 545 |
-
result = subprocess.run(
|
| 546 |
-
[sys.executable, f"lecture_5/{script}.py", "--steps", "20", "--quiet"],
|
| 547 |
-
cwd=ROOT, capture_output=True, text=True, env={**os.environ, "PYTHONPATH": str(ROOT)},
|
| 548 |
-
)
|
| 549 |
-
assert result.returncode == 0, result.stderr[-2000:]
|
| 550 |
-
assert "nan" not in result.stdout.lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_lecture_6.py
DELETED
|
@@ -1,398 +0,0 @@
|
|
| 1 |
-
"""The worked numbers of Chapter 6, asserted against the Lecture 6 files.
|
| 2 |
-
|
| 3 |
-
Every number checked here appears in the chapter text, and every one is computed by the file under
|
| 4 |
-
test and not stored in it. The training loops are not run; the tests call the closed-form and
|
| 5 |
-
numerical functions the lectures build their reports from.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import importlib.util
|
| 11 |
-
import math
|
| 12 |
-
import sys
|
| 13 |
-
from pathlib import Path
|
| 14 |
-
|
| 15 |
-
import pytest
|
| 16 |
-
import torch
|
| 17 |
-
|
| 18 |
-
ROOT = Path(__file__).resolve().parents[1]
|
| 19 |
-
sys.path.insert(0, str(ROOT))
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
def load(name: str):
|
| 23 |
-
"""Import one lecture file by path, so that ``lecture_6`` needs no package marker."""
|
| 24 |
-
spec = importlib.util.spec_from_file_location(f"lecture_6_{name}",
|
| 25 |
-
ROOT / "lecture_6" / f"{name}.py")
|
| 26 |
-
module = importlib.util.module_from_spec(spec)
|
| 27 |
-
spec.loader.exec_module(module)
|
| 28 |
-
return module
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def close(values, expected, tolerance=5e-4):
|
| 32 |
-
"""Compare a list of numbers against the chapter's, coordinate by coordinate."""
|
| 33 |
-
assert len(values) == len(expected)
|
| 34 |
-
for got, want in zip(values, expected):
|
| 35 |
-
assert got == pytest.approx(want, abs=tolerance)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# %% 01. flow_map.py, Sections 6.3 to 6.12
|
| 39 |
-
def test_flow_map_listing():
|
| 40 |
-
flow_map = load("flow_map")
|
| 41 |
-
x = torch.tensor(0.7, dtype=torch.float64)
|
| 42 |
-
assert float(flow_map.flow(x, 0.0, 1.0)) == pytest.approx(4.1, abs=1e-9)
|
| 43 |
-
assert float(flow_map.flow(flow_map.flow(x, 0.0, 0.5), 0.5, 1.0)) == pytest.approx(4.1, abs=1e-9)
|
| 44 |
-
assert float(flow_map.flow(flow_map.flow(x, 0.0, 1.0), 1.0, 0.0)) == pytest.approx(0.7, abs=1e-9)
|
| 45 |
-
from torch.func import jacrev
|
| 46 |
-
|
| 47 |
-
assert float(jacrev(lambda z: flow_map.flow(z, 0.0, 1.0))(x)) == pytest.approx(3.0, abs=1e-9)
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def test_flow_map_axioms_and_liouville():
|
| 51 |
-
flow_map = load("flow_map")
|
| 52 |
-
x = torch.tensor(0.7, dtype=torch.float64)
|
| 53 |
-
identity, cocycle, tangent = flow_map.check_axioms(x, 200)
|
| 54 |
-
assert identity < 1e-12 and cocycle < 1e-10 and tangent < 1e-4
|
| 55 |
-
assert flow_map.liouville_log_det(0.0, 1.0, 4000) == pytest.approx(math.log(3.0), abs=1e-6)
|
| 56 |
-
assert float(flow_map.jacobian_by_ode(x, 0.0, 1.0, 4000)) == pytest.approx(3.0, abs=1e-3)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def test_flow_map_transport_and_likelihood():
|
| 60 |
-
flow_map = load("flow_map")
|
| 61 |
-
source = flow_map.normal_density(0.7, 0.0, 1.0)
|
| 62 |
-
data = flow_map.normal_density(4.1, 2.0, 3.0)
|
| 63 |
-
assert source == pytest.approx(0.3123, abs=1e-4)
|
| 64 |
-
assert data == pytest.approx(0.1041, abs=1e-4)
|
| 65 |
-
assert source / data == pytest.approx(3.0, abs=1e-6)
|
| 66 |
-
assert math.log(source) - math.log(3.0) == pytest.approx(-2.2625, abs=1e-4)
|
| 67 |
-
assert math.log(data) == pytest.approx(-2.2625, abs=1e-4)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
# %% 02. residuals.py, Sections 6.16 and 6.17
|
| 71 |
-
def test_stay_still_residuals():
|
| 72 |
-
residuals = load("residuals")
|
| 73 |
-
report = residuals.stay_still_residuals()
|
| 74 |
-
assert report["stay-still Lagrangian"] == pytest.approx(1.0, abs=1e-9)
|
| 75 |
-
assert report["stay-still Eulerian"] == pytest.approx(1.0, abs=1e-9)
|
| 76 |
-
assert report["stay-still composition"] == pytest.approx(0.0, abs=1e-12)
|
| 77 |
-
assert report["stay-still diagonal gap"] == pytest.approx(0.0, abs=1e-12)
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def test_identity_axiom_is_free():
|
| 81 |
-
residuals = load("residuals")
|
| 82 |
-
torch.manual_seed(0)
|
| 83 |
-
model = residuals.Map(dim=2, width=16)
|
| 84 |
-
x, t = torch.randn(8, 2), torch.rand(8)
|
| 85 |
-
assert torch.allclose(model.X(x, t, t), x)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
# %% 03. meanflow.py, Sections 6.13 and 6.18
|
| 89 |
-
def test_meanflow_scalar_identity():
|
| 90 |
-
meanflow = load("meanflow")
|
| 91 |
-
report = meanflow.scalar_check()
|
| 92 |
-
assert report["v_0,1(1)"] == pytest.approx(math.e - 1.0, abs=1e-5)
|
| 93 |
-
assert report["b(1, 0)"] == pytest.approx(1.0, abs=1e-6)
|
| 94 |
-
assert report["partial_s v"] == pytest.approx(-1.0, abs=1e-5)
|
| 95 |
-
assert report["grad_x v_0,1"] == pytest.approx(math.e - 1.0, abs=1e-5)
|
| 96 |
-
assert report["d/ds v_0,1"] == pytest.approx(math.e - 2.0, abs=1e-5)
|
| 97 |
-
assert report["identity gap"] == pytest.approx(0.0, abs=1e-5)
|
| 98 |
-
assert report["one map evaluation"] == pytest.approx(math.e, abs=1e-5)
|
| 99 |
-
assert report["one Euler step"] == pytest.approx(2.0, abs=1e-6)
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def test_meanflow_correction_vanishes_on_the_gaussian_flow():
|
| 103 |
-
meanflow = load("meanflow")
|
| 104 |
-
report = meanflow.gaussian_check()
|
| 105 |
-
assert report["v_0,t(0.7) spread"] == pytest.approx(0.0, abs=1e-6)
|
| 106 |
-
assert report["v_0,1(0.7)"] == pytest.approx(3.4, abs=1e-5)
|
| 107 |
-
assert report["b(0.7, 0)"] == pytest.approx(3.4, abs=1e-5)
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
# %% 04. shortcut.py, Section 6.20
|
| 111 |
-
def test_shortcut_doubling():
|
| 112 |
-
shortcut = load("shortcut")
|
| 113 |
-
report = shortcut.scalar_doubling()
|
| 114 |
-
assert report["s(1, 0, d)"] == pytest.approx(1.136102, abs=1e-6)
|
| 115 |
-
assert report["state after the first step"] == pytest.approx(1.284025, abs=1e-6)
|
| 116 |
-
assert report["s(x', d, d)"] == pytest.approx(1.458783, abs=1e-6)
|
| 117 |
-
assert report["average of the two"] == pytest.approx(1.297443, abs=1e-6)
|
| 118 |
-
assert report["s(1, 0, 2d) direct"] == pytest.approx(1.297443, abs=1e-6)
|
| 119 |
-
assert report["doubling gap"] == pytest.approx(0.0, abs=1e-12)
|
| 120 |
-
assert report["state after the doubled step"] == pytest.approx(math.exp(0.5), abs=1e-9)
|
| 121 |
-
assert shortcut.ladder()[0] == 1.0 / 128.0 and shortcut.ladder()[-1] == 1.0
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
# %% 05. consistency.py, Section 6.20
|
| 125 |
-
def test_consistency_boundary_parameterization():
|
| 126 |
-
consistency = load("consistency")
|
| 127 |
-
report = consistency.scalar_consistency()
|
| 128 |
-
assert report["f(0.5)"] == pytest.approx(math.exp(0.5), abs=1e-6)
|
| 129 |
-
assert report["f(0.9)"] == pytest.approx(math.exp(0.1), abs=1e-6)
|
| 130 |
-
assert report["f(1.0)"] == pytest.approx(1.0, abs=1e-9)
|
| 131 |
-
assert report["c_out(1.0)"] == pytest.approx(0.0, abs=1e-12)
|
| 132 |
-
assert report["v_0.5,1(1)"] == pytest.approx(1.297443, abs=1e-6)
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def test_consistency_teacher_floor():
|
| 136 |
-
consistency = load("consistency")
|
| 137 |
-
report = consistency.teacher_floor()
|
| 138 |
-
assert report["f(1, 0.5)"] == pytest.approx(1.648721, abs=1e-6)
|
| 139 |
-
assert report["f(1.01, 0.51)"] == pytest.approx(1.648639, abs=1e-6)
|
| 140 |
-
assert report["distillation residual"] == pytest.approx(8.189e-5, rel=1e-3)
|
| 141 |
-
assert report["local Euler error"] == pytest.approx(5.0167e-5, rel=1e-3)
|
| 142 |
-
assert report["carried to the data end"] == pytest.approx(
|
| 143 |
-
report["distillation residual"], rel=1e-6)
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
def test_consistency_trajectory_head():
|
| 147 |
-
consistency = load("consistency")
|
| 148 |
-
report = consistency.scalar_ctm()
|
| 149 |
-
assert report["a_0.0,0.5"] == pytest.approx(0.5, abs=1e-12)
|
| 150 |
-
assert report["g_0.0,0.5"] == pytest.approx(2.297443, abs=1e-6)
|
| 151 |
-
assert report["a_0.0,1.0"] == pytest.approx(0.0, abs=1e-12)
|
| 152 |
-
assert report["g_0.0,1.0"] == pytest.approx(math.e, abs=1e-6)
|
| 153 |
-
assert report["two routes apart"] == pytest.approx(0.0, abs=1e-12)
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
# %% 06. fmlm.py, Sections 6.26 and 6.27
|
| 157 |
-
def test_fmlm_denoiser_cocycle():
|
| 158 |
-
fmlm = load("fmlm")
|
| 159 |
-
report = fmlm.scalar_cocycle()
|
| 160 |
-
assert report["gamma"] == pytest.approx(0.2, abs=1e-12)
|
| 161 |
-
assert report["delta_s,t"] == pytest.approx(2.096158, abs=1e-6)
|
| 162 |
-
assert report["delta_s,u"] == pytest.approx(1.932957, abs=1e-6)
|
| 163 |
-
assert report["delta_u,t"] == pytest.approx(2.136959, abs=1e-6)
|
| 164 |
-
assert report["two legs combined"] == pytest.approx(2.096158, abs=1e-6)
|
| 165 |
-
assert report["convex weights"] == (0.25, 0.75)
|
| 166 |
-
assert report["convex form"] == pytest.approx(math.exp(0.6), abs=1e-6)
|
| 167 |
-
assert report["X_s,t(x)"] == pytest.approx(math.exp(0.6), abs=1e-9)
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
def test_fmlm_worked_kl():
|
| 171 |
-
fmlm = load("fmlm")
|
| 172 |
-
report = fmlm.worked_kl()
|
| 173 |
-
assert report["worked gamma"] == pytest.approx(0.2, abs=1e-12)
|
| 174 |
-
close(report["composed target"], [0.06, 0.12, 0.76, 0.06], 1e-6)
|
| 175 |
-
assert report["target mass"] == pytest.approx(1.0, abs=1e-6)
|
| 176 |
-
close(report["the four terms"], [-0.030650, -0.026777, 0.118823, -0.030650], 1e-6)
|
| 177 |
-
assert report["worked KL"] == pytest.approx(0.030747, abs=1e-6)
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
def test_fmlm_gradient_comparison():
|
| 181 |
-
"""Section 6.27.4, the two gradients in the logits at the chapter's target and student."""
|
| 182 |
-
fmlm = load("fmlm")
|
| 183 |
-
report = fmlm.gradient_comparison()
|
| 184 |
-
close(report["KL gradient"], [0.04, 0.03, -0.11, 0.04], 1e-6)
|
| 185 |
-
close(report["squared-error gradient"], [0.0099, 0.01335, -0.03315, 0.0099], 1e-6)
|
| 186 |
-
close(report["squared over KL"], [0.2475, 0.445, 0.3014, 0.2475], 1e-4) # A quarter to a half.
|
| 187 |
-
close(report["saturated student"], [1e-6, 0.1666665, 0.7222215, 0.111111], 1e-6)
|
| 188 |
-
assert report["saturated KL gradient"] == pytest.approx(-0.0600, abs=1e-4)
|
| 189 |
-
# The chapter quotes -6.10e-08 here. Differentiating its own expression
|
| 190 |
-
# d/dz_i (1/2) sum_j (q_j - p_j)^2 = q_i (q_i - p_i) - q_i sum_j q_j (q_j - p_j)
|
| 191 |
-
# at q_1 = 1e-06 gives -4.6171e-08. The first term on its own is -6.00e-08, so the quoted
|
| 192 |
-
# number is the first term with the second dropped, and this file prints the full expression.
|
| 193 |
-
assert report["saturated squared-error gradient, in 1e-08"] == pytest.approx(-4.6171, abs=1e-3)
|
| 194 |
-
assert report["orders of magnitude between them"] == pytest.approx(6.11, abs=0.01)
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
def test_fmlm_decoding_clock():
|
| 198 |
-
fmlm = load("fmlm")
|
| 199 |
-
tau, start = fmlm.clock(fmlm.BOOK_VOCAB)
|
| 200 |
-
assert start == pytest.approx(0.999967, abs=1e-5)
|
| 201 |
-
assert tau(0.60) == pytest.approx(0.006, abs=5e-4)
|
| 202 |
-
assert tau(0.75) == pytest.approx(0.141, abs=5e-4)
|
| 203 |
-
assert tau(0.80) == pytest.approx(0.456, abs=1e-3)
|
| 204 |
-
assert tau(0.85) == pytest.approx(0.931, abs=1e-3)
|
| 205 |
-
grid = fmlm.even_in_tau(fmlm.BOOK_VOCAB)
|
| 206 |
-
close(grid, [0.0, 0.773490, 0.804481, 0.828095, 1.0], 5e-5)
|
| 207 |
-
close([tau(t) for t in (0.25, 0.5, 0.75)], [0.00009, 0.00124, 0.14109], 1e-5)
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
def test_fmlm_clock_weights():
|
| 211 |
-
fmlm = load("fmlm")
|
| 212 |
-
rounded = [0.0, 0.773, 0.804, 0.828, 1.0] # The chapter quotes the grid to three decimals.
|
| 213 |
-
pairs = [((1 - b) / (1 - a), (b - a) / (1 - a)) for a, b in zip(rounded[:-1], rounded[1:])]
|
| 214 |
-
close([p[0] for p in pairs], [0.2270, 0.8634, 0.8776, 0.0], 1e-4)
|
| 215 |
-
close([p[1] for p in pairs], [0.7730, 0.1366, 0.1224, 1.0], 1e-4)
|
| 216 |
-
times = [torch.tensor([v]) for v in rounded[1:4]]
|
| 217 |
-
assert float(fmlm.composition_weight(*times)) == pytest.approx(0.494620, abs=1e-5)
|
| 218 |
-
early = [torch.tensor([v]) for v in (0.0, 0.773, 0.804)]
|
| 219 |
-
assert float(fmlm.composition_weight(*early)) == pytest.approx(0.830144, abs=1e-5)
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
def test_fmlm_endpoint_slice_is_trained():
|
| 223 |
-
"""Section 6.26 asks for t = 1 on purpose, since a continuous draw never lands there."""
|
| 224 |
-
fmlm = load("fmlm")
|
| 225 |
-
assert 0.0 < fmlm.ENDPOINT_FRACTION < 1.0
|
| 226 |
-
tokens, _ = fmlm.synthetic_dna(num_sequences=64, length=16, seed=0)
|
| 227 |
-
model = fmlm.TwoTimeMLP(dim=16 * 4, width=16)
|
| 228 |
-
torch.manual_seed(0)
|
| 229 |
-
loss_fn = fmlm.make_loss(model, tokens, 16, 4, 256, torch.device("cpu"))
|
| 230 |
-
assert torch.isfinite(loss_fn(0)) # The rows at t = 1 go through the loss like any other.
|
| 231 |
-
# The map at t = 1 is the prediction itself, so the one-step sampler reads that slice alone.
|
| 232 |
-
x = torch.randn(2, 16, 4)
|
| 233 |
-
s, t = torch.zeros(2), torch.ones(2)
|
| 234 |
-
prediction = fmlm.predict(model, x, s, t, 16, 4)
|
| 235 |
-
assert torch.allclose(fmlm.step_map(x, prediction, s, t), prediction)
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
# %% 07. categorical_map.py, Section 6.28
|
| 239 |
-
def test_categorical_worked_cancellation():
|
| 240 |
-
categorical = load("categorical_map")
|
| 241 |
-
report = categorical.worked_cancellation()
|
| 242 |
-
close(report["state x"], [-0.2591, -0.6675, -0.0467], 1e-4)
|
| 243 |
-
close(report["X_s,t(x)"], [0.1611, -0.2420, 0.0943], 1e-4)
|
| 244 |
-
close(report["pi_s,t(x)"], [0.5813, 0.1835, 0.2352], 1e-4)
|
| 245 |
-
close(report["pi_t,t(X_s,t(x))"], [0.6927, 0.0917, 0.2157], 1e-4)
|
| 246 |
-
assert report["gamma_s,t"] == pytest.approx(0.2, abs=1e-12)
|
| 247 |
-
close(report["d pi / dt"], [0.5570, -0.4592, -0.0979], 1e-4)
|
| 248 |
-
close(report["central difference"], [0.5570, -0.4592, -0.0979], 1e-4)
|
| 249 |
-
close(report["gap a"], [-0.111, 0.092, 0.020], 1e-3)
|
| 250 |
-
close(report["drift b"], [0.111, -0.092, -0.020], 1e-3)
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
def test_categorical_bound_is_positive_where_the_residual_is_zero():
|
| 254 |
-
categorical = load("categorical_map")
|
| 255 |
-
report = categorical.worked_cancellation()
|
| 256 |
-
assert report["residual |a + b|^2"] < 1e-20
|
| 257 |
-
assert report["4 KL"] == pytest.approx(0.156, abs=1e-3)
|
| 258 |
-
assert report["2 |b|^2"] == pytest.approx(0.042, abs=1e-3)
|
| 259 |
-
assert report["the bound"] == pytest.approx(0.199, abs=2e-3)
|
| 260 |
-
assert report["teacher entropy"] == pytest.approx(0.8043, abs=1e-3)
|
| 261 |
-
assert report["cross-entropy"] == pytest.approx(0.8434, abs=1e-3)
|
| 262 |
-
assert report["cross-entropy"] == pytest.approx(
|
| 263 |
-
0.25 * report["4 KL"] + report["teacher entropy"], abs=1e-6)
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
def test_categorical_averaged_bound():
|
| 267 |
-
categorical = load("categorical_map")
|
| 268 |
-
report = categorical.averaged_bound(1000, seed=0)
|
| 269 |
-
assert report["averaged residual"] < 1e-18
|
| 270 |
-
assert report["averaged 4 KL"] == pytest.approx(0.39, abs=0.06)
|
| 271 |
-
assert report["averaged 2 |b|^2"] == pytest.approx(0.095, abs=0.03)
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
# %% 08. discrete_map.py, Sections 6.26 and 6.29
|
| 275 |
-
def test_discrete_derivative_target_leaves_the_simplex():
|
| 276 |
-
discrete = load("discrete_map")
|
| 277 |
-
report = discrete.worked_derivative()
|
| 278 |
-
assert report["derivative prefactor"] == pytest.approx(6.6667, abs=1e-3)
|
| 279 |
-
close(report["derivative target"], [-0.3333, -0.6667, 1.3333, -0.3333], 1e-4)
|
| 280 |
-
assert report["target sum"] == pytest.approx(0.0, abs=1e-6)
|
| 281 |
-
assert report["negative entries"] == 3
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
def test_discrete_logit_repair():
|
| 285 |
-
discrete = load("discrete_map")
|
| 286 |
-
report = discrete.worked_repair()
|
| 287 |
-
assert report["weighted average of dz/dt"] == pytest.approx(0.0, abs=1e-6)
|
| 288 |
-
close(report["d psi/dt"], [1.92, -1.14, -0.78], 1e-4)
|
| 289 |
-
close(report["additive target"], [-0.284, 0.828, 0.456], 1e-4)
|
| 290 |
-
assert report["additive target sum"] == pytest.approx(1.0, abs=1e-6)
|
| 291 |
-
assert report["additive negative entries"] == 1
|
| 292 |
-
close(report["multiplicative factor"], [2.920, 0.544, 0.480], 1e-4)
|
| 293 |
-
close(report["quotient"], [0.0342, 1.1029, 0.6250], 1e-4)
|
| 294 |
-
assert report["quotient mass"] == pytest.approx(1.7622, abs=1e-4)
|
| 295 |
-
close(report["repaired target"], [0.0194, 0.6259, 0.3547], 1e-4)
|
| 296 |
-
close(report["log of the factor"], [1.0716, -0.6088, -0.7340], 1e-4)
|
| 297 |
-
assert report["gamma_s,t"] == pytest.approx(0.2, abs=1e-12)
|
| 298 |
-
assert report["gamma^E_s,t"] == pytest.approx(0.8, abs=1e-12)
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
# %% 09. stochastic_map.py, Section 6.31
|
| 302 |
-
def test_stochastic_weak_floor():
|
| 303 |
-
stochastic = load("stochastic_map")
|
| 304 |
-
report = stochastic.weak_floor(draws=200_000, seed=0)
|
| 305 |
-
assert report["K_1"] == pytest.approx(3.194528, abs=1e-5)
|
| 306 |
-
assert report["sqrt K_1"] == pytest.approx(1.7873, abs=1e-4)
|
| 307 |
-
assert report["sqrt K_0.5"] == pytest.approx(0.9269, abs=1e-4)
|
| 308 |
-
assert report["one full step"] == pytest.approx(2.2453, abs=1e-3)
|
| 309 |
-
assert report["two half steps"] == pytest.approx(0.4673, abs=1e-3)
|
| 310 |
-
assert report["gap"] == pytest.approx(1.778, abs=1e-3)
|
| 311 |
-
assert report["squared gap"] == pytest.approx(3.161, abs=1e-3)
|
| 312 |
-
assert report["floor 2 K_1"] == pytest.approx(math.e ** 2 - 1.0, abs=1e-9)
|
| 313 |
-
assert report["carried variance equals K_1"] == pytest.approx(report["K_1"], abs=1e-9)
|
| 314 |
-
assert report["the draw that would close it"] == pytest.approx(-0.195, abs=1e-3)
|
| 315 |
-
assert report["Monte Carlo floor"] == pytest.approx(6.389, abs=0.05)
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
def test_stochastic_chen_relations():
|
| 319 |
-
stochastic = load("stochastic_map")
|
| 320 |
-
report = stochastic.worked_chen()
|
| 321 |
-
assert report["I0 over [0,1]"] == pytest.approx(0.50, abs=1e-9)
|
| 322 |
-
assert report["I1 over [0,1]"] == pytest.approx(-0.20, abs=1e-9)
|
| 323 |
-
assert report["fused variance at h=1"] == pytest.approx(1.0 / 3.0, abs=1e-9)
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
def test_stochastic_truncation():
|
| 327 |
-
stochastic = load("stochastic_map")
|
| 328 |
-
report = stochastic.truncation_error(paths=512, grid=2048, seed=0)
|
| 329 |
-
for n, expected in enumerate((1.0, 1.0 / 3.0, 1.0 / 5.0)):
|
| 330 |
-
assert report[f"coefficient variance I{n}"] == pytest.approx(expected, rel=0.15)
|
| 331 |
-
errors = [report[f"pathwise error with {k}"] for k in (1, 2, 3)]
|
| 332 |
-
assert errors[0] > 5.0 * errors[1] > 25.0 * errors[2]
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
# %% 10. expanding.py, Section 6.32
|
| 336 |
-
def test_expanding_local_clocks():
|
| 337 |
-
expanding = load("expanding")
|
| 338 |
-
report = expanding.worked_clocks()
|
| 339 |
-
close(report["local clocks at t=0.8"], [0.800, 0.800, 0.692, 0.333], 1e-3)
|
| 340 |
-
close(report["clock speeds"], [1.0, 1.0, 1.538, 3.333], 1e-3)
|
| 341 |
-
close(report["source share"], [0.200, 0.200, 0.308, 0.667], 1e-3)
|
| 342 |
-
assert report["last clock from its speed"] == pytest.approx(0.667, abs=1e-3)
|
| 343 |
-
assert report["live coordinates at 0.8"] == 4 and report["live coordinates at 0.6"] == 3
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
def test_expanding_composition_weight():
|
| 347 |
-
expanding = load("expanding")
|
| 348 |
-
report = expanding.worked_omega()
|
| 349 |
-
assert report["omega at (0.3, 0.6, 0.9)"] == pytest.approx(0.125, abs=1e-6)
|
| 350 |
-
assert report["omega for a birth at 0.0"] == pytest.approx(0.125, abs=1e-6)
|
| 351 |
-
assert report["omega for a birth at 0.7"] == pytest.approx(0.0, abs=1e-9)
|
| 352 |
-
assert 0.0 < report["omega for a birth at 0.35"] < report["omega for a birth at 0.0"]
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
# %% 11. posterior_map.py, Sections 6.33 and 6.34
|
| 356 |
-
def test_posterior_tilt_and_jensen_gap():
|
| 357 |
-
posterior = load("posterior_map")
|
| 358 |
-
report = posterior.two_point()
|
| 359 |
-
close(report["tilted target"], [0.1192, 0.8808], 1e-4)
|
| 360 |
-
close(report["posterior at x=0.7"], [0.9820, 0.0180], 1e-4)
|
| 361 |
-
close(report["tilted posterior"], [0.8808, 0.1192], 1e-4)
|
| 362 |
-
assert report["V_t(0.7)"] == pytest.approx(0.1088, abs=1e-4)
|
| 363 |
-
assert report["V over the data"] == pytest.approx(1.4338, abs=1e-4)
|
| 364 |
-
assert report["single-endpoint estimate"] == pytest.approx(1.0, abs=1e-9)
|
| 365 |
-
assert report["Jensen gap"] == pytest.approx(0.434, abs=1e-3)
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
def test_posterior_value_bias_and_gradient():
|
| 369 |
-
posterior = load("posterior_map")
|
| 370 |
-
report = posterior.value_bias()
|
| 371 |
-
close([report[f"E[V_{k}]"] for k in (1, 2, 4, 8)], [1.000, 1.217, 1.340, 1.393], 1e-3)
|
| 372 |
-
close([report[f"bias at K={k}"] for k in (1, 2, 4, 8)], [0.434, 0.217, 0.093, 0.041], 1e-3)
|
| 373 |
-
weights, weighted, plain = posterior.value_gradient(
|
| 374 |
-
torch.tensor([0.2, 1.5, 0.9]), torch.tensor([0.4, -1.2, 0.5]))
|
| 375 |
-
close(weights.tolist(), [0.1496, 0.5491, 0.3013], 1e-4)
|
| 376 |
-
assert weighted == pytest.approx(-0.4483, abs=1e-4)
|
| 377 |
-
assert plain == pytest.approx(-0.1000, abs=1e-6)
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
def test_posterior_renoising_and_meta_fixed_point():
|
| 381 |
-
posterior = load("posterior_map")
|
| 382 |
-
report = posterior.renoise_example()
|
| 383 |
-
assert report["renoise scale"] == pytest.approx(0.75, abs=1e-9)
|
| 384 |
-
assert report["renoise variance"] == pytest.approx(0.1375, abs=1e-9)
|
| 385 |
-
assert report["renoise standard deviation"] == pytest.approx(0.3708, abs=1e-4)
|
| 386 |
-
assert report["proposal weight on the far mode"] == pytest.approx(0.420, abs=1e-3)
|
| 387 |
-
assert report["penalty at z=2"] == pytest.approx(10.125, abs=1e-6)
|
| 388 |
-
assert report["penalty at z=0"] == pytest.approx(6.125, abs=1e-6)
|
| 389 |
-
assert report["corrected weight on the far mode"] == pytest.approx(0.0131, abs=1e-4)
|
| 390 |
-
meta = posterior.stop_gradient_fixed_point(torch.tensor([1.0, 2.0, 5.0]),
|
| 391 |
-
torch.tensor([0.4, 1.0, 3.6]))
|
| 392 |
-
assert meta["E[w]"] == pytest.approx(8.0 / 3.0, abs=1e-6)
|
| 393 |
-
assert meta["E[G]"] == pytest.approx(5.0 / 3.0, abs=1e-6)
|
| 394 |
-
assert meta["stop-gradient fixed point"] == pytest.approx(0.625, abs=1e-6)
|
| 395 |
-
assert meta["per-sample ratio average"] == pytest.approx(0.540, abs=1e-6)
|
| 396 |
-
assert meta["gap"] == pytest.approx(-0.085, abs=1e-3)
|
| 397 |
-
assert meta["covariance of G/w and w"] == pytest.approx(0.2267, abs=1e-4)
|
| 398 |
-
assert meta["gap from the covariance"] == pytest.approx(meta["gap"], abs=1e-6)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_lecture_7.py
DELETED
|
@@ -1,709 +0,0 @@
|
|
| 1 |
-
"""Lecture 7: every number the chapter works by hand, asserted against the file that prints it.
|
| 2 |
-
|
| 3 |
-
Chapter 7 computes almost all of its results on paper, so these tests are the chapter itself read
|
| 4 |
-
back. The tolerances are the number of digits the chapter quotes.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from __future__ import annotations
|
| 8 |
-
|
| 9 |
-
import importlib.util
|
| 10 |
-
import math
|
| 11 |
-
import sys
|
| 12 |
-
from pathlib import Path
|
| 13 |
-
|
| 14 |
-
import pytest
|
| 15 |
-
import torch
|
| 16 |
-
|
| 17 |
-
LECTURE = Path(__file__).resolve().parent.parent / "lecture_7"
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def load(name: str):
|
| 21 |
-
"""Import one lecture file by path, since ``lecture_7`` is a folder of scripts, not a package."""
|
| 22 |
-
spec = importlib.util.spec_from_file_location(f"lecture_7_{name}", LECTURE / f"{name}.py")
|
| 23 |
-
module = importlib.util.module_from_spec(spec)
|
| 24 |
-
sys.modules[spec.name] = module
|
| 25 |
-
spec.loader.exec_module(module)
|
| 26 |
-
return module
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def tensor(*values):
|
| 30 |
-
return torch.tensor(values, dtype=torch.float64)
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
# %% Sections 7.1 to 7.6, optimal_transport.py
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def test_four_points_and_two_pairings():
|
| 37 |
-
ot = load("optimal_transport")
|
| 38 |
-
pairings = ot.two_pairings()
|
| 39 |
-
assert pairings["arrows_ordered"] == pytest.approx(2.0)
|
| 40 |
-
assert pairings["arrows_crossed"] == pytest.approx(10.0)
|
| 41 |
-
assert pairings["mass_ordered"] == pytest.approx(1.0)
|
| 42 |
-
assert pairings["mass_crossed"] == pytest.approx(5.0)
|
| 43 |
-
assert pairings["mass_independent"] == pytest.approx(3.0)
|
| 44 |
-
assert pairings["C"].tolist() == [[1.0, 9.0], [1.0, 1.0]]
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def test_two_by_two_plan_and_prices():
|
| 48 |
-
ot = load("optimal_transport")
|
| 49 |
-
assert ot.feasible_interval() == (0.0, 0.3)
|
| 50 |
-
C = ot.cost_matrix(ot.SOURCE_X, ot.TARGET_Y)
|
| 51 |
-
assert ot.cost_along_the_segment(0.0, C) == pytest.approx(5.8)
|
| 52 |
-
assert ot.cost_along_the_segment(0.3, C) == pytest.approx(3.4)
|
| 53 |
-
plan = ot.feasible_plan(0.3)
|
| 54 |
-
assert plan.tolist() == [[0.3, 0.3], [0.0, 0.4]]
|
| 55 |
-
assert ot.is_coupling(plan, ot.SOURCE_A, ot.TARGET_B)
|
| 56 |
-
f, g = ot.prices_from_loaded_routes(C, plan)
|
| 57 |
-
assert f.tolist() == [0.0, -8.0] and g.tolist() == [1.0, 9.0]
|
| 58 |
-
assert ot.slack(C, f, g).tolist() == [[0.0, 0.0], [8.0, 0.0]]
|
| 59 |
-
assert ot.dual_value(ot.SOURCE_A, f, ot.TARGET_B, g) == pytest.approx(3.4)
|
| 60 |
-
assert ot.plan_cost(C, plan) ** 0.5 == pytest.approx(1.844, abs=5e-4)
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def test_no_crossing_and_three_points():
|
| 64 |
-
ot = load("optimal_transport")
|
| 65 |
-
expanded, collected = ot.crossing_difference(0.0, 2.0, 1.0, 3.0)
|
| 66 |
-
assert expanded == pytest.approx(collected) and collected == pytest.approx(-8.0)
|
| 67 |
-
x = tensor(4.0, 1.0, 6.0)
|
| 68 |
-
y = tensor(9.0, 2.0, 3.0)
|
| 69 |
-
costs = [c for c, _ in ot.monge_permutations(x.sort().values, y.sort().values)]
|
| 70 |
-
assert costs == pytest.approx([11 / 3, 17 / 3, 35 / 3, 45 / 3, 77 / 3, 81 / 3])
|
| 71 |
-
assert ot.wasserstein_on_the_line(x, y, 2.0, 3072) == pytest.approx(11 / 3)
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def test_gaussian_closed_form():
|
| 75 |
-
ot = load("optimal_transport")
|
| 76 |
-
assert ot.gaussian_w2_squared(0.0, 1.0, 3.0, 2.0) == pytest.approx(10.0)
|
| 77 |
-
assert math.sqrt(10.0) == pytest.approx(3.162, abs=5e-4)
|
| 78 |
-
points = tensor(-1.0, 0.0, 1.0)
|
| 79 |
-
assert ot.brenier_map(points, 0.0, 1.0, 3.0, 2.0).tolist() == [1.0, 3.0, 5.0]
|
| 80 |
-
source = torch.special.ndtri(ot.quantile_levels(3072))
|
| 81 |
-
mapped = ot.brenier_map(source, 0.0, 1.0, 3.0, 2.0)
|
| 82 |
-
assert float(((mapped - source) ** 2).mean()) == pytest.approx(10.0, abs=1e-3)
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
# %% Sections 7.9 and 7.10, sinkhorn.py
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def test_entropies_of_the_three_couplings():
|
| 89 |
-
sk = load("sinkhorn")
|
| 90 |
-
independent = sk.independent_coupling(sk.SOURCE_A, sk.TARGET_B)
|
| 91 |
-
assert independent.flatten().tolist() == pytest.approx([0.18, 0.42, 0.12, 0.28])
|
| 92 |
-
assert sk.entropy(sk.EXACT_PLAN) == pytest.approx(1.089, abs=5e-4)
|
| 93 |
-
assert sk.entropy(independent) == pytest.approx(1.284, abs=5e-4)
|
| 94 |
-
assert sk.entropy(sk.SOURCE_A) == pytest.approx(0.673, abs=5e-4)
|
| 95 |
-
assert sk.entropy(sk.TARGET_B) == pytest.approx(0.611, abs=5e-4)
|
| 96 |
-
assert sk.relative_entropy(sk.EXACT_PLAN, independent) == pytest.approx(0.195, abs=5e-4)
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def test_sinkhorn_first_sweep_by_hand():
|
| 100 |
-
sk = load("sinkhorn")
|
| 101 |
-
K = sk.gibbs_kernel(sk.COST, 2.0)
|
| 102 |
-
assert K.flatten().tolist() == pytest.approx([0.606531, 0.011109, 0.606531, 0.606531],
|
| 103 |
-
abs=5e-7)
|
| 104 |
-
v = torch.ones(2, dtype=torch.float64)
|
| 105 |
-
assert (K @ v).tolist() == pytest.approx([0.617640, 1.213061], abs=5e-7)
|
| 106 |
-
u = sk.SOURCE_A / (K @ v)
|
| 107 |
-
assert u.tolist() == pytest.approx([0.971440, 0.329744], abs=5e-7)
|
| 108 |
-
assert (K.T @ u).tolist() == pytest.approx([0.789208, 0.210792], abs=5e-7)
|
| 109 |
-
v = sk.TARGET_B / (K.T @ u)
|
| 110 |
-
assert v.tolist() == pytest.approx([0.38013, 3.32081], abs=5e-6)
|
| 111 |
-
plan = sk.scaling_form(u, K, v)
|
| 112 |
-
assert plan.flatten().tolist() == pytest.approx([0.2240, 0.0358, 0.0760, 0.6642], abs=5e-5)
|
| 113 |
-
assert plan.sum(dim=0).tolist() == pytest.approx([0.3, 0.7])
|
| 114 |
-
assert plan.sum(dim=1).tolist() == pytest.approx([0.2598, 0.7402], abs=5e-5)
|
| 115 |
-
after = sk.project_rows(plan, sk.SOURCE_A)
|
| 116 |
-
assert after.sum(dim=0).tolist() == pytest.approx([0.558, 0.442], abs=5e-4)
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def test_sinkhorn_convergence_and_epsilon():
|
| 120 |
-
sk = load("sinkhorn")
|
| 121 |
-
K = sk.gibbs_kernel(sk.COST, 2.0)
|
| 122 |
-
u, v, errors = sk.sinkhorn_sweeps(K, sk.SOURCE_A, sk.TARGET_B, 40)
|
| 123 |
-
assert errors[:5] == pytest.approx([0.680, 0.382, 0.155, 0.047, 0.012], abs=5e-4)
|
| 124 |
-
assert sk.sweeps_to_tolerance(K, sk.SOURCE_A, sk.TARGET_B) == 17
|
| 125 |
-
plan = sk.scaling_form(u, K, v)
|
| 126 |
-
assert plan.flatten().tolist() == pytest.approx(
|
| 127 |
-
[0.293122, 0.306878, 0.006878, 0.393122], abs=5e-7)
|
| 128 |
-
assert float((sk.COST * plan).sum()) == pytest.approx(3.45502, abs=5e-6)
|
| 129 |
-
assert sk.entropy(plan) == pytest.approx(1.124, abs=5e-4)
|
| 130 |
-
independent = sk.independent_coupling(sk.SOURCE_A, sk.TARGET_B)
|
| 131 |
-
assert sk.entropic_objective(plan, sk.COST, 2.0) == pytest.approx(1.208, abs=5e-4)
|
| 132 |
-
assert sk.entropic_objective(sk.EXACT_PLAN, sk.COST, 2.0) == pytest.approx(1.222, abs=5e-4)
|
| 133 |
-
assert sk.entropic_objective(independent, sk.COST, 2.0) == pytest.approx(1.792, abs=5e-4)
|
| 134 |
-
assert sk.relative_entropy(plan, independent) == pytest.approx(0.160, abs=5e-4)
|
| 135 |
-
f = sk.multipliers_from_scalings(u, 2.0)
|
| 136 |
-
g = sk.multipliers_from_scalings(v, 2.0)
|
| 137 |
-
assert f.tolist() == pytest.approx([3.766, -3.739], abs=5e-4)
|
| 138 |
-
assert g.tolist() == pytest.approx([-3.220, 4.872], abs=5e-4)
|
| 139 |
-
expected = {10.0: (4.0540165, 7), 2.0: (3.4550204, 17), 0.5: (3.4000004, 28),
|
| 140 |
-
0.1: (3.4000000, 80)}
|
| 141 |
-
for epsilon, (cost, sweeps) in expected.items():
|
| 142 |
-
kernel = sk.gibbs_kernel(sk.COST, epsilon)
|
| 143 |
-
su, sv, _ = sk.sinkhorn_sweeps(kernel, sk.SOURCE_A, sk.TARGET_B, 400)
|
| 144 |
-
assert float((sk.COST * sk.scaling_form(su, kernel, sv)).sum()) == pytest.approx(
|
| 145 |
-
cost, abs=5e-8)
|
| 146 |
-
assert sk.sweeps_to_tolerance(kernel, sk.SOURCE_A, sk.TARGET_B) == sweeps
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
# %% Sections 7.7 to 7.7.5, displacement.py
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
def test_one_particle_and_the_averaged_bound():
|
| 153 |
-
dp = load("displacement")
|
| 154 |
-
times = torch.linspace(0.0, 1.0, 1025, dtype=torch.float64)
|
| 155 |
-
assert dp.action_of_a_path(dp.steady_particle(times), times) == pytest.approx(4.0)
|
| 156 |
-
assert dp.action_of_a_path(dp.dawdling_particle(times), times) == pytest.approx(8.0)
|
| 157 |
-
x, y = tensor(0.0, 2.0), tensor(1.0, 3.0)
|
| 158 |
-
ordered = torch.tensor([[0.5, 0.0], [0.0, 0.5]], dtype=torch.float64)
|
| 159 |
-
crossed = torch.tensor([[0.0, 0.5], [0.5, 0.0]], dtype=torch.float64)
|
| 160 |
-
independent = torch.full((2, 2), 0.25, dtype=torch.float64)
|
| 161 |
-
assert dp.average_squared_displacement(x, y, ordered) == pytest.approx(1.0)
|
| 162 |
-
assert dp.average_squared_displacement(x, y, independent) == pytest.approx(3.0)
|
| 163 |
-
assert dp.average_squared_displacement(x, y, crossed) == pytest.approx(5.0)
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def test_displacement_interpolation_and_its_velocity():
|
| 167 |
-
dp = load("displacement")
|
| 168 |
-
mean, std = dp.displacement_law(0.5)
|
| 169 |
-
assert (mean, std) == pytest.approx((1.5, 1.5))
|
| 170 |
-
assert dp.gaussian_w2(0.0, 1.0, mean, std) == pytest.approx(math.sqrt(2.5))
|
| 171 |
-
assert math.sqrt(2.5) == pytest.approx(1.581, abs=5e-4)
|
| 172 |
-
assert dp.gaussian_w2(0.0, 1.0, 3.0, 2.0) == pytest.approx(3.162, abs=5e-4)
|
| 173 |
-
grid = torch.linspace(-6.0, 9.0, 15001, dtype=torch.float64)
|
| 174 |
-
peak = float(grid[int(dp.mixture_density(grid).argmax())])
|
| 175 |
-
assert peak == pytest.approx(0.13, abs=5e-3)
|
| 176 |
-
half = torch.tensor(0.5, dtype=torch.float64)
|
| 177 |
-
assert float(dp.velocity_field(torch.tensor(1.5, dtype=torch.float64), half)) == pytest.approx(3.0)
|
| 178 |
-
assert float(dp.velocity_field(torch.tensor(3.0, dtype=torch.float64), half)) == pytest.approx(4.0)
|
| 179 |
-
for t in (0.0, 0.25, 0.5, 0.75, 1.0):
|
| 180 |
-
assert dp.action_density(t, 4096) == pytest.approx(10.0, abs=1e-3)
|
| 181 |
-
assert abs(dp.continuity_residual(1.5, 0.5)) < 1e-12
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
# %% Sections 7.8 and 7.8.1, ot_flow_matching.py
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def test_batch_coupling_costs():
|
| 188 |
-
fm = load("ot_flow_matching")
|
| 189 |
-
x = torch.tensor([[0.0], [2.0]])
|
| 190 |
-
y = torch.tensor([[1.0], [3.0]])
|
| 191 |
-
C = fm.batch_cost_matrix(x, y)
|
| 192 |
-
assert C.flatten().tolist() == pytest.approx([1.0, 9.0, 1.0, 1.0])
|
| 193 |
-
assert float(C.mean()) == pytest.approx(3.0)
|
| 194 |
-
pairing = fm.optimal_pairing(x, y)
|
| 195 |
-
assert pairing.tolist() == [0, 1]
|
| 196 |
-
assert float(C[torch.arange(2), pairing].mean()) == pytest.approx(1.0)
|
| 197 |
-
assert fm.vertex_count(256, 256) == {"unknowns": 65536, "constraints": 512,
|
| 198 |
-
"loaded_routes": 511}
|
| 199 |
-
assert fm.conditional_velocity_energy(x, fm.couple(x, y, "optimal")) == pytest.approx(1.0)
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
def test_flow_matching_pipeline_runs():
|
| 203 |
-
fm = load("ot_flow_matching")
|
| 204 |
-
from cis6270.nets import VelocityMLP
|
| 205 |
-
|
| 206 |
-
torch.manual_seed(0)
|
| 207 |
-
model = VelocityMLP(dim=2, width=16)
|
| 208 |
-
x0 = torch.randn(8, 2)
|
| 209 |
-
x1 = torch.randn(8, 2)
|
| 210 |
-
assert float(fm.flow_matching_loss(model, x0, fm.couple(x0, x1, "optimal"))) > 0.0
|
| 211 |
-
samples, straightness = fm.integrate(model, x0, 5)
|
| 212 |
-
assert samples.shape == (8, 2) and straightness >= 0.0
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
# %% Sections 7.12 to 7.15.3, static_bridge.py
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
def test_eight_paths_and_the_chain_rule():
|
| 219 |
-
sb = load("static_bridge")
|
| 220 |
-
law = sb.path_law(sb.PI_0, [sb.A, sb.A])
|
| 221 |
-
assert law[(0, 0, 1)] == pytest.approx(0.105)
|
| 222 |
-
assert law[(0, 0, 0)] == pytest.approx(0.245)
|
| 223 |
-
assert law[(1, 1, 1)] == pytest.approx(0.180)
|
| 224 |
-
assert sum(law.values()) == pytest.approx(1.0)
|
| 225 |
-
table = torch.tensor([[0.5, 0.1], [0.1, 0.3]], dtype=torch.float64)
|
| 226 |
-
uniform = torch.full((2, 2), 0.25, dtype=torch.float64)
|
| 227 |
-
total, marginal, conditional = sb.chain_rule_terms(table, uniform)
|
| 228 |
-
assert total == pytest.approx(0.2180, abs=5e-5)
|
| 229 |
-
assert marginal == pytest.approx(0.0201, abs=5e-5)
|
| 230 |
-
assert conditional == pytest.approx(0.1979, abs=5e-5)
|
| 231 |
-
assert marginal + conditional == pytest.approx(total)
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
def test_proportional_fitting_on_the_chain():
|
| 235 |
-
sb = load("static_bridge")
|
| 236 |
-
R01 = sb.reference_endpoint_law(sb.PI_0, sb.A, 2)
|
| 237 |
-
assert R01.flatten().tolist() == pytest.approx([0.305, 0.195, 0.26, 0.24])
|
| 238 |
-
assert float((R01.sum(dim=0) - sb.PI_1).abs().sum()) == pytest.approx(0.73)
|
| 239 |
-
f, g, errors = sb.proportional_fitting(R01, sb.PI_0, sb.PI_1, 4)
|
| 240 |
-
assert errors == pytest.approx([3.933e-3, 2.098e-5, 1.119e-7, 5.966e-10], rel=1e-3)
|
| 241 |
-
assert f.tolist() == pytest.approx([1.072002, 0.937019], abs=5e-7)
|
| 242 |
-
assert g.tolist() == pytest.approx([0.350517, 1.843637], abs=5e-7)
|
| 243 |
-
plan = f[:, None] * R01 * g[None, :]
|
| 244 |
-
assert plan.flatten().tolist() == pytest.approx(
|
| 245 |
-
[0.114605, 0.385395, 0.085395, 0.414605], abs=5e-7)
|
| 246 |
-
assert sb.kl_vectors(plan.flatten(), R01.flatten()) == pytest.approx(0.2820, abs=5e-5)
|
| 247 |
-
independent = sb.PI_0[:, None] * sb.PI_1[None, :]
|
| 248 |
-
assert sb.kl_vectors(independent.flatten(), R01.flatten()) == pytest.approx(0.2847, abs=5e-5)
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
def test_h_transform_and_the_bridge_path_law():
|
| 252 |
-
sb = load("static_bridge")
|
| 253 |
-
R01 = sb.reference_endpoint_law(sb.PI_0, sb.A, 2)
|
| 254 |
-
f, g, _ = sb.proportional_fitting(R01, sb.PI_0, sb.PI_1, 40)
|
| 255 |
-
potentials = sb.backward_potential(sb.A, g, 2)
|
| 256 |
-
assert potentials[2].tolist() == pytest.approx([0.350517, 1.843637], abs=5e-7)
|
| 257 |
-
assert potentials[1].tolist() == pytest.approx([0.798453, 1.246389], abs=5e-7)
|
| 258 |
-
assert potentials[0].tolist() == pytest.approx([0.932834, 1.067215], abs=5e-7)
|
| 259 |
-
tilted = [sb.tilted_matrix(sb.A, potentials[k], potentials[k + 1]) for k in range(2)]
|
| 260 |
-
assert tilted[0].flatten().tolist() == pytest.approx(
|
| 261 |
-
[0.599160, 0.400840, 0.299266, 0.700734], abs=5e-7)
|
| 262 |
-
assert tilted[1].flatten().tolist() == pytest.approx(
|
| 263 |
-
[0.307297, 0.692703, 0.112490, 0.887510], abs=5e-7)
|
| 264 |
-
assert tilted[0].sum(dim=1).tolist() == pytest.approx([1.0, 1.0])
|
| 265 |
-
middle = sb.PI_0 @ tilted[0]
|
| 266 |
-
assert middle.tolist() == pytest.approx([0.449213, 0.550787], abs=5e-7)
|
| 267 |
-
assert (middle @ tilted[1]).tolist() == pytest.approx([0.2, 0.8])
|
| 268 |
-
assert (1.0 / potentials[0]).tolist() == pytest.approx(f.tolist(), abs=1e-9)
|
| 269 |
-
plan = f[:, None] * R01 * g[None, :]
|
| 270 |
-
bridge = sb.fill_with_reference_bridges(plan, sb.A)
|
| 271 |
-
reference = sb.path_law(sb.PI_0, [sb.A, sb.A])
|
| 272 |
-
assert bridge[(0, 0, 0)] == pytest.approx(0.092060, abs=5e-7)
|
| 273 |
-
assert bridge[(0, 0, 1)] == pytest.approx(0.207520, abs=5e-7)
|
| 274 |
-
assert bridge[(1, 1, 1)] == pytest.approx(0.310954, abs=5e-7)
|
| 275 |
-
ratios = {round(bridge[p] / reference[p], 6) for p in reference}
|
| 276 |
-
assert ratios == {0.375755, 1.976383, 0.328441, 1.727522}
|
| 277 |
-
assert sb.relative_entropy(bridge, reference) == pytest.approx(0.281961, abs=5e-7)
|
| 278 |
-
assert sum(v for k, v in bridge.items() if k[1] == 0) == pytest.approx(0.449213, abs=5e-7)
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
def test_brownian_reference_recovers_the_entropic_plan():
|
| 282 |
-
sb = load("static_bridge")
|
| 283 |
-
plan = sb.brownian_reference_plan(1.0)
|
| 284 |
-
assert plan.flatten().tolist() == pytest.approx([0.2931, 0.3069, 0.0069, 0.3931], abs=5e-5)
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
# %% Sections 7.16 to 7.18, dynamic_bridge.py
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
def test_constant_control_two_ways():
|
| 291 |
-
db = load("dynamic_bridge")
|
| 292 |
-
assert db.control_energy_entropy(2.0, 1.0) == pytest.approx(2.0)
|
| 293 |
-
assert db.gaussian_entropy_gap(2.0, 1.0) == pytest.approx(2.0)
|
| 294 |
-
generator = torch.Generator().manual_seed(0)
|
| 295 |
-
_, log_ratio = db.simulate_controlled(2.0, 200, 8192, 1.0, generator)
|
| 296 |
-
assert float(log_ratio.mean()) == pytest.approx(2.0, abs=0.1)
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
def test_the_bridge_potential_solves_both_equations():
|
| 300 |
-
db = load("dynamic_bridge")
|
| 301 |
-
value = db.hopf_cole(db.bridge_potential)
|
| 302 |
-
for x, t in ((-0.5, 0.2), (0.5, 0.5), (2.5, 0.8)):
|
| 303 |
-
assert abs(db.backward_residual(db.bridge_potential, x, t)) < 1e-12
|
| 304 |
-
assert abs(db.hjb_residual(value, x, t)) < 1e-12
|
| 305 |
-
drift = float(db.bridge_drift(torch.tensor(x, dtype=torch.float64),
|
| 306 |
-
torch.tensor(t, dtype=torch.float64)))
|
| 307 |
-
assert db.optimal_control(value, x, t) == pytest.approx(drift, abs=1e-10)
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
def test_brownian_bridge_numbers():
|
| 311 |
-
db = load("dynamic_bridge")
|
| 312 |
-
mean, variance = db.bridge_law(0.25)
|
| 313 |
-
assert mean == pytest.approx(0.5)
|
| 314 |
-
assert variance == pytest.approx(0.1875)
|
| 315 |
-
assert math.sqrt(variance) == pytest.approx(0.4330, abs=5e-5)
|
| 316 |
-
drift = float(db.bridge_drift(torch.tensor(0.6732, dtype=torch.float64),
|
| 317 |
-
torch.tensor(0.25, dtype=torch.float64)))
|
| 318 |
-
assert drift == pytest.approx(1.7691, abs=5e-5)
|
| 319 |
-
assert db.bridge_law(0.5)[1] == pytest.approx(0.25)
|
| 320 |
-
for epsilon, spread in ((0.25, 0.25), (1.0, 0.5), (4.0, 1.0)):
|
| 321 |
-
assert math.sqrt(db.bridge_law(0.5, epsilon=epsilon)[1]) == pytest.approx(spread)
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
# %% Section 7.19.1, dsb.py
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
def test_dsb_half_steps_on_the_chain():
|
| 328 |
-
dsb = load("dsb")
|
| 329 |
-
reference = dsb.reference_path_law(dsb.PI_0, dsb.A, 2)
|
| 330 |
-
assert dsb.marginal(reference, 1).tolist() == pytest.approx([0.55, 0.45])
|
| 331 |
-
assert dsb.marginal(reference, 2).tolist() == pytest.approx([0.565, 0.435])
|
| 332 |
-
B2 = dsb.reverse_step(reference, 1)
|
| 333 |
-
assert B2.flatten().tolist() == pytest.approx(
|
| 334 |
-
[0.681416, 0.318584, 0.379310, 0.620690], abs=5e-7)
|
| 335 |
-
assert float(dsb.reverse_step(reference, 0)[0, 0]) == pytest.approx(0.636364, abs=5e-7)
|
| 336 |
-
half = dsb.fix_terminal(reference, dsb.PI_1)
|
| 337 |
-
assert dsb.marginal(half, 2).tolist() == pytest.approx([0.2, 0.8])
|
| 338 |
-
assert dsb.marginal(half, 1).tolist() == pytest.approx([0.439731, 0.560269], abs=5e-7)
|
| 339 |
-
assert dsb.marginal(half, 0).tolist() == pytest.approx([0.466585, 0.533415], abs=5e-7)
|
| 340 |
-
assert float((dsb.marginal(half, 0) - dsb.PI_0).abs().sum()) == pytest.approx(0.0668, abs=5e-5)
|
| 341 |
-
full = dsb.fix_initial(half, dsb.PI_0)
|
| 342 |
-
assert dsb.one_step_conditional(full, 0).flatten().tolist() == pytest.approx(
|
| 343 |
-
[0.599738, 0.400262, 0.299771, 0.700229], abs=5e-7)
|
| 344 |
-
assert dsb.one_step_conditional(full, 1).flatten().tolist() == pytest.approx(
|
| 345 |
-
[0.309924, 0.690076, 0.113725, 0.886275], abs=5e-7)
|
| 346 |
-
assert dsb.marginal(full, 2).tolist() == pytest.approx([0.201967, 0.798033], abs=5e-7)
|
| 347 |
-
gap = max(float((dsb.one_step_conditional(full, 0) - dsb.TILTED_0).abs().max()),
|
| 348 |
-
float((dsb.one_step_conditional(full, 1) - dsb.TILTED_1).abs().max()))
|
| 349 |
-
assert gap < 0.003
|
| 350 |
-
_, errors, _ = dsb.proportional_fitting(reference, dsb.PI_0, dsb.PI_1, 4)
|
| 351 |
-
assert errors == pytest.approx([3.933e-3, 2.098e-5, 1.119e-7, 5.966e-10], rel=1e-3)
|
| 352 |
-
assert errors[0] / errors[1] == pytest.approx(187.5, abs=0.1)
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
# %% Section 7.19.2, dsbm.py
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
def test_markov_drift_at_one_point():
|
| 359 |
-
dm = load("dsbm")
|
| 360 |
-
assert dm.bridge_drift(3.0, 1.0, 0.5) == pytest.approx(4.0)
|
| 361 |
-
assert dm.bridge_drift(0.0, 1.0, 0.5) == pytest.approx(-2.0)
|
| 362 |
-
endpoints = tensor(3.0, 0.0)
|
| 363 |
-
weights = tensor(0.5, 0.5)
|
| 364 |
-
assert dm.markov_drift(endpoints, weights, 1.0, 0.5) == pytest.approx(1.0)
|
| 365 |
-
assert dm.least_squares_derivative(1.0, endpoints, weights, 1.0, 0.5) == pytest.approx(0.0)
|
| 366 |
-
assert dm.least_squares_derivative(0.0, endpoints, weights, 1.0, 0.5) == pytest.approx(-2.0)
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
def test_one_imf_round_on_the_chain():
|
| 370 |
-
dm = load("dsbm")
|
| 371 |
-
bridges = dm.reference_bridge_table(dm.A)
|
| 372 |
-
assert float(bridges[0, 0, 0]) == pytest.approx(0.803279, abs=5e-7)
|
| 373 |
-
assert float(bridges[0, 0, 1]) == pytest.approx(0.538462, abs=5e-7)
|
| 374 |
-
target = dm.bridge_coupling(dm.A, dm.PI_0, dm.PI_1)
|
| 375 |
-
independent = dm.PI_0[:, None] * dm.PI_1[None, :]
|
| 376 |
-
assert independent.flatten().tolist() == pytest.approx([0.1, 0.4, 0.1, 0.4])
|
| 377 |
-
assert float((independent - target).abs().sum()) == pytest.approx(0.0584, abs=5e-5)
|
| 378 |
-
coupling, gaps, first = dm.imf_rounds(independent, dm.A, 9, target)
|
| 379 |
-
assert first[0].flatten().tolist() == pytest.approx(
|
| 380 |
-
[0.591425, 0.408575, 0.307692, 0.692308], abs=5e-7)
|
| 381 |
-
assert first[1].flatten().tolist() == pytest.approx(
|
| 382 |
-
[0.298457, 0.701543, 0.119588, 0.880412], abs=5e-7)
|
| 383 |
-
projected = torch.diag(dm.PI_0) @ first[0] @ first[1]
|
| 384 |
-
assert projected.flatten().tolist() == pytest.approx(
|
| 385 |
-
[0.112688, 0.387312, 0.087312, 0.412688], abs=5e-7)
|
| 386 |
-
assert projected.sum(dim=0).tolist() == pytest.approx([0.2, 0.8])
|
| 387 |
-
assert projected.sum(dim=1).tolist() == pytest.approx([0.5, 0.5])
|
| 388 |
-
assert gaps[0] == pytest.approx(0.0077, abs=5e-5)
|
| 389 |
-
assert gaps[3] / gaps[4] == pytest.approx(7.37, abs=5e-3)
|
| 390 |
-
assert gaps[8] < 1e-9
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
# %% Section 7.19.3, sf2m.py
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
def test_the_two_regression_targets():
|
| 397 |
-
sm = load("sf2m")
|
| 398 |
-
x0, x1 = tensor(0.0)[0], tensor(2.0)[0]
|
| 399 |
-
z = tensor(0.4)[0]
|
| 400 |
-
for t_value, mean, sample, drift, flow, coefficient in (
|
| 401 |
-
(0.25, 0.5, 0.6732, 1.7691, 2.2309, 4 / 3),
|
| 402 |
-
(0.75, 1.5, 1.6732, 1.3072, 1.7691, -4 / 3),
|
| 403 |
-
):
|
| 404 |
-
t = torch.tensor(t_value, dtype=torch.float64)
|
| 405 |
-
assert float(sm.bridge_mean(x0, x1, t)) == pytest.approx(mean)
|
| 406 |
-
assert float(sm.bridge_std(t)) == pytest.approx(0.4330, abs=5e-5)
|
| 407 |
-
x = sm.bridge_sample(x0, x1, t, z)
|
| 408 |
-
assert float(x) == pytest.approx(sample, abs=5e-5)
|
| 409 |
-
assert float(sm.conditional_drift(x1, x, t)) == pytest.approx(drift, abs=5e-5)
|
| 410 |
-
assert float(sm.conditional_score(x0, x1, x, t)) == pytest.approx(-0.9238, abs=5e-5)
|
| 411 |
-
assert float(sm.flow_velocity(x0, x1, x, t)) == pytest.approx(flow, abs=5e-5)
|
| 412 |
-
assert float(sm.flow_velocity_collected(x0, x1, x, t)) == pytest.approx(flow, abs=5e-5)
|
| 413 |
-
assert float(sm.flow_coefficient(t)) == pytest.approx(coefficient)
|
| 414 |
-
assert float(sm.scaled_score(x0, x1, x, t)) == pytest.approx(-0.4, abs=5e-7)
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
def test_the_batch_plan_of_sf2m():
|
| 418 |
-
sm = load("sf2m")
|
| 419 |
-
x, y = tensor(0.0, 2.0), tensor(1.0, 3.0)
|
| 420 |
-
uniform = tensor(0.5, 0.5)
|
| 421 |
-
plan = sm.entropic_plan(x, y, uniform, uniform, 1.0)
|
| 422 |
-
assert plan.sum(dim=0).tolist() == pytest.approx([0.5, 0.5])
|
| 423 |
-
assert float(plan[0, 0] + plan[1, 1]) > float(plan[0, 1] + plan[1, 0])
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
# %% Sections 7.21 to 7.23.3, discrete_bridge.py
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
def test_rate_error_is_not_symmetric():
|
| 430 |
-
db = load("discrete_bridge")
|
| 431 |
-
assert db.scaling_cost(0.5) == pytest.approx(0.1534, abs=5e-5)
|
| 432 |
-
assert db.scaling_cost(2.0) == pytest.approx(0.3863, abs=5e-5)
|
| 433 |
-
assert float(db.rate_error(tensor(1.0)[0], tensor(1.0)[0])) == pytest.approx(0.0)
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
def test_two_state_continuous_time_bridge():
|
| 437 |
-
db = load("discrete_bridge")
|
| 438 |
-
assert torch.matrix_exp(db.Q).flatten().tolist() == pytest.approx(
|
| 439 |
-
[0.6328, 0.3672, 0.5507, 0.4493], abs=5e-5)
|
| 440 |
-
R01 = db.endpoint_law(db.Q, db.PI_0)
|
| 441 |
-
assert R01.flatten().tolist() == pytest.approx([0.3164, 0.1836, 0.2754, 0.2246], abs=5e-5)
|
| 442 |
-
assert R01.sum(dim=0).tolist() == pytest.approx([0.5918, 0.4082], abs=5e-5)
|
| 443 |
-
f, g, plan = db.sinkhorn(R01, db.PI_0, db.PI_1)
|
| 444 |
-
assert f.tolist() == pytest.approx([1.0716, 0.9373], abs=5e-5)
|
| 445 |
-
assert g.tolist() == pytest.approx([0.3349, 1.9643], abs=5e-5)
|
| 446 |
-
assert float(g[1] / g[0]) == pytest.approx(5.865, abs=5e-4)
|
| 447 |
-
assert plan.flatten().tolist() == pytest.approx([0.1136, 0.3864, 0.0864, 0.4136], abs=5e-5)
|
| 448 |
-
assert torch.matrix_exp(db.Q / 2).flatten().tolist() == pytest.approx(
|
| 449 |
-
[0.7146, 0.2854, 0.4281, 0.5719], abs=5e-5)
|
| 450 |
-
phi = db.backward_potential(db.Q, g, 0.5)
|
| 451 |
-
assert phi.tolist() == pytest.approx([0.7999, 1.2667], abs=5e-5)
|
| 452 |
-
tilted = db.tilted_generator(db.Q, phi)
|
| 453 |
-
assert tilted.flatten().tolist() == pytest.approx(
|
| 454 |
-
[-1.5836, 1.5836, 0.9472, -0.9472], abs=5e-5)
|
| 455 |
-
assert db.backward_potential(db.Q, g, 0.0).tolist() == pytest.approx([0.9331, 1.0669], abs=5e-5)
|
| 456 |
-
at_zero = db.tilted_generator(db.Q, db.backward_potential(db.Q, g, 0.0))
|
| 457 |
-
assert float(at_zero[0, 1]) == pytest.approx(1.1433, abs=5e-5)
|
| 458 |
-
assert float(at_zero[1, 0]) == pytest.approx(1.3120, abs=5e-5)
|
| 459 |
-
at_one = db.tilted_generator(db.Q, g)
|
| 460 |
-
assert float(at_one[0, 1]) == pytest.approx(5.8652, abs=5e-5)
|
| 461 |
-
assert float(at_one[1, 0]) == pytest.approx(0.2557, abs=5e-5)
|
| 462 |
-
marginal = db.bridge_marginal(db.Q, db.PI_0, f, g, 0.5)
|
| 463 |
-
assert marginal.tolist() == pytest.approx([0.4668, 0.5332], abs=5e-5)
|
| 464 |
-
assert db.bridge_marginal(db.Q, db.PI_0, f, g, 1.0).tolist() == pytest.approx([0.2, 0.8])
|
| 465 |
-
reverse = db.reverse_rates(tilted, marginal)
|
| 466 |
-
assert float(reverse[1, 0]) == pytest.approx(1.3862, abs=5e-5)
|
| 467 |
-
assert float(reverse[0, 1]) == pytest.approx(1.0821, abs=5e-5)
|
| 468 |
-
assert float(marginal[0] * tilted[0, 1]) == pytest.approx(
|
| 469 |
-
float(marginal[1] * reverse[1, 0]), abs=5e-5)
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
def test_euler_sampler_and_the_cost_computed_twice():
|
| 473 |
-
db = load("discrete_bridge")
|
| 474 |
-
R01 = db.endpoint_law(db.Q, db.PI_0)
|
| 475 |
-
f, g, plan = db.sinkhorn(R01, db.PI_0, db.PI_1)
|
| 476 |
-
errors = {}
|
| 477 |
-
for steps, expected in ((10, 0.0223), (20, 0.0106), (40, 0.0051), (100, 0.0021)):
|
| 478 |
-
terminal, leaving = db.euler_marginal(db.Q, db.PI_0, f, g, steps)
|
| 479 |
-
errors[steps] = float((terminal - db.PI_1).abs().sum() / 2)
|
| 480 |
-
assert errors[steps] == pytest.approx(expected, abs=1e-4)
|
| 481 |
-
if steps == 10:
|
| 482 |
-
assert leaving[:3] == pytest.approx([0.1143, 0.1187, 0.1245], abs=5e-5)
|
| 483 |
-
assert leaving[-1] == pytest.approx(0.3649, abs=5e-5)
|
| 484 |
-
assert db.relative_entropy(plan, R01) == pytest.approx(0.3235, abs=5e-5)
|
| 485 |
-
integral, at_half = db.path_cost(db.Q, db.PI_0, f, g)
|
| 486 |
-
assert at_half == pytest.approx(0.1300, abs=5e-5)
|
| 487 |
-
assert integral == pytest.approx(0.3235, abs=5e-5)
|
| 488 |
-
expected = {0.5: 0.3148, 1.0: 0.3235, 2.0: 0.3337, 4.0: 0.3348}
|
| 489 |
-
for lam, value in expected.items():
|
| 490 |
-
law = db.endpoint_law(lam * db.Q, db.PI_0)
|
| 491 |
-
_, _, scaled = db.sinkhorn(law, db.PI_0, db.PI_1)
|
| 492 |
-
assert db.relative_entropy(scaled, law) == pytest.approx(value, abs=5e-5)
|
| 493 |
-
assert db.relative_entropy(db.PI_1, db.STATIONARY) == pytest.approx(0.3348, abs=5e-5)
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
# %% Section 7.24, ddsbm.py
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
def test_ddsbm_single_bond():
|
| 500 |
-
dd = load("ddsbm")
|
| 501 |
-
kernel = dd.replacement_kernel(0.4, dd.PRIOR)
|
| 502 |
-
assert kernel.flatten().tolist() == pytest.approx(
|
| 503 |
-
[0.6, 0.2, 0.2, 0.2, 0.6, 0.2, 0.2, 0.2, 0.6])
|
| 504 |
-
reference = torch.diag(dd.BOND_0) @ kernel
|
| 505 |
-
assert reference.sum(dim=0).tolist() == pytest.approx([0.48, 0.28, 0.24])
|
| 506 |
-
assert (dd.BOND_1 / reference.sum(dim=0)).tolist() == pytest.approx(
|
| 507 |
-
[0.2083, 0.7143, 2.9167], abs=5e-5)
|
| 508 |
-
f, g, plan, errors = dd.sinkhorn(reference, dd.BOND_0, dd.BOND_1, 200)
|
| 509 |
-
assert f.tolist() == pytest.approx([1.1779, 0.9506, 0.5062], abs=5e-5)
|
| 510 |
-
assert g.tolist() == pytest.approx([0.1842, 0.6918, 3.0004], abs=5e-5)
|
| 511 |
-
assert errors[-1] < 1e-12
|
| 512 |
-
assert float(plan[0, 2]) == pytest.approx(0.495, abs=5e-4)
|
| 513 |
-
assert dd.held_mass(plan) == pytest.approx(0.261, abs=5e-4)
|
| 514 |
-
assert float((dd.BOND_0 * dd.BOND_1).sum()) == pytest.approx(0.18)
|
| 515 |
-
assert float(torch.minimum(dd.BOND_0, dd.BOND_1).sum()) == pytest.approx(0.4)
|
| 516 |
-
expected = {0.2: 0.2250, 0.4: 0.2612, 0.6: 0.2926, 0.8: 0.3248, 0.99: 0.3812}
|
| 517 |
-
for r, value in expected.items():
|
| 518 |
-
held = dd.sinkhorn(torch.diag(dd.BOND_0) @ dd.replacement_kernel(r, dd.PRIOR),
|
| 519 |
-
dd.BOND_0, dd.BOND_1, 200)[2]
|
| 520 |
-
assert dd.held_mass(held) == pytest.approx(value, abs=5e-5)
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
def test_ddsbm_pinned_rates_and_the_posterior_average():
|
| 524 |
-
dd = load("ddsbm")
|
| 525 |
-
beta = dd.resampling_rate(0.4)
|
| 526 |
-
assert beta == pytest.approx(0.9163, abs=5e-5)
|
| 527 |
-
rates = dd.replacement_generator(beta, dd.PRIOR)
|
| 528 |
-
assert float(rates[0, 1]) == pytest.approx(0.3054, abs=5e-5)
|
| 529 |
-
assert rates.sum(dim=1).abs().max() < 1e-12
|
| 530 |
-
assert dd.half_survival_time(0.4) == pytest.approx(math.log(0.8) / math.log(0.4))
|
| 531 |
-
assert dd.half_survival_time(0.4) == pytest.approx(0.2435, abs=5e-5)
|
| 532 |
-
pinned = dd.replacement_kernel(0.5, dd.PRIOR)
|
| 533 |
-
assert float(pinned[0, 0]) == pytest.approx(2 / 3, abs=5e-5)
|
| 534 |
-
assert float(pinned[0, 1]) == pytest.approx(1 / 6, abs=5e-5)
|
| 535 |
-
toward = dd.pinned_rate(rates, pinned, 0, 2, 2)
|
| 536 |
-
away = dd.pinned_rate(rates, pinned, 2, 0, 2)
|
| 537 |
-
between = dd.pinned_rate(rates, pinned, 0, 1, 2)
|
| 538 |
-
assert toward == pytest.approx(1.2217, abs=5e-5)
|
| 539 |
-
assert away == pytest.approx(0.0764, abs=5e-5)
|
| 540 |
-
assert between == pytest.approx(0.3054, abs=5e-5)
|
| 541 |
-
assert toward / away == pytest.approx(16.0, abs=5e-4)
|
| 542 |
-
assert dd.rate_error(toward, 1.0) == pytest.approx(0.0229, abs=5e-5)
|
| 543 |
-
posterior = 0.6 * toward + 0.4 * between
|
| 544 |
-
assert posterior == pytest.approx(0.8552, abs=5e-5)
|
| 545 |
-
rates_two = [toward, between]
|
| 546 |
-
weights = [0.6, 0.4]
|
| 547 |
-
assert dd.posterior_loss(rates_two, weights, posterior) == pytest.approx(0.1357, abs=5e-5)
|
| 548 |
-
assert dd.posterior_loss(rates_two, weights, 1.0) == pytest.approx(0.1467, abs=5e-5)
|
| 549 |
-
assert dd.posterior_loss(rates_two, weights, 0.5) == pytest.approx(0.2395, abs=5e-5)
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
def test_ddsbm_factorization_counts():
|
| 553 |
-
dd = load("ddsbm")
|
| 554 |
-
assert dd.feature_count(9) == (9, 36, 45)
|
| 555 |
-
assert dd.state_space_size(9, 5, 4) == pytest.approx(9.2e27, rel=5e-3)
|
| 556 |
-
assert dd.total_jump_rate(36, dd.resampling_rate(0.4), 4) == pytest.approx(24.74, abs=5e-3)
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
# %% Section 7.25, csbm.py
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
def test_csbm_references():
|
| 563 |
-
cs = load("csbm")
|
| 564 |
-
A = cs.uniform_reference(0.35, 2)
|
| 565 |
-
assert A.flatten().tolist() == pytest.approx([0.65, 0.35, 0.35, 0.65])
|
| 566 |
-
assert torch.linalg.matrix_power(A, 3).flatten().tolist() == pytest.approx(
|
| 567 |
-
[0.5135, 0.4865, 0.4865, 0.5135], abs=5e-5)
|
| 568 |
-
assert cs.uniform_reference(0.35, 5).sum(dim=1).tolist() == pytest.approx([1.0] * 5)
|
| 569 |
-
ordered = cs.ordered_reference(1.0, 5)
|
| 570 |
-
assert ordered[2].tolist() == pytest.approx(
|
| 571 |
-
[0.0545, 0.2442, 0.4026, 0.2442, 0.0545], abs=5e-5)
|
| 572 |
-
assert cs.transition_loss(tensor(0.3160, 0.6840), tensor(0.25, 0.75)) == pytest.approx(
|
| 573 |
-
0.0110, abs=5e-5)
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
def test_csbm_dimf_on_sixteen_paths():
|
| 577 |
-
cs = load("csbm")
|
| 578 |
-
A = cs.uniform_reference(cs.ALPHA, 2)
|
| 579 |
-
plan = cs.sinkhorn(torch.diag(cs.PI_0) @ torch.linalg.matrix_power(A, 3), cs.PI_0, cs.PI_1)
|
| 580 |
-
assert plan.flatten().tolist() == pytest.approx(
|
| 581 |
-
[0.10432, 0.39568, 0.09568, 0.40432], abs=5e-6)
|
| 582 |
-
assert float(cs.bridge_table(A, 3)[0, 0, 0, 0]) == pytest.approx(0.5348, abs=5e-5)
|
| 583 |
-
gap, start, target = cs.starting_gap(A, 3, cs.PI_0, cs.PI_1)
|
| 584 |
-
assert float(target[0, 0, 0, 0]) == pytest.approx(0.0558, abs=5e-5)
|
| 585 |
-
assert float(target[1, 1, 1, 1]) == pytest.approx(0.2162, abs=5e-5)
|
| 586 |
-
assert float(start[0, 0, 0, 0]) == pytest.approx(0.0535, abs=5e-5)
|
| 587 |
-
assert float(start[1, 1, 1, 1]) == pytest.approx(0.2139, abs=5e-5)
|
| 588 |
-
assert gap == pytest.approx(0.0173, abs=5e-5)
|
| 589 |
-
for axis, value in ((1, [0.4754, 0.5246]), (2, [0.4107, 0.5893])):
|
| 590 |
-
marginal = start.sum(dim=[d for d in range(4) if d != axis])
|
| 591 |
-
assert marginal.tolist() == pytest.approx(value, abs=5e-5)
|
| 592 |
-
gaps, conditionals = cs.d_imf(cs.PI_0[:, None] * cs.PI_1[None, :], A, 3, 5, target)
|
| 593 |
-
assert conditionals[0].flatten().tolist() == pytest.approx(
|
| 594 |
-
[0.6243, 0.3757, 0.3265, 0.6735], abs=5e-5)
|
| 595 |
-
assert conditionals[1].flatten().tolist() == pytest.approx(
|
| 596 |
-
[0.5626, 0.4374, 0.2729, 0.7271], abs=5e-5)
|
| 597 |
-
assert conditionals[2].flatten().tolist() == pytest.approx(
|
| 598 |
-
[0.3160, 0.6840, 0.1192, 0.8808], abs=5e-5)
|
| 599 |
-
assert gaps == pytest.approx([2.9e-4, 5.0e-6, 8.4e-8, 1.4e-9, 2.4e-11], rel=0.05)
|
| 600 |
-
assert gaps[0] / gaps[1] == pytest.approx(59.0, abs=1.0)
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
def test_csbm_mixing_and_length():
|
| 604 |
-
cs = load("csbm")
|
| 605 |
-
independent = cs.PI_0[:, None] * cs.PI_1[None, :]
|
| 606 |
-
expected = {0.2: (0.13589, 5.0), 0.4: (0.00512, 284.3)}
|
| 607 |
-
for alpha, (start, ratio) in expected.items():
|
| 608 |
-
A = cs.uniform_reference(alpha, 2)
|
| 609 |
-
gap, _, target = cs.starting_gap(A, 3, cs.PI_0, cs.PI_1)
|
| 610 |
-
assert gap == pytest.approx(start, abs=5e-5)
|
| 611 |
-
later, _ = cs.d_imf(independent, A, 3, 2, target)
|
| 612 |
-
assert later[0] / later[1] == pytest.approx(ratio, rel=0.01)
|
| 613 |
-
A = cs.uniform_reference(cs.ALPHA, 2)
|
| 614 |
-
for steps, (start, ratio) in {2: (0.0574, 7.3), 5: (0.00156, 4674.0)}.items():
|
| 615 |
-
gap, _, target = cs.starting_gap(A, steps, cs.PI_0, cs.PI_1)
|
| 616 |
-
assert gap == pytest.approx(start, abs=5e-5)
|
| 617 |
-
later, _ = cs.d_imf(independent, A, steps, 2, target)
|
| 618 |
-
assert later[0] / later[1] == pytest.approx(ratio, rel=0.01)
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
# %% Sections 7.26 and 7.27, branched.py
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
def test_two_branches_worked_out():
|
| 625 |
-
br = load("branched")
|
| 626 |
-
linear = lambda t: 0.3 * t
|
| 627 |
-
late = lambda t: 0.3 * t ** 2
|
| 628 |
-
assert float(br.branch_weights(linear, torch.tensor(0.5))) == pytest.approx(0.15)
|
| 629 |
-
assert float(br.branch_weights(linear, torch.tensor(1.0))) == pytest.approx(0.3)
|
| 630 |
-
near_zero = br.weighted_energy(2.0, lambda t: 1.0 - linear(t))
|
| 631 |
-
near_one = br.weighted_energy(-2.0, linear)
|
| 632 |
-
assert near_zero == pytest.approx(1.7, abs=5e-5)
|
| 633 |
-
assert near_one == pytest.approx(0.3, abs=5e-5)
|
| 634 |
-
assert near_zero + near_one == pytest.approx(2.0, abs=5e-5)
|
| 635 |
-
far_linear = near_zero + br.weighted_energy(-4.0, linear)
|
| 636 |
-
far_late = br.weighted_energy(2.0, lambda t: 1.0 - late(t)) + br.weighted_energy(-4.0, late)
|
| 637 |
-
assert far_linear == pytest.approx(2.9, abs=5e-5)
|
| 638 |
-
assert far_late == pytest.approx(2.6, abs=5e-5)
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
def test_the_interpolant():
|
| 642 |
-
br = load("branched")
|
| 643 |
-
grid = torch.tensor([0.0, 0.5, 1.0])
|
| 644 |
-
assert br.interpolant(0.0, 2.0, 1.0, grid).tolist() == pytest.approx([0.0, 1.25, 2.0])
|
| 645 |
-
assert br.interpolant_velocity(0.0, 2.0, 1.0, grid).tolist() == pytest.approx([3.0, 2.0, 1.0])
|
| 646 |
-
assert br.interpolant_energy(0.0, 2.0, 1.0) == pytest.approx(2.1667, abs=5e-5)
|
| 647 |
-
assert br.interpolant_energy(0.0, 2.0, 0.0) == pytest.approx(2.0, abs=5e-5)
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
def test_entangled_tilt_and_parameterization():
|
| 651 |
-
br = load("branched")
|
| 652 |
-
normalizer = 0.1 * 1.0 + 0.9 * 0.01
|
| 653 |
-
assert normalizer == pytest.approx(0.109)
|
| 654 |
-
tilted = 0.1 / normalizer
|
| 655 |
-
assert tilted == pytest.approx(0.917, abs=5e-4)
|
| 656 |
-
assert tilted / 0.1 == pytest.approx(9.17, abs=5e-3)
|
| 657 |
-
reward = (1.0 - tilted) * math.log(0.01)
|
| 658 |
-
assert reward == pytest.approx(-0.380, abs=5e-4)
|
| 659 |
-
assert reward - math.log(normalizer) == pytest.approx(1.836, abs=5e-4)
|
| 660 |
-
assert -math.log(normalizer) == pytest.approx(2.216, abs=5e-4)
|
| 661 |
-
assert br.NOISE == pytest.approx(1.4142, abs=5e-5)
|
| 662 |
-
step = (-0.3 + 0.5) * 0.01 - 1.0 * 0.8 * 0.01 + br.NOISE * math.sqrt(0.01) * 0.4
|
| 663 |
-
assert step == pytest.approx(0.0506, abs=5e-5)
|
| 664 |
-
assert 0.5 / br.NOISE == pytest.approx(0.3536, abs=5e-5)
|
| 665 |
-
assert 0.5 * (0.5 / br.NOISE) ** 2 * 0.01 == pytest.approx(0.000625, abs=5e-7)
|
| 666 |
-
force = br.compose_bias(torch.tensor([[0.5]]), torch.tensor([[[0.3, -0.4]]]),
|
| 667 |
-
torch.tensor([[[1.0, 0.0]]]))[0, 0]
|
| 668 |
-
assert force.tolist() == pytest.approx([0.5, -0.4])
|
| 669 |
-
assert float(force.norm()) == pytest.approx(0.640, abs=5e-4)
|
| 670 |
-
weights = torch.softmax(torch.tensor([-1.0, 0.0, 1.0]), dim=0)
|
| 671 |
-
assert weights.tolist() == pytest.approx([0.090, 0.245, 0.665], abs=5e-4)
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
def test_entangled_training_lowers_the_loss():
|
| 675 |
-
br = load("branched")
|
| 676 |
-
torch.manual_seed(0)
|
| 677 |
-
generator = torch.Generator().manual_seed(0)
|
| 678 |
-
force = br.BiasForce(width=16)
|
| 679 |
-
rollout = br.roll_out(force, 16, 8, torch.device("cpu"), generator)
|
| 680 |
-
weights = br.importance_weights(rollout)
|
| 681 |
-
assert float(weights.sum()) == pytest.approx(1.0)
|
| 682 |
-
assert br.force_check(rollout["positions"][-1]) < 1e-5
|
| 683 |
-
loss = br.cross_entropy_loss(force, rollout, weights)
|
| 684 |
-
assert torch.isfinite(loss)
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
# %% Every file runs end to end
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
@pytest.mark.parametrize("name,flags", [
|
| 691 |
-
("optimal_transport", ["--steps", "300"]),
|
| 692 |
-
("sinkhorn", ["--steps", "20"]),
|
| 693 |
-
("displacement", ["--steps", "64"]),
|
| 694 |
-
("ot_flow_matching", ["--steps", "3", "--samples", "64", "--sample-steps", "5"]),
|
| 695 |
-
("static_bridge", ["--steps", "3"]),
|
| 696 |
-
("dynamic_bridge", ["--steps", "20"]),
|
| 697 |
-
("dsb", ["--steps", "2"]),
|
| 698 |
-
("dsbm", ["--steps", "3"]),
|
| 699 |
-
("sf2m", ["--steps", "21"]),
|
| 700 |
-
("discrete_bridge", ["--steps", "4"]),
|
| 701 |
-
("ddsbm", ["--steps", "20"]),
|
| 702 |
-
("csbm", ["--steps", "3"]),
|
| 703 |
-
("branched", ["--steps", "2", "--samples", "16"]),
|
| 704 |
-
])
|
| 705 |
-
def test_the_script_runs(name, flags, monkeypatch, capsys):
|
| 706 |
-
module = load(name)
|
| 707 |
-
monkeypatch.setattr(sys, "argv", [name] + flags + ["--quiet"])
|
| 708 |
-
module.main()
|
| 709 |
-
assert capsys.readouterr().out.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|