diff --git a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Pds.java b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Pds.java index e652628f46..9cefd5865a 100644 --- a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Pds.java +++ b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Pds.java @@ -68,6 +68,8 @@ public static Grib2Pds factory(int template, byte[] input) { return new Grib2Pds32(input); case 40: return new Grib2Pds40(input); + case 41: + return new Grib2Pds41(input); case 48: return new Grib2Pds48(input); case 60: @@ -1698,6 +1700,65 @@ public int templateLength() { /////////////////////////////////////////////////////////////////////////////// + /* + * Product definition template 4.41 – individual ensemble forecast, control and perturbed, for atmospheric + * constituents + * Octet No. Contents + * 10 Parameter category (see Code table 4.1) + * 11 Parameter number (see Code table 4.2) + * 12–13 Constituent Type (see Code Table 4.230) + * 14 Type of Generating Process (see Code table 4.3) + * 15 Background Process + * 16 Generating Process Identifier + * 17–18 Hours of observational data cut-off after reference time (see Note) + * 19 Minutes of observational data cut-off after reference time + * 20 Indicator of unit of time range (see Code table 4.4) + * 21-24 Forecast time in units defined by octet 18 + * 25 Type of first fixed surface (see Code table 4.5) + * 26 Scale factor of first fixed surface + * 27–30 Scaled value of first fixed surface + * 31 Type of second fixed surface (see Code table 4.5) + * 32 Scale factor of second fixed surface + * 33-36 Scaled value of second fixed surface + * 37 Type of ensemble forecast (see Code table 4.6) + * 38 Perturbation number + * 39 Number of forecasts in ensemble + * Note: Hours greater than 65534 will be coded as 65534. + */ + + private static class Grib2Pds41 extends Grib2Pds40 implements PdsEnsemble { + + Grib2Pds41(byte[] input) { + super(input); + } + + public boolean isEnsemble() { + return true; + } + + /* Type of ensemble forecast (see Code table 4.6) */ + public int getPerturbationType() { + return getOctet(37); + } + + /* Perturbation Ensemble Member Number */ + public int getPerturbationNumber() { + return getOctet(38); + } + + /* Number of forecasts in ensemble */ + public int getNumberEnsembleForecasts() { + return getOctet(39); + } + + @Override + public int templateLength() { + return 39; + } + } + + /////////////////////////////////////////////////////////////////////////////// + /* * Product definition template 4.48 – analysis or forecast at a horizontal level or in a horizontal layer at a point * in time for optical properties of aerosol diff --git a/grib/src/test/data/index/icon-ch2-eps-202606230000-0-poacsnc-ctrl.grib2.gbx9 b/grib/src/test/data/index/icon-ch2-eps-202606230000-0-poacsnc-ctrl.grib2.gbx9 new file mode 100644 index 0000000000..d6c6a8b57a Binary files /dev/null and b/grib/src/test/data/index/icon-ch2-eps-202606230000-0-poacsnc-ctrl.grib2.gbx9 differ diff --git a/grib/src/test/java/ucar/nc2/grib/grib2/TestPds41.java b/grib/src/test/java/ucar/nc2/grib/grib2/TestPds41.java new file mode 100644 index 0000000000..27cbc7c085 --- /dev/null +++ b/grib/src/test/java/ucar/nc2/grib/grib2/TestPds41.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2026 University Corporation for Atmospheric Research/Unidata + * See LICENSE for license information. + */ + +package ucar.nc2.grib.grib2; + +import static com.google.common.truth.Truth.assertThat; + +import java.io.IOException; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Test data from Meteo Swiss CH2 https://data.geo.admin.ch/browser/#/collections/ch.meteoschweiz.ogd-forecasting-icon-ch2?.language=en + * Pollen Grasses Data (POACsnc parameter) was used to create a .gbx9 file for testing. + *

+ * PDS (Secton 4) output from ecCodes grib_dump at the end of the file, uses as a basis for + * testing PDS (Secton 4) parsing + */ +@RunWith(JUnit4.class) +public class TestPds41 { + + private Grib2Pds pds; + + @Before + public void openTestFile() throws IOException { + String testfile = "../grib/src/test/data/index/icon-ch2-eps-202606230000-0-poacsnc-ctrl.grib2.gbx9"; + + Grib2Index gi = new Grib2Index(); + boolean success = gi.readIndex(testfile, -1); + assertThat(success).isTrue(); + List records = gi.getRecords(); + Grib2Record record = records.get(0); + pds = record.getPDS(); + } + + @Test + public void testPdsBasic() { + assertThat(pds.getRawLength()).isEqualTo(63); + assertThat(pds.getTemplateNumber()).isEqualTo(41); + } + + // check overrides + @Test + public void testGenProcessType() { + assertThat(pds.getGenProcessType()).isEqualTo(4);; + } + + @Test + public void testBackProcessId() { + assertThat(pds.getBackProcessId()).isEqualTo(0);; + } + + @Test + public void testGenProcessId() { + assertThat(pds.getGenProcessId()).isEqualTo(142);; + } + + @Test + public void testTimeUnit() { + assertThat(pds.getTimeUnit()).isEqualTo(0); + } + + @Test + public void testLevelType1() { + assertThat(pds.getLevelType1()).isEqualTo(150); + } + + @Test + public void testLevelScale1() { + assertThat(pds.getLevelScale1()).isEqualTo(0); + } + + @Test + public void testLevelValue1() { + assertThat(pds.getLevelValue1()).isEqualTo(80); + } + + @Test + public void testLevelType2() { + assertThat(pds.getLevelType2()).isEqualTo(150); + } + + @Test + public void testLevelScale2() { + assertThat(pds.getLevelScale2()).isEqualTo(0); + } + + @Test + public void testLevelValue2() { + assertThat(pds.getLevelValue2()).isEqualTo(81); + } + + @Test + public void testTemplateLength() { + assertThat(pds.templateLength()).isEqualTo(39); + } + + @Test + public void testIsEnsemble() { + assertThat(pds.isEnsemble()).isTrue(); + } + + @Test + public void testPerturbationType() { + assertThat(((Grib2Pds.PdsEnsemble) pds).getPerturbationType()).isEqualTo(192); + } + + @Test + public void testPerturbationNumber() { + assertThat(((Grib2Pds.PdsEnsemble) pds).getPerturbationNumber()).isEqualTo(0); + } + + @Test + public void testNumberEnsembleForecasts() { + assertThat(((Grib2Pds.PdsEnsemble) pds).getNumberEnsembleForecasts()).isEqualTo(21); + } +} + +// grib_dump -O icon-ch2-eps-202606230000-0-poacsnc-ctrl.grib2 +// ***** FILE: icon-ch2-eps-202606230000-0-poacsnc-ctrl.grib2 +// ... +// ====================== SECTION_4 ( length=63, padding=0 ) ====================== +// 1-4 section4Length = 63 +// 5 numberOfSection = 4 +// 6-7 NV = 6 +// 8-9 productDefinitionTemplateNumber = 41 [Individual ensemble forecast, control and perturbed, at a horizontal level +// or in a horizontal layer at a point in time for atmospheric chemical constituents (grib2/tables/15/4.0.table) ] +// 10 parameterCategory = 20 [Atmospheric chemical constituents (grib2/tables/15/4.1.0.table) ] +// 11 parameterNumber = 60 [Unknown code table entry (grib2/tables/15/4.2.0.20.table) ] +// 12-13 constituentType = 62300 [Unknown code table entry (grib2/tables/15/4.230.table) ] +// 14 typeOfGeneratingProcess = 4 [Ensemble forecast (grib2/tables/15/4.3.table) ] +// 15 backgroundProcess = 0 +// 16 generatingProcessIdentifier = 142 +// 17-18 hoursAfterDataCutoff = 0 +// 19 minutesAfterDataCutoff = 0 +// 20 indicatorOfUnitForForecastTime = 0 [Minute (grib2/tables/15/4.4.table) ] +// 21-24 forecastTime = 0 +// 25 typeOfFirstFixedSurface = 150 [Generalized vertical height coordinate (grib2/tables/15/4.5.table) ] +// 26 scaleFactorOfFirstFixedSurface = 0 +// 27-30 scaledValueOfFirstFixedSurface = 80 +// 31 typeOfSecondFixedSurface = 150 [Generalized vertical height coordinate (grib2/tables/15/4.5.table) ] +// 32 scaleFactorOfSecondFixedSurface = 0 +// 33-36 scaledValueOfSecondFixedSurface = 81 +// 37 typeOfEnsembleForecast = 192 [Unknown code table entry (grib2/tables/15/4.6.table) ] +// 38 perturbationNumber = 0 +// 39 numberOfForecastsInEnsemble = 21 +// 40-43 nlev = 81 +// 44-47 numberOfVGridUsed = 4 +// 48-63 uuidOfVGrid = 16 { +// 4d, 01, b8, 87, 1a, 1c, 80, 1c, 03, 16, f2, 80, f2, e6, 2c, c0 +// } # bytes uuidOfVGrid