OralExamsTest.java

/*
 * Copyright (c) 2021 Mārtiņš Avots (Martins Avots) and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0, or the MIT License,
 * which is available at https://spdx.org/licenses/MIT.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR MIT
 */
package net.splitcells.gel.test.functionality;

import net.splitcells.dem.data.atom.Bools;
import net.splitcells.dem.data.set.list.List;
import net.splitcells.dem.environment.config.IsDeterministic;
import net.splitcells.dem.resource.communication.interaction.LogLevel;
import net.splitcells.dem.resource.communication.log.MessageFilter;
import net.splitcells.dem.testing.TestSuiteI;
import net.splitcells.dem.utils.random.DeterministicRootSourceSeed;
import net.splitcells.dem.utils.random.Randomness;
import net.splitcells.gel.constraint.Constraint;
import net.splitcells.gel.data.table.attribute.Attribute;
import net.splitcells.gel.problem.Problem;
import net.splitcells.gel.rating.type.Cost;
import net.splitcells.gel.solution.Solution;
import net.splitcells.gel.solution.optimization.meta.hill.climber.FunctionalHillClimber;
import net.splitcells.gel.solution.optimization.primitive.repair.ConstraintGroupBasedRepair;
import org.junit.jupiter.api.*;

import java.time.ZonedDateTime;
import java.util.Optional;
import java.util.stream.IntStream;

import static java.lang.Math.floorMod;
import static net.splitcells.dem.data.set.list.Lists.list;
import static net.splitcells.dem.lang.namespace.NameSpaces.STRING;
import static net.splitcells.dem.lang.perspective.PerspectiveI.perspective;
import static net.splitcells.dem.resource.communication.log.Domsole.domsole;
import static net.splitcells.dem.testing.TestTypes.*;
import static net.splitcells.dem.utils.random.RandomnessSource.randomness;
import static net.splitcells.gel.GelEnv.*;
import static net.splitcells.gel.constraint.type.ForAlls.*;
import static net.splitcells.gel.constraint.type.Then.then;
import static net.splitcells.gel.data.table.attribute.AttributeI.integerAttribute;
import static net.splitcells.gel.rating.rater.HasSize.hasSize;
import static net.splitcells.gel.rating.rater.MinimalDistance.has_minimal_distance_of;
import static net.splitcells.gel.rating.type.Cost.cost;
import static net.splitcells.gel.rating.type.Cost.noCost;
import static net.splitcells.gel.solution.SolutionBuilder.defineProblem;
import static net.splitcells.gel.solution.optimization.meta.Escalator.escalator;
import static net.splitcells.gel.solution.optimization.meta.hill.climber.FunctionalHillClimber.functionalHillClimber;
import static net.splitcells.gel.solution.optimization.primitive.repair.ConstraintGroupBasedRepair.simpleConstraintGroupBasedRepair;
import static net.splitcells.gel.solution.optimization.primitive.LinearInitialization.linearInitialization;
import static org.assertj.core.api.Assertions.assertThat;

/**
 * TODO IDEA Test object orientation by making all people an instance of a certain
 * class.
 * <p>
 * TODO IDEA The number of days with exams for a teacher should be smaller or equals to a given number.
 * <p>
 * TODO Pupil and teachers are not available on certain days or at certain shifts in certain days.
 * <p>
 * TODO Prefered days and shifts for pupil and teachers.
 */
public class OralExamsTest extends TestSuiteI {
    public static final Attribute<Integer> STUDENTS = integerAttribute("students");
    public static final Attribute<Integer> EXAMINER = integerAttribute("examiner");
    public static final Attribute<Integer> OBSERVER = integerAttribute("observer");
    public static final Attribute<Integer> SHIFT = integerAttribute("shift");
    public static final Attribute<Integer> DATE = integerAttribute("date");
    public static final Attribute<Integer> ROOM_NUMBER = integerAttribute("room-number");

    @Tag(CAPABILITY_TEST)
    @Test
    public void testRandomInstanceSolving() {
        analyseProcess(() -> {
            final var testSubject = randomOralExams
                    (88
                            , 177
                            , 40
                            , 41
                            , 2
                            , 5
                            , 5
                            , 6
                            , randomness(0L))
                    .asSolution();
            testSubject.optimize(linearInitialization());
            testSubject.optimizeWithFunction(simpleConstraintGroupBasedRepair(3)
                    , (currentSolution, step) -> step <= 100 && !currentSolution.isOptimal());
            testSubject.optimizeWithFunction(simpleConstraintGroupBasedRepair(4, 2)
                    , (currentSolution, step) -> step <= 100 && !currentSolution.isOptimal());
            testSubject.optimizeWithFunction(simpleConstraintGroupBasedRepair(4, 3)
                    , (currentSolution, step) -> step <= 100 && !currentSolution.isOptimal());
            testSubject.optimizeWithFunction(simpleConstraintGroupBasedRepair(1), (currentSolution, step) ->
                    step <= 100 && !currentSolution.isOptimal());
            assertThat(testSubject.isOptimal()).isTrue();
        }, standardDeveloperConfigurator().andThen(env -> {
            env.config()
                    .withConfigValue(MessageFilter.class, a -> false)
                    .withConfigValue(IsDeterministic.class, Optional.of(Bools.truthful()))
                    .withConfigValue(DeterministicRootSourceSeed.class, 1000L);
        })).requireErrorFree();
    }

    /**
     * This test shows, that the {@link FunctionalHillClimber} is not able to solve this problem
     * as efficiently as the {@link ConstraintGroupBasedRepair}.
     * This is done by trying as many allocations via the {@link FunctionalHillClimber} as is done
     * in {@link #testRandomInstanceSolving} via the {@link ConstraintGroupBasedRepair}.
     */
    @Tag(CAPABILITY_TEST)
    @Test
    public void testComplexity() {
        analyseProcess(() -> {
            final var testSubject = randomOralExams
                    (88
                            , 177
                            , 40
                            , 41
                            , 2
                            , 5
                            , 5
                            , 6
                            , randomness(0L))
                    .asSolution();
            testSubject.optimize(functionalHillClimber(400 * 177));
            assertThat(testSubject.isOptimal()).isFalse();
        }, standardDeveloperConfigurator().andThen(env -> {
            env.config()
                    .withConfigValue(MessageFilter.class, a -> false)
                    .withConfigValue(IsDeterministic.class, Optional.of(Bools.truthful()))
                    .withConfigValue(DeterministicRootSourceSeed.class, 1000L);
        })).requireErrorFree();
    }

    @Disabled
    @Test
    @Deprecated
    public void testCurrentDevelopment() {
        final var testSubject = randomOralExams
                (88
                        , 177
                        , 40
                        , 41
                        , 2
                        , 5
                        , 5
                        , 6
                        , randomness(0L))
                .asSolution();
        final var initialSolutionTemplate = testSubject.dataContainer().resolve("previous").resolve("results.fods");
        /*
        if (Files.exists(initialSolutionTemplate)) {
            testSubject.optimize
                    (templateInitializer
                            (databaseOfFods(objectAttributes(testSubject.headerView())
                                    , Xml.parse(initialSolutionTemplate).getDocumentElement())));
        }*/
        testSubject.optimize(linearInitialization());
        IntStream.rangeClosed(1, 100).forEach(i -> {
            if (testSubject.isOptimal()) {
                return;
            }
            domsole().append(
                    perspective(i + ""
                            , STRING)
                    , () -> list("debugging")
                    , LogLevel.DEBUG);
            testSubject.optimizeWithFunction(simpleConstraintGroupBasedRepair(3), (currentSolution, step) -> {
                domsole().append(
                        perspective(ZonedDateTime.now().toString()
                                        + testSubject.constraint().rating().asMetaRating().getContentValue(Cost.class).value()
                                        + currentSolution.isComplete()
                                        + currentSolution.demandsFree().size()
                                , STRING)
                        , () -> list("debugging")
                        , LogLevel.DEBUG);
                return step <= 100 && !currentSolution.isOptimal();
            });
            testSubject.optimizeWithFunction(simpleConstraintGroupBasedRepair(4, 2), (currentSolution, step) -> {
                domsole().append(
                        perspective(ZonedDateTime.now().toString()
                                        + testSubject.constraint().rating().asMetaRating().getContentValue(Cost.class).value()
                                        + currentSolution.isComplete()
                                        + currentSolution.demandsFree().size()
                                , STRING)
                        , () -> list("debugging")
                        , LogLevel.DEBUG);
                return step <= 100 && !currentSolution.isOptimal();
            });
            testSubject.optimizeWithFunction(simpleConstraintGroupBasedRepair(4, 3), (currentSolution, step) -> {
                domsole().append(
                        perspective(ZonedDateTime.now().toString()
                                        + testSubject.constraint().rating().asMetaRating().getContentValue(Cost.class).value()
                                        + currentSolution.isComplete()
                                        + currentSolution.demandsFree().size()
                                , STRING)
                        , () -> list("debugging")
                        , LogLevel.DEBUG);
                return step <= 100 && !currentSolution.isOptimal();
            });
            testSubject.optimizeWithFunction(simpleConstraintGroupBasedRepair(1), (currentSolution, step) -> {
                domsole().append(
                        perspective(ZonedDateTime.now().toString()
                                        + testSubject.constraint().rating().asMetaRating().getContentValue(Cost.class).value()
                                        + currentSolution.isComplete()
                                        + currentSolution.demandsFree().size()
                                , STRING)
                        , () -> list("debugging")
                        , LogLevel.DEBUG);
                return step <= 100 && !currentSolution.isOptimal();
            });
        });
    }

    public Problem randomOralExams(int studentCount, int examCount, int examinerCount, int checkerCount,
                                   int weekCount
            , int examDayCountPerWeek, int shiftsPerDayCount, int roomCount, Randomness randomness) {
        final List<List<Object>> supplies = list();
        for (int room = 1; room <= roomCount; ++room) {
            for (int week = 1; week <= weekCount; ++week) {
                for (int examDay = 1; examDay <= examDayCountPerWeek; ++examDay) {
                    for (int shift = 1; shift <= shiftsPerDayCount; ++shift) {
                        supplies.add
                                (list
                                        (floorMod(examDay, examDayCountPerWeek) + 1
                                                        + (week - 1) * 7
                                                , shift
                                                , room));
                    }
                }
            }
        }
        final List<List<Object>> demands = list();
        for (int student = 1; student <= studentCount; ++student) {
            for (int exam = 1; exam <= examCount / studentCount; ++exam) {
                demands.add(list(student, randomness.integer(1, examinerCount), randomness.integer(1, checkerCount)));
            }
        }
        return oralExams(demands, supplies);
    }

    public Problem oralExams(List<List<Object>> demands, List<List<Object>> supplies) {
        return defineProblem()
                .withDemandAttributes(STUDENTS, EXAMINER, OBSERVER)
                .withDemands(demands)
                .withSupplyAttributes(DATE, SHIFT, ROOM_NUMBER)
                .withSupplies(supplies)
                .withConstraint
                        (forAll()
                                .withChildren(forEach(OBSERVER)
                                                .withChildren(forAllCombinationsOf(DATE, SHIFT)
                                                        .withChildren(then(hasSize(1))))
                                        , forEach(EXAMINER)
                                                .withChildren(forAllCombinationsOf(DATE, SHIFT)
                                                        .withChildren(then(hasSize(1))))
                                        , forEach(STUDENTS)
                                                .withChildren(forAllCombinationsOf(DATE, SHIFT)
                                                                .withChildren(then(hasSize(1)))
                                                        , then(has_minimal_distance_of(DATE, 3.0))
                                                        , then(has_minimal_distance_of(DATE, 5.0))
                                                )
                                        /** TODO Every examiner and observer wants to minimize the number of days with exams.
                                         * <p/>
                                         * TODO Every examiner and observer wants to minimize the pause between 2 exams of one day.
                                         * <p/>
                                         * TODO Every examiner and observer wants to minimize the number of room switches per day.
                                         */
                                        , forAllCombinationsOf(DATE, SHIFT, ROOM_NUMBER)
                                                .withChildren(then(hasSize(1)))
                                        , studentSpecificConstraints()
                                        , checkerSpecificConstraints()
                                        , examinerSpecificConstraints()
                                )
                        ).toProblem();
    }

    @Tag(INTEGRATION_TEST)
    @Test
    public void testRatingsOfSingleOralExam() {
        Solution testSubject = oralExams(list(list(1, 1, 1)), list(list(1, 1, 1))).asSolution();
        testSubject.optimize(linearInitialization());
        assertThat(testSubject.constraint().rating()).isEqualTo(noCost());
    }

    @Tag(INTEGRATION_TEST)
    @Test
    public void testRatingsOfPeopleWithMultipleExamClones() {
        Solution testSubject = oralExams
                (list
                                (list(1, 1, 1)
                                        , list(1, 1, 1))
                        , list(list(1, 1, 1), list(1, 1, 1)))
                .asSolution();
        testSubject.optimize(linearInitialization());
        {
            assertThat(testSubject.constraint().query()
                    .forAll(OBSERVER)
                    .forAllCombinationsOf(DATE, SHIFT)
                    .then(hasSize(1))
                    .rating()
            ).isEqualTo(cost(1));
            assertThat(testSubject.constraint().query()
                    .forAll(EXAMINER)
                    .forAllCombinationsOf(DATE, SHIFT)
                    .then(hasSize(1))
                    .rating()
            ).isEqualTo(cost(1));
            {
                assertThat
                        (testSubject.constraint().query()
                                .forAll(STUDENTS)
                                .forAllCombinationsOf(DATE, SHIFT)
                                .then(hasSize(1))
                                .rating()
                        ).isEqualTo(cost(1));
                assertThat
                        (testSubject.constraint().query()
                                .forAll(STUDENTS)
                                .then(has_minimal_distance_of(DATE, 3.0))
                                .rating()
                        ).isEqualTo(cost(3));
                assertThat
                        (testSubject.constraint().query()
                                .forAll(STUDENTS)
                                .then(has_minimal_distance_of(DATE, 5.0))
                                .rating()
                        ).isEqualTo(cost(5));
            }
        }
        {
            assertThat
                    (testSubject.constraint().query()
                            .forAll(OBSERVER)
                            .rating()
                    ).isEqualTo(cost(1));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(EXAMINER)
                            .rating()
                    ).isEqualTo(cost(1));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(STUDENTS)
                            .rating()
                    ).isEqualTo(cost(9));
            assertThat(
                    testSubject.constraint().query()
                            .forAllCombinationsOf(DATE, SHIFT, ROOM_NUMBER)
                            .then(hasSize(1))
                            .rating()
            ).isEqualTo(cost(1));
        }
        assertThat(testSubject.constraint().query().rating()).isEqualTo(cost(12));
        assertThat(testSubject.constraint().rating()).isEqualTo(cost(12));
    }

    @Tag(INTEGRATION_TEST)
    @Test
    public void testRatingsOfExamsInSameTimeslot() {
        Solution testSubject = oralExams
                (list
                                (list(1, 1, 1)
                                        , list(1, 1, 1)
                                        , list(1, 1, 1)
                                        , list(1, 1, 1)
                                        , list(1, 1, 1))
                        , list
                                (list(1, 1, 1)
                                        , list(1, 1, 1)
                                        , list(1, 1, 2)
                                        , list(1, 2, 2)
                                        , list(2, 1, 2))
                ).asSolution();
        testSubject.optimize(linearInitialization());
        {
            assertThat
                    (testSubject.constraint().query()
                            .forAll(STUDENTS)
                            .forAllCombinationsOf(DATE, SHIFT)
                            .then(hasSize(1)).rating()
                    ).isEqualTo(cost(2));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(STUDENTS)
                            .then(has_minimal_distance_of(DATE, 3.0))
                            .rating()
                    ).isEqualTo(cost(26));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(STUDENTS)
                            .then(has_minimal_distance_of(DATE, 5.0))
                            .rating()
                    ).isEqualTo(cost(46));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(EXAMINER)
                            .forAllCombinationsOf(DATE, SHIFT)
                            .then(hasSize(1))
                            .rating()
                    ).isEqualTo(cost(2));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(OBSERVER)
                            .forAllCombinationsOf(DATE, SHIFT)
                            .then(hasSize(1))
                            .rating()
                    ).isEqualTo(cost(2));
            assertThat
                    (testSubject.constraint().query()
                            .forAllCombinationsOf(DATE, SHIFT, ROOM_NUMBER)
                            .then(hasSize(1))
                            .rating()
                    ).isEqualTo(cost(1));
        }
        assertThat(testSubject.constraint().query().rating()).isEqualTo(cost(79));
        assertThat(testSubject.constraint().rating()).isEqualTo(cost(79));
    }

    @Tag(INTEGRATION_TEST)
    @Test
    public void testRatingsOfStudentWithMultipleExamsInSameDay() {
        Solution testSubject = oralExams
                (list
                                (list(1, 1, 1)
                                        , list(1, 1, 1)
                                        , list(1, 1, 1))
                        , list
                                (list(1, 1, 1)
                                        , list(1, 2, 1)
                                        , list(1, 1, 2))
                ).asSolution();
        testSubject.optimize(linearInitialization());
        {
            assertThat
                    (testSubject.constraint().query()
                            .forAll(STUDENTS)
                            .then(has_minimal_distance_of(DATE, 3.0))
                            .rating()
                    ).isEqualTo(cost(9));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(STUDENTS)
                            .then(has_minimal_distance_of(DATE, 5.0))
                            .rating()
                    ).isEqualTo(cost(15));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(STUDENTS)
                            .forAllCombinationsOf(DATE, SHIFT)
                            .then(hasSize(1))
                            .rating()
                    ).isEqualTo(cost(1));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(STUDENTS)
                            .rating()
                    ).isEqualTo(cost(25));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(OBSERVER)
                            .forAllCombinationsOf(DATE, SHIFT)
                            .then(hasSize(1))
                            .rating()
                    ).isEqualTo(cost(1));
            assertThat
                    (testSubject.constraint().query()
                            .forAll(EXAMINER)
                            .forAllCombinationsOf(DATE, SHIFT)
                            .then(hasSize(1))
                            .rating()
                    ).isEqualTo(cost(1));
            assertThat
                    (testSubject.constraint().query()
                            .forAllCombinationsOf(DATE, SHIFT, ROOM_NUMBER)
                            .then(hasSize(1))
                            .rating()
                    ).isEqualTo(noCost());
        }
        assertThat(testSubject.constraint().query().rating()).isEqualTo(cost(27));
        assertThat(testSubject.constraint().rating()).isEqualTo(cost(27));
    }

    /**
     * TODO
     */
    private Constraint studentSpecificConstraints() {
        Constraint rVal = forAll();
        return rVal;
    }

    /**
     * TODO
     */
    private Constraint examinerSpecificConstraints() {
        Constraint rVal = forAll();
        return rVal;
    }

    /**
     * TODO
     */
    private Constraint checkerSpecificConstraints() {
        Constraint rVal = forAll();
        return rVal;
    }
}