Skip to content

Propagate NaN and Inf to the unstored entries when scaling a sparse array by a Diagonal - #1129

Closed
ViralBShah wants to merge 2 commits into
mainfrom
vs/diagonal-scale-nonfinite
Closed

ViralBShah wants to merge 2 commits into
mainfrom
vs/diagonal-scale-nonfinite

Conversation

@ViralBShah

@ViralBShah ViralBShah commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1121.

Scaling a sparse array by a Diagonal scaled only its stored entries, so an entry that is not stored stayed zero where dense computes 0 * Inf or NaN \ 0, which is NaN. #1101 fixed this for a scalar; this does it for a Diagonal, in *, \, /, rmul!, lmul!, ldiv!, rdiv!, and 3- and 5-argument mul!, for matrices, their adjoints and transposes, and Diagonal * x and ldiv!(D, x) for sparse vectors.

Before

julia> A = sparse([1.0 0; 0 1]); D = Diagonal([Inf, 1]);

julia> Matrix(A) * D
2×2 Matrix{Float64}:
  Inf  0.0
 NaN   1.0

julia> Matrix(A * D)
2×2 Matrix{Float64}:
 Inf   0.0
  0.0  1.0

julia> Matrix(rmul!(copy(A), D))
2×2 Matrix{Float64}:
 Inf   0.0
  0.0  1.0

julia> Matrix(Diagonal([NaN, 1]) \ A)   # dense: [NaN NaN; 0.0 1.0]
2×2 Matrix{Float64}:
 NaN    0.0
   0.0  1.0

After

julia> Matrix(A * D)
2×2 Matrix{Float64}:
  Inf  0.0
 NaN   1.0

julia> Matrix(rmul!(copy(A), D))
2×2 Matrix{Float64}:
  Inf  0.0
 NaN   1.0

julia> Matrix(Diagonal([NaN, 1]) \ A)
2×2 Matrix{Float64}:
 NaN    NaN
   0.0    1.0

_fullscaled finds the columns (or rows) in which op(0, d[j]) (or op(d[i], 0), with α folded in for mul!) is not zero and that are not already stored in full. When there are any, _storeinfull stores them in full, the new entries as stored zeros, and the unchanged kernels then scale those zeros as dense does. A fixed-pattern destination goes through _copyto_fixed!, so it throws with the destination unchanged when the result needs an entry it does not store, and a result too large for the index type throws before anything is written. When every diagonal entry scales zero to zero, the only extra cost is one op per diagonal entry, so the OpCount tests now count nnz + n products. An eltype without a zero keeps scaling only its stored entries, as in #1101. I swept sparse against dense locally, with Inf, -Inf and NaN (and complex ones) in the first or last diagonal entry, real and complex A with an empty row and a stored zero, and α, β in {(2, 0), (2, 3), (1, 1), (0, 2), (Inf, 0.5), (NaN, 0.5)}. Every result matches dense apart from the sign of a zero.

Timings at nnz = 10^6 (n = 10^5, finite D, minimum of 30 runs; allocations are unchanged):

before after
A * D 3.74 ms 3.75 ms
D * A 3.78 ms 3.78 ms
rmul!(A, D) 1.14 ms 1.18 ms
lmul!(D, A) 1.27 ms 1.29 ms
mul!(C, A, D, 2, 0) 1.48 ms 1.50 ms
D * A * D 1.82 ms 1.83 ms

Potential breakage: this changes what working code returns. A product or solve with a Diagonal that holds Inf or NaN now gives NaN in the affected columns or rows, as dense does, and stores those columns or rows in full. In-place scaling of a fixed-pattern array whose pattern does not hold them now throws an ArgumentError. The mul! docstring gains a sentence on this.

Left out: a non-finite β in 5-argument mul!, which scales the zeros of C rather than those of A. A dense C gives NaN there too.

🤖 Generated with Claude Code

ViralBShah and others added 2 commits October 26, 2026 13:16
…, `ldiv!` and `rdiv!`

Scaling a sparse matrix by a number or a `Diagonal` into a destination gave it the pattern
of the source before converting the values to its eltype, so an `InexactError` left the
destination with the new pattern and uninitialized values. When the conversion can fail,
the values are now converted first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rray by a `Diagonal`

Products, solves and in-place scaling of a sparse matrix or vector by a `Diagonal`
scaled only the stored entries, so an unstored zero times `Inf` or `NaN` stayed zero
where dense gives `NaN`. The affected columns or rows are now stored in full first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.89744% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.63%. Comparing base (dd7db50) to head (eba8bc7).

Files with missing lines Patch % Lines
src/linalg.jl 83.58% 11 Missing ⚠️
Additional details and impacted files
@@                       Coverage Diff                       @@
##           vs/scale-into-dest-validate    #1129      +/-   ##
===============================================================
- Coverage                        94.69%   94.63%   -0.07%     
===============================================================
  Files                               16       16              
  Lines                            10318    10395      +77     
===============================================================
+ Hits                              9771     9837      +66     
- Misses                             547      558      +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.

Base automatically changed from vs/scale-into-dest-validate to main October 26, 2026 21:09
@ViralBShah ViralBShah closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant