Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,68 @@ of it**, measured on trees verified identical beforehand. What made that safe to
it was that the README transcripts and the figure gate would have said
otherwise if it were not.

### `strict=` is the whole point, and ruff's own fix takes it away

`zip()` without `strict=` truncates to the shorter argument and says nothing.
The ratchet recorded 41, and the obvious move is `ruff --fix --unsafe-fixes`.
**It writes `strict=False`** -- the current behaviour, spelled out loud. That
takes the count to zero, looks exactly like the job being done, and preserves
every defect the rule exists to expose. Check *which* keyword a mechanical fix
inserted before believing the number it produced.

Done by hand instead, and the count was the least interesting part:

- **58 sites, not 41.** The other 17 are in `notebooks/`, which is not in
`SOURCE_DIRS` in `tests/test_lint_debt_only_shrinks.py`, so no ruff guard here
had ever read them -- in the three notebooks a student is most likely to open.
Same shape as `KNOWN_NON_EXAMPLE_CHAPTER_FILES`: **a file outside the sweep's
scope is a file nothing sweeps**, and that scope is a tuple somebody wrote
once.
- **One real defect**, in `ch5_fingerprinting/example_comparison.py`, where
`colors_box` held five shades against six methods. The sixth box kept
matplotlib's default blue among five pastels and read as deliberately
highlighted. Nothing raised, nothing left a range, no printed number moved --
the sixth box simply never got its colour, which only opening the PNG shows.
- **One site where `strict=True` is wrong.** `zip(scales, scales[1:])` is the
consecutive-pairs idiom and *depends* on the truncation, so strict raises on
every call; it is `itertools.pairwise` now. `zip(xs, itertools.count())` and
`zip(xs, cycle(ys))` are the same family. **The tell is a second argument
derived from the first**, and a grep for `[1:]` finds only the spelling you
thought of -- read the hunks.

**The entry is deleted from BASELINE rather than set to 0, and the deletion is
the guard**: `test_no_rule_has_more_findings_than_it_did` fails on any rule not
listed, so one new unguarded `zip()` turns it red. Verified by adding one and
watching `B905: 1 findings, new`, not by reading the assertion.

**A green sweep only means something where the line runs, so that was measured
too. 41 of the 42 `.py` sites execute**, and the one that does not is the
interesting entry. 34 run under the examples, the generators and two flag-gated
paths a plain run never reaches -- ch4's `--data` branch and ch7's `--animate`
callback, which between them hold two sites nothing else touches. Six more sit
inside test bodies and one in `core/fingerprinting`, all seven confirmed rather
than assumed. The last, ch8's unobservable-mode print, is behind
`if n_unobservable > 0`, which the fixes-aided configuration never enters. It
is equal-length by construction -- `Vt` from `svd(..., full_matrices=True)` is
square, so a mode column is `n_states` long and `state_names` comes from the
same analysis -- and its identical twin in the odometry-only branch executes.
The 17 notebook sites are covered by `tests/docs/test_notebooks_run.py`.

**The measurement lied twice before it said anything true**, both times looking
exactly like "this code never runs":

- `[tool.coverage.run] source = ["ipin_examples", "core"]` in `pyproject.toml`
means a plain `coverage run` over a chapter example measures `core/` and
**not the example**, so the first cross-reference reported 42 of 43 sites as
never loaded. Pass `--source` explicitly when measuring something the project
does not normally measure.
- `coverage combine` **starts clean unless you pass `--append`**. Running it a
second time discarded a finished 16-step sweep, and the same script then
reported 1 site executed where it had reported 32.

Both were caught the same way: a number moved while the code did not. That is
the fifth harness in this file to report the thing it could not read as broken.

## Parallel sessions

Several agents often work this repo at once, on separate worktrees off `main`.
Expand Down
2 changes: 1 addition & 1 deletion ch3_estimators/example_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,7 @@ def main():
ax.grid(True, alpha=0.3, axis="y")

# Add value labels on bars
for bar, n_evals in zip(bars, evaluations):
for bar, n_evals in zip(bars, evaluations, strict=True):
height = bar.get_height()
ax.text(
bar.get_x() + bar.get_width() / 2.0,
Expand Down
2 changes: 1 addition & 1 deletion ch4_rf_point_positioning/example_aoa_positioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ def visualize_aoa_geometry():
#
# Negating gives the anchor-to-agent direction, so the rays now meet at
# the target the way an AOA geometry figure is meant to show.
for i, (anchor, psi) in enumerate(zip(anchors, aoa_noisy)):
for i, (anchor, psi) in enumerate(zip(anchors, aoa_noisy, strict=True)):
# In ENU: psi is from North (+y), so the agent lies from the anchor at
# E-component = -sin(psi), N-component = -cos(psi)
line_length = 15
Expand Down
10 changes: 5 additions & 5 deletions ch4_rf_point_positioning/example_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ def plot_dataset_results(results: Dict, output_file: str = None):
results["gdop"]["AOA"],
]
bp = ax3.boxplot(gdop_data, tick_labels=["TOA", "TDOA", "AOA"], patch_artist=True)
for patch, color in zip(bp["boxes"], ["blue", "red", "green"]):
for patch, color in zip(bp["boxes"], ["blue", "red", "green"], strict=True):
patch.set_facecolor(color)
patch.set_alpha(0.6)
ax3.set_ylabel("GDOP")
Expand Down Expand Up @@ -934,7 +934,7 @@ def plot_inline_comparison(noise_levels, results):

# 1. RMSE vs Noise
ax1 = axes[0, 0]
for method, color in zip(methods, colors):
for method, color in zip(methods, colors, strict=True):
rmse_values = [
np.sqrt(np.mean(e**2)) if len(e) > 0 else np.nan for e in results[method]
]
Expand All @@ -950,7 +950,7 @@ def plot_inline_comparison(noise_levels, results):
# 2. Error CDF
ax2 = axes[0, 1]
noise_idx = 2
for method, color in zip(methods, colors):
for method, color in zip(methods, colors, strict=True):
errors = results[method][noise_idx]
if len(errors) > 0:
sorted_errors = np.sort(errors)
Expand All @@ -968,7 +968,7 @@ def plot_inline_comparison(noise_levels, results):
data = [results[m][noise_idx] for m in methods if len(results[m][noise_idx]) > 0]
labels = [m for m in methods if len(results[m][noise_idx]) > 0]
bp = ax3.boxplot(data, tick_labels=labels, patch_artist=True, showfliers=False)
for patch, color in zip(bp["boxes"], colors[: len(data)]):
for patch, color in zip(bp["boxes"], colors[: len(data)], strict=True):
patch.set_facecolor(color)
patch.set_alpha(0.6)
ax3.set_ylabel("Position Error (m)")
Expand All @@ -986,7 +986,7 @@ def plot_inline_comparison(noise_levels, results):
ax4 = axes[1, 1]
total_points = 50
dashes = ["-", "--", "-.", ":"]
for method, color, dash in zip(methods, colors, dashes):
for method, color, dash in zip(methods, colors, dashes, strict=True):
rates = [len(e) / total_points * 100 for e in results[method]]
ax4.plot(
noise_levels,
Expand Down
2 changes: 1 addition & 1 deletion ch5_fingerprinting/example_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ def evaluate_hierarchical_localization(db: FingerprintDatabase, rng=None):
ax.set_title("RMSE Comparison", fontsize=12)
ax.grid(True, alpha=0.3, axis="y")
# Add values on bars
for bar, rmse in zip(bars, rmses):
for bar, rmse in zip(bars, rmses, strict=True):
height = bar.get_height()
ax.text(
bar.get_x() + bar.get_width() / 2,
Expand Down
29 changes: 20 additions & 9 deletions ch5_fingerprinting/example_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ def evaluate_scenario(scenario_name, db, queries, true_locs, floor_id=None):
method_name = "NN (Euclidean)"
print(f" {method_name}...", end=" ", flush=True)
errors, times = [], []
for query, true_loc in zip(queries, true_locs):
for query, true_loc in zip(queries, true_locs, strict=True):
t_start = time.perf_counter()
est_loc = nn_localize(query, db, metric="euclidean", floor_id=floor_id)
t_end = time.perf_counter()
Expand All @@ -315,7 +315,7 @@ def evaluate_scenario(scenario_name, db, queries, true_locs, floor_id=None):
method_name = "k-NN (k=3)"
print(f" {method_name}...", end=" ", flush=True)
errors, times = [], []
for query, true_loc in zip(queries, true_locs):
for query, true_loc in zip(queries, true_locs, strict=True):
t_start = time.perf_counter()
est_loc = knn_localize(
query,
Expand Down Expand Up @@ -356,7 +356,7 @@ def evaluate_scenario(scenario_name, db, queries, true_locs, floor_id=None):
method_name = "MAP"
print(f" {method_name}...", end=" ", flush=True)
errors, times = [], []
for query, true_loc in zip(queries, true_locs):
for query, true_loc in zip(queries, true_locs, strict=True):
t_start = time.perf_counter()
est_loc = map_localize(query, model_bayes, floor_id=floor_id)
t_end = time.perf_counter()
Expand All @@ -382,7 +382,7 @@ def evaluate_scenario(scenario_name, db, queries, true_locs, floor_id=None):
method_name = "Posterior Mean"
print(f" {method_name}...", end=" ", flush=True)
errors, times = [], []
for query, true_loc in zip(queries, true_locs):
for query, true_loc in zip(queries, true_locs, strict=True):
t_start = time.perf_counter()
est_loc = posterior_mean_localize(query, model_bayes, floor_id=floor_id)
t_end = time.perf_counter()
Expand All @@ -408,7 +408,7 @@ def evaluate_scenario(scenario_name, db, queries, true_locs, floor_id=None):
method_name = "Post.Mean (k=10)"
print(f" {method_name}...", end=" ", flush=True)
errors, times = [], []
for query, true_loc in zip(queries, true_locs):
for query, true_loc in zip(queries, true_locs, strict=True):
t_start = time.perf_counter()
est_loc = posterior_mean_localize(
query, model_bayes, floor_id=floor_id, top_k=10
Expand Down Expand Up @@ -443,7 +443,7 @@ def evaluate_scenario(scenario_name, db, queries, true_locs, floor_id=None):
method_name = "Linear Regression"
print(f" {method_name}...", end=" ", flush=True)
errors, times = [], []
for query, true_loc in zip(queries, true_locs):
for query, true_loc in zip(queries, true_locs, strict=True):
t_start = time.perf_counter()
est_loc = model_lr.predict(query)
t_end = time.perf_counter()
Expand Down Expand Up @@ -604,8 +604,19 @@ def main():
ax4 = plt.subplot(3, 3, 4)
error_data = [r["errors"] for r in all_results["Baseline"]]
bp = ax4.boxplot(error_data, tick_labels=methods, patch_artist=True)
colors_box = ["lightblue", "lightcyan", "lightcoral", "lightsalmon", "lightgreen"]
for patch, color in zip(bp["boxes"], colors_box):
# One light shade per entry of `colors` above, in the same order. It was a
# shade short, so the sixth method's box kept matplotlib's default facecolor
# and read as deliberately highlighted among five pastel ones. strict=True
# below is what stops the two lists drifting apart again.
colors_box = [
"lightblue",
"lightcyan",
"lightcoral",
"lightsalmon",
"lightgreen",
"plum",
]
for patch, color in zip(bp["boxes"], colors_box, strict=True):
patch.set_facecolor(color)
ax4.set_ylabel("Positioning Error (m)")
ax4.set_title("Error Distribution (Baseline)")
Expand Down Expand Up @@ -641,7 +652,7 @@ def main():
# actual finding, so it should not be what breaks the labelling. Colours
# match the per-query cost panel above, so a method reads the same in both.
ax6 = plt.subplot(3, 3, 6)
for r, color in zip(all_results["Baseline"], colors):
for r, color in zip(all_results["Baseline"], colors, strict=True):
ax6.scatter(
r["ops_per_query"],
r["rmse"],
Expand Down
4 changes: 2 additions & 2 deletions ch5_fingerprinting/example_deterministic.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def generate_test_queries(db, n_queries=100, floor_id=None, noise_std=0.0, seed=
# Generate fingerprints by interpolating from nearby RPs
query_fingerprints = []

for i, (true_loc, fid) in enumerate(zip(true_locs, floor_ids_out)):
for i, (true_loc, fid) in enumerate(zip(true_locs, floor_ids_out, strict=True)):
# Find k nearest RPs for interpolation
if floor_id is not None:
dists = np.linalg.norm(rp_locs - true_loc, axis=1)
Expand Down Expand Up @@ -171,7 +171,7 @@ def evaluate_positioning_method(method_name, method_fn, queries, true_locs, **kw
errors = []
times = []

for query, true_loc in zip(queries, true_locs):
for query, true_loc in zip(queries, true_locs, strict=True):
t_start = time.perf_counter()
est_loc = method_fn(query, **kwargs)
t_end = time.perf_counter()
Expand Down
4 changes: 2 additions & 2 deletions ch5_fingerprinting/example_probabilistic.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def generate_test_queries(db, n_queries=100, floor_id=None, noise_std=0.0, seed=

query_fingerprints = []

for true_loc, fid in zip(true_locs, floor_ids_out):
for true_loc, fid in zip(true_locs, floor_ids_out, strict=True):
if floor_id is not None:
dists = np.linalg.norm(rp_locs - true_loc, axis=1)
else:
Expand Down Expand Up @@ -99,7 +99,7 @@ def evaluate_method(method_name, method_fn, queries, true_locs, **kwargs):
errors = []
times = []

for query, true_loc in zip(queries, true_locs):
for query, true_loc in zip(queries, true_locs, strict=True):
t_start = time.perf_counter()
est_loc = method_fn(query, **kwargs)
t_end = time.perf_counter()
Expand Down
Binary file modified ch5_fingerprinting/figs/comparison_all_methods.pdf
Binary file not shown.
Binary file modified ch5_fingerprinting/figs/comparison_all_methods.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion ch5_fingerprinting/figs/comparison_all_methods.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion ch7_slam/example_pose_graph_slam.py
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,7 @@ def build_map_from_poses(
from core.slam import se2_apply

all_points = []
for pose, scan in zip(poses, scans):
for pose, scan in zip(poses, scans, strict=True):
if len(scan) > 0:
# Transform scan to global frame
transformed = se2_apply(pose, scan)
Expand Down
8 changes: 6 additions & 2 deletions ch7_slam/example_scan_matching_visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ def plot_icp_correspondences(max_pairs: int = 45) -> plt.Figure:
label="source scan",
)
stride = max(1, len(matched_source) // max_pairs)
for src_pt, tgt_pt in zip(matched_source[::stride], matched_target[::stride]):
for src_pt, tgt_pt in zip(
matched_source[::stride], matched_target[::stride], strict=True
):
ax.plot(
[src_pt[0], tgt_pt[0]],
[src_pt[1], tgt_pt[1]],
Expand Down Expand Up @@ -609,7 +611,9 @@ def update(frame: int):
label="source scan",
)
stride = max(1, len(matched_source) // max_pairs)
for src_pt, tgt_pt in zip(matched_source[::stride], matched_target[::stride]):
for src_pt, tgt_pt in zip(
matched_source[::stride], matched_target[::stride], strict=True
):
axes[0].plot(
[src_pt[0], tgt_pt[0]],
[src_pt[1], tgt_pt[1]],
Expand Down
2 changes: 1 addition & 1 deletion ch8_sensor_fusion/example_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,7 @@ def interpolate_truth(t_est):
ax9.grid(True, alpha=0.3, axis="y")

# Add value labels on bars
for i, (lc_val, tc_val) in enumerate(zip(lc_values, tc_values)):
for i, (lc_val, tc_val) in enumerate(zip(lc_values, tc_values, strict=True)):
ax9.text(
i - width / 2, lc_val, f"{lc_val:.1f}", ha="center", va="bottom", fontsize=8
)
Expand Down
5 changes: 3 additions & 2 deletions ch8_sensor_fusion/example_observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,8 @@ def main():
# numpy 2 reprs as np.float64(-0.9999999999999998) -- accurate and
# unreadable in a table the chapter asks the reader to interpret.
components = {
name: round(float(value), 3) for name, value in zip(state_names, mode)
name: round(float(value), 3)
for name, value in zip(state_names, mode, strict=True)
}
print(f" Mode {i+1}: {components}")
# Identify dominant components. Taking argsort()[-2:] unconditionally
Expand Down Expand Up @@ -1039,7 +1040,7 @@ def main():
print("\n Unobservable modes:")
for i in range(obs_analysis_fixes["n_unobservable"]):
mode = obs_analysis_fixes["unobservable_modes"][:, i]
print(f" Mode {i+1}: {dict(zip(state_names, mode))}")
print(f" Mode {i+1}: {dict(zip(state_names, mode, strict=True))}")
else:
print("\n System is FULLY OBSERVABLE!")

Expand Down
4 changes: 2 additions & 2 deletions ch8_sensor_fusion/example_robust_tuning.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ def get_errors(history):
ax7.grid(True, alpha=0.3, axis="y")

# Add value labels
for bar, rmse in zip(bars, rmses):
for bar, rmse in zip(bars, rmses, strict=True):
height = bar.get_height()
ax7.text(
bar.get_x() + bar.get_width() / 2.0,
Expand Down Expand Up @@ -648,7 +648,7 @@ def get_errors(history):
ax8.grid(True, alpha=0.3, axis="y")

# Add value labels
for bar, rate in zip(bars, acceptance_rates):
for bar, rate in zip(bars, acceptance_rates, strict=True):
height = bar.get_height()
ax8.text(
bar.get_x() + bar.get_width() / 2.0,
Expand Down
4 changes: 2 additions & 2 deletions ch8_sensor_fusion/example_temporal_calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ def get_errors(history):
ax6.grid(True, alpha=0.3, axis="y")

# Add value labels
for bar, val in zip(bars1, no_corr_vals):
for bar, val in zip(bars1, no_corr_vals, strict=True):
height = bar.get_height()
ax6.text(
bar.get_x() + bar.get_width() / 2.0,
Expand All @@ -600,7 +600,7 @@ def get_errors(history):
va="bottom",
fontsize=8,
)
for bar, val in zip(bars2, with_corr_vals):
for bar, val in zip(bars2, with_corr_vals, strict=True):
height = bar.get_height()
ax6.text(
bar.get_x() + bar.get_width() / 2.0,
Expand Down
2 changes: 1 addition & 1 deletion core/fingerprinting/classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def predict(
if return_proba and hasattr(self.classifier, "predict_proba"):
probas = self.classifier.predict_proba(z_2d)[0]
classes = self.classifier.classes_
info["class_probabilities"] = dict(zip(classes, probas))
info["class_probabilities"] = dict(zip(classes, probas, strict=True))
info["top_k_classes"] = classes[np.argsort(probas)[::-1][:5]]

return predicted_location, info
Expand Down
2 changes: 1 addition & 1 deletion notebooks/ch3_state_estimation.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@
"ax.plot(x_true[0], x_true[1], 'g*', markersize=20, label='True Position')\n",
"ax.plot(x_wls[0], x_wls[1], 'r^', markersize=15, label=f'WLS Estimate (err={error_wls:.3f}m)')\n",
"\n",
"for i, (anchor, r) in enumerate(zip(anchors, ranges_meas)):\n",
"for i, (anchor, r) in enumerate(zip(anchors, ranges_meas, strict=True)):\n",
" circle = plt.Circle(anchor, r, fill=False, linestyle='--', alpha=0.5)\n",
" ax.add_patch(circle)\n",
"\n",
Expand Down
Loading
Loading