```python
# Erasmus+ ICCT project (2018-1-SI01-KA203-047081)

# Toggle cell visibility

from IPython.display import HTML
tag = HTML('''
Toggle cell visibility <a href="javascript:code_toggle()">here</a>.''')
display(tag)

# Hide the code completely

# from IPython.display import HTML
# tag = HTML('''<style>
# div.input {
#     display:none;
# }
# </style>''')
# display(tag)
```

Toggle cell visibility here.

```python
%matplotlib notebook

import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np
import sympy as sym

from ipywidgets import widgets, Layout
from ipywidgets import interact

from IPython.display import Latex, display, Markdown # For displaying Markdown and LaTeX code

from matplotlib import patches
```

    Traceback (most recent call last):
      File "C:\Users\Uporabnik\Anaconda3\envs\icct2\lib\site-packages\matplotlib\cbook\__init__.py", line 216, in process
        func(*args, **kwargs)
      File "C:\Users\Uporabnik\Anaconda3\envs\icct2\lib\site-packages\matplotlib\animation.py", line 1465, in _stop
        self.event_source.remove_callback(self._loop_delay)
    AttributeError: 'NoneType' object has no attribute 'remove_callback'

## Sistem za krmiljenje položaja antene

Shematski primer enostavnega sistema za krmiljenje položaja antene je shematsko prikazan na levi sliki spodaj. Cilj sistema za krmiljenje je vzdrževanje referenčnega (zahtevanega) položaja antene, ki ga določimo z nastavitvijo zahtevanega kota antene $\theta_{ref}$ na referenčnem potenciometru (RP). Vstopni signal v blokovni diagram sistema (prikazan na sliki desno spodaj) je tako $\theta_{ref}$, ki se nato pretvori v napetostni signal $U_1$, od katerega se odšteje napetostni signal $U_2$, ki predstavlja izstopni signal za merilnega potenciometra (MP). $U_2$ nosi informacijo o dejanskem položajnem kotu antene. Razlika napetostnih signalov $U_1-U_2$ predstavlja odstopek, ki nam pove, koliko dejanski položajni kot odstopa od zahtevanega. Glede na ta odstopek, sistem za krmiljenje deluje na elektromotor, kateri zavrti anteno na način, da se zmanjša odstopek. $d_w$ predstavlja motnjo zaradi vetra, ki naključno rotira anteno.

        Shematski prikaz sistema za krmiljenje položaja antene
        Blokovni diagram sistema za krmiljenje položaja antene

        Legend: RP-referenčni potenciometer, MP-merilni potenciometer, dw-motnja zaradi vetra.

---

### Kako upravljati s tem interaktivnim primerom?

- Z uporabo drsnikov spreminjamo vrednosti zahtevanega položaja antene ($\theta_{ref}$), motnje zaradi vetra ($d_w$), ter koeficientov proporcionalnega ($K_p$), integracijskega ($K_i$) in diferencirnega ($K_d$) objekta.

- S preklopnimi gumbi izbiramo med proporcionalnim (P), proporcionalno-integrirnim (PI) in proporcionalno-integrirno-diferencirnim (PID) tipom krmilnika.

---

### Opombe

- Velikost rdeče puščice označene na shematskem prikazu sistema za krmiljenje položaja antene je proporcionalno z magnitudo motnje zaradi vetra ($d_w$), medtem ko smer te puščice kaže smer motnje.
- Črtkana modra črta na shematskem prikazu sistema za krmiljenje položaja antene označuje dejanski položaj antene.
- Črtkana zelena črta na shematskem prikazu sistema za krmiljenje položaja antene označuje zahtevani položaj antene.
- Rdeča črta iz pik in črtic na shematskem prikazu sistema za krmiljenje položaja antene predstavlja predhodni položaj antene.

Izbiraš lahko med dvema načinoma za prikaz rezultatov:
1. Ponastavi shematski prikaz sistema ob spremembi tipa krmilnika.
2. Ponastavi graf položaja antene v odvisnosti od časa ob spremembi tipa krmilnika.

```python
# define system constants
_Kpot = 0.318

_K1 = 100
_a = 100
_Km = 2.083
_am = 1.71
_Kg = 0.1
_R = 8
_Kt = 0.5
_Tv = 200 #in milliseconds

#set current theta and theta reference:
th = [0,0,0,0,0,0]
thref = [0,0,0,0,0,0]
# disturbance:
m = [0,0,0,0,0,0]
#joined together (first theta reference, second disturbance, then theta measured):
variables = [thref, m, th]

# variables of controller:
_K = 1
_taui = 1
_taud = 1

```

```python
# symbolic calculus:
taui, taud, K, s, z = sym.symbols('taui, taud, K, s, z')

_alpha=0.1
#controller:
P = K
I = K/(taui*s)
D = K*taud*s/(_alpha*taud*s+1)

def make_model(controller):
    if controller == 'P':
        C = P
    elif controller == 'PI':
        C = P+I
    elif controller == 'PID':
        C = P+I+D
    else:
        print('Controller type not in library')

    tf_s = C*_K1*_Km*_Kg*_Kpot/(s*(s+_a)*(s+_am)+C*_K1*_Km*_Kg*_Kpot)
    tf_s = tf_s.simplify()

    tf_z = tf_s.subs(s,2/(_Tv/1000)*(z-1)/(z+1))
    tf_z = tf_z.simplify()

    num = [sym.fraction(tf_z.factor())[0].expand().coeff(z, i) for i in reversed(range(1+sym.degree(sym.fraction(tf_z.factor())[0], gen=z)))]
    den = [sym.fraction(tf_z.factor())[1].expand().coeff(z, i) for i in reversed(range(1+sym.degree(sym.fraction(tf_z.factor())[1], gen=z)))]
    #print(num)
    #print(den)

    tf_sM = _Km*_Kg*_R*(s+_a)/(s*(s+_a)*(s+_am)*_Kt+C*_K1*_Km*_Kg*_Kpot*_Kt)

    tf_zM = tf_sM.subs(s,2/(_Tv/1000)*(z-1)/(z+1))
    tf_zM = tf_zM.simplify()
    num_M = [sym.fraction(tf_zM.factor())[0].expand().coeff(z, i) for i in reversed(range(1+sym.degree(sym.fraction(tf_zM.factor())[0], gen=z)))]
    #print(num_M)
    #print(den_M)

    #print('\n........finished........')
    return sym.lambdify((K, taui, taud), [np.array(num), -np.array(num_M), -np.array(den)])

z_transform_p = make_model('P')
z_transform_pi = make_model('PI')
z_transform_pid = make_model('PID')
```

```python
def calculate_next(z_transform):
    variables[-1][0] = 0 # set current to zero
    z_transform = z_transform(_K, _taui, _taud)

    temp = 0
    for i in range(len(z_transform)): # for every polynomial
        for j in range(len(z_transform[i])): # for every term in polynomial
            temp += z_transform[i][j] * variables[i][j]

    return temp / z_transform[-1][0]*(-1)
```

```python
fig = plt.figure(figsize=(9.8, 4),num='Antenna azimuth position control system')
# add axes
ax = fig.add_subplot(121)
graph = fig.add_subplot(122)

#set current theta and theta reference:
th = [0,0,0,0,0,0]
thref = [1,0,0,0,0,0]
# disturbance:
m = [.1,0,0,0,0,0]
#joined together (first theta reference, second disturbance, then theta measured):
variables = [thref, m, th]

# variables of controller:
_K = 20
_taui = 10
_taud = 1

new_flag_value = [True, 0] # flag for displaying old value of th, before th_ref was changed [flag, angle]

#slider widgets:
th_ref_widget = widgets.FloatSlider(value=variables[0][0],min=0.0,max=2*np.pi,step=.01,description=r'\(\theta_{ref} \) [rad]',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.2f')
m_widget = widgets.FloatSlider(value=variables[1][0],min=-.3,max=.3,step=.01,description=r'\(d_{w} \)',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.2f')
K_widget = widgets.FloatSlider(value=_K,min=0.0,max=40,step=.1,description=r'\(K_p \)',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.1f')
taui_widget = widgets.FloatSlider(value=_taui,min=0.01,max=60,step=.01,description=r'\(K_i \)',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.2f')
taud_widget = widgets.FloatSlider(value=_taud,min=0.0,max=5,step=.1,description=r'\(K_d \)',
                    disabled=False,continuous_update=True,orientation='horizontal',readout=True,readout_format='.2f')
#interact(set_coefficients, setK=K_widget, setthref=th_ref_widget, setm=m_widget, settaui=taui_widget, settaud=taud_widget)

#checkboxes
#checkbox_reset_antenna = widgets.Checkbox(value=False, description='Reset schematic representation of antenna when type of controller is changed', disabled=False)
#checkbox_reset_graph = widgets.Checkbox(value=False, description='Reset graph when type of controller is changed', disabled=False)

checkbox_reset_antenna = widgets.Checkbox(value=False, disabled=False, layout=Layout(width='100px'))
label_scheme = widgets.Label('Ponastavi shematski prikaz sistema ob spremembi tipa krmilnika', layout=Layout(width='500px'))
box1 = widgets.HBox([checkbox_reset_antenna, label_scheme])

checkbox_reset_graph = widgets.Checkbox(value=False, disabled=False, layout=Layout(width='100px'))
label_graph = widgets.Label('Ponastavi graf položaja antene v odvisnosti od časa ob spremembi tipa krmilnika', layout=Layout(width='500px'))
box2 = widgets.HBox([checkbox_reset_graph, label_graph])

style = {'description_width': 'initial'}

#buttons:
def buttons_clicked(event):
    global controller_type, equation, list_th, list_th_ref, list_time
    controller_type = buttons.options[buttons.index]
    if controller_type =='P':
        taui_widget.disabled=True
        taud_widget.disabled=True
        equation = '$Kp$'
    if controller_type =='PI':
        taui_widget.disabled=False
        taud_widget.disabled=True
        equation = '$Kp\,(1+\dfrac{1}{T_{i}\,s})$'
    if controller_type =='PID':
        taui_widget.disabled=False
        taud_widget.disabled=False
        equation = '$Kp\,(1+\dfrac{1}{T_{i}\,s}+\dfrac{T_{d}\,s}{a\,T_{d}\,s+1})$'
    if checkbox_reset_antenna.value:
        #reset values to zero:
        for i in range(len(variables)):
            for j in range(1, len(variables[i])):
                variables[i][j] = 0
        variables[-1][0] = 0
    if checkbox_reset_graph.value:
        list_th = []
        list_th_ref = []
        list_time = []

buttons = widgets.ToggleButtons(
    options=['P', 'PI', 'PID'],
    description='Izberi tip krmilnika:',
    disabled=False,
    style=style)
buttons.observe(buttons_clicked)

#updating values
def set_values(event):
    global _K, _taui, _taud
    if event['name'] != 'value':
        return
    if th_ref_widget.value != variables[0][0] and not new_flag_value[0]:
        new_flag_value[0] = True
        new_flag_value[1] = variables[-1][0]

    variables[0][0] = th_ref_widget.value
    variables[1][0] = m_widget.value
    _K = K_widget.value
    _taui = taui_widget.value
    _taud = taud_widget.value
th_ref_widget.observe(set_values)
m_widget.observe(set_values)
K_widget.observe(set_values)
taui_widget.observe(set_values)
taud_widget.observe(set_values)

#displaying widgets:
display(buttons)
vbox1 = widgets.VBox([th_ref_widget, m_widget, K_widget, taui_widget, taud_widget])
vbox2 = widgets.VBox([box1, box2])
hbox = widgets.HBox([vbox1, vbox2])
display(hbox)

#setting at start:
controller_type = 'P'
taui_widget.disabled=True
taud_widget.disabled=True
equation = '$Kp$'
set_values({'name':'value'})

#lists for graph in time:
list_time = []
list_th = []
list_th_ref = []

#previous th before change of th_ref:
prev_th = 0

cycles_flag = True

def update_figure(i_time):
    global cycles_flag, variables, _K, controller_type, equation

    if cycles_flag == True:
        cycles_flag = False
        return

    if controller_type == 'P':
        th = calculate_next(z_transform_p)
    elif controller_type == 'PI':
        th = calculate_next(z_transform_pi)
    elif controller_type == 'PID':
        th = calculate_next(z_transform_pid)
    variables[-1][0] = th

    # save variables for next time step:
    for i in range(len(variables)):
        for j in reversed(range(len(variables[i])-1)):
            variables[i][j+1] = variables[i][j]

    list_time.append((i_time+1)*_Tv/1000)
    list_th.append(th)
    list_th_ref.append(variables[0][0])

    #plot:
    ax.clear()
    ax.plot([-1.5, 1.5, 1.5, -1.5], [-1.5, -1.5, 1.5, 1.5], ',', color='b')

    #plot line:
    ax.plot([np.cos(th)*-.5, np.cos(th)*1.5], [np.sin(th)*-.5, np.sin(th)*1.5], 'b--', linewidth=.7, alpha=.7)

    #plot antenna:
    center1 = 1
    center2 = 3
    d1 = 2.2
    d2 = 5.5
    x1 = center1*np.cos(th)
    y1 = center1*np.sin(th)
    x2 = center2*np.cos(th)
    y2 = center2*np.sin(th)
    arc1 = patches.Arc((x1, y1), d1, d1,
                 angle=th/np.pi*180+180, theta1=-58, theta2=58, linewidth=2, color='black', alpha=.7)
    arc2 = patches.Arc((x2, y2), d2, d2,
                 angle=th/np.pi*180+180, theta1=-20, theta2=20, linewidth=2, color='black', alpha=.7)
    ax.add_patch(arc1)
    ax.add_patch(arc2)
    if m_widget.value > 0:
        ax.plot(0, 0, 'r', alpha=.1, marker=r'$\circlearrowright$',ms=150*m_widget.value)
    elif m_widget.value < 0:
        ax.plot(0, 0, 'r', alpha=.1, marker=r'$\circlearrowleft$',ms=-150*m_widget.value)
    ax.set_title('Shematski prikaz sistema')

    #plot direction of antenna before thref change
    if abs(variables[0][0] - th) < 0.03:
        new_flag_value[0] = False
    if new_flag_value[0]:
        ax.plot([0,np.cos(new_flag_value[1])], [0, np.sin(new_flag_value[1])], 'r-.', alpha=.3, linewidth=0.5)
    #plot desired direction of antenna
    ax.plot([0,np.cos(variables[0][0])], [0, np.sin(variables[0][0])], 'g-.', alpha=.7, linewidth=0.7)

    ax.text(-1, 1.3, 'dejanski kot: %.2f rad' %th)
    ax.text(-1, -1.3, 'tip krmilnika:')
    ax.text(-1, -1.6, equation)

    ax.set_aspect('equal', adjustable='datalim')
    ax.set_xlim(-1.5,1.5)
    ax.set_ylim(-1.5,1.5)
    ax.axis('off')

    graph.clear()
    graph.plot(list_time, list_th_ref, 'g', label='zahtevani položaj')
    graph.plot(list_time, list_th, 'b', label='dejanski položaj')
    graph.set_xlabel('$t$ [s]')
    graph.set_ylabel('$\\theta$ [rad]')
    graph.legend(loc=4, fontsize=8)
    graph.set_title('Položaj antene v odvisnosti od časa')

    plt.show()

ani = animation.FuncAnimation(fig, update_figure, interval=_Tv)
```

    <IPython.core.display.Javascript object>

    ToggleButtons(description='Izberi tip krmilnika:', options=('P', 'PI', 'PID'), style=ToggleButtonsStyle(descri…

    HBox(children=(VBox(children=(FloatSlider(value=1.0, description='\\(\\theta_{ref} \\) [rad]', max=6.283185307…

```python

```


 **Chapter 4: [Spectroscopy](CH4-Spectroscopy.ipynb)**

# Introduction to Core-Loss Spectroscopy
Working with X-Sections

[Download](https://raw.githubusercontent.com/gduscher/MSE672-Introduction-to-TEM/main/Spectroscopy/CH4_07-Introduction_Core_Loss.ipynb)

[](
    https://colab.research.google.com/github/gduscher/MSE672-Introduction-to-TEM/blob/main/Spectroscopy/CH4_07-Introduction_Core_Loss.ipynb)

part of

 **[MSE672:  Introduction to Transmission Electron Microscopy](../_MSE672_Intro_TEM.ipynb)**

by Gerd Duscher, Spring 2021

Microscopy Facilities

Joint Institute of Advanced Materials

Materials Science & Engineering

The University of Tennessee, Knoxville

Background and methods to analysis and quantification of data acquired with transmission electron microscopes.

## Core --Loss Spectroscopy

As we can see in figure below the energies of the core shells are well defined and can be viewed as delta functions, unlike in the low loss region where the broad valence bands are the initial state. In both cases, however, we excite electrons into the conduction band.

*Excitation from a core-shell state up into the conduction band above the Fermi level.*

If we look at the transition between two states $< \Phi_f | H | \Phi_i >$ the transition should be quite sharp. In the case of the low-loss spectrum, we have many initial (the valence) states and many final (the conduction) states. The spectrum will be a convolution of these states.

The features of the core--loss edges are, therefore, much sharper than any details in the low--loss region. Because only the final states contribute to the features. These sharp features enable a wide variety of analysis to determine the chemical compositions and chemical bonding, probing the local conduction band of the sample.

### Chemical Composition

In this chapter we use the area under the ionization edge to determine the chemical composition of a (small) sample volume.
The equation used to determine the number of atoms per unit volume $N$ (also called areal density) is:
\begin{equation}
I_{edge}(\beta, \Delta E) = N I_{0}(\beta) \sigma_{edge}(\beta, \Delta E)
\end{equation}

$I_0$ is the number of electrons hitting the sample, and so directly comparable to the beam current.

The equation can be approximated assuming that the spectrum has not been corrected for single scattering:
\begin{equation}
I_{edge}(\beta, \Delta E) = N I_{low-loss}(\beta,\Delta E) \sigma_{edge}(\beta, \Delta E)
\end{equation}
where $\beta$ is the collection angle and $\sigma_{edge}$ is the **partial** cross--section (for energy window $\Delta E$) for the core--loss excitation.

>
> It is this cross-section $ \sigma_{edge}$ that we want to explorein this notebook.
>
We will do the chemical composition in the [next notebook](CH4_08-Chemical_Composition.ipynb)

## Load important packages

### Check Installed Packages

```python
import sys
from pkg_resources import get_distribution, DistributionNotFound

def test_package(package_name):
    """Test if package exists and returns version or -1"""
    try:
        version = get_distribution(package_name).version
    except (DistributionNotFound, ImportError) as err:
        version = '-1'
    return version

# Colab setup ------------------
if 'google.colab' in sys.modules:
    !pip install pyTEMlib -q
# pyTEMlib setup ------------------
else:
    if test_package('sidpy') < '0.0.5':
        print('installing sidpy')
        !{sys.executable} -m pip install  --upgrade pyTEMlib -q
    if test_package('pyTEMlib') < '0.2021.4.20':
        print('installing pyTEMlib')
        !{sys.executable} -m pip install  --upgrade pyTEMlib -q
# ------------------------------
print('done')
```

    done

### Import all relevant libraries

Please note that the EELS_tools package from pyTEMlib is essential.

```python
import sys
if 'google.colab' in sys.modules:
    %pylab --no-import-all inline
else:
    %pylab --no-import-all notebook
    %gui qt

import warnings
warnings.filterwarnings('ignore')

## We need to import a few important additional function from matplotlib,
## because we want to demonstrate a few more hidden functionalities of the EELS_tools of pytTEMlib.
from matplotlib.widgets import Cursor
from matplotlib.patches import Rectangle
from matplotlib.widgets import SpanSelector

## import the configuration files of pyTEMlib (we need access to the data folder)
import pyTEMlib
import pyTEMlib.eels_tools as eels
import pyTEMlib.interactive_eels as ieels

# For archiving reasons it is a good idea to print the version numbers out at this point
print('pyTEM version: ',pyTEMlib.__version__)
```

    Populating the interactive namespace from numpy and matplotlib
    pyTEM version:  0.2021.04.20

## Load Cross-Sections

The form factors are from:
X-Ray Form Factor, Attenuation, and Scattering Tables
NIST Standard Reference Database 66

 DOI: https://dx.doi.org/10.18434/T4HS32

Detailed Tabulation of Atomic Form Factors, Photoelectric Absorption and Scattering Cross Section, and Mass Attenuation Coefficients for Z = 1-92 from E = 1-10 eV to E = 0.4-1.0 MeV
C.T. Chantler,1 K. Olsen, R.A. Dragoset, J. Chang, A.R. Kishore, S.A. Kotochigova, and D.S. Zucker
NIST, Physical Measurement Laboratory

The cross sections are part of the pyTEMlib package and are stored as a pickled dictionary in the package data directory.

Below are the lines for accessing the cross sections with eels_tools of pyTEMlib.

```python
Xsections = eels.get_x_sections()
```

## Plot Cross Sections

Please add your favourite element ot the list of atomic numbers.

With the code cell above we made the whole database of cross secitons available for this notebook.

```python
# -----Input ------------ #
atomic_numbers = [16, 42]
# ----------------------- #
fig, ax = plt.subplots()
for Z in atomic_numbers:
    ax.plot(Xsections[str(Z)]['ene'], Xsections[str(Z)]['dat'], label = Xsections[str(Z)]['name'])

ax.set_xlim(0,600)
ax.set_ylim(0,2.5e17)
plt.legend();
```

    <IPython.core.display.Javascript object>

### List All Edges of an Element

```python
element = str(42)
for key in Xsections[element]:
    if isinstance(Xsections[element][key], dict):
        if 'onset' in Xsections[element][key]:
            print(f"{Xsections[element]['name']}-{key}: {Xsections[element][key]['onset']:8.1f} eV ")

```

    Mo-N3:     34.8 eV
    Mo-M5:    227.0 eV
    Mo-N5:      1.8 eV
    Mo-M4:    230.3 eV
    Mo-N2:     34.8 eV
    Mo-N1:     61.8 eV
    Mo-L3:   2520.2 eV
    Mo-M1:    504.6 eV
    Mo-L2:   2625.1 eV
    Mo-L1:   2865.5 eV
    Mo-K1:  19999.5 eV
    Mo-N4:      1.8 eV
    Mo-M3:    392.3 eV
    Mo-M2:    409.7 eV

Or ordered

```python
major_edges = ['K1', 'L3', 'M5', 'N5']
all_edges = ['K1','L1','L2','L3','M1','M2','M3','M4','M5','N1', 'N2','N3','N4','N5','N6','N7','O1','O2','O3','O4','O5','O6','O7', 'P1', 'P2', 'P3']
first_close_edges = ['K1', 'L3', 'M5', 'M3', 'N5', 'N3']

element = str(42)

def list_all_edges(Z):
    element = str(Z)
    print('All edges')
    for key in all_edges:
        if key in Xsections[element]:
            if 'onset' in Xsections[element][key]:
                print(f" {Xsections[element]['name']}-{key}: {Xsections[element][key]['onset']:8.1f} eV ")

def list_major_edges(Z):
    element = str(Z)
    print('Major edges')
    for key in major_edges:
        if key in Xsections[element]:
            if 'onset' in Xsections[element][key]:
                print(f" {Xsections[element]['name']}-{key}: {Xsections[element][key]['onset']:8.1f} eV ")
## Here with the function of the EELS_tools package
list_all_edges(element)
list_major_edges(element)
```

    All edges
     Mo-K1:  19999.5 eV
     Mo-L1:   2865.5 eV
     Mo-L2:   2625.1 eV
     Mo-L3:   2520.2 eV
     Mo-M1:    504.6 eV
     Mo-M2:    409.7 eV
     Mo-M3:    392.3 eV
     Mo-M4:    230.3 eV
     Mo-M5:    227.0 eV
     Mo-N1:     61.8 eV
     Mo-N2:     34.8 eV
     Mo-N3:     34.8 eV
     Mo-N4:      1.8 eV
     Mo-N5:      1.8 eV
    Major edges
     Mo-K1:  19999.5 eV
     Mo-L3:   2520.2 eV
     Mo-M5:    227.0 eV
     Mo-N5:      1.8 eV

### Plotting all edges of an element in view

Now, let's do it graphically

```python
major_edges = ['K1', 'L3', 'M5', 'N5']
all_edges = ['K1','L1','L2','L3','M1','M2','M3','M4','M5','N1', 'N2','N3','N4','N5','N6','N7','O1','O2','O3','O4','O5','O6','O7', 'P1', 'P2', 'P3']
first_close_edges = ['K1', 'L3', 'M5', 'M3', 'N5', 'N3']

def get_Z(Z):
    """
    returns the atomic number independent of input as a string or number

    input:
    Z: atomic number of chemical symbol (0 if not valid)
    """
    Xsections = eels.get_x_sections()

    Z_out = 0
    if str(Z).isdigit():
        Z_out = Z
    elif isinstance(Z, str):
        for key in Xsections:
            if Xsections[key]['name'].lower() == Z.lower(): ## Well one really should know how to write elemental
                Z_out = int(key)
    return Z_out

class ElementalEdges(object):
    def __init__(self, ax, Z):
        self.ax = ax
        self.labels = None
        self.lines = None

        self.Z = get_Z(Z)
        self.color = 'black'
        self.Xsections = eels.get_x_sections()
        self.cid = ax.figure.canvas.mpl_connect('draw_event', self.onresize)

        #self.update()
    def set_edge(self,Z):
        self.Z = get_Z(Z)

        self.update()
    def onresize(self, event):
        self.update()

    def update(self):

        if self.labels != None:
            for label in self.labels:
                label.remove()
        if self.lines != None:
            for line in self.lines:
                line.remove()
        if self.Z>0:
            self.labels = [] ; self.lines =[]
            x_min, x_max = self.ax.get_xlim()
            y_min, y_max = self.ax.get_ylim()
            x_bounds = ax.get_xlim()
            element = str(self.Z)
            Xsections = self.Xsections
            for key in all_edges:
                if key in Xsections[element]:
                    if 'onset' in Xsections[element][key]:
                        x = Xsections[element][key]['onset']
                        if x > x_min and x < x_max:
                            if key in first_close_edges:
                                label2 = self.ax.text(x, y_max,f"{Xsections[element]['name']}-{key}",
                                                      verticalalignment='top', rotation = 0, color = self.color)
                            else:
                                label2 = self.ax.text(x, y_max,f"\n{Xsections[element]['name']}-{key}",
                                                      verticalalignment='top', color = self.color)
                            line2 = self.ax.axvline(x,ymin = 0,ymax = 1,color=self.color)

                            self.labels.append(label2)

                            self.lines.append(line2)

    def disconnect(self):
        if self.labels != None:
            for label in self.labels:
                label.remove()
        if self.lines != None:
            for line in self.lines:
                line.remove()
        self.labels = None
        self.lines = None
        self.ax.figure.canvas.mpl_disconnect(self.cid)
    def reconnect(self):
        self.cid = ax.figure.canvas.mpl_connect('draw_event', self.onresize)
        ax.figure.canvas.draw_idle()

fig, ax_Xsec = plt.subplots()
for Z in atomic_numbers:
    ax_Xsec.plot(Xsections[str(Z)]['ene'], Xsections[str(Z)]['dat'], label = Xsections[str(Z)]['name'])
ax_Xsec.set_xlim(100,550)
ax_Xsec.set_ylim(0,1e17)
plt.legend();
Z = 42
edges = ieels.ElementalEdges(ax_Xsec, 'Mo')

```

    <IPython.core.display.Javascript object>

Let's make the lines disappear

```python
edges.disconnect()
```

and reappear in the plot above

```python
edges.set_edge(Z)
```

Let's set another edge

```python
edges.set_edge(16)
```

### Find Edges Listed in Xsection Dictionary

please note that the two functions below are as ususal available in the EELS_tools of pyTEMlib

```python
def find_major_edges(edge_onset, maximal_chemical_shift = 5):
    text = ''
    for element in Xsections:
        for key in Xsections[element]:

            #if isinstance(Xsections[element][key], dict):
            if key in major_edges:

                #if 'onset' in Xsections[element][key]:
                #print(key, Xsections[element][key])
                if abs(Xsections[element][key]['onset'] - edge_onset) < maximal_chemical_shift:
                    #print(element, Xsections[element]['name'], key, Xsections[element][key]['onset'])
                    text = text+ f"\n {Xsections[element]['name']:2s}-{key}: {Xsections[element][key]['onset']:8.1f} eV "

    return text
def find_all_edges(edge_onset, maximal_chemical_shift = 5):
    text = ''
    for element in Xsections:
        for key in Xsections[element]:

            if isinstance(Xsections[element][key], dict):

                if 'onset' in Xsections[element][key]:

                    if abs(Xsections[element][key]['onset'] - edge_onset) < maximal_chemical_shift:
                        #print(element, Xsections[element]['name'], key, Xsections[element][key]['onset'])
                        text = text+ f"\n {Xsections[element]['name']:2s}-{key}: {Xsections[element][key]['onset']:8.1f} eV "

    return text

edge_onset = 284
maximal_chemical_shift = 7
print(f'Major Edges within {maximal_chemical_shift:.1f} eV of {edge_onset:.1f}')
print(eels.find_major_edges(edge_onset, maximal_chemical_shift))
print(f'\nAll Edges within {maximal_chemical_shift:.1f} eV of {edge_onset:.1f}')
print(eels.find_all_edges(edge_onset, maximal_chemical_shift))
```

    Major Edges within 7.0 eV of 284.0

     C -K1:    283.8 eV
     Ru-M5:    279.4 eV

    All Edges within 7.0 eV of 284.0

     C -K1:    283.8 eV
     Gd-N2:    288.5 eV
     Ce-N1:    289.6 eV
     Kr-M1:    288.3 eV
     Tb-N3:    285.0 eV
     Os-N4:    289.4 eV
     Sr-M2:    279.8 eV
     Eu-N2:    283.9 eV
     Ru-M5:    279.4 eV
     Ru-M4:    283.6 eV

### Find Edges Depending on Cursor Postion

```python
# We are using
# from matplotlib.widgets import Cursor

maximal_chemical_shift = 5
fig, ax = plt.subplots()
plt.title(f'Click with left for major and right mouse button for all \n ionization edges within {maximal_chemical_shift:.1f} eV of cursor')
maximal_chemical_shift = 5
cursor = ieels.EdgesAtCursor(ax, Xsections['42']['ene'], Xsections['42']['dat'],maximal_chemical_shift)
cursor.maximal_chemical_shift =maximal_chemical_shift
cid =  plt.connect('motion_notify_event', cursor.mouse_move)

ax.plot(Xsections['16']['ene'], Xsections['16']['dat']*2, label = 'S')
ax.plot(Xsections['42']['ene'], Xsections['42']['dat'], 'r', label = 'Mo')
ax.set_xlim(0,500)
ax.set_ylim(0,2.5e17);

```

    <IPython.core.display.Javascript object>

## EELS cross sections
### Determine Effective Collection Angle

EELS cross sections are dependent on the momentum transfer (angle dependence), while photons cannot transfer any momentum. The angle dependence is given by the experimental set-up and can be calculated by the convolution of collection and convergence angle.

Here we use the method of [Pierre Trebbia, Ultramicroscopy **24** (1988) pp.399-408](https://doi.org/10.1016/0304-3991(88)90130-1)

```python
def effective_collection_angle(ene,  alpha, beta, beamkV):
    """ effective collection angle for convergent beam setup

    Original abstract of function y = effbeta(ene,  alpha, beta, beamkV)
    #
    #       This program computes etha(alpha,beta), that is the collection
    #       efficiency associated to the following geometry :
    #
    #       alpha = half angle of illumination  (0 -> pi/2)
    #       beta  = half angle of collection    (0 -> pi/2)
    #                                           (pi/2 = 1570.795 mrad)
    #
    #           A constant angular distribution of incident electrons is assumed
    #       for any incident angle (-alpha,alpha). These electrons impige the
    #       target and a single energy loss event occurs, with a characteristic
    #       angle theta-e (relativistic). The angular distribution of the
    #       electrons after the target is analytically derived.
    #           This program integrates this distribution from theta=0 up to
    #       theta=beta with an adjustable angular step.
    #           This program also computes beta* which is the theoretical
    #       collection angle which would give the same value of etha(alpha,beta)
    #       with a parallel incident beam.
    #
    #       subroutines and function subprograms required
    #       ---------------------------------------------
    #       none
    #
    #       comments
    #       --------
    #
    #       The following parameters are asked as input :
    #        accelerating voltage (kV), energy loss range (eV) for the study,
    #        energy loss step (eV) in this range, alpha (mrad), beta (mrad).
    #       The program returns for each energy loss step :
    #        alpha (mrad), beta (mrad), theta-e (relativistic) (mrad),
    #        energy loss (eV), etha (#), beta * (mrad)
    #
    #       author :
    #       --------
    #       Pierre TREBBIA
    #       US 41 : "Microscopie Electronique Analytique Quantitative"
    #       Laboratoire de Physique des Solides, Bat. 510
    #       Universite Paris-Sud, F91405 ORSAY Cedex
    #       Phone : (33-1) 69 41 53 68
    #
    #
    """

    z1 = beamkV*1000.  ;   # eV
    z2 = ene[0];
    z3 = ene[-1]
    z4 = 100.0
    z5 = alpha*0.001   # rad
    z6 = beta*0.001    # rad
    z7 = 500          # number of integration steps to be modified at will

    # main loop on energy loss

    for  zx in range(int(z2),int(z3),int(z4)): #! zx = current energy loss
        eta=0.0;
        x0=float(zx)*(z1+511060.)/(z1*(z1+1022120.));  # x0 = relativistic theta-e
        x1 = np.pi/(2.*x0);
        x2=x0*x0+z5*z5;
        x3=z5/x0*z5/x0;
        x4=0.1*np.sqrt(x2);
        dtheta=(z6-x4)/z7;
    #
    # calculation of the analytical expression
    #
    for zi in range(1, int(z7)):
        theta=x4+dtheta*float(zi);
        x5=theta*theta;
        x6=4.*x5*x0*x0;
        x7=x2-x5;
        x8=np.sqrt(x7*x7+x6);
        x9=(x8+x7)/(2.*x0*x0);
        x10=2.*theta*dtheta*np.log(x9);
        eta=eta+x10;

    eta=eta+x2/100.*np.log(1.+x3)   ;  # addition of the central contribution
    x4=z5*z5*np.log(1.+x1*x1);         # normalisation
    eta=eta/x4;
    #
    #        correction by geometrical factor (beta/alpha)**2
    #
    if (z6<z5):
        x5=z5/z6;
        eta=eta*x5*x5;

    etha2=eta*100.;
    #
    #        calculation of beta *
    #
    x6=np.power((1.+x1*x1),eta);
    x7=x0*np.sqrt(x6-1.);
    y=x7*1000.;

    return y

```

```python
energy_scale = np.linspace(100,500,1000)
alpha=30
beta=33
eff_beta = eels.effective_collection_angle(energy_scale,  alpha=alpha, beta=beta, beam_kv=200000)
print(f'Convergence angle  {alpha:.2f} mrad and collection angle {beta:.2f} mrad \n '
      f'results in effective collection angle of is {eff_beta:.2f} mrad')
```

    Convergence angle  30.00 mrad and collection angle 33.00 mrad
     results in effective collection angle of is 24.05 mrad

### Calculating EELS Cross Section

EELS cross sections are dependent on the momentum transfer (angle dependence), while photons cannot transfer any momentum. The angle dependence of the cross-section is aproximated by the equation 4 in:
[Egerton Ultramicroscopy 50 (1993) 13-28](https://doi.org/10.1016/0304-3991(93)90087-E)

The units of atom density in  electron microscopy is atoms per nm$^2$.
Cross sections are normally given in barns (a neutron scattering joke).

A barn is 10$^{-28}$ m$^2$ which is then 1 barn = 10$^{-10}$ nm$^2$

Therefore, to get a probability per nm$^2$ we have to divide the cross-section in barns by 10$^{10}$

𝐼𝑒𝑑𝑔𝑒(𝛽,Δ𝐸)=𝑁𝐼𝑙𝑜𝑤−𝑙𝑜𝑠𝑠(𝛽,Δ𝐸)𝜎𝑒𝑑𝑔𝑒(𝛽,Δ𝐸)

```python

from scipy.interpolate import splev,splrep,splint
#from scipy.integrate import quad
from scipy.interpolate import interp1d
from scipy.optimize import leastsq

def xsec_xrpa(energy_scale, E0, Z, beta, shift=0 ):
    """
    Calculate momentum-integrated cross-section for EELS from X-ray photoaborption  cross-sections.

    Input:
    ------
    energy_scale: energyscale of spectrum to be analyzed
    E0: acceleration voltage in keV
    Z: atomic number of element
    beta: effective collection angle in mrad
    shift: chemical shift of edge in eV
    """

    beta = beta * 0.001;     #% collection half angle theta [rad]
    #thetamax = self.parent.spec[0].convAngle * 0.001;  #% collection half angle theta [rad]
    dispersion = energy_scale[1]-energy_scale[0]

    Xsections = eels.get_Xsections(Z)
    enexs = Xsections['ene']
    datxs = Xsections['dat']

    #####
    ## Cross Section according to Egerton Ultramicroscopy 50 (1993) 13-28 equation (4)
    #####

    # Relativistic correction factors
    T = 511060.0*(1.0-1.0/(1.0+E0/(511.06))**2)/2.0;
    gamma=1+E0/511.06;
    A = 6.5#e-14 *10**14
    b = beta

    thetaE = enexs/(2*gamma*T)

    G = 2*np.log(gamma)-np.log((b**2+thetaE**2)/(b**2+thetaE**2/gamma**2))-(gamma-1)*b**2/(b**2+thetaE**2/gamma**2)
    datxs = datxs*(A/enexs/T)*(np.log(1+b**2/thetaE**2)+G)/1e8

    datxs = datxs * dispersion # from per eV to per dispersion
    coeff = splrep(enexs,datxs,s=0) # now in areal density atoms / m^2
    xsec = np.zeros(len(energy_scale ))
    #shift = 0# int(ek -onsetXRPS)#/dispersion
    lin = interp1d(enexs,datxs,kind='linear') # Linear instead of spline interpolation to avoid oscillations.
    xsec = lin(energy_scale-shift)

    return xsec
energy_scale = np.arange(100,600)
B_Xsection = eels.xsec_xrpa(energy_scale, 200, 5,  10. )/1e10
N_Xsection = eels.xsec_xrpa(energy_scale, 200, 7,  10. )/1e10       #  xsec  is in barns = 10^28 m2 = 10^10 nm2

```

### Plotting of Cross Sections
please note that spectrum and cross sections are not on the same scale

```python
S_Xsection  = eels.xsec_xrpa(energy_scale, 200, 16, 10. )/1e10
Mo_Xsection = eels.xsec_xrpa(energy_scale, 200, 42, 10. ,shift=0)/1e10       #  xsec  is in barns = 10^28 m2 = 10^10 nm2

fig, ax1 = plt.subplots()

ax1.plot(energy_scale, S_Xsection, label='S X-section' )
ax1.plot(energy_scale, Mo_Xsection, label='Mo X-section' )
ax1.set_xlabel('energy_loss [eV]')
ax1.set_ylabel('probability [atoms/nm$^{2}$]')

plt.legend();
fig.tight_layout();
```

    <IPython.core.display.Javascript object>

## Summary

The cross section is key to determine the chemical composition of an EELS spectrum.
These cross sections are dependent on:
- acceleration voltage
- effective collection angle
- element

So these experimental parameters have to be provided for a calculations of cross sections.

We will use these cross sections in the [chemical compostions notebook](CH4_08-Chemical_Composition.ipynb)

## Navigation
-   **Up Chapter 4: [Imaging](CH4_00-Spectroscopy.ipynb)**
-   **Back: [Dielectric Function](CH4_03-Drude.ipynb)**
-   **Next: [Chemical Composition](CH4_08-Chemical_Composition.ipynb)**
-   **List of Content: [Front](../_MSE672_Intro_TEM.ipynb)**

```python

```


```python
#
```

```python
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import matplotlib.pyplot as plt
from matplotlib import cm
from matplotlib.ticker import LinearLocator, FormatStrFormatter
%matplotlib inline
```

 # Seminario: Optimización con Pyomo

 _Jose Angel Velasco (javelascor@indra.es)_

 _Indra Digital Labs | IA_

 _26 Marzo de 2021_

## Motivación

## Introducción a la optimizacion
* La __optimización__ consiste en la toma de la mejor decisión posible (de un conjunto de soluciones factibles) en base a un objetivo concreto (funcion objetivo) y sujeto a ciertos requisitos (restricciones)

* Mediante __programación matemática__ se puede formular dicha funcion objetivo y restricciones utilizando un lenguaje algebraico

* Alternativa a programación matemática: _métodos heuristicos_

* El soporte a la toma de decisiones mediante optimización con programación matemática es a menudo desconocida o bien descartada por falta de tiempo y/o recursos.

* En este seminario se realizará un breve repaso a conceptos de optimización y se
presentará Pyomo para la resolución de diferentes problemas tipo.

##### Ejemplos

* Problemas Lineales
    * __Problemas del transporte__
    * Problema de la planificación de la producción
    * Problema de la dieta
    * __Problema del flujo en una red__
    * __Problema de la cartera de valores__
    * Problema del despacho economico de centrales eléctricas
* Problemas lineal entero-mixto
    * __Optimización de rutas__
    * Problema de la mochila
    * Problema de Identificación de sintomas relevantes
    * Problema de la academia de ingenieria
    * Problema del horario
    * __Problema de localización de plantas logisticas__
    * __Problema de programación de centrales eletricas (térmica)__
    * Problema de coordinación de centrales electricas (termica e hidrotérmica)

* Problemas no-lineales
    * __Problema de estimación de estado de sensores (state estimation)__
    * Problema del reparto óptimo de carga en redes electricas (Optimal power flow)
    * __Problema de la asignación de trafico__
    * Problemas de regresión (mínimos cuadrados)
    * Aplicaciones de inteligencia artificial (ej: redes neuronales RMSE como objetivo)
    * Control óptimo para el alcance de un blanco

### Programacion matematica
* La programación matemática tiene su origen en 1947 cuando George B. Dantzig publicó el algoritmo [Simplex](https://en.wikipedia.org/wiki/Simplex_algorithm) (Programación lineal)

* En la páctica, un problema de optimización consiste minimizar (o maximizar) una función objetivo __convexa__ y real $f(x)$ eligiendo sistemáticamente valores de entrada (tomados de un conjunto permitido) y calculando el valor de la función sujeto a ciertas restricciones de igualdad y desigualdad.

\begin{array}{ll}
\min & f(x) = c^{T} x\\
s.t. & g(x) = b\\
     & h(x) \leq c\\
     & x \geq 0
\end{array}

   * $x$ es la variable de decision

   * $f(x)$ es la funcion objetivo (minimizar costes, retrasos, riesgos, etc...)

   * $g(x)$ y $h(x)$ son restricciones y definen limitaciones, realidades de negocio, etc.

### Forma standard
   * minimizacion
   * restricciones de igualdad ( si las hay, existen tecnicas para convertir a igualdad)
   * $x>0$
   * $b>0$
* Si la función objetivo o las restricciones tienen elementos no lineales ($x^2, sin(x), ...$), el problema es NLP (Non-Linear Programming)

* Si existen variables enteras $x \in (0,1)$ el problema es MIP (Mixed-Integer Programming)

* Una solución $\tilde{x} = (x_1 , x_2 , \ldots , x_n )$ que satisface todas las restricciones  se denomina _solucion factible_. El conjunto de todas esas soluciones es la _region de factibilidad_
* Un punto factible $\tilde{x}^{*}$ tal que $f(\tilde{x}^{*}) \leq f(\tilde{x})$ para cualquier otro punto factible $\tilde{x}$ se denomina una _solucion optima_ del problema.

### Dualidad
Es una relacion simetrica, esto es, si el problema D es
el dual del problema P, entonces P es el dual de D.

Dado el problema de programacion lineal denominado problema _primal_,

\begin{array}{ll}
\min & f(x) = c^{T} x\\
s.t. & Ax \ge  b\\
     & x \geq 0
\end{array}

su problema _dual_ es maximizar:

\begin{array}{ll}
\max & f^{'}(y) = b^{T} y\\
s.t. & A^{T} y \leq  c\\
     & y \geq 0
\end{array}

$y$ es __variable dual__

Bajo ciertas hipotesis, los problemas _primal_ y _dual_ dan lugar al mismo valor optimo de la funcion objetivo, y por tanto se puede resolver indirectamente el problema primal resolviendo el problema dual.

### Programacion Lineal

* Funcion objetivo y restricciones lineales
* Algoritmo:
    * simplex
    * Punto interior

* Ejemplo:

\begin{array}{ll}
\min & 2x + 3y \\
s.t. & 3x + 4y \geq 1\\
     & x,y \geq 0
\end{array}

```python
x = np.linspace(0, 1)
y = np.linspace(0, 1)
xx, yy = np.meshgrid(x, y)

plt.figure(figsize=(8,6))
plt.contour(xx, yy, 2 * xx + 3 * yy, colors='k')
plt.contourf(xx, yy, 3 * xx + 4 * yy, cmap=plt.cm.viridis)
plt.colorbar()
plt.xlabel('x', fontsize=18)
plt.ylabel('y',fontsize=18)
plt.show()
```

```python
#we will use numpy functions in order to work with numpy arrays
def function(x,y):
    z  = 2 * x + 3 * y
    #z  = 3 * x + 4 * y
    return z

n = 200
x = np.random.uniform(0, 1, n)
y = np.random.uniform(0, 1, n)
z = function(x, y)

#we create two 1D arrays of the desired lengths:
x_1d = np.linspace(min(x), max(x), n)
y_1d = np.linspace(min(y), max(y), n)

#And we use the meshgrid function to create the X and Y matrices!
X, Y = np.meshgrid(x_1d, y_1d)

#Using Numpy arrays, calculating the function value at the points is easy!
Z = function(X,Y)

fig = plt.figure(figsize=(16,10))
ax = fig.add_subplot(111, projection='3d')

surf = ax.plot_surface(X, Y, Z,
                       rstride=2,
                       cstride=2,
                       alpha=0.6,
                       cmap=plt.cm.viridis, #cm.jet, # coolwarm
                       linewidth=0, antialiased=False)
# Customize the z axis.
#ax.set_zlim(-120, 120)
ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))

# add contour
cset = ax.contour(X, Y, Z, zdir='y', cmap=cm.coolwarm)

ax.set_xlabel('X')
#ax.set_xlim(-8, 8)
ax.set_ylabel('Y')
#ax.set_ylim(-8, 8)
ax.set_zlabel('Z')
#ax.set_zlim(-2, 2)

# Add a color bar which maps values to colors.
fig.colorbar(surf, shrink=0.4, aspect=5)
plt.show()
```

### Programación Lineal Entera-Mixta (MILP)

* Funcion objetivo y restricciones lineales
* Incluye toma de decisiones logicas con variables binarias
* Algoritmos:
    * Ramificación y acotamiento (branch & bound)
    * Cortes de gomory

* Ejemplo: Seleción de la mejor ruta de producción

\begin{array}{ll}
\min & c^T + d^Ty \\
s.t. & Ax + By \leq 1\\
     & x \geq 0  \\
     & y \in (0,1)
\end{array}

### Programación No-Lineal (NLP)

* Funcion objetivo y/o restricciones no-lineales
* Algoritmos:
    * Condiciones de optimalidad de Karush-Kuhn-Tucker

* Ejemplo: Flujo de carga óptimo en red de energía eléctrica

\begin{array}{ll}
\min & z = f(x) \\
s.t. & h(x)=0\\
     & g(x)\le 0  \\
     & x \le 0
\end{array}

* Ejemplo de funciones no lineales: Función de Rosenbrock

$$ f(x, y) = (a - x)^2 + b (y - x^2)^2 $$

$$a=1$$
$$b=100$$

```python
#we will use numpy functions in order to work with numpy arrays
def rosembrock(x,y):
    z  = (1-x)**2+100*(y-x**2)**2
    return z

n = 150
x = np.random.uniform(-1, 1, n)
y = np.random.uniform(-1, 1, n)
z = rosembrock(x, y)

#we create two 1D arrays of the desired lengths:
x_1d = np.linspace(min(x), max(x), n)
y_1d = np.linspace(min(y), max(y), n)

#And we use the meshgrid function to create the X and Y matrices!
X, Y = np.meshgrid(x_1d, y_1d)

#Using Numpy arrays, calculating the function value at the points is easy!
Z = rosembrock(X,Y)

fig = plt.figure(figsize=(16,10))
ax = fig.add_subplot(111, projection='3d')

surf = ax.plot_surface(X, Y, Z,
                       rstride=2,
                       cstride=2,
                       alpha=0.6,
                       cmap=cm.jet,
                       linewidth=0,
                       antialiased=False)
# Customize the z axis.
#ax.set_zlim(-120, 120)
ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))

# add contour
cset = ax.contour(X, Y, Z, zdir='y', cmap=cm.coolwarm)

ax.set_xlabel('X')
#ax.set_xlim(-8, 8)
ax.set_ylabel('Y')
#ax.set_ylim(-8, 8)
ax.set_zlabel('Z')
#ax.set_zlim(-2, 2)

ax.view_init(30, 20)

# Add a color bar which maps values to colors.
fig.colorbar(surf, shrink=0.4, aspect=5)
plt.show()
```

### Programación No Lineal Entero Mixta (MINLP)

* Funcion objetivo y/o restricciones no-lineales
* Incluye toma de decisiones logicas con variables binarias
* Algoritmos:
    * Aproximaciones Exteriores

* Ejemplo: Planificación de la expansión de la red eléctrica

\begin{array}{ll}
\min & z = f(x) + d^T y \\
s.t. & h(x) + By=0\\
     & g(x) + Dy \le 0  \\
     & x \le 0 \\
     & y \in (0,1)
\end{array}

# Pyomo

* Dos de los lenguajes de modelado comerciales más utilizados para resolver este tipo de problemas matemáticos son [GAMS](https://www.gams.com) y [AMPL](https://ampl.com).

* Como alternativa libre y gratuita existe [Pyomo](pyomo.org) que es un lenguaje de modelado algebraico basado en __Python__

* Otras alternativas: [JuMP (julia)](https://jump.dev), __SciPy__, __CVXopt__, __PuLP__

* Pyomo es un un lenguaje de modelado algebraico compatible con una amplia gama de tipos de problemas de optimización tipo:

    * [LP](https://www.analyticsvidhya.com/blog/2017/02/lintroductory-guide-on-linear-programming-explained-in-simple-english/) (Linear Prograaming)
    * [NLP](https://en.wikipedia.org/wiki/Nonlinear_programming) (Non-Linear Programming)
    * [MIP](https://en.wikipedia.org/wiki/Integer_programming) (Mixed-Integer Programming)
    * [MINLP](https://neos-guide.org/content/mixed-integer-nonlinear-programming) (Mixed-Integer Non-Linear Programming)

* Deseable optimización convexa

* Pyomo se comunica con los principales solvers comerciales, gratuitos y/o libres:

    * [GLPK (GNU Linear Programming Kit)](https://www.gnu.org/software/glpk/) (libre, LP/MIP)
    * [IPOPT](https://coin-or.github.io/Ipopt/) (libre, LP/NLP)
    * [CONOPT](http://www.conopt.com) (gratuito, NLP)
    * [CPLEX](https://www.ibm.com/es-es/analytics/cplex-optimizer) (comercial, LP/MIP)
    * [GUROBI](https://www.gurobi.com/products/gurobi-optimizer/) (comercial, LP/MIP)
    * [BARON (Branch-and-Reduce Optimization Navigator)](https://minlp.com/baron) (comercial MIP/MINLP)
    * [CBC (Coin-or branch and cut)](https://projects.coin-or.org/Cbc)(libre, MIP)
    * [MindtPy (Mixed-Integer Nonlinear Decomposition Toolbox in Pyomo)](https://pyomo.readthedocs.io/en/stable/contributed_packages/mindtpy.html) (libre, MINLP)

| Solver        | LP   | MIP | NLP   |MINLP  |
| :-----------: |:----:| :--:|:-----:|:-----:|
| GLPK          | X    | X   |   -    |   -    |
| IPOPT         |    -  | -    | X     |   -    |
| CONOPT        | -     | -    | X     |   -    |
| CBC           | -     | X    | -     |   -    |
| BARON         | -     | X    | X     |   X    |
| GUROBI        | X    | X   |    -   |   -    |
| CPLEX         | X    | X   |   -    |   -    |
| MindtPy       |    -  | -    |   -    | X     |

## Instalación
Crear entorno de python con version 3.6 o 3.7 con una ventana de comandos

```
conda create -n pyomo36 python=3.6 numpy pandas matplotlib plotly scipy jupyter ipython
```
Comprobar entorno

```
conda env list
```

Activar entorno

```
conda activate pyomo36
```

Instalar pyomo

```
conda install pyomo -c conda-forge
```

Comprobar que se ha instalado correctamente

```
pyomo
```

Instalar extras: [NEOS server](https://neos-server.org/neos/)

```
conda install pyomo.extras --channel conda-forge
```

Instalar solver GLPK

```
conda install glpk -c conda-forge
```

Instalar solvers IPOPT

```
conda install -c cachemeorg ipopt_bin
```

Detectar solvers instalados

```
pyomo help -s
```

Ayuda sobre componentes
```
pyomo help --components
```

Ayuda sobre pyomo en general

```
pyomo help -a
```

## Modelado
* El modelado es un proceso fundamental en muchos aspectos de la investigación, ingeniería y los negocios. El modelado involucra la formulación de una representación simplificada de una sistema o de un objecto del mundo real.

* Un modelo matemático incluye los siquientes componentes:
    * __Variables__: Representan incognitas dentro del modelo y el objetivo es determinar su valor. Los valores que toman las variables corresponden con la solución del problema y proporcionan la decisión a tomar óptima ya que son el resultado de un proceso de optimización.
    * __Parámetros__: Representan los datos conocidos sobre el problema de optimización. Pueden ser vectores o matrices.
    * __Función objectivo__: Ecuación a minimizar o maximizar que relaciona las variables con los parámetros del modelo.
    * __Restricciones__: Ecuaciones de igualdad o desigualdad que imponen ciertos comportamientos a las variables.

## Proceso
* Pyomo soporta un diseño orientado a objectos para la modelización. Los pasos a seguir son los siguientes

    * Definir modelo y componentes
    * Instanciar modelo
    * Aplicar solver
    * Consultar resultados

## Componentes
Un modelo de pyomo consiste en un conjunto de componentes de lenguaje algebraico definidos a traves de clases de pyomo:
   * Indices
   * conjuntos
   * variables de decisión
   * parámetros
   * función objetivo
   * restricciones.
   * expresiones

## Tipos de modelos

### Concreto
   * Paramteros del modelo directos

\begin{array}{ll}
\min & z= 2x_1 + 3x_2\\
s.t. & 3x_1 + 4x_2 \geq 1\\
     & x_1,x_2 \geq 0
\end{array}

### Abstracto
   * Parámetros del modelo
   * Independecia modelo y datos

\begin{array}{lll}
\min & z=  \sum_{j=1}^n c_j x_j & \\
s.t. & \sum_{j=1}^n a_{i,j} \cdot x_j \geq b_i & \forall i = 1 \ldots m\\
     & x_j \geq 0 & \forall j = 1 \ldots n
\end{array}

## Ejemplo Modelo Abstracto: Problema del transporte

* Original problem formulation:
  - Dantzig, G B, Chapter 3.3. In Linear Programming and Extensions. Princeton University Press, Princeton, New Jersey, 1963.

\begin{equation}
\begin{array}{lll}
\min & z =  \sum_{j=1}^n c_{i,j} x_{i,j}  & (1a) & \\
s.t. & \sum_{j=1}^n x_{i,j} \leq u_i & ,\forall i \in (1, \ldots, m) & (1b) \\
     & \sum_{i=1}^m x_{i,j} = d_j & ,\forall i \in (1, \ldots, n)& (1c)  \\
     & x_{i,j} \geq 0 & ,\forall j \in (1 ,\ldots, n) & (1d)
\end{array}
\end{equation}

__Datos__
* $m$ Numero de origenes
* $n$ Numero de destinos
* $u_i$ Stock de producto en origen $i$
* $d_j$ Demanda de producto en destino $j$
* $c_{i,j}$ coste unitario de envio de producto desde origen $i$ a destino $j$ (€/ud)

__Variable objetivo (o de decisión)__
* $x_{i,j}$ Cantidad de producto que va del origen $i$ al destino $j$

__Restricciónes__
* __1b__: Limitación stock

* __1c__: garantizar suministro demanda

* __1d__: Cantidades positivas. _Esto implica que la direccion de envıo del producto esta prefijada desde los distintos orıgenes hasta los destinos. No obstante, otras hipotesis podrıan tenerse en cuenta. Por ejemplo, podrıa no limitarse el signo de las variables , si no se quiere predeterminar cuales son los puntos de partida y llegada_

### importar libreria pyomo

```python
import pyomo.environ as pyo
```

### Iniciar modelo abstracto

```python
model = pyo.AbstractModel()
```

### Definir conjuntos (Sets) [opcion a]

```python
model.origen = pyo.Set() # origenes i in (1,...,m)
model.destino = pyo.Set() # destinos j in (1,...,n)
```

### Definir conjuntos (Sets) [opcion b]

```python
# Crear primero un parametro que marca la dimension del indice
model.m = pyo.Param(within='NonNegativeIntegers') # 'PositiveReals' Si incluimos el 0
model.n = pyo.Param(within='NonNegativeIntegers')

# Definir luego los sets como un rango de valores entre 1 y la dimension del indice
model.I = pyo.RangeSet(1, model.m)
model.J = pyo.RangeSet(1, model.n)
```

### Definir parametros (datos conocidos)

```python
model.stock = pyo.Param(model.origen) # stock en origen i
model.demanda = pyo.Param(model.destino) # demanda en destino j
model.coste = pyo.Param(model.origen, model.destino) # coste de envio unitario de origen i a destino J
```

### definir variable de decision

```python
model.x = pyo.Var(model.origen, model.destino, domain=pyo.NonNegativeReals)
```

### Definir funcion objetivo

```python
### primero definir expresion que define la funcion objetivo
def pyomo_of(model):
    return sum(model.c[i,j] * model.x[i,j] for i in model.origen for j in model.destino)

### despues definir el component de la funcion objetivo
model.total_cost = pyo.Objective(rule=pyomo_of)
```

### Definir restricciones

```python
# Definir restriccion stock
def pyomo_constraint_stock(model, i):
    # devuelve la expression de la restriccion para el valor de i
    return sum(model.x[i,j] for j in model.destino) <= model.stock[i]

# definir entidad de la restriccion para indice I
model.const_stock = pyo.Constraint(model.origen, rule=pyomo_constraint_stock)
```

```python
# Definir restriccion demanda
def pyomo_constraint_demand(model, j):
    # devuelve la expression de la restriccion para el valor de i
    return sum(model.x[i,j] for i in model.origen) == model.demanda[j]

# definir entidad de la restriccion para indice I
model.const_demand = pyo.Constraint(model.destino, rule=pyomo_constraint_demand)
```

### Definir funcion que genera el modelo

```python
def dl_ia_opt_build_model():
    """ Builds an abstract pyomo model for the transport problem based
    :return: abstract pyomo model
    """

    import pyomo.environ as pyo

    model = pyo.AbstractModel()

    # Sets
    model.origen = pyo.Set() #  origenes (1,...,m)
    model.destino = pyo.Set() # destinos (1,...,n)

    # Parametros
    model.stock = pyo.Param(model.origen, domain=pyo.NonNegativeReals) # stock en origen i
    model.demanda = pyo.Param(model.destino, domain=pyo.NonNegativeReals) # demanda en destino j
    model.coste = pyo.Param(model.origen, model.destino, domain=pyo.NonNegativeReals) # coste de envio unitario de origen i a destino J

    # variable objetivo
    model.x = pyo.Var(model.origen, model.destino, domain=pyo.NonNegativeReals)

    ### primero definir expresion que define la funcion objetivo
    def of_expression(model):
        return sum(model.coste[i,j] * model.x[i,j] for i in model.origen for j in model.destino)

    ### despues definir el component de la funcion objetivo
    model.total_cost = pyo.Objective(rule=of_expression)

    # Definir restriccion stock
    def pyomo_constraint_stock(model, i):
        # devuelve la expression de la restriccion para el valor de i
        return sum(model.x[i,j] for j in model.destino) <= model.stock[i]

    # definir entidad de la restriccion para indice I
    model.const_stock = pyo.Constraint(model.origen, rule=pyomo_constraint_stock)

    # Definir restriccion demanda
    def pyomo_constraint_demand(model, j):
        # devuelve la expression de la restriccion para el valor de i
        return sum(model.x[i,j] for i in model.origen) == model.demanda[j]

    # definir entidad de la restriccion para indice I
    model.const_demand = pyo.Constraint(model.destino, rule=pyomo_constraint_demand)

    return model
```

```python
model = dl_ia_opt_build_model()
```

### Datos

```python
df_data = pd.DataFrame({ 'origen':['Barcelona'   , 'Barcelona'   , 'Barcelona'   ,'Valencia'   ,'Valencia'   ,'Valencia'   ,'Bilbao'    ,'Bilbao'    ,'Bilbao'   ],
                         'destino':['Madrid' ,'Sevilla' ,'Malaga'  ,'Madrid' ,'Sevilla'  ,'Malaga' ,'Madrid' ,'Sevilla'  ,'Malaga'  ],
                         'coste':[0.682,0.995 ,0.976 ,0.562, 0.756, 0.720, 0.402 ,0.861 ,0.922],
                         'stock':[174  ,174   ,174   ,36  ,36  ,36  ,57   ,57   ,57  ] ,
                         'demanda':[54  ,45   ,23   ,54  ,45  ,23  ,54   ,45   ,23  ],
                   })

df_data
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>origen</th>
      <th>destino</th>
      <th>coste</th>
      <th>stock</th>
      <th>demanda</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>Barcelona</td>
      <td>Madrid</td>
      <td>0.682</td>
      <td>174</td>
      <td>54</td>
    </tr>
    <tr>
      <th>1</th>
      <td>Barcelona</td>
      <td>Sevilla</td>
      <td>0.995</td>
      <td>174</td>
      <td>45</td>
    </tr>
    <tr>
      <th>2</th>
      <td>Barcelona</td>
      <td>Malaga</td>
      <td>0.976</td>
      <td>174</td>
      <td>23</td>
    </tr>
    <tr>
      <th>3</th>
      <td>Valencia</td>
      <td>Madrid</td>
      <td>0.562</td>
      <td>36</td>
      <td>54</td>
    </tr>
    <tr>
      <th>4</th>
      <td>Valencia</td>
      <td>Sevilla</td>
      <td>0.756</td>
      <td>36</td>
      <td>45</td>
    </tr>
    <tr>
      <th>5</th>
      <td>Valencia</td>
      <td>Malaga</td>
      <td>0.720</td>
      <td>36</td>
      <td>23</td>
    </tr>
    <tr>
      <th>6</th>
      <td>Bilbao</td>
      <td>Madrid</td>
      <td>0.402</td>
      <td>57</td>
      <td>54</td>
    </tr>
    <tr>
      <th>7</th>
      <td>Bilbao</td>
      <td>Sevilla</td>
      <td>0.861</td>
      <td>57</td>
      <td>45</td>
    </tr>
    <tr>
      <th>8</th>
      <td>Bilbao</td>
      <td>Malaga</td>
      <td>0.922</td>
      <td>57</td>
      <td>23</td>
    </tr>
  </tbody>
</table>
</div>

#### Visualizar datos entrada

##### Stock y demanda

```python
# Plot stocks
df_aux = df_data[['origen','stock']].drop_duplicates()

fig = px.bar(df_aux,
             x="origen",
             y="stock",
             title="Stocks")
fig.update_yaxes(title_text='Stocks (ud)')
fig.update_xaxes(title_text='Origenes')
fig.update_layout(font=dict(family="Courier New, monospace", size=18, color="#7f7f7f"))
fig.show()

# Plot demanda
df_aux = df_data[['destino','demanda']].drop_duplicates()

fig = px.bar(df_aux,
             x="destino",
             y="demanda",
             title="Demanda")
fig.update_yaxes(title_text='Demanda (ud)')
fig.update_xaxes(title_text='Destinos')
fig.update_layout(font=dict(family="Courier New, monospace", size=18, color="#7f7f7f"))
fig.show()
```

<div>                            <div id="b27a33cb-d9f2-4263-bf0e-11112e267fcc" class="plotly-graph-div" style="height:525px; width:100%;"></div>                    </div>

<div>                            <div id="8fe8d9d1-8bf9-4491-ba73-021aaacb39c9" class="plotly-graph-div" style="height:525px; width:100%;"></div>                    </div>

##### Costes

```python
df_aux = df_data[['origen','destino','coste']].drop_duplicates().pivot(index='destino',values='coste', columns='origen').fillna(0)

fig = go.Figure(data=go.Heatmap(
                   z=df_aux.values,
                   y=df_data['origen'].unique().tolist(),
                   x=df_data['destino'].unique().tolist(),
                   hoverongaps = False))
fig.update_yaxes(title_text='origen')
fig.update_xaxes(title_text='destino')
fig.update_layout(title='Costes de envio')
fig.update_layout(font=dict(family="Courier New, monospace", size=18, color="#7f7f7f"))
fig.show()
```

<div>                            <div id="fe746e74-18c0-47c5-9a64-31a772552333" class="plotly-graph-div" style="height:525px; width:100%;"></div>                    </div>

### Guardar datos en formato adecuado

```python
df_data[['origen', 'stock']].drop_duplicates()
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>origen</th>
      <th>stock</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>Barcelona</td>
      <td>174</td>
    </tr>
    <tr>
      <th>3</th>
      <td>Valencia</td>
      <td>36</td>
    </tr>
    <tr>
      <th>6</th>
      <td>Bilbao</td>
      <td>57</td>
    </tr>
  </tbody>
</table>
</div>

```python
df_data[['origen', 'stock']].drop_duplicates()['origen'].to_csv('data/set_origen.tab', index=False, sep=' ')
df_data[['destino', 'demanda']].drop_duplicates()['destino'].to_csv('data/set_destino.tab', index=False, sep=' ')

df_data[['origen', 'stock']].drop_duplicates().to_csv('data/param_stock.tab', index=False, sep=' ')
df_data[['destino', 'demanda']].drop_duplicates().to_csv('data/param_demand.tab', index=False, sep=' ')

df_data[['origen', 'destino', 'coste']].to_csv('data/param_coste.tab', index=False, sep=' ')
```

### Crear instancia de datos

```python
data = pyo.DataPortal(model=model)

data.load(filename='data/set_origen.tab', set="origen", format="set")
data.load(filename='data/set_destino.tab', set="destino", format="set")

data.load(filename='data/param_stock.tab', param=model.stock)
data.load(filename='data/param_demand.tab', param=model.demanda)

data.load(filename='data/param_coste.tab', param=model.coste)
```

```python
data.data()
```

    {'origen': {None: ['Barcelona', 'Valencia', 'Bilbao']},
     'destino': {None: ['Madrid', 'Sevilla', 'Malaga']},
     'stock': {'Barcelona': 174, 'Valencia': 36, 'Bilbao': 57},
     'demanda': {'Madrid': 54, 'Sevilla': 45, 'Malaga': 23},
     'coste': {('Barcelona', 'Madrid'): 0.682,
      ('Barcelona', 'Sevilla'): 0.995,
      ('Barcelona', 'Malaga'): 0.976,
      ('Valencia', 'Madrid'): 0.562,
      ('Valencia', 'Sevilla'): 0.756,
      ('Valencia', 'Malaga'): 0.72,
      ('Bilbao', 'Madrid'): 0.402,
      ('Bilbao', 'Sevilla'): 0.861,
      ('Bilbao', 'Malaga'): 0.922}}

### Instanciar modelo con datos

```python
instance = model.create_instance(data)

### comprobar instancia creada
instance.pprint()
```

    4 Set Declarations
        coste_index : Size=1, Index=None, Ordered=True
            Key  : Dimen : Domain         : Size : Members
            None :     2 : origen*destino :    9 : {('Barcelona', 'Madrid'), ('Barcelona', 'Sevilla'), ('Barcelona', 'Malaga'), ('Valencia', 'Madrid'), ('Valencia', 'Sevilla'), ('Valencia', 'Malaga'), ('Bilbao', 'Madrid'), ('Bilbao', 'Sevilla'), ('Bilbao', 'Malaga')}
        destino : Size=1, Index=None, Ordered=Insertion
            Key  : Dimen : Domain : Size : Members
            None :     1 :    Any :    3 : {'Madrid', 'Sevilla', 'Malaga'}
        origen : Size=1, Index=None, Ordered=Insertion
            Key  : Dimen : Domain : Size : Members
            None :     1 :    Any :    3 : {'Barcelona', 'Valencia', 'Bilbao'}
        x_index : Size=1, Index=None, Ordered=True
            Key  : Dimen : Domain         : Size : Members
            None :     2 : origen*destino :    9 : {('Barcelona', 'Madrid'), ('Barcelona', 'Sevilla'), ('Barcelona', 'Malaga'), ('Valencia', 'Madrid'), ('Valencia', 'Sevilla'), ('Valencia', 'Malaga'), ('Bilbao', 'Madrid'), ('Bilbao', 'Sevilla'), ('Bilbao', 'Malaga')}

    3 Param Declarations
        coste : Size=9, Index=coste_index, Domain=NonNegativeReals, Default=None, Mutable=False
            Key                      : Value
             ('Barcelona', 'Madrid') : 0.682
             ('Barcelona', 'Malaga') : 0.976
            ('Barcelona', 'Sevilla') : 0.995
                ('Bilbao', 'Madrid') : 0.402
                ('Bilbao', 'Malaga') : 0.922
               ('Bilbao', 'Sevilla') : 0.861
              ('Valencia', 'Madrid') : 0.562
              ('Valencia', 'Malaga') :  0.72
             ('Valencia', 'Sevilla') : 0.756
        demanda : Size=3, Index=destino, Domain=NonNegativeReals, Default=None, Mutable=False
            Key     : Value
             Madrid :    54
             Malaga :    23
            Sevilla :    45
        stock : Size=3, Index=origen, Domain=NonNegativeReals, Default=None, Mutable=False
            Key       : Value
            Barcelona :   174
               Bilbao :    57
             Valencia :    36

    1 Var Declarations
        x : Size=9, Index=x_index
            Key                      : Lower : Value : Upper : Fixed : Stale : Domain
             ('Barcelona', 'Madrid') :     0 :  None :  None : False :  True : NonNegativeReals
             ('Barcelona', 'Malaga') :     0 :  None :  None : False :  True : NonNegativeReals
            ('Barcelona', 'Sevilla') :     0 :  None :  None : False :  True : NonNegativeReals
                ('Bilbao', 'Madrid') :     0 :  None :  None : False :  True : NonNegativeReals
                ('Bilbao', 'Malaga') :     0 :  None :  None : False :  True : NonNegativeReals
               ('Bilbao', 'Sevilla') :     0 :  None :  None : False :  True : NonNegativeReals
              ('Valencia', 'Madrid') :     0 :  None :  None : False :  True : NonNegativeReals
              ('Valencia', 'Malaga') :     0 :  None :  None : False :  True : NonNegativeReals
             ('Valencia', 'Sevilla') :     0 :  None :  None : False :  True : NonNegativeReals

    1 Objective Declarations
        total_cost : Size=1, Index=None, Active=True
            Key  : Active : Sense    : Expression
            None :   True : minimize : 0.682*x[Barcelona,Madrid] + 0.995*x[Barcelona,Sevilla] + 0.976*x[Barcelona,Malaga] + 0.562*x[Valencia,Madrid] + 0.756*x[Valencia,Sevilla] + 0.72*x[Valencia,Malaga] + 0.402*x[Bilbao,Madrid] + 0.861*x[Bilbao,Sevilla] + 0.922*x[Bilbao,Malaga]

    2 Constraint Declarations
        const_demand : Size=3, Index=destino, Active=True
            Key     : Lower : Body                                                           : Upper : Active
             Madrid :  54.0 :    x[Barcelona,Madrid] + x[Valencia,Madrid] + x[Bilbao,Madrid] :  54.0 :   True
             Malaga :  23.0 :    x[Barcelona,Malaga] + x[Valencia,Malaga] + x[Bilbao,Malaga] :  23.0 :   True
            Sevilla :  45.0 : x[Barcelona,Sevilla] + x[Valencia,Sevilla] + x[Bilbao,Sevilla] :  45.0 :   True
        const_stock : Size=3, Index=origen, Active=True
            Key       : Lower : Body                                                             : Upper : Active
            Barcelona :  -Inf : x[Barcelona,Madrid] + x[Barcelona,Sevilla] + x[Barcelona,Malaga] : 174.0 :   True
               Bilbao :  -Inf :          x[Bilbao,Madrid] + x[Bilbao,Sevilla] + x[Bilbao,Malaga] :  57.0 :   True
             Valencia :  -Inf :    x[Valencia,Madrid] + x[Valencia,Sevilla] + x[Valencia,Malaga] :  36.0 :   True

    11 Declarations: origen destino stock demanda coste_index coste x_index x total_cost const_stock const_demand

### Definir Solver

```python
# Definimos Solver
from pyomo.opt import SolverFactory
opt = SolverFactory("glpk")
```

### Resolver problema de optimización
Al resolver el modelo se dará una de las siguientes situaciones:
*  __El problema es no factible__: No existe ninguna solución que cumpla simultáneamente todas las restricciones
*  __El problema es no acotado__: Funcion no convexa (z=infinito)
* __Tiene una única solución__: El problema tiene solución factible, es acotado y existe un óptimo y es único
*  __Tiene múltiples soluciones__: El problema tiene solución factible, es acotado y existen varios óptimos. Todos los óptimos serán equivalentes, esto es, proporcionan el mismo valor de z

```python
results = opt.solve(instance, tee=False) # tee is a option to plot process

#send results to stdout
#results.write()
#!cat results.yml

print('--- Problem Results ---')
print(results.Problem())
print('--- Solver Results ---')
print(results.Solver())

print('--- Restriccion Stock ---')
print(instance.const_stock.display())
print('--- Restriccion Demanda ---')
print(instance.const_demand.display())
print('--- Coste Total ---')
print(instance.total_cost.display())
```

    --- Problem Results ---

    Name: unknown
    Lower bound: 79.534
    Upper bound: 79.534
    Number of objectives: 1
    Number of constraints: 7
    Number of variables: 10
    Number of nonzeros: 19
    Sense: minimize

    --- Solver Results ---

    Status: ok
    Termination condition: optimal
    Statistics:
      Branch and bound:
        Number of bounded subproblems: 0
        Number of created subproblems: 0
    Error rc: 0
    Time: 0.022034168243408203

    --- Restriccion Stock ---
    const_stock : Size=3
        Key       : Lower : Body : Upper
        Barcelona :  None : 29.0 : 174.0
           Bilbao :  None : 57.0 :  57.0
         Valencia :  None : 36.0 :  36.0
    None
    --- Restriccion Demanda ---
    const_demand : Size=3
        Key     : Lower : Body : Upper
         Madrid :  54.0 : 54.0 :  54.0
         Malaga :  23.0 : 23.0 :  23.0
        Sevilla :  45.0 : 45.0 :  45.0
    None
    --- Coste Total ---
    total_cost : Size=1, Index=None, Active=True
        Key  : Active : Value
        None :   True : 79.53399999999999
    None

### Explorar Resultados

```python
def dl_ia_opt_get_results(instance):
    """ Funtion to check resutls
        :param results: optimization resutls
        :return: dataframe with resutls
    """

    import pandas as pd
    try:
        origen = []
        destino = []
        cantidad = []
        for i in instance.origen:
            for j in instance.destino:
                origen.append(i)
                destino.append(j)
                cantidad.append(instance.x[(i, j)].value)

        df_out = pd.DataFrame({'origen':origen, 'destino':destino, 'cantidad':cantidad})
        df_out['origen'] = df_out['origen'].astype(str)
        df_out['destino'] = df_out['destino'].astype(str)
        df_out['cantidad'] = df_out['cantidad'].astype(int)

        return df_out

    except Exception as exception_msg:
        print('(!) Error in dl_ia_opt_get_results: {}'.format(str(exception_msg)))
```

```python
df_results = dl_ia_opt_get_results(instance)
print('Coste total: {} euros'.format(instance.total_cost.expr()))
print('Tiempo de ejecución: {} s'.format(round(results.Solver.Time,4)))
print('Suministro: {} lotes'.format(df_results['cantidad'].sum()))
df_results
```

    Coste total: 79.53399999999999 euros
    Tiempo de ejecución: 0.022 s
    Suministro: 122 lotes

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>origen</th>
      <th>destino</th>
      <th>cantidad</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>Barcelona</td>
      <td>Madrid</td>
      <td>0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>Barcelona</td>
      <td>Sevilla</td>
      <td>29</td>
    </tr>
    <tr>
      <th>2</th>
      <td>Barcelona</td>
      <td>Malaga</td>
      <td>0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>Valencia</td>
      <td>Madrid</td>
      <td>0</td>
    </tr>
    <tr>
      <th>4</th>
      <td>Valencia</td>
      <td>Sevilla</td>
      <td>13</td>
    </tr>
    <tr>
      <th>5</th>
      <td>Valencia</td>
      <td>Malaga</td>
      <td>23</td>
    </tr>
    <tr>
      <th>6</th>
      <td>Bilbao</td>
      <td>Madrid</td>
      <td>54</td>
    </tr>
    <tr>
      <th>7</th>
      <td>Bilbao</td>
      <td>Sevilla</td>
      <td>3</td>
    </tr>
    <tr>
      <th>8</th>
      <td>Bilbao</td>
      <td>Malaga</td>
      <td>0</td>
    </tr>
  </tbody>
</table>
</div>

```python
df_aux = df_results[['origen','destino','cantidad']].drop_duplicates().pivot(index='destino',values='cantidad', columns='origen').fillna(0)

fig = go.Figure(data=go.Heatmap(
                   z=df_aux.values,
                   y=df_results['origen'].unique().tolist(),
                   x=df_results['destino'].unique().tolist(),
                   hoverongaps = False))
fig.update_yaxes(title_text='origen')
fig.update_xaxes(title_text='destino')
fig.update_layout(title='Cantidad de producto')
fig.update_layout(font=dict(family="Courier New, monospace", size=18, color="#7f7f7f"))
fig.show()
```

<div>

            <div id="991ffe09-bcdd-4c7f-ab04-bf67ce2c3cb5" class="plotly-graph-div" style="height:525px; width:100%;"></div>

        </div>

### Mejoras

   * __¿Y si existe stock minimo?__
       * _Restriccion stock minimo_
   * __¿Y si hay mas de un producto?__
       * _costes por origen, destino y producto_
   * __¿Y si hay diferentes modos de transporte?__
       * _costes origen, destino, modo_
   * __¿Y si los productos se pueden agrupar?__
       * _Añadir caracterisitcas del producto: peso, volumen, precio, reglas de agrupamiento_
   * __¿Y si no hay suficiente stock?__
       * _Introducir preferencias en destinos_
   * __¿Y si hay productos alternativos?__
       * _Introducir preferencia por original, suministrar alternativo en caso de no stock_
   * __¿Y si es multiperiodo?__
       * _Programación dinámica, introducir demanda y stock por horizonte temporal_
   * __¿Y que pasa si hay nodos intermedios?__
       * _Definir mediante expresiones ecuaciones auxiliares_

# Proximamente

* Optimización multi-objetivo
* Comparativa de tiempos de ejecuión y esfuerzo computacional
* Mas ejemplos con aplicación a sensores, transporte, flujos, cartera de valores, etc..

# Referencias
* [pyomo repository](https://github.com/Pyomo)
* [Pyomo Modelling Components](https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/index.html)
* [Pyomo Documentation](https://pyomo.readthedocs.io/en/latest/)
* [Expressions pyomo](https://pyomo.readthedocs.io/en/stable/pyomo_modeling_components/Expressions.html)
* [Gallery](https://github.com/Pyomo/PyomoGallery/wiki)
* [Accessing Pyomo variable values and objective function value](http://hselab.org/pyomo-get-variable-values.html)
* [latex symbols](https://oeis.org/wiki/List_of_LaTeX_mathematical_symbols)
* [latex math](https://www.overleaf.com/learn/latex/Matrices)
* [Gams solvers](https://www.gams.com/latest/docs/S_MAIN.html)
* [IBM optimization](https://www.ibm.com/analytics/optimization-solver)
* [Solvers comparative](https://www.researchgate.net/publication/314750497_A_Comparative_Analysis_of_Optimization_Solvers)
* [More about optimization solvers](https://cran.r-project.org/web/views/Optimization.html)
* [Book optimization UCLM](http://www.dia.fi.upm.es/~jafernan/teaching/operational-research/LibroCompleto.pdf)
* [Pareto Frontier Method](https://www.researchgate.net/publication/265600679_On_Epsilon-Constraint_Based_Methods_for_the_Generation_of_Pareto_Frontiers)
* [(Udemy) Complete Pyomo Bootcamp: Python Optimization Beginners](https://www.udemy.com/share/103zuUAEAYdlhWTXQB/)

```python

```


```python
#remove cell visibility
from IPython.display import HTML
tag = HTML('''
Toggle cell visibility <a href="javascript:code_toggle()">here</a>.''')
display(tag)
```

Toggle cell visibility here.

```python
%matplotlib inline
import control
import numpy
import sympy as sym
from IPython.display import display, Markdown
import ipywidgets as widgets
import matplotlib.pyplot as plt

#print a matrix latex-like
def bmatrix(a):
     """Returns a LaTeX bmatrix - by Damir Arbula (ICCT project)

     :a: numpy array
     :returns: LaTeX bmatrix as a string
     """
     if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
     lines = str(a).replace('[', '').replace(']', '').splitlines()
     rv = [r'\begin{bmatrix}']
     rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
     rv +=  [r'\end{bmatrix}']
     return '\n'.join(rv)

# Display formatted matrix:
def vmatrix(a):
    if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
    lines = str(a).replace('[', '').replace(']', '').splitlines()
    rv = [r'\begin{vmatrix}']
    rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
    rv +=  [r'\end{vmatrix}']
    return '\n'.join(rv)

#matrixWidget is a matrix looking widget built with a VBox of HBox(es) that returns a numPy array as value !
class matrixWidget(widgets.VBox):
    def updateM(self,change):
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.M_[irow,icol] = self.children[irow].children[icol].value
                #print(self.M_[irow,icol])
        self.value = self.M_

    def dummychangecallback(self,change):
        pass

    def __init__(self,n,m):
        self.n = n
        self.m = m
        self.M_ = numpy.matrix(numpy.zeros((self.n,self.m)))
        self.value = self.M_
        widgets.VBox.__init__(self,
                             children = [
                                 widgets.HBox(children =
                                              [widgets.FloatText(value=0.0, layout=widgets.Layout(width='90px')) for i in range(m)]
                                             )
                                 for j in range(n)
                             ])

        #fill in widgets and tell interact to call updateM each time a children changes value
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
                self.children[irow].children[icol].observe(self.updateM, names='value')
        #value = Unicode('example@example.com', help="The email value.").tag(sync=True)
        self.observe(self.updateM, names='value', type= 'All')

    def setM(self, newM):
        #disable callbacks, change values, and reenable
        self.unobserve(self.updateM, names='value', type= 'All')
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].unobserve(self.updateM, names='value')
        self.M_ = newM
        self.value = self.M_
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].observe(self.updateM, names='value')
        self.observe(self.updateM, names='value', type= 'All')

                #self.children[irow].children[icol].observe(self.updateM, names='value')

#overlaod class for state space systems that DO NOT remove "useless" states (what "professor" of automatic control would do this?)
class sss(control.StateSpace):
    def __init__(self,*args):
        #call base class init constructor
        control.StateSpace.__init__(self,*args)
    #disable function below in base class
    def _remove_useless_states(self):
        pass
```

## Quadrotor longitudinal velocity control

The quadrotor (or quadcopter) longitudinal velocity $v$ may be controlled by tilting the vehicle by the pitch angle. The angle $\theta$ is controlled by applying the torque $T$ using the propellers. The vehicle moment of inertia is $J= 1.3e-2$. When the vehicle is tilted by the angle $\theta$, the propellers produce a forward force approximately equal to $F_v = F\theta = mg\theta$ and the aerodynamic drag is $F_c=-cv=-0.9v$, where $m=2000$ g is vehicle mass, and $g = 9.81$ m/s^2 gravity acceleration. Maximum torque equals $5000$ mNm. Pitch angle $\theta$ must be limited to $\pm30$ degrees during all operations and maximum velocity to 2 m/s. The pitch angle is estimated by an appropriate sensor and velocity is measured with GPS.

The design procedure follows the following two-step procedure:
1. Write the system equations in state space form for the rotational dynamics (torque $T$ to pitch angle $\theta$) and the longitudinal dynamics (pitch angle to forward velocity $v$).
2. Design a regulator for $v$ in order to satisfy the following specifications:
    - Settling time for 5% tolerance band of less than 2.5 seconds.
    - No Overshoot.
    - No steady-state error in response to a step velocity request.

### System equations

The system equations are equal to:

\begin{cases}
    m\dot{v} = F_v + F_c = mg\theta -cv \\
    J\ddot{\theta} = T.
\end{cases}

By defining the state vector as $x = \begin{bmatrix} x_1 & x_2 & x_3 \end{bmatrix}^T = \begin{bmatrix} v & \theta & \dot{\theta} \end{bmatrix}^T$ and the input $u=T$, system equations in state space form become:

\begin{cases}
\dot{x} = \begin{bmatrix} -c/m & g & 0 \\ 0 & 0 & 1 \\ 0 & 0 & 0 \end{bmatrix}x + \begin{bmatrix} 0 \\ 0 \\ 1/J \end{bmatrix}u \\
y = \begin{bmatrix} y_1 \\ y_2 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ 0 & 1 & 0 \end{bmatrix}x
\end{cases}

The dynamics from $u$ to $\theta$ is a double integrator while that from $\theta$ to $v$ is a first order dynamics with a pole in $-c/m$. The system has two outputs: velocity and tilt angle.

The controllability matrix $\mathcal{C}$ is

```python
A = numpy.matrix('-0.45 9.81 0; 0 0 1; 0 0 0')
B = numpy.matrix([[0],[0],[1/1.3E-02]])
C = numpy.matrix('1 0 0; 0 1 0')
D = numpy.matrix('0; 0')

CM = control.ctrb(A,B)
display(Markdown(bmatrix(CM)))
# print(numpy.linalg.matrix_rank(CM))
```

\begin{bmatrix}
  0. & 0. & 754.61538462\\
  0. & 76.92307692 & 0.\\
  76.92307692 & 0. & 0.\\
\end{bmatrix}

and has rank equal to 3 so the system is controllable.
The observability matrix $\mathcal{O}$ is

```python
OM = control.obsv(A,C)
display(Markdown(bmatrix(OM)))
# print(numpy.linalg.matrix_rank(OM))
```

\begin{bmatrix}
  1. & 0. & 0.\\
  0. & 1. & 0.\\
  -0.45 & 9.81 & 0.\\
  0. & 0. & 1.\\
  0.2025 & -4.4145 & 9.81\\
  0. & 0. & 0.\\
\end{bmatrix}

and has rank equal to 3 so the system is observable.

### Regulator design
#### Observer design
Since we have the direct measurements of $x_1$ and $x_2$ we are only interested in estimating $x_3$. If we look at the subsystem $(x_2, \, x_3)$ we note that it is observable, so it is possible to design an observer by considering only this subsystem. The structure of our estimator is therefore:

$$
\begin{bmatrix} \dot{\hat{x}_2} \\ \dot{\hat{x}_3} \end{bmatrix} = \begin{bmatrix} 0 & 1 \\ 0 & 0 \end{bmatrix}\begin{bmatrix} \hat{x}_2 \\ \hat{x}_3 \end{bmatrix} + \begin{bmatrix} 0 \\ 1/J \end{bmatrix}u + \begin{bmatrix} l_1 \\ l_2 \end{bmatrix}\left( y - C\begin{bmatrix} \hat{x}_2 \\ \hat{x}_3 \end{bmatrix} \right) = \begin{bmatrix} -l_1 & 1 \\ -l_2 & 0 \end{bmatrix}\begin{bmatrix} \hat{x}_2 \\ \hat{x}_3 \end{bmatrix} + \begin{bmatrix} 0 \\ 1/J \end{bmatrix}u + \begin{bmatrix} l_1 \\ l_2 \end{bmatrix}y
$$

applying the Laplace transform and solving for $\hat{x}_3(s)$ we arrive at

$$
\hat{x}_3(s) = \frac{l_2s}{s^2+l_1s+l_2}y_2(s) + \frac{s+l_1}{s^2+l_1s+l_2}\frac{u(s)}{J}.
$$

We now have a simple linear estimator for $x_3$ that is asymptotically stable for any $l_1>0$ and $l_2>0$. It is interesting to note that if $l_2\rightarrow \infty$, the estimator transfer function simplifies to $\hat{x}_3(s) = s y_2(s)$ and the result equals $\hat{x}_3 = \dot{\theta}$; it is obtained by differentiating the measured $y_2 = \theta$.

Selecting $l_1 = 20$ and $l_2 = 100$ places both observer eigenvalues in $-10$.

#### Controller design
For the requirement on settling time, the frequency of the poles must be greater than $3/T_S$ for real poles and greater than $3/\zeta Ts$ for complex poles, where $T_s$ is the settling time (5%) and $\zeta$ the damping. Good poles locations, in terms of response and input energy, for a double integrator system, should lay within a range of $\pm 45°$ w.r.t. the real negative axis. By first considering these facts and then proceeding iteratively, dominant poles were chosen in $-2.8\pm1.0i$, whereas the third pole was chosen at much higher frequency: $-15$.
For the requirement of zero steady-state error the reference input is scaled by a gain equal to the inverse of the steady-state gain of the closed-loop system, yielding total closed-loop gain of $1.0$.

### How to use this notebook?
- Verify the requested specifications in case of initial error in the estimate of $x_3$ for both positive and negative error.
- Watch the changed response and, by having a physical system in your mind, try to understand why it changed the way it did.

```python
# Preparatory cell

X0 = 0.0
K = numpy.matrix([8/15,-4.4,-4])
L = numpy.matrix([[66],[107/3]])

Aw = matrixWidget(3,3)
Aw.setM(A)
Bw = matrixWidget(3,1)
Bw.setM(B)
Cw = matrixWidget(1,3)
Cw.setM(C)
X0w = widgets.FloatText(
    value=X0,
    description='',
    disabled=False
)
Kw = matrixWidget(1,3)
Kw.setM(K)
Lw = matrixWidget(2,1)
Lw.setM(L)

eig1c = matrixWidget(1,1)
eig2c = matrixWidget(2,1)
eig3c = matrixWidget(1,1)
eig1c.setM(numpy.matrix([-15.]))
eig2c.setM(numpy.matrix([[-2.8],[-1.0]]))
eig3c.setM(numpy.matrix([-15.]))

eig2o = matrixWidget(2,1)
eig3o = matrixWidget(1,1)
eig2o.setM(numpy.matrix([[-10.],[0.]]))
eig3o.setM(numpy.matrix([-10.]))
```

```python
# Misc

#create dummy widget
DW = widgets.FloatText(layout=widgets.Layout(width='0px', height='0px'))

#create button widget
START = widgets.Button(
    description='Test',
    disabled=False,
    button_style='', # 'success', 'info', 'warning', 'danger' or ''
    tooltip='Test',
    icon='check'
)

def on_start_button_clicked(b):
    #This is a workaround to have intreactive_output call the callback:
    #   force the value of the dummy widget to change
    if DW.value> 0 :
        DW.value = -1
    else:
        DW.value = 1
    pass
START.on_click(on_start_button_clicked)

# Define type of method
selm = widgets.Dropdown(
    options= ['Set K and L', 'Set the eigenvalues'],
    value= 'Set the eigenvalues',
    description='',
    disabled=False
)

# Define the number of complex eigenvalues
sele = widgets.Dropdown(
    options= ['0 complex eigenvalues', '2 complex eigenvalues'],
    value= '2 complex eigenvalues',
    description='Complex eigenvalues:',
    style = {'description_width': 'initial'},
    disabled=False
)

#define type of ipout
selu = widgets.Dropdown(
    options=['impulse', 'step', 'sinusoid', 'square wave'],
    value='step',
    description='Type of reference:',
    style = {'description_width': 'initial'},
    disabled=False
)
# Define the values of the input
u = widgets.FloatSlider(
    value=2,
    min=0,
    max=4,
    step=0.1,
    description='Reference:',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.1f',
)
period = widgets.FloatSlider(
    value=0.5,
    min=0.001,
    max=10,
    step=0.001,
    description='Period: ',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)

gain_w2 = widgets.FloatText(
    value=1.,
    description='',
    disabled=True
)

simTime = widgets.FloatText(
    value=3,
    description='',
    disabled=False
)
```

```python
# Support functions

def eigen_choice(sele):
    if sele == '0 complex eigenvalues':
        eig1c.children[0].children[0].disabled = False
        eig2c.children[1].children[0].disabled = True
        eig2o.children[1].children[0].disabled = True
        eig = 0
    if sele == '2 complex eigenvalues':
        eig1c.children[0].children[0].disabled = True
        eig2c.children[1].children[0].disabled = False
        eig2o.children[1].children[0].disabled = False
        eig = 2
    return eig

def method_choice(selm):
    if selm == 'Set K and L':
        method = 1
        sele.disabled = True
    if selm == 'Set the eigenvalues':
        method = 2
        sele.disabled = False
    return method
```

```python
# Reduced system
Ar = numpy.matrix('0 1; 0 0')
Br = numpy.matrix([[0],[1/1.3E-02]])
Cr = numpy.matrix('1 0')
Dr = numpy.matrix('0')

def main_callback2(Aw, Bw, X0w, K, L, eig1c, eig2c, eig3c, eig2o, eig3o, u, period, selm, sele, selu, simTime, DW):
    eige = eigen_choice(sele)
    method = method_choice(selm)

    if method == 1:
        solc = numpy.linalg.eig(A-B*K)
        solo = numpy.linalg.eig(Ar-L*Cr)
    if method == 2:
        if eige == 0:
            K = control.acker(A, B, [eig1c[0,0], eig2c[0,0], eig3c[0,0]])
            Kw.setM(K)

            L = control.acker(Ar.T, Cr.T, [eig2o[0,0], eig3o[0,0]]).T
            Lw.setM(L)
        if eige == 2:
            K = control.acker(A, B, [eig3c[0,0],
                                     numpy.complex(eig2c[0,0],eig2c[1,0]),
                                     numpy.complex(eig2c[0,0],-eig2c[1,0])])
            Kw.setM(K)

            L = control.acker(Ar.T, Cr.T, [numpy.complex(eig2o[0,0],eig2o[1,0]),
                                           numpy.complex(eig2o[0,0],-eig2o[1,0])]).T
            Lw.setM(L)

    sys = control.ss(A,B,numpy.vstack((C,numpy.zeros((B.shape[1],C.shape[1])))),numpy.vstack((D,numpy.eye(B.shape[1]))))
    sysC = control.ss(numpy.zeros((1,1)),
                      numpy.zeros((1,numpy.shape(A)[0])),
                      numpy.zeros((numpy.shape(B)[1],1)),
                      -K)

    sysE = control.ss(Ar-L*Cr,
                      numpy.hstack((L,Br-L*Dr)),
                      numpy.matrix('0 1'),
                      numpy.zeros((1,2)))

    sys_append = control.append(sys, sysE, sysC, control.ss(A,B,numpy.eye(A.shape[0]),numpy.zeros((A.shape[0],B.shape[1]))))
    Q = []
    # y in ingresso a sysE
    for i in range(1):
        Q.append([B.shape[1]+i+1, i+2])
    # u in ingresso a sysE
    for i in range(B.shape[1]):
        Q.append([B.shape[1]+1+i+1, C.shape[0]+i+1])
    # u in ingresso a sys
    for i in range(B.shape[1]):
        Q.append([i+1, C.shape[0]+B.shape[1]+1+i+1])
    # u in ingresso al sistema che ha come uscite gli stati reali
    for i in range(B.shape[1]):
        Q.append([2*B.shape[1]+1+A.shape[0]+i+1, C.shape[0]+i+1])
    # xe in ingresso a sysC
    Q.append([2*B.shape[1]+1+1, 1])
    Q.append([2*B.shape[1]+1+1+1, 1+1])
    Q.append([2*B.shape[1]+1+2+1, C.shape[0]+B.shape[1]+1])

    inputv = [i+1 for i in range(B.shape[1])]
    outputv = [i+1 for i in range(numpy.shape(sys_append.C)[0])]
    sys_CL = control.connect(sys_append,
                             Q,
                             inputv,
                             outputv)

    t = numpy.linspace(0, 100000, 2)
    t, yout = control.step_response(sys_CL[0,0],T=t)
    dcgain = yout[-1]
    gain_w2.value = dcgain
    if dcgain != 0:
        u1 = u/gain_w2.value
    else:
        print('The feedforward gain is set to 0 and it changed to 1.')
        u1 = u/1
    print('The static gain of the closed-loop system (from the reference to the output) is: %.5f' %dcgain)

    X0w1 = numpy.zeros((2*A.shape[0]+2,1))
    X0w1[A.shape[0]+1,0] = X0w
    if simTime != 0:
        T = numpy.linspace(0, simTime, 10000)
    else:
        T = numpy.linspace(0, 1, 10000)

    if selu == 'impulse': #selu
        U = [0 for t in range(0,len(T))]
        U[0] = u
        U1 = [0 for t in range(0,len(T))]
        U1[0] = u1
        T, yout, xout = control.forced_response(sys_CL,T,U1,X0w1)
    if selu == 'step':
        U = [u for t in range(0,len(T))]
        U1 = [u1 for t in range(0,len(T))]
        T, yout, xout = control.forced_response(sys_CL,T,U1,X0w1)
    if selu == 'sinusoid':
        U = u*numpy.sin(2*numpy.pi/period*T)
        U1 = u1*numpy.sin(2*numpy.pi/period*T)
        T, yout, xout = control.forced_response(sys_CL,T,U1,X0w1)
    if selu == 'square wave':
        U = u*numpy.sign(numpy.sin(2*numpy.pi/period*T))
        U1 = u1*numpy.sign(numpy.sin(2*numpy.pi/period*T))
        T, yout, xout = control.forced_response(sys_CL,T,U1,X0w1)
    # N.B. i primi 3 stati di xout sono quelli del sistema, mentre gli ultimi 3 sono quelli dell'osservatore

    step_info_dict = control.step_info(sys_CL[0,0],SettlingTimeThreshold=0.05,T=T)
    print('Step info: \n\tRise time =',step_info_dict['RiseTime'],'\n\tSettling time (5%) =',step_info_dict['SettlingTime'],'\n\tOvershoot (%)=',step_info_dict['Overshoot'])
    print('Max x2 value (%)=', max(abs(yout[C.shape[0]+2*B.shape[1]+1+1]))/(numpy.pi/180*30)*100)

    fig = plt.figure(num='Simulation1', figsize=(14,12))

    fig.add_subplot(221)
    plt.title('Output response')
    plt.ylabel('Output')
    plt.plot(T,yout[0],T,U,'r--')
    plt.xlabel('$t$ [s]')
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.legend(['$y$','Reference'])
    plt.grid()

    fig.add_subplot(222)
    plt.title('Input')
    plt.ylabel('$u$')
    plt.plot(T,yout[C.shape[0]])
    plt.xlabel('$t$ [s]')
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.grid()

    fig.add_subplot(223)
    plt.title('States response')
    plt.ylabel('States')
    plt.plot(T,yout[C.shape[0]+2*B.shape[1]+1],
             T,yout[C.shape[0]+2*B.shape[1]+1+1],
             T,yout[C.shape[0]+2*B.shape[1]+1+2],
             T,[numpy.pi/180*30 for i in range(len(T))],'r--',
             T,[-numpy.pi/180*30 for i in range(len(T))],'r--')
    plt.xlabel('$t$ [s]')
    plt.legend(['$x_{1}$','$x_{2}$','$x_{3}$','limit +$x_{2}$','limit -$x_{2}$'])
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.grid()

    fig.add_subplot(224)
    plt.title('Estimation error')
    plt.ylabel('Error')
    plt.plot(T,yout[C.shape[0]+2*B.shape[1]+1+2]-yout[C.shape[0]+B.shape[1]])
    plt.xlabel('$t$ [s]')
    plt.legend(['$e_{3}$'])
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.grid()
    #plt.tight_layout()

alltogether2 = widgets.VBox([widgets.HBox([selm,
                                          sele,
                                          selu]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('K:',border=3), Kw,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('Eigenvalues:',border=3),
                                          eig1c,
                                          eig2c,
                                          eig3c,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3)]),
                            widgets.Label('X0 est.:',border=3), X0w,
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('L:',border=3), Lw,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('Eigenvalues:',border=3),
                                          eig2o,
                                          eig3o,
                                          widgets.Label(' ',border=3),
                                          widgets.VBox([widgets.Label('Inverse reference gain:',border=3),
                                                        widgets.Label('Simulation time (s):',border=3)]),
                                          widgets.VBox([gain_w2,simTime])]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([u,
                                          period,
                                          START])])
out2 = widgets.interactive_output(main_callback2, {'Aw':Aw, 'Bw':Bw, 'X0w':X0w, 'K':Kw, 'L':Lw,
                                                 'eig1c':eig1c, 'eig2c':eig2c, 'eig3c':eig3c, 'eig2o':eig2o, 'eig3o':eig3o,
                                                 'u':u, 'period':period, 'selm':selm, 'sele':sele, 'selu':selu, 'simTime':simTime, 'DW':DW})
out2.layout.height = '870px'
display(out2, alltogether2)
```

    Output(layout=Layout(height='870px'))

    VBox(children=(HBox(children=(Dropdown(index=1, options=('Set K and L', 'Set the eigenvalues'), value='Set the…

```python

```