Skip to content

Commit

Permalink
add AveragingOptionValues class to be used with choice algorithms
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolaspayette committed Nov 12, 2024
1 parent 6752aa0 commit 3f47d34
Show file tree
Hide file tree
Showing 8 changed files with 345 additions and 0 deletions.
1 change: 1 addition & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules/agents/POSEIDON.agents.test.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* POSEIDON: an agent-based model of fisheries
* Copyright (c) 2024 CoHESyS Lab [email protected]
*
* 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 <http://www.gnu.org/licenses/>.
*
*/

package uk.ac.ox.poseidon.agents.behaviours.choices;

import java.util.HashMap;
import java.util.Map;

public class AveragingOptionValues<T> extends MapBasedOptionValues<T> {

private final Map<T, Integer> 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();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* POSEIDON: an agent-based model of fisheries
* Copyright (c) 2024 CoHESyS Lab [email protected]
*
* 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 <http://www.gnu.org/licenses/>.
*
*/

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<O> implements OptionValues<O> {
protected final Map<O, Double> values = new HashMap<>();
private List<Map.Entry<O, Double>> cachedBest = null;

@Override
public Optional<Double> getValue(final O option) {
return Optional.ofNullable(values.get(option));
}

@Override
public List<O> getBestOptions() {
return getBestEntries().stream().map(Map.Entry::getKey).collect(toImmutableList());
}

@Override
public Optional<O> getBestOption(final MersenneTwisterFast rng) {
return getBestEntry(rng).map(Map.Entry::getKey);
}

@Override
public Optional<Double> getBestValue() {
return getBestEntries().stream().findAny().map(Map.Entry::getValue);
}

@Override
public List<Map.Entry<O, Double>> getBestEntries() {
if (cachedBest == null) {
cachedBest = values
.entrySet()
.stream()
.collect(maxAll(comparingByValue(), toImmutableList()));
}
return cachedBest;
}

@Override
public Optional<Map.Entry<O, Double>> getBestEntry(final MersenneTwisterFast rng) {
final List<Map.Entry<O, Double>> bestEntries = getBestEntries();
return bestEntries.isEmpty()
? Optional.empty()
: Optional.of(oneOf(bestEntries, rng));
}

protected void invalidateCache() {
cachedBest = null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* POSEIDON: an agent-based model of fisheries
* Copyright (c) 2024 CoHESyS Lab [email protected]
*
* 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 <http://www.gnu.org/licenses/>.
*
*/

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<O> {

void observe(
O option,
double value
);

Optional<Double> getValue(O option);

List<O> getBestOptions();

Optional<O> getBestOption(MersenneTwisterFast rng);

Optional<Double> getBestValue();

List<Entry<O, Double>> getBestEntries();

Optional<Entry<O, Double>> getBestEntry(MersenneTwisterFast rng);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* POSEIDON: an agent-based model of fisheries
* Copyright (c) 2024 CoHESyS Lab [email protected]
*
* 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 <http://www.gnu.org/licenses/>.
*
*/

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<Int2D> greedyDestinationSupplier;
private final Supplier<Int2D> nonGreedyDestinationSupplier;
private final MersenneTwisterFast rng;

EpsilonGreedyDestinationSupplier(
final double epsilon,
final Supplier<Int2D> greedyDestinationSupplier,
final Supplier<Int2D> 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* POSEIDON: an agent-based model of fisheries
* Copyright (c) 2024 CoHESyS Lab [email protected]
*
* 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 <http://www.gnu.org/licenses/>.
*
*/

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<String> 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<String> 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<String> 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<String> 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<String> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 3f47d34

Please sign in to comment.