{- Copyright (C) 2013-2015 Dr. Alistair Ward This file is part of WeekDaze. WeekDaze is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. WeekDaze 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 General Public License for more details. You should have received a copy of the GNU General Public License along with WeekDaze. If not, see . -} {- | [@AUTHOR@] Dr. Alistair Ward [@DESCRIPTION@] * Defines a single /criterion/, which quantifies the significant of some concept. These criteria represent /soft/-constraints of the problem; /hard/-constraints aren't relevent, because they are never allowed to be violated at all. * Many such criteria may exist, & their weighted-mean drives the selection amongst either competing /lesson/-definitions at a specific time-slot in the /timetable/, or between competing /timetable/s when attempting to optimise the solution. * Each /criterion/ is quantified by some 'Fractional' value, but since the weighted-mean should ideally be affected by the suitability of the solution rather than the dimensions of the problem, each is required to be normalised into the /closed unit-interval/. [@CAVEAT@] * While this data-type could implement the classes Num, Fractional & Real, these interfaces allow one to construct invalid instances. -} module WeekDaze.ExecutionConfiguration.Criterion( -- * Types -- ** Data-types Criterion(), -- * Constants median, -- * Functions invertNaturalNumbersIntoUnitInterval, invertWholeNumbersIntoUnitInterval, reflectUnitInterval, calculateWeightedMean, -- ** Constructors mkCriterion, mkCriterionFrom ) where import Control.Arrow((&&&), (***)) import qualified Control.Monad.Writer import qualified Factory.Math.Statistics import qualified ToolShed.SelfValidate import qualified WeekDaze.ExecutionConfiguration.CriterionWeight as ExecutionConfiguration.CriterionWeight {- | * Quantifies criteria used to assess the desirability of a /resource/. * The larger the value the better, relative to the same criterion applied other resources. -} newtype Criterion c = MkCriterion { deconstruct :: c } deriving (Eq, Ord, Show) instance Num c => Bounded (Criterion c) where minBound = MkCriterion 0 maxBound = MkCriterion 1 -- | True if the specified 'criterion-weight' falls within the /closed unit-interval/; . instance (Ord c, Real c) => ToolShed.SelfValidate.SelfValidator (Criterion c) where getErrors c = ToolShed.SelfValidate.extractErrors [ ( any ($ c) [(< minBound), (> maxBound)], "'" ++ show (realToFrac $ deconstruct c :: Double {-hide the data-constructor & the actual type-}) ++ "' should be within the closed unit-interval '[0,1]'" ) -- Pair. ] -- | Smart constructor. mkCriterion :: Real c => String -> c -> Criterion c mkCriterion name r | ToolShed.SelfValidate.isValid criterion = criterion | otherwise = error $ "WeekDaze.ExecutionConfiguration.Criterion.mkCriterion:\t" ++ show name ++ " " ++ ToolShed.SelfValidate.getFirstError criterion ++ "." where criterion = MkCriterion r -- | Build a /criterion/ from a 'Bool', arbitrarily assuming 'True' is better than 'False'. mkCriterionFrom :: Num c => Bool -> Criterion c mkCriterionFrom True = maxBound mkCriterionFrom _ = minBound -- | Define the middle of the range of possible values. median :: Fractional c => Criterion c median = MkCriterion $ recip 2 -- | Map a natural number into the unit-interval, by reflecting about 'one' & compressing the range; so that 'one' remains 'one', but 'infinity' becomes 'zero'. invertNaturalNumbersIntoUnitInterval :: (Fractional f, Real f) => String -> f -> Criterion f invertNaturalNumbersIntoUnitInterval name = mkCriterion name . recip -- | Map a whole number into the unit-interval, so that zero becomes one, & infinity becomes zero. invertWholeNumbersIntoUnitInterval :: ( Enum c, Fractional c, Real c ) => String -> c -> Criterion c invertWholeNumbersIntoUnitInterval name = invertNaturalNumbersIntoUnitInterval name . succ {-avoid divide-by-zero-} {- | * Reflect a number in the unit-interval, so that zero becomes one, & one becomes zero. * CAVEAT: if the number provided exceeds one, then an error will be generated. -} reflectUnitInterval :: Real c => String -> c -> Criterion c reflectUnitInterval name = mkCriterion name . (1 -) {- | * Calculates the /weighted mean/ of the specified criterion-values using the corresponding criterion-weights. * Also writes individual unweighted criterion-values, to facilitate post-analysis; if the corresponding weight is zero, evaluation of the criterion is avoided, both for efficiency & to avoid the possibility of generating an error while evaluating a criterion which may have no validity in the context of the current problem. * CAVEAT: if all weights are zero, then the result can't be evaluated. -} calculateWeightedMean :: ( Fractional weightedMean, Real criterionValue, Real criterionWeight ) => [(Criterion criterionValue, ExecutionConfiguration.CriterionWeight.CriterionWeight criterionWeight)] -> Control.Monad.Writer.Writer [Maybe criterionValue] weightedMean calculateWeightedMean assocList | all ( (== minBound) . snd ) assocList = error "WeekDaze.ExecutionConfiguration.Criterion.calculateWeightedMean:\tzero weight => indeterminate result." | otherwise = Control.Monad.Writer.writer . ( Factory.Math.Statistics.getWeightedMean &&& map ( \(criterionValue, criterionWeight) -> if criterionWeight == 0 then Nothing -- Avoid unnecessary evaluation. else Just criterionValue ) ) $ map (deconstruct *** ExecutionConfiguration.CriterionWeight.deconstruct) assocList