# Structures Tiled describes data in one of a fixed group of standard *structure families*. These are *not* Python-specific structures. They can be encoded in standard, language-agnostic formats and transferred from the service to a client in potentially any language. ## Supported structure families The structure families are: * array --- a strided array, like a [numpy](https://numpy.org) array * awkward --- nested, variable-sized data (as implemented by [AwkwardArray](https://awkward-array.org/)) * container --- a of other structures, akin to a dictionary or a directory * sparse --- a sparse array (i.e. an array which is mostly zeros) * table --- tabular data, as in [Apache Arrow](https://arrow.apache.org) or [pandas](https://pandas.pydata.org/) ## How structure is encoded Tiled can describe a structure---its shape, chunking, labels, and so on---for the client so that the client can intelligently request the pieces that it wants. The structures encodings are designed to be as unoriginal as possible, using established standards and, where some invention is required, using established names from numpy, pandas/Arrow, and dask. Some structures are encoded in two parts: ## Examples These examples were generated by serving the demo tree ``` tiled serve pyobject --public tiled.examples.generated:tree ``` making an HTTP request with [httpie](https://httpie.io/) and then extracting the portion of interest with [jq](https://stedolan.github.io/jq/), as shown below. ### Array (single chunk) An array is described with a shape, chunk sizes, and a data type. The parameterization and spelling of the data type follows the [numpy `__array_interface__` protocol](https://numpy.org/doc/stable/reference/arrays.interface.html#object.__array_interface__). Both built-in data types and [structured data types](https://numpy.org/doc/stable/user/basics.rec.html) are supported. An optional field, `dims` ("dimensions") may contain a list with a string label for each dimension. This `(10, 10)`-shaped array fits in a single `(10, 10)`-shaped chunk. ``` $ http :8000/api/v1/metadata/small_image | jq .data.attributes.structure ``` ```json { "chunks": [ [ 100 ], [ 100 ] ], "shape": [ 100, 100 ], "dims": null, "resizable": false, "data_type": { "endianness": "little", "kind": "f", "itemsize": 8 } } ``` ### Array (multiple chunks) This `(10000, 10000)`-shaped array is subdivided into 4 × 4 = 16 chunks, `(2500, 2500)`. Chunks do *not* in general have to be equally-sized, which is why the size of each chunk is given explicitly. ``` $ http :8000/api/v1/metadata/big_image | jq .data.attributes.structure ``` ```json { "chunks": [ [ 2500, 2500, 2500, 2500 ], [ 2500, 2500, 2500, 2500 ] ], "shape": [ 10000, 10000 ], "dims": null, "resizable": false, "data_type": { "endianness": "little", "kind": "f", "itemsize": 8 } } ``` ### Array (with a structured data type) This is a 1D array where each item has internal structure, as in numpy's [strucuted data types](https://numpy.org/doc/stable/user/basics.rec.html) ``` $ http :8000/api/v1/metadata/structured_data/pets | jq .data.attributes.structure ``` ```json { "chunks": [ [ 2 ] ], "shape": [ 2 ], "dims": null, "resizable": false, "data_type": { "itemsize": 48, "fields": [ { "name": "name", "dtype": { "endianness": "little", "kind": "U", "itemsize": 40 }, "shape": null }, { "name": "age", "dtype": { "endianness": "little", "kind": "i", "itemsize": 4 }, "shape": null }, { "name": "weight", "dtype": { "endianness": "little", "kind": "f", "itemsize": 4 }, "shape": null } ] } } ``` ### Awkward [AwkwardArrays](https://awkward-array.org/) express nested, variable-sized data, including arbitrary-length lists, records, mixed types, and missing data. This often comes up in the context of event-based data, such as is used in high-energy physics, neutron experiments, quantum computing, and very high-rate detectors. AwkwardArrays are specified by: * An outer `length` (always an integer) * A JSON `form` (specified by AwkwardArray, giving the internal layout) * Named buffers of bytes, whose names match information in the `form` The first two are included in the structure. ``` $ http :8000/api/v1/metadata/awkward_array | jq .data.attributes.structure ``` ```json { "length": 3, "form": { "class": "ListOffsetArray", "offsets": "i64", "content": { "class": "RecordArray", "fields": [ "x", "y" ], "contents": [ { "class": "NumpyArray", "primitive": "float64", "inner_shape": [], "parameters": {}, "form_key": "node2" }, { "class": "ListOffsetArray", "offsets": "i64", "content": { "class": "NumpyArray", "primitive": "int64", "inner_shape": [], "parameters": {}, "form_key": "node4" }, "parameters": {}, "form_key": "node3" } ], "parameters": {}, "form_key": "node1" }, "parameters": {}, "form_key": "node0" } } ``` ### Sparse Array There are a variety of ways to represent [sparse arrays](https://en.wikipedia.org/wiki/Sparse_matrix). The [Coordinate list (COO)](https://en.wikipedia.org/wiki/Sparse_matrix#Coordinate_list_(COO)) layout consists of writing the coordinate (e.g. row, column, or N-dimensional position) and value of each nonzero element. A N-dimensional COO array with M nonzero elements is described by an NxM array of coordinates and a length-M array of values. Tiled describes this as a chunked array where each chunk contains a table of coordinates and their values. The data types within each table are not described at this level; they are self-described by the individual chunk payloads. The key `layout` is always set to `COO` currently. In the future if other sparse representations are supported, this key will be used to indicate which is used. ```json { "shape": [ 100, 100 ], "chunks": [ [ 100 ], [ 100 ] ], "dims": null, "resizable": false } ``` ### Table With tables, we speak of "partitions" instead of "chunks". There are a couple important distinctions. We always know the size of chunk before we ask for it, but we will not know the number of rows in a partition until we actually read it and enumerate them. Therefore, we cannot slice into table the same way that we can slice in to arrays. We can ask for a subset of the *columns*, and we can fetch partitions one at a time in any order, but we cannot make requests like "rows 100-200". (Dask has the same limitation, for the same reason.) ``` $ http :8000/api/v1/metadata/long_table | jq .data.attributes.structure ``` ```json { "npartitions": 5, "columns": [ "A", "B", "C" ], "resizable": false "arrow_schema": "data:application/vnd.apache.arrow.file;base64,...", } ``` The structure contains a base64-encoded Apache Arrow schema. Apache Arrow is a binary format. It explicitly does not support JSON. (There is a JSON implementation, but the documentation states that it is intended only for integration testing and should not be used by external code.) Therefore, we base64-encode it. ### Container This structure is a container for other structures. It may be compared to a directory, a JSON object, a Python dictionary, or an HDF5 Group. Containers may contain other containers, any other structure, or a mixture. Some may contain a small number of nodes, easy to list in a single request, while others may contain many listed via multiple paginated requests. Some Tiled deployments currently in use have containers with up to hundreds of thousands of nodes. Typically, a container's structure tell us only how many nodes it contains (`count`). The `contents` key is typically set to `null`, which indicates that we will need a separate request to fetch information about each child node. ```json { "contents": null, "count": 2 } ``` In certain cases, it is efficient to in-line all the information about the container's contents (their metadata, structure, and more) in a single response. ```json { "contents": { "lat": { "attributes": { "ancestors": [ "structured_data", "xarray_dataset" ], "metadata": {}, "sorting": null, "specs": [ "xarray_coord" ], "structure": { "chunks": [ [ 2 ], [ 2 ] ], "dims": [ "x", "y" ], "resizable": false, "shape": [ 2, 2 ], "data_type": { "endianness": "little", "itemsize": 8, "kind": "f" } }, "structure_family": "array" }, "id": "lat", "links": { "block": "http://localhost:8000/api/v1/array/block/structured_data/xarray_dataset/lat?block={index_0},{index_1}", "full": "http://localhost:8000/api/v1/array/full/structured_data/xarray_dataset/lat", "self": "http://localhost:8000/api/v1/metadata/structured_data/xarray_dataset/lat" }, "meta": null }, "lon": { "attributes": { "ancestors": [ "structured_data", "xarray_dataset" ], "metadata": {}, "sorting": null, "specs": [ "xarray_coord" ], "structure": { "chunks": [ [ 2 ], [ 2 ] ], "dims": [ "x", "y" ], "resizable": false, "shape": [ 2, 2 ], "data_type": { "endianness": "little", "itemsize": 8, "kind": "f" } }, "structure_family": "array" }, "id": "lon", "links": { "block": "http://localhost:8000/api/v1/array/block/structured_data/xarray_dataset/lon?block={index_0},{index_1}", "full": "http://localhost:8000/api/v1/array/full/structured_data/xarray_dataset/lon", "self": "http://localhost:8000/api/v1/metadata/structured_data/xarray_dataset/lon" }, "meta": null }, "precipitation": { "attributes": { "ancestors": [ "structured_data", "xarray_dataset" ], "metadata": {}, "sorting": null, "specs": [ "xarray_data_var" ], "structure": { "chunks": [ [ 2 ], [ 2 ], [ 3 ] ], "dims": [ "x", "y", "time" ], "resizable": false, "shape": [ 2, 2, 3 ], "data_type": { "endianness": "little", "itemsize": 8, "kind": "f" } }, "structure_family": "array" }, "id": "precipitation", "links": { "block": "http://localhost:8000/api/v1/array/block/structured_data/xarray_dataset/precipitation?block={index_0},{index_1},{index_2}", "full": "http://localhost:8000/api/v1/array/full/structured_data/xarray_dataset/precipitation", "self": "http://localhost:8000/api/v1/metadata/structured_data/xarray_dataset/precipitation" }, "meta": null }, "temperature": { "attributes": { "ancestors": [ "structured_data", "xarray_dataset" ], "metadata": {}, "sorting": null, "specs": [ "xarray_data_var" ], "structure": { "chunks": [ [ 2 ], [ 2 ], [ 3 ] ], "dims": [ "x", "y", "time" ], "resizable": false, "shape": [ 2, 2, 3 ], "data_type": { "endianness": "little", "itemsize": 8, "kind": "f" } }, "structure_family": "array" }, "id": "temperature", "links": { "block": "http://localhost:8000/api/v1/array/block/structured_data/xarray_dataset/temperature?block={index_0},{index_1},{index_2}", "full": "http://localhost:8000/api/v1/array/full/structured_data/xarray_dataset/temperature", "self": "http://localhost:8000/api/v1/metadata/structured_data/xarray_dataset/temperature" }, "meta": null }, "time": { "attributes": { "ancestors": [ "structured_data", "xarray_dataset" ], "metadata": {}, "sorting": null, "specs": [ "xarray_coord" ], "structure": { "chunks": [ [ 3 ] ], "dims": [ "time" ], "resizable": false, "shape": [ 3 ], "data_type": { "endianness": "little", "itemsize": 8, "kind": "M" } }, "structure_family": "array" }, "id": "time", "links": { "block": "http://localhost:8000/api/v1/array/block/structured_data/xarray_dataset/time?block={index_0}", "full": "http://localhost:8000/api/v1/array/full/structured_data/xarray_dataset/time", "self": "http://localhost:8000/api/v1/metadata/structured_data/xarray_dataset/time" }, "meta": null } }, "count": 5 } ```