Skip to content

Commit 55dc235

Browse files
authored
Update notebook on rues de paris (#31)
* Add notebook on Chinese Postman * Add algorithm to solve shortest path in paris * ruff * fix ut * fix ut * doc * update notebook * fix unit tests * fix euler path * fix unit tests * skip unit test * fix unit tests
1 parent 713b84d commit 55dc235

File tree

7 files changed

+368
-393
lines changed

7 files changed

+368
-393
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ _doc/examples/data/*.optimized.onnx
2828
_doc/examples/*.html
2929
_doc/_static/require.js
3030
_doc/_static/viz.js
31+
_doc/practice/algo-compose/paris_54000.*

_doc/practice/algo-compose/paris_parcours.ipynb

Lines changed: 258 additions & 331 deletions
Large diffs are not rendered by default.

_unittests/ut_xrun_doc/test_documentation_examples.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,24 +42,19 @@ def run_test(self, fold: str, name: str, verbose=0) -> int:
4242
res = p.communicate()
4343
out, err = res
4444
st = err.decode("ascii", errors="ignore")
45+
if "No such file or directory" in st:
46+
raise FileNotFoundError(st)
4547
if len(st) > 0 and "Traceback" in st:
4648
if '"dot" not found in path.' in st:
4749
# dot not installed, this part
4850
# is tested in onnx framework
4951
if verbose:
5052
print(f"failed: {name!r} due to missing dot.")
5153
return -1
52-
if "No such file or directory: 'schema_pb2.py'" in str(st):
53-
if verbose:
54-
print(
55-
f"failed: {name!r} due to missing protoc "
56-
f"(or wrong version)."
57-
)
58-
return -1
5954
raise AssertionError(
60-
"Example '{}' (cmd: {} - exec_prefix='{}') "
61-
"failed due to\n{}"
62-
"".format(name, cmds, sys.exec_prefix, st)
55+
f"Example {name!r} (cmd: {cmds!r} - "
56+
f"exec_prefix={sys.exec_prefix!r}) "
57+
f"failed due to\n{st}"
6358
)
6459
dt = time.perf_counter() - perf
6560
if verbose:
@@ -75,9 +70,20 @@ def add_test_methods(cls):
7570
if name.startswith("plot_") and name.endswith(".py"):
7671
short_name = os.path.split(os.path.splitext(name)[0])[-1]
7772

78-
def _test_(self, name=name):
79-
res = self.run_test(fold, name, verbose=VERBOSE)
80-
self.assertIn(res, (-1, 1))
73+
if sys.platform == "win32" and (
74+
"protobuf" in name or "td_note_2021" in name
75+
):
76+
77+
@unittest.skip("notebook with questions or issues with windows")
78+
def _test_(self, name=name):
79+
res = self.run_test(fold, name, verbose=VERBOSE)
80+
self.assertIn(res, (-1, 1))
81+
82+
else:
83+
84+
def _test_(self, name=name):
85+
res = self.run_test(fold, name, verbose=VERBOSE)
86+
self.assertIn(res, (-1, 1))
8187

8288
setattr(cls, f"test_{short_name}", _test_)
8389

_unittests/ut_xrun_doc/test_documentation_notebook.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import sys
44
import importlib
55
import subprocess
6-
import tempfile
76
import time
87
from nbconvert import PythonExporter
98
from teachpyx import __file__ as teachpyx_file
@@ -48,31 +47,40 @@ def run_test(self, nb_name: str, verbose=0) -> int:
4847
content = self.post_process(exporter.from_filename(nb_name)[0])
4948
bcontent = content.encode("utf-8")
5049

51-
with tempfile.NamedTemporaryFile(suffix=".py") as tmp:
52-
self.assertEndsWith(tmp.name, ".py")
53-
tmp.write(bcontent)
54-
tmp.seek(0)
50+
tmp = "temp_notebooks"
51+
if not os.path.exists(tmp):
52+
os.mkdir(tmp)
53+
# with tempfile.NamedTemporaryFile(suffix=".py") as tmp:
54+
name = os.path.splitext(os.path.split(nb_name)[-1])[0]
55+
if os.path.exists(tmp):
56+
tmp_name = os.path.join(tmp, name + ".py")
57+
self.assertEndsWith(tmp_name, ".py")
58+
with open(tmp_name, "wb") as f:
59+
f.write(bcontent)
5560

56-
fold, name = os.path.split(tmp.name)
61+
fold, name = os.path.split(tmp_name)
5762

5863
try:
5964
mod = import_source(fold, os.path.splitext(name)[0])
6065
assert mod is not None
6166
except (FileNotFoundError, RuntimeError):
6267
# try another way
63-
cmds = [sys.executable, "-u", name]
68+
cmds = [sys.executable, "-u", tmp_name]
6469
p = subprocess.Popen(
6570
cmds, stdout=subprocess.PIPE, stderr=subprocess.PIPE
6671
)
6772
res = p.communicate()
6873
out, err = res
6974
st = err.decode("ascii", errors="ignore")
75+
if "No such file or directory" in st:
76+
raise FileNotFoundError(st)
7077
if len(st) > 0 and "Traceback" in st:
71-
raise AssertionError(
78+
msg = (
7279
f"Example {nb_name!r} (cmd: {cmds} - "
7380
f"exec_prefix={sys.exec_prefix!r}) "
74-
f"failed due to\n{st}\n-----\n{content}"
81+
f"failed due to\n{st}"
7582
)
83+
raise AssertionError(msg)
7684

7785
dt = time.perf_counter() - perf
7886
if verbose:
@@ -84,14 +92,23 @@ def add_test_methods_path(cls, fold):
8492
found = os.listdir(fold)
8593
last = os.path.split(fold)[-1]
8694
for name in found:
87-
if "interro_rapide_" in name:
88-
continue
8995
if name.endswith(".ipynb"):
9096
fullname = os.path.join(fold, name)
97+
if "interro_rapide_" in name or (
98+
sys.platform == "win32"
99+
and ("protobuf" in name or "td_note_2021" in name)
100+
):
91101

92-
def _test_(self, fullname=fullname):
93-
res = self.run_test(fullname, verbose=VERBOSE)
94-
self.assertIn(res, (-1, 1))
102+
@unittest.skip("notebook with questions or issues with windows")
103+
def _test_(self, fullname=fullname):
104+
res = self.run_test(fullname, verbose=VERBOSE)
105+
self.assertIn(res, (-1, 1))
106+
107+
else:
108+
109+
def _test_(self, fullname=fullname):
110+
res = self.run_test(fullname, verbose=VERBOSE)
111+
self.assertIn(res, (-1, 1))
95112

96113
lasts = last.replace("-", "_")
97114
names = os.path.splitext(name)[0].replace("-", "_")
@@ -104,6 +121,7 @@ def add_test_methods(cls):
104121
os.path.join(this, "..", "..", "_doc", "practice", "exams"),
105122
os.path.join(this, "..", "..", "_doc", "practice", "py-base"),
106123
os.path.join(this, "..", "..", "_doc", "practice", "algo-base"),
124+
os.path.join(this, "..", "..", "_doc", "practice", "algo-compose"),
107125
]
108126
for fold in folds:
109127
cls.add_test_methods_path(os.path.normpath(fold))

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ install:
1515
build: off
1616

1717
test_script:
18-
- "%PYTHON%\\python -m pytest _unittests"
18+
- "%PYTHON%\\python -m pytest _unittests -v"
1919

2020
after_test:
2121
- "%PYTHON%\\python -u setup.py bdist_wheel"

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ lifelines
1111
matplotlib
1212
mutagen # mp3
1313
nbsphinx
14+
networkx
1415
pandas
1516
pillow
1617
protobuf<4

teachpyx/practice/rues_paris.py

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ def get_data(
4646
:param dest: répertoire dans lequel télécharger les données
4747
:param timeout: timeout (seconds) when estabishing the connection
4848
:param verbose: affiche le progrès
49-
:param keep: garde tout si la valeur est -1, sinon garde les 1000 premières rues
49+
:param keep: garde tout si la valeur est -1,
50+
sinon garde les 1000 premières rues, ces rues sont choisies
51+
de façon à construire un ensemble connexe
5052
:return: liste d'arcs
5153
5254
Un arc est défini par un 6-uple contenant les informations suivantes :
@@ -88,22 +90,38 @@ def get_data(
8890
pairs[p] = True
8991

9092
if keep is not None:
91-
short_edges = edges[:keep]
9293
new_vertices = {}
93-
edges = []
94-
for edge in short_edges:
95-
p1, p2 = edge[-3:-1]
96-
if p1 not in new_vertices:
97-
new_vertices[p1] = len(new_vertices)
98-
if p2 not in new_vertices:
99-
new_vertices[p2] = len(new_vertices)
100-
i1, i2 = new_vertices[p1], new_vertices[p2]
101-
edges.append((i1, i2, edge[2], p1, p2, edge[-1]))
94+
already_added = set()
95+
new_edges = []
96+
for _ in range(0, int(keep**0.5) + 1):
97+
for edge in edges:
98+
if edge[:2] in already_added:
99+
continue
100+
p1, p2 = edge[-3:-1]
101+
if (
102+
len(new_vertices) > 0
103+
and p1 not in new_vertices
104+
and p2 not in new_vertices
105+
):
106+
# On considère des rues connectées à des rues déjà sélectionnées.
107+
continue
108+
if p1 not in new_vertices:
109+
new_vertices[p1] = len(new_vertices)
110+
if p2 not in new_vertices:
111+
new_vertices[p2] = len(new_vertices)
112+
i1, i2 = new_vertices[p1], new_vertices[p2]
113+
new_edges.append((i1, i2, edge[2], p1, p2, edge[-1]))
114+
already_added.add(edge[:2])
115+
if len(new_edges) >= keep:
116+
break
117+
if len(new_edges) >= keep:
118+
break
102119
items = [(v, i) for i, v in new_vertices.items()]
103120
items.sort()
104121
vertices = [_[1] for _ in items]
122+
edges = new_edges
105123

106-
return edges
124+
return edges, vertices
107125

108126

109127
def graph_degree(
@@ -321,7 +339,7 @@ def eulerien_extension(
321339
totali = 0
322340
while len(allow) > 0:
323341
if verbose:
324-
print(f"------- nb odd vertices {len(allow)} iteration {totali}")
342+
print(f"------- # odd vertices {len(allow)} iteration {totali}")
325343
allowset = set(allow)
326344
init = bellman(
327345
edges,
@@ -384,9 +402,9 @@ def euler_path(
384402
edges_from = {}
385403
somme = 0
386404
for e in edges:
387-
k = e[:2]
388-
v = e[-1]
389-
alledges[k] = ["street"] + list(k + (v,))
405+
k = e[:2] # indices des noeuds
406+
v = e[-1] # distance
407+
alledges[k] = ["street", *k, v]
390408
a, b = k
391409
alledges[b, a] = alledges[a, b]
392410
if a not in edges_from:
@@ -398,10 +416,10 @@ def euler_path(
398416
somme += v
399417

400418
for e in added_edges: # il ne faut pas enlever les doublons
401-
k = e[:2]
402-
v = e[-1]
419+
k = e[:2] # indices ds noeuds
420+
v = e[-1] # distance
403421
a, b = k
404-
alledges[k] = ["jump"] + list(k + (v,))
422+
alledges[k] = ["jump", *k, v]
405423
alledges[b, a] = alledges[a, b]
406424
if a not in edges_from:
407425
edges_from[a] = []
@@ -411,39 +429,43 @@ def euler_path(
411429
edges_from[b].append(alledges[a, b])
412430
somme += v
413431

414-
degre = {}
415-
for a, v in edges_from.items():
416-
t = len(v)
417-
degre[t] = degre.get(t, 0) + 1
418-
419-
two = [a for a, v in edges_from.items() if len(v) == 2]
432+
# les noeuds de degré impair
420433
odd = [a for a, v in edges_from.items() if len(v) % 2 == 1]
421434
if len(odd) > 0:
422-
raise ValueError("some vertices have an odd degree")
435+
raise ValueError("Some vertices have an odd degree.")
436+
# les noeuds de degré 2, on les traverse qu'une fois
437+
two = [a for a, v in edges_from.items() if len(v) == 2]
423438
begin = two[0]
424439

425440
# checking
426441
for v, le in edges_from.items():
442+
# v est une extrémité
427443
for e in le:
444+
# to est l'autre extrémité
428445
to = e[1] if v != e[1] else e[2]
429446
if to not in edges_from:
430-
raise RuntimeError(
431-
"unable to find vertex {0} for edge {0},{1}".format(to, v)
432-
)
447+
raise RuntimeError(f"Unable to find vertex {to} for edge {to},{v}")
433448
if to == v:
434-
raise RuntimeError(f"circular edge {to}")
449+
raise RuntimeError(f"Circular edge {to}")
435450

436-
# loop
451+
# On sait qu'il existe un chemin. La fonction explore les arcs
452+
# jusqu'à revenir à son point de départ. Elle supprime les arcs
453+
# utilisées de edges_from.
437454
path = _explore_path(edges_from, begin)
438-
for p in path:
439-
if len(p) == 0:
440-
raise RuntimeError("this exception should not happen")
455+
456+
# Il faut s'assurer que le chemin ne contient pas de boucles non visitées.
441457
while len(edges_from) > 0:
458+
# Il reste des arcs non visités. On cherche le premier
459+
# arc connecté au chemin existant.
442460
start = None
443461
for i, p in enumerate(path):
444462
if p[0] in edges_from:
445463
start = i, p
446464
break
465+
if start is None:
466+
raise RuntimeError(
467+
f"start should not be None\npath={path}\nedges_from={edges_from}"
468+
)
447469
sub = _explore_path(edges_from, start[1][0])
448470
i = start[0]
449471
path[i : i + 1] = path[i : i + 1] + sub

0 commit comments

Comments
 (0)