Skip to content

Commit 6fb9695

Browse files
konstibobnormanrzclaude
authored
add cast_value (#83)
* add cast_value * Fix nearest-away rounding for float targets and add fast paths to cast_value Fix #1 (correctness): CastValueConverter.roundToFloat/roundToDouble treated nearest-away as nearest-even for floating-point targets, so exact ties rounded to even instead of away from zero (spec violation for float->float narrowing). Both now round ties away from zero and fall back to nearest for non-ties. Fix #2 (performance): castArray now dispatches common casts (identity, float<->float, integer->integer) to primitive-arithmetic paths, avoiding the per-element Scalar/BigDecimal/BigInteger allocation of the exact path. Casts with scalar_map, uint64, or float directed-rounding fall back to the exact path, which remains the correctness reference. Adds tests for the float nearest-away tie and the integer/identity fast paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address cast_value review nits - Drop the unused DataType target parameter from the roundTrip test helper (the target already lives in the Configuration). - Reference parseFillValue via its defining class dev.zarr.zarrjava.core.ArrayMetadata instead of the v3 subclass. - Add uint64-above-Long.MAX_VALUE coverage, exercising the exact unsigned cast path (large fill value) and the identity fast path (array data). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Norman Rzepka <code@normanrz.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5a62756 commit 6fb9695

5 files changed

Lines changed: 1232 additions & 0 deletions

File tree

‎src/main/java/dev/zarr/zarrjava/v3/codec/CodecBuilder.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,20 @@ public CodecBuilder withTranspose(int[] order) {
6464
return this;
6565
}
6666

67+
public CodecBuilder withCastValue(CastValueCodec.Configuration configuration) {
68+
codecs.add(new CastValueCodec(configuration));
69+
return this;
70+
}
71+
72+
public CodecBuilder withCastValue(DataType dataType) {
73+
return withCastValue(new CastValueCodec.Configuration(dataType, null, null, null));
74+
}
75+
76+
public CodecBuilder withCastValue(DataType dataType, CastValueCodec.Rounding rounding,
77+
CastValueCodec.OutOfRange outOfRange) {
78+
return withCastValue(new CastValueCodec.Configuration(dataType, rounding, outOfRange, null));
79+
}
80+
6781
/**
6882
* Adds a {@code reshape} codec. Each entry of {@code shape} must be a positive {@link Integer}, the
6983
* special value {@code -1} (at most once), or an {@code int[]} / array of input dimension indices.

‎src/main/java/dev/zarr/zarrjava/v3/codec/CodecRegistry.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public class CodecRegistry {
1212

1313
static {
1414
addType("transpose", TransposeCodec.class);
15+
addType("cast_value", CastValueCodec.class);
1516
addType("reshape", ReshapeCodec.class);
1617
addType("bytes", BytesCodec.class);
1718
addType("blosc", BloscCodec.class);
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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.JsonInclude;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
import com.fasterxml.jackson.annotation.JsonValue;
8+
import dev.zarr.zarrjava.ZarrException;
9+
import dev.zarr.zarrjava.core.ArrayMetadata.CoreArrayMetadata;
10+
import dev.zarr.zarrjava.core.codec.ArrayArrayCodec;
11+
import dev.zarr.zarrjava.v3.DataType;
12+
import dev.zarr.zarrjava.v3.codec.Codec;
13+
import dev.zarr.zarrjava.v3.codec.core.CastValueConverter.ScalarEntry;
14+
import ucar.ma2.Array;
15+
16+
import javax.annotation.Nonnull;
17+
import javax.annotation.Nullable;
18+
import java.math.RoundingMode;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
/**
23+
* The {@code cast_value} codec converts (casts) the numeric value of every array element to a
24+
* different data type. It is an {@code array -> array} codec: it changes the stored data type while
25+
* leaving every other array property intact, and it does not reinterpret binary representations.
26+
*
27+
* <p>On encode the values are cast from the array data type to {@code configuration.data_type}; on
28+
* decode the same procedure runs with the input and output data types swapped. The actual numeric
29+
* conversion lives in {@link CastValueConverter}; this class only handles the Zarr codec integration:
30+
* configuration parsing, propagating the new data type to downstream codecs, and casting the fill
31+
* value.
32+
*
33+
* <p>Supported data types are the real-number types this library models: {@code int8/16/32/64},
34+
* {@code uint8/16/32/64}, {@code float32} and {@code float64}. Other data types from the codec
35+
* specification (e.g. {@code float8_*}, {@code bfloat16}, {@code int2}) are not modelled here.
36+
*/
37+
public class CastValueCodec extends ArrayArrayCodec implements Codec {
38+
39+
@JsonIgnore
40+
@Nonnull
41+
public final String name = "cast_value";
42+
@Nonnull
43+
public final Configuration configuration;
44+
45+
// Set up in resolveArrayMetadata (once, before any encode/decode call), then only read.
46+
@JsonIgnore
47+
private CastValueConverter converter;
48+
@JsonIgnore
49+
private List<ScalarEntry> encodeEntries = new ArrayList<>();
50+
@JsonIgnore
51+
private List<ScalarEntry> decodeEntries = new ArrayList<>();
52+
53+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
54+
public CastValueCodec(
55+
@Nonnull @JsonProperty(value = "configuration", required = true) Configuration configuration
56+
) {
57+
this.configuration = configuration;
58+
}
59+
60+
// ===== Codec pipeline integration ========================================================
61+
62+
@Override
63+
public Array encode(Array chunkArray) throws ZarrException {
64+
return converter.castArray(chunkArray, arrayDataType(), configuration.dataType, encodeEntries);
65+
}
66+
67+
@Override
68+
public Array decode(Array chunkArray) throws ZarrException {
69+
return converter.castArray(chunkArray, configuration.dataType, arrayDataType(), decodeEntries);
70+
}
71+
72+
@Override
73+
public long computeEncodedSize(long inputByteLength, CoreArrayMetadata arrayMetadata)
74+
throws ZarrException {
75+
long numElements = inputByteLength / arrayMetadata.dataType.getByteCount();
76+
return numElements * configuration.dataType.getByteCount();
77+
}
78+
79+
/**
80+
* Runs once when the codec pipeline is built. It validates the configuration, prepares the
81+
* converter and scalar_map lookups, casts the fill value to the target type, and reports the new
82+
* data type (and cast fill value) to the codecs downstream of this one.
83+
*/
84+
@Override
85+
public CoreArrayMetadata resolveArrayMetadata() throws ZarrException {
86+
super.resolveArrayMetadata();
87+
DataType inputType = arrayDataType();
88+
DataType outputType = configuration.dataType;
89+
CastValueConverter.requireSupported(inputType);
90+
CastValueConverter.requireSupported(outputType);
91+
if (configuration.outOfRange == OutOfRange.WRAP && CastValueConverter.isFloatTarget(outputType)) {
92+
throw new ZarrException(
93+
"The cast_value 'out_of_range' value 'wrap' is only permitted for integral target data types.");
94+
}
95+
96+
this.converter = new CastValueConverter(configuration.rounding, configuration.outOfRange);
97+
this.encodeEntries = converter.buildEntries(
98+
configuration.scalarMap == null ? null : configuration.scalarMap.encode, inputType, outputType);
99+
this.decodeEntries = converter.buildEntries(
100+
configuration.scalarMap == null ? null : configuration.scalarMap.decode, outputType, inputType);
101+
102+
Object srcFillValue = arrayMetadata.parsedFillValue;
103+
Object castFillValue = converter.castFillValue(srcFillValue, inputType, outputType, encodeEntries);
104+
if (srcFillValue != null
105+
&& !converter.fillValueRoundTrips(castFillValue, srcFillValue, outputType, inputType, decodeEntries)) {
106+
throw new ZarrException(
107+
"The cast_value fill value '" + srcFillValue + "' does not survive a round-trip cast.");
108+
}
109+
110+
return new CoreArrayMetadata(
111+
arrayMetadata.shape, arrayMetadata.chunkShape, outputType, castFillValue);
112+
}
113+
114+
private DataType arrayDataType() throws ZarrException {
115+
if (!(arrayMetadata.dataType instanceof DataType)) {
116+
throw new ZarrException("The cast_value codec requires a Zarr v3 data type.");
117+
}
118+
return (DataType) arrayMetadata.dataType;
119+
}
120+
121+
// ===== Configuration =====================================================================
122+
123+
/** How values are rounded when the target data type cannot exactly represent a value. */
124+
public enum Rounding {
125+
NEAREST_EVEN("nearest-even", RoundingMode.HALF_EVEN),
126+
TOWARDS_ZERO("towards-zero", RoundingMode.DOWN),
127+
TOWARDS_POSITIVE("towards-positive", RoundingMode.CEILING),
128+
TOWARDS_NEGATIVE("towards-negative", RoundingMode.FLOOR),
129+
NEAREST_AWAY("nearest-away", RoundingMode.HALF_UP);
130+
131+
private final String value;
132+
final RoundingMode mode;
133+
134+
Rounding(String value, RoundingMode mode) {
135+
this.value = value;
136+
this.mode = mode;
137+
}
138+
139+
@JsonValue
140+
public String getValue() {
141+
return value;
142+
}
143+
144+
@JsonCreator
145+
public static Rounding fromValue(String value) {
146+
for (Rounding rounding : values()) {
147+
if (rounding.value.equals(value)) {
148+
return rounding;
149+
}
150+
}
151+
throw new IllegalArgumentException("Unknown cast_value rounding: '" + value + "'.");
152+
}
153+
}
154+
155+
/** How values outside the representable range of the target data type are handled. */
156+
public enum OutOfRange {
157+
CLAMP("clamp"),
158+
WRAP("wrap");
159+
160+
private final String value;
161+
162+
OutOfRange(String value) {
163+
this.value = value;
164+
}
165+
166+
@JsonValue
167+
public String getValue() {
168+
return value;
169+
}
170+
171+
@JsonCreator
172+
public static OutOfRange fromValue(String value) {
173+
for (OutOfRange outOfRange : values()) {
174+
if (outOfRange.value.equals(value)) {
175+
return outOfRange;
176+
}
177+
}
178+
throw new IllegalArgumentException("Unknown cast_value out_of_range: '" + value + "'.");
179+
}
180+
}
181+
182+
/** Explicit input-to-output scalar mappings, evaluated before any other casting rule. */
183+
public static final class ScalarMap {
184+
@Nullable
185+
@JsonProperty("encode")
186+
public final Object[][] encode;
187+
@Nullable
188+
@JsonProperty("decode")
189+
public final Object[][] decode;
190+
191+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
192+
public ScalarMap(
193+
@Nullable @JsonProperty("encode") Object[][] encode,
194+
@Nullable @JsonProperty("decode") Object[][] decode) {
195+
this.encode = encode;
196+
this.decode = decode;
197+
}
198+
}
199+
200+
public static final class Configuration {
201+
202+
@Nonnull
203+
@JsonProperty("data_type")
204+
public final DataType dataType;
205+
206+
@Nonnull
207+
@JsonProperty("rounding")
208+
public final Rounding rounding;
209+
210+
@Nullable
211+
@JsonInclude(JsonInclude.Include.NON_NULL)
212+
@JsonProperty("out_of_range")
213+
public final OutOfRange outOfRange;
214+
215+
@Nullable
216+
@JsonInclude(JsonInclude.Include.NON_NULL)
217+
@JsonProperty("scalar_map")
218+
public final ScalarMap scalarMap;
219+
220+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
221+
public Configuration(
222+
@Nonnull @JsonProperty(value = "data_type", required = true) DataType dataType,
223+
@Nullable @JsonProperty("rounding") Rounding rounding,
224+
@Nullable @JsonProperty("out_of_range") OutOfRange outOfRange,
225+
@Nullable @JsonProperty("scalar_map") ScalarMap scalarMap) {
226+
this.dataType = dataType;
227+
this.rounding = rounding == null ? Rounding.NEAREST_EVEN : rounding;
228+
this.outOfRange = outOfRange;
229+
this.scalarMap = scalarMap;
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)