# Introduction to Mathematical Optimization Modeling


## Objective and prerequisites
The goal of this modeling example is to introduce the key components in the formulation of mixed integer programming (MIP) problems. For each component of a MIP problem formulation, we provide a description,  the associated Python code,  and the mathematical notation describing the component.

To fully understand the content of this notebook, the reader should:

* Be familiar with Python.
* Have a background in any branch of engineering, computer science, economics, statistics, any branch of the “hard” sciences, or any discipline that uses quantitative models and methods.

The reader should also consult the  [documentation](https://www.gurobi.com/resources/?category-filter=documentation)
of the Gurobi Python API.
This notebook is explained in detail in our series of tutorial videos on mixed integer linear programming.
You can watch these videos by clicking 
[here](https://www.gurobi.com/resource/tutorial-mixed-integer-linear-programming/)

**Note:** You can download the repository containing this and other examples by clicking [here](https://github.com/Gurobi/modeling-examples/archive/master.zip). In order to run this Jupyter Notebook properly, you must have a Gurobi license. If you do not have one, you can request an [evaluation license](https://www.gurobi.com/downloads/request-an-evaluation-license/?utm_source=Github&utm_medium=website_JupyterME&utm_campaign=CommercialDataScience) as a *commercial user*, or download a [free license](https://www.gurobi.com/academia/academic-program-and-licenses/?utm_source=Github&utm_medium=website_JupyterME&utm_campaign=AcademicDataScience) as an *academic user*.

## Problem description

Consider a consulting company that has three open positions: Tester, Java Developer, and Architect. The three top candidates (resources) for the positions are: Carlos, Joe, and Monika. The consulting company administered competency tests to each candidate in order to assess their ability to perform each of the jobs. The results of these tests are called *matching scores*. Assume that only one candidate can be assigned to a job, and at most one job can be assigned to a candidate.

The problem is to determine an assignment of resources and jobs such that each job is fulfilled, each resource is assigned to at most one job, and the total matching scores of the assignments is maximized.


## Mathematical optimization 

Mathematical optimization (which is also known as mathematical programming) is a declarative approach where the modeler formulates an  optimization problem that captures the key features of a complex decision problem. The Gurobi Optimizer solves the mathematical optimization problem using state-of-the-art mathematics and computer science.

A mathematical optimization model has five components:

* Sets
* Parameters
* Decision variables
* Constraints
* Objective function(s)


The following Python code imports the Gurobi callable library and imports the ``GRB`` class into the main namespace.


```python
import gurobipy as gp
from gurobipy import GRB
```

## Resource Assignment Problem
### Data
The list $R$ contains the names of the three resources: Carlos, Joe, and Monika.

The list $J$ contains the names of the job positions: Tester, Java Developer, and Architect.

$r \in R$: index and set of resources. The resource $r$ belongs to the set of resources $R$.

$j \in J$: index and set of jobs. The job $j$ belongs to the set of jobs $J$.


```python
# Resource and job sets
R = ['Carlos', 'Joe', 'Monika']
J = ['Tester', 'JavaDeveloper', 'Architect']
```

The ability of each resource to perform each of the jobs is listed in the following matching scores table:



For each resource $r$ and job $j$, there is a corresponding matching score $s$. The matching score $s$ can only take values between 0 and 100. That is, $s_{r,j} \in [0, 100]$ for all resources $r \in R$ and jobs $j \in J$. 

We use the Gurobi Python ``multidict`` function to initialize one or more dictionaries with a single statement. The function takes a dictionary as its argument. The keys represent the possible combinations of resources and jobs.



```python
# Matching score data
combinations, scores = gp.multidict({
    ('Carlos', 'Tester'): 53,
    ('Carlos', 'JavaDeveloper'): 27,
    ('Carlos', 'Architect'): 13,
    ('Joe', 'Tester'): 80,
    ('Joe', 'JavaDeveloper'): 47,
    ('Joe', 'Architect'): 67,
    ('Monika', 'Tester'): 53,
    ('Monika', 'JavaDeveloper'): 73,
    ('Monika', 'Architect'): 47
})
```

The following constructor creates an empty ``Model`` object “m”. We specify the model name by passing the string "RAP" as an argument. The ``Model`` object “m” holds a single optimization problem. It consists of a set of variables, a set of constraints, and the objective function.


```python
# Declare and initialize model
m = gp.Model('RAP')
```

    Using license file c:\gurobi\gurobi.lic
    Set parameter TokenServer to value SANTOS-SURFACE-


## Decision variables

To solve this assignment problem, we need to identify which resource is assigned to which job. We introduce a decision variable for each possible assignment of resources to jobs. Therefore, we have 9 decision variables.

To simplify the mathematical notation of the model formulation, we define the following indices for resources and jobs:



For example, $x_{2,1}$ is the decision variable associated with assigning the resource Joe to the job Tester. Therefore, decision variable $x_{r,j}$ equals 1 if resource $r \in R$  is assigned to job $j \in J$, and 0 otherwise.

The ``Model.addVars()`` method creates the decision variables for a ``Model`` object.
This method returns a Gurobi ``tupledict`` object that contains the newly created variables. We supply the ``combinations`` object as the first argument to specify the variable indices. The ``name`` keyword is used to specify a name for the newly created decision variables. By default, variables are assumed to be non-negative.


```python
# Create decision variables for the RAP model
x = m.addVars(combinations, name="assign")
```

## Job constraints

We now discuss the constraints associated with the jobs. These constraints need to ensure that each job is filled by exactly one resource.

The job constraint for the Tester position requires that resource 1 (Carlos), resource 2 (Joe), or resource 3 (Monika) is assigned to this job. This corresponds to the following constraint.

Constraint (Tester=1)

$$
x_{1,1} + x_{2,1} + x_{3,1} = 1
$$

Similarly, the constraints for the Java Developer and Architect positions can be defined as follows.

Constraint (Java Developer = 2)

$$
x_{1,2} + x_{2,2} + x_{3,2} = 1
$$

Constraint (Architect = 3)

$$
x_{1,3} + x_{2,3} + x_{3,3} = 1
$$

The job constraints are defined by the columns of the following table.



In general, the constraint for the job Tester can defined as follows.

$$
x_{1,1} + x_{2,1} + x_{3,1} = \sum_{r=1}^{3 } x_{r,1} =  \sum_{r \in R} x_{r,1} = 1
$$

All of the job constraints can be defined in a similarly succinct manner. For each job $j \in J$, take the summation of the decision variables over all the resources. We can write the corresponding job constraint as follows.

$$
\sum_{r \in R} x_{r,j} = 1
$$

The ``Model.addConstrs()`` method of the Gurobi/Python API defines the job constraints of the ``Model`` object “m”. This method returns a Gurobi ``tupledict`` object that contains the job constraints. 
The first argument of this method, "x.sum(‘*’, j)", is the sum method and defines the LHS of the jobs constraints as follows:
For each job $j$ in the set of jobs $J$, take the summation of the decision variables over all the resources. The $==$  defines an equality constraint, and the number "1" is the RHS of the constraints.
These constraints are saying that exactly one resource should be assigned to each job.
The second argument is the name of this type of constraints.



```python
# Create job constraints
jobs = m.addConstrs((x.sum('*',j) == 1 for j in J), name='job')
```

## Resource constraints

The constraints for the resources need to ensure that at most one job is assigned to each resource. That is, it is possible that not all the resources are assigned.

For example, we want a constraint that requires Carlos to be assigned to at most one of the jobs: either job 1 (Tester), job 2 (Java Developer ), or job 3 (Architect). We can write this constraint as follows.

Constraint (Carlos=1)

$$
x_{1, 1} + x_{1, 2} + x_{1, 3}  \leq 1.
$$

This constraint is less or equal than 1 to allow the possibility that Carlos is not assigned to any job. Similarly, the constraints for the resources Joe and Monika can be defined as follows:

Constraint (Joe=2) 

$$
x_{2, 1} + x_{2, 2} + x_{2, 3}  \leq 1.
$$

Constraint (Monika=3)

$$
x_{3, 1} + x_{3, 2} + x_{3, 3}  \leq 1.
$$

Observe that the resource constraints are defined by the rows of the following table.



The constraint for the resource Carlos can be defined as follows.

$$
x_{1, 1} + x_{1, 2} + x_{1, 3} = \sum_{j=1}^{3 } x_{1,j} = \sum_{j \in J} x_{1,j} \leq 1.
$$

Again, each of these constraints can be written in a succinct manner. For each resource $r \in R$, take the summation of the decision variables over all the jobs. We can write the corresponding resource constraint as follows.

$$
\sum_{j \in J} x_{r,j} \leq  1.
$$

The ``Model.addConstrs()`` method of the Gurobi/Python API defines the resource constraints of the ``Model`` object “m”. 
The first argument of this method, "x.sum(r, ‘*’)", is the sum method and defines the LHS of the resource constraints as follows: For each resource $r$ in the set of resources $R$, take the summation of the decision variables over all the jobs.
The $<=$  defines a less or equal constraints, and the number “1” is the RHS of the constraints.
These constraints are saying that each resource can be assigned to at most 1 job.
The second argument is the name of this type of constraints.



```python
# Create resource constraints
resources = m.addConstrs((x.sum(r,'*') <= 1 for r in R), name='resource')
```

## Objective function

The objective function is to maximize the total matching score of the assignments that satisfy the job and resource constraints. 

For the Tester job, the matching score is $53x_{1,1}$, if resource Carlos is assigned, or $80x_{2,1}$, if resource Joe is assigned, or $53x_{3,1}$, if resource Monika is assigned.
Consequently, the matching score for the Tester job is as follows, where only one term in this summation will be nonzero.

$$
53x_{1,1} + 80x_{2,1} + 53x_{3,1}. 
$$

Similarly, the matching scores for the Java Developer and Architect jobs are defined as follows. The matching score for the Java Developer job is:

$$
27x_{1, 2} + 47x_{2, 2} + 73x_{3, 2}.
$$

The matching score for the Architect job is:

$$
13x_{1, 3} + 67x_{2, 3} + 47x_{3, 3}.
$$

The total matching score is the summation of each cell in the following table.



The goal is to  maximize the total matching score of the assignments. Therefore, the objective function is defined as follows.

\begin{equation}
\text{Maximize} \quad (53x_{1,1} + 80x_{2,1} + 53x_{3,1}) \; +
\end{equation}

\begin{equation}
\quad (27x_{1, 2} + 47x_{2, 2} + 73x_{3, 2}) \; +
\end{equation}

\begin{equation}
\quad (13x_{1, 3} + 67x_{2, 3} + 47x_{3, 3}).
\end{equation}

Each term in parenthesis in the objective function can be expressed as follows.

\begin{equation}
(53x_{1,1} + 80x_{2,1} + 53x_{3,1}) = \sum_{r \in R} s_{r,1}x_{r,1}.
\end{equation}

\begin{equation}
(27x_{1, 2} + 47x_{2, 2} + 73x_{3, 2}) = \sum_{r \in R} s_{r,2}x_{r,2}.
\end{equation}

\begin{equation}
(13x_{1, 3} + 67x_{2, 3} + 47x_{3, 3}) = \sum_{r \in R} s_{r,3}x_{r,3}.
\end{equation}

Hence, the objective function can be concisely written as:

\begin{equation}
\text{Maximize} \quad \sum_{j \in J} \sum_{r \in R} s_{r,j}x_{r,j}.
\end{equation}

The ``Model.setObjective()`` method of the Gurobi/Python API defines the objective function of the ``Model`` object “m”. The objective expression is specified in the first argument of this method.
Notice that both the matching score parameters “score” and the assignment decision variables “x” are defined over the “combinations” keys. Therefore, we use the method “x.prod(score)” to obtain the summation of the elementwise multiplication of the "score" matrix and the "x" variable matrix.
The second argument, ``GRB.MAXIMIZE``, is the optimization "sense." In this case, we want to *maximize* the total matching scores of all assignments.


```python
# Objective: maximize total matching score of all assignments
m.setObjective(x.prod(scores), GRB.MAXIMIZE)
```

We use the “write()” method of the Gurobi/Python API to write the model formulation to a file named "RAP.lp".


```python
# Save model for inspection
m.write('RAP.lp')
```



We use the “optimize( )” method of the Gurobi/Python API to solve the problem we have defined for the model object “m”.


```python
# Run optimization engine
m.optimize()
```

    Gurobi Optimizer version 9.0.0 build v9.0.0rc2 (win64)
    Optimize a model with 6 rows, 9 columns and 18 nonzeros
    Model fingerprint: 0xb6602fb2
    Coefficient statistics:
      Matrix range     [1e+00, 1e+00]
      Objective range  [1e+01, 8e+01]
      Bounds range     [0e+00, 0e+00]
      RHS range        [1e+00, 1e+00]
    Presolve time: 0.00s
    Presolved: 6 rows, 9 columns, 18 nonzeros
    
    Iteration    Objective       Primal Inf.    Dual Inf.      Time
           0    4.6000000e+32   1.800000e+31   4.600000e+02      0s
           5    1.9300000e+02   0.000000e+00   0.000000e+00      0s
    
    Solved in 5 iterations and 0.01 seconds
    Optimal objective  1.930000000e+02


The ``Model.getVars()`` method of the Gurobi/Python API 
retrieves a list of all variables in the Model object “m”. The ``.x`` variable attribute is used to query solution values and the ``.varName`` attribute is used to query the name of the decision variables.  


```python
# Display optimal values of decision variables
for v in m.getVars():
    if v.x > 1e-6:
        print(v.varName, v.x)

# Display optimal total matching score
print('Total matching score: ', m.objVal)
```

    assign[Carlos,Tester] 1.0
    assign[Joe,Architect] 1.0
    assign[Monika,JavaDeveloper] 1.0
    Total matching score:  193.0


The optimal assignment is to assign:

* Carlos to the Tester job, with a matching score of 53
* Joe to the Architect job, with a matching score of 67
* Monika to the Java Developer job, with a matching score of 73.

The maximum total matching score is 193.

## Resource Assignment Problem with a budget constraint

Now, assume there is a fixed cost $C_{r,j}$ associated with assigning a resource $r \in R$ to job $j \in J$. Assume also that there is a limited budget $B$ that can be used for job assignments.

The cost of assigning Carlos, Joe, or Monika to any of the jobs is $\$1,000$ , $\$2,000$ , and $\$3,000$  respectively. The available budget is $\$5,000$.

### Data

The list $R$ contains the names of the three resources: Carlos, Joe, and Monika.
The list $J$ contains the names of the job positions: Tester, Java Developer, and Architect.

The Gurobi Python ``multidict`` function initialize two dictionaries: 
* "scores" defines the matching scores for each resource and job combination.
* "costs" defines the fixed cost associated of assigning a resource to a job.




```python
# Resource and job sets
R = ['Carlos', 'Joe', 'Monika']
J = ['Tester', 'JavaDeveloper', 'Architect']

# Matching score data
# Cost is given in thousands of dollars
combinations, scores, costs = gp.multidict({
    ('Carlos', 'Tester'): [53, 1],
    ('Carlos', 'JavaDeveloper'): [27, 1],
    ('Carlos', 'Architect'): [13,1],
    ('Joe', 'Tester'): [80, 2],
    ('Joe', 'JavaDeveloper'): [47, 2],
    ('Joe', 'Architect'): [67, 2],
    ('Monika', 'Tester'): [53, 3] ,
    ('Monika', 'JavaDeveloper'): [73, 3],
    ('Monika', 'Architect'): [47, 3]
})

# Available budget (thousands of dollars)
budget = 5
```

The following constructor creates an empty ``Model`` object “m”. The ``Model`` object “m” holds a single optimization problem. It consists of a set of variables, a set of constraints, and the objective function.


```python
# Declare and initialize model
m = gp.Model('RAP2')
```

### Decision variables

The decision variable $x_{r,j}$ is 1 if $r \in R$ is assigned to job $j \in J$, and 0 otherwise.

The ``Model.addVars()`` method defines the decision variables for the model object “m”.  

Because there is a budget constraint, it is possible that not all of the jobs will be filled. To account for this, we define a new decision variable that indicates whether or not a job is filled.

Let $g_{j}$ be equal 1 if job $j \in J$ is not filled, and 0 otherwise. This variable is a gap variable that indicates that a job cannot be filled.

***Remark:*** For the previous formulation of the RAP, we defined the assignment variables as non-negative and continuous which is the default value of the ``vtype`` argument of the ``Model.addVars()`` method.
However, in this extension of the RAP, because of the budget constraint we added to the model, we need to explicitly define these variables as binary. The ``vtype=GRB.BINARY`` argument of the ``Model.addVars()`` method defines the assignment variables as binary.


```python
# Create decision variables for the RAP model
x = m.addVars(combinations, vtype=GRB.BINARY, name="assign")

# Create gap variables for the RAP model
g = m.addVars(J, name="gap")
```

### Job constraints

Since we have a limited budget to assign resources to jobs, it is possible that not all the jobs can be filled. For the job constraints, there are two possibilities either a resource is assigned to fill the job, or this job cannot be filled and we need to declare a gap. This latter possibility is captured by the decision variable $g_j$. Therefore, the job constraints are written as follows. 

For each job $j \in J$, exactly one resource must be assigned to the job, or the corresponding $g_j$ variable must be set to 1:

$$
\sum_{r \: \in \: R} x_{r,\; j} + g_{j} = 1.
$$



```python
# Create job constraints
jobs = m.addConstrs((x.sum('*',j) + g[j]  == 1 for j in J), name='job')
```

### Resource constraints

The constraints for the resources need to ensure that at most one job is assigned to each resource. That is, it is possible that not all the resources are assigned. Therefore, the resource constraints are written as follows.

For each resource $r \in R$, at most one job can be assigned to the resource:

$$
\sum_{j \: \in \: J} x_{r,\; j} \leq 1.
$$


```python
# Create resource constraints
resources = m.addConstrs((x.sum(r,'*') <= 1 for r in R), name='resource')
```

### Budget constraint

This constraint ensures that the cost of assigning resources to fill job requirements do not exceed the budget available. The costs of assignment and budget are in thousands of dollars.

The cost of filling the Tester job is $1x_{1,1}$, if resource Carlos is assigned, or $2x_{2,1}$, if resource Joe is assigned, or $3x_{3,1}$, if resource Monika is assigned.
Consequently, the cost of filling the Tester job is as follows, where at most one term in this summation will be nonzero.

$$
1x_{1,1} + 2x_{2,1} + 3x_{3,1}. 
$$

Similarly, the cost of filling the Java Developer and Architect jobs are defined as follows. The cost of filling the Java Developer job is:

$$
1x_{1, 2} + 2x_{2, 2} + 3x_{3, 2}.
$$

The cost of filling the Architect job is:

$$
1x_{1, 3} + 2x_{2, 3} + 3x_{3, 3}.
$$

Hence, the total cost of filling the jobs should be less or equal than the budget available.

\begin{equation}
(1x_{1,1} + 2x_{2,1} + 3x_{3,1}) \; +
\end{equation}

\begin{equation}
(1x_{1, 2} + 2x_{2, 2} + 3x_{3, 2}) \; +
\end{equation}

\begin{equation}
(1x_{1, 3} + 2x_{2, 3} + 3x_{3, 3}) \leq 5
\end{equation}

Each term in parenthesis in the budget constraint can be expressed as follows.

\begin{equation}
(1x_{1,1} + 2x_{2,1} + 3x_{3,1}) = \sum_{r \in R} C_{r,1}x_{r,1}.
\end{equation}

\begin{equation}
(1x_{1, 2} + 2x_{2, 2} + 3x_{3, 2}) = \sum_{r \in R} C_{r,2}x_{r,2}.
\end{equation}

\begin{equation}
(1x_{1, 3} + 2x_{2, 3} + 3x_{3, 3}) = \sum_{r \in R} C_{r,3}x_{r,3}.
\end{equation}

Therefore, the budget constraint can be concisely written as:

\begin{equation}
\sum_{j \in J} \sum_{r \in R} C_{r,j}x_{r,j} \leq B.
\end{equation}

The ``Model.addConstr()`` method of the Gurobi/Python API defines the budget constraint of the ``Model`` object “m”. 
The first argument of this method, "x.prod(costs)", is the prod method and defines the LHS of the budget constraint. The $<=$ defines a less or equal constraint, and the budget amount available is the RHS of the constraint.
This constraint is saying that the total cost of assigning resources to fill jobs requirements cannot exceed the budget available.
The second argument is the name of this constraint.


```python
budget = m.addConstr((x.prod(costs) <= budget), name='budget')
```

## Objective function

The objective function is similar to the RAP. The first term in the objective is the total matching score of the assignments. In this extension of the RAP, it is possible that not all jobs are filled; however, we want to heavily penalize this possibility. For this purpose, we have a second term in the objective function that takes the summation of the gap variables over all the jobs and multiply it by a big penalty $M$.

Observe that the maximum value of a matching score is 100, and the value that we give to $M$ is 101. The rationale behind the value of $M$ is that having gaps heavily deteriorates the total matching scores value.

Consequently, the objective function is to maximize the total matching score of the assignments minus the penalty associated of having gap variables with a value equal to 1.

$$
\max \; \sum_{j \; \in \; J} \sum_{r \; \in \; R} s_{r,j}x_{r,j} -M \sum_{j \in J} g_{j}
$$


```python
# Penalty for not filling a job position
M = 101
```


```python
# Objective: maximize total matching score of assignments
# Unfilled jobs are heavily penalized
m.setObjective(x.prod(scores) - M*g.sum(), GRB.MAXIMIZE)
```


```python
# Run optimization engine
m.optimize()
```

    Gurobi Optimizer version 9.0.0 build v9.0.0rc2 (win64)
    Optimize a model with 7 rows, 12 columns and 30 nonzeros
    Model fingerprint: 0xf3c6f8c8
    Variable types: 3 continuous, 9 integer (9 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 3e+00]
      Objective range  [1e+01, 1e+02]
      Bounds range     [1e+00, 1e+00]
      RHS range        [1e+00, 5e+00]
    Presolve time: 0.00s
    Presolved: 7 rows, 12 columns, 30 nonzeros
    Variable types: 0 continuous, 12 integer (12 binary)
    Found heuristic solution: objective 52.0000000
    
    Root relaxation: objective 1.350000e+02, 4 iterations, 0.00 seconds
    
        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time
    
         0     0  135.00000    0    2   52.00000  135.00000   160%     -    0s
         0     0  121.66667    0    7   52.00000  121.66667   134%     -    0s
         0     0     cutoff    0        52.00000   52.00000  0.00%     -    0s
    
    Cutting planes:
      Gomory: 1
      GUB cover: 1
      RLT: 1
    
    Explored 1 nodes (7 simplex iterations) in 0.03 seconds
    Thread count was 8 (of 8 available processors)
    
    Solution count 1: 52 
    
    Optimal solution found (tolerance 1.00e-04)
    Best objective 5.200000000000e+01, best bound 5.200000000000e+01, gap 0.0000%


The definition of the objective function includes the penalty of no filling jobs. However, we are interested in the optimal total matching score value when not all the jobs are filled. For this purpose, we need to compute the total matching score value using the matching score values $s_{r,j}$ and the assignment decision variables $x_{r,j}$.


```python
# Compute total matching score from assignment variables
total_matching_score = 0
for r, j in combinations:
    if x[r, j].x > 1e-6:
        print(x[r, j].varName, x[r, j].x) 
        total_matching_score += scores[r, j]*x[r, j].x

print('Total matching score: ', total_matching_score)  
```

    assign[Joe,Tester] 1.0
    assign[Monika,JavaDeveloper] 1.0
    Total matching score:  153.0


### Analysis

Recall that the budget is $\$5,000$, and the total  cost associated of allocating the three resources is $\$6,000$. This means that there is not enough budget to allocate the three resources we have. Consequently, the Gurobi Optimizer must choose two resources to fill the jobs demand, leave one job unfilled, and maximize the total matching scores. Notice that the two top matching scores are 80% (Joe for the Tester job) and 73% (Monika for the Java Developer job). Also, notice that the lowest score is 13% (Carlos for the Architect job). Assigning Joe to the Tester job, Monika to the Java Developer job, and nobody to the Architect job costs $\$5,000$  and yields a total matching score of 153. This is the optimal solution found by the Gurobi Optimizer.

Copyright © 2020 Gurobi Optimization, LLC

2010ae03f18cb5e682a8b355ae2b40ec2b290ec6
ipynb
Jupyter Notebook
Gurobi/Gurobi-modeling-examples-master/intro_to_modeling/introduction_to_modeling.ipynb
FlameCiri/operations_research_notebook
ada2b7d5fb6789620f44f3b1500fa91d811597eb
MIT
2021-08-29T08:49:57.000Z
2022-03-13T01:48:58.000Z
Gurobi/Gurobi-modeling-examples-master/intro_to_modeling/introduction_to_modeling.ipynb
FlameCiri/operations_research_notebook
ada2b7d5fb6789620f44f3b1500fa91d811597eb
MIT
Gurobi/Gurobi-modeling-examples-master/intro_to_modeling/introduction_to_modeling.ipynb
FlameCiri/operations_research_notebook
ada2b7d5fb6789620f44f3b1500fa91d811597eb
MIT
2021-09-02T09:20:36.000Z
2021-09-02T09:20:36.000Z
Qwen/Qwen-72B
1. YES
2. YES