io.github.oliviercailloux.grade.utils.Compressor Maven / Gradle / Ivy
The newest version!
package io.github.oliviercailloux.grade.utils;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Verify.verify;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.graph.ImmutableGraph;
import com.google.common.math.DoubleMath;
import io.github.oliviercailloux.grade.Criterion;
import io.github.oliviercailloux.grade.IGrade;
import io.github.oliviercailloux.grade.IGrade.CriteriaPath;
import io.github.oliviercailloux.grade.WeightingGrade.WeightedGrade;
import io.github.oliviercailloux.grade.WeightingGrade.WeightedMark;
import io.github.oliviercailloux.grade.old.GradeStructure;
import io.github.oliviercailloux.grade.old.Mark;
import io.github.oliviercailloux.utils.Utils;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Compressor {
@SuppressWarnings("unused")
private static final Logger LOGGER = LoggerFactory.getLogger(Compressor.class);
public static IGrade compress(IGrade grade, GradeStructure model) {
final WeightedGrade compressed = compress(ImmutableSet.of(CriteriaPath.ROOT), grade, model);
verify(DoubleMath.fuzzyEquals(compressed.getWeight(), 1d, 1e-6d));
final IGrade compressedGrade = compressed.getGrade();
verify(DoubleMath.fuzzyEquals(compressedGrade.getPoints(), grade.getPoints(), 1e-6d));
verify(model.getSuccessorCriteria(CriteriaPath.ROOT)
.containsAll(compressedGrade.getSubGrades().keySet()));
verify(model.asGraph().edges().containsAll(compressedGrade.toTree().asGraph().edges()));
return compressedGrade;
}
private static WeightedGrade compress(Set originalPaths, IGrade grade,
GradeStructure model) {
LOGGER.debug("Compressing from {} targetting {}.", originalPaths, model);
checkArgument(!originalPaths.isEmpty());
/**
* It would be cleaner to build a tree of grades: at model path [a], a grade [x/a/blah,
* x/a/blih, y/a/stuff, …]; given such a grade and a model (the part following [a]), return for
* the sub-model-branch [a/c] the grade [x/a/blah/c, x/a/blih/c, …]. When can’t match more
* sub-model-branches, we’re done for that branch (compress to a mark). Thus, suffices to return
* one WeightedGrade for each model branch.
*/
final ImmutableSet nextLevel = model.getSuccessorCriteria(CriteriaPath.ROOT);
final GradeStructure tree = grade.toTree();
final ImmutableSet allPaths = tree.getPaths();
checkArgument(originalPaths.stream().allMatch(allPaths::contains), originalPaths.stream()
.filter(p -> !allPaths.contains(p)).collect(ImmutableSet.toImmutableSet()));
final ImmutableGraph graph = tree.asGraph();
final ImmutableSet paths;
if (nextLevel.isEmpty()) {
paths = ImmutableSet.of();
} else {
paths = findPaths(nextLevel, originalPaths, tree);
}
final ImmutableSet nextCriteria =
paths.stream().map(p -> tree.getSuccessorCriteria(p)).distinct()
.collect(Utils.singleOrEmpty()).orElse(ImmutableSet.of());
LOGGER.debug("Registering next criteria: {}.", nextCriteria);
verify(nextLevel.containsAll(nextCriteria));
final WeightedGrade compressed;
if (nextCriteria.isEmpty()) {
final ImmutableSet leaves = originalPaths.stream()
.flatMap(p -> graph.nodes().stream()
.filter(p2 -> p2.startsWith(p) && graph.successors(p2).isEmpty()))
.collect(ImmutableSet.toImmutableSet());
verify(!leaves.isEmpty());
compressed = compressMarks(Maps.toMap(leaves, l -> grade.getWeightedMark(l)));
} else {
final ImmutableMap.Builder builder = ImmutableMap.builder();
for (Criterion criterion : nextCriteria) {
final GradeStructure sub = model.getStructure(criterion);
final Set subPaths =
paths.stream().map(p -> p.withSuffix(criterion)).collect(ImmutableSet.toImmutableSet());
final WeightedGrade subGrade = compress(subPaths, grade, sub);
builder.put(criterion, subGrade);
}
final ImmutableMap subGrades = builder.build();
verify(subGrades.values().stream().anyMatch(g -> g.getWeight() > 0d), subGrades.toString());
compressed = WeightedGrade.given(subGrades);
}
if (compressed.getGrade().getSubGrades().isEmpty() && model.getPaths().size() >= 2) {
LOGGER.debug("Failed compressing at: {}, {}.", originalPaths, model);
}
return compressed;
}
public static Mark toMark(IGrade grade) {
return (Mark) compress(grade, GradeStructure.given(ImmutableSet.of(CriteriaPath.ROOT)));
}
/**
* @param marks the keys are used only as part of the comment of the resulting grade
* @return a mark that aggregates all the given grades (with weighted sum of the points) and with
* a weight of the sum of the given weights.
*/
static WeightedMark compressMarks(Map marks) {
checkArgument(!marks.isEmpty());
if (marks.size() == 1) {
return Iterables.getOnlyElement(marks.values());
}
final double sumOfAbsolutePoints =
marks.values().stream().mapToDouble(WeightedGrade::getAbsolutePoints).sum();
final String comment = marks.keySet().stream()
.map(l -> l.withoutTail().toSimpleString() + " – " + marks.get(l).getGrade().getComment())
.collect(Collectors.joining("\n"));
final double weight = marks.values().stream().mapToDouble(WeightedGrade::getWeight).sum();
final double normalizedPoints = sumOfAbsolutePoints / weight;
final Mark mark = Mark.given(normalizedPoints, comment);
return WeightedMark.given(mark, weight);
}
private static ImmutableSet findPaths(Set nodes,
Set startingPaths, GradeStructure tree) {
checkArgument(!startingPaths.isEmpty());
final Set> nodeSets = Sets.powerSet(nodes);
final Iterable> wholeFirst = Iterables.concat(ImmutableSet.of(nodes), nodeSets);
ImmutableSet result = null;
for (Set nodeSubset : wholeFirst) {
if (nodeSubset.isEmpty()) {
continue;
}
final ImmutableSet found = findConformingPaths(nodeSubset, startingPaths, tree);
if (!found.isEmpty()) {
result = found;
break;
}
}
if (result == null) {
result = ImmutableSet.of();
}
LOGGER.debug("Searching for {} starting from {} among {}, found {}.", nodes, startingPaths,
tree, result);
return result;
}
/**
* @param nodes the criteria that must be children of all paths, non empty
* @param startingPath the starting path in the tree
* @param tree containing the paths among which to search
* @return a set of paths that cover a pruned version of the tree, such that all paths end with
* the given set of nodes; an empty set iff such a cover does not exist
*/
private static ImmutableSet findConformingPaths(Set nodes,
CriteriaPath startingPath, GradeStructure tree) {
checkArgument(!nodes.isEmpty());
final ArrayDeque toCheck = new ArrayDeque<>();
toCheck.add(startingPath);
final ImmutableSet.Builder builder = ImmutableSet.builder();
boolean failed = false;
do {
final CriteriaPath current = toCheck.pop();
final ImmutableSet succ = tree.getSuccessorCriteria(current);
if (succ.isEmpty()) {
failed = true;
break;
}
final ImmutableSet toVisit;
if (succ.containsAll(nodes)) {
builder.add(current);
toVisit = Sets.difference(succ, nodes).immutableCopy();
} else {
toVisit = succ;
}
toCheck
.addAll(toVisit.stream().map(current::withSuffix).collect(ImmutableSet.toImmutableSet()));
} while (!toCheck.isEmpty());
if (failed) {
return ImmutableSet.of();
}
return builder.build();
}
private static ImmutableSet findConformingPaths(Set nodes,
Set startingPaths, GradeStructure tree) {
final ImmutableSet.Builder builder = ImmutableSet.builder();
for (CriteriaPath startingPath : startingPaths) {
final ImmutableSet found = findConformingPaths(nodes, startingPath, tree);
if (found.isEmpty()) {
return ImmutableSet.of();
}
builder.addAll(found);
}
return builder.build();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy