pranamanam commited on
Commit
ba677af
·
verified ·
1 Parent(s): a4e5137

Delete tests

Browse files
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()