Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
- name: Install dependencies
run: |
pip install -e .
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.11"
python-version: "3.14"
- name: Install dependencies
run: |
pip install -e .
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,4 @@ healthchecksdb
.vscode
*.gwl
*Sandbox*.ipynb
uv.lock
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@
exclude: \.(ipynb|nblink)$
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v6.0.0
hooks:
- id: check-merge-conflict
- id: check-toml
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
rev: 9.0.2
hooks:
- id: isort
name: isort
- repo: https://github.com/psf/black
rev: 22.3.0
rev: 26.5.1
hooks:
- id: black
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
rev: v2.4.0
hooks:
- id: mypy
exclude: 'test_.*?\.py$'
12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@ build-backend = "setuptools.build_meta"

[project]
name = "robotools"
version = "1.15.0"
version = "1.16.0"
description = "Pythonic in-silico liquid handling and creation of Tecan FreedomEVO worklists."
readme = "README.md"
requires-python = ">=3.10"
license = {text = "GNU Affero General Public License v3"}
requires-python = ">=3.11"
license = "AGPL-3.0-or-later"
authors = [
{name = "Michael Osthege", email = "m.osthege@fz-juelich.de"},
]
classifiers = [
"Programming Language :: Python",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Intended Audience :: Science/Research",
"Topic :: Scientific/Engineering",
]
dependencies = [
"numpy",
"typing_extensions",
]

[project.urls]
Expand Down
9 changes: 9 additions & 0 deletions robotools/evotools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,12 @@
from robotools.evotools.utils import get_well_position
from robotools.evotools.worklist import EvoWorklist
from robotools.worklists.exceptions import InvalidOperationError

__all__ = (
"Labwares",
"Tip",
"int_to_tip",
"get_well_position",
"EvoWorklist",
"InvalidOperationError",
)
94 changes: 52 additions & 42 deletions robotools/evotools/commands.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""This module implements functions to create advanced worklist commands."""
from typing import Iterable, List, Optional, Sequence, Tuple, Union

from typing import Iterable, Sequence

import numpy as np
import numpy.typing as npt

from robotools.evotools.types import Tip, int_to_tip
from robotools.evotools.utils import to_hex
from robotools.typing import Bools2D, Strings2D
from robotools.worklists.exceptions import InvalidOperationError

from .. import transform
Expand All @@ -21,7 +24,7 @@
""""Maximum dilutor volume in µL"""


def evo_make_selection_array(rows: int, columns: int, wells: Union[Iterable[str], np.ndarray]) -> np.ndarray:
def evo_make_selection_array(rows: int, columns: int, wells: Iterable[str] | npt.NDArray[np.str_]) -> Bools2D:
"""Translate well IDs to a numpy array with 1s (selected) and 0s (not selected).

Parameters
Expand All @@ -38,17 +41,10 @@ def evo_make_selection_array(rows: int, columns: int, wells: Union[Iterable[str]
selection_array : np.ndarray
Numpy array in labware dimensions with selected wells as 1 and others as 0
"""
# create array with a shape beffiting the labware dimensions
selection_array = np.zeros((rows, columns))
# get a dictionary with the "coordinates" of well IDs (A01, B01 etc) as tuples
well_index_dict = transform.make_well_index_dict(rows, columns)
# insert 1s for all selected wells
for well in np.asarray(wells).flatten():
selection_array[well_index_dict[well]] = 1
return selection_array
return np.isin(transform.make_well_array(rows, columns), wells)


def evo_get_selection(rows: int, cols: int, selected: np.ndarray) -> str:
def evo_get_selection(rows: int, cols: int, selected: Bools2D) -> str:
"""Function to generate the code string for the well selection of pipetting actions in EvoWare scripts (.esc).

Adapted from the C++ function detailed in the EvoWare manual to Python by Martin Beyß (except the test at the end).
Expand All @@ -73,7 +69,7 @@ def evo_get_selection(rows: int, cols: int, selected: np.ndarray) -> str:
bit_mask = 0
for x in range(cols):
for y in range(rows):
if selected[y, x] == 1:
if selected[y, x]:
bit_mask |= 1 << bit_counter
bit_counter += 1
if bit_counter > 6:
Expand All @@ -87,15 +83,15 @@ def evo_get_selection(rows: int, cols: int, selected: np.ndarray) -> str:


def prepare_evo_aspirate_dispense_parameters(
wells: Union[str, Sequence[str], np.ndarray],
wells: str | Sequence[str] | Strings2D,
*,
labware_position: Tuple[int, int],
volume: Union[float, Sequence[float], int],
labware_position: tuple[int, int],
volume: float | Sequence[float] | int,
liquid_class: str,
tips: Union[Sequence[Tip], Sequence[int]],
tips: Sequence[Tip] | Sequence[int],
arm: int,
max_volume: Optional[Union[int, float]] = None,
) -> Tuple[List[str], Tuple[int, int], List[float], str, List[Tip]]:
max_volume: int | float | None = None,
) -> tuple[list[str], tuple[int, int], list[float], str, list[Tip]]:
# wells, labware_position, volume, liquid_class, tecan_tips
"""Validates and prepares aspirate/dispense parameters.

Expand Down Expand Up @@ -175,7 +171,7 @@ def prepare_evo_aspirate_dispense_parameters(
raise ValueError(f"Invalid volume: {volume}")

# apply rounding and corrections for the right string formatting
volume_list: List[float] = np.round(volume, decimals=2).tolist()
volume_list: list[float] = np.round(volume, decimals=2).tolist()

if liquid_class is None:
raise ValueError(f"Missing required parameter: liquid_class")
Expand All @@ -202,9 +198,9 @@ def prepare_evo_aspirate_dispense_parameters(
return wells_list, labware_position, volume_list, liquid_class, tecan_tips


def require_single_column_selection(selection: np.ndarray):
def require_single_column_selection(selection: Bools2D):
"""Raises an error if wells from more than one column are selected."""
ncols = np.any(selection > 0, axis=0).sum()
ncols = np.any(selection, axis=0).sum()
if ncols >= 2:
raise ValueError(
"Wells from more than one column are selected.\nSelect only wells from one column per pipetting action."
Expand All @@ -216,13 +212,13 @@ def evo_aspirate(
*,
n_rows: int,
n_columns: int,
wells: Union[str, Sequence[str]],
labware_position: Tuple[int, int],
volume: Union[float, Sequence[float], int],
wells: str | Sequence[str],
labware_position: tuple[int, int],
volume: float | Sequence[float] | int,
liquid_class: str,
tips: Union[Sequence[Tip], Sequence[int]],
tips: Sequence[Tip] | Sequence[int],
arm: int = 0,
max_volume: Optional[Union[int, float]] = np.nan,
max_volume: int | float | None = np.nan,
) -> str:
"""Command for aspirating with the EvoWARE aspirate command WITHOUT digital volume tracking.

Expand Down Expand Up @@ -257,7 +253,13 @@ def evo_aspirate(
max_volume = MAX_DILUTOR_VOLUME

# perform consistency checks
(wells, labware_position, volume, liquid_class, tips,) = prepare_evo_aspirate_dispense_parameters(
(
wells,
labware_position,
volume,
liquid_class,
tips,
) = prepare_evo_aspirate_dispense_parameters(
wells=wells,
labware_position=labware_position,
volume=volume,
Expand Down Expand Up @@ -293,13 +295,13 @@ def evo_dispense(
*,
n_rows: int,
n_columns: int,
wells: Union[str, Sequence[str]],
labware_position: Tuple[int, int],
volume: Union[float, Sequence[float], int],
wells: str | Sequence[str],
labware_position: tuple[int, int],
volume: float | Sequence[float] | int,
liquid_class: str,
tips: Union[Sequence[Tip], Sequence[int]],
tips: Sequence[Tip] | Sequence[int],
arm: int = 0,
max_volume: Optional[Union[int, float]] = np.nan,
max_volume: int | float | None = np.nan,
) -> str:
"""Command for dispensing using the EvoWARE dispense command WITHOUT digital volume tracking.

Expand Down Expand Up @@ -334,7 +336,13 @@ def evo_dispense(
max_volume = MAX_DILUTOR_VOLUME

# perform consistency checks
(wells, labware_position, volume, liquid_class, tips,) = prepare_evo_aspirate_dispense_parameters(
(
wells,
labware_position,
volume,
liquid_class,
tips,
) = prepare_evo_aspirate_dispense_parameters(
wells=wells,
labware_position=labware_position,
volume=volume,
Expand Down Expand Up @@ -368,9 +376,9 @@ def evo_dispense(

def prepare_evo_wash_parameters(
*,
tips: Union[List[Tip], List[int]],
waste_location: Tuple[int, int],
cleaner_location: Tuple[int, int],
tips: Sequence[Tip] | Sequence[int],
waste_location: tuple[int, int],
cleaner_location: tuple[int, int],
arm: int = 0,
waste_vol: float = 3.0,
waste_delay: int = 500,
Expand All @@ -381,7 +389,7 @@ def prepare_evo_wash_parameters(
retract_speed: int = 30,
fastwash: int = 1,
low_volume: int = 0,
) -> Tuple[List[Tip], Tuple[int, int], Tuple[int, int], int, float, int, float, int, int, int, int, int, int]:
) -> tuple[list[Tip], tuple[int, int], tuple[int, int], int, float, int, float, int, int, int, int, int, int]:
"""Validates and prepares aspirate/dispense parameters.

Parameters
Expand Down Expand Up @@ -445,7 +453,7 @@ def prepare_evo_wash_parameters(
if tips is None:
raise ValueError("Missing required parameter: tips")

tecan_tips: List[Tip] = []
tecan_tips: list[Tip] = []
for tip in tips:
if isinstance(tip, int) and not isinstance(tip, Tip):
# User-specified integers from 1-8 need to be converted to Tecan logic
Expand Down Expand Up @@ -547,9 +555,9 @@ def prepare_evo_wash_parameters(

def evo_wash(
*,
tips: Union[List[Tip], List[int]],
waste_location: Tuple[int, int],
cleaner_location: Tuple[int, int],
tips: Sequence[Tip] | Sequence[int],
waste_location: tuple[int, int],
cleaner_location: tuple[int, int],
arm: int = 0,
waste_vol: float = 3.0,
waste_delay: int = 500,
Expand All @@ -561,7 +569,9 @@ def evo_wash(
fastwash: int = 1,
low_volume: int = 0,
) -> str:
"""Command for aspirating with the EvoWARE aspirate command. As many wells in one column may be selected as your liquid handling arm has pipettes.
"""Command for aspirating with the EvoWARE aspirate command.

As many wells in one column may be selected as your liquid handling arm has pipettes.
This method generates the full command (as can be observed when opening a .esc file with an editor) and calls upon other functions to create the code string
specifying the target wells.

Expand Down
1 change: 1 addition & 0 deletions robotools/evotools/types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Enums and classes, and related helper functions."""

import enum

__all__ = (
Expand Down
2 changes: 2 additions & 0 deletions robotools/evotools/utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Generic utility functions."""

import re

from robotools.liquidhandling import Labware


def to_hex(dec: int):
"""Method from stackoverflow to convert decimal to hex.

Link: https://stackoverflow.com/questions/5796238/python-convert-decimal-to-hex
Solution posted by user "Chunghee Kim" on 21.11.2020.
"""
Expand Down
Loading
Loading