C# Development Using Mathematica

Frank J. Kampas
fkampas@wamsystems.com

WAM Systems
Plymouth Meeting, Pa

Introduction

Most of the C# development I do involves "number crunching".  When I develop a C# application, I start by doing a prototype in Mathematica and include tests in the notebook.  When the prototype is working properly I move it piece by piece to C#, using NETLink to connect to the C# code, and rerun the tests after each change.  When the tests in the notebook show a problem with the code I've just written in C#, I use Mathematica to diagnose the problem.  Originally I wrote the C# code in Microsoft Visual Studio.  This meant going back and forth between my notebook and Visual Studio and necessitated uninstalling NETLink to free up the C# dll each time.  Eventually I realized that I could actually write and compile C# code in my notebook, using the .NET runtime built-in compiler, called through NETLink.  This talk illustrates how to do that.  (Compiling C# inside Mathematica using NETLink was described previously by "ToolmakerSteve at shawstudio.com" in MathGroup.

Compiling C# Inside a Notebook

Mathematica Code

Set Up

The first steps are loading and installing NETLink.

Needs["NETLink`"]
InstallNET[];

Compilation Function

The following function will compile a C# class inside a namespace.  (Note there is also a VBCodeProvider class for compiling VB.NET on the fly).  Depending on the functionality used in the code, it may be necessary to add more referenced assemblies.  If the compiler returns errors, the line numbers and position of the errors in the lines are returned.

compileNamespace[code_String]:=
NETBlock[Module[{provider,params,compilerResults,err,ct,scode},
provider = NETNew["Microsoft.CSharp.CSharpCodeProvider"];
params = NETNew["System.CodeDom.Compiler.CompilerParameters"];
params @ ReferencedAssemblies @ Add["System.dll"];
params @ ReferencedAssemblies @ Add["System.Data.dll"];
params @ ReferencedAssemblies @ Add["mscorlib.dll"];
params @ GenerateInMemory = True;
compilerResults=provider @ CompileAssemblyFromSource[params,{code}];
provider@Dispose[];
err = compilerResults @ Errors;
ct = err@Count;
If[ct > 0,
	scode = StringSplit[code,"\n"]; 
	Print["number of errors = ",ct];
	Print[ 
err[#]@ErrorText,
	" in line ",
	err[#]@ Line,
	StringJoin @ Insert[Characters[scode[[err[#]@Line]]],ToString[▶],err[#]@Column]
		]& /@ Range[0,ct-1];
	Return[]
];
compilerResults@CompiledAssembly
]]

Simple Example

C# Code

The following C# code had one class, add and two functions for adding two integers.  One function is static, the other is not.

ncode = 
"using System;
namespace Nm
{
	public class add
	{
		public int f(int x, int y)
		{
			return x + y;
		}

		public static int fs(int x, int y)
		{
			return x + y;
		}
	}
}";

Compiling and Using the Code

The first step is compilation of the code using the compileNamespace function.  The result is a .NET Reflection Assembly.

res =compileNamespace[ncode]

To use the static function, it is necessary to use LoadNETType to load the class.  Calling the function is performed using the class name followed by ` and then the function with its arguments.  This is the standard NETLink procedure for calling a static function.

LoadNETType["Nm.add",res]

add`fs[2,4]

To use the non-static function, one first determines the RuntimeType and then uses NETNew to create an instance of the class to call the function.

ad=res@GetType["Nm.add"]

nad=NETNew[ad]

nad@f[3,5]

Error Trapping

If there is an error in the code, the compiler function will return the line and position of the error.

ncodeerror = 
"using System;
namespace Nm
{
	public class add
	{
		public int f(x, int y)
		{
			return x + y;
		}

		public static int fs(int x, int y)
		{
			return x + y;
		}
	}
}";

res1 =compileNamespace[ncodeerror]

SQL Reader

C# Code

The following C# code uses ADO.NET to perform SQL calls.  It returns a List of Object Arrays.

rdrcode = 
	"using System;
	   using System.Collections.Generic;
       using System.Data.SqlClient;
	namespace sql
	{
	   public class sqlrdr
	   {
			public static List<object[]> Rdr( string cnctstr, string sql)
			{
				SqlConnection con = new SqlConnection(cnctstr);
				con.Open();
				SqlCommand cmd = new SqlCommand(sql, con);
				cmd.CommandTimeout = 300;
				SqlDataReader r = cmd.ExecuteReader();
				List<object[]> a = new List<object[]>();
				int n = r.FieldCount;
				while(r.Read())
				{
					object[] v = new object[n];
					r.GetValues(v);
					a.Add(v);
				}
				r.Close();
				con.Close();
				return a;
			}
		}
	}";

The NETLink equivalent to the C# code is shown below.  It is over an order of magnitude slower.

readSQL[cnct_String,sql_String]:=
Module[
{res ,con,cmd,nl,r },
res = NETNew["System.Collections.Generic.List`1[[System.Object[]]]"];
con = NETNew["System.Data.SqlClient.SqlConnection",cnct];
cmd=NETNew["System.Data.SqlClient.SqlCommand",sql,con];
con@Open[];
r=cmd@ExecuteReader[];
n=r@FieldCount;
While[r@Read[],
values=NETNew["System.Object[]",n];
r@GetValues[values];
res@Add[values]
];
con@Close[];
NETObjectToExpression @ res 
]

Example Usage

The first step is compilation of the C# code.

sq = compileNamespace[rdrcode]

Since the function is static, LoadNETType is used.

LoadNETType["sql.sqlrdr",sq]

The server, database, user name and password in the connection string have been obscured for security reasons.

cstr= "server=****;database=****;uid=****;pwd=****;";

Some of the fields retrieved are Null, so it is necessary to get the .NET representation of DBNull.

nl = NETObjectToExpression[sqlrdr`Rdr[cstr,"select null"]][[1,1]]

The function getSql performs a SQL call s on the database.

getSql[s_String]:=NETObjectToExpression[sqlrdr`Rdr[cstr,s]]/. nl -> Null

Here's an example usage, selecting everything from the plans table.

getSql["select * from plans"]//TableForm

Calling the CPLEX Callable Library

C# Code

The CPLEX callable library is a C-type dll which called through the .NET DllImport.  The code also includes functions which call the dll.  NETLink can also interface to C-type dlls in this fashion but cannot set up C# functions which use those dll calls.

cplexcode = 
"using System;
   using System.Runtime.InteropServices;
   using System.Collections.Generic;
   using System.Text;
namespace nm
{
	public class cpx
	{
		private const string dllName = \"cplex112.dll\";
		
		[DllImport(dllName)] 
		public static extern int CPXopenCPLEX(ref int status);

		[DllImport(dllName)] 
		public static extern int CPXcreateprob(int env, ref int status, string probname);

		[DllImport(dllName)]
        public static extern void CPXchgobjsen(int env, int prob, int maxormin);

		[DllImport(dllName)]
        public static extern int CPXnewcols(int env, int prob, int ccnt, double[] obj,
           double[] lb, double[] ub, char[] ctype, string[] colname);

        [DllImport(dllName)]
        public static extern int CPXnewrows(int env, int prob, int rcnt, double[] rhs,
           string sense, double[] rngval, string[] rowname);

        [DllImport(dllName)]
        public static extern int CPXchgcoeflist(int env, int prob, int numcoefs, int[] rowlist, int[] collist, double[] vallist);

        [DllImport(dllName)]
        public static extern int CPXlpopt(int env, int prob);

        [DllImport(dllName)]
        public static extern int CPXgetitcnt(int env, int prob);

        [DllImport(dllName)]
        public static extern int CPXgetobjval(int env, int prob, ref double objval);

		[DllImport(dllName)]
        public static extern int CPXgetnumcols(int env, int prob);

        [DllImport(dllName)]
        public static extern int CPXgetnumrows(int env, int prob);

		[DllImport(dllName)]
        public static extern int CPXsolution(int env, int prob, ref int lpstat, 
           ref double objval, ref double x, ref double pi, ref double slack, ref double dj);

		[DllImport(dllName)] 
		public static extern int CPXfreeprob(int env, ref int prob);

		[DllImport(dllName)] 
		public static extern int CPXcloseCPLEX(ref int env);

		public static int[] getenv()
		{ 
			int status = 0;
			int env = CPXopenCPLEX(ref status);
			return new int[]{env, status};
		}

		public static int[] getprob(int env)
		{ 
			int status = 0;
			int prob = CPXcreateprob(env, ref status,\"test\");
			return new int[]{prob, status};
		}

		public static object[] getresults(int env, int prob, int rows, int cols)
		{
			double[] x = new double[cols];
			double[] pi = new double[rows];
			double[] slack = new double[rows];
			double[] dj = new double[cols];
			int lpstat = 0;
			double objval = 0.0;
			int status = CPXsolution(env, prob, ref lpstat, ref objval, ref x[0], ref pi[0], 
                ref slack[0], ref dj[0]);
			object[] res = new object[]{status, lpstat, objval, x, pi, slack, dj};
			return res;
		}

		
	}
}";

Example Usage

As before the first steps are to compile the code and load the NETType..

cpx = compileNamespace[cplexcode]

LoadNETType["nm.cpx",cpx]

This steps sets up the CPLEX environment.

{env,status} = cpx`getenv[]

The next step is setting up the problem.

{prob,status} = cpx`getprob[env]

Set the sense of the optimization (maximization).

cpx`CPXchgobjsen[env,prob,-1](* maximize *)

Set up the variables with objective function coefficiens, lower bounds, upper bounds and variable names.

numvars = 3;objcoefs = {1,2,-1};lbs = {0,0,0};ubs = {1000,1000,1000};varnames = {"x0","x1","x2"};

cpx`CPXnewcols[env,prob,numvars,objcoefs, lbs, ubs, Null, varnames]

Set up the constraints with their right-hand sides, types (<=), range values and names.

numcons = 3; rhs = {14, 28, 30};contypes = "LLL"; rngvals = {0,0,0};consnames = {"c0","c1","c2"};

cpx`CPXnewrows[env,prob,numcons, rhs, contypes, rngvals, consnames]

Set up the constraint coefficients by row, column and value.

numcoefs = 9; coefrows = {0,0,0,1,1,1,2,2,2};coefcols = {0,1,2,0,1,2,0,1,2};coefvals = {2.,1.,1.,4.,2.,3.,2.,5.,5.};

cpx`CPXchgcoeflist[env,prob,numcoefs, coefrows, coefcols, coefvals]

Optimize

cpx`CPXlpopt[env,prob]

Get the number of simplex iterations.

cpx`CPXgetitcnt[env,prob]

Get the call status, the status of the optimization, the objective function value, the variable values, the constraint dual values, the constraint slacks and the variable reduced costs.

{status, lpstat, objval, x, pi, slack, dj} = cpx`getresults[env, prob, numcons, numvars]

Release the problem.

cpx`CPXfreeprob[env,prob]

Free the CPLEX environment.

cpx`CPXcloseCPLEX[env]

Visualization Examples

The Constraint Coefficients of an Optimization Problem

The Log of the Values of the Variables After Optimization

A Parametric Study of Of One Part of the Objective Function vs the Whole Objective Function

A Customer's Supply Chain

An Inventory Simulation

Conclusions

Compilation of C# code inside Mathematica is straight-forward to do and can greatly simplify the development of C# numerical applications.  I suggest that this functionality be added to NETLink.