Skip to content

feat: add Matrix.mmulByTranspose()#207

Open
lpatiny wants to merge 2 commits into
mainfrom
add-mmul-by-transpose
Open

feat: add Matrix.mmulByTranspose()#207
lpatiny wants to merge 2 commits into
mainfrom
add-mmul-by-transpose

Conversation

@lpatiny

@lpatiny lpatiny commented Jun 20, 2026

Copy link
Copy Markdown
Member

What

Adds Matrix.prototype.mmulByTranspose(scale?):

  • J.mmulByTranspose()J · Jᵀ (an m×m result from an m×n matrix).
  • J.mmulByTranspose(scale)J · diag(scale) · Jᵀ, the weighted Gram matrix, with scale a per-column factor.

The result is symmetric, so only the upper triangle is computed and mirrored, and the transpose is never materialized.

Why

This is the Gram-style product at the heart of the (weighted) normal equations — e.g. the Gauss-Newton Hessian J·W·Jᵀ in Levenberg–Marquardt. Computing it as J.mmul(J.transpose().scale(...)) allocates a full transpose, makes a separate scaling pass, and computes all entries; this method does none of that.

Numbers (48×2000 matrix)

new mmul(transpose()) speedup
J·Jᵀ (unweighted) 1.46 ms 3.53 ms 2.41×
J·diag(w)·Jᵀ (weighted) 1.60 ms 3.84 ms 2.41×

Unweighted result is bit-for-bit identical to mmul(transpose()); weighted matches to ~1e-13 (summation order).

Notes

  • src/matrix.js (implementation), matrix.d.ts (types), src/__tests__/matrix/utility.test.js (tests).
  • npm test passes (251 tests, eslint, prettier).
  • Profiling a Levenberg–Marquardt step (48 params × 2000 points) showed this product is ~92% of the per-iteration cost, so it's the single highest-leverage primitive there.

🤖 Generated with Claude Code

Adds `mmulByTranspose()`, which returns the matrix product of a matrix by
its own transpose (`this · thisᵀ`). The result is symmetric, so only the
upper triangle is computed and mirrored, and the transpose is never
materialized — about twice as fast as `this.mmul(this.transpose())`.

This is the Gram-style product at the heart of the normal equations
(e.g. the Gauss-Newton Hessian `J·Jᵀ` in Levenberg-Marquardt). Measured
~2.4x faster than `mmul(transpose())` on a 48x2000 matrix, bit-for-bit
identical.

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.03%. Comparing base (67cda77) to head (f1e7b11).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #207      +/-   ##
==========================================
+ Coverage   64.83%   65.03%   +0.19%     
==========================================
  Files          47       47              
  Lines        5625     5657      +32     
  Branches      954      961       +7     
==========================================
+ Hits         3647     3679      +32     
  Misses       1967     1967              
  Partials       11       11              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

`mmulByTranspose(scale)` now computes `this · diag(scale) · thisᵀ`, the
weighted Gram matrix, with `scale` a per-column factor. Without `scale`
it is unchanged (`this · thisᵀ`).

This covers weighted normal equations (`J·W·Jᵀ`) in a single symmetric,
transpose-free pass — ~2.4x faster than `mmul(transpose().scale(...))`.

Assisted-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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