File size: 24,998 Bytes
0ba9d09 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 | """Chapter 1's hand-computed numbers, asserted against the six Lecture 1 scripts.
Every row below is a value Chapter 1 works out by hand, paired with the label under which one of
the Lecture 1 scripts prints its own computation of it. The tolerance on each row is the rounding the
chapter itself prints: we check a value given to three decimals to 5e-4, one given to five decimals
to 5e-6, and a value the chapter states exactly to machine precision. A value the chapter predicts
and a script measures by sampling has a test of its own, with a band wide enough for the Monte
Carlo error at the sample size the test uses.
cd cis6270 && PYTHONPATH=. python -m pytest tests/test_lecture_1.py -q
"""
import math
import subprocess
import sys
from pathlib import Path
import pytest
from cis6270.runner import set_seed
from lecture_1 import calculus, fields, information, linear_algebra, optimization, probability
SEED = 0 # The seed every fixture sets before running a script's sections.
SAMPLES = 4000 # Monte Carlo draws and random competitors, fixed so the printed labels are fixed.
GRID = 1000 # Points in the numerical checks of calculus.py and fields.py.
STEPS = 600 # Gradient-descent steps, chain steps, Euler steps and estimator repeats.
EXACT = 1e-12 # The tolerance for a value the chapter states exactly.
ROOT = Path(__file__).resolve().parents[1] # The repository root, for the command-line check.
# %% 01. Probability, Sections 1.3 to 1.11
@pytest.fixture(scope="module")
def probability_worked() -> dict:
"""Run every worked example of probability.py and return the values it computed."""
set_seed(SEED)
probability.WORKED.clear()
probability.two_token_distribution(quiet=True)
probability.continuous_densities(quiet=True)
probability.expected_adenine(quiet=True)
probability.monte_carlo(SAMPLES, STEPS, quiet=True)
probability.worked_variance(quiet=True)
probability.peptide_covariance(quiet=True)
probability.noising_covariance(quiet=True)
probability.joint_marginal_conditional(quiet=True)
probability.bayes(quiet=True)
probability.independence(quiet=True)
return dict(probability.WORKED)
PROBABILITY_ROWS = [
# Section 1.3.1, the two-token distribution and the event that the first token is A.
("sum_x p_X(x)", 1.0, EXACT),
("P(x_1 = A) = p(AA) + p(AC)", 0.5, EXACT),
# Section 1.4, densities whose height exceeds one.
("uniform[0, 0.1] density height", 10.0, EXACT),
("P(0 <= X <= 0.02)", 0.2, EXACT),
("peak of N(0, 0.1^2)", 3.989, 5e-4),
# Sections 1.5.1 and 1.5.3, the expected adenine count and the Monte Carlo error scale.
("E[h(X)], expected A count", 1.1, EXACT),
("expected A count in 1000 draws", 1100.0, EXACT),
("E[h(X)^2]", 1.9, EXACT),
("Var(h(X))", 0.69, EXACT),
("sd of a 10-sample average", 0.263, 5e-4),
("sd of a 1000-sample average", 0.0263, 5e-5),
# Section 1.6.2, the variance of the values 0, 1, 2 by both routes.
("E[X]", 1.1, EXACT),
("E[X^2]", 1.7, EXACT),
("Var(X) from the two moments", 0.49, EXACT),
("Var(X) from the definition", 0.49, EXACT),
("sigma_X", 0.7, EXACT),
# Sections 1.7.1 and 1.7.2, the three peptides and their covariance matrix.
("mu_X", 2.0, EXACT),
("mu_Y", 4.0, EXACT),
("Cov(X, Y)", 5.0 / 3.0, EXACT),
("Var(X) = Sigma_11", 2.0 / 3.0, EXACT),
("Var(Y) = Sigma_22", 14.0 / 3.0, EXACT),
("sigma_X across the peptides", 0.8165, 5e-5),
("sigma_Y across the peptides", 2.160, 5e-4),
("rho_XY", 0.9449, 5e-5),
("Sigma symmetry, Sigma_21 - Sigma_12", 0.0, EXACT),
# Section 1.8, the noising equation at alpha_t = 0.6 and sigma_t = 0.8.
("alpha_t^2 + sigma_t^2", 1.0, EXACT),
("Cov(X_t)_11 with Cov(X_0) = I", 1.0, EXACT),
("Cov(X_t)_11 with diag(4, 1)", 2.08, EXACT),
("Cov(X_t)_22 with diag(4, 1)", 1.00, EXACT),
# Sections 1.9.1 to 1.9.3, the motif-and-binding table with its marginals and conditionals.
("sum over the four cells", 1.0, EXACT),
("p(X = 1), motif present", 0.60, EXACT),
("p(X = 0), motif absent", 0.40, EXACT),
("p(Y = 1), high binding", 0.44, EXACT),
("p(Y = 0), low binding", 0.56, EXACT),
("p(Y = 1 | X = 1)", 0.6, EXACT),
("p(Y = 0 | X = 1)", 0.4, EXACT),
("p(Y = 1 | X = 0)", 0.2, EXACT),
("p(Y = 0 | X = 0)", 0.8, EXACT),
# Section 1.10, Bayes' rule and the law of total probability.
("evidence p(Y = 1)", 0.44, EXACT),
("posterior p(X = 1 | Y = 1)", 0.818, 5e-4),
("likelihood ratio 0.6 / 0.2", 3.0, EXACT),
# Section 1.11, the failure of independence on the same table.
("p(X = 1) p(Y = 1)", 0.264, EXACT),
("p(X = 1, Y = 1)", 0.36, EXACT),
("association missed by factorizing", 0.096, EXACT),
]
@pytest.mark.parametrize("label, chapter, tolerance", PROBABILITY_ROWS)
def test_probability(probability_worked, label, chapter, tolerance):
assert probability_worked[label] == pytest.approx(chapter, abs=tolerance)
def test_monte_carlo_lands_inside_its_own_error_bar():
"""Section 1.5.3: the sample average lands within four standard deviations of E[h(X)] = 1.1."""
set_seed(SEED)
estimates = probability.monte_carlo(SAMPLES, STEPS, quiet=True)
size, value, error = estimates["ladder"][-1] # The last rung, at the largest N the ladder reached.
assert 10 * size > SAMPLES >= size
assert abs(value - estimates["exact"]) < 4.0 * error
def test_the_measured_spread_of_ten_sample_averages_matches_0_263(probability_worked):
"""Section 1.5.3: repeating the ten-sample average measures the 0.263 the chapter predicts.
The spread of a sample standard deviation over 600 rounds is a few percent, so the band is
ten percent of the predicted value, looser than the rounding of the printed digits.
"""
predicted = math.sqrt(0.69 / 10.0)
assert probability_worked[f"sd of {STEPS} ten-sample averages"] == pytest.approx(
predicted, rel=0.10)
def test_the_same_seed_gives_the_same_monte_carlo_ladder():
"""Rule 8 of the conventions: a run is reproducible from its seed alone."""
set_seed(SEED)
first = probability.monte_carlo(1000, 20, quiet=True)["ladder"]
set_seed(SEED)
second = probability.monte_carlo(1000, 20, quiet=True)["ladder"]
assert first == second
# %% 02. Linear algebra, Sections 1.12 to 1.18
@pytest.fixture(scope="module")
def linear_algebra_worked() -> dict:
"""Run every worked example of linear_algebra.py and return the values it computed."""
set_seed(SEED)
linear_algebra.WORKED.clear()
linear_algebra.simplex(quiet=True)
linear_algebra.softmax(quiet=True)
linear_algebra.one_transition(quiet=True)
spectrum = linear_algebra.eigen(quiet=True)
linear_algebra.powers(spectrum["values"], spectrum["vectors"], quiet=True)
linear_algebra.diagonalization(spectrum["values"], spectrum["vectors"], quiet=True)
linear_algebra.stationary(10, quiet=True)
linear_algebra.matrix_exponential(spectrum["values"], spectrum["vectors"], quiet=True)
return dict(linear_algebra.WORKED)
LINEAR_ALGEBRA_ROWS = [
# Sections 1.12.2 and 1.12.3, expectation as a dot product and a mixture of two points.
("E[h(X)] = p^T h", 1.3, EXACT),
("mixture r_1 at lambda = 0.5", 0.30, EXACT),
("mixture r_2 at lambda = 0.5", 0.20, EXACT),
("mixture r_3 at lambda = 0.5", 0.50, EXACT),
("sum_i r_i, still a distribution", 1.0, EXACT),
# Section 1.13.1, the softmax of z = (2, 1, 0).
("e^{z_1}", 7.389, 5e-4),
("e^{z_2}", 2.718, 5e-4),
("e^{z_3}", 1.000, EXACT),
("partition function sum_j e^{z_j}", 11.107, 5e-4),
("softmax(z)_1", 0.665, 5e-4),
("softmax(z)_2", 0.245, 5e-4),
("softmax(z)_3", 0.090, 5e-4),
("sum_i softmax(z)_i", 1.0, EXACT),
("shift invariance, largest change", 0.0, 1e-12),
# Section 1.15.1, one step of the two-state chain from an even split.
("p_{t+1}(1)", 0.55, EXACT),
("p_{t+1}(2)", 0.45, EXACT),
# Sections 1.16 and 1.16.1, the eigenvalues and eigenvectors of A = [[2, 1], [1, 2]].
("trace A, lambda_1 + lambda_2", 4.0, EXACT),
("det A, lambda_1 lambda_2", 3.0, EXACT),
("lambda_1", 3.0, 1e-12),
("lambda_2", 1.0, 1e-12),
("v_1 second entry over first", 1.0, 1e-12),
("v_2 second entry over first", -1.0, 1e-12),
("largest |A v - lambda v|", 0.0, 1e-12),
# Section 1.16.2, powers along an eigendirection from x_0 = (3, 1).
("c_1", 2.0, 1e-12),
("c_2", 1.0, 1e-12),
("lambda_1^10", 59049.0, 1e-6),
("x_2 first entry, eigen route", 19.0, 1e-10),
("x_2 second entry, eigen route", 17.0, 1e-10),
("x_10 first entry", 118099.0, 1e-6),
("x_10 second entry", 118097.0, 1e-6),
# Sections 1.16.3 and 1.16.4, diagonalization and the dominant eigenvalue.
("V^{-1}_11", 0.5, 1e-12),
("largest |V Lambda V^{-1} - A|", 0.0, 1e-12),
("A^3_11 through the eigenvalues", 14.0, 1e-10),
("A^3_12 through the eigenvalues", 13.0, 1e-10),
("dominant coefficient 2^10", 1024.0, EXACT),
("decaying coefficient 0.5^10", 0.000977, 5e-7),
("ratio of the two, in millions", 1.05, 5e-3),
# Section 1.17, the stationary distribution and the rate of approach.
("p*_1", 0.6, 1e-10),
("p*_2", 0.4, 1e-10),
("largest |P^T p* - p*|", 0.0, 1e-10),
("trace P", 1.5, EXACT),
("det P", 0.5, EXACT),
("second eigenvalue, the rate", 0.5, 1e-12),
("p_1(1)", 0.55, EXACT),
("p_2(1)", 0.575, EXACT),
("p_3(1)", 0.5875, EXACT),
("p_10(1)", 0.59990, 5e-6),
# Section 1.18.3, the four-term series against the eigenvalue formula at t = 0.1.
("four-term series, entry 11", 1.22733, 5e-6),
("four-term series, entry 12", 0.12217, 5e-6),
("eigenvalue route, entry 11", 1.22751, 5e-6),
("eigenvalue route, entry 12", 0.12234, 5e-6),
("largest gap between the routes", 1.8e-4, 5e-6),
("first discarded term, entry 11", 1.71e-4, 5e-7),
("first discarded term, entry 12", 1.67e-4, 5e-7),
("x_1 first entry, 2e^3 + e", 42.889, 5e-4),
("x_1 second entry, 2e^3 - e", 37.453, 5e-4),
# Sections 1.18.4 and 1.18.5, stability and the covariance read through its eigenvalues.
("decay e^{-2t} at t = 1", 0.1353, 5e-5),
("decay e^{-2t} at t = 3", 0.002479, 5e-7),
("sqrt(lambda_1), Gaussian long axis", 1.732, 5e-4),
]
@pytest.mark.parametrize("label, chapter, tolerance", LINEAR_ALGEBRA_ROWS)
def test_linear_algebra(linear_algebra_worked, label, chapter, tolerance):
assert linear_algebra_worked[label] == pytest.approx(chapter, abs=tolerance)
def test_the_chain_halves_its_deviation_at_every_step():
"""Section 1.17: the deviation from p* lies along the second eigenvector, so d_n = 0.5^n d_0."""
chain = linear_algebra.stationary(12, quiet=True)
for step, point in enumerate(chain["history"], start=1):
deviation = float((point - chain["p_star"]).abs().max())
assert deviation == pytest.approx(0.1 * 0.5 ** step, abs=1e-12)
# %% 03. Calculus, Sections 1.22 to 1.30
@pytest.fixture(scope="module")
def calculus_worked() -> dict:
"""Run every worked example of calculus.py and return the values it computed."""
set_seed(SEED)
calculus.WORKED.clear()
calculus.jacobian(quiet=True)
calculus.determinant(quiet=True)
calculus.change_of_variables(GRID, quiet=True)
calculus.composition(quiet=True)
calculus.branches(GRID, quiet=True)
calculus.gradient(quiet=True)
calculus.divergences(quiet=True)
calculus.continuity(GRID, STEPS, quiet=True)
return dict(calculus.WORKED)
CALCULUS_ROWS = [
# Sections 1.22.2 and 1.22.3, the Jacobian at (1, 0) and the displacement it predicts.
("J_phi(1, 0)_11", 2.0, EXACT),
("J_phi(1, 0)_21 = cos(1)", 0.5403, 5e-5),
("largest |autograd - closed form|", 0.0, EXACT),
("predicted change, first output", 0.0, 1e-12),
("predicted change, second output", -0.0546, 5e-5),
("exact change, first output", 0.0001, 5e-6),
("exact change, second output", -0.05464, 5e-6),
# Section 1.23, the determinant as the local volume factor.
("det A for diag(2, 3)", 6.0, EXACT),
("area of the image rectangle", 6.0, EXACT),
("det of the collapsing map", 0.0, EXACT),
("det J_phi at x_1 = 2", 4.0, 1e-12),
("det J_phi at x_1 = 0.25", 0.5, 1e-12),
# Sections 1.24.1 and 1.24.2, change of variables in one and in two dimensions.
("preimage of [1.4, 2.0], lower end", 0.2, EXACT),
("preimage of [1.4, 2.0], upper end", 0.5, EXACT),
("P(X in the preimage)", 0.3, EXACT),
("p_Y on [1, 3] after Y = 2X + 1", 0.5, EXACT),
("integral of p_Y over [1, 3]", 1.0, EXACT),
("p_Y inside the image of A", 1.0 / 6.0, EXACT),
("density times area for A", 1.0, EXACT),
("p_Y(y_1 = 1) for the x_1^2 map", 0.5, EXACT),
("largest |p_Y |det J| - p_X|", 0.0, 1e-12),
("total mass after the x_1^2 map", 1.0, 1e-12),
# Section 1.25, the composition and its log-determinant.
("det J_psi", 6.0, 1e-12),
("chained Jacobian, entry 11", 8.0, 1e-12),
("det of the composition", 24.0, 1e-10),
("log 4", 1.3863, 5e-5),
("log 6", 1.7918, 5e-5),
("log 4 + log 6 = log 24", 3.1781, 5e-5),
("log p_Y from a unit base density", -3.1781, 5e-5),
# Section 1.26, the two inverse branches of the squaring map.
("p_Y(0.25), two branches at ±0.5", 1.0, 1e-12),
("p_Y(0.04), two branches at ±0.2", 2.5, 1e-12),
("integral of p_Y over [0.01, 1]", 0.9, 5e-5),
# Sections 1.27.1 and 1.27.2, the gradient and the directional derivative.
("grad f(1, 2), first entry", 2.0, EXACT),
("grad f(1, 2), second entry", 8.0, EXACT),
("|grad f(1, 2)| = sqrt(68)", math.sqrt(68.0), 1e-12),
("steepest direction, first entry", 0.243, 5e-4),
("steepest direction, second entry", 0.970, 5e-4),
("D_u f along (1, 0)", 2.0, EXACT),
("D_u f along (0, 1)", 8.0, EXACT),
# Section 1.29, the divergence of the three fields.
("div of (-x_1, -2 x_2)", -3.0, EXACT),
("div of (2 x_1, -x_2)", 1.0, EXACT),
("div of (-x_2, x_1)", 0.0, EXACT),
("area factor e^{div t} at t = 0.5", 1.649, 5e-4),
# Section 1.30.2, the worked flow v(x) = x on a uniform source.
("largest continuity residual", 0.0, 1e-6),
("log p along the trajectory at t = 1", -1.0, 1e-12),
("p_t(x_t) at t = 1", 0.3679, 5e-5),
("density at t = log 2", 0.5, EXACT),
("mass at t = log 2", 1.0, EXACT),
("mass at t = 1", 1.000, 5e-4),
]
@pytest.mark.parametrize("label, chapter, tolerance", CALCULUS_ROWS)
def test_calculus(calculus_worked, label, chapter, tolerance):
assert calculus_worked[label] == pytest.approx(chapter, abs=tolerance)
def test_the_euler_walk_approaches_the_exact_trajectory(calculus_worked):
"""Section 1.30.2: x_t = x_0 e^t, so a first-order walk closes on 0.5e as the step shrinks."""
coarse = calculus.continuity(GRID, 50, quiet=True)["x_t"]
fine = calculus.continuity(GRID, 4 * 50, quiet=True)["x_t"]
assert abs(fine - 0.5 * math.e) < abs(coarse - 0.5 * math.e) / 3.0
# %% 04. Information, Sections 1.33 to 1.35
@pytest.fixture(scope="module")
def information_worked() -> dict:
"""Run every worked example of information.py and return the values it computed."""
set_seed(SEED)
information.WORKED.clear()
information.information(quiet=True)
information.entropies(quiet=True)
information.cross_entropies(quiet=True)
information.kl(SAMPLES, quiet=True)
information.monte_carlo(SAMPLES, STEPS, quiet=True)
information.maximum_likelihood(quiet=True)
return dict(information.WORKED)
INFORMATION_ROWS = [
# Section 1.33.1, the information content of a common and of a rare outcome.
("I(x) at p(x) = 0.5", 0.693, 5e-4),
("I(x) at p(x) = 0.01", 4.605, 5e-4),
("I(x) at p(x) = 1", 0.0, EXACT),
("ratio of the two surprises", 6.6, 5e-2),
# Section 1.33.2, entropy on a skewed, a balanced and a uniform distribution.
("H((0.9, 0.1))", 0.325, 5e-4),
("H((0.5, 0.5)) = log 2", 0.693, 5e-4),
("H of a point mass", 0.0, EXACT),
("H of the uniform on 4 states", 1.386, 5e-4),
# Section 1.33.3, the cross-entropy of p = (0.8, 0.2) scored by q = (0.6, 0.4).
("H(p, q)", 0.592, 5e-4),
("H(p)", 0.500, 5e-4),
("excess cost H(p, q) - H(p)", 0.092, 5e-4),
("H(p, p) - H(p)", 0.0, EXACT),
# Section 1.34, the divergence, its split, its nonnegativity and its asymmetry.
("KL(p || q)", 0.0915, 5e-5),
("KL(q || p)", 0.1046, 5e-5),
("H(p) + KL(p || q) = H(p, q)", 0.592, 5e-4),
("KL(p || p)", 0.0, EXACT),
("reverse over forward, percent", 14.0, 0.5),
("largest log t - (t - 1)", 0.0, 1e-12),
(f"largest KL breach over {SAMPLES} pairs", 0.0, EXACT),
# Section 1.35, the empirical loss on ten DNA bases and its gap to the uniform model.
("term for A, 0.4 log(1/0.4)", 0.36652, 5e-6),
("term for C, 0.3 log(1/0.3)", 0.36119, 5e-6),
("term for G, 0.2 log(1/0.2)", 0.32189, 5e-6),
("term for T, 0.1 log(1/0.1)", 0.23026, 5e-6),
("loss at the frequencies", 1.280, 5e-4),
("loss under the uniform model", 1.386, 5e-4),
("gap between the two losses", 0.1064, 5e-5),
("KL(p_hat || uniform)", 0.1064, 5e-5),
]
@pytest.mark.parametrize("label, chapter, tolerance", INFORMATION_ROWS)
def test_information(information_worked, label, chapter, tolerance):
assert information_worked[label] == pytest.approx(chapter, abs=tolerance)
def test_cross_entropy_by_sampling_lands_inside_its_error_bar():
"""Section 1.33.3 estimated by the Monte Carlo of Section 1.5.3, to within four error bars."""
set_seed(SEED)
estimate = information.monte_carlo(SAMPLES, STEPS, quiet=True)
assert abs(estimate["estimate"] - estimate["exact"]) < 4.0 * estimate["error"]
assert estimate["measured"] == pytest.approx(estimate["predicted"], rel=0.15)
# %% 05. Optimization, Sections 1.36 to 1.38
@pytest.fixture(scope="module")
def optimization_worked() -> dict:
"""Run every worked example of optimization.py and return the values it computed."""
set_seed(SEED)
optimization.WORKED.clear()
optimization.unconstrained(STEPS, quiet=True)
optimization.equality_constraint(quiet=True)
optimization.normalization(quiet=True)
optimization.maximum_entropy(SAMPLES, quiet=True)
optimization.kkt(quiet=True)
optimization.over_the_simplex(STEPS, quiet=True)
return dict(optimization.WORKED)
OPTIMIZATION_ROWS = [
# Section 1.36, three steps of gradient descent on (theta - 3)^2 at eta = 0.1.
("theta_1", 0.600, 5e-4),
("theta_2", 1.080, 5e-4),
("theta_3", 1.464, 5e-4),
(f"theta_{STEPS} from the closed form", 3.0, 1e-12),
("gradient at the minimizer theta = 3", 0.0, EXACT),
# Section 1.37.1, the squared distance to the origin along x_1 + x_2 = 1.
("x_1 at the constrained minimum", 0.5, 1e-12),
("x_2 at the constrained minimum", 0.5, 1e-12),
("lambda, the equality multiplier", -1.0, 1e-12),
("f at the minimum", 0.5, 1e-12),
("f at the endpoint (1, 0)", 1.0, EXACT),
("largest |grad f + lambda grad g|", 0.0, 1e-12),
# Section 1.37.2, normalization alone, and the likelihood minimizer it produces.
("uniform p_i at K = 4", 0.25, EXACT),
("max entropy under normalization", 1.386, 5e-4),
("p_hat(A) = n(A) / N", 0.4, EXACT),
("p_hat(T) = n(T) / N", 0.1, EXACT),
("minimum empirical loss", 1.280, 5e-4),
("loss drop from moving off p_hat", 0.0, EXACT),
# Section 1.37.3, maximum entropy under a GC-content target of 0.6.
("e^lambda", 1.5, 1e-12),
("lambda = log 1.5", 0.4055, 5e-5),
("partition function Z", 5.0, 1e-12),
("p*(A)", 0.2, 1e-12),
("p*(C)", 0.3, 1e-12),
("GC content of p*", 0.6, 1e-12),
("H(p*)", 1.366, 5e-4),
("H of the unconstrained uniform", 1.386, 5e-4),
("entropy cost of the constraint", 0.0201, 5e-5),
(f"H above H(p*) over {SAMPLES} draws", 0.0, EXACT),
# Section 1.38, the bounded quadratic and its four KKT conditions.
("x* at the bound", 1.0, EXACT),
("lambda, the inequality multiplier", 4.0, EXACT),
("g(x*) = x* - 1", 0.0, EXACT),
("complementary slackness lambda g", 0.0, EXACT),
("stationarity 2(x* - 3) + lambda", 0.0, EXACT),
("f at the unconstrained minimum", 0.0, EXACT),
("f under the bound", 4.0, EXACT),
("value drop for epsilon = 0.001", 0.004, 5e-6),
# Section 1.38.1, the same likelihood problem solved on logits through the softmax.
("sum_i p_i at the end", 1.0, 1e-12),
("entries at or below zero", 0.0, EXACT),
("p(A) after the descent", 0.4, 5e-4),
("p(T) after the descent", 0.1, 5e-4),
("loss after the descent", 1.280, 5e-4),
("largest |p - p_hat|", 0.0, 5e-4),
]
@pytest.mark.parametrize("label, chapter, tolerance", OPTIMIZATION_ROWS)
def test_optimization(optimization_worked, label, chapter, tolerance):
assert optimization_worked[label] == pytest.approx(chapter, abs=tolerance)
def test_the_softmax_descent_reaches_the_multiplier_solution():
"""Section 1.38.1: the unconstrained route on logits reaches what Section 1.37.2 derived."""
set_seed(SEED)
fitted = optimization.over_the_simplex(2000, quiet=True)
frequencies = optimization.BASE_COUNTS / optimization.BASE_COUNTS.sum()
assert fitted["distance"] < 1e-5
assert fitted["losses"][-1] == pytest.approx(
float(-(frequencies * frequencies.log()).sum()), abs=1e-9)
assert fitted["losses"][0] > fitted["losses"][-1]
# %% 06. Fields, Sections 1.31 and 1.32
@pytest.fixture(scope="module")
def fields_worked() -> dict:
"""Run every worked example of fields.py and return the values it computed."""
set_seed(SEED)
fields.WORKED.clear()
fields.scores(quiet=True)
fields.curvatures(quiet=True)
fields.heat(GRID, quiet=True)
fields.fokker_planck(GRID, quiet=True)
fields.euler_maruyama(SAMPLES, STEPS, quiet=True)
return dict(fields.WORKED)
FIELDS_ROWS = [
# Section 1.31, the score of the standard Gaussian and of N(1, 4).
("s(x) at x = 3", -3.0, EXACT),
("s(x) at x = -2", 2.0, EXACT),
("s(x) at the mode x = 0", 0.0, EXACT),
("largest |s(x) + x|", 0.0, EXACT),
("largest change from dropping Z", 0.0, EXACT),
("s(5) under N(1, 4)", -1.0, EXACT),
# Section 1.32, the two curvatures of the bowl and the cancelling pair of the saddle.
("d^2 f / d x_1^2", 2.0, EXACT),
("d^2 f / d x_2^2", 6.0, EXACT),
("Laplacian of x_1^2 + 3 x_2^2", 8.0, EXACT),
("Laplacian from the four neighbors", 8.0, 5e-6),
("Laplacian of the saddle", 0.0, EXACT),
# Section 1.32.1, the heat equation on the widening Gaussian p_t = N(0, t) at D = 1/2.
("p_1(0) = 1 / sqrt(2 pi)", 0.39894, 5e-6),
("d p_t(0) / d t at t = 1", -0.19947, 5e-6),
("Laplacian of p_1 at 0", -0.39894, 5e-6),
("-p_1(0) / sigma^2 at sigma^2 = 1", -0.39894, 5e-6),
("D times the Laplacian of p_1 at 0", -0.19947, 5e-6),
("largest heat-equation residual", 0.0, 1e-6),
# Section 1.32.2, the Fokker-Planck equation and the two equations it reduces to.
("center of p_t at t = 1", 0.5, EXACT),
("largest residual, drift and noise", 0.0, 1e-6),
("largest residual at sigma_t = 0", 0.0, 1e-6),
]
@pytest.mark.parametrize("label, chapter, tolerance", FIELDS_ROWS)
def test_fields(fields_worked, label, chapter, tolerance):
assert fields_worked[label] == pytest.approx(chapter, abs=tolerance)
def test_the_heat_equation_matches_the_time_derivative_at_the_center(fields_worked):
"""Section 1.32.1: D times the Laplacian at the origin equals d p_t(0)/dt to every digit."""
assert fields_worked["D times the Laplacian of p_1 at 0"] == pytest.approx(
fields_worked["d p_t(0) / d t at t = 1"], abs=EXACT)
def test_the_simulated_walk_tracks_the_density_it_solves():
"""Section 1.32.2: dX_t = b dt + sigma dW_t has density N(bt, t), so the variance tracks t."""
paths = 20000 # Enough paths that four standard errors is a tight band on both moments.
set_seed(SEED)
walk = fields.euler_maruyama(paths, 200, quiet=True)
assert abs(walk["mean"] - 0.5) < 4.0 * math.sqrt(1.0 / paths)
assert abs(walk["variance"] - 1.0) < 4.0 * math.sqrt(2.0 / paths)
assert walk["variance_gap"] < 0.05
# %% 07. Every script runs from the command line
@pytest.mark.parametrize("script", ["probability", "linear_algebra", "calculus", "information",
"optimization", "fields"])
def test_the_script_runs_clean(script):
"""Rule 7 of the conventions: --steps 20 --quiet finishes in a few seconds on a CPU."""
finished = subprocess.run(
[sys.executable, f"lecture_1/{script}.py", "--steps", "20", "--quiet"],
cwd=ROOT, env={"PYTHONPATH": str(ROOT), "PATH": "/usr/bin:/bin"},
capture_output=True, text=True, timeout=120,
)
assert finished.returncode == 0, finished.stderr
assert finished.stderr == ""
assert "Sections" in finished.stdout
|