Warning: This method does NOT return the overall best solution, but the * best one within the current population. If {@link #shake(int, ShakingType, int)} - * or {@link #reset()} is called, the best solution may be lost in the populations. - * However, if you are using {@link #run(ControlParams)}, the best solution is - * returned by that method; otherwise you must keep track of the best solution - * yourself. + * or {@link #reset()} is called, the overall best may be lost from the populations, + * so after a {@link #run(ControlParams)} whose control parameters enable shaking or + * resetting this can return a solution worse than the run's best. To recover the + * best solution of the last run in that case, use {@link #getLastRunBestChromosome()}, + * whose fitness matches the {@link AlgorithmStatus#bestFitness} returned by + * {@code run}. * * @return the best chromosome as a {@code double[]} of genes in [0,1). * @throws RuntimeException with message {@code "No best chromosome available"} when @@ -730,6 +732,34 @@ public double[] getBestChromosome() { catch(Throwable t) { throw new RuntimeException("getBestChromosome failed", t); } } + /** + * Returns the best chromosome recorded during the last {@link #run(ControlParams)}, + * the overall incumbent of that run. + * + *
Unlike {@link #getBestChromosome()}, this is not the current-population best:
+ * {@code run} snapshots the incumbent on every improvement, so this survives any
+ * {@link #shake(int, ShakingType, int)} or {@link #reset()} triggered during the
+ * run. Its fitness equals the {@link AlgorithmStatus#bestFitness} that {@code run}
+ * returned. This is the method to use to rebuild the solution a run converged to.
+ *
+ * @return the last run's best chromosome as a {@code double[]} of genes in [0,1).
+ * @throws RuntimeException with message
+ * {@code "No best chromosome available from the last run"}
+ * when no run has recorded a best yet (no {@code run} has
+ * completed at least one improving iteration), or wrapping
+ * the native error message if the native call otherwise fails.
+ */
+ public double[] getLastRunBestChromosome() {
+ try(Arena temp = Arena.ofConfined()) {
+ MemorySegment out = temp.allocate((long) chromosomeSize * Double.BYTES);
+ int rc = (int) lib.getLastStatusBestChromosome.invoke(algo, out, (long) chromosomeSize);
+ if(rc != 0) throw new RuntimeException("No best chromosome available from the last run");
+ return out.toArray(JAVA_DOUBLE);
+ }
+ catch(RuntimeException e) { throw e; }
+ catch(Throwable t) { throw new RuntimeException("getLastRunBestChromosome failed", t); }
+ }
+
/**
* Returns a chromosome of the given population.
*
diff --git a/brkga_mp_ipr_java/src/main/java/brkga/NativeBrkga.java b/brkga_mp_ipr_java/src/main/java/brkga/NativeBrkga.java
index 8544b5e..e6ca597 100644
--- a/brkga_mp_ipr_java/src/main/java/brkga/NativeBrkga.java
+++ b/brkga_mp_ipr_java/src/main/java/brkga/NativeBrkga.java
@@ -81,6 +81,7 @@ final class NativeBrkga {
// Getters.
final MethodHandle getBestFitness;
final MethodHandle getBestChromosome;
+ final MethodHandle getLastStatusBestChromosome;
final MethodHandle getBestChromosomeExtra;
final MethodHandle getChromosome;
final MethodHandle getChromosomeExtra;
@@ -163,6 +164,8 @@ private NativeBrkga(Path soPath, int numObjectives) {
FunctionDescriptor.ofVoid(ADDRESS, ADDRESS));
getBestChromosome = dc("brkga_get_best_chromosome",
FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_LONG));
+ getLastStatusBestChromosome = dc("brkga_get_last_status_best_chromosome",
+ FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_LONG));
getBestChromosomeExtra = dc("brkga_get_best_chromosome_extra",
FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_LONG));
getChromosome = dc("brkga_get_chromosome",
diff --git a/brkga_mp_ipr_java/src/test/java/brkga/LastRunBestChromosomeTest.java b/brkga_mp_ipr_java/src/test/java/brkga/LastRunBestChromosomeTest.java
new file mode 100644
index 0000000..d1294a1
--- /dev/null
+++ b/brkga_mp_ipr_java/src/test/java/brkga/LastRunBestChromosomeTest.java
@@ -0,0 +1,83 @@
+/******************************************************************************
+ * LastRunBestChromosomeTest.java: getLastRunBestChromosome() returns the
+ * historical best of a run, surviving shake/reset, unlike getBestChromosome().
+ *****************************************************************************/
+
+package brkga;
+
+import brkga.tsp.TspInstance;
+import brkga.tsp.single.TspDecoder;
+import org.junit.jupiter.api.Test;
+
+import java.lang.foreign.Arena;
+import java.lang.foreign.MemorySegment;
+import java.lang.foreign.ValueLayout;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class LastRunBestChromosomeTest {
+
+ /** Before any run has recorded a best, the getter must fail, not return garbage. */
+ @Test
+ void throwsBeforeAnyRun() throws Exception {
+ Brkga brkga = Brkga.single();
+ TspInstance instance = new TspInstance(TestSupport.BURMA14);
+ try(BrkgaMpIpr algo = TestSupport.buildSingle(brkga, instance, TestSupport.SEED, 1)) {
+ assertThrows(RuntimeException.class, algo::getLastRunBestChromosome);
+ }
+ }
+
+ @Test
+ void lastRunBestSurvivesShakeAndReset() throws Exception {
+ Brkga brkga = Brkga.single();
+ TspInstance instance = new TspInstance(TestSupport.BURMA14);
+ try(BrkgaMpIpr algo = TestSupport.buildSingle(brkga, instance, TestSupport.SEED, 1)) {
+ // Force shakes and resets during the run; stop by iteration count so the
+ // run reliably ends after several perturbations have fired.
+ ControlParams control = new ControlParams();
+ control.maximumRunningTime = Long.MAX_VALUE;
+ control.stallOffset = (int) 0xFFFFFFFFL; // never stop by stall
+ control.shakeInterval = 3;
+ control.resetInterval = 8;
+ algo.setStoppingCriteria(s -> s.currentIteration >= 200);
+
+ AlgorithmStatus status = algo.run(control);
+
+ // The run must actually have shaken and reset, or the test would not be
+ // exercising the scenario this method exists for.
+ assertTrue(status.numShakes > 0, "expected shaking to fire during the run");
+ assertTrue(status.numResets > 0, "expected a reset to fire during the run");
+
+ TspDecoder decoder = new TspDecoder(instance);
+ double[] runBest = algo.getLastRunBestChromosome();
+
+ // The last-run best chromosome must decode to the fitness run() reported...
+ assertEquals(status.bestFitness[0], decode(decoder, runBest), 1e-9,
+ "getLastRunBestChromosome must decode to the run's best fitness");
+ // ...and can never be worse than the current-population best.
+ assertTrue(decode(decoder, runBest) <= decode(decoder, algo.getBestChromosome()) + 1e-9,
+ "historical best must be <= population best");
+
+ // A reset() wipes the populations (getBestChromosome would change), but the
+ // historical best is snapshotted in the run status, so it must survive
+ // unchanged. This is the guarantee getLastRunBestChromosome exists to give.
+ algo.reset();
+ assertArrayEquals(runBest, algo.getLastRunBestChromosome(),
+ "last-run best must survive an explicit reset()");
+ }
+ }
+
+ /** Decodes a chromosome's genes with the TSP decoder, returning its single fitness. */
+ private static double decode(TspDecoder decoder, double[] genes) {
+ try(Arena arena = Arena.ofConfined()) {
+ MemorySegment seg = arena.allocate((long) genes.length * Double.BYTES);
+ for(int i = 0; i < genes.length; ++i) {
+ seg.setAtIndex(ValueLayout.JAVA_DOUBLE, i, genes[i]);
+ }
+ return decoder.decode(new Chromosome(seg, genes.length), false)[0];
+ }
+ }
+}
diff --git a/examples/pom.xml b/examples/pom.xml
index ba13c16..0d8775a 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -11,7 +11,7 @@
with only a JDK + Maven — no C++ toolchain. -->