package com.elphel.imagej.cuas.rt;

import com.elphel.imagej.tileprocessor.DttRad2;
import com.elphel.imagej.tileprocessor.ImageDttCPU;
import com.elphel.imagej.tileprocessor.ImageDttParameters;

/**
 * Fold the CUAS LoG pre-filter into the aberration-correction CLT kernels
 * ("LoG for free" in convert_direct) + a headless synthetic self-test.
 *
 * Math reference: attic/imagej-elphel-internal/handoffs/
 * 2026-07-05_mclt_dtt_convolution_reference.md (esp. section 4). The LoG is
 * even in both axes, so of its 4 DTT-III symmetry classes only the SS quadrant
 * survives; folding is a per-bin REAL scalar multiply of all 4 quadrants of
 * every stored kernel (rq = cq * L0), which commutes with offsetKernelSensor
 * and the convert-time fractional shifts. CltExtra offsets are not touched,
 * and folded kernels are NEVER re-normalized (LoG sum ~ 0).
 *
 * Self-test (synthetic impulse/grating validation, roadmap "RT seed" step 1,
 * run BEFORE any full-system A/B):
 *   java -cp target/classes:&lt;deps&gt; com.elphel.imagej.cuas.rt.CuasLogFold
 * Uses only public CPU-oracle methods: ImageDttCPU.clt_2d (MCLT fold+DTT-IV),
 * convolve_tile (Java twin of the GPU convolveTiles), iclt_2d (inverse +
 * unfold + overlap-add). Exit code 0 = all tests passed.
 *
 * By Claude on 07/06/2026.
 */
public class CuasLogFold {
	public static final int    TRANSFORM_SIZE = 8;
	public static final int    KERNEL_WIDTH   = 2 * TRANSFORM_SIZE - 1;              // 15
	public static final double TD_SCALE       = 2 * TRANSFORM_SIZE;                  // 16, see floatGetCltLoGFd / clt_normalize_kernel
	public static final double LOG_Q_LEAK_MAX = 1e-12; // max allowed |quadrant 1..3| of the (even-even) LoG spectrum

	/**
	 * Build the per-bin transform-domain response L0[64] of the CUAS LoG
	 * (CuasRTUtils.getLoGKenel, sigma = psf_radius/sqrt(2), flux-normalized),
	 * scaled so an identity (delta) filter maps to all-ones (verified by the
	 * self-test). This is the multiplier for foldKernels().
	 * @param psf_radius curt.psf_radius (optical PSF radius, pix)
	 * @param n_sigma    curt.n_sigma (LoG support cutoff, sigmas)
	 * @return L0[TRANSFORM_SIZE*TRANSFORM_SIZE] per-bin real response
	 */
	public static double [] getLogTd(
			double psf_radius,
			double n_sigma) {
		int radius = CuasRTUtils.getRadiusLoGKernel(psf_radius, n_sigma);
		if (radius > TRANSFORM_SIZE - 1) {
			throw new IllegalArgumentException("CuasLogFold.getLogTd(): LoG radius "+radius+
					" exceeds kernel support "+(TRANSFORM_SIZE-1)+" (psf_radius="+psf_radius+", n_sigma="+n_sigma+")");
		}
		double [] log_pix = CuasRTUtils.getLoGKenel(psf_radius, radius);
		// Pre-compensate taps by 1/(cos(pi*dx/16)*cos(pi*dy/16)): a per-bin TD
		// quad-multiply is equivalent to pixel convolution with cos-ATTENUATED
		// taps (verified to machine precision by the self-test; same weights as
		// norm_sym_weights in QuadCLTCPU.calculateCLTKernel:8339). Dividing here
		// makes the EFFECTIVE applied filter equal the true pixel LoG.
		int lw = 2 * radius + 1;
		double [] log_comp = new double [log_pix.length];
		for (int dy = -radius; dy <= radius; dy++) {
			for (int dx = -radius; dx <= radius; dx++) {
				log_comp[(dy + radius) * lw + (dx + radius)] =
						log_pix[(dy + radius) * lw + (dx + radius)] / cosAtten(dx, dy);
			}
		}
		double [] k15 = embedCentered(log_comp, lw);
		double [][] quad = symmetrizeDtt3(k15, true);
		// even-even filter: quadrants 1..3 must vanish (guards the embedding/centering)
		for (int q = 1; q < 4; q++) {
			for (int i = 0; i < quad[q].length; i++) {
				if (Math.abs(quad[q][i]) > LOG_Q_LEAK_MAX) {
					throw new IllegalStateException("CuasLogFold.getLogTd(): LoG quadrant "+q+
							" leak "+quad[q][i]+" at bin "+i+" - kernel not centered/even?");
				}
			}
		}
		return quad[0];
	}

	/**
	 * Fold a per-bin real response into CLT convolution kernels: multiply all 4
	 * quadrants of every tile/sensor kernel by log_td, copy everything else
	 * (CltExtra offsets/derivatives) verbatim. Input is NOT modified - the
	 * original kernels stay available for A/B re-upload mid-run.
	 * @param clt_kernels [ncam][color][tilesY][tilesX][4+extra][64 or extra len]
	 * @param log_td      per-bin response from getLogTd()
	 * @return new kernel array of the same shape
	 */
	public static double [][][][][][] foldKernels(
			double [][][][][][] clt_kernels,
			double []           log_td) {
		double [][][][][][] folded = new double [clt_kernels.length][][][][][];
		for (int ncam = 0; ncam < clt_kernels.length; ncam++) {
			if (clt_kernels[ncam] == null) continue;
			folded[ncam] = new double [clt_kernels[ncam].length][][][][];
			for (int col = 0; col < clt_kernels[ncam].length; col++) {
				if (clt_kernels[ncam][col] == null) continue;
				folded[ncam][col] = new double [clt_kernels[ncam][col].length][][][];
				for (int ty = 0; ty < clt_kernels[ncam][col].length; ty++) {
					if (clt_kernels[ncam][col][ty] == null) continue;
					folded[ncam][col][ty] = new double [clt_kernels[ncam][col][ty].length][][];
					for (int tx = 0; tx < clt_kernels[ncam][col][ty].length; tx++) {
						double [][] ktile = clt_kernels[ncam][col][ty][tx];
						if (ktile == null) continue;
						double [][] ftile = new double [ktile.length][];
						for (int p = 0; p < ktile.length; p++) {
							ftile[p] = ktile[p].clone();
							if (p < 4) {
								for (int i = 0; i < ftile[p].length; i++) {
									ftile[p][i] *= log_td[i];
								}
							}
						}
						folded[ncam][col][ty][tx] = ftile;
					}
				}
			}
		}
		return folded;
	}

	/**
	 * Build space-INVARIANT kernels in the shape of a template kernel array:
	 * quadrant 0 = the given per-bin response (null = all-ones = IDENTITY, i.e.
	 * aberration correction skipped), quadrants 1..3 = 0, CltExtra = all zeros
	 * (no fractional offsets). For the LoG-only / identity isolation test
	 * (Andrey 07/06/2026: separate the fold mechanism from the aberration
	 * kernel content). By Claude on 07/06/2026.
	 * @param template  existing kernels supplying the array shape
	 * @param response  per-bin quadrant-0 response [64], or null for identity
	 * @return new kernel array, same shape as template
	 */
	public static double [][][][][][] uniformKernels(
			double [][][][][][] template,
			double []           response) {
		double [][][][][][] uni = new double [template.length][][][][][];
		for (int ncam = 0; ncam < template.length; ncam++) {
			if (template[ncam] == null) continue;
			uni[ncam] = new double [template[ncam].length][][][][];
			for (int col = 0; col < template[ncam].length; col++) {
				if (template[ncam][col] == null) continue;
				uni[ncam][col] = new double [template[ncam][col].length][][][];
				for (int ty = 0; ty < template[ncam][col].length; ty++) {
					if (template[ncam][col][ty] == null) continue;
					uni[ncam][col][ty] = new double [template[ncam][col][ty].length][][];
					for (int tx = 0; tx < template[ncam][col][ty].length; tx++) {
						double [][] ktile = template[ncam][col][ty][tx];
						if (ktile == null) continue;
						double [][] utile = new double [ktile.length][];
						for (int p = 0; p < ktile.length; p++) {
							utile[p] = new double [ktile[p].length]; // zeros
							if (p == 0) {
								if (response != null) {
									System.arraycopy(response, 0, utile[0], 0, response.length);
								} else {
									java.util.Arrays.fill(utile[0], 1.0); // identity
								}
							}
						}
						uni[ncam][col][ty][tx] = utile;
					}
				}
			}
		}
		return uni;
	}

	/** Inherent per-tap attenuation of a TD per-bin multiply: pixel-domain
	 *  equivalent kernel taps are scaled by cos(pi*dx/(2*TS))*cos(pi*dy/(2*TS))
	 *  (the lapped-window/convolution non-commutation term; same weights as
	 *  norm_sym_weights in QuadCLTCPU.calculateCLTKernel). */
	public static double cosAtten(int dx, int dy) {
		return Math.cos(Math.PI * dx / (2 * TRANSFORM_SIZE)) *
		       Math.cos(Math.PI * dy / (2 * TRANSFORM_SIZE));
	}

	/** Embed a centered odd-sized pixel kernel into the centered KERNEL_WIDTH^2 support. */
	public static double [] embedCentered(
			double [] kernel,
			int       kwidth) {
		if (kwidth > KERNEL_WIDTH) {
			throw new IllegalArgumentException("embedCentered(): kernel "+kwidth+" > "+KERNEL_WIDTH);
		}
		double [] k15 = new double [KERNEL_WIDTH * KERNEL_WIDTH];
		int off = (KERNEL_WIDTH - kwidth) / 2;
		for (int i = 0; i < kwidth; i++) {
			System.arraycopy(kernel, i * kwidth, k15, (i + off) * KERNEL_WIDTH + off, kwidth);
		}
		return k15;
	}

	/**
	 * Centered pixel kernel (KERNEL_WIDTH^2) -> 4 symmetry classes -> DTT-III
	 * spectra * TD_SCALE. Mirrors ImageDttCPU.clt_symmetrize_kernel +
	 * clt_dtt3_kernel (package-private there).
	 * @param aa_math_sign true = mathematically-derived AA projection
	 *        (+k00 -k01 -k10 +k11); false = bit-copy of the existing
	 *        clt_symmetrize_kernel quadrant 3 (which textually repeats the AS
	 *        expression - see the self-test that discriminates the two).
	 */
	public static double [][] symmetrizeDtt3(
			double [] k15,
			boolean   aa_math_sign) {
		int ts = TRANSFORM_SIZE;
		int in_size = KERNEL_WIDTH;
		int m1 = ts - 1;
		int center = m1 * in_size + m1;
		double [][] sym = new double [4][ts * ts];
		for (int i = 0; i < ts; i++) {
			for (int j = 0; j < ts; j++) {
				double kmm = k15[center - i * in_size - j]; // k(-i,-j)
				double kmp = k15[center - i * in_size + j]; // k(-i,+j)
				double kpm = k15[center + i * in_size - j]; // k(+i,-j)
				double kpp = k15[center + i * in_size + j]; // k(+i,+j)
				sym[0][i * ts + j] = 0.25 * ( kmm + kmp + kpm + kpp);
				if (j > 0)            sym[1][ i    * ts + j - 1] = 0.25 * (-kmm + kmp - kpm + kpp);
				if (i > 0)            sym[2][(i-1) * ts + j    ] = 0.25 * (-kmm - kmp + kpm + kpp);
				if ((i > 0) && (j > 0)) {
					sym[3][(i-1) * ts + (j-1)] = aa_math_sign?
							(0.25 * ( kmm - kmp - kpm + kpp)) : // mathematical AA projection
							(0.25 * (-kmm + kmp - kpm + kpp));  // as in clt_symmetrize_kernel:10531
				}
			}
		}
		DttRad2 dtt = new DttRad2(ts);
		for (int q = 0; q < 4; q++) {
			sym[q] = dtt.dttt_iiie(sym[q], q, ts);
			for (int i = 0; i < sym[q].length; i++) {
				sym[q][i] *= TD_SCALE;
			}
		}
		return sym;
	}

	// ------------------------------------------------------------------
	// Synthetic self-test below (headless, no GUI classes touched)
	// ------------------------------------------------------------------

	/** Plain zero-padded pixel-domain convolution with a centered odd-width kernel.
	 *  convention: out(x,y) = sum k(dy,dx) * in(y - dy, x - dx) (true convolution) */
	static double [] pixConvolve(
			double [] img,
			int       width,
			double [] kernel,
			int       kwidth,
			boolean   mirror) { // mirror=true: correlation orientation in(y+dy, x+dx)
		int height = img.length / width;
		int kr = kwidth / 2;
		double [] out = new double [img.length];
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				double s = 0.0;
				for (int dy = -kr; dy <= kr; dy++) {
					int yy = mirror? (y + dy) : (y - dy);
					if ((yy < 0) || (yy >= height)) continue;
					for (int dx = -kr; dx <= kr; dx++) {
						int xx = mirror? (x + dx) : (x - dx);
						if ((xx < 0) || (xx >= width)) continue;
						s += kernel[(dy + kr) * kwidth + (dx + kr)] * img[yy * width + xx];
					}
				}
				out[y * width + x] = s;
			}
		}
		return out;
	}

	/** MCLT -> per-tile convolve_tile(ktile) -> IMCLT. Output is (width-8)x(height-8),
	 *  out(x,y) corresponds to in(x+4, y+4) (iclt_2d half-tile offset). */
	static double [] cltConvolve(
			ImageDttCPU imageDtt,
			double []   img,
			int         width,
			double [][] ktile) {
		final int window_type = 1; // sine (Princen-Bradley / TDAC)
		double [][][][] clt = imageDtt.clt_2d(
				img, width, window_type,
				0, 0,     // shiftX, shiftY
				-1, -1,   // debug_tileX, debug_tileY (none)
				0,        // debug_mode
				8,        // threadsMax
				0);       // globalDebugLevel
		for (int ty = 0; ty < clt.length; ty++) {
			for (int tx = 0; tx < clt[ty].length; tx++) {
				imageDtt.convolve_tile(clt[ty][tx], ktile, false);
			}
		}
		return imageDtt.iclt_2d(clt, window_type, 15, 0, 8, 0); // all 4 quadrants
	}

	/** max |a-b| over the interior / max |b| over the interior; a is the (width-8) clt image,
	 *  b the full-size reference (b sampled at +4,+4). */
	static double relErrInterior(
			double [] a, // (width-8) x (height-8)
			double [] b, // width x height reference
			int       width,
			int       margin) {
		int aw = width - TRANSFORM_SIZE;
		int ah = a.length / aw;
		int half = TRANSFORM_SIZE / 2;
		double max_diff = 0.0, max_ref = 0.0;
		for (int y = margin; y < ah - margin; y++) {
			for (int x = margin; x < aw - margin; x++) {
				double ref = b[(y + half) * width + (x + half)];
				double d = Math.abs(a[y * aw + x] - ref);
				if (d > max_diff) max_diff = d;
				double r = Math.abs(ref);
				if (r > max_ref) max_ref = r;
			}
		}
		return (max_ref > 0)? (max_diff / max_ref) : max_diff;
	}

	static double [] makeImpulses(int width, int height) {
		double [] img = new double [width * height];
		// smooth background so relative error is meaningful everywhere
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				img[y * width + x] = 2.0 + Math.cos(2 * Math.PI * (0.7 * x + 0.4 * y) / 64.0);
			}
		}
		int [][] pts = {{53, 61}, {64, 64}, {58, 71}, {37, 44}, {90, 83}}; // y,x - off tile centers
		double [] amps = {10.0, -7.0, 5.0, 8.0, -6.0};
		for (int p = 0; p < pts.length; p++) {
			img[pts[p][0] * width + pts[p][1]] += amps[p];
		}
		return img;
	}

	static double [] makeGrating(int width, int height, double fx, double fy, double phase) {
		double [] img = new double [width * height];
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				img[y * width + x] = Math.cos(2 * Math.PI * (fx * x + fy * y) + phase);
			}
		}
		return img;
	}

	static boolean report(String name, double rel_err, double tol) {
		boolean pass = rel_err <= tol;
		System.out.println(String.format("%-58s rel_err=%9.3e  tol=%7.1e  %s",
				name, rel_err, tol, pass? "PASS" : "FAIL"));
		return pass;
	}

	public static void main(String[] args) {
		final int width = 128, height = 128;
		final int margin = 24;
		final double tol = 1e-9;      // machine-precision expectations
		// symmetric-extension wrap residual of per-tile TD convolution
		// (inherent to the lapped structure, grows with tap offset; measured
		// 4.5e-3 for a 1-px shift tap, ~1e-2 for spread kernels)
		final double WRAP_TOL = 1e-2;
		boolean all_pass = true;

		ImageDttCPU imageDtt = new ImageDttCPU(
				16, TRANSFORM_SIZE, new ImageDttParameters(),
				true,  // aux
				true,  // mono
				true,  // lwir
				1.0);  // scale_strengths

		// --- test 0: delta filter -> all-ones TD response (pins the TD_SCALE chain)
		double [] delta = new double [KERNEL_WIDTH * KERNEL_WIDTH];
		delta[(KERNEL_WIDTH * KERNEL_WIDTH) / 2] = 1.0;
		double [][] id_ktile = symmetrizeDtt3(delta, true);
		double max_dev = 0.0;
		for (int i = 0; i < id_ktile[0].length; i++) {
			max_dev = Math.max(max_dev, Math.abs(id_ktile[0][i] - 1.0));
		}
		for (int q = 1; q < 4; q++) {
			for (int i = 0; i < id_ktile[q].length; i++) {
				max_dev = Math.max(max_dev, Math.abs(id_ktile[q][i]));
			}
		}
		all_pass &= report("0: delta kernel -> identity TD response", max_dev, 1e-12);

		// --- test 1: identity kernel round trip (pins MCLT scaffold + iclt offset)
		double [] impulses = makeImpulses(width, height);
		double [] rt = cltConvolve(imageDtt, impulses, width, id_ktile);
		all_pass &= report("1: identity kernel: MCLT->conv->IMCLT == input",
				relErrInterior(rt, impulses, width, margin), tol);

		// --- test 1b: single off-center tap. The TD path must equal pixel
		//     convolution with the cos-ATTENUATED tap (the model this class is
		//     built on): shift-by-1 scaled by cos(pi/16). Raw (unattenuated)
		//     comparison is printed as reference - its error IS 1-cos(pi/16).
		double [] shift1 = new double [KERNEL_WIDTH * KERNEL_WIDTH];
		shift1[(KERNEL_WIDTH * KERNEL_WIDTH) / 2 + 1] = 1.0; // k(0,+1)
		{
			double [][] sh_ktile = symmetrizeDtt3(shift1, true);
			double [] sh = cltConvolve(imageDtt, impulses, width, sh_ktile);
			double e_atten = relErrInterior(sh,
					pixConvolve(impulses, width, attenuate(shift1, KERNEL_WIDTH), KERNEL_WIDTH, false), width, margin);
			double e_raw = relErrInterior(sh,
					pixConvolve(impulses, width, shift1, KERNEL_WIDTH, false), width, margin);
			System.out.println(String.format(
					"   1b raw-tap err=%9.3e (should be 1-cos(pi/16)=%9.3e)", e_raw, 1.0 - Math.cos(Math.PI/16)));
			all_pass &= report("1b: single-tap shift == cos-attenuated pixel conv", e_atten, WRAP_TOL);
		}

		// --- the LoG (curt defaults: psf_radius=1.0, n_sigma=4)
		final double psf_radius = 1.0, n_sigma = 4.0;
		double [] log_td = getLogTd(psf_radius, n_sigma);
		int log_rad = CuasRTUtils.getRadiusLoGKernel(psf_radius, n_sigma);
		int log_w = 2 * log_rad + 1;
		double [] log_pix = CuasRTUtils.getLoGKenel(psf_radius, log_rad);
		System.out.println("LoG: radius="+log_rad+" ("+log_w+"x"+log_w+
				"), TD response min/max = "+min(log_td)+" / "+max(log_td));

		// --- test 2: folded identity == pixel-domain LoG (impulses)
		double [][] log_ktile = mulQuad(id_ktile, log_td);
		double [] folded_img = cltConvolve(imageDtt, impulses, width, log_ktile);
		double [] oracle_img = pixConvolve(impulses, width, log_pix, log_w, false);
		all_pass &= report("2: folded identity == pixel LoG (impulses)",
				relErrInterior(folded_img, oracle_img, width, margin), WRAP_TOL);

		// --- test 3: grating sweep (pins per-bin scale across frequency).
		//     Gated on error RELATIVE TO THE UNIT GRATING AMPLITUDE: near the
		//     LoG null the filtered reference is tiny and ref-relative error
		//     inflates without meaning. Non-bin frequencies are machine-exact
		//     (the OLA-exact regime); bin frequencies carry the wrap residual.
		double [][] freqs = {{1.0/32, 0}, {1.0/16, 0}, {0, 1.0/16}, {1.0/16, 1.0/16},
				{3.0/32, 1.0/32}, {1.0/8, 1.0/8}, {3.0/16, 0}, {7.0/32, 5.0/32}};
		double worst_grating = 0.0;
		for (double [] f : freqs) {
			double [] g = makeGrating(width, height, f[0], f[1], 0.6);
			double [] gf = cltConvolve(imageDtt, g, width, log_ktile);
			double [] go = pixConvolve(g, width, log_pix, log_w, false);
			double e_rel = relErrInterior(gf, go, width, margin);
			double e_amp = absErrInterior(gf, go, width, margin); // grating amplitude = 1
			System.out.println(String.format(
					"   grating fx=%7.4f fy=%7.4f  err/amp=%9.3e  (ref-relative %9.3e)",
					f[0], f[1], e_amp, e_rel));
			worst_grating = Math.max(worst_grating, e_amp);
		}
		all_pass &= report("3: grating sweep folded == pixel LoG (err/amplitude)", worst_grating, 3e-2);

		// --- test 4: asymmetric kernel (all 4 quadrants nonzero) vs the
		//     cos-attenuated pixel oracle - machine-precision MODEL test that
		//     also discriminates the two AA sign variants of clt_symmetrize_kernel
		double [] asym = new double [KERNEL_WIDTH * KERNEL_WIDTH];
		int c = (KERNEL_WIDTH * KERNEL_WIDTH) / 2;
		asym[c + 1 * KERNEL_WIDTH + 2] = 0.6;  // k(+1,+2)
		asym[c - 2 * KERNEL_WIDTH + 1] = 0.3;  // k(-2,+1)
		asym[c]                        = 0.4;  // k(0,0)
		asym[c + 2 * KERNEL_WIDTH - 3] = -0.2; // k(+2,-3)
		double [] asym_atten = attenuate(asym, KERNEL_WIDTH);
		double [] best = new double [2]; // best rel_err per AA variant (0=math, 1=code)
		for (int variant = 0; variant < 2; variant++) {
			boolean aa_math = (variant == 0);
			double [][] asym_ktile = symmetrizeDtt3(asym, aa_math);
			double [] path_a = cltConvolve(imageDtt, impulses, width, asym_ktile);
			double e_conv = relErrInterior(path_a,
					pixConvolve(impulses, width, asym_atten, KERNEL_WIDTH, false), width, margin);
			double e_corr = relErrInterior(path_a,
					pixConvolve(impulses, width, asym_atten, KERNEL_WIDTH, true), width, margin);
			best[variant] = Math.min(e_conv, e_corr);
			System.out.println(String.format(
					"   4%s AA %s sign: conv=%9.3e  corr=%9.3e  (best orientation: %s)",
					aa_math? "a":"b", aa_math? "math":"code", e_conv, e_corr,
					(e_conv <= e_corr)? "convolution" : "correlation"));
		}
		// exactly one AA sign variant should reproduce (attenuated) pixel-domain
		// convolution; which one documents the file/GPU convention (and whether
		// clt_symmetrize_kernel quadrant 3 is a latent copy-paste bug)
		all_pass &= report(String.format(
				"4: asym kernel model test (winner: AA %s sign)",
				(best[0] <= best[1])? "math" : "code"),
				Math.min(best[0], best[1]), 2e-2);
		// --- test 4c: PURE-AA kernel - the sharpest discriminator. The "code"
		//     variant projects AA content onto the (already-populated) AS class,
		//     i.e. loses it entirely for a pure-AA kernel: expect O(1) error
		//     there and wrap-floor error for the math variant.
		double [] pure_aa = new double [KERNEL_WIDTH * KERNEL_WIDTH];
		pure_aa[c + 1 * KERNEL_WIDTH + 1] =  0.25;
		pure_aa[c + 1 * KERNEL_WIDTH - 1] = -0.25;
		pure_aa[c - 1 * KERNEL_WIDTH + 1] = -0.25;
		pure_aa[c - 1 * KERNEL_WIDTH - 1] =  0.25;
		pure_aa[c] = 1.0; // carrier so output is not ~0
		double [] pure_aa_atten = attenuate(pure_aa, KERNEL_WIDTH);
		double [] aa_err = new double [2];
		for (int variant = 0; variant < 2; variant++) {
			double [][] aa_ktile = symmetrizeDtt3(pure_aa, variant == 0);
			double [] out_aa = cltConvolve(imageDtt, impulses, width, aa_ktile);
			aa_err[variant] = Math.min(
					relErrInterior(out_aa, pixConvolve(impulses, width, pure_aa_atten, KERNEL_WIDTH, false), width, margin),
					relErrInterior(out_aa, pixConvolve(impulses, width, pure_aa_atten, KERNEL_WIDTH, true),  width, margin));
			System.out.println(String.format("   4c AA %s sign (pure-AA kernel): rel_err=%9.3e",
					(variant == 0)? "math":"code", aa_err[variant]));
		}
		all_pass &= report("4c: pure-AA kernel, math sign", aa_err[0], 2e-2);
		if (aa_err[1] < 10 * aa_err[0]) {
			System.out.println("   WARNING: code AA sign not clearly refuted (expected O(1) error)");
		}

		// --- test 5: full-run A/B PREDICTION. Production-like aberration kernel
		//     K (its taps are TD-effective by calibration), folded K*L0 vs the
		//     oracle pixelLoG(TD-render with K). Per-bin folding composes as
		//     (k (*) log/c)*c while the oracle is (k*c) (*) log - the difference
		//     is a second-order cross term this test quantifies (REPORTED, loose
		//     tolerance; if too big, the exact route is a per-tile pixel-domain
		//     composite: q = ((k*c) (*) log)/c, host-side at upload).
		double [] kab = new double [KERNEL_WIDTH * KERNEL_WIDTH];
		double s_eff = 0.0;
		for (int dy = -KERNEL_WIDTH/2; dy <= KERNEL_WIDTH/2; dy++) {
			for (int dx = -KERNEL_WIDTH/2; dx <= KERNEL_WIDTH/2; dx++) {
				// asymmetric decaying blur: Gaussian sigma 1.2 centered (+0.4,-0.3)
				double g = Math.exp(-((dx-0.4)*(dx-0.4) + (dy+0.3)*(dy+0.3)) / (2*1.2*1.2));
				kab[(dy + KERNEL_WIDTH/2) * KERNEL_WIDTH + (dx + KERNEL_WIDTH/2)] = g;
				s_eff += g * cosAtten(dx, dy); // production normalization convention
			}
		}
		for (int i = 0; i < kab.length; i++) kab[i] /= s_eff;
		double [][] kab_ktile = symmetrizeDtt3(kab, true);
		double [] render_k   = cltConvolve(imageDtt, impulses, width, kab_ktile);          // current pipeline
		double [] render_kl  = cltConvolve(imageDtt, impulses, width, mulQuad(kab_ktile, log_td)); // folded
		// oracle: pixel LoG applied to the current-pipeline render (interior only;
		// render_k is already the cropped clt-size image)
		int aw = width - TRANSFORM_SIZE;
		double [] oracle_ab = pixConvolve(render_k, aw, log_pix, log_w, false);
		double max_d = 0.0, max_r = 0.0;
		for (int y = margin; y < (render_kl.length/aw) - margin; y++) {
			for (int x = margin; x < aw - margin; x++) {
				max_d = Math.max(max_d, Math.abs(render_kl[y*aw+x] - oracle_ab[y*aw+x]));
				max_r = Math.max(max_r, Math.abs(oracle_ab[y*aw+x]));
			}
		}
		double e5 = max_d / max_r;
		System.out.println(String.format(
				"   5: fold-vs-pixel-LoG cross-term residual = %9.3e (predicts full-run A/B floor)", e5));
		all_pass &= report("5: A/B prediction: folded K*L0 vs pixelLoG(render(K))", e5, 1e-1);

		// --- test 6: SMOOTH-CONTENT response (the sky-band mechanism, 2026-07-06
		//     LOG-01 run: folded-vs-pixel-LoG difference = horizontal bands, exactly
		//     8-row (tile-pitch) periodic, amplitude ~ local vertical gradient).
		//     Pixel LoG of constant/ramp is ~0; the folded path's per-tile wrap
		//     residual is not. Measures the response to each content moment.
		{
			System.out.println("--- test 6: folded-LoG response to smooth content (pixel LoG of these is ~0)");
			double [][] smooth = new double [3][width*height];
			String [] snames = {"constant 2000", "ramp 5/row (y)", "quadratic 0.05*y^2"};
			for (int y = 0; y < height; y++) {
				for (int x = 0; x < width; x++) {
					smooth[0][y*width+x] = 2000.0;
					smooth[1][y*width+x] = 5.0 * y;
					smooth[2][y*width+x] = 0.05 * y * y;
				}
			}
			// same ramp through kernel types: identity, aberration-like (test-5 kab),
			// pure LoG, folded kab*L0 - crease-band amplitude per kernel (Andrey
			// 07/06/2026: aberration kernels share the machinery - do THEY band?)
			{
				double [] ramp = smooth[1];
				String [] knames = {"identity", "aberration-like kab", "pure LoG", "folded kab*L0"};
				double [][][] ktiles = {id_ktile, kab_ktile, log_ktile, mulQuad(kab_ktile, log_td)};
				double [][] oracles = { // cos-attenuated pixel-domain truth per kernel
						pixConvolve(ramp, width, attenuate(delta, KERNEL_WIDTH), KERNEL_WIDTH, false),
						pixConvolve(ramp, width, attenuate(kab, KERNEL_WIDTH), KERNEL_WIDTH, false),
						pixConvolve(ramp, width, log_pix, log_w, false),
						pixConvolve(pixConvolve(ramp, width, attenuate(kab, KERNEL_WIDTH), KERNEL_WIDTH, false),
								width, log_pix, log_w, false)};
				System.out.println("   crease bands on ramp 5/row, by kernel (max |8-row band| in diff vs pixel truth; out_rms = output scale):");
				for (int t = 0; t < ktiles.length; t++) {
					double [] out = cltConvolve(imageDtt, ramp, width, ktiles[t]);
					int aw6 = width - TRANSFORM_SIZE, half = TRANSFORM_SIZE/2;
					double max_band = 0; double orms = 0; int ocnt = 0;
					for (int y = 56; y < 72; y++) {
						double sum = 0;
						for (int x = margin; x < aw6-margin; x++) {
							sum += out[y*aw6+x] - oracles[t][(y+half)*width + (x+half)];
							orms += out[y*aw6+x]*out[y*aw6+x]; ocnt++;
						}
						max_band = Math.max(max_band, Math.abs(sum/(aw6-2*margin)));
					}
					System.out.println(String.format("     %-20s max|band| = %7.4f   out_rms = %9.3f   band/out = %8.5f",
							knames[t], max_band, Math.sqrt(orms/ocnt), max_band/Math.max(Math.sqrt(orms/ocnt),1e-12)));
				}
			}
			for (int t = 0; t < smooth.length; t++) {
				double [] out = cltConvolve(imageDtt, smooth[t], width, log_ktile);
				double [] orc = pixConvolve(smooth[t], width, log_pix, log_w, false);
				int aw6 = width - TRANSFORM_SIZE, half = TRANSFORM_SIZE/2;
				// horizontal-mean profile of the difference, one 8-row block mid-image
				System.out.print("   "+snames[t]+": diff profile rows 56..71:");
				double max_abs = 0;
				for (int y = 56; y < 72; y++) {
					double s = 0;
					for (int x = margin; x < aw6-margin; x++) s += out[y*aw6+x] - orc[(y+half)*width + (x+half)];
					s /= (aw6 - 2*margin);
					max_abs = Math.max(max_abs, Math.abs(s));
					System.out.print(String.format(" %+.3f", s));
				}
				System.out.println(String.format("  (max|band| = %.3f)", max_abs));
			}
		}
		// --- test 7: SEPARATE windowed-LoG TD product (Andrey's proposal 07/07/2026,
		//     "while we are in MCLT alchemy"): the LoG folded like DATA (16x16 pixel
		//     tile, windowed MCLT fold + DTT-IV, pixel-centered at (8,8) -> all 4
		//     quadrants), full 16-mult quad product per tile (the correlation
		//     machinery; LoG symmetric => correlation == convolution), DTT-II back
		//     (dttt_iie per quadrant = GPU dttii_2d) + corr_unfold_tile -> 15x15
		//     patch, windowed OLA with per-pixel sum(w^2) normalization.
		//     RESULT (07/07/2026): the crease bands are IDENTICAL to the per-bin
		//     fold (+/-0.546 per 5/row ramp) - the window only makes the DTT
		//     symmetric-extension REFLECTION small (w~0.1-0.3 at edge pixels), it
		//     does not remove it; the per-bin fold is the degenerate case of this
		//     same quad product. The crease is BASIS-INHERENT to any single-tile
		//     TD product. Correlation tolerates it (smooth additive bias does not
		//     move peaks); filtering exposes it. Broadband fidelity is excellent
		//     (impulse 2e-3, better than the kernel fold - no aberration-kernel
		//     cross-term since this is a separate pass). Band-free LoG requires
		//     pixel-domain convolution on the ASSEMBLED image (no tile boundaries).
		{
			System.out.println("--- test 7: windowed-LoG TD product (correlation-style, separate pass)");
			final int n2 = 2 * TRANSFORM_SIZE;
			double [] k16 = new double [n2 * n2];
			int lr = log_w / 2;
			// pre-divide taps by the sine window: the fold WINDOWS the tile, and at
			// tap offset 3 the window is already ~0.78 - without this the LoG's
			// negative surround is under-weighted vs its center, breaking the flux
			// balance (measured ~8% DC leakage). w*(log/w) = exact LoG after fold.
			double [] w16 = new double [n2];
			for (int i = 0; i < n2; i++) w16[i] = Math.sin(Math.PI * (i + 0.5) / n2);
			// ... and by cosAtten (the OLA window non-commutation, same as getLogTd):
			// the effective applied filter is then the EXACT flux-normalized LoG
			// (DC response = 0 - without this the attenuated LoG has DC gain ~0.06,
			// which showed as exactly 0.0607*ramp in the first run of this test)
			for (int dy = -lr; dy <= lr; dy++) {
				for (int dx = -lr; dx <= lr; dx++) {
					k16[(TRANSFORM_SIZE + dy) * n2 + TRANSFORM_SIZE + dx] =
							log_pix[(dy + lr) * log_w + dx + lr] /
							(w16[TRANSFORM_SIZE + dy] * w16[TRANSFORM_SIZE + dx] * cosAtten(dx, dy));
				}
			}
			DttRad2 dtt7 = new DttRad2(TRANSFORM_SIZE);
			dtt7.set_window(1);
			double [][] log_tdq = new double [4][];
			for (int m = 0; m < 4; m++) {
				log_tdq[m] = dtt7.dttt_iv(dtt7.fold_tile(k16, TRANSFORM_SIZE, m), m, TRANSFORM_SIZE);
			}
			// calibrate patch orientation + conjugation side + global gain on the
			// impulse image against the cos-attenuated pixel LoG (the OLA window
			// non-commutation is still expected - benign, precompensable)
			double [] oracle_imp = pixConvolve(impulses, width, log_pix, log_w, false); // exact pixel LoG
			double best_err = Double.MAX_VALUE; int best_var = -1; double best_gain = 1.0;
			int height7 = impulses.length / width;
			for (int variant = 0; variant < 8; variant++) {
				double [] o7 = tdLogFilter(imageDtt, impulses, width, log_tdq,
						(variant & 1) != 0, (variant & 2) != 0, (variant & 4) != 0);
				double sxy = 0, sxx = 0;
				for (int y = margin; y < height7 - margin; y++) {
					for (int x = margin; x < width - margin; x++) {
						sxy += o7[y*width+x] * oracle_imp[y*width+x];
						sxx += o7[y*width+x] * o7[y*width+x];
					}
				}
				double g = sxy / sxx;
				double md7 = 0, mr7 = 0;
				for (int y = margin; y < height7 - margin; y++) {
					for (int x = margin; x < width - margin; x++) {
						md7 = Math.max(md7, Math.abs(g * o7[y*width+x] - oracle_imp[y*width+x]));
						mr7 = Math.max(mr7, Math.abs(oracle_imp[y*width+x]));
					}
				}
				double e = md7 / mr7;
				System.out.println(String.format(
						"   7 variant %d (conj_log=%b, flip=%b, modeT=%b): gain=%9.5f  rel_err=%9.3e",
						variant, (variant & 1) != 0, (variant & 2) != 0, (variant & 4) != 0, g, e));
				if (e < best_err) { best_err = e; best_var = variant; best_gain = g; }
			}
			all_pass &= report(String.format("7: windowed-LoG TD product == pixel LoG broadband (var %d)", best_var),
					best_err, WRAP_TOL);
			// THE BAND TEST: same ramp as test 6 - crease bands should VANISH
			double [] ramp7 = new double [width * height];
			for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) ramp7[y*width+x] = 5.0 * y;
			double [] o7r = tdLogFilter(imageDtt, ramp7, width, log_tdq,
					(best_var & 1) != 0, (best_var & 2) != 0, (best_var & 4) != 0);
			double max_band7 = 0;
			System.out.print("   ramp 5/row band profile rows 56..71 (test 6 fold gave +/-0.547):");
			for (int y = 56; y < 72; y++) {
				double s = 0;
				for (int x = margin; x < width - margin; x++) s += best_gain * o7r[y*width+x];
				s /= (width - 2*margin);
				max_band7 = Math.max(max_band7, Math.abs(s));
				System.out.print(String.format(" %+.3f", s));
			}
			System.out.println();
			// CHARACTERIZATION, not a target: the crease is basis-inherent (see the
			// block comment) - assert it stays at the known level, catching regressions
			all_pass &= report("7b: ramp crease bands (EXPECTED ~0.547, basis-inherent)", max_band7, 0.6);
		}
		System.out.println(all_pass? "ALL PASSED" : "FAILURES PRESENT");
		System.exit(all_pass? 0 : 1);
	}

	/**
	 * Filter an image with the LoG entirely in the transform domain using the
	 * CORRELATION machinery (test 7 / Andrey's separate-LoG proposal): per tile,
	 * the full 16-multiply quad product of the image MCLT with a windowed+folded
	 * 16x16 LoG tile (DATA-side fold - no symmetric extension, hence no crease),
	 * DTT-II back (dttt_iie per quadrant, as GPU dttii_2d), corr_unfold_tile to a
	 * 15x15 pixel patch, windowed overlap-add normalized by the accumulated w^2.
	 * Output is full image size (margins under-covered - compare interior only).
	 * Global scale is NOT normalized here (calibrated by the caller on an impulse).
	 * By Claude on 07/07/2026.
	 */
	static double [] tdLogFilter(
			ImageDttCPU imageDtt,
			double []   img,
			int         width,
			double [][] log_tdq,   // [4][64] windowed+folded LoG tile (DTT-IV)
			boolean     conj_log,  // conjugate the LoG side (else the image side)
			boolean     flip,      // patch index mapping direction
			boolean     mode_t) {  // transpose the DTT-II mode bits (GPU works transposed)
		final int ts = TRANSFORM_SIZE;
		final int n2 = 2 * ts;
		final int height = img.length / width;
		double [][][][] clt = imageDtt.clt_2d(
				img, width, 1, // window_type = sine
				0, 0, -1, -1, 0, 8, 0);
		final int tilesY = clt.length, tilesX = clt[0].length;
		double [] out  = new double [img.length];
		double [] wsum = new double [img.length];
		DttRad2 dtt = new DttRad2(ts);
		dtt.set_window(1);
		double [] w1 = new double [n2];
		for (int i = 0; i < n2; i++) w1[i] = Math.sin(Math.PI * (i + 0.5) / n2);
		for (int ty = 0; ty < tilesY; ty++) {
			for (int tx = 0; tx < tilesX; tx++) {
				// conjugation (correlation vs convolution orientation): negate the
				// S components (quadrants 1,2; quadrant 3 flips twice) of ONE side
				double [][] a = new double [4][];
				double [][] b = new double [4][];
				for (int q = 0; q < 4; q++) {
					a[q] = clt[ty][tx][q].clone();
					b[q] = log_tdq[q].clone();
					if ((q == 1) || (q == 2)) {
						double [] tgt = conj_log ? b[q] : a[q];
						for (int i = 0; i < tgt.length; i++) tgt[i] = -tgt[i];
					}
				}
				imageDtt.convolve_tile(a, b, false); // the same 16-mult quad product
				double [][] pd = new double [4][];
				for (int q = 0; q < 4; q++) {
					int m2 = mode_t ? (((q & 1) << 1) | ((q >> 1) & 1)) : q;
					pd[q] = dtt.dttt_iie(a[q], m2, ts); // GPU dttii_2d equivalent
				}
				double [] patch = dtt.corr_unfold_tile(pd, ts); // 15x15
				for (int py = 0; py < n2 - 1; py++) {
					int ly = flip ? (n2 - 1 - py) : (py + 1);
					int gy = ty * ts + ly;
					if ((gy < 0) || (gy >= height)) continue;
					for (int px = 0; px < n2 - 1; px++) {
						int lx = flip ? (n2 - 1 - px) : (px + 1);
						int gx = tx * ts + lx;
						if ((gx < 0) || (gx >= width)) continue;
						double w = w1[ly] * w1[lx];
						out [gy * width + gx] += w * patch[py * (n2 - 1) + px];
						wsum[gy * width + gx] += w * w;
					}
				}
			}
		}
		for (int i = 0; i < out.length; i++) {
			out[i] = (wsum[i] > 0) ? (out[i] / wsum[i]) : 0.0;
		}
		return out;
	}

	/** max |a-b| over the interior (no normalization); a is the clt-cropped image. */
	static double absErrInterior(
			double [] a,
			double [] b,
			int       width,
			int       margin) {
		int aw = width - TRANSFORM_SIZE;
		int ah = a.length / aw;
		int half = TRANSFORM_SIZE / 2;
		double max_diff = 0.0;
		for (int y = margin; y < ah - margin; y++) {
			for (int x = margin; x < aw - margin; x++) {
				max_diff = Math.max(max_diff, Math.abs(a[y * aw + x] - b[(y + half) * width + (x + half)]));
			}
		}
		return max_diff;
	}

	/** Copy of a centered odd-width tap kernel with each tap scaled by cosAtten. */
	static double [] attenuate(double [] k, int kwidth) {
		int kr = kwidth / 2;
		double [] out = new double [k.length];
		for (int dy = -kr; dy <= kr; dy++) {
			for (int dx = -kr; dx <= kr; dx++) {
				out[(dy + kr) * kwidth + (dx + kr)] = k[(dy + kr) * kwidth + (dx + kr)] * cosAtten(dx, dy);
			}
		}
		return out;
	}

	static double [][] mulQuad(double [][] ktile, double [] l0) {
		double [][] out = new double [ktile.length][];
		for (int q = 0; q < ktile.length; q++) {
			out[q] = ktile[q].clone();
			if (q < 4) {
				for (int i = 0; i < out[q].length; i++) {
					out[q][i] *= l0[i];
				}
			}
		}
		return out;
	}

	static double min(double [] a) { double m = a[0]; for (double v : a) m = Math.min(m, v); return m; }
	static double max(double [] a) { double m = a[0]; for (double v : a) m = Math.max(m, v); return m; }
}
