packages feed

glpk-headers-0.5.0: test/Math/Programming/Glpk/HeadersSpec.hs

module Math.Programming.Glpk.HeadersSpec where

import Control.Monad
import Foreign.C.Types
import Foreign.Ptr
import Math.Programming.Glpk.Header
import Test.Hspec
import Text.Printf

spec :: Spec
spec =
  describe "GPLK bindings" $ do
    it "can solve the diet problem" basicDiet

data Food = Corn | Milk | Bread
  deriving
    ( Eq,
      Ord,
      Read,
      Show
    )

data Nutrient = Calories | VitaminA
  deriving
    ( Eq,
      Ord,
      Read,
      Show
    )

basicDiet :: IO ()
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 `shouldBe` glpkSimplexSuccess

        -- Check the variable values
        forM_ amounts $ \(food, column) -> do
          x <- glp_get_col_prim problem column
          abs (x - expected food) `shouldSatisfy` (<= 1e-1)

        -- Check the objective value
        objective <- glp_get_obj_val problem
        abs (objective - expectedCost) `shouldSatisfy` (<= 1e-1)