From ef5e555edc5557c347978b940dced8e68a7005c3 Mon Sep 17 00:00:00 2001 From: Mike Bartles Date: Thu, 13 Aug 2026 15:54:41 -0700 Subject: [PATCH] Fix silent loss of CF-conformant NetCDF grids A NetCDF variable that declares its statistic the way CF requires, as "time: mean", resolved to UNDEFINED and fell through to type inference. Inference then typed the record INSTANTANEOUS despite a non-zero interval, and the instantaneous index holds only records whose start and end times are equal, so every record was skipped. The reader reported no time range and every read came back empty. Callers received a null grid with nothing to explain it. NetcdfDataReader now extracts the method applied to the time coordinate before mapping it to a data type. VortexDataType.fromString stays a plain token mapper because it is shared with the DSS path. The entry may be keyed on either the literal "time" or the file's own time axis name, both of which CF permits. Bare tokens still resolve as they did, so files vortex wrote itself are unaffected. Inference no longer returns a point type for a record that spans a period. A record with a non-zero interval is a period type by definition, and calling it instantaneous produces a record the index cannot hold. This alone fixes the reported file, independently of any cell_methods parsing. A record dropped for disagreeing with its own declared type is now logged and reported through DataReader.isValid(). Previously isValid() returned true with an empty message list for a file that could not be read at all, so every validation gate passed and the defect surfaced only at compute time. The NetCDF writer now emits "time: " so vortex stops producing the non-conformant form it was the sole reader of. Two fixtures that differ only in that attribute, cf_style.nc and bare_mean.nc, pin both spellings against regression. Resolves: HMS-5051 --- .../mil/army/usace/hec/vortex/VortexGrid.java | 7 +- .../hec/vortex/io/GridDatasetReader.java | 13 ++- .../io/InstantaneousRecordIndexQuery.java | 32 +++++- .../usace/hec/vortex/io/NetcdfDataReader.java | 94 +++++++++++++++++- .../usace/hec/vortex/io/NetcdfWriterPrep.java | 16 ++- .../usace/hec/vortex/io/VariableDsReader.java | 13 ++- .../src/main/resources/message.properties | 1 + .../hec/vortex/io/NetcdfDataReaderTest.java | 75 ++++++++++++++ vortex-api/src/test/resources/bare_mean.cdl | 48 +++++++++ vortex-api/src/test/resources/bare_mean.nc | Bin 0 -> 15439 bytes vortex-api/src/test/resources/cf_style.cdl | 48 +++++++++ vortex-api/src/test/resources/cf_style.nc | Bin 0 -> 15439 bytes 12 files changed, 334 insertions(+), 13 deletions(-) create mode 100644 vortex-api/src/test/resources/bare_mean.cdl create mode 100644 vortex-api/src/test/resources/bare_mean.nc create mode 100644 vortex-api/src/test/resources/cf_style.cdl create mode 100644 vortex-api/src/test/resources/cf_style.nc diff --git a/vortex-api/src/main/java/mil/army/usace/hec/vortex/VortexGrid.java b/vortex-api/src/main/java/mil/army/usace/hec/vortex/VortexGrid.java index 317e1f48..0065fd44 100644 --- a/vortex-api/src/main/java/mil/army/usace/hec/vortex/VortexGrid.java +++ b/vortex-api/src/main/java/mil/army/usace/hec/vortex/VortexGrid.java @@ -327,10 +327,13 @@ public float[][][] data3D() { private VortexDataType inferDataType() { if (interval == null || interval.isZero()) return VortexDataType.INSTANTANEOUS; + // Only a non-zero interval reaches here, so the record spans a period by definition. Naming it a + // point type would make it self-contradictory, and InstantaneousRecordIndexQuery would then drop + // every such record from its index. AVERAGE is the safe period type for an unrecognized variable: + // ACCUMULATION would imply the values are summable over the interval, which is not knowable here. return switch (VortexVariable.fromName(shortName)) { case PRECIPITATION -> VortexDataType.ACCUMULATION; - case TEMPERATURE, SHORTWAVE_RADIATION, WINDSPEED, PRESSURE -> VortexDataType.AVERAGE; - default -> VortexDataType.INSTANTANEOUS; + default -> VortexDataType.AVERAGE; }; } diff --git a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/GridDatasetReader.java b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/GridDatasetReader.java index 3bbf232b..edc4db5a 100644 --- a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/GridDatasetReader.java +++ b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/GridDatasetReader.java @@ -1,6 +1,7 @@ package mil.army.usace.hec.vortex.io; import mil.army.usace.hec.vortex.VortexData; +import mil.army.usace.hec.vortex.VortexDataType; import mil.army.usace.hec.vortex.VortexGrid; import mil.army.usace.hec.vortex.geo.*; import mil.army.usace.hec.vortex.util.UnitUtil; @@ -235,7 +236,7 @@ private VortexGrid buildGrid(float[] data, VortexDataInterval timeRecord) { .startTime(timeRecord.startTime()) .endTime(timeRecord.endTime()) .interval(timeRecord.getRecordDuration()) - .dataType(getVortexDataType(variableDS)) + .dataType(getDeclaredDataType()) .build(); } @@ -416,6 +417,16 @@ private VortexDataInterval adjustTimeForSpecialFile(CoordinateAxis1DTime tAxis, return VortexDataInterval.of(adjustedStart, adjustedEnd); } + private String getTimeAxisName() { + CoordinateAxis timeAxis = gridCoordSystem.getTimeAxis(); + return timeAxis != null ? timeAxis.getShortName() : null; + } + + @Override + VortexDataType getDeclaredDataType() { + return getVortexDataType(variableDS, getTimeAxisName()); + } + private boolean isSpecialTimeBounds() { return specialFileType != null && specialFileType != UNDEFINED; } diff --git a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/InstantaneousRecordIndexQuery.java b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/InstantaneousRecordIndexQuery.java index 1d87b445..42122e5e 100644 --- a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/InstantaneousRecordIndexQuery.java +++ b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/InstantaneousRecordIndexQuery.java @@ -91,17 +91,45 @@ private static List queryPeriod(NavigableMap in private static NavigableMap initInstantaneousDataTree(List recordList) { TreeMap treeMap = new TreeMap<>(); + int undefinedCount = 0; + int spanningCount = 0; + for (int i = 0; i < recordList.size(); i++) { VortexDataInterval timeRecord = recordList.get(i); - boolean isUndefined = !VortexDataInterval.isDefined(timeRecord); - if (isUndefined || !timeRecord.isInstantaneous()) { + if (!VortexDataInterval.isDefined(timeRecord)) { + undefinedCount++; + continue; + } + + if (!timeRecord.isInstantaneous()) { + spanningCount++; continue; } treeMap.put(timeRecord.startTime(), i); } + logSkippedRecords(recordList.size(), undefinedCount, spanningCount); + return Collections.unmodifiableNavigableMap(treeMap); } + + /** + * A record that spans a period cannot be indexed as an instant, so it is dropped. That is a + * classification defect — the data was typed INSTANTANEOUS but its start and end times differ — and + * dropping every record leaves the reader with no time range at all. Report it rather than let the + * caller discover it as an empty read. + */ + private static void logSkippedRecords(int total, int undefinedCount, int spanningCount) { + if (spanningCount > 0) { + logger.warning(() -> "Skipped " + spanningCount + " of " + total + " instantaneous records " + + "with differing start and end times. Data typed as instantaneous must not span a " + + "period; check the source's cell_methods and time bounds."); + } + + if (undefinedCount > 0) { + logger.info(() -> "Skipped " + undefinedCount + " of " + total + " instantaneous records with undefined times."); + } + } } diff --git a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/NetcdfDataReader.java b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/NetcdfDataReader.java index fed4e31b..fff7695f 100644 --- a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/NetcdfDataReader.java +++ b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/NetcdfDataReader.java @@ -24,12 +24,25 @@ import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +/** + * Reads grids from NetCDF files written against the CF conventions. No CF version is enforced on + * read: the Conventions attribute is never inspected. The subset this reader consumes — time + * cell_methods, time_bnds, lat/lon/time axes, fill values and CRS WKT — is common to every CF + * version from the first through CF-1.11, as is the bare-token cell_methods form ("mean", "sum", + * "point") that vortex itself wrote before it keyed methods on the time dimension. What falls + * outside that subset is not rejected but left UNDEFINED, deferring to + * {@link mil.army.usace.hec.vortex.VortexGrid}'s type inference. + */ abstract class NetcdfDataReader extends DataReader { private static final Logger logger = Logger.getLogger(NetcdfDataReader.class.getName()); private static final PathMatcher NC_MATCHER = FileSystems.getDefault().getPathMatcher("regex:(?i).*\\.nc4?"); private static final String TIME_BOUNDS = "time_bnds"; + private static final Pattern CELL_METHODS_QUALIFIER = Pattern.compile("\\([^)]*\\)"); + private static final Pattern TIME_CELL_METHOD = compileCellMethodPattern(CF.TIME); /* Factory Method */ public static NetcdfDataReader createInstance(String pathToFile, String pathToData) throws DataReadException { @@ -154,28 +167,99 @@ static void shiftGrid(Grid grid) { } } - static VortexDataType getVortexDataType(VariableDS variableDS) { + /** + * Resolves a variable's data type from its CF {@code cell_methods} attribute. + * + * @param timeAxisName the short name of the time coordinate variable, or null when it is unknown. + * CF allows a cell_methods entry to be keyed on the time coordinate variable's + * own name rather than the literal "time"; both are accepted. + */ + static VortexDataType getVortexDataType(VariableDS variableDS, String timeAxisName) { String cellMethods = variableDS.findAttributeString(CF.CELL_METHODS, ""); - return VortexDataType.fromString(cellMethods); + return VortexDataType.fromString(parseTimeCellMethod(cellMethods, timeAxisName)); + } + + /** + * Extracts the method applied to the time coordinate from a CF cell_methods string. CF §7.3 + * defines the attribute as a blank-separated list of "name: method [(qualifiers)]" entries — a + * grammar unchanged across every CF version — so the method has to be pulled out before it can be + * mapped to a {@link VortexDataType}. A string with no + * "name:" entry is returned unchanged, so the bare tokens written by {@link NetcdfWriterPrep} + * before it emitted CF-conformant output ("mean", "sum", "point") keep resolving as they always have. + * + * @return the method applied to the time coordinate, or an empty string when the attribute declares + * no method for it (a cell_methods of "area: mean" says nothing about how time was reduced). + */ + static String parseTimeCellMethod(String cellMethods, String timeAxisName) { + if (cellMethods == null || cellMethods.isBlank()) return ""; + + // Qualifiers are dropped first: "(interval: 1 day)" contains a colon of its own. + String stripped = CELL_METHODS_QUALIFIER.matcher(cellMethods).replaceAll(" "); + if (!stripped.contains(":")) return stripped.trim(); + + Matcher matcher = timeCellMethodPattern(timeAxisName).matcher(stripped); + return matcher.find() ? matcher.group(1) : ""; + } + + private static Pattern timeCellMethodPattern(String timeAxisName) { + boolean isDefaultName = timeAxisName == null || timeAxisName.isBlank() || timeAxisName.equalsIgnoreCase(CF.TIME); + if (isDefaultName) return TIME_CELL_METHOD; + return compileCellMethodPattern(CF.TIME + "|" + Pattern.quote(timeAxisName)); + } + + private static Pattern compileCellMethodPattern(String names) { + return Pattern.compile("(?:^|\\s)(?:" + names + ")\\s*:\\s*([A-Za-z_]+)", Pattern.CASE_INSENSITIVE); } @Override public Validation isValid() { + List messages = new ArrayList<>(); + try (NetcdfDataset dataset = NetcdfDatasets.openDataset(path)) { Path pathToFile = Path.of(path); if (NC_MATCHER.matches(pathToFile)) { Variable variable = dataset.findVariable(TIME_BOUNDS); if (variable == null) { - String message = Message.format("warn_nc_time_bnds"); - return Validation.of(true, message); + messages.add(Message.format("warn_nc_time_bnds")); } } } catch (IOException e) { String message = Message.format("error_invalid_file", path); return Validation.of(false, message); } - return Validation.of(true); + + if (hasSpanningInstantaneousRecords()) { + messages.add(Message.format("warn_nc_instantaneous_span", variableName)); + } + + return messages.isEmpty() ? Validation.of(true) : Validation.of(true, messages); } + /** + * Reports whether the variable declares itself instantaneous while its time bounds span a period. + * Such records cannot be indexed as instants and are dropped, which leaves the reader with no time + * range and every read empty. Without this check the condition is invisible until compute time. + */ + private boolean hasSpanningInstantaneousRecords() { + if (getDeclaredDataType() != VortexDataType.INSTANTANEOUS) { + return false; + } + + try { + return getDataIntervals().stream() + .filter(VortexDataInterval::isDefined) + .anyMatch(interval -> !interval.isInstantaneous()); + } catch (DataReadException e) { + logger.log(Level.INFO, e, e::getMessage); + return false; + } + } + + /** + * The data type declared by the source variable's CF cell_methods attribute, before any inference + * {@link mil.army.usace.hec.vortex.VortexGrid#dataType()} applies on top of it. + */ + abstract VortexDataType getDeclaredDataType(); + abstract double getNoDataValue(); } diff --git a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/NetcdfWriterPrep.java b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/NetcdfWriterPrep.java index eb0275ee..0387f10b 100644 --- a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/NetcdfWriterPrep.java +++ b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/NetcdfWriterPrep.java @@ -232,7 +232,7 @@ private static void addVariableProjection(NetcdfFormatWriter.Builder writerBuild } // Adding CRS WKT for Grid's Coordinate System information - // CF Conventions: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.11/cf-conventions.html#use-of-the-crs-well-known-text-format + // CF Conventions: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#use-of-the-crs-well-known-text-format variableBuilder.addAttribute(new Attribute("crs_wkt", gridCollection.getWkt())); } @@ -263,11 +263,23 @@ private static void addVariableGridCollection(NetcdfFormatWriter.Builder writerB .addAttribute(new Attribute(CF.COORDINATES, "latitude longitude")) .addAttribute(new Attribute(CF.MISSING_VALUE, (float) vortexGrid.noDataValue())) .addAttribute(new Attribute(CF._FILLVALUE, (float) vortexGrid.noDataValue())) - .addAttribute(new Attribute(CF.CELL_METHODS, vortexGrid.dataType().getNcString())); + .addAttribute(new Attribute(CF.CELL_METHODS, getCellMethods(vortexGrid))); } } + /** + * Builds the CF cell_methods attribute for a grid. CF-1.10 §7.3 requires each entry to name the + * dimension the method was applied to, so the method is keyed on the time dimension this writer + * creates, and the file declares "CF-1.10" globally. Earlier versions wrote the bare method + * ("mean", "sum", "point"); NetcdfDataReader still reads that form, so files written before this + * change keep resolving to the same data type. + */ + private static String getCellMethods(VortexGrid vortexGrid) { + String method = vortexGrid.dataType().getNcString(); + return method.isBlank() ? method : CF.TIME + ": " + method; + } + private static void addGlobalAttributes(NetcdfFormatWriter.Builder writerBuilder) { writerBuilder.addAttribute(new Attribute("Conventions", "CF-1.10")); } diff --git a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/VariableDsReader.java b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/VariableDsReader.java index c912c224..e4b9c7ec 100644 --- a/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/VariableDsReader.java +++ b/vortex-api/src/main/java/mil/army/usace/hec/vortex/io/VariableDsReader.java @@ -1,6 +1,7 @@ package mil.army.usace.hec.vortex.io; import mil.army.usace.hec.vortex.VortexData; +import mil.army.usace.hec.vortex.VortexDataType; import mil.army.usace.hec.vortex.VortexGrid; import mil.army.usace.hec.vortex.geo.Grid; import mil.army.usace.hec.vortex.geo.ReferenceUtils; @@ -148,6 +149,16 @@ private CoordinateAxis1D getTimeAxis() { return timeAxis instanceof CoordinateAxis1D axis ? axis : null; } + private String getTimeAxisName() { + CoordinateAxis1D timeAxis = getTimeAxis(); + return timeAxis != null ? timeAxis.getShortName() : null; + } + + @Override + VortexDataType getDeclaredDataType() { + return getVortexDataType(variableDS, getTimeAxisName()); + } + private List getYearMonthTimeRecords(CoordinateAxis1D timeAxis) { List timeRecords = new ArrayList<>(); for (int i = 0; i < getDtoCount(); i++) { @@ -249,7 +260,7 @@ private VortexGrid buildGrid(float[] data, VortexDataInterval timeRecord) { .startTime(timeRecord.startTime()) .endTime(timeRecord.endTime()) .interval(timeRecord.getRecordDuration()) - .dataType(getVortexDataType(variableDS)) + .dataType(getDeclaredDataType()) .build(); } diff --git a/vortex-api/src/main/resources/message.properties b/vortex-api/src/main/resources/message.properties index 3b404025..ae4a2d61 100644 --- a/vortex-api/src/main/resources/message.properties +++ b/vortex-api/src/main/resources/message.properties @@ -61,6 +61,7 @@ time_shifter_time=Elapsed time: {0}. error_invalid_file=File "\{0}\" could not be opened. # NetcdfDataReader warn_nc_time_bnds=CF compliance check failed: One or more NetCDF datasets do not contain a "time_bnds" variable. Imported start/end times may be inaccurate. Use the time-shifter utility to shift start and/or end times after import. \n\nDo you want to proceed? +warn_nc_instantaneous_span=CF compliance check failed: Variable "{0}" declares a "cell_methods" of "point" but its time bounds span a period. Records that span a period cannot be read as instantaneous values and will be skipped. \n\nDo you want to proceed? # ImportMetWizard error_archive_file=Unrecognized archive format for file: \"{0}\". error_archive_file_suggestion=Try extracting the contents of the archive before import. \ No newline at end of file diff --git a/vortex-api/src/test/java/mil/army/usace/hec/vortex/io/NetcdfDataReaderTest.java b/vortex-api/src/test/java/mil/army/usace/hec/vortex/io/NetcdfDataReaderTest.java index 867a9719..cbf82b04 100644 --- a/vortex-api/src/test/java/mil/army/usace/hec/vortex/io/NetcdfDataReaderTest.java +++ b/vortex-api/src/test/java/mil/army/usace/hec/vortex/io/NetcdfDataReaderTest.java @@ -24,6 +24,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.util.*; @@ -1074,4 +1075,78 @@ void gpmHalfHourV07C_intermediatePrecipFields() throws Exception { assertEquals("PER-CUM", grid.dataType().getDssString(), variable); } } + + @Test + void parseTimeCellMethodReadsCfSyntax() { + // CF §7.3: a blank-separated list of "name: method [(qualifiers)]" entries, a grammar stable + // across CF versions. + assertEquals("mean", NetcdfDataReader.parseTimeCellMethod("time: mean", null)); + assertEquals("sum", NetcdfDataReader.parseTimeCellMethod("time: sum", null)); + assertEquals("point", NetcdfDataReader.parseTimeCellMethod("time: point", null)); + + // The time entry is found wherever it appears in the list. + assertEquals("maximum", NetcdfDataReader.parseTimeCellMethod("area: mean time: maximum", null)); + + // Qualifiers are dropped, including the colon inside them. + assertEquals("mean", NetcdfDataReader.parseTimeCellMethod("time: mean (interval: 1 day)", null)); + + // Climatological statistics list several time entries; the first method wins. + assertEquals("mean", NetcdfDataReader.parseTimeCellMethod("time: mean within days time: mean over days", null)); + + // Nothing was declared about time, so nothing is claimed about it. + assertEquals("", NetcdfDataReader.parseTimeCellMethod("area: mean", null)); + + // Bare tokens, the form vortex itself wrote before it emitted CF-conformant output. + assertEquals("mean", NetcdfDataReader.parseTimeCellMethod("mean", null)); + assertEquals("sum", NetcdfDataReader.parseTimeCellMethod("sum", null)); + assertEquals("point", NetcdfDataReader.parseTimeCellMethod("point", null)); + + // Absent or empty attribute. + assertEquals("", NetcdfDataReader.parseTimeCellMethod("", null)); + assertEquals("", NetcdfDataReader.parseTimeCellMethod(" ", null)); + assertEquals("", NetcdfDataReader.parseTimeCellMethod(null, null)); + + // CF permits the entry to name the time coordinate variable instead of the literal "time". + assertEquals("mean", NetcdfDataReader.parseTimeCellMethod("valid_time: mean", "valid_time")); + assertEquals("", NetcdfDataReader.parseTimeCellMethod("valid_time: mean", null)); + assertEquals("mean", NetcdfDataReader.parseTimeCellMethod("time: mean", "valid_time")); + } + + @Test + void parseTimeCellMethodMapsToDataType() { + assertEquals(VortexDataType.AVERAGE, VortexDataType.fromString(NetcdfDataReader.parseTimeCellMethod("time: mean", null))); + assertEquals(VortexDataType.ACCUMULATION, VortexDataType.fromString(NetcdfDataReader.parseTimeCellMethod("time: sum", null))); + assertEquals(VortexDataType.INSTANTANEOUS, VortexDataType.fromString(NetcdfDataReader.parseTimeCellMethod("time: point", null))); + assertEquals(VortexDataType.UNDEFINED, VortexDataType.fromString(NetcdfDataReader.parseTimeCellMethod("area: mean", null))); + assertEquals(VortexDataType.AVERAGE, VortexDataType.fromString(NetcdfDataReader.parseTimeCellMethod("mean", null))); + } + + /** + * cf_style.nc and bare_mean.nc are identical but for the cell_methods attribute: the first declares + * CF's "time: mean", the second the bare "mean" that vortex used to write. Both must classify the + * same, and their records must be reachable through TemporalDataReader. + */ + @Test + void cellMethodsVariantsReadTheSame() throws Exception { + for (String resource : List.of("/cf_style.nc", "/bare_mean.nc")) { + String file = new File(Objects.requireNonNull(getClass().getResource(resource)).getFile()).toString(); + + try (DataReader reader = DataReader.builder().path(file).variable("SWE_Post").build()) { + VortexGrid grid = (VortexGrid) reader.getDtos().get(0); + assertEquals(VortexDataType.AVERAGE, grid.dataType(), resource); + + // Each step spans a day, per time_bnds. + assertEquals(Instant.parse("2002-10-01T00:00:00Z"), grid.startTime().toInstant(), resource); + assertEquals(Instant.parse("2002-10-02T00:00:00Z"), grid.endTime().toInstant(), resource); + + TemporalDataReader temporal = TemporalDataReader.create(reader); + assertEquals(Instant.parse("2002-10-01T00:00:00Z"), + temporal.getStartTime().orElseThrow().toInstant(), resource); + // A period type reports the end of the last interval, not the last timestamp. + assertEquals(Instant.parse("2002-10-04T00:00:00Z"), + temporal.getEndTime().orElseThrow().toInstant(), resource); + assertTrue(temporal.readNearest(ZonedDateTime.parse("2002-10-01T12:00Z")).isPresent(), resource); + } + } + } } \ No newline at end of file diff --git a/vortex-api/src/test/resources/bare_mean.cdl b/vortex-api/src/test/resources/bare_mean.cdl new file mode 100644 index 00000000..caf596fd --- /dev/null +++ b/vortex-api/src/test/resources/bare_mean.cdl @@ -0,0 +1,48 @@ +netcdf bare_mean { +dimensions: + time = 3 ; + y = 2 ; + x = 2 ; + nv = 2 ; +variables: + double time(time) ; + time:units = "days since 1970-01-01" ; + time:calendar = "standard" ; + time:bounds = "time_bnds" ; + time:standard_name = "time" ; + time:axis = "T" ; + double time_bnds(time, nv) ; + double x(x) ; + x:units = "m" ; + x:standard_name = "projection_x_coordinate" ; + x:axis = "X" ; + double y(y) ; + y:units = "m" ; + y:standard_name = "projection_y_coordinate" ; + y:axis = "Y" ; + int crs ; + crs:grid_mapping_name = "transverse_mercator" ; + crs:longitude_of_central_meridian = 75. ; + crs:latitude_of_projection_origin = 0. ; + crs:scale_factor_at_central_meridian = 0.9996 ; + crs:false_easting = 500000. ; + crs:false_northing = 0. ; + crs:semi_major_axis = 6378137. ; + crs:inverse_flattening = 298.257223563 ; + crs:GeoTransform = "177000.0 1000.0 0 4722000.0 0 -1000.0" ; + float SWE_Post(time, y, x) ; + SWE_Post:units = "mm" ; + SWE_Post:grid_mapping = "crs" ; + SWE_Post:standard_name = "lwe_thickness_of_surface_snow_amount" ; + SWE_Post:cell_methods = "mean" ; + +// global attributes: + :Conventions = "CF-1.11" ; +data: + time = 11961.5, 11962.5, 11963.5 ; + time_bnds = 11961, 11962, 11962, 11963, 11963, 11964 ; + x = 177500, 178500 ; + y = 4721500, 4720500 ; + crs = 1 ; + SWE_Post = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ; +} diff --git a/vortex-api/src/test/resources/bare_mean.nc b/vortex-api/src/test/resources/bare_mean.nc new file mode 100644 index 0000000000000000000000000000000000000000..4efe6b2ca0d4a146da15866e101a10713903a2f9 GIT binary patch literal 15439 zcmeGj3v66Raqr&QIbY6+n}5o$d7CFJtQk5H;Xkd<~X};ydKmM~^IcmXlxVaXRKRghKo>q*@hiMx={cN9y{^tv-RDrVW949{PY1>7yynu^`?&**c2Mz z3i?JG!oy1w9A2`S!LL9poXU&4ke|>>1z#2jPVJ!CY>A&p0c#N-8 zQDXmIF9-Pim*lZIZ>sYqF>eC%Coflk@+2WoPV&SmZ?b!y`?;Lu>m8BZz7P%wE6N;> z71+uNW|UMTs3M-O$`@o+h5a7AOW?Xk;v^qJncRnH#HtFL2VMji_-MOk#^QZdp;Rgz z*A3OwGOP@AlhCC;ZvKg2f&u}#5E*IDw@DJ1bSY%^#GcbHb%dHX6yJme4j1YnOeLnC z)S_FAc&1u93oAb|^dpOFmhVBG5>&2%ONXS{c|&wY`xy08@L0qDN5k!K$T(2vj zxrM{l8tm%gt{R$9tkBTX*4We1fhTw91FJH%|jn+v&i5z8P6 z5jgA>xpe~Y-`NM3xUspY@tE}P_R1Pwm9~P#MN{>YN$z#_l_gQngi+7 zHZ6u;ZFDFav&7O1HnTWOv^u2cchJk&+1^y?^a#*p$>aA_fRJ_9EI=>3xanwC0^XPf zxFFRDz>4|ua=1tZIflPCQ+@*Aee%lwY+FZx(lcC`^zczmi}VZ^AU*do7CGDej3qsM z!J#xNUi9^HHDZxRY;mv(d>Wzc&HO6CL0YS%ic8pD=_am_4%Xe|Xnk3<_}}|I`~B)j zRssUj;SWhdi@ERUaM4JWSUPh7y=$2c8$Xz1aguYzsa$RTkh_>j1}RUF6#XrhEn4Vi z3qK1ja%X_CzwZ)S?tV$-d5$%^ot`ndl5Y|H7hnpBSb~TEC72UE8*}55`?X{DWI6c? zSLck?%38&s)KI#^;ImdQj@ebLNd zx=)R1(TtJW8C8?1LBsUIz6Xth>^G(*649h)_NU@Pb))=n3N_VWY{|11o(y+&H8R>m zI$%3igx^`+1@j<6E4}+>v=(Os(g3Y95>$fX^EgLE7ES=5nblID=9^Kl?IP_ve5q;t zX7r@&yTt0{V_(@UuVF#6*uS1$^y4u_fPx99tg}orc+1G{0er_ke~^o zI@w$O&!->h3}caN^84p0&B~|(~3t}*|Alk9ho2Ua$J*JS+18X0~PwWdmz!#Ou4P> z7yWUk5{=lDwj|s1|SDCS{f4`S{{+p=!hAGkPOHV14D*c`AqU~|Cc!26y97-^s* zEV|7_duMM#oCeH>t3(iih%|;9BaQ8$#wd?Kd>BFk7=ehZrkc^rY$(EtcQAME!5Ky< zD7rw9&i*Oi_VSmEY=Gvx9$RG(C98{%as+meFY$?b-NPi+_Y}L)dxQ3@)F}Qb|R`ECs4%2ivzMAMV z>_h_3DLNhrWYMaog*E5uLn^Ir)eA%mxD~ccP@9RV2`vgIaxjFbYF<0RL;JsZ>+^44 zT#s|d`Ov2(fIXU;G4cGcka3SZur!E^HwTT8O2a8)fq0OI3aB2AC!-~GIL+LK!^Arw z1rQYYPIoU5G*=+f;W9N&d|yH}P0c8Ts69#Z@tdDY!=%ci&L%C@jiS_-N+-E^!wD=G zu!@=*zu#Z(_f-hs_f^$YR?_<#QHtZU(=~E{uYbBw&T;I!zwE~Ay2*uD3OK*G`eXoO z-dw%FK>%Qy(h){pr`h{dctnAy;_7h5m(h)w=Brp+<6q;ifIkI}k-3(`#gEskxa^@_ z!zOg)N&$PtYi)r;dU+I&wvH&|m+~xCO|7!Z5+nOm*Ba|p_p!QhrT?c)B6f#~Ah`{q zlrb`ql(*skH3yDw{AmCg5crgU$0Zz*+qe|0!Y09{q33Xw|;}Hs{n- literal 0 HcmV?d00001 diff --git a/vortex-api/src/test/resources/cf_style.cdl b/vortex-api/src/test/resources/cf_style.cdl new file mode 100644 index 00000000..a4f781dd --- /dev/null +++ b/vortex-api/src/test/resources/cf_style.cdl @@ -0,0 +1,48 @@ +netcdf cf_style { +dimensions: + time = 3 ; + y = 2 ; + x = 2 ; + nv = 2 ; +variables: + double time(time) ; + time:units = "days since 1970-01-01" ; + time:calendar = "standard" ; + time:bounds = "time_bnds" ; + time:standard_name = "time" ; + time:axis = "T" ; + double time_bnds(time, nv) ; + double x(x) ; + x:units = "m" ; + x:standard_name = "projection_x_coordinate" ; + x:axis = "X" ; + double y(y) ; + y:units = "m" ; + y:standard_name = "projection_y_coordinate" ; + y:axis = "Y" ; + int crs ; + crs:grid_mapping_name = "transverse_mercator" ; + crs:longitude_of_central_meridian = 75. ; + crs:latitude_of_projection_origin = 0. ; + crs:scale_factor_at_central_meridian = 0.9996 ; + crs:false_easting = 500000. ; + crs:false_northing = 0. ; + crs:semi_major_axis = 6378137. ; + crs:inverse_flattening = 298.257223563 ; + crs:GeoTransform = "177000.0 1000.0 0 4722000.0 0 -1000.0" ; + float SWE_Post(time, y, x) ; + SWE_Post:units = "mm" ; + SWE_Post:grid_mapping = "crs" ; + SWE_Post:standard_name = "lwe_thickness_of_surface_snow_amount" ; + SWE_Post:cell_methods = "time: mean" ; + +// global attributes: + :Conventions = "CF-1.11" ; +data: + time = 11961.5, 11962.5, 11963.5 ; + time_bnds = 11961, 11962, 11962, 11963, 11963, 11964 ; + x = 177500, 178500 ; + y = 4721500, 4720500 ; + crs = 1 ; + SWE_Post = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ; +} diff --git a/vortex-api/src/test/resources/cf_style.nc b/vortex-api/src/test/resources/cf_style.nc new file mode 100644 index 0000000000000000000000000000000000000000..99868d5ec6e854d95e5286c76b5c7357ba8272f7 GIT binary patch literal 15439 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 literal 0 HcmV?d00001