Deployment Methods

Deployment on Azure functions

To facilitate installation and deployment of AMPL in .NET, we created a set of NuGet packages that include everything needed for the execution. A deployment of AMPL usable from .NET comprises of:

  1. The .NET Api (package AMPL.Api)

  2. AMPL binaries for the specific platform (package AMPL.Module.base.[platform])

  3. All required solver(s) binaries (package AMPL.Module.[solvername].[platform])

See for example this list for the platforms where a NuGet module with the AMPL binaries is available.

Tutorial: create and deploy an Azure function using Visual Studio

Visual Studio

  • Create a new project using the Azure functions template

Create new project
  • Right click on the project icon and click Manage NuGet Packages

  • Search and install AMPL.Api, AMPL.Module.base.mswin64 if deployment target is windows, or AMPL.Module.base.linux64 if target is linux. Also install AMPL.Module.highs.[platform] and AMPL.Module.gurobi.[platform]. Note: enable the “include prerelease” box.

Add NuGet packages
  • With the current template, there is a dependency conflict. Edit the project file and downgrade the module Microsoft.Azure.Functions.Worker.Sdk to version 1.16.2.

Add NuGet packages
  • When the source code is ready (see section below), right click on the project file and choose “Publish”. Create a new Azure functions app, or bind to an existing one and follow the instructions on screen.

Add NuGet packages

VS Code or command line

  • Follow this guide to create an azure functions project

  • To add relevant references, execute:

dotnet add package AMPL.Api --prerelease
dotnet add package AMPL.Module.base.linux64 --prerelease
dotnet add package AMPL.Module.highs.linux64 --prerelease
dotnet add package AMPL.Module.gurobi.linux64 --prerelease

Source code

In this tutorial, we will create a simple function that accepts a JSON payload. The JSON file will represent a dictionary with three entries:

  • “model” : a string containing AMPL statements

  • “data” : a dictionary containing mappings between AMPL parameters (defined in the string above) and their values

  • “solver : a string defining which solver to use

[Function("SolveAMPLModel")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req)
{
   if (req.Method == "POST")
   {
         // Read the request body
         string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
         var data = JsonConvert.DeserializeObject<dynamic>(requestBody);
         _logger.LogInformation($"Received data: {data}");

         if (data == null)
         {
            return new BadRequestObjectResult(new { message = "Empty data received" });
         }

         // Access individual members
         string model = data.model;
         var datavalues = data.data;
         string solver = data.solver;

         try
         {
            // Enable the NuGet packages
            AMPL.LoadModules();
         }
         catch (Exception ex)
         {
            _logger.LogCritical($"LoadModules exception: {ex.Message}");
            return new BadRequestObjectResult(new { message = ex.Message });
         }
         StringBuilder sb = new StringBuilder();
         try
         {
            using (var ampl = new AMPL())
            {
               _logger.LogInformation($"Created AMPL");
               // *** Output redirection *** Enable output redirection
               ampl.EnableOutputRouting();
               ampl.EnableErrorAndWarningRouting();

               // Assign handlers
               ampl.Output += (kind, message) => { _logger.LogInformation(message); sb.Append(message); };
               ampl.Error += (amplexception) => { _logger.LogCritical(amplexception.Message); sb.Append(amplexception.Message); };
               ampl.Warning += (amplexception) => { _logger.LogWarning(amplexception.Message); sb.Append(amplexception.Message); };
               // Evaluate the statement
               ampl.Eval(model);
               // Assign the data (note the limitations above)
               AssignDataFromJsonDictionary(datavalues, ampl);
               // Set the solver
               ampl.SetOption("solver", solver);
               // Solve the model
               ampl.Solve();

               // Get the first objective and the time taken to solve
               var objective = ampl.GetObjectives().First();
               var timeToSolve = ampl.GetValue("_total_solve_time");

               // Return information
               return new OkObjectResult(new { message = sb.ToString() + $"\n{objective.Name} = {objective.Value} (solved by {solver} in {timeToSolve}s)" });
            }
         }
         catch (Exception e)
         {
            return new OkObjectResult(new { message = e.Message });
         }
   }
   return new BadRequestObjectResult("Please send a POST request with JSON data.");
}

The following helper function is also (temporarily) used to pass JSON data to AMPL:

private void AssignDataFromJsonDictionary(JObject data, AMPL a)
{
   // Iterate through the key-value pairs in 'data'
   foreach (var item in data)
   {
         string key = item.Key;
         JToken value = item.Value;

         _logger.LogDebug($"Key: {key}, Value: {value}");

         // Check if the value is a number
         if (value.Type == JTokenType.Integer)
         {
            int number = value.ToObject<int>();
            a.Param[key].Set(number);
         }
         else if (value.Type == JTokenType.String)
         {
            string str = value.ToObject<string>();
            a.Param[key].Set(str);
         }
         // Check if the value is an array
         else if (value.Type == JTokenType.Array)
         {
            try
            {
               var arrdbl = value?.ToObject<double[]>();
               a.Param[key].SetValues(arrdbl);
               continue;
            }
            catch (Exception)
            { }
            try
            {
               var arrstr = value?.ToObject<string[]>();
               a.Param[key].SetValues(arrstr);
            }
            catch (Exception ex)
            {
               _logger.LogCritical($"Invalid type: {value.Type.ToString()}");
            }
         }
         else
            _logger.LogCritical($"Invalid type: {value.Type.ToString()}");
   }
}

Running the example

To run the example, we can use the following JSON file:

{
"model": "param a; param b; param c{1..2}; var x<=a+b+sum{i in 1..2} c[i]; maximize z: x;",
"data" :
{
   "a" : 11,
   "b" : 12,
   "c" :  [ 13, 14 ]
},
"solver" : "gurobi"
}

and submit it with CURL:

curl -X POST -H "Content-Type: application/json" -d @model.json https://amplfunctionlinux.azurewebsites.net/api/SolveAMPLModel?code=cepQpdsOwA9eFW_ZvCb31ho674FT8_j_YEoEWfdbFnqJAzFuT28gag==

Monitoring

In the Azure portal, we can see the logs of any execution:

Logs