Skip to content

fix(linalg): reject non-square input in svd_invert instead of returning garbage (refs #102) - #106

Merged
kalwalt merged 1 commit into
devfrom
fix/102-svd-invert-reject-nonsquare
Jul 27, 2026
Merged

fix(linalg): reject non-square input in svd_invert instead of returning garbage (refs #102)#106
kalwalt merged 1 commit into
devfrom
fix/102-svd-invert-reject-nonsquare

Conversation

@kalwalt

@kalwalt kalwalt commented Jul 26, 2026

Copy link
Copy Markdown
Member

Implements option (c) of #102. Does not close it — the correct rectangular pseudo-inverse (option b) stays open.

The problem

svd_invert inherited a defect from jsfeat: for a non-square matrix it silently returns a wrong pseudo-inverse (only the first column is correct), with no error at all. Its TSDoc even claimed "Valid for any matrix shape/rank" — promising precisely what it fails to deliver.

The decision

It now throws on non-square input. The reasoning:

  • Upstream jsfeat is frozen — the bug can't be fixed there, so "bug-for-bug compatible" can't be an absolute principle.
  • Handing a caller wrong numbers quietly is worse than failing.
  • "We raise an error where jsfeat returned garbage" is a far easier divergence to justify than "we return different numbers than jsfeat."

Why throw rather than a 0 failure code: a wrong input shape is a programmer error, whereas lu_solve returning 0 signals a singular matrix — a legitimate numerical outcome. Different situations, different mechanisms. Throwing also avoids changing the signature void → number, which would itself be a typed-API break.

Scope is narrow

  • Square inversion is untouched and stays bit-compatible with jsfeat → tests/parity/linalg.test.ts (a 4×4) is unaffected.
  • Nothing inside the library calls svd_invert — it's purely public API — so there's no internal regression risk.

New: tests/divergences.test.ts

A deliberate counterpart to tests/parity/ — a registry of places where jsfeatNext intentionally does not match jsfeat, each with its reason and tracking issue.

Keeping these separate matters: a divergence buried in the parity suite looks like a broken test; here it reads as a decision. The three cases assert that (1) we throw, (2) jsfeat does not throw and really is wrong for the same input, and (3) square input still matches jsfeat exactly and satisfies A · A⁻¹ = I.

Also

  • Corrected the misleading TSDoc.
  • Updated examples/linalg_example.html, whose comment said the non-square result was "not trustworthy" — it now throws, so the wording had to change.

Verification

tsc --noEmit clean · npm test 66/66 (63 existing + 3 new), parity suite unchanged · square inversion still exact (0.6, -0.7, -0.2, 0.4 for [[4,7],[2,6]]) while non-square throws · all five API-demo examples still run clean · prettier clean.

🤖 Generated with Claude Code

…ng garbage (refs #102)

`svd_invert` inherited a defect from jsfeat: for a non-square matrix it
silently returns a WRONG pseudo-inverse — only the first column is correct —
with no error of any kind. Its TSDoc even claimed "Valid for any matrix
shape/rank", promising exactly what it fails to deliver.

Option (c) of #102: the method now throws on non-square input. Rationale, in
short — upstream jsfeat is frozen, so the bug cannot be fixed there; handing a
caller wrong numbers quietly is worse than failing; and "we raise an error
where jsfeat returned garbage" is a far easier divergence to justify than
"we return different numbers than jsfeat".

Chose throwing over a numeric failure code deliberately: a wrong input SHAPE
is a programmer error, whereas `lu_solve` returning 0 signals a singular
matrix, which is a legitimate numerical outcome. Different situations, so a
different mechanism. Throwing also avoids changing the public signature from
`void` to `number`, which would itself be a typed-API break.

Scope of the divergence is narrow: square inversion is untouched and remains
bit-compatible with jsfeat, so tests/parity/linalg.test.ts (which uses a 4x4)
is unaffected. Nothing inside the library calls svd_invert at all — it is
purely public API — so there is no internal regression risk.

Adds tests/divergences.test.ts, a deliberate counterpart to tests/parity/:
a registry of places where jsfeatNext intentionally does NOT match jsfeat,
each with its reason and tracking issue. Keeping them out of the parity suite
matters — a divergence buried there reads as a broken test, whereas here it
reads as a decision. The three cases added assert that we throw, that jsfeat
does not throw and really is wrong for the same input, and that square input
still matches jsfeat exactly and satisfies A * A^-1 = I.

Also corrects the misleading TSDoc, and updates examples/linalg_example.html,
whose comment said the non-square result was "not trustworthy" — it now
throws, so the wording had to change.

This does NOT close #102: the correct rectangular pseudo-inverse (option b)
is still open, and is a good candidate for the correctness work in #87.

Verified: tsc --noEmit clean; npm test 66/66 (63 existing + 3 new), parity
suite unchanged; square inversion still returns the exact inverse
(0.6, -0.7, -0.2, 0.4 for [[4,7],[2,6]]) while non-square now throws; all five
API-demo examples still run clean; prettier clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request Typescript all about Typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant