001package gudusoft.gsqlparser.resolver2.iterative; 002 003import java.util.ArrayList; 004import java.util.List; 005 006/** 007 * Detects when iterative resolution has converged. 008 * 009 * <p>Convergence is reached when one of these conditions is met: 010 * 1. No progress in current pass (no new columns resolved) 011 * 2. All columns are resolved 012 * 3. Maximum iterations reached 013 * 4. No change in last N passes (stable state) 014 * 5. Progress rate falls below threshold 015 * 016 * <p>The detector uses multiple strategies to avoid: 017 * - Infinite loops (max iterations) 018 * - Premature stopping (stable state detection) 019 * - Wasted iterations (progress rate threshold) 020 * 021 * <p>Example: 022 * <pre> 023 * Pass 1: 10 columns resolved -> continue 024 * Pass 2: 5 columns resolved -> continue 025 * Pass 3: 2 columns resolved -> continue 026 * Pass 4: 0 columns resolved -> CONVERGED (no progress) 027 * </pre> 028 */ 029public class ConvergenceDetector { 030 031 /** Maximum number of iterations allowed */ 032 private final int maxIterations; 033 034 /** Number of stable passes required to declare convergence */ 035 private final int stablePasses; 036 037 /** Minimum progress rate to continue (0.0 - 1.0) */ 038 private final double minProgressRate; 039 040 /** History of passes */ 041 private final List<ResolutionPass> passHistory; 042 043 public ConvergenceDetector() { 044 this(10, 2, 0.01); 045 } 046 047 public ConvergenceDetector(int maxIterations, int stablePasses, double minProgressRate) { 048 this.maxIterations = maxIterations; 049 this.stablePasses = stablePasses; 050 this.minProgressRate = minProgressRate; 051 this.passHistory = new ArrayList<>(); 052 } 053 054 /** 055 * Record a completed pass. 056 * 057 * @param pass the completed pass 058 */ 059 public void recordPass(ResolutionPass pass) { 060 passHistory.add(pass); 061 } 062 063 /** 064 * Check if resolution has converged. 065 * 066 * @return convergence result with reason 067 */ 068 public ConvergenceResult checkConvergence() { 069 if (passHistory.isEmpty()) { 070 return new ConvergenceResult(false, null); 071 } 072 073 ResolutionPass lastPass = passHistory.get(passHistory.size() - 1); 074 075 // Check 1: Maximum iterations reached 076 if (lastPass.getPassNumber() >= maxIterations) { 077 return new ConvergenceResult( 078 true, 079 "Maximum iterations reached (" + maxIterations + ")" 080 ); 081 } 082 083 // Check 2: No progress in last pass 084 if (!lastPass.madeProgress()) { 085 return new ConvergenceResult( 086 true, 087 "No progress in pass " + lastPass.getPassNumber() 088 ); 089 } 090 091 // Check 3: All columns resolved (100% resolution) 092 if (lastPass.getAfterStats() != null) { 093 int total = lastPass.getAfterStats().getTotalReferences(); 094 int resolved = lastPass.getAfterStats().getExactMatches(); 095 if (total > 0 && resolved == total) { 096 return new ConvergenceResult( 097 true, 098 "All columns resolved (" + resolved + "/" + total + ")" 099 ); 100 } 101 } 102 103 // Check 4: Stable state (no progress in last N passes) 104 if (passHistory.size() >= stablePasses) { 105 boolean allStable = true; 106 for (int i = passHistory.size() - stablePasses; i < passHistory.size(); i++) { 107 if (passHistory.get(i).madeProgress()) { 108 allStable = false; 109 break; 110 } 111 } 112 if (allStable) { 113 return new ConvergenceResult( 114 true, 115 "Stable state (no progress in last " + stablePasses + " passes)" 116 ); 117 } 118 } 119 120 // Check 5: Progress rate too low 121 if (passHistory.size() >= 3) { 122 double progressRate = calculateProgressRate(); 123 if (progressRate < minProgressRate) { 124 return new ConvergenceResult( 125 true, 126 String.format("Progress rate too low (%.2f%% < %.2f%%)", 127 progressRate * 100, minProgressRate * 100) 128 ); 129 } 130 } 131 132 // Not converged yet 133 return new ConvergenceResult(false, null); 134 } 135 136 /** 137 * Calculate the progress rate over recent passes. 138 * Returns ratio of columns resolved / total columns. 139 */ 140 private double calculateProgressRate() { 141 if (passHistory.isEmpty()) { 142 return 0.0; 143 } 144 145 ResolutionPass lastPass = passHistory.get(passHistory.size() - 1); 146 if (lastPass.getAfterStats() == null) { 147 return 0.0; 148 } 149 150 int totalReferences = lastPass.getAfterStats().getTotalReferences(); 151 if (totalReferences == 0) { 152 return 1.0; // No columns to resolve 153 } 154 155 int resolved = lastPass.getColumnsResolvedInPass(); 156 return (double) resolved / totalReferences; 157 } 158 159 /** 160 * Get the total number of passes so far. 161 * 162 * @return pass count 163 */ 164 public int getPassCount() { 165 return passHistory.size(); 166 } 167 168 /** 169 * Get the pass history. 170 * 171 * @return list of all passes 172 */ 173 public List<ResolutionPass> getPassHistory() { 174 return new ArrayList<>(passHistory); 175 } 176 177 /** 178 * Get statistics about the iteration process. 179 * 180 * @return statistics summary 181 */ 182 public String getStatistics() { 183 if (passHistory.isEmpty()) { 184 return "No passes yet"; 185 } 186 187 int totalResolved = 0; 188 int totalInferences = 0; 189 long totalTime = 0; 190 191 for (ResolutionPass pass : passHistory) { 192 totalResolved += pass.getColumnsResolvedInPass(); 193 totalInferences += pass.getNewInferences(); 194 totalTime += pass.getTimeTaken(); 195 } 196 197 return String.format( 198 "Iterations: %d, Resolved: %d, Inferences: %d, Time: %dms", 199 passHistory.size(), totalResolved, totalInferences, totalTime 200 ); 201 } 202 203 /** 204 * Result of convergence check. 205 */ 206 public static class ConvergenceResult { 207 private final boolean converged; 208 private final String reason; 209 210 public ConvergenceResult(boolean converged, String reason) { 211 this.converged = converged; 212 this.reason = reason; 213 } 214 215 public boolean hasConverged() { 216 return converged; 217 } 218 219 public String getReason() { 220 return reason; 221 } 222 223 @Override 224 public String toString() { 225 return converged ? "Converged: " + reason : "Not converged"; 226 } 227 } 228}