|
| 1 | +package dev.zarr.zarrjava.v3.codec.core; |
| 2 | + |
| 3 | +import com.fasterxml.jackson.annotation.JsonCreator; |
| 4 | +import com.fasterxml.jackson.annotation.JsonIgnore; |
| 5 | +import com.fasterxml.jackson.annotation.JsonProperty; |
| 6 | +import dev.zarr.zarrjava.ZarrException; |
| 7 | +import dev.zarr.zarrjava.core.codec.ArrayArrayCodec; |
| 8 | +import dev.zarr.zarrjava.v3.ArrayMetadata; |
| 9 | +import dev.zarr.zarrjava.v3.codec.Codec; |
| 10 | +import ucar.ma2.Array; |
| 11 | + |
| 12 | +import javax.annotation.Nonnull; |
| 13 | +import java.util.ArrayList; |
| 14 | +import java.util.Arrays; |
| 15 | +import java.util.List; |
| 16 | + |
| 17 | +/** |
| 18 | + * {@code array -> array} codec that reshapes a chunk. |
| 19 | + * |
| 20 | + * <p>The codec preserves the (logical) lexicographical / C-order traversal of the elements, i.e. |
| 21 | + * {@code ravel(B) == ravel(A)}. It never transposes; combine it with the {@code transpose} codec if |
| 22 | + * a reordering is also required. |
| 23 | + * |
| 24 | + * <p>The target {@code shape} is described relative to the input (chunk) shape {@code A_shape}. Each |
| 25 | + * entry is one of: |
| 26 | + * <ul> |
| 27 | + * <li>a positive integer {@code size}, giving {@code B_shape[i] == size};</li> |
| 28 | + * <li>an array of input dimension indices {@code input_dims}, giving |
| 29 | + * {@code B_shape[i] == prod(A_shape[input_dims])};</li> |
| 30 | + * <li>the special value {@code -1} (at most once), inferred so that |
| 31 | + * {@code prod(B_shape) == prod(A_shape)}.</li> |
| 32 | + * </ul> |
| 33 | + */ |
| 34 | +public class ReshapeCodec extends ArrayArrayCodec implements Codec { |
| 35 | + |
| 36 | + @JsonIgnore |
| 37 | + @Nonnull |
| 38 | + public final String name = "reshape"; |
| 39 | + @Nonnull |
| 40 | + public final Configuration configuration; |
| 41 | + |
| 42 | + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) |
| 43 | + public ReshapeCodec( |
| 44 | + @Nonnull @JsonProperty(value = "configuration", required = true) Configuration configuration |
| 45 | + ) { |
| 46 | + this.configuration = configuration; |
| 47 | + } |
| 48 | + |
| 49 | + @Override |
| 50 | + public Array encode(Array chunkArray) throws ZarrException { |
| 51 | + int[] inputShape = arrayMetadata.chunkShape; |
| 52 | + if (!Arrays.equals(chunkArray.getShape(), inputShape)) { |
| 53 | + throw new ZarrException( |
| 54 | + "reshape codec received an array of shape " + Arrays.toString(chunkArray.getShape()) |
| 55 | + + " but expected the chunk shape " + Arrays.toString(inputShape) + "."); |
| 56 | + } |
| 57 | + int[] outputShape = resolveOutputShape(inputShape); |
| 58 | + // Array.reshape copies the elements in lexicographical (C) order, hence ravel(B) == ravel(A) |
| 59 | + // even when the input array is a non-contiguous view. |
| 60 | + return chunkArray.reshape(outputShape); |
| 61 | + } |
| 62 | + |
| 63 | + @Override |
| 64 | + public Array decode(Array chunkArray) throws ZarrException { |
| 65 | + int[] inputShape = arrayMetadata.chunkShape; |
| 66 | + int[] outputShape = resolveOutputShape(inputShape); |
| 67 | + if (!Arrays.equals(chunkArray.getShape(), outputShape)) { |
| 68 | + throw new ZarrException( |
| 69 | + "reshape codec received an array of shape " + Arrays.toString(chunkArray.getShape()) |
| 70 | + + " but expected the reshaped shape " + Arrays.toString(outputShape) + "."); |
| 71 | + } |
| 72 | + // Inverse operation: reshape back to the original chunk shape. |
| 73 | + return chunkArray.reshape(inputShape); |
| 74 | + } |
| 75 | + |
| 76 | + @Override |
| 77 | + public long computeEncodedSize(long inputByteLength, |
| 78 | + ArrayMetadata.CoreArrayMetadata arrayMetadata) throws ZarrException { |
| 79 | + // Reshaping does not add or remove any elements. |
| 80 | + return inputByteLength; |
| 81 | + } |
| 82 | + |
| 83 | + @Override |
| 84 | + public ArrayMetadata.CoreArrayMetadata resolveArrayMetadata() throws ZarrException { |
| 85 | + super.resolveArrayMetadata(); |
| 86 | + |
| 87 | + int[] inputChunkShape = arrayMetadata.chunkShape; |
| 88 | + int[] outputChunkShape = resolveOutputShape(inputChunkShape); |
| 89 | + |
| 90 | + // Derive the output (grid) array shape so that number of chunks along each output dimension is |
| 91 | + // consistent with the input. For a merged output dimension this is the product of the input |
| 92 | + // chunk counts; a split input dimension keeps its chunk count on the outermost output dimension. |
| 93 | + long[] inputArrayShape = arrayMetadata.shape; |
| 94 | + long[] gridMultiplier = new long[outputChunkShape.length]; |
| 95 | + Arrays.fill(gridMultiplier, 1L); |
| 96 | + |
| 97 | + long[] outputStart = new long[outputChunkShape.length + 1]; |
| 98 | + outputStart[0] = 1L; |
| 99 | + for (int i = 0; i < outputChunkShape.length; i++) { |
| 100 | + outputStart[i + 1] = outputStart[i] * outputChunkShape[i]; |
| 101 | + } |
| 102 | + |
| 103 | + long inputStart = 1L; |
| 104 | + for (int d = 0; d < inputChunkShape.length; d++) { |
| 105 | + long numChunks = inputArrayShape[d] / inputChunkShape[d]; |
| 106 | + // Attach the chunk count of input dimension d to the output dimension whose flat range |
| 107 | + // contains the start of input dimension d. |
| 108 | + int target = outputChunkShape.length - 1; |
| 109 | + for (int i = 0; i < outputChunkShape.length; i++) { |
| 110 | + if (inputStart >= outputStart[i] && inputStart < outputStart[i + 1]) { |
| 111 | + target = i; |
| 112 | + break; |
| 113 | + } |
| 114 | + } |
| 115 | + gridMultiplier[target] *= numChunks; |
| 116 | + inputStart *= inputChunkShape[d]; |
| 117 | + } |
| 118 | + |
| 119 | + long[] outputArrayShape = new long[outputChunkShape.length]; |
| 120 | + for (int i = 0; i < outputChunkShape.length; i++) { |
| 121 | + outputArrayShape[i] = gridMultiplier[i] * outputChunkShape[i]; |
| 122 | + } |
| 123 | + |
| 124 | + return new ArrayMetadata.CoreArrayMetadata( |
| 125 | + outputArrayShape, |
| 126 | + outputChunkShape, |
| 127 | + arrayMetadata.dataType, |
| 128 | + arrayMetadata.parsedFillValue |
| 129 | + ); |
| 130 | + } |
| 131 | + |
| 132 | + /** |
| 133 | + * Resolves the concrete output shape {@code B_shape} from the {@code shape} configuration and the |
| 134 | + * given input shape {@code A_shape}, validating all invariants mandated by the specification. |
| 135 | + */ |
| 136 | + int[] resolveOutputShape(int[] inputShape) throws ZarrException { |
| 137 | + int ndim = inputShape.length; |
| 138 | + long inputTotal = 1L; |
| 139 | + for (int size : inputShape) { |
| 140 | + inputTotal *= size; |
| 141 | + } |
| 142 | + |
| 143 | + int n = configuration.shape.length; |
| 144 | + if (n == 0) { |
| 145 | + throw new ZarrException("reshape codec: 'shape' must not be empty."); |
| 146 | + } |
| 147 | + |
| 148 | + long[] outputShape = new long[n]; |
| 149 | + int[][] inputDimsPerOutput = new int[n][]; |
| 150 | + int minusOnePos = -1; |
| 151 | + List<Integer> flatInputDims = new ArrayList<>(); |
| 152 | + |
| 153 | + for (int i = 0; i < n; i++) { |
| 154 | + Object element = configuration.shape[i]; |
| 155 | + int[] inputDims = asInputDims(element); |
| 156 | + if (inputDims == null) { |
| 157 | + int size = asInteger(element); |
| 158 | + if (size == -1) { |
| 159 | + if (minusOnePos != -1) { |
| 160 | + throw new ZarrException("reshape codec: 'shape' may contain -1 at most once."); |
| 161 | + } |
| 162 | + minusOnePos = i; |
| 163 | + outputShape[i] = -1; |
| 164 | + } else if (size <= 0) { |
| 165 | + throw new ZarrException( |
| 166 | + "reshape codec: 'shape' entries must be a positive integer, -1, or an array of " |
| 167 | + + "input dimensions, but got " + size + "."); |
| 168 | + } else { |
| 169 | + outputShape[i] = size; |
| 170 | + } |
| 171 | + } else { |
| 172 | + inputDimsPerOutput[i] = inputDims; |
| 173 | + long product = 1L; |
| 174 | + for (int d : inputDims) { |
| 175 | + if (d < 0 || d >= ndim) { |
| 176 | + throw new ZarrException( |
| 177 | + "reshape codec: input dimension " + d + " is out of range for an input array with " |
| 178 | + + ndim + " dimensions."); |
| 179 | + } |
| 180 | + product *= inputShape[d]; |
| 181 | + flatInputDims.add(d); |
| 182 | + } |
| 183 | + outputShape[i] = product; |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + // The flattened list of input dimensions must be strictly monotonically increasing. This rules |
| 188 | + // out configurations that would (incorrectly) suggest a transpose. |
| 189 | + for (int j = 1; j < flatInputDims.size(); j++) { |
| 190 | + if (flatInputDims.get(j) <= flatInputDims.get(j - 1)) { |
| 191 | + throw new ZarrException( |
| 192 | + "reshape codec: the flattened list of input dimensions must be strictly increasing, but got " |
| 193 | + + flatInputDims + "."); |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + // Resolve the automatic dimension (-1). |
| 198 | + if (minusOnePos != -1) { |
| 199 | + long known = 1L; |
| 200 | + for (int i = 0; i < n; i++) { |
| 201 | + if (i != minusOnePos) { |
| 202 | + known *= outputShape[i]; |
| 203 | + } |
| 204 | + } |
| 205 | + if (known == 0 || inputTotal % known != 0) { |
| 206 | + throw new ZarrException( |
| 207 | + "reshape codec: cannot infer the -1 dimension because prod(output shape) would not equal " |
| 208 | + + "prod(input shape) (" + inputTotal + ")."); |
| 209 | + } |
| 210 | + outputShape[minusOnePos] = inputTotal / known; |
| 211 | + } |
| 212 | + |
| 213 | + // Invariant: prod(B_shape) == prod(A_shape). |
| 214 | + long outputTotal = 1L; |
| 215 | + for (long size : outputShape) { |
| 216 | + outputTotal *= size; |
| 217 | + } |
| 218 | + if (outputTotal != inputTotal) { |
| 219 | + throw new ZarrException( |
| 220 | + "reshape codec: prod(output shape)=" + outputTotal + " does not equal prod(input shape)=" |
| 221 | + + inputTotal + "."); |
| 222 | + } |
| 223 | + |
| 224 | + // When an output dimension is specified by input_dims, verify that the coordinates along those |
| 225 | + // input dimensions actually correspond to the raveled index along that output dimension. |
| 226 | + for (int i = 0; i < n; i++) { |
| 227 | + int[] inputDims = inputDimsPerOutput[i]; |
| 228 | + if (inputDims == null || inputDims.length == 0) { |
| 229 | + continue; |
| 230 | + } |
| 231 | + long outputPrefix = 1L; |
| 232 | + for (int j = 0; j < i; j++) { |
| 233 | + outputPrefix *= outputShape[j]; |
| 234 | + } |
| 235 | + long outputSuffix = 1L; |
| 236 | + for (int j = i + 1; j < n; j++) { |
| 237 | + outputSuffix *= outputShape[j]; |
| 238 | + } |
| 239 | + long inputPrefix = 1L; |
| 240 | + for (int d = 0; d < inputDims[0]; d++) { |
| 241 | + inputPrefix *= inputShape[d]; |
| 242 | + } |
| 243 | + long inputSuffix = 1L; |
| 244 | + for (int d = inputDims[inputDims.length - 1] + 1; d < ndim; d++) { |
| 245 | + inputSuffix *= inputShape[d]; |
| 246 | + } |
| 247 | + if (outputPrefix != inputPrefix || outputSuffix != inputSuffix) { |
| 248 | + throw new ZarrException( |
| 249 | + "reshape codec: output dimension " + i + " specified by input dimensions " |
| 250 | + + Arrays.toString(inputDims) + " does not align with the raveled input array " |
| 251 | + + "(prefix " + outputPrefix + " vs " + inputPrefix + ", suffix " + outputSuffix |
| 252 | + + " vs " + inputSuffix + ")."); |
| 253 | + } |
| 254 | + } |
| 255 | + |
| 256 | + int[] result = new int[n]; |
| 257 | + for (int i = 0; i < n; i++) { |
| 258 | + if (outputShape[i] > Integer.MAX_VALUE) { |
| 259 | + throw new ZarrException("reshape codec: output dimension " + i + " exceeds Integer.MAX_VALUE."); |
| 260 | + } |
| 261 | + result[i] = (int) outputShape[i]; |
| 262 | + } |
| 263 | + return result; |
| 264 | + } |
| 265 | + |
| 266 | + private static int[] asInputDims(Object element) throws ZarrException { |
| 267 | + if (element instanceof int[]) { |
| 268 | + return (int[]) element; |
| 269 | + } |
| 270 | + if (element instanceof Object[]) { |
| 271 | + Object[] array = (Object[]) element; |
| 272 | + int[] dims = new int[array.length]; |
| 273 | + for (int i = 0; i < array.length; i++) { |
| 274 | + dims[i] = asInteger(array[i]); |
| 275 | + } |
| 276 | + return dims; |
| 277 | + } |
| 278 | + if (element instanceof List) { |
| 279 | + List<?> list = (List<?>) element; |
| 280 | + int[] dims = new int[list.size()]; |
| 281 | + for (int i = 0; i < list.size(); i++) { |
| 282 | + dims[i] = asInteger(list.get(i)); |
| 283 | + } |
| 284 | + return dims; |
| 285 | + } |
| 286 | + return null; |
| 287 | + } |
| 288 | + |
| 289 | + private static int asInteger(Object element) throws ZarrException { |
| 290 | + if (element instanceof Number) { |
| 291 | + return ((Number) element).intValue(); |
| 292 | + } |
| 293 | + throw new ZarrException( |
| 294 | + "reshape codec: 'shape' entries must be integers or arrays of integers, but got " + element + "."); |
| 295 | + } |
| 296 | + |
| 297 | + public static final class Configuration { |
| 298 | + @Nonnull |
| 299 | + public final Object[] shape; |
| 300 | + |
| 301 | + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) |
| 302 | + public Configuration(@Nonnull @JsonProperty(value = "shape", required = true) Object[] shape) { |
| 303 | + this.shape = shape; |
| 304 | + } |
| 305 | + } |
| 306 | +} |
0 commit comments