https://github.com/dmarkham/grib2
Pure Go GRIB2 reader/writer — all major compression formats, no CGo, no external dependencies
https://github.com/dmarkham/grib2
Last synced: 3 months ago
JSON representation
Pure Go GRIB2 reader/writer — all major compression formats, no CGo, no external dependencies
- Host: GitHub
- URL: https://github.com/dmarkham/grib2
- Owner: dmarkham
- License: mit
- Created: 2026-04-08T23:14:47.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2026-04-09T01:32:03.000Z (3 months ago)
- Last Synced: 2026-04-09T03:14:33.658Z (3 months ago)
- Language: Go
- Size: 160 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# grib2
A pure Go library for reading and writing GRIB2 (WMO FM 92 GRIB Edition 2) files.
[](https://pkg.go.dev/github.com/dmarkham/grib2)
[](LICENSE)
## Overview
GRIB2 is the World Meteorological Organization's binary format for gridded meteorological data, used by weather services worldwide (NOAA, ECMWF, JMA, DWD, etc.). This library provides a complete, idiomatic Go implementation for both decoding and encoding GRIB2 messages -- no CGo, no external C libraries, no system dependencies.
## Features
### Packing Formats
| Format | Template | Decode | Encode |
|--------|----------|--------|--------|
| Simple packing | 5.0 | Yes | Yes |
| Complex packing | 5.2 | Yes | Yes |
| Complex packing + spatial differencing | 5.3 | Yes | Yes |
| IEEE floating point (32/64-bit) | 5.4 | Yes | Yes |
| JPEG2000 | 5.40 | Yes | Yes |
| PNG | 5.41 | Yes | Yes |
| CCSDS/AEC (Adaptive Entropy Coding) | 5.42 | Yes | Yes |
| Run-length packing with level values | 5.200 | Yes | -- |
### Grid Definition Templates (Section 3)
| Grid Type | Template |
|-----------|----------|
| Latitude/Longitude (equidistant cylindrical) | 3.0 |
| Rotated Latitude/Longitude | 3.1 |
| Mercator | 3.10 |
| Polar Stereographic | 3.20 |
| Lambert Conformal | 3.30 |
| Gaussian Latitude/Longitude (regular and reduced) | 3.40 |
| Space View Perspective (geostationary satellite) | 3.90 |
### Product Definition Templates (Section 4)
| Product Type | Template |
|--------------|----------|
| Analysis or forecast at a point in time | 4.0 |
| Individual ensemble forecast | 4.1 |
| Derived ensemble forecast | 4.2 |
| Statistically processed values over time interval | 4.8 |
| Ensemble forecast over time interval | 4.11 |
| Derived ensemble over time interval | 4.12 |
### High-Level API
- **Bitmap support** -- automatic expansion of bitmap-masked fields (NaN for missing points)
- **Scanning mode reordering** -- all 16 scanning mode combinations normalized to canonical west-to-east, north-to-south order
- **Grid coordinates** -- `GridCoordinates()` returns lat/lon arrays for all supported grid types, including projected grids (Lambert, Polar Stereographic, Mercator, Space View)
- **Nearest-neighbour lookup** -- `NearestValue(lat, lon)` with O(1) direct index computation for regular grids, haversine fallback for others
- **Bilinear interpolation** -- `BilinearValue(lat, lon)` for smooth value estimation between grid points
- **Code table lookups** -- human-readable names for disciplines, centres, parameters, surface types, and all template numbers
- **Multi-message files** -- `Decoder` reads files containing any number of concatenated GRIB2 messages
- **Byte-perfect round-trips** -- `ReadMessage` followed by `WriteMessage` produces identical bytes
## Installation
```
go get github.com/dmarkham/grib2
```
Requires Go 1.25 or later. Zero external dependencies outside the standard library.
## Quick Start
### Read a GRIB2 file and extract values
```go
package main
import (
"fmt"
"os"
"github.com/dmarkham/grib2"
)
func main() {
f, err := os.Open("forecast.grib2")
if err != nil {
panic(err)
}
defer f.Close()
dec := grib2.NewDecoder(f)
for {
msg, err := dec.Decode()
if err != nil {
break // io.EOF or parse error
}
fmt.Printf("Discipline: %s\n", grib2.DisciplineName(msg.Section0.Discipline))
fmt.Printf("Centre: %s\n", grib2.CentreName(msg.Section1.Centre))
fmt.Printf("Reference: %s\n", msg.Section1.ReferenceTime())
for i, field := range msg.Fields {
values, err := field.Values()
if err != nil {
fmt.Printf(" field %d: unpack error: %v\n", i, err)
continue
}
fmt.Printf(" field %d: %d values, grid template %d, packing template %d\n",
i, len(values), field.Section3.TemplateNumber, field.Section5.TemplateNumber)
}
}
}
```
### Query a specific location (nearest-neighbour)
```go
field := msg.Fields[0]
// Find the grid point closest to Denver, CO
value, idx, lat, lon, err := field.NearestValue(39.7392, -104.9903)
if err != nil {
panic(err)
}
fmt.Printf("Nearest grid point: (%.4f, %.4f) index=%d value=%.2f\n", lat, lon, idx, value)
// Or use bilinear interpolation for smoother results
interpolated, err := field.BilinearValue(39.7392, -104.9903)
if err != nil {
panic(err)
}
fmt.Printf("Interpolated value: %.2f\n", interpolated)
```
### Write a GRIB2 file
```go
package main
import (
"os"
"github.com/dmarkham/grib2"
)
func main() {
// Create a simple 4x3 temperature grid
values := []float64{
280.0, 281.0, 282.0, 283.0,
279.0, 280.5, 281.5, 282.5,
278.0, 279.5, 280.5, 281.0,
}
// Pack with simple packing at 16 bits per value
packed, tmpl50, err := grib2.PackSimple(values, 16)
if err != nil {
panic(err)
}
msg := &grib2.Message{
Section0: grib2.Section0{
Discipline: 0, // Meteorological
Edition: 2,
},
Section1: grib2.Section1{
Centre: 7, // NCEP
MasterTablesVersion: 2,
Year: 2025, Month: 1, Day: 15,
Hour: 12, Minute: 0, Second: 0,
},
Fields: []grib2.Field{{
Section3: grib2.Section3{
NumberOfDataPoints: 12,
TemplateNumber: 0,
Template: grib2.Template30{
Ni: 4, Nj: 3,
LatitudeOfFirstGridPoint: 40000000, // 40.0N
LongitudeOfFirstGridPoint: 350000000, // 350.0E (-10.0)
LatitudeOfLastGridPoint: 38000000, // 38.0N
LongitudeOfLastGridPoint: 353000000, // 353.0E (-7.0)
IDirectionIncrement: 1000000, // 1.0 degree
JDirectionIncrement: 1000000, // 1.0 degree
},
},
Section4: grib2.Section4{
TemplateNumber: 0,
Template: grib2.Template40{
ParameterCategory: 0, // Temperature
ParameterNumber: 0, // Temperature (K)
},
},
Section5: grib2.Section5{
NumberOfValues: 12,
TemplateNumber: 0,
Template: tmpl50,
},
Section6: grib2.Section6{Indicator: 255}, // no bitmap
Section7: grib2.Section7{Data: packed},
}},
}
out, err := os.Create("output.grib2")
if err != nil {
panic(err)
}
defer out.Close()
if err := grib2.WriteMessage(out, msg); err != nil {
panic(err)
}
}
```
## API Reference
### Reading
| Function/Type | Description |
|---------------|-------------|
| `NewDecoder(r io.Reader) *Decoder` | Create a decoder for reading multiple messages from a stream |
| `(*Decoder).Decode() (*Message, error)` | Read the next GRIB2 message (returns `io.EOF` when done) |
| `ReadMessage(r io.Reader) (*Message, error)` | Convenience function to read a single message |
### Message Structure
```
Message
Section0 -- Indicator (discipline, edition, total length)
Section1 -- Identification (centre, reference time, production status)
Section2 -- Local Use (optional, centre-specific data)
Fields[] -- One or more data fields, each containing:
Section3 -- Grid Definition (template defining the grid geometry)
Section4 -- Product Definition (what the data represents)
Section5 -- Data Representation (packing method and parameters)
Section6 -- Bitmap (missing data mask)
Section7 -- Data (packed values)
```
### Field Methods
| Method | Description |
|--------|-------------|
| `Values() ([]float64, error)` | Unpack, apply bitmap, reorder by scanning mode |
| `GridCoordinates() (lats, lons []float64, error)` | Lat/lon of every grid point in data order |
| `NearestValue(lat, lon float64)` | Value at closest grid point (O(1) for regular grids) |
| `BilinearValue(lat, lon float64)` | Bilinear interpolation from 4 surrounding points |
### Writing
| Function | Description |
|----------|-------------|
| `WriteMessage(w io.Writer, msg *Message) error` | Write a complete GRIB2 message (patches total length automatically) |
| `PackSimple(values, bpv)` | Encode with simple packing (Template 5.0) |
| `PackComplex(values, bpv)` | Encode with complex packing (Template 5.2) |
| `PackComplexSpatialDiff(values, bpv, order)` | Encode with complex packing + spatial differencing (Template 5.3) |
| `PackIEEE(values, precision)` | Encode as IEEE 32-bit or 64-bit floats (Template 5.4) |
| `PackJPEG2000(values, bpv)` | Encode with JPEG2000 lossless compression (Template 5.40) |
| `PackPNG(values, bpv, width, height)` | Encode with PNG compression (Template 5.41) |
| `PackCCSDS(values, bpv, blockSize, rsi)` | Encode with CCSDS/AEC compression (Template 5.42) |
### Code Table Lookups
| Function | Table |
|----------|-------|
| `DisciplineName(d uint8)` | Table 0.0 |
| `CentreName(c uint16)` | Common Code Table C-11 |
| `ParameterName(discipline, category, number uint8)` | Table 4.2.x.x |
| `ParameterUnit(discipline, category, number uint8)` | Units for Table 4.2 |
| `ParameterCategoryName(discipline, category uint8)` | Table 4.1.x |
| `SurfaceTypeName(t uint8)` | Table 4.5 |
| `GridDefinitionTemplateName(n uint16)` | Table 3.1 |
| `ProductDefinitionTemplateName(n uint16)` | Table 4.0 |
| `DataRepresentationTemplateName(n uint16)` | Table 5.0 |
| `ReferenceTimeName(t uint8)` | Table 1.2 |
| `ProductionStatusName(s uint8)` | Table 1.3 |
| `DataTypeName(t uint8)` | Table 1.4 |
## Compression Format Details
| Format | Template | Decode | Encode | Notes |
|--------|----------|--------|--------|-------|
| Simple | 5.0 | Yes | Yes | Fast paths for 8/16/32-bit aligned widths; arbitrary bit widths supported |
| Complex | 5.2 | Yes | Yes | Full group splitting with eccodes-compatible merge optimization |
| Complex + Spatial Diff | 5.3 | Yes | Yes | Order 1 and 2 spatial differencing; missing value management |
| IEEE Float | 5.4 | Yes | Yes | 32-bit and 64-bit precision |
| JPEG2000 | 5.40 | Yes | Yes | Pure Go J2K codec (internal/j2k); lossless mode |
| PNG | 5.41 | Yes | Yes | Uses Go standard library `image/png`; 8-bit and 16-bit grayscale |
| CCSDS/AEC | 5.42 | Yes | Yes | Pure Go AEC codec (internal/aec); CCSDS 121.0-B-3 compliant |
| Run-length | 5.200 | Yes | -- | Variable-base run-length decoding with level values |
All decode paths are cross-validated against eccodes reference output at tolerances from 1e-4 to 1e-10 depending on the format.
## Testing
The test suite contains 138 unit tests and 7 fuzz targets covering:
- **Byte-perfect round-trips**: read a GRIB2 file, write it back, compare bytes
- **Cross-validation against eccodes**: decoded values compared to eccodes `grib_get_data` output for every supported packing format (simple, complex, complex+spatial diff, JPEG2000, PNG, CCSDS, IEEE, run-length)
- **Encode/decode round-trips**: pack values, unpack them, verify they match within quantization tolerance
- **Grid coordinate validation**: lat/lon arrays compared against eccodes for regular lat/lon, Mercator, Gaussian, Lambert, Polar Stereographic, and Space View grids
- **Section-level parsing**: individual tests for all section readers and writers (Sections 0-8)
- **Template coverage**: dedicated tests for all grid, product, and data representation templates
- **Fuzz testing**: 7 fuzz targets covering the full message parser and each individual unpacker (JPEG2000, CCSDS, Complex, Run-length, Simple, PNG)
- **Hardening**: all allocations are capped (100M elements max), all bit extractions are bounds-checked, section sizes are limited to 256 MiB
## Performance
- **Pure Go, zero CGo** -- no overhead from C interop, straightforward cross-compilation, no system library requirements
- **Fast paths** for common bit widths (8, 16, 32-bit aligned simple packing)
- **Streaming decoder** -- reads messages one at a time via `Decoder`, suitable for large multi-message files
- **O(1) nearest-neighbour** for regular lat/lon and regular Gaussian grids (direct index computation instead of brute-force search)
- **Fuzz-tested and hardened** -- safe to use on untrusted input; allocation caps and bounds checks prevent resource exhaustion
## License
MIT