{-
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