Spaces:
Sleeping
Sleeping
v0.2 Material template et généralisation usagers destination
Browse files- CHANGELOG.md +5 -2
- app.py +26 -22
- vrp.py +2 -3
CHANGELOG.md
CHANGED
|
@@ -6,10 +6,13 @@ The format is based on [Keep a Changelog],
|
|
| 6 |
and this project adheres to [Semantic Versioning].
|
| 7 |
|
| 8 |
|
| 9 |
-
## [0.
|
| 10 |
|
| 11 |
### Added
|
| 12 |
-
Ce CHANGELOG.md...
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
### Changed
|
| 15 |
|
|
|
|
| 6 |
and this project adheres to [Semantic Versioning].
|
| 7 |
|
| 8 |
|
| 9 |
+
## [0.2.0] - 2025-10-10
|
| 10 |
|
| 11 |
### Added
|
| 12 |
+
* Ce CHANGELOG.md...
|
| 13 |
+
* Généralisation à "Destinations" et "Usagers"
|
| 14 |
+
* Ajout d'un thème "Material template"
|
| 15 |
+
* Versionnage
|
| 16 |
|
| 17 |
### Changed
|
| 18 |
|
app.py
CHANGED
|
@@ -39,7 +39,7 @@ def cree_adresse(df, cols):
|
|
| 39 |
return df[cols].astype(str).agg(' '.join, axis=1).str.strip()
|
| 40 |
|
| 41 |
|
| 42 |
-
def geocode(adresse,
|
| 43 |
r=requests.get(url="https://data.geopf.fr/geocodage/search",
|
| 44 |
params={"q": adresse})
|
| 45 |
if not r.ok:
|
|
@@ -47,15 +47,17 @@ def geocode(adresse, return_dict=True):
|
|
| 47 |
try:
|
| 48 |
js=r.json()
|
| 49 |
feature=js['features'][0]
|
| 50 |
-
if
|
|
|
|
|
|
|
| 51 |
return {js["query"] : feature}
|
| 52 |
-
return
|
| 53 |
except:
|
| 54 |
return None
|
| 55 |
|
| 56 |
|
| 57 |
def add_coo(adresse, details=False):
|
| 58 |
-
feature=geocode(adresse)
|
| 59 |
coo=shapely.Point(feature['geometry']['coordinates'])
|
| 60 |
score=feature['properties']['score']
|
| 61 |
suggestion=feature['properties']['label']
|
|
@@ -73,13 +75,13 @@ class Tableur():
|
|
| 73 |
self.dfeuilles={}
|
| 74 |
self.df=pd.DataFrame()
|
| 75 |
self.dmime={'csv': 'text/csv', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
|
| 76 |
-
self.feuille=pn.widgets.Select(name='Feuille de calcul', disabled=True)
|
| 77 |
-
self.ecole_col=pn.widgets.Select(name="Colonne identifiant
|
| 78 |
-
self.adr_col=pn.widgets.MultiChoice(name="Colonnes composant l'adresse", disabled=True)
|
| 79 |
self.tabd=pn.widgets.Tabulator(height=1000,
|
| 80 |
-
formatters={"
|
| 81 |
-
editors={'
|
| 82 |
-
widths={'
|
| 83 |
page_size=50,
|
| 84 |
)
|
| 85 |
self.file_dropper = pn.widgets.FileDropper(accepted_filetypes=list(self.dmime.values()))
|
|
@@ -121,14 +123,14 @@ class Tableur():
|
|
| 121 |
def lancer_calculs(self, _):
|
| 122 |
self.df["adresse"]=cree_adresse(self.df, self.adr_col.value)
|
| 123 |
self.df["geometry"], self.df["suggestion"], self.df["score"] = zip(*self.df.adresse.apply(add_coo, details=True))
|
| 124 |
-
display_cols= self.adr_col.value + ["suggestion", "score", self.ecole_col.value, "
|
| 125 |
if self.delivery_tab is None:
|
| 126 |
auj9=dateparser.parse("aujourd'hui 9:00")
|
| 127 |
-
self.df["
|
| 128 |
else:
|
| 129 |
-
self.delivery_tab.df["
|
| 130 |
self.df=self.df.merge(
|
| 131 |
-
self.delivery_tab.df[[self.delivery_tab.ecole_col.value, "geometry", "
|
| 132 |
left_on=self.ecole_col.value, right_on=self.delivery_tab.ecole_col.value,
|
| 133 |
how="left", suffixes=["_p", "_d"]
|
| 134 |
)
|
|
@@ -136,7 +138,7 @@ class Tableur():
|
|
| 136 |
self.df.index = self.idx_offset+np.arange(len(self.df))
|
| 137 |
self.tabd.value=self.df[display_cols]
|
| 138 |
self.tabd.style.background_gradient(subset=["score"], cmap="RdYlGn", vmin=0.5, vmax=1.0)\
|
| 139 |
-
.map(lambda v: '' if v == v else 'background-color:grey;', subset=["indice école", "
|
| 140 |
self.tabd.param.trigger("value")
|
| 141 |
return None
|
| 142 |
|
|
@@ -275,7 +277,7 @@ def tous_calculs(_):
|
|
| 275 |
sol=calcul_routes(dfp, ladr, mat_dist, mat_dur, duree_taxi.value, duree_ecole.value, capacité.value)
|
| 276 |
#print(sol.to_dict()["summary"])
|
| 277 |
|
| 278 |
-
gpu=dfp.loc[list({u["id"] for u in sol.to_dict()["unassigned"]})][['suggestion', 'indice école', 'ECOLE', '
|
| 279 |
gpu["Distance (km)"]=gpu.apply(lambda row: mat_dist[ladr.index(row["suggestion"])][ladr.index( row["ECOLE"])], axis=1)
|
| 280 |
gpu["Durée (min)"]=gpu.apply(lambda row: mat_dur[ladr.index( row["suggestion"])][ladr.index( row["ECOLE"])]//60, axis=1)
|
| 281 |
tabs.append(('Non assignés', pn.widgets.Tabulator(gpu, disabled=True, height=1000)))
|
|
@@ -296,9 +298,11 @@ def tous_calculs(_):
|
|
| 296 |
bouton=pn.widgets.Button(name="Lancer les calculs", button_type="primary", on_click=tous_calculs)
|
| 297 |
|
| 298 |
tabs=pn.Tabs(
|
| 299 |
-
('
|
| 300 |
-
('
|
| 301 |
-
('Calculs des routes', pn.Column(duree_taxi, duree_ecole, capacité, bouton) ),
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
| 39 |
return df[cols].astype(str).agg(' '.join, axis=1).str.strip()
|
| 40 |
|
| 41 |
|
| 42 |
+
def geocode(adresse, as_point=False, as_dict=False):
|
| 43 |
r=requests.get(url="https://data.geopf.fr/geocodage/search",
|
| 44 |
params={"q": adresse})
|
| 45 |
if not r.ok:
|
|
|
|
| 47 |
try:
|
| 48 |
js=r.json()
|
| 49 |
feature=js['features'][0]
|
| 50 |
+
if as_point:
|
| 51 |
+
return shapely.Point(feature['geometry']['coordinates'])
|
| 52 |
+
if as_dict:
|
| 53 |
return {js["query"] : feature}
|
| 54 |
+
return feature
|
| 55 |
except:
|
| 56 |
return None
|
| 57 |
|
| 58 |
|
| 59 |
def add_coo(adresse, details=False):
|
| 60 |
+
feature=geocode(adresse)
|
| 61 |
coo=shapely.Point(feature['geometry']['coordinates'])
|
| 62 |
score=feature['properties']['score']
|
| 63 |
suggestion=feature['properties']['label']
|
|
|
|
| 75 |
self.dfeuilles={}
|
| 76 |
self.df=pd.DataFrame()
|
| 77 |
self.dmime={'csv': 'text/csv', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
|
| 78 |
+
self.feuille=pn.widgets.Select(name='Feuille de calcul', disabled=True, width=200)
|
| 79 |
+
self.ecole_col=pn.widgets.Select(name="Colonne identifiant la destination", disabled=True, width=200)
|
| 80 |
+
self.adr_col=pn.widgets.MultiChoice(name="Colonnes composant l'adresse", disabled=True, width=200)
|
| 81 |
self.tabd=pn.widgets.Tabulator(height=1000,
|
| 82 |
+
formatters={"arrivée_max" : DateFormatter(format="%H:%M")},
|
| 83 |
+
editors={'arrivée_max': 'datetime'},
|
| 84 |
+
widths={'arrivée_max': 160},
|
| 85 |
page_size=50,
|
| 86 |
)
|
| 87 |
self.file_dropper = pn.widgets.FileDropper(accepted_filetypes=list(self.dmime.values()))
|
|
|
|
| 123 |
def lancer_calculs(self, _):
|
| 124 |
self.df["adresse"]=cree_adresse(self.df, self.adr_col.value)
|
| 125 |
self.df["geometry"], self.df["suggestion"], self.df["score"] = zip(*self.df.adresse.apply(add_coo, details=True))
|
| 126 |
+
display_cols= self.adr_col.value + ["suggestion", "score", self.ecole_col.value, "arrivée_max"]
|
| 127 |
if self.delivery_tab is None:
|
| 128 |
auj9=dateparser.parse("aujourd'hui 9:00")
|
| 129 |
+
self.df["arrivée_max"] = (auj9+ (self.df[self.ecole_col.value].str.split().str[0].isin({'Collège', 'Lycée'}))*timedelta(hours=-0.5))
|
| 130 |
else:
|
| 131 |
+
self.delivery_tab.df["arrivée_max"]=self.delivery_tab.tabd.value["arrivée_max"]
|
| 132 |
self.df=self.df.merge(
|
| 133 |
+
self.delivery_tab.df[[self.delivery_tab.ecole_col.value, "geometry", "arrivée_max"]].reset_index(names="indice école"),
|
| 134 |
left_on=self.ecole_col.value, right_on=self.delivery_tab.ecole_col.value,
|
| 135 |
how="left", suffixes=["_p", "_d"]
|
| 136 |
)
|
|
|
|
| 138 |
self.df.index = self.idx_offset+np.arange(len(self.df))
|
| 139 |
self.tabd.value=self.df[display_cols]
|
| 140 |
self.tabd.style.background_gradient(subset=["score"], cmap="RdYlGn", vmin=0.5, vmax=1.0)\
|
| 141 |
+
.map(lambda v: '' if v == v else 'background-color:grey;', subset=["indice école", "arrivée_max"])
|
| 142 |
self.tabd.param.trigger("value")
|
| 143 |
return None
|
| 144 |
|
|
|
|
| 277 |
sol=calcul_routes(dfp, ladr, mat_dist, mat_dur, duree_taxi.value, duree_ecole.value, capacité.value)
|
| 278 |
#print(sol.to_dict()["summary"])
|
| 279 |
|
| 280 |
+
gpu=dfp.loc[list({u["id"] for u in sol.to_dict()["unassigned"]})][['suggestion', 'indice école', 'ECOLE', 'arrivée_max']].copy()
|
| 281 |
gpu["Distance (km)"]=gpu.apply(lambda row: mat_dist[ladr.index(row["suggestion"])][ladr.index( row["ECOLE"])], axis=1)
|
| 282 |
gpu["Durée (min)"]=gpu.apply(lambda row: mat_dur[ladr.index( row["suggestion"])][ladr.index( row["ECOLE"])]//60, axis=1)
|
| 283 |
tabs.append(('Non assignés', pn.widgets.Tabulator(gpu, disabled=True, height=1000)))
|
|
|
|
| 298 |
bouton=pn.widgets.Button(name="Lancer les calculs", button_type="primary", on_click=tous_calculs)
|
| 299 |
|
| 300 |
tabs=pn.Tabs(
|
| 301 |
+
('Destinations', tabd.get_panel()),
|
| 302 |
+
('Usagers', tabp.get_panel()),
|
| 303 |
+
('Calculs des routes', pn.Column(duree_taxi, duree_ecole, capacité, bouton) ), )
|
| 304 |
+
|
| 305 |
+
template= pn.template.MaterialTemplate(title="Problème de tournée des véhicules avec capacités et fenêtre temporelle")
|
| 306 |
+
template.main.append(tabs)
|
| 307 |
+
template.header.append("v0.2.0")
|
| 308 |
+
template.servable()
|
vrp.py
CHANGED
|
@@ -13,14 +13,13 @@ def calcul_routes(dfp, ladr, mat_dist, mat_dur, durée_max_taxi=45, durée_max_e
|
|
| 13 |
def tw(fin, delta):
|
| 14 |
return [TimeWindow(deltaepoch(fin, delta), deltaepoch(fin))]
|
| 15 |
|
| 16 |
-
|
| 17 |
for (irow, row) in dfp.iterrows():
|
| 18 |
idxp=ladr.index(row["suggestion"])
|
| 19 |
idxd=ladr.index(row["ECOLE"])
|
| 20 |
vrm.add_job(
|
| 21 |
Shipment(
|
| 22 |
-
pickup=ShipmentStep(id=irow, location=idxp, time_windows=tw(row["
|
| 23 |
-
delivery=ShipmentStep(id=irow, location=idxd, time_windows=tw(row["
|
| 24 |
amount=[1],
|
| 25 |
# skills
|
| 26 |
)
|
|
|
|
| 13 |
def tw(fin, delta):
|
| 14 |
return [TimeWindow(deltaepoch(fin, delta), deltaepoch(fin))]
|
| 15 |
|
|
|
|
| 16 |
for (irow, row) in dfp.iterrows():
|
| 17 |
idxp=ladr.index(row["suggestion"])
|
| 18 |
idxd=ladr.index(row["ECOLE"])
|
| 19 |
vrm.add_job(
|
| 20 |
Shipment(
|
| 21 |
+
pickup=ShipmentStep(id=irow, location=idxp, time_windows=tw(row["arrivée_max"], durée_max_taxi)),
|
| 22 |
+
delivery=ShipmentStep(id=irow, location=idxd, time_windows=tw(row["arrivée_max"], durée_max_ecole)),
|
| 23 |
amount=[1],
|
| 24 |
# skills
|
| 25 |
)
|