NetworkFlow Code Explanation – Chapter 19 Code in the Module Main Code There is no user form in this application. The application first retrieves the data from the Access database, and then it sets up an optimization model with these data, optimizes it, and reports the results. The main sub calls the various subs to perform these tasks. It first gets the node and arc data by calling the GetNodeInfo and GetArcInfo subs. If it happens that there are more than 200 arcs, it quits because Solver can handle only 200 changing cells. Otherwise, it turns off screen updating (to prevent screen flickering), and then it calls the CreateModel, RunSolver, and CreateReport subs to perform the remaining tasks. Finally, it turns screen updating back on. Sub Main() ' Get data on nodes and arcs from the Access database. Call GetNodeInfo Call GetArcInfo

' Quit if there are more than 200 arcs, because Solver can't handle this many. If NArcs > 200 Then MsgBox "The Solver can't handle this database because there " _ & "are more than 200 arcs (Solver's maximum number of changing " _ & "cells). Try again with a smaller network.", vbInformation, "Too many arcs" End End If

Application.ScreenUpdating = False

' Set up the model, solve it, and report the results. Call CreateModel Call RunSolver Call CreateReport

Application.ScreenUpdating = True End Sub GetNodeInfo Code The GetNodeInfo sub gets the data from the Nodes table of the Access database. It uses ADO to create a connection to the NetworkFlow.mdb database file, and then it opens a recordset based on the Nodes table. Once this recordset is open, it loops through the records and stores data about the nodes in arrays for later use in the Model sheet. You should refer to the general discussion of ADO in Section 19.7 as you examine this code. In the discussion below, we break this long sub into pieces. Sub GetNodeInfo() Dim cn As ADODB.Connection, rs As ADODB.Recordset

‘ Initialize counters. NSupNodes = 0 NTransNodes = 0 NDemNodes = 0 NNodes = 0 MaxPossFlow = 0 When getting data from an external database, the first step is to set up a connection object, that is, a connection to the database. The connection string below specifies the name and location of the database, assumed here to be in the same path as the application file. The provider indicates the type of database, in this case Access. ‘ The Access database must be in the same directory as this workbook. ' Open a connection to the database file. Set cn = New ADODB.Connection With cn .ConnectionString = "Data Source=" & ThisWorkbook.Path & "\NetworkFlow.mdb" .Provider = "Microsoft Jet 4.0 OLE DB Provider" .Open End With

Now that a connection has been established, the next step is to run a query on the database to obtain a “recordset”. In this case, the query is a table-type query: We ask for the entire Nodes table. The arguments adOpenKeyset and adLockOptimistic are optional and could be omitted. (Default values of these arguments would then be used, which would work fine in this application.) ' Open the Nodes table as a recordset. Set rs = New ADODB.Recordset With rs .Open "Nodes", cn, adOpenKeyset, adLockOptimistic

We now have the Nodes table in a recordset, that is, in temporary memory, so we are able to loop through its records, one for each node, and capture its field data in arrays for later use. Note how we refer to the data in a particular field. For example, rs.Fields(“NodeName”) gets the value of the field NodeName from the current record. ' Loop through the records of the recordset to capture the information on the nodes. Do Until .EOF

‘ Each new record means another node. NNodes = NNodes + 1 ReDim Preserve Node(NNodes)

‘ Get the node’s name from the NodeName field. Node(NNodes) = .Fields("NodeName")

We next capture the NodeType field value, which can be 1 (supplier), 2 (transshipment point), or 3 (demander), and we use a Case construction to distinguish the three types. In all cases, we redimension (with the Preserve option) the relevant arrays and then fill them with the data from the recordset. ' Do three cases, depending on whether the node is a supplier, transshipment point, or customer. Select Case .Fields("NodeType")

Case 1 ' a supplier

‘ Update the number of suppliers. NSupNodes = NSupNodes + 1 ReDim Preserve SupNode(NSupNodes) ReDim Preserve Capacity(NSupNodes)

‘ Get the supplier’s name and capacity from the NodeName and SupplyOrDemand fields. SupNode(NSupNodes) = .Fields("NodeName") Capacity(NSupNodes) = .Fields("SupplyOrDemand")

' MaxPossFlow is the total capacity of the supplier nodes. No arc could possibly ship more than this, so it ‘ serves as an upper bound on all arc flows for arcs without a given maximum flow requirement. MaxPossFlow = MaxPossFlow + Capacity(NSupNodes)

Case 2 ' a transshipment point

‘ Update number of transshipment nodes. NTransNodes = NTransNodes + 1 ReDim Preserve TransNode(NTransNodes)

‘ Get the node’s name from the NodeName field. TransNode(NTransNodes) = .Fields("NodeName")

Case 3 ' a customer

‘ Update the number of customers. NDemNodes = NDemNodes + 1 ReDim Preserve DemNode(NDemNodes) ReDim Preserve Demand(NDemNodes)

‘ Get the customer’s name and demand from the NodeName and SupplyOrDemand fields. DemNode(NDemNodes) = .Fields("NodeName") Demand(NDemNodes) = .Fields("SupplyOrDemand") End Select

When looping through the records of the recordset, it is important to remember to move to the next record. We do this with the MoveNext method of the recordset object. ‘ Move to the next record. .MoveNext Loop

Finally, we have extracted all of the data from this recordset, so we close it and then close the connection to the database. ' Close the recordset. .Close End With

' Close the connection to the database. cn.Close End Sub GetArcInfo Code The GetArcInfo sub is very similar. It also opens a connection to the database, and then it opens a recordset based on the Arcs table. Again, the Do loop goes through the arcs and stores data about them in arrays for later use in the Model sheet. Sub GetArcInfo() Dim cn As ADODB.Connection, rs As ADODB.Recordset Set cn = New ADODB.Connection

' Initialize the counter. NArcs = 0

' Open a connection to the database. With cn .ConnectionString = "Data Source=" & ThisWorkbook.Path & "\NetworkFlow.mdb" .Provider = "Microsoft Jet 4.0 OLE DB Provider" .Open End With

' Open the Arcs table as a recordset. Set rs = New ADODB.Recordset With rs .Open "Arcs", cn, adOpenKeyset, adLockOptimistic

' Loop through the records of the recordset to capture the information on the arcs. Do Until .EOF

‘ Each new record means another arc. NArcs = NArcs + 1

‘ Redimension the arrays appropriately. ReDim Preserve Origin(NArcs) ReDim Preserve Dest(NArcs) ReDim Preserve UnitCost(NArcs) ReDim Preserve MinFlow(NArcs) ReDim Preserve MaxFlow(NArcs)

Note that by now, the Node array has already been filled in. This was done earlier in the GetNodeInfo sub. For example, Node(1)=Pittsburgh, because Pittsburgh is the first node in the Node table. Now, in this sub, NArcs is a counter of all arcs seen so far. Therefore, the Origin and Dest statements below capture the names of the cities in the Origin and Dest arrays for each value of NArcs. As a specific example, the first arc in the arcs table, when NArcs=1, is from node 3 to node 1. By this time, our program knows that Node(1)=Pittsburgh and Node(3)=Scranton. Therefore, it will set Origin(1)=Node(3)=Scranton and Dest(1)=Node(1)=Pittsburgh in the code below. ‘ Fill the arrays with data from the current record. Origin(NArcs) = Node(.Fields("OriginID")) Dest(NArcs) = Node(.Fields("DestID")) UnitCost(NArcs) = .Fields("UnitCost") If .Fields("MinFlow") <> "" Then MinFlow(NArcs) = .Fields("MinFlow") Else MinFlow(NArcs) = 0 End If If .Fields("MaxFlow") <> "" Then MaxFlow(NArcs) = .Fields("MaxFlow") Else MaxFlow(NArcs) = MaxPossFlow End If

‘ Move to the next record. .MoveNext Loop

' Close the recordset. .Close End With

' Close the connection to the database. cn.Close End Sub CreateModel Code By this time, the required data have been captured from the Access database and have been stored in arrays. Now it is time to use these data to build the optimization model. The CreateModel sub begins by clearing the data in the Model sheet from a previous run, if any. Then it calls three subs, EnterArcData, FormConstraints, and CalcCost, to build the model. As you read the details of these subs, you should refer back to the Model sheet in Figure 19.3 of the book. Sub CreateModel() ' Clear any old model, which would be below cells A4 and H4 of the Model sheet. With Worksheets("Model") .Visible = True .Activate End With

With Range("A4") Range(.Offset(1, 0), .End(xlDown).Offset(0, 5)).ClearContents End With

With Range("H4") Range(.Offset(1, 0), .End(xlDown).Offset(0, 3)).ClearContents End With

' The following subroutines do the bulk of the work. Call EnterArcData Call FormConstraints Call CalcCost End Sub EnterArcData Code The EnterArcData sub transfers the data now in the Origin, Dest, UnitCost, MinFlow, and MaxFlow arrays to the appropriate columns of the Model sheet and then names some ranges for later use. Sub EnterArcData() ' This sub enters data for the arcs, including the changing cells (the flows). Dim i As Integer First, loop through all arcs and record the names of the origin and destination nodes, their unit costs, and their minimum and maximum allowable flows. We could use any initial flows on the arcs (and then let Solver eventually find the optimal flows). It is most convenient to set initial flows to 0. With Range("A4") For i = 1 To NArcs .Offset(i, 0) = Origin(i) .Offset(i, 1) = Dest(i) .Offset(i, 2) = UnitCost(i)

‘ Enter initial flows of 0. .Offset(i, 3) = 0

.Offset(i, 4) = MinFlow(i) .Offset(i, 5) = MaxFlow(i) Next

Now supply appropriate range names for later use. Each name refers to a long column. Range(.Offset(1, 0), .Offset(NArcs, 0)).Name = "Origins" Range(.Offset(1, 1), .Offset(NArcs, 1)).Name = "Dests" Range(.Offset(1, 2), .Offset(NArcs, 2)).Name = "UnitCosts" Range(.Offset(1, 3), .Offset(NArcs, 3)).Name = "Flows" Range(.Offset(1, 4), .Offset(NArcs, 4)).Name = "MinFlows" Range(.Offset(1, 5), .Offset(NArcs, 5)).Name = "MaxFlows" End With End Sub FormConstraints Code The FormConstraints sub is used to enter the left- and right-hand sides of the flow balance constraints for the network model. It does this separately for suppliers, transshipment points, and customers. Each left- hand side uses Excel’s SumIf function to find the net inflow or outflow for the node. The right-hand sides are the capacities for the suppliers, 0’s for the transshipment points, and the demands for the customers. (For more information on node balance constraints and the use of the SumIf function in this context, see Chapter 5 of Practical Management Science, 2nd edition. ) Sub FormConstraints() Dim i As Integer

The supplier constraints are typical. For each supplier, we record its name (from the SupNode array), a formula that sums all flows out of it minus all flows into it, a “<=” label, and its capacity. Then we range name the left and right-hand sides of the inequalities NetOutflows and Capacities for later use in the Solver setup. ' Supplier constraints: With Range("H4") For i = 1 To NSupNodes .Offset(i, 0) = SupNode(i) .Offset(i, 1).FormulaR1C1 = "=SumIf(Origins,RC[-1],Flows)-SumIf(Dests,RC[- 1],Flows)" .Offset(i, 2) = "<=" .Offset(i, 3) = Capacity(i) Next Range(.Offset(1, 1), .Offset(NSupNodes, 1)).Name = "NetOutflows" Range(.Offset(1, 3), .Offset(NSupNodes, 3)).Name = "Capacities" End With

The transshipment constraints are similar, except that they are equalities with right-hand sides equal to 0. ' Transshipment constraints: With Range("H4").Offset(NSupNodes, 0) For i = 1 To NTransNodes .Offset(i, 0) = TransNode(i) .Offset(i, 1).FormulaR1C1 = "=SumIf(Origins,RC[-1],Flows)-SumIf(Dests,RC[- 1],Flows)" .Offset(i, 2) = "=" .Offset(i, 3) = 0 Next Range(.Offset(1, 1), .Offset(NTransNodes, 1)).Name = "NetFlows" End With

The demand constraints are also similar, except that they are “>=” constraints with right-hand sides equal to the demands. Note here that the formula is reversed—it is the sum of flows into the node minus the sum of flows out of it. ' Customer constraints: With Range("H4").Offset(NSupNodes + NTransNodes, 0) For i = 1 To NDemNodes .Offset(i, 0) = DemNode(i) .Offset(i, 1).FormulaR1C1 = "=SumIf(Dests,RC[-1],Flows)-SumIf(Origins,RC[- 1],Flows)" .Offset(i, 2) = ">=" .Offset(i, 3) = Demand(i) Next Range(.Offset(1, 1), .Offset(NDemNodes, 1)).Name = "NetInflows" Range(.Offset(1, 3), .Offset(NDemNodes, 3)).Name = "Demands" End With End Sub CalcCost Code The CalcCost sub calculates the total shipping cost with a SumProduct formula in the Model sheet. Sub CalcCost() ' Calculate the total cost of all flows (the objective to minimize). Range("I1").Formula = "=Sumproduct(UnitCosts,Flows)" End Sub RunSolver Code The RunSolver sub sets up and then runs the Solver. Sub RunSolver() ' Set up and run the Solver. SolverReset SolverOk SetCell:=Range("TotalCost"), MaxMinVal:=2, ByChange:=Range("Flows") SolverAdd Range("Flows"), 1, "MaxFlows" SolverAdd Range("Flows"), 3, "MinFlows" SolverAdd Range("NetOutflows"), 1, "Capacities" SolverAdd Range("NetFlows"), 2, 0 SolverAdd Range("NetInflows"), 3, "Demands" SolverOptions AssumeLinear:=True, AssumeNonNeg:=True

The model could very possibly have no feasible solutions. This would occur when there is not enough supplier capacity or arc capacity to meet customer demand. Therefore, we check for this possibility by checking whether SolverSolve returns the index 5. If it does, an explanation is displayed, and the program ends. Otherwise, the Model sheet is hidden—the Report sheet is what the user wants to see. If SolverSolve(UserFinish:=True) = 5 Then Worksheets("Model").Visible = False Worksheets("Explanation").Activate MsgBox "There is no feasible solution. Evidently, there is not enough " _ & "capacity in the system to meet the demands.", vbInformation, "No feasible solution" End

Else ' Hide the Model sheet. Worksheets("Model").Visible = False End If End Sub CreateReport Code Finally, the CreateReport sub transfers the total shipping cost and the information about all arcs with positive flows to the Report sheet. Sub CreateReport() ' Summarize the optimal shipping policy on the Report sheet. Dim i As Integer, NPos As Integer

‘ Unhide and activate the Report sheet. With Worksheets("Report") .Visible = True .Activate End With

' Enter the total cost. Range("D3") = Range("TotalCost")

With Range("B6") ' Clear out old data from any previous run. Range(.Offset(1, 0), .Offset(0, 3).End(xlDown)).Clear

The variable NPos is a counter for the number of positive flows. (We could list arcs with 0 flows, but we decided to list only those with positive flows—the interesting arcs.) NPos indicates the appropriate row offset for placing the next positive flow on the Report sheet. ' For each arc, check whether there is any positive flow. If so, report its origin and ' destination nodes, its flow, and the total cost of this flow. NPos is a counter for the ' arcs with positive flows. NPos = 0 For i = 1 To NArcs If Range("Flows").Cells(i) > 0 Then NPos = NPos + 1 .Offset(NPos, 0) = Range("Origins").Cells(i) .Offset(NPos, 1) = Range("Dests").Cells(i) .Offset(NPos, 2) = Range("Flows").Cells(i) ‘ Calculate the cost of this flow. .Offset(NPos, 3) = Range("Flows").Cells(i) * Range("UnitCosts").Cells(i) End If Next End With

Finally, we format the results (not really necessary, but it makes the report look better), and we end by placing the cursor in cell D3. ' Do some formatting. With Range("B6") Range(.Offset(0, 0), .End(xlDown).End(xlToRight)).AutoFormat Format:=xlRangeAutoFormatColor2 Range(.Offset(0, 0), .Offset(0, 1)).HorizontalAlignment = xlLeft End With

Range("D3").Select End Sub