diff --git a/grib/src/main/java/ucar/nc2/grib/collection/Grib2Collection.java b/grib/src/main/java/ucar/nc2/grib/collection/Grib2Collection.java index 0430891b54..fcbce2fad8 100644 --- a/grib/src/main/java/ucar/nc2/grib/collection/Grib2Collection.java +++ b/grib/src/main/java/ucar/nc2/grib/collection/Grib2Collection.java @@ -19,6 +19,7 @@ import ucar.nc2.grib.GribNumbers; import ucar.nc2.grib.GribTables; import ucar.nc2.grib.coverage.GribCoverageDataset; +import ucar.nc2.grib.grib2.Grib2Utils; import ucar.nc2.grib.grib2.table.Grib2Tables; import ucar.unidata.util.StringUtil2; import java.io.IOException; @@ -166,11 +167,12 @@ public void addGlobalAttributes(AttributeContainer result) { * if variable name is duplicate of existing variable name (this should not normally occur): * _n (where n begins with 1 for first duplicate) (ex: TMAX_P8_L103_GCA0_6h_1) * - * VAR_%d-%d-%d[_error][_L%d][_layer][_I%s_S%d][_D%d][_Prob_%s] + * VAR_%d-%d-%d[_error][_L%d][_layer][_I%s_S%d][_D%d][_Prob_%s][_A%d_%s] * %d-%d-%d = discipline-category-paramNo * L = level type * S = stat type * D = derived type + * A = aerosol type */ @Override protected String makeVariableId(GribCollectionImmutable.VariableIndex vindex) { @@ -213,6 +215,11 @@ private static String makeVariableId(GribCollectionImmutable.VariableIndex vinde f.format("_Prob_%s", s); } + if (vindex.getAerosolType() >= 0) { + f.format("_A%d", vindex.getAerosolType()); + f.format("_%s", Grib2Utils.makeAerosolRangeSuffix(vindex.getAerosolRange())); + } + return f.toString(); } } @@ -252,6 +259,12 @@ else if (vindex.getProbabilityName() != null && !vindex.getProbabilityName().isE v.addAttribute(new Attribute("Grib2_Probability_Name", vindex.getProbabilityName())); } + if (vindex.getAerosolType() >= 0) { + v.addAttribute(new Attribute("Grib2_Aerosol_Type", vindex.getAerosolType())); + v.addAttribute(new Attribute("Grib2_Aerosol_Name", vindex.getAerosolName())); + v.addAttribute(new Attribute("Grib2_Aerosol_Range", vindex.getAerosolRange())); + } + if (vindex.getGenProcessType() >= 0) { String genProcessTypeName = cust2.getGeneratingProcessTypeName(vindex.getGenProcessType()); if (genProcessTypeName != null) diff --git a/grib/src/main/java/ucar/nc2/grib/collection/Grib2Iosp.java b/grib/src/main/java/ucar/nc2/grib/collection/Grib2Iosp.java index 7e8c61931c..770e763518 100644 --- a/grib/src/main/java/ucar/nc2/grib/collection/Grib2Iosp.java +++ b/grib/src/main/java/ucar/nc2/grib/collection/Grib2Iosp.java @@ -84,6 +84,11 @@ static String makeVariableNameFromTable(Grib2Tables cust, GribCollectionImmutabl if (vindex.getPercentileValue() >= 0) { f.format("_Percentile%2d", vindex.getPercentileValue()); } + + if (vindex.getAerosolType() >= 0) { + f.format("_%s", GribUtils.makeNameFromDescription(vindex.getAerosolName())); + f.format("_%s", Grib2Utils.makeAerosolRangeSuffix(vindex.getAerosolRange())); + } return f.toString(); } } @@ -144,6 +149,10 @@ static String makeVariableLongName(Grib2Tables cust, GribCollectionImmutable.Var f.format(" %d Percentile", vindex.getPercentileValue()); } + if (vindex.getAerosolName() != null && !vindex.getAerosolName().isEmpty()) { + f.format(" (%s %s)", vindex.getAerosolName(), vindex.getAerosolRange()); + } + if (vindex.getLevelType() != GribNumbers.UNDEFINED) { // satellite data doesnt have a level f.format(" @ %s", cust.getCodeTableValue("4.5", vindex.getLevelType())); if (vindex.isLayer()) { diff --git a/grib/src/main/java/ucar/nc2/grib/collection/GribCollectionImmutable.java b/grib/src/main/java/ucar/nc2/grib/collection/GribCollectionImmutable.java index 792ed5d66a..be0741b8e6 100644 --- a/grib/src/main/java/ucar/nc2/grib/collection/GribCollectionImmutable.java +++ b/grib/src/main/java/ucar/nc2/grib/collection/GribCollectionImmutable.java @@ -706,6 +706,18 @@ public String getProbabilityName() { return info.probabilityName; } + public int getAerosolType() { + return info.aerosolType; + } + + public String getAerosolName() { + return info.aerosolName; + } + + public String getAerosolRange() { + return info.aerosolRange; + } + public boolean isLayer() { return info.isLayer; } @@ -825,6 +837,9 @@ public final class Info { @Nullable final String intvName; // eg "mixed intervals, 3 Hour, etc" final String probabilityName; + final int aerosolType; + final String aerosolName; + final String aerosolRange; final boolean isLayer, isEnsemble; final int genProcessType; final int spatialStatType; @@ -840,6 +855,9 @@ public Info(GribCollectionMutable.VariableIndex gcVar) { this.probType = gcVar.probType; this.intvName = gcVar.getTimeIntvName(); this.probabilityName = gcVar.probabilityName; + this.aerosolType = gcVar.aerosolType; + this.aerosolName = gcVar.aerosolName; + this.aerosolRange = gcVar.aerosolRange; this.isLayer = gcVar.isLayer; this.isEnsemble = gcVar.isEnsemble; this.genProcessType = gcVar.genProcessType; diff --git a/grib/src/main/java/ucar/nc2/grib/collection/GribCollectionMutable.java b/grib/src/main/java/ucar/nc2/grib/collection/GribCollectionMutable.java index 480e13be3e..b62b023286 100644 --- a/grib/src/main/java/ucar/nc2/grib/collection/GribCollectionMutable.java +++ b/grib/src/main/java/ucar/nc2/grib/collection/GribCollectionMutable.java @@ -435,6 +435,9 @@ public class VariableIndex implements Comparable { public final int category, parameter, levelType, intvType, ensDerivedType, probType, percentile; private String intvName; // eg "mixed intervals, 3 Hour, etc" public final String probabilityName; + public final int aerosolType; + public final String aerosolName; + public final String aerosolRange; public final boolean isLayer, isEnsemble; public final int genProcessType; public final int spatialStatType; @@ -476,6 +479,9 @@ private VariableIndex(GroupGC g, GribTables customizer, int discipline, int cent this.ensDerivedType = -1; this.probType = -1; this.probabilityName = null; + this.aerosolType = -1; + this.aerosolName = null; + this.aerosolRange = null; this.percentile = -1; this.genProcessType = pds.getGenProcess(); // LOOK process vs process type ?? @@ -517,6 +523,17 @@ private VariableIndex(GroupGC g, GribTables customizer, int discipline, int cent this.probabilityName = null; } + if (pds.isAerosol()) { + Grib2Pds.PdsAerosol pdsAerosol = (Grib2Pds.PdsAerosol) pds; + this.aerosolType = pdsAerosol.getAerosolType(); + this.aerosolName = pdsAerosol.getAerosolName(); + this.aerosolRange = pdsAerosol.getAerosolRange(); + } else { + this.aerosolType = -1; + this.aerosolName = null; + this.aerosolRange = null; + } + if (pds.isPercentile()) { Grib2Pds.PdsPercentile pdsPctl = (Grib2Pds.PdsPercentile) pds; this.percentile = pdsPctl.getPercentileValue(); @@ -560,6 +577,9 @@ protected VariableIndex(GroupGC g, VariableIndex other) { this.ensDerivedType = other.ensDerivedType; this.probabilityName = other.probabilityName; this.probType = other.probType; + this.aerosolType = other.aerosolType; + this.aerosolName = other.aerosolName; + this.aerosolRange = other.aerosolRange; this.genProcessType = other.genProcessType; this.spatialStatType = other.spatialStatType; this.isEnsemble = other.isEnsemble; @@ -619,8 +639,9 @@ public String toString() { return MoreObjects.toStringHelper(this).add("tableVersion", tableVersion).add("discipline", discipline) .add("category", category).add("parameter", parameter).add("levelType", levelType).add("intvType", intvType) .add("ensDerivedType", ensDerivedType).add("probType", probType).add("intvName", intvName) - .add("probabilityName", probabilityName).add("isLayer", isLayer).add("genProcessType", genProcessType) - .add("cdmHash", gribVariable.hashCode()).toString(); + .add("probabilityName", probabilityName).add("aerosolName", aerosolName).add("aerosolRange", aerosolRange) + .add("isLayer", isLayer).add("genProcessType", genProcessType).add("cdmHash", gribVariable.hashCode()) + .toString(); } public String toStringComplete() { @@ -629,8 +650,9 @@ public String toStringComplete() { .add("recordsLen", recordsLen).add("gribVariable", gribVariable).add("coordIndex", coordIndex) .add("category", category).add("parameter", parameter).add("levelType", levelType).add("intvType", intvType) .add("ensDerivedType", ensDerivedType).add("probType", probType).add("intvName", intvName) - .add("probabilityName", probabilityName).add("isLayer", isLayer).add("isEnsemble", isEnsemble) - .add("genProcessType", genProcessType).add("spatialStatType", spatialStatType).toString(); + .add("probabilityName", probabilityName).add("aerosolName", aerosolName).add("aerosolRange", aerosolRange) + .add("isLayer", isLayer).add("isEnsemble", isEnsemble).add("genProcessType", genProcessType) + .add("spatialStatType", spatialStatType).toString(); } public String toStringShort() { @@ -644,6 +666,9 @@ public String toStringShort() { if (probabilityName != null && !probabilityName.isEmpty()) { sb.format(" prob=%s", probabilityName); } + if (aerosolName != null && !aerosolName.isEmpty()) { + sb.format(" aerosol=%s (%s)", aerosolName, aerosolRange); + } sb.format(" cdmHash=%d}", gribVariable.hashCode()); return sb.toString(); } 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 9cefd5865a..890b2e90a1 100644 --- a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Pds.java +++ b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Pds.java @@ -7,14 +7,17 @@ import ucar.nc2.grib.GribNumbers; import ucar.nc2.time.CalendarDate; +import ucar.nc2.wmo.CommonCodeTable; import ucar.unidata.util.Format; import ucar.unidata.util.StringUtil2; +import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; import java.util.Formatter; +import java.util.StringJoiner; import java.util.zip.CRC32; -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; +import static ucar.nc2.grib.grib2.Grib2Utils.intervalToRangeDescriptor; /** * Abstract superclass for GRIB2 PDS handling. @@ -33,9 +36,9 @@ public abstract class Grib2Pds { * * @param template pds template number * @param input raw bytes - * @return Grib2Pds or null on error + * @return Grib2Pds or exception on error */ - @Nullable + @Nonnull public static Grib2Pds factory(int template, byte[] input) { switch (template) { case 0: @@ -70,6 +73,8 @@ public static Grib2Pds factory(int template, byte[] input) { return new Grib2Pds40(input); case 41: return new Grib2Pds41(input); + case 46: + return new Grib2Pds46(input); case 48: return new Grib2Pds48(input); case 60: @@ -77,7 +82,6 @@ public static Grib2Pds factory(int template, byte[] input) { case 61: return new Grib2Pds61(input); default: - log.warn("Missing template " + template); throw new UnsupportedOperationException("Product Definition " + template + " not yet implemented."); } } @@ -221,7 +225,7 @@ public int getLevelScale2() { } public boolean isAerosol() { - return (template == 48); + return false; } public boolean isEnsemble() { @@ -336,19 +340,81 @@ public int getStatisticalProcessType() { ////////////////////////////////////////////////////////////////////////////////////////////// public interface PdsAerosol { + /** + * @return Aerosol Type (see Code table 4.233) + */ int getAerosolType(); - double getAerosolIntervalSizeType(); + /** + * @return Type of interval for first and second size (see Code table 4.91) + */ + int getAerosolIntervalSizeType(); + /** + * @return First aerosol size (in meters) + */ double getAerosolSize1(); + /** + * @return Second aerosol size (in meters) + */ double getAerosolSize2(); - double getAerosolIntervalWavelengthType(); + /** + * @return Type of interval for first and second wavelength (see Code table 4.91) + */ + int getAerosolIntervalWavelengthType(); + /** + * @return First aerosol wavelength (in meters) + */ double getAerosolWavelength1(); + /** + * @return Second aerosol wavelength (in meters) + */ double getAerosolWavelength2(); + + @Nonnull + default String getAerosolName() { + int code = getAerosolType(); + String name = CommonCodeTable.getTableValue(14, code); + return name != null ? name : String.format("UnknownAerosolType%d", code); + } + + /** + * @return The size and/or wavelength range of this aerosol product (examples: ">2.5um", ">=2.5um,<10um") + */ + @Nonnull + default String getAerosolRange() { + StringJoiner sj = new StringJoiner(" "); + + String sizeRange = intervalToRangeDescriptor(getAerosolIntervalSizeType(), getAerosolSize1() * 1e6, + getAerosolSize2() * 1e6, "um"); // microns + if (!sizeRange.isEmpty()) { + sj.add(sizeRange); + } + + String wavelengthRange = intervalToRangeDescriptor(getAerosolIntervalWavelengthType(), + getAerosolWavelength1() * 1e9, getAerosolWavelength2() * 1e9, "nm"); // nanometers + if (!wavelengthRange.isEmpty()) { + sj.add(wavelengthRange); + } + + return sj.toString(); + } + + default int getAerosolHashcode() { + int result = 17; + result = 31 * result + getAerosolType(); + result = 31 * result + getAerosolIntervalSizeType(); + result = 31 * result + Double.hashCode(getAerosolSize1()); + result = 31 * result + Double.hashCode(getAerosolSize2()); + result = 31 * result + getAerosolIntervalWavelengthType(); + result = 31 * result + Double.hashCode(getAerosolWavelength1()); + result = 31 * result + Double.hashCode(getAerosolWavelength2()); + return result; + } } public interface PdsInterval { @@ -1759,6 +1825,183 @@ public int templateLength() { /////////////////////////////////////////////////////////////////////////////// + /* + * Product definition template 4.46 – average, accumulation, and/or extreme values or other statistically-processed + * values at a horizontal level or in a horizontal layer in a continuous or non-continuous time interval for aerosol + * https://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc/grib2_temp4-46.shtml + */ + + private static class Grib2Pds46 extends Grib2Pds implements PdsAerosol, PdsInterval { + + Grib2Pds46(byte[] input) { + super(input); + } + + @Override + public boolean isAerosol() { + return true; + } + + @Override + public boolean isTimeInterval() { + return true; + } + + // 12–13 Aerosol type (see Code table 4.233) + public int getAerosolType() { + return GribNumbers.uint2(getOctet(12), getOctet(13)); + } + + // 14 Type of interval for first and second size (see Code table 4.91) + public int getAerosolIntervalSizeType() { + return getOctet(14); + } + + // 15 Scale factor of first size + // 16–19 Scaled value of first size in meters + public double getAerosolSize1() { + return getScaledValue(15); + } + + // 20 Scale factor of second size + // 21–24 Scaled value of second size in meters + public double getAerosolSize2() { + return getScaledValue(20); + } + + // 25 Type of generating process (see Code table 4.3) + @Override + public int getGenProcessType() { + return getOctet(25); + } + + // 26 Background generating process identifier (defined by originating center) + @Override + public int getBackProcessId() { + return getOctet(26); + } + + // 27 Analysis or forecast generating process identifier (see Code ON388 Table A) + @Override + public int getGenProcessId() { + return getOctet(27); + } + + // 28–29 Hours after reference time of data cutoff + public int getHoursAfterCutoff() { + return GribNumbers.int2(getOctet(28), getOctet(29)); + } + + // 30 Minutes after reference time of data cutoff + public int getMinutesAfterCutoff() { + return getOctet(30); + } + + // 31 Indicator of unit of time range (see Code table 4.4) + @Override + public int getTimeUnit() { + return getOctet(31); + } + + // 32–35 Forecast time in units defined by octet 31 + @Override + public int getForecastTime() { + return GribNumbers.int4(getOctet(32), getOctet(33), getOctet(34), getOctet(35)); + } + + // 36 Type of first fixed surface (see Code table 4.5) + @Override + public int getLevelType1() { + return getOctet(36); + } + + // 37 Scale factor of first fixed surface + @Override + public int getLevelScale1() { + return getOctet(37); + } + + // 38–41 Scaled value of first fixed surface + @Override + public double getLevelValue1() { + return getScaledValue(37); + } + + // 42 Type of second fixed surface (see Code table 4.5) + @Override + public int getLevelType2() { + return getOctet(42); + } + + // 43 Scale factor of second fixed surface + @Override + public int getLevelScale2() { + return getOctet(43); + } + + // 44–47 Scaled value of second fixed surface + @Override + public double getLevelValue2() { + return getScaledValue(43); + } + + // 48–54 End of overall time interval + public CalendarDate getIntervalTimeEnd() { + return calcTime(48); + } + + // 55 number of time ranges specifications describing the time intervals used to calculate the + // statistically-processed field + public int getNumberTimeRanges() { + return getOctet(55); + } + + // 56–59 Total number of data values missing in the statistical process + public final int getNumberMissing() { + return GribNumbers.int4(getOctet(56), getOctet(57), getOctet(58), getOctet(59)); + } + + public TimeInterval[] getTimeIntervals() { + return readTimeIntervals(getNumberTimeRanges(), 60); + } + + @Override + public int templateLength() { + return 59 + getNumberTimeRanges() * 12; + } + + public long getIntervalHash() { + CRC32 crc32 = new CRC32(); + crc32.update(input, 59, 12); + if (getNumberTimeRanges() > 1) + crc32.update(input, 71, 1); + return crc32.getValue(); + } + + public void show(Formatter f) { + super.show(f); + f.format("%n Grib2Pds46: endInterval=%s%n", getIntervalTimeEnd()); + for (TimeInterval ti : getTimeIntervals()) { + ti.show(f); + } + } + + // template 4.46 has no wavelength fields + public int getAerosolIntervalWavelengthType() { + return GribNumbers.UNDEFINED; + } + + public double getAerosolWavelength1() { + return GribNumbers.UNDEFINED; + } + + public double getAerosolWavelength2() { + return GribNumbers.UNDEFINED; + } + } + + /////////////////////////////////////////////////////////////////////////////// + /* * 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 @@ -1798,13 +2041,18 @@ private static class Grib2Pds48 extends Grib2Pds implements PdsAerosol { super(input); } + @Override + public boolean isAerosol() { + return true; + } + // 12–13 Aerosol type (see Common Code table C–14) public int getAerosolType() { return GribNumbers.uint2(getOctet(12), getOctet(13)); } // 14 Type of interval for first and second size (see Code table 4.91) - public double getAerosolIntervalSizeType() { + public int getAerosolIntervalSizeType() { return getOctet(14); } @@ -1821,7 +2069,7 @@ public double getAerosolSize2() { } // 25 Type of interval for first and second wavelength (see Code table 4.91) - public double getAerosolIntervalWavelengthType() { + public int getAerosolIntervalWavelengthType() { return getOctet(25); } diff --git a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Utils.java b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Utils.java index d842fbb9e8..5b05e91974 100644 --- a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Utils.java +++ b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Utils.java @@ -5,7 +5,9 @@ package ucar.nc2.grib.grib2; +import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.StringJoiner; import ucar.nc2.constants.AxisType; import ucar.nc2.grib.GribTables; import ucar.nc2.grib.grib2.table.WmoParamTable; @@ -85,6 +87,109 @@ public static CalendarPeriod getCalendarPeriod(int timeUnit) { } } + /** + *
+   *   Code table 4.91 - Type of Interval
+   *   https://www.nco.ncep.noaa.gov/pmb/docs/grib2/grib2_doc/grib2_table4-91.shtml
+   *
+   *      0  Smaller than first limit
+   *      1  Greater than second limit
+   *      2  Between first and second limit. The range includes the first limit but not the second limit.
+   *      3  Greater than first limit
+   *      4  Smaller than second limit
+   *      5  Smaller or equal first limit
+   *      6  Greater or equal second limit
+   *      7  Between first and second limit. The range includes the first limit and the second limit.
+   *      8  Greater or equal first limit
+   *      9  Smaller or equal second limit
+   *     10  Between first and second limit. The range includes the second limit but not the first limit.
+   *     11  Equal to first limit
+   * 
+ * + * @return the relational operator applied to the first limit of the interval, or null if not applicable + */ + @Nullable + public static String getFirstLimitOperator(int intervalType) { + switch (intervalType) { + case 0: + return "<"; + case 2: + case 7: + case 8: + return ">="; + case 3: + case 10: + return ">"; + case 5: + return "<="; + case 11: + return ""; // No need to put "=" sign in front of the value. The value by itself is fine. + default: + return null; + } + } + + /** + * See {@link #getFirstLimitOperator}. + * + * @return the relational operator applied to the second limit of the interval, or null if not applicable + */ + @Nullable + public static String getSecondLimitOperator(int intervalType) { + switch (intervalType) { + case 1: + return ">"; + case 2: + case 4: + return "<"; + case 6: + return ">="; + case 7: + case 9: + case 10: + return "<="; + default: + return null; + } + } + + @Nullable + private static String rangeTermFor(String operator, double val, String unit) { + if (operator == null) { + return null; + } + String formattedValue = (int) val == val ? Integer.toString((int) val) : Double.toString(val); + return operator + formattedValue + unit; + } + + /** + * Constructs a human-readable representation of an interval. Whether the first or second limit (or both) is used + * depends on the type of interval. Examples: ">2.5um", ">=2.5um,<10um" + * + * @param intervalType code from Code table 4.91 + * @param firstLimit first limit of the interval + * @param secondLimit second limit of the interval + * @param unit the unit of the limits, applied as a suffix + */ + @Nonnull + public static String intervalToRangeDescriptor(int intervalType, double firstLimit, double secondLimit, String unit) { + StringJoiner sj = new StringJoiner(","); + String firstTerm = rangeTermFor(getFirstLimitOperator(intervalType), firstLimit, unit); + if (firstTerm != null) { + sj.add(firstTerm); + } + String secondTerm = rangeTermFor(getSecondLimitOperator(intervalType), secondLimit, unit); + if (secondTerm != null) { + sj.add(secondTerm); + } + return sj.toString(); + } + + public static String makeAerosolRangeSuffix(String aerosolRange) { + return aerosolRange.replace('.', 'p').replace("<=", "le_").replace(">=", "ge_").replace("<", "lt_") + .replace(">", "gt_").replace(',', '_'); + } + /** * Check to see if this pds is a layer variable * diff --git a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Variable.java b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Variable.java index dc304b7a94..7e792f5490 100644 --- a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Variable.java +++ b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Variable.java @@ -154,6 +154,15 @@ public boolean equals(Object o) { return false; } + if (pds.isAerosol() != pds2.isAerosol()) + return false; + if (pds.isAerosol()) { + Grib2Pds.PdsAerosol pdsAerosol = (Grib2Pds.PdsAerosol) pds; + Grib2Pds.PdsAerosol pdsAerosol2 = (Grib2Pds.PdsAerosol) pds2; + if (pdsAerosol.getAerosolHashcode() != pdsAerosol2.getAerosolHashcode()) + return false; + } + // if this uses any local tables, then we have to add the center id, and subcenter if present if ((pds2.getParameterCategory() > 191) || (pds2.getParameterNumber() > 191) || (pds2.getLevelType1() > 191) || (pds2.isTimeInterval() && pds2.getStatisticalProcessType() > 191) || (ensDerivedType > 191) @@ -229,6 +238,11 @@ public int hashCode() { result += result * 31 + pdsPerc.getPercentileValue(); } + if (pds.isAerosol()) { + Grib2Pds.PdsAerosol pdsAerosol = (Grib2Pds.PdsAerosol) pds; + result += result * 31 + pdsAerosol.getAerosolHashcode(); + } + // if this uses any local tables, then we have to add the center id, and subcenter if present if ((pds.getParameterCategory() > 191) || (pds.getParameterNumber() > 191) || (pds.getLevelType1() > 191) || (pds.isTimeInterval() && pds.getStatisticalProcessType() > 191) || (ensDerivedType > 191) diff --git a/grib/src/test/data/index/rrfs.t00z.2dfld.3km.f003.conus.grib2.gbx9 b/grib/src/test/data/index/rrfs.t00z.2dfld.3km.f003.conus.grib2.gbx9 new file mode 100644 index 0000000000..009104ff96 Binary files /dev/null and b/grib/src/test/data/index/rrfs.t00z.2dfld.3km.f003.conus.grib2.gbx9 differ diff --git a/grib/src/test/data/rrfs.t00z.2dfld.3km.f003.conus.grib2 b/grib/src/test/data/rrfs.t00z.2dfld.3km.f003.conus.grib2 new file mode 100644 index 0000000000..bbe3db502d Binary files /dev/null and b/grib/src/test/data/rrfs.t00z.2dfld.3km.f003.conus.grib2 differ diff --git a/grib/src/test/java/ucar/nc2/grib/grib2/TestPds46.java b/grib/src/test/java/ucar/nc2/grib/grib2/TestPds46.java new file mode 100644 index 0000000000..d6ab75134b --- /dev/null +++ b/grib/src/test/java/ucar/nc2/grib/grib2/TestPds46.java @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2026 University Corporation for Atmospheric Research/Unidata + * See LICENSE for license information. + */ + +package ucar.nc2.grib.grib2; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ucar.nc2.grib.GribNumbers; +import ucar.nc2.grib.grib2.Grib2Pds.PdsAerosol; +import ucar.nc2.grib.grib2.Grib2Pds.PdsInterval; + +import java.util.List; +import java.util.stream.Collectors; + +import static com.google.common.truth.Truth.assertThat; + +/** + * Test data from an RRFS 2dfld product, filtered to contain just aerosol records (templates 4.46 and 4.48), and used + * to create a .gbx9 file for testing. Only the 4.48 sections are tested here. + */ +@RunWith(JUnit4.class) +public class TestPds46 { + + private List sections; + + @Before + public void openTestFile() { + String testfile = "../grib/src/test/data/index/rrfs.t00z.2dfld.3km.f003.conus.grib2.gbx9"; + + Grib2Index gi = new Grib2Index(); + boolean success = gi.readIndex(testfile, -1); + assertThat(success).isTrue(); + sections = gi.getRecords().stream().map(Grib2Record::getPDS).filter(e -> e.getTemplateNumber() == 46) + .collect(Collectors.toList()); + assertThat(sections).hasSize(2); + } + + @Test + public void testPdsBasic() { + sections.forEach(pds -> { + assertThat(pds.getRawLength()).isEqualTo(71); + assertThat(pds.getTemplateNumber()).isEqualTo(46); + }); + } + + @Test + public void testTemplateLength() { + sections.forEach(pds -> { + assertThat(pds.templateLength()).isEqualTo(71); + }); + } + + @Test + public void testIsAerosol() { + sections.forEach(pds -> { + assertThat(pds.isAerosol()).isTrue(); + }); + } + + @Test + public void testIsTimeInterval() { + sections.forEach(pds -> { + assertThat(pds.isTimeInterval()).isTrue(); + }); + } + + @Test + public void testAerosolType() { + sections.forEach(pds -> { + // 62000: Total aerosol (https://codes.ecmwf.int/grib/format/grib2/ctables/4/230/) + assertThat(((PdsAerosol) pds).getAerosolType()).isEqualTo(62000); + }); + } + + @Test + public void testAerosolIntervalSizeType() { + sections.forEach(pds -> { + assertThat(((PdsAerosol) pds).getAerosolIntervalSizeType()).isEqualTo(0); + }); + } + + @Test + public void testAerosolSize1() { + PdsAerosol pds0 = (PdsAerosol) sections.get(0); + PdsAerosol pds1 = (PdsAerosol) sections.get(1); + + assertThat(pds0.getAerosolSize1()).isWithin(1e-12).of(2.5e-6); // PM 2.5um + assertThat(pds1.getAerosolSize1()).isWithin(1e-12).of(10e-6); // PM 10um + } + + @Test + public void testAerosolSize2() { + sections.forEach(pds -> { + assertThat(((PdsAerosol) pds).getAerosolSize2()).isEqualTo(0); + }); + } + + @Test + public void testAerosolWavelengthFieldsAreUndefined() { + sections.forEach(pds -> { + PdsAerosol aerosol = (PdsAerosol) pds; + assertThat(aerosol.getAerosolIntervalWavelengthType()).isEqualTo(GribNumbers.UNDEFINED); + assertThat(aerosol.getAerosolWavelength1()).isEqualTo(GribNumbers.UNDEFINED); + assertThat(aerosol.getAerosolWavelength2()).isEqualTo(GribNumbers.UNDEFINED); + }); + } + + @Test + public void testGenProcessType() { + sections.forEach(pds -> { + assertThat(pds.getGenProcessType()).isEqualTo(2); + }); + } + + @Test + public void testBackProcessId() { + sections.forEach(pds -> { + assertThat(pds.getBackProcessId()).isEqualTo(0); + }); + } + + @Test + public void testGenProcessId() { + sections.forEach(pds -> { + assertThat(pds.getGenProcessId()).isEqualTo(134); + }); + } + + @Test + public void testTimeUnit() { + sections.forEach(pds -> { + assertThat(pds.getTimeUnit()).isEqualTo(1); + }); + } + + @Test + public void testForecastTime() { + sections.forEach(pds -> { + assertThat(pds.getForecastTime()).isEqualTo(2); + }); + } + + @Test + public void testLevelType1() { + sections.forEach(pds -> { + assertThat(pds.getLevelType1()).isEqualTo(103); + }); + } + + @Test + public void testLevelScale1() { + sections.forEach(pds -> { + assertThat(pds.getLevelScale1()).isEqualTo(0); + }); + } + + @Test + public void testLevelValue1() { + sections.forEach(pds -> { + assertThat(pds.getLevelValue1()).isEqualTo(8); + }); + } + + @Test + public void testLevelType2() { + sections.forEach(pds -> { + assertThat(pds.getLevelType2()).isEqualTo(255); + }); + } + + @Test + public void testLevelScale2() { + sections.forEach(pds -> { + assertThat(pds.getLevelScale2()).isEqualTo(0); + }); + } + + @Test + public void testLevelValue2() { + sections.forEach(pds -> { + assertThat(pds.getLevelValue2()).isEqualTo(0); + }); + } + + @Test + public void testIntervalTimeEnd() { + sections.forEach(pds -> { + assertThat(((PdsInterval) pds).getIntervalTimeEnd().toString()).isEqualTo("2026-09-08T03:00:00Z"); + }); + } + + @Test + public void testNumberTimeRanges() { + sections.forEach(pds -> { + assertThat(((PdsInterval) pds).getNumberTimeRanges()).isEqualTo(1); + }); + } + + @Test + public void testNumberMissing() { + sections.forEach(pds -> { + assertThat(((PdsInterval) pds).getNumberMissing()).isEqualTo(0); + }); + } + + @Test + public void testStatisticalProcessType() { + sections.forEach(pds -> { + assertThat(pds.getStatisticalProcessType()).isEqualTo(0); + }); + } + + @Test + public void testTimeIntervals() { + sections.forEach(pds -> { + Grib2Pds.TimeInterval[] tis = ((PdsInterval) pds).getTimeIntervals(); + assertThat(tis).hasLength(1); + Grib2Pds.TimeInterval ti = tis[0]; + assertThat(ti.statProcessType).isEqualTo(0); + assertThat(ti.timeIncrementType).isEqualTo(2); + assertThat(ti.timeRangeUnit).isEqualTo(1); + assertThat(ti.timeRangeLength).isEqualTo(1); + assertThat(ti.timeIncrementUnit).isEqualTo(255); + assertThat(ti.timeIncrement).isEqualTo(0); + }); + } +} + +// grib_dump -O rrfs.t00z.2dfld.3km.f003.conus.grib2 +// ***** FILE: rrfs.t00z.2dfld.3km.f003.conus.grib2 +// ... +// ====================== SECTION_4 ( length=71, padding=0 ) ====================== +// 1-4 section4Length = 71 +// 5 numberOfSection = 4 +// 6-7 NV = 0 +// 8-9 productDefinitionTemplateNumber = 46 [Average, accumulation, extreme values or other statistically +// processed values at a horizontal level or in a horizontal layer in a continuous or non-continuous time +// interval for atmospheric aerosol (grib2/tables/2/4.0.table) ] +// 10 parameterCategory = 20 [Atmospheric chemical or physical constituents (grib2/tables/2/4.1.0.table) ] +// 11 parameterNumber = 0 [Mass density (concentration) (grib2/tables/2/4.2.0.20.table) ] +// 12-13 constituentType = 62000 [Unknown code table entry () ] +// 14 typeOfSizeInterval = 0 [Below lower limit (grib2/tables/2/4.91.table) ] +// 15 scaleFactorOfFirstSize = 7 +// 16-19 scaledValueOfFirstSize = 25 +// 20 scaleFactorOfSecondSize = 0 +// 21-24 scaledValueOfSecondSize = 0 +// 25 typeOfGeneratingProcess = 2 [Forecast (grib2/tables/2/4.3.table) ] +// 26 backgroundProcess = 0 +// 27 generatingProcessIdentifier = 134 +// 28-29 hoursAfterDataCutoff = 0 +// 30 minutesAfterDataCutoff = 0 +// 31 indicatorOfUnitForForecastTime = 1 [Hour (grib2/tables/2/4.4.table) ] +// 32-35 forecastTime = 2 +// 36 typeOfFirstFixedSurface = 103 [Specified height level above ground (m) (grib2/tables/2/4.5.table , +// grib2/tables/local/kwbc/1/4.5.table) ] +// 37 scaleFactorOfFirstFixedSurface = 0 +// 38-41 scaledValueOfFirstFixedSurface = 8 +// 42 typeOfSecondFixedSurface = 255 [Missing (grib2/tables/2/4.5.table , grib2/tables/local/kwbc/1/4.5.table) ] +// 43 scaleFactorOfSecondFixedSurface = 0 +// 44-47 scaledValueOfSecondFixedSurface = 0 +// 48-49 yearOfEndOfOverallTimeInterval = 2026 +// 50 monthOfEndOfOverallTimeInterval = 9 +// 51 dayOfEndOfOverallTimeInterval = 8 +// 52 hourOfEndOfOverallTimeInterval = 3 +// 53 minuteOfEndOfOverallTimeInterval = 0 +// 54 secondOfEndOfOverallTimeInterval = 0 +// 55 numberOfTimeRanges = 1 +// 56-59 numberOfMissingInStatisticalProcess = 0 +// 60 typeOfStatisticalProcessing = 0 [Average (grib2/tables/2/4.10.table) ] +// 61 typeOfTimeIncrement = 2 [Successive times processed have same start time of forecast, forecast time is +// incremented (grib2/tables/2/4.11.table) ] +// 62 indicatorOfUnitForTimeRange = 1 [Hour (grib2/tables/2/4.4.table) ] +// 63-66 lengthOfTimeRange = 1 +// 67 indicatorOfUnitForTimeIncrement = 255 [Missing (grib2/tables/2/4.4.table) ] +// 68-71 timeIncrement = 0 +// ... +// ====================== SECTION_4 ( length=71, padding=0 ) ====================== +// 1-4 section4Length = 71 +// 5 numberOfSection = 4 +// 6-7 NV = 0 +// 8-9 productDefinitionTemplateNumber = 46 [Average, accumulation, extreme values or other statistically +// processed values at a horizontal level or in a horizontal layer in a continuous or non-continuous time +// interval for atmospheric aerosol (grib2/tables/2/4.0.table) ] +// 10 parameterCategory = 20 [Atmospheric chemical or physical constituents (grib2/tables/2/4.1.0.table) ] +// 11 parameterNumber = 0 [Mass density (concentration) (grib2/tables/2/4.2.0.20.table) ] +// 12-13 constituentType = 62000 [Unknown code table entry () ] +// 14 typeOfSizeInterval = 0 [Below lower limit (grib2/tables/2/4.91.table) ] +// 15 scaleFactorOfFirstSize = 7 +// 16-19 scaledValueOfFirstSize = 100 +// 20 scaleFactorOfSecondSize = 0 +// 21-24 scaledValueOfSecondSize = 0 +// 25 typeOfGeneratingProcess = 2 [Forecast (grib2/tables/2/4.3.table) ] +// 26 backgroundProcess = 0 +// 27 generatingProcessIdentifier = 134 +// 28-29 hoursAfterDataCutoff = 0 +// 30 minutesAfterDataCutoff = 0 +// 31 indicatorOfUnitForForecastTime = 1 [Hour (grib2/tables/2/4.4.table) ] +// 32-35 forecastTime = 2 +// 36 typeOfFirstFixedSurface = 103 [Specified height level above ground (m) (grib2/tables/2/4.5.table , +// grib2/tables/local/kwbc/1/4.5.table) ] +// 37 scaleFactorOfFirstFixedSurface = 0 +// 38-41 scaledValueOfFirstFixedSurface = 8 +// 42 typeOfSecondFixedSurface = 255 [Missing (grib2/tables/2/4.5.table , grib2/tables/local/kwbc/1/4.5.table) ] +// 43 scaleFactorOfSecondFixedSurface = 0 +// 44-47 scaledValueOfSecondFixedSurface = 0 +// 48-49 yearOfEndOfOverallTimeInterval = 2026 +// 50 monthOfEndOfOverallTimeInterval = 9 +// 51 dayOfEndOfOverallTimeInterval = 8 +// 52 hourOfEndOfOverallTimeInterval = 3 +// 53 minuteOfEndOfOverallTimeInterval = 0 +// 54 secondOfEndOfOverallTimeInterval = 0 +// 55 numberOfTimeRanges = 1 +// 56-59 numberOfMissingInStatisticalProcess = 0 +// 60 typeOfStatisticalProcessing = 0 [Average (grib2/tables/2/4.10.table) ] +// 61 typeOfTimeIncrement = 2 [Successive times processed have same start time of forecast, forecast time is +// incremented (grib2/tables/2/4.11.table) ] +// 62 indicatorOfUnitForTimeRange = 1 [Hour (grib2/tables/2/4.4.table) ] +// 63-66 lengthOfTimeRange = 1 +// 67 indicatorOfUnitForTimeIncrement = 255 [Missing (grib2/tables/2/4.4.table) ] +// 68-71 timeIncrement = 0 +// ... diff --git a/grib/src/test/java/ucar/nc2/grib/grib2/TestPdsAerosol.java b/grib/src/test/java/ucar/nc2/grib/grib2/TestPdsAerosol.java new file mode 100644 index 0000000000..a803aa951b --- /dev/null +++ b/grib/src/test/java/ucar/nc2/grib/grib2/TestPdsAerosol.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2026 University Corporation for Atmospheric Research/Unidata + * See LICENSE for license information. + */ + +package ucar.nc2.grib.grib2; + +import com.google.common.io.BaseEncoding; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import ucar.nc2.NetcdfFile; +import ucar.nc2.NetcdfFiles; +import ucar.nc2.Variable; +import ucar.nc2.grib.collection.Grib; +import ucar.nc2.grib.grib2.Grib2Pds.PdsAerosol; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.google.common.truth.Truth.assertThat; + +/** + * Test data taken from a sample RRFS 2dfld 3km conus product, filtered to contain only aerosol records. + */ +@RunWith(JUnit4.class) +public class TestPdsAerosol { + + private static byte[] decode(String chars) { + return BaseEncoding.base16().decode(chars); + } + + private static byte[] newPds46() { + // string-encoded byte[] using octets from rrfs.t00z.2dfld.3km.f003.conus.grib2, modified to have no intervals + return decode("00000047040000002E1400F230FF000000000000000000000200860000000100000002670000000008FF000000000007EA09" + + "08030000010000000000020100000001FF00000000"); + } + + private static byte[] newPds48() { + // string-encoded byte[] using octets from rrfs.t00z.2dfld.3km.f003.conus.grib2, modified to have no intervals + return decode("0000003A04000000301400F231FF00000000000000000000FF00000000000000000000020086000000010000000367000000" + + "0008FF0000000000"); + } + + private static Grib2Pds makePds(byte[] data) { + return new Grib2SectionProductDefinition(data).getPDS(); + } + + private static String aerosolRange(byte[] data) { + return ((PdsAerosol) makePds(data)).getAerosolRange(); + } + + private static byte[] withInterval(byte[] data, int offset, int intervalType, int firstLimitScale, + int firstLimitValue, int secondLimitScale, int secondLimitValue) { + data[offset] = (byte) intervalType; + data[offset + 1] = (byte) firstLimitScale; + data[offset + 5] = (byte) firstLimitValue; + data[offset + 6] = (byte) secondLimitScale; + data[offset + 10] = (byte) secondLimitValue; + return data; + } + + @Test + public void testPds46SizeInterval() { + // 13: byte offset of size interval in Pds46 + assertThat(aerosolRange(withInterval(newPds46(), 13, 0, 7, 25, 5, 1))).isEqualTo("<2.5um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 1, 7, 25, 5, 1))).isEqualTo(">10um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 2, 7, 25, 5, 1))).isEqualTo(">=2.5um,<10um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 3, 7, 25, 5, 1))).isEqualTo(">2.5um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 4, 7, 25, 5, 1))).isEqualTo("<10um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 5, 7, 25, 5, 1))).isEqualTo("<=2.5um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 6, 7, 25, 5, 1))).isEqualTo(">=10um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 7, 7, 25, 5, 1))).isEqualTo(">=2.5um,<=10um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 8, 7, 25, 5, 1))).isEqualTo(">=2.5um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 9, 7, 25, 5, 1))).isEqualTo("<=10um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 10, 7, 25, 5, 1))).isEqualTo(">2.5um,<=10um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 11, 7, 25, 5, 1))).isEqualTo("2.5um"); + assertThat(aerosolRange(withInterval(newPds46(), 13, 12, 7, 25, 5, 1))).isEqualTo(""); + assertThat(aerosolRange(withInterval(newPds46(), 13, 255, 7, 25, 5, 1))).isEqualTo(""); + } + + @Test + public void testPds48SizeInterval() { + // 13: byte offset of size interval in Pds48 + assertThat(aerosolRange(withInterval(newPds48(), 13, 0, 7, 25, 5, 1))).isEqualTo("<2.5um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 1, 7, 25, 5, 1))).isEqualTo(">10um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 2, 7, 25, 5, 1))).isEqualTo(">=2.5um,<10um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 3, 7, 25, 5, 1))).isEqualTo(">2.5um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 4, 7, 25, 5, 1))).isEqualTo("<10um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 5, 7, 25, 5, 1))).isEqualTo("<=2.5um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 6, 7, 25, 5, 1))).isEqualTo(">=10um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 7, 7, 25, 5, 1))).isEqualTo(">=2.5um,<=10um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 8, 7, 25, 5, 1))).isEqualTo(">=2.5um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 9, 7, 25, 5, 1))).isEqualTo("<=10um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 10, 7, 25, 5, 1))).isEqualTo(">2.5um,<=10um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 11, 7, 25, 5, 1))).isEqualTo("2.5um"); + assertThat(aerosolRange(withInterval(newPds48(), 13, 12, 7, 25, 5, 1))).isEqualTo(""); + assertThat(aerosolRange(withInterval(newPds48(), 13, 255, 7, 25, 5, 1))).isEqualTo(""); + } + + @Test + public void testPds48WavelengthInterval() { + // 24: byte offset of wavelength interval in Pds48 + assertThat(aerosolRange(withInterval(newPds48(), 24, 0, 8, 55, 8, 124))).isEqualTo("<550nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 1, 8, 55, 8, 124))).isEqualTo(">1240nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 2, 8, 55, 8, 124))).isEqualTo(">=550nm,<1240nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 3, 8, 55, 8, 124))).isEqualTo(">550nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 4, 8, 55, 8, 124))).isEqualTo("<1240nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 5, 8, 55, 8, 124))).isEqualTo("<=550nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 6, 8, 55, 8, 124))).isEqualTo(">=1240nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 7, 8, 55, 8, 124))).isEqualTo(">=550nm,<=1240nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 8, 8, 55, 8, 124))).isEqualTo(">=550nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 9, 8, 55, 8, 124))).isEqualTo("<=1240nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 10, 8, 55, 8, 124))).isEqualTo(">550nm,<=1240nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 11, 8, 55, 8, 124))).isEqualTo("550nm"); + assertThat(aerosolRange(withInterval(newPds48(), 24, 12, 8, 55, 8, 124))).isEqualTo(""); + assertThat(aerosolRange(withInterval(newPds48(), 24, 255, 8, 55, 8, 124))).isEqualTo(""); + } + + @Test + public void testPds48AerosolSizeAndWavelengthIntervals() { + // theoretically possible to have both intervals at the same time... + byte[] data = newPds48(); + + data = withInterval(data, 13, 7, 7, 25, 5, 1); + data = withInterval(data, 24, 7, 8, 55, 8, 124); + + assertThat(aerosolRange(data)).isEqualTo(">=2.5um,<=10um >=550nm,<=1240nm"); + } + + @Test + public void testDistinctAerosolVariableNames() throws IOException { + try (NetcdfFile nc = NetcdfFiles.open("../grib/src/test/data/rrfs.t00z.2dfld.3km.f003.conus.grib2")) { + + List variables = nc.getVariables().stream().filter(e -> e.getFullName().toLowerCase().contains("mass")) + .collect(Collectors.toList()); + + List varNames = variables.stream().map(Variable::getFullName).collect(Collectors.toList()); + assertThat(varNames).containsExactly( + "Atmosphere_emission_mass_flux_entire_atmosphere_single_layer_Dust_dry_lt_10um", + "Atmosphere_emission_mass_flux_entire_atmosphere_single_layer_Particulate_organic_matter_dry_lt_2p5um", + "Column-integrated_mass_density_entire_atmosphere_single_layer_Dust_dry_ge_2p5um_lt_10um", + "Column-integrated_mass_density_entire_atmosphere_single_layer_Dust_dry_lt_10um", + "Column-integrated_mass_density_entire_atmosphere_single_layer_Dust_dry_lt_2p5um", + "Column-integrated_mass_density_entire_atmosphere_single_layer_Particulate_organic_matter_dry_lt_2p5um", + "Mass_density_concentration_height_above_ground_1_Hour_Average_Total_aerosol_lt_10um", + "Mass_density_concentration_height_above_ground_1_Hour_Average_Total_aerosol_lt_2p5um", + "Mass_density_concentration_height_above_ground_Dust_dry_ge_2p5um_lt_10um", + "Mass_density_concentration_height_above_ground_Dust_dry_lt_2p5um", + "Mass_density_concentration_height_above_ground_Particulate_organic_matter_dry_lt_2p5um"); + } + } + + @Test + public void testDistinctAerosolVariableDescriptions() throws IOException { + try (NetcdfFile nc = NetcdfFiles.open("../grib/src/test/data/rrfs.t00z.2dfld.3km.f003.conus.grib2")) { + + List variables = nc.getVariables().stream().filter(e -> e.getFullName().toLowerCase().contains("mass")) + .collect(Collectors.toList()); + + List varDescriptions = variables.stream().map(Variable::getDescription).collect(Collectors.toList()); + assertThat(varDescriptions).containsExactly( + "Atmosphere emission mass flux (Dust dry <10um) @ Entire atmosphere layer", + "Atmosphere emission mass flux (Particulate organic matter dry <2.5um) @ Entire atmosphere layer", + "Column-integrated mass density (Dust dry >=2.5um,<10um) @ Entire atmosphere layer", + "Column-integrated mass density (Dust dry <10um) @ Entire atmosphere layer", + "Column-integrated mass density (Dust dry <2.5um) @ Entire atmosphere layer", + "Column-integrated mass density (Particulate organic matter dry <2.5um) @ Entire atmosphere layer", + "Mass density (concentration) (1_Hour Average) (Total aerosol <10um) @ Specified height level above ground", + "Mass density (concentration) (1_Hour Average) (Total aerosol <2.5um) @ Specified height level above ground", + "Mass density (concentration) (Dust dry >=2.5um,<10um) @ Specified height level above ground", + "Mass density (concentration) (Dust dry <2.5um) @ Specified height level above ground", + "Mass density (concentration) (Particulate organic matter dry <2.5um) @ Specified height level above ground"); + } + } + + @Test + public void testDistinctAerosolVariableGribIds() throws IOException { + try (NetcdfFile nc = NetcdfFiles.open("../grib/src/test/data/rrfs.t00z.2dfld.3km.f003.conus.grib2")) { + + List variables = nc.getVariables().stream().filter(e -> e.getFullName().toLowerCase().contains("mass")) + .collect(Collectors.toList()); + + List varIds = variables.stream() + .map(v -> Objects.requireNonNull(v.findAttribute(Grib.VARIABLE_ID_ATTNAME)).getStringValue()) + .collect(Collectors.toList()); + assertThat(varIds).containsExactly("VAR_0-20-0_L103_A62001_ge_2p5um_lt_10um", "VAR_0-20-0_L103_A62001_lt_2p5um", + "VAR_0-20-0_L103_A62010_lt_2p5um", "VAR_0-20-0_L103_I1_Hour_S0_A62000_lt_10um", + "VAR_0-20-0_L103_I1_Hour_S0_A62000_lt_2p5um", "VAR_0-20-1_L200_A62001_ge_2p5um_lt_10um", + "VAR_0-20-1_L200_A62001_lt_10um", "VAR_0-20-1_L200_A62001_lt_2p5um", "VAR_0-20-1_L200_A62010_lt_2p5um", + "VAR_0-20-3_L200_A62001_lt_10um", "VAR_0-20-3_L200_A62010_lt_2p5um"); + } + } +} diff --git a/uicdm/src/main/java/ucar/nc2/ui/grib/Grib2CollectionPanel.java b/uicdm/src/main/java/ucar/nc2/ui/grib/Grib2CollectionPanel.java index 243e69f639..995749986a 100644 --- a/uicdm/src/main/java/ucar/nc2/ui/grib/Grib2CollectionPanel.java +++ b/uicdm/src/main/java/ucar/nc2/ui/grib/Grib2CollectionPanel.java @@ -1406,29 +1406,29 @@ public int getAerType() { return ((Grib2Pds.PdsAerosol) pds).getAerosolType(); } - public double getAerIntSizeType() { + public int getAerIntSizeType() { return ((Grib2Pds.PdsAerosol) pds).getAerosolIntervalSizeType(); } public double getAerSize1() { - return ((Grib2Pds.PdsAerosol) pds).getAerosolSize1() * 10e6; + return ((Grib2Pds.PdsAerosol) pds).getAerosolSize1() * 1e6; } // microns public double getAerSize2() { - return ((Grib2Pds.PdsAerosol) pds).getAerosolSize2() * 10e6; + return ((Grib2Pds.PdsAerosol) pds).getAerosolSize2() * 1e6; } // microns - public double getAerIntWavelType() { + public int getAerIntWavelType() { return ((Grib2Pds.PdsAerosol) pds).getAerosolIntervalWavelengthType(); } public double getAerWavel1() { - return ((Grib2Pds.PdsAerosol) pds).getAerosolWavelength1(); - } + return ((Grib2Pds.PdsAerosol) pds).getAerosolWavelength1() * 1e9; + } // nanometers public double getAerWavel2() { - return ((Grib2Pds.PdsAerosol) pds).getAerosolWavelength2(); - } + return ((Grib2Pds.PdsAerosol) pds).getAerosolWavelength2() * 1e9; + } // nanometers /////////////////////////////// // Ensembles