Quick Reference

This is a short introduction to sasoptpy functionality, mainly for new users. You can find more details in the linked chapters.

Using sasoptpy usually consists of the following steps:

  1. Create a CAS session or a SAS session

  2. Initialize the model

  3. Process the input data

  4. Add the model components

  5. Solve the model

Solving an optimization problem via sasoptpy starts with having a running CAS (SAS Viya) Server or having a SAS 9.4 installation. It is possible to model a problem without a connection but solving a problem requires access to SAS Optimization or SAS/OR solvers at runtime.

Creating a session

Creating a SAS Viya session

To create a SAS Viya session (also called a CAS session), see SWAT documentation. A simple connection can be made using:

In [1]: from swat import CAS

In [2]: s = CAS(hostname, port, userid, password)

The last two parameters are optional for some use cases.

Creating a SAS 9.4 session

To create a SAS 9.4 session (also called a SAS session), see SASPy documentation. After customizing the configurations for your setup, you can create a session as follows:

import saspy
s = saspy.SASsession(cfgname='winlocal')

Initializing a model

After creating a CAS or SAS session, you can create an empty model as follows:

In [3]: import sasoptpy as so

In [4]: m = so.Model(name='my_first_model', session=s)
NOTE: Initialized model my_first_model.

This command initializes the optimization model as a Model object, called m.

Processing input data

The easiest way to work with sasoptpy is to define problem inputs as pandas DataFrames. You can define objective and cost coefficients, and lower and upper bounds by using the DataFrame and Series objects, respectively. See pandas documentation to learn more.

In [5]: import pandas as pd

In [6]: prob_data = pd.DataFrame([
   ...:     ['Period1', 30, 5],
   ...:     ['Period2', 15, 5],
   ...:     ['Period3', 25, 0]
   ...:     ], columns=['period', 'demand', 'min_prod']).set_index(['period'])

In [7]: price_per_product = 10

In [8]: capacity_cost = 10

You can refer the set PERIODS and the other fields demand and min_production as follows:

In [9]: PERIODS = prob_data.index.tolist()

In [10]: demand = prob_data['demand']

In [11]: min_production = prob_data['min_prod']

Adding variables

You can add a single variable or a set of variables to Model objects.

  • Model.add_variable() method is used to add a single variable.

    In [12]: production_cap = m.add_variable(vartype=so.INT, name='production_cap', lb=0)

    When working with multiple models, you can create a variable independent of the model, such as

    >>> production_cap = so.Variable(name='production_cap', vartype=so.INT, lb=0)

    Then you can add it to an existing model by using Model.include():

    >>> m.include(production_cap)
  • Model.add_variables() method is used to add a set of variables.

    In [13]: production = m.add_variables(PERIODS, vartype=so.INT, name='production',
       ....:                              lb=min_production)

    When the input is a set of variables, you can retrieve individual variables by using individual keys, such as production['Period1']. To create multidimensional variables, simply list all the keys as follows:

    >>> multivar = m.add_variables(KEYS1, KEYS2, KEYS3, name='multivar')

Creating expressions

Expression objects hold mathematical expressions. Although these objects are mostly used under the hood when defining a model, it is possible to define a custom Expression to use later.

When Variable objects are used in a mathematical expression, sasoptpy creates an Expression object automatically:

In [14]: totalRevenue = production.sum('*')*price_per_product

In [15]: totalCost = production_cap * capacity_cost

Note the use of the VariableGroup.sum() method over a variable group. This method returns the sum of variables inside the group as an Expression object. Its multiplication with a scalar price_per_product gives the final expression.

Similarly, totalCost is simply multiplication of a Variable object with a scalar.

Setting an objective function

You can define objective functions in terms of expressions. In this problem, the objective is to maximize the profit, so the Model.set_objective() method is used as follows:

In [16]: m.set_objective(totalRevenue-totalCost, sense=so.MAX, name='totalProfit')
Out[16]: sasoptpy.Expression(exp = 10 * production[Period1] + 10 * production[Period2] + 10 * production[Period3] - 10 * production_cap, name='totalProfit')

Notice that you can define the same objective by using:

>>> m.set_objective(production.sum('*')*price_per_product - production_cap*capacity_cost, sense=so.MAX, name='totalProfit')

The mandatory argument sense should be assigned the value of either so.MIN for a minimization problem or so.MAX for a maximization problems.

Adding constraints

In sasoptpy, constraints are simply expressions that have a direction. It is possible to define an expression and add it to a model by defining which direction the linear relation should have.

There are two methods to add constraints. The first is Model.add_constraint(), which adds a single constraint to amodel.

The second is Model.add_constraints(), which adds multiple constraints to a model.

In [17]: m.add_constraints((production[i] <= production_cap for i in PERIODS),
   ....:                   name='capacity')
Out[17]: sasoptpy.ConstraintGroup([production[Period1] - production_cap <=  0, production[Period2] - production_cap <=  0, production[Period3] - production_cap <=  0], name='capacity')
In [18]: m.add_constraints((production[i] <= demand[i] for i in PERIODS),
   ....:                   name='demand')
Out[18]: sasoptpy.ConstraintGroup([production[Period1] <=  30, production[Period2] <=  15, production[Period3] <=  25], name='demand')

The first term, provides a Python generator, which is then translated into constraints in the problem. The symbols <=, >=, and == are used for less than or equal to, greater than or equal to, and equal to, respectively. You can define range constraints by using the == symbol followed by a list of two values that represent lower and upper bounds.

In [19]: m.add_constraint(production['Period1'] == [10, 100], name='production_bounds')
Out[19]: sasoptpy.Constraint(production[Period1] ==  [10, 100], name='production_bounds')

Solving a problem

After a problem is defined, you can send it to the CAS server or SAS session by calling the Model.solve() method, which returns the primal solution when it is available, and None otherwise.

In [20]: m.solve()
NOTE: Added action set 'optimization'.
NOTE: Converting model my_first_model to OPTMODEL.
NOTE: Submitting OPTMODEL code to CAS server.
NOTE: Problem generation will use 8 threads.
NOTE: The problem has 4 variables (0 free, 0 fixed).
NOTE: The problem has 0 binary and 4 integer variables.
NOTE: The problem has 7 linear constraints (6 LE, 0 EQ, 0 GE, 1 range).
NOTE: The problem has 10 linear constraint coefficients.
NOTE: The problem has 0 nonlinear constraints (0 LE, 0 EQ, 0 GE, 0 range).
NOTE: The OPTMODEL presolver is disabled for linear problems.
NOTE: The initial MILP heuristics are applied.
NOTE: The MILP presolver value AUTOMATIC is applied.
NOTE: The MILP presolver removed all variables and constraints.
NOTE: Optimal.
NOTE: Objective = 400.
NOTE: The output table 'SOLUTION' in caslib 'CASUSER(casuser)' has 4 rows and 6 columns.
NOTE: The output table 'DUAL' in caslib 'CASUSER(casuser)' has 7 rows and 4 columns.
NOTE: The CAS table 'solutionSummary' in caslib 'CASUSER(casuser)' has 18 rows and 4 columns.
NOTE: The CAS table 'problemSummary' in caslib 'CASUSER(casuser)' has 20 rows and 4 columns.
Selected Rows from Table SOLUTION

     i                  var  value   lb             ub  rc
0  1.0       production_cap   25.0 -0.0  1.797693e+308 NaN
1  2.0  production[Period1]   25.0  5.0  1.797693e+308 NaN
2  3.0  production[Period2]   15.0  5.0  1.797693e+308 NaN
3  4.0  production[Period3]   25.0 -0.0  1.797693e+308 NaN

At the end of the solve operation, the solver returns a “Problem Summary” table and a “Solution Summary” table. These tables can later be accessed by using m.get_problem_summary() and m.get_solution_summary().

In [21]: print(m.get_solution_summary())
Selected Rows from Table SOLUTIONSUMMARY

Solver                           MILP
Algorithm              Branch and Cut
Objective Function        totalProfit
Solution Status               Optimal
Objective Value                   400
Relative Gap                        0
Absolute Gap                        0
Primal Infeasibility                0
Bound Infeasibility                 0
Integer Infeasibility               0
Best Bound                        400
Nodes                               0
Solutions Found                     3
Iterations                          0
Presolve Time                    0.01
Solution Time                    0.02

Printing solutions

You can retrieve the solutions by using the get_solution_table() method. It is strongly suggested that you group variables and expressions that share the same keys in a call.

In [22]: print(so.get_solution_table(demand, production))
        demand  production
Period1     30        25.0
Period2     15        15.0
Period3     25        25.0

Initializing a workspace

If you want to use the extensive abstract modeling capabilities of sasoptpy, you can create a workspace. Workspaces support features such as server-side for loops, cofor loops (parallel), reading and creating CAS tables. You can initialize a Workspace by using Python’s with keyword. For example, you can create a workspace that has a set and a variable group as follows:

In [23]: def create_workspace():
   ....:    with so.Workspace(name='my_workspace', session=s) as w:
   ....:       I = so.Set(name='I', value=range(1, 11))
   ....:       x = so.VariableGroup(I, name='x', lb=0)
   ....:    return w
In [24]: workspace = create_workspace()

In [25]: print(so.to_optmodel(workspace))
proc optmodel;
   set I = 1..10;
   var x {{I}} >= 0;

You can submit a workspace to a CAS server and retrieve the response by using:

In [26]: workspace.submit()
NOTE: Added action set 'optimization'.
NOTE: The output table 'SOLUTION' in caslib 'CASUSER(casuser)' has 10 rows and 6 columns.
NOTE: The output table 'DUAL' in caslib 'CASUSER(casuser)' has 0 rows and 4 columns.
Selected Rows from Table SOLUTION

      i    var  value   lb             ub  rc
0   1.0   x[1]    0.0  0.0  1.797693e+308 NaN
1   2.0   x[2]    0.0  0.0  1.797693e+308 NaN
2   3.0   x[3]    0.0  0.0  1.797693e+308 NaN
3   4.0   x[4]    0.0  0.0  1.797693e+308 NaN
4   5.0   x[5]    0.0  0.0  1.797693e+308 NaN
5   6.0   x[6]    0.0  0.0  1.797693e+308 NaN
6   7.0   x[7]    0.0  0.0  1.797693e+308 NaN
7   8.0   x[8]    0.0  0.0  1.797693e+308 NaN
8   9.0   x[9]    0.0  0.0  1.797693e+308 NaN
9  10.0  x[10]    0.0  0.0  1.797693e+308 NaN

Package configurations

sasoptpy comes with certain package configurations. The configuration parameters and their default values are as follows:

  • verbosity (default 3)

  • max_digits (default 12)

  • print_digits (default 6)

  • default_sense (default so.minimization)

  • default_bounds

  • valid_outcomes

It is possible to override these configuration parameters. As an example, consider the following constraint representation:

In [27]: x = so.Variable(name='x')

In [28]: c = so.Constraint(10 / 3 * x + 1e-20 * x ** 2 <= 30 + 1e-11, name='c')

In [29]: print(so.to_definition(c))
con c : 3.333333333333 * x + 0.0 * ((x) ^ (2)) <= 30.00000000001;

You can change the number of digits to be printed as follows:

In [30]: so.config['max_digits'] = 2
In [31]: print(so.to_definition(c))
con c : 3.33 * x + 0.0 * ((x) ^ (2)) <= 30.0;

You can remove the maximum number of digits to print as follows:

In [32]: so.config['max_digits'] = None
In [33]: print(so.to_definition(c))
con c : 3.3333333333333335 * x + 1e-20 * ((x) ^ (2)) <= 30.00000000001;

You can reset the parameter to its default value by deleting the parameter:

In [34]: del so.config['max_digits']

You can also create a new configuration to be used globally:

In [35]: so.config['myvalue'] = 2