Search code examples
timefold

Purpose of additional fact in ConstraintVerifier


I'm trying to undersand how ConstraintVerifier works by reviewing the VehicleRoutingContraintProviderTest test cases and the one thing that is not clear to me is the inclusion of other two Visit facts to the .given method. Looking at the VehicleRoutingContraintProviderTest.vehicleCapacityPenalized method I see the test being setup with new insteances of Vehicle and two Visit instances. The visits are then added to the Vehicle List. Then both the Vehicle and two aditional Visit are passed to the .given method. My question: given the Visit objects are now a child of the Vehicle object, why pass them to the given method? What actions or checks are taken on the other two Visit objects? Doesn't the test simply invoke the VehicleRoutingConstraintProvider::minimizeTraveTime method with only the Vehicle visible? If I remove the two Visit instance parameters, the test still succeeds.


Solution

  • For the vehicle capacity constraint:

        protected Constraint vehicleCapacity(ConstraintFactory factory) {
            return factory.forEach(Vehicle.class)
                    .filter(vehicle -> vehicle.getTotalDemand() > vehicle.getCapacity())
                    .penalizeLong(HardSoftLongScore.ONE_HARD,
                            vehicle -> vehicle.getTotalDemand() - vehicle.getCapacity())
                    .justifyWith((vehicle, score) -> new VehicleCapacityJustification(vehicle.getId(), vehicle.getTotalDemand(),
                            vehicle.getCapacity()))
                    .asConstraint(VEHICLE_CAPACITY);
        }
    

    which has this test:

        @Test
        void vehicleCapacityPenalized() {
            LocalDateTime tomorrow_07_00 = LocalDateTime.of(TOMORROW, LocalTime.of(7, 0));
            LocalDateTime tomorrow_08_00 = LocalDateTime.of(TOMORROW, LocalTime.of(8, 0));
            LocalDateTime tomorrow_10_00 = LocalDateTime.of(TOMORROW, LocalTime.of(10, 0));
            Vehicle vehicleA = new Vehicle("1", 100, LOCATION_1, tomorrow_07_00);
            Visit visit1 = new Visit("2", "John", LOCATION_2, 80, tomorrow_08_00, tomorrow_10_00, Duration.ofMinutes(30L));
            vehicleA.getVisits().add(visit1);
            Visit visit2 = new Visit("3", "Paul", LOCATION_3, 40, tomorrow_08_00, tomorrow_10_00, Duration.ofMinutes(30L));
            vehicleA.getVisits().add(visit2);
    
            constraintVerifier.verifyThat(VehicleRoutingConstraintProvider::vehicleCapacity)
                    .given(vehicleA, visit1, visit2)
                    .penalizesBy(20);
        }
    

    The Visit facts are not required to be given, since Visit is not used in a forEach, join, ifExists or ifNotExists building block.

    However, if the constraint was hypothetically modified later to use Visit via one of the above building blocks:

        protected Constraint vehicleCapacity(ConstraintFactory factory) {
            return factory.forEach(Vehicle.class)
                    .filter(vehicle -> vehicle.getTotalDemand() > vehicle.getCapacity())
                    .ifExists(Visit.class, Joiners.filtering((vehicle, visit) -> vehicle.getVisits().contains(visit)))
                    .penalizeLong(HardSoftLongScore.ONE_HARD,
                            vehicle -> vehicle.getTotalDemand() - vehicle.getCapacity())
                    .justifyWith((vehicle, score) -> new VehicleCapacityJustification(vehicle.getId(), vehicle.getTotalDemand(),
                            vehicle.getCapacity()))
                    .asConstraint(VEHICLE_CAPACITY);
        }
    

    Then the tests will fail without the Visit facts being given, since the visits from the Vehicle are not automatically considered by the ConstraintVerifier (so ifExists will fail to find any Visit that match the predicate despite the two Visits in vehicle.visits meeting the predicate).

    Thus the purpose of adding the visit facts to given is so the test does not need to be modified if the Constraint was changed to use Visit via one of the above building blocks.