Monday, May 11, 2009

MS OML model using C# and data from Access

Looking at http://code.msdn.microsoft.com/solverfoundation/Thread/List.aspx it is noticeable that many users have problems coding simple problems in C#. One possible reason is that they use C# to assemble the model. Although not extremely difficult, it is gives very unwieldy models: the signal-to-noise ratio in the code is small as you need lots of syntactic clutter just to specify all variables and constraints compared to a specialized modeling language. Large models can easily have dozens of blocks of variables and equations. In this post I want to emphasize an alternative that is somewhat underrated: it is possible to use OML directly in your C# application. That will immediately make the model much more compact and readable.

The second issue is that the data-binding is often not completely straightforward. Many questions are related to data binding. Below is the simplest solution I could come up with for the following architecture: the math programming model is a simple transportation model and all data is stored in an Access database. The goal is to provide a skeleton example that is both readable and simple while being useful as a starting point for larger, more complex applications. The advanced features of LINQ as used throughout the Solver Foundation documentation are largely geared towards SQL Server. Therefore I wanted to explore how a simpler database like Access could be handled. If Access is working, there is good reason to believe that any other major database will also work, as we are working with the lowest common denominator in some respects. Many databases are accessible through OleDb. In practice it may be a problem that all data has to come from the database: OML has no facilities for data manipulation. Large models often have large amount of data, which may need some form of processing (aggregation etc.). Even if your real database is say Oracle, it may be useful to use Access as front-end tool to perform these data manipulation steps.

The model is the trnsport.gms model from the GAMS model library. It is small and has a few small parameters. For more info see http://www.amsterdamoptimization.com/models/msf/oml.pdf. In OML the model looks like:

Model[
  Parameters[Sets,Plants,Markets],
  Parameters[Reals,Capacity[Plants],Demand[Markets],Cost[Plants,Markets]],

  Decisions[Reals[0,Infinity],x[Plants,Markets],TotalCost],

  Constraints[
     TotalCost == Sum[{i,Plants},{j,Markets},Cost[i,j]*x[i,j]],
     Foreach[{i,Plants}, Sum[{j,Markets},x[i,j]]<=Capacity[i]],
     Foreach[{j,Markets}, Sum[{i,Plants},x[i,j]]>=Demand[j]]
  ],

  Goals[Minimize[TotalCost]]
]

In C# we can do:

/// <summary>
/// Holds the OML model
/// </summary>
string strModel = @"Model[
      Parameters[Sets,Plants,Markets],
      Parameters[Reals,Capacity[Plants],Demand[Markets],Cost[Plants,Markets]],

      Decisions[Reals[0,Infinity],x[Plants,Markets],TotalCost],

      Constraints[
         TotalCost == Sum[{i,Plants},{j,Markets},Cost[i,j]*x[i,j]],
         Foreach[{i,Plants}, Sum[{j,Markets},x[i,j]]<=Capacity[i]],
         Foreach[{j,Markets}, Sum[{i,Plants},x[i,j]]>=Demand[j]]
      ],

      Goals[Minimize[TotalCost]]
   ]";

followed by:

SolverContext context;
context.LoadModel(FileFormat.OML, new StringReader(strModel));
Solution solution = context.Solve();
Console.Write("{0}", solution.GetReport());

This was easy and as short as can be. Now we need to get the data. The database is organized as:

image

We will use the tables Capacity and Demand and the Query Cost.  The data looks like:

image image image

To bind the data we use the following code:

/// <summary>
/// Solve the problem
/// </summary>
public void Solve()
{
    context.LoadModel(FileFormat.OML, new StringReader(strModel));

    foreach (Parameter p in context.CurrentModel.Parameters)
    {
        switch (p.Name)
        {
            case "Capacity":
                setBinding(p,"select plant,capacity from capacity",
                    "capacity", new string[]{"plant"});
                break;
            case "Demand":
                setBinding(p,"select market,demand from demand",
                    "demand", new string[]{"market"});
                break;
            case "Cost":
                setBinding(p,"select plant,market,cost from cost",
                    "cost", new string[]{"plant", "market"});
                break;
        }

    }

    Solution solution = context.Solve();
    Console.Write("{0}", solution.GetReport());

}

In each binding operation we specify:

  1. The SFS parameter, which we retrieve from the CurrentModel
  2. The query to be used against the database
  3. The name of the data column
  4. The names of the index columns (passed on as an array of strings)

The complete model looks like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Data.OleDb;
using System.Data.Linq;
using System.Text;
using Microsoft.SolverFoundation.Services;
using System.IO;

namespace OML1
{
class Trnsport
{
/// <summary>
/// Called by the OS
/// </summary>
/// <param name="args"></param>
static void Main(string[] args)
{
Trnsport t = new Trnsport();
t.Solve();
}

/// <summary>
/// Holds the OML model
/// </summary>
string strModel = @"Model[
Parameters[Sets,Plants,Markets],
Parameters[Reals,Capacity[Plants],Demand[Markets],Cost[Plants,Markets]],

Decisions[Reals[0,Infinity],x[Plants,Markets],TotalCost],

Constraints[
TotalCost == Sum[{i,Plants},{j,Markets},Cost[i,j]*x[i,j]],
Foreach[{i,Plants}, Sum[{j,Markets},x[i,j]]<=Capacity[i]],
Foreach[{j,Markets}, Sum[{i,Plants},x[i,j]]>=Demand[j]]
],

Goals[Minimize[TotalCost]]
]";

/// <summary>
/// Connection string for MS Access
/// Use x86 architecture!
/// </summary>
string connection = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source=C:\projects\ms\OML1\OML1\trnsport.accdb;Persist Security Info=False;";

/// <summary>
/// SFS
/// </summary>
SolverContext context;

/// <summary>
/// Constructor
/// </summary>
public Trnsport()
{
context = SolverContext.GetContext();
}

/// <summary>
/// get query result as DataSet
/// </summary>
/// <param name="connection">connection string</param>
/// <param name="query">query as string</param>
/// <returns></returns>
private DataSet SelectOleDbSrvRows(string connection, string query)
{
DataSet ds = new DataSet();
OleDbConnection conn = new OleDbConnection(connection);
OleDbDataAdapter adapter = new OleDbDataAdapter();
adapter.SelectCommand = new OleDbCommand(query, conn);
adapter.Fill(ds);
return ds;
}

/// <summary>
/// Perform some magic to make sure the query output arrives in OML model.
/// </summary>
/// <param name="p">OML/SFS parameter</param>
/// <param name="query">database query</param>
/// <param name="valueColumn">column with values</param>
/// <param name="IndexColumns">columns with indices</param>
private void setBinding(Parameter p, string query, string valueColumn, string[] IndexColumns)
{
DataSet ds = SelectOleDbSrvRows(connection, query);
DataTable dt = ds.Tables[0];
p.SetBinding(dt.AsEnumerable(), valueColumn, IndexColumns);
}

/// <summary>
/// Solve the problem
/// </summary>
public void Solve()
{
context.LoadModel(FileFormat.OML, new StringReader(strModel));

foreach (Parameter p in context.CurrentModel.Parameters)
{
switch (p.Name)
{
case "Capacity":
setBinding(p,"select plant,capacity from capacity",
"capacity",new string[]{"plant"});
break;
case "Demand":
setBinding(p,"select market,demand from demand",
"demand", new string[]{"market"});
break;
case "Cost":
setBinding(p,"select plant,market,cost from cost",
"cost", new string[]{"plant", "market"});
break;

}

}

Solution solution = context.Solve();
Console.Write("{0}", solution.GetReport());

}

}
}

Some notes:

  • This should work with almost any database. Just change the connection string accordingly.

  • MS Access drivers are 32 bit so make sure you compile as 32 bit application. When targeting a 64 bit environment I got an exception about not being able to find an appropriate driver.

  • The contents of the sets are derived from the parameter bindings: set elements are the union of the set elements used in the parameter binding.

  • It may be useful to test the model beforehand using the Excel plug-in.

  • Should use only one connection: make OleDbConnection conn a field of the object.

  • How to write results back? This is often more complicated.