
io.github.oliviercailloux.javagrade.utils.Summarizer Maven / Gradle / Ivy
The newest version!
package io.github.oliviercailloux.javagrade.utils;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verify;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.MoreCollectors;
import com.google.common.collect.Multiset.Entry;
import com.google.common.collect.Multisets;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.graph.GraphBuilder;
import com.google.common.graph.ImmutableGraph;
import io.github.oliviercailloux.git.github.model.GitHubUsername;
import io.github.oliviercailloux.grade.Criterion;
import io.github.oliviercailloux.grade.CriterionGradeWeight;
import io.github.oliviercailloux.grade.Exam;
import io.github.oliviercailloux.grade.Grade;
import io.github.oliviercailloux.grade.IGrade;
import io.github.oliviercailloux.grade.IGrade.CriteriaPath;
import io.github.oliviercailloux.grade.MarksTree;
import io.github.oliviercailloux.grade.WeightingGrade;
import io.github.oliviercailloux.grade.WeightingGrade.PathGradeWeight;
import io.github.oliviercailloux.grade.comm.InstitutionalStudent;
import io.github.oliviercailloux.grade.comm.StudentOnGitHub;
import io.github.oliviercailloux.grade.comm.json.JsonStudents;
import io.github.oliviercailloux.grade.format.CsvGrades;
import io.github.oliviercailloux.grade.format.HtmlGrades;
import io.github.oliviercailloux.grade.format.json.JsonSimpleGrade;
import io.github.oliviercailloux.grade.old.GradeStructure;
import io.github.oliviercailloux.grade.old.Mark;
import io.github.oliviercailloux.grade.utils.Compressor;
import io.github.oliviercailloux.jaris.collections.CollectionUtils;
import io.github.oliviercailloux.javagrade.graders.TwoFiles;
import io.github.oliviercailloux.utils.Utils;
import io.github.oliviercailloux.xml.XmlUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Summarizer {
@SuppressWarnings("unused")
private static final Logger LOGGER = LoggerFactory.getLogger(Summarizer.class);
private static final Path WORK_DIR = Path.of("");
public static void main(String[] args) throws Exception {
convert();
// final Summarizer summarizer = new Summarizer().setPrefix("UML")
// .setDissolveCriteria(ImmutableSet.of(Criterion.given("Warnings")));
// .setPatched()
// summarizer.getReader().restrictTo(ImmutableSet.of(GitHubUsername.given("…")));
// summarizer.summarize();
}
private static void convert() throws IOException {
final String prefix = TwoFiles.PREFIX;
final JsonStudents students =
JsonStudents.from(Files.readString(WORK_DIR.resolve("usernames.json")));
final Exam exam =
JsonSimpleGrade.asExam(Files.readString(WORK_DIR.resolve("grades " + prefix + ".json")));
final ImmutableBiMap studentsMap =
students.getStudentsByGitHubUsername();
final ImmutableMap trees = CollectionUtils
.transformKeys(exam.grades(), u -> Optional.ofNullable(studentsMap.get(u)).orElseThrow(
() -> new NoSuchElementException(u.getUsername() + " among " + studentsMap.keySet())));
final String csv = CsvGrades.newInstance(CsvGrades.STUDENT_IDENTITY_FUNCTION, 20)
.gradesToCsv(exam.aggregator(), trees);
Files.writeString(WORK_DIR.resolve("grades " + prefix + ".csv"), csv);
final ImmutableMap grades = exam.getUsernames().stream()
.collect(ImmutableMap.toImmutableMap(GitHubUsername::getUsername, exam::getGrade));
final String html =
XmlUtils.asString(HtmlGrades.asHtml(grades, prefix + " " + Instant.now(), 20d));
Files.writeString(WORK_DIR.resolve("grades " + prefix + ".html"), html);
}
public static Summarizer create() {
return new Summarizer();
}
private GradesByGitHubReader reader;
private Predicate filter;
private Predicate dissolve;
private Path csvOutputPath;
private Path htmlOutputPath;
private GradeStructure model;
private Summarizer() {
filter = g -> g.getWeight() != 0d;
reader = new GradesByGitHubReader(Path.of("grades.json"));
csvOutputPath = Path.of("grades.csv");
htmlOutputPath = Path.of("grades.html");
model = null;
dissolve = g -> false;
}
public Path getInputPath() {
return reader.getGradesInputPath();
}
public Summarizer setInputPath(Path inputPath) {
reader.setInputPath(checkNotNull(inputPath));
return this;
}
public Path getCsvOutputPath() {
return csvOutputPath;
}
public Summarizer setCsvOutputPath(Path csvOutputPath) {
this.csvOutputPath = checkNotNull(csvOutputPath);
return this;
}
public Path getHtmlOutputPath() {
return htmlOutputPath;
}
public Summarizer setHtmlOutputPath(Path htmlOutputPath) {
this.htmlOutputPath = checkNotNull(htmlOutputPath);
return this;
}
public Summarizer setPrefix(String prefix) {
checkNotNull(prefix);
reader.setInputPath(Path.of("grades " + prefix + ".json"));
csvOutputPath = Path.of("grades " + prefix + ".csv");
htmlOutputPath = Path.of("grades " + prefix + ".html");
return this;
}
public GradeStructure getModel() {
return model;
}
public Summarizer setModel(GradeStructure model) {
this.model = model;
LOGGER.info("Set model to {}.", model);
return this;
}
public Summarizer setDissolve(Predicate dissolve) {
this.dissolve = checkNotNull(dissolve);
return this;
}
public Summarizer setDissolveCriteria(Set dissolve) {
this.dissolve = g -> dissolve.stream()
.anyMatch(c -> g.getPath().endsWith(c) && Iterables.frequency(g.getPath(), c) == 1);
return this;
}
public void summarize() throws IOException {
final ImmutableMap grades = reader.readGrades();
LOGGER.debug("Grades: {}.", grades.values().stream().map(g -> g.limitedDepth(1))
.collect(ImmutableList.toImmutableList()));
// final ImmutableMap dissolved = Maps.toMap(grades.keySet(), u ->
// dissolveInto(
// dissolve(dissolveTimePenalty(grades.get(u)), Criterion.given("Penalty: commit by GitHub"))));
// LOGGER.debug("Dissolved: {}.",
// dissolved.values().stream().map(g -> g.toTree()).collect(ImmutableList.toImmutableList()));
final ImmutableMap filtered = Maps.toMap(grades.keySet(),
// u -> filter(dissolved.get(u)));
u -> nonZero(grades.get(u)));
LOGGER.info("Filtered: {}.", filtered.values().stream().map(g -> g.limitedDepth(1))
.collect(ImmutableList.toImmutableList()));
// if (model == null) {
// final GradeStructure struct;
// if (filtered.values().size() > 1) {
// struct = getAutoModel(ImmutableSet.copyOf(filtered.values()));
// struct = getMajoritarianModel(ImmutableSet.copyOf(filtered.values()));
// } else {
// struct = Iterables.getOnlyElement(filtered.values()).toTree();
// }
// setModel(struct);
// }
final ImmutableMap modeled =
Maps.toMap(grades.keySet(), u -> model(filtered.get(u)));
LOGGER.debug("Modeled: {}.", modeled.values().stream().map(g -> g.limitedDepth(1))
.collect(ImmutableList.toImmutableList()));
final ImmutableBiMap usernames = readKnownUsernames();
final ImmutableSet missing =
Sets.difference(usernames.keySet(), grades.keySet()).immutableCopy();
final ImmutableSet unknown =
Sets.difference(grades.keySet(), usernames.keySet()).immutableCopy();
// checkState(unknown.isEmpty(), unknown);
if ((!missing.isEmpty()) || (!unknown.isEmpty())) {
LOGGER.warn("Missing: {}; unknown: {}.", missing, unknown);
}
/* NB we want to iterate using the reading order. */
final ImmutableSet allUsernames =
Streams.concat(grades.keySet().stream(), usernames.keySet().stream())
.collect(ImmutableSet.toImmutableSet());
final ImmutableMap completedGrades =
Maps.toMap(allUsernames, s -> modeled.getOrDefault(s, Mark.zero("No grade")));
// final ImmutableMap completedGradesByStudent = allUsernames.stream()
// .collect(ImmutableMap.toImmutableMap(usernames::get, completedGrades::get));
LOGGER.info("Writing grades Html.");
final ImmutableMap gradesByString =
CollectionUtils.transformKeys(grades, GitHubUsername::getUsername);
// final Document doc = HtmlGrades.asHtmlIGrades(gradesByString, "All grades", 20d);
// Files.writeString(htmlOutputPath, XmlUtils.asString(doc));
LOGGER.info("Writing grades CSV.");
final Function> identityFunction = u -> ImmutableMap.of("Name",
Optional.ofNullable(usernames.get(u)).map(InstitutionalStudent::getLastName).orElse(""),
"GitHub username", u.getUsername());
Files.writeString(csvOutputPath,
CsvGrades.newInstance(identityFunction, 20d).toCsv(completedGrades));
}
private static IGrade dissolveTimePenalty(IGrade grade) {
final Criterion toDissolve = Criterion.given("Time penalty");
return dissolve(grade, toDissolve);
}
private static IGrade dissolve(IGrade grade, Criterion toDissolve) {
final ImmutableSet penaltyPaths = grade.toTree().getPaths().stream()
.filter(p -> !p.isRoot() && p.getTail().equals(toDissolve))
.collect(ImmutableSet.toImmutableSet());
LOGGER.debug("Found penalty paths: {}.", penaltyPaths);
final boolean allLeaves = penaltyPaths.isEmpty()
|| GradeStructure.given(penaltyPaths).getLeaves().equals(penaltyPaths);
checkArgument(allLeaves, penaltyPaths);
IGrade result = grade;
for (CriteriaPath penaltyPath : penaltyPaths) {
result = dissolve(result, penaltyPath);
}
return result;
}
public static GradeStructure getMajoritarianModel(Set grades) {
checkArgument(!grades.isEmpty());
final Criterion userName = Criterion.given("user.name");
final Criterion main = Criterion.given("main");
// final ImmutableSet templates = grades.stream()
// .filter(g -> g.getSubGrades().keySet().equals(ImmutableSet.of(userName,
// main))).map(IGrade::toTree)
// .map(t -> t.getStructure(main)).collect(ImmutableSet.toImmutableSet());
final Stream templates = grades.stream()
.flatMap(g -> Streams.concat(Stream.of(g), g.getSubGrades().values().stream()))
.filter(g -> g.getSubGrades().keySet().equals(ImmutableSet.of(userName, main)))
.map(IGrade::toTree).filter(s -> !s.getPaths().contains(CriteriaPath.from("main/Code/")));
// final Stream templates = grades.stream().map(IGrade::toTree)
// .flatMap(s -> s.getPaths().stream().map(s::getStructure));
final ImmutableMultiset trees =
templates.collect(ImmutableMultiset.toImmutableMultiset());
final Entry highestCountEntry =
Multisets.copyHighestCountFirst(trees).entrySet().iterator().next();
final int majCount = highestCountEntry.getCount();
LOGGER.info("Maj count: {}, among {}.", majCount, trees);
final GradeStructure structure = trees.entrySet().stream().filter(e -> e.getCount() == majCount)
.map(Entry::getElement).distinct().collect(MoreCollectors.onlyElement());
LOGGER.info("Structure: {}.", structure);
return structure;
}
public static GradeStructure getAutoModel(Set grades) {
final Criterion userName = Criterion.given("user.name");
final ImmutableSet templates =
grades.stream().filter(g -> g.getSubGrades().keySet().contains(userName))
.map(IGrade::toTree).collect(ImmutableSet.toImmutableSet());
return templates.stream().map(Summarizer::getAutoModel).distinct()
.collect(MoreCollectors.onlyElement());
}
private static GradeStructure getAutoModel(GradeStructure main) {
final ImmutableSet mainSubs = main.getSuccessorCriteria(CriteriaPath.ROOT);
return mainSubs.stream().map(main::getStructure).map(Summarizer::getAutoModelFromMainSubTree)
.distinct().collect(MoreCollectors.onlyElement());
}
private static GradeStructure getAutoModelFromMainSubTree(GradeStructure mainSubTree) {
final Criterion userName = Criterion.given("user.name");
final Criterion main = Criterion.given("main");
final GradeStructure sU =
GradeStructure.given(ImmutableSet.of(CriteriaPath.ROOT.withSuffix(userName)));
LOGGER.debug("mainSubTree: {}.", mainSubTree);
final GradeStructure mainEmbedded = GradeStructure.toTree(ImmutableMap.of(main, mainSubTree));
final GradeStructure merged = GradeStructure.merge(ImmutableSet.of(sU, mainEmbedded));
LOGGER.debug("Auto model: {}.", merged);
return merged;
}
private static ImmutableBiMap readKnownUsernames()
throws IOException {
LOGGER.debug("Reading usernames.");
final JsonStudents students = JsonStudents.from(Files.readString(Path.of("usernames.json")));
final ImmutableBiMap usernames =
students.getInstitutionalStudentsByGitHubUsername();
return usernames;
}
public IGrade filter(IGrade grade) {
final Optional filtered = filter(CriteriaPath.ROOT, grade);
checkState(!filtered.isEmpty(), grade);
final CriterionGradeWeight result = filtered.get();
verify(result.getCriterion().getName().isEmpty());
verify(result.getWeight() == 1d);
return result.getGrade();
}
/**
* The predicate must ensure that we are not left with only subgrades with a zero weight. (TODO
* consider allowing only zero weight children with an arbitrary points of one – NO should still
* aggregate using the sub points, eg at root with single child w = 0 and 0 points would be
* strange to aggregate as 1 point! So better aggregate using equal weights. But then while we’re
* at it we should also advertise equal weights! Therefore, guarantee weights do not sum to zero
* is good. However we could ease construction.)
*/
private Optional filter(CriteriaPath context, IGrade grade) {
final IGrade result;
final IGrade thisGrade = grade.getGrade(context).get();
final GradeStructure tree = grade.toTree();
final ImmutableSet successorPaths = tree.getSuccessorPaths(context);
LOGGER.debug("Considering {} and successors {}.", context, successorPaths);
verify((thisGrade instanceof Mark) == successorPaths.isEmpty());
if (successorPaths.isEmpty()) {
result = thisGrade;
} else {
final ImmutableSet subGrades = successorPaths.stream()
.map(grade::getPathGradeWeight).filter(filter).map(g -> filter(g.getPath(), grade))
.flatMap(Optional::stream).collect(ImmutableSet.toImmutableSet());
LOGGER.debug("Filtering {}, obtained {}.", context, subGrades);
if (subGrades.isEmpty()) {
result = null;
} else {
// if (subGrades.size() == 1) {
// final IGrade subGradesOnlyGrade =
// Iterables.getOnlyElement(subGrades).getGrade();
// LOGGER.info("Instead of {}, returning {}.", w, subGradesOnlyGrade);
// return subGradesOnlyGrade;
// }
result = WeightingGrade.from(subGrades);
}
}
LOGGER.debug("Filtering {}, returning {}.", context, result);
if (result == null) {
return Optional.empty();
}
final Criterion criterion = context.isRoot() ? Criterion.given("") : context.getTail();
return Optional.of(CriterionGradeWeight.from(criterion, result, grade.getLocalWeight(context)));
}
public IGrade dissolveInto(IGrade grade) {
final ImmutableSet pathsToDissolve = grade.toTree().getPaths().stream()
.map(grade::getPathGradeWeight).filter(dissolve).collect(ImmutableSet.toImmutableSet());
LOGGER.debug("Found paths to dissolve: {}.", pathsToDissolve.stream()
.map(PathGradeWeight::getPath).collect(ImmutableSet.toImmutableSet()));
IGrade result = grade;
for (PathGradeWeight toDissolve : pathsToDissolve) {
result = dissolve(result, toDissolve.getPath());
}
return result;
}
private static IGrade dissolve(IGrade grade, CriteriaPath toDissolvePath) {
checkArgument(grade.toTree().getPaths().contains(toDissolvePath));
// TODO
// checkArgument(grade.toTree().getSiblings(toDissolvePath).size() >= 2);
// verify(!toDissolvePath.isRoot());
checkArgument(!toDissolvePath.isRoot());
LOGGER.debug("Dissolving {}.", toDissolvePath);
final CriteriaPath parent = toDissolvePath.withoutTail();
final Criterion toDissolveCriterion = toDissolvePath.getTail();
/* Need to replace parent with parent-with-dissolved-child. */
final IGrade parentGrade = grade.getGrade(parent).get();
final IGrade newParent = parentGrade.withDissolved(toDissolveCriterion);
final IGrade newG = grade.withSubGrade(parent, newParent);
LOGGER.debug("Old str: {}. New str: {}.", grade.toTree(), newG.toTree());
return newG;
}
public IGrade model(IGrade grade) {
if (model == null) {
return grade;
}
return Compressor.compress(grade, model);
}
public static IGrade withTimePenalty(IGrade grade) {
if (!(grade instanceof WeightingGrade)) {
return grade;
}
/**
* More general: should observe that the small trees (a/b/c) are included in the large trees
* (g/a/b/c + t) and align them with zero or one weight grades
*/
final WeightingGrade w = (WeightingGrade) grade;
if (w.getSubGrades().keySet().contains(Criterion.given("Time penalty"))) {
return w;
}
return WeightingGrade.from(ImmutableSet
.of(CriterionGradeWeight.from(Criterion.given("grade"), grade, 1d), CriterionGradeWeight
.from(Criterion.given("Time penalty"), Mark.one("No time penalty"), 0d)));
}
public static IGrade nonZero(IGrade grade) {
if (!(grade instanceof WeightingGrade)) {
return grade;
}
final WeightingGrade w = (WeightingGrade) grade;
final ImmutableSet subGrades = w.getSubGradesAsSet().stream()
.filter(g -> g.getWeight() != 0d)
.map(g -> CriterionGradeWeight.from(g.getCriterion(), nonZero(g.getGrade()), g.getWeight()))
.collect(ImmutableSet.toImmutableSet());
verify(!subGrades.isEmpty());
if (subGrades.size() == 1) {
final IGrade subGradesOnlyGrade = Iterables.getOnlyElement(subGrades).getGrade();
LOGGER.info("Instead of {}, returning {}.", w, subGradesOnlyGrade);
return subGradesOnlyGrade;
}
return w;
}
public static Optional getCommonTree(Set grades) {
checkArgument(!grades.isEmpty());
final Optional> commonChildrenOpt =
grades.stream().map(IGrade::getSubGrades).map(Map::keySet).collect(Utils.singleOrEmpty());
final Optional common;
if (commonChildrenOpt.isEmpty()) {
/** ≠ children? intersect all possibilities until empty or finished. */
Set remainingIntersection = null;
for (IGrade grade : grades) {
LOGGER.info("Remaining intersection: {}.", remainingIntersection);
final GradeStructure basicTree = grade.toTree();
final Optional commonSubTree = grade.getSubGrades().values().stream()
.map(IGrade::toTree).collect(Utils.singleOrEmpty());
final ImmutableSet.Builder builder = ImmutableSet.builder();
builder.add(basicTree);
if (commonSubTree.isPresent()) {
builder.add(commonSubTree.get());
}
final ImmutableSet current = builder.build();
if (remainingIntersection == null) {
remainingIntersection = current;
} else {
remainingIntersection = Sets.intersection(remainingIntersection, current);
}
if (remainingIntersection.isEmpty()) {
break;
}
}
assert remainingIntersection != null;
verify(remainingIntersection.size() <= 1);
if (remainingIntersection.isEmpty()) {
common = Optional.empty();
} else {
common = Optional.of(Iterables.getOnlyElement(remainingIntersection));
}
} else {
/** same children? investigate each child. */
final Set commonChildren = commonChildrenOpt.get();
final ImmutableMap> treesOptPerCriterion =
Maps.toMap(commonChildren, c -> getCommonTree(grades.stream()
.map(g -> g.getSubGrades().get(c)).collect(ImmutableSet.toImmutableSet())));
if (treesOptPerCriterion.values().stream().anyMatch(Optional::isEmpty)) {
common = Optional.empty();
} else {
final ImmutableMap treesPerCriterion =
Maps.toMap(treesOptPerCriterion.keySet(), c -> treesOptPerCriterion.get(c).get());
final ImmutableMap rightlyRootedTrees =
Maps.toMap(treesPerCriterion.keySet(), c -> treesPerCriterion.get(c).getStructure(c));
final ImmutableGraph.Builder builder = GraphBuilder.directed().immutable();
for (Criterion criterion : rightlyRootedTrees.keySet()) {
final GradeStructure tree = rightlyRootedTrees.get(criterion);
builder.putEdge(CriteriaPath.ROOT, CriteriaPath.ROOT.withSuffix(criterion));
tree.asGraph().edges().stream().forEach(builder::putEdge);
}
common = Optional.of(GradeStructure.given(builder.build()));
}
}
return common;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy