Skip to content

Conversation

@ogauthe
Copy link
Collaborator

@ogauthe ogauthe commented Dec 11, 2024

This PR defines nsymbol, a function to accede the number of time label s3 appears in the tensor product s1⊗s2. To stay consistent with the interface, the output is a labelled integer.

@ogauthe ogauthe mentioned this pull request Dec 11, 2024
@mtfishman
Copy link
Member

mtfishman commented Dec 11, 2024

Maybe as an alternative, if we had an API for getting the block length associated with a certain sector it would be easy enough to write this function outside of the library.

I.e. we could redefine blocklengths(a::AbstractGradedUnitRange) such that you can write:

r = gradedrange([U1(0) => 2, U1(1) => 3])
blocklengths(r)[U1(1)] == 3

(or I guess it would output labelled(3, U1(1))). That could be done by defining blocklengths so that it outputs an object that wraps the graded unit range, maybe called SectorBlockLengths, such that it acts like the vector of block lengths but you can index into it either with integers or with a symmetry sector (similar to the design of AxisKeys.jl as we have discussed, where we can think about integer positions of the blocks as indices and sectors as the keys).

Then, the logic of not being sure if the sector exists can be handled by the standard Julia API get(blocklengths(r), U1(2), nothing).

That is analogous to how in BlockArrays.jl, blocksizes(a::AbstractBlockArray) outputs a BlockSizes object:

julia> a = BlockedArray(randn(5, 5), [2, 3], [2, 3])
2×2-blocked 5×5 BlockedMatrix{Float64}:
  0.0903129   1.485560.210218   0.36306    0.327547 
 -1.16907    -1.512920.38288    1.82632   -1.43893  
 ──────────────────────┼──────────────────────────────────
 -0.268323   -1.160971.18574    0.258269   0.362909 
 -0.37301     0.90419-0.552486  -0.218437  -0.0195218
  2.22222    -1.60995-0.704768  -0.317308   0.0766319

julia> blocksizes(a)
2×2 BlockArrays.BlockSizes{Tuple{Int64, Int64}, 2, BlockedMatrix{Float64, Matrix{Float64}, Tuple{BlockedOneTo{Int64, Vector{Int64}}, BlockedOneTo{Int64, Vector{Int64}}}}}:
 (2, 2)  (2, 3)
 (3, 2)  (3, 3)

@codecov
Copy link

codecov bot commented Dec 11, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 83.45%. Comparing base (c379b0c) to head (033baba).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main       #4      +/-   ##
==========================================
+ Coverage   83.25%   83.45%   +0.20%     
==========================================
  Files          13       13              
  Lines         412      417       +5     
==========================================
+ Hits          343      348       +5     
  Misses         69       69              
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.

@lkdvos
Copy link
Contributor

lkdvos commented Dec 11, 2024

Would you like me to wait for this PR, or shall I already register a version for convenience?

@mtfishman
Copy link
Member

Is nsymbol a standard name for this functionality?

@mtfishman
Copy link
Member

Would you like me to wait for this PR, or shall I already register a version for convenience?

Go ahead and register the latest main, I want to use it in ITensor/BlockSparseArrays.jl#10.

@lkdvos
Copy link
Contributor

lkdvos commented Dec 11, 2024

I am not sure how standard it is, but it is quite common to write something like this for decomposing tensor products (or category theory decompositions)

$$a \otimes b \rightarrow \bigoplus_c N_{ab}^c c$$

This data is what qualifies the fusion ring of the irreps (or simple objects), ie how these multiply. For example the notation is here, and of course TensorKit has Nsymbol.

On the other hand, the fusion category bible just talks about fusion rings, and doesn't actually name the coefficients.

In any case, I would say that this is definitely a convenient concept to have a shorthand function for, since it is quite often that you need to loop over 1:Nsymbol when dealing with groups with multiplicities. Of course, for abelian groups and even for SU(2), $$N \in 0,1$$, so there it can look like it's not that useful

@mtfishman
Copy link
Member

mtfishman commented Dec 11, 2024

I see, thanks for clarifying. The notation nsymbol confused me, I thought it stood for "number of symbols". I see now that I'm supposed to think about it as an order-3 tensor that you are indexing into with a set of sectors to get the degeneracy. In this case, I think it makes sense to follow TensorKit's approach and use the capitalized version Nsymbol, even though that goes against Julia's standard naming conventions.

I also see that in many cases you can directly get the number and circumvent getting all of the sectors through fusion so it makes sense to have a devoted function for it, but even so using fusion seems like a reasonable fallback definition. But I would still vote for basing this implementation that uses fusion around a more general functionality for getting the block length associated with a sector as I outlined above.

I would almost prefer going "all in" on the tensor analogy, and define Nsymbol as a type and define indexing so that the notation is Nsymbol[s1, s2, s3]. Though maybe that is too fancy. (Maybe what makes more sense is a notation Nsymbol(s1, s2)[s3], i.e. Nsymbol(s1, s2) creates an object representing the the fused sectors and indexing into it gets the length, so going back to my suggestions above it could be defined as Nsymbol(s1, s2) = blocklengths(s1 ⊗ s2).)

@lkdvos
Copy link
Contributor

lkdvos commented Dec 12, 2024

I do quite like the Nsymbol[a, b, c] approach, since it typically really is group data that you are indexing into. The one thing that's slightly annoying is that it's a different array for different groups, so that kind of goes back to it being a function Nsymbol(a, b, c) which dispatches on the types of the arguments. (of course you can achieve this with getindex as well, but it's not obvious to me if that is very "julian").

It's not obvious to me why this should return a labelled integer though, this integer is really the number of times c appears in the decomposition of a ⊗ b, and really is just counting that. In mathematical terms, it's the dimension of the fusion space associated with that tensor product, so it's less obvious to give it a label c or a or b, since it really requires all three. (it's the vertex labels on the fusion tree)

@mtfishman
Copy link
Member

mtfishman commented Dec 12, 2024

Yeah, I'm fine with Nsymbol returning an unlabelled integer, what I think could arguably return a labelled integer is blocklengths(s1 ⊗ s2)[s3], since in our current design the length of a graded unit range also has the sector attached to it. But even there maybe one could argue it should output an unlabelled integer.

@mtfishman
Copy link
Member

I do quite like the Nsymbol[a, b, c] approach, since it typically really is group data that you are indexing into.

Right, I guess following that logic there could be Nsymbol(U1)[1, 1, 2] == Nsymbol(U1(1), U1(1), U1(2)) or something like that.

@ogauthe
Copy link
Collaborator Author

ogauthe commented Dec 12, 2024

We may have a similar function inside GradedUnitRanges, however I think nsymbol has its place in SymmetrySectors, it is a well defined mathematical object (sharing GradedUnitRanges implementation). I hesitated between returning LabelledInteger and Int, both are possible.

I will have a try for a custom SectorBlockLengths, hopefully it will stay compatible with blocklengths(::BlockedUnitRange)

@ogauthe
Copy link
Collaborator Author

ogauthe commented Mar 28, 2025

I would like to merge this in order to simplify FusionTensors before the refactor of GradedUnitRanges. I see two possibilities:

  • the current version, a minimal function nsymbol(s1::AbstractSector, s2::AbstractSector, s3::AbstractSector) -> Int
  • a dedicated struct
struct Nsymbol{D}
  nsymbols::D
  function Nsymbol(s1::AbstractSector, s2::AbstractSector)
    r = to_gradedrange(s1  s2)
    nsymbols_pairs = map(s3 -> (s1, s2, s3), blocklabels(r)) .=> Int.(blocklengths(r))
    nsymbols = Dict(nsymbols_pairs)
    return new{typeof(nsymbols)}(nsymbols)
  end
end

nsymbols(ns::Nsymbol) = ns.nsymbols

function Base.getindex(
  ns::Nsymbol, s1::AbstractSector, s2::AbstractSector, s3::AbstractSector
)
  return get(nsymbols(ns), (s1, s2, s3), 0)
end

s1 = SU2(1)
ns = Nsymbol(s1, s1)
@test ns[s1, s1, s1] == 1

I personally favor the function as it is simpler and avoids introducing a custom struct.

@mtfishman
Copy link
Member

I'm good with keeping it as a function for now.

@mtfishman mtfishman merged commit b17abce into ITensor:main Mar 28, 2025
12 checks passed
@ogauthe ogauthe deleted the nsymbol branch March 28, 2025 20:14
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.

3 participants