From 0503034e4087735eb9e308c02e71159d937120c9 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart" <git@edgarpierre.fr>
Date: Thu, 27 Feb 2025 17:32:53 +0100
Subject: [PATCH 1/2] Update workflow configuration and replace Altair with
 Matplotlib for signal visualizations

---
 .forgejo/workflows/serve.yaml |   6 +-
 cours/SIN/01-capteurs.md      |   2 +-
 cours/SIN/02-signaux.md       | 117 +++++++++++++++++-----------------
 cours/SIN/matplotlibrc        |   1 +
 matplotlibrc                  |  43 +++++++++++++
 requirements.txt              |   7 ++
 6 files changed, 115 insertions(+), 61 deletions(-)
 create mode 120000 cours/SIN/matplotlibrc
 create mode 100644 matplotlibrc
 create mode 100644 requirements.txt

diff --git a/.forgejo/workflows/serve.yaml b/.forgejo/workflows/serve.yaml
index 247f473..5c43a11 100644
--- a/.forgejo/workflows/serve.yaml
+++ b/.forgejo/workflows/serve.yaml
@@ -9,12 +9,12 @@ jobs:
       - name: Checkout repository
         uses: actions/checkout@v4
       - name: Initialize virtual environment
-        run: /usr/bin/python -m venv .env
+        run: /usr/bin/python -m venv .venv
       - name: Install dependencies
-        run: ./.env/bin/pip install mystmd jupyter jupyterlab_myst ipykernel altair pandas
+        run: ./.venv/bin/pip install -r requirements.txt
       - name: Build static HTML
         run: |
-          . .env/bin/activate
+          . .venv/bin/activate
           myst build --execute --html
       - name: Copy files
         run: |
diff --git a/cours/SIN/01-capteurs.md b/cours/SIN/01-capteurs.md
index 0c3deb9..77bd675 100644
--- a/cours/SIN/01-capteurs.md
+++ b/cours/SIN/01-capteurs.md
@@ -60,7 +60,7 @@ Une thermistance (@thermistance) ou une jauge de déformation (@jauge) sont des
 capteurs analogiques.
 
 ::::{figure}
-:label: analogique
+:label: cap_analogique
 :::{figure} https://upload.wikimedia.org/wikipedia/commons/3/3b/NTC_bead.jpg
 :label: thermistance
 
diff --git a/cours/SIN/02-signaux.md b/cours/SIN/02-signaux.md
index 919c48e..039f085 100644
--- a/cours/SIN/02-signaux.md
+++ b/cours/SIN/02-signaux.md
@@ -32,32 +32,26 @@ et un niveau **bas** ("Low").
 :label: logique
 ```{code-cell} python
 :tags: [remove-input]
-import altair as alt
+import matplotlib.pyplot as plt
+from matplotlib import ticker
 import numpy as np
-import pandas as pd
 
 rng = np.random.default_rng(25)
 
 n = 16
 t = np.arange(n+1)
-s = rng.choice([0, 1], n+1)
-s[-1] = s[-2]
-data = pd.DataFrame({
-  "t": t,
-  "s": s,
-})
-alt.Chart(
-  data
-).mark_line(
-  interpolate="step-after",
-  strokeWidth=3,
-).encode(
-  alt.X("t:Q").axis(title="Temps (s)").scale(domain=(0,n)),
-  alt.Y("s:Q", axis=alt.Axis(title="Signal logique", values=[0, 1], format=".0f")).scale(domain=(0,1)),
-).properties(
-  width="container",
-  height=100,
+s = rng.choice([0, 1], n)
+
+fig, ax = plt.subplots()
+ax.stairs(s, t, lw=3)
+ax.set(
+  xlim=(0, n),
+  ylim=(-.5, 1.5),
+  xlabel="Temps (s)",
+  ylabel="Signal logique",
 )
+ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
+ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
 ```
 Exemple de signal logique
 ````
@@ -77,35 +71,38 @@ Un exemple de signal analogique est donné en @analogique.
 :label: analogique
 ```{code-cell} python
 :tags: [remove-input]
-import altair as alt
+import matplotlib.pyplot as plt
+from matplotlib import ticker
 import numpy as np
-import pandas as pd
+from scipy.interpolate import CubicSpline
+from scipy.stats import qmc
+
 
 rng = np.random.default_rng(25)
 
 n = 20
 t_max = 16
 
-t = np.linspace(0, t_max, n)
-
+t_base = np.linspace(0, t_max, n)
+lhs = (qmc.LatinHypercube(d=n-2, rng=rng).random(1)[0] - .5) * t_max/n
+t = t_base + np.concatenate(([0], lhs, [0]))
+t = t_base
 s = 5 * rng.random(n)
 s[-1] = s[-2]
-data = pd.DataFrame({
-  "t": t,
-  "s": s,
-})
-alt.Chart(
-  data
-).mark_line(
-  interpolate="basis",
-  strokeWidth=3,
-).encode(
-  alt.X("t:Q").axis(title="Temps (s)").scale(domain=(0,t_max)),
-  alt.Y("s:Q", axis=alt.Axis(title="Signal analogique")).scale(domain=(0,5)),
-).properties(
-  width="container",
-  height=200,
+
+t_interp = np.linspace(0, t_max, 1024)
+s_interp = np.clip(CubicSpline(t, s)(t_interp), 0, 5)
+
+fig, ax = plt.subplots()
+ax.plot(t_interp, s_interp, lw=3)
+ax.set(
+  xlim=(0, t_max),
+  ylim=(-.5, 5.5),
+  xlabel="Temps (s)",
+  ylabel="Signal analogique",
 )
+ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
+ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
 ```
 Exemple de signal analogique
 ````
@@ -119,32 +116,38 @@ Un exemple de signal analogique est donné en @numerique.
 :label: numerique
 ```{code-cell} python
 :tags: [remove-input]
-import altair as alt
+import matplotlib.pyplot as plt
+from matplotlib import ticker
 import numpy as np
-import pandas as pd
 
 rng = np.random.default_rng(25)
 
 n = 16
 t = np.arange(n+1)
-s = rng.integers(0, 16, n+1)
-s[-1] = s[-2]
-data = pd.DataFrame({
-  "t": t,
-  "s": s,
-})
-alt.Chart(
-  data
-).mark_line(
-  interpolate="step-after",
-  strokeWidth=3,
-).encode(
-  alt.X("t:Q").axis(title="Temps (s)").scale(domain=(0,n)),
-  alt.Y("s:Q", axis=alt.Axis(title="Signal numérique", values=np.arange(0, 16))).scale(domain=(0,15)),
-).properties(
-  width="container",
-  height=200,
+s = rng.integers(0, 16, n)
+
+fig, ax = plt.subplots()
+ax.stairs(s, t, lw=3)
+ax.set(
+  xlim=(0, n),
+  ylim=(-.5, 16.5),
+  xlabel="Temps (s)",
+  ylabel="Signal numérique",
 )
+ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
+ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
+# alt.Chart(
+#   data
+# ).mark_line(
+#   interpolate="step-after",
+#   strokeWidth=3,
+# ).encode(
+#   alt.X("t:Q").axis(title="Temps (s)").scale(domain=(0,n)),
+#   alt.Y("s:Q", axis=alt.Axis(title="Signal numérique", values=np.arange(0, 16))).# scale(domain=(0,15)),
+# ).properties(
+#   width="container",
+#   height=200,
+# )
 ```
 Exemple de signal numérique
 ````
\ No newline at end of file
diff --git a/cours/SIN/matplotlibrc b/cours/SIN/matplotlibrc
new file mode 120000
index 0000000..b48e529
--- /dev/null
+++ b/cours/SIN/matplotlibrc
@@ -0,0 +1 @@
+../../matplotlibrc
\ No newline at end of file
diff --git a/matplotlibrc b/matplotlibrc
new file mode 100644
index 0000000..6075cf1
--- /dev/null
+++ b/matplotlibrc
@@ -0,0 +1,43 @@
+lines.linewidth: 3
+
+font.family: Fira Code
+
+image.cmap: inferno
+
+axes.linewidth: 1
+axes.grid: True
+axes.grid.which: major
+axes.titlelocation: right
+axes.titleweight: 700
+axes.axisbelow: True
+
+axes.prop_cycle: cycler(color=["#4269d0","#efb118","#ff725c","#6cc5b0","#3ca951","#ff8ab7","#a463f2","#97bbf5","#9c6b4e","#9498a0"])
+
+axes.formatter.use_locale: True
+
+grid.color: "#bebebe"
+grid.linewidth: 1
+grid.alpha: 1
+
+hatch.linewidth: 8
+hatch.color: "#00000013"
+
+boxplot.showmeans: true
+boxplot.meanprops.markeredgecolor: "k"
+boxplot.meanprops.marker: "+"
+boxplot.flierprops.markerfacecolor: C0
+boxplot.medianprops.color: C0
+
+figure.figsize: 8, 4.5
+figure.dpi: 96
+figure.constrained_layout.use: True
+
+xtick.direction: in
+xtick.major.size: 4
+xtick.minor.size: 2
+
+ytick.direction: in
+ytick.major.size: 4
+ytick.minor.size: 2
+
+savefig.format: pdf
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..0bbb937
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,7 @@
+mystmd
+jupyter-server
+ipykernel
+matplotlib
+numpy
+pandas
+scipy
\ No newline at end of file

From bc9d8904e2e505c4e63be5da5d78f0cbb01d2a26 Mon Sep 17 00:00:00 2001
From: "Edgar P. Burkhart" <git@edgarpierre.fr>
Date: Thu, 27 Feb 2025 18:33:19 +0100
Subject: [PATCH 2/2] Refactor signal visualizations to use Matplotlib,
 removing Altair dependencies and updating figure configurations for clarity

---
 cours/SIN/02-signaux.md |  17 +----
 cours/SIN/03-can.md     | 144 ++++++++++++++++++++--------------------
 2 files changed, 73 insertions(+), 88 deletions(-)

diff --git a/cours/SIN/02-signaux.md b/cours/SIN/02-signaux.md
index b9cbeb5..49288b0 100644
--- a/cours/SIN/02-signaux.md
+++ b/cours/SIN/02-signaux.md
@@ -43,7 +43,7 @@ t = np.arange(n+1)
 s = rng.choice([0, 1], n)
 
 fig, ax = plt.subplots()
-ax.stairs(s, t, lw=3)
+ax.stairs(s, t, lw=3, baseline=None)
 ax.set(
   xlim=(0, n),
   ylim=(-.5, 1.5),
@@ -88,7 +88,6 @@ lhs = (qmc.LatinHypercube(d=n-2, rng=rng).random(1)[0] - .5) * t_max/n
 t = t_base + np.concatenate(([0], lhs, [0]))
 t = t_base
 s = 5 * rng.random(n)
-s[-1] = s[-2]
 
 t_interp = np.linspace(0, t_max, 1024)
 s_interp = np.clip(CubicSpline(t, s)(t_interp), 0, 5)
@@ -127,7 +126,7 @@ t = np.arange(n+1)
 s = rng.integers(0, 16, n)
 
 fig, ax = plt.subplots()
-ax.stairs(s, t, lw=3)
+ax.stairs(s, t, lw=3, baseline=None)
 ax.set(
   xlim=(0, n),
   ylim=(-.5, 16.5),
@@ -136,18 +135,6 @@ ax.set(
 )
 ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
 ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
-# alt.Chart(
-#   data
-# ).mark_line(
-#   interpolate="step-after",
-#   strokeWidth=3,
-# ).encode(
-#   alt.X("t:Q").axis(title="Temps (s)").scale(domain=(0,n)),
-#   alt.Y("s:Q", axis=alt.Axis(title="Signal numérique", values=np.arange(0, 16))).# scale(domain=(0,15)),
-# ).properties(
-#   width="container",
-#   height=200,
-# )
 ```
 Exemple de signal numérique
 ````
\ No newline at end of file
diff --git a/cours/SIN/03-can.md b/cours/SIN/03-can.md
index 9b40196..ff412e3 100644
--- a/cours/SIN/03-can.md
+++ b/cours/SIN/03-can.md
@@ -37,69 +37,64 @@ La **caractéristique** du CAN est la courbe représentant la valeur numérique
 :label: fig:exemple-can
 ```{code-cell} python
 :tags: [remove-input]
-import altair as alt
+import matplotlib.pyplot as plt
+from matplotlib import ticker
 import numpy as np
-import pandas as pd
-from scipy import interpolate
+from scipy.interpolate import CubicSpline
+from scipy.stats import qmc
 
-rng = np.random.default_rng(25)
+rng = np.random.default_rng(50)
 
 n = 20
-t_max = 16
+t_max = 8
+n_interp = t_max * 100 + 1
 
-T = np.linspace(0, t_max, 1601)
-y = np.clip(
-    interpolate.BSpline(np.linspace(0, t_max, n), 5 * rng.random(n), 2)(T),
-    0,
-    5,
+t_base = np.linspace(0, t_max, n)
+lhs = (qmc.LatinHypercube(d=n-2, rng=rng).random(1)[0] - .5) * t_max/n
+t = t_base + np.concatenate(([0], lhs, [0]))
+t = t_base
+s = 5 * rng.random(n)
+
+t_interp = np.linspace(0, t_max, n_interp)
+s_interp = np.clip(CubicSpline(t, s)(t_interp), 0, 5)
+s_n = np.full_like(t_interp[::50], np.nan)
+s_n = np.floor(s_interp[::50] * 8 / 5)
+s_n[s_n == 8] = 7
+
+fig, ax = plt.subplots()
+ax2 = ax.twinx()
+
+ax.plot(t_interp, s_interp, lw=3)
+ax2.scatter(t_interp[::50], s_n, color="C1")
+
+ax.grid(False, axis="y")
+ax.grid(True, axis="x", which="both")
+ax2.grid(True)
+
+ax.set(
+  xlim=(0, t_max),
+  ylim=(-.5, 5.5),
+  xlabel="Temps (s)",
 )
-y_n = np.full([1601], np.nan)
-y_n[::50] = np.floor(y[::50] * 8 / 5)
-y_n[y_n == 8] = 7
-
-
-data = pd.DataFrame({
-  "t": T,
-  "s": y,
-  "s_n": y_n,
-})
-
-base = alt.Chart(
-  data
-).encode(
-  alt.X("t:Q").axis(title="Temps (s)").scale(domain=(0,t_max)),
+ax.set_ylabel("Signal analogique (V)", color="C0")
+ax2.set(
+  ylim=(-8/5*.5, 8/5*5.5),
 )
+ax2.set_ylabel("Signal numérique", color="C1")
 
-ch = base.mark_line(
-  interpolate="basis",
-  strokeWidth=3,
-  color="#6666cc",
-).encode(
-  alt.Y("s:Q", axis=alt.Axis(title="Signal analogique", titleColor="#6666cc")).scale(domain=(0,5)),
-)
+ax.yaxis.set_major_locator(ticker.MultipleLocator(1))
+ax.xaxis.set_major_locator(ticker.MultipleLocator(1))
+ax.xaxis.set_minor_locator(ticker.MultipleLocator(.5))
+ax2.set_yticks(np.arange(9), np.concatenate((np.arange(8), [""])))
 
-ch_n = base.mark_point(
-  filled=True,
-  color="#ff6600",
-).encode(
-  alt.Y(
-    "s_n:Q",
-    axis=alt.Axis(
-      title="Signal numérisé",
-      titleColor="#ff6600",
-      values=np.arange(8),
-    )
-  ).scale(domain=(0,8)),
-)
-
-alt.layer(ch_n, ch).resolve_scale(
-  y="independent",
-).properties(
-  width="container",
-  height=200,
-)
+arr = ax2.annotate("", xy=(0, 0), xytext=(0.5, 0), arrowprops=dict(arrowstyle="<->"))
+ax2.annotate("$T_e$", (0.5, 1), xycoords=arr, ha="center", va="bottom")
 
+arr2 = ax2.annotate("", xy=(0.5, 0), xytext=(0.5, 1), arrowprops=dict(arrowstyle="<->"))
+ax2.annotate("$q$", (1, 0.5), xycoords=arr2, ha="left", va="center")
 
+arr3 = ax2.annotate("", xy=(1, 0), xytext=(1, 8), arrowprops=dict(arrowstyle="<->"))
+ax2.annotate("$V_{pe}$", (1, 0.5), xycoords=arr3, ha="left", va="center")
 ```
 Signal analogique et signal numérisé.
 ````
@@ -108,34 +103,37 @@ Signal analogique et signal numérisé.
 :label: fig:carac-can
 ```{code-cell} python
 :tags: [remove-input]
-import altair as alt
+import matplotlib.pyplot as plt
+from matplotlib import ticker
 import numpy as np
-import pandas as pd
-from scipy import interpolate
 
 N = 8
-s_n = np.arange(N+1)
-s_n[-1] = s_n[-2]
-data = pd.DataFrame({
-  "s_n": s_n,
-  "s_a": np.linspace(0, 5, N+1),
-})
+s_n = np.arange(N)
+s_a = np.linspace(0, 5, N+1)
 
-alt.Chart(
-  data
-).mark_line(
-  interpolate="step-after",
-  strokeWidth=3,
-  color="#ff6600",
-).encode(
-  alt.X("s_a:Q").axis(title="Signal Analogique").scale(domain=(0,5)),
-  alt.Y("s_n:Q", axis=alt.Axis(title="Signal numérique", values=np.arange(N))).scale(domain=(0,N)),
-).properties(
-  width=200,
-  height=200,
+fig, ax = plt.subplots()
+
+ax.stairs(s_n, s_a, color="C1", lw=3, baseline=None)
+
+ax.set(
+  xlim=(0, 5),
+  ylim=(-1, N),
+  yticks=s_n,
+  xlabel="Signal analogique (V)",
+  ylabel="Signal numérique",
 )
+ax.set_xticks(s_a, [f"{v:.3f}" for v in s_a], rotation=45, ha="right", rotation_mode="anchor")
+ax.set_aspect(5/8, 'box')
 
+arr4 = ax.annotate(
+    "", xy=(s_a[0], 0), xytext=(s_a[1], 0), arrowprops=dict(arrowstyle="<->")
+)
+ax.annotate("$q$", (0.5, 1), xycoords=arr4, ha="center", va="bottom")
 
+arr5 = ax.annotate(
+    "", xy=(s_a[0], 1.5), xytext=(s_a[-1], 1.5), arrowprops=dict(arrowstyle="<->")
+)
+ax.annotate("$V_{pe}$", (0.5, 1), xycoords=arr5, ha="center", va="bottom")
 ```
 Caractéristique du CAN.
 ````
\ No newline at end of file