Skip to content

Bug fixes, broadcasting, matricize, and display improvements#151

Merged
mtfishman merged 18 commits into
mainfrom
mf/demo-readiness
Apr 15, 2026
Merged

Bug fixes, broadcasting, matricize, and display improvements#151
mtfishman merged 18 commits into
mainfrom
mf/demo-readiness

Conversation

@mtfishman

@mtfishman mtfishman commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary

A bundle of fixes, refactors, and display improvements for AbelianGradedArray and FusedGradedMatrix.

Bug fixes / correctness

  • Allocating broadcast correctness: handle β = 0 in scale! and drop the iszero guard in permutedimsopadd!. Previously scale!(y, 0) didn't reliably zero y when a block held NaN/Inf (from undef memory), which leaked garbage into unstored-block slots of results from 3 * a / a + b. Annotated with a comment at the call site.
  • FusedGradedMatrix broadcasting: shared GradedStyle (renamed from AbelianGradedStyle) and a generic permutedimsopadd! path.

Features

  • FusedGradedMatrix([sector => block, ...]) Pair constructor.
  • Non-canonical axes support for matricize / unmatricize (via insert_missing_sectors / delete_missing_sectors).
  • Strict 1-arg AbelianGradedArray(m::AbstractGradedMatrix) conversion constructor (works for both AbelianGradedArray and FusedGradedMatrix input).

Display

  • Compact SectorRange display: U1(0) instead of Irrep[U₁](0). For duals, factored wrapper dual(U1(0)) (not ', which collides with Julia's range adjoint (1:4)').
  • GradedOneTo shows as gradedrange([U1(0) => 2, U1(1) => 3]) compact; dual form factored to dual(gradedrange([...])). MIME\"text/plain\" inserts a sectors: [...] / sectors: dual.([...]) line between the blocked summary and the element body.
  • SectorOneTo shows as SectorOneTo(U1(0), 2); duals factored to the outside as dual(SectorOneTo(U1(0), 2)).
  • AbstractSectorArray prints as the Kronecker structure sector ⊗ data.
  • AbelianGradedArray / FusedGradedMatrix text/plain:
    • Summary uses typeof(a) so the Vector/Matrix type aliases render naturally.
    • Colon on the first line; each axis gets its own Dim d: ... line; block body follows.
  • Block-structured matrix display for graded arrays with │ ─ ┼ separators and for unstored blocks. Generalized to arbitrary rank via collect(Array(sector(blk)) ⊗ data(blk)) (using KroneckerArrays.⊗), so 3D arrays display with [:, :, k] = ... slice-by-slice block layout.

Internals / cleanup

Several suspicious helpers from an earlier PR were flagged and either simplified or removed:

  • pad_to_canonical_dualsinsert_missing_sectors: renamed to reflect what it actually does (pad each axis with multiplicity-0 entries for sectors missing on that side). Simplified implementation (~50 → ~20 lines) using block views + sectoraxes instead of integer round-trips. Preconditions (sorted + unique sectors) routed through check_input(::typeof(insert_missing_sectors), ...).
  • _pad_fused removed: replaced by a generic delete_missing_sectors(m::AbstractGradedMatrix, cod_ax, dom_ax) helper in src/fusion.jl — the inverse of insert_missing_sectors. Per-axis set-operation preconditions (sorted+unique, target ⊆ m, m-only sectors are empty blocks) routed through check_input.
  • _mul_mismatched removed: * on FusedGradedMatrix now requires matching sector lists (the standard matrix-multiplication semantics). Shared the check_mul_axes-equivalent logic between * and mul! via check_input(::typeof(*), A, B) / check_input(::typeof(mul!), C, A, B) (MatrixAlgebraKit-style).
  • zero! audit: dropped two redundant calls (in AbelianGradedArray(m) and permutedims) and annotated the remaining ones with invariant comments explaining when they're needed.
  • GradedOneTo <: AbstractBlockedUnitRange (needs first + blocklasts).
  • Minimal SparseArraysBase interface (eachstoredindex, storedvalues, isstored) on AbelianBlocks so blockstoredlength(a) = storedlength(blocks(a)) reflects dict-of-keys storage.
  • nondual(::SectorRange) helper.
  • Package-wide enumeratepairs for consistency.

Test plan

  • Pkg.test(\"GradedArrays\") — all tests pass, including new tests for:
    • allocating-broadcast correctness (graded and sector arrays)
    • FusedGradedMatrix Pair constructor (both U1 and SU2)
    • FusedGradedMatrix * FusedGradedMatrix for abelian (U1) and non-abelian (SU2) sectors, plus a DimensionMismatch case
    • 3D matricize + unmatricize round-trip (exercises insert_missing_sectors / delete_missing_sectors + the new axis-aware AbelianGradedArray constructor)
    • block-structured display (separators, dots, type aliases)
    • GradedOneTo / SectorOneTo / sector printing (new dual(...) notation)

🤖 Generated with Claude Code

mtfishman and others added 10 commits April 15, 2026 00:00
scale! now zeros the array when β=0 instead of multiplying (which
produced 0*NaN=NaN on uninitialized memory). The iszero(β) guard in
permutedimsopadd! for AbelianGradedArray is removed — scale! is always
called, correctly handling the β=0 case.

Fixes allocating broadcasts like `3 .* a` and `a - a` producing
garbage in positions that should be zero.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…atrix broadcasting

Generalizes the broadcast style to all AbstractGradedArray types.
permutedimsopadd! now dispatches on AbstractGradedArray instead of
AbelianGradedArray, giving FusedGradedMatrix scalar multiplication,
addition, and fused broadcasting (3 * m, m + n, c .= 3 .* m .+ 2 .* n).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace show(io, label(s)) with show(io, s) in display methods for
GradedOneTo, SectorOneTo, and FusedGradedMatrix. This prints U1(0)
instead of Irrep[U₁](0). The label() function is an implementation
detail that should not appear in user-facing output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ata)

Adds print_array, compact show, and text/plain show for
AbstractSectorArray following KroneckerArrays display pattern.
Covers both AbelianSectorArray and SectorMatrix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds FusedGradedMatrix([sector => block, ...]) as a convenience
constructor alongside the existing two-vector form.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Matrix

Uses a thin _GradedPrintView that converts GradedOneTo axes to
BlockedOneTo for BlockArrays printing infrastructure. Shows dots (⋅)
for unstored blocks and block separators (│, ─, ┼) at block boundaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…Matrix

GradedOneTo now subtypes AbstractBlockedUnitRange (just needs first +
blocklasts). Display converts to BlockSparseArray for printing, which
provides block separators (│, ─, ┼) and dots (⋅) for unstored blocks.
Defines isstored for AbstractGradedMatrix. No scalar getindex added.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- `GradedOneTo` now shows as `gradedrange([s => m, ...])`, with duality
  on individual sectors rather than a trailing `'` on the whole range
  — matches a valid `gradedrange` constructor call.
- `SectorOneTo` no longer duplicates the trailing `'`; the inner
  sector already carries the dual marker.
- `Base.adjoint(r::SectorOneTo) = dual(r)` for consistency with
  `GradedOneTo`.
- `AbelianGradedArray` summary moves the colon to the first line and
  prints per-axis "Dim d:" entries in `show(::MIME"text/plain", ...)`.
- `AbelianGradedArray` / `FusedGradedMatrix` summaries use `typeof(a)`
  so ndims aliases (Matrix/Vector) render naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Print duals as `dual(...)` rather than a trailing `'` since `'` is already
  Julia's range-adjoint operator (`(1:4)'` gives a 1×N adjoint matrix) and
  reusing it for duality is ambiguous. The `dual` wrapper is factored to
  the outside for compound types so the printed form round-trips cleanly:
  - `dual(gradedrange([s1 => m1, s2 => m2]))`
  - `dual(SectorOneTo(s, m))`
  - leaves the inner `SectorRange` renderer as `dual(U1(0))`.
- `GradedOneTo` now has a dedicated `MIME"text/plain"` show that injects a
  `sectors: [...]` line between the default `AbstractArray` summary and the
  block-separated element listing. Dual ranges render as
  `sectors: dual.([...])`.
- Generalize `_to_blocksparsearray` (and the `Base.print_array` override)
  from `AbstractGradedMatrix` to `AbstractGradedArray{T,N}`. Use
  `collect(Array(sector(blk)) ⊗ data(blk))` (via `KroneckerArrays.⊗`),
  which supports arbitrary rank instead of the 2D-only `kron`. 3D arrays
  now display with the standard `[:, :, k] =` slice-by-slice block layout
  inherited from `BlockSparseArray`.
- Implement the minimal `SparseArraysBase` interface on `AbelianBlocks`
  (`eachstoredindex`, `storedvalues`, `isstored`) so
  `storedlength(blocks(a))` — and by extension `blockstoredlength(a)` —
  reflects the dict-of-keys storage. The summary now uses
  `blockstoredlength(a)` directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mirror the `AbelianGradedArray` format — print each axis on its own line
between the summary and the block body, so the codomain/domain structure
(and its duality) is visible without inspecting the sectors list.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Apr 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.30233% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.33%. Comparing base (5b0ed3a) to head (b1008b8).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/gradedoneto.jl 35.29% 11 Missing ⚠️
src/abeliangradedarray.jl 78.57% 6 Missing ⚠️
src/sectorrange.jl 33.33% 4 Missing ⚠️
src/broadcast.jl 75.00% 2 Missing ⚠️
src/fusedgradedmatrix.jl 92.30% 2 Missing ⚠️
src/abeliansectordelta.jl 0.00% 1 Missing ⚠️
src/sectoroneto.jl 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #151      +/-   ##
==========================================
+ Coverage   86.12%   87.33%   +1.20%     
==========================================
  Files          20       20              
  Lines        1139     1263     +124     
==========================================
+ Hits          981     1103     +122     
- Misses        158      160       +2     
Flag Coverage Δ
docs 0.00% <0.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 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.

…_fused

Three suspicious helpers are reduced / removed:

- `pad_to_canonical_duals` (`src/fusion.jl`): ~50 lines → ~25 lines.
  Same algorithm, reorganized: one `Dict` for the canonical-sector
  position lookup (reused for both codomain and domain remapping),
  direct `a′[Data(new_i, new_j)] = data(...)` instead of manual
  `view(...) ∘ copyto!`.

- `_pad_fused` (`src/fusion.jl`): removed. The 2-arg axis-aware
  `AbelianGradedArray(m::FusedGradedMatrix, cod_ax, dom_ax)` constructor
  now builds the target `AbelianGradedMatrix` directly, so there is no
  need to first pad `m` with zero-size blocks and then convert. The old
  1-arg constructor delegates to the new 2-arg one with `axes(m)...`.

- `_mul_mismatched` (`src/fusedgradedmatrix.jl`): removed. `*` is now a
  three-line wrapper — check-input, allocate, `mul!` — that requires
  matching sector lists (the standard semantics when the two fused
  matrices have compatible contracted axes). The check_mul_axes/axes
  helpers are refactored into the MatrixAlgebraKit-style
  `check_input(::typeof(op), ...)` so `*` and `mul!` share one
  source of truth for the A/B axis-compat check.

Refresh the `iszero(β) ? … : …` ternary and `one(α)` (instead of the
`true` literal) in `permutedimsopadd!`.

New tests covering demo-relevant functionality that slipped through
the last PR:

- 3D `matricize` + `unmatricize` round-trip (the actual path the demo
  exercises; the existing 2D and 4D tests didn't hit `pad_to_canonical_duals`
  because their cod/dom axes are already canonical duals).
- `FusedGradedMatrix` `Pair` constructor with non-abelian `SU2`.
- `FusedGradedMatrix * FusedGradedMatrix` for both abelian (`U1`) and
  non-abelian (`SU2`) sectors, plus a `DimensionMismatch` test for
  incompatible sector lists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mtfishman mtfishman changed the title Demo-readiness: bug fixes, broadcasting, matricize, and display improvements Bug fixes, broadcasting, matricize, and display improvements Apr 15, 2026
mtfishman and others added 7 commits April 15, 2026 18:12
…emap

Rather than unpacking `Int.(Tuple(I))` and indexing back into the
precomputed `cod_sects`/`dom_sects` arrays to look up the sector of each
stored block, grab a view of the block and pull the sectors off it via
`sectoraxes(aI)`. The block already carries its (cod, dom) sector labels,
so there's no need to round-trip through integer indices.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`insert_missing_sectors` is more honest: the function doesn't just "pad
to duals"; it inserts multiplicity-0 entries for sectors missing from
one axis so that the two axes carry the same (non-dual) sector list.

Also add explicit precondition checks — each axis must have sorted,
unique sectors (as produced by `sectormergesort`). Repeated sectors
would be silently collapsed by the dict-based multiplicity lookup,
and unsorted input would be reordered by the internal `sort!`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…er AbstractGradedMatrix

Replaces the ad-hoc axis-aware `AbelianGradedArray(m::FusedGradedMatrix,
cod_ax, dom_ax)` constructor (which silently dropped m-sectors absent
from the target and lived in fusedgradedmatrix.jl) with a named helper
in fusion.jl:

- `delete_missing_sectors(m::AbstractGradedMatrix, cod_ax, dom_ax)`
  embeds `m` in target `(cod_ax, dom_ax)` axes, written in terms of
  block indexing (so it works on any `AbstractGradedMatrix` — tested
  with both `AbelianGradedArray` and `FusedGradedMatrix`).

Preconditions are explicit:
- each axis (both `m` and target) must be sorted + unique;
- `m`'s codomain/domain must be canonical duals (`cod_m == dual.(dom_m)`);
- any sector of `m` absent from either target axis must have
  multiplicity 0 on at least one side (empty block, no data loss).

The target axes themselves are *not* required to be canonical duals —
`unmatricize` calls this with the un-padded merged cod/dom, which may
have different sector sets.

Also:
- Add a strict 1-arg `AbelianGradedArray(m::AbstractGradedMatrix)`
  constructor (keeps `axes(m)` exactly, iterates via
  `eachblockstoredindex`).
- Replace `enumerate` with `pairs` throughout (same package convention
  as in `insert_missing_sectors`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… preconditions through `check_input`

Matches the pattern already used by `check_input(::typeof(*), A, B)` and
`check_input(::typeof(mul!), C, A, B)` on `FusedGradedMatrix`: each
sector-axis helper now has a corresponding `check_input(::typeof(fn),
args...)` method that holds the precondition checks, keeping the bodies
of the helpers themselves focused on the actual work.

Forward-declare the two functions (`function f end`) so that
`::typeof(f)` resolves on the `check_input` methods preceding the
function bodies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…=0 fix

- `AbelianGradedArray(m::AbstractGradedMatrix)`: every allowed block of
  the target is also in `m`'s stored blocks, so the loop below
  overwrites every allocation — no `zero!` needed. Add a comment.
- `permutedimsopadd!` (graded path): add a comment above the
  `iszero(β) ? FI.zero!(y) : scale!(y, β)` line explaining why
  `scale!(y, 0)` isn't equivalent (NaN * 0 == NaN would leak
  uninitialized garbage from unstored-block slots into allocating
  broadcasts like `3 * a`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two `zero!` calls after `similar(...)` were redundant and are removed:

- `insert_missing_sectors` (`src/fusion.jl`): every non-empty allowed
  block of the result corresponds to a stored block of the input (which
  is a post-`sectormergesort` matrix, so all allowed blocks are stored).
  Blocks coming purely from padding have multiplicity 0 on one axis —
  zero-size allocations with no data to clean up.
- `Base.permutedims(::AbelianGradedArray, perm)`: the subsequent
  `permutedims!` → `permutedimsopadd!(β=0)` already zeros the
  destination internally, so the outer `zero!` was doing the same work
  twice.

Left in place (each annotated with an invariant comment explaining why):
- `BlockReshapeFusion` `matricize`/`unmatricize` (zeros allocated dest
  blocks when input is sparse),
- `delete_missing_sectors` (target axes can have sectors absent from m,
  so some allowed blocks of the result never get written),
- Splitting and merging `Base.getindex` on `AbelianGradedArray` (each
  copy only writes a sub-range of the destination block).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The function is the per-axis inverse of `insert_missing_sectors`: given
`m` with canonical-dual-padded axes and target axes that correspond to
the un-padded originals, drop the padding entries. The target axes are
subsets of `m`'s, per axis.

The precondition reduces to three uniform checks applied independently
to the codomain axis and the domain axis:

1. Both the input (m) and output (target) axes are sorted and unique.
2. Target sectors are a subset of `m`'s sectors.
3. Sectors in `m` but not in the target have multiplicity 0 (safe to
   drop, no data loss).

Since target sectors are guaranteed to be a subset of m's, every
allocated block of the result is overwritten by the loop — no `zero!`
needed. Drop the `FI.zero!` and move the invariant comment to the
function body next to the `similar` call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@mtfishman mtfishman merged commit d470cf8 into main Apr 15, 2026
15 of 16 checks passed
@mtfishman mtfishman deleted the mf/demo-readiness branch April 15, 2026 23:42
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.

1 participant