Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: address various issues identified during benchmarking #90

Merged
merged 1 commit into from
Jun 14, 2023
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ai.timefold.solver.benchmark.impl.statistic;

import java.util.ArrayList;
import java.util.List;

import jakarta.xml.bind.annotation.XmlSeeAlso;
Expand Down Expand Up @@ -30,7 +31,7 @@ public abstract class PureSubSingleStatistic<Solution_, StatisticPoint_ extends
protected SingleStatisticType singleStatisticType;

@XmlTransient
protected List<Chart_> chartList;
protected List<Chart_> chartList = new ArrayList<>();

protected PureSubSingleStatistic() {
// For JAXB.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ protected List<LineChart<Long, Long>> generateCharts(BenchmarkReport benchmarkRe
long timeMillisSpent = point.getTimeMillisSpent();
long mutationCount = point.getMutationCount();
builder.add(solverLabel, timeMillisSpent, mutationCount);
builder.add(solverLabel, timeMillisSpent + 1, 0L); // Drop back to zero.
}
}
}
return singletonList(builder.build("bestSolutionMutationProblemStatisticChart",
problemBenchmarkResult.getName() + " best solution mutation statistic", "Time spent",
"Best solution mutation count.", true, true, false));
"Best solution mutation count", true, true, false));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ var ${chartId} = new Chart(document.getElementById('${chartId}'), {
}
</#if>
},
suggestedMin: ${chart.yMin()?cn},
suggestedMax: ${chart.yMax()?cn},
type: '<#if yAxisLogarithmic>logarithmic<#else>linear</#if>',
display: true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,6 @@ var ${chartId} = new Chart(document.getElementById('${chartId}'), {
}
</#if>
},
suggestedMin: ${chart.yMin()?cn},
suggestedMax: ${chart.yMax()?cn},
type: '<#if yAxisLogarithmic>logarithmic<#else>linear</#if>',
display: true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import ai.timefold.solver.core.api.score.stream.bi.BiConstraintStream;
import ai.timefold.solver.core.api.score.stream.bi.BiJoiner;
import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream;
import ai.timefold.solver.core.config.util.ConfigUtils;
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
Expand Down Expand Up @@ -55,9 +54,7 @@ public <A> BiConstraintStream<A, A> forEachUniquePair(Class<A> sourceClass, BiJo

private <A> DefaultBiJoiner<A, A> buildLessThanId(Class<A> sourceClass) {
SolutionDescriptor<Solution_> solutionDescriptor = getSolutionDescriptor();
MemberAccessor planningIdMemberAccessor =
ConfigUtils.findPlanningIdMemberAccessor(sourceClass, solutionDescriptor.getMemberAccessorFactory(),
solutionDescriptor.getDomainAccessType());
MemberAccessor planningIdMemberAccessor = solutionDescriptor.getPlanningIdAccessor(sourceClass);
if (planningIdMemberAccessor == null) {
throw new IllegalArgumentException("The fromClass (" + sourceClass + ") has no member with a @"
+ PlanningId.class.getSimpleName() + " annotation,"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package ai.timefold.solver.core.impl.domain.solution.descriptor;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.function.Function;

import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;

final class DummyMemberAccessor implements MemberAccessor {

static final MemberAccessor INSTANCE = new DummyMemberAccessor();

private DummyMemberAccessor() {

}

@Override
public Class<?> getDeclaringClass() {
return null;
}

@Override
public String getName() {
return null;
}

@Override
public Class<?> getType() {
return null;
}

@Override
public Type getGenericType() {
return null;
}

@Override
public Object executeGetter(Object bean) {
return null;
}

@Override
public <Fact_, Result_> Function<Fact_, Result_> getGetterFunction() {
return null;
}

@Override
public boolean supportSetter() {
return false;
}

@Override
public void executeSetter(Object bean, Object value) {

}

@Override
public String getSpeedNote() {
return null;
}

@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
return null;
}

@Override
public <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ private static List<Class<?>> sortEntityClassList(List<Class<?>> entityClassList
private final Map<Class<?>, EntityDescriptor<Solution_>> entityDescriptorMap = new LinkedHashMap<>();
private final List<Class<?>> reversedEntityClassList = new ArrayList<>();
private final ConcurrentMap<Class<?>, EntityDescriptor<Solution_>> lowestEntityDescriptorMap = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<?>, MemberAccessor> planningIdMemberAccessorMap = new ConcurrentHashMap<>();

private SolutionCloner<Solution_> solutionCloner;
private boolean assertModelForCloning = false;
Expand Down Expand Up @@ -855,6 +856,28 @@ public int getEntityCount(Solution_ solution) {
return entityCount.intValue();
}

/**
* Return accessor for a given member of a given class, if present,
* and cache it for future use.
*
* @param factClass never null
* @return null if no such member exists
*/
public MemberAccessor getPlanningIdAccessor(Class<?> factClass) {
MemberAccessor memberAccessor = planningIdMemberAccessorMap.get(factClass);
if (memberAccessor == null) {
memberAccessor =
ConfigUtils.findPlanningIdMemberAccessor(factClass, getMemberAccessorFactory(), getDomainAccessType());
MemberAccessor nonNullMemberAccessor = Objects.requireNonNullElse(memberAccessor, DummyMemberAccessor.INSTANCE);
planningIdMemberAccessorMap.put(factClass, nonNullMemberAccessor);
return memberAccessor;
} else if (memberAccessor == DummyMemberAccessor.INSTANCE) {
return null;
} else {
return memberAccessor;
}
}

public void visitAllEntities(Solution_ solution, Consumer<Object> visitor) {
visitAllEntities(solution, visitor, collection -> collection.forEach(visitor));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import java.util.List;

import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;

/**
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
Expand Down Expand Up @@ -35,11 +37,32 @@ public int countMutations(Solution_ a, Solution_ b) {
Object bEntity = bIt.next();
for (GenuineVariableDescriptor<Solution_> variableDescriptor : entityDescriptor
.getGenuineVariableDescriptorList()) {
// TODO broken if the value is an entity, because then it's never the same
// But we don't want to depend on value/entity equals() => use surrogate entity IDs to compare
// https://issues.redhat.com/browse/PLANNER-170
if (variableDescriptor.getValue(aEntity) != variableDescriptor.getValue(bEntity)) {
mutationCount++;
if (variableDescriptor.isListVariable()) {
ListVariableDescriptor<Solution_> listVariableDescriptor =
(ListVariableDescriptor<Solution_>) variableDescriptor;
List<Object> aValues = listVariableDescriptor.getListVariable(aEntity);
List<Object> bValues = listVariableDescriptor.getListVariable(bEntity);
int aSize = aValues.size();
int bSize = bValues.size();
if (aSize != bSize) {
// First add mutations for the elements that are missing in one list.
mutationCount += Math.abs(aSize - bSize);
}
// Then iterate over the list and count every item that is different.
int shorterListSize = Math.min(aSize, bSize);
for (int i = 0; i < shorterListSize; i++) {
Object aValue = aValues.get(i);
Object bValue = bValues.get(i);
if (areDifferent(aValue, bValue)) {
mutationCount++;
}
}
} else {
Object aValue = variableDescriptor.getValue(aEntity);
Object bValue = variableDescriptor.getValue(bEntity);
if (areDifferent(aValue, bValue)) {
mutationCount++;
}
}
}
}
Expand All @@ -51,6 +74,44 @@ public int countMutations(Solution_ a, Solution_ b) {
return mutationCount;
}

private boolean areDifferent(Object aValue, Object bValue) {
EntityDescriptor<Solution_> aValueEntityDescriptor =
solutionDescriptor.findEntityDescriptor(aValue.getClass());
EntityDescriptor<Solution_> bValueEntityDescriptor =
solutionDescriptor.findEntityDescriptor(bValue.getClass());
if (aValueEntityDescriptor == null && bValueEntityDescriptor == null) { // Neither are entities.
if (aValue == bValue) {
return false;
}
return areDifferentPlanningIds(aValue, bValue);
} else if (aValueEntityDescriptor != null && bValueEntityDescriptor != null) {
/*
* Both are entities.
* Entities will all be cloned and therefore different.
* But maybe they have the same planning ID?
*/
if (aValueEntityDescriptor != bValueEntityDescriptor) {
// Different entities means mutation guaranteed.
return true;
}
return areDifferentPlanningIds(aValue, bValue);
} else { // One is entity and the other one is not.
return true;
}
}

private boolean areDifferentPlanningIds(Object aValue, Object bValue) {
MemberAccessor aIdAccessor = solutionDescriptor.getPlanningIdAccessor(aValue.getClass());
MemberAccessor bIdAccessor = solutionDescriptor.getPlanningIdAccessor(bValue.getClass());
if (aIdAccessor != null && bIdAccessor != null) {
Object aId = aIdAccessor.executeGetter(aValue);
Object bId = bIdAccessor.executeGetter(bValue);
return !aId.equals(bId);
} else {
return aValue != bValue; // This counts all entities that get as far as here.
}
}

@Override
public String toString() {
return "MutationCounter(" + solutionDescriptor + ")";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ public String getSimpleMoveTypeDescription() {

@Override
public Collection<?> getPlanningEntities() {
if (firstEntity == secondEntity) {
return Collections.singleton(firstEntity);
}
return Set.of(firstEntity, secondEntity);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
Expand All @@ -25,7 +24,6 @@
import ai.timefold.solver.core.api.score.stream.ConstraintJustification;
import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification;
import ai.timefold.solver.core.config.solver.EnvironmentMode;
import ai.timefold.solver.core.config.util.ConfigUtils;
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
import ai.timefold.solver.core.impl.domain.constraintweight.descriptor.ConstraintConfigurationDescriptor;
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
Expand Down Expand Up @@ -58,7 +56,6 @@ public abstract class AbstractScoreDirector<Solution_, Score_ extends Score<Scor

protected final transient Logger logger = LoggerFactory.getLogger(getClass());

private final Map<Class, MemberAccessor> planningIdAccessorCacheMap = new HashMap<>(0);
protected final Factory_ scoreDirectorFactory;
protected final boolean lookUpEnabled;
protected final LookUpManager lookUpManager;
Expand Down Expand Up @@ -175,15 +172,8 @@ public void assertNonNullPlanningIds() {
}

private void assertNonNullPlanningId(Object fact) {
Class factClass = fact.getClass();
// Cannot use Map.computeIfAbsent(), as we also want to cache null values.
if (!planningIdAccessorCacheMap.containsKey(factClass)) {
SolutionDescriptor<Solution_> solutionDescriptor = getSolutionDescriptor();
planningIdAccessorCacheMap.put(factClass,
ConfigUtils.findPlanningIdMemberAccessor(factClass, solutionDescriptor.getMemberAccessorFactory(),
solutionDescriptor.getDomainAccessType()));
}
MemberAccessor planningIdAccessor = planningIdAccessorCacheMap.get(factClass);
Class<?> factClass = fact.getClass();
MemberAccessor planningIdAccessor = getSolutionDescriptor().getPlanningIdAccessor(factClass);
if (planningIdAccessor == null) { // There is no planning ID annotation.
return;
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,11 @@ image::benchmarking-and-tweaking/bestSolutionMutationStatistic.png[align="center

Use Tabu Search - an algorithm that behaves like a human - to get an estimation on how difficult it would be for a human to improve the previous best solution to that new best solution.

[NOTE]
====
This statistic can slow down the solver noticeably, which affects the benchmark results.
That's why it is optional and not enabled by default.
====

[[benchmarkReportMoveCountPerStepStatistic]]
=== Move count per step statistic (graph and CSV)
Expand Down Expand Up @@ -842,7 +847,7 @@ Multiple `singleStatisticType` elements are allowed.

[NOTE]
====
These statistic per single benchmark can slow down the solver noticeably, which affects the benchmark results.
This statistic per single benchmark can slow down the solver noticeably, which affects the benchmark results.
That's why they are optional and not enabled by default.
====

Expand Down