# Notebook from DaneseAnna/scATAC-benchmarking
Path: Real_Data/Cusanovich_2018_subset/extra_clustering/Cicero/Cicero_cusanovich2018subset.ipynb

### Installation_____no_output_____

# if (!requireNamespace("BiocManager", quietly = TRUE))
#      install.packages("BiocManager")
#   BiocManager::install("cicero")_____no_output_____

Vignette: https://www.bioconductor.org/packages/devel/bioc/vignettes/cicero/inst/doc/website.html_____no_output_____### Import packages_____no_output_____

library(cicero)
library(data.table)
library(Matrix)
library(proxy)
library(reshape2)
library(BuenColors)
library(umap)Loading required package: monocle
Loading required package: Matrix
Loading required package: Biobase
Loading required package: BiocGenerics
Loading required package: parallel

Attaching package: ‘BiocGenerics’

The following objects are masked from ‘package:parallel’:

    clusterApply, clusterApplyLB, clusterCall, clusterEvalQ,
    clusterExport, clusterMap, parApply, parCapply, parLapply,
    parLapplyLB, parRapply, parSapply, parSapplyLB

The following objects are masked from ‘package:Matrix’:

    colMeans, colSums, rowMeans, rowSums, which

The following objects are masked from ‘package:stats’:

    IQR, mad, sd, var, xtabs

The following objects are masked from ‘package:base’:

    anyDuplicated, append, as.data.frame, basename, cbind, colMeans,
    colnames, colSums, dirname, do.call, duplicated, eval, evalq,
    Filter, Find, get, grep, grepl, intersect, is.unsorted, lapply,
    lengths, Map, mapply, match, mget, order, paste, pmax, pmax.int,
    pmin, pmin.int, Position, rank, rbind, Reduce, rowMeans, rownames,
    rowSums, sapply, setdiff, sort, table, tapply, union, unique,
    unsplit, which, which.max, which.min

Welcome to Bioconductor

    Vignettes contain introductory material; view with
    'browseVignettes()'. To cite Bioconductor, see
    'citation("Biobase")', and for packages 'citation("pkgname")'.

Loading required package: ggplot2
Loading required package: VGAM
Loading required package: stats4
Loading required package: splines
Loading required package: DDRTree
Loading required package: irlba
Loading required package: Gviz
Loading required package: S4Vectors

Attaching package: ‘S4Vectors’

The following object is masked from ‘package:Matrix’:

    expand

The following object is masked from ‘package:base’:

    expand.grid

Loading required package: IRanges
Loading required package: GenomicRanges
Loading required package: GenomeInfoDb
Loading required package: grid

Attaching package: 'data.table'

The following object is masked from 'package:GenomicRanges':

    shift

The following object is masked from 'package:IRanges':

    shift

The following objects are masked from 'package:S4Vectors':

    first, second

Attaching package: 'proxy'

The following object is masked from 'package:IRanges':

    as.matrix

The following object is masked from 'package:S4Vectors':

    as.matrix

The following object is masked from 'package:Matrix':

    as.matrix

The following objects are masked from 'package:stats':

    as.dist, dist

The following object is masked from 'package:base':

    as.matrix

Attaching package: 'reshape2'

The following objects are masked from 'package:data.table':

    dcast, melt

Loading required package: MASS

### Load Data_____no_output_____

load('../../run_methods/Cicero/Cicero_cusanovich2018subset.RData')_____no_output_____

### Cluster cells_____no_output_____

dim(datafr)
dim(fm_Cicero)_____no_output_____fm_Cicero[1:5,1:5]_____no_output_____pd <- new("AnnotatedDataFrame", data = data.frame(label=metadata[colnames(fm_Cicero),'label'],row.names =colnames(fm_Cicero)))
fd <- new("AnnotatedDataFrame", data = data.frame(row.names =rownames(fm_Cicero)))
cds <- newCellDataSet(fm_Cicero, phenoData = pd, featureData = fd, expressionFamily = tobit())Warning message in newCellDataSet(fm_Cicero, phenoData = pd, featureData = fd, expressionFamily = tobit()):
"Warning: featureData must contain a column verbatim named 'gene_short_name' for certain functions"Warning message in newCellDataSet(fm_Cicero, phenoData = pd, featureData = fd, expressionFamily = tobit()):
"Warning: featureData must contain a column verbatim named 'gene_short_name' for certain functions"Warning message in newCellDataSet(fm_Cicero, phenoData = pd, featureData = fd, expressionFamily = tobit()):
"Warning: featureData must contain a column verbatim named 'gene_short_name' for certain functions"cds <- detectGenes(cds)
cds <- estimateSizeFactors(cds)
# cds <- estimateDispersions(cds)_____no_output_____cds <- reduceDimension(cds, max_components = 2, num_dim = 15,reduction_method = 'tSNE', verbose = T)Warning message in if (cds@expressionFamily@vfamily %in% c("negbinomial", "negbinomial.size")) {:
"the condition has length > 1 and only the first element will be used"Warning message in if (cds@expressionFamily@vfamily == "binomialff") {:
"the condition has length > 1 and only the first element will be used"Warning message in if (cds@expressionFamily@vfamily == "Tobit") {:
"the condition has length > 1 and only the first element will be used"Warning message in if (cds@expressionFamily@vfamily == "uninormal") {:
"the condition has length > 1 and only the first element will be used"Remove noise by PCA ...
Reduce dimension by tSNE ...
cds <- clusterCells(cds,num_clusters = length(unique(metadata$label)),method = 'densityPeak')Distance cutoff calculated to 4.847424
plot_cell_clusters(cds, 1, 2, color = "label")_____no_output_____plot_cell_clusters(cds, 1, 2, color = "Cluster")_____no_output_____df_out = data.frame(Cicero = cds$Cluster, row.names = sampleNames(phenoData(cds)))_____no_output_____all(rownames(df_out) == rownames(metadata))_____no_output_____head(df_out)_____no_output_____write.table(df_out, file='clusteringSolution.tsv', quote=FALSE, sep='\t', col.names = NA)_____no_output_____


# Notebook from tonywu71/AirFrance-ST7
Path: .ipynb_checkpoints/Version Finale-checkpoint.ipynb

  Projet AirFrance - Placement de Passagers
  CentraleSupélec - 2ème Année - 2021
  Thomas Bouquet, Caio De Prospero Iglesias, Quentin Guilhot, Thomas Melkior, Tony Wu  _____no_output_____
Sommaire

1  Importation des modules

2  L'avion

2.1  Modèle de l'avion

2.1.1  Représentation informatique

2.1.1.1  Affichage du plan de l'avion

2.1.2  Implémentation informatique des objets

2.1.2.1  Schéma simplifié de la cabine

2.1.3  Ensemble des passagers

2.1.3.1  Récupération de la liste des passagers à partir du jeu de données réelles au format .xlsx

2.1.3.2  Ensemble des groupes avec strictement plus d'un passager

2.1.4  Formalisme mathématique

2.1.4.1  Ensembles principaux

2.1.4.2  Définition du modèle

2.1.4.3  Variables indicatrices

2.1.4.4  Variables binaires $\pi_{xyp}$

3  Contraintes

3.1  Centrage de l'avion

3.2  Formalisme mathématique

3.3  Expression à l'aide des $\pi_{xyp}$

3.4  Sièges fictifs

3.5  Au plus un passager par siège

3.6  Exactement un siège par passager

3.7  Pas d'enfant au niveau des issues de secours

3.8  Pas d'enfant isolé

3.9  Pas de passager Business en classe économique

3.10  Pas de passager économique en classe Business

3.11  Contraintes sur les Wheelchairs

4  Le paradigme

4.1  Bonheur des passagers

4.2  Principe de compactage

4.3  Méthode de la mise sous vide

4.4  Nouvelles variables de décision

4.5  Fonction objectif

4.6  Ajout des temps de correspondance

4.7  Nouvelle écriture de la fonction objectif

5  Optimisation

6  Affichage de la solution

6.1  Permutation des groupes

6.1.1  Export du résultat en CSV

6.1.2  Affichage de la figure de l'avion

_____no_output_____On s'intéresse ici à un problème d'optimisation visant à placer des passagers dans un avion de ligne de telle sorte à maximiser la satisfaction client tout en respectant un lot de consignes de sécurité obligatoires._____no_output_____# Importation des modules_____no_output_____

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import plotly.express as px

from gurobipy import *

import os
from typing import Generator
import pandas as pd

from random import randint
import json
import math_____no_output_____

# L'avion_____no_output_____## Modèle de l'avion_____no_output_____Pour ce problème, on considère principalement un avion Airbus A320 de la compagnie Air France dont l'organisation intérieure est présentée sur l'image suivante :

Il y a donc 28 rangées, chacune possédant entre 2 et 6 sièges. Les dix premières rangées sont réservées à la classe Business (bien qu'on ne considère pas cette dernière dans la première instance de notre solution). Les issues de secours sont quant à elles situées aux rangées 11 et 12 (aucun enfant ne devra donc être assis sur un siège de l'une de ces deux rangées; mais encore une fois, cette contrainte ne s'appliquera pour les deuxième et troisième instances)._____no_output_____Toutefois, pour le vol du 8 novembre, un Airbus A320 est trop petit pour accueillir les 174 passagers. Pour cette date, on utilisera donc un Airbus A321 dont le plan est présenté ci-dessous :

La cabine de cet avion est un peu plus complexe et est composé de 34 rangées pour un total de 200 sièges._____no_output_____### Représentation informatique_____no_output_____Afin de rendre la modélisation informatique plus efficace, il convient de légèrement simplifier la disposition géométrique de la cabine afin de la rendre plus "régulière". Pour cela, on considère les hypothèses simplificatrices suivantes :

 L'espace entre deux rangées successives est uniforme dans toute la cabine;

 L'allée centrale est de même largeur qu'une colonne de sièges;

 L'espace entre deux sièges de classe Business est lui aussi de même largeur qu'un siège.

Une autre hypothèse que nous allons effectuer est qu'il n'y a pas de surbooking, c'est-à-dire que :

 Il y a de la place pour tous les passagers dans l'avion;

 Il n'y a pas plus de passagers en classe Business que de place Business disponibles dans la cabine.

Cela permet donc d'avoir une représentation géométrique de l'intérieur de la cabine plus simple : une simple grille de 28 points par 7 ; chaque point étant un siège. Il suffit donc juste de considérer que certains sièges sont des sièges vides, dans le sens où personne ne pourra s'asseoir dessus, étant donné qu'en réalité, il s'agit d'espaces vides.

Le code Python suivant permet donc d'afficher la cabine simplifiée telle que nous l'utiliserons dans la résolution du problème de placement des passagers.

Il suffit pour cela de choisir la catégorie d"avion dans la cellule ci-dessous._____no_output_____‼ **Instructions :** ‼
> Dans le cas où on choisit un A321 et qu'il n'y a pas de passagers en classe *Business*, décommenter la ligne `AVION = "A321_nb"`._____no_output_____

#AVION = "A320"
#AVION = "A321"
AVION = "A321_nb"

# choix de la date
date = '10Nov' # Pour le 10 novembre, bien choisir un A321_____no_output_____# Chargement de l'avion à partir d'un fichier JSON
with open('./'+AVION+'.json') as f:
    preprocess = json.load(f)_____no_output_____# Adaptation en dictionnaire
avion = {
    'x_max': preprocess['x_max'],
    'y_max': preprocess['y_max'],
    'exit': preprocess['exit'],
    'hallway': preprocess['hallway'],
    'barycentre': preprocess['barycentre'],
    'background': preprocess['background'],
    'seats': {
        'real': [],
        'fictive': [],
        'business': [],
        'exit': [],
        'eco': []
    }
}

for seat_category in preprocess['seats'].keys():
    for couple in preprocess['seats'][seat_category]:
        x,y = couple[0], couple[1]
        avion['seats'][seat_category].append((x,y))        _____no_output_____

#### Affichage du plan de l'avion_____no_output_____

def coordToSiege(x,y):
    gapRangee = 0

    if AVION == "A320":

        if x > 12:
            gapRangee = 1

    elif AVION == "A321":

        if x >= 10 and x <= 13:
            gapRangee = -1
        elif x >= 24:
            gapRangee = -1

    lettre = ""
    if y == 1:
        lettre = "A"
    elif y == 2:
        lettre = "B"
    elif y == 3:
        lettre = "C"
    elif y == 5:
        lettre = "D"
    elif y == 6:
        lettre = "E"
    elif y == 7:
        lettre = "F"
    return str(x+gapRangee)+lettre

def get_classe(x, y):

    if AVION == "A320":
        if y == 4 or (x==1 and y in [4, 5, 6, 7]):
            return 'Siège vide'

        elif 1 <= x <= 9 and 1 <= y <= 7:
            return 'Business'

        elif x in [11, 12] and y in [1, 2, 3, 5, 6, 7]:
            return 'Issue de secours'

        else:
            return 'Economie'

    elif AVION == "A321":
        if y == 4 or x in [9,23] or (x,y) in [(10,5),(22,1),(22,7),(24,5)]:
            return 'Siège vide'

        elif 1 <= x <= 8 and 1 <= y <= 7:
            return 'Business'

        elif x in [10, 24] and y in [1, 2, 3, 6, 7]:
            return 'Issue de secours'

        else:
            return 'Economie' _____no_output_____# Initialisation du DataFrame
list_x = []
list_y = []

if AVION == "A320":
    x_lim = 29
elif AVION == "A321":
    x_lim = 37

for i in range(1, x_lim):
    for j in range(1, 8):
        list_x.append(i)
        list_y.append(j)

df_layout = pd.DataFrame({'x': list_x, 'y': list_y})

# Ajout de la colonne 'Siège'
df_layout['Siège'] = None
df_layout['Classe'] = None

for idx, row in df_layout.iterrows():
    x = row['x']
    y = row['y']

    df_layout['Siège'].iloc[idx] = coordToSiege(x, y)
    df_layout['Classe'].iloc[idx] = get_classe(x, y)

df_layout_____no_output_____px.scatter(df_layout,
           x='x',
           y='y',
           color='Classe',
           hover_data=['Siège', 'Classe'],
           title=f"Plan de l'{AVION}")_____no_output_____

### Implémentation informatique des objets_____no_output_____####  Schéma simplifié de la cabine_____no_output_____

plt.scatter(1000,1000,color="red",label="Exit")
plt.scatter(1000,1000,color="blue",label="Economie")
plt.scatter(1000,1000,color="orange",label="Business")

for x in range(1,avion['x_max']+1):
    for y in range(1,avion['y_max']+1):
        if (x,y) in avion['seats']['fictive']:
            plt.scatter(x,y,color="grey",alpha=0.3)
        elif (x,y) in avion['seats']['business']:
            plt.scatter(x,y,color="orange")
        elif (x,y) in avion['seats']['exit']:
            plt.scatter(x,y,color="red")
        else:
            plt.scatter(x,y,color="blue")

plt.plot([avion['barycentre'][0],avion['barycentre'][1],avion['barycentre'][1],avion['barycentre'][0],avion['barycentre'][0]],
         [avion['barycentre'][3],avion['barycentre'][3],avion['barycentre'][2],avion['barycentre'][2],avion['barycentre'][3]],
         color='green',
         label="Zone de Centrage")

plt.axis([0,avion['x_max']+1,0,8],'equal')
plt.title("Disposition des sièges dans l'"+AVION)
plt.legend(bbox_to_anchor=(1.45,1),title="Légende")
plt.show()            C:\Users\33695\Anaconda3\lib\site-packages\ipykernel_launcher.py:21: MatplotlibDeprecationWarning: Passing more than one positional argument to axis() is deprecated and will raise a TypeError two minor releases later.

###  Ensemble des passagers_____no_output_____Afin de simplifier l'utilisation des différentes données relatives aux passagers (âge, sexe, poids, classe, temps de correspondance, ...), nous avons fait le choix d'implémenter une classe objet Python pour les modéliser informatiquement. Cela permettra un appel à ces données plus aisé et commode. La classe ```Passager``` est définie ci-dessous._____no_output_____

numPassager = 0
numFemme = 0
numHomme = 0
numEnfant = 0
numWHCR = 0_____no_output_____class Passager:

    """
    Une classe représentant un passager et toutes les données qui lui sont associées.
    """

    def __init__(self, idx, id_passager, categorie, classe, transit_time=0):

        """
        Constructeur pour la classe Passager.

        Args:
            idx (int): Indice du groupe dans le fichier Excel
            categorie (Categories): Catégorie du passager (Femme, Homme, Enfant, Fauteil Roulant)
            classe (Classes): Lettre donnant la classe du passager
            transit_time (string, optional): String au format %H:%M:%S donnat le
                temps de transit pour le passager. Defaults to 0.
        """

        self.idx = idx
        self.id_passager = id_passager
        self.categorie = categorie
        self.classe = classe
        self.transit_time = transit_time
        self.poids()

    def __str__(self):
        return f'Passager # {self.id_passager}, catégorie {self.categorie} du groupe #{self.idx}, classe {self.classe} et temps de correspondance de {self.transit_time}'

    def __repr__(self):
        return f'passager # {self.id_passager} du groupe #{self.idx}'

    def poids(self):
        """
        Définit le poids du passager selon sa catégorie.
        """

        global numFemme, numHomme, numWHCR, numEnfant

        if self.categorie == "femmes":
            self.poids = 65 + numFemme / 100
            numFemme += 1
        elif self.categorie == "hommes":
            self.poids = 80 + numHomme / 100
            numHomme += 1
        elif self.categorie == "WHCR":
            self.poids = 70 + numWHCR / 100
            numWHCR += 1
        else:
            self.poids = 35 + numEnfant / 100
            numEnfant += 1_____no_output_____

Dans cette même logique, on définit une classe ```Groupe``` pour décrire les groupes de passagers voyageant ensemble._____no_output_____

class Groupe:

    """
    Une classe représentant un groupe de passagers ayant réservé leur places d'avion ensemble.
    Un groupe correspond donc à une ligne dans le fichier Excel de départ.
    """

    def __init__(self, idx, nb_femmes, nb_hommes, nb_enfants, nb_WCHR, classe, transit_time=0):

        """
        Constructeur pour la classe Groupe.

        Args:
            idx (int): Indice du groupe dans le fichier Excel
            nb_femmes (int): Nombre de femmes dans le groupe
            nb_hommes (int): Nombre d'hommes dans le groupe
            nb_enfants (int): Nombre d'enfants dans le groupe
            nb_WCHR (int): Nombre de personnes handicapés dans le groupe
            classe (Classes): Lettre donnant la classe du groupe
            transit_time (string, optional): String au format %H:%M:%S donnat le
                temps de transit pour le groupe. Defaults to 0.
        """

        self.idx = idx
        self.composition = {
            'femmes': nb_femmes,
            'hommes': nb_hommes,
            'enfants': nb_enfants,
            'WHCR': nb_WCHR
        }
        self.classe = classe
        self.transit_time = transit_time

        self.list_passagers = []
        global numPassager

        for categorie in self.composition.keys():
            for nbParCategorie in range(self.composition[categorie]):
                self.list_passagers.append(Passager(
                    self.idx,
                    numPassager,
                    categorie,
                    self.classe,
                    self.transit_time))
                numPassager += 1

        return

    def __str__(self):
        return f'Groupe #{self.idx} avec {self.get_nombre_passagers()} passager(s), classe {self.classe} et temps de correspondance de {self.transit_time}'

    def __repr__(self):
        return f'groupe #{self.idx}'

    def iter_passagers(self):
        """Générateur pour la liste des passagers dans le groupe.
        Yields:
            Passager: instance de la classe Passager
        """
        for passager in self.list_passagers:
            yield passager

    def est_seul(self):
        """Renvoie True si le groupe contient un seul
        passager et False sinon.
        """
        return len(self.list_passagers) == 1

    def get_nombre_passagers(self):
        """Renvoie le nombre de passagers dans le groupe considéré.
        """
        return len(self.list_passagers)

    def comprend_enfants(self):
        """Renvoie True si le groupe contient au moins un enfant
        en son sein et False sinon.
        """
        return any([passager.categorie == 'enfants' for passager in self.list_passagers])      _____no_output_____

####  Récupération de la liste des passagers à partir du jeu de données réelles au format ```.xlsx```_____no_output_____On définit ci-dessous des fonctions Python permettant d'extraire les données des différents vols._____no_output_____

def read_and_preprocess(date):
    df = pd.read_csv(get_filepath(date))

    # Conversion en TimeStamp
    df['TransitTime'] = pd.to_datetime(df['TransitTime']).dt.time

    return df

def get_list_passagers(df):
    list_groupes = {}

    for idx, row in df.iterrows():

        list_groupes[idx] = Groupe(
            idx=idx,
            nb_femmes=row['Femmes'],
            nb_hommes=row['Hommes'],
            nb_enfants=row['Enfants'],
            nb_WCHR=row['WCHR'],
            classe=row['Classe'],
            transit_time=string_to_min(str(row['TransitTime']))
        )

    return list_groupes

def get_filepath(date, data_source='data'):
    """Renvoie le filepath d'un fichier donné
    Args:
        date (string): string contenant la date sous
            le format {jour}{Mois abbrégé}
            -> exemple : 7Nov
    Returns:
        string
    """
    return os.path.join(data_source, f'data_seating_{date}.csv')_____no_output_____

On commence par afficher sous forme de tableau les données du jour à des fins visuelles. Pour cela, on choisit la date dans la cellule ci-dessous._____no_output_____

df = read_and_preprocess(date)
df # Affichage_____no_output_____def string_to_min(date):
    """
    Fonction qui transforme une heure au format HH:MM:SS
    en l'entier correspondant au nombre de minutes de cette dernière.
    """

    hms = date.split(":")
    if date == "00:00:00":
        return float('inf')
    else:
        return int(60*int(hms[0]) + int(hms[1]) + int(hms[2])*50/3)_____no_output_____

Ensuite, on récupère la liste des passagers numériquement afin de l'utiliser dans notre code Python :_____no_output_____

# Liste des groupes
listeGroupes = get_list_passagers(df)

# Liste des passagers, récupérée via la liste des groupes
listePassagers = []

for groupe in listeGroupes.keys():
    listePassagers += listeGroupes[groupe].list_passagers

numPassager = 0 # Réinitialisation du compteur
numFemme = 0
numHomme = 0
numEnfant = 0
numWHCR = 0_____no_output_____

Ceci nous permet de connaître exactement le nombre de passagers dans l'avion :_____no_output_____

nombrePassagers = len(listePassagers)
print("Il y a {} passagers dans l'avion.".format(nombrePassagers))Il y a 174 passagers dans l'avion.

#### Ensemble des groupes avec strictement plus d'un passager_____no_output_____Comme nous le verrons plus tard, il nous est fondamental de distinguer les groupes de plus d'un passager des autres. Nous les identifions avec le code ci-dessous._____no_output_____

liste_groupes_sup1 = {}

for groupe in listeGroupes.keys():
    if listeGroupes[groupe].get_nombre_passagers() > 1:
        liste_groupes_sup1[groupe] = listeGroupes[groupe]

print("Il y a {} groupes de plus d'un passagers dans l'avion. Il s'agit des groupes {}.".format(len(liste_groupes_sup1.keys()),list(liste_groupes_sup1.keys())))      Il y a 32 groupes de plus d'un passagers dans l'avion. Il s'agit des groupes [2, 5, 13, 25, 26, 27, 33, 35, 36, 37, 40, 42, 43, 46, 54, 61, 62, 64, 65, 66, 71, 75, 78, 79, 84, 90, 106, 108, 111, 114, 116, 120].

### Formalisme mathématique_____no_output_____#### Ensembles principaux_____no_output_____Afin de formellement décrire l'ensemble des passagers de l'avion ainsi que les sièges de la cabine, introduisons les ensembles suivants :

 $\mathcal{P}$ l'ensemble des passagers ;

 $\mathcal{S}_\emptyset$ l'ensemble des sièges vides ;

 $\mathcal{S} = [1,28] \times [1,7] \setminus \mathcal{S}_\emptyset$ l'ensemble des sièges réels.

A noter que si on voulait exprimer exhaustivement l'ensemble des sièges vides, on aurait donc :

$$\mathcal{S}_\emptyset = ([ 1,28 ] \times \lbrace 4 \rbrace) \cup (\lbrace 1 \rbrace \times [ 5,8 ])$$_____no_output_____#### Définition du modèle_____no_output_____

m = Model("Placement Passagers "+AVION)Academic license - for non-commercial use only - expires 2021-04-10
Using license file C:\Users\33695\gurobi.lic

#### Variables indicatrices_____no_output_____A partir de ces ensembles, on peut donc introduire les variables indicatrices $\pi_{xyp} \in \lbrace 0,1 \rbrace$ $(p \in \mathcal{P}, (x,y) \in \mathcal{S})$ valant $1$ si le passager $p$ est assis sur le siège de coordonnées $(x,y)$, et $0$ sinon._____no_output_____Les variables $\pi_{xyp}$ nous serons très utiles pour exprimer des conditions liées à l'autorisation ou non que des passagers s'assoient sur un siège. Les conditions de base sont :

 Au plus un passager par siège : $\forall (x,y) \in \mathcal{S}$, $\sum_{p \in \mathcal{P}} \pi_{xyp} \leq 1$ ;

 Au plus un siège par passager : $\forall p \in \mathcal{P}$, $\sum_{(x,y) \in \mathcal{S}} \pi_{xyp} \leq 1$ ;

 Tous les passagers ont un siège : $\sum_{p \in \mathcal{P}} \sum_{(x,y) \in \mathcal{S}} \pi_{xyp} = \# \mathcal{P}$ ;

_____no_output_____Ce modèle sera enrichi par la suite avec les différentes contraintes qui s'ajouteront à notre modèle. Par exemple, un détail important sur lequel nous nous attarderont subséquemment est le fait qu'aucun passager enfant ne doit être assis sur une rangée située au niveau des issues de secours de l'avion. Sans s'épancher sur la définition formel des deux ensemble suivants, nous pouvons écrire :

 $\mathcal{E} \subset \mathcal{P}$ l'ensemble des passagers enfants ;

 $\mathcal{S}_\mathcal{I} \subset \mathcal{S}$ l'ensemble des sièges situés au niveau des issues de secours.

Dès lors, on aura : $\sum_{p \in \mathcal{E}} \sum_{(x,y) \in \mathcal{S}_\mathcal{I}} \pi_{xyp} = 0$, ce qui signifie bien qu'aucun enfant ne devra être assis près d'une issue de secours. Toutefois, cette condition ne sera envisagée que dans la seconde phase de notre étude donc nous y reviendrons ultérieurement.

Cette partie avait juste pour but d'introduire formellement quelques ensembles d'intérêt ainsi que les variable $\pi_{xyp}$ qui nous serons très utiles par la suite, notamment en tant que variable de décision pour le problème d'optimisation._____no_output_____#### Variables binaires $\pi_{xyp}$_____no_output_____

Le problème du placement des passagers est un problème typique d'affectation. Cette catégorie de problèmes d'optimisation a communément recours à des variables binaires commes briques de base afin de représenter numériquement lesdites affectations.

Dans notre cas, nous souhaitons savoir quel passager est affecté à quel siège de l'avion. Pour cela, nous introduisons donc des variables binaires $\pi_{xyp}$ valant 1 si et seulement si le passager $p$ est assis sur le siège dont les coordonnées sont $(x,y)$; et 0 sinon.

Ces variables sont définies pour chaque passager et chaque siège, réel comme fictif. En effet, il est plus pratique de les définir même pour des sièges vides, quitte à les imposer égales à $0$, afin de ne pas avoir des effets de bord.

 _____no_output_____

PI = {}

for x in range(avion['x_max']+2):
    for y in range(avion['y_max']+2):
        for passager in listePassagers:
            PI[x,y,passager.id_passager] = m.addVar(vtype = GRB.BINARY, name = "pi_{},{},{}".format(x,y,passager.id_passager))_____no_output_____

#  Contraintes _____no_output_____## Centrage de l'avion_____no_output_____Afin que l'avion puisse décoller sans risque et qu'il reste stable une fois en vol, le barycentre ce de dernier doit se situer dans une zone très particulière appelée "zone de centrage". Il s'agit d'une contrainte de sécurité forte devant être respectée obligatoirement. Cette zone se situe entre la $13^{ème}$ et la $17^{ème}$ rangée dans le sens de la longeur, et entre les deux colonnes bordant l'allée centrale dans le sens de la hauteur._____no_output_____Dans le cadre de notre problème, on considère que le barycentre de l'avion à vide se situe effectivement dans cette zone. Il nous faut donc vérifier que la répartition des masses due au positionnement des passagers conserve le barycentre dans cette même zone de centrage._____no_output_____## Formalisme mathématique_____no_output_____Considérons un ensemble de $N$ masses $(m_i)_{i \in [1,N]}$ réparties dans le plan. Pour chaque masse $m_i$, on désigne par $\vec{p_i}=(x_i,y_i)^T$ son vecteur position en partant de l'origine dudit plan. Dès lors, le vecteur position du barycentre $\vec{B}$ se calcule par :
$$\vec{B}=\frac{\sum_{i=1}^N m_i\vec{p_i}}{\sum_{i=1}^N m_i}$$
Dès lors, en projetant cette relation selon la direction $x$ et la direction $y$, on obtient deux équations découplées donnant la position barycentre :

 Positionnement selon $x$ : $B_x=\frac{\sum_{i=1}^N m_i x_i}{\sum_{i=1}^N m_i}$

 Positionnement selon $y$ : $B_y=\frac{\sum_{i=1}^N m_i y_i}{\sum_{i=1}^N m_i}$

        _____no_output_____Compte tenu des définitions précédentes, on peut alors réécrire les conditions à l'aide des variables $\pi_{xyp}$ en considérant la direction $x$ définie ci-dessus comme la direction dans la longueur de l'avion, et $y$ celle dans la hauteur de ce dernier :

 Centrage selon la longueur : $13 \sum_{p \in \mathcal{P}} m_p \leq \sum_{p \in \mathcal{P}} \sum_{(x,y) \in \mathcal{S}} m_p x \pi_{xyp} \leq 17 \sum_{p \in \mathcal{P}} m_p$

 Centrage selon la largeur : $3 \sum_{p \in \mathcal{P}} m_p \leq \sum_{p \in \mathcal{P}} \sum_{(x,y) \in \mathcal{S}} m_p y \pi_{xyp} \leq 5 \sum_{p \in \mathcal{P}} m_p$

_____no_output_____## Expression à l'aide des $\pi_{xyp}$_____no_output_____

masseTotalePassagers = sum(passager.poids for passager in listePassagers)

contraintesCentrage = {}
contraintesCentrage['x_inf'] = m.addConstr(quicksum(passager.poids*x*PI[x,y,passager.id_passager] for x,y in avion['seats']['real'] for passager in listePassagers) >= avion['barycentre'][0]*masseTotalePassagers, name = "Barycentre_x_inf")
contraintesCentrage['x_sup'] = m.addConstr(quicksum(passager.poids*x*PI[x,y,passager.id_passager] for x,y in avion['seats']['real'] for passager in listePassagers) <= avion['barycentre'][1]*masseTotalePassagers, name = "Barycentre_x_sup")
contraintesCentrage['y_inf'] = m.addConstr(quicksum(passager.poids*y*PI[x,y,passager.id_passager] for x,y in avion['seats']['real'] for passager in listePassagers) >= avion['barycentre'][2]*masseTotalePassagers, name = "Barycentre_y_inf")
contraintesCentrage['y_sup'] = m.addConstr(quicksum(passager.poids*y*PI[x,y,passager.id_passager] for x,y in avion['seats']['real'] for passager in listePassagers) <= avion['barycentre'][3]*masseTotalePassagers, name = "Barycentre_y_sup")_____no_output_____

## Sièges fictifs_____no_output_____

Comme nous l'avons expliqué précédemment, nous avons introduit des sièges fictifs afin de simplifier la représentation informatique de l'avion et pour négliger les effets de bord. Il faut donc imposer qu'aucun passager ne soit assis dessus. Mathématiquement, cette contrainte s'écrit :

$$\sum_{(x,y) \in \mathcal{S}_{\emptyset}} \sum_{p \in \mathcal{P}} \pi_{xyp} = 0$$

    _____no_output_____

contrainteSiegesFictifs = m.addConstr(quicksum(PI[x,y,passager.id_passager] for x,y in avion['seats']['fictive'] for passager in listePassagers) == 0)_____no_output_____

## Au plus un passager par siège_____no_output_____

Il faut de plus s'assurer qu'il y ait au plus un passager par siège. Mathématiquement, cette contrainte s'écrit :

$$\forall (x,y) \in \mathcal{S}, \; \sum_{p \in \mathcal{P}} \pi_{xyp} \leq 1$$

    _____no_output_____

contraintePPS = {}

for x,y in avion['seats']['real']:
    contraintePPS[(x,y)] = m.addConstr(quicksum(PI[x,y,passager.id_passager] for passager in listePassagers) <= 1, name = "C - Au plus un passager sur le siège {},{}".format(x,y))_____no_output_____

## Exactement un siège par passager_____no_output_____

Un autre prérequis est que tous les passagers ayant acheté un billet puissent avoir un siège. Mathématiquement, cette contrainte s'écrit :

$$\forall p \in \mathcal{P}, \; \sum_{(x,y) \in \mathcal{S}_{\emptyset}} \pi_{xyp} = 1$$

    _____no_output_____

contraintePPS = {}

for passager in listePassagers:
    contraintePPS[passager.id_passager] = m.addConstr(quicksum(PI[x,y,passager.id_passager] for x,y in avion['seats']['real']) == 1, name = "C - Au plus un siège pour le passager n°{}".format(passager.id_passager))_____no_output_____

## Pas d'enfant au niveau des issues de secours_____no_output_____

Une autre contrainte portant sur la sécurité est qu'aucun enfant ne soit assis dans une rangée située au niveau d'une issue de secours. Mathématiquement, cette contrainte s'écrit :

$$\sum_{(x,y) \in \mathcal{S}_{\mathbf{I}}} \sum_{e \in \mathcal{P}_{\mathbf{E}}} \pi_{xye} = 0$$

    _____no_output_____

contrainte_issue_de_secours = m.addConstr(quicksum(PI[x,y,passager.id_passager] for x,y in avion['seats']['exit'] for passager in listePassagers if passager.categorie == "enfants") == 0)_____no_output_____

## Pas d'enfant isolé_____no_output_____

La contrainte de placement des enfants consiste à imposer qu'un enfant est toujours à côté d'un adulte de son groupe, tant que cela est possible. Si ce n'est pas possible, alors on impose seulement qu'il soit à côté d'un membre du groupe (adulte ou enfant). Ainsi, s'il y a plus de deux fois plus d'enfants que d'adultes, il est impossible que tous les enfants soient à côté d'un adulte donc nous sommes dans la deuxième situation.

Un enfant pourra donc toujours être à côté un adulte tant que $\#E \leq 2\#A$

   Afin de formaliser mathématiquement cette contrainte considérons un passager enfant $e \in  \mathcal{P}_{\mathbf{E}}$ tel qu'$e$ fasse partie d'un groupe que l'on note $G \in \mathcal{G}_{2}^{+}$. Dès lors, pour tout siège $(x,y) \in \mathcal{S}$ il y a deux cas à considérer :

 Soit l'enfant n'est pas assis sur le siège, donc $\pi_{xye} = 0$, et dans ce cas il n'y a rien de particulier à dire;

 Soit l'enfant est effectivement assis sur le siège, donc $\pi_{xye} = 1$, et dans ce cas, la traduction mathématique de la contrainte s'écrit $\sum_{p \in G, p \;adulte}\pi_{x,y-1,p}+\sum_{p \in G, p \; adulte}\pi_{x,y+1,p} \geq 1$ (avec des mots : il y a au moins un adulte du groupe de $e$ à gauche et/ou à droite de ce dernier).

On constate donc que ces deux cas peuvent fusionner en une seule formule qui est la suivante :

$$\sum_{p \in G, p \;adulte} \pi_{x,y-1,p}+\sum_{p \in G, p \;adulte}\pi_{x,y+1,p} \geq \pi_{xye}$$

En effet, soit $\pi_{xye} = 1$ auquel cas on retrouve bien le second cas, soit $\pi_{xye} = 0 \Rightarrow \sum_{p \in G, p \;adulte}\pi_{x,y-1,p}+\sum_{p \in G, p \;adulte}\pi_{x,y+1,p} \geq 0$ ce qui n'apporte aucune restriction supplémentaire.

Ainsi, dans le cas général, la contrainte s'écrit :

$$\forall (x,y) \in \mathcal{S}, \; \forall g \in \mathcal{G}_{2}^{+}, \; \forall e \in g \cap \mathcal{P}_{\mathbf{E}}, \; \sum_{p \in G, p \;adulte}\pi_{x,y-1,p}+\sum_{p \in G, p \;adulte}\pi_{x,y+1,p} \geq \pi_{xye}$$

Si nous sommes dans le cas où il y a trop d'enfants, on impose les mêmes contraintes mais sans imposer que p soit un adulte.

_____no_output_____

contrainte_aucun_enfant_isole = {}

def voisinage_lateral(x,y):

    """
    Définit le voisinage latéral selon la position du siège.
    """

    if (x,y) in avion['seats']['business']:
        if y == 1:
            return [2]
        elif y == 7:
            return [-2]
        else:
            return [-2, 2]
    else:
        return [-1, 1]

for groupe in liste_groupes_sup1.keys():
    nombre_enfants = listeGroupes[groupe].composition['enfants']
    nombre_adultes = listeGroupes[groupe].composition['femmes'] + listeGroupes[groupe].composition['hommes'] + listeGroupes[groupe].composition['WHCR']
    if nombre_enfants >= 1: # s'il y a au moins un enfant...
        for passager in listeGroupes[groupe].list_passagers:
            if passager.categorie == 'enfants':
                e = passager.id_passager
                for x, y in avion['seats']['real']:
                    voisinage = voisinage_lateral(x,y)
                    if nombre_enfants > 2*nombre_adultes:
                        contrainte_aucun_enfant_isole[x, y, e] = m.addConstr(
                            quicksum(PI[x, y+i, passager.id_passager] for i in voisinage
                                     for passager in listeGroupes[groupe].list_passagers
                                     if passager.id_passager != e)
                            >= PI[x, y, e]
                        )
                    else:
                        contrainte_aucun_enfant_isole[x, y, e] = m.addConstr(
                            quicksum(PI[x, y+i, adulte.id_passager] for i in voisinage for adulte in listeGroupes[groupe].list_passagers if adulte.categorie != 'enfants') >= PI[x, y, e]
                        )_____no_output_____

## Pas de passager Business en classe économique_____no_output_____

Les avions sont également dotés d'une cabine Business dans laquelle doivent être assis tous les passagers ayant acheté un billet Business. Dès lors, il faut s'assurer qu'aucun passager Business ne puisse s'asseoir en cabine économie. Mathématiquement, cette contrainte s'écrit :

$$\sum_{p \in \mathcal{P}_{\mathbf{J}}} \sum_{(x,y) \in \mathcal{S}_{\mathbf{Y}}} \pi_{xyp} = 0$$

_____no_output_____

contrainte_business = m.addConstr(quicksum(PI[x,y,passager.id_passager] for x,y in avion['seats']['eco'] for passager in listePassagers if passager.classe == "J") == 0)_____no_output_____

## Pas de passager économique en classe Business_____no_output_____

A l'inverse, lorsqu'il y a effectivement des passagers Business dans l'avion, il faut faire en sorte que les passager de la classe économique ne puissent pas s'asseoir en cabine Business. Nous avons fait choix car nous considérons qu'il n'est pas rentable pour la compagnie de surclasser trop de passager et de plus, en surclassant des passagers, elle risque de recevoir des critiques des passagers Business ayant payé plus cher pour un service supplémentaire, mais qui, en fin de compte, partagent ces services avec d'autres passagers ne les ayant pas payés. Mathématiquement, il vient donc :

$$\sum_{p \in \mathcal{P}_{\mathbf{Y}}} \sum_{(x,y) \in \mathcal{S}_{\mathbf{J}}} \pi_{xyp} = 0$$

_____no_output_____

nombreBusiness = 0
nombreEco = 0

for passager in listePassagers:
    if passager.classe == "J":
        nombreBusiness += 1
    else:
        nombreEco += 1 + 3 * int(passager.categorie == "WHCR") _____no_output_____contrainte_eco = m.addConstr(quicksum(PI[x,y,passager.id_passager] for x,y in avion['seats']['business'] for passager in listePassagers if passager.classe == "Y") == 0)_____no_output_____

## Contraintes sur les Wheelchairs_____no_output_____Concernant les passagers en siège roulant, nous les plaçons dans les rangées $y=2$ et $y=6$ en leur réservant un carré. Nous définissons ainsi des valeurs interdites pour les sièges roulant, qui sont pour $y \in \lbrace {1,3,5,7} \rbrace$. En effet, placer un passager en siège roulant au bord du couloir ne lui permettrait pas de manoeuvrer efficacement et le placer à côté du hublot lui donnerait moins de place. La contrainte s'écrit donc :
$$\sum_{(x,y)\in S, \;y \in \lbrace {1,3,5,7} \rbrace, \;w \; wheelchair} \pi_{x,y,w} = 0$$_____no_output_____

cstr_wchr = {}
val_interdites_y = [1,3,5,7]

cstr_wchr['contrainte_globale'] = m.addConstr(quicksum(PI[x,y,w.id_passager] for x,y in avion['seats']['real'] for w in listePassagers if w.categorie == 'WHCR' and y in val_interdites_y) == 0, name = 'cstr_globale')_____no_output_____

Nous voulons aussi que si $\pi_{x,y,w}=1$, nous interdisions les sièges alentours. Par exemple, pour $y=2$, on souhaite imposer que les sièges en $(x,y+1)$, $(x-1,y+1)$ et $(x-1,y)$ soient interdits, i.e $\sum_{p\ne w}\pi_{x,y+1,p}+\pi_{x-1,y+1,p}+\pi_{x-1,y,p}=0$. On raisonne de la même manière pour $y=6$ par symétrie par rapport au couloir. On rassemble ces contraintes sous la forme ($\delta_{y,2}$ est le symbole de kronecker):
$$ \forall w  \;\forall (x,y) \in S \sum_{p\ne w}\delta_{y,2}(\pi_{x,y+1,p}+\pi_{x-1,y+1,p}+\pi_{x-1,y,p})+\delta_{y,6}(\pi_{x,y-1,p}+\pi_{x-1,y-1,p}+\pi_{x-1,y,p}) \leq 3\times (1-\pi_{x,y,w})$$

Ainsi, si $\pi_{x,y,w}=0$, la contrainte n'est pas restrictive puisque les 3 sièges sont occupés au maximum par 3 personnes. SI $\pi_{x,y,w}=1$, alors pour $y=2$, la contrainte devient $\sum_{p\ne w}\pi_{x,y+1,p}+\pi_{x-1,y+1,p}+\pi_{x-1,y,p} \leq 0$_____no_output_____

for x,y in avion['seats']['real']:
    if y in [2,6]:
        for passager in listePassagers:
            if passager.categorie == 'WHCR':
                cstr_wchr[(x,y,passager.id_passager)] = m.addConstr(quicksum((y == 2) * (PI[x,y+1,p.id_passager] + PI[x-1,y+1,p.id_passager]) + (y == 6) * (PI[x,y-1,p.id_passager] + PI[x-1,y-1,p.id_passager]) + PI[x-1,y,p.id_passager] for p in listePassagers if p.id_passager != passager.id_passager) <= 3 * (1 - PI[x,y,passager.id_passager]), name = 'wheel_{}_{}_{}'.format(x,y,passager.id_passager))_____no_output_____

# Le paradigme_____no_output_____Maintenant que nous avons introduit formellement les briques de bases de notre modèle à l'aide des variables binaires de décision $\pi_{xyp}$ et que nous avons traduit mathématiquement et informatiquement les contraintes élémentaires à l'aide de ces même variables, nous pouvons nous intéresser au paradigme que nous avons établi afin de maximiser les bonheur des passagers de l'avion. Nous allons donc commencer par définir concrètement cette notion de bonheur._____no_output_____## Bonheur des passagers_____no_output_____Considérons un passager $p$ de l'avion, voyageant au sein d'un groupe $g$ et assis sur le siège $s$. On introduit la notion de *score de voisinage* de la manière suivante : il s'agit d'un nombre valant initialement 0 et pouvant être incrémenté par deux propriétés.

* si un autre membre de $g$ est assis à côté de $p$ (sur le siège à gauche de $s$ ou celui à droite), le score de voisinage est incrémenté de 1;
* si un autre membre de $g$ est assis à devant ou derrière $p$, le score de voisinage est incrémenté de 0,5._____no_output_____Compte tenu de cette métrique, nous avons été capable d'établir la disposition optimale des passagers au sein d'un groupe afin de maximiser le bonheur au sein de ce dernier. Ces dispositions sont illustrées ci-dessous. _____no_output_____La fonction ci-dessous permet de donner les dimensions du rectangle optimal permettant de contenir tous les membres du groupe tout en maximisant le bonheur de ce dernier._____no_output_____

def constante_y(taille_groupe,classe):

    """
    Largeur du rectangle optimal pour le compactage des groupes.
    """

    coeff = 1 + int(classe == "J") # On prend en compte les plus grands espacements entre sièges en classe Business
    if taille_groupe == 2:
        return coeff
    elif taille_groupe == 3:
        return 2*coeff
    elif taille_groupe < 7 and taille_groupe > 3:
        return (math.ceil(taille_groupe/2) - 1)*coeff
    elif taille_groupe <= 12 and taille_groupe >= 7:
        return (math.ceil(taille_groupe/2))*coeff
    else :
        return (taille_groupe//4+2)*coeff

def constante_x(taille_groupe):

    """
    Hauteur du rectangle optimal pour le compactage des groupes.
    """

    if taille_groupe <= 3:
        return 0
    elif taille_groupe >= 4 and taille_groupe <= 12:
        return 1
    else :
        return 2_____no_output_____

On remarque donc que les dispositions optimales sont relativement compactes d'où la notion de *compactage* que nous alons introduire à présent._____no_output_____## Principe de compactage_____no_output_____Dès lors, notre méthode consiste à faire en sorte que Gurobi place les passagers des groupes suivant le schémas de disposition optimale présenté ci-dessus. Initialement, nous avions pensé calculer le score de voisinage directement au sein de la simulation Gurobi en calculant ce dernier pour chaque passager à l'aide des variables $\pi_{xyp}$ et en le maximisant via la fonction objectif._____no_output_____Toutefois, le score étant intrinsèquement inclus dans la disposition optimale, pour notre implémentation finale, nous avons donc fait le choix de nous concentrer sur le compactage des groupes plutôt que le calcul du score de voisinage (qui de toute façon sera maximal si il est possible de placer les groupes de manière optimale)._____no_output_____## Méthode de la *mise sous vide*_____no_output_____Afin d'imager la méthode d'optimisation que nous avons inventée, nous avons choisi de l'appeler la méthode de *mise sous vide* à cause de sa similarité relative au principe de mise sous vide d'aliments. En effet, cette dernière se déroule au travers des étapes suivantes :

* on sélectionne les aliments que l'on souhaite mettre sous vide ;
* on les dispose dans un sachet aussi petit que possible mais pouvant tous les contenir ;
* on enlève les bulles d'air restantes._____no_output_____Notre méthode informatique de *mise sous vide* fonctionne donc suivant un principe similaire dont les étapes sont détaillées ci-dessous :

* on s'intéresse uniquement aux groupes d'au moins 2 passagers ;
* en fonction de la taille du groupe, on détermine le plus petit rectangle (ou *sachet* optimal) pouvant contenir l'intégralité du groupe conformément aux dispositions optimales ;
* on encourage Gurobi à placer les membres du groupe dans le *sachet* en jouant sur les espacement dans le sens le la largeur (resp. longueur) de l'avion entre les passagers du groupe assis les plus à gauche (resp. les plus en haut) et ceux assis les plus à droite (resp. les plus en bas) ;
* si des sièges vides ou des passagers hors du groupe sont dans le sachet - ce qui est tout à fait possible *a priori* - (ce que l'on appelle des *bulles d'air*), on les *aspire* vers les coins du rectangle afin de maximiser la proximité des membres du groupe au sein du rectangle et donc leur bonheur. Pour cette dernière étape, il suffit de permuter la *bulle d'air* et un membre du groupe assis dans un coin en veillant à ce que les contraintes ne soient pas violées._____no_output_____Nous allons à présent passer à l'implémentation informatique de cette méthode._____no_output_____## Nouvelles variables de décision_____no_output_____Comme expliqué lors de la decription de la méthode de *mise sous vide*, nous avons besoin de connaître les espacements verticaux ou latéraux entre les membres du groupe situés aux *extrêmes* du rectangle les contenant tous. Pour cela, on introduit donc des variables $x_{min}^g$, $x_{max}^g$, $y_{min}^g$ et $y_{max}^g$ correspondant respectivement au plus petit numéro de rangée occupée par un membre du groupe $g$, au plus grand numéro de rangée occupée par un membre du groupe $g$, au plus petit numéro de colonne occupée par un membre du groupe $g$ et au grand petit numéro de colonne occupée par un membre du groupe $g$._____no_output_____Dans les faits, il n'est pas nécessaire d'imposer, par exemple, que $x_{min}^g$ est exactement égal au minimum des numéros de rangée pour l'ensemble des passagers de $g$, mais juste d'imposer qu'il est inférieur à tous les numéros de rangée pour l'ensemble des passagers du groupe car en optimisant, Gurobi fera en sorte que $x_{min}^g$ prenne exactement la valeur du minimum des numéros de rangée du groupe. Un raisonnement analogue existe évidemment pour les trois autres variables._____no_output_____On introduit également les variables d'écart $\Delta_x^g$ et $\Delta_y^g$ correspondant à la différence des valeurs extrêmes de $x$ et $y$ pour le groupe $g$, soit : $\Delta_x^g = x_{max}^g - x_{min}^g$ et $\Delta_y^g = y_{max}^g - y_{min}^g$._____no_output_____

X_MIN, X_MAX = {}, {}
DELTA_x = {}

for groupe in liste_groupes_sup1.keys():
    if listeGroupes[groupe].get_nombre_passagers() >= 2:
        X_MIN[groupe] = m.addVar(vtype = GRB.CONTINUOUS, lb = 1, ub = avion['x_max'], name = "x_min_{},{},{}".format(x,y,groupe))
        X_MAX[groupe] = m.addVar(vtype = GRB.CONTINUOUS, lb = 1, ub = avion['x_max'], name = "x_max_{},{},{}".format(x,y,groupe))
        DELTA_x[groupe] = m.addVar(vtype = GRB.CONTINUOUS, lb = constante_x(listeGroupes[groupe].get_nombre_passagers()), ub = 2*listeGroupes[groupe].get_nombre_passagers(), name = "delta_{},{},{}".format(x,y,groupe))_____no_output_____X_MIN_constr, X_MAX_constr = {}, {}
DELTA_constr_x = {}

for groupe in liste_groupes_sup1.keys():
    if listeGroupes[groupe].get_nombre_passagers() >= 2:
        for passager in listeGroupes[groupe].list_passagers:
            p = passager.id_passager
            X_MIN_constr[p] = m.addConstr(X_MIN[groupe] <= quicksum(x*PI[x,y,p] for x,y in avion['seats']['real']))
            X_MAX_constr[p] = m.addConstr(X_MAX[groupe] >= quicksum(x*PI[x,y,p] for x,y in avion['seats']['real']))
            DELTA_constr_x[groupe] = m.addConstr(DELTA_x[groupe] == (X_MAX[groupe] - X_MIN[groupe]))_____no_output_____Y_MIN, Y_MAX = {},{}
DELTA_y = {}

for groupe in liste_groupes_sup1.keys():
    if listeGroupes[groupe].get_nombre_passagers() >= 2:
        Y_MIN[groupe] = m.addVar(vtype = GRB.CONTINUOUS, lb = 1, ub = avion['y_max'], name = "y_min_{},{},{}".format(x,y,groupe))
        Y_MAX[groupe] = m.addVar(vtype = GRB.CONTINUOUS, lb = 1, ub = avion['y_max'], name = "y_max_{},{},{}".format(x,y,groupe))
        DELTA_y[groupe] = m.addVar(vtype = GRB.CONTINUOUS, lb = constante_y(listeGroupes[groupe].get_nombre_passagers(),listeGroupes[groupe].classe), ub = 6, name = "delta_{},{},{}".format(x,y,groupe))_____no_output_____Y_MIN_constr, Y_MAX_constr = {}, {}
DELTA_constr_y = {}

for groupe in liste_groupes_sup1.keys():
    if listeGroupes[groupe].get_nombre_passagers() >= 2:
        for passager in listeGroupes[groupe].list_passagers:
            p = passager.id_passager
            Y_MIN_constr[p] = m.addConstr(Y_MIN[groupe] <= quicksum(y*PI[x,y,p] for x,y in avion['seats']['real']))
            Y_MAX_constr[p] = m.addConstr(Y_MAX[groupe] >= quicksum(y*PI[x,y,p] for x,y in avion['seats']['real']))
            DELTA_constr_y[groupe] = m.addConstr(DELTA_y[groupe] == (Y_MAX[groupe] - Y_MIN[groupe]))_____no_output_____

## Fonction objectif_____no_output_____On souhaite donc faire en sorte que Gurobi dispose les membres du même groupe dans le *sachet* optimal pour nous permettre ensuite d'en retirer les *bulles d'air*. En notant $C_x^g$ et $C_y^g$ les écarts maximaux latéral et vertical entre les passagers du groupe $g$ quand ceux-ci sont placés dans le *sachet* optimal, il est immédiat que les différences $\Delta_x^g - C_x^g$ et $\Delta_y^g - C_y^g$ sont toutes les deux positives, et en particulier nulles quand les espacements maximaux entre membres du groupe coïncident avec ceux au sein du *sachet* optimal._____no_output_____Dans le cas optimal, on souhaite donc que toutes ces différences soient nulles, c'est-à-dire que tous les groupes soient placés dans leur sachet optimal, de telle sorte que nous n'ayons plus qu'à opérer des permutations intra-*sachet* pour maximiser le bonheur des groupes. A noter que les *sachets* sont tous indépendants les uns des autres donc la permutation de l'un n'affecte pas la disposition de l'autre._____no_output_____Ainsi, notre fonction objectif s'écrit : $$\mbox{Minimize} \; \; \; \sum_{g \in \mathcal{G}_{2}^{+}} (\Delta_x^g - C_x^g) + (\Delta_y^g - C_y^g)$$_____no_output_____## Ajout des temps de correspondance_____no_output_____Toutefois, le modèle n'est pas encore complet car les temps de correspondance des passagers ne sont pas pris en compte. Or, ces derniers jouent un rôle non négligeable dans la disposition des passagers dans l'avion. En effet, moins le temps de correspondance d'un passager est long, plus ce dernier doit être assis à l'avant de la cabine afin d'en sortir le plus vite possible et ainsi être en mesure d'avoir son avion suivant._____no_output_____Considérons un passager $p$ ayant un temps de correspondance $\tau_p$ et assis sur un siège de la rangée $x$. On introduit une *constante de sortie* $K_s$ qui matérialise en secondes le temps nécéssaire aux passagers pour sortir de leur rangée. Afin de sortir de la cabine, notre passager $p$ doit donc attendre $x-1$ fois un temps $K_s$ afin que les rangées devant la sienne ne se vident, puis nécessitera un temps $K_s$ afin de quitter la sienne. Au total, c'est d'un temps $K_sx$ dont il aura eu besoin pour sortir de la cabine._____no_output_____Ainsi, la formule $1-K_s\frac{x}{\tau_p}$ donne un nombre en 0 et 1 qui indique le pourcentage du temps de correspondance de $p$ dédié à son déplacment dans l'aéroport pour aller prendre son autre avion. On comprend donc que pour limiter le stress et accroître le bonheur des passagers, il convient de maximiser ce pourcentage autant que possible. Il conviendrait donc de maximiser la somme : $$\sum_{p \in \mathcal{P}_\tau} 1-K_s\frac{x_p}{\tau_p}$$_____no_output_____Or, on sait d'après la partie précédente que dans la situation optimale, la somme $\sum_{g \in \mathcal{G}_{2}^{+}} (\Delta_x^g - C_x^g) + (\Delta_y^g - C_y^g)$ vaut exactement 0. On entrevoit donc la possibilité de fusionner nos deux sommes en une seule._____no_output_____On commence pour cela à multiplier par $-1$ cette somme afin que nous passions d'un problème de minimisation à un problème de maximisation. De plus, on souhaite compacter les groupes dans leur *sachet* optimal. Nous ne pouvons pas, pour cela, l'imposer sous forme de contrainte car ce ne serait pas assez permissif et pourrait conduire à l'infaisibilité du modèle. Toutefois, pour outrepasser cet écueil, il suffit de remplacer le poids $-1$ devant la somme $\sum_{g \in \mathcal{G}_{2}^{+}} (\Delta_x^g - C_x^g) + (\Delta_y^g - C_y^g)$ par un coefficient de pénalité $-\lambda \; (\lambda \geq 0)$ de notre choix - mais vraisemblablement plus grand en valeur absolue que 1 - de telle sorte que le modèle soit tellement pénalisé par un placement non optimal du groupe que Gurobi a plus qu'intérêt à la rendre égale à 0, c'est-à-dire compacter les groupes de manière optimale._____no_output_____## Nouvelle écriture de la fonction objectif_____no_output_____Compte tenu de toutes les remarques précédentes, on peut donc écrire la version finale de notre fonction objectif qui assure un placement optimal des groupes couplé à un placement à l'avant des passagers en correspondance._____no_output_____$$\boxed{\mbox{Maximize} \; \; \; \sum_{p \in \mathcal{P}_\tau} \left ( 1-K_s\frac{x_p}{\tau_p} \right )- \lambda \sum_{g \in \mathcal{G}_{2}^{+}} \left [ (\Delta_x^g - C_x^g) + (\Delta_y^g - C_y^g) \right ]}$$_____no_output_____

KS = 0.5 # Constante de sortie
LAMBDA = 100 # Coefficient de pénalité_____no_output_____m.setObjective(
    quicksum(1 - KS * quicksum(x * PI[x,y,passager.id_passager] for x,y in avion['seats']['real'])/passager.transit_time for passager in listePassagers if passager.transit_time<float('inf'))
    - LAMBDA * quicksum(DELTA_x[groupe] - constante_x(listeGroupes[groupe].get_nombre_passagers()) for groupe in DELTA_x.keys())
    - LAMBDA * quicksum(DELTA_y[groupe] - constante_y(listeGroupes[groupe].get_nombre_passagers(),listeGroupes[groupe].classe) for groupe in DELTA_y.keys())
    , GRB.MAXIMIZE
)_____no_output_____

# Optimisation_____no_output_____

# -- Paramétrage --
m.params.outputflag = 1

# -- Mise à jour du modèle  --
m.update()Parameter outputflag unchanged
   Value: 1  Min: 0  Max: 1  Default: 1
# Définition du callback
## -> interruption de l'optimization au delà d'un certain temps et en deçà d'un certain seuil

def softtime(model, where):
    if where == GRB.Callback.MIP:
        runtime = model.cbGet(GRB.Callback.RUNTIME)
        objbst = model.cbGet(GRB.Callback.MIP_OBJBST)
        objbnd = model.cbGet(GRB.Callback.MIP_OBJBND)
        gap = abs((objbst - objbnd) / objbst)

        if runtime > 250 :#and gap < 0.375:
            model.terminate()
            _____no_output_____# -- Résolution --
#m.optimize(softtime)
m.optimize()Gurobi Optimizer version 9.1.1 build v9.1.1rc0 (win64)
Thread count: 6 physical cores, 12 logical processors, using up to 12 threads
Optimize a model with 4293 rows, 59700 columns and 392328 nonzeros
Model fingerprint: 0x80b555e7
Variable types: 192 continuous, 59508 integer (59508 binary)
Coefficient statistics:
  Matrix range     [1e+00, 3e+03]
  Objective range  [2e-03, 1e+02]
  Bounds range     [1e+00, 4e+01]
  RHS range        [1e+00, 3e+05]
Presolve removed 4 rows and 24602 columns
Presolve time: 0.83s
Presolved: 4289 rows, 35098 columns, 276430 nonzeros
Variable types: 96 continuous, 35002 integer (34861 binary)

Root relaxation: objective 1.151095e+02, 7445 iterations, 0.76 seconds

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0  115.10949    0  175          -  115.10949      -     -    2s
H    0     0                    -4085.593413  115.10949   103%     -    2s
H    0     0                    -2284.891796  115.10949   105%     -    5s
H    0     0                    -1485.174180  115.10949   108%     -    8s
     0     0  115.10949    0  297 -1485.1742  115.10949   108%     -   10s
H    0     0                    -885.1599787  115.10949   113%     -   16s
     0     0  115.10949    0  299 -885.15998  115.10949   113%     -   16s
     0     0  115.10949    0  121 -885.15998  115.10949   113%     -   20s
     0     0  115.10949    0  160 -885.15998  115.10949   113%     -   21s
     0     0  115.10949    0  138 -885.15998  115.10949   113%     -   25s
     0     0  115.10949    0  174 -885.15998  115.10949   113%     -   25s
     0     0  115.10949    0  123 -885.15998  115.10949   113%     -   28s
     0     0  115.10949    0  194 -885.15998  115.10949   113%     -   30s
     0     0  115.10949    0  125 -885.15998  115.10949   113%     -   32s
     0     0  115.10949    0  125 -885.15998  115.10949   113%     -   34s
     0     2  115.10949    0  120 -885.15998  115.10949   113%     -   39s
     1     3  115.10949    1  110 -885.15998  115.10949   113%   477   41s
    11    14  115.10949    4  211 -885.15998  115.10949   113%   185   47s
    17    19  115.10903    5  358 -885.15998  115.10949   113%   662   74s
    23    25  115.10941    5  221 -885.15998  115.10941   113%  1603   75s
H   29    31                    -485.1605663  115.10941   124%  1286   92s
H   30    31                    -385.1142675  115.10941   130%  1247   92s
    46    47  115.10873    9  228 -385.11427  115.10941   130%   830   96s
    90   150  115.10864   17  233 -385.11427  115.10941   130%   448  108s
H  110   150                    -284.9902175  115.10941   140%   372  108s
H  160   150                    -284.9829809  115.10941   140%   276  108s
   161   261  115.10865   29  223 -284.98298  115.10941   140%   276  117s
   288   336  115.10862   46  237 -284.98298  115.10941   140%   191  142s
H  300   336                    -185.2835070  115.10941   162%   188  142s
H  366   684                    -185.2729337  115.10941   162%   170  167s
H  758   684                      14.5923572  115.10941   689%   128  167s

# Affichage de la solution_____no_output_____Une fois la solution optimale du problème calculée par Gurobi, on commence par récupérer, pour chaque passager, les coordonnées du siège où il est assis à l'aide des variables de décision $\pi_{xyp}$._____no_output_____

placement = {}

for passager in listePassagers:
    for x,y in avion['seats']['real']:
        if PI[x,y,passager.id_passager].x == 1:
            placement[passager.id_passager] = (x,y)

for groupe in listeGroupes.keys():
    print("Groupe #{} (temps de correspondance : {} min):".format(groupe,listeGroupes[groupe].transit_time))
    for passager in listeGroupes[groupe].list_passagers:
        if passager.id_passager in placement.keys():
            coord = placement[passager.id_passager]
            x,y = coord
            print("{} assis sur le siège {} = {}".format(passager.id_passager,coord,coordToSiege(x,y)))
    print("\n")    _____no_output_____

## Permutation des groupes_____no_output_____Conformément au paragraphe détaillant la méthode de *mise sous vide*, nous devons à présent nous assurer que la disposition des membres des groupes est optimale au sein de leurs *sachets* respectifs et opérer des permutatations dans le cas contraire._____no_output_____

for groupe in listeGroupes:
    if listeGroupes[groupe].get_nombre_passagers() >= 4:
        x_min = min([placement[passager.id_passager][0] for passager in listeGroupes[groupe].list_passagers])
        x_max = max([placement[passager.id_passager][0] for passager in listeGroupes[groupe].list_passagers])
        y_min = min([placement[passager.id_passager][1] for passager in listeGroupes[groupe].list_passagers])
        y_max = max([placement[passager.id_passager][1] for passager in listeGroupes[groupe].list_passagers])
        print(x_min,x_max,y_min,y_max)
        cluster = [(i,j) for i in range(x_min,x_max+1) for j in range(y_min,y_max+1)]
        corners = [(x_min,y_min),(x_min,y_max),(x_max,y_min),(x_max,y_max)]

        if listeGroupes[groupe].get_nombre_passagers() != 13: # On gère à part le cas pathologique
            positions = {}
            void_coord = (0,0)
            for i,j in cluster:
                if (i,j) in avion['seats']['fictive'] or sum((passager.idx+1)*PI[i,j,passager.id_passager].x for passager in listePassagers) == groupe + 1:
                    positions[i,j] = 1
                elif sum((passager.idx+1)*PI[i,j,passager.id_passager].x for passager in listePassagers) == 0:
                    positions[i,j] = 0
                    void_coord = (i,j)
                else:
                    positions[i,j] = -1
                    void_coord = (i,j)
            print(positions)
            if void_coord not in corners and void_coord != (0,0):
                i,j = void_coord
                print("hello")
                if positions[i,j] == 0: # S'il y a un vide entre les passagers du groupe
                    if (i,j) in avion['seats']['exit']: # Si le trou est au niveau d'une issue de secours
                        if j <= 4:
                            # On échange avec le passager tout à gauche de la rangée
                            swap_passager = sum((passager.id_passager)*PI[i,y_min,passager.id_passager].x for passager in listePassagers)
                            placement[int(swap_passager)] = (i,j)

                        else:
                            # On échange avec le passager tout à droite de la rangée
                            swap_passager = sum((passager.id_passager)*PI[i,y_max,passager.id_passager].x for passager in listePassagers)
                            placement[int(swap_passager)] = (i,j)

                    else: # Sinon dans le cas général
                        # On échange avec le passager tout en bas à gauche
                        swap_passager = sum((passager.id_passager)*PI[x_max,y_min,passager.id_passager].x for passager in listePassagers)
                        print(swap_passager)
                        placement[int(swap_passager)] = (i,j)

                elif positions[i,j] == 1: # S'il y a un passager qui n'est pas membre du groupe dans le cluster
                    if (i,j) in avion['seats']['exit'] or i == x_min: # Si le trou est au niveau d'une issue de secours ou que le passager a un temps de correspondance inférieur à celui du groupe
                        stranger = sum((passager.id_passager)*PI[i,j,passager.id_passager].x for passager in listePassagers)
                        swap_passager = sum((passager.id_passager)*PI[i,y_min,passager.id_passager].x for passager in listePassagers)
                        placement[int(swap_passager)] = (i,j)
                        placement[int(stranger)] = (i,y_min)
                    else: # Sinon dans le cas général
                        stranger = sum((passager.id_passager)*PI[i,j,passager.id_passager].x for passager in listePassagers)
                        swap_passager = sum((passager.id_passager)*PI[x_max,y_min,passager.id_passager].x for passager in listePassagers)
                        placement[int(swap_passager)] = (i,j)
                        placement[int(stranger)] = (x_max,y_min)

            _____no_output_____

### Export du résultat en CSV_____no_output_____

def exporter_resultats(listeGroupes, placement):
    list_id_groupe = []
    list_id_passager = []
    list_categorie = []
    list_classe = []
    list_transit_time = []
    list_poids = []
    list_x = []
    list_y = []
    list_siege = []
    list_classes = []

    for groupe in listeGroupes.keys():
        for passager in listeGroupes[groupe].list_passagers:
            if passager.id_passager in placement.keys():
                x, y = placement[passager.id_passager]
                siege = coordToSiege(x, y)

                list_id_groupe.append(groupe)
                list_id_passager.append(passager.id_passager)
                list_categorie.append(passager.categorie)
                list_classe.append(passager.classe)
                list_transit_time.append(passager.transit_time)
                list_poids.append(passager.poids)
                list_x.append(x)
                list_y.append(y)
                list_siege.append(siege)
                list_classes.append(get_classe(x, y))

    df = pd.DataFrame({
        'ID Groupe': list_id_groupe,
        'ID Passager': list_id_passager,
        'Catégorie': list_categorie,
        'Classe': list_classe,
        'Transit Time': list_transit_time,
        'Poids': list_poids,
        'x': list_x,
        'y': list_y,
        'Siège': list_siege,
        'Classe': list_classes
    })

    return df_____no_output_____df_ans = exporter_resultats(listeGroupes, placement)_____no_output_____df_ans.to_csv('solution_1.csv', index=False)_____no_output_____

### Affichage de la figure de l'avion_____no_output_____

marker_list = []

for passager in listePassagers:
    if passager.classe == "J":
        dot = "-dot"
    else:
        dot = ""
    if passager.categorie == "femmes":
        marker_list.append('circle'+dot)
    elif passager.categorie == "hommes":
        marker_list.append('hexagon'+dot)
    elif passager.categorie == "enfants":
        marker_list.append('star'+dot)
    elif passager.categorie == "WHCR":
        marker_list.append('star-square'+dot)  _____no_output_____import plotly.graph_objects as go
fig = px.scatter(
    df_ans,
    x='x',
    y='y',
    color='ID Groupe',
    hover_name='Siège',
    size='Poids',
    hover_data=df_ans.columns,
    title=f"Placement de passagers optimal pour l'{AVION} pour le {date[:date.index('N')]} Novembre (contentement général de {m.objVal})",
    template="plotly_white",
    color_continuous_scale=px.colors.diverging.RdBu)

fig.update_traces(marker=dict(line=dict(width=2, color='black')),
                  marker_symbol=marker_list,
                  selector=dict(mode='markers'))

## Ajout du barycentre
fig.add_trace(
    go.Scatter(x=[sum(x*p.poids*PI[x,y,p.id_passager].x for x,y in avion['seats']['real'] for p in listePassagers) / masseTotalePassagers],
               y=[sum(y*p.poids*PI[x,y,p.id_passager].x for x,y in avion['seats']['real'] for p in listePassagers) / masseTotalePassagers],
               name="Barycentre",
               showlegend=True,
               marker_symbol=["star-triangle-up-dot"],
               mode="markers",
               marker=dict(size=20,
                           color="green",
                           line=dict(width=2, color='DarkSlateGrey'))))

# Positionnement de la légende
fig.update_layout(legend=dict(
    orientation="h",
    yanchor="bottom",
    y=1.02,
    xanchor="right",
    x=1
))

## Ajout des barres colorées pour visualiser les emplacements spécifiques de l'avion
#Issues de Secours
fig.add_shape(x0=10.5,
              x1=12.5,
              y0=0.5,
              y1=0.6,
              fillcolor="red",
              line=dict(width=0),
              opacity=0.25)
fig.add_shape(x0=10.5,
              x1=12.5,
              y0=7.4,
              y1=7.5,
              fillcolor="red",
              line=dict(width=0),
              opacity=0.25)

#Classe Business
fig.add_shape(x0=0.5,
              x1=9.5,
              y0=0.5,
              y1=0.6,
              fillcolor="orange",
              line=dict(width=0),
              opacity=0.25)
fig.add_shape(x0=0.5,
              x1=9.5,
              y0=7.4,
              y1=7.5,
              fillcolor="orange",
              line=dict(width=0),
              opacity=0.25)

#Classe Economie
fig.add_shape(x0=9.5,
              x1=10.5,
              y0=0.5,
              y1=0.6,
              fillcolor="blue",
              line=dict(width=0),
              opacity=0.25)
fig.add_shape(x0=9.5,
              x1=10.5,
              y0=7.4,
              y1=7.5,
              fillcolor="blue",
              line=dict(width=0),
              opacity=0.25)
fig.add_shape(x0=12.5,
              x1=28.5,
              y0=0.5,
              y1=0.6,
              fillcolor="blue",
              line=dict(width=0),
              opacity=0.25)
fig.add_shape(x0=12.5,
              x1=28.5,
              y0=7.4,
              y1=7.5,
              fillcolor="blue",
              line=dict(width=0),
              opacity=0.25)

# Add images
fig.add_layout_image(avion['background'])

fig.show()_____no_output_____