module Diet where
import Control.Monad
import Foreign.C.Types
import Foreign.Ptr
import Test.Tasty
import Test.Tasty.HUnit
import Text.Printf
import Math.Programming.Glpk.Header
test_diet :: TestTree
test_diet = testGroup "Diet problem"
[ testCase "Basic diet problem" basicDiet
]
data Food = Corn | Milk | Bread
deriving
( Eq
, Ord
, Read
, Show
)
data Nutrient = Calories | VitaminA
deriving
( Eq
, Ord
, Read
, Show
)
basicDiet :: Assertion
basicDiet =
let
cost :: Food -> CDouble
cost Corn = 0.18
cost Milk = 0.23
cost Bread = 0.05
nutrition :: Nutrient -> Food -> CDouble
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 :: CDouble
maxServings = 10
nutrientBounds :: Nutrient -> (CDouble, CDouble)
nutrientBounds Calories = (2000, 2250)
nutrientBounds VitaminA = (5000, 50000)
numAmountVariables :: CInt
numAmountVariables = fromIntegral $ length foods
numNutrientConstraints :: CInt
numNutrientConstraints = fromIntegral (length nutrients)
expected :: Food -> CDouble
expected Corn = 1.94
expected Milk = 10
expected Bread = 10
expectedCost :: CDouble
expectedCost = 3.15
in do
_ <- glp_term_out glpkOff
problem <- glp_create_prob
-- Create the decision variables
amountStart <- glp_add_cols problem numAmountVariables
let amounts = zip foods [amountStart..]
-- Create the nutrient constraints
nutrientConStart <- glp_add_rows problem numNutrientConstraints
let nutrientConstraints = zip nutrients [nutrientConStart..]
-- Set the objective
glp_set_obj_dir problem glpkMin
forM_ amounts $ \(food, column) ->
glp_set_obj_coef problem column (cost food)
-- Set the right-hand side of the nutrient constraints
forM_ nutrientConstraints $ \(nutrient, row) -> do
let (lower, upper) = nutrientBounds nutrient
glp_set_row_bnds problem row glpkBounded lower upper
-- Set the body of the nutrient constraints
forM_ nutrientConstraints $ \(nutrient, row) ->
allocaGlpkArray (map snd amounts) $ \indices ->
allocaGlpkArray (map (nutrition nutrient . fst) amounts) $ \coefs ->
glp_set_mat_row problem row numAmountVariables indices coefs
-- Ensure we don't have too many servings of any one food
forM_ amounts $ \(_, column) ->
glp_set_col_bnds problem column glpkBounded 0 maxServings
-- Check that we reached optimality
status <- glp_simplex problem nullPtr
status @?= glpkSimplexSuccess
-- Check the variable values
forM_ amounts $ \(food, column) -> do
x <- glp_get_col_prim problem column
let correct = expected food
msg = printf
"Amount of %s should be about %.2f, but is %.3f"
(show food)
(realToFrac correct :: Double)
(realToFrac x :: Double)
assertBool msg (abs (x - correct) <= 1e-1)
-- Check the objective value
objective <- glp_get_obj_val problem
let msg = printf
"Objective should be about %.2f, but is %.3f"
(realToFrac expectedCost :: Double)
(realToFrac objective :: Double)
assertBool msg (abs (objective - expectedCost) < 1e-1)