From f04682b6389e0355fc4f9c5212921458850328c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrique=20Silv=C3=A9rio?= <29920212+HGSilveri@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:53:05 +0200 Subject: [PATCH] Drop support for Python 3.8 (#743) * Drop support for python 3.8 * Relax matplotlib requirement * Incorporate matplotlib type hints --- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 8 +-- .mypy.ini | 2 +- README.md | 2 +- docs/source/installation.rst | 2 +- pulser-core/pulser/register/_reg_drawer.py | 56 ++++++++++----- pulser-core/pulser/register/register.py | 13 ++-- .../pulser/register/register_layout.py | 4 +- pulser-core/pulser/register/weight_maps.py | 3 +- pulser-core/pulser/sequence/_seq_drawer.py | 72 +++++++++++++------ pulser-core/pulser/waveforms.py | 1 + pulser-core/requirements.txt | 2 +- pulser-core/setup.py | 2 +- pulser-simulation/setup.py | 2 +- setup.py | 2 +- 16 files changed, 114 insertions(+), 61 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47ced7815..804cf235a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.8", "3.12"] + python-version: ["3.9", "3.12"] with-torch: ["with-torch", "no-torch"] steps: - name: Check out Pulser diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dcda49311..7b9eefb7d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -66,7 +66,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - name: Check out Pulser uses: actions/checkout@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf79e9208..68b4d1748 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,14 +15,12 @@ jobs: strategy: fail-fast: false matrix: - # Python 3.8 and 3.9 does not run on macos-latest (14) - # Uses macos-13 for 3.8 and 3.9 and macos-latest for >=3.10 + # Python 3.9 does not run on macos-latest (14) + # Uses macos-13 for 3.9 and macos-latest for >=3.10 os: [ubuntu-latest, macos-13, macos-latest, windows-latest] with-torch: ["with-torch", "no-torch"] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] exclude: - - os: macos-latest - python-version: "3.8" - os: macos-latest python-version: "3.9" - os: macos-13 diff --git a/.mypy.ini b/.mypy.ini index c1a1146f0..3c4bca272 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -11,7 +11,7 @@ warn_unused_ignores = True disallow_untyped_defs = True # 3rd-party libs without type hints nor stubs -[mypy-matplotlib.*,scipy.*,qutip.*,jsonschema.*,py.*] +[mypy-scipy.*,qutip.*,jsonschema.*,py.*] follow_imports = silent ignore_missing_imports = True diff --git a/README.md b/README.md index 848ec72cb..ac88d54ae 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ For a comprehensive overview of Pulser, check out [Pulser's white paper](https:/ **Note**: *Pulser v0.6 introduced a split of the ``pulser`` package that prevents it from being correctly upgraded. If you have an older version of ``pulser`` installed and wish to upgrade, make sure to uninstall it first by running ``pip uninstall pulser``.* -To install the latest release of ``pulser``, have Python 3.8 or higher installed, then use ``pip``: +To install the latest release of ``pulser``, have Python 3.9 or higher installed, then use ``pip``: ```bash pip install pulser diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 57a7905c7..65ea72fde 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -10,7 +10,7 @@ Installation before proceeding to any of the steps below. -To install the latest release of ``pulser``, have Python 3.8 or higher +To install the latest release of ``pulser``, have Python 3.9 or higher installed, then use ``pip``: :: pip install pulser diff --git a/pulser-core/pulser/register/_reg_drawer.py b/pulser-core/pulser/register/_reg_drawer.py index f0ed27011..a50ce41d4 100644 --- a/pulser-core/pulser/register/_reg_drawer.py +++ b/pulser-core/pulser/register/_reg_drawer.py @@ -18,7 +18,7 @@ from collections.abc import Mapping from collections.abc import Sequence as abcSequence from itertools import combinations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Any, Optional, Tuple, cast import matplotlib.pyplot as plt import numpy as np @@ -48,7 +48,7 @@ def default_qubit_color() -> str: @staticmethod def _draw_2D( - ax: plt.axes._subplots.AxesSubplot, + ax: plt.Axes, pos: np.ndarray, ids: abcSequence[QubitId], plane: tuple = (0, 1), @@ -56,7 +56,7 @@ def _draw_2D( blockade_radius: Optional[float] = None, draw_graph: bool = True, draw_half_radius: bool = False, - qubit_colors: Mapping[QubitId, str] = dict(), + qubit_colors: Mapping[QubitId, Any] = dict(), masked_qubits: set[QubitId] = set(), are_traps: bool = False, dmm_qubits: Mapping[QubitId, float] = {}, @@ -68,6 +68,7 @@ def _draw_2D( ix, iy = plane + params: dict[str, Any] if are_traps: params = dict( s=50, @@ -107,7 +108,7 @@ def _draw_2D( dmm_arr[:, iy], marker="s", s=1200, - alpha=alpha, + alpha=alpha, # type: ignore[arg-type] c="black" if not qubit_colors else ordered_qubit_colors, ) axes = "xyz" @@ -194,12 +195,12 @@ def _draw_2D( fontsize=12 if i not in final_plot_det_map else 8.3, multialignment="right", ) - txt._get_wrap_line_width = lambda: 50.0 + txt._get_wrap_line_width = lambda: 50.0 # type: ignore if draw_half_radius and blockade_radius is not None: for p, color in zip(pos, ordered_qubit_colors): circle = plt.Circle( - tuple(p[[ix, iy]]), + (p[ix], p[iy]), blockade_radius / 2, alpha=0.1, color=color, @@ -214,7 +215,11 @@ def _draw_2D( lines = bonds[:, :, (ix, iy)] else: lines = np.array([]) - lc = mc.LineCollection(lines, linewidths=0.6, colors="grey") + lc = mc.LineCollection( + cast(abcSequence[np.ndarray], lines), + linewidths=0.6, + colors="grey", + ) ax.add_collection(lc) else: @@ -258,9 +263,14 @@ def _draw_3D( blockade_radius=blockade_radius, draw_half_radius=draw_half_radius, ) - fig.get_layout_engine().set(w_pad=6.5) - - for ax, (ix, iy) in zip(axes, combinations(np.arange(3), 2)): + _layout_engine = fig.get_layout_engine() + assert _layout_engine is not None + _layout_engine.set(w_pad=6.5) # type: ignore[call-arg] + + for ax, (ix, iy) in zip( + cast(abcSequence[plt.Axes], axes), + combinations(np.arange(3), 2), + ): RegDrawer._draw_2D( ax, pos, @@ -284,7 +294,12 @@ def _draw_3D( ) else: - fig = plt.figure(figsize=2 * plt.figaspect(0.5)) + fig = plt.figure( + figsize=cast( + Tuple[float, float], + tuple(2 * np.array(plt.figaspect(0.5))), + ) + ) if draw_graph and blockade_radius is not None: bonds = {} @@ -293,6 +308,7 @@ def _draw_3D( xj, yj, zj = pos[j] bonds[(i, j)] = [[xi, xj], [yi, yj], [zi, zj]] + params: dict[str, Any] if are_traps: params = dict(s=50, c="white", edgecolors="black") else: @@ -313,7 +329,7 @@ def _draw_3D( coords[0], coords[1], coords[2], - q, + q, # type: ignore[arg-type] fontsize=12, ha="left", va="bottom", @@ -336,7 +352,13 @@ def _draw_3D( y = radius * np.sin(u) * np.sin(v) + y0 z = radius * np.cos(v) + z0 # alpha controls opacity - ax.plot_surface(x, y, z, color=color, alpha=0.1) + ax.plot_surface( # type: ignore[attr-defined] + x, + y, + z, + color=color, + alpha=0.1, + ) if draw_graph and blockade_radius is not None: for x, y, z in bonds.values(): @@ -344,7 +366,7 @@ def _draw_3D( ax.set_xlabel("x (µm)") ax.set_ylabel("y (µm)") - ax.set_zlabel("z (µm)") + ax.set_zlabel("z (µm)") # type: ignore[attr-defined] @staticmethod def _register_dims( @@ -367,7 +389,7 @@ def _initialize_fig_axes( blockade_radius: Optional[float] = None, draw_half_radius: bool = False, nregisters: int = 1, - ) -> tuple[plt.figure.Figure, plt.axes.Axes]: + ) -> tuple[plt.Figure, plt.Axes | abcSequence[plt.Axes]]: """Creates the Figure and Axes for drawing the register.""" diffs = RegDrawer._register_dims( pos, @@ -394,7 +416,9 @@ def _initialize_fig_axes_projection( blockade_radius: Optional[float] = None, draw_half_radius: bool = False, nregisters: int = 1, - ) -> tuple[plt.figure.Figure, plt.axes.Axes]: + ) -> tuple[ + plt.Figure, abcSequence[plt.Axes] | abcSequence[abcSequence[plt.Axes]] + ]: """Creates the Figure and Axes for drawing the register projections.""" diffs = RegDrawer._register_dims( pos, diff --git a/pulser-core/pulser/register/register.py b/pulser-core/pulser/register/register.py index 69f4002fc..2ed7379a7 100644 --- a/pulser-core/pulser/register/register.py +++ b/pulser-core/pulser/register/register.py @@ -17,7 +17,7 @@ import warnings from collections.abc import Mapping -from typing import Any, Optional, Union +from typing import Any, Optional, Union, cast import matplotlib.pyplot as plt import numpy as np @@ -408,10 +408,13 @@ def draw( pos = self._coords_arr.as_array(detach=True) if custom_ax is None: - _, custom_ax = self._initialize_fig_axes( - pos, - blockade_radius=blockade_radius, - draw_half_radius=draw_half_radius, + custom_ax = cast( + plt.Axes, + self._initialize_fig_axes( + pos, + blockade_radius=blockade_radius, + draw_half_radius=draw_half_radius, + )[1], ) super()._draw_2D( custom_ax, diff --git a/pulser-core/pulser/register/register_layout.py b/pulser-core/pulser/register/register_layout.py index 8cb2e720f..f543e7e1a 100644 --- a/pulser-core/pulser/register/register_layout.py +++ b/pulser-core/pulser/register/register_layout.py @@ -21,7 +21,7 @@ from collections.abc import Sequence as abcSequence from dataclasses import dataclass from operator import itemgetter -from typing import Any, Optional +from typing import Any, Optional, cast import matplotlib.pyplot as plt import numpy as np @@ -180,7 +180,7 @@ def draw( draw_half_radius=draw_half_radius, ) self._draw_2D( - ax, + cast(plt.Axes, ax), coords, ids, blockade_radius=blockade_radius, diff --git a/pulser-core/pulser/register/weight_maps.py b/pulser-core/pulser/register/weight_maps.py index d740b53f6..f2d146e1d 100644 --- a/pulser-core/pulser/register/weight_maps.py +++ b/pulser-core/pulser/register/weight_maps.py @@ -121,8 +121,7 @@ def draw( need to set this flag to False. """ pos = self.trap_coordinates - if custom_ax is None: - _, custom_ax = self._initialize_fig_axes(pos) + custom_ax = custom_ax or cast(Axes, self._initialize_fig_axes(pos)[1]) labels_ = labels if labels is not None else list(range(len(pos))) diff --git a/pulser-core/pulser/sequence/_seq_drawer.py b/pulser-core/pulser/sequence/_seq_drawer.py index 42372f065..7fc8f52f0 100644 --- a/pulser-core/pulser/sequence/_seq_drawer.py +++ b/pulser-core/pulser/sequence/_seq_drawer.py @@ -16,6 +16,8 @@ import warnings from collections import defaultdict +from collections.abc import Iterable +from collections.abc import Sequence as abcSequence from dataclasses import dataclass, field from itertools import chain, combinations from typing import Any, Optional, Union, cast @@ -76,8 +78,8 @@ def draw(self, ax: Axes) -> None: """Draws a rectangle between the start and end value.""" if not self.isempty: ax.axvspan( - self.ti, - self.tf, + cast(int, self.ti), + cast(int, self.tf), color=self.color, alpha=self.alpha, zorder=-100, @@ -98,7 +100,7 @@ def smooth_draw(self, ax: Axes, decreasing: bool = False) -> None: zorder=-100, ) ax.axvline( - self.tf if decreasing else self.ti, + cast(int, self.tf if decreasing else self.ti), ax.get_ylim()[0], ax.get_ylim()[1], color=self.color, @@ -387,6 +389,9 @@ def _draw_register_det_maps( nregisters = ( int(register is not None) + int(draw_detuning_maps) * n_det_maps ) + axes_reg: ( + plt.Axes | abcSequence[plt.Axes] | abcSequence[abcSequence[plt.Axes]] + ) # Draw masked register if register: pos = register._coords_arr.as_array(detach=True) @@ -403,10 +408,12 @@ def _draw_register_det_maps( draw_half_radius=True, nregisters=nregisters, ) - for ax_reg, (ix, iy) in zip( - axes_reg if nregisters == 1 else axes_reg[0], - combinations(np.arange(3), 2), + cast( + abcSequence[plt.Axes], + axes_reg if nregisters == 1 else axes_reg[0], + ), + cast(Iterable, combinations((0, 1, 2), 2)), ): register._draw_2D( ax=ax_reg, @@ -430,7 +437,9 @@ def _draw_register_det_maps( draw_half_radius=True, nregisters=nregisters, ) - ax_reg = axes_reg if nregisters == 1 else axes_reg[0] + ax_reg = ( + axes_reg if isinstance(axes_reg, plt.Axes) else axes_reg[0] + ) register._draw_2D( ax=ax_reg, pos=pos, @@ -466,14 +475,23 @@ def _draw_register_det_maps( nregisters=nregisters, ) need_init = False - ax_reg = ( - axes_reg - if nregisters == 1 - else axes_reg[i + int(register is not None)] + ax_reg_ = cast( + Union[plt.Axes, abcSequence[plt.Axes]], + ( + axes_reg + if nregisters == 1 + else cast( + Union[ + abcSequence[plt.Axes], + abcSequence[abcSequence[plt.Axes]], + ], + axes_reg, + )[i + int(register is not None)] + ), ) - if det_map.dimensionality == 3: + if not isinstance(ax_reg_, plt.Axes): for sub_ax_reg, (ix, iy) in zip( - ax_reg, combinations(np.arange(3), 2) + ax_reg_, combinations(np.arange(3), 2) ): det_map._draw_2D( ax=sub_ax_reg, @@ -490,12 +508,12 @@ def _draw_register_det_maps( ) else: det_map._draw_2D( - ax=ax_reg, + ax=ax_reg_, pos=pos, ids=list(qubits.keys()), dmm_qubits=reg_det_map, ) - ax_reg.set_title(ch, pad=10) + ax_reg_.set_title(ch, pad=10) return fig_reg @@ -588,8 +606,10 @@ def phase_str(phi: Any) -> str: ) gs = fig.add_gridspec(n_channels, 1, hspace=0.075, height_ratios=ratios) - ch_axes = {} - for i, (ch, gs_) in enumerate(zip(sampled_seq.channels, gs)): + ch_axes: dict[str, abcSequence[plt.Axes]] = {} + for i, (ch, gs_) in enumerate( + zip(sampled_seq.channels, gs) # type: ignore[call-overload] + ): ax = fig.add_subplot(gs_) for side in ("top", "bottom", "left", "right"): ax.spines[side].set_color("none") @@ -705,7 +725,9 @@ def phase_str(phi: Any) -> str: color=COLORS[i], linestyle="dotted", ) - special_kwargs = dict(labelpad=10) if i == 0 else {} + special_kwargs: dict[str, Any] = ( + dict(labelpad=10) if i == 0 else {} + ) ax.set_ylabel(LABELS[i], fontsize=14, **special_kwargs) if draw_phase_area: @@ -852,7 +874,9 @@ def phase_str(phi: Any) -> str: if end != total_duration - 1 or "measurement" in data: end += 1 / time_scale for t_, delta in ref.changes(start, end, time_scale=time_scale): - conf = dict(linestyle="--", linewidth=1.5, color="black") + conf: dict[str, Any] = dict( + linestyle="--", linewidth=1.5, color="black" + ) for ax in axes: ax.axvline(t_, **conf) msg = "\u27F2 " + phase_str(delta) @@ -902,7 +926,9 @@ def phase_str(phi: Any) -> str: bbox=slm_box, ) - hline_kwargs = dict(linestyle="-", linewidth=0.5, color="grey") + hline_kwargs: dict[str, Any] = dict( + linestyle="-", linewidth=0.5, color="grey" + ) if "measurement" in data: msg = f"Basis: {data['measurement']}" if len(axes) == 1: @@ -1011,7 +1037,9 @@ def _draw_qubit_content( fig.suptitle("Quantities per qubit over time", fontsize=16) cmap = LinearSegmentedColormap.from_list("", COLORS) - hline_kwargs = dict(linestyle="-", linewidth=0.5, color="grey") + hline_kwargs: dict[str, Any] = dict( + linestyle="-", linewidth=0.5, color="grey" + ) max_targets = 20 # maximum number of targets shown in legend # If qubits can be defined, another figure is added to display legend dmm_samples: list[DMMSamples] = [ @@ -1031,7 +1059,7 @@ def _draw_qubit_content( UserWarning, ) fig_legend: None | Figure = None - axes_legend: None | Axes = None + axes_legend: Axes | abcSequence[Axes] | abcSequence[abcSequence[Axes]] dimensionality_3d: bool | None = None if register or dmm_samples: dimensionality_3d = isinstance(register, Register3D) or ( diff --git a/pulser-core/pulser/waveforms.py b/pulser-core/pulser/waveforms.py index e5d324234..7c7e62646 100644 --- a/pulser-core/pulser/waveforms.py +++ b/pulser-core/pulser/waveforms.py @@ -367,6 +367,7 @@ def _plot( # Repeats the times on the edges once ts = np.pad(ts, 1, mode="edge") + color_dict: dict[str, Any] if color: color_dict = {"color": color} hline_color = color diff --git a/pulser-core/requirements.txt b/pulser-core/requirements.txt index 85604b327..367a36e05 100644 --- a/pulser-core/requirements.txt +++ b/pulser-core/requirements.txt @@ -1,6 +1,6 @@ jsonschema >= 4.17.3 referencing -matplotlib < 3.8 +matplotlib < 4 # Numpy 1.20 introduces type hints, 1.24.0 breaks matplotlib < 3.6.1 numpy >= 1.20, != 1.24.0 scipy \ No newline at end of file diff --git a/pulser-core/setup.py b/pulser-core/setup.py index cb6582346..4e341933e 100644 --- a/pulser-core/setup.py +++ b/pulser-core/setup.py @@ -53,7 +53,7 @@ long_description=open("README.md", "r", encoding="utf-8").read(), long_description_content_type="text/markdown", author="Pulser Development Team", - python_requires=">=3.8", + python_requires=">=3.9", license="Apache 2.0", classifiers=[ "Development Status :: 3 - Alpha", diff --git a/pulser-simulation/setup.py b/pulser-simulation/setup.py index 2a85f65c4..5f81b0bef 100644 --- a/pulser-simulation/setup.py +++ b/pulser-simulation/setup.py @@ -53,7 +53,7 @@ long_description=open("README.md", "r", encoding="utf-8").read(), long_description_content_type="text/markdown", author="Pulser Development Team", - python_requires=">=3.8", + python_requires=">=3.9", license="Apache 2.0", classifiers=[ "Development Status :: 3 - Alpha", diff --git a/setup.py b/setup.py index 5a739adc1..c99c4b6ae 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ long_description=open("README.md", "r", encoding="utf-8").read(), long_description_content_type="text/markdown", author="Pulser Development Team", - python_requires=">=3.8", + python_requires=">=3.9", license="Apache 2.0", classifiers=[ "Development Status :: 3 - Alpha",