All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.oliviercailloux.grade.Grade Maven / Gradle / Ivy

The newest version!
package io.github.oliviercailloux.grade;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Verify.verify;

import com.google.common.base.MoreObjects;
import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.MoreCollectors;
import com.google.common.math.DoubleMath;
import io.github.oliviercailloux.grade.IGrade.CriteriaPath;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A tree with marks at each node, plus, for non-leaf nodes, the ability to explain the aggregation
 * strategy used to obtain them.
 * 

* Equivalent to a single mark iff there are no children in the (root of the) tree. *

*/ public class Grade { @SuppressWarnings("unused") private static final Logger LOGGER = LoggerFactory.getLogger(Grade.class); public static Grade transformToPerCriterionWeighting(Grade original) { /* * We want to transform aggregator and marks tree so that the aggregator still accepts every * tree it originally accepted and the resulting mark is the same, while losing as few * information as possible. We want the transformed aggregation to have fixed weights, in the * sense that it gives to every node a weight that is independent of the marks tree (i.e., a * weight that depends only on the aggregator). * * Unfortunately, it is not sufficient to transform a grade into a grade by visiting * simultaneously the marks tree and the aggregator tree: this might transform aggregators * differently (from one user to another, even if they start with the same aggregator). That is * because different trees stop at different places due to their limited depths, so this method * would not ensure exploration of the whole aggregator. */ final WeightingGradeAggregator transformedAggregator = transformToPerCriterionWeighting(original.toAggregator()); final MarksTree transformedMarks = adaptMarksForPerCriterionWeighting(original); final Grade transformed = Grade.given(transformedAggregator, transformedMarks); verify( DoubleMath.fuzzyEquals(original.mark().getPoints(), transformed.mark().getPoints(), 1e-6d)); return transformed; } public static WeightingGradeAggregator transformToPerCriterionWeighting(GradeAggregator original) { if (original instanceof WeightingGradeAggregator w) { return w; } final MarkAggregator a = original.getMarkAggregator(); if ((a instanceof MaxAggregator || a instanceof MinAggregator) && original.getSpecialSubAggregators().isEmpty()) { return transformToPerCriterionWeighting(original.getDefaultSubAggregator()); } /* * Note that switching, for example, from ParametricWeighter to AbsoluteAggregator extends the * admissible sub-trees. */ final ImmutableMap specialSubs = original.getSpecialSubAggregators(); /* * Some further pruning is possible (and might be welcome) as some of these sub-aggregators will * never be used: in parametric nodes, the weighting branch will not be used as the * corresponding marks tree is reduced to an absolute mark; and max nodes treated here become * terminal nodes in marks trees. */ final Map transformedSubs = Maps.transformValues(specialSubs, Grade::transformToPerCriterionWeighting); final PerCriterionWeighter newAggregator = (a instanceof PerCriterionWeighter c) ? c : AbsoluteAggregator.INSTANCE; return WeightingGradeAggregator.given(newAggregator, transformedSubs, transformToPerCriterionWeighting(original.getDefaultSubAggregator())); // return switch(original.getMarkAggregator()) { // case ParametricWeighter w -> AbsoluteAggregator.INSTANCE; // default -> throw new IllegalArgumentException("Unexpected value: " + // original.getMarkAggregator()); // } } public static GradeAggregator integrateMaxesTODO(GradeAggregator original) { if (original instanceof WeightingGradeAggregator w) { return w; } final MarkAggregator a = original.getMarkAggregator(); if (a instanceof OwaWeighter && original.getSpecialSubAggregators().isEmpty()) { return transformToPerCriterionWeighting(original.getDefaultSubAggregator()); } /* * Note that switching, for example, from ParametricWeighter to AbsoluteAggregator extends the * admissible sub-trees. */ final ImmutableMap specialSubs = original.getSpecialSubAggregators(); /* * Some further pruning is possible (and might be welcome) as some of these sub-aggregators will * never be used: in parametric nodes, the weighting branch will not be used as the * corresponding marks tree is reduced to an absolute mark; and max nodes treated here become * terminal nodes in marks trees. */ final Map transformedSubs = Maps.transformValues(specialSubs, Grade::transformToPerCriterionWeighting); final PerCriterionWeighter newAggregator = (a instanceof PerCriterionWeighter c) ? c : AbsoluteAggregator.INSTANCE; return WeightingGradeAggregator.given(newAggregator, transformedSubs, transformToPerCriterionWeighting(original.getDefaultSubAggregator())); // return switch(original.getMarkAggregator()) { // case ParametricWeighter w -> AbsoluteAggregator.INSTANCE; // default -> throw new IllegalArgumentException("Unexpected value: " + // original.getMarkAggregator()); // } } public static MarksTree adaptMarksForPerCriterionWeighting(Grade original) { if (original.getWeightedSubMarks().isEmpty()) { return original.toMarksTree(); } final GradeAggregator originalAggregator = original.toAggregator(); final MarksTree originalMarks = original.toMarksTree(); final MarkAggregator a = originalAggregator.getMarkAggregator(); final boolean parametricWeightedSum = (a instanceof ParametricWeighter p) && originalMarks.getCriteria().size() == 3; // final boolean owaAndMultipleSubs = (a instanceof OwaWeighter) // && !originalAggregator.getSpecialSubAggregators().isEmpty(); // final boolean reducibleOwa = (a instanceof OwaAggregator o) // && o.weights().stream().filter(w -> w != 0d).count() == 1 // && originalAggregator.getSpecialSubAggregators().isEmpty(); final boolean reducibleOwa = (a instanceof MaxAggregator || a instanceof MinAggregator) && originalAggregator.getSpecialSubAggregators().isEmpty(); final boolean nonReducibleOwa = (a instanceof OwaWeighter) && !reducibleOwa; if (nonReducibleOwa || parametricWeightedSum || (a instanceof NormalizingStaticWeighter)) { /* * All these criteria are associated to dynamic weights (weights that depend on the marks * tree), that we thus can’t integrate into a static structure (an aggregator that would not * depend on the tree), so we must prune the tree. * * TODO include here the case of OWA with a non-unique non-zero value. And consider keeping * more of the structure by flattening the sub-criteria and averaging them, if it makes sense. */ final ImmutableMap weightedSubMarks = original.getWeightedSubMarks(); final ImmutableMap absoluteMarks = weightedSubMarks.keySet().stream() .collect(ImmutableMap.toImmutableMap(SubMark::getCriterion, s -> Mark.given(s.getPoints() * weightedSubMarks.get(s), s.comment()))); LOGGER.debug("Reduced to absolute: from {} to {}.", weightedSubMarks, absoluteMarks); return MarksTree.composite(absoluteMarks); } if ((a instanceof ParametricWeighter p) && originalMarks.getCriteria().size() == 2) { final ImmutableMap weightedSubMarks = p.weightsWithPenalty(original.subMarks()); LOGGER.debug("Given {}, points: {}.", weightedSubMarks, weightedSubMarks.keySet().stream().collect(ImmutableMap.toImmutableMap( SubMark::getCriterion, s -> s.getPoints() * weightedSubMarks.get(s)))); final ImmutableMap absoluteMarks = weightedSubMarks.keySet().stream() .collect(ImmutableMap.toImmutableMap(SubMark::getCriterion, s -> Mark.given(s.getPoints() * weightedSubMarks.get(s), s.comment()))); verify(absoluteMarks.size() == 2); final Criterion multipliedCriterion = p.multipliedCriterion(); verify(absoluteMarks.keySet().contains(multipliedCriterion)); final LinkedHashMap newMarks = new LinkedHashMap<>(absoluteMarks); newMarks.put(multipliedCriterion, adaptMarksForPerCriterionWeighting(original.getGrade(multipliedCriterion))); return MarksTree.composite(newMarks); } if (reducibleOwa) { final ImmutableMap weightedSubMarks = original.getWeightedSubMarks(); final Criterion criterionWithAllWeight = Maps.filterEntries(weightedSubMarks, e -> e.getValue() != 0d).keySet().stream() .collect(MoreCollectors.onlyElement()).getCriterion(); /* * TODO this forgets the criterion, I suppose, therefore inducing a loss of information. */ return adaptMarksForPerCriterionWeighting(original.getGrade(criterionWithAllWeight)); } if (a instanceof CriteriaWeighter) { final ImmutableSet criteria = original.toMarksTree().getCriteria(); final ImmutableMap subTrees = criteria.stream().collect(ImmutableMap .toImmutableMap(c -> c, c -> adaptMarksForPerCriterionWeighting(original.getGrade(c)))); return MarksTree.composite(subTrees); } throw new VerifyException(a.toString()); } public static Grade given(GradeAggregator aggregator, MarksTree marks) { return new Grade(aggregator, marks); } private final GradeAggregator aggregator; private final MarksTree marks; private final ImmutableMap weightedSubMarks; private final Mark mark; private final ImmutableMap subGrades; private Grade(GradeAggregator aggregator, MarksTree marks) throws AggregatorException { this.aggregator = checkNotNull(aggregator); this.marks = checkNotNull(marks); /* To check that it is able to compute it. */ subGrades = marks.getCriteria().stream() .collect(ImmutableMap.toImmutableMap(c -> c, this::computeGrade)); weightedSubMarks = computeWeightedSubMarks(); mark = computeMark(); } public Mark mark() { return mark; } public Mark mark(Criterion criterion) { return getGrade(criterion).mark(); } public Mark mark(CriteriaPath path) { return getGrade(path).mark(); } private Mark computeMark() { if (marks.isMark()) { return marks.getMark(CriteriaPath.ROOT); } final double weightedSum = weightedSubMarks.keySet().stream() .mapToDouble(s -> weightedSubMarks.get(s) * s.getPoints()).sum(); return Mark.given(Double.min(1d, Double.max(weightedSum, 0d)), ""); } /** * Returns a mark aggregator able to aggregate the children of this grade node (and possibly other * ones). */ public MarkAggregator getMarkAggregator() { return aggregator.getMarkAggregator(); } public MarkAggregator getMarkAggregator(Criterion criterion) { return aggregator.getGradeAggregator(criterion).getMarkAggregator(); } public MarkAggregator getMarkAggregator(CriteriaPath path) { return aggregator.getGradeAggregator(path).getMarkAggregator(); } /** * @throws NoSuchElementException iff the given criterion is not in this tree. */ public double getWeight(Criterion criterion) throws NoSuchElementException { return weightedSubMarks.keySet().stream().filter(s -> s.getCriterion().equals(criterion)) .map(weightedSubMarks::get).collect(MoreCollectors.onlyElement()); } public double getWeight(CriteriaPath path) throws NoSuchElementException { if (path.isRoot()) { return 1d; } return getWeight(path.getHead()) * getGrade(path.getHead()).getWeight(path.withoutHead()); } /** * @return the criteria in the key set equal the child criteria at the root of this tree. */ public ImmutableMap getWeightedSubMarks() { return weightedSubMarks; } private ImmutableMap computeWeightedSubMarks() throws AggregatorException { final ImmutableSet subMarks = subMarks(); final MarkAggregator markAggregator = aggregator.getMarkAggregator(); final ImmutableMap weights = markAggregator.weights(subMarks); LOGGER.debug("Obtained via {}, from {}: {}.", markAggregator, subMarks, weights); verify(subMarks.size() == weights.size()); return weights; } private ImmutableSet subMarks() { return marks.getCriteria().stream().map(c -> SubMark.given(c, getGrade(c).mark())) .collect(ImmutableSet.toImmutableSet()); } /** * @throws NoSuchElementException iff the given criterion is not in this tree. */ public Grade getGrade(Criterion criterion) throws NoSuchElementException { return Optional.ofNullable(subGrades.get(criterion)) .orElseThrow(() -> new NoSuchElementException(criterion.getName())); } private Grade computeGrade(Criterion criterion) throws NoSuchElementException, AggregatorException { /* * Note that performance would be much better by returning a Grade that delegates to this object * with a shifted root (the shift being a CriteriaPath). */ final MarksTree subMarks = marks.getTree(criterion); final GradeAggregator subAggregator = aggregator.getGradeAggregator(criterion); return given(subAggregator, subMarks); } /** * @throws NoSuchElementException iff the given criterion path is not in this tree. */ public Grade getGrade(CriteriaPath path) throws NoSuchElementException { if (path.isRoot()) { return this; } return getGrade(path.getHead()).getGrade(path.withoutHead()); } public MarksTree toMarksTree() { return marks; } /** * Returns the aggregator associated to this grade (able to aggregate the marks tree associated to * this grade and possibly other ones) */ public GradeAggregator toAggregator() { return aggregator; } @Override public String toString() { return MoreObjects.toStringHelper(this).add("Aggregator", aggregator).add("Marks", marks) .toString(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy