diff --git a/.github/workflows/full_tests.yml b/.github/workflows/full_tests.yml index b2d5b9c9..a717891c 100644 --- a/.github/workflows/full_tests.yml +++ b/.github/workflows/full_tests.yml @@ -17,7 +17,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.14"] # Lower and higher versions we support + python-version: ["3.10", "3.14"] # Lower and higher versions we support os: [macos-latest, windows-latest, ubuntu-latest] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index 2ac2c250..24cb803c 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install Tools run: | python -m pip install --upgrade pip diff --git a/examples/ex_12_plot_values.py b/examples/ex_12_plot_values.py index ad59065a..57a3b325 100644 --- a/examples/ex_12_plot_values.py +++ b/examples/ex_12_plot_values.py @@ -34,7 +34,7 @@ fig, ax = plt.subplots() poly, poly_contour = plot_probe(probe, contacts_values=values, - cmap='jet', ax=ax, contacts_kargs={'alpha' : 1}, title=False) + cmap='jet', ax=ax, contact_kwargs={'alpha' : 1}, title=False) poly.set_clim(-2, 2) fig.colorbar(poly) diff --git a/pyproject.toml b/pyproject.toml index fa605272..51231d83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ description = "Python package to handle probe layout, geometry and wiring to device." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", diff --git a/src/probeinterface/generator.py b/src/probeinterface/generator.py index a9be26b3..05e9b5ab 100644 --- a/src/probeinterface/generator.py +++ b/src/probeinterface/generator.py @@ -3,10 +3,9 @@ """ -from __future__ import annotations import numpy as np -from typing import Optional +from typing import Literal from .probe import Probe from .probegroup import ProbeGroup @@ -15,7 +14,7 @@ _default_shape_to_params = {"circle": "radius", "square": "width", "rect": "height"} -def generate_dummy_probe(elec_shapes: "circle" | "square" | "rect" = "circle") -> Probe: +def generate_dummy_probe(elec_shapes: Literal["circle", "square", "rect"] = "circle") -> Probe: """ Generate a dummy probe with 3 columns and 32 contacts. Mainly used for testing and examples. @@ -103,8 +102,8 @@ def generate_multi_columns_probe( num_contact_per_column: int | list[int] = 10, xpitch: float = 20, ypitch: float = 20, - y_shift_per_column: Optional[np.array | list] = None, - contact_shapes: "circle" | "rect" | "square" = "circle", + y_shift_per_column: np.ndarray | list | None = None, + contact_shapes: Literal["circle", "rect", "square"] = "circle", contact_shape_params: dict = {"radius": 6}, ) -> Probe: """Generate a Probe with several columns. @@ -119,7 +118,7 @@ def generate_multi_columns_probe( Pitch in x direction ypitch : float, default: 20 Pitch in y direction - y_shift_per_column : Optional[array-like], default: None + y_shift_per_column : array-like | None, default: None Shift in y direction per column. It needs to have the same length as num_columns, by default None contact_shapes : "circle" | "rect" | "square", default: "circle" Shape of the contacts @@ -167,7 +166,7 @@ def generate_multi_columns_probe( def generate_linear_probe( num_elec: int = 16, ypitch: float = 20, - contact_shapes: "circle" | "rect" | "square" = "circle", + contact_shapes: Literal["circle", "rect", "square"] = "circle", contact_shape_params: dict = {"radius": 6}, ) -> Probe: """Generate a one-column linear probe. diff --git a/src/probeinterface/io.py b/src/probeinterface/io.py index ccb391be..ad1a60ae 100644 --- a/src/probeinterface/io.py +++ b/src/probeinterface/io.py @@ -9,9 +9,7 @@ """ -from __future__ import annotations from pathlib import Path -from typing import Union, Optional import re import warnings import json @@ -108,7 +106,7 @@ def write_probeinterface(file: str | Path, probe_or_probegroup: Probe | ProbeGro tsv_label_map_to_probeinterface = {v: k for k, v in tsv_label_map_to_BIDS.items()} -def read_BIDS_probe(folder: str | Path, prefix: Optional[str] = None) -> ProbeGroup: +def read_BIDS_probe(folder: str | Path, prefix: str | None = None) -> ProbeGroup: """ Read to BIDS probe format. @@ -636,8 +634,8 @@ def read_3brain(file: str | Path, mea_pitch: float = None, electrode_width: floa def write_prb( file: str, probegroup: ProbeGroup, - total_nb_channels: Optional[int] = None, - radius: Optional[float] = None, + total_nb_channels: int | None = None, + radius: float | None = None, group_mode: str = "by_probe", ): """ @@ -664,9 +662,9 @@ def write_prb( The name of the file to be written probegroup: ProbeGroup The Probegroup to be used for writing - total_nb_channels: Optional[int], default None + total_nb_channels: int | None, default None ***to do - radius: Optional[float], default None + radius: float | None, default None *** to do group_mode: str One of "by_probe" or "by_shank diff --git a/src/probeinterface/library.py b/src/probeinterface/library.py index 8238c226..f719ff6b 100644 --- a/src/probeinterface/library.py +++ b/src/probeinterface/library.py @@ -9,15 +9,14 @@ """ -from __future__ import annotations import os import warnings from pathlib import Path from urllib.request import urlopen import requests -from typing import Optional from .io import read_probeinterface +from .probe import Probe # OLD URL on gin # public_url = "https://web.gin.g-node.org/spikeinterface/probeinterface_library/raw/master/" @@ -37,7 +36,7 @@ def get_cache_folder() -> Path: return Path(os.path.expanduser("~")) / ".config" / "probeinterface" / "library" -def download_probeinterface_file(manufacturer: str, probe_name: str, tag: Optional[str] = None) -> None: +def download_probeinterface_file(manufacturer: str, probe_name: str, tag: str | None = None) -> None: """Download the probeinterface file to the cache directory. Note that the file is itself a ProbeGroup but on the repo each file represents one probe. @@ -65,7 +64,7 @@ def download_probeinterface_file(manufacturer: str, probe_name: str, tag: Option f.write(rem.read()) -def get_from_cache(manufacturer: str, probe_name: str, tag: Optional[str] = None) -> Optional["Probe"]: +def get_from_cache(manufacturer: str, probe_name: str, tag: str | None = None) -> Probe | None: """ Get Probe from local cache @@ -105,8 +104,8 @@ def get_from_cache(manufacturer: str, probe_name: str, tag: Optional[str] = None def get_probe( manufacturer: str, probe_name: str, - name: Optional[str] = None, - tag: Optional[str] = None, + name: str | None = None, + tag: str | None = None, force_download: bool = False, ) -> "Probe": """ diff --git a/src/probeinterface/neuropixels_tools.py b/src/probeinterface/neuropixels_tools.py index 2e4eb888..682416ef 100644 --- a/src/probeinterface/neuropixels_tools.py +++ b/src/probeinterface/neuropixels_tools.py @@ -7,9 +7,7 @@ """ -from __future__ import annotations from pathlib import Path -from typing import Union, Optional import warnings from packaging.version import parse import json @@ -114,7 +112,7 @@ def get_probe_length(probe_part_number: str) -> int: return 10_000 -def make_mux_table_array(mux_information) -> np.array: +def make_mux_table_array(mux_information) -> np.ndarray: """ Function to parse the mux_table from ProbeTable. @@ -210,7 +208,7 @@ def get_probe_contour_vertices(shank_width, tip_length, probe_length) -> list: return polygon_vertices -def read_imro(file_path: Union[str, Path]) -> Probe: +def read_imro(file_path: str | Path) -> Probe: """ Read probe position from the imro file used in input of SpikeGlx and Open-Ephys for neuropixels probes. @@ -571,7 +569,7 @@ def _annotate_probe_with_adc_sampling_info(probe: Probe, adc_sampling_table: str _annotate_contacts_from_mux_table(probe, adc_groups_array) -def _read_imro_string(imro_str: str, imDatPrb_pn: Optional[str] = None) -> Probe: +def _read_imro_string(imro_str: str, imDatPrb_pn: str | None = None) -> Probe: """ Parse the IMRO table when presented as a string and create a Probe object. @@ -1027,7 +1025,7 @@ def parse_spikeglx_snsGeomMap(meta): # parse_spikeglx_snsGeomMap(meta) -def get_saved_channel_indices_from_spikeglx_meta(meta_file: str | Path) -> np.array: +def get_saved_channel_indices_from_spikeglx_meta(meta_file: str | Path) -> np.ndarray: """ Utils function to get the saved channels. @@ -1067,7 +1065,7 @@ def _parse_openephys_settings( settings_file: str | Path, fix_x_position_for_oe_5: bool = True, raise_error: bool = True, -) -> Optional[list[dict]]: +) -> list[dict] | None: """ Parse an Open Ephys settings.xml and extract per-probe metadata. @@ -1396,11 +1394,11 @@ def _parse_openephys_settings( def _select_openephys_probe_info( probes_info: list[dict], - stream_name: Optional[str] = None, - probe_name: Optional[str] = None, - serial_number: Optional[str] = None, + stream_name: str | None = None, + probe_name: str | None = None, + serial_number: str | None = None, raise_error: bool = True, -) -> Optional[dict]: +) -> dict | None: """ Select one probe's info dict from the list returned by `_parse_openephys_settings`. @@ -1587,9 +1585,9 @@ def _annotate_openephys_probe(probe: Probe, probe_info: dict) -> None: def read_openephys( settings_file: str | Path, - stream_name: Optional[str] = None, - probe_name: Optional[str] = None, - serial_number: Optional[str] = None, + stream_name: str | None = None, + probe_name: str | None = None, + serial_number: str | None = None, fix_x_position_for_oe_5: bool = True, raise_error: bool = True, ) -> Probe: @@ -1676,9 +1674,7 @@ def read_openephys( return probe -def get_saved_channel_indices_from_openephys_settings( - settings_file: str | Path, stream_name: str -) -> Optional[np.array]: +def get_saved_channel_indices_from_openephys_settings(settings_file: str | Path, stream_name: str) -> np.ndarray | None: """ Returns an array with the subset of saved channels indices (if used) diff --git a/src/probeinterface/plotting.py b/src/probeinterface/plotting.py index 35cd2a8f..8283f90e 100644 --- a/src/probeinterface/plotting.py +++ b/src/probeinterface/plotting.py @@ -5,7 +5,7 @@ Depending on Probe.ndim, the plotting is done in 2D or 3D """ -from __future__ import annotations +import warnings import numpy as np from matplotlib import path as mpl_path @@ -46,8 +46,6 @@ def create_probe_polygons( The polygon collection for the probe shape """ if contacts_kargs is not None: - import warnings - warnings.warn( "contacts_kargs is deprecated and will be removed in 0.3.4. Please use `contacts_kwargs` instead.", category=DeprecationWarning, @@ -107,13 +105,14 @@ def plot_probe( contacts_values: list | np.ndarray | None = None, cmap: str = "viridis", title: bool = True, - contacts_kargs: dict = {}, + contact_kwargs: dict = {}, probe_shape_kwargs: dict = {}, xlims: tuple | None = None, ylims: tuple | None = None, zlims: tuple | None = None, show_channel_on_click: bool = False, - side=None, + side: str | None = None, + contacts_kargs: dict | None = None, ): """Plot a Probe object. Generates a 2D or 3D axis, depending on Probe.ndim @@ -138,7 +137,7 @@ def plot_probe( A colormap color title : bool, default: True If True, the axis title is set to the probe name - contacts_kargs : dict, default: {} + contact_kwargs : dict, default: {} Dict with kwargs for contacts (e.g. alpha, edgecolor, lw) probe_shape_kwargs : dict, default: {} Dict with kwargs for probe shape (e.g. alpha, edgecolor, lw) @@ -152,6 +151,8 @@ def plot_probe( If True, the channel information is shown upon click side : None | "front" | "back" If the probe is two side, then the side must be given otherwise this raises an error. + contacts_kargs : dict | None, default: None + DEPRECATED, use contact_kwargs instead. Dict with kwargs for contacts (e.g. alpha, edgecolor, lw) Returns ------- @@ -162,6 +163,14 @@ def plot_probe( """ import matplotlib.pyplot as plt + if contacts_kargs is not None: + warnings.warn( + "contacts_kwargs is deprecated and will be removed in 0.3.4. Please use `contact_kwargs` instead.", + category=DeprecationWarning, + stacklevel=2, + ) + contact_kwargs = contacts_kargs + if probe.contact_sides is not None: if side is None or side not in ("front", "back"): raise ValueError( @@ -187,7 +196,7 @@ def plot_probe( contacts_colors=contacts_colors, contacts_values=contacts_values, cmap=cmap, - contacts_kargs=contacts_kargs, + contact_kwargs=contact_kwargs, probe_shape_kwargs=probe_shape_kwargs, ) diff --git a/src/probeinterface/probe.py b/src/probeinterface/probe.py index 4867d355..c19ddd65 100644 --- a/src/probeinterface/probe.py +++ b/src/probeinterface/probe.py @@ -1,6 +1,5 @@ -from __future__ import annotations import numpy as np -from typing import Optional +from typing import Literal from pathlib import Path from .shank import Shank @@ -56,10 +55,10 @@ def __init__( self, ndim: int = 2, si_units: str = "um", - name: Optional[str] = None, - serial_number: Optional[str] = None, - model_name: Optional[str] = None, - manufacturer: Optional[str] = None, + name: str | None = None, + serial_number: str | None = None, + model_name: str | None = None, + manufacturer: str | None = None, ): """ Some attributes are protected and have to be set with setters: @@ -427,7 +426,7 @@ def set_planar_contour(self, contour_polygon: list): raise ValueError(f"contour_polygon.shape[1] {contour_polygon.shape[1]} and ndim {self.ndim} do not match!") self.probe_planar_contour = contour_polygon - def create_auto_shape(self, probe_type: "tip" | "rect" | "circular" = "tip", margin: float = 20.0): + def create_auto_shape(self, probe_type: Literal["tip", "rect", "circular"] = "tip", margin: float = 20.0): """Create a planar contour automatically based on probe contact positions. This function generates a 2D polygon that outlines the shape of the probe, adjusted @@ -515,7 +514,7 @@ def create_auto_shape(self, probe_type: "tip" | "rect" | "circular" = "tip", mar self.set_planar_contour(polygon) - def set_device_channel_indices(self, channel_indices: np.array | list): + def set_device_channel_indices(self, channel_indices: np.ndarray | list): """ Manually set the device channel indices. @@ -553,7 +552,7 @@ def wiring_to_device(self, pathway: str, channel_offset: int = 0): wire_probe(self, pathway, channel_offset=channel_offset) - def set_contact_ids(self, contact_ids: np.array | list): + def set_contact_ids(self, contact_ids: np.ndarray | list): """ Set contact ids. Channel ids are converted to strings. Contact ids must be **unique** for the **Probe** @@ -586,7 +585,7 @@ def set_contact_ids(self, contact_ids: np.array | list): if self._probe_group is not None: self._probe_group.check_global_device_wiring_and_ids() - def set_shank_ids(self, shank_ids: np.array | list): + def set_shank_ids(self, shank_ids: np.ndarray | list): """ Set shank ids. @@ -682,7 +681,7 @@ def copy(self): # channel_indices are not copied return other - def to_3d(self, axes: "xy" | "yz" | "xz" = "xz"): + def to_3d(self, axes: Literal["xy", "yz", "xz"] = "xz"): """ Transform 2d probe to 3d probe. @@ -719,7 +718,7 @@ def to_3d(self, axes: "xy" | "yz" | "xz" = "xz"): return probe3d - def to_2d(self, axes: "xy" | "yz" | "xz" = "xy"): + def to_2d(self, axes: Literal["xy", "yz", "xz"] = "xy"): """ Transform 3d probe to 2d probe. @@ -790,7 +789,7 @@ def get_contact_vertices(self) -> list: vertices.append(one_vertice) return vertices - def move(self, translation_vector: np.array | list): + def move(self, translation_vector: np.ndarray | list): """ Translate the probe in one direction. @@ -808,7 +807,9 @@ def move(self, translation_vector: np.array | list): if self.probe_planar_contour is not None: self.probe_planar_contour += translation_vector - def rotate(self, theta: float, center: list | np.ndarray | None = None, axis: "xy" | "yz" | "xz" | None = None): + def rotate( + self, theta: float, center: list | np.ndarray | None = None, axis: Literal["xy", "yz", "xz"] | None = None + ): """ Rotate the probe around a specified axis. @@ -856,7 +857,7 @@ def rotate(self, theta: float, center: list | np.ndarray | None = None, axis: "x new_vertices = (self.probe_planar_contour - center) @ R + center self.probe_planar_contour = new_vertices - def rotate_contacts(self, thetas: float | np.array[float] | list[float]): + def rotate_contacts(self, thetas: float | np.ndarray[float] | list[float]): """ Rotate each contact of the probe. Internally, it modifies the contact_plane_axes. @@ -976,7 +977,7 @@ def from_dict(d: dict) -> "Probe": return probe - def to_numpy(self, complete: bool = False) -> np.array: + def to_numpy(self, complete: bool = False) -> np.ndarray: """ Export the probe to a numpy structured array. This array handles all contact attributes. @@ -1387,12 +1388,12 @@ def from_dataframe(df: "pandas.DataFrame") -> "Probe": def to_image( self, - values: np.array | list, + values: np.ndarray | list, pixel_size: float = 0.5, - num_pixel: Optional[int] = None, - method: "linear" | "nearest" | "cubic" = "linear", - xlims: Optional[tuple] = None, - ylims: Optional[tuple] = None, + num_pixel: int | None = None, + method: Literal["linear", "nearest", "cubic"] = "linear", + xlims: tuple | None = None, + ylims: tuple | None = None, ) -> tuple[np.ndarray, tuple, tuple]: """ Generated a 2d (image) from a values vector with an interpolation @@ -1404,13 +1405,13 @@ def to_image( vector same size as contact number to be color plotted pixel_size : float, default: 0.5 size of one pixel in micrometers - num_pixel : Optional[int] | None, default: None + num_pixel : int | None, default: None alternative to pixel_size give pixel number of the image width method : "linear" | "nearest" | "cubic", default: "linear" Method of interpolation to generate a grid mesh - xlims : Optional[tuple], default: None + xlims : tuple | None, default: None Force image xlims - ylims : Optional[tuple], default: None + ylims : tuple | None, default: None Force image ylims Returns @@ -1511,7 +1512,7 @@ def get_slice(self, selection: np.ndarray[bool | int]): return sliced_probe -def _2d_to_3d(data2d: np.ndarray, axes: "xy" | "yz" | "xz") -> np.ndarray: +def _2d_to_3d(data2d: np.ndarray, axes: Literal["xy", "yz", "xz"]) -> np.ndarray: """ Add a third dimension on the given axes @@ -1535,7 +1536,7 @@ def _2d_to_3d(data2d: np.ndarray, axes: "xy" | "yz" | "xz") -> np.ndarray: return data3d -def select_axes(data: np.ndarray, axes: "xy" | "yz" | "xz" = "xy") -> np.ndarray: +def select_axes(data: np.ndarray, axes: Literal["xy", "yz", "xz", "xyz"] = "xy") -> np.ndarray: """ Select axes in a 3d or 2d array. @@ -1558,7 +1559,7 @@ def select_axes(data: np.ndarray, axes: "xy" | "yz" | "xz" = "xy") -> np.ndarray return data[:, dims] -def _3d_to_2d(data3d: np.ndarray, axes: "xy" | "yz" | "xz" = "xy") -> np.ndarray: +def _3d_to_2d(data3d: np.ndarray, axes: Literal["xy", "yz", "xz"] = "xy") -> np.ndarray: """ Reduce 3d array to 2d array on given axes. @@ -1599,7 +1600,7 @@ def _rotation_matrix_2d(theta: float) -> np.ndarray: return R -def _rotation_matrix_3d(axis: np.array | list, theta: float) -> np.ndarray: +def _rotation_matrix_3d(axis: np.ndarray | list, theta: float) -> np.ndarray: """ Returns 3D rotation matrix diff --git a/src/probeinterface/probegroup.py b/src/probeinterface/probegroup.py index ed55b25d..0ece2830 100644 --- a/src/probeinterface/probegroup.py +++ b/src/probeinterface/probegroup.py @@ -1,4 +1,3 @@ -from __future__ import annotations import numpy as np from .utils import generate_unique_ids from .probe import Probe @@ -211,13 +210,13 @@ def get_global_device_channel_indices(self) -> np.ndarray: channels["device_channel_indices"] = arr["device_channel_indices"] return channels - def set_global_device_channel_indices(self, channels: np.array | list): + def set_global_device_channel_indices(self, channels: np.ndarray | list): """ Set global indices for all probes Parameters ---------- - channels: np.array | list + channels: np.ndarray | list The device channal indices to be set """ channels = np.asarray(channels) diff --git a/src/probeinterface/shank.py b/src/probeinterface/shank.py index 4d872e08..73cb2096 100644 --- a/src/probeinterface/shank.py +++ b/src/probeinterface/shank.py @@ -1,4 +1,3 @@ -from __future__ import annotations import numpy as np diff --git a/src/probeinterface/utils.py b/src/probeinterface/utils.py index 6e8aa8ac..0aea21a1 100644 --- a/src/probeinterface/utils.py +++ b/src/probeinterface/utils.py @@ -2,7 +2,6 @@ Some utility functions """ -from __future__ import annotations from importlib import import_module from types import ModuleType @@ -104,7 +103,7 @@ def combine_probes(probes: list[Probe], connect_shape: bool = True) -> Probe: return multi_shank -def generate_unique_ids(min: int, max: int, n: int, trials: int = 20) -> np.array: +def generate_unique_ids(min: int, max: int, n: int, trials: int = 20) -> np.ndarray: """ Create n unique identifiers. Creates `n` unique integer identifiers between `min` and `max` within a diff --git a/src/probeinterface/wiring.py b/src/probeinterface/wiring.py index b2a1169e..8378ad7b 100644 --- a/src/probeinterface/wiring.py +++ b/src/probeinterface/wiring.py @@ -2,7 +2,6 @@ Automatically set the `Probe.device_channel_indices` field. """ -from __future__ import annotations import numpy as np # This code will not be formatted by Black