11# Backend-agnostic serialization layer for NDTensors storage types.
22#
3- # Pairs each storage type with its [`SerializedX`](@ref) schema struct via
4- # [`serialized_type`](@ref), and defines `Base.convert` overloads to do the value-level
5- # transform in both directions. Serialization backends (today only `NDTensorsJLD2Ext`)
6- # layer on top: they map their own writeas mechanism to `serialized_type`, and the
7- # `Base.convert` overloads here handle the actual byte-shape transform.
3+ # Each section below defines, for one in-memory storage type:
4+ # 1. The [`SerializedX`](@ref) schema struct (the on-disk layout).
5+ # 2. The [`serialized_type`](@ref) declaration mapping the in-memory type to the
6+ # schema struct, used by serialization backends to bridge into their own writeas /
7+ # type-mapping mechanism.
8+ # 3. `Base.convert` overloads for the value-level transform in both directions.
9+ #
10+ # JLD2's default `wconvert` / `rconvert` already delegate to `Base.convert`, so backends
11+ # (today only `NDTensorsJLD2Ext`) only need to register a `JLD2.writeas` declaration
12+ # pointing at `serialized_type` — no JLD2-specific value-conversion code is required.
13+ #
14+ # Integer-width conventions for cross-language readability:
15+ # * `version::UInt32` matches `Base.VersionNumber`'s field width.
16+ # * Block-sparse layouts use `block_indices::Matrix{Int64}` shaped `(ndims, num_blocks)`,
17+ # following the COO sparse-tensor convention used by Apache Arrow, PyData Sparse, and
18+ # PyTorch's sparse format. The tensor rank is implicit in `size(block_indices, 1)`
19+ # and is preserved even when `num_blocks == 0` because HDF5 stores both matrix
20+ # dimensions.
821
922"""
1023 NDTensors.serialized_type(::Type{T}) -> Type
@@ -19,21 +32,7 @@ NDTensors.serialized_type(Dense{Float64}) # => NDTensors.SerializedDense{Float6
1932"""
2033function serialized_type end
2134
22- serialized_type (:: Type{<:Dense{T}} ) where {T} = SerializedDense{T}
23- serialized_type (:: Type{<:BlockSparse{T}} ) where {T} = SerializedBlockSparse{T}
24- serialized_type (:: Type{<:NonuniformDiag{T}} ) where {T} = SerializedDiag{T}
25- serialized_type (:: Type{<:UniformDiag{T}} ) where {T} = SerializedUniformDiag{T}
26- function serialized_type (:: Type{<:NonuniformDiagBlockSparse{T}} ) where {T}
27- return SerializedDiagBlockSparse{T}
28- end
29- function serialized_type (:: Type{<:UniformDiagBlockSparse{T}} ) where {T}
30- return SerializedUniformDiagBlockSparse{T}
31- end
32- serialized_type (:: Type{<:EmptyStorage{T}} ) where {T} = SerializedEmptyStorage{T}
33-
34- # Shared block-offset (de)serialization helpers, used here for BlockSparse and
35- # DiagBlockSparse. Block positions are written as the columns of a
36- # `(ndims, num_blocks)` `Matrix{Int64}` (COO sparse-tensor convention).
35+ # Shared block-offset (de)serialization helpers, used by `BlockSparse` and `DiagBlockSparse`.
3736
3837function _serialize_blockoffsets (storage, :: Val{N} ) where {N}
3938 boffs = blockoffsets (storage)
@@ -56,7 +55,24 @@ function _deserialize_blockoffsets(s, ::Val{N}) where {N}
5655 return boffs
5756end
5857
59- # Dense
58+ # --- Dense ---
59+
60+ """
61+ SerializedDense{T}
62+
63+ On-disk schema for `Dense{T}` storage. Version 1.
64+
65+ Fields:
66+
67+ - `version::UInt32`
68+ - `data::Vector{T}`
69+ """
70+ struct SerializedDense{T}
71+ version:: UInt32
72+ data:: Vector{T}
73+ end
74+
75+ serialized_type (:: Type{<:Dense{T}} ) where {T} = SerializedDense{T}
6076
6177function Base. convert (:: Type{SerializedDense{T}} , d:: Dense{T} ) where {T}
6278 return SerializedDense {T} (UInt32 (1 ), convert (Vector{T}, data (d)))
@@ -68,7 +84,30 @@ function Base.convert(::Type{S}, s::SerializedDense) where {T, S <: Dense{T}}
6884 return S (s. data)
6985end
7086
71- # BlockSparse
87+ # --- BlockSparse ---
88+
89+ """
90+ SerializedBlockSparse{T}
91+
92+ On-disk schema for `BlockSparse{T}` storage. Version 1.
93+
94+ Fields:
95+
96+ - `version::UInt32`
97+ - `data::Vector{T}` — flat element buffer.
98+ - `block_indices::Matrix{Int64}` — shape `(ndims, num_blocks)`, each column a block
99+ position (COO convention; tensor rank is `size(block_indices, 1)`).
100+ - `block_offsets::Vector{Int64}` — length `num_blocks`, the offset into `data` for each
101+ block.
102+ """
103+ struct SerializedBlockSparse{T}
104+ version:: UInt32
105+ data:: Vector{T}
106+ block_indices:: Matrix{Int64}
107+ block_offsets:: Vector{Int64}
108+ end
109+
110+ serialized_type (:: Type{<:BlockSparse{T}} ) where {T} = SerializedBlockSparse{T}
72111
73112function Base. convert (
74113 :: Type{SerializedBlockSparse{T}} , bs:: BlockSparse{T, <:Any, N}
@@ -87,7 +126,24 @@ function Base.convert(::Type{S}, s::SerializedBlockSparse) where {T, S <: BlockS
87126 return BlockSparse (s. data, _deserialize_blockoffsets (s, Val (N))):: S
88127end
89128
90- # Diag (nonuniform)
129+ # --- Diag (nonuniform) ---
130+
131+ """
132+ SerializedDiag{T}
133+
134+ On-disk schema for non-uniform `Diag{T}` storage. Version 1.
135+
136+ Fields:
137+
138+ - `version::UInt32`
139+ - `data::Vector{T}` — the diagonal entries.
140+ """
141+ struct SerializedDiag{T}
142+ version:: UInt32
143+ data:: Vector{T}
144+ end
145+
146+ serialized_type (:: Type{<:NonuniformDiag{T}} ) where {T} = SerializedDiag{T}
91147
92148function Base. convert (:: Type{SerializedDiag{T}} , d:: NonuniformDiag{T} ) where {T}
93149 return SerializedDiag {T} (UInt32 (1 ), convert (Vector{T}, data (d)))
@@ -100,7 +156,24 @@ function Base.convert(::Type{S}, s::SerializedDiag) where {T, S <: NonuniformDia
100156 return Diag {T} (s. data):: S
101157end
102158
103- # Diag (uniform)
159+ # --- Diag (uniform) ---
160+
161+ """
162+ SerializedUniformDiag{T}
163+
164+ On-disk schema for uniform `Diag{T}` storage (all diagonal entries equal). Version 1.
165+
166+ Fields:
167+
168+ - `version::UInt32`
169+ - `value::T` — the shared diagonal value.
170+ """
171+ struct SerializedUniformDiag{T}
172+ version:: UInt32
173+ value:: T
174+ end
175+
176+ serialized_type (:: Type{<:UniformDiag{T}} ) where {T} = SerializedUniformDiag{T}
104177
105178function Base. convert (:: Type{SerializedUniformDiag{T}} , d:: UniformDiag{T} ) where {T}
106179 return SerializedUniformDiag {T} (UInt32 (1 ), convert (T, data (d)))
@@ -113,7 +186,31 @@ function Base.convert(::Type{S}, s::SerializedUniformDiag) where {T, S <: Unifor
113186 return Diag (s. value):: S
114187end
115188
116- # DiagBlockSparse (nonuniform)
189+ # --- DiagBlockSparse (nonuniform) ---
190+
191+ """
192+ SerializedDiagBlockSparse{T}
193+
194+ On-disk schema for non-uniform `DiagBlockSparse{T}` storage. Version 1. Layout matches
195+ [`SerializedBlockSparse`](@ref).
196+
197+ Fields:
198+
199+ - `version::UInt32`
200+ - `data::Vector{T}`
201+ - `block_indices::Matrix{Int64}` shape `(ndims, num_blocks)`
202+ - `block_offsets::Vector{Int64}` length `num_blocks`
203+ """
204+ struct SerializedDiagBlockSparse{T}
205+ version:: UInt32
206+ data:: Vector{T}
207+ block_indices:: Matrix{Int64}
208+ block_offsets:: Vector{Int64}
209+ end
210+
211+ function serialized_type (:: Type{<:NonuniformDiagBlockSparse{T}} ) where {T}
212+ return SerializedDiagBlockSparse{T}
213+ end
117214
118215function Base. convert (
119216 :: Type{SerializedDiagBlockSparse{T}} ,
@@ -135,7 +232,30 @@ function Base.convert(
135232 return DiagBlockSparse (s. data, _deserialize_blockoffsets (s, Val (N))):: S
136233end
137234
138- # DiagBlockSparse (uniform)
235+ # --- DiagBlockSparse (uniform) ---
236+
237+ """
238+ SerializedUniformDiagBlockSparse{T}
239+
240+ On-disk schema for uniform `DiagBlockSparse{T}` storage. Version 1.
241+
242+ Fields:
243+
244+ - `version::UInt32`
245+ - `value::T` — the shared diagonal value.
246+ - `block_indices::Matrix{Int64}` shape `(ndims, num_blocks)`
247+ - `block_offsets::Vector{Int64}` length `num_blocks`
248+ """
249+ struct SerializedUniformDiagBlockSparse{T}
250+ version:: UInt32
251+ value:: T
252+ block_indices:: Matrix{Int64}
253+ block_offsets:: Vector{Int64}
254+ end
255+
256+ function serialized_type (:: Type{<:UniformDiagBlockSparse{T}} ) where {T}
257+ return SerializedUniformDiagBlockSparse{T}
258+ end
139259
140260function Base. convert (
141261 :: Type{SerializedUniformDiagBlockSparse{T}} ,
@@ -157,14 +277,26 @@ function Base.convert(
157277 return DiagBlockSparse (s. value, _deserialize_blockoffsets (s, Val (N))):: S
158278end
159279
160- # EmptyStorage
280+ # --- EmptyStorage ---
161281
162- function Base. convert (
163- :: Type{SerializedEmptyStorage{T}} , :: EmptyStorage{T}
164- ) where {T}
165- return SerializedEmptyStorage {T} (UInt32 (1 ))
282+ """
283+ SerializedEmptyStorage{T}
284+
285+ On-disk schema for `EmptyStorage{T}`. Version 1. The element type `T` is carried in the
286+ parametric type name on disk, so no data field is needed.
287+
288+ Fields:
289+
290+ - `version::UInt32`
291+ """
292+ struct SerializedEmptyStorage{T}
293+ version:: UInt32
166294end
167295
168- function Base. convert (:: Type{S} , :: SerializedEmptyStorage ) where {T, S <: EmptyStorage{T} }
169- return S ()
296+ serialized_type (:: Type{<:EmptyStorage{T}} ) where {T} = SerializedEmptyStorage{T}
297+
298+ function Base. convert (:: Type{SerializedEmptyStorage{T}} , :: EmptyStorage{T} ) where {T}
299+ return SerializedEmptyStorage {T} (UInt32 (1 ))
170300end
301+
302+ Base. convert (:: Type{S} , :: SerializedEmptyStorage ) where {T, S <: EmptyStorage{T} } = S ()
0 commit comments