Models

Creating a model

You can create an empty model by using the Model constructor:

In [1]: import sasoptpy as so

In [2]: m = so.Model(name='model1')
NOTE: Initialized model model1.

Adding new components to a model

Add a variable:

In [3]: x = m.add_variable(name='x', vartype=so.BIN)

In [4]: print(m)
Model: [
  Name: model1
  Objective: MIN [0]
  Variables (1): [
    x
  ]
  Constraints (0): [
  ]
]

In [5]: y = m.add_variable(name='y', lb=1, ub=10)

In [6]: print(m)
Model: [
  Name: model1
  Objective: MIN [0]
  Variables (2): [
    x
    y
  ]
  Constraints (0): [
  ]
]

Add a constraint:

In [7]: c1 = m.add_constraint(x + 2 * y <= 10, name='c1')

In [8]: print(m)
Model: [
  Name: model1
  Objective: MIN [0]
  Variables (2): [
    x
    y
  ]
  Constraints (1): [
    x + 2 * y <=  10
  ]
]

Adding existing components to a model

A new model can use existing variables. The typical way to include a variable is to use the Model.include() method:

In [9]: new_model = so.Model(name='new_model')
NOTE: Initialized model new_model.

In [10]: new_model.include(x, y)

In [11]: print(new_model)
Model: [
  Name: new_model
  Objective: MIN [0]
  Variables (2): [
    x
    y
  ]
  Constraints (0): [
  ]
]

In [12]: new_model.include(c1)

In [13]: print(new_model)
Model: [
  Name: new_model
  Objective: MIN [0]
  Variables (2): [
    x
    y
  ]
  Constraints (1): [
    x + 2 * y <=  10
  ]
]

In [14]: z = so.Variable(name='z', vartype=so.INT, lb=3)

In [15]: new_model.include(z)

In [16]: print(new_model)
Model: [
  Name: new_model
  Objective: MIN [0]
  Variables (3): [
    x
    y
    z
  ]
  Constraints (1): [
    x + 2 * y <=  10
  ]
]

Note that variables are added to Model objects by reference. Therefore, after Model.solve() is called, the values of variables are replaced with optimal values.

Accessing components

You can access a list of model variables by using the Model.get_variables() method:

In [17]: print(m.get_variables())
[sasoptpy.Variable(name='x', lb=0, ub=1, vartype='BIN'), sasoptpy.Variable(name='y', lb=1, ub=10, vartype='CONT')]

Similarly, you can access a list of constraints by using the Model.get_constraints() method:

In [18]: c2 = m.add_constraint(2 * x - y >= 1, name='c2')

In [19]: print(m.get_constraints())
[sasoptpy.Constraint(x + 2 * y <=  10, name='c1'), sasoptpy.Constraint(2 * x - y >=  1, name='c2')]

To access a certain constraint by using its name, you can use the Model.get_constraint() method:

In [20]: print(m.get_constraint('c2'))
2 * x - y >=  1

Dropping components

You can drop a variable inside a model by using Model.drop_variable() method. Similarly, you can drop a set of variables by using the Model.drop_variables() method.

In [21]: m.drop_variable(y)

In [22]: print(m)
Model: [
  Name: model1
  Objective: MIN [0]
  Variables (1): [
    x
  ]
  Constraints (2): [
    x + 2 * y <=  10
    2 * x - y >=  1
  ]
]

You can drop a constraint by using the Model.drop_constraint() method. Similarly, you can drop a set of constraints by using the Model.drop_constraints() method.

In [23]: m.drop_constraint(c1)

In [24]: m.drop_constraint(c2)

In [25]: print(m)
Model: [
  Name: model1
  Objective: MIN [0]
  Variables (2): [
    x
    y
  ]
  Constraints (0): [
  ]
]
In [26]: m.include(c1)

In [27]: print(m)
Model: [
  Name: model1
  Objective: MIN [0]
  Variables (2): [
    x
    y
  ]
  Constraints (1): [
    x + 2 * y <=  10
  ]
]

Copying a model

You can copy an existing model by including the Model object itself.

In [28]: copy_model = so.Model(name='copy_model')
NOTE: Initialized model copy_model.

In [29]: copy_model.include(m)

In [30]: print(copy_model)
Model: [
  Name: copy_model
  Objective: MIN [0]
  Variables (2): [
    x
    y
  ]
  Constraints (1): [
    x + 2 * y <=  10
  ]
]

Note that all variables and constraints are included by reference.

Solving a model

A model is solved by using the Model.solve() method. This method converts Python definitions into OPTMODEL language and submits using SWAT or SASPy packages.

>>> m.solve()
NOTE: Initialized model model_1
NOTE: Added action set 'optimization'.
NOTE: Converting model model_1 to OPTMODEL.
...
NOTE: Optimal.
NOTE: Objective = 124.343.
NOTE: The Dual Simplex solve time is 0.01 seconds.

Solve options

Solver Options

You can pass either solve options from the OPTMODEL procedure or solve parameter from the solveLp and solveMilp actions by using the options parameter of the Model.solve() method.

>>> m.solve(options={'with': 'milp', 'maxtime': 600})
>>> m.solve(options={'with': 'lp', 'algorithm': 'ipm'})

The parameter with is used to specificy the optimization solver in OPTMODEL procedure. If the with parameter is not passed, PROC OPTMODEL chooses a solver that depends on the problem type. Possible with values are listed in the SAS/OR documentation.

You can find specific solver options in the SAS Optimization documentation:

The options parameter can also pass solveLp and solveMilp action parameter when frame=True is used when the Model.solve() method is called.

Call parameters

Besides the options parameter, you can pass following parameters into the Model.solve() method:

  • name: Name of the uploaded problem information

  • drop: Drops the data from server after the solve

  • replace: Replaces an existing data with the same name

  • primalin: Uses the current values of the variables as an initial solution.

    When the value of this parameter is True, the solve method grabs Variable objects’ _init fields. You can modify this field by using the Variable.set_init() method.

  • submit: Calls the CAS action or SAS procedure

  • frame: Uses the frame (MPS) method. If the value of this parameter is False, then the method uses OPTMODEL codes.

  • verbose: Prints the generated PROC OPTMODEL code or MPS DataFrame object before the solve

Getting solutions

After the solve is completed, all variable and constraint values are parsed automatically. You can access a summary of the problem by using the Model.get_problem_summary() method, and a summary of the solution by using the Model.get_solution_summary() method.

To print the values of any object, you can use the get_solution_table() method:

>>> print(so.get_solution_table(x, y))

All variables and constraints that are passed into this method are returned on the basis of their indices. See Examples for more details.

Tuning MILP model parameters

SAS Optimization solvers provide a variety of settings. However, it might be difficult to find the best settings for a particular model. In order to compare parameters and make a good choice, you can use the optimization.tune action for mixed integer linear optimization problems.

The Model.tune_parameters() method is a wrapper for the tune action. Consider the following knapsack problem example:

In [31]: def get_model():
   ....:    m = so.Model(name='knapsack_with_tuner', session=cas_conn)
   ....:    data = [
   ....:        ['clock', 8, 4, 3],
   ....:        ['mug', 10, 6, 5],
   ....:        ['headphone', 15, 7, 2],
   ....:        ['book', 20, 12, 10],
   ....:        ['pen', 1, 1, 15]
   ....:    ]
   ....:    df = pd.DataFrame(data, columns=['item', 'value', 'weight', 'limit']).set_index(['item'])
   ....:    ITEMS = df.index
   ....:    value = df['value']
   ....:    weight = df['weight']
   ....:    limit = df['limit']
   ....:    total_weight = 55
   ....:    get = m.add_variables(ITEMS, name='get', vartype=so.INT)
   ....:    m.add_constraints((get[i] <= limit[i] for i in ITEMS), name='limit_con')
   ....:    m.add_constraint(so.expr_sum(weight[i] * get[i] for i in ITEMS) <= total_weight, name='weight_con')
   ....:    total_value = so.expr_sum(value[i] * get[i] for i in ITEMS)
   ....:    m.set_objective(total_value, name='total_value', sense=so.MAX)
   ....:    return m
   ....: 

In [32]: m = get_model()
NOTE: Initialized model knapsack_with_tuner.

For this problem, you can compare configurations as follows:

In [33]: results = m.tune_parameters(tunerParameters={'maxConfigs': 10})
NOTE: Added action set 'optimization'.
NOTE: Uploading the problem DataFrame to the server.
NOTE: Cloud Analytic Services made the uploaded file available as table KNAPSACK_WITH_TUNER in caslib CASUSER(casuser).
NOTE: The table KNAPSACK_WITH_TUNER has been created in caslib CASUSER(casuser) from binary data uploaded to Cloud Analytic Services.
NOTE: Start to tune the MILP
         SolveCalls  Configurations    BestTime        Time
                  1               1        0.19*       0.27
                  2               2        0.19*       0.52
                  3               3        0.19*       0.73
                  4               4        0.19*       0.93
                  5               5        0.19*       1.14
                  6               6        0.19*       1.35
                  7               7        0.19*       1.56
                  8               8        0.19*       1.79
                  9               9        0.19*       2.00
                 10              10        0.19*       2.22
NOTE: Configuration limit reached.
NOTE: The tuning time is 2.22 seconds.
In [34]: print(results)
   Configuration conflictSearch  cutGomory cutMiLifted cutStrategy  \
0            0.0      automatic  automatic   automatic   automatic   
1            1.0       moderate       none   automatic   automatic   
2            2.0           none       none    moderate  aggressive   
3            3.0      automatic       none        none    moderate   
4            4.0     aggressive       none  aggressive        none   
5            5.0     aggressive       none        none  aggressive   
6            6.0       moderate       none   automatic    moderate   
7            7.0           none   moderate    moderate        none   
8            8.0     aggressive       none        none    moderate   
9            9.0     aggressive       none        none    moderate   

  cutZeroHalf heuristics           nodelSel  presolver      probe   restarts  \
0   automatic  automatic          automatic  automatic  automatic  automatic   
1        none  automatic          automatic       none       none      basic   
2        none  automatic  bestEstimatedepth   moderate       none      basic   
3    moderate  automatic          automatic       none  automatic       none   
4    moderate  automatic          bestBound   moderate  automatic       none   
5    moderate       none              depth  automatic  automatic       none   
6  aggressive       none          bestBound   moderate  automatic       none   
7  aggressive       none              depth  automatic  automatic  automatic   
8    moderate   moderate          bestBound      basic       none   moderate   
9    moderate   moderate          bestBound   moderate       none   moderate   

     symmetry      varSel  Mean of Run Times  Sum of Run Times  \
0   automatic   automatic               0.19              0.19   
1   automatic      pseudo               0.20              0.20   
2        none   automatic               0.18              0.18   
3       basic   minInfeas               0.18              0.18   
4    moderate  ryanFoster               0.17              0.17   
5  aggressive   minInfeas               0.17              0.17   
6   automatic  ryanFoster               0.18              0.18   
7        none   minInfeas               0.19              0.19   
8  aggressive      strong               0.19              0.19   
9  aggressive      strong               0.18              0.18   

   Percentage Successful  
0                    0.0  
1                    0.0  
2                    0.0  
3                    0.0  
4                    0.0  
5                    0.0  
6                    0.0  
7                    0.0  
8                    0.0  
9                    0.0  

Model.tune_parameters() accepts three main arguments

  • milpParameters

  • tunerParameters

  • tuningParameters

For a full set of tuning parameters and acceptable values of these arguments, see the SAS Optimization documentation.

For the example problem, you can tune the presolver, cutStrategy, and strongIter settings, by using initial values and candidate values, and limit the maximum number of configurations and maximum running time as follows:

In [35]: results = m.tune_parameters(
   ....:    milpParameters={'maxtime': 10},
   ....:    tunerParameters={'maxConfigs': 20, 'logfreq': 5},
   ....:    tuningParameters=[
   ....:       {'option': 'presolver', 'initial': 'none', 'values': ['basic', 'aggressive', 'none']},
   ....:       {'option': 'cutStrategy'},
   ....:       {'option': 'strongIter', 'initial': -1, 'values': [-1, 100, 1000]}
   ....:    ])
   ....: 
NOTE: Added action set 'optimization'.
NOTE: Uploading the problem DataFrame to the server.
NOTE: Cloud Analytic Services made the uploaded file available as table KNAPSACK_WITH_TUNER in caslib CASUSER(casuser).
NOTE: The table KNAPSACK_WITH_TUNER has been created in caslib CASUSER(casuser) from binary data uploaded to Cloud Analytic Services.
NOTE: Start to tune the MILP
         SolveCalls  Configurations    BestTime        Time
                  5               5        0.20*       1.09
                 10              10        0.20*       2.15
                 15              15        0.20*       3.19
                 20              20        0.20*       4.25
NOTE: Configuration limit reached.
NOTE: The tuning time is 4.25 seconds.
In [36]: print(results)
    Configuration conflictSearch  cutGomory cutMiLifted cutStrategy  \
0             0.0      automatic  automatic   automatic   automatic   
1             1.0       moderate       none   automatic   automatic   
2             2.0           none       none    moderate  aggressive   
3             3.0      automatic       none        none    moderate   
4             4.0     aggressive       none  aggressive        none   
5             5.0     aggressive       none        none  aggressive   
6             6.0       moderate       none   automatic    moderate   
7             7.0           none   moderate    moderate        none   
8             8.0           none       none    moderate  aggressive   
9             9.0           none       none    moderate  aggressive   
10           10.0           none       none  aggressive  aggressive   
11           11.0           none       none    moderate  aggressive   
12           12.0           none       none    moderate  aggressive   
13           13.0           none  automatic    moderate  aggressive   
14           14.0           none       none    moderate  aggressive   
15           15.0           none       none    moderate  aggressive   
16           16.0           none       none    moderate  aggressive   
17           17.0      automatic  automatic   automatic   automatic   
18           18.0      automatic  automatic   automatic   automatic   
19           19.0      automatic  automatic   automatic   automatic   

   cutZeroHalf  heuristics           nodelSel  presolver      probe  \
0    automatic   automatic          automatic  automatic  automatic   
1         none   automatic          automatic       none       none   
2         none   automatic  bestEstimatedepth   moderate       none   
3     moderate   automatic          automatic       none  automatic   
4     moderate   automatic          bestBound   moderate  automatic   
5     moderate        none              depth  automatic  automatic   
6   aggressive        none          bestBound   moderate  automatic   
7   aggressive        none              depth  automatic  automatic   
8     moderate       basic              depth       none  automatic   
9   aggressive       basic  bestEstimatedepth       none  automatic   
10    moderate       basic  bestEstimatedepth       none  automatic   
11    moderate       basic  bestEstimatedepth       none  automatic   
12    moderate       basic  bestEstimatedepth       none      basic   
13    moderate       basic  bestEstimatedepth       none  automatic   
14    moderate       basic          automatic       none  automatic   
15    moderate       basic  bestEstimatedepth   moderate  automatic   
16    moderate       basic  bestEstimatedepth       none  automatic   
17   automatic   automatic          bestBound  automatic  automatic   
18   automatic  aggressive          automatic  automatic  automatic   
19   automatic   automatic          automatic  automatic  automatic   

     restarts    symmetry      varSel  Mean of Run Times  Sum of Run Times  \
0   automatic   automatic   automatic               0.19              0.19   
1       basic   automatic      pseudo               0.19              0.19   
2       basic        none   automatic               0.18              0.18   
3        none       basic   minInfeas               0.18              0.18   
4        none    moderate  ryanFoster               0.20              0.20   
5        none  aggressive   minInfeas               0.18              0.18   
6        none   automatic  ryanFoster               0.19              0.19   
7   automatic        none   minInfeas               0.18              0.18   
8        none   automatic   maxInfeas               0.18              0.18   
9        none   automatic   maxInfeas               0.18              0.18   
10       none   automatic   maxInfeas               0.18              0.18   
11       none   automatic   minInfeas               0.17              0.17   
12       none   automatic   maxInfeas               0.18              0.18   
13       none   automatic   maxInfeas               0.18              0.18   
14       none   automatic   maxInfeas               0.17              0.17   
15       none   automatic   maxInfeas               0.18              0.18   
16       none   automatic   maxInfeas               0.17              0.17   
17  automatic   automatic   automatic               0.19              0.19   
18  automatic   automatic   automatic               0.18              0.18   
19  automatic        none   automatic               0.18              0.18   

    Percentage Successful  
0                     0.0  
1                     0.0  
2                     0.0  
3                     0.0  
4                     0.0  
5                     0.0  
6                     0.0  
7                     0.0  
8                     0.0  
9                     0.0  
10                    0.0  
11                    0.0  
12                    0.0  
13                    0.0  
14                    0.0  
15                    0.0  
16                    0.0  
17                    0.0  
18                    0.0  
19                    0.0  

You can retrieve full details by using the Model.get_tuner_results() method.

Exporting models

sasoptpy can return problem representation as an OPTMODEL string using Model.to_optmodel() method:

In [37]: print(m.to_optmodel())
proc optmodel;
   var get {{'clock','mug','headphone','book','pen'}} integer;
   con limit_con_clock : get['clock'] <= 3;
   con limit_con_mug : get['mug'] <= 5;
   con limit_con_headphone : get['headphone'] <= 2;
   con limit_con_book : get['book'] <= 10;
   con limit_con_pen : get['pen'] <= 15;
   con weight_con : 4 * get['clock'] + 6 * get['mug'] + 7 * get['headphone'] + 12 * get['book'] + get['pen'] <= 55;
   max total_value = 8 * get['clock'] + 10 * get['mug'] + 15 * get['headphone'] + 20 * get['book'] + get['pen'];
   solve;
quit;

An MPS representation of the model is available for LP and MILP problems using Model.to_mps() method:

In [38]: print(m.to_mps())
     Field1                  Field2                  Field3  Field4  \
0      NAME                             knapsack_with_tuner     0.0   
1      ROWS                                                     NaN   
2       MAX             total_value                             NaN   
3         L      limit_con['clock']                             NaN   
4         L        limit_con['mug']                             NaN   
5         L  limit_con['headphone']                             NaN   
6         L       limit_con['book']                             NaN   
7         L        limit_con['pen']                             NaN   
8         L              weight_con                             NaN   
9   COLUMNS                                                     NaN   
10                         MARK0000                'MARKER'     NaN   
11                       get[clock]             total_value     8.0   
12                       get[clock]              weight_con     4.0   
13                         get[mug]             total_value    10.0   
14                         get[mug]              weight_con     6.0   
15                   get[headphone]             total_value    15.0   
16                   get[headphone]              weight_con     7.0   
17                        get[book]             total_value    20.0   
18                        get[book]              weight_con    12.0   
19                         get[pen]             total_value     1.0   
20                         get[pen]              weight_con     1.0   
21                         MARK0001                'MARKER'     NaN   
22      RHS                                                     NaN   
23                              RHS      limit_con['clock']     3.0   
24                              RHS  limit_con['headphone']     2.0   
25                              RHS        limit_con['pen']    15.0   
26   RANGES                                                     NaN   
27   BOUNDS                                                     NaN   
28       FR                     BND              get[clock]     NaN   
29       FR                     BND                get[mug]     NaN   
30       FR                     BND          get[headphone]     NaN   
31       FR                     BND               get[book]     NaN   
32       FR                     BND                get[pen]     NaN   
33   ENDATA                                                     0.0   

                    Field5  Field6  _id_  
0                              0.0     1  
1                              NaN     2  
2                              NaN     3  
3                              NaN     4  
4                              NaN     5  
5                              NaN     6  
6                              NaN     7  
7                              NaN     8  
8                              NaN     9  
9                              NaN    10  
10                'INTORG'     NaN    11  
11      limit_con['clock']     1.0    12  
12                             NaN    13  
13        limit_con['mug']     1.0    14  
14                             NaN    15  
15  limit_con['headphone']     1.0    16  
16                             NaN    17  
17       limit_con['book']     1.0    18  
18                             NaN    19  
19        limit_con['pen']     1.0    20  
20                             NaN    21  
21                'INTEND'     NaN    22  
22                             NaN    23  
23        limit_con['mug']     5.0    24  
24       limit_con['book']    10.0    25  
25              weight_con    55.0    26  
26                             NaN    27  
27                             NaN    28  
28                             NaN    29  
29                             NaN    30  
30                             NaN    31  
31                             NaN    32  
32                             NaN    33  
33                             0.0    34  

Finally, a model can be exported into an MPS file using Model.export_mps() method. You can use filename argument in this method to write the multi-line Python string into a file on disk. When fetch argument is used, it returns the generated string back to user.

In [39]: print(m.export_mps(fetch=True))
NAME     knapsack_with_tuner                                                               
ROWS                                                                                       
 MAX     total_value                                                                       
 L       limit_con['clock']                                                                
 L       limit_con['mug']                                                                  
 L       limit_con['headphone']                                                            
 L       limit_con['book']                                                                 
 L       limit_con['pen']                                                                  
 L       weight_con                                                                        
COLUMNS                                                                                    
         MARK0000                'MARKER'                      'INTORG'                    
         get[clock]              total_value              8.0  limit_con['clock']       1.0
         get[clock]              weight_con               4.0                              
         get[mug]                total_value             10.0  limit_con['mug']         1.0
         get[mug]                weight_con               6.0                              
         get[headphone]          total_value             15.0  limit_con['headphone']   1.0
         get[headphone]          weight_con               7.0                              
         get[book]               total_value             20.0  limit_con['book']        1.0
         get[book]               weight_con              12.0                              
         get[pen]                total_value              1.0  limit_con['pen']         1.0
         get[pen]                weight_con               1.0                              
         MARK0001                'MARKER'                      'INTEND'                    
RHS                                                                                        
         RHS                     limit_con['clock']       3.0  limit_con['mug']         5.0
         RHS                     limit_con['headphone']   2.0  limit_con['book']       10.0
         RHS                     limit_con['pen']        15.0  weight_con              55.0
RANGES                                                                                     
BOUNDS                                                                                     
 FR      BND                     get[clock]                                                
 FR      BND                     get[mug]                                                  
 FR      BND                     get[headphone]                                            
 FR      BND                     get[book]                                                 
 FR      BND                     get[pen]                                                  
ENDATA