package com.elphel.imagej.cuas.rt;

import java.awt.Rectangle;
import java.nio.file.Path;
import java.text.SimpleDateFormat; // By Claude on 06/11/2026
import java.util.Calendar;         // By Claude on 06/11/2026

import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.cameras.EyesisCorrectionParameters.CorrectionParameters;
import com.elphel.imagej.common.ShowDoubleFloatArrays;
import com.elphel.imagej.cuas.CuasMotion;
import com.elphel.imagej.cuas.CuasRanging;
import com.elphel.imagej.ims.UasLogReader;
import com.elphel.imagej.tileprocessor.QuadCLT;
import com.elphel.imagej.tileprocessor.QuadCLTCPU;

import ij.ImagePlus;
import ij.ImageStack;

public class CuasDetectRT {
	public static final String SUFFIX_FPIXELS_TIFF = "-CUAS-MERGED-CUAS.tiff";
	public static final String SUFFIX_SYNTH_TIFF =   "-CUAS-SYNTHETIC-CUAS.tiff"; // synthetic input (curt_synth_src) // By Claude on 06/11/2026
	public static final String SUFFIX_LOG = "-LoG";
	public static final String SUFFIX_CONV2D = "-CONV2D";
	public static final String SUFFIX_CONV3D3 = "-CONV3D3";
	public static final String SUFFIX_CONV5D =  "-CONV5D";
	public static final String SUFFIX_RECUR =   "-RECUR"; // By Claude on 06/09/2026
	// ROI override for synthetic input: covers the radial 11x11 target block of   // By Claude on 06/11/2026
	// gen_synth_cuas.py (center (320.5,256.5), step 30 -> +/-150 px) + kernel
	// margin, so the stored curt_save_select parameter keeps matching the REAL image.
	public static final Rectangle SYNTH_ROI = new Rectangle(160, 96, 320, 320); // By Claude on 06/11/2026
	public static final String [] CONV3D3_MODES = {"plain", "winner", "power"};
	public static final int CONV3D3_MODE_PLAIN =  0;
	public static final int CONV3D3_MODE_WINNER = 1;
	public static final int CONV3D3_MODE_POWER =  2;
	public static final int MAXDECIMALS =         4;
	
	CuasRanging     cuasRanging;
	UasLogReader    uasLogReader;
	CLTParameters   clt_parameters;
	String          model_directory;
//	String []       model_names;
	QuadCLT         master_CLT;
	String          fpixels_file = null;
	String          base_name = null; // add suffix and 
	float [][]      fpixels; 
	double [][]     dpixels; // same as fpixels for faster calculations on 64-bit processors 
	int             width;
	int             height;
	String []       time_stamps;
	double          infinity;
	
	public CuasDetectRT(
			CLTParameters     clt_parameters,
			UasLogReader      uasLogReader,
			String            model_directory,
			int               debugLevel) {
		this.uasLogReader =   uasLogReader;
		this.clt_parameters = clt_parameters;
		this.model_directory = model_directory;
		this.infinity = clt_parameters.imp.cuas_infinity;
		final String fpixels_suffix = clt_parameters.imp.curt_synth_src ? SUFFIX_SYNTH_TIFF : SUFFIX_FPIXELS_TIFF; // By Claude on 06/11/2026
		String [] fpixels_paths = CorrectionParameters.getFilesByExtensionAsArray(model_directory, fpixels_suffix); // By Claude on 06/11/2026
		
		String [] dbg_slices=null;
		float [][] dbg_pixels=null;
		boolean save_copy = false; // true;
		if (fpixels_paths.length > 0) {
			if (fpixels_paths.length > 1) {
				System.out.println((fpixels_paths.length)+" files ending with \""+fpixels_suffix+"\" found in "+model_directory+", will use the first one."); // By Claude on 06/11/2026
			}
			fpixels_file = fpixels_paths[0];
			ImagePlus imp = new ImagePlus(fpixels_file);
			width =  imp.getWidth();
			height = imp.getHeight();
			if (width > 0) {
					ImageStack stack = imp.getStack();
					// Skip non-ts ("average") slices;
					int first_slice = 1;
					int num_slices = stack.getSize();
					for (; !Character.isDigit(stack.getSliceLabel(first_slice).charAt(0)); first_slice++);
					time_stamps = new String [num_slices - first_slice + 1];
					if (save_copy) {
						dbg_slices = new String [num_slices];
						dbg_pixels = new float [num_slices][];
						for (int i = 0; i < dbg_slices.length; i++) {
							dbg_slices[i] = stack.getSliceLabel(1 + i);
							dbg_pixels[i] = (float[]) stack.getPixels (1 + i);
						}
					}
					fpixels = new float [time_stamps.length][width*height];
					for (int i = 0; i < time_stamps.length; i++) {
						time_stamps[i] = stack.getSliceLabel(first_slice + i);
						fpixels[i] = (float[]) stack.getPixels (first_slice + i);
					}
					String fpixels_name = Path.of(fpixels_file).getFileName().toString();
					base_name = fpixels_name.substring(0,fpixels_name.length() - fpixels_suffix.length()); // By Claude on 06/11/2026
					if (clt_parameters.imp.curt_synth_src) { // mark all output titles - synthetic and real runs share the directory // By Claude on 06/11/2026
						// Runtime mixing: synthetic file holds NORMALIZED (peak 1) clean targets; // By Claude on 06/12/2026
						// scale to the wanted peak (counts) and optionally add the real scene
						// (label-matched frames of *-CUAS-MERGED-CUAS.tiff) as background.
						double synth_scale = clt_parameters.imp.curt_synth_scale;
						boolean synth_bg =   clt_parameters.imp.curt_synth_bg;
						for (int i = 0; i < fpixels.length; i++) {
							float [] fpix_scaled = new float [fpixels[i].length]; // do not modify the stack's arrays
							for (int pix = 0; pix < fpixels[i].length; pix++) {
								fpix_scaled[pix] = (float) (fpixels[i][pix] * synth_scale);
							}
							fpixels[i] = fpix_scaled;
						}
						int bg_avg = Math.max(1, clt_parameters.imp.curt_synth_bg_avg);         // By Claude on 06/13/2026
						if (synth_bg) {
							String [] real_paths = CorrectionParameters.getFilesByExtensionAsArray(model_directory, SUFFIX_FPIXELS_TIFF);
							if (real_paths.length > 0) {
								ImagePlus imp_real = new ImagePlus(real_paths[0]);
								ImageStack real_stack = imp_real.getStack();
								java.util.HashMap<String,Integer> real_idx = new java.util.HashMap<String,Integer>();
								for (int sl = 1; sl <= real_stack.getSize(); sl++) {
									real_idx.put(real_stack.getSliceLabel(sl), sl);
								}
								if (bg_avg <= 1) {                                             // By Claude on 06/13/2026 (original label-matched path)
									int n_added = 0;
									for (int i = 0; i < time_stamps.length; i++) {
										Integer sl = real_idx.get(time_stamps[i]);
										if (sl != null) {
											float [] real_pix = (float[]) real_stack.getPixels(sl);
											for (int pix = 0; pix < fpixels[i].length; pix++) {
												fpixels[i][pix] += real_pix[pix];
											}
											n_added++;
										}
									}
									System.out.println("Synthetic mixing: scale "+synth_scale+", real background added to "+
											n_added+" of "+time_stamps.length+" frames from "+real_paths[0]);
								} else {                                                       // By Claude on 06/13/2026
									// Pyramid-level emulation by DECIMATION: output frame i is the mean of
									// bg_avg consecutive frames [i*N .. i*N+N-1] of BOTH the scaled synthetic
									// target AND the real background (non-overlapping groups). This matches
									// level log2(N): the sequence is DECIMATED by N (fewer output frames), the
									// moving target is motion-averaged like the real pyramid, and the noise
									// statistics are those of an N-frame average. Timestamp = LAST real frame
									// of each group (pyramid-level convention), so labels increment by N/fps.
									Integer sl0 = real_idx.get(time_stamps[0]);
									if (sl0 == null) sl0 = 1; // fall back to the start of the real stack
									int n_synth = time_stamps.length;                          // By Claude on 06/13/2026
									int npix_frame = width * height;
									int real_groups = (real_stack.getSize() - sl0 + 1) / bg_avg;
									int num_out = Math.min(n_synth / bg_avg, real_groups);     // complete groups only
									float  [][] dec_pixels = new float  [num_out][npix_frame];
									String  []  dec_ts     = new String [num_out];
									double  []  acc        = new double [npix_frame];
									for (int i = 0; i < num_out; i++) {
										java.util.Arrays.fill(acc, 0.0);
										for (int n = 0; n < bg_avg; n++) {
											float [] syn_pix  =          fpixels[i * bg_avg + n];          // already scaled by synth_scale
											float [] real_pix = (float[]) real_stack.getPixels(sl0 + i * bg_avg + n);
											for (int pix = 0; pix < npix_frame; pix++) acc[pix] += syn_pix[pix] + real_pix[pix];
										}
										for (int pix = 0; pix < npix_frame; pix++) dec_pixels[i][pix] = (float) (acc[pix] / bg_avg);
										dec_ts[i] = real_stack.getSliceLabel(sl0 + i * bg_avg + bg_avg - 1);
									}
									fpixels     = dec_pixels;
									time_stamps = dec_ts;
									System.out.println("Synthetic mixing: scale "+synth_scale+", pyramid-level emulation by decimation x"+bg_avg+
											" (mean of "+bg_avg+" consecutive synth+real frames per output frame), "+
											num_out+" output frames from "+n_synth+" synthetic / "+real_stack.getSize()+" real");
								}
							} else {
								System.out.println("Synthetic mixing: real background requested but no \""+SUFFIX_FPIXELS_TIFF+"\" found - synthetic only");
								synth_bg = false;
							}
						} else {
							System.out.println("Synthetic mixing: scale "+synth_scale+", no background (clean targets)");
						}
						base_name += "-SYNTH"+d2s(synth_scale)+(synth_bg ? "B" : "")+((synth_bg && (bg_avg > 1)) ? ("AVG"+bg_avg) : ""); // By Claude on 06/13/2026
					}
					System.out.println("Read image data from "+fpixels_file);
			} else {
				System.out.println("Failed to read image data from "+fpixels_file);
				fpixels_file = null;
				fpixels = null;
			}
		} else {
			System.out.println("No files ending with \""+fpixels_suffix+"\" found in "+model_directory); // By Claude on 06/11/2026
			fpixels_file = null;
			fpixels = null;
		}
		if (fpixels != null) {
			dpixels = new double [fpixels.length][width*height];
			for (int i = 0; i < fpixels.length; i++) for (int pix = 0; pix < fpixels[i].length; pix++) {
				dpixels[i][pix] = fpixels[i][pix]; 	
			}
		} else {
			dpixels = null;
		}
		// time_stamps
		if (dbg_pixels != null) {
			ImagePlus imp_copy = ShowDoubleFloatArrays.makeArrays(
					dbg_pixels, // float[][] pixels,
					getWidth(),  // int width,
					getHeight(), // int height,
					base_name+"-COPY",					
					dbg_slices); //String [] titles)
      		QuadCLTCPU.saveImagePlusInDirectory(imp_copy, model_directory);
 		}
		return;
	}
	
	public static String d2s(double d, int maxDecimalsPlaces) {
		String fmt = String.format("%%.%df", maxDecimalsPlaces);
		String s = String.format(fmt, d);
		s = s.trim();
		while (s.endsWith("0")) {
			s = s.substring(0,s.length()-1);
		}
		if (s.endsWith(".")) {
			s = s.substring(0,s.length()-1);
		}
		return s;
	}

	public static String d2s(double d) {
		return d2s(d, MAXDECIMALS);
	}

	/** Wall-clock stamp for tracing long-running stages (as in OpticalFlow.java). */
	public static String now() { // By Claude on 06/11/2026
		return new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime());
	}
	
	
	public int getWidth() {
		return width;
	}
	public int getHeight() {
		return height;
	}
	String [] getTimeStamps() {
		return time_stamps;
	}

	String [] getTimeStamps(int from_indx) {
		String [] time_stamps_from = new String[time_stamps.length - from_indx];
		System.arraycopy(time_stamps, from_indx, time_stamps_from, 0, time_stamps_from.length);
		return time_stamps_from;
	}
	String [] getTimeStamps(int from_indx, int tdecimate) {
		String [] time_stamps_decimated = new String[time_stamps.length / tdecimate];
		for (int i = 0; i < time_stamps_decimated.length; i++) {
			time_stamps_decimated[i] = time_stamps[(i+1)* tdecimate - 1];
		}
		return time_stamps_decimated;
	}

	
	
	public double [][] getDPpixels(){
		return dpixels;
	}
	
	public float [][] getFPixels() {
		return fpixels;
	}
	
	public String getBaseName() {
		return base_name;
	}
	
	public String getModelDirectory() {
		return model_directory;
	}

	/**
	 * Combine several same-size RECT renderings into one hyperstack: bottom slider // By Claude on 06/11/2026
	 * selects the view type (what was previously in separate files), inner slider -
	 * timestamp, so corresponding areas of all processing stages are immediately
	 * comparable at the same timestamp without aligning sliders across windows.
	 * @param type_imps   rendered RECT stacks: [0] - full-rate (every filter step),
	 *                    [1..] - recurrent-rate (every `period`-th step)
	 * @param type_titles one per type, e.g. {"CONV5D","NEW","PRESH","RECUR"}
	 * @param ts_full     all (filter-rate) timestamps
	 * @param ts_recur    recurrent-rate timestamps
	 * @param period      filter steps per recurrent step
	 * @param full        true - a slice per filter-rate timestamp, all-NaN filler
	 *                    where recurrent data is absent (3 of 4 now); false -
	 *                    decimated to recurrent-rate timestamps (filter subsampled)
	 * @param title       result title
	 * @return hyperstack ImagePlus (not shown), or null on pixel size mismatch
	 */
	private static ImagePlus combineRect5d( // By Claude on 06/11/2026
			ImagePlus [] type_imps,
			String []    type_titles,
			String []    ts_full,
			String []    ts_recur,
			int          period,
			boolean      full,
			String       title) {
		int width =  type_imps[0].getWidth();
		int height = type_imps[0].getHeight();
		for (ImagePlus imp : type_imps) {
			if ((imp == null) || (imp.getWidth() != width) || (imp.getHeight() != height)) {
				System.out.println("combineRect5d(): missing image or pixel size mismatch - skipping combined output");
				return null;
			}
		}
		int num_types =  type_imps.length;
		int num_slices = full ? ts_full.length : ts_recur.length;
		float [] nan_slice = new float [width * height]; // shared by all empty slices (display/save only)
		java.util.Arrays.fill(nan_slice, Float.NaN);
		float [][][] combo = new float [num_types][num_slices][];
		for (int n = 0; n < num_slices; n++) {
			int n_full =  full ? n : (n * period);  // index into the full-rate stack
			int n_recur = full ? (n / period) : n;  // index into the recurrent-rate stacks
			boolean has_recur = !full || (((n % period) == 0) && (n_recur < ts_recur.length));
			combo[0][n] = (n_full < type_imps[0].getStackSize()) ?
					(float []) type_imps[0].getStack().getPixels(n_full + 1) : nan_slice;
			for (int t = 1; t < num_types; t++) {
				combo[t][n] = (has_recur && (n_recur < type_imps[t].getStackSize())) ?
						(float []) type_imps[t].getStack().getPixels(n_recur + 1) : nan_slice;
			}
		}
		return ShowDoubleFloatArrays.showArraysHyperstack(
				combo,                          // float[][][] pixels [type][timestamp][pix]
				width,                          // int          width
				title,                          // String       title
				full ? ts_full : ts_recur,      // String []    titles (slice = timestamp)
				type_titles,                    // String []    frame_titles (view type)
				false);                         // boolean      show
	}

	/**
	 * Run the Layer 2 recurrent accumulation buffer over one pyramid level's filtered // By Claude on 06/11/2026
	 * fine-velocity ROI sequence and save the debug imagery. The pyramid is temporal
	 * (same pixel size at all levels), so the ROI and the buffer geometry apply
	 * unchanged - only the time step is 2^level base units, making deeper levels
	 * sensitive to proportionally slower (far/weak) targets.
	 * Always saves: -VMAX (per-pixel {Vx,Vy,W} fractional argmax hyperstack),
	 * -CMB-FULL-RECT and -CMB-DEC-RECT (combined {CONV5D,NEW,PRESH,RECUR} views).
	 * With save_individual (level 0 legacy set) also: -RECUR-RECT,
	 * -RECUR-HYPER-RECT, -NEW-RECT, -PRESH-RECT.
	 * All curt_recur_* / velocity-geometry parameters are read from clt_parameters.
	 * @param cuasRTUtils     renderers (showConvKernel5d etc.)
	 * @param conv5d_roi      [n5d][roi_npix][nsub][nvel] - filtered convolve3d ROI output of this level
	 * @param ts_5d           timestamps of this level (filter rate)
	 * @param roi             ROI rectangle (curt_save_select)
	 * @param title_base      title prefix for this level (level suffix already included)
	 * @param save_individual also save the separate per-stage files
	 */
	private void runRecurrentLevel( // By Claude on 06/11/2026
			CuasRTUtils     cuasRTUtils,
			double [][][][] conv5d_roi,
			String []       ts_5d,
			Rectangle       roi,
			String          title_base,
			boolean         save_individual) {
		int    period =       clt_parameters.imp.curt_recur_period;
		double w =            clt_parameters.imp.curt_recur_w;
		double leak =         clt_parameters.imp.curt_recur_leak;
		double wthresh =      clt_parameters.imp.curt_recur_wthresh;
		double spread =       clt_parameters.imp.curt_recur_spread;
		int    max_clust =    clt_parameters.imp.curt_recur_max_clust;
		double fit_rat =      clt_parameters.imp.curt_recur_fit_rat;
		double rs_frac =      clt_parameters.imp.curt_recur_rs_frac;
		double rs_min =       clt_parameters.imp.curt_recur_rs_min;
		int    pix_decimate = clt_parameters.imp.curt_pix_decimate;
		int    vel_radius =   clt_parameters.imp.curt_vel_radius;
		int    vel_decimate = clt_parameters.imp.curt_vel_decimate;
		double dbg_min =      clt_parameters.imp.curt_vel_dbgmin;
		int num_5d = conv5d_roi.length;
		CuasRecurrentBuffer recurrentBuffer = new CuasRecurrentBuffer(
				roi.width,                   // int roi_width,
				roi.height,                  // int roi_height,
				pix_decimate * pix_decimate, // int num_sub,
				vel_radius,                  // int velocity_radius,
				vel_decimate,                // int velocity_decimate,
				period,                      // int period,
				spread,                      // double spread,
				max_clust,                   // int max_clust,
				fit_rat,                     // double fit_rat,
				rs_frac,                     // double rs_frac,
				rs_min);                     // double rs_min)
		recurrentBuffer.setSaveStages(true);
		int max_recur = num_5d / period + 1;
		double [][][][] recur_states = new double[max_recur][][][];
		double [][][][] recur_cond =   new double[max_recur][][][]; // conditioned measurements (new data to combine)
		double [][][][] recur_pre =    new double[max_recur][][][]; // merged states before re-sharpening
		String []       recur_ts =     new String[max_recur];
		int num_recur = 0;
		for (int n5d = 0; n5d < num_5d; n5d++) {
			if (((n5d % period) != 0) || (conv5d_roi[n5d] == null)) continue;
			recurrentBuffer.update(
					conv5d_roi[n5d], // double [][][] measurement,
					w,               // double        w,
					leak,            // double        leak,
					wthresh);        // double        wthresh)
			recur_states[num_recur] = recurrentBuffer.getBufferCopy();
			recur_cond[num_recur] =   recurrentBuffer.getLastConditioned();
			recur_pre[num_recur] =    recurrentBuffer.getPreResharpen();
			recur_ts[num_recur++] =   ts_5d[n5d];
		}
		if (num_recur == 0) {
			return;
		}
		double [][][][] recur_show =     new double[num_recur][][][];
		double [][][][] recur_cond_show = new double[num_recur][][][];
		double [][][][] recur_pre_show =  new double[num_recur][][][];
		String []       recur_ts_show =  new String[num_recur];
		System.arraycopy(recur_states, 0, recur_show,      0, num_recur);
		System.arraycopy(recur_cond,   0, recur_cond_show, 0, num_recur);
		System.arraycopy(recur_pre,    0, recur_pre_show,  0, num_recur);
		System.arraycopy(recur_ts,     0, recur_ts_show,   0, num_recur);
		String title_recur = title_base+
				"-RP"+period+"-RW"+d2s(w)+"-RL"+d2s(leak)+
				"-RWT"+d2s(wthresh)+"-RS"+d2s(spread)+
				"-RC"+max_clust+"-RF"+d2s(fit_rat)+
				"-RR"+d2s(rs_frac)+"-RM"+d2s(rs_min)+SUFFIX_RECUR+"-D"+dbg_min;
		ImagePlus imp_recur_rect = cuasRTUtils.showConvKernel5d(
				recur_show,
				roi,
				recur_ts_show,
				title_recur+"-RECT");
		ImagePlus imp_recur_new = cuasRTUtils.showConvKernel5d(
				recur_cond_show,
				roi,
				recur_ts_show,
				title_recur+"-NEW-RECT");
		ImagePlus imp_recur_presh = cuasRTUtils.showConvKernel5d(
				recur_pre_show,
				roi,
				recur_ts_show,
				title_recur+"-PRESH-RECT");
		if (save_individual) {
			QuadCLTCPU.saveImagePlusInDirectory(imp_recur_rect,  getModelDirectory());
			QuadCLTCPU.saveImagePlusInDirectory(imp_recur_new,   getModelDirectory());
			QuadCLTCPU.saveImagePlusInDirectory(imp_recur_presh, getModelDirectory());
			ImagePlus imp_recur_hyper = cuasRTUtils.showConvKernel5dHyperRect(
					recur_show,
					roi,
					recur_ts_show,
					title_recur+"-HYPER-RECT");
			QuadCLTCPU.saveImagePlusInDirectory(imp_recur_hyper, getModelDirectory());
		}
		// low-resolution summary: per ROI pixel {Vx, Vy, W} - integer argmax over the
		// velocity plane (best sub), fractional-pixel argmax by cosine-windowed local
		// centroid (Correlation2d.getMaxXYCm), W = plane maximum
		final double vmax_cm_radius = 2.0; // cosine window radius, covers the +/-1.5 bump support
		final int    vmax_cm_passes = 1;   // window re-centering passes
		final double vmax_thresh =    1.0; // min plane max to report Vx,Vy (below - NaN); W is always kept,
		                                   // so ImageJ histogram/statistics (NaN-blind) of Vx,Vy show only
		                                   // the real cluster, and the W slice guides threshold choice
		final int vel_dim_recur = 2 * vel_radius + 1;
		final int roi_npix = roi.width * roi.height;
		double [][][] recur_vmax = new double [3][num_recur][roi_npix];
		for (int n = 0; n < num_recur; n++) {
			for (int p = 0; p < roi_npix; p++) {
				double [][] subs = recur_show[n][p];
				int best_s = -1;
				double mx = 0;
				for (int s = 0; s < subs.length; s++) {
					for (int v = 0; v < subs[s].length; v++) {
						if (subs[s][v] > mx) {
							mx = subs[s][v];
							best_s = s;
						}
					}
				}
				double [] xy = ((best_s >= 0) && (mx >= vmax_thresh)) ? com.elphel.imagej.tileprocessor.Correlation2d.getMaxXYCm(
						subs[best_s],    // double []  data (not modified: fpn_mask == null)
						vel_dim_recur,   // int        data_width
						vmax_cm_radius,  // double     radius
						vmax_cm_passes,  // int        refine (window re-centering)
						null,            // boolean [] fpn_mask
						false,           // boolean    ignore_border
						false) : null;   // boolean    debug
				if (xy != null) {
					recur_vmax[0][n][p] = xy[0];
					recur_vmax[1][n][p] = xy[1];
				} else {
					recur_vmax[0][n][p] = Double.NaN;
					recur_vmax[1][n][p] = Double.NaN;
				}
				recur_vmax[2][n][p] = mx; // W always kept (0 for empty) - guides vmax_thresh choice
			}
		}
		ImagePlus imp_recur_vmax = ShowDoubleFloatArrays.showArraysHyperstack(
				recur_vmax,                     // double[][][] pixels [{Vx,Vy,W}][scene][pix]
				roi.width,                      // int          width
				title_recur+"-VMAX",            // String       title
				recur_ts_show,                  // String []    titles (scene timestamps)
				new String[] {"Vx","Vy","W"},   // String []    frame_titles
				false);                         // boolean      show
		QuadCLTCPU.saveImagePlusInDirectory(imp_recur_vmax, getModelDirectory());
		// combined RECT hyperstacks: bottom slider - view type, inner slider - timestamp
		ImagePlus imp_filter_rect = cuasRTUtils.showConvKernel5d(
				conv5d_roi,
				roi,
				ts_5d,
				title_base+"-RECT");
		ImagePlus [] cmb_imps =   {imp_filter_rect, imp_recur_new, imp_recur_presh, imp_recur_rect};
		String []    cmb_titles = {"CONV5D", "NEW", "PRESH", "RECUR"};
		ImagePlus imp_cmb_full = combineRect5d(
				cmb_imps, cmb_titles, ts_5d, recur_ts_show,
				period, true, title_recur+"-CMB-FULL-RECT");
		if (imp_cmb_full != null) {
			QuadCLTCPU.saveImagePlusInDirectory(imp_cmb_full, getModelDirectory());
		}
		ImagePlus imp_cmb_dec = combineRect5d(
				cmb_imps, cmb_titles, ts_5d, recur_ts_show,
				period, false, title_recur+"-CMB-DEC-RECT");
		if (imp_cmb_dec != null) {
			QuadCLTCPU.saveImagePlusInDirectory(imp_cmb_dec, getModelDirectory());
		}
	}

	public CuasMotion detectTargets(
			CLTParameters     clt_parameters,
			boolean           batch_mode,
			int               debugLevel) {
		boolean  save_linear_cuas =  true;
		int      curt_pyramid =      clt_parameters.imp.curt_pyramid;
		double   curt_psf_radius =   clt_parameters.imp.curt_psf_radius;
		double   curt_n_sigma =      clt_parameters.imp.curt_n_sigma;
		double   curt_rleak0 =       clt_parameters.imp.curt_rleak0;
		int      curt_pix_decimate = clt_parameters.imp.curt_pix_decimate;
		int      curt_vel_decimate = clt_parameters.imp.curt_vel_decimate;
		int      curt_vel_radius =   clt_parameters.imp.curt_vel_radius;
		boolean  curt_vel_thresh_en =clt_parameters.imp.curt_vel_thresh_en;
		int      curt_vel_suppr_rad =clt_parameters.imp.curt_vel_suppr_rad;
		double   curt_vel_athresh =  clt_parameters.imp.curt_vel_athresh;
		double   curt_vel_rthresh =  clt_parameters.imp.curt_vel_rthresh;
		double   curt_vel_dbg_min =  clt_parameters.imp.curt_vel_dbgmin; // temporary, testing with strong targets, make clean background
		boolean  curt_recur_en =     clt_parameters.imp.curt_recur_en;      // By Claude on 06/09/2026
		int      curt_recur_period = clt_parameters.imp.curt_recur_period;  // By Claude on 06/09/2026
		double   curt_recur_w =      clt_parameters.imp.curt_recur_w;       // By Claude on 06/09/2026
		double   curt_recur_leak =   clt_parameters.imp.curt_recur_leak;    // By Claude on 06/09/2026
		double   curt_recur_wthresh =clt_parameters.imp.curt_recur_wthresh; // By Claude on 06/09/2026
		double   curt_recur_spread = clt_parameters.imp.curt_recur_spread;  // By Claude on 06/09/2026
		int      curt_recur_max_clust = clt_parameters.imp.curt_recur_max_clust; // By Claude on 06/09/2026
		double   curt_recur_fit_rat = clt_parameters.imp.curt_recur_fit_rat; // By Claude on 06/10/2026
		double   curt_recur_rs_frac = clt_parameters.imp.curt_recur_rs_frac; // By Claude on 06/10/2026
		double   curt_recur_rs_min =  clt_parameters.imp.curt_recur_rs_min;  // By Claude on 06/10/2026

		double[] curt_temp_weights = clt_parameters.imp.curt_temp_weights.clone();
      	
		boolean  curt_save_c5full =  clt_parameters.imp.curt_save_c5full;
		boolean  curt_save_c5rect =  clt_parameters.imp.curt_save_c5rect;
		Rectangle curt_save_select =  clt_parameters.imp.curt_save_select;
		if (clt_parameters.imp.curt_synth_src) { // synthetic targets live in their own block - keep the stored ROI matching the real image // By Claude on 06/11/2026
			curt_save_select = new Rectangle(SYNTH_ROI);
			System.out.println("Synthetic input: overriding ROI with SYNTH_ROI = "+curt_save_select);
		}
        final double      min_frac_last = clt_parameters.imp.curt_min_frac; // By Claude on 06/12/2026
        if (clt_parameters.imp.curt_synth_src && (min_frac_last > 0)) { // By Claude on 06/12/2026
        	System.out.println("WARNING: synthetic (B-measurement) run with curt_min_frac = "+min_frac_last+
        			" - this NONLINEAR gate punches zeros into the strongest ridge cells; set 0 for a linear measurement");
        }
		
// Initialize LoG and other kernel parameters, calculate LoG first, show results
       	int      kernel3d3_rad = 1;
       	
       	CuasRTUtils cuasRTUtils = new CuasRTUtils (
    			curt_psf_radius,          // double   psf_radius,
				curt_n_sigma,             // double   n_sigma,
    			kernel3d3_rad,            // int      kernel3d3_rad,
    			curt_temp_weights,        // double[] temporal_weights,
                curt_pix_decimate,        // int      pixel_decimate,
                curt_vel_decimate,        // int      velocity_decimate,
                curt_vel_radius,          // int      velocity_radius,
                curt_vel_suppr_rad,       // int      vel_suppr_rad,
    			getWidth (),              // int      width,
    			getHeight(),              // int      height,
    			clt_parameters.imp.curt_c5_matched); // boolean c5_matched (matched-filter vs trapezoid direct kernel) // By Claude on 06/12/2026
       	// For testing, create whole arrays?
       	boolean save_LoG =        debugLevel> -4;
       	boolean save_5d =          debugLevel> -4;
       	boolean save_LoG_pixels = debugLevel> -4;
       	boolean save_3d3_pixels = debugLevel> -4;
   		int num_hist_5d = cuasRTUtils.getNumHist();
       	
       	if (save_LoG) {
       		String title_log = getBaseName()+SUFFIX_LOG+"-PSF"+d2s(cuasRTUtils.getPsfRadius())+"-KR"+d2s(cuasRTUtils.getKernel2dRadius());
       		ImagePlus imp_log = cuasRTUtils.showKernel2d(title_log);
       		QuadCLTCPU.saveImagePlusInDirectory(imp_log,getModelDirectory());
       	}
       	if (save_5d) {
       	    String title_5d = getBaseName()+SUFFIX_CONV5D+
       	            "-NH"+num_hist_5d+
       	            "-VR"+curt_vel_radius+"-VD"+curt_vel_decimate;
       		ImagePlus imp_5d = cuasRTUtils.showKernel5d(title_5d);
       		QuadCLTCPU.saveImagePlusInDirectory(imp_5d,getModelDirectory());
       	}
       	double [][] dpixels = getDPpixels();
       	double [][] dpixels_log = new double [dpixels.length][dpixels[0].length];
    	double []   kernel2d = cuasRTUtils.getKernel2d();
    	double alpha0 = 1.0 - clt_parameters.imp.curt_rleak0;
    	double alpha_pyr = 1.0 - clt_parameters.imp.curt_rleak_pyr;
    	int    curt_3d3_mode = clt_parameters.imp.curt_3d3_mode;
    	double curt_3d3_frac = clt_parameters.imp.curt_3d3_frac;
    	double curt_3d3_thrsh = clt_parameters.imp.curt_3d3_thrsh;
    	double curt_3d3_power = clt_parameters.imp.curt_3d3_power;
       	double w3d3_now =  clt_parameters.imp.curt_3d3_wnow;
       	double w3d3_prev = clt_parameters.imp.curt_3d3_wprev;
    	double alpha1 = 1.0 - clt_parameters.imp.curt_rleak1;

    	System.out.println(now()+" detectTargets(): convolve2DLReLU (LoG)"); // By Claude on 06/11/2026
    	for (int nseq = 0; nseq < dpixels_log.length; nseq++) {
       		cuasRTUtils.convolve2DLReLU(
       				dpixels[nseq],     // final double [] data,
       				kernel2d,          // final double [] kernel, // square
       				dpixels_log[nseq], // double []       result_in,
       				alpha0);            // final double    alpha)
       	}
       	if (save_LoG_pixels) {
       		String title_conv2d = getBaseName()+SUFFIX_CONV2D+"-PSF"+d2s(cuasRTUtils.getPsfRadius())+
       				"-KR"+d2s(cuasRTUtils.getKernel2dRadius())+"-ALPHA0_"+d2s(alpha0);
       		
			ImagePlus imp_conv2d = ShowDoubleFloatArrays.makeArrays(
					dpixels_log, // float[][] pixels,
					getWidth(),  // int width,
					getHeight(), // int height,
					title_conv2d,					
					getTimeStamps()); //String [] titles)
			imp_conv2d.setDisplayRange(CuasRTUtils.IMP_MINMAX[0],CuasRTUtils.IMP_MINMAX[1]);

       		QuadCLTCPU.saveImagePlusInDirectory(imp_conv2d, getModelDirectory());
       	}

       	int    dim3d3 = (2*cuasRTUtils.get3d3Radius()+1);
//       	int    size3x3 = dim3d3 * dim3d3;
       	double [][][] dpixels_3d3 = new double [dpixels.length-1][width*height][dim3d3 * dim3d3];
  		String title_conv3d3 = getBaseName()+SUFFIX_CONV3D3+"-PSF"+d2s(cuasRTUtils.getPsfRadius())+
   				"-KR"+d2s(cuasRTUtils.getKernel2dRadius())+"-ALPHA0_"+d2s(alpha0)+
   				"-ALPHAPYR_"+d2s(alpha_pyr)+"-M"+curt_3d3_mode+"-F"+d2s(curt_3d3_frac)+"-T"+d2s(curt_3d3_thrsh)+"-P"+d2s(curt_3d3_power)+
   				"-N"+d2s(w3d3_now)+":"+d2s(w3d3_prev)+
   				"-ALPHA1_"+d2s(alpha1);
   		// getTimeStamps(int from_indx)
   		int dbg_seq = 480;
       	System.out.println(now()+" detectTargets(): convolve3D3LReLU"); // By Claude on 06/11/2026
       	for (int nseq = 1; nseq < dpixels_log.length; nseq++) {
       		int dbg_lev = (nseq == dbg_seq) ? 1 : -10;
       		cuasRTUtils.convolve3D3LReLU(
   				dpixels_log[nseq],   // final double [] data,
   				dpixels_log[nseq-1], // final double [] data_prev,
   				dpixels_3d3[nseq-1], // double [][]     result_in,
   				curt_3d3_mode,       // final int       mode_3d3,
   				curt_3d3_frac,       // final double   curt_3d3_frac, //  =        0.5;   // for stronger signals give this fraction of the total (of 9) weight to the 1-of-9 winner
   				curt_3d3_thrsh,      //final double   curt_3d3_thrsh, //  =       1.0;   // scale down curt_3d3_frac for weaker signals (in ratio of min(data[now]/curt_3d3_thrsh,1.0)
   				curt_3d3_power,      //final double    curt_3d3_power, //  = 2.0; // in mode_3d3=2, redistribute coarse velocities weights as the specified power of their positive values (ignore negative ones)
   				w3d3_now,            // final double    w_now, // normally 0.5, so center will propagate to the next pyramid levels
   				w3d3_prev,           // final double    w_prev,// normally 0.5, so center will propagate to the next pyramid levels
   				alpha1,              // final double    alpha) {
   				dbg_lev); // debugLevel);         // final int       debugLevel)
   		}
   		if (save_3d3_pixels) {
   			ImagePlus imp_3d3 = cuasRTUtils.render3d3Hyperstack(
   					dpixels_3d3,      // double [][][] coarse_vel_seq, // [nser][npix][nvel]
   					getTimeStamps(1), // String []     scene_names,
   					title_conv3d3);   // String        title)
       		QuadCLTCPU.saveImagePlusInDirectory(imp_3d3, getModelDirectory());
   		}
   		
       	// building pyramid
   		int pyramid_levels = 	clt_parameters.imp.curt_pyramid;
   		if (clt_parameters.imp.curt_synth_src) { // B-measurement on synthetic needs only level 0 - bounds memory and runtime (lift this when synthetic LEV2+ tests are wanted) // By Claude on 06/11/2026
   			pyramid_levels = 1;
   			System.out.println("Synthetic input: limiting processing to pyramid level 0");
   		}
   		
   		double [][][]   dpixels_pyramid =     new double [pyramid_levels][][];
   		String [][]     ts_pyramid =          new String [pyramid_levels][];
   		double [][][][] dpixels_3d3_pyramid = new double [pyramid_levels][][][];
   		double [][][][][] dpixels_5d_pyramid =     new double [pyramid_levels][][][][];
   		double [][][][][] dpixels_5d_roi_pyramid = new double [pyramid_levels][][][][];
   		ts_pyramid[0] =          getTimeStamps(1);
   		dpixels_3d3_pyramid[0] = dpixels_3d3;
   		dpixels_pyramid[0] = new double[dpixels_log.length-1][width*height];
		for (int nseq = 1; nseq < dpixels_log.length; nseq++) {
			cuasRTUtils.temporalAverageLReLU(
					dpixels_log[nseq],          // final double [] data_now,
					dpixels_log[nseq-1],        // final double [] data_prev,
					dpixels_pyramid[0][nseq-1], // double []       result_in,
					alpha_pyr);                 // final double    alpha)
		}
//   		System.arraycopy(dpixels_log, 1, dpixels_pyramid[0], 0, dpixels_pyramid[0].length);
//   		int center9 = 4;
   		boolean save_5d_pixels = curt_save_c5full; // debugLevel > -4;
   		String filt_suffix = curt_vel_thresh_en? ("-TE1" + "-TA"+d2s(curt_vel_athresh) + "-TR"+d2s(curt_vel_rthresh)+"-TD"+d2s(curt_vel_dbg_min)):
   			"-TE0";
   	    String title_conv5d = getBaseName()+SUFFIX_CONV5D+"-PSF"+d2s(cuasRTUtils.getPsfRadius())+
   	            "-KR"+d2s(cuasRTUtils.getKernel2dRadius())+"-A0_"+d2s(alpha0)+
   	            "-AP_"+d2s(alpha_pyr)+"-M"+curt_3d3_mode+"-F"+d2s(curt_3d3_frac)+"-T"+d2s(curt_3d3_thrsh)+"-P"+d2s(curt_3d3_power)+
   	            "-N"+d2s(w3d3_now)+":"+d2s(w3d3_prev)+
   	            "-A1_"+d2s(alpha1)+   				
   	            "-NH"+num_hist_5d+
   	            "-VR"+curt_vel_radius+"-VD"+curt_vel_decimate+"-MF"+min_frac_last+
   	            filt_suffix +
   	            "-SR"+curt_vel_suppr_rad+"-D"+curt_vel_dbg_min;
   		// Level-0 5D convolution (fine velocity from coarse-velocity history)
   		int num_5d_lev0 = Math.max(0, dpixels_3d3.length - num_hist_5d + 1);
   		dpixels_5d_pyramid[0] =     new double[num_5d_lev0][][][];
   		dpixels_5d_roi_pyramid[0] = new double[num_5d_lev0][][][];
   		String[] ts_5d_lev0 = new String[num_5d_lev0];
   		// Layer 2 recurrent accumulation buffer (ROI only, every pyramid level).            // By Claude on 06/11/2026
   		// convolve3d() still runs at every level time step; runRecurrentLevel() consumes    // By Claude on 06/11/2026
   		// every curt_recur_period-th output after each level's sequence is complete.        // By Claude on 06/11/2026
   		boolean run_recur = curt_recur_en && (curt_save_select != null);                      // By Claude on 06/09/2026
   		System.out.println(now()+" detectTargets(): will run convolve3d()"); // By Claude on 06/11/2026
   		for (int n5d = 0; n5d < num_5d_lev0; n5d++) {
   			double tmp_min=curt_vel_dbg_min;
/*   			
   			if ((n5d == 0) || (n5d == 8) || (n5d == 12) || (n5d == 16)) { // disabling outputs for certain timestamps for debugging
   				tmp_min = 1000;
   			}
*/   			
   			System.out.print ("n5d="+n5d+":"+tmp_min+" ");
   			double[][][] window = new double[num_hist_5d][][];
   			for (int h = 0; h < num_hist_5d; h++) {
   				window[h] = dpixels_3d3[n5d + num_hist_5d - 1 - h];
   			}
   			if (curt_save_c5full) {
   				dpixels_5d_pyramid[0][n5d] = cuasRTUtils.convolve3d(
   						window,
   						null,
   						min_frac_last); // final double      min_frac_last){

   			}
   			if ((curt_save_c5rect || run_recur) && (curt_save_select != null)) { // By Claude on 06/09/2026 (added run_recur)
   				dpixels_5d_roi_pyramid[0][n5d] = cuasRTUtils.convolve3d(
   						window,
   						curt_save_select,
   						null,
   						min_frac_last); // final double        min_frac_last)
   				if (curt_vel_thresh_en) {
   					dpixels_5d_roi_pyramid[0][n5d] = cuasRTUtils.filterConv5dROI(
   							dpixels_5d_roi_pyramid[0][n5d],
   							curt_save_select,
   							curt_vel_athresh,
   							curt_vel_rthresh,
   							tmp_min); // curt_vel_dbg_min); // final double        dbg_min) { // zero if below, for testing with strong signals
   				}
   			}
   			ts_5d_lev0[n5d] = ts_pyramid[0][n5d + num_hist_5d - 1];
   		}
   		if (save_5d_pixels && num_5d_lev0 > 0) {
   			ImagePlus imp_5d = cuasRTUtils.showConvKernel5d(
   					dpixels_5d_pyramid[0],
   					ts_5d_lev0,
   					title_conv5d);
   			QuadCLTCPU.saveImagePlusInDirectory(imp_5d, getModelDirectory());
   		}
   		if (curt_save_c5rect && num_5d_lev0 > 0) {
   			ImagePlus imp_5d_rect = cuasRTUtils.showConvKernel5d(
   					dpixels_5d_roi_pyramid[0],
   					curt_save_select,
   					ts_5d_lev0,
   					title_conv5d+"-RECT");
   			QuadCLTCPU.saveImagePlusInDirectory(imp_5d_rect, getModelDirectory());
   			ImagePlus imp_5d_hyper = cuasRTUtils.showConvKernel5dHyperRect(
   					dpixels_5d_roi_pyramid[0],
   					curt_save_select,
   					ts_5d_lev0,
   					title_conv5d+"-HYPER-RECT");
   			QuadCLTCPU.saveImagePlusInDirectory(imp_5d_hyper, getModelDirectory());
   		}
   		if (run_recur && (num_5d_lev0 > 0)) {                                                 // By Claude on 06/11/2026
   			System.out.println(now()+" detectTargets(): runRecurrentLevel LEV0"); // By Claude on 06/11/2026
   			runRecurrentLevel(                                                                // By Claude on 06/11/2026
   					cuasRTUtils,                // CuasRTUtils     cuasRTUtils,              // By Claude on 06/11/2026
   					dpixels_5d_roi_pyramid[0],  // double [][][][] conv5d_roi,               // By Claude on 06/11/2026
   					ts_5d_lev0,                 // String []       ts_5d,                    // By Claude on 06/11/2026
   					curt_save_select,           // Rectangle       roi,                      // By Claude on 06/11/2026
   					title_conv5d,               // String          title_base,               // By Claude on 06/11/2026
   					true);                      // boolean         save_individual           // By Claude on 06/11/2026
   		}                                                                                     // By Claude on 06/11/2026
   		for (int nlev = 0; nlev < pyramid_levels; nlev++) {
   	       	if (save_LoG_pixels) {
   	       		String title_conv2d_lev = getBaseName()+SUFFIX_CONV2D+"-PSF"+d2s(cuasRTUtils.getPsfRadius())+
   	       				"-KR"+d2s(cuasRTUtils.getKernel2dRadius())+"-ALPHA0_"+d2s(alpha0)+"-ALPHAPYR_"+d2s(alpha_pyr)+"-LEV"+nlev;
   				ImagePlus imp_conv2d = ShowDoubleFloatArrays.makeArrays(
   						dpixels_pyramid[nlev], // float[][] pixels,
   						getWidth(),  // int width,
   						getHeight(), // int height,
   						title_conv2d_lev,					
   						ts_pyramid[nlev]); //String [] titles)
   				imp_conv2d.setDisplayRange(CuasRTUtils.IMP_MINMAX[0],CuasRTUtils.IMP_MINMAX[1]);
   	       		QuadCLTCPU.saveImagePlusInDirectory(imp_conv2d, getModelDirectory());
   	       	}
   			
   			// run next pyramid level (use half of the temporal slices)
   			if (nlev < (pyramid_levels -1)) {
   				System.out.println(now()+" detectTargets(): building pyramid level "+(nlev+1)); // By Claude on 06/11/2026
   				int num_lev_scenes = (dpixels_pyramid[nlev].length)/2 -1;
   				dpixels_pyramid[nlev+1] = new double[num_lev_scenes][width*height];
   				
   				dpixels_3d3_pyramid[nlev+1] = new double [num_lev_scenes][width*height][dim3d3 * dim3d3];
   				ts_pyramid[nlev+1] = new String[num_lev_scenes];
   				for (int nseq = 0; nseq < num_lev_scenes; nseq++) {
   					ts_pyramid[nlev+1][nseq] = ts_pyramid[nlev][2*(nseq + 1)];
   					cuasRTUtils.temporalAverageLReLU(
   							dpixels_pyramid[nlev][2*nseq+2],          // final double [] data_now,
   							dpixels_pyramid[nlev][2*nseq],        // final double [] data_prev,
   							dpixels_pyramid[nlev+1][nseq], // double []       result_in,
   							alpha_pyr);                 // final double    alpha)
   					
   					cuasRTUtils.convolve3D3LReLU(
   							dpixels_pyramid[nlev][2*nseq+2],   // final double [] data,
   							dpixels_pyramid[nlev][2*nseq],     // final double [] data_prev,
   							dpixels_3d3_pyramid[nlev+1][nseq], // double [][]     result_in,
   			   				curt_3d3_mode,                     // final int       mode_3d3,
   			   				curt_3d3_frac,                     // final double   curt_3d3_frac, //  =        0.5;   // for stronger signals give this fraction of the total (of 9) weight to the 1-of-9 winner
   			   				curt_3d3_thrsh,                    //final double   curt_3d3_thrsh, //  =       1.0;   // scale down curt_3d3_frac for weaker signals (in ratio of min(data[now]/curt_3d3_thrsh,1.0)
   			   				curt_3d3_power,                    //final double    curt_3d3_power, //  = 2.0; // in mode_3d3=2, redistribute coarse velocities weights as the specified power of their positive values (ignore negative ones)
   							w3d3_now,                          // final double    w_now, // normally 0.5, so center will propagate to the next pyramid levels
   							w3d3_prev,                         // final double    w_prev,// normally 0.5, so center will propagate to the next pyramid levels
   			   				alpha1,                            // final double    alpha) {
   			   				debugLevel);                       // final int       debugLevel)
   				}
   		   		if (save_3d3_pixels) {
   		   			ImagePlus imp_3d3 = cuasRTUtils.render3d3Hyperstack(
   		   					dpixels_3d3_pyramid[nlev+1],      // double [][][] coarse_vel_seq, // [nser][npix][nvel]
   		   					ts_pyramid[nlev+1], // String []     scene_names,
   		   					title_conv3d3+"-LEV"+(nlev+1));   // String        title)
   		   			QuadCLTCPU.saveImagePlusInDirectory(imp_3d3, getModelDirectory());
   		   		}
   		   		// 5D convolution for pyramid level nlev+1
   		   		int num_5d_lev = Math.max(0, dpixels_3d3_pyramid[nlev+1].length - num_hist_5d + 1);
   		   		dpixels_5d_pyramid[nlev+1] =     new double[num_5d_lev][][][];
   		   		dpixels_5d_roi_pyramid[nlev+1] = new double[num_5d_lev][][][];
   		   		String[] ts_5d_lev = new String[num_5d_lev];
   		   		for (int n5d = 0; n5d < num_5d_lev; n5d++) {
   		   			double[][][] window = new double[num_hist_5d][][];
   		   			for (int h = 0; h < num_hist_5d; h++) {
   		   				window[h] = dpixels_3d3_pyramid[nlev+1][n5d + num_hist_5d - 1 - h];
   		   			}
   		   			if (curt_save_c5full) {
   		   				dpixels_5d_pyramid[nlev+1][n5d] = cuasRTUtils.convolve3d(
   		   						window,
   		   						null,
   		   						min_frac_last); // final double      min_frac_last){
   		   			}
   		   			if ((curt_save_c5rect || run_recur) && (curt_save_select != null)) { // By Claude on 06/11/2026 (added run_recur)
   		   				dpixels_5d_roi_pyramid[nlev+1][n5d] = cuasRTUtils.convolve3d(
   		   						window,
   		   						curt_save_select,
   		   						null,
   		   						min_frac_last); // final double        min_frac_last)
   		   				if (curt_vel_thresh_en) {
   		   					dpixels_5d_roi_pyramid[nlev+1][n5d] = cuasRTUtils.filterConv5dROI(
   		   							dpixels_5d_roi_pyramid[nlev+1][n5d],
   		   							curt_save_select,
   		   							curt_vel_athresh,
   		   							curt_vel_rthresh,
   		   							curt_vel_dbg_min); // final double        dbg_min) { // zero if below, for testing with strong signals
   		   				}
   		   			}
   		   			ts_5d_lev[n5d] = ts_pyramid[nlev+1][n5d + num_hist_5d - 1];
   		   		}
   		   		if (save_5d_pixels && num_5d_lev > 0) {
   		   			ImagePlus imp_5d = cuasRTUtils.showConvKernel5d(
   		   					dpixels_5d_pyramid[nlev+1],
   		   					ts_5d_lev,
   		   					title_conv5d+"-LEV"+(nlev+1));
   		   			QuadCLTCPU.saveImagePlusInDirectory(imp_5d, getModelDirectory());
   		   		}
   		   		if (curt_save_c5rect && num_5d_lev > 0) {
   		   			ImagePlus imp_5d_rect = cuasRTUtils.showConvKernel5d(
   		   					dpixels_5d_roi_pyramid[nlev+1],
   		   					curt_save_select,
   		   					ts_5d_lev,
   		   					title_conv5d+"-LEV"+(nlev+1)+"-RECT");
   		   			QuadCLTCPU.saveImagePlusInDirectory(imp_5d_rect, getModelDirectory());
   		   			ImagePlus imp_5d_hyper = cuasRTUtils.showConvKernel5dHyperRect(
   		   					dpixels_5d_roi_pyramid[nlev+1],
   		   					curt_save_select,
   		   					ts_5d_lev,
   		   					title_conv5d+"-LEV"+(nlev+1)+"-HYPER-RECT");
   		   			QuadCLTCPU.saveImagePlusInDirectory(imp_5d_hyper, getModelDirectory());
   		   		}
   		   		if (run_recur && (num_5d_lev > 0)) {                                          // By Claude on 06/11/2026
   		   			runRecurrentLevel(                                                        // By Claude on 06/11/2026
   		   					cuasRTUtils,                     // CuasRTUtils     cuasRTUtils,   // By Claude on 06/11/2026
   		   					dpixels_5d_roi_pyramid[nlev+1],  // double [][][][] conv5d_roi,    // By Claude on 06/11/2026
   		   					ts_5d_lev,                       // String []       ts_5d,         // By Claude on 06/11/2026
   		   					curt_save_select,                // Rectangle       roi,           // By Claude on 06/11/2026
   		   					title_conv5d+"-LEV"+(nlev+1),    // String          title_base,    // By Claude on 06/11/2026
   		   					false);                          // boolean         save_individual // By Claude on 06/11/2026
   		   		}                                                                             // By Claude on 06/11/2026
   			}
   		}
   		// Direct pixel-domain fine-velocity path (-C5P): bypass the 3x3 coarse stage -    // By Claude on 06/11/2026
   		// a single-stage velocity-matched filter straight on the LoG+LReLU frames of     // By Claude on 06/11/2026
   		// each pyramid level (better at low SNR: conv3d3 adds ~2x background noise at    // By Claude on 06/11/2026
   		// equal contrast). Parallel to the 3d3-based path for comparison.                // By Claude on 06/11/2026
   		if (clt_parameters.imp.curt_c5_from_pix && (curt_save_select != null)) {           // By Claude on 06/11/2026
   			int num_hist_p = cuasRTUtils.getNumHistDirect();                               // By Claude on 06/11/2026
   			int [] c5_levels = clt_parameters.imp.curt_c5_levels;                          // empty/null = all levels // By Claude on 06/13/2026
   			boolean white_en = clt_parameters.imp.curt_c5_white;                           // linear sharpening before nonlinearities // By Claude on 06/13/2026
			double  white_sp = clt_parameters.imp.curt_c5_white_sp;                        // By Claude on 06/13/2026
			double  white_vel = clt_parameters.imp.curt_c5_white_vel;                      // By Claude on 06/13/2026
			boolean recur_soft = clt_parameters.imp.curt_c5_recur_soft;                    // By Claude on 06/13/2026
   			double  recur_lambda = clt_parameters.imp.curt_c5_recur_lambda;                // operative lambda for the recurrent feed // By Claude on 06/13/2026
   			double  recur_keep = clt_parameters.imp.curt_c5_recur_keep;                    // By Claude on 06/13/2026
   			double  op_null_score = 0.5 * clt_parameters.imp.curt_c5_null_z * clt_parameters.imp.curt_c5_null_z; // By Claude on 06/13/2026
   			double [] op_log_prior = cuasRTUtils.getVelLogPrior(                           // shared by gate + sweep // By Claude on 06/13/2026
   					clt_parameters.imp.curt_c5_prior_r0, clt_parameters.imp.curt_c5_prior_r1); // By Claude on 06/13/2026
   			for (int nlev = 0; nlev < pyramid_levels; nlev++) {                            // By Claude on 06/11/2026
   				boolean lev_sel = (c5_levels == null) || (c5_levels.length == 0);          // By Claude on 06/13/2026
   				if (!lev_sel) for (int l : c5_levels) if (l == nlev) { lev_sel = true; break; } // By Claude on 06/13/2026
   				if (!lev_sel) continue;                                                    // skip non-selected levels (speed) // By Claude on 06/13/2026
   				double [][] frames = dpixels_pyramid[nlev];                                // By Claude on 06/11/2026
   				if (frames == null) continue;                                              // By Claude on 06/11/2026
   				int num_5dp = Math.max(0, frames.length - num_hist_p + 1);                 // By Claude on 06/11/2026
   				if (num_5dp == 0) continue;                                                // By Claude on 06/11/2026
   				double [][][][] c5p_roi = new double[num_5dp][][][];                       // By Claude on 06/11/2026
   				double [][][][] c5p_raw = clt_parameters.imp.curt_c5_post ?               // unfiltered copy for the Bayes posterior sweep // By Claude on 06/12/2026
   						new double[num_5dp][][][] : null;                                  // By Claude on 06/12/2026
   				String [] ts_5dp = new String[num_5dp];                                    // By Claude on 06/11/2026
   				System.out.println(now()+" detectTargets(): convolve3dDirect() LEV"+nlev+ // By Claude on 06/11/2026
   						(recur_soft ? (", recurrent feed = posterior gate (lambda="+recur_lambda+", keep="+recur_keep+")") : "")); // By Claude on 06/13/2026
   				for (int n5d = 0; n5d < num_5dp; n5d++) {                                  // By Claude on 06/11/2026
   					double [][] window = new double[num_hist_p][];                         // By Claude on 06/11/2026
   					for (int h = 0; h < num_hist_p; h++) {                                 // By Claude on 06/11/2026
   						window[h] = frames[n5d + num_hist_p - 1 - h];                      // By Claude on 06/11/2026
   					}                                                                      // By Claude on 06/11/2026
   					c5p_roi[n5d] = cuasRTUtils.convolve3dDirect(                           // By Claude on 06/11/2026
   							window,           // final double [][] data, [0] - newest     // By Claude on 06/11/2026
   							curt_save_select, // final Rectangle   roi,                   // By Claude on 06/11/2026
   							null,             // double [][][]     result_in,             // By Claude on 06/11/2026
   							min_frac_last);   // final double      min_frac_last)         // By Claude on 06/11/2026
   					if (white_en) {                                                        // linear sharpening (still linear, before all nonlinearities) // By Claude on 06/13/2026
   						c5p_roi[n5d] = cuasRTUtils.whitenC5PROI(                           // By Claude on 06/13/2026
   								c5p_roi[n5d], curt_save_select, white_sp, white_vel);      // By Claude on 06/13/2026
   					}                                                                      // By Claude on 06/13/2026
   					if (c5p_raw != null) {                                                 // By Claude on 06/12/2026
   						c5p_raw[n5d] = c5p_roi[n5d]; // raw reference stays valid - the filter/gate returns a new array; here 'raw' = whitened (so posterior images show the sharpening) // By Claude on 06/13/2026
   					}                                                                      // By Claude on 06/12/2026
   					if (recur_soft) {                                                      // lambda-controlled SOFT gate (posterior) replaces hard NMS // By Claude on 06/13/2026
   						c5p_roi[n5d] = cuasRTUtils.posteriorGateC5PROI(                    // By Claude on 06/13/2026
   								c5p_roi[n5d], curt_save_select, recur_lambda,              // By Claude on 06/13/2026
   								op_log_prior, op_null_score, recur_keep,                   // By Claude on 06/13/2026
   								clt_parameters.imp.curt_c5_post_exact);                    // By Claude on 06/13/2026
   					} else if (curt_vel_thresh_en) {                                       // By Claude on 06/11/2026
   						c5p_roi[n5d] = cuasRTUtils.filterConv5dROI(                        // By Claude on 06/11/2026
   								c5p_roi[n5d],                                              // By Claude on 06/11/2026
   								curt_save_select,                                          // By Claude on 06/11/2026
   								curt_vel_athresh,                                          // By Claude on 06/11/2026
   								curt_vel_rthresh,                                          // By Claude on 06/11/2026
   								curt_vel_dbg_min);                                         // By Claude on 06/11/2026
   					}                                                                      // By Claude on 06/11/2026
   					ts_5dp[n5d] = ts_pyramid[nlev][n5d + num_hist_p - 1];                  // By Claude on 06/11/2026
   				}                                                                          // By Claude on 06/11/2026
   				String title_c5p = title_conv5d+"-C5P"+(clt_parameters.imp.curt_c5_matched?"MK":"TK")+ // MK=matched-filter, TK=trapezoid kernel // By Claude on 06/12/2026
   						(white_en ? ("-W"+white_sp+"_"+white_vel) : "")+ // linear whitening marker // By Claude on 06/13/2026
   						(recur_soft ? ("-PG"+recur_lambda+"K"+recur_keep) : "")+ // posterior-gated recurrent feed marker // By Claude on 06/13/2026
   						((nlev > 0)? ("-LEV"+nlev) : ""); // By Claude on 06/12/2026
   				if (curt_save_c5rect) {                                                    // By Claude on 06/11/2026
   					ImagePlus imp_c5p_rect = cuasRTUtils.showConvKernel5d(                 // By Claude on 06/11/2026
   							c5p_roi, curt_save_select, ts_5dp, title_c5p+"-RECT");         // By Claude on 06/11/2026
   					QuadCLTCPU.saveImagePlusInDirectory(imp_c5p_rect, getModelDirectory()); // By Claude on 06/11/2026
   				}                                                                          // By Claude on 06/11/2026
   				if (clt_parameters.imp.curt_save_c5hyper) {                                // per-velocity view: bottom slider = velocity, second = time // By Claude on 06/13/2026
   					ImagePlus imp_c5p_hyper = cuasRTUtils.showConvKernel5dHyperRect(       // By Claude on 06/13/2026
   							c5p_roi, curt_save_select, ts_5dp, title_c5p+"-HYPER-RECT");   // By Claude on 06/13/2026
   					QuadCLTCPU.saveImagePlusInDirectory(imp_c5p_hyper, getModelDirectory()); // By Claude on 06/13/2026
   				}                                                                          // By Claude on 06/13/2026
   				// Bayes posterior lambda sweep: per-cell score z^2/2+ln(prior) (-SCORE),  // By Claude on 06/12/2026
   				// softmax competition over the 4D NMS window with a null-hypothesis floor //
   				// (-POST), and per-pixel marginal sum_v P (-PMARG). lambda = assumed      //
   				// input noise sigma, the "fat zero" of the competition: small -> sharp    //
   				// argmax-like posterior + amplified noise, large -> blur toward prior.    //
   				if ((c5p_raw != null) && (clt_parameters.imp.curt_c5_lambdas != null)      // By Claude on 06/12/2026
   						&& (clt_parameters.imp.curt_c5_lambdas.length > 0)) {              // By Claude on 06/12/2026
   					double [] lams = clt_parameters.imp.curt_c5_lambdas;                   // By Claude on 06/12/2026
   					double null_z = clt_parameters.imp.curt_c5_null_z;                     // By Claude on 06/12/2026
   					double null_score = 0.5 * null_z * null_z;                             // By Claude on 06/12/2026
   					int n_sweep = clt_parameters.imp.curt_c5_post_scenes;                  // 0 - all scenes // By Claude on 06/13/2026
   					int n0_sweep = ((n_sweep > 0) && (n_sweep < num_5dp)) ? (num_5dp - n_sweep) : 0; // last n_sweep scenes - bounds memory (score+post per lambda per scene) // By Claude on 06/13/2026
   					int nsc_sweep = num_5dp - n0_sweep;                                    // By Claude on 06/13/2026
   					String [] ts_sweep = java.util.Arrays.copyOfRange(ts_5dp, n0_sweep, num_5dp); // By Claude on 06/13/2026
   					System.out.println(now()+" detectTargets(): C5P posterior sweep LEV"+nlev+", "+lams.length+" lambdas, "+nsc_sweep+" scenes"); // By Claude on 06/12/2026
   					double [] log_prior = cuasRTUtils.getVelLogPrior(                      // By Claude on 06/12/2026
   							clt_parameters.imp.curt_c5_prior_r0,                           // By Claude on 06/12/2026
   							clt_parameters.imp.curt_c5_prior_r1);                          // By Claude on 06/12/2026
   					String [] lam_titles = new String [lams.length];                       // By Claude on 06/12/2026
   					double [][][][][] score_lam = new double [lams.length][nsc_sweep][][][]; // By Claude on 06/13/2026 (sweep scenes only)
   					double [][][][][] post_lam  = new double [lams.length][nsc_sweep][][][]; // By Claude on 06/13/2026
   					for (int il = 0; il < lams.length; il++) {                             // By Claude on 06/12/2026
   						lam_titles[il] = "lam="+lams[il];                                  // By Claude on 06/12/2026
   						for (int n5d = 0; n5d < nsc_sweep; n5d++) {                        // By Claude on 06/13/2026
   							score_lam[il][n5d] = cuasRTUtils.scoreC5PROI(                  // By Claude on 06/12/2026
   									c5p_raw[n0_sweep + n5d], lams[il], log_prior,          // By Claude on 06/13/2026
   									clt_parameters.imp.curt_c5_post_exact);                // By Claude on 06/13/2026
   							post_lam[il][n5d]  = cuasRTUtils.softmaxC5PROI(                // By Claude on 06/12/2026
   									score_lam[il][n5d], curt_save_select, null_score);     // By Claude on 06/12/2026
   						}                                                                  // By Claude on 06/12/2026
   						System.out.println(now()+" detectTargets(): C5P posterior lambda="+lams[il]+" done ("+(il+1)+"/"+lams.length+")"); // By Claude on 06/13/2026
   					}                                                                      // By Claude on 06/12/2026
   					String post_sfx = "-NZ"+null_z;                                        // By Claude on 06/12/2026
   					ImagePlus imp_score = cuasRTUtils.showC5PLambdaHyper(score_lam,        // By Claude on 06/12/2026
   							curt_save_select, ts_sweep, lam_titles,                        // By Claude on 06/13/2026
   							title_c5p+post_sfx+"-SCORE", false,                            // By Claude on 06/12/2026
   							new double [] {0.0, 4*null_score});                            // By Claude on 06/12/2026
   					QuadCLTCPU.saveImagePlusInDirectory(imp_score, getModelDirectory());   // By Claude on 06/12/2026
   					ImagePlus imp_post = cuasRTUtils.showC5PLambdaHyper(post_lam,          // By Claude on 06/12/2026
   							curt_save_select, ts_sweep, lam_titles,                        // By Claude on 06/13/2026
   							title_c5p+post_sfx+"-POST", false,                             // By Claude on 06/12/2026
   							new double [] {0.0, 1.0});                                     // By Claude on 06/12/2026
   					QuadCLTCPU.saveImagePlusInDirectory(imp_post, getModelDirectory());    // By Claude on 06/12/2026
   					ImagePlus imp_marg = cuasRTUtils.showC5PLambdaHyper(post_lam,          // By Claude on 06/12/2026
   							curt_save_select, ts_sweep, lam_titles,                        // By Claude on 06/13/2026
   							title_c5p+post_sfx+"-PMARG", true,                             // By Claude on 06/12/2026
   							new double [] {0.0, 1.0});                                     // By Claude on 06/12/2026
   					QuadCLTCPU.saveImagePlusInDirectory(imp_marg, getModelDirectory());    // By Claude on 06/12/2026
   					if (clt_parameters.imp.curt_save_c5hyper) {                            // per-velocity posterior view, one image per lambda // By Claude on 06/13/2026
   						// (the lambda-sweep hyperstack already uses both sliders for lambda x time, so velocity gets its own images) // By Claude on 06/13/2026
   						for (int il = 0; il < lams.length; il++) {                         // By Claude on 06/13/2026
   							ImagePlus imp_post_hyper = cuasRTUtils.showConvKernel5dHyperRect( // By Claude on 06/13/2026
   									post_lam[il], curt_save_select, ts_sweep,              // By Claude on 06/13/2026
   									title_c5p+post_sfx+"-POST-LAM"+lams[il]+"-HYPER-RECT"); // By Claude on 06/13/2026
   							imp_post_hyper.setDisplayRange(0.0, 1.0);                      // posterior in [0,1] // By Claude on 06/13/2026
   							QuadCLTCPU.saveImagePlusInDirectory(imp_post_hyper, getModelDirectory()); // By Claude on 06/13/2026
   						}                                                                  // By Claude on 06/13/2026
   					}                                                                      // By Claude on 06/13/2026
   				}                                                                          // By Claude on 06/12/2026
   				if (run_recur) {                                                           // By Claude on 06/11/2026
   					runRecurrentLevel(                                                     // By Claude on 06/11/2026
   							cuasRTUtils,      // CuasRTUtils     cuasRTUtils,              // By Claude on 06/11/2026
   							c5p_roi,          // double [][][][] conv5d_roi,               // By Claude on 06/11/2026
   							ts_5dp,           // String []       ts_5d,                    // By Claude on 06/11/2026
   							curt_save_select, // Rectangle       roi,                      // By Claude on 06/11/2026
   							title_c5p,        // String          title_base,               // By Claude on 06/11/2026
   							false);           // boolean         save_individual           // By Claude on 06/11/2026
   				}                                                                          // By Claude on 06/11/2026
   			}                                                                              // By Claude on 06/11/2026
   		}                                                                                  // By Claude on 06/11/2026
   		// DNN front-end (-DNN): if curt_dnn_model is set, run the trained FCN per-pixel       // By Claude on 06/13/2026
   		// (stride-1, matching training) over the ROI -> (Vx,Vy,s) field = softmax(vel)*s,
   		// same shape as the C5P output, saved as -DNN-RECT / -DNN-HYPER-RECT. N=8 frames
   		// (training). Reuses curt_c5_levels for level gating. Recurrent feed is the next step
   		// (the field is [0,1]-scaled, unlike the C5P matched-filter response - needs rescale).
   		if (!clt_parameters.imp.curt_dnn_model.isEmpty() && (curt_save_select != null)) {     // By Claude on 06/13/2026
   			final int N_dnn = 8;                                                              // By Claude on 06/13/2026
   			final int vr_dnn = clt_parameters.imp.curt_vel_radius;                            // By Claude on 06/13/2026
   			int [] c5_levels_dnn = clt_parameters.imp.curt_c5_levels;                         // By Claude on 06/13/2026
   			try {                                                                             // By Claude on 06/13/2026
   				CuasDnnInfer dnn = new CuasDnnInfer(clt_parameters.imp.curt_dnn_model, N_dnn); // By Claude on 06/13/2026
   				for (int nlev = 0; nlev < pyramid_levels; nlev++) {                           // By Claude on 06/13/2026
   					boolean lev_sel = (c5_levels_dnn == null) || (c5_levels_dnn.length == 0); // By Claude on 06/13/2026
   					if (!lev_sel) for (int l : c5_levels_dnn) if (l == nlev) { lev_sel = true; break; } // By Claude on 06/13/2026
   					if (!lev_sel) continue;                                                   // By Claude on 06/13/2026
   					double [][] framesD = dpixels_pyramid[nlev];                              // By Claude on 06/13/2026
   					if (framesD == null) continue;                                            // By Claude on 06/13/2026
   					int num = Math.max(0, framesD.length - N_dnn + 1);                       // all available scenes // By Claude on 06/13/2026
   					if (num == 0) continue;                                                   // By Claude on 06/13/2026
   					double dnn_thresh = clt_parameters.imp.curt_dnn_thresh;                   // By Claude on 06/13/2026
   					double [][][][] dnn_roi = new double [num][][][];                         // By Claude on 06/13/2026
   					String [] ts_dnn = new String [num];                                      // By Claude on 06/13/2026
   					System.out.println(now()+" detectTargets(): DNN inferROI LEV"+nlev+" ("+num+" scenes, "+ // By Claude on 06/13/2026
   							(curt_save_select.width*curt_save_select.height)+" px/scene, thresh="+dnn_thresh+")"); // By Claude on 06/13/2026
   					for (int n5d = 0; n5d < num; n5d++) {                                     // By Claude on 06/13/2026
   						int newest = n5d + N_dnn - 1;                                         // absolute newest-frame index in framesD // By Claude on 06/13/2026
   						float [][] window = new float [N_dnn][];                              // By Claude on 06/13/2026
   						for (int h = 0; h < N_dnn; h++) {                                     // By Claude on 06/13/2026
   							double [] src = framesD[newest - h];                              // h=0 newest // By Claude on 06/13/2026
   							float [] fw = new float [src.length];                             // By Claude on 06/13/2026
   							for (int k = 0; k < src.length; k++) fw[k] = (float) src[k];      // By Claude on 06/13/2026
   							window[h] = fw;                                                   // By Claude on 06/13/2026
   						}                                                                     // By Claude on 06/13/2026
   						dnn_roi[n5d] = dnn.inferROI(window, width, height, curt_save_select, vr_dnn, dnn_thresh); // By Claude on 06/13/2026
   						ts_dnn[n5d] = ts_pyramid[nlev][newest] + " f"+newest;                 // timestamp + abs frame index for matching // By Claude on 06/13/2026
   						System.out.println(now()+"   DNN scene "+(n5d+1)+"/"+num+" (frame "+newest+", "+ts_pyramid[nlev][newest]+") done"); // By Claude on 06/13/2026
   					}                                                                         // By Claude on 06/13/2026
   					String title_dnn = title_conv5d+"-DNN"+((nlev > 0)?("-LEV"+nlev):"");      // By Claude on 06/13/2026
   					if (curt_save_c5rect) {                                                   // By Claude on 06/13/2026
   						QuadCLTCPU.saveImagePlusInDirectory(cuasRTUtils.showConvKernel5d(      // By Claude on 06/13/2026
   								dnn_roi, curt_save_select, ts_dnn, title_dnn+"-RECT"), getModelDirectory()); // By Claude on 06/13/2026
   					}                                                                         // By Claude on 06/13/2026
   					if (clt_parameters.imp.curt_save_c5hyper) {                               // By Claude on 06/13/2026
   						QuadCLTCPU.saveImagePlusInDirectory(cuasRTUtils.showConvKernel5dHyperRect( // By Claude on 06/13/2026
   								dnn_roi, curt_save_select, ts_dnn, title_dnn+"-HYPER-RECT"), getModelDirectory()); // By Claude on 06/13/2026
   					}                                                                         // By Claude on 06/13/2026
   				}                                                                             // By Claude on 06/13/2026
   				dnn.close();                                                                  // By Claude on 06/13/2026
   			} catch (Exception e) {                                                           // By Claude on 06/13/2026
   				System.out.println("DNN inference failed: "+e); e.printStackTrace();          // By Claude on 06/13/2026
   			}                                                                                 // By Claude on 06/13/2026
   		}                                                                                     // By Claude on 06/13/2026
   		System.out.println(now()+" detectTargets(): done"); // By Claude on 06/11/2026
   		return null;
   		/*
		CuasMotion cuasMotion = new CuasMotion (
				clt_parameters,              // CLTParameters     clt_parameters,
				getTimeStamps(),                // String []         scene_titles,
				cuasRanging.getCenter_CLT(), // QuadCLT           parentCLT,
				null, // fpixels_avg,                 // float []          fpixels_avg,
				uasLogReader,                // UasLogReader      uasLogReader,
				debugLevel);                 // int               debugLevel)
		return cuasMotion;
		*/
	}
}
/*
					uasLogReader,   // UasLogReader    uasLogReader,
batch_mode);    // boolean         batch_mode)

*/