Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions grib/src/main/java/ucar/nc2/grib/grib2/Grib2Pds.java
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Binary file not shown.
156 changes: 156 additions & 0 deletions grib/src/test/java/ucar/nc2/grib/grib2/TestPds41.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<Grib2Record> 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
Loading