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