{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Math.Programming.Tests.LP where import Control.Monad import Control.Monad.IO.Class import qualified Data.Text as T import Math.Programming import Test.Hspec import Text.Printf makeLPTests :: (MonadIO m, MonadLP v c o m) => -- | The runner for the API being tested. (m () -> IO ()) -> -- | The resulting test suite. Spec makeLPTests runner = describe "LP problems" $ do it "solves the diet problem" (runner dietProblemTest) data Food = Corn | Milk | Bread deriving ( Eq, Ord, Read, Show ) data Nutrient = Calories | VitaminA deriving ( Eq, Ord, Read, Show ) dietProblemTest :: (MonadIO m, MonadLP v c o m) => m () dietProblemTest = let cost :: Food -> Double cost Corn = 0.18 cost Milk = 0.23 cost Bread = 0.05 nutrition :: Nutrient -> Food -> Double nutrition Calories Corn = 72 nutrition VitaminA Corn = 107 nutrition Calories Milk = 121 nutrition VitaminA Milk = 500 nutrition Calories Bread = 65 nutrition VitaminA Bread = 0 foods :: [Food] foods = [Corn, Milk, Bread] nutrients :: [Nutrient] nutrients = [Calories, VitaminA] maxServings :: Double maxServings = 10 nutrientBounds :: Nutrient -> (Double, Double) nutrientBounds Calories = (2000, 2250) nutrientBounds VitaminA = (5000, 50000) expected :: Food -> Double expected Corn = 1.94 expected Milk = 10 expected Bread = 10 expectedCost :: Double expectedCost = 3.15 amountInterval :: Bounds amountInterval = Interval 0 maxServings amountName :: Food -> T.Text amountName food = T.pack $ printf "amount[%s]" (show food) nutrientMaxName :: Nutrient -> T.Text nutrientMaxName nutrient = T.pack $ printf "%s_max" (show nutrient) nutrientMinName :: Nutrient -> T.Text nutrientMinName nutrient = T.pack $ printf "%s_min" (show nutrient) in do -- Create the decision variables amounts <- forM foods $ \food -> do v <- free `within` amountInterval setVariableName v (amountName food) return (food, v) -- Create the nutrient constraints forM_ nutrients $ \nutrient -> do let lhs = esum [nutrition nutrient food *. v | (food, v) <- amounts] (lower, upper) = nutrientBounds nutrient u <- lhs .<= upper setConstraintName u (nutrientMaxName nutrient) l <- lhs .>= lower setConstraintName l (nutrientMinName nutrient) pure () -- Set the objective let objectiveExpr = esum [cost food *. v | (food, v) <- amounts] objective <- addObjective objectiveExpr setObjectiveSense objective Minimization -- Solve the problem status <- optimizeLP -- Check that we reached optimality liftIO $ status `shouldBe` Optimal -- Check the variable values forM_ amounts $ \(food, v) -> do x <- getVariableValue v liftIO $ abs (x - expected food) `shouldSatisfy` (<= 1e-1) -- Check the objective value objectiveValue <- evalExpr objectiveExpr liftIO $ abs (objectiveValue - expectedCost) `shouldSatisfy` (<= 1e-1)