diff --git a/README.md b/README.md index 7536811..b799ef7 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ git submodule update --init io.github.codeurjc brkga-mp-ipr-java - 0.2.0 + 0.3.0 ``` @@ -42,6 +42,11 @@ for other platforms, an older Linux, or custom builds, see and [*Running on an older Linux*](brkga_mp_ipr_java/docs/JAVA_GUIDE.md#running-on-an-older-linux-glibc--glibcxx-too-new) in the Java guide. +> **Upgrading to 0.3.0:** this release adds the native symbol +> `brkga_get_last_status_best_chromosome` behind `BrkgaMpIpr.getLastRunBestChromosome()`. +> If you supply your own native library via `-Dbrkga.bridge.dir`, rebuild it from the +> 0.3.0 sources — a 0.2.0 `.so` lacks the new symbol and the wrapper will fail to link it. + ## Documentation - **[brkga_mp_ipr_java/docs/GUIDE.md](brkga_mp_ipr_java/docs/GUIDE.md)** — usage guide / diff --git a/brkga_mp_ipr_java/docs/GUIDE.md b/brkga_mp_ipr_java/docs/GUIDE.md index 40a3770..2423b34 100644 --- a/brkga_mp_ipr_java/docs/GUIDE.md +++ b/brkga_mp_ipr_java/docs/GUIDE.md @@ -54,7 +54,7 @@ binaries, so **no C++ compiler is needed**: io.github.codeurjc brkga-mp-ipr-java - 0.2.0 + 0.3.0 ``` diff --git a/brkga_mp_ipr_java/docs/JAVA_GUIDE.md b/brkga_mp_ipr_java/docs/JAVA_GUIDE.md index dcc832f..1b0771f 100644 --- a/brkga_mp_ipr_java/docs/JAVA_GUIDE.md +++ b/brkga_mp_ipr_java/docs/JAVA_GUIDE.md @@ -45,7 +45,7 @@ just a JDK and your build tool. Add the dependency: io.github.codeurjc brkga-mp-ipr-java - 0.2.0 + 0.3.0 ``` diff --git a/brkga_mp_ipr_java/native/brkga_bridge.cpp b/brkga_mp_ipr_java/native/brkga_bridge.cpp index 1cc7148..59434b9 100644 --- a/brkga_mp_ipr_java/native/brkga_bridge.cpp +++ b/brkga_mp_ipr_java/native/brkga_bridge.cpp @@ -572,6 +572,29 @@ int brkga_get_best_chromosome(void* algo, double* out, size_t size) { } } +/// Copies the best chromosome recorded in the last run's status (the historical +/// incumbent, which survives shake/reset) into `out` (size doubles). Unlike +/// brkga_get_best_chromosome, this is NOT the live population best: run() snapshots +/// it into status.best_chromosome on every improvement, so it is the true best of +/// the whole run even if a later shake/reset dropped it from the populations. +/// Returns 0 on success, non-zero if no run has recorded a best yet or on size +/// mismatch. +int brkga_get_last_status_best_chromosome(void* algo, double* out, size_t size) { + try { + const auto& chr = static_cast(algo) + ->last_status.best_chromosome; + // Empty until the first improvement of a run records a best. + if(chr.size() == 0 || chr.size() < size) + return 1; + for(size_t i = 0; i < size; ++i) + out[i] = chr[i]; + return 0; + } + catch(std::exception&) { + return 1; + } +} + /// Capacity (in doubles) of every chromosome's inline extra blob. unsigned brkga_extra_capacity() { return BRKGA::CHROMOSOME_EXTRA_CAP; diff --git a/brkga_mp_ipr_java/pom.xml b/brkga_mp_ipr_java/pom.xml index 6215456..f833ee6 100644 --- a/brkga_mp_ipr_java/pom.xml +++ b/brkga_mp_ipr_java/pom.xml @@ -12,7 +12,7 @@ ==================================================================== --> io.github.codeurjc brkga-mp-ipr-java - 0.2.0 + 0.3.0 jar BRKGA-MP-IPR for Java diff --git a/brkga_mp_ipr_java/src/main/java/brkga/BrkgaMpIpr.java b/brkga_mp_ipr_java/src/main/java/brkga/BrkgaMpIpr.java index 6cb8e6e..4a042e5 100644 --- a/brkga_mp_ipr_java/src/main/java/brkga/BrkgaMpIpr.java +++ b/brkga_mp_ipr_java/src/main/java/brkga/BrkgaMpIpr.java @@ -709,10 +709,12 @@ public double[] getBestFitness() { * *

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. --> io.github.codeurjc brkga-mp-ipr-java-examples - 0.2.0 + 0.3.0 jar BRKGA-MP-IPR for Java — examples @@ -21,7 +21,7 @@ 25 UTF-8 - 0.2.0 + 0.3.0 diff --git a/scripts/build_brkga_native.sh b/scripts/build_brkga_native.sh index 2a5f717..a4e7cd6 100755 --- a/scripts/build_brkga_native.sh +++ b/scripts/build_brkga_native.sh @@ -30,13 +30,13 @@ # Usage (all knobs are optional environment variables): # scripts/build_brkga_native.sh # -# BRKGA_TAG git tag to clone (keep in sync with the version you use) [v0.2.0] +# BRKGA_TAG git tag to clone (keep in sync with the version you use) [v0.3.0] # IMAGE older base image to build in [ubuntu:20.04] # TUPLE_NS objective counts to build, space-separated [1] # e.g. TUPLE_NS="1 2 3" for the single + two/three-objective demos # GENERIC set to 1 to also build libbrkga_bridge_generic.so [0] # EXTRA_CAP inline extra-blob capacity (doubles), uniform across binaries [8] -# CAP ABI capacity; only 20 is accepted (must match Java 0.2.0) [20] +# CAP ABI capacity; only 20 is accepted (must match Java 0.3.0) [20] # MATING mating mode (MATING_SEQUENTIAL|MATING_SEED_ONLY|MATING_FULL_SPEED) # [MATING_SEQUENTIAL] # OUT_DIR where the .so files are written [$PWD/native] @@ -50,7 +50,7 @@ set -euo pipefail # Keep BRKGA_TAG in sync with the artifact version you depend on. -BRKGA_TAG="${BRKGA_TAG:-v0.2.0}" +BRKGA_TAG="${BRKGA_TAG:-v0.3.0}" IMAGE="${IMAGE:-ubuntu:20.04}" TUPLE_NS="${TUPLE_NS:-1}" GENERIC="${GENERIC:-0}" @@ -64,7 +64,7 @@ KEEP_WORK_DIR="${KEEP_WORK_DIR:-0}" # network access or Docker work. CAP is part of BridgeStatus's binary layout and # therefore must match AlgorithmStatus.CAP in the published Java artifact. if [ "${CAP}" != "20" ]; then - echo "CAP=${CAP} is incompatible with the Java 0.2.0 ABI; only CAP=20 is supported." >&2 + echo "CAP=${CAP} is incompatible with the Java 0.3.0 ABI; only CAP=20 is supported." >&2 echo "Changing CAP requires rebuilding the Java wrapper and AlgorithmStatus layout too." >&2 exit 2 fi