Use Scalar Parameters for SFS Data Binding in F#

Parameters in Solver Foundation Services (SFS) provide a convenient way of separating models from their input data. In the MSF-SFSProgrammingPrimer doc included in the release, we have the following example:

Sample 4: Using Parameters for LINQ Data Binding

Often we will want to iteratively run the same model with different data parameters. Instead of recreating the same model again using different data, you can use a Parameter object. The Parameter is bound to a database by LINQ, or to any other object which implements IEnumerable<T>. The current values are pulled from the data source each time the model is run.

SolverContext context = SolverContext.GetContext();

Model model = context.CreateModel();

Decision vz = new Decision(Domain.RealNonnegative, "barrels_venezuela");

Decision sa = new Decision(Domain.RealNonnegative, "barrels_saudiarabia");

model.AddDecisions(vz, sa);

// A parameter represents an input to the model

Parameter maxvz = new Parameter(Domain.RealNonnegative, "maxproduction_venezuela");

Parameter maxsa = new Parameter(Domain.RealNonnegative, "maxproduction_saudiarabia");

// A parameter is bound to a data source using LINQ

maxvz.SetBinding(from row in ProductionCapacity where row.Country == "Venezuela" select row, "MaxProduction");

maxsa.SetBinding(from row in ProductionCapacity where row.Country == "Saudi Arabia" select row, "MaxProduction");

model.AddParameters(maxvz, maxsa);

// We use parameters instead of literal numbers to represent the maximum

// production

model.AddConstraints("limits",

  0 <= vz <= maxvz,

  0 <= sa <= maxsa);

model.AddConstraints("production",

  0.3 * sa + 0.4 * vz >= 2000,

  0.4 * sa + 0.2 * vz >= 1500,

  0.2 * sa + 0.3 * vz >= 500);

model.AddGoal("cost", GoalKind.Minimize,

  20 * sa + 15 * vz);

Solution solution = context.Solve(new SimplexDirective());

Report report = solution.GetReport();

Console.WriteLine("vz: {0}, sa: {1}", vz, sa);

Console.Write("{0}", report);

Here maxvz and maxsa are two parameters whose values come externally. The model does not need to change when it accepts different values for the two parameters.

In this example, the two parameters are scalar parameters as they do not involve any index sets. Note that the signature of Parameters.SetBinding is as follows:

// Binds the parameter to data. Each parameter must be bound

// before solving.

// The data must be specified as a sequence of objects, where

// each object has properties for the value and index(es) of

// the data element. The data is read each time Context.Solve()

// is called.

public void SetBinding<T>(IEnumerable<T> binding,

                          string valueField,

                          params string[] indexFields);

When there is no indexFields specified, the parameter instance is scalar.

Now the problem in F# is that, F# does not recognize the params attribute in this method and requires any call to Parameter.SetBinding to have three parameters being passed in. Here is a trick to define scalar parameters in F#. The sample code is shown below.

#light

open System

open System.Runtime.InteropServices

open Microsoft.SolverFoundation.Common

open Microsoft.SolverFoundation.Services

/// Need a simple class to hold the value

type RowBound = { Value:double }

do

    let context = SolverContext.GetContext()

    let model = context.CreateModel()

   

    let rational x = Rational.op_Implicit(x:float)

    let constant x = Term.op_Implicit(x:float)

    let maxbound(t,x) = Model.LessEqual([|t; x|])

    let minbound(t,x) = Model.GreaterEqual([|t; x|])

   

    /// Data source for Parameters

    let constant1 = [| { Value = 430.0 } |]

    let constant2 = [| { Value = 460.0 } |]

    let constant3 = [| { Value = 420.0 } |]

   

let boundC1 = Parameter(Domain.RealNonnegative,

                        "boundC1")

/// The trick is to use an empty array [| |]

/// as the third parameter.

    boundC1.SetBinding(constant1, "Value", [| |])

    model.AddParameter(boundC1)

   

let boundC2 = Parameter(Domain.RealNonnegative,

                      "boundC2")

    boundC2.SetBinding(constant2, "Value", [| |])

    model.AddParameter(boundC2)

let boundC3 = Parameter(Domain.RealNonnegative,

                        "boundC3")

    boundC3.SetBinding(constant3, "Value", [| |])

    model.AddParameter(boundC3)

    let x = Decision(Domain.Real, "x")

    let y = Decision(Domain.Real, "y")

    let z = Decision(Domain.Real, "z")

   

    model.AddDecision(x)

    model.AddDecision(y)

    model.AddDecision(z)

let AddConstraint = model.AddConstraint("x>=0",

                    minbound(x, constant 0.0))

let AddConstraint = model.AddConstraint("y>=0",

                    minbound(y, constant 0.0))

let AddConstraint = model.AddConstraint("z>=0",

                    minbound(z, constant 0.0))

let AddGoal = model.AddGoal("goal", GoalKind.Maximize,

  constant 3.0 * x + constant 2.0 * y + constant 5.0 * z)

let AddConstraint = model.AddConstraint("c1",

  maxbound(constant 1.0 * x + constant 2.0 * y +

           constant 1.0 * z, boundC1))

let AddConstraint = model.AddConstraint("c2",

  maxbound(constant 3.0 * x + constant 2.0 * z, boundC2))

let AddConstraint = model.AddConstraint("c3",

  maxbound(constant 1.0 * x + constant 4.0 * y, boundC3))

    let sd = SimplexDirective()

    sd.Algorithm <- SimplexAlgorithm.Dual

    sd.GetSensitivity <- true

    let sol = context.Solve([|(sd :> Directive)|])

    let report = sol.GetReport()

   

    Console.WriteLine(report)

Note that the use of Parameter instances in this example is especially useful because, without the use of Parameters, we will not get shadow pricing in the sensitivity analysis from SFS because in general there is no one-to-one correspondence between a row in the linear model and a constraint in SFS (a constraint in SFS could be aggregated).

 - Lengning Liu