Commit 1acb4341 authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: gpu.testdata package: standalone kernel-test export/import (oracle contract)

New package com.elphel.imagej.gpu.testdata implementing the Java side of
tile_processor_gpu/src/tests/testdata_format.md:
- TestDataWriter: raw little-endian arrays + manifest.txt (prm/buf records)
- TestDataReader: parse manifest or C++ results back for ImageJ comparison
- AvgTdExport: full clt_average_sensors test case (per-sensor TD from
  GpuQuad, CuasTD.consolidateSensorsTD expected outputs + counts, optional
  TpTask per-sensor XY for the OOB stage) + compareResults round-trip check

Round-trip verified bit-exact (max|diff|=0): TestDataWriter -> CUDA
test_avg_td on GPU -> TestDataReader.
Co-Authored-By: 's avatarClaude Fable 5 <noreply@anthropic.com>
parent 6a274e46
/**
**
** AvgTdExport.java - export a complete standalone test case for the
** clt_average_sensors CUDA kernel (16-sensor TD consolidation, roadmap
** step 2c): per-sensor TD data straight from the GPU, the CPU oracle's
** expected output (CuasTD.consolidateSensorsTD) and, when available,
** per-sensor tile XY from TpTask for the OOB/edge weighting extension.
** The C++ side (tile_processor_gpu/src/tests/test_avg_td.cu, debuggable in
** NSight) loads the directory, runs the kernel and self-checks bit-exactly.
**
** Copyright (C) 2026 Elphel, Inc.
**
** -----------------------------------------------------------------------------**
**
** AvgTdExport.java is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see <http://www.gnu.org/licenses/>.
** -----------------------------------------------------------------------------**
**
** By Claude on 07/09/2026
*/
package com.elphel.imagej.gpu.testdata;
import java.io.IOException;
import com.elphel.imagej.cuas.rt.CuasTD;
import com.elphel.imagej.gpu.GPUTileProcessor;
import com.elphel.imagej.gpu.GpuQuad;
import com.elphel.imagej.gpu.TpTask;
public class AvgTdExport {
/**
* Export everything the standalone clt_average_sensors test needs.
* Call with a GpuQuad holding a current 16-sensor TD (after
* convert_direct), e.g. next to CuasTD.validateConsolidation().
*
* @param gpuQuad GPU instance with per-sensor CLT data
* @param use_ref use the reference-scene TD buffers
* @param tp_tasks task list used for the conversion (per-sensor tile XY
* for the OOB stage) or null to skip
* @param dir_path test-case directory, e.g. <model>/testdata/avg_td
* @param comment provenance line (scene timestamp etc.)
* @param debugLevel debug level
* @return dir_path on success
*/
public static String export(
GpuQuad gpuQuad,
boolean use_ref,
TpTask [] tp_tasks,
String dir_path,
String comment,
int debugLevel) throws IOException {
final int dtt = GPUTileProcessor.DTT_SIZE;
final int [] wh = gpuQuad.getWH(use_ref);
final int tilesX = wh[0] / dtt;
final int tilesY = wh[1] / dtt;
final int colors = gpuQuad.num_colors;
final int num_sens = gpuQuad.num_cams;
final int num_tiles = tilesY * tilesX * colors;
final float [][] fclt = gpuQuad.getCltData(use_ref); // [num_sens][num_tiles*256]
final int chunk = CuasTD.TD_CHUNK; // 256
if (fclt[0].length != (num_tiles * chunk)) {
throw new IllegalStateException("AvgTdExport: CLT length " + fclt[0].length +
" != tiles " + num_tiles + " * " + chunk);
}
// CPU oracle: NaN-aware average + contributing-sensor counts
final int [] counts = new int [num_tiles];
final float [] avg = CuasTD.consolidateSensorsTD(fclt, counts);
final float [] fcounts = new float [num_tiles]; // kernel outputs counts as f32
for (int i = 0; i < num_tiles; i++) fcounts[i] = counts[i];
final float [] td_sens = new float [num_sens * num_tiles * chunk];
for (int nsens = 0; nsens < num_sens; nsens++) {
System.arraycopy(fclt[nsens], 0, td_sens, nsens * num_tiles * chunk, num_tiles * chunk);
}
try (TestDataWriter tdw = new TestDataWriter(dir_path, "avg_td test case: " + comment)) {
tdw.prm("num_sensors", num_sens);
tdw.prm("tilesx", tilesX);
tdw.prm("tilesy", tilesY);
tdw.prm("colors", colors);
tdw.buf("td_sens", td_sens, num_sens, tilesY, tilesX, colors, chunk);
tdw.buf("expected_td_avg", avg, tilesY, tilesX, colors, chunk);
tdw.buf("expected_counts", fcounts, num_tiles);
if (tp_tasks != null) { // inputs for the OOB/edge-weighting stage
final int ntasks = tp_tasks.length;
final int [] task_tilexy = new int [ntasks * 2];
final float [] task_xy = new float [ntasks * num_sens * 2];
for (int i = 0; i < ntasks; i++) {
task_tilexy[2 * i] = tp_tasks[i].getTileX();
task_tilexy[2 * i + 1] = tp_tasks[i].getTileY();
float [][] xy = tp_tasks[i].getXY(); // [num_sens][2] per-sensor px
for (int nsens = 0; nsens < num_sens; nsens++) {
task_xy[(i * num_sens + nsens) * 2] = xy[nsens][0];
task_xy[(i * num_sens + nsens) * 2 + 1] = xy[nsens][1];
}
}
tdw.prm("img_width", wh[0]);
tdw.prm("img_height", wh[1]);
tdw.buf("task_tilexy", task_tilexy, ntasks, 2);
tdw.buf("task_xy", task_xy, ntasks, num_sens, 2);
}
if (debugLevel > -2) {
System.out.println("AvgTdExport: wrote " + tdw.getDir() +
" (" + num_sens + " sensors, " + tilesY + "x" + tilesX +
"x" + colors + " tiles" +
((tp_tasks != null) ? (", " + tp_tasks.length + " tasks") : "") + ")");
}
return tdw.getDir();
}
}
/**
* Import the C++ test results back and compare against the oracle
* reference stored in the same test-case directory (Java-side round-trip
* check; the C++ test already self-checks).
* @return max abs difference over finite elements (NaN-mismatch counts as
* Double.POSITIVE_INFINITY)
*/
public static double compareResults(String dir_path, int debugLevel) throws IOException {
TestDataReader expected = new TestDataReader(dir_path);
TestDataReader results = TestDataReader.results(dir_path);
double max_diff = 0.0;
for (String name : new String [] {"td_avg", "counts"}) {
if (!results.has(name) || !expected.has("expected_" + name)) continue;
float [] r = results.getFloats(name);
float [] e = expected.getFloats("expected_" + name);
double md = 0.0;
int nan_mismatch = 0;
for (int i = 0; i < r.length; i++) {
boolean rn = Float.isNaN(r[i]), en = Float.isNaN(e[i]);
if (rn || en) {
if (rn != en) nan_mismatch++;
continue;
}
md = Math.max(md, Math.abs((double) r[i] - (double) e[i]));
}
if (nan_mismatch > 0) md = Double.POSITIVE_INFINITY;
if (debugLevel > -2) {
System.out.println("AvgTdExport.compareResults: '" + name +
"' max|diff|=" + md + ", NaN-mismatch=" + nan_mismatch);
}
max_diff = Math.max(max_diff, md);
}
return max_diff;
}
}
/**
**
** TestDataReader.java - read a standalone-GPU-test data directory back into
** Java: parses manifest.txt (or results/results_manifest.txt written by the
** C++ per-kernel test) and loads raw little-endian arrays, so kernel results
** can be compared against the oracle and inspected in ImageJ. Counterpart of
** TestDataWriter; contract in tile_processor_gpu/src/tests/testdata_format.md.
**
** Copyright (C) 2026 Elphel, Inc.
**
** -----------------------------------------------------------------------------**
**
** TestDataReader.java is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see <http://www.gnu.org/licenses/>.
** -----------------------------------------------------------------------------**
**
** By Claude on 07/09/2026
*/
package com.elphel.imagej.gpu.testdata;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class TestDataReader {
public static class Entry {
public String name;
public String dtype; // "f32" | "i32"
public int [] dims; // row-major, last fastest
public String file; // relative to the manifest directory
}
private final File dir;
private final Map<String,Double> params = new HashMap<>();
private final Map<String,Entry> bufs = new LinkedHashMap<>();
/** Read <dir_path>/manifest.txt (a test-case input directory). */
public TestDataReader(String dir_path) throws IOException {
this(dir_path, "manifest.txt");
}
/** Read the C++ test results: <case_dir>/results/results_manifest.txt. */
public static TestDataReader results(String case_dir) throws IOException {
return new TestDataReader(case_dir + File.separator + "results",
"results_manifest.txt");
}
private TestDataReader(String dir_path, String manifest_name) throws IOException {
dir = new File(dir_path);
File mf = new File(dir, manifest_name);
List<String> lines = Files.readAllLines(mf.toPath());
int lnum = 0;
for (String line : lines) {
lnum++;
int hash = line.indexOf('#');
if (hash >= 0) line = line.substring(0, hash);
String [] tok = line.trim().split("\\s+");
if ((tok.length == 1) && tok[0].isEmpty()) continue;
if (tok[0].equals("prm")) {
if (tok.length != 3) throw new IOException(badLine(mf, lnum));
params.put(tok[1], Double.parseDouble(tok[2]));
} else if (tok[0].equals("buf")) {
if (tok.length < 5) throw new IOException(badLine(mf, lnum));
Entry e = new Entry();
e.name = tok[1];
e.dtype = tok[2];
int nd = Integer.parseInt(tok[3]);
if (tok.length != (5 + nd)) throw new IOException(badLine(mf, lnum));
e.dims = new int [nd];
for (int i = 0; i < nd; i++) e.dims[i] = Integer.parseInt(tok[4 + i]);
e.file = tok[4 + nd];
bufs.put(e.name, e);
} else {
throw new IOException(badLine(mf, lnum));
}
}
}
private static String badLine(File mf, int lnum) {
return "TestDataReader: bad manifest line " + lnum + " in " + mf.getPath();
}
public boolean has(String name) { return bufs.containsKey(name); }
public String [] getNames() { return bufs.keySet().toArray(new String [0]); }
public Entry getEntry(String name) { return bufs.get(name); }
public double param(String name, double dflt) {
Double v = params.get(name);
return (v == null) ? dflt : v;
}
public int paramInt(String name, int dflt) {
return (int) Math.round(param(name, dflt));
}
public int [] dims(String name) {
return entry(name).dims;
}
/** Load a buffer as float32 (must be dtype f32). */
public float [] getFloats(String name) throws IOException {
Entry e = entry(name);
if (!e.dtype.equals("f32")) {
throw new IOException("TestDataReader: '" + name + "' is " + e.dtype + ", not f32");
}
ByteBuffer bb = readRaw(e);
float [] data = new float [bb.remaining() / 4];
bb.asFloatBuffer().get(data);
return data;
}
/** Load a buffer as int32 (must be dtype i32). */
public int [] getInts(String name) throws IOException {
Entry e = entry(name);
if (!e.dtype.equals("i32")) {
throw new IOException("TestDataReader: '" + name + "' is " + e.dtype + ", not i32");
}
ByteBuffer bb = readRaw(e);
int [] data = new int [bb.remaining() / 4];
bb.asIntBuffer().get(data);
return data;
}
private Entry entry(String name) {
Entry e = bufs.get(name);
if (e == null) {
throw new IllegalArgumentException("TestDataReader: no buffer '" + name +
"' in " + dir.getPath());
}
return e;
}
private ByteBuffer readRaw(Entry e) throws IOException {
byte [] raw = Files.readAllBytes(new File(dir, e.file).toPath());
long expect = 4;
for (int d : e.dims) expect *= d;
if (raw.length != expect) {
throw new IOException("TestDataReader: " + e.file + " is " + raw.length +
" bytes, manifest dims say " + expect);
}
return ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN);
}
}
/**
**
** TestDataWriter.java - write a standalone-GPU-test data directory:
** raw little-endian arrays + manifest.txt describing every buffer and
** scalar parameter. Contract documented in
** tile_processor_gpu/src/tests/testdata_format.md: the Java oracle exports
** ALL inputs a per-kernel CUDA test needs, plus its own expected outputs
** (name prefix "expected_"), so the C++ side can run and self-check with
** no dependency on old calibration files or a live Java process.
**
** Copyright (C) 2026 Elphel, Inc.
**
** -----------------------------------------------------------------------------**
**
** TestDataWriter.java is free software: you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation, either version 3 of the License, or
** (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program. If not, see <http://www.gnu.org/licenses/>.
** -----------------------------------------------------------------------------**
**
** By Claude on 07/09/2026
*/
package com.elphel.imagej.gpu.testdata;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public class TestDataWriter implements AutoCloseable {
private final File dir;
private final StringBuilder manifest = new StringBuilder();
private boolean closed = false;
/**
* @param dir_path test-case directory (created if missing), one per kernel
* test case, e.g. .../testdata/1773135476_186641/avg_td
* @param comment free-text provenance line for the manifest header
*/
public TestDataWriter(String dir_path, String comment) throws IOException {
dir = new File(dir_path);
if (!dir.isDirectory() && !dir.mkdirs()) {
throw new IOException("TestDataWriter: cannot create " + dir_path);
}
manifest.append("# ").append(comment).append("\n");
}
/** Add a scalar parameter ("prm" manifest record). */
public void prm(String name, double value) {
manifest.append("prm ").append(name).append(" ").append(value).append("\n");
}
/**
* Write a float32 buffer: raw little-endian <name>.f32 + "buf" manifest
* record. Dims are row-major, LAST dimension fastest (flat Java order).
*/
public void buf(String name, float [] data, int ... dims) throws IOException {
checkDims(name, data.length, dims);
ByteBuffer bb = ByteBuffer.allocate(data.length * 4).order(ByteOrder.LITTLE_ENDIAN);
bb.asFloatBuffer().put(data);
writeRaw(name + ".f32", bb);
manifestBuf(name, "f32", name + ".f32", dims);
}
/** Write an int32 buffer: raw little-endian <name>.i32 + manifest record. */
public void buf(String name, int [] data, int ... dims) throws IOException {
checkDims(name, data.length, dims);
ByteBuffer bb = ByteBuffer.allocate(data.length * 4).order(ByteOrder.LITTLE_ENDIAN);
bb.asIntBuffer().put(data);
writeRaw(name + ".i32", bb);
manifestBuf(name, "i32", name + ".i32", dims);
}
/** Finalize: write manifest.txt. Buffers already on disk. */
@Override
public void close() throws IOException {
if (closed) return;
closed = true;
Files.write(new File(dir, "manifest.txt").toPath(),
manifest.toString().getBytes(StandardCharsets.UTF_8));
}
public String getDir() {
return dir.getPath();
}
private static void checkDims(String name, int length, int [] dims) {
long product = 1;
for (int d : dims) product *= d;
if (product != length) {
throw new IllegalArgumentException("TestDataWriter: buffer '" + name +
"' length " + length + " does not match dims product " + product);
}
}
private void writeRaw(String fname, ByteBuffer bb) throws IOException {
bb.rewind();
try (FileOutputStream fos = new FileOutputStream(new File(dir, fname));
WritableByteChannel ch = fos.getChannel()) {
ch.write(bb);
}
}
private void manifestBuf(String name, String dtype, String fname, int [] dims) {
manifest.append("buf ").append(name).append(" ").append(dtype)
.append(" ").append(dims.length);
for (int d : dims) manifest.append(" ").append(d);
manifest.append(" ").append(fname).append("\n");
}
/** Debug print of the manifest built so far. */
public void print(PrintWriter pw) {
pw.print(manifest);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment