Skip to content

Fix copysign() dropping the magnitude when y is zero - #214

Merged
ekzhang merged 1 commit into
ekzhang:mainfrom
MaxFreedomPollard:fix-copysign-zero
Sep 6, 2026
Merged

ekzhang merged 1 commit into
ekzhang:mainfrom
MaxFreedomPollard:fix-copysign-zero

Conversation

@MaxFreedomPollard

@MaxFreedomPollard MaxFreedomPollard commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

np.copysign(x, y) returns zero whenever y is zero. It is implemented in src/library/numpy.ts as absolute(x).mul(sign(y)), and sign(0) is 0, so the magnitude of x is multiplied away.

np.copysign(np.array([5, -5]), np.array([0, 0]))
// before: [0, 0]
// numpy:  [5, 5]

sign() also cannot see the sign bit, so -0 in y came back positive.

signbit() sits a few lines above copysign() in the same file and bitcasts to read the sign bit for every value class, including -0 and NaN. Reading the sign from it instead of from sign() fixes all of these cases and needs no new machinery.

Changes

  • src/library/numpy.ts: copysign() is now where(signbit(y), -abs(x), abs(x)).
  • test/numpy.test.ts: the existing "copysign with zero" test asserted the old [0, 0] result, so it now asserts numpy's [5, 5]. Added a test for signed zero in y and one for infinite and NaN y.

Verification

Node 22 on macOS. I ran vitest with --no-browser, which exercises the cpu and wasm devices.

  • pnpm test run test/numpy.test.ts -t "copysign" --no-browser gives 4 failed and 4 passed before the source change (the two affected tests on both devices) and 8 passed after.
  • pnpm test run test/numpy.test.ts test/refcount.test.ts test/dtype.test.ts test/random.test.ts src/library/numpy/einsum.test.ts --no-browser gives 1327 passed with the change and 1323 on unmodified main, with the same 4 failures on both. Those 4 are ReferenceError: Float16Array is not defined, which is Node 22 on this machine rather than anything in the diff; CI is on Node 26.
  • pnpm check, pnpm lint --max-warnings 0 and pnpm exec prettier --check are all clean.
  • I compared all 64 combinations of x in [1, -1, 5, -5, 0, -0, 3.5, -3.5] with y in [0, -0, 2, -2, inf, -inf, 1e-45, -1e-45] against numpy 2.4.6. Every pair matches on both cpu and wasm, and signbit(np.copysign(np.array([0]), np.array([-1]))) is true, so the signed zero result keeps its sign.

copysign() in src/library/numpy.ts was `absolute(x).mul(sign(y))`. sign()
returns 0 for a zero input, so any zero in y zeroed the whole result:
np.copysign([5, -5], [0, 0]) returned [0, 0] where numpy returns [5, 5].
sign() also cannot see the sign bit, so -0 and negative NaN in y were
treated as positive.

Read the sign from signbit() instead, which sits a few lines above in the
same file and bitcasts to get the sign bit for every value class. Checked
all 64 combinations of x in [1, -1, 5, -5, 0, -0, 3.5, -3.5] and y in
[0, -0, 2, -2, inf, -inf, 1e-45, -1e-45] against numpy 2.4.6; cpu and wasm
now agree with numpy on all of them.

The existing "copysign with zero" test asserted the old [0, 0]; it now
asserts [5, 5], with two more tests for signed zero and for infinite and
NaN sign arguments.

@ekzhang ekzhang left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Sorry for overlooking that

The browser tests passed in CI across chrome, safari, firefox on windows, macos, linux

@ekzhang
ekzhang merged commit 970fa22 into ekzhang:main Sep 6, 2026
5 checks passed

This branch was successfully deployed

1 active deployment
Preview — 7323cd00 Deployed Sep 6, 2026 by vercel[bot]
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