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
41 changes: 27 additions & 14 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1475,7 +1475,7 @@ lines). Two traps found doing it:
single constant, so one file compiled differently while printing exactly the
same thing. Compare output, not code objects.

### The other four linters are configured and the repo does not pass them
### The other four linters: black passes now, mypy is the gap

pyflakes is the exception, not the rule. The README used to tell readers to run
`black .`, `ruff check .`, `mypy .` and `pylint`, as though the repository
Expand All @@ -1491,27 +1491,40 @@ was checked to differ only by trailing whitespace before the change was
believed — 3961 removed lines against 3961 added, zero differing by anything
else. That took the count to 1879 at no semantic risk.

**Ruff refused the other 907, and was right to.** They sit inside docstrings,
where whitespace is string content rather than layout — and here that content is
*printed*, since every example now passes `description=__doc__` to argparse. A
tool declining an unsafe fix is not an obstacle to route around with
`--unsafe-fixes`.
**Ruff refused the other 907 and was right to** — they sit inside string
literals, where whitespace is content rather than layout. Reaching for
`--unsafe-fixes` there would have been wrong.

What remains is mostly not lint: 727 are `List[int]`-for-`list[int]` style
modernisations that only became legal when the floor moved to 3.10. The ~200
after that are the ones with content, and **B905 is the group to read first** —
41 `zip()` calls with no `strict=`, which truncate to the shorter argument
without saying so.
**Black cleared 889 of those 907, and that is the interesting part.** Black
knows which triple-quoted strings are *docstrings* and normalises those, where
ruff could only see a string and had to stop. So the answer to a tool declining
an unsafe fix was a tool that could tell the difference, not overriding the
first one. The twelve that survive both are in argparse `epilog=` strings, which
are not docstrings and whose blank lines get printed.

Running black took ruff from 1879 to **951** and made `black --check` pass on
all 299 files. What remains is mostly not lint: 727 are
`List[int]`-for-`list[int]` modernisations that only became legal when the floor
moved to 3.10. The ~140 after that are the ones with content, and **B905 is the
group to read first** — 41 `zip()` calls with no `strict=`, which truncate to
the shorter argument without saying so.

`tests/test_lint_debt_only_shrinks.py` records the count **per rule**, not as a
total: a total lets ten fixed W293 pay for ten new B905, which is the opposite
of what a ratchet is for. It fails in both directions — a rule that grows, and a
baseline left above the real count, because a stale number hides the debt it
exists to expose.

Black is untouched deliberately. It would reformat 237 files, which is a diff
nobody can review and a `git blame` nobody can read, for no defect fixed. Worth
doing when no other session is mid-flight, in its own commit, and not before.
**mypy is now the honest remaining gap**, at 406 errors in `core/` alone, and
it is the one of the four that would be red on arrival and stay red. Nothing
about the black run touched it; formatting does not change types.

The black run itself was 241 files, and it changed `.py` only — no figure or
data byte moved. **The full suite reads 3404 passed / 21 skipped on both sides
of it**, measured on trees verified identical beforehand. What made that safe to
*believe* was not the argument that black is a formatter and therefore harmless;
it was that the README transcripts and the figure gate would have said
otherwise if it were not.

## Parallel sessions

Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,22 +213,22 @@ runs in CI on every pull request:
pytest
```

### The linters are configured, and the repository does not pass them
### Where the linters stand

This is worth stating plainly, because the section used to imply otherwise and
a reader who ran these got thousands of complaints and reasonably concluded
they had broken something. Measured over `core/`, the chapters, `scripts/`,
`tools/` and `tests/`:
Stated plainly, because this section used to imply the repository passed all of
them and a reader who ran them got thousands of complaints. Measured over
`core/`, the chapters, `scripts/`, `tools/` and `tests/`:

| Tool | Today |
|---|---|
| `ruff check` | **1879 findings.** 907 are whitespace inside docstrings that ruff will not safely fix — there it is string content, not layout. 727 are annotation modernisations (`List[int]` → `list[int]`) that only became available when the floor moved to 3.10. |
| `black --check` | 237 of 293 files would be reformatted |
| `mypy` | 404 errors in `core/` alone |
| Tool | Today | Was |
|---|---|---|
| `black --check` | **passes** — 299 files unchanged | 237 of 288 reformatted |
| `ruff check` | **951 findings.** 727 are annotation modernisations (`List[int]` → `list[int]`) that only became available when the floor moved to 3.10; the ~140 after that are the ones with content, `zip()` without `strict=` first | 5836 |
| `mypy` | 406 errors in `core/` alone — **the remaining gap** | 404 |

`tests/test_lint_debt_only_shrinks.py` records the ruff count per rule and fails
if any of them grows, so the number can only go down from here. It is not
pass/fail on the linters themselves, which would be red on arrival and stay red.
both when one grows and when a baseline sits above the real count, so the number
can only go down. It is not pass/fail on the linters themselves; mypy would be
red on arrival and stay red.

```bash
ruff check core ch*_* tests
Expand Down
1 change: 0 additions & 1 deletion ch2_coords/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@

Reference: Chapter 2 of the IPIN book
"""

84 changes: 56 additions & 28 deletions ch2_coords/example_attitude_visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,34 @@ def _style_3d(ax, elev: float = VIEW_ELEV, azim: float = VIEW_AZIM) -> None:

def _draw_reference(ax, axis_names=("X", "Y", "Z")) -> None:
"""Draw the faint unrotated frame, without labels, behind the subject."""
plot_frame_3d(ax, None, alpha=0.22, linewidth=1.4, linestyle="--",
axis_names=axis_names, show_labels=False)
plot_frame_3d(
ax,
None,
alpha=0.22,
linewidth=1.4,
linestyle="--",
axis_names=axis_names,
show_labels=False,
)


def _draw_rotation_axis(ax, axis: str, radius: float = 1.45) -> None:
"""Mark the axis a rotation happens about, as a dashed grey line."""
direction = {"x": np.array([1.0, 0.0, 0.0]),
"y": np.array([0.0, 1.0, 0.0]),
"z": np.array([0.0, 0.0, 1.0])}[axis]
direction = {
"x": np.array([1.0, 0.0, 0.0]),
"y": np.array([0.0, 1.0, 0.0]),
"z": np.array([0.0, 0.0, 1.0]),
}[axis]
span = np.outer(np.array([-radius, radius]), direction)
ax.plot(span[:, 0], span[:, 1], span[:, 2],
color="0.25", linestyle=":", linewidth=2.0, zorder=0)
ax.plot(
span[:, 0],
span[:, 1],
span[:, 2],
color="0.25",
linestyle=":",
linewidth=2.0,
zorder=0,
)


def plot_euler_convention(angle_deg: float = 35.0) -> plt.Figure:
Expand All @@ -133,14 +149,26 @@ def plot_euler_convention(angle_deg: float = 35.0) -> plt.Figure:
"""
angle = np.deg2rad(angle_deg)
panels = [
(f"Yaw psi = {angle_deg:g} deg, about Z\nEq. (2.14)",
euler_to_rotation_matrix(0.0, 0.0, angle), YAW_AXIS),
(f"Roll phi = {angle_deg:g} deg, about Y\nEq. (2.15)",
euler_to_rotation_matrix(angle, 0.0, 0.0), ROLL_AXIS),
(f"Pitch theta = {angle_deg:g} deg, about X\nEq. (2.16)",
euler_to_rotation_matrix(0.0, angle, 0.0), PITCH_AXIS),
("Composed C = Rx(theta) Ry(phi) Rz(psi)\nEq. (2.17)",
euler_to_rotation_matrix(angle, angle, angle), None),
(
f"Yaw psi = {angle_deg:g} deg, about Z\nEq. (2.14)",
euler_to_rotation_matrix(0.0, 0.0, angle),
YAW_AXIS,
),
(
f"Roll phi = {angle_deg:g} deg, about Y\nEq. (2.15)",
euler_to_rotation_matrix(angle, 0.0, 0.0),
ROLL_AXIS,
),
(
f"Pitch theta = {angle_deg:g} deg, about X\nEq. (2.16)",
euler_to_rotation_matrix(0.0, angle, 0.0),
PITCH_AXIS,
),
(
"Composed C = Rx(theta) Ry(phi) Rz(psi)\nEq. (2.17)",
euler_to_rotation_matrix(angle, angle, angle),
None,
),
]

fig = plt.figure(figsize=(14, 4.2))
Expand All @@ -162,9 +190,9 @@ def plot_euler_convention(angle_deg: float = 35.0) -> plt.Figure:
return fig


def plot_passive_vs_active(roll_deg: float = 0.0,
pitch_deg: float = 0.0,
yaw_deg: float = 50.0) -> plt.Figure:
def plot_passive_vs_active(
roll_deg: float = 0.0, pitch_deg: float = 0.0, yaw_deg: float = 50.0
) -> plt.Figure:
"""Figure 2: the passive/active transpose trap.

Chapter 2's C is passive: ``x_new = C x_old`` rotates the *coordinates*.
Expand All @@ -188,10 +216,8 @@ def plot_passive_vs_active(roll_deg: float = 0.0,
fig = plt.figure(figsize=(11, 4.6))
for index, (title, matrix) in enumerate(
[
("Passive: C, Eq. (2.21)\n'x_new = C x_old' -- rotates coordinates",
C),
("Active: C^T, Ch. 6 Eq. (6.13)\nbody-to-map -- rotates the vector",
C.T),
("Passive: C, Eq. (2.21)\n'x_new = C x_old' -- rotates coordinates", C),
("Active: C^T, Ch. 6 Eq. (6.13)\nbody-to-map -- rotates the vector", C.T),
],
start=1,
):
Expand Down Expand Up @@ -254,7 +280,8 @@ def plot_gimbal_lock() -> plt.Figure:
_style_3d(ax, **gimbal_view)
ax.set_title(
f"roll = 90, yaw = {yaw_deg:g}, pitch = {pitch_deg:g}\n"
"(identical to its neighbour -- that is the lock)", fontsize=9
"(identical to its neighbour -- that is the lock)",
fontsize=9,
)

# Show numerically that the recovery collapses to a single angle.
Expand Down Expand Up @@ -299,9 +326,7 @@ def plot_frame_chain() -> plt.Figure:
"""
# ENU->NED as an explicit matrix, obtained by mapping the basis vectors
# through the library function rather than hard-coding it here.
C_enu_to_ned = np.column_stack(
[enu_to_ned(basis) for basis in np.eye(3)]
)
C_enu_to_ned = np.column_stack([enu_to_ned(basis) for basis in np.eye(3)])

attitude = euler_to_rotation_matrix(
np.deg2rad(15.0), np.deg2rad(10.0), np.deg2rad(40.0)
Expand All @@ -310,8 +335,11 @@ def plot_frame_chain() -> plt.Figure:
panels = [
("ENU (local tangent)\nEast, North, Up", np.eye(3), ("E", "N", "U")),
("NED, Eq. (2.5)\nNorth, East, Down", C_enu_to_ned, ("N", "E", "D")),
("Body, Eqs. (2.6)/(2.7)\nroll 15, pitch 10, yaw 40",
attitude, ("x", "y", "z")),
(
"Body, Eqs. (2.6)/(2.7)\nroll 15, pitch 10, yaw 40",
attitude,
("x", "y", "z"),
),
]

fig = plt.figure(figsize=(12, 4.4))
Expand Down
Loading
Loading