Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ git submodule update --init
<dependency>
<groupId>io.github.codeurjc</groupId>
<artifactId>brkga-mp-ipr-java</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</dependency>
```

Expand All @@ -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 /
Expand Down
2 changes: 1 addition & 1 deletion brkga_mp_ipr_java/docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ binaries, so **no C++ compiler is needed**:
<dependency>
<groupId>io.github.codeurjc</groupId>
<artifactId>brkga-mp-ipr-java</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</dependency>
```

Expand Down
2 changes: 1 addition & 1 deletion brkga_mp_ipr_java/docs/JAVA_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ just a JDK and your build tool. Add the dependency:
<dependency>
<groupId>io.github.codeurjc</groupId>
<artifactId>brkga-mp-ipr-java</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</dependency>
```

Expand Down
23 changes: 23 additions & 0 deletions brkga_mp_ipr_java/native/brkga_bridge.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<AlgorithmContext*>(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;
Expand Down
2 changes: 1 addition & 1 deletion brkga_mp_ipr_java/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
==================================================================== -->
<groupId>io.github.codeurjc</groupId>
<artifactId>brkga-mp-ipr-java</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
<packaging>jar</packaging>

<name>BRKGA-MP-IPR for Java</name>
Expand Down
38 changes: 34 additions & 4 deletions brkga_mp_ipr_java/src/main/java/brkga/BrkgaMpIpr.java
Original file line number Diff line number Diff line change
Expand Up @@ -709,10 +709,12 @@ public double[] getBestFitness() {
*
* <p><b>Warning:</b> 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
Expand All @@ -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.
*
* <p>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.
*
Expand Down
3 changes: 3 additions & 0 deletions brkga_mp_ipr_java/src/main/java/brkga/NativeBrkga.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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];
}
}
}
4 changes: 2 additions & 2 deletions examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
with only a JDK + Maven — no C++ toolchain. -->
<groupId>io.github.codeurjc</groupId>
<artifactId>brkga-mp-ipr-java-examples</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
<packaging>jar</packaging>

<name>BRKGA-MP-IPR for Java — examples</name>
Expand All @@ -21,7 +21,7 @@
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Keep in sync with the wrapper version you depend on. -->
<brkga.version>0.2.0</brkga.version>
<brkga.version>0.3.0</brkga.version>
</properties>

<dependencies>
Expand Down
8 changes: 4 additions & 4 deletions scripts/build_brkga_native.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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}"
Expand All @@ -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
Expand Down
Loading