# Generating C Code to implement Method of Lines Timestepping for Explicit Runge Kutta Methods

## Authors: Zach Etienne & Brandon Clark

## This tutorial notebook generates three blocks of C Code in order to perform Method of Lines timestepping.

**Notebook Status:**  Validated

**Validation Notes:** This tutorial notebook has been confirmed to be self-consistent with its corresponding NRPy+ module, as documented [below](#code_validation). All Runge-Kutta (RK) Butcher tables were validated using truncated Taylor series in [a separate module](Tutorial-RK_Butcher_Table_Validation.ipynb). Finally, C-code implementation of RK4 was validated against a trusted version. C-code implementations of other RK methods seem to work as expected in the context of solving the scalar wave equation in Cartesian coordinates.

### NRPy+ Source Code for this module:
* [MoLtimestepping/C_Code_Generation.py](../edit/MoLtimestepping/C_Code_Generation.py)
* [MoLtimestepping/RK_Butcher_Table_Dictionary.py](../edit/MoLtimestepping/RK_Butcher_Table_Dictionary.py) ([**Tutorial**](Tutorial-RK_Butcher_Table_Dictionary.ipynb)) Stores the Butcher tables for the explicit Runge Kutta methods

## Introduction:

When numerically solving a partial differential equation initial-value problem, subject to suitable boundary conditions,  we implement Method of Lines to "integrate" the solution forward in time.

### The Method of Lines:

Once we have the initial data for a PDE, we "evolve it forward in time", using the [Method of Lines](https://reference.wolfram.com/language/tutorial/NDSolveMethodOfLines.html). In short, the Method of Lines enables us to handle
1. the **spatial derivatives** of an initial value problem PDE using **standard finite difference approaches**, and
2. the **temporal derivatives** of an initial value problem PDE using **standard strategies for solving ordinary differential equations (ODEs), like Runge Kutta methods** so long as the initial value problem PDE can be written in the first-order-in-time form
$$\partial_t \vec{f} = \mathbf{M}\ \vec{f},$$
where $\mathbf{M}$ is an $N\times N$ matrix containing only *spatial* differential operators that act on the $N$-element column vector $\vec{f}$. $\mathbf{M}$ may not contain $t$ or time derivatives explicitly; only *spatial* partial derivatives are allowed to appear inside $\mathbf{M}$.

You may find the next module [Tutorial-ScalarWave](Tutorial-ScalarWave.ipynb) extremely helpful as an example for implementing the Method of Lines for solving the Scalar Wave equation in Cartesian coordinates.

### Generating the C code:

This module describes how core C functions are generated to implement Method of Lines timestepping for a specified RK method. There are three core functions:

1. Allocate memory for gridfunctions.
1. Step forward the solution one full timestep.
1. Free memory for gridfunctions.

The first function is called first, then the second function is repeated within a loop to a fixed "final" time (such that the end state of each iteration is the initial state for the next iteration), and the third function is called at the end of the calculation.

The generated codes are essential for a number of Start-to-Finish example tutorial notebooks that demonstrate how to numerically solve hyperbolic PDEs.

# Table of Contents
$$\label{toc}$$

This notebook is organized as follows

1. [Step 1](#initializenrpy): Initialize needed Python/NRPy+ modules
1. [Step 2](#diagonal): Checking if Butcher Table is Diagonal
1. [Step 3](#ccode): Generating the C Code
    1. [Step 3.a](#generategfnames): `generate_gridfunction_names()`: Uniquely and descriptively assign names to sets of gridfunctions
    1. [Step 3.b](#alloc): Memory allocation: `MoL_malloc_y_n_gfs()` and `MoL_malloc_non_y_n_gfs()`
    1. [Step 3.c](#molstep): Take one Method of Lines time step:  `MoL_step_forward_in_time()`
    1. [Step 3.d](#free): Memory deallocation: `MoL_free_memory()`
    1. [Step 3.e](#nrpybasicdefines): Define & register `MoL_gridfunctions_struct` in `NRPy_basic_defines.h`: `NRPy_basic_defines_MoL_timestepping_struct()`
    1. [Step 3.f](#setupall): Add all MoL C codes to C function dictionary, and add MoL definitions to `NRPy_basic_defines.h`: `register_C_functions_and_NRPy_basic_defines()`
1. [Step 4](#code_validation): Code Validation against `MoLtimestepping.RK_Butcher_Table_Generating_C_Code` NRPy+ module
1. [Step 5](#latex_pdf_output): Output this notebook to $\LaTeX$-formatted PDF file

# Step 1: Initialize needed Python/NRPy+ modules [Back to [top](#toc)\]
$$\label{initializenrpy}$$

Let's start by importing all the needed modules from Python/NRPy+:

```python
import sympy as sp  # Import SymPy, a computer algebra system written entirely in Python
import os, sys      # Standard Python modules for multiplatform OS-level functions
from MoLtimestepping.RK_Butcher_Table_Dictionary import Butcher_dict
from outputC import add_to_Cfunction_dict, indent_Ccode, outC_NRPy_basic_defines_h_dict  # NRPy+: Basic C code output functionality
```

# Step 2: Checking if a Butcher table is Diagonal [Back to [top](#toc)\]
$$\label{diagonal}$$

A diagonal Butcher table takes the form

$$\begin{array}{c|cccccc}
    0 & \\
    a_1 & a_1 & \\
    a_2 & 0 & a_2 & \\
    a_3 & 0 & 0 & a_3 & \\
    \vdots & \vdots & \ddots & \ddots & \ddots \\
    a_s & 0 & 0 & 0 & \cdots & a_s \\ \hline
     & b_1 & b_2 & b_3 & \cdots & b_{s-1} & b_s
\end{array}$$

where $s$ is the number of required predictor-corrector steps for a given RK method (see [Butcher, John C. (2008)](https://onlinelibrary.wiley.com/doi/book/10.1002/9780470753767)). One known diagonal RK method is the classic RK4 represented in Butcher table form as:

$$\begin{array}{c|cccc}
    0 & \\
    1/2 & 1/2 & \\
    1/2 & 0 & 1/2 & \\
    1 & 0 & 0 & 1 & \\ \hline
     & 1/6 & 1/3 & 1/3 & 1/6
\end{array} $$

Diagonal Butcher tables are nice when it comes to saving required memory space. Each new step for a diagonal RK method, when computing the new $k_i$, does not depend on the previous calculation, and so there are ways to save memory. Significantly so in large three-dimensional spatial grid spaces.

```python
# Check if Butcher Table is diagonal
def diagonal(key):
    Butcher = Butcher_dict[key][0]
    L = len(Butcher)-1  # Establish the number of rows to check for diagonal trait, all bust last row
    row_idx = 0  # Initialize the Butcher table row index
    for i in range(L):  # Check all the desired rows
        for j in range(1,row_idx):  # Check each element before the diagonal element in a row
            if Butcher[i][j] != sp.sympify(0):  # If any non-diagonal coeffcient is non-zero,
                                                # then the table is not diagonal
                return False
        row_idx += 1  # Update to check the next row
    return True

# Loop over all Butcher tables to check whether each is diagonal or not
for key, value in Butcher_dict.items():
    if diagonal(key) == True:
        print("The MoL method "+str(key)+" is diagonal!")
    else:
        print("The MoL method "+str(key)+" is NOT diagonal!")
```

    The MoL method Euler is diagonal!
    The MoL method RK2 Heun is diagonal!
    The MoL method RK2 MP is diagonal!
    The MoL method RK2 Ralston is diagonal!
    The MoL method RK3 is NOT diagonal!
    The MoL method RK3 Heun is diagonal!
    The MoL method RK3 Ralston is diagonal!
    The MoL method SSPRK3 is NOT diagonal!
    The MoL method RK4 is diagonal!
    The MoL method DP5 is NOT diagonal!
    The MoL method DP5alt is NOT diagonal!
    The MoL method CK5 is NOT diagonal!
    The MoL method DP6 is NOT diagonal!
    The MoL method L6 is NOT diagonal!
    The MoL method DP8 is NOT diagonal!

# Step 3: Generating the C Code [Back to [top](#toc)\]
$$\label{ccode}$$

The following sections build up the C code for implementing the [Method of Lines timestepping algorithm](http://www.scholarpedia.org/article/Method_of_lines) for solving hyperbolic PDEs.

**First an important note on efficiency:**

Memory efficiency is incredibly important here, as $\vec{f}$ is usually the largest object in memory.

If we only made use of the Butcher tables without concern for memory efficiency, `generate_gridfunction_names()` and `MoL_step_forward_in_time()` would be very simple functions.

It turns out that several of the Runge-Kutta-like methods in MoL can be made more efficient; for example "RK4" can be performed using only 4 "timelevels" of $\vec{f}$ in memory (i.e., a total memory usage of `sizeof(f) * 4`). A naive implementation might use 5 or 6 copies. RK-like methods that have diagonal Butcher tables can be made far more efficient than the naive approach.

**Exercise to student:** Improve the efficiency of other RK-like methods.

## Step 3.a: `generate_gridfunction_names()`: Uniquely and descriptively assign names to sets of gridfunctions [Back to [top](#toc)\]
$$\label{generategfnames}$$

`generate_gridfunction_names()` names gridfunctions to be consistent with a given RK substep. For example we might call the set of gridfunctions stored at substep $k_1$ `k1_gfs`.

```python
# Each MoL method has its own set of names for groups of gridfunctions,
#   aiming to be sufficiently descriptive. So for example a set of
#   gridfunctions that store "k_1" in an RK-like method could be called
#   "k1_gfs".
def generate_gridfunction_names(MoL_method = "RK4"):
    # Step 3.a: MoL gridfunctions fall into 3 overlapping categories:
    #           1) y_n=y_i(t_n) gridfunctions y_n_gfs, which stores data for the vector of gridfunctions y_i at t_n,
    #              the start of each MoL timestep.
    #           2) non-y_n gridfunctions, needed to compute the data at t_{n+1}. Often labeled with k_i in the name,
    #              these gridfunctions are *not* needed at the start of each timestep, so are available for temporary
    #              storage when gridfunctions needed for diagnostics are computed at the start of each timestep.
    #              These gridfunctions can also be freed during a regrid, to enable storage for the post-regrid
    #              destination y_n_gfs.
    #           3) Diagnostic output gridfunctions diagnostic_output_gfs, which simply uses the memory from auxiliary
    #              gridfunctions at one auxiliary time to compute diagnostics at t_n.

    # Here we specify which gridfunctions fall into each category, starting with the obvious: y_n_gridfunctions
    y_n_gridfunctions = "y_n_gfs"

    # Next the less-obvious, which depend on non-y_n_gfs
    non_y_n_gridfunctions_list = []

    # No matter the method we define gridfunctions "y_n_gfs" to store the initial data
    if diagonal(MoL_method) and "RK3" in MoL_method:
        non_y_n_gridfunctions_list.append("k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs")
        non_y_n_gridfunctions_list.append("k2_or_y_nplus_a32_k2_gfs")
        diagnostic_gridfunctions_point_to = "k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs"
    else:
        if not diagonal(MoL_method):  # Allocate memory for non-diagonal Butcher tables
            # Determine the number of k_i steps based on length of Butcher Table
            num_k = len(Butcher_dict[MoL_method][0])-1
            # For non-diagonal tables an intermediate gridfunction "next_y_input" is used for rhs evaluations
            non_y_n_gridfunctions_list.append("next_y_input_gfs")
            for i in range(num_k): # Need to allocate all k_i steps for a given method
                non_y_n_gridfunctions_list.append("k" + str(i + 1) + "_gfs")
            diagnostic_gridfunctions_point_to = "k1_gfs"
        else:  # Allocate memory for diagonal Butcher tables, which use a "y_nplus1_running_total gridfunction"
            non_y_n_gridfunctions_list.append("y_nplus1_running_total_gfs")
            if MoL_method != 'Euler':  # Allocate memory for diagonal Butcher tables that aren't Euler
                # Need k_odd for k_1,3,5... and k_even for k_2,4,6...
                non_y_n_gridfunctions_list.append("k_odd_gfs")
                non_y_n_gridfunctions_list.append("k_even_gfs")
            diagnostic_gridfunctions_point_to = "y_nplus1_running_total_gfs"
    non_y_n_gridfunctions_list.append("auxevol_gfs")

    return y_n_gridfunctions, non_y_n_gridfunctions_list, diagnostic_gridfunctions_point_to
```

## Step 3.b: Memory allocation: `MoL_malloc_y_n_gfs()` and `MoL_malloc_non_y_n_gfs()`:  [Back to [top](#toc)\]
$$\label{alloc}$$

Generation of C functions `MoL_malloc_y_n_gfs()` and `MoL_malloc_non_y_n_gfs()` read the full list of needed lists of gridfunctions, provided by (Python) function `generate_gridfunction_names()`, to allocate space for all gridfunctions.

```python
# add_to_Cfunction_dict_MoL_malloc() registers
#           MoL_malloc_y_n_gfs() and
#           MoL_malloc_non_y_n_gfs(), which allocate memory for
#           the indicated sets of gridfunctions
def add_to_Cfunction_dict_MoL_malloc(MoL_method, which_gfs):
    includes = ["NRPy_basic_defines.h", "NRPy_function_prototypes.h"]
    desc  = "Method of Lines (MoL) for \"" + MoL_method + "\" method: Allocate memory for \""+which_gfs+"\" gridfunctions\n"
    desc += "   * y_n_gfs are used to store data for the vector of gridfunctions y_i at t_n, at the start of each MoL timestep\n"
    desc += "   * non_y_n_gfs are needed for intermediate (e.g., k_i) storage in chosen MoL method\n"
    c_type = "void"

    y_n_gridfunctions, non_y_n_gridfunctions_list, diagnostic_gridfunctions_point_to = \
        generate_gridfunction_names(MoL_method = MoL_method)

    gridfunctions_list = []
    if which_gfs == "y_n_gfs":
        gridfunctions_list = [y_n_gridfunctions]
    elif which_gfs == "non_y_n_gfs":
        gridfunctions_list = non_y_n_gridfunctions_list
    else:
        print("ERROR: which_gfs = \"" + which_gfs + "\" unrecognized.")
        sys.exit(1)
    name = "MoL_malloc_" + which_gfs
    params = "const paramstruct *restrict params, MoL_gridfunctions_struct *restrict gridfuncs"
    body = "const int Nxx_plus_2NGHOSTS_tot = Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2;\n"
    for gridfunctions in gridfunctions_list:
        num_gfs = "NUM_EVOL_GFS"
        if gridfunctions == "auxevol_gfs":
            num_gfs = "NUM_AUXEVOL_GFS"
        body += "gridfuncs->" + gridfunctions + " = (REAL *restrict)malloc(sizeof(REAL) * " + num_gfs + " * Nxx_plus_2NGHOSTS_tot);\n"
    body += "\ngridfuncs->diagnostic_output_gfs = gridfuncs->" + diagnostic_gridfunctions_point_to + ";\n"
    add_to_Cfunction_dict(
        includes=includes,
        desc=desc,
        c_type=c_type, name=name, params=params,
        body=indent_Ccode(body, "  "),
        rel_path_to_Cparams=os.path.join("."))
```

## Step 3.c: Take one Method of Lines time step:  `MoL_step_forward_in_time()` [Back to [top](#toc)\]
$$\label{molstep}$$

An MoL step consists in general of a series of Runge-Kutta-like substeps, and the `MoL_step_forward_in_time()` C function pulls together all of these substeps.

The basic C code for an MoL substep, set up by the Python function `single_RK_substep()` below, is as follows.

1. Evaluate the right-hand side of $\partial_t \vec{f}=$ `RHS`, to get the time derivative of the set of gridfunctions $\vec{f}$ at our current time.
1. Perform the Runge-Kutta update, which depends on $\partial_t \vec{f}$ on the current and sometimes previous times.
1. Call post-right-hand side functions as desired.

The `single_RK_substep()` function generates the C code for performing the above steps, applying substitutions for e.g., `RK_INPUT_GFS` and `RK_OUTPUT_GFS` as appropriate.

```python
# single_RK_substep() performs necessary replacements to
#   define C code for a single RK substep
#   (e.g., computing k_1 and then updating the outer boundaries)
def single_RK_substep(commentblock, RHS_str, RHS_input_str, RHS_output_str, RK_lhss_list, RK_rhss_list,
                      post_RHS_list, post_RHS_output_list, indent="  "):
    addl_indent = ""
    return_str  = commentblock + "\n"
    if not isinstance(RK_lhss_list, list):
        RK_lhss_list = [RK_lhss_list]
    if not isinstance(RK_rhss_list, list):
        RK_rhss_list = [RK_rhss_list]

    if not isinstance(post_RHS_list, list):
        post_RHS_list = [post_RHS_list]
    if not isinstance(post_RHS_output_list, list):
        post_RHS_output_list = [post_RHS_output_list]

    # Part 1: RHS evaluation:
    return_str += indent_Ccode(RHS_str.replace("RK_INPUT_GFS",  RHS_input_str).
                                       replace("RK_OUTPUT_GFS", RHS_output_str)+"\n", indent=addl_indent)

    # Part 2: RK update
    return_str += addl_indent + "LOOP_ALL_GFS_GPS(i) {\n"
    for lhs, rhs in zip(RK_lhss_list, RK_rhss_list):
        return_str += addl_indent + indent + lhs + "[i] = " + rhs + ";\n"
    return_str += addl_indent + "}\n"

    # Part 3: Call post-RHS functions
    for post_RHS, post_RHS_output in zip(post_RHS_list, post_RHS_output_list):
        return_str += indent_Ccode(post_RHS.replace("RK_OUTPUT_GFS", post_RHS_output), indent=addl_indent)

    return return_str
```

In the `add_to_Cfunction_dict_MoL_step_forward_in_time()` Python function below we construct and register the core C function for MoL timestepping: `MoL_step_forward_in_time()`. `MoL_step_forward_in_time()` implements Butcher tables for Runge-Kutta-like methods, leveraging the `single_RK_substep()` helper function above as needed. Again, we aim for maximum memory efficiency so that e.g., RK4 needs to store only 4 levels of $\vec{f}$.

```python
########################################################################################################################
# EXAMPLE
# ODE: y' = f(t,y), y(t_0) = y_0
# Starting at time t_n with solution having value y_n and trying to update to y_nplus1 with timestep dt

# Example of scheme for RK4 with k_1, k_2, k_3, k_4 (Using non-diagonal algorithm) Notice this requires storage of
# y_n, y_nplus1, k_1 through k_4

# k_1      = dt*f(t_n, y_n)
# k_2      = dt*f(t_n + 1/2*dt, y_n + 1/2*k_1)
# k_3      = dt*f(t_n + 1/2*dt, y_n + 1/2*k_2)
# k_4      = dt*f(t_n + dt, y_n + k_3)
# y_nplus1 = y_n + 1/3k_1 + 1/6k_2 + 1/6k_3 + 1/3k_4

# Example of scheme RK4 using only k_odd and k_even (Diagonal algroithm) Notice that this only requires storage

# k_odd     = dt*f(t_n, y_n)
# y_nplus1  = 1/3*k_odd
# k_even    = dt*f(t_n + 1/2*dt, y_n + 1/2*k_odd)
# y_nplus1 += 1/6*k_even
# k_odd     = dt*f(t_n + 1/2*dt, y_n + 1/2*k_even)
# y_nplus1 += 1/6*k_odd
# k_even    = dt*f(t_n + dt, y_n + k_odd)
# y_nplus1 += 1/3*k_even
########################################################################################################################
def add_to_Cfunction_dict_MoL_step_forward_in_time(MoL_method, RHS_string = "", post_RHS_string = "",
                                                  enable_rfm=False, enable_curviBCs=False):
    includes = ["NRPy_basic_defines.h", "NRPy_function_prototypes.h"]
    desc  = "Method of Lines (MoL) for \"" + MoL_method + "\" method: Step forward one full timestep.\n"
    c_type = "void"
    name = "MoL_step_forward_in_time"
    params  = "const paramstruct *restrict params, "
    if enable_rfm:
        params += "const rfm_struct *restrict rfmstruct, "
    else:
        params += "REAL *xx[3], "
    if enable_curviBCs:
        params += "const bc_struct *restrict bcstruct, "
    params += "MoL_gridfunctions_struct *restrict gridfuncs, const REAL dt"

    indent = ""  # We don't bother with an indent here.

    body = indent + "// C code implementation of -={ " + MoL_method + " }=- Method of Lines timestepping.\n\n"

    y_n_gridfunctions, non_y_n_gridfunctions_list, _throwaway = generate_gridfunction_names(MoL_method)
    body += "// First set gridfunction aliases from gridfuncs struct\n\n"
    body += "// y_n gridfunctions:\n"
    body += "REAL *restrict " + y_n_gridfunctions + " = gridfuncs->" + y_n_gridfunctions + ";\n"
    body += "\n"
    body += "// Temporary timelevel & AUXEVOL gridfunctions:\n"
    for gf in non_y_n_gridfunctions_list:
        body += "REAL *restrict " + gf + " = gridfuncs->" + gf + ";\n"
    body += "\n"
    body += "// Next perform a full step forward in time\n"

    # Implement Method of Lines (MoL) Timestepping
    Butcher = Butcher_dict[MoL_method][0] # Get the desired Butcher table from the dictionary
    num_steps = len(Butcher)-1 # Specify the number of required steps to update solution

    # Diagonal RK3 only!!!
    if diagonal(MoL_method) and "RK3" in MoL_method:
        #  In a diagonal RK3 method, only 3 gridfunctions need be defined. Below implements this approach.

        # k_1
        body += """
// In a diagonal RK3 method like this one, only 3 gridfunctions need be defined. Below implements this approach.
// Using y_n_gfs as input, k1 and apply boundary conditions\n"""

        body += single_RK_substep(
            commentblock = """// -={ START k1 substep }=-
// RHS evaluation:
//  1. We will store k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs now as
//     ...  the update for the next rhs evaluation y_n + a21*k1*dt
// Post-RHS evaluation:
//  1. Apply post-RHS to y_n + a21*k1*dt""",
            RHS_str = RHS_string,
            RHS_input_str = "y_n_gfs", RHS_output_str = "k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs",
            RK_lhss_list = ["k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs"],
            RK_rhss_list = ["("+sp.ccode(Butcher[1][1]).replace("L","")+")*k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i]*dt + y_n_gfs[i]"],
            post_RHS_list = [post_RHS_string], post_RHS_output_list = ["k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs"]) + "// -={ END k1 substep }=-\n\n"

        # k_2
        body += single_RK_substep(
            commentblock="""// -={ START k2 substep }=-
// RHS evaluation:
//    1. Reassign k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs to be the running total y_{n+1}; a32*k2*dt to the running total
//    2. Store k2_or_y_nplus_a32_k2_gfs now as y_n + a32*k2*dt
// Post-RHS evaluation:
//    1. Apply post-RHS to both y_n + a32*k2 (stored in k2_or_y_nplus_a32_k2_gfs)
//       ... and the y_{n+1} running total, as they have not been applied yet to k2-related gridfunctions""",
            RHS_str=RHS_string,
            RHS_input_str="k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs", RHS_output_str="k2_or_y_nplus_a32_k2_gfs",
            RK_lhss_list=["k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs","k2_or_y_nplus_a32_k2_gfs"],
            RK_rhss_list=["("+sp.ccode(Butcher[3][1]).replace("L","")+")*(k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] - y_n_gfs[i])/("+sp.ccode(Butcher[1][1]).replace("L","")+") + y_n_gfs[i] + ("+sp.ccode(Butcher[3][2]).replace("L","")+")*k2_or_y_nplus_a32_k2_gfs[i]*dt",
                          "("+sp.ccode(Butcher[2][2]).replace("L","")+")*k2_or_y_nplus_a32_k2_gfs[i]*dt + y_n_gfs[i]"],
            post_RHS_list=[post_RHS_string,post_RHS_string],
            post_RHS_output_list=["k2_or_y_nplus_a32_k2_gfs","k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs"]) + "// -={ END k2 substep }=-\n\n"

        # k_3
        body += single_RK_substep(
            commentblock="""// -={ START k3 substep }=-
// RHS evaluation:
//    1. Add k3 to the running total and save to y_n
// Post-RHS evaluation:
//    1. Apply post-RHS to y_n""",
            RHS_str=RHS_string,
            RHS_input_str="k2_or_y_nplus_a32_k2_gfs", RHS_output_str="y_n_gfs",
            RK_lhss_list=["y_n_gfs","k2_or_y_nplus_a32_k2_gfs"],
            RK_rhss_list=["k1_or_y_nplus_a21_k1_or_y_nplus1_running_total_gfs[i] + ("+sp.ccode(Butcher[3][3]).replace("L","")+")*y_n_gfs[i]*dt"],
            post_RHS_list=[post_RHS_string],
            post_RHS_output_list=["y_n_gfs"]) + "// -={ END k3 substep }=-\n\n"
    else:
        y_n = "y_n_gfs"
        if not diagonal(MoL_method):
            for s in range(num_steps):
                next_y_input = "next_y_input_gfs"

                # If we're on the first step (s=0), we use y_n gridfunction as input.
                #      Otherwise next_y_input is input. Output is just the reverse.
                if s == 0:  # If on first step:
                    RHS_input = y_n
                else:    # If on second step or later:
                    RHS_input = next_y_input
                RHS_output = "k" + str(s + 1) + "_gfs"
                if s == num_steps-1: # If on final step:
                    RK_lhs = y_n
                    RK_rhs = y_n + "[i] + dt*("
                else:                # If on anything but the final step:
                    RK_lhs = next_y_input
                    RK_rhs = y_n + "[i] + dt*("
                for m in range(s+1):
                    if Butcher[s+1][m+1] != 0:
                        if Butcher[s+1][m+1] != 1:
                            RK_rhs += " + k"+str(m+1)+"_gfs[i]*("+sp.ccode(Butcher[s+1][m+1]).replace("L","")+")"
                        else:
                            RK_rhs += " + k"+str(m+1)+"_gfs[i]"
                RK_rhs += " )"

                post_RHS = post_RHS_string
                if s == num_steps-1: # If on final step:
                    post_RHS_output = y_n
                else:                # If on anything but the final step:
                    post_RHS_output = next_y_input

                body += single_RK_substep(
                    commentblock="// -={ START k" + str(s + 1) + " substep }=-",
                    RHS_str=RHS_string,
                    RHS_input_str=RHS_input, RHS_output_str=RHS_output,
                    RK_lhss_list=[RK_lhs],   RK_rhss_list=[RK_rhs],
                    post_RHS_list=[post_RHS],
                    post_RHS_output_list=[post_RHS_output]) + "// -={ END k" + str(s + 1) + " substep }=-\n\n"
        else:  # diagonal case:
            y_nplus1_running_total = "y_nplus1_running_total_gfs"
            if MoL_method == 'Euler': # Euler's method doesn't require any k_i, and gets its own unique algorithm
                body += single_RK_substep(
                    commentblock=indent + "// ***Euler timestepping only requires one RHS evaluation***",
                    RHS_str=RHS_string,
                    RHS_input_str=y_n, RHS_output_str=y_nplus1_running_total,
                    RK_lhss_list=[y_n],   RK_rhss_list=[y_n+"[i] + "+y_nplus1_running_total+"[i]*dt"],
                    post_RHS_list=[post_RHS_string],
                    post_RHS_output_list=[y_n])
            else:
                for s in range(num_steps):
                    # If we're on the first step (s=0), we use y_n gridfunction as input.
                    # and k_odd as output.
                    if s == 0:
                        RHS_input  = "y_n_gfs"
                        RHS_output = "k_odd_gfs"
                    # For the remaining steps the inputs and ouputs alternate between k_odd and k_even
                    elif s % 2 == 0:
                        RHS_input = "k_even_gfs"
                        RHS_output = "k_odd_gfs"
                    else:
                        RHS_input = "k_odd_gfs"
                        RHS_output = "k_even_gfs"

                    RK_lhs_list = []
                    RK_rhs_list = []
                    if s != num_steps-1:  # For anything besides the final step
                        if s == 0:  # The first RK step
                            RK_lhs_list.append(y_nplus1_running_total)
                            RK_rhs_list.append(RHS_output+"[i]*dt*("+sp.ccode(Butcher[num_steps][s+1]).replace("L","")+")")

                            RK_lhs_list.append(RHS_output)
                            RK_rhs_list.append(y_n+"[i] + "+RHS_output+"[i]*dt*("+sp.ccode(Butcher[s+1][s+1]).replace("L","")+")")
                        else:
                            if Butcher[num_steps][s+1] != 0:
                                RK_lhs_list.append(y_nplus1_running_total)
                                if Butcher[num_steps][s+1] != 1:
                                    RK_rhs_list.append(y_nplus1_running_total+"[i] + "+RHS_output+"[i]*dt*("+sp.ccode(Butcher[num_steps][s+1]).replace("L","")+")")
                                else:
                                    RK_rhs_list.append(y_nplus1_running_total+"[i] + "+RHS_output+"[i]*dt")
                            if Butcher[s+1][s+1] != 0:
                                RK_lhs_list.append(RHS_output)
                                if Butcher[s+1][s+1] != 1:
                                    RK_rhs_list.append(y_n+"[i] + "+RHS_output+"[i]*dt*("+sp.ccode(Butcher[s+1][s+1]).replace("L","")+")")
                                else:
                                    RK_rhs_list.append(y_n+"[i] + "+RHS_output+"[i]*dt")
                        post_RHS_output = RHS_output
                    if s == num_steps-1:  # If on the final step
                        if Butcher[num_steps][s+1] != 0:
                            RK_lhs_list.append(y_n)
                            if Butcher[num_steps][s+1] != 1:
                                RK_rhs_list.append(y_n+"[i] + "+y_nplus1_running_total+"[i] + "+RHS_output+"[i]*dt*("+sp.ccode(Butcher[num_steps][s+1]).replace("L","")+")")
                            else:
                                RK_rhs_list.append(y_n+"[i] + "+y_nplus1_running_total+"[i] + "+RHS_output+"[i]*dt)")
                        post_RHS_output = y_n
                    body += single_RK_substep(
                        commentblock=indent + "// -={ START k" + str(s + 1) + " substep }=-",
                        RHS_str=RHS_string,
                        RHS_input_str=RHS_input, RHS_output_str=RHS_output,
                        RK_lhss_list=RK_lhs_list, RK_rhss_list=RK_rhs_list,
                        post_RHS_list=[post_RHS_string],
                        post_RHS_output_list=[post_RHS_output]) + "// -={ END k" + str(s + 1) + " substep }=-\n\n"

    add_to_Cfunction_dict(
        includes=includes,
        desc=desc,
        c_type=c_type, name=name, params=params,
        body=indent_Ccode(body, "  "),
        rel_path_to_Cparams=os.path.join("."))
```

## Step 3.d: Memory deallocation: `MoL_free_memory()` [Back to [top](#toc)\]
$$\label{free}$$

We define the function `MoL_free_memory()` which generates the C code for freeing the memory that was being occupied by the grid functions lists that had been allocated.

```python
# add_to_Cfunction_dict_MoL_free_memory() registers
#           MoL_free_memory_y_n_gfs() and
#           MoL_free_memory_non_y_n_gfs(), which free memory for
#           the indicated sets of gridfunctions
def add_to_Cfunction_dict_MoL_free_memory(MoL_method, which_gfs):
    includes = ["NRPy_basic_defines.h", "NRPy_function_prototypes.h"]
    desc  = "Method of Lines (MoL) for \"" + MoL_method + "\" method: Free memory for \""+which_gfs+"\" gridfunctions\n"
    desc += "   - y_n_gfs are used to store data for the vector of gridfunctions y_i at t_n, at the start of each MoL timestep\n"
    desc += "   - non_y_n_gfs are needed for intermediate (e.g., k_i) storage in chosen MoL method\n"
    c_type = "void"

    y_n_gridfunctions, non_y_n_gridfunctions_list, diagnostic_gridfunctions_point_to = \
        generate_gridfunction_names(MoL_method = MoL_method)

    gridfunctions_list = []
    if which_gfs == "y_n_gfs":
        gridfunctions_list = [y_n_gridfunctions]
    elif which_gfs == "non_y_n_gfs":
        gridfunctions_list = non_y_n_gridfunctions_list
    else:
        print("ERROR: which_gfs = \"" + which_gfs + "\" unrecognized.")
        sys.exit(1)
    name = "MoL_free_memory_" + which_gfs
    params = "const paramstruct *restrict params, MoL_gridfunctions_struct *restrict gridfuncs"
    body = ""
    for gridfunctions in gridfunctions_list:
        body += "    free(gridfuncs->" + gridfunctions + ");\n"
    add_to_Cfunction_dict(
        includes=includes,
        desc=desc,
        c_type=c_type, name=name, params=params,
        body=indent_Ccode(body, "  "),
        rel_path_to_Cparams=os.path.join("."))
```

## Step 3.e: Define & register `MoL_gridfunctions_struct` in `NRPy_basic_defines.h`: `NRPy_basic_defines_MoL_timestepping_struct()` [Back to [top](#toc)\]
$$\label{nrpybasicdefines}$$

`MoL_gridfunctions_struct` stores pointers to all the gridfunctions needed by MoL, and we define this struct within `NRPy_basic_defines.h`.

```python
def NRPy_basic_defines_MoL_timestepping_struct(MoL_method="RK4"):
    y_n_gridfunctions, non_y_n_gridfunctions_list, diagnostic_gridfunctions_point_to = \
        generate_gridfunction_names(MoL_method=MoL_method)
    # Step 3.b: Create MoL_timestepping struct:
    indent = "  "
    Nbd = "typedef struct __MoL_gridfunctions_struct__ {\n"
    Nbd += indent + "REAL *restrict " + y_n_gridfunctions + ";\n"
    for gfs in non_y_n_gridfunctions_list:
        Nbd += indent + "REAL *restrict " + gfs + ";\n"
    Nbd += indent + "REAL *restrict diagnostic_output_gfs;\n"
    Nbd += "} MoL_gridfunctions_struct;\n"
    Nbd += """#define LOOP_ALL_GFS_GPS(ii) _Pragma("omp parallel for") \\
  for(int (ii)=0;(ii)<Nxx_plus_2NGHOSTS0*Nxx_plus_2NGHOSTS1*Nxx_plus_2NGHOSTS2*NUM_EVOL_GFS;(ii)++)\n"""

    outC_NRPy_basic_defines_h_dict["MoL"] = Nbd
```

## Step 3.f: Add all MoL C codes to C function dictionary, and add MoL definitions to `NRPy_basic_defines.h`: `register_C_functions_and_NRPy_basic_defines()` \[Back to [top](#toc)\]
$$\label{setupall}$$

```python
def register_C_functions_and_NRPy_basic_defines(MoL_method = "RK4",
            RHS_string =  "rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, RK_INPUT_GFS, RK_OUTPUT_GFS);",
       post_RHS_string = "apply_bcs(Nxx,Nxx_plus_2NGHOSTS, RK_OUTPUT_GFS);",
       enable_rfm=False, enable_curviBCs=False):
    for which_gfs in ["y_n_gfs", "non_y_n_gfs"]:
        add_to_Cfunction_dict_MoL_malloc(MoL_method, which_gfs)
        add_to_Cfunction_dict_MoL_free_memory(MoL_method, which_gfs)
    add_to_Cfunction_dict_MoL_step_forward_in_time(MoL_method, RHS_string, post_RHS_string,
                                                   enable_rfm=enable_rfm, enable_curviBCs=enable_curviBCs)
    NRPy_basic_defines_MoL_timestepping_struct(MoL_method = MoL_method)
```

# Step 4: Code Validation against `MoLtimestepping.MoL_new_way` NRPy+ module [Back to [top](#toc)\]
$$\label{code_validation}$$

As a code validation check, we verify agreement in the dictionary of Butcher tables between

1. this tutorial and
2. the NRPy+ [MoLtimestepping.MoL_new_way](../edit/MoLtimestepping/MoL_new_way.py) module.

We generate the header files for each RK method and check for agreement with the NRPy+ module.

```python
import sys
import MoLtimestepping.MoL_new_way as MoLC
import difflib
import pprint
# Courtesy https://stackoverflow.com/questions/12956957/print-diff-of-python-dictionaries ,
#   which itself is an adaptation of some Cpython core code
def compare_dicts(d1, d2):
    return ('\n' + '\n'.join(difflib.ndiff(
                   pprint.pformat(d1).splitlines(),
                   pprint.pformat(d2).splitlines())))

print("\n\n ### BEGIN VALIDATION TESTS ###")
import filecmp
for key, value in Butcher_dict.items():
    register_C_functions_and_NRPy_basic_defines(key,
                            "rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, RK_INPUT_GFS, RK_OUTPUT_GFS);",
                           "apply_bcs(Nxx,Nxx_plus_2NGHOSTS, RK_OUTPUT_GFS);")
    from outputC import outC_function_dict
    notebook_dict = outC_function_dict.copy()
    outC_function_dict.clear()
    from outputC import outC_function_dict
    if outC_function_dict != {}:
        print("Error in clearing dictionary.")
        sys.exit(1)
    MoLC.register_C_functions_and_NRPy_basic_defines(key,
                            "rhs_eval(Nxx,Nxx_plus_2NGHOSTS,dxx, RK_INPUT_GFS, RK_OUTPUT_GFS);",
                           "apply_bcs(Nxx,Nxx_plus_2NGHOSTS, RK_OUTPUT_GFS);")
    from outputC import outC_function_dict
    trusted_dict = outC_function_dict

    if notebook_dict != trusted_dict:
        print("VALIDATION TEST FAILED.\n")
        print(compare_dicts(notebook_dict, trusted_dict))
        sys.exit(1)
    print("VALIDATION TEST PASSED on all files from "+str(key)+" method")
print("### END VALIDATION TESTS ###")
```

     ### BEGIN VALIDATION TESTS ###
    VALIDATION TEST PASSED on all files from Euler method
    VALIDATION TEST PASSED on all files from RK2 Heun method
    VALIDATION TEST PASSED on all files from RK2 MP method
    VALIDATION TEST PASSED on all files from RK2 Ralston method
    VALIDATION TEST PASSED on all files from RK3 method
    VALIDATION TEST PASSED on all files from RK3 Heun method
    VALIDATION TEST PASSED on all files from RK3 Ralston method
    VALIDATION TEST PASSED on all files from SSPRK3 method
    VALIDATION TEST PASSED on all files from RK4 method
    VALIDATION TEST PASSED on all files from DP5 method
    VALIDATION TEST PASSED on all files from DP5alt method
    VALIDATION TEST PASSED on all files from CK5 method
    VALIDATION TEST PASSED on all files from DP6 method
    VALIDATION TEST PASSED on all files from L6 method
    VALIDATION TEST PASSED on all files from DP8 method
    ### END VALIDATION TESTS ###

# Step 5: Output this notebook to $\LaTeX$-formatted PDF \[Back to [top](#toc)\]
$$\label{latex_pdf_output}$$

The following code cell converts this Jupyter notebook into a proper, clickable $\LaTeX$-formatted PDF file. After the cell is successfully run, the generated PDF may be found in the root NRPy+ tutorial directory, with filename
[Tutorial-RK_Butcher_Table_Generating_C_Code.pdf](Tutorial-RK_Butcher_Table_Generating_C_Code.pdf) (Note that clicking on this link may not work; you may need to open the PDF file through another means.)

```python
import cmdline_helper as cmd    # NRPy+: Multi-platform Python command-line interface
cmd.output_Jupyter_notebook_to_LaTeXed_PDF("Tutorial-Method_of_Lines-C_Code_Generation_new_way")
```

    Created Tutorial-Method_of_Lines-C_Code_Generation_new_way.tex, and
        compiled LaTeX file to PDF file Tutorial-Method_of_Lines-
        C_Code_Generation_new_way.pdf


# HOMEWORK 3

This homework involves designing a closed loop system with a PID controller.

```python
!pip -q install controlSBML
import controlSBML as ctl
import control
from controlSBML.util import makeSimulationTimes
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import sympy as sy
import tellurium as te
import sympy
print("controlSBML version: " + ctl.__version__)
```

```python
# Use the local file if it's available
try:
    BIOMD823 = "/home/ubuntu/controlSBML/data/BIOMD0000000823.xml"
    _ = ctl.ControlSBML(BIOMD823)
except:
    BIOMD823 = "https://www.ebi.ac.uk/biomodels/model/download/BIOMD0000000823.2?filename=Varusai2018.xml"
```

**Transfer Functions**
\begin{eqnarray}
H_{RY}(s) & = & \frac{Y(s)}{R(s)} & = & \frac{C(s) G(s)}{1 + C(s) G(s) F(s)} \\
H_{RE}(s) & = & \frac{E(s)}{R(s)} & = & \frac{1}{1 + C(s) G(s) F(s)} \\
H_{NY}(s) & = & \frac{Y(s)}{N(s)} & = & -\frac{ F(s)}{1 + C(s) G(s) F(s)} \\
H_{DY}(s) & = & \frac{Y(s)}{D(s)} & = & \frac{ C(s)}{1 + C(s) G(s) F(s)} \\
\end{eqnarray}

In this homework, you will design a closed loop system for controlling pDEPTOR in the mTOR system as modelled in BioModels 823.
Use pAkt as the control input.
1. **Build**. (4 pt) Implement the function ``evaluateDesign`` that evaluates the control design of
a closed loop system for mTOR.
The evaluation is in the form of a plot of the step response of the system
to a change in the reference input.
The plot should include the reference input along with the pAkt and pDEPTO
values for the closed loop mTOR system. The function has as inputs the design parameters kp, ki, kd.
1. **Design**. (2 pt) Use ``evaluateDesign`` to design a system that has no bias for a unit step response in the reference and settles within 10 seconds.
1. **Analysis**. (3 pt) Construct the transfer function
for mTOR from pAkt to pDEPTOR
and the transfer function $H_{NY} (s)$ for the design parameters you used in (2)
(Obtain the Jacobian at time 1 from the open loop mTOR system.)
Use these transfer functions to answer the following questions about how the closed loop system should behave *in theory*.
   1. Why are large control inputs required to control the mTOR system?
   1. Why are there no long term oscillations in the closed loop system?
   1. How will a step increase in noise affect the output of the closed loop system?
1. **Discussion**. (1 pt) Plot the values of pAkt for the closed loop system you designed in (2).
What issues do you see with using this closed loop system in practice?

# Q1. Build

```python
def evaluateDesign(
      model_reference=BIOMD823,
      kp=1000, ki=2, kd=0, kf=None, noise_amp=0, noise_frq=20, disturbance_amp=0, disturbance_frq=2,
      step_size=1, end_time=100, is_plot=True,
      **kwargs):
    """
    Create an evaluation test and plots the step response.

    Parameters
    ----------
    name: str
        Name of the resulting Interconncted system
    model_reference: str
        antimony string or file path or URL
    kp: float
    ki: float
    kd: float
    kf: float
    step_size: float
    kwargs: dict
        plot options

    """
    ctlsb = ctl.ControlSBML(
        model_reference=model_reference,
        input_names=["pAkt"], output_names=["pDEPTOR"])
    siso = ctl.SISOClosedLoopSystem(ctlsb)
    closed_loop_outputs=["entry.out", "sum_R_F.out",
          "sum_U_D.out", "exit.out"]
    siso.makePIDClosedLoopSystem("cl_sys", kp=kp, ki=ki, kd=kd, kf=kf,
        noise_amp=noise_amp, noise_frq=noise_frq,
        disturbance_amp=disturbance_amp, disturbance_frq=disturbance_frq,
        closed_loop_outputs=closed_loop_outputs)
    ts = siso.makeStepResponse(time=1, step_size=1, end_time=end_time,
      points_per_time=2)
    ts.columns = ["input", "e(t)", "system.in", "output"]
    if is_plot:
        ctl.plotOneTS(ts, **kwargs)

# Tests
evaluateDesign(end_time=20, kp=3000, ylim=[-2, 2], figsize=[15, 5], is_plot=False)
print("OK!")
```

# Q2. Design

```python
evaluateDesign( noise_amp=0, end_time=20, # Environment
               kp=1000, ki=2,  kf=None,       # Design
               ylim=[-5, 5],figsize=[15, 5])  # Plot
```

# Q3. Analyze

```python
def analyze(
    model_reference=BIOMD823, time=0, kp=1000, ki=2, kd=0, kf=None):
    """
    Create an evaluation test and plots the step response.

    Parameters
    ----------
    name: str
        Name of the resulting Interconncted system
    model_reference: str
        antimony string or file path or URL
    time: float
        time at which Jacobian is calculated
    kp: float
    ki: float
    kd: float
    kf: float

    Returns
    -------
    dict
        key: str ("H_RY", "H_NY", "H_DY")
        value: control.TransferFunction
    """
    ctlsb = ctl.ControlSBML(
        model_reference=model_reference,
        input_names=["pAkt"], output_names=["pDEPTOR"])
    Gs = ctlsb.makeTransferFunction(time=time)
    Cs = control.TransferFunction([kp], [1]) + control.TransferFunction([ki], [1, 0]) + control.TransferFunction([kd, 0], [1])
    if kf is None:
        Fs = 1
    else:
        Fs = control.TransferFunction([-kf], [1, kf])
    denom = 1 + Cs*Gs*Fs
    dct = {
        "Gs": Gs,
        "H_RY": Cs*Gs/denom,
        "H_NY": -Fs/denom,
        "H_DY": Gs/denom,
    }
    return dct

# Tests
dct = analyze()
```

```python
dct = analyze(time=1)
```

```python
dct["Gs"].dcgain()
```

```python
dct["H_RY"].pole()
```

```python
dct["H_NY"]
```

1. Why are large control inputs required to control the mTOR system?
   * Large control inputs are required because there is a small DC gain for mTOR from pAkt to pDEPTOR.
1. Why are there no long term oscillations in the closed loop system?
   * In $H_{RY}$, There is no pole close to zero if a non-zero imaginary part.
1. How will a step increase in noise affect the system?
   * Since the DC gain of $H_{NY}$ is 0, in theory, noise will not affect the output.

# Q4. Discussion

1. The closed loop system requires that pAkt be extremely large.
1. The closed loop system requires negative values of pAkt.


```python
# import pandas as pd
# import numpy as np
import matplotlib.pyplot as plt
# import math
# from networkx import minimum_cut, DiGraph
from mip import *
# from itertools import product

from src.read_instance import MDOVRP
# rnd = np.random
# rnd.seed(454)
```

```python
# filepath = "Instancias/ES-n78-m2-Q10138.txt"
filepath = "Instancias/Vrp-Set-A/A-n80-m2-Q60.vrp"
```

```python
N, D, V, Q, q, c, coord_x, coord_y = MDOVRP(filepath)
# N -> cidades
# D -> depositos
# V = N + D
```

```python
# fazendo c ser simétrica
# c = {(i,j) : (c[i,j] + c[j, i])/2  for (i,j) in c}
```

Usando a formulação $MDOVRP_{2i− flv}$ de Lalla-Ruiz e Mes (2019)

    Lalla-Ruiz, Eduardo, and Martijn Mes. "Mathematical formulations and improvements for the multi-depot open vehicle routing problem." Optimization Letters 15 (2021): 271-286.

```python
#cria o modelo
model = Model('PRVMD', solver_name = GUROBI)

# Variaveis de decisao
x = [[model.add_var(var_type=BINARY) if i!=j else model.add_var(lb=0, ub=0) for i in V] for j in V]
u = [[model.add_var(var_type=CONTINUOUS) if i!=j else model.add_var(lb=0, ub=0) for i in V] for j in V]
y = [model.add_var(var_type=BINARY) for i in N]
w = [model.add_var(var_type=BINARY) for i in N] # decide se cidade será atendida
```

    Set parameter WLSAccessID
    Set parameter WLSSecret
    Set parameter LicenseID
    Academic license - for non-commercial use only - registered to thiago.giachetto@aluno.ufop.edu.br

\begin{equation}
\label{eq:9}
    x_{ij} \in \{0, 1\}, \quad \forall i, j \in V
\end{equation}

\begin{equation}
\label{eq:10}
    u_{ij} \geq 0, \quad \forall i, j \in V
\end{equation}

```python
# # Funcao objetivo
M = 1000
model.objective = minimize(xsum(x[i][j] * c[i,j] for i in V for j in V if i != j) - M*xsum(w[j] * q[j] for j in N))
```

\begin{equation}
\label{eq:1}
 Minimizar \quad \sum_{i \in V }\sum_{j \in V} c_{ij}x_{ij}
\end{equation}

```python
# maximizar as demandas atendidas
# model.objective = maximize(xsum(w[j] * q[j] for j in N))
```

```python
# tentando melhorar a solução encontrada
# model += xsum(x[i][j] * c[i,j] for i in V for j in V if i != j) <= 400
```

```python
# # Restricao 2
# for j in N:
#     model += xsum(x[i][j] for i in V if i != j) == 1
```

```python
# Restricao se cidade pertencera a alguma rota

for j in N:
    model += xsum(x[i][j] for i in V if i != j) == w[j]
#     model += xsum(x[j][i] for i in N if i != j) <= w[j] # nao volta para o deposito
```

\begin{equation}
    \sum_{i \in V, i \neq j}x_{ij} = 1, \quad \forall j \in N
    \tag{2}
\end{equation}

```python
# Limitar se cidade pode ter sua "necessidade" atendida
# isso tem q ser feito relacionando w[j]
```

```python
# Restricao 18

for j in N:
    model += (xsum(x[i][j] for i in V if i != j) - xsum(x[j][i] for i in N if i != j)) >= 0
```

\begin{equation}
    \sum_{i \in V, i \neq j}x_{ij} - \sum_{i \in N, i \neq j}x_{ji} \geq 0, \quad \forall j \in N
    \tag {18}
\end{equation}

```python
# Restricao 19 modificada
for i in V:
    for j in N:
        if i != j:
            model += x[i][j] + x[j][i] <= w[j]
            if i not in D:
                model += x[i][j] + x[j][i] <= w[i]

for i in V:
    for k in D:
        if i != k:
            model += x[i][k] + x[k][i] <= 1
```

```python
for i in V:
    for j in N:
        if i != j:
            model += x[i][j] >= w[j]
```

```python
# for i in V:
#     for j in N:
#         if i != j:
#             model += x[i][j] <= w[j]
```

```python
# # Restricao 19
# for i in V:
#     for j in V:
#         if i != j:
#             model += x[i][j] + x[j][i] <= 1
```

\begin{equation}
    x_{ij} + x_{ji} \leq 1, \quad \forall i, j \in V, i \neq j
    \tag {19}
\end{equation}

```python
# Restricao 20 -> nenhum veiculo volta para o deposito
model += xsum(x[j][k] for k in D for j in V) == 0
```

\begin{equation}
    \sum_{k \in D}\sum_{j \in V}x_{jk} = 0
    \tag {20}
\end{equation}

```python
# # Restricao 21
# for j in N:
#     model += xsum(u[i][j] for i in V if i != j) - xsum(u[j][i] for i in V if i != j) >= q[j]
```

```python
# Restricao 21 modificada, para so atender a demanda se cidade for visitada
for j in N:
    model += xsum(u[i][j] for i in V if i != j) - xsum(u[j][i] for i in V if i != j) >= q[j]*w[j]
```

\begin{equation}
    \sum_{i \in V, i \neq j}u_{ij} - \sum_{i \in V, i \neq j}u_{ji} \geq q_j, \quad \forall j \in N
    \tag {21}
\end{equation}

```python
#Restricao 22
for i in N:
    for j in N:
        model += (Q - q[i])*x[i][j] >= u[i][j]
```

\begin{equation}
    (Q - q_i) \cdot x_{ij} \geq u_{ij}, \quad \forall i, j \in N
    \tag {22}
\end{equation}

```python
# Restricao 23
for k in D:
    for j in N:
        model += Q * x[k][j] >= u[k][j]
```

```python
# se cidade H não é comtemplada => w[H]=0
# Q*x[k][H] = 0! => u[k][H]=0
```

\begin{equation}
    Q \cdot x_{kj} \geq u_{kj}, \quad \forall k \in D, j \in N
    \tag {23}
\end{equation}

```python
# # Restricao 8
# d = [min([c[j, i] for j in D]) for i in N]
# r = [min([c[j, i] for j in N if i != j]) for i in N]
# M = max(r)

# for i in N:
#     model += d[i] + M * y[i] >= r[i]
```

\begin{equation}
    d_{i} + M y_{i} \geq r_{i}, \quad \forall k \in N
    \tag {8}
\end{equation}

```python
# # Restricao 9 (e rest 8) - estabelece se é vantajoso visitar determinada cidade diretamente do deposito

# for i in N:
#     k_l = D[np.argmin([c[j, i] for j in D], axis=0)]
#     model += x[k_l][i] >= y[i]
```

\begin{equation}
    x_{k'i} \geq y_{i}, \quad \forall i \in N, k' = argmin(c_{ki})_{k \in D}
    \tag {9}
\end{equation}

```python
# # Restricao 10 - estabelece um limite inferior para o numero de veiculos

# model += xsum(x[k][i] for i in N for k in D) >= np.ceil(sum(q)/Q)
```

\begin{equation}
    \sum_{i \in N}\sum_{k \in D}x_{ki} \geq \lceil \frac{\sum_{i \in N} q_{i}}{Q} \rceil
    \tag {10}
\end{equation}

```python
# # Restricao de distancia maxima entre duas cidades

# for i in V:
#     for j in V:
#         if i != j:
#             model += c[i,j]*x[i][j] <= 180
```

```python
# # limite de atendimento da demanda

# # demanda total
# W = sum([q[j] for j in N])
# # porcentagem de demanda atendida
# alfa = 0.8
# # alfa = 1

# model += xsum(q[j]*w[j] for j in N) >= alfa*W

# # # Restricao 10 modificada - estabelece um limite inferior para o numero de veiculos, considerando alfa

# # model += xsum(x[k][i] for i in N for k in D) >= np.ceil(alfa*sum(q)/Q)

```

```python
# # limite de carros por deposito

# for k in D:
#     model += xsum(x[k][i] for i in N) <= 7
```

```python
# limite de carros total
NUM_VEICULOS = 8

model += xsum(x[k][i] for k in D for i in N) <= NUM_VEICULOS
# limite da capacidade
# model += xsum(u[k][i] for k in D for i in N) <= NUM_VEICULOS*Q
```

```python
# Resolve o modelo
# model.optimize(max_seconds=7200)
model.optimize(max_seconds=30)
print("status: {} objective value : {} best possible: {}".format(model.status, model.objective_value, model.objective_bound))
```

    Set parameter TimeLimit to value 30
    Set parameter NodeLimit to value 1073741824
    Set parameter SolutionLimit to value 1073741824
    Set parameter IntFeasTol to value 1e-06
    Set parameter Method to value 3
    Gurobi Optimizer version 9.5.0 build v9.5.0rc5 (linux64)
    Thread count: 2 physical cores, 4 logical processors, using up to 4 threads
    Academic license - for non-commercial use only - registered to thiago.giachetto@aluno.ufop.edu.br
    Optimize a model with 26244 rows, 13608 columns and 97528 nonzeros
    Model fingerprint: 0x6d1b66a3
    Variable types: 6806 continuous, 6802 integer (6802 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 6e+01]
      Objective range  [1e+00, 1e+01]
      Bounds range     [1e+00, 1e+00]
      RHS range        [1e+00, 8e+00]
    Found heuristic solution: objective -0.0000000
    Presolve removed 26244 rows and 13608 columns
    Presolve time: 0.13s
    Presolve: All rows and columns removed

    Explored 0 nodes (0 simplex iterations) in 0.15 seconds (0.05 work units)
    Thread count was 1 (of 4 available processors)

    Solution count 1: -0
    No other solutions better than -0

    Optimal solution found (tolerance 1.00e-04)
    Best objective -0.000000000000e+00, best bound -0.000000000000e+00, gap 0.0000%
    status: OptimizationStatus.OPTIMAL objective value : -0.0 best possible: -0.0

```python
# --- PLOTA O GRAFICO ---
plt.figure(figsize=(10, 6))
plt.scatter(coord_x[0:], coord_y[0:])
for i in N:
    plt.annotate(f"{i}", (coord_x[i], coord_y[i]))
for i in D:
    plt.plot(coord_x[i], coord_y[i], c = 'r', marker = 's')

for i in V:
    for j in V:
        if x[i][j].x > 0:
            plt.plot([coord_x[i], coord_x[j]], [coord_y[i], coord_y[j]], c='g', zorder=0)

#escala dos eixos
plt.yticks([i for i in range(1,80,20)]);
plt.xticks([i for i in range(1,80,20)]);

```

```python
# dist percorrida
sum([(1 if x[i][j].x>0.5 else 0) * c[i,j] for i in V for j in V if i != j])
```

    0.0

```python
# nao atendidos
[i for i in N if w[i].x < 0.5]
```

    [0,
     1,
     2,
     3,
     4,
     5,
     6,
     7,
     8,
     9,
     10,
     11,
     12,
     13,
     14,
     15,
     16,
     17,
     18,
     19,
     20,
     21,
     22,
     23,
     24,
     25,
     26,
     27,
     28,
     29,
     30,
     31,
     32,
     33,
     34,
     35,
     36,
     37,
     38,
     39,
     40,
     41,
     42,
     43,
     44,
     45,
     46,
     47,
     48,
     49,
     50,
     51,
     52,
     53,
     54,
     55,
     56,
     57,
     58,
     59,
     60,
     61,
     62,
     63,
     64,
     65,
     66,
     67,
     68,
     69,
     70,
     71,
     72,
     73,
     74,
     75,
     76,
     77,
     78,
     79]

```python
# nro de veiculos
sum(x[k][i].x for k in D for i in N)
```

    0.0

```python
# cidades que tem mais de dois arcos ligadas a ela (depositos podem ter mais)
print([j for j in V if len([i for i in V if x[j][i].x > 0.5 or x[i][j].x > 0.5]) > 2])
```

    []

```python
# [el.x for el in w]
# cidades nao atendidas
[i for (i, el) in enumerate(w) if el.x < 0.5]
```

    [0,
     1,
     2,
     3,
     4,
     5,
     6,
     7,
     8,
     9,
     10,
     11,
     12,
     13,
     14,
     15,
     16,
     17,
     18,
     19,
     20,
     21,
     22,
     23,
     24,
     25,
     26,
     27,
     28,
     29,
     30,
     31,
     32,
     33,
     34,
     35,
     36,
     37,
     38,
     39,
     40,
     41,
     42,
     43,
     44,
     45,
     46,
     47,
     48,
     49,
     50,
     51,
     52,
     53,
     54,
     55,
     56,
     57,
     58,
     59,
     60,
     61,
     62,
     63,
     64,
     65,
     66,
     67,
     68,
     69,
     70,
     71,
     72,
     73,
     74,
     75,
     76,
     77,
     78,
     79]