Skip to content

Commit c8020f6

Browse files
committed
grib: fix NullPointerException for legacy NOAA RAP models
Attempting to load a [legacy] NOAA Rapid Refresh GRIB2 file fails with a NullPointerException. java -jar netcdfAll-5.10.0-SNAPSHOT.jar \ rap_130_20170720_0000_000.grb2 This file loads correctly in netcdf-java 4.6, but it has a quirk: it contains at least one PDS that does not define a time range. grib_dump -O -p section_4 \ -w section4Length=58,numberOfTimeRange=0 \ rap_130_20170720_0000_000.grb2 6bef2fb (Fix for issue #606, version 5., 2021-02-26) corrects time calculations for GRIB2 PDS which have incompatible units for **time intervals** and related time quantities. The above patch introduces a `TimeIntervalAndUnits` parser for PDS times. If a PDS does not define *any* time intervals, the previous version of Grib2Tables.getForecastTimeInterval(Grib2Record) returns a duration of zero—i.e., no elapsed time. This is likely the correct interpretation if no time intervals are present. After 6bef2fb, this method raises a NullPointerException instead. This is because the inner function Grib2Tables.getForecastTimeInterval(Grib2Pds.PdsInterval) returns an invalid `TimeIntervalAndUnits` when it ought to return a zero duration instead. The calling functions do not treat this data type as fallible and may use it in conversions that return null. If the PDS does not contain any time intervals, return a "zero hour" duration instead. This restores the previous behavior. * In legacy and modern RAP models, PDS which *do* define time intervals define them as zero. The interpretation of the "quirky" PDS remains consistent with its brethren. * This quirk is not present in more [recent] RAP model runs, so this bug is unlikely to affect current RAP data. [legacy]: https://www.ncei.noaa.gov/oa/prod-model/rapid-refresh/access/historical/analysis/201707/20170720/rap_130_20170720_0000_000.grb2 [recent]: https://www.ncei.noaa.gov/oa/prod-model/rapid-refresh/access/rap-130-13km/analysis/202504/20250401/rap_130_20250401_0000_000.grb2
1 parent 3f0b68c commit c8020f6

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

grib/src/main/java/ucar/nc2/grib/grib2/table/Grib2Tables.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,10 @@ private TimeIntervalAndUnits getForecastTimeInterval(Grib2Pds.PdsInterval pdsInt
607607
range += ti.timeIncrement;
608608
}
609609
}
610+
if (timeUnitIntv < 0) {
611+
range = 0;
612+
timeUnitIntv = 1; /* hours */
613+
}
610614
return new TimeIntervalAndUnits(timeUnitIntv, range);
611615
}
612616

376 Bytes
Binary file not shown.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package ucar.nc2.grib.grib2;
2+
3+
import org.junit.Before;
4+
import org.junit.Test;
5+
import java.io.IOException;
6+
import org.junit.runner.RunWith;
7+
import org.junit.runners.JUnit4;
8+
9+
import ucar.nc2.grib.coord.TimeCoordIntvDateValue;
10+
import ucar.nc2.grib.grib2.table.Grib2Tables;
11+
import static org.junit.Assert.assertEquals;
12+
import static org.junit.Assert.assertTrue;
13+
14+
@RunWith(JUnit4.class)
15+
public class TestPds8 {
16+
@Before
17+
public void openTestFile() throws IOException {
18+
String testfile = "../grib/src/test/data/index/example_pds_8_quirks.grib2.gbx9";
19+
20+
Grib2Index gi = new Grib2Index();
21+
boolean success = gi.readIndex(testfile, -1);
22+
assertTrue(success);
23+
assertEquals(gi.getRecords().size(), 2);
24+
quirky = gi.getRecords().get(0);
25+
normal = gi.getRecords().get(1);
26+
}
27+
28+
@Test
29+
public void testForQuirkyRecord() {
30+
Grib2Pds normal_pds = normal.getPDS();
31+
Grib2Pds quirky_pds = quirky.getPDS();
32+
33+
assertEquals(normal_pds.getRawLength(), 58);
34+
assertEquals(quirky_pds.getRawLength(), 58);
35+
36+
// byte 41 is numberOfTimeRange, which is generally 1 for
37+
// this type of GRIB2 record. For the quirk, it is zero.
38+
assertEquals(normal_pds.getOctet(42), 1);
39+
assertEquals(quirky_pds.getOctet(42), 0);
40+
}
41+
42+
@Test
43+
public void testPds8ReadBasic() {
44+
Grib2Pds pds8 = quirky.getPDS();
45+
assertEquals(pds8.getTemplateNumber(), 8);
46+
assertEquals(pds8.getParameterCategory(), 1);
47+
assertEquals(pds8.getParameterNumber(), 29);
48+
assertEquals(pds8.getGenProcessType(), 2);
49+
assertEquals(pds8.getGenProcessId(), 105);
50+
assertEquals(pds8.getBackProcessId(), 0);
51+
assertEquals(pds8.getTimeUnit(), 1);
52+
assertEquals(pds8.getForecastTime(), 0);
53+
}
54+
55+
@Test
56+
public void testPds8CanGetNormalTime() {
57+
Grib2Tables tbl = Grib2Tables.factory(normal);
58+
TimeCoordIntvDateValue tc = tbl.getForecastTimeInterval(normal);
59+
// start and end times are the same
60+
assertEquals(tc.getStart().getMillis(), tc.getEnd().getMillis());
61+
assertEquals(tc.getStart().getMillis(), 1500508800000L);
62+
}
63+
64+
@Test
65+
public void testPds8CanStillGetQuirkyTime() {
66+
Grib2Tables tbl = Grib2Tables.factory(quirky);
67+
TimeCoordIntvDateValue tc = tbl.getForecastTimeInterval(quirky);
68+
// start and end times are the same
69+
assertEquals(tc.getStart().getMillis(), tc.getEnd().getMillis());
70+
assertEquals(tc.getStart().getMillis(), 1500508800000L);
71+
}
72+
73+
/*
74+
* ====================== SECTION_4 ( length=58, padding=0 ) ======================
75+
* 1-4 section4Length = 58
76+
* 5 numberOfSection = 4
77+
* 6-7 NV = 0
78+
* 8-9 productDefinitionTemplateNumber = 8 [Average, accumulation, extreme values or other statistically processed
79+
* values at a horizontal level or in a horizontal layer in a continuous or non-continuous time interval
80+
* (grib2/tables/2/4.0.table) ]
81+
* 10 parameterCategory = 1 [Moisture (grib2/tables/2/4.1.0.table) ]
82+
* 11 parameterNumber = 8 [Total precipitation (kg m-2) (grib2/tables/2/4.2.0.1.table) ]
83+
* 12 typeOfGeneratingProcess = 2 [Forecast (grib2/tables/2/4.3.table) ]
84+
* 13 backgroundProcess = 0
85+
* 14 generatingProcessIdentifier = 105
86+
* 15-16 hoursAfterDataCutoff = 0
87+
* 17 minutesAfterDataCutoff = 0
88+
* 18 indicatorOfUnitOfTimeRange = 1 [Hour (grib2/tables/2/4.4.table) ]
89+
* 19-22 forecastTime = 0
90+
* 23 typeOfFirstFixedSurface = 1 [Ground or water surface (grib2/tables/2/4.5.table ,
91+
* grib2/tables/local/kwbc/1/4.5.table) ]
92+
* 24 scaleFactorOfFirstFixedSurface = 0
93+
* 25-28 scaledValueOfFirstFixedSurface = 0
94+
* 29 typeOfSecondFixedSurface = 255 [Missing (grib2/tables/2/4.5.table , grib2/tables/local/kwbc/1/4.5.table) ]
95+
* 30 scaleFactorOfSecondFixedSurface = 0
96+
* 31-34 scaledValueOfSecondFixedSurface = 0
97+
* 35-36 yearOfEndOfOverallTimeInterval = 2017
98+
* 37 monthOfEndOfOverallTimeInterval = 7
99+
* 38 dayOfEndOfOverallTimeInterval = 20
100+
* 39 hourOfEndOfOverallTimeInterval = 0
101+
* 40 minuteOfEndOfOverallTimeInterval = 0
102+
* 41 secondOfEndOfOverallTimeInterval = 0
103+
* 42 numberOfTimeRange = 1
104+
* 43-46 numberOfMissingInStatisticalProcess = 0
105+
* 47 typeOfStatisticalProcessing = 1 [Accumulation (grib2/tables/2/4.10.table) ]
106+
* 48 typeOfTimeIncrement = 2 [Successive times processed have same start time of forecast, forecast time is
107+
* incremented (grib2/tables/2/4.11.table) ]
108+
* 49 indicatorOfUnitForTimeRange = 1 [Hour (grib2/tables/2/4.4.table) ]
109+
* 50-53 lengthOfTimeRange = 0
110+
* 54 indicatorOfUnitForTimeIncrement = 255 [Missing (grib2/tables/2/4.4.table) ]
111+
* 55-58 timeIncrement = 0
112+
*/
113+
private Grib2Record quirky;
114+
115+
/*
116+
* ====================== SECTION_4 ( length=58, padding=0 ) ======================
117+
* 1-4 section4Length = 58
118+
* 5 numberOfSection = 4
119+
* 6-7 NV = 0
120+
* 8-9 productDefinitionTemplateNumber = 8 [Average, accumulation, extreme values or other statistically processed
121+
* values at a horizontal level or in a horizontal layer in a continuous or non-continuous time interval
122+
* (grib2/tables/2/4.0.table) ]
123+
* 10 parameterCategory = 1 [Moisture (grib2/tables/2/4.1.0.table) ]
124+
* 11 parameterNumber = 8 [Total precipitation (kg m-2) (grib2/tables/2/4.2.0.1.table) ]
125+
* 12 typeOfGeneratingProcess = 2 [Forecast (grib2/tables/2/4.3.table) ]
126+
* 13 backgroundProcess = 0
127+
* 14 generatingProcessIdentifier = 105
128+
* 15-16 hoursAfterDataCutoff = 0
129+
* 17 minutesAfterDataCutoff = 0
130+
* 18 indicatorOfUnitOfTimeRange = 1 [Hour (grib2/tables/2/4.4.table) ]
131+
* 19-22 forecastTime = 0
132+
* 23 typeOfFirstFixedSurface = 1 [Ground or water surface (grib2/tables/2/4.5.table ,
133+
* grib2/tables/local/kwbc/1/4.5.table) ]
134+
* 24 scaleFactorOfFirstFixedSurface = 0
135+
* 25-28 scaledValueOfFirstFixedSurface = 0
136+
* 29 typeOfSecondFixedSurface = 255 [Missing (grib2/tables/2/4.5.table , grib2/tables/local/kwbc/1/4.5.table) ]
137+
* 30 scaleFactorOfSecondFixedSurface = 0
138+
* 31-34 scaledValueOfSecondFixedSurface = 0
139+
* 35-36 yearOfEndOfOverallTimeInterval = 2017
140+
* 37 monthOfEndOfOverallTimeInterval = 7
141+
* 38 dayOfEndOfOverallTimeInterval = 20
142+
* 39 hourOfEndOfOverallTimeInterval = 0
143+
* 40 minuteOfEndOfOverallTimeInterval = 0
144+
* 41 secondOfEndOfOverallTimeInterval = 0
145+
* 42 numberOfTimeRange = 1
146+
* 43-46 numberOfMissingInStatisticalProcess = 0
147+
* 47 typeOfStatisticalProcessing = 1 [Accumulation (grib2/tables/2/4.10.table) ]
148+
* 48 typeOfTimeIncrement = 2 [Successive times processed have same start time of forecast, forecast time is
149+
* incremented (grib2/tables/2/4.11.table) ]
150+
* 49 indicatorOfUnitForTimeRange = 1 [Hour (grib2/tables/2/4.4.table) ]
151+
* 50-53 lengthOfTimeRange = 0
152+
* 54 indicatorOfUnitForTimeIncrement = 255 [Missing (grib2/tables/2/4.4.table) ]
153+
* 55-58 timeIncrement = 0
154+
*/
155+
private Grib2Record normal;
156+
}
157+
158+
/*
159+
* # steps to reproduce test data
160+
*
161+
* baseurl='www.ncei.noaa.gov/oa/prod-model/rapid-refresh/access/historical/analysis'
162+
* curl -O "https://${baseurl}/201707/20170720/rap_130_20170720_0000_000.grb2"
163+
*
164+
* codes_split_file -1 rap_130_20170720_0000_000.grb2
165+
*
166+
* # quirky
167+
* grib_dump -O -p section_4 \
168+
* -w section4Length=58,numberOfTimeRange=0 \
169+
* rap_130_20170720_0000_000.grb2_199
170+
*
171+
* # normal
172+
* grib_dump -O -p section_4 \
173+
* -w section4Length=58,numberOfTimeRange=1 \
174+
* rap_130_20170720_0000_000.grb2_212
175+
*
176+
* cat \
177+
* rap_130_20170720_0000_000.grb2_199 \
178+
* rap_130_20170720_0000_000.grb2_212 \
179+
* >example_pds_8_quirks.grib2
180+
*
181+
* # index
182+
* java -jar netcdfAll-5.10.0-SNAPSHOT.jar \
183+
* example_pds_8_quirks.grib2
184+
*
185+
* # generates example_pds_8_quirks.grib2.gbx9
186+
*/

0 commit comments

Comments
 (0)