opt_transport#

  • Series: lecture-python.myst

  • File: lectures/opt_transport.md

  • Audit date: 2026-08-26

  • Corpus snapshot: e25fdf2345

  • Categories audited: writing, math, code, figures, references, links, admonitions (JAX out of scope)

  • Overall score: 7.3 / 10

  • Priority: HIGH

Score breakdown#

Category

Score

One-line note

Writing

3/10

qe-writing-006 ×9; qe-writing-005 ×14; qe-writing-003 ×5, +4 more.

Math

3/10

qe-math-002 ×14; qe-math-004 ×10; qe-math-009 ×3.

Code

7/10

qe-code-001 ×5; qe-code-004 ×5.

JAX

out of scope

JAX rules target lecture-jax.

Figures

8/10

qe-fig-005 ×2; qe-fig-002 ×1; qe-fig-001 ×1.

References

10/10

no mechanical violations detected.

Links

10/10

no mechanical violations detected.

Admonitions

10/10

no mechanical violations detected.

Issues#

Critical#

None found.

High severity#

  • [qe-code-001] (reviewer) — Follow PEP8 unless closer to mathematical notation. Count: 5. Lines: 411, 435, 445, 741, 819. Example: 435 creates rng = np.random.default_rng() with no seed, and the 1000-shuffle experiment at 438-448 then supports two claims that depend on what it finds: “putting constraints in different orders in this case uncovers two optimal transportation plans” (456) and “These are the same two plans computed earlier” (458). Neither is reproducible across builds. 445 compounds it by testing if sol not in sol_found on tuples of floats, so two plans that differ in the last bit are counted as distinct and the number “two” is a floating-point accident rather than a fact about the LP. 411, 415, 532, 537 and 760 use %time on a fifteen-variable linear program and then draw conclusions from single un-repeated measurements - “Evidently, it is slightly quicker to work with the system that removed a redundant constraint” (418) and “the quantecon.optimize.linprog_simplex is much faster” (540) - where the operations take microseconds and %time reports wall clock; qe.timeit over many repetitions is what those two sentences need. 741-742 rebinds p and q as loop variables ([p.mass for p in p_list]), shadowing the capacity and requirement vectors set at 333-334, and 782-796 does it again, so after that cell p and q are Node objects and re-running any earlier cell (352, 407, 463, 664) fails; 749 likewise rebinds c from 493. And 819-821 is an empty code cell at the end of the file.

  • [qe-code-004] — Use quantecon Timer context manager. Count: 5. Lines: 411, 415, 532, 537, 760. Example: %time.

  • [qe-math-002] — Use \top for transpose notation. Count: 14. Lines: 154, 159, 161, 175, 201, 214, 222, 225, 236, 247, …. Example: apostrophe transpose C'.

  • [qe-math-004] — Do not use bold face for matrices or vectors. Count: 10. Lines: 206, 213, 214, 217, 222, 228, 247, 248, 567, 571. Example: \mathbf.

  • [qe-writing-003] (reviewer) — Maintain logical flow. Count: 5. Lines: 341, 493, 580, 610, 627. Example: the lecture insists on a vectorization convention and then silently switches it. 362-372 stops to explain that C_vec = C.reshape((m*n, 1), order='F') uses Fortran column-major order because that is what \(\operatorname{vec}\) means, and even links an alternative row-major treatment; then 493 writes c = C.flatten(), which is row-major, and 496-500 builds A_eq with i*n+j indexing to match - so the QuantEcon solver is handed a different vectorization of the same problem, which is why 521 reshapes with order='C' and 525 with order='F' four lines apart, and why 514 has to say “the solutions differs”. Nothing tells the reader the convention changed. Second, {eq}`dualproblem2` at 577-582 writes the dual constraint as an equality, A' [u; v] = \operatorname{vec}(C), contradicting {eq}`dualproblem` at 555 (\(u_i + v_j \le c_{ij}\)) and the code at 588, which passes A_ub=A.T, b_ub=C_vec - an inequality; 579 also writes the objective as p u + q v where 554 has \(\sum_i p_i u_i + \sum_j q_j v_j\), so two transposes are missing. Third, 610 promises “We can verify these match the dual solution from SciPy” and the cell at 612-615 reprints the same res_dual.x[:m] and res_dual.x[-n:] that 595-596 already printed, without touching res_qe.lambd from 606-607 - the verification is a repeat, not a comparison, and it matters because \(A\) is rank deficient so the dual is not unique. Fourth, that same non-uniqueness undercuts 627-641 and is never mentioned there: the lecture spends 376-394 and 420-458 establishing that one equality constraint is redundant, which means \((u, v)\) is determined only up to adding a constant to every \(u_i\) and subtracting it from every \(v_j\) - so “\(u_i\) describes the cost of shipping one unit from factory \(i\)” (629) is not a well-defined number, and 627’s comparative static (“One unit more capacity in factory \(i\) … results in \(u_i\) more transportation costs”) is infeasible on its own, since {eq}`sumconstraints` requires \(\sum p_i = \sum q_j\). Fifth, 668 says “Sure enough, we have the same solution and the same cost” where nothing is compared - total_cost is printed at 671 and res.fun was printed 300 lines earlier at 357 - and “the same solution” is ambiguous in a lecture that has just shown there are two optimal plans.

  • [qe-writing-005] (reviewer) — Use bold for definitions, italic for emphasis. Count: 14. Lines: 76, 78, 374, 420, 426, 456, 472, 550, 563, 629, …. Example: the file has three italics, all correct emphasis (matrix, vector, vector at 139-144), and fourteen bold spans that are emphasis rather than definition: from and to at 76, 78, 472, 473, 629 and 633 (six occurrences of the same two words), minimization and maximization at 550, two at 420, all at 426, Ah hah! at 456, values at 563 and 573, and plus at 637 and 639. 374 additionally uses bold as a heading in mid-prose, Interpreting the solver behavior:. The genuine definitions are all handled correctly (transportation/optimal transport 18, optimal transport problem 108, vectorization 168, Kronecker product 179 and 184, dual 550, shadow prices 573, strong duality 620, complementary slackness 641), so the marker is carrying both jobs and the italic marker is doing almost nothing.

  • [qe-writing-006] — Capitalize lecture titles properly. Count: 9. Lines: 60, 131, 137, 258, 480, 546, 646, 658, 677. Example: H2 Title Case: ‘The Optimal Transport Problem’ (Optimal, Transport, Problem).

  • [qe-writing-008] — Remove excessive whitespace between words. Count: 44. Lines: 29, 66, 68, 74, 76, 110, 114, 155, 166, 168, …. Example: 2 spaces.

Medium severity#

  • [qe-fig-005] — Descriptive figure names for cross-referencing. Count: 2. Lines: 84, 771. Example: {figure} without :name:.

  • [qe-math-009] (reviewer) — Choose simplicity in mathematical notation. Count: 3. Lines: 102, 154, 206. Example: the five constrained-optimization displays use \mbox{subject to } (102, 160, 237, 555, 580) where \text{} is the LaTeX form - \mbox is plain-TeX and does not scale with the surrounding math. 154-155 defines the ones vector twice over, first as \((1, 1, \dots, 1)'\) and then as \(\mathbf{1}_n\), and the bold form is then used in ten places (qe-math-004); the style guide asks for \mathbb{1} with an explanation, which 154 already supplies. 206 sets up the Kronecker identity awkwardly: Let $A = \mathbf{I}_m', B = \mathbf{1}_n$ assigns \(A\) the transpose of the identity purely so that the \(A'\) appearing in {eq}`kroneckerprop` comes out as \(I_m\), and the reader has to unwind a double transpose to see it. Stating {eq}`kroneckerprop` as \(\operatorname{vec}(AXB) = (B^\top \otimes A)\operatorname{vec}(X)\) lets \(A = I_m\) be written directly. The letter \(A\) also carries three meanings in seventy lines - a generic \(m \times s\) matrix (181), \(\mathbf{I}_m'\) (206), and the \((m+n) \times mn\) constraint matrix (237, 245) that the code then builds at 344-346.

  • [qe-writing-001] — Use one sentence per paragraph. Count: 1. Lines: 28. Example: 2 sentences in one paragraph.

  • [qe-writing-002] (reviewer) — Keep writing clear, concise, and valuable. Count: 3. Lines: 133, 402, 654. Example: 398-404 takes four one-sentence paragraphs and an exclamation mark to say one thing: “But we can drop one of the equality constraints, and use only 7 of them”, “After doing this, we attain the same minimized cost”, “However, we find a different transportation plan”, “Though it is a different plan, it attains the same cost!” - the fourth restates the second and third. 654-655 is a parenthetical defence of material the reader has already read (“That said, the discussion provided above on vectorization remains important, since we want to understand what happens under the hood”), and 26 (“We will provide other applications in followup lectures”) and 126 (“More about this later”) are two more unanchored forward promises with no {ref}. 133 also carries a duplicated word - “we discuss using using standard linear programming solvers”.

  • [qe-writing-007] (reviewer) — Use visual elements to enhance understanding. Count: 3. Lines: 84, 386, 618. Example: 84 embeds a static PNG, optimal_transport_splitting_experiment.png, with no caption and no :name:, to illustrate factories and locations in the plane with vertex sizes proportional to capacity and demand and arrows for the transport plan - which is precisely the figure the lecture generates from code at 771-816 with networkx, node sizes scaled by mass and arrows for positive transfers. The opening picture cannot be regenerated, resized, or re-parameterised, and the machinery to build it arrives 700 lines later. Second, the lecture’s most interesting result - that the same LP has two optimal plans, and that constraint ordering rather than redundancy selects between them (420-464) - is delivered entirely as printed 15-vectors at 451-453; each plan is a \(3\times5\) shipment matrix over three factories and five locations, and two small heatmaps or two arrow diagrams side by side would show what actually differs between them, which the printed tuple of fifteen floats does not. Third, “Interpretation of dual problem” (618-641) explains ship-out and ship-in costs, complementary slackness, and that \(u_i + v_j = c_{ij}\) exactly on the shipped routes - all in prose, with the numbers \(u\) and \(v\) printed three cells earlier and never brought together with the plan. One table of \(c_{ij}\), \(u_i + v_j\) and \(x_{ij}\) over the fifteen routes would make complementary slackness visible in the worked example.

Low severity#

  • [qe-fig-001] — Do not set figure size unless necessary. Count: 1. Lines: 799. Example: figsize=.

  • [qe-fig-002] — Prefer code-generated figures. Count: 1. Lines: 84. Example: static image .png.

Strengths#

  • The vectorization is derived rather than asserted: 137-155 says why it is needed (SciPy’s linprog wants a vector, the problem has a matrix of unknowns), 166-176 defines \(\operatorname{vec}\) and rewrites the objective as an inner product, 178-202 introduces the Kronecker product with its block form and the one identity that matters, and 204-255 then converts both constraint blocks and assembles \(A\) and \(b\) - so the np.kron calls at 344-346 are readable line by line against the algebra.

  • 362-372 stops on the one implementation detail that silently breaks this problem, order='F', explains that it corresponds to stacking columns, says what ‘F’ stands for, and links a treatment that uses the other convention - a two-line digression that saves a reader from an off-by-permutation bug.

  • The rank deficiency is not hidden but pursued: 376-380 tests it with np.linalg.matrix_rank, 384-394 traces it to {eq}`sumconstraints` (the first three and last five constraints both assert that total capacity equals total requirements), and 396 notes that linprog handles it silently “without explicitly warning about rank deficiency”.

  • 420-464 is a genuine investigation with a stated hypothesis and a controlled test: the lecture notices it has two different plans at the same cost, suspects the dropped constraint, states in an {admonition} Hint that this is not what mattered, then shuffles the full constraint set 1000 times to show that constraint ordering selects the plan, and finally traces the original plan to dropping the first rather than the last constraint.

  • 512-518 compares the two solvers on the right footing - it says they use different algorithms, that multiple optima exist so the plans need not agree, and then checks the thing that must agree, np.allclose(-res_qe.fun, res.fun) - and 542-544 adds the honest caveat that the faster QuantEcon routine is less battle-tested than SciPy’s.

  • The dual is given an economic reading rather than left as a vector: 627-641 names \(u_i\) the ship-out cost and \(v_j\) the ship-in cost, states strong duality as total cost equals total ship-out plus total ship-in, and closes with complementary slackness in words a reader can check (\(u_i + v_j = c_{ij}\) exactly on routes with positive shipments).

  • The three implementations are introduced in a numbered list at 32-39 with links to each, then delivered in that order - SciPy linprog (352), quantecon.optimize.linprog_simplex (509), ot.emd (664) - and 660-673 checks the third against the first before moving to the larger problem.

  • The larger application (677-816) defines a Node class carrying location, group and mass, builds the cost matrix as Euclidean distance between factory and location coordinates (748-754), and then draws the solved plan as a directed graph with node size proportional to mass and an edge for each positive transfer - so the abstract \(x_{ij} > 0\) becomes an arrow on a map.