/**
 **
 ** PoseCorrExport.java - export a complete standalone test case for the lean
 ** pose-chain CHECKPOINT 1 (roadmap 3-C rung 1): the raw transform-domain
 ** correlation after correlate2D_inter, for ONE scene of the CUAS RT pose
 ** sequence. The case replays the per-LMA-iteration GPU chain
 **   calculate_tiles_offsets -> convert_direct (MB pair: SET + SUBTRACT) ->
 **   consolidation (index_consolidate/consolidate_oob/clt_average_sensors_list)
 **   -> correlate2D_inter vs the resident virtual-center TD (gpu_clt_ref cam 0)
 ** with everything data-driven: the 16 conditioned images exactly as uploaded,
 ** geometry-correction/correction-vector float arrays, aberration kernels, LPF
 ** constants, the per-iteration task streams exactly as handed to the GPU
 ** (pre-offsets) and as received by the consolidation (post-offsets), and the
 ** expected corr indices + raw TD correlation tiles (the oracle outputs).
 ** The C++ side is tile_processor_gpu/src/tests/test_pose_corr.cu (tol 0,
 ** NSight/cuda-gdb debuggable; newly exercises calculate_tiles_offsets and
 ** correlate2D_inter - convert_direct and the consolidation chain are already
 ** covered by test_convert_direct / test_avg_td_oob).
 **
 ** CHECKPOINT 2 (rung 2, added 07/13/2026): the same case also carries the
 ** FZ-normalized pixel-domain correlation (expected_corr_pd_it<k> - the tol-0
 ** target for the IN-PLACE corr2D_normalize replay, GPU-produced floats) and
 ** the frozen 15-arg getMaxXYCmEig peak-contract oracle (expected_peaks_it<k>,
 ** {dx,dy,strength,e0x,e0y,l0,l1} per PD tile, NaN row = weak-peak reject;
 ** double-precision Java = the epsilon tier for the float GPU kernel). Contract
 ** params (eig_*, pose_recenter, pose_debias + the optional debias array) are
 ** in the manifest - Andrey's ruling 07/13/2026: closed-form 2x2 eigen,
 ** recentering and envelope de-bias as runtime parameters.
 **
 ** CAPTURE MODEL: static armed-capture hooks called from the PRODUCTION lean
 ** chain (CuasPoseRT.leanMeasure / leanFitScene, CuasConditioning ingest).
 ** All hooks are no-ops unless arm() was called (the curt.kernel_test
 ** "pose_corr" dropdown entry, one Java run = one case). The capture binds to
 ** the first scene leanMeasure processes, records every LMA iteration of that
 ** scene, writes the case when the scene's fit completes, and the pose loop
 ** exits early (kernel tests are exclusive diagnostics - no products needed).
 **
 ** Copyright (C) 2026 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  PoseCorrExport.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/13/2026
 */
package com.elphel.imagej.gpu.testdata;

import java.io.IOException;
import java.util.ArrayList;

import com.elphel.imagej.gpu.GPUTileProcessor;
import com.elphel.imagej.gpu.GpuQuad;
import com.elphel.imagej.gpu.TpTask;
import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.tileprocessor.GeometryCorrection;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.QuadCLT;

public class PoseCorrExport {
	// ---- armed-capture state (single-threaded RT diagnostic path; all static) ----
	private static boolean armed = false;
	private static boolean done  = false;      // case written - the pose loop may stop
	private static String  dir_path = null;
	private static int     debug = 0;
	private static String  bound_scene = null; // image name of the scene being captured

	// conditioned images: the LAST upload offered while armed (overwritten per scene
	// until the capture binds; conditionSceneToGpu runs BEFORE leanMeasure)
	private static float [][] images = null;   // [num_sens][img_h*img_w], exactly as H2D'd
	private static String     images_scene = null;

	// once-per-case statics
	private static float []   fgc, fcv;                       // geometry corr + correction vector floats
	private static float []   lpf_data, lpf_corr, lpf_rb_corr, log_corr; // __constant__ payloads
	private static float []   clt_ref0 = null;                // resident center TD, cam-0 slice of gpu_clt_ref
	private static float [][] fkernels, foffsets;             // per-cam aberration kernels (GPU flatten)
	private static float []   scales3;                        // correlate2D_inter color scales triplet
	private static int        sel_sensors, task_size, kernels_hor, kernels_vert;
	private static double     soft_margin, hard_margin;
	private static int        num_sens, colors, tilesx, tilesy, img_w, img_h;
	private static boolean    has_mb;
	private static boolean    statics_ok = false;

	// per-LMA-iteration streams
	private static final ArrayList<float []> tasks0_pre  = new ArrayList<float []>(); // as handed to the GPU (pre-offsets)
	private static final ArrayList<float []> tasks1_pre  = new ArrayList<float []>();
	private static final ArrayList<float []> tasks0_post = new ArrayList<float []>(); // as the consolidation received (post-offsets)
	private static final ArrayList<float []> tasks1_post = new ArrayList<float []>();
	private static final ArrayList<int []>   corr_indices = new ArrayList<int []>();  // packed (tile<<8)|sensor, 0xff=sum
	private static final ArrayList<float []> corr_td      = new ArrayList<float []>(); // raw TD, [n_corr*256]

	// checkpoint 2 (rung 2): FZ-normalized pixel-domain correlation + peak oracle // By Claude on 07/13/2026
	private static final ArrayList<int []>   pd_indices = new ArrayList<int []>();   // dense tile index per PD tile
	private static final ArrayList<float []> pd_data    = new ArrayList<float []>(); // normalized PD, [n_pd*225]
	private static final ArrayList<float []> peaks      = new ArrayList<float []>(); // oracle {dx,dy,str,e0x,e0y,l0,l1}, NaN row = rejected
	private static final ArrayList<float []> gpu_peaks  = new ArrayList<float []>(); // PRODUCTION float-kernel rows [n_corr*8], corr_indices order - the tol-0 peak tier // By Claude on 07/14/2026
	private static double  fz_inter, corr_weight;                                    // execCorr2D_normalize inputs
	private static double  eig_min_abs, eig_min_rel, min_str_sum, eig_sub_frac;      // frozen peak-contract params
	private static int     pose_recenter;                                            // recentering passes (runtime contract param)
	private static double  eig_sub_frac1, eig_scale_axes, eig_inc_axes, pose_debias;
	private static float []  debias_arr = null;                                      // envelope de-bias (only when pose_debias > 0)
	private static double [] debias_oracle = null;                                   // same, double - the oracle multiplies this
	private static boolean pd_statics_ok = false;

	/** Arm the capture (CuasRT.kernelTest "pose_corr"). One case per arm. */
	public static synchronized void arm(String dir, int debugLevel) {
		disarm();
		armed = true;
		dir_path = dir;
		debug = debugLevel;
	}
	/** Disarm + drop all captured state (safety net after the export run). */
	public static synchronized void disarm() {
		armed = false;
		done = false;
		dir_path = null;
		bound_scene = null;
		images = null;
		images_scene = null;
		clt_ref0 = null;
		statics_ok = false;
		resetIterations();
	}
	public static boolean isArmed()     { return armed; }
	/** True once the case has been written - the pose scene loop may break early. */
	public static boolean captureDone() { return done; }

	private static void resetIterations() {
		tasks0_pre.clear();  tasks1_pre.clear();
		tasks0_post.clear(); tasks1_post.clear();
		corr_indices.clear(); corr_td.clear();
		pd_indices.clear(); pd_data.clear(); peaks.clear(); // By Claude on 07/13/2026
		gpu_peaks.clear(); // By Claude on 07/14/2026
		debias_arr = null;
		debias_oracle = null;
		pd_statics_ok = false;
	}

	/**
	 * Hook (CuasConditioning.conditionSceneToGpuInner): the conditioned image data
	 * is uploaded straight to the GPU and never stored in the scene's QuadCLT, so
	 * the exporter takes its copy here - same channel combine + double->float cast
	 * as GpuQuad/GpuQuadJna.setBayerImages (bit-exact vs the upload).
	 */
	public static synchronized void offerImages(QuadCLT scene, double [][][] data) {
		if (!armed || done) return;
		if ((bound_scene != null) && !bound_scene.equals(scene.getImageName())) return; // capture already bound elsewhere
		final float [][] f = new float [data.length][];
		for (int ncam = 0; ncam < data.length; ncam++) {
			final double [][] chans = data[ncam];
			if ((chans == null) || (chans[0] == null)) return; // incomplete - cannot capture
			f[ncam] = new float [chans[0].length];
			for (int i = 0; i < f[ncam].length; i++) {
				double s = chans[0][i];
				for (int j = 1; j < chans.length; j++) s += chans[j][i];
				f[ncam][i] = (float) s;
			}
		}
		images = f;
		images_scene = scene.getImageName();
	}

	/**
	 * Hook (CuasPoseRT.leanMeasure, per LMA iteration, BEFORE interCorrTD*): the
	 * task streams exactly as the GPU receives them - setTasks ORs 511 into the
	 * task word before flattening, replicated here so the capture precedes the
	 * in-place mutation. tp_tasks[1] == null = no-MB single-set mode.
	 */
	public static synchronized void iterTasksPre(
			CLTParameters clt_parameters,
			ImageDtt      image_dtt,
			GpuQuad       gpuQuad,
			QuadCLT       scene,
			TpTask [][]   tp_tasks) {
		if (!armed || done) return;
		if (bound_scene == null) { // bind to this scene
			if ((images_scene == null) || !images_scene.equals(scene.getImageName())) {
				System.out.println("PoseCorrExport: no conditioned-image capture for scene "+
						scene.getImageName()+" (curt.pose_raw ingest required) - export DISARMED");
				disarm();
				return;
			}
			bound_scene = scene.getImageName();
			if (!captureStatics(clt_parameters, image_dtt, gpuQuad, scene, tp_tasks)) {
				disarm();
				return;
			}
		} else if (!bound_scene.equals(scene.getImageName())) {
			return; // another scene (bound scene failed mid-fit and was reported) - keep what we have
		}
		tasks0_pre.add(flattenTasks(tp_tasks[0], true));
		tasks1_pre.add((tp_tasks.length > 1) && (tp_tasks[1] != null) ? flattenTasks(tp_tasks[1], true) : null);
	}

	/**
	 * Hook (CuasPoseRT.leanMeasure, after interCorrTD* / before consolidateToSlot0):
	 * the SAME task arrays after updateTasks() replaced them with the GPU-computed
	 * offsets - the exact stream execConsolidateSensorsTD flattens (plain
	 * asFloatArray: disp_dist tail stays zero, matching production).
	 */
	public static synchronized void iterTasksPost(QuadCLT scene, TpTask [][] tp_tasks) {
		if (!armed || done || (bound_scene == null) || !bound_scene.equals(scene.getImageName())) return;
		tasks0_post.add(flattenTasks(tp_tasks[0], false));
		tasks1_post.add((tp_tasks.length > 1) && (tp_tasks[1] != null) ? flattenTasks(tp_tasks[1], false) : null);
	}

	/**
	 * Hook (CuasPoseRT.leanMeasure, right after execCorr2D_inter_TD): the expected
	 * output of checkpoint 1 - packed corr indices + raw TD correlation tiles
	 * (BEFORE fat-zero normalization). First call also snapshots the resident
	 * center reference TD (gpu_clt_ref cam 0 - the only slice sensor mask 1 reads)
	 * and the correlate scales triplet.
	 */
	public static synchronized void iterCorr(GpuQuad gpuQuad, QuadCLT scene, double [] col_weights) {
		if (!armed || done || (bound_scene == null) || !bound_scene.equals(scene.getImageName())) return;
		if (clt_ref0 == null) {
			clt_ref0 = gpuQuad.getCltData(true)[0];
			sel_sensors = gpuQuad.getSensorMaskInter();
			// same triplet reduction as GpuQuadJna.scaleTriplet (monochrome -> {1,0,0})
			final int ncol = Math.min(colors, 3);
			scales3 = new float [] {
					(ncol > 1) ? (float) col_weights[0] : 1f,
					(ncol > 1) ? (float) col_weights[1] : 0f,
					(ncol > 2) ? (float) col_weights[2] : 0f};
		}
		corr_indices.add(gpuQuad.getCorrIndices());
		corr_td.add(gpuQuad.getCorrTdData());
	}

	/**
	 * Hook (CuasPoseRT.leanMeasure, right after TDCorrTile.convertTDtoPD): CHECKPOINT 2
	 * (rung 2) expected data. Two comparison tiers in one capture:
	 *  - expected_corr_pd: the FZ-normalized pixel-domain correlation exactly as the GPU
	 *    produced it (corr_pd doubles are exact copies of the getCorr2D floats, so the
	 *    cast back is bit-exact) - the tol-0 target for the IN-PLACE normalize replay
	 *    (corr2D_normalize math is per-tile, position-independent);
	 *  - expected_peaks: the frozen 15-arg getMaxXYCmEig contract (Andrey's ruling
	 *    07/13/2026: closed-form 2x2 eigen, recentering + envelope de-bias as runtime
	 *    params) run HERE on every non-null PD tile - the double-precision oracle tier
	 *    for the new argmax/centroid/eigen kernel (float kernel compares at an epsilon;
	 *    the tol-0 verdict for it comes later from NVRTC production capture, rung-1 rule).
	 * Runs the oracle on ALL PD tiles (not only pXpYD-valid ones) so the kernel's
	 * reject path (weak peak -> NaN row) is exercised. By Claude on 07/13/2026.
	 */
	public static synchronized void iterPd(
			CLTParameters clt_parameters,
			GpuQuad       gpuQuad,
			QuadCLT       scene,
			double [][]   corr_pd) {
		if (!armed || done || (bound_scene == null) || !bound_scene.equals(scene.getImageName())) return;
		final int corr_width = 2 * GPUTileProcessor.DTT_SIZE - 1;
		if (!pd_statics_ok) {
			fz_inter      = clt_parameters.curt.fz_inter;
			corr_weight   = gpuQuad.getNumCamsInter(); // TDCorrTile.getFromGpu weight = index_combo = num pairs - 1
			eig_min_abs   = clt_parameters.imp.eig_min_abs;
			eig_min_rel   = clt_parameters.imp.eig_min_rel;
			min_str_sum   = clt_parameters.imp.min_str_sum;
			eig_sub_frac  = clt_parameters.imp.eig_sub_frac;
			pose_recenter = clt_parameters.curt.pose_recenter;
			eig_sub_frac1 = clt_parameters.imp.eig_sub_frac1;
			eig_scale_axes= clt_parameters.imp.eig_scale_axes;
			eig_inc_axes  = clt_parameters.imp.eig_inc_axes;
			pose_debias   = clt_parameters.curt.pose_debias;
			if (pose_debias > 0) {
				debias_oracle = com.elphel.imagej.cuas.rt.CuasPoseRT.windowAutocorrDebias(
						GPUTileProcessor.DTT_SIZE, pose_debias);
				debias_arr = new float [debias_oracle.length];
				for (int i = 0; i < debias_oracle.length; i++) debias_arr[i] = (float) debias_oracle[i];
			}
			pd_statics_ok = true;
		}
		int n_pd = 0;
		for (int i = 0; i < corr_pd.length; i++) if (corr_pd[i] != null) n_pd++;
		final int []   indices = new int [n_pd];
		final float [] pd      = new float [n_pd * corr_width * corr_width];
		final float [] pk      = new float [n_pd * 7];
		int np = 0;
		for (int i = 0; i < corr_pd.length; i++) if (corr_pd[i] != null) {
			indices[np] = i;
			for (int k = 0; k < corr_pd[i].length; k++) {
				pd[np * corr_width * corr_width + k] = (float) corr_pd[i][k];
			}
			double [] corr_tile = corr_pd[i];
			if (debias_oracle != null) {
				corr_tile = new double [corr_pd[i].length];
				for (int k = 0; k < corr_tile.length; k++) corr_tile[k] = corr_pd[i][k] * debias_oracle[k];
			}
			final double [] r = com.elphel.imagej.tileprocessor.Correlation2d.getMaxXYCmEig(
					corr_tile, corr_width,
					eig_min_abs, eig_min_rel, min_str_sum, eig_sub_frac,
					pose_recenter, eig_sub_frac1, eig_scale_axes, eig_inc_axes,
					null, false, null,
					true,   // eig_fast2x2 FROZEN true - the contract's closed-form eigen
					false);
			for (int k = 0; k < 7; k++) {
				pk[np * 7 + k] = (r == null) ? Float.NaN : (float) r[k];
			}
			np++;
		}
		pd_indices.add(indices);
		pd_data.add(pd);
		peaks.add(pk);
	}

	/**
	 * Hook (CuasPoseRT.leanMeasure, after execCorr2DPeaks): the PRODUCTION peak rows
	 * exactly as the float GPU kernel produced them ([n_corr][8], aligned with the
	 * resident corr_indices order = expected_corr_indices_it<k>). This is the TOL-0
	 * tier for the peak kernel: the NVRTC replay (test_pose_corr_jna, use_float=1)
	 * must reproduce these bit-exactly. null (JCuda fallback ran) = not captured.
	 * By Claude on 07/14/2026.
	 */
	public static synchronized void iterGpuPeaks(QuadCLT scene, float [] peaks_rows) {
		if (!armed || done || (bound_scene == null) || !bound_scene.equals(scene.getImageName())) return;
		gpu_peaks.add((peaks_rows == null) ? null : peaks_rows.clone());
	}

	/**
	 * Hook (CuasPoseRT.leanFitScene, after the measure<->solve cycles succeeded):
	 * all iterations of the bound scene are captured - write the case.
	 */
	public static synchronized void sceneDone(QuadCLT scene) {
		if (!armed || done || (bound_scene == null) || !bound_scene.equals(scene.getImageName())) return;
		try {
			writeCase();
			done = true;
		} catch (IOException e) {
			System.out.println("PoseCorrExport: FAILED to write "+dir_path+": "+e.getMessage());
			e.printStackTrace();
			disarm();
		}
	}

	/**
	 * Hook (CuasPoseRT.leanFitScene, fit failed / measurement null): drop the
	 * partial capture and let the next scene bind instead.
	 */
	public static synchronized void sceneFailed(QuadCLT scene) {
		if (!armed || done || (bound_scene == null) || !bound_scene.equals(scene.getImageName())) return;
		System.out.println("PoseCorrExport: scene "+bound_scene+" fit failed mid-capture - rebinding to the next scene");
		bound_scene = null;
		clt_ref0 = null;
		statics_ok = false;
		resetIterations();
	}

	// flatten a task stream in the exact GPU struct layout; or511 replicates the
	// task-word mutation setTasks applies before its own flatten (pre-offsets
	// stream); post-offsets streams are flattened plain, as execConsolidateSensorsTD
	private static float [] flattenTasks(TpTask [] tasks, boolean or511) {
		final int ts = tasks[0].getSize();
		final float [] ftasks = new float [tasks.length * ts];
		for (int i = 0; i < tasks.length; i++) {
			tasks[i].asFloatArray(ftasks, i, false);
			if (or511) {
				ftasks[i * ts] = Float.intBitsToFloat(Float.floatToIntBits(ftasks[i * ts]) | 511);
			}
		}
		return ftasks;
	}

	// once-per-case inputs that do not change across iterations
	private static boolean captureStatics(
			CLTParameters clt_parameters,
			ImageDtt      image_dtt,
			GpuQuad       gpuQuad,
			QuadCLT       scene,
			TpTask [][]   tp_tasks) {
		final QuadCLT bound = gpuQuad.getQuadCLT(); // the scene the GPU geometry/kernels come from
		if ((bound == null) || !bound.getImageName().equals(scene.getImageName())) {
			System.out.println("PoseCorrExport: GPU-bound scene "+
					((bound == null) ? "null" : bound.getImageName())+" != measured scene "+
					scene.getImageName()+" - cannot capture");
			return false;
		}
		num_sens = gpuQuad.num_cams;
		colors   = gpuQuad.num_colors;
		img_w    = gpuQuad.img_width;
		img_h    = gpuQuad.img_height;
		tilesx   = img_w / GPUTileProcessor.DTT_SIZE;
		tilesy   = img_h / GPUTileProcessor.DTT_SIZE;
		kernels_hor  = gpuQuad.kernels_hor;
		kernels_vert = gpuQuad.kernels_vert;
		task_size = tp_tasks[0][0].getSize();
		has_mb    = (tp_tasks.length > 1) && (tp_tasks[1] != null);
		soft_margin = clt_parameters.curt.oob_soft;
		hard_margin = clt_parameters.curt.oob_hard;
		// geometry: same float arrays GpuQuad/GpuQuadJna upload (execSetTilesOffsets)
		final GeometryCorrection gc = bound.getGeometryCorrection().expandSensors(GPUTileProcessor.MAX_NUM_CAMS);
		fgc = gc.toFloatArray();
		final double [] dcv = gc.getCorrVector().toFullRollArray();
		fcv = new float [dcv.length];
		for (int i = 0; i < dcv.length; i++) fcv[i] = (float) dcv[i];
		// LPF/HPF __constant__ payloads: the exact arrays interCorrTD*/MotionBlur uploads
		final boolean mono = scene.isMonochrome();
		final float [][] lpf_rgb = new float [][] {
			image_dtt.floatGetCltLpfFd(clt_parameters.gpu_sigma_r),
			image_dtt.floatGetCltLpfFd(clt_parameters.gpu_sigma_b),
			image_dtt.floatGetCltLpfFd(clt_parameters.gpu_sigma_g),
			image_dtt.floatGetCltLpfFd(clt_parameters.gpu_sigma_m)};
		lpf_data = new float [lpf_rgb.length * lpf_rgb[0].length]; // same flatten as setLpfRbg
		for (int i = 0; i < lpf_rgb.length; i++) {
			System.arraycopy(lpf_rgb[i], 0, lpf_data, i * lpf_rgb[0].length, lpf_rgb[i].length);
		}
		lpf_corr    = image_dtt.floatGetCltLpfFd(clt_parameters.getGpuCorrSigma(mono));
		lpf_rb_corr = image_dtt.floatGetCltLpfFd(mono ? 1.0 : clt_parameters.gpu_sigma_rb_corr);
		log_corr    = image_dtt.floatGetCltHpfFd(clt_parameters.getGpuCorrLoGSigma(mono));
		// aberration kernels: the exact per-cam flatten of GpuQuad/GpuQuadJna.setConvolutionKernels
		final double [][][][][][] clt_kernels = bound.getCLTKernels();
		if (clt_kernels == null) {
			System.out.println("PoseCorrExport: scene "+scene.getImageName()+" has no CLT kernels - cannot capture");
			return false;
		}
		final int num_kernels = clt_kernels[0][0].length * clt_kernels[0][0][0].length * clt_kernels[0].length;
		final int kernel_length = num_kernels * 4 * GPUTileProcessor.DTT_SIZE * GPUTileProcessor.DTT_SIZE;
		fkernels = new float [num_sens][];
		foffsets = new float [num_sens][];
		for (int ncam = 0; ncam < num_sens; ncam++) {
			final float [] fkernel  = new float [kernel_length];
			final float [] foffset  = new float [num_kernels * 8];
			int indx = 0;
			for (int ty = 0; ty < clt_kernels[ncam][0].length; ty++)
				for (int tx = 0; tx < clt_kernels[ncam][0][ty].length; tx++)
					for (int col = 0; col < clt_kernels[ncam].length; col++)
						for (int p = 0; p < 4; p++) {
							final double [] pa = clt_kernels[ncam][col][ty][tx][p];
							for (int i0 = 0; i0 < 64; i0++) {
								final int i = ((i0 & 7) << 3) + ((i0 >> 3) & 7); // transpose, as production
								fkernel[indx++] = (float) pa[i];
							}
						}
			indx = 0;
			for (int ty = 0; ty < clt_kernels[ncam][0].length; ty++)
				for (int tx = 0; tx < clt_kernels[ncam][0][ty].length; tx++)
					for (int col = 0; col < clt_kernels[ncam].length; col++) {
						final double [] pa = clt_kernels[ncam][col][ty][tx][4];
						for (int i = 0; i < pa.length; i++) foffset[indx++] = (float) pa[i];
					}
			fkernels[ncam] = fkernel;
			foffsets[ncam] = foffset;
		}
		statics_ok = true;
		return true;
	}

	private static void writeCase() throws IOException {
		if (!statics_ok || (clt_ref0 == null) || tasks0_pre.isEmpty() ||
				(tasks0_pre.size() != tasks0_post.size()) || (tasks0_pre.size() != corr_td.size())) {
			throw new IOException("incomplete capture: iters pre/post/corr = "+
					tasks0_pre.size()+"/"+tasks0_post.size()+"/"+corr_td.size());
		}
		if (!pd_statics_ok || (pd_indices.size() != tasks0_pre.size())) { // By Claude on 07/13/2026
			throw new IOException("incomplete checkpoint-2 capture: pd iters = "+pd_indices.size()+
					" vs "+tasks0_pre.size()+" (iterPd hook missing?)");
		}
		final int num_iters = tasks0_pre.size();
		final int chunk = 4 * GPUTileProcessor.DTT_SIZE * GPUTileProcessor.DTT_SIZE; // 256
		final int pd_size = (2 * GPUTileProcessor.DTT_SIZE - 1) * (2 * GPUTileProcessor.DTT_SIZE - 1); // 225
		try (TestDataWriter tdw = new TestDataWriter(dir_path, "pose_corr test case: scene "+bound_scene+
				" ("+num_iters+" LMA iterations, checkpoint 1 = raw TD correlation after correlate2D_inter,"+
				" checkpoint 2 = FZ-normalized PD + getMaxXYCmEig peak oracle)")) {
			tdw.prm("num_sensors",  num_sens);
			tdw.prm("colors",       colors);
			tdw.prm("tilesx",       tilesx);
			tdw.prm("tilesy",       tilesy);
			tdw.prm("img_width",    img_w);
			tdw.prm("img_height",   img_h);
			tdw.prm("kernels_hor",  kernels_hor);
			tdw.prm("kernels_vert", kernels_vert);
			tdw.prm("task_size",    task_size);
			tdw.prm("num_iters",    num_iters);
			tdw.prm("has_mb",       has_mb ? 1 : 0);
			tdw.prm("sel_sensors",  sel_sensors);
			tdw.prm("soft_margin",  soft_margin);
			tdw.prm("hard_margin",  hard_margin);
			tdw.prm("scale0",       scales3[0]);
			tdw.prm("scale1",       scales3[1]);
			tdw.prm("scale2",       scales3[2]);
			// checkpoint 2: FZ-normalize + frozen peak contract params // By Claude on 07/13/2026
			tdw.prm("fz_inter",      fz_inter);
			tdw.prm("corr_weight",   corr_weight);
			tdw.prm("eig_min_abs",   eig_min_abs);
			tdw.prm("eig_min_rel",   eig_min_rel);
			tdw.prm("min_str_sum",   min_str_sum);
			tdw.prm("eig_sub_frac",  eig_sub_frac);
			tdw.prm("pose_recenter", pose_recenter);
			tdw.prm("eig_sub_frac1", eig_sub_frac1);
			tdw.prm("eig_scale_axes",eig_scale_axes);
			tdw.prm("eig_inc_axes",  eig_inc_axes);
			tdw.prm("pose_debias",   pose_debias);
			final float [] images_flat = new float [num_sens * img_h * img_w];
			for (int ncam = 0; ncam < num_sens; ncam++) {
				System.arraycopy(images[ncam], 0, images_flat, ncam * img_h * img_w, img_h * img_w);
			}
			tdw.buf("images", images_flat, num_sens, img_h, img_w);
			final float [] kern_flat = new float [num_sens * fkernels[0].length];
			final float [] offs_flat = new float [num_sens * foffsets[0].length];
			for (int ncam = 0; ncam < num_sens; ncam++) {
				System.arraycopy(fkernels[ncam], 0, kern_flat, ncam * fkernels[0].length, fkernels[0].length);
				System.arraycopy(foffsets[ncam], 0, offs_flat, ncam * foffsets[0].length, foffsets[0].length);
			}
			tdw.buf("kernels",        kern_flat, num_sens, fkernels[0].length);
			tdw.buf("kernel_offsets", offs_flat, num_sens, foffsets[0].length);
			tdw.buf("geometry_correction", fgc, fgc.length);
			tdw.buf("correction_vector",   fcv, fcv.length);
			tdw.buf("lpf_data",    lpf_data,    4, lpf_data.length / 4);
			tdw.buf("lpf_corr",    lpf_corr,    lpf_corr.length);
			tdw.buf("lpf_rb_corr", lpf_rb_corr, lpf_rb_corr.length);
			tdw.buf("log_corr",    log_corr,    log_corr.length);
			tdw.buf("clt_ref_cam0", clt_ref0, tilesy, tilesx, colors, chunk);
			if (debias_arr != null) { // envelope de-bias array (the kernel's optional pointer) // By Claude on 07/13/2026
				tdw.buf("debias", debias_arr, debias_arr.length);
			}
			for (int k = 0; k < num_iters; k++) {
				tdw.buf("ftasks0_it"+k,         tasks0_pre.get(k),  tasks0_pre.get(k).length / task_size,  task_size);
				tdw.buf("expected_tasks0_it"+k, tasks0_post.get(k), tasks0_post.get(k).length / task_size, task_size);
				if (has_mb) {
					tdw.buf("ftasks1_it"+k,         tasks1_pre.get(k),  tasks1_pre.get(k).length / task_size,  task_size);
					tdw.buf("expected_tasks1_it"+k, tasks1_post.get(k), tasks1_post.get(k).length / task_size, task_size);
				}
				tdw.buf("expected_corr_indices_it"+k, corr_indices.get(k), corr_indices.get(k).length);
				tdw.buf("expected_corr_td_it"+k,      corr_td.get(k),      corr_td.get(k).length / chunk, chunk);
				// checkpoint 2: normalized PD (tol-0 target for the in-place FZ-norm replay) +
				// the double-precision peak oracle (epsilon tier for the float kernel) // By Claude on 07/13/2026
				tdw.buf("expected_pd_indices_it"+k, pd_indices.get(k), pd_indices.get(k).length);
				tdw.buf("expected_corr_pd_it"+k,    pd_data.get(k),    pd_data.get(k).length / pd_size, pd_size);
				tdw.buf("expected_peaks_it"+k,      peaks.get(k),      peaks.get(k).length / 7, 7);
				// production float-kernel rows, corr_indices order (the tol-0 peak tier;
				// absent when the JCuda fallback measured the scene). By Claude on 07/14/2026.
				if ((k < gpu_peaks.size()) && (gpu_peaks.get(k) != null)) {
					tdw.buf("expected_gpu_peaks_it"+k, gpu_peaks.get(k), gpu_peaks.get(k).length / 8, 8);
				}
			}
			if (debug > -3) {
				System.out.println("PoseCorrExport: wrote "+tdw.getDir()+" (scene "+bound_scene+", "+
						num_iters+" iterations, "+(tasks0_pre.get(0).length / task_size)+" tiles/set, "+
						(corr_td.get(0).length / chunk)+" corr tiles/iteration, "+
						pd_indices.get(0).length+" PD/peak tiles/iteration, sel_sensors="+sel_sensors+
						", recenter="+pose_recenter+", debias="+pose_debias+")"); // ckpt-2 counts by Claude on 07/13/2026
			}
		}
	}

	/**
	 * Import the C++ test results (results/corr_td_it<k>, already reordered by the
	 * test into the expected index order) and compare against the oracle reference
	 * (Java-side round trip; the C++ test already self-checks at tol 0).
	 * @return max abs difference over finite elements (NaN mismatch = +inf)
	 */
	public static double compareResults(String dir, int debugLevel) throws IOException {
		final TestDataReader expected = new TestDataReader(dir);
		final TestDataReader results = TestDataReader.results(dir);
		double max_diff = 0.0;
		// results the C++ test may write, vs their expected_ counterparts. corr_pd/peaks
		// (checkpoint 2) are optional until the rung-2 test lands. By Claude on 07/13/2026.
		final String [] pairs = {"corr_td", "corr_pd", "peaks"};
		for (String base : pairs) {
			if (!expected.has("expected_"+base+"_it0")) continue; // not in this case vintage
			for (int k = 0; expected.has("expected_"+base+"_it"+k); k++) {
				if (!results.has(base+"_it"+k)) {
					if (base.equals("corr_td") || (debugLevel > -3)) {
						System.out.println("PoseCorrExport.compareResults: results/"+base+"_it"+k+" missing"+
								(base.equals("corr_td") ? "" : " (rung-2 outputs optional)"));
					}
					continue;
				}
				final float [] r = results.getFloats(base+"_it"+k);
				final float [] e = expected.getFloats("expected_"+base+"_it"+k);
				double md = 0.0;
				int nan_mismatch = 0;
				final int n = Math.min(r.length, e.length);
				for (int i = 0; i < n; i++) {
					final 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 ((r.length != e.length) || (nan_mismatch > 0)) md = Double.POSITIVE_INFINITY;
				if (debugLevel > -3) {
					System.out.println("PoseCorrExport.compareResults: "+base+"_it"+k+" max|diff|="+md+
							", NaN-mismatch="+nan_mismatch);
				}
				max_diff = Math.max(max_diff, md);
			}
		}
		return max_diff;
	}
}
