Learning F#: Case study with branch and bound – Part II (page 3)

The code is getting a bit hard to follow with everything in one file, we can use some code organization features to better organize (see this and this), particularly when we will later add more instance types and examples. If you are using Visual Studio, see this regarding Visual Studio and the order that .fs files appear in the solution pane. Generally, all definitions must come before use so the input files need to be arranged as such, although there are provisions for mutual recursion.

F# namespaces are an organization of modules, classes and other namespaces, meant to be used to group these elements together, possibly hierarchically. They are not necessary, but can be used to avoid naming conflicts — in larger programs the same union member or class name may appear and namespaces can be used to distinguish them. This will arise for us later as well.

F# modules are also used for organization. According to this, you can think of them like static classes in C++ or C# and are mostly used to group together related functions. All functions are ultimately part of a module, however if a source code file does not include a namespace or module declaration then it is automatically placed into a module with the same name as the file — note we haven’t used these declarations to this point so everything we’ve done was placed in an unnamed module. The open statement works like an include from other languages to bring external defintions in. There is also an [<AutoOpen>] attribute that can be specified for a module, this makes all of the contents of the module visible externally for other modules in the same namespace without explicitly using the open statement.

So let’s start by moving the DiscreteVar into a separate file, this is the content of the DiscreteVarTypes file now:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
namespace BandBapplication

[<AutoOpen>]
module DiscreteVariableTypes =
    type ZeroOneVarSetting =
        | Unset                         (* this is a discriminated union type in F# *)
        | One
        | Zero
        static member isUnset a =
            match a with
            | Unset -> true
            | _ -> false
     

As can be seen, we placed the DiscreteVarTypes module in the BandBapplication namespace. We will place all of our other code in this namespace as well. We also specify the [<AutoOpen>] attribute, so that we do not need to explicitly use open DiscreteVarTypes in the other files in the namespace.

We next split our core branch and bound algortihm into its own .fs file, called BandB.fs:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
namespace BandBapplication

module BandB =

    let BandB f g branch unFinished node initLowestValue =
        let mutable bestUtility = initLowestValue;
        let mutable queue = [node]
        let mutable bestNode = node    // best full sol'n found

        // these are only informational, not needed in the routine
        let mutable numberOfQueues = 1   // we just queued the start, so 1
        let mutable numberOfEvals = 0    // number of times we evaluated the function with all vars set

        while not queue.IsEmpty do
            match queue with
            | curNode :: restOfQueue -> 
                if unFinished curNode then
                    let toQueue = branch g bestUtility curNode
                    queue <- toQueue @ restOfQueue
                    numberOfQueues <- numberOfQueues + toQueue.Length
                else
                    let util = f curNode         
                    numberOfEvals <- numberOfEvals + 1
                    if util > bestUtility then  (* Any assumptions? *)
                        bestUtility <- util
                        bestNode <- curNode
                                      
                    queue <- restOfQueue // we've processed curVars, take it off the queue
                                       
            | _ -> ()  // queue isempty, do nothing
       
        printfn "BandB: total settings queued = %d, total number of evals: %d" numberOfQueues numberOfEvals
        (bestUtility, bestNode)     

I am uncomfortable with setting bestNode on line 8 to the initial search node as it is not a full solution (indeed the opposite). I think it is better defined as an optional type but leave it for now

I took the liberty of making a few name changes here. These will make more sense as we add different problem types for the solver. The unsetVars function was renamed unFinished — the meaning is that a full solution setting has not yet been reached. The vars argument was renamed node to reflect that it is a node in the branch and bound search tree (see figures on prior pages). When this node is ‘finished’ then it means that all descisions have been selected, in the case of knapsack this means that all variables are set to Zero or One. A ‘node’ for knapsack is a partial setting of its variables, hence it was called vars before.

And then we split the functions particular to the 0-1 knapsack into their own file, called knapsack01. At the same time as doing this, I removed the active pattern match for the bound estimates against the bestUtility found as now we are also considering feasibility of the search node and its descendants (this will change once again later – its a development process).

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
    let branchknap wlimit g bestUtility vars =
        let unsetVarIndex = unsetVarIndex vars      // should only be called when there are Unset vars, so no -1 check
        let varArrayZero = vars 
                           |> Array.mapi (fun index var -> if index = unsetVarIndex then {var with Setting = Zero} else var)
        let varArrayOne  = vars 
                           |> Array.mapi (fun index var -> if index = unsetVarIndex then {var with Setting = One} else var)

        let gEstimateZero = g varArrayZero
        let gEstimateOne = g varArrayOne

        // not only use g but also ensure feasibilty!
        let zeroMakesWeight = (knapmultiplySumWeight varArrayZero) <= wlimit
        let oneMakesWeight = (knapmultiplySumWeight varArrayOne) <= wlimit

        let takeZero = zeroMakesWeight && gEstimateZero > bestUtility
        let takeOne = oneMakesWeight && gEstimateOne > bestUtility
                
        // update the queue, first match is the 
        match (takeZero, takeOne) with
        | true,true -> 
            varArrayOne :: [varArrayZero]
        | true,false -> 
            [varArrayZero]
        | false,true -> 
            [varArrayOne] 
        | _ -> [] // Neither case, nothing 

Finally, here is the main code:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
[<EntryPoint>]
let main argv = 
    (* *************************************************************************************** *)
    (* KNAPSACK EXAMPLE 1 *)
    let vars1 = [|{Name = "food"; Setting = ZeroOneVarSetting.Unset; Weight = 5.0; Utility = 8.0}; 
                    {Name = "tent"; Setting = ZeroOneVarSetting.Unset; Weight = 14.5; Utility = 5.0}; 
                    {Name = "gps"; Setting = ZeroOneVarSetting.Unset; Weight = 1.0; Utility = 3.0}; 
                    {Name = "map"; Setting = ZeroOneVarSetting.Unset; Weight = 0.5; Utility = 3.0} |]
    let weightLimit1 = 16.0
    let vars1Sorted = (Array.sortBy (fun elem -> -elem.Utility / elem.Weight) vars1)  // still have this

    let gknap1 = knapestimator weightLimit1
    let knapAnyUnset = Array.exists (fun (elem:KnapDiscreteVar) -> ZeroOneVarSetting.isUnset elem.Setting) 
    let branchknap1 = branchknap weightLimit1

    (* Invoke *)
    let (solutionUtility, solutionVars) = BandB knapmultiplySumUtility gknap1 branchknap1 knapAnyUnset vars1Sorted -1.0
    printfn "Best Utility = %f" solutionUtility
    for var in solutionVars do
        printfn "%A" var

    System.Console.ReadKey() |> ignore  // don't close the output window until a key is hit
    0
 

The old knapmultiplySumUtility is the f function, and each of gknap1 and branchknap1 leverage the knapsack specific functions with the problem instance weightLimit1 value baked in.

In the downloaded code you will notice that open DiscreteVarTypes is commented out, we don’t need this since we used the auto open feature for that module and we’re in the same namespace; of course we do use the ZeroOneVarSetting from that module. We defined KnapDiscreteVar here instead of in the DiscreteVarTypes module as that variable definition is specific to the knapsack problem. Otherwise everything works just as before, we’ve just gotten our code better organized.