-
Notifications
You must be signed in to change notification settings - Fork 55
Show improvements + subblocks iterator
#304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8ca49b8
a6e091a
f48e980
a2b595f
9710451
f121193
070dab2
53f0b61
fe20385
1586be7
5fcd778
a7a6af4
cdd6649
077de13
4bbdb93
1c156fe
53a5dc9
e1ef363
9266ed9
0285d36
4c77cb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -250,6 +250,12 @@ Return an iterator over all splitting - fusion tree pairs of a tensor. | |
| """ | ||
| fusiontrees(t::AbstractTensorMap) = fusionblockstructure(t).fusiontreelist | ||
|
|
||
| fusiontreetype(t::AbstractTensorMap) = fusiontreetype(typeof(t)) | ||
| function fusiontreetype(::Type{T}) where {T <: AbstractTensorMap} | ||
| I = sectortype(T) | ||
| return Tuple{fusiontreetype(I, numout(T)), fusiontreetype(I, numin(T))} | ||
| end | ||
|
|
||
| # auxiliary function | ||
| @inline function trivial_fusiontree(t::AbstractTensorMap) | ||
| sectortype(t) === Trivial || | ||
|
|
@@ -295,6 +301,145 @@ function blocktype(::Type{T}) where {T <: AbstractTensorMap} | |
| return Core.Compiler.return_type(block, Tuple{T, sectortype(T)}) | ||
| end | ||
|
|
||
| # tensor data: subblock access | ||
| # ---------------------------- | ||
| @doc """ | ||
| subblocks(t::AbstractTensorMap) | ||
|
|
||
| Return an iterator over all subblocks of a tensor, i.e. all fusiontrees and their | ||
| corresponding tensor subblocks. | ||
|
|
||
| See also [`subblock`](@ref), [`fusiontrees`](@ref), and [`hassubblock`](@ref). | ||
| """ | ||
| subblocks(t::AbstractTensorMap) = SubblockIterator(t, fusiontrees(t)) | ||
|
|
||
| const _doc_subblock = """ | ||
| Return a view into the data of `t` corresponding to the splitting - fusion tree pair | ||
| `(f₁, f₂)`. In particular, this is an `AbstractArray{T}` with `T = scalartype(t)`, of size | ||
| `(dims(codomain(t), f₁.uncoupled)..., dims(codomain(t), f₂.uncoupled)...)`. | ||
|
|
||
| Whenever `FusionStyle(sectortype(t)) isa UniqueFusion` , it is also possible to provide only | ||
| the external `sectors`, in which case the fusion tree pair will be constructed automatically. | ||
| """ | ||
|
|
||
| @doc """ | ||
| subblock(t::AbstractTensorMap, (f₁, f₂)::Tuple{FusionTree,FusionTree}) | ||
| subblock(t::AbstractTensorMap, sectors::Tuple{Vararg{Sector}}) | ||
|
|
||
| $_doc_subblock | ||
|
|
||
| In general, new tensor types should provide an implementation of this function for the | ||
| fusion tree signature. | ||
|
|
||
| See also [`subblocks`](@ref) and [`fusiontrees`](@ref). | ||
| """ subblock | ||
|
|
||
| Base.@propagate_inbounds function subblock(t::AbstractTensorMap, sectors::Tuple{I, Vararg{I}}) where {I <: Sector} | ||
| # input checking | ||
| I === sectortype(t) || throw(SectorMismatch("Not a valid sectortype for this tensor.")) | ||
| FusionStyle(I) isa UniqueFusion || | ||
| throw(SectorMismatch("Indexing with sectors is only possible for unique fusion styles.")) | ||
| length(sectors) == numind(t) || throw(ArgumentError("invalid number of sectors")) | ||
|
|
||
| # convert to fusiontrees | ||
| s₁ = TupleTools.getindices(sectors, codomainind(t)) | ||
| s₂ = map(dual, TupleTools.getindices(sectors, domainind(t))) | ||
| c1 = length(s₁) == 0 ? unit(I) : (length(s₁) == 1 ? s₁[1] : first(⊗(s₁...))) | ||
| @boundscheck begin | ||
| hassector(codomain(t), s₁) && hassector(domain(t), s₂) || throw(BoundsError(t, sectors)) | ||
| c2 = length(s₂) == 0 ? unit(I) : (length(s₂) == 1 ? s₂[1] : first(⊗(s₂...))) | ||
| c2 == c1 || throw(SectorMismatch("Not a valid fusion channel for this tensor")) | ||
| end | ||
| f₁ = FusionTree(s₁, c1, map(isdual, tuple(codomain(t)...))) | ||
| f₂ = FusionTree(s₂, c1, map(isdual, tuple(domain(t)...))) | ||
| return @inbounds subblock(t, (f₁, f₂)) | ||
| end | ||
| Base.@propagate_inbounds function subblock(t::AbstractTensorMap, sectors::Tuple) | ||
| return subblock(t, map(Base.Fix1(convert, sectortype(t)), sectors)) | ||
| end | ||
| # attempt to provide better error messages | ||
| function subblock(t::AbstractTensorMap, (f₁, f₂)::Tuple{FusionTree, FusionTree}) | ||
| (sectortype(t)) == sectortype(f₁) == sectortype(f₂) || | ||
| throw(SectorMismatch("Not a valid sectortype for this tensor.")) | ||
| numout(t) == length(f₁) && numin(t) == length(f₂) || | ||
| throw(DimensionMismatch("Invalid number of fusiontree legs for this tensor.")) | ||
| throw(MethodError(subblock, (t, (f₁, f₂)))) | ||
| end | ||
|
|
||
| @doc """ | ||
| subblocktype(t) | ||
| subblocktype(::Type{T}) | ||
|
|
||
| Return the type of the tensor subblocks of a tensor. | ||
| """ subblocktype | ||
|
|
||
| function subblocktype(::Type{T}) where {T <: AbstractTensorMap} | ||
| return Core.Compiler.return_type(subblock, Tuple{T, fusiontreetype(T)}) | ||
| end | ||
| subblocktype(t) = subblocktype(typeof(t)) | ||
| subblocktype(T::Type) = throw(MethodError(subblocktype, (T,))) | ||
|
|
||
| # Indexing behavior | ||
| # ----------------- | ||
| @doc """ | ||
| Base.view(t::AbstractTensorMap, sectors::Tuple{Vararg{Sector}}) | ||
| Base.view(t::AbstractTensorMap, f₁::FusionTree, f₂::FusionTree) | ||
|
|
||
| $_doc_subblock | ||
|
|
||
| !!! note | ||
| Contrary to Julia's array types, the default indexing behavior is to return a view | ||
| into the tensor data. As a result, `getindex` and `view` have the same behavior. | ||
|
|
||
| See also [`subblock`](@ref), [`subblocks`](@ref) and [`fusiontrees`](@ref). | ||
| """ Base.view(::AbstractTensorMap, ::Tuple{I, Vararg{I}}) where {I <: Sector}, | ||
| Base.view(::AbstractTensorMap, ::FusionTree, ::FusionTree) | ||
|
|
||
| @inline Base.view(t::AbstractTensorMap, sectors::Tuple{I, Vararg{I}}) where {I <: Sector} = | ||
| subblock(t, sectors) | ||
| @inline Base.view(t::AbstractTensorMap, f₁::FusionTree, f₂::FusionTree) = | ||
| subblock(t, (f₁, f₂)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think it is useful to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if I would redesign everything right now, I would have gone with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. First an aside, your remark about the "method piracy" on For In fact, I believe returning a view is necessary if we want some syntax like So while I don't think our However, also recycling It seems There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think most of the complaints surrounding |
||
|
|
||
| # by default getindex returns views | ||
| @doc """ | ||
| Base.getindex(t::AbstractTensorMap, sectors::Tuple{Vararg{Sector}}) | ||
| t[sectors] | ||
| Base.getindex(t::AbstractTensorMap, f₁::FusionTree, f₂::FusionTree) | ||
| t[f₁, f₂] | ||
|
|
||
| $_doc_subblock | ||
|
|
||
| !!! warning | ||
| Contrary to Julia's array types, the default behavior is to return a view into the tensor data. | ||
| As a result, modifying the view will modify the data in the tensor. | ||
|
|
||
| See also [`subblock`](@ref), [`subblocks`](@ref) and [`fusiontrees`](@ref). | ||
| """ Base.getindex(::AbstractTensorMap, ::Tuple{I, Vararg{I}}) where {I <: Sector}, | ||
| Base.getindex(::AbstractTensorMap, ::FusionTree, ::FusionTree) | ||
|
|
||
| @inline Base.getindex(t::AbstractTensorMap, sectors::Tuple{I, Vararg{I}}) where {I <: Sector} = | ||
| view(t, sectors) | ||
| @inline Base.getindex(t::AbstractTensorMap, f₁::FusionTree, f₂::FusionTree) = | ||
| view(t, f₁, f₂) | ||
|
|
||
| @doc """ | ||
| Base.setindex!(t::AbstractTensorMap, v, sectors::Tuple{Vararg{Sector}}) | ||
| t[sectors] = v | ||
| Base.setindex!(t::AbstractTensorMap, v, f₁::FusionTree, f₂::FusionTree) | ||
| t[f₁, f₂] = v | ||
|
|
||
| Copies `v` into the data slice of `t` corresponding to the splitting - fusion tree pair `(f₁, f₂)`. | ||
| By default, `v` can be any object that can be copied into the view associated with `t[f₁, f₂]`. | ||
|
|
||
| See also [`subblock`](@ref), [`subblocks`](@ref) and [`fusiontrees`](@ref). | ||
| """ Base.setindex!(::AbstractTensorMap, ::Any, ::Tuple{I, Vararg{I}}) where {I <: Sector}, | ||
| Base.setindex!(::AbstractTensorMap, ::Any, ::FusionTree, ::FusionTree) | ||
|
|
||
| @inline Base.setindex!(t::AbstractTensorMap, v, sectors::Tuple{I, Vararg{I}}) where {I <: Sector} = | ||
| copy!(view(t, sectors), v) | ||
| @inline Base.setindex!(t::AbstractTensorMap, v, f₁::FusionTree, f₂::FusionTree) = | ||
| copy!(view(t, (f₁, f₂)), v) | ||
|
|
||
| # Derived indexing behavior for tensors with trivial symmetry | ||
| #------------------------------------------------------------- | ||
| using TensorKit.Strided: SliceIndex | ||
|
|
@@ -480,6 +625,10 @@ end | |
|
|
||
| # Conversion to Array: | ||
| #---------------------- | ||
| Base.ndims(t::AbstractTensorMap) = numind(t) | ||
| Base.size(t::AbstractTensorMap) = ntuple(Base.Fix1(size, t), numind(t)) | ||
| Base.size(t::AbstractTensorMap, i) = dim(space(t, i)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am still not convinced about the usefulness of this. This is mostly aimed at making it more friendly for newcomers? But shouldn't we stimulate them to just learn from the start that One reason why I think this might not be a good idea is that this forces There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I definitely agree with the sentiment to steer people towards our interface, but I don't think we currently have one for this exact concept, and I have to say that I am not sure I agree that your reason is really related to this. I don't think Ultimately, it's mostly just about being pragmatic. It's useful functionality, it's a short, easily available function name that is easy to remember and it's clear what it does, and I see very little harm in overloading it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have had a couple people mention that they found it surprising that it doesn't work (technically, I quote: "this is why no one is using TensorKit") There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I especially look forward to the warning on the first call: julia >size(::TensorMap)
WARNING: a TensorMap is not an AbstractArray and has a different interface; expect non-integer dimensions and other unexpected behavior. RTFM!There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Jokes aside, I would actually be quite happy with a |
||
|
|
||
| # probably not optimized for speed, only for checking purposes | ||
| function Base.convert(::Type{Array}, t::AbstractTensorMap) | ||
| I = sectortype(t) | ||
|
|
@@ -499,3 +648,38 @@ function Base.convert(::Type{Array}, t::AbstractTensorMap) | |
| return A | ||
| end | ||
| end | ||
|
|
||
| # Show and friends | ||
| # ---------------- | ||
|
|
||
| function Base.dims2string(V::HomSpace) | ||
| str_cod = numout(V) == 0 ? "()" : join(dim.(codomain(V)), '×') | ||
| str_dom = numin(V) == 0 ? "()" : join(dim.(domain(V)), '×') | ||
| return str_cod * "←" * str_dom | ||
| end | ||
|
|
||
| function Base.summary(io::IO, t::AbstractTensorMap) | ||
| V = space(t) | ||
| print(io, Base.dims2string(V), " ") | ||
| Base.showarg(io, t, true) | ||
| return nothing | ||
| end | ||
|
|
||
| # Human-readable: | ||
| function Base.show(io::IO, ::MIME"text/plain", t::AbstractTensorMap) | ||
| # 1) show summary: typically d₁×d₂×… ← d₃×d₄×… $(typeof(t)): | ||
| summary(io, t) | ||
| println(io, ":") | ||
|
|
||
| # 2) show spaces | ||
| # println(io, " space(t):") | ||
| println(io, " codomain: ", codomain(t)) | ||
| println(io, " domain: ", domain(t)) | ||
|
|
||
| # 3) [optional]: show data | ||
| get(io, :compact, true) && return nothing | ||
| ioc = IOContext(io, :typeinfo => sectortype(t)) | ||
| println(io, "\n\n blocks(t):") | ||
| show_blocks(io, MIME"text/plain"(), blocks(t)) | ||
| return nothing | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think this
d-elementprinting style is not really appropriate. When is this method called; is this in notebooks?How about some printing style for an object like
V = SU2Space(0=>3, 1=>2, 2=>1):"Rep[SU₂] object with dimension 14 (reduced dimension 6)"
or for V'
"Rep[SU₂] dual object with dimension 14 (reduced dimension 6)"
where also the extra information in parentheses can be omitted if
dim(V) == reduceddim(V).Just a suggestion, let me know what you think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing another deep dive into the implementations and documentation, this is what I can find:
The different show methods are mostly used like this:
show(io, MIME"text/plain"(), x)is what gets called mostly in interactive usage, i.e. it ends up being the implementation when youdisplaysomething to an output that can support text. (I.e. REPL, or notebook output, ...). This is supposed to be for human consumptionshow(io, MIME"text/plain"(), x)for elementsx, with:compact => trueset. In this case you should avoid\nto not break the printing of the container.:limit => truecan be set to print…in place of most elements in a container.show(io, x)is the default implementation/fallback, which is typically used for computer consumption (i.e. parseable)For example
Arrayhandles this for the 3-arg version by writing a summary first, and then, on new line(s) prints the data. Ifcompact => true, the summary is omitted and the[x; y;; ...]single-line notation is used to show the data.So, what I would like is:
:compact => trueshould not contain any\n. We can decide to either showVect[I](x => d, ...)or show the summary, depending on what we think is the most useful information to show at a glance in a container of spaces. This is the thing that would get called e.g. when showing the spaces of an MPS, which you could think of as a container spaceI don't actually feel to strongly about how this information is worded exactly, but I do think showing the dimension somewhere is a strict improvement, for example in MPS or PEPS methods, the relevant spaces to inspect are the virtual spaces, which tend to become completely unreadable when there are many sectors. In this case, simply truncating the sectors and going for the
…reduces the screen clutter, but then there is no more information on the screen.I have no real preference about the dim vs reduced dim, I argue that the information is mostly in comparing different values anyways (I want to see if my dimension increased or decreased during a DMRG run), but given that we are typically not using
reduceddimin many places I feel like to not make this number confusing we would have to be a little explicit about it."Rep[SU₂] dual object with dimension 14 (reduced dimension 6)"seems a bit long, I think one of the dimensions is sufficient? I would also prefer to avoid the categoric "object" language when this isn't necessary, so maybe to suggest either alternative:"(dual) Rep[SU₂] of dim d""(dual) Rep[SU₂] of reduceddim d"This gives all information, would be
Floatdim-friendly since the floating point number is at the end, avoids (more) use of category language and is reasonably short.dimandreduceddimare idiomatic since we are already using that as a function name, so it cannot be confusing what this number is referring to.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of 'object' was supposed to be such that only category-minded people (like you) think about categorical objects 😄 , and other people think of a computer-science object of type
Rep[SU₂]. Though the possible occurance ofdualthen spoils that a little bit. I just think the one guideline should be not to takeArrayas an example, as really spaces are supposed to be like small bits types (even ifGradedSpaceisn't really. Furthermore,Arraystill prints out all elements even with:compact => true, so by that guideline we can still print all the sectors. But I agree that this is not what you want to see when an MPS is printed. However, I do think it is strange ifℂ^4is printed just as this, andRep[SU₂](...)is printed asdual Rep[SU₂] of dim d. I kind of dislike the implicit implication that it is an instance and not a type.How about the following compromise: With
:compact => true, aV::GradedSpaceis printed astype_repr(typeof(V)) * "(…)" * (isdual(V) ? "'" : "") * " with dim $(dim(V))"Depending on the length of the information that you want to show, you can add additional information such as
" with $(length(sectors(V)) sectors and dim $(dim(V))"but this is really up to you.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still confused by the
showmethods. If you just have an interactive REPL session, it goes viadisplay(V)and ends up callingshow(stdout, MIME"text/plain", V). Is that than also the version that should check for:limit? So we might have:compact=>falsebut:limit=>true, so we would have a second line printing the sectors? In fact, forshow(stdout, MIME"text/plain", V), I wouldn't mind if the sectors are printed underneath each other as in your original design.The version without MIME type,
show(io, V), is only called if you call explicitlyshow(V). That one, even forArray, prints out all information and is supposed to be parseable. So can that just remain the current version that prints out all the sectors? ForArray, the only difference between:compacttrue or false in that case is that it truncates floating point numbers to less accuracy, but that doesn't really make sense for sector dimensions.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wait,
show(io, V)does also listen to:limit. Pff, this is annoying.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think (this is my interpretation, it's all very badly defined/documented) that
:compactmeans no linebreaks and optionally more dense representations (less floating point digits, orx=>yinstead ofx => y), and:limitmeans respecting thedisplaysize.The version without MIME type is only called if you explicitly
show(V)orprint(io, V), where I think it is supposed to be parseable but can still be:limitted if you like