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
3 changes: 2 additions & 1 deletion cdm/core/src/main/java/ucar/nc2/constants/CDM.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 1998-2018 John Caron and University Corporation for Atmospheric Research/Unidata
* Copyright (c) 1998-2025 John Caron and University Corporation for Atmospheric Research/Unidata
* See LICENSE for license information.
*/

Expand Down Expand Up @@ -66,6 +66,7 @@ public class CDM {
public static final String NCPROPERTIES = "_NCProperties";
public static final String ISNETCDF4 = "_IsNetcdf4";
public static final String SUPERBLOCKVERSION = "_SuperblockVersion";
public static final String ARRAYDIMENSIONS = "_ARRAY_DIMENSIONS";

public static final String[] SPECIALS = {NCPROPERTIES, ISNETCDF4, SUPERBLOCKVERSION};

Expand Down
69 changes: 47 additions & 22 deletions cdm/zarr/src/main/java/ucar/nc2/iosp/zarr/ZarrHeader.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
/*
* Copyright (c) 2021 University Corporation for Atmospheric Research/Unidata
* Copyright (c) 2021-2025 University Corporation for Atmospheric Research/Unidata
* See LICENSE for license information.
*/

package ucar.nc2.iosp.zarr;

import static ucar.nc2.constants.CDM.ARRAYDIMENSIONS;

import com.fasterxml.jackson.databind.ObjectMapper;

import ucar.ma2.ArrayObject;
Expand Down Expand Up @@ -34,6 +36,7 @@ public class ZarrHeader {
private final RandomAccessDirectory rootRaf;
private final Group.Builder rootGroup;
private final String rootLocation;

private static final ObjectMapper objectMapper = new ObjectMapper();

public ZarrHeader(RandomAccessDirectory raf, Group.Builder rootGroup) {
Expand Down Expand Up @@ -219,20 +222,25 @@ private void makeVariable(RandomAccessDirectoryItem item, long dataOffset, ZArra

for (Attribute attr : attrs) {
final String attrName = attr.getName();
if ("_ARRAY_DIMENSIONS".equals(attrName)) {
if (ARRAYDIMENSIONS.equals(attrName)) {
try {
final ArrayObject.D1 aod1 = (ArrayObject.D1) attr.getValues();

// getSize returns a long
final int aodSize = (int) aod1.getSize();
dimNames = new String[aodSize];

for (int i = 0; i < aodSize; ++i) {
dimNames[i] = (String) aod1.get(i);
if (attr.getLength() == 1 && attr.getStringValue().equals("")) {
// scalar array without a named dimension
logger.debug(" {} is a scalar array without a named dimension", vname);
} else {
final ArrayObject.D1 aod1 = (ArrayObject.D1) attr.getValues();

// getSize returns a long
final int aodSize = (int) aod1.getSize();
dimNames = new String[aodSize];

for (int i = 0; i < aodSize; ++i) {
dimNames[i] = (String) aod1.get(i);
}
hasNamedDimensions = true;
}
hasNamedDimensions = true;
} catch (final Exception exc) {
logger.debug(" Could not extract _ARRAY_DIMENSIONS for {}, {}", vname, exc.getMessage());
logger.debug(" Could not extract {} for {}, {}", ARRAYDIMENSIONS, vname, exc.getMessage());
}
}
}
Expand Down Expand Up @@ -260,7 +268,8 @@ private void makeVariable(RandomAccessDirectoryItem item, long dataOffset, ZArra
final Dimension.Builder dim = Dimension.builder(dname, shape[i]);
dim.setIsVariableLength(false);
dim.setIsUnlimited(false);
dim.setIsShared(false);
// if using named dimensions from _ARRAY_DIMENSIONS, mark the dimension as shared
dim.setIsShared(hasNamedDimensions);

final Dimension dd = dim.build();

Expand All @@ -275,6 +284,9 @@ private void makeVariable(RandomAccessDirectoryItem item, long dataOffset, ZArra
if (dd.getLength() != prevd.getLength()) {
throw new ZarrFormatException("Named dimension " + dname + " seen with inconsistent lengths.");
}
// replace newly created dimension with the previously added dimension
dims.remove(dd);
dims.add(prevd);
} else {
logger.trace("adding {} to group as a shared dimension", dname);
parentGroup.addDimension(dd);
Expand Down Expand Up @@ -327,7 +339,13 @@ private List<Attribute> makeAttributes(RandomAccessDirectoryItem item) {
Attribute.Builder attr = Attribute.builder(key);
Object val = attrMap.get(key);
if (val instanceof Collection<?>) {
attr.setValues(Arrays.asList(((Collection) val).toArray()), false);
Collection<?> collection = (Collection<?>) val;
if (collection.isEmpty() && key.equals(ARRAYDIMENSIONS)) {
// scalar array
attr.setValues(Collections.singletonList(""), false);
} else {
attr.setValues(Arrays.asList(collection.toArray()), false);
}
} else if (val instanceof Number) {
attr.setNumericValue((Number) val, false);
} else {
Expand All @@ -354,7 +372,8 @@ private static int getChunkIndex(RandomAccessDirectoryItem item, ZArray zarray)

int nDims = zarray.getShape().length;
// verify is data file, else return -1
String pattern = String.format("([0-9]+%c){%d}[0-9]+", zarray.getSeparator().charAt(0), nDims - 1);
String pattern = String.format("([0-9]+%c){%d}[0-9]+", zarray.getSeparator().charAt(0), nDims == 0 ? 0 : nDims - 1);

if (!fileName.matches(pattern)) {
return -1;
}
Expand All @@ -363,14 +382,20 @@ private static int getChunkIndex(RandomAccessDirectoryItem item, ZArray zarray)
String[] dims = fileName.split(String.format("\\%c", zarray.getSeparator().charAt(0)));
int[] subs = Arrays.stream(dims).mapToInt(dim -> Integer.parseInt(dim)).toArray();

// get number of chunks in each dimension
int[] nChunks = new int[nDims];
int[] shape = zarray.getShape();
int[] chunkSize = zarray.getChunks();
for (int i = 0; i < nDims; i++) {
nChunks[i] = (int) Math.ceil(shape[i] / chunkSize[i]);
// find chunk number as a flat index
if (nDims != 0) {
// get number of chunks in each dimension
int[] nChunks = new int[nDims];
int[] shape = zarray.getShape();
int[] chunkSize = zarray.getChunks();
for (int i = 0; i < nDims; i++) {
nChunks[i] = (int) Math.ceil(shape[i] / chunkSize[i]);
}
return ZarrUtils.subscriptsToIndex(subs, nChunks);
} else {
// scalar array
return 0;
}
return ZarrUtils.subscriptsToIndex(subs, nChunks);
}

/**
Expand Down
25 changes: 18 additions & 7 deletions cdm/zarr/src/main/java/ucar/nc2/iosp/zarr/ZarrLayoutBB.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/*
* Copyright (c) 2021-2025 University Corporation for Atmospheric Research/Unidata
* See LICENSE for license information.
*/

package ucar.nc2.iosp.zarr;

import ucar.ma2.Range;
Expand Down Expand Up @@ -127,14 +132,20 @@ public LayoutBBTiled.DataChunk next() {

private void incrementChunk() {
// increment index from inner dimension outward
int i = this.currChunk.length - 1;
while (this.currChunk[i] + 1 >= nChunks[i] && i > 0) {
this.currChunk[i] = 0;
i--;
if (this.currChunk.length != 0) {
int i = this.currChunk.length - 1;
while (this.currChunk[i] + 1 >= nChunks[i] && i > 0) {
this.currChunk[i] = 0;
i--;
}
this.currChunk[i]++;
this.currOffset += initializedChunks.getOrDefault(this.chunkNum, (long) 0);
this.chunkNum = ZarrUtils.subscriptsToIndex(this.currChunk, nChunks);
} else {
// scalar array
this.currOffset = 0;
this.chunkNum = 0;
}
this.currChunk[i]++;
this.currOffset += initializedChunks.getOrDefault(this.chunkNum, (long) 0);
this.chunkNum = ZarrUtils.subscriptsToIndex(this.currChunk, nChunks);
}
}

Expand Down
5 changes: 5 additions & 0 deletions cdm/zarr/src/test/data/geozarr/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Examples obtained from https://github.com/zarr-developers/geozarr-spec/tree/cnl-examples/ on 2025-04-28 (commit d1eabecbea63d6f300600d529d7818bbf6df5bcc).
No changes were made.

Examples are licensed under Creative Commons Attribution 4.0 International.
Full text can be found at https://raw.githubusercontent.com/zarr-developers/geozarr-spec/refs/heads/cnl-examples/LICENSE
7 changes: 7 additions & 0 deletions cdm/zarr/src/test/data/geozarr/xyt-raster.zarr/.zattrs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"Conventions": "CF-1.8",
"profile": [
"time-series-raster",
"scalar-raster"
]
}
3 changes: 3 additions & 0 deletions cdm/zarr/src/test/data/geozarr/xyt-raster.zarr/.zgroup
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"zarr_format": 2
}
164 changes: 164 additions & 0 deletions cdm/zarr/src/test/data/geozarr/xyt-raster.zarr/.zmetadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
{
"metadata": {
".zattrs": {
"Conventions": "CF-1.8",
"profile": [
"time-series-raster",
"scalar-raster"
]
},
".zgroup": {
"zarr_format": 2
},
"spatial_ref/.zarray": {
"chunks": [],
"compressor": null,
"dtype": "<i8",
"fill_value": null,
"filters": null,
"order": "C",
"shape": [],
"zarr_format": 2
},
"spatial_ref/.zattrs": {
"GeoTransform": "319281.23 12.5 0 5639331.75 0 -12.5",
"_ARRAY_DIMENSIONS": [],
"crs_wkt": "PROJCS[\"WGS 84 / UTM zone 30N\",GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]],PROJECTION[\"Transverse_Mercator\"],PARAMETER[\"latitude_of_origin\",0],PARAMETER[\"central_meridian\",-3],PARAMETER[\"scale_factor\",0.9996],PARAMETER[\"false_easting\",500000],PARAMETER[\"false_northing\",0],UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH],AUTHORITY[\"EPSG\",\"32630\"]]",
"false_easting": 500000.0,
"false_northing": 0.0,
"geographic_crs_name": "WGS 84",
"grid_mapping_name": "transverse_mercator",
"horizontal_datum_name": "World Geodetic System 1984",
"inverse_flattening": 298.257223563,
"latitude_of_projection_origin": 0.0,
"longitude_of_central_meridian": -3.0,
"prime_meridian_name": "Greenwich",
"projected_crs_name": "WGS 84 / UTM zone 30N",
"reference_ellipsoid_name": "WGS 84",
"scale_factor_at_central_meridian": 0.9996,
"semi_major_axis": 6378137.0,
"semi_minor_axis": 6356752.314245179
},
"temperature/.zarray": {
"chunks": [
2,
128,
128
],
"compressor": {
"blocksize": 0,
"clevel": 5,
"cname": "lz4",
"id": "blosc",
"shuffle": 1
},
"dtype": "<f8",
"fill_value": "NaN",
"filters": null,
"order": "C",
"shape": [
4,
128,
128
],
"zarr_format": 2
},
"temperature/.zattrs": {
"AREA_OR_POINT": "Point",
"_ARRAY_DIMENSIONS": [
"time",
"y",
"x"
],
"grid_mapping": "spatial_ref",
"standard_name": "air_temperature",
"units": "K"
},
"time/.zarray": {
"chunks": [
4
],
"compressor": {
"blocksize": 0,
"clevel": 5,
"cname": "lz4",
"id": "blosc",
"shuffle": 1
},
"dtype": "<i8",
"fill_value": null,
"filters": null,
"order": "C",
"shape": [
4
],
"zarr_format": 2
},
"time/.zattrs": {
"_ARRAY_DIMENSIONS": [
"time"
],
"calendar": "proleptic_gregorian",
"long_name": "time of observation",
"standard_name": "time",
"units": "days since 2020-01-01 00:00:00"
},
"x/.zarray": {
"chunks": [
128
],
"compressor": {
"blocksize": 0,
"clevel": 5,
"cname": "lz4",
"id": "blosc",
"shuffle": 1
},
"dtype": "<f8",
"fill_value": "NaN",
"filters": null,
"order": "C",
"shape": [
128
],
"zarr_format": 2
},
"x/.zattrs": {
"_ARRAY_DIMENSIONS": [
"x"
],
"long_name": "x coordinate of projection",
"standard_name": "projection_x_coordinate",
"units": "metre"
},
"y/.zarray": {
"chunks": [
128
],
"compressor": {
"blocksize": 0,
"clevel": 5,
"cname": "lz4",
"id": "blosc",
"shuffle": 1
},
"dtype": "<f8",
"fill_value": "NaN",
"filters": null,
"order": "C",
"shape": [
128
],
"zarr_format": 2
},
"y/.zattrs": {
"_ARRAY_DIMENSIONS": [
"y"
],
"long_name": "y coordinate of projection",
"standard_name": "projection_y_coordinate",
"units": "metre"
}
},
"zarr_consolidated_format": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"chunks": [],
"compressor": null,
"dtype": "<i8",
"fill_value": null,
"filters": null,
"order": "C",
"shape": [],
"zarr_format": 2
}
Loading