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:
Create a CAS session or a SAS session
Initialize the model
Process the input data
Add the model components
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.
Out[20]:
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
Value
Label
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
period
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;
quit;
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.
Out[26]:
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