opt_transport#
Series: lecture-python.myst
File:
lectures/opt_transport.mdAudit date: 2026-08-26
Corpus snapshot:
e25fdf2345Categories 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 |
|
Math |
3/10 |
|
Code |
7/10 |
|
JAX |
out of scope |
JAX rules target |
Figures |
8/10 |
|
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 testingif sol not in sol_foundon 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%timeon 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 “thequantecon.optimize.linprog_simplexis much faster” (540) - where the operations take microseconds and%timereports wall clock;qe.timeitover many repetitions is what those two sentences need. 741-742 rebindspandqas 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 cellpandqareNodeobjects and re-running any earlier cell (352, 407, 463, 664) fails; 749 likewise rebindscfrom 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 writesc = C.flatten(), which is row-major, and 496-500 buildsA_eqwithi*n+jindexing to match - so the QuantEcon solver is handed a different vectorization of the same problem, which is why 521 reshapes withorder='C'and 525 withorder='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 passesA_ub=A.T, b_ub=C_vec- an inequality; 579 also writes the objective asp u + q vwhere 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 sameres_dual.x[:m]andres_dual.x[-n:]that 595-596 already printed, without touchingres_qe.lambdfrom 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_costis printed at 671 andres.funwas 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 -\mboxis 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 withnetworkx, 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 printedtupleof 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
linprogwants 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 thenp.kroncalls 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 thatlinproghandles 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} Hintthat 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
Nodeclass 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.
Recommended actions#
Fix
{eq}`dualproblem2`at 577-582: the constraint is an inequality, not an equality - 555 and theA_ub/b_ubcall at 588 both say so - and the objective needs its transposes, \(p^\top u + q^\top v\).Make the verification at 610-615 verify something. It currently reprints the SciPy dual from 595-596 rather than comparing it with
res_qe.lambdfrom 606-607, and because \(A\) is rank deficient the two solvers may legitimately disagree - which is worth saying, since it also means the \(u_i\) of 629 are pinned down only up to a constant.Seed the generator at 435. The claims at 456-458 that the shuffle experiment finds exactly two plans and that they are the two found earlier are not reproducible as written, and the exact-tuple test at 445 makes the count a floating-point accident.
Replace the five
%timemagics (411, 415, 532, 537, 760) withqe.timeitover repetitions, and re-check the two conclusions drawn from them - 418 and 540 - on a problem large enough for the difference to exceed timer noise; the current one is fifteen variables.Say that the convention changes at 493.
C_vecis column-major by 341 andc = C.flatten()is row-major, withA_eqat 496-500 built to match - which is the real reason 521 and 525 reshape with differentorder=flags and why the two plans look unrelated.Generate the opening figure. 84 is a static PNG with no caption or name, and 771-816 already builds the same picture with
networkxfrom a solved plan - moving that code earlier, or adding a small version of it, replaces an unreproducible image with one a reader can re-run.Convert the ten
\mathbf{1}/\mathbf{I}to\mathbb{1}and plain \(I\) (qe-math-004), the fourteen apostrophe transposes to\top(154, 159, 161, 175, 201, 214, 222, 225, 236, 247 and the rest), and\mboxto\textin the five optimization displays; lower-case the nine Title Case headings at 60, 131, 137, 258, 480, 546, 646, 658 and 677.Sweep the small items: the 44 double spaces (29, 66, 68, 74, 76, 110, 114, 155, 166, 168 and 34 more), the empty code cell at 819-821,
p/q/crebound as loop variables at 741-742, 749 and 782-796,`fun`written as math at 477 (it renders as \(f\cdot u\cdot n\)), and the typos at 133 (“using using”), 225 (“rewriten”), 472 (“factor” for factory), 514 (“solutions differs”) and 637 (“transprotation”).