Skip to content

Commit d2c6f31

Browse files
authored
Merge pull request #12 from metno/9-update-internal-format-docs
9 update internal format docs
2 parents 533bfd3 + da4aa03 commit d2c6f31

11 files changed

Lines changed: 113 additions & 73 deletions

File tree

docs/internal_format.md

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,82 @@
11
# Forti's internal data format
22

3-
Forti uses an internal data format for storing data. This format is used both in the blob storage and by `rawdataforecaster`. It also dictates parts of the contents of the grpc protocol.
4-
Each area/version consists of one or several separate forecasts grouped by their grid resolution.
3+
This document is intended for developers who need to read or write Forti’s internal data format directly.
54

6-
## Data for each area/version
5+
Forti uses an internal data format for storing data. This format is used both in the blob storage and by `rawdataforecaster`.
76

8-
Each area/version has one or several grids (strictly speaking, it is not a grid, but a collection of latitude/longitude coordinates within a particular area). Each grid is supposed to have the same geographic extent, but differs in what resolution the grid points in each grid has.
7+
## Overview
98

10-
The data for each grid resolution is described in a uniquely-named subfolder under `/<area>/<version>/` in the blob storage. The data for each resolution is described below.
9+
Data is split into areas and versions.
10+
An area refers to to a limited geographic area, typically the domain of a single forecast model.
11+
Version numbers are used to identify which forecast for an area is the newest.
12+
Highest version number is newest.
1113

12-
Each combination of `area` and `version` has accompanying metadata. These data are described in `DatasetMeta` in [the code](../upload/pkg/fortiblob/collector.go). On the blob store, these data are stored in a file called `complete.json`.
14+
A single rawdataforecaster instance can serve data from several areas, but only one area for a single request.
15+
This is done by selecting the area with a grid point which is closest to the requested location.
16+
It will only ever serve the latest version for each area, as determined by that area's version number.
1317

18+
The data in the single area is expressed as one or more lists of latitude/longitude values with accompanying data for several parameters.
19+
If there are more than one list of latitudes and longitudes, they are expected to cover the same area, but with different values for latitude and longitude.
20+
This allows some parameters to have a different resolution than others, even if the cover the same area.
1421

15-
## Format for each grid resolution
22+
All data for each area/version is placed in a uniquely-named subfolder under `/<area>/<version>/` in the blob storage.
23+
The structure of this is described below.
1624

17-
The format consists of four pieces of data:
1825

19-
* forecast values
20-
* metadata
21-
* latitudes
22-
* longitudes
26+
## Object store layout
2327

24-
### Forecast values
28+
Under a single area/version in the blob storage, the following layout is expected:
2529

26-
The forecast values are the actual values for the forecast. The data is organized so that all values for each location is stored together, one location after the other. Each value in the data is merely a little-endian encoded `int16`, and the meaning of each one is defined in the metadata.
30+
* complete.json
31+
* sub-folders, containing the following objects:
32+
* meta.json
33+
* data
34+
* longitude
35+
* latitude
2736

28-
On the blob store, these data are stored in a file called `data`.
37+
### complete.json
2938

30-
### Metadata
39+
This contains metadata about the area/version itself.
40+
Its format is described in `DatasetMeta` in [the code](../fortiup/pkg/fortiblob/collector.go).
3141

32-
The metadata describes the meaning of the forecast values. The data structure is described in `MetaCollection` in [the code](../upload/pkg/fortiblob/collector.go). There is also [an example](../upload/pkg/fortiblob/collector_test.go) available for how to interpret raw data.
42+
When uploading data to the object store, this is supposed to be the last file uploaded, as its existence will trigger an update on `rawdataforecaster`.
3343

34-
In the blob store, this is json-encoded in a file called `meta.json`.
44+
### Sub-folders
3545

36-
### Latitudes and longitudes
46+
Different data for the same geographic area can have different resolutions.
47+
This will be expressed as different values for longitude and latitudes.
48+
For each of these resolutions, a subfolder is made.
3749

38-
In the blob store, latitudes and longitudes exist in two separate files. These define the lat/lon of each forecast point in the data, and each latitude/longitude is binary encoded as a `float32` in the files.
50+
The name of this sub-folder is expected to be unique for each set of lat/lon lists.
51+
For example, the name can be equal to the md5 sum of the concatenated latitude and longitude lists.
3952

40-
The index of each lat/lon pair matches the index of the forecast in the `data` file. So, if you want to look up the data for latitude/longitude index `x`, that data starts in the data file at index `(x * meta.LocationCount)`. Of course, if you make a lookup into a file, you must multiply this by 2 to account for int16 taking up two bytes.
53+
Four files are expected to exist here:
54+
55+
* meta.json
56+
* longitude
57+
* latitude
58+
* data
59+
60+
#### meta.json
61+
62+
This describes the meaning of the forecast values.
63+
The data structure is described in `MetaCollection` in [the code](../fortiup/pkg/fortiblob/collector.go). An example of such data can be found in [the same folder](../fortiup/pkg/fortiblob/collector_test.go)
64+
65+
#### longitude and latitude
66+
67+
In the blob store, latitudes and longitudes exist in two separate files.
68+
These define the lat/lon of each forecast point in the data, and each latitude/longitude is binary encoded as a `float32` in the files.
69+
The ordering of the values are not important, as long as the same ordering is used in the logintude, latitude and data files.
70+
71+
#### data
72+
73+
Data contains the actual values for the forecast.
74+
It consists of a series of little-endian encoded `ìnt16` values, and their meaning is defined in meta.json.
75+
76+
To look up data for a specific location, you need two things:
77+
* An index from the latitude and longitude arrays.
78+
* The length of the relevant data - this is the metadata's number_of_points value.
79+
80+
Multiply the two values to get the starting index.
81+
You can then use the metadata to interpret the relavant values.
82+
[The example](../fortiup/pkg/fortiblob/collector_test.go) shows how to do the interpretation.

fortiup/cmd/mktestdata/main.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,11 @@ func writeMeta(path, parameters string) (*fortiblob.MetaCollection, error) {
101101
pm := fortiblob.ParameterMeta{
102102
Units: "u_" + match[1],
103103
Times: times,
104-
SliceFrom: meta.LocationCount,
104+
SliceFrom: meta.NumberOfPoints,
105105
}
106-
meta.LocationCount += len(times)
106+
meta.NumberOfPoints += len(times)
107107
if len(times) == 0 {
108-
meta.LocationCount++
108+
meta.NumberOfPoints++
109109
}
110110
meta.Parameters[match[1]] = pm
111111
}
@@ -124,20 +124,20 @@ func writeMeta(path, parameters string) (*fortiblob.MetaCollection, error) {
124124
}
125125

126126
func writeData(path string, meta *fortiblob.MetaCollection, lat, lon []float32) error {
127-
totalSize := len(lat) * meta.LocationCount
127+
totalSize := len(lat) * meta.NumberOfPoints
128128

129129
data := make([]int16, totalSize)
130130

131131
for i := range lat {
132132
for _, pMeta := range meta.Parameters {
133133
if len(pMeta.Times) == 0 {
134134
value := (i * 100) + pMeta.SliceFrom
135-
idx := (i * meta.LocationCount) + pMeta.SliceFrom
135+
idx := (i * meta.NumberOfPoints) + pMeta.SliceFrom
136136
data[idx] = int16(value) * 10
137137
}
138138
for t := range pMeta.Times {
139139
value := (i * 100) + (t * 10) + pMeta.SliceFrom
140-
idx := (i * meta.LocationCount) + pMeta.SliceFrom + t
140+
idx := (i * meta.NumberOfPoints) + pMeta.SliceFrom + t
141141
data[idx] = int16(value) * 10
142142
}
143143
}

fortiup/internal/blob2blob/collector/grid/simpledatagroup.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ func collectSimpleDataGroup(ctx context.Context, out io.Writer, source *modelpro
5858

5959
return &fortiblob.MetaCollection{
6060
Parameters: pMeta,
61-
LocationCount: elements,
61+
NumberOfPoints: elements,
6262
}, nil
6363
}
6464

fortiup/internal/nc/store/collect/collect.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func Collect(ctx context.Context, variables []*netcdf.Variable, out io.Writer) (
2828
func getMetaCollection(ctx context.Context, variables []*netcdf.Variable) (*fortiblob.MetaCollection, error) {
2929
ret := fortiblob.MetaCollection{
3030
Parameters: make(map[string]fortiblob.ParameterMeta),
31-
LocationCount: 0,
31+
NumberOfPoints: 0,
3232
}
3333

3434
for _, v := range variables {
@@ -56,12 +56,12 @@ func getMetaCollection(ctx context.Context, variables []*netcdf.Variable) (*fort
5656
meta := fortiblob.ParameterMeta{
5757
Units: units,
5858
Times: times,
59-
SliceFrom: ret.LocationCount,
59+
SliceFrom: ret.NumberOfPoints,
6060
ScaleFactor: getScaleFactor(v),
6161
}
6262

6363
ret.Parameters[v.Name] = meta
64-
ret.LocationCount += len(times)
64+
ret.NumberOfPoints += len(times)
6565
}
6666

6767
return &ret, nil

fortiup/internal/upload/upload_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func makeTestingClient() fortiblob.Client {
4949
SliceFrom: 3,
5050
},
5151
},
52-
LocationCount: 4,
52+
NumberOfPoints: 4,
5353
}
5454
if err := u.SetGridMeta(ctx, &paMeta, area, version, gridid); err != nil {
5555
panic(err)

fortiup/pkg/fortiblob/collector.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,20 @@ type GeographicArea struct {
3131
SRS string `json:"srs"`
3232
}
3333

34-
// MetaCollection is defines the meaning of the forecast data values. It
34+
// MetaCollection defines the meaning of the forecast data values. It
3535
// refers to a set of indexable data consisting of int16 values, where index 0
3636
// contains the first piece of data for a particular location.
3737
type MetaCollection struct {
3838

3939
// Parameters maps parameter names to metadata about that parameter.
4040
Parameters map[string]ParameterMeta `json:"parameters"`
4141

42-
// LocationCount is the total number of forecast values for a single
42+
// NumberOfPoints is the total number of forecast values for a single
4343
// location. It is equal to the sum of the length of all times slices
4444
// under Parameters, and is therefore redundant.
4545
// We keep it as a separate value to avoid having to calculate it over
4646
// and over again upon usage.
47-
LocationCount int `json:"number_of_points"`
47+
NumberOfPoints int `json:"number_of_points"`
4848
}
4949

5050
// ParameterMeta contains metadata about a forecast for a single parameter

fortiup/pkg/fortiblob/collector_test.go

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,43 @@
11
package fortiblob
22

33
import (
4+
"encoding/json"
45
"fmt"
5-
"time"
66
)
77

88
// ExampleMetaCollection shows how to use MetaCollection to interpret raw data.
99
func ExampleMetaCollection() {
1010
sampleData := []int16{
1111
142, 139, 92, 0, 1,
1212
}
13-
meta := MetaCollection{
14-
Parameters: map[string]ParameterMeta{
15-
"temperature": {
16-
Units: "c",
17-
Times: []time.Time{
18-
time.Date(2021, 3, 3, 0, 0, 0, 0, time.UTC),
19-
time.Date(2021, 3, 3, 1, 0, 0, 0, time.UTC),
20-
},
21-
SliceFrom: 0,
22-
ScaleFactor: 0.1,
23-
},
24-
"altitude": {
25-
Units: "m",
26-
Times: []time.Time{
27-
{},
28-
},
29-
SliceFrom: 2,
30-
ScaleFactor: 1,
31-
},
32-
"precipitation": {
33-
Units: "kg/m²",
34-
Times: []time.Time{
35-
time.Date(2021, 3, 3, 0, 0, 0, 0, time.UTC),
36-
time.Date(2021, 3, 3, 1, 0, 0, 0, time.UTC),
37-
},
38-
SliceFrom: 3,
39-
ScaleFactor: 0.1,
40-
},
41-
},
42-
LocationCount: len(sampleData),
13+
metaJSON := `{
14+
"parameters": {
15+
"temperature": {
16+
"units": "c",
17+
"times": ["2021-03-03T00:00:00Z", "2021-03-03T01:00:00Z"],
18+
"slice_from": 0,
19+
"scale_factor": 0.1
20+
},
21+
"altitude": {
22+
"units": "m",
23+
"times": ["0001-01-01T00:00:00Z"],
24+
"slice_from": 2,
25+
"scale_factor": 1
26+
},
27+
"precipitation": {
28+
"units": "kg/m²",
29+
"times": ["2021-03-03T00:00:00Z", "2021-03-03T01:00:00Z"],
30+
"slice_from": 3,
31+
"scale_factor": 0.1
32+
}
33+
},
34+
"number_of_points": 5
35+
}
36+
`
37+
38+
var meta MetaCollection
39+
if err := json.Unmarshal([]byte(metaJSON), &meta); err != nil {
40+
panic(err)
4341
}
4442

4543
for parameter, meta := range meta.Parameters {

fortiup/pkg/fortiblob/sampleblob/sampleblob.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (c *client) GetGridMeta(ctx context.Context, d *fortiblob.DatasetMeta, grid
8282
ScaleFactor: 0.1,
8383
},
8484
},
85-
LocationCount: 5,
85+
NumberOfPoints: 5,
8686
}, nil
8787
}
8888

rawdataforecaster/internal/server/forecast/dataset/values/blob/blob.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,20 @@ func (r *Reader) Read(idx int) (*values.LocationDataCollection, error) {
4444
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
4545
defer cancel()
4646

47-
reader, err := r.source.GetDataRange(ctx, &r.datasetMeta, r.grid, r.gridMeta.LocationCount*idx*2, r.gridMeta.LocationCount*2)
47+
reader, err := r.source.GetDataRange(ctx, &r.datasetMeta, r.grid, r.gridMeta.NumberOfPoints*idx*2, r.gridMeta.NumberOfPoints*2)
4848
if err != nil {
4949
return nil, err
5050
}
5151
defer reader.Close()
5252

53-
buffer := make([]int16, r.gridMeta.LocationCount)
53+
buffer := make([]int16, r.gridMeta.NumberOfPoints)
5454
if err := binary.Read(reader, binary.LittleEndian, &buffer); err != nil {
5555
return nil, err
5656
}
5757

5858
ret := values.LocationDataCollection{
5959
ParameterMeta: r.gridMeta.Parameters,
60-
Data: make([]float32, r.gridMeta.LocationCount),
60+
Data: make([]float32, r.gridMeta.NumberOfPoints),
6161
}
6262

6363
for _, meta := range r.gridMeta.Parameters {

rawdataforecaster/internal/server/forecast/dataset/values/memory/memory_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func getSampleReader() *MemoryReader {
5555
ScaleFactor: 0.1,
5656
},
5757
},
58-
LocationCount: 3,
58+
NumberOfPoints: 3,
5959
}
6060

6161
mad := allocate(3 * 4)

0 commit comments

Comments
 (0)