From ad5938847a630a0472f111a8521447d5c5d4cc45 Mon Sep 17 00:00:00 2001 From: Sean Arms <67096+lesserwhirls@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:21:26 -0600 Subject: [PATCH 1/5] Improve libaec tests Fix typo in previous test, and add a test for multithreaded use of libaec. --- .../compression/jna/libaec/TestLibAec.java | 28 +-- .../jna/libaec/TestLibAecMultithreaded.java | 217 ++++++++++++++++++ 2 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAecMultithreaded.java diff --git a/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAec.java b/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAec.java index 6824c6c7b6..c6f3a894b1 100644 --- a/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAec.java +++ b/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAec.java @@ -81,7 +81,7 @@ public void aecBufferDecode() { try (Memory inputMemory = new Memory(expectedEncoded.length * Byte.BYTES); Memory outputMemory = new Memory(origData.length * Integer.BYTES)) { // set encoding parameters - AecStream aecStreamEncode = AecStream.create(bitsPerSample, blockSize, rsi, flags); + AecStream aecStreamDecode = AecStream.create(bitsPerSample, blockSize, rsi, flags); // load expected encoded data into native memory byte[] expectedEncodedBytes = new byte[expectedEncoded.length]; @@ -90,18 +90,18 @@ public void aecBufferDecode() { } inputMemory.write(0, expectedEncodedBytes, 0, expectedEncodedBytes.length); - aecStreamEncode.setInputMemory(inputMemory); - aecStreamEncode.setOutputMemory(outputMemory); + aecStreamDecode.setInputMemory(inputMemory); + aecStreamDecode.setOutputMemory(outputMemory); // decode - int ok = LibAec.aec_buffer_decode(aecStreamEncode); + int ok = LibAec.aec_buffer_decode(aecStreamDecode); assertThat(ok).isEqualTo(AEC_OK); // check expected number of bytes decoded - assertThat(aecStreamEncode.total_out.intValue()).isEqualTo(origData.length * Integer.BYTES); + assertThat(aecStreamDecode.total_out.intValue()).isEqualTo(origData.length * Integer.BYTES); - // read encoded data from native memory - int[] decodedData = new int[aecStreamEncode.total_out.intValue() / Integer.BYTES]; + // read decoded data from native memory + int[] decodedData = new int[aecStreamDecode.total_out.intValue() / Integer.BYTES]; outputMemory.read(0, decodedData, 0, decodedData.length); // compare decoded data to original values @@ -150,23 +150,23 @@ public void roundTrip() { Memory outputMemory = new Memory(origData.length * Integer.BYTES)) { // set encoding parameters - AecStream aecStreamEncode = AecStream.create(bitsPerSample, blockSize, rsi, flags); + AecStream aecStreamDecode = AecStream.create(bitsPerSample, blockSize, rsi, flags); // load encoded data into memory inputMemory.write(0, encodedData, 0, encodedData.length); - aecStreamEncode.setInputMemory(inputMemory); - aecStreamEncode.setOutputMemory(outputMemory); + aecStreamDecode.setInputMemory(inputMemory); + aecStreamDecode.setOutputMemory(outputMemory); // decode - int ok = LibAec.aec_buffer_decode(aecStreamEncode); + int ok = LibAec.aec_buffer_decode(aecStreamDecode); assertThat(ok).isEqualTo(AEC_OK); // check expected number of bytes decoded - assertThat(aecStreamEncode.total_out.intValue()).isEqualTo(origData.length * Integer.BYTES); + assertThat(aecStreamDecode.total_out.intValue()).isEqualTo(origData.length * Integer.BYTES); - // read encoded data from native memory - int[] decodedData = new int[aecStreamEncode.total_out.intValue() / Integer.BYTES]; + // read decoded data from native memory + int[] decodedData = new int[aecStreamDecode.total_out.intValue() / Integer.BYTES]; outputMemory.read(0, decodedData, 0, decodedData.length); // compare decoded data to original values diff --git a/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAecMultithreaded.java b/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAecMultithreaded.java new file mode 100644 index 0000000000..4dfbf8c4e9 --- /dev/null +++ b/native-compression/libaec-jna/src/test/java/edu/ucar/unidata/compression/jna/libaec/TestLibAecMultithreaded.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2025 University Corporation for Atmospheric Research/Unidata + * See LICENSE for license information. + */ + +package edu.ucar.unidata.compression.jna.libaec; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_DATA_3BYTE; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_DATA_MSB; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_DATA_PREPROCESS; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_DATA_SIGNED; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_OK; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_RESTRICTED; + +import com.sun.jna.Memory; +import edu.ucar.unidata.compression.jna.libaec.LibAec.AecStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.IntStream; +import org.junit.Test; + +/** + * Test multithreaded access to native libaec by round tripping data in multiple threads + */ +public class TestLibAecMultithreaded { + + AtomicReference failed = new AtomicReference<>(); + CountDownLatch startupLatch, readyLatch, finishedLatch; + boolean testDebugMessages = true; + + static class AecTestCase { + int[] origData; + int bitsPerSample, blockSize, rsi, flags; + + AecTestCase(int[] origData, int bitsPerSample, int blockSize, int rsi, int flags) { + this.origData = origData; + this.bitsPerSample = bitsPerSample; + this.blockSize = blockSize; + this.rsi = rsi; + this.flags = flags; + } + } + + public static int[] createRandom(int n) { + Random r = new Random(); + int min = -10000; + int max = 10000; + return IntStream.generate(() -> r.nextInt(max - min) + min).limit(n).toArray(); + } + + public class RoundTripRunnable implements Runnable { + + AecTestCase myTestCase; + + RoundTripRunnable(AecTestCase testCase) { + this.myTestCase = testCase; + } + + @Override + public void run() { + if (testDebugMessages) { + System.out.println(Thread.currentThread().getId() + ", awaiting execution signal"); + } + try { + readyLatch.countDown(); + boolean startupReady = startupLatch.await(1, TimeUnit.SECONDS); + assertWithMessage("test startup took too long").that(startupReady).isTrue(); + } catch (InterruptedException e) { + failed.set(new AssertionError("test startup took too long", e));; + } + if (testDebugMessages) { + System.out.println(Thread.currentThread().getId() + ", executing run() method!"); + } + // byte array to hold the encoded data + byte[] encodedData; + + // convert input data into byte array + ByteBuffer bb = ByteBuffer.allocate(myTestCase.origData.length * Integer.BYTES); + bb.order(ByteOrder.nativeOrder()); + bb.asIntBuffer().put(myTestCase.origData); + byte[] inputData = bb.array(); + final int maxTotalOutBytes = myTestCase.origData.length * Integer.BYTES * 67 / 64 + 256; + try { + // initialize native memory to hold input and output data + try (Memory inputMemory = new Memory(inputData.length * Byte.BYTES); + Memory outputMemory = new Memory(maxTotalOutBytes)) { + + // set encoding parameters + AecStream aecStreamDecode = + AecStream.create(myTestCase.bitsPerSample, myTestCase.blockSize, myTestCase.rsi, myTestCase.flags); + + // load input data into native memory + inputMemory.write(0, inputData, 0, inputData.length); + aecStreamDecode.setInputMemory(inputMemory); + + aecStreamDecode.setOutputMemory(outputMemory); + + // encode + int ok = LibAec.aec_buffer_encode(aecStreamDecode); + assertThat(ok).isEqualTo(AEC_OK); + + // read encoded data from native memory + encodedData = new byte[aecStreamDecode.total_out.intValue()]; + outputMemory.read(0, encodedData, 0, encodedData.length); + } + + assertThat(encodedData).isNotNull(); + + try (Memory inputMemory = new Memory(encodedData.length * Byte.BYTES); + Memory outputMemory = new Memory((long) myTestCase.origData.length * Integer.BYTES)) { + + // set encoding parameters + AecStream aecStreamDecode = + AecStream.create(myTestCase.bitsPerSample, myTestCase.blockSize, myTestCase.rsi, myTestCase.flags); + + // load encoded data into memory + inputMemory.write(0, encodedData, 0, encodedData.length); + + aecStreamDecode.setInputMemory(inputMemory); + aecStreamDecode.setOutputMemory(outputMemory); + + // decode + int ok = LibAec.aec_buffer_decode(aecStreamDecode); + assertThat(ok).isEqualTo(AEC_OK); + + // check expected number of bytes decoded + assertThat(aecStreamDecode.total_out.intValue()).isEqualTo(myTestCase.origData.length * Integer.BYTES); + + // read decoded data from native memory + int[] decodedData = new int[aecStreamDecode.total_out.intValue() / Integer.BYTES]; + outputMemory.read(0, decodedData, 0, decodedData.length); + + // compare decoded data to original values + for (int i = 0; i < decodedData.length; i++) { + assertThat(decodedData[i]).isEqualTo(myTestCase.origData[i]); + } + } + if (testDebugMessages) { + System.out.println(Thread.currentThread().getId() + ", finished!"); + } + } catch (AssertionError ae) { + failed.set(ae); + } finally { + finishedLatch.countDown(); + } + } + } + + void runTest(AecTestCase[] myTestCases) { + startupLatch = new CountDownLatch(1); + if (testDebugMessages) { + System.out.println("Main thread is: " + Thread.currentThread().getName()); + } + + + finishedLatch = new CountDownLatch(myTestCases.length); + readyLatch = new CountDownLatch(myTestCases.length); + for (AecTestCase myTestCase : myTestCases) { + Thread t = new Thread(new RoundTripRunnable(myTestCase)); + t.start(); + } + // ensure threaded tests start running at the same time + try { + boolean testThreadsReady = readyLatch.await(1, TimeUnit.SECONDS); + assertWithMessage("test threads took too long to prepare").that(testThreadsReady).isTrue(); + } catch (InterruptedException e) { + failed.set(new AssertionError("test threads took too long to prepare", e));; + } + // trigger threaded tests to start running + if (testDebugMessages) { + System.out.println("Start testing threads from: " + Thread.currentThread().getName()); + } + startupLatch.countDown(); + // wait for tests to finish + try { + boolean threadsComplete = finishedLatch.await(5, TimeUnit.SECONDS); + assertWithMessage("test threads failed to complete").that(threadsComplete).isTrue(); + } catch (InterruptedException e) { + failed.set(new AssertionError("test threads took too long to complete", e));; + } + if (failed.get() != null) { + throw failed.get(); + } + } + + @Test + public void testMultithreadedDifferentParams() { + AecTestCase[] differentParamTestCases = + new AecTestCase[] {new AecTestCase(createRandom(2500), 32, 32, 64, AEC_DATA_SIGNED), + new AecTestCase(createRandom(210), 32, 16, 128, AEC_DATA_MSB | AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(24), 32, 8, 128, AEC_DATA_3BYTE | AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(2400), 32, 16, 128, AEC_RESTRICTED), + new AecTestCase(createRandom(29), 32, 32, 128, AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(255), 32, 8, 128, AEC_DATA_SIGNED | AEC_DATA_MSB | AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(2045), 32, 16, 128, AEC_DATA_3BYTE)}; + runTest(differentParamTestCases); + } + + @Test + public void testMultiThreadedSameParams() { + AecTestCase[] sameParamTestCases = + new AecTestCase[] {new AecTestCase(createRandom(2500), 32, 16, 128, AEC_DATA_SIGNED | AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(2500), 32, 16, 128, AEC_DATA_SIGNED | AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(2500), 32, 16, 128, AEC_DATA_SIGNED | AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(2500), 32, 16, 128, AEC_DATA_SIGNED | AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(2500), 32, 16, 128, AEC_DATA_SIGNED | AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(2500), 32, 16, 128, AEC_DATA_SIGNED | AEC_DATA_PREPROCESS), + new AecTestCase(createRandom(2500), 32, 16, 128, AEC_DATA_SIGNED | AEC_DATA_PREPROCESS)}; + runTest(sameParamTestCases); + } +} From 2c0a7cbacaaae1784ae781d3cda9b335a1bb003a Mon Sep 17 00:00:00 2001 From: Sean Arms <67096+lesserwhirls@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:24:53 -0600 Subject: [PATCH 2/5] Add GRIB2 DRS 42 support Closes Unidata/netcdf-java#753 --- .../overview/UsingNetcdfJava.md | 19 +++- grib/build.gradle | 6 ++ .../ucar/nc2/grib/grib2/Grib2DataReader.java | 89 +++++++++++++++- .../java/ucar/nc2/grib/grib2/Grib2Drs.java | 71 +++++++++++-- grib/src/test/data/sref.pds2.drs42.grib2 | Bin 0 -> 11640 bytes .../java/ucar/nc2/grib/grib2/TestDrs42.java | 95 ++++++++++++++++++ 6 files changed, 271 insertions(+), 9 deletions(-) create mode 100644 grib/src/test/data/sref.pds2.drs42.grib2 create mode 100644 grib/src/test/java/ucar/nc2/grib/grib2/TestDrs42.java diff --git a/docs/src/site/pages/netcdfJava_tutorial/overview/UsingNetcdfJava.md b/docs/src/site/pages/netcdfJava_tutorial/overview/UsingNetcdfJava.md index 1f356ca547..b1ead0fb64 100644 --- a/docs/src/site/pages/netcdfJava_tutorial/overview/UsingNetcdfJava.md +++ b/docs/src/site/pages/netcdfJava_tutorial/overview/UsingNetcdfJava.md @@ -123,13 +123,30 @@ dependencies { runtimeOnly "edu.ucar:cdm-mcidas:${netcdfJavaVersion}" } ~~~ + +## Native compression code + +netCDF-Java `>=v5.8.0` supports libaec compression for GRIB2 messages using JNA to call the libaec C library. +To ease the use of this feature, we now distribute a jar file containing the native libraries for the following platforms/architectures: + +|--- +| platform | x86-64 | aarch64 +|:-|:-:|:-: +| Linux | | +| MacOS | | +| Windows | | + +If you are using on the of the supported platform/architecture combinations above, you may include the `edu.ucar:libaec-native:${netcdfJavaVersion}` artifact in your project to bypass the need to install libaec on your system. +Otherwise, libaec will need to be installed and reachable in your system library path in order to read data compressed using libaec. + ## Building with netcdfAll This is the appropriate option if you’re not using a dependency management tool like Maven or Gradle and you don’t care about jar size or compatibility with other libraries. Simply include netcdfAll-${netcdfJavaVersion}.jar on the classpath when you run your program. You’ll also need a logger. -Currently does not include `cdm-s3` due to the size of the AWS S3 SDK dependency. +Currently, the netcdfAll jar does not include `cdm-s3` due to the size of the AWS S3 SDK dependency, and does not include the `libaec-native` (native library binaries for libaec). ## Logging + The netCDF-Java library uses the SLF4J logging facade. This allows applications to choose their own logging implementation, by including the appropriate jar file on the classpath at runtime. Common choices are `JDK logging` and `Log4J 2`: diff --git a/grib/build.gradle b/grib/build.gradle index 047909fd9c..1af91b43ba 100644 --- a/grib/build.gradle +++ b/grib/build.gradle @@ -11,6 +11,8 @@ dependencies { api project(':cdm:cdm-core') + implementation project(':native-compression:libaec-jna') + implementation 'com.google.protobuf:protobuf-java' implementation 'org.jdom:jdom2' implementation 'com.google.code.findbugs:jsr305' @@ -19,11 +21,15 @@ dependencies { implementation 'com.beust:jcommander' implementation 'com.google.re2j:re2j' implementation 'org.slf4j:slf4j-api' + implementation 'net.java.dev.jna:jna' testImplementation project(':cdm-test-utils') testImplementation project(':udunits') testImplementation 'com.google.truth:truth' testImplementation 'org.jsoup:jsoup' + + testRuntimeOnly project(':native-compression:libaec-native') + testRuntimeOnly 'ch.qos.logback:logback-classic' } diff --git a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2DataReader.java b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2DataReader.java index ce4e0af311..164c119a37 100644 --- a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2DataReader.java +++ b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2DataReader.java @@ -1,10 +1,16 @@ /* - * Copyright (c) 1998-2021 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. */ package ucar.nc2.grib.grib2; +import static edu.ucar.unidata.compression.jna.libaec.LibAec.AEC_OK; + +import com.sun.jna.Memory; +import edu.ucar.unidata.compression.jna.libaec.LibAec; +import edu.ucar.unidata.compression.jna.libaec.LibAec.AecStream; +import java.nio.ByteBuffer; import javax.annotation.Nullable; import ucar.nc2.grib.GribNumbers; import ucar.nc2.grib.GribUtils; @@ -65,7 +71,7 @@ public class Grib2DataReader { } /* - * Code Table Code table 5.0 - Data representation template number (5.0) + * Code table 5.0 - Data representation template number (5.0) * 0: Grid point data - simple packing * 1: Matrix value at grid point - simple packing * 2: Grid point data - complex packing @@ -73,10 +79,12 @@ public class Grib2DataReader { * 4: Grid point data - IEEE floating point data * 40: Grid point data - JPEG 2000 code stream format * 41: Grid point data - Portable Network Graphics (PNG) + * 42: Grid point data - CCSDS recommended lossless compression * 50: Spectral data - simple packing * 51: Spherical harmonics data - complex packing * 61: Grid point data - simple packing with logarithm pre-processing * 200: Run length packing with level values + * 50002: Second order packing * 65535: Missing */ @@ -111,6 +119,9 @@ public float[] getData(RandomAccessFile raf, Grib2SectionBitMap bitmapSection, G case 41: data = getData41(raf, (Grib2Drs.Type0) gdrs); break; + case 42: + data = getData42(raf, (Grib2Drs.Type42) gdrs); + break; case 50002: data = getData50002(raf, (Grib2Drs.Type50002) gdrs); break; @@ -1073,6 +1084,80 @@ private float[] getData50002(RandomAccessFile raf, Grib2Drs.Type50002 gdrs) thro } + private float[] getData42(RandomAccessFile raf, Grib2Drs.Type42 gdrs) throws IOException { + byte[] decodedData; + + // read CCSDS encoded stream from message + int encodedLength = dataLength - 5; + byte[] inputData = new byte[encodedLength]; + raf.readFully(inputData); + + int nbytesPerSample = (gdrs.numberOfBits + 7) / 8; + + try (Memory inputMemory = new Memory(encodedLength); + Memory outputMemory = new Memory((long) nbytesPerSample * totalNPoints)) { + + // set encoding parameters + AecStream aecStreamDecode = AecStream.create(gdrs.numberOfBits, gdrs.blockSize, gdrs.referenceSampleInterval, + gdrs.compressionOptionsMask); + + // load data from grib message into memory + inputMemory.write(0, inputData, 0, inputData.length); + + aecStreamDecode.setInputMemory(inputMemory); + aecStreamDecode.setOutputMemory(outputMemory); + + // decode + int ok = LibAec.aec_buffer_decode(aecStreamDecode); + if (ok != AEC_OK) { + System.out.printf("AEC Error: %s%n", ok); + } + + // read decoded data from native memory + decodedData = new byte[nbytesPerSample * totalNPoints]; + outputMemory.read(0, decodedData, 0, decodedData.length); + } + + // will use this to read out a long value using nbytesPerSample bytes + // see long getNextLong(ByteBuffer bb, int numberOfBytes) + ByteBuffer bb = ByteBuffer.wrap(decodedData); + + // decode following regulation 92.9.4, Note 4 + int D = gdrs.decimalScaleFactor; + float DD = (float) Math.pow((double) 10, (double) D); + float R = gdrs.referenceValue; + int E = gdrs.binaryScaleFactor; + float EE = (float) Math.pow(2.0, (double) E); + float[] data = new float[decodedData.length]; + if (bitmap == null) { + for (int i = 0; i < totalNPoints; i++) { + data[i] = (R + getNextLong(bb, nbytesPerSample) * EE) / DD; + } + } else { + for (int i = 0; i < totalNPoints; i++) { + if (GribNumbers.testBitIsSet(bitmap[i / 8], i % 8)) { + data[i] = (R + getNextLong(bb, nbytesPerSample) * EE) / DD; + } else { + data[i] = staticMissingValue; + } + } + } + return data; + } + + private long getNextLong(ByteBuffer bb, int numberOfBytes) throws IOException { + switch (numberOfBytes) { + case 1: + return Byte.toUnsignedLong(bb.get()); + case 2: + return Short.toUnsignedLong(bb.getShort()); + case 4: + return Integer.toUnsignedLong(bb.getInt()); + default: + throw new IOException("Invalid number of bytes per sample for GDR42: " + numberOfBytes); + } + } + /* * * Flag table 3.4 – Scanning mode diff --git a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Drs.java b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Drs.java index 8dda0d266f..96ea4e8e6c 100644 --- a/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Drs.java +++ b/grib/src/main/java/ucar/nc2/grib/grib2/Grib2Drs.java @@ -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. */ @@ -22,15 +22,17 @@ public abstract class Grib2Drs { public static Grib2Drs factory(int template, RandomAccessFile raf) throws IOException { switch (template) { - case 0: - case 41: + case 0: // simple packing + case 41: // PNG return new Type0(raf); - case 2: + case 2: // complex packing return new Type2(raf); - case 3: + case 3: // complex packing and spatial differencing return new Type3(raf); - case 40: + case 40: // JPEG2000 return new Type40(raf); + case 42: // CCSDS + return new Type42(raf); case 50002: // ECMWF's second order packing return new Type50002(raf); default: @@ -441,6 +443,63 @@ public int hashCode() { } } + + /* + * Data representation template 5.42 – Grid point data - CCSDS recommended lossless compression + * Note: For most templates, details of the packing process are described in Regulation 92.9.4. + * Octet No. Contents + * 12-15 Reference value (R) (IEEE 32-bit floating-point value) + * 16-17 Binary scale factor (E) + * 18-19 Decimal scale factor (D) + * 20 Number of bits required to hold the resulting scaled and referenced data values. (see Note 1) + * 21 Type of original field values (see Code Table 5.1) + * --- additions to Type0 below --- + * 22 CCSDS compression options mask (see Note 3) + * 23 Block size + * 24-25 Reference sample interval + */ + public static class Type42 extends Type0 { + + int compressionOptionsMask, blockSize, referenceSampleInterval; + + Type42(RandomAccessFile raf) throws IOException { + super(raf); + this.compressionOptionsMask = raf.read(); + this.blockSize = raf.read(); + this.referenceSampleInterval = GribNumbers.uint2(raf); + System.out.println("hello"); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("compressionOptionsMask", compressionOptionsMask) + .add("blockSize", blockSize).add("referenceSampleInterval", referenceSampleInterval).toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + + Type42 type2 = (Type42) o; + + if (compressionOptionsMask != type2.compressionOptionsMask) { + return false; + } + if (blockSize != type2.blockSize) { + return false; + } + return referenceSampleInterval == type2.referenceSampleInterval; + } + } + // pull request #52 "lost-carrier" jkaehler@meteomatics.com public static class Type50002 extends Grib2Drs { diff --git a/grib/src/test/data/sref.pds2.drs42.grib2 b/grib/src/test/data/sref.pds2.drs42.grib2 new file mode 100644 index 0000000000000000000000000000000000000000..c41e9db58c78c7d1d7ead03a23de6e4c4ee137f2 GIT binary patch literal 11640 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 literal 0 HcmV?d00001 diff --git a/grib/src/test/java/ucar/nc2/grib/grib2/TestDrs42.java b/grib/src/test/java/ucar/nc2/grib/grib2/TestDrs42.java new file mode 100644 index 0000000000..66f174ecb0 --- /dev/null +++ b/grib/src/test/java/ucar/nc2/grib/grib2/TestDrs42.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025 University Corporation for Atmospheric Research/Unidata + * See LICENSE for license information. + */ + +package ucar.nc2.grib.grib2; + +import static com.google.common.truth.Truth.assertThat; + +import java.io.IOException; +import java.util.Formatter; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import ucar.ma2.Array; +import ucar.ma2.MAMath; +import ucar.ma2.MAMath.MinMax; +import ucar.nc2.NetcdfFile; +import ucar.nc2.NetcdfFiles; +import ucar.nc2.Variable; +import ucar.nc2.util.CompareNetcdf2; +import ucar.unidata.util.test.TestDir; +import ucar.unidata.util.test.category.NeedsCdmUnitTest; + +public class TestDrs42 { + + @Test + public void CCSDS1byte() throws IOException { + String origFile = TestDir.localTestDataDir + "sref.pds2.grib2"; + // drs42File is origFile convert from Simple Packing to CCSDS by ecCodes + // grib_set -w isGridded=1 -r -s packingType=grid_ccsds sref.pds2.grib2 sref.pds2.drs42.grib2 + // This file uses 8 bits per sample in the CCSDS configuration + String drs42File = TestDir.localTestDataDir + "sref.pds2.drs42.grib2"; + final String variableName = "u-component_of_wind_height_above_ground_weightedMean"; + + // stats from ecCodes + final float expectedMax = 13.4F; + final float expectedMin = -9.9F; + final float expectedAverage = 0.246189F; + + final int expectedLength = 23865; + final float tol = 1e-6F; + + try (NetcdfFile nc42 = NetcdfFiles.open(drs42File)) { + Variable v = nc42.findVariable(variableName); + assertThat(v != null).isTrue(); + Array data = v.read(); + + assertThat(data).isNotNull(); + assertThat(data.getSize()).isEqualTo(expectedLength); + MinMax extremes = MAMath.getMinMax(data); + assertThat(extremes.max).isWithin(tol).of(expectedMax); + assertThat(extremes.min).isWithin(tol).of(expectedMin); + assertThat(MAMath.sumDouble(data) / data.getSize()).isWithin(tol).of(expectedAverage); + + // compare repacked ccsds data with original data + try (NetcdfFile ncOrig = NetcdfFiles.open(origFile)) { + Formatter f = new Formatter(); + CompareNetcdf2 compare = new CompareNetcdf2(f, false, false, true); + boolean ok = compare.compare(ncOrig, nc42, null); + System.out.printf("%s %s%n", ok ? "OK" : "NOT OK", f); + assertThat(ok).isTrue(); + } + } + } + + @Test + @Category(NeedsCdmUnitTest.class) + public void checkVariable2Bytes() throws IOException { + // This file uses 16 bits per sample in the CCSDS configuration + String drs42File = TestDir.cdmUnitTestDir + "formats/grib2/drs42/" + + "icon-eu_europe_regular-lat-lon_single-level_2025031912_014_T_2M_CCSDS.grib2"; + final String variableName = "Temperature_height_above_ground"; + + // stats from ecCodes + final float expectedMax = 298.187F; + final float expectedMin = 255.639F; + final float expectedAverage = 278.009F; + + final int expectedLength = 904689; + final float tol = 1e-3F; + + try (NetcdfFile nc42 = NetcdfFiles.open(drs42File)) { + Variable v = nc42.findVariable(variableName); + assertThat(v != null).isTrue(); + Array data = v.read(); + + assertThat(data).isNotNull(); + assertThat(data.getSize()).isEqualTo(expectedLength); + MinMax extremes = MAMath.getMinMax(data); + assertThat(extremes.max).isWithin(tol).of(expectedMax); + assertThat(extremes.min).isWithin(tol).of(expectedMin); + assertThat(MAMath.sumDouble(data) / data.getSize()).isWithin(tol).of(expectedAverage); + } + } +} From c9ebc73b7b67f731ed327cad83e165abf804d673 Mon Sep 17 00:00:00 2001 From: Sean Arms <67096+lesserwhirls@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:26:54 -0600 Subject: [PATCH 3/5] Update year in license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 857e3e1f36..e974842e46 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 1998-2023, University Corporation for Atmospheric Research/Unidata +Copyright (c) 1998-2025, University Corporation for Atmospheric Research/Unidata All rights reserved. Redistribution and use in source and binary forms, with or without From c1ef2cca47e7814b59330cb8c76e0388e34f66e7 Mon Sep 17 00:00:00 2001 From: Sean Arms <67096+lesserwhirls@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:27:22 -0600 Subject: [PATCH 4/5] Add libaec projects to bom --- netcdf-java-bom/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netcdf-java-bom/build.gradle b/netcdf-java-bom/build.gradle index 872e2f063d..bc5524b8f0 100644 --- a/netcdf-java-bom/build.gradle +++ b/netcdf-java-bom/build.gradle @@ -23,6 +23,8 @@ dependencies { api project(':grib') api project(':httpservices') api project(':legacy') + api project(':native-compression:libaec-jna') + api project(':native-compression:libaec-native') api project(':netcdf4') api project(':opendap') api project(':dap4') From 39aad4ef01edb191c8a0af9e1b58516de511580f Mon Sep 17 00:00:00 2001 From: Sean Arms <67096+lesserwhirls@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:28:04 -0600 Subject: [PATCH 5/5] Bump netCDF-Java version to 5.8.0-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 53d732edee..3aba602d43 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ allprojects { // We try to follow semantic versioning, and thus we use ..- // may be SNAPSHOT, alphax, betax, etc. // Note - if bumping to a new major or minor version, be sure to update the docs - version = '5.7.1-SNAPSHOT' + version = '5.8.0-SNAPSHOT' status = 'development' }