Skip to content

Specialise GradedSpace functions based on storage type - #511

Merged
lkdvos merged 41 commits into
mainfrom
bd/gradedspace-storage
Sep 16, 2026
Merged

lkdvos merged 41 commits into
mainfrom
bd/gradedspace-storage

Conversation

@borisdevos

Copy link
Copy Markdown
Member

This is somewhat connected to what I was doing in QuantumKitHub/TensorKitSectors.jl#106, but beneficial for all sector types. Since I've firsthand experienced how my code transitioned from being unusable to performing well by going from NTuple storage to SectorDict storage, I thought it was about time to look at these storage paths.

I had two options going into this. The one I'm still looking into is seeing whether there's a cutoff (or range) where NTuple storage severely starts underperforming. The other approach I'm taking in this PR is to specialise GradedSpace functions and constructors based on their storage type. The overarching problems I tried to fix were the following:

  • the NTuple constructor could take unboundedly long to compile for sector types with many sectors
  • the NTuple versions of these functions were not making use of the fact that the sector types match, so there's efficient ways to accessing the sectors, knowing always how many there are as well.
  • the SectorDict versions of these functions were doing more per-pair dictionary work than the algorithm actually needs (extra hash lookups, double work, etc)

Summary of changes I made:

  • GradedSpace{I, NTuple{N,Int}} constructor: the old constructor built up the dims tuple via TupleTools.setindex, which fell back to Base's ntuple(f, Val(N)). This requires compiling this for every distinct N, which I found to scale terribly with N. I first tried building into a vector and then annotating the splat into a tuple, but it turns out that it has a cost that scales with N, which dominated for large enough N. So now I directly convert the vector through a Base iterator-to-tuple constructor which Julia specialised to make faster depending on N. The NTuple fuse and truncate_space make use of this as well.
  • dim specialisation: in general I tried avoiding constructing sectors(V) where possible, and just directly checking the NTuple directly (through values(I)) or the pairs in SectorDict.
  • ⊕, ⊖, infimum, supremum specialisations:
    • NTuple storage: the two spaces here are always of the same sector type, so their tuples are aligned. I could just do the appropriate map without looking up sectors. Again sectors(V) is the plague.
    • SectorDict storage: I made use of how the keys are sorted here to merge them in an appropriate way depending on the what the function actually wanted to achieve. These structurally looked the same, so I refactored them into _sortedmerge. This outperforms having to work directly with a SectorDict.
  • fuse SectorDict path: previously a bunch of gets and setindex!s were done in the double for-loop on the SectorDict, which accumulated inefficiently due to lookup cost for this type of dictionary. Plain Dicts don't have this, so I just do the accumulation in this and then sort at the end. The complexity hasn't changed since there's still two for-loops, but there's a speedup.
  • truncate_space SectorDict path: same structure as fuse for SectorDicts, but now with vectors because you don't have to look up anything along the way.
  • Restored binary search in SectorDict's _searchsortedfirst: I looked into when this was implemented, and goes back to 2019 back when product sectors didn't even exist. So I guess back then N was always fairly small, and the linear search was more efficient. However, it seems now that's not particularly the case, so I took the liberty of having it default to Base's method.

Benchmarks

I tested Julia 1.10.10 (LTS) and 1.12.6 (stable) since I think those are the two versions most people are on. For the NTuple storage sector types, I tested N = 2 / 8 / 64 / 256 / 1296 with Z2Irrep / ZNIrrep{8} / Z4Irrep⊠^3 / Z4Irrep⊠^4 / ZNIrrep{6}⊠^6. I also tested N = 15625 with ZNIrrep{5}⊠^6 where possible, which is important to mention. For the SectorDict I tested U1Irrep with charges -6:6, -50:50, and -200:200 (13/101/401 sectors).

And here the many many numbers. I spared my sanity by having a robot friend write this in markdown.

Constructor compile time (the Val effect)

Details
Julia before after speedup
1.10.10 104 ms / 144 ms / 697 ms / 1.55 s / 14.3 s 158 ms / 157 ms / 274 ms / 342 ms / 135 ms 0.66x / 0.91x / 2.5x / 4.5x / 106x
1.12.6 170 ms / 170 ms / 1.01 s / 1.64 s / 11.2 s 102 ms / 152 ms / 247 ms / 239 ms / 136 ms 1.67x / 1.12x / 4.1x / 6.9x / 83x

N=15625 before does not complete (at least within 5 minutes on my laptop) on either version. After: 973 ms (1.10.10), 1.00 s (1.12.6).

NTuple storage (after above compilation time)

Details
operation Julia before after speedup
constructor 1.10.10 468 ns / 2.67 µs / 465 µs / 3.83 ms / 99.2 ms 1.03 µs / 5.57 µs / 41.7 µs / 375 µs / 5.30 ms 0.45x / 0.48x / 11.1x / 10.2x / 18.7x
constructor 1.12.6 466 ns / 2.47 µs / 177 µs / 2.78 ms / 72.4 ms 538 ns / 2.50 µs / 32.2 µs / 184 µs / 2.59 ms 0.87x / 0.99x / 5.5x / 15.1x / 28.0x
dim 1.10.10 19.0 ns / 45.3 ns / 21.3 µs / 239 µs / 4.43 ms 10.8 ns / 11.8 ns / 22.8 µs / 306 µs / 5.22 ms 1.8x / 3.8x / 0.93x / 0.78x / 0.85x
dim 1.12.6 19.5 ns / 47.9 ns / 12.7 µs / 260 µs / 4.76 ms 5.7 ns / 5.1 ns / 7.41 µs / 148 µs / 2.19 ms 3.4x / 9.4x / 1.7x / 1.8x / 2.2x
⊕ 1.10.10 520 ns / 1.04 µs / 259 µs / 4.45 ms / 102 ms 10.0 ns / 10.8 ns / 4.56 µs / 16.7 µs / 112 µs 52.0x / 96.4x / 56.8x / 267x / 909x
⊕ 1.12.6 305 ns / 843 ns / 220 µs / 3.31 ms / 94.9 ms 5.1 ns / 5.6 ns / 3.35 µs / 16.2 µs / 107 µs 59.7x / 151x / 65.7x / 205x / 886x
⊖ 1.10.10 138 ns / 384 ns / 260 µs / 5.24 ms / 106 ms 202 ns / 328 ns / 48.1 µs / 651 µs / 11.1 ms 0.68x / 1.17x / 5.40x / 8.05x / 9.56x
⊖ 1.12.6 76.3 ns / 222 ns / 205 µs / 3.37 ms / 84.9 ms 56.2 ns / 131 ns / 21.1 µs / 296 µs / 5.18 ms 1.4x / 1.7x / 9.7x / 11.4x / 16.4x
infimum 1.10.10 542 ns / 1.54 µs / 287 µs / 4.76 ms / 102 ms 10.8 ns / 11.3 ns / 4.57 µs / 17.3 µs / 125 µs 50.2x / 136x / 62.9x / 275x / 820x
infimum 1.12.6 326 ns / 907 ns / 218 µs / 3.41 ms / 87.6 ms 5.1 ns / 5.7 ns / 3.35 µs / 12.6 µs / 106 µs 63.9x / 159x / 65.2x / 270x / 825x
supremum 1.10.10 311 ns / 688 ns / 265 µs / 4.48 ms / 99.1 ms 10.8 ns / 11.3 ns / 4.59 µs / 17.2 µs / 114 µs 28.7x / 60.8x / 57.8x / 261x / 866x
supremum 1.12.6 219 ns / 556 ns / 216 µs / 3.30 ms / 83.1 ms 5.6 ns / 7.3 ns / 3.39 µs / 12.4 µs / 105 µs 39.0x / 76.2x / 63.7x / 267x / 790x
fuse 1.10.10 268 ns / 1.86 µs / 2.86 ms / 280 ms / 25.9 s 111 ns / 281 ns / 2.56 ms / 135 ms / 12.4 s 2.4x / 6.6x / 1.1x / 2.1x / 2.1x
fuse 1.12.6 173 ns / 1.69 µs / 2.45 ms / 286 ms / 13.2 s 16.2 ns / 65.7 ns / 1.16 ms / 135 ms / 12.6 s 10.7x / 25.7x / 2.1x / 2.1x / 1.05x
truncate_space 1.10.10 41.9 ns / 300 ns / 247 µs / 4.52 ms / 230 ms 145 ns / 217 ns / 32.9 µs / 525 µs / 9.68 ms 0.29x / 1.4x / 7.5x / 8.6x / 23.8x
truncate_space 1.12.6 40.1 ns / 130 ns / 199 µs / 3.20 ms / 91.3 ms 46.8 ns / 80.4 ns / 18.4 µs / 251 µs / 4.50 ms 0.86x / 1.6x / 10.8x / 12.7x / 20.3x

And now just the N=15625 case separately, also just after only as before doesn't finish:

operation 1.10.10 1.12.6
constructor (warm) 533 ms 526 ms
dim 554 ms 580 ms
⊕ 1.08 ms 1.28 ms
⊖ 1.14 s 290.7 s cold (see note below)
infimum 937 µs 1.32 ms
supremum 869 µs 1.01 ms
truncate_space 972 ms 1.17 s
fuse not run (O(N^2))

On the ⊖/1.12.6 number: the first call at this N takes ~290-353s on 1.12.6 specifically (reproduced 3×), but the second call in the same session takes ~1.2s, matching 1.10.10's steady-state ~1.1s for the same op at the same N. Only compiling the whole ⊖ method together on 1.12.6 is this slow. I didn't look deeper into this, also since its use-case is extremely limited for this large N.

SectorDict U1Irrep

Details
operation Julia N=13 before→after N=101 before→after N=401 before→after
constructor 1.10.10 7.38 µs → 7.02 µs (1.05x) 56.0 µs → 45.2 µs (1.24x) 344 µs → 203 µs (1.69x)
constructor 1.12.6 5.29 µs → 4.27 µs (1.24x) 37.3 µs → 27.3 µs (1.37x) 199 µs → 112 µs (1.78x)
dim 1.10.10 279 ns → 21.6 ns (12.9x) 12.7 µs → 45.1 ns (282x) 170 µs → 87.5 ns (1941x)
dim 1.12.6 129 ns → 12.3 ns (10.5x) 6.65 µs → 74.1 ns (89.8x) 75.3 µs → 222 ns (340x)
⊕ 1.10.10 5.67 µs → 872 ns (6.50x) 91.0 µs → 4.98 µs (18.3x) 967 µs → 21.8 µs (44.4x)
⊕ 1.12.6 2.43 µs → 201 ns (12.1x) 46.8 µs → 1.01 µs (46.1x) 435 µs → 3.71 µs (117x)
⊖ 1.10.10 2.83 µs → 1.52 µs (1.86x) 66.4 µs → 11.3 µs (5.88x) 774 µs → 54.7 µs (14.1x)
⊖ 1.12.6 1.53 µs → 566 ns (2.71x) 29.9 µs → 4.77 µs (6.27x) 375 µs → 21.8 µs (17.2x)
infimum 1.10.10 6.25 µs → 866 ns (7.22x) 80.1 µs → 4.51 µs (17.7x) 816 µs → 22.3 µs (36.6x)
infimum 1.12.6 4.99 µs → 206 ns (24.2x) 38.4 µs → 963 ns (39.9x) 381 µs → 4.49 µs (84.8x)
supremum 1.10.10 4.86 µs → 856 ns (5.68x) 71.5 µs → 4.47 µs (16.0x) 767 µs → 22.2 µs (34.6x)
supremum 1.12.6 3.06 µs → 198 ns (15.5x) 32.4 µs → 997 ns (32.5x) 344 µs → 3.62 µs (94.9x)
fuse 1.10.10 20.9 µs → 13.2 µs (1.58x) 6.50 ms → 694 µs (9.36x) 335 ms → 11.1 ms (30.3x)
fuse 1.12.6 16.1 µs → 5.87 µs (2.74x) 3.65 ms → 333 µs (11.0x) 171 ms → 5.62 ms (30.5x)
truncate_space 1.10.10 2.44 µs → 2.31 µs (1.06x) 36.4 µs → 17.8 µs (2.05x) 441 µs → 108 µs (4.07x)
truncate_space 1.12.6 1.07 µs → 890 ns (1.21x) 18.3 µs → 7.68 µs (2.39x) 206 µs → 35.6 µs (5.80x)

(Negative) conclusions/remarks from the benchmarks:

  • For very small N the constructor is still somewhat slower (0.45x-0.99x), but the alternative, per the first table, is to make large N impossible to compile in reasonable time.
  • truncate_space is still slightly slower at N=2 (0.29x/0.86x) but wins from N=8 up.
  • Why NTuple fuse scales the way it does with N I have no clue, but it's at least better for reasonable ranges.
  • ⊖ on 1.12.6 is a one-time compile bottleneck, but afterwards does fine (see note above).
  • SectorDict wins scale with sector count.

All in all, these are improvements, notably the SectorDict. So it makes you wonder if there's in fact some cutoff N above which you want to say SizeUnknown to the SectorValues' length 🤔

Comment thread src/auxiliary/dicts.jl Outdated
@codecov

codecov Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.22222% with 22 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/spaces/gradedspace.jl 89.65% 12 Missing ⚠️
src/factorizations/truncation.jl 85.71% 8 Missing ⚠️
src/auxiliary/dicts.jl 96.00% 2 Missing ⚠️
Files with missing lines Coverage Δ
src/TensorKit.jl 17.24% <ø> (ø)
src/factorizations/factorizations.jl 84.61% <ø> (ø)
src/factorizations/pullbacks.jl 70.45% <100.00%> (ø)
src/tensors/sectorvector.jl 98.57% <100.00%> (ø)
src/auxiliary/dicts.jl 71.76% <96.00%> (+8.12%) ⬆️
src/factorizations/truncation.jl 87.86% <85.71%> (+1.30%) ⬆️
src/spaces/gradedspace.jl 90.68% <89.65%> (-2.17%) ⬇️

... and 8 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lkdvos lkdvos left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for starting to have a look at this!

I am trying to go over this in slightly more detail, but let me put some global remarks/thoughts I have here:

The first thing relates to the NTuple case for large N. I absolutely agree that the compile time of that is untenable, and should be addressed, and while I would probably do something similar to you, there is something that looks a bit off to first constructing a vector, only to then immediately convert it to a tuple. I would definitely prefer to keep the small N values non-allocating and fully specialized, if at all possible, since otherwise this is a lot of extra code to maintain and we might also consider just keeping everything SectorDict and optimizing that.

In some sense, if we are already allocating a vector, we might as well just store the result as a vector and see if the extra pointer indirections actually matter, which I'm not expecting them to do.
So basically, I think it might be useful to just have a GradedSpace{I, Vector{Int}} implementation that has the exact same semantics as the tuple version, but avoids the compile-time issues.

@assume_effects :foldable function sectorstoragetype(::Type{I}) where {I <: Sector}
    if Base.IteratorSize(values(I)) isa Union{HasLength, HasShape}
        N = length(values(I))
        return N <= 10 ? NTuple{N, Int} : Vector{Int}
    else
        return SectorDict{I, Int}
    end
end

Base.getindex(::SpaceTable, I::Type{<:Sector}) = GradedSpace{I, sectorstoragetype(I)}

A different thing that could be relevant is that in principle we could also reduce the compile time issues for the smaller N values by "blocking" the compiled types, e.g. NTuple{N, Int} for the next value in the set N = 1,2,4,8,16,32 which basically trades some compilation for storage efficiency (see e.g. the packages SmallCollections.jl or similar. I do however think that this probably hints at the Vector{Int} approach being more appropriate anyways.

A final comment is that I think one of the inefficiencies of the implementations e.g. for truncated factorizations is probably that we are always constructing these as SectorDicts, which is a bit wasteful in the case of NTuple storage. There might be additional optimizations in that realm as well.

Comment thread src/factorizations/truncation.jl Outdated
Comment thread src/spaces/gradedspace.jl Outdated
Comment thread src/spaces/gradedspace.jl Outdated
Comment thread src/spaces/gradedspace.jl
Comment thread src/auxiliary/dicts.jl Outdated
Comment thread src/factorizations/truncation.jl
Comment thread src/spaces/gradedspace.jl
Comment thread src/spaces/gradedspace.jl Outdated
@borisdevos

Copy link
Copy Markdown
Member Author

Short summary of the recent commits:

  • addressed the code review on truncate_space non-dual spaces and dim checking a zero contribution
  • blockdims is the thing that iterates as zip(sectors(V), dims(V)) without rehashing
  • Base.mergewith for the sorted merge, but doesn't quite work for the intersect case of infimum
  • sectorstoragetype defined as the name suggests, with a cutoff now above which SectorDict is used even for finite sector types.
  • Truncation code is now also somewhat rewritten to not always work with SectorDicts. I'll be honest, it took me a while to get this working (it's unfamiliar code to me), and the gain is like 2x-3x on my machine, so I don't know if it's worth the maintenance of the code I wrote there. I'm happy to revert this.

Some comments of the review I didn't immediately address, and why:

  • The NTuple graded space constructor and NTuple fuse still allocate a vector, and then convert to an NTuple at the end. I kept this because the gain in this on my machine is significant, namely factor of the order 20-60 within the range of the NTuple cutoff. So I thought that's merit enough to actually not restrict to a tuple there.
  • Julia internals make tuples up till size 32 very efficient, and then there's a notable drop in performance, so the cutoff is put there. This means that product sectors could quickly enter this regime. For that reason I also kept the binary search, as N can get big now for SectorDicts.

There was a suggestion to run some DMRG code with these changes which I haven't done yet. I believe since that will be dominated by LAPACK, the only thing I'd have to look for is no notable regression. So if things remain in the same ballpark (or just compile at all, since that was an issue), then I think things are fine.

Comment thread src/auxiliary/dicts.jl Outdated
Comment thread src/spaces/gradedspace.jl Outdated
Comment thread src/factorizations/truncation.jl Outdated
Comment thread src/spaces/gradedspace.jl Outdated
@Jutho

Jutho commented Sep 11, 2026

Copy link
Copy Markdown
Member

My apologies for the slow review. I find the current code quite difficult to properly review, as I am not entirely convinced of the structure.

I do like the modifications to GradedSpace with a cutoff based on length, which could be even smaller for all I am concerned (e.g. 4 or 8 instead of 32). Also the use of mergewith as a paradigm, and its specialized implementation for SortedVectorDict, is very clever.

However, I am less convinced by the FullVectorDict implementation in auxiliary.jl and the associated sectormap paradigm. As far as I can tell, this seems to be used only to support the truncation. One issue is that I view auxiliary.jl as a place for some very basic auxiliary tools that do not depend on the rest of the package, but are used at various places throughout the package. I.e., it would be code that could be moved to its own package if there were a more general use for it. This seems somewhat orthogonal to the FullVectorDict and sectormap paradigm, which are only used in truncation.jl and which depend strongly on methods from the Sector interface, (even though FullVectorDict looks like a generic AbstractDict subtype, aside from the restriction on the key type parameter K).

I still have to read through truncation.jl to see if there is anything better I could come up with, which is what I will do next. The reason for bringing this up and being difficult is that I want to ensure that the code remains logically structured and therefore easier to maintain.

@Jutho

Jutho commented Sep 11, 2026

Copy link
Copy Markdown
Member

In particular, is all the code of FullVectorDict worth the performance gain instead of just using SectorDict = SortedVectorDict for all sectors when applying sectormap, irrespective of whether the GradedSpace uses NTuple or not?

Comment thread src/factorizations/truncation.jl Outdated
Comment thread src/factorizations/truncation.jl Outdated
Comment thread src/factorizations/truncation.jl Outdated
Comment thread src/factorizations/truncation.jl Outdated
Comment thread src/factorizations/truncation.jl Outdated
Comment thread src/factorizations/truncation.jl Outdated
Comment thread src/factorizations/truncation.jl Outdated
lkdvos and others added 4 commits September 14, 2026 20:46
`FullVectorDict`/`sectormap` existed only to give the truncation code a dense
"sector => index" map for tuple-backed spaces. Their call sites all go through
`pairs(::SectorVector)`, which materialises a `SectorDict` anyway, so the dense
map bought little for a lot of `Sector`-specific machinery in `auxiliary/`.

Every truncation index map is a plain `SectorDict` again; instead
`pairs(::SectorVector)` is built directly from the already-sorted structure
rather than by repeated sorted insertion. The storage-specialised
`truncate_space` methods are kept, since they never used the dense map.

Also from the review:
- `⊖` for dict storage reuses `_sortedmerge`, whose `_keepunmatched` trait is
  generalised into per-side `_unmatched1`/`_unmatched2` hooks
- `_ntuple_storage_threshold` -> `_NTUPLE_STORAGE_THRESHOLD`, lowered to 8
- `ZNSpace{N}` deprecated in favour of `Vect[ZNIrrep{N}]`, which the alias can
  no longer track once `N` exceeds the threshold
- drop the over-strong `I == Trivial` assert in `truncate_space`, and name the
  index variables `ind`/`inds` instead of shadowing the sector type `I`

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`valtype(::SectorVector)` claims a `SubArray`, but `view` of a GPU array is
itself a `CuArray`/`ROCArray`, so pinning the element type to `valtype` broke
every GPU factorization. Take the element type from the views instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- add `TupleGradedSpace{I,N}` / `DictGradedSpace{I}` aliases for the two
  variants `sectorstoragetype` selects between, and use them for dispatch
- `SubtractDims` doubles as the unmatched handler for `⊖`, and is used for the
  tuple variant as well, so both paths share one callable
- `_sortedmerge` takes the unmatched handlers as arguments; `mergewith` selects
  them inline
- `pairs(::SectorVector)` is lazy, like `blocks(::AbstractTensorMap)`: every
  consumer only iterates, and lookups go through the vector itself
- document `sectorstoragetype`, whose docstring interpolates the threshold

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

lkdvos commented Sep 15, 2026

Copy link
Copy Markdown
Member

I think this is ready for another round of review. I've incorporated the code style suggestions and variable names, removed the FullVectorDict and slightly improved the pairs(::SectorVector) iterator to compensate for that. It would probably be good to remeasure performance and re-evaluate after merging this PR, but since there are already quite a few useful changes here I'd vote in favor of merging this first.

Small thing I noted, the type alias ZNSpace{N} is no longer compatible with the split sectorstoragetype based on the sectortype alone, as this will give a wrong result as soon as N > tuple_threshold. I think it's fine to simply deprecate this, the only other thing I can come up with is to change this to ZNSpace(N)(args...), which also looks a bit off to me.

@lkdvos
lkdvos requested a review from Jutho September 15, 2026 15:50
Comment thread docs/src/lib/spaces.md Outdated
Comment thread docs/src/Changelog.md Outdated
Comment thread src/factorizations/factorizations.jl Outdated
Comment thread src/factorizations/truncation.jl Outdated
Comment on lines +44 to +49
newdims = MutableNTuple(ntuple(_ -> 0, StaticLength(N)))
for (c, ind) in pairs(inds)
d = dim(V, c)
n_write = findindex(vals, c)
@inbounds newdims[n_write] = _blocklength(d, ind)
end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do we need this construction via MutableNTuple? How about

Suggested change
newdims = MutableNTuple(ntuple(_ -> 0, StaticLength(N)))
for (c, ind) in pairs(inds)
d = dim(V, c)
n_write = findindex(vals, c)
@inbounds newdims[n_write] = _blocklength(d, ind)
end
newdims = ntuple(N) do n
c = vals[n]
d = V.dims[n]
ind = inds[c]
return _blocklength(d, ind)
end

or thus as onliner

Suggested change
newdims = MutableNTuple(ntuple(_ -> 0, StaticLength(N)))
for (c, ind) in pairs(inds)
d = dim(V, c)
n_write = findindex(vals, c)
@inbounds newdims[n_write] = _blocklength(d, ind)
end
newdims = ntuple(n->_blocklength(V.dims[n], inds[vals[n]]), N)

So we are then not iterating the inds dictionary, but since we anyway know that N is pretty small in this case, I do wonder how severe the key lookups are.

(We are also not needing to do findindex(vals, c) anymore, which might sometimes be a simple calculation, but sometimes also an inefficient lookup).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Good call, although it requires a little more care because not all inds[vals[n]] will work, but I think I can actually work around this because we always have either V.dims[n] == 0 or the key is present

Comment thread src/spaces/gradedspace.jl Outdated
Comment thread src/spaces/gradedspace.jl Outdated

@Jutho Jutho left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I left some smaller final comments, but otherwise approved.

@Jutho

Jutho commented Sep 15, 2026

Copy link
Copy Markdown
Member

I have not actually approved since I hadn't checked wether automerge was on, and wanted to give you time to look at my suggestions. However, feel free to merge after having looked at them.

lkdvos and others added 3 commits September 15, 2026 18:38
Co-authored-by: Jutho <Jutho@users.noreply.github.com>
…inds

Address Jutho's final review round on #511:
- replace `StaticLength(N)` with plain `N` (constant propagation makes
  the old TupleTools artefact unnecessary now that N is already a type
  parameter), and drop the now-dead `TupleTools: StaticLength` imports.
- rewrite `truncate_space(::TupleGradedSpace, inds)` as a pure `ntuple`
  closure instead of mutating a `MutableNTuple`, guarding zero-dimension
  sectors before indexing into `inds` (which only holds keys for sectors
  with nonzero dimension).
- `truncate_space(::DictGradedSpace, inds)` no longer collects and sorts:
  `inds` (whether a `SectorDict` or a `SectorVector`, depending on the
  truncation strategy) already iterates in sorted order by sector.
- drop the now-unused `MutableNTuple`/`findindex` imports in the
  factorizations submodule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`pairs(::SectorVector)` is intentionally lazy (a `Base.Generator`), so
comparing it directly to a `Dict` with `==` silently evaluates to
`false` rather than erroring, since no `==` is defined between those
unrelated iterator types. Collect it before comparing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@lkdvos
lkdvos merged commit f3f660c into main Sep 16, 2026
52 of 65 checks passed
@lkdvos
lkdvos deleted the bd/gradedspace-storage branch September 16, 2026 16:53
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.

4 participants