diff --git a/.idea/modules.xml b/.idea/modules.xml index 9c766faf6..6f643a784 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,6 +3,7 @@ + diff --git a/.idea/modules/agents/POSEIDON.agents.test.iml b/.idea/modules/agents/POSEIDON.agents.test.iml new file mode 100644 index 000000000..c2ebdb559 --- /dev/null +++ b/.idea/modules/agents/POSEIDON.agents.test.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/choices/AveragingOptionValues.java b/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/choices/AveragingOptionValues.java new file mode 100644 index 000000000..e1b08361c --- /dev/null +++ b/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/choices/AveragingOptionValues.java @@ -0,0 +1,41 @@ +/* + * POSEIDON: an agent-based model of fisheries + * Copyright (c) 2024 CoHESyS Lab cohesys.lab@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package uk.ac.ox.poseidon.agents.behaviours.choices; + +import java.util.HashMap; +import java.util.Map; + +public class AveragingOptionValues extends MapBasedOptionValues { + + private final Map counts = new HashMap<>(); + + @Override + public void observe( + final T option, + final double value + ) { + final int count = counts.getOrDefault(option, 0); + counts.put(option, count + 1); + final double oldValue = values.getOrDefault(option, 0.0); + values.put(option, (count * oldValue + value) / (count + 1)); + invalidateCache(); + } + +} diff --git a/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/choices/MapBasedOptionValues.java b/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/choices/MapBasedOptionValues.java new file mode 100644 index 000000000..65b736032 --- /dev/null +++ b/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/choices/MapBasedOptionValues.java @@ -0,0 +1,80 @@ +/* + * POSEIDON: an agent-based model of fisheries + * Copyright (c) 2024 CoHESyS Lab cohesys.lab@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package uk.ac.ox.poseidon.agents.behaviours.choices; + +import ec.util.MersenneTwisterFast; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.util.Map.Entry.comparingByValue; +import static one.util.streamex.MoreCollectors.maxAll; +import static uk.ac.ox.poseidon.core.MasonUtils.oneOf; + +public abstract class MapBasedOptionValues implements OptionValues { + protected final Map values = new HashMap<>(); + private List> cachedBest = null; + + @Override + public Optional getValue(final O option) { + return Optional.ofNullable(values.get(option)); + } + + @Override + public List getBestOptions() { + return getBestEntries().stream().map(Map.Entry::getKey).collect(toImmutableList()); + } + + @Override + public Optional getBestOption(final MersenneTwisterFast rng) { + return getBestEntry(rng).map(Map.Entry::getKey); + } + + @Override + public Optional getBestValue() { + return getBestEntries().stream().findAny().map(Map.Entry::getValue); + } + + @Override + public List> getBestEntries() { + if (cachedBest == null) { + cachedBest = values + .entrySet() + .stream() + .collect(maxAll(comparingByValue(), toImmutableList())); + } + return cachedBest; + } + + @Override + public Optional> getBestEntry(final MersenneTwisterFast rng) { + final List> bestEntries = getBestEntries(); + return bestEntries.isEmpty() + ? Optional.empty() + : Optional.of(oneOf(bestEntries, rng)); + } + + protected void invalidateCache() { + cachedBest = null; + } +} diff --git a/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/choices/OptionValues.java b/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/choices/OptionValues.java new file mode 100644 index 000000000..f85e3a921 --- /dev/null +++ b/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/choices/OptionValues.java @@ -0,0 +1,46 @@ +/* + * POSEIDON: an agent-based model of fisheries + * Copyright (c) 2024 CoHESyS Lab cohesys.lab@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package uk.ac.ox.poseidon.agents.behaviours.choices; + +import ec.util.MersenneTwisterFast; + +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; + +public interface OptionValues { + + void observe( + O option, + double value + ); + + Optional getValue(O option); + + List getBestOptions(); + + Optional getBestOption(MersenneTwisterFast rng); + + Optional getBestValue(); + + List> getBestEntries(); + + Optional> getBestEntry(MersenneTwisterFast rng); +} diff --git a/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/destination/EpsilonGreedyDestinationSupplier.java b/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/destination/EpsilonGreedyDestinationSupplier.java new file mode 100644 index 000000000..d71e37d88 --- /dev/null +++ b/agents/src/main/java/uk/ac/ox/poseidon/agents/behaviours/destination/EpsilonGreedyDestinationSupplier.java @@ -0,0 +1,59 @@ +/* + * POSEIDON: an agent-based model of fisheries + * Copyright (c) 2024 CoHESyS Lab cohesys.lab@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package uk.ac.ox.poseidon.agents.behaviours.destination; + +import ec.util.MersenneTwisterFast; +import sim.util.Int2D; + +import java.util.function.Supplier; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +class EpsilonGreedyDestinationSupplier implements DestinationSupplier { + + private final double epsilon; + private final Supplier greedyDestinationSupplier; + private final Supplier nonGreedyDestinationSupplier; + private final MersenneTwisterFast rng; + + EpsilonGreedyDestinationSupplier( + final double epsilon, + final Supplier greedyDestinationSupplier, + final Supplier nonGreedyDestinationSupplier, + final MersenneTwisterFast rng + ) { + checkArgument( + epsilon >= 0 && epsilon <= 1, + "epsilon must be between 0 and 1" + ); + this.epsilon = epsilon; + this.greedyDestinationSupplier = checkNotNull(greedyDestinationSupplier); + this.nonGreedyDestinationSupplier = checkNotNull(nonGreedyDestinationSupplier); + this.rng = checkNotNull(rng); + } + + @Override + public Int2D get() { + return rng.nextBoolean(epsilon) + ? greedyDestinationSupplier.get() + : nonGreedyDestinationSupplier.get(); + } +} diff --git a/agents/src/test/java/uk/ac/ox/poseidon/agents/behaviours/choices/AveragingOptionValuesTest.java b/agents/src/test/java/uk/ac/ox/poseidon/agents/behaviours/choices/AveragingOptionValuesTest.java new file mode 100644 index 000000000..c031e0cf1 --- /dev/null +++ b/agents/src/test/java/uk/ac/ox/poseidon/agents/behaviours/choices/AveragingOptionValuesTest.java @@ -0,0 +1,109 @@ +/* + * POSEIDON: an agent-based model of fisheries + * Copyright (c) 2024 CoHESyS Lab cohesys.lab@gmail.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package uk.ac.ox.poseidon.agents.behaviours.choices; + +import ec.util.MersenneTwisterFast; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +class AveragingOptionValuesTest { + + private AveragingOptionValues optionValues; + private MersenneTwisterFast rng; + + @BeforeEach + void setUp() { + optionValues = new AveragingOptionValues<>(); + rng = new MersenneTwisterFast(12345); // Fixed seed for repeatable results + } + + @Test + void testObserveSingleOption() { + optionValues.observe("OptionA", 10.0); + assertEquals(Optional.of(10.0), optionValues.getValue("OptionA")); + } + + @Test + void testObserveMultipleValuesForSameOption() { + optionValues.observe("OptionA", 10.0); + optionValues.observe("OptionA", 20.0); + assertEquals(Optional.of(15.0), optionValues.getValue("OptionA")); + } + + @Test + void testGetBestOptionWithTie() { + optionValues.observe("OptionA", 20.0); + optionValues.observe("OptionB", 20.0); + + final List bestOptions = optionValues.getBestOptions(); + assertTrue(bestOptions.contains("OptionA")); + assertTrue(bestOptions.contains("OptionB")); + assertEquals(2, bestOptions.size()); // Both options should be in the best options list + } + + @Test + void testRandomBestOptionSelectionWithTie() { + optionValues.observe("OptionA", 20.0); + optionValues.observe("OptionB", 20.0); + optionValues.observe("OptionC", 10.0); // Lower value, should not be selected + + // Collecting multiple random selections to verify randomness with ties + int countA = 0; + int countB = 0; + for (int i = 0; i < 1000; i++) { + final Optional bestOption = optionValues.getBestOption(rng); + assertTrue(bestOption.isPresent()); + if (bestOption.get().equals("OptionA")) countA++; + if (bestOption.get().equals("OptionB")) countB++; + } + + // With a large sample, both options should be selected approximately equally + assertTrue(countA > 400 && countB > 400); // Roughly equal distribution + } + + @Test + void testCacheInvalidationOnObserve() { + optionValues.observe("OptionA", 10.0); + optionValues.observe("OptionB", 20.0); + + // Cache the best option entries + final List initialBestOptions = optionValues.getBestOptions(); + assertTrue(initialBestOptions.contains("OptionB")); + + // Update OptionA to make it the best, invalidating the cache + optionValues.observe("OptionA", 30.0); // Average should now be equal to OptionB + + final List updatedBestOptions = optionValues.getBestOptions(); + assertTrue(updatedBestOptions.contains("OptionA")); + assertTrue(updatedBestOptions.contains("OptionB")); + } + + @Test + void testGetBestWhenNoObservations() { + assertTrue(optionValues.getBestOptions().isEmpty()); + assertFalse(optionValues.getBestOption(rng).isPresent()); + assertFalse(optionValues.getBestValue().isPresent()); + } +} diff --git a/buildSrc/src/main/kotlin/buildlogic.java-common-conventions.gradle.kts b/buildSrc/src/main/kotlin/buildlogic.java-common-conventions.gradle.kts index 930be44e0..dcb0dff9e 100644 --- a/buildSrc/src/main/kotlin/buildlogic.java-common-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/buildlogic.java-common-conventions.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation("org.projectlombok:lombok:1.18.30") annotationProcessor("org.projectlombok:lombok:1.18.30") implementation("com.google.guava:guava:33.2.1-jre") + implementation("one.util:streamex:0.8.3") testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") testImplementation("net.jqwik:jqwik:1.9.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher")