Skip to content

Commit 9a528fc

Browse files
authored
Merge pull request #1603 from lesserwhirls/valid
Unpacked valid min/max values
2 parents e6895b4 + b722ea0 commit 9a528fc

4 files changed

Lines changed: 95 additions & 1 deletion

File tree

cdm/core/src/main/java/ucar/nc2/dataset/VariableDS.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,9 @@ private void createEnhancements() {
973973
this.unsignedConversion = UnsignedConversion.createFromVar(this);
974974
this.dataType = unsignedConversion.getOutType();
975975
}
976+
// this needs to be created before the scale/offset enhancement
977+
// to properly handle the case where a variable is packed but the valid
978+
// max/min values (or range) are not.
976979
if (this.enhanceMode.contains(Enhance.ConvertMissing)) {
977980
this.convertMissing = ConvertMissing.createFromVariable(this);
978981
}

cdm/core/src/main/java/ucar/nc2/filter/ConvertMissing.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
/*
2+
* Copyright (c) 2022-2026 University Corporation for Atmospheric Research/Unidata
3+
* See LICENSE for license information.
4+
*/
5+
16
package ucar.nc2.filter;
27

38
import ucar.ma2.Array;
49
import ucar.ma2.DataType;
10+
import ucar.ma2.DataType.Signedness;
511
import ucar.ma2.IndexIterator;
612
import ucar.nc2.Attribute;
713
import ucar.nc2.constants.CDM;
@@ -14,6 +20,7 @@
1420
public class ConvertMissing implements Enhancement {
1521

1622
private boolean hasValidMin, hasValidMax;
23+
// If variable is packed and these must be packed values
1724
private double validMin, validMax;
1825

1926
private boolean hasFillValue;
@@ -35,12 +42,14 @@ public static ConvertMissing createFromVariable(VariableDS var) {
3542
// assume here its in units of unpacked data. correct this below
3643
Attribute validRangeAtt = var.findAttribute(CDM.VALID_RANGE);
3744
DataType validType = null;
45+
boolean validRangeDifferentDataType = false;
3846
if (validRangeAtt != null && !validRangeAtt.isString() && validRangeAtt.getLength() > 1) {
3947
validType = FilterHelpers.getAttributeDataType(validRangeAtt, signedness);
4048
validMin = var.convertUnsigned(validRangeAtt.getNumericValue(0), validType).doubleValue();
4149
validMax = var.convertUnsigned(validRangeAtt.getNumericValue(1), validType).doubleValue();
4250
hasValidMin = true;
4351
hasValidMax = true;
52+
validRangeDifferentDataType = !validType.equals(var.getDataType());
4453
}
4554

4655
Attribute validMinAtt = var.findAttribute(CDM.VALID_MIN);
@@ -52,15 +61,27 @@ public static ConvertMissing createFromVariable(VariableDS var) {
5261
validType = FilterHelpers.getAttributeDataType(validMinAtt, signedness);
5362
validMin = var.convertUnsigned(validMinAtt.getNumericValue(), validType).doubleValue();
5463
hasValidMin = true;
64+
validRangeDifferentDataType = !validType.equals(var.getDataType());
5565
}
5666

5767
if (validMaxAtt != null && !validMaxAtt.isString()) {
5868
validType = FilterHelpers.largestOf(validType, FilterHelpers.getAttributeDataType(validMaxAtt, signedness));
5969
validMax = var.convertUnsigned(validMaxAtt.getNumericValue(), validType).doubleValue();
6070
hasValidMax = true;
71+
validRangeDifferentDataType = !validType.equals(var.getDataType());
6172
}
6273
}
6374

75+
if (validRangeDifferentDataType && !signedness.equals(Signedness.UNSIGNED)) {
76+
// Signal that valid range (or min/max) was specified in unpacked values, so we
77+
// need to repack those values. Only applies when the DataTypes do not match because
78+
// the variable is unsigned.
79+
double scale = var.attributes().findAttributeDouble(CDM.SCALE_FACTOR, 1);
80+
double offset = var.attributes().findAttributeDouble(CDM.ADD_OFFSET, 0);
81+
validMin = (validMin - offset) / scale;
82+
validMax = (validMax - offset) / scale;
83+
}
84+
6485
if (validMin > validMax) {
6586
double temp = validMin;
6687
validMin = validMax;
@@ -113,6 +134,7 @@ public ConvertMissing(boolean fillValueIsMissing, boolean invalidDataIsMissing,
113134
this.missingDataIsMissing = missingDataIsMissing;
114135
this.hasValidMin = hasValidMin;
115136
this.hasValidMax = hasValidMax;
137+
// If variable data is packed, validMin, validMax must also be packed
116138
this.validMin = validMin;
117139
this.validMax = validMax;
118140
this.hasFillValue = hasFillValue;
@@ -150,14 +172,37 @@ public boolean hasValidData() {
150172
return hasValidMin || hasValidMax;
151173
}
152174

175+
/**
176+
*
177+
* Return the minimum valid value used to enhance a variable.
178+
* <p>
179+
* If the variable is packed, this value will also be packed.
180+
*
181+
* @return the minimum valid value as a double.
182+
*/
153183
public double getValidMin() {
154184
return validMin;
155185
}
156186

187+
/**
188+
*
189+
* Return the maximum valid value used to enhance a variable.
190+
* <p>
191+
* If the variable is packed, this value will also be packed.
192+
*
193+
* @return the maximum valid value as a double.
194+
*/
157195
public double getValidMax() {
158196
return validMax;
159197
}
160198

199+
/**
200+
*
201+
* Return true if the value is outside the valid range.
202+
*
203+
* @param val the value to test (must be packed if the variable is packed).
204+
* @return true if the value is invalid.
205+
*/
161206
public boolean isInvalidData(double val) {
162207
if (Double.isNaN(val)) {
163208
return true;

cdm/core/src/test/java/ucar/nc2/dataset/TestScaleOffsetMissing.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1998-2020 University Corporation for Atmospheric Research/Unidata
2+
* Copyright (c) 2020-2026 University Corporation for Atmospheric Research/Unidata
33
* See LICENSE for license information.
44
*/
55

@@ -26,6 +26,13 @@ public class TestScaleOffsetMissing {
2626
private static final byte expectedValidMin = 1;
2727
private static final byte expectedValidMax = 2;
2828

29+
// Same thing as above, but for variable that is packed with an unpacked valid_range (min/max)
30+
private static final float[] expectedMismatch = new float[] {NaN, 5.0f, 7.0f, 9.0f};
31+
private static final int expectedValidMinMismatchUnpacked = 4;
32+
private static final int expectedValidMaxMismatchUnpacked = 9;
33+
private static final float expectedValidMinMismatchPacked = 1.5f;
34+
private static final float expectedValidMaxMismatchPacked = 4;
35+
2936
@Rule
3037
public TemporaryFolder tempFolder = new TemporaryFolder();
3138

@@ -128,4 +135,35 @@ public void testNegScaleOffsetValidRangeDeprecatedApi() throws URISyntaxExceptio
128135
}
129136
}
130137
}
138+
139+
@Test
140+
public void testScaleOffsetValidRangeDiffTypes() throws URISyntaxException, IOException {
141+
File testResource = new File(getClass().getResource("testScaleOffsetMissing.ncml").toURI());
142+
143+
try (NetcdfDataset ncd = NetcdfDatasets.openDataset(testResource.getAbsolutePath(), true, null)) {
144+
// Same as scaleOffsetValidMaxMin, but uses valid_range attribute instead of valid_min and valid_max attributes.
145+
VariableDS var = (VariableDS) ncd.findVariable("packedUnmatchedType");
146+
147+
// Packed value of valid min, max should only be used internally to ConvertMissing, so make sure it is
148+
// not leaking through
149+
assertThat(var.getValidMin()).isNotWithin(fpTol).of(expectedValidMinMismatchPacked);
150+
assertThat(var.getValidMax()).isNotWithin(fpTol).of(expectedValidMaxMismatchPacked);
151+
// Make sure unpacked values still make it through
152+
assertThat(var.getValidMin()).isWithin(fpTol).of(expectedValidMinMismatchUnpacked);
153+
assertThat(var.getValidMax()).isWithin(fpTol).of(expectedValidMaxMismatchUnpacked);
154+
155+
// This will only work if the unpacked values of valid min/max are used by
156+
// ConvertMissing
157+
float[] actual = (float[]) var.read().getStorage();
158+
for (int i = 0; i < actual.length; i++) {
159+
if (var.isInvalidData(actual[i])) {
160+
assertThat(actual[i]).isNaN();
161+
assertThat(expectedMismatch[i]).isNaN();
162+
} else {
163+
assertThat(actual[i]).isNotNaN();
164+
assertThat(actual[i]).isWithin(fpTol).of(expectedMismatch[i]);
165+
}
166+
}
167+
}
168+
}
131169
}

cdm/core/src/test/resources/ucar/nc2/dataset/testScaleOffsetMissing.ncml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,12 @@
2626
<attribute name="valid_range" type="byte" value="0 100" />
2727
<values>-1 0 100 101</values>
2828
</variable>
29+
<variable name="packedUnmatchedType" type="int" shape="4">
30+
<attribute name="scale_factor" type="float" value="2.0" />
31+
<attribute name="add_offset" type="float" value="1.0" />
32+
<attribute name="valid_range" type="float" value="4 9" />
33+
<!-- When opening as a NetcdfDataset, the first value should be missing, where as the remaining
34+
three values should be valid. -->
35+
<values>1 2 3 4</values>
36+
</variable>
2937
</netcdf>

0 commit comments

Comments
 (0)