packages feed

weekdaze-0.0.0.1: src-lib/WeekDaze/ExecutionConfiguration/Criterion.hs

{-
	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 <http://www.gnu.org/licenses/>.
-}
{- |
 [@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/; <https://en.wikipedia.org/wiki/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