org.libreplan.business.planner.entities.SigmoidFunction.java Source code

Java tutorial

Introduction

Here is the source code for org.libreplan.business.planner.entities.SigmoidFunction.java

Source

/*
 * This file is part of LibrePlan
 *
 * Copyright (C) 2010-2011 Igalia, S.L.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.libreplan.business.planner.entities;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang3.Validate;
import org.joda.time.LocalDate;
import org.libreplan.business.calendars.entities.BaseCalendar;
import org.libreplan.business.workingday.EffortDuration;
import org.libreplan.business.workingday.IntraDayDate.PartialDay;

/**
 *
 * @author Diego Pino Garcia<dpino@igalia.com>
 *
 */
public class SigmoidFunction extends AssignmentFunction {

    private static final int PRECISSION = 6;

    private static final int ROUND_MODE = BigDecimal.ROUND_HALF_EVEN;

    // Fragmentation of hours (0.25, 0.50, 0.75, 1). 1 indicates no fragmentation
    private static final BigDecimal HOUR_FRAGMENTATION = BigDecimal.valueOf(0.25);

    public static SigmoidFunction create() {
        return create(new SigmoidFunction());
    }

    protected SigmoidFunction() {

    }

    @Override
    public String getName() {
        return AssignmentFunctionName.SIGMOID.toString();
    }

    @Override
    public void applyTo(ResourceAllocation<?> resourceAllocation) {
        final Task task = resourceAllocation.getTask();
        final int totalHours = resourceAllocation.getAssignedHours();
        apply(resourceAllocation, task.getStartAsLocalDate(), task.getEndAsLocalDate(), totalHours);
    }

    private void apply(ResourceAllocation<?> resourceAllocation, LocalDate start, LocalDate end, int totalHours) {

        final LocalDate previousEndDate = resourceAllocation.getEndDate();

        EffortDuration capacity;
        BaseCalendar calendar = resourceAllocation.getTask().getCalendar();
        int daysDuration = daysWithAllocatedHours(resourceAllocation).size();

        // Calculate hours per day and round values
        BigDecimal[] hoursToAllocatePerDay = generateHoursToAllocateFor(daysDuration, totalHours);
        hoursToAllocatePerDay = roundValues(hoursToAllocatePerDay, HOUR_FRAGMENTATION);

        // Calculate reminder (difference between totalHours and sum of hours calculated)
        BigDecimal totalHoursToAllocate = sumHoursPerDay(hoursToAllocatePerDay);
        assert (totalHoursToAllocate.compareTo(BigDecimal.valueOf(totalHours)) <= 0);
        BigDecimal remindingHours = BigDecimal.valueOf(totalHours).subtract(totalHoursToAllocate);
        allocateRemindingHours(hoursToAllocatePerDay, remindingHours);
        avoidZeroHoursInDays(hoursToAllocatePerDay);

        assert (hoursToAllocatePerDay.length == daysDuration);

        // Starting from startDate do allocation, one slot of hours per day in resource
        LocalDate day = new LocalDate(start);
        EffortDuration hours = EffortDuration.zero();
        int i = 0;
        while (i < hoursToAllocatePerDay.length) {
            hours = EffortDuration.fromHoursAsBigDecimal(hoursToAllocatePerDay[i]);
            capacity = calendar.getCapacityOn(PartialDay.wholeDay(day));
            if (!EffortDuration.zero().equals(capacity)) {
                allocate(resourceAllocation, day, hours);
                i++;
            }
            day = day.plusDays(1);
        }
        Validate.isTrue(resourceAllocation.getEndDate().equals(previousEndDate));
    }

    private List<BigDecimal> daysWithAllocatedHours(ResourceAllocation<?> resourceAllocation) {

        List<BigDecimal> result = new ArrayList<BigDecimal>();
        LocalDate day = new LocalDate(resourceAllocation.getStartDate());
        final LocalDate end = resourceAllocation.getEndDate();

        while (day.isBefore(end)) {
            int hoursAllocated = resourceAllocation.getAssignedHours(day, day.plusDays(1));
            if (hoursAllocated != 0) {
                result.add(new BigDecimal(hoursAllocated));
            }
            day = day.plusDays(1);
        }
        return result;
    }

    /**
     * Days with zero hours can occur at the beginning days.
     *
     * To avoid allocating days with zero hours, we iterate through the days and
     * subtract a day from the next day to the current day, until we come up
     * with a day which is no zero
     *
     * @param hoursToAllocatePerDay
     */
    private void avoidZeroHoursInDays(BigDecimal[] hoursToAllocatePerDay) {
        int length = hoursToAllocatePerDay.length;
        for (int i = 0; i < length; i++) {
            BigDecimal hours = hoursToAllocatePerDay[i];
            if (hours.doubleValue() != 0) {
                return;
            }
            if (i + 1 <= length) {
                BigDecimal next = hoursToAllocatePerDay[i + 1];
                hoursToAllocatePerDay[i + 1] = next.subtract(HOUR_FRAGMENTATION);
                hoursToAllocatePerDay[i] = hours.add(HOUR_FRAGMENTATION);
            }
        }
    }

    private void allocateRemindingHours(BigDecimal[] hoursToAllocatePerDay, BigDecimal remindingHours) {
        final int length = hoursToAllocatePerDay.length;

        // Add reminding hours to best fit in a way that the distribution of
        // hours grows continuously
        for (int i = 0; i < length - 1; i++) {
            BigDecimal current = hoursToAllocatePerDay[i];
            BigDecimal next = hoursToAllocatePerDay[i + 1];

            if (current.add(remindingHours).compareTo(next) <= 0) {
                hoursToAllocatePerDay[i] = current.add(remindingHours);
                return;
            }
        }

        // Add reminding hours to last day
        BigDecimal lastDay = hoursToAllocatePerDay[length - 1];
        hoursToAllocatePerDay[length - 1] = lastDay.add(remindingHours);
    }

    private void allocate(ResourceAllocation<?> resourceAllocation, LocalDate day, EffortDuration hours) {
        final LocalDate nextDay = day.plusDays(1);
        resourceAllocation.withPreviousAssociatedResources().onInterval(day, nextDay).allocate(hours);
    }

    private BigDecimal[] roundValues(BigDecimal[] allocatedHoursPerDay, BigDecimal truncateValue) {

        BigDecimal[] result = new BigDecimal[allocatedHoursPerDay.length];
        BigDecimal reminder = BigDecimal.ZERO;

        for (int i = 0; i < result.length; i++) {
            BigDecimal value = allocatedHoursPerDay[i];
            value = value.add(reminder);

            BigDecimal intPart = intPart(value);
            BigDecimal decimalPart = decimalPart(value);
            reminder = calculateReminder(decimalPart, truncateValue);
            decimalPart = decimalPart.subtract(reminder);
            result[i] = intPart.add(decimalPart);
        }
        return result;
    }

    private BigDecimal calculateReminder(BigDecimal decimalPart, BigDecimal truncateValue) {
        BigDecimal[] result = decimalPart.divideAndRemainder(truncateValue);
        return result[1];
    }

    private BigDecimal intPart(BigDecimal bd) {
        return BigDecimal.valueOf(bd.intValue());
    }

    private BigDecimal decimalPart(BigDecimal bd) {
        return bd.subtract(intPart(bd));
    }

    private BigDecimal sumHoursPerDay(BigDecimal[] hoursPerDay) {
        BigDecimal result = BigDecimal.ZERO;
        for (int i = 0; i < hoursPerDay.length; i++) {
            result = result.add(hoursPerDay[i]);
        }
        return result;
    }

    private BigDecimal[] generateHoursToAllocateFor(int days, int hours) {
        BigDecimal[] valuesPerDay = generatePointValuesForDays(days);
        BigDecimal[] acummulatedHoursPerDay = calculateNumberOfAccumulatedHoursForDays(valuesPerDay, hours);
        BigDecimal[] allocatedHoursPerDay = calculateNumberOfAllocatedHoursForDays(acummulatedHoursPerDay);
        return allocatedHoursPerDay;
    }

    private BigDecimal[] generatePointValuesForDays(int days) {
        final BigDecimal dayIntervalConstant = getDayIntervalConstant(days);

        BigDecimal[] result = new BigDecimal[days];
        for (int i = 0; i < days; i++) {
            result[i] = BigDecimal.valueOf(-6).add(dayIntervalConstant.multiply(BigDecimal.valueOf(i)));
        }
        return result;
    }

    private BigDecimal[] calculateNumberOfAllocatedHoursForDays(BigDecimal[] acummulatedHoursPerDay) {
        BigDecimal[] result = new BigDecimal[acummulatedHoursPerDay.length];

        result[0] = acummulatedHoursPerDay[0];
        for (int i = 1; i < result.length; i++) {
            result[i] = acummulatedHoursPerDay[i].subtract(acummulatedHoursPerDay[i - 1]);
        }
        return result;
    }

    private BigDecimal[] calculateNumberOfAccumulatedHoursForDays(BigDecimal[] dayValues, int totalHours) {
        BigDecimal[] result = new BigDecimal[dayValues.length];
        for (int i = 0; i < dayValues.length; i++) {
            result[i] = calculateNumberOfAccumulatedHoursAtDay(dayValues[i], totalHours);
        }
        return result;
    }

    private BigDecimal calculateNumberOfAccumulatedHoursAtDay(BigDecimal valueAtOneDay, int totalHours) {
        BigDecimal epow = BigDecimal.valueOf(Math.pow(Math.E, valueAtOneDay.negate().doubleValue()));
        BigDecimal denominator = BigDecimal.valueOf(1).add(epow);
        return BigDecimal.valueOf(totalHours).divide(denominator, PRECISSION, ROUND_MODE);
    }

    // 12 divide by days
    private BigDecimal getDayIntervalConstant(int days) {
        return BigDecimal.valueOf(12).divide(BigDecimal.valueOf(days), PRECISSION, ROUND_MODE);
    }

    @Override
    public boolean isManual() {
        return false;
    }

}