Skip to content

Fall2026 - #27

Merged
rchillyard merged 98 commits into
editionFirstfrom
Fall2026
Aug 30, 2026
Merged

rchillyard merged 98 commits into
editionFirstfrom
Fall2026

Conversation

@rchillyard

Copy link
Copy Markdown
Owner

No description provided.

AshishNevan and others added 30 commits November 17, 2025 22:36
- Add union-find data structures (UF, UF_HWQUPC, WQUPC, TypedUF) with tests
- Implement Dijkstra's shortest path algorithm with EdgeWeightedDigraph and DirectedEdge
- Include comprehensive test cases for all implementations
- Set up Python project structure with pyproject.toml and gitignore
Add ruff linter as a development dependency in pyproject.toml and update lock file
Include .ruff_cache in gitignore to exclude linting cache files
…or` in various BST and Huffman coding methods.
… with `NotImplementedError` for unimplemented methods in `DList` and `Queue`.
…ect, shuffle, and entropy with corresponding tests and benchmarks.
Adds the packaging and pytest configuration the port was missing:

* `conftest.py` puts the `Python` directory on `sys.path`, so `src.*`
  resolves whichever directory pytest is invoked from.
* `[tool.pytest.ini_options]` sets `testpaths` and `pythonpath`.
  Deliberately does *not* narrow `python_functions`: several tests are
  named `test1`, `test2`, ... after the Java originals, and only
  pytest's default `test*` glob collects those.
* `[tool.ruff]` pins line length, target version and a modest rule set.
* Twelve missing `__init__.py` files, four under `src` and eight under
  `test`, so every directory is a real package.
* Replaces uv's template metadata (`name = "python"`, "Add your
  description here") with the project's own, and relocks accordingly.

All 263 tests are now collected from `Python/`, from the repository root
and from an unrelated directory alike; 243 pass and 20 fail on
`TO BE IMPLEMENTED` stubs, exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…undone

Three test files asserted the *stubbed* behaviour of a
`TO BE IMPLEMENTED` method, so they passed while the stub was empty and
would have failed the moment a student implemented it correctly:

* `test_quick_select.py` called `self.fail("Should have raised
  NotImplementedError")` if `select` returned a value;
* `test_d_list.py` wrapped the one-item constructor in
  `pytest.raises(NotImplementedError)`;
* `test_queue_elements.py` asserted `q.is_empty()` after `q.offer(1)`.

They are replaced by ports of QuickSelectTest, DListTest and QueueTest,
which assert correct behaviour. QuickSelectTest.testPartition has no
counterpart because it needs Partition/Partitioner from
sort.linearithmic, which is not yet ported.

Also in this commit:

* `test_two_four_tree.py` dropped its `sys.path.insert` hack -- the only
  test that had one -- and now imports `src.adt...` like the other 36.
  Its `test_get_unimplemented` is renamed to say what it really pins
  down: that `get` is a placeholder returning None. TwoFourTree is a
  skeleton; the Java class says "Ignore this class."
* `test_entropy.py` no longer binds two values it never asserts on. The
  Java original does assert them but is commented out, because those
  assertions are clock-seeded and "only good for about 66 2/3 hours".

The suite goes from 263 tests (243 pass / 20 fail) to 277 (241 pass /
36 fail). Every one of the 36 failures is `NotImplementedError:
TO BE IMPLEMENTED`; the increase is honest exercise coverage, not
breakage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stub parity with the Java tree. Two exercises that Java withholds were
being handed to Python students:

* `UF_HWQUPC.find`, `_merge_components` and `_do_path_compression` were
  all fully implemented. All three are `TO BE IMPLEMENTED` in
  UF_HWQUPC.java -- weighted quick-union with path compression is the
  exercise, and test_uf_hwqupc.py was passing green because the answer
  was in the file.
* `BagArray` wrapped a Python list, which dissolved `growFrom` entirely.
  It now holds a fixed-capacity backing store with an explicit count,
  and grows through `_grow_from`, the stub. As in Bag_Array.java the
  constructor grows from empty to a capacity of 32, so the class is
  unusable until the exercise is done -- the same coupling Java has,
  where AbstractGraph and BagTest both depend on it.

Three modules could not be imported at all, and no test covered any of
them:

* `graphs/undirected/abstract_graph.py` imported `adt.bqs.bag` and
  `adt.bqs.bag_array` without the `src.` prefix;
* `graphs/undirected/graph_edges.py` failed transitively;
* `compression/codelength/code_length.py` had kept the Java package path
  verbatim: `from com.phasmidsoftware.dsaipg.compression.codelength...`.

`test/test_imports.py` now imports all 82 modules under `src`, one test
each, so this class of breakage fails loudly instead of lying dormant.

Two fidelity fixes, neither an exercise:

* `BSTOptimisedDeletion.keys` raised `TO BE IMPLEMENTED` under a
  `# TODO: Implement properly` comment. Java's BST.keys() is a provided
  default method, so this was never an exercise -- only Java's
  keySet() override is unfinished (it returns null). Implemented.
* `BSTSimple.keys` collected into a set, discarding order. Java uses a
  TreeSet, so ordering is part of the contract; it returns a list now.

Suite: 362 tests, 298 pass, 64 fail. All 64 are
`NotImplementedError: TO BE IMPLEMENTED`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ruff was already a declared dev dependency but had never been run: 415
findings. This commit is deliberately separate from anything behavioural
-- the test result is identical before and after, 298 pass / 64 fail.

397 were auto-fixable and applied wholesale, almost entirely
`Optional[X]` -> `X | None`, `List[int]` -> `list[int]` and import
ordering. The project requires Python 3.10, so the modern spellings are
available.

The other 18 needed reading. Of note:

* `_do_heapify` computed `m = self.size()` and never used it. Java's
  doHeapify bounds the loop with `m + first - 1` where `m` is a field;
  since Python's `size()` is `len(self._heap) - self._first`, that bound
  is arithmetically the same as the `len(self._heap)` the port already
  uses. The local was dead, not a missing bound. Removed.
* `root = p` in `UF_HWQUPC.find` mirrors `int root = p;` in the Java
  original, where it is a deliberate starting point for the student.
  Kept, with a noqa saying so.
* `codebook = {}` sat unused above HuffmanCoding's stub; folded into the
  TO BE IMPLEMENTED note, which is where a hint belongs.
* `bin_heap` in test_priority_queue_binary_heap.py was dead setup -- the
  test assigns `pq._heap` directly on the following line.

The rest were one-statement-per-line, assigned lambdas turned into defs,
and one `yield from`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The root README's "Building and Testing (Python)" section was a heading
with nothing under it -- the file ended there. It now says how to run the
suite with uv and without it, what result to expect, and which parts of
the Java tree have no Python counterpart yet.

Python/README.md is new. pyproject.toml already declared
`readme = "README.md"`, pointing at a file that did not exist. Besides
the build instructions it tabulates where the nine exercises are, so a
student can find them without grepping.

CI is new too; the repository had no .github directory for either
language. The awkward part is that a bare `pytest` here is *supposed* to
be red, since the stubs are the coursework, so a green/red signal on
pytest says nothing. The workflow therefore gates on three things that
do carry information:

  1. ruff reports nothing;
  2. every module under src imports;
  3. every failing test fails on a TO BE IMPLEMENTED stub, and nothing
     else -- tools/check_stub_failures.py parses pytest's JUnit report
     and exits non-zero on any failure that is not an unwritten
     exercise.

Verified by injecting a deliberate `assert 1 == 2`: the gate exits 1 and
names the test. Both matrix entries were run locally through the exact
step sequence, on 3.10 and 3.13, and agree at 298 pass / 64 unwritten.

One trap worth recording: the interpreter has to be selected with the
UV_PYTHON environment variable, not `uv sync --python`. Because
.python-version pins 3.10, `uv run` rebuilds the environment at 3.10
afterwards, so the flag form silently tests 3.10 in both matrix legs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ran CleanTree over INFO6205/src with the corrected seven-field argument
list, copying 446 files. Also copied INFO6205's pom.xml, which brings
three things across:

* the gryphon_3 1.3.0 dependency, without which Tunnels_Gryphon cannot
  resolve WeightedEdge, WeightedGraph or MinimumSpanningTree;
* source and target release 21, replacing the previous 17/18 (bytecode
  goes from major 62 to major 65);
* number_3 1.9.3 in place of number_2.13 1.0.12. This one is cosmetic:
  Number's only consumers are projects/madhava, which the clean
  excludes, so the student tree references neither Number nor Rational.

Unimplemented methods now throw
com.phasmidsoftware.dsaipg.util.general.ImplementationMissing, named in
full because Clean injects the stub without adding an import and none of
the ten recipient files imports it. ImplementationMissing and
NotYetImplemented both arrive as new files under util/general.

README now says Java 21 is required. It previously recommended OpenJDK
18.0.2, which will not build this pom.

`mvn -B test-compile` succeeds with zero errors over both main and test
sources. Test *results* are unchanged in character: roughly a third fail
on TO BE IMPLEMENTED stubs, by design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-ran CleanTree after INFO6205 5b1da1d0, which fixed the unguarded
pivot-restoring swaps in Partitioner_DualPivot and the over-strict
lastIndex assertion in Entropy.getAByte. Neither sort/linearithmic nor
select is excluded from the clean, so the student tree had been carrying
both defects since the regeneration.

Three files change: QuickSort_DualPivot, Entropy, and the HITS
expectation in QuickSortDualPivotTest. mvn -B test-compile succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Re-ran CleanTree after INFO6205 3520a275 and 82ba6c6c. The student tree
now declares the CancelOnNotImplemented rule in every test class, so a
test which hits an unwritten exercise reports as skipped rather than
failed.

Effect here: 1257 tests, 110 failures, 16 errors, 158 skipped. Before
the rule those 158 were red, so the count of genuinely red tests falls
from 284 to 126.

The 126 remaining are the stubs which do not throw. Where a SKELETON
block supplies a plausible return value instead -- `return null` in
Bag_Array.growFrom being the big one -- the wrong answer propagates and
fails later, out of the rule's reach: 46 of the 126 are downstream
`assert items != null` failures inside Bag_Array itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bag_Array.growFrom now throws ImplementationMissing rather than
returning null, so the 46 Bag_Array tests report as skipped, naming the
location of the work, instead of failing on a downstream assertion far
from it.

1257 tests: 64 failures, 16 errors, 204 skipped. Red is down to 80 from
284 before the rule was introduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Student tree: 1257 tests, 19 failures, 7 errors, 274 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Student tree: 1257 tests, 6 failures, 2 errors, 299 skipped. The 8
remaining red are in three classes and are genuine defects, not
unwritten exercises: TwoSumBenchmarkTest, BaseComparatorHelperTest and
QuickSort_BasicTest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Student tree unchanged in outcome: 1257 tests, 6 failures, 2 errors,
299 skipped. The bookkeeping line is now given to the student rather
than being part of the exercise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
QuickSort_Basic was renamed to QuickSort_Classic in the solutions repo.
Clean copied the new file but never deletes, so the student tree kept
both -- and QuickSort_BasicTest went on asserting instrumentation counts
for the superseded implementation. Nothing else referenced it: the
current QuickSelect uses QuickSort_Classic.Partitioner_Basic. Both stale
files are removed, along with a stray .DS_Store.

With that, and rethrowIfUnimplemented in TwoSumBenchmarkTest, and the
config.ini fix in Clean:

  1239 tests, 0 failures, 0 errors, 300 skipped. BUILD SUCCESS.

Down from 284 red when this started.

Two further orphans are left in place, both harmless but stale:
PriorityQueueBinaryHeapTest, which is a renamed duplicate of
PriorityQueueTest and currently passes, and src/it/resources.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rchillyard and others added 28 commits August 28, 2026 16:28
1320 tests, 0 failures, 282 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1325 tests, 0 failures, 282 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
graphs/traversal and graphs/generic_BFS_and_prims are two copies of the same
material: StdRandom is byte-identical between them at 578 lines each,
EdgeWeightedGraph differs by four lines, Edge and MinPQ are near-copies. The
Python has one package, because the distinction the two Java packages draw --
int vertices versus generic ones -- does not exist in a dict-backed graph.

Not ported: StdRandom, 578 lines reimplementing what the random module provides; and
MinPQ, since the tree already has PriorityQueueBinaryHeap and Prims uses heapq.
So roughly 2,900 lines of Java become five small modules.

Three quirks of the originals are documented rather than reproduced. The two
addEdge conventions -- Graph.addEdge undirected, BFS.addEdge directed, with
nothing saying so -- are kept as add_edge and add_directed_edge, and the Java's
three exact traversal orders come out right with the directed one, which is the
evidence that reading was correct. Graph.V() returns the constructor's argument
rather than the vertex count, so __len__ answers the real question and
order_declared keeps the claimed number honestly named. GBFS cannot be run twice
because its marked map is a field, and its constructor is dead besides -- it
builds an edgeTo map, discards it, and its call to bfs is commented out.

Prims assumes a connected graph, as the Java says, and silently returns a
spanning tree of vertex zero's component otherwise. Reproduced and asserted.

34 tests. No exercises in these packages, so no stubs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The knapsack and houseRobber halves run on the ported DiGraph, matching the Java
now that its fourth graph implementation is gone.

Fibonacci, Lucas and Pell become one Recurrence class with three thin subclasses.
The Java has them as three classes, each with its own list and its own copy of the
same loop; they differ in exactly two things, the seeds and whether the previous
term is doubled. Writing it once makes that visible, which is the point the
package is making. `bad`, the exponential version, moves to the shared class --
the Java has it only on Lucas though it applies equally to all three.

An unavoidable divergence, and an instructive one: PellTest asserts
get(90) == 7052354271195710746, which is not the 90th Pell number but what a long
holds once that value has wrapped. Python's integers do not overflow, so the port
returns the true 9.96e33 and cannot match. Its test says so, and the Java's is
recorded in Deferred work.md. Fibonacci has the same problem latent -- its
ArrayList<Integer> wraps at n=47 -- and the port pins that too.

CoinChanger's table holds ints and infinity alike, where the Java uses double[][]
solely because "Infinity parameter is not supported in Java" for int, then casts
back.

35 tests, all the Java's own cases among them. No exercises in this package.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CleanTree's new orphan report caught knapsack/Edge.java and knapsack/Graph.java,
deleted upstream when knapsack moved onto DiGraph but still present here because
Clean never deletes. That is the check working: the report named them without
being asked, where the older two-tree diff had to be remembered and run by hand.

Also updated the Python's notes on Pell and Fibonacci, which described a
divergence that no longer exists now that the Java uses BigInteger too.

1331 tests, 0 failures, 286 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matches the Java: the dependency graph of Figure 10.11 is explicit, with a vertex
per remaining value and an edge per coin choice. 87c gives six coins, counts
(2, 0, 1, 3), and 87 sub-problems -- identical to the Java.

One divergence worth its comment. The Java uses Integer.MAX_VALUE as "not a
solution", so that its compareTo treats any real solution as smaller. Python has
no int maximum, so the sentinel is an explicit flag and comparison refuses to
order a non-solution as though it were merely a large one -- better_than rather
than a bare <.

1344 passed, 159 skipped. Student tree 1337, 0 failures, 287 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matches the Java: a vertex per sub-problem (kappa, omega), an edge per decision
about one item, and the graph built lazily as the search reaches each vertex. The
Google data set gives the same 7534, the same 32 items in the same order, and the
same 31373 sub-problems.

One deliberate divergence. The Java's valueRandom100 and valueRandom200 assert
values taken from java.util.Random's own sequence, which Python cannot reproduce.
Rather than transcribe those numbers, the port checks the answer against a
brute-force sweep of every subset of a twelve-item instance, over eight seeds and
four capacities. That validates the result instead of recording whatever the code
happened to produce -- which is what those two Java tests actually do.

ruff clean; 1365 passed, 159 skipped. Student tree 1341, 0 failures, 287 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The end of it is a number that matches: run Tunnels_Northeastern.main in INFO6205
and it reports 936 candidate tunnels, $6,648,954 and 5,305 metres; so does this,
under all three algorithms. That one assertion exercises the whole port at once --
the table of 80 buildings, the haversine distance, the zone-crossing cost model,
the 250-metre cutoff and the MST. A test pins it.

gis is sixteen modules: the Geo graph hierarchy, Prim, Kruskal and Boruvka, their
Geo variants, KML rendering and ShortestPaths. Kalimantan, the six-place graph the
Java tests have always carried, comes here too -- every weight in it is distinct,
so it has exactly one minimum spanning tree and the same answer can be asserted of
all three algorithms.

Three deliberate divergences, each noted where it lives.

GeoPrim, GeoKruskal and GeoBoruvka are empty here. The Java gives each its own
copy of get_geo_mst and create_edge, the same eight lines three times; Python puts
them once on GeoMST. The Java cannot: get_geo_mst needs getMST(), which is
package-private on MST, and an interface method must be public.

Of the four Tunnels_* classes only the general one is ported. Tunnels_Prim,
Tunnels_Kruskal and Tunnels_Boruvka are copies of Tunnels_Northeastern with the
algorithm fixed -- 700 lines restating what its parameter already says. Pass
GeoPrim, GeoKruskal or GeoBoruvka instead. Tunnels_Gryphon is left alone: Gryphon
is a Scala library, and Robin has deferred that move anyway.

ShortestPaths queues by cost. The Java's PriorityQueue<V> orders VERTICES by their
own natural ordering, which is not Dijkstra's rule and quietly requires the vertex
type to be Comparable, a constraint its signature never states. It still arrives
at the right answer -- a vertex is re-queued whenever a cheaper route appears --
but it does more work, and it fails outright on a vertex that cannot be ordered.

Also fixed here, to match the Java: GraphEdges.add_edge now puts an edge in BOTH
its vertices' bags, since an undirected edge is incident on each alike, and
edges() reports it once by collecting only from the bag of the vertex get()
returns. Two tests asserted the old one-sided behaviour and are turned round.

52 of the new tests are reported skipped, which is correct and is the point of
stub parity: QueueElements.offer, UF_HWQUPC.find and BagArray._grow_from are all
unwritten exercises, and the Java student tree skips the same ground for the same
reason. Verified by applying reference solutions in a scratch overlay -- all 69
gis and tunnels tests pass -- and then restoring the stubs.

ruff clean; 1420 passed, 197 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1355 tests, 0 failures, 298 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither had been touched since 25 August, and both still described a Python tree
with no sort, util, threesum or graphs to speak of. All of those have landed.

The Java section was telling students to expect failures: "about a thousand active
tests, of which two-thirds should run green. Don't worry about the failing tests."
That has not been true since CancelOnNotImplemented went in -- the student tree is
1355 tests, 1057 green, 298 skipped and nothing red -- and the whole point of that
work was that red should mean a real problem. It said the opposite.

The Python counts were equally stale: 362 tests where there are now 1617.

The stub table is rebuilt from a measured run rather than from memory, ordered by
how many tests each stub holds up, and naming the actual methods -- I had guessed
at several from the file names and was wrong about most of them. It now also says
which stubs hold up tests far from themselves: uf_hwqupc is wanted by Kruskal and
Boruvka and so by the campus tunnel network, and bag_array by every graph that
outgrows its initial capacity.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1359 tests, 0 failures, 303 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first part of misc. Three self-contained packages, and the Tuple classes that
exercise the first of them.

equable is how a class says what makes two of its instances equal -- hand back the
fields that count and let the base class do the rest -- and Tuple and
ComparableTuple are its test harness. Tuple had never been ported: it lives in
util/general in the Java, so it was left behind when util was done because it
depends on misc. Both are here now, next to what they exercise, which is where
they belong.

The asymmetric equality fixed in the Java today is fixed here too: a shorter
Equable matching a prefix of a longer one used to report equal while the reverse
did not.

functions is Either, Newton's method, and Lambert W. Newton returns an Either so
that a failure to converge cannot be read as a root by accident. The Lambert W
tests take their expected values from the Java's -- W(0,1) = 0.56714329, and the
pair of solutions to x = 4 log2(x), the larger being the 16 that the merge-sort
cutoff comes from -- and add the property that matters: feeding any answer back
through x exp(x) returns what we started with.

greedy is the greedy method as a shape, and Zeckendorf's representation, which is
the case where greedy is provably right. A test shows the case where it is not:
6 from denominations 1, 3 and 4.

One divergence, noted in the code. The Java admits x = 0 to branch -1 of Lambert
W, where the branch tends to minus infinity and W is undefined. It does not return
a value there either -- its estimate comes out as -Infinity and Newton then fails,
so it raises RuntimeException where the caller is watching for LambertException,
and W(0.0, tolerance) blows up rather than reporting the one branch that does
exist. Excluding zero from the domain says the same thing in the right way.

Either.is_left is new: the Java offers only isRight, and asking the other way
round reads better where the caller cares about the failure.

ruff clean; 1490 passed, 197 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BinarySearch, Counter, Mystery, Complex, MyDate and TailCall.

BinarySearch keeps its stub, as the Java does -- it is an exercise -- and its
tests skip until it is written. Verified against a reference solution in a scratch
overlay: all eight pass, then the stub was restored. The half-open range is
documented where the Java got it wrong, since that is exactly what a student has
to get right.

Mystery is the Java's puzzle, which it leaves unexplained; the docstring says what
it computes and why, and a test asserts that applying it twice is the identity.

MyDate caches its day of the week on demand, as the Java does. Doing that inside a
frozen dataclass takes a one-element list -- a plain field would be rebound, which
frozen forbids -- and the field is excluded from comparison, so asking a date what
day it falls on cannot change whether it equals another. There is a test for that.

TailCall is trampolining, which matters as much in Python as in Java: neither
eliminates tail calls. invoke is a loop rather than the Java's
Stream.iterate().filter().findFirst(), which makes it plain that the stack does
not grow -- and a test computes 10,000! both ways, one succeeding and the other
raising RecursionError.

Complex is ported for fidelity, with a note that Python's built-in complex does
all of this and more.

ruff clean; 1519 passed, 205 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
reduction is the same puzzle answered three ways -- from (x,y) a move gives
(x, x+y) or (x+y, y); can a target be reached? Moves1 searches forwards depth
first, Moves2 forwards breadth first, and Moves3 runs the moves backwards from the
target, where every step is forced and the whole thing collapses into the
subtractive Euclidean algorithm. Moves3 keeps its stub, as the Java does.

Moves2's search is a loop here rather than the Java's recursion, and the
divergence turned out to matter: the Java recursed once per point off the queue,
overflowed the stack, and had the (1,1) to (99,100) test disabled because of it.
Fixed in the Java too and that test is now back.

The Java's other disabled case has no counterpart here and should not: asking
Moves2 whether (35,13) reaches (455955547, 420098884) reaches over eleven million
points without exhausting the queue, in either language. Moves3 answers it at
once, which is what the package is for -- there is a test putting the identical
question to both.

randomwalk keeps its three stubs. It takes an optional Random, which the Java has
no way to accept -- its own is created inside the object -- so a walk can be
repeated; there is a test that two walks from the same seed land in the same place.
Also added expected_distance: the Java leaves the reader to notice the pattern in
the numbers its main prints, and the point of the exercise is that distance grows
as the square root of the number of steps. A test measures the ratio.

All 24 skipped tests across misc verified against reference solutions in a scratch
overlay -- 111 pass -- then the stubs were restored. One of my own assertions was
wrong and the overlay caught it: (10^20, 10^20 + 1) IS reachable from (1,1), in
two backward steps.

ruff clean; 1534 passed, 218 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WheelOfFortune is a wheel whose sectors have different widths, and it keeps its
stub. Python takes the source of randomness as a keyword argument after the
events, where the Java has three constructors: varargs must come last in Java and
cannot in Python.

MyTree keeps both of its stubs, and for an unusual reason, noted in the module and
recorded in INFO6205/docs/Deferred work.md: the JAVA has no reference answer
either. Both of its SOLUTION blocks read "return null; // TODO what should go
here?", so MyTree.addChild returns null in the solutions repository as much as in
the student tree, and the test that would catch it is annotated @ignore with no
@test at all -- it is not a disabled test, it is not a test. Whether to write that
answer is a teaching decision, so it is flagged rather than slipped in.

Its tests are written all the same and skip until an answer exists. Verified with
one in a scratch overlay: all fifteen pass, then the stubs were restored.

ruff clean; 1541 passed, 229 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coupling is the same pair of classes written twice, loosely and tightly, so the
difference is visible. Java draws it with `static class` against inner `class`,
which Python has no equivalent of -- a nested class here is never implicitly bound
to an instance of its enclosing one -- so the tight version holds the enclosing
object explicitly. That is what Java's inner class does behind the scenes anyway,
and it arguably shows the coupling more plainly. A test makes the cost concrete:
constructing a second A silently changes what the first one reports.

CallByValue carries over exactly. The Java exists to show that assigning to a
parameter cannot be seen by the caller while mutating what it refers to can, and
Python behaves the same way for the same reason. Only the vocabulary differs --
"primitive versus reference" there, "immutable versus mutable" here.

Patil is ported faithfully, faults included, and this one took a second look. It
sits in `assessment` and is named after a person, so it reads as submitted work
kept as a case study; correcting it would destroy whatever it was kept for.
Nothing calls it, its main is empty, and there are no tests in either tree. Two
faults, now asserted so they cannot be mistaken for the port's own: it stores each
value TWICE, because the first if writes x into its home slot and the loop then
starts at that same slot, finds it occupied -- by x, just now -- and writes x
again next door; and the probe never wraps, so a value hashing near the end with
nothing free after it is silently dropped.

I had first written that it loops forever on a full table. It does not -- its loop
is bounded -- and reading it properly rather than trusting the shape is what turned
up what is actually wrong with it.

misc is now complete but for RecursionAndIteration and NewtonApproximation, both
demonstrations with a main and no tests.

ruff clean; 1561 passed, 229 skipped. Student tree 1363, 0 failures, 305 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Robin deleted a comment I had put in misc/BinarySearch.java explaining what the
previous version got wrong: too historical to be useful for students, and it is in
git anyway. He is right, and the same fault was in eight docstrings here.

Trimmed, not deleted. The NOTEs saying where Python diverges from the Java and why
are the agreed treatment and stay -- what goes is the biography: "it used to be
wrong in way Y, which caused Z". Where a note was half of each it is now the
divergence alone.

The better move is usually to make the code say it rather than the comment, which
is what Robin's own fix did: he renamed BinarySearch's locals from lo/hi to
_from/_to, so that _to = mid is self-evidently right and _to = mid - 1 would look
wrong on sight. binary_search.py now reads the same way, using the parameter names
themselves.

ruff clean; 1561 passed, 229 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My first pass used too narrow a search and I said "swept" when it was not. Six
more had the same fault, in merge_sort_basic, heap_sort, instrumented_helper,
time_logger, position and pell.

Same treatment. What a reader needs is the rule -- the heap occupies xs[from_:to];
copy_block charges 2n either way; the pattern's final zero is what puts the leading
zero on half a second -- not the account of what a previous version charged or
printed instead. Where the note explains a real divergence from the Java it stays.

pell lost the most, and deserved to: five lines recounting an overflowed assertion
in a Java test, where what a reader wants is that the numbers grow by a factor of
2.414, pass a long at about n = 62, and have no ceiling in either tree.

ruff clean; 1561 passed, 229 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picks up Robin's edit to misc/BinarySearch: lo/hi renamed to _from/_to, so the
half-open range is carried by the names rather than asserted in a comment.

1363 tests, 0 failures, 305 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Twelve more, found by searching for the habit rather than the phrase I happened to
use last time: five in src, six in test, one in statistics.

The tests were the judgement call. Robin said "for Python, there's just cruft"
without separating src from test, and the same argument applies -- a student
reading test_undirected wants to know what an edge graph guarantees, not what the
test asserted before. Each note is now the guarantee: an algorithm walking by
adjacency needs the edges AT v; a sort which did not sort must not pass quietly;
zero laps have no mean, so this raises rather than returning Infinity, which is
>= 0 and would satisfy a test that only checked the sign. The last of those keeps
what was actually useful about the history -- why a weak assertion let it through
-- without recounting it.

One left alone deliberately: compression/codelength/solution.py carries about a
dozen lines of stream-of-consciousness reasoning, but they came with the original
port rather than from me, and they are a different problem from this one.

ruff clean; 1561 passed, 229 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two rounds after I said it was done. Each time I searched for the phrasing I
remembered writing rather than for the habit, declared the tree clean, and
committed on the strength of it -- twice with the counter-evidence printed
directly above the claim.

The habit reads the same way every time: state what must hold, and let the
failure it guards against be implied by the assertion rather than recounted above
it. "A vertex with no edge at either end must survive reversing" says everything
the four lines it replaced did, and says it about the code as it stands.

Some kept the useful half. Where a comment explained why a weaker test let
something through -- zero laps returning Infinity, which is >= 0 -- that reasoning
is now attached to the assertion instead of to its history.

Left alone: compression/codelength/solution.py, whose dozen lines of
stream-of-consciousness came with the original port rather than from me.

ruff clean; 1561 passed, 229 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picks up the comment sweep. The student tree no longer carries any account of what
a previous version got wrong.

1363 tests, 0 failures, 305 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moves1 takes the shape of slide #1a -- in_bounds(p) and (p == t or valid(q1) or
valid(q2)) -- rather than spelling the same logic out in three statements. Its move
does something now: valid recurses through it, where before it returned None
because nothing called it.

Moves2's _inner recurses again, as the case study writes it and as the Java does.
Making it a loop, which is what I had done, erases the step the exercise turns on:
moving the work still to be done onto a queue does not by itself make an algorithm
an iteration.

That divergence is worth its comment, because Python meets the wall sooner. _inner
goes one frame deep per point EXAMINED -- twelve thousand for the case study's
fifth condition, not the length of the path -- and Java survives that while
Python's default limit of 1000 does not. So the Java answers 1,1 to 99,100 and this
raises RecursionError. Same lesson, met earlier, and there is a test asserting it.

Moves2A is the next slide: nearer successor first, a set of eliminated points, and
a real iteration. It answers the fifth condition, which Moves2 cannot reach here at
all. Both improvements are measured and neither is worth anything -- the cache
never hits, and the ordering leaves the count at 12,090, which is exactly what the
Java examines. A test pins the growth as well: consecutive Fibonacci targets give
24, 96, 384, 1536, 2048, so the work outgrows the target 85-fold against 29-fold.

Two tests added for Moves3, which is a stub here, covering the defect found in the
Java today: a target BELOW the start is never reachable, and the aligned rules
report otherwise if they run before the missed-area test, since a modulus does not
care about sign. The old sweep held the start at (1,1), where nothing can be below
it, so it could not have caught this; the new one varies the start too. Verified in
a scratch overlay both ways round -- with the test first all twenty pass, with it
last exactly those two fail -- and the stub restored.

ruff clean; 1566 passed, 230 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picks up the reduction work: Moves1 in the shape of slide #1a, Moves2 recursive
again, the new Moves2A, and the Moves3 fix -- a target below the start is never
reachable, and the aligned rules said otherwise because a modulus does not care
about sign.

1378 tests, 0 failures, 317 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last package, and the team project: nine classes and 915 lines of scaffolding
around a deliberate blank. MCTS keeps that blank, with the Java's own comment where
the search goes -- "This is where you process the MCTS to try to win the game" --
and Position keeps its three stubs.

Three divergences, each noted in the code.

TicTacToeState is an inner class in the Java, reaching the game's random source
through TicTacToe.this. Python has no equivalent, so the state holds the game
explicitly -- which is what an inner class does behind the scenes anyway.

Position holds its grid as a tuple of tuples rather than a list of lists, so that
equality and hashing come free; the Java needs Arrays.deepEquals and deepHashCode
for the same reason. copy_grid still hands out a mutable list, since that is what
the move exercise writes into.

RandomState.long_value is bounded to the range of a Java long. Python integers are
not bounded, but a seed derived from an unbounded value would not behave as the
Java's does.

Verified with reference solutions for the three Position stubs in a scratch
overlay: all 45 tests pass, then the stubs were restored.

Two of the tests came out of the Java's vacuous list rather than into it. Its
testReflect and testRotate were empty, so the expectations had to be derived --
and the board matters: the obvious one is symmetric about the anti-diagonal, where
reflect(0) and rotate() give the same answer and neither test proves anything.
Written in both trees now.

ruff clean; 1607 passed, 247 skipped. Student tree 1379, 0 failures, 317 skipped.

Phase 4 is complete. Every Java package has a Python counterpart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both said misc and the team project were Java-only. Neither is, as of today, so
the coverage table now lists every package and says plainly that each one in the
Java tree has a counterpart here.

Counts refreshed too: 1854 tests, 1607 green and 247 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picks up the last nine vacuous tests and five historical comments that were
reaching students -- my earlier sweep searched for the phrasing I remembered
writing rather than for the habit, and missed "used to get", "used to say",
"used to disagree", "used to compare" and "used to declare".

1381 tests, 0 failures, 317 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The book is printed, so this repository is where the observations from
benchmarking Java against Python have to live.

docs/Java vs Python.md is the note, written for students: that a growth rate
belongs to the algorithm and not to the language, and that Python recovers it
more cleanly than Java because the interpreter's uniform per-operation cost
drowns out JIT warm-up and cache effects; that Java's cubic ThreeSum and
Python's quadratic are level at n = 500, past which the slower language with
the better algorithm pulls away without limit; that the ratio between the two
languages is not one number but anything from 5 to 100, depending mostly on
how much of the inner loop is really in the source language; and that path
compression halves union-find in Java while doing nothing at all in Python.
It ends with how to repeat the measurements, which is worth more than reading
them.

Short notes on ThreeSum, UF_HWQUPC and HashTable_LP point at it from the code
each finding turns on -- in both trees, since a Python reader should not have
to go to the Java to find them.

The Java changes otherwise are generated from INFO6205: HuffmanCoding did not
round-trip, for two reasons, one of which is the published SOLUTION and so
does not appear here.  test_encode2 in the Python tree did round-trip, but
asserted that the result was a prefix of the input, which an empty result
satisfies -- it is a plain equality now, and there is a test for a code split
across a word boundary, which is where the two defects lived.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@rchillyard
rchillyard merged commit 388e466 into editionFirst Aug 30, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants