From f1742ba6f3abbde02bc8712a35a7498e187a600d Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Fri, 12 Dec 2025 18:04:00 +0000 Subject: [PATCH 1/6] create `sort_by_xy_and_chunk_by_x` proposal --- pylabrobot/resources/utils.py | 75 ++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/pylabrobot/resources/utils.py b/pylabrobot/resources/utils.py index b33866cf63c..91223387308 100644 --- a/pylabrobot/resources/utils.py +++ b/pylabrobot/resources/utils.py @@ -1,6 +1,7 @@ import re from string import ascii_uppercase as LETTERS -from typing import Dict, List, Optional, Type, TypeVar +from typing import Any, Dict, List, Optional, Type, TypeVar +from itertools import groupby from pylabrobot.resources.coordinate import Coordinate from pylabrobot.resources.resource import Resource @@ -227,3 +228,75 @@ def query( ) ) return matched + +def sort_by_xy_and_chunk_by_x( + resources: list[Any], + max_chunk_size: int, + sort_chunks_by_size: bool = True, +) -> list[list[Any]]: + """ + Sort resources by... + 1. x ascending + 2. y descending within each x + 3. grouped into chunks by x + 4. split into sub-chunks of size <= max_chunk_size + 5. chunks sorted by length (smallest → largest) + + Parameters + ---------- + resources : list[Any] + List of resources that implement .get_absolute_location() + returning an object with x and y attributes. + max_chunk_size : int + Maximum size for any single chunk. + + Returns + ------- + list[list[Any]] + A list of grouped and sorted resources. + + Example + ------- + >>> sorted_chunks = sort_by_xy_and_chunk_by_x(well_list, max_chunk_size=8) + >>> [ + ... list( + ... zip( + ... [r.get_identifier() for r in chunk], + ... [r.get_absolute_location() for r in chunk], + ... ) + ... ) + ... for chunk in sorted_chunks + ... ] + [[('D1', Coordinate(x=450.9, y=402.3, z=164.45)), + ('H1', Coordinate(x=450.9, y=366.3, z=164.45)), ...], + [('D2', Coordinate(x=459.9, y=402.3, z=164.45)), ...]] + """ + # 1. & 2.: Sort by x ascending, y descending + sorted_resources = sorted( + resources, + key=lambda res: ( + res.get_absolute_location().x, + -res.get_absolute_location().y, + ), + ) + + # 3. Group into chunks by x + chunks = [ + list(group) + for _, group in groupby( + sorted_resources, + key=lambda res: res.get_absolute_location().x, + ) + ] + + # 4. Split chunks by max_chunk_size + split_chunks: list[list[Any]] = [] + for chunk in chunks: + for i in range(0, len(chunk), max_chunk_size): + split_chunks.append(chunk[i : i + max_chunk_size]) + + # Optional 5: Sort chunks by number of elements + if sort_chunks_by_size: + return sorted(split_chunks, key=len) + else: + return split_chunks From 309eb979567a734bffbad4a56caea9f0b87d3d99 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Fri, 12 Dec 2025 18:05:44 +0000 Subject: [PATCH 2/6] `make format` --- pylabrobot/resources/utils.py | 139 +++++++++++++++++----------------- 1 file changed, 70 insertions(+), 69 deletions(-) diff --git a/pylabrobot/resources/utils.py b/pylabrobot/resources/utils.py index 91223387308..01cce590c36 100644 --- a/pylabrobot/resources/utils.py +++ b/pylabrobot/resources/utils.py @@ -1,7 +1,7 @@ import re +from itertools import groupby from string import ascii_uppercase as LETTERS from typing import Any, Dict, List, Optional, Type, TypeVar -from itertools import groupby from pylabrobot.resources.coordinate import Coordinate from pylabrobot.resources.resource import Resource @@ -229,74 +229,75 @@ def query( ) return matched + def sort_by_xy_and_chunk_by_x( - resources: list[Any], - max_chunk_size: int, - sort_chunks_by_size: bool = True, + resources: list[Any], + max_chunk_size: int, + sort_chunks_by_size: bool = True, ) -> list[list[Any]]: - """ - Sort resources by... - 1. x ascending - 2. y descending within each x - 3. grouped into chunks by x - 4. split into sub-chunks of size <= max_chunk_size - 5. chunks sorted by length (smallest → largest) - - Parameters - ---------- - resources : list[Any] - List of resources that implement .get_absolute_location() - returning an object with x and y attributes. - max_chunk_size : int - Maximum size for any single chunk. - - Returns - ------- - list[list[Any]] - A list of grouped and sorted resources. - - Example - ------- - >>> sorted_chunks = sort_by_xy_and_chunk_by_x(well_list, max_chunk_size=8) - >>> [ - ... list( - ... zip( - ... [r.get_identifier() for r in chunk], - ... [r.get_absolute_location() for r in chunk], - ... ) - ... ) - ... for chunk in sorted_chunks - ... ] - [[('D1', Coordinate(x=450.9, y=402.3, z=164.45)), - ('H1', Coordinate(x=450.9, y=366.3, z=164.45)), ...], - [('D2', Coordinate(x=459.9, y=402.3, z=164.45)), ...]] - """ - # 1. & 2.: Sort by x ascending, y descending - sorted_resources = sorted( - resources, - key=lambda res: ( - res.get_absolute_location().x, - -res.get_absolute_location().y, - ), - ) + """ + Sort resources by... + 1. x ascending + 2. y descending within each x + 3. grouped into chunks by x + 4. split into sub-chunks of size <= max_chunk_size + 5. chunks sorted by length (smallest → largest) + + Parameters + ---------- + resources : list[Any] + List of resources that implement .get_absolute_location() + returning an object with x and y attributes. + max_chunk_size : int + Maximum size for any single chunk. + + Returns + ------- + list[list[Any]] + A list of grouped and sorted resources. + + Example + ------- + >>> sorted_chunks = sort_by_xy_and_chunk_by_x(well_list, max_chunk_size=8) + >>> [ + ... list( + ... zip( + ... [r.get_identifier() for r in chunk], + ... [r.get_absolute_location() for r in chunk], + ... ) + ... ) + ... for chunk in sorted_chunks + ... ] + [[('D1', Coordinate(x=450.9, y=402.3, z=164.45)), + ('H1', Coordinate(x=450.9, y=366.3, z=164.45)), ...], + [('D2', Coordinate(x=459.9, y=402.3, z=164.45)), ...]] + """ + # 1. & 2.: Sort by x ascending, y descending + sorted_resources = sorted( + resources, + key=lambda res: ( + res.get_absolute_location().x, + -res.get_absolute_location().y, + ), + ) - # 3. Group into chunks by x - chunks = [ - list(group) - for _, group in groupby( - sorted_resources, - key=lambda res: res.get_absolute_location().x, - ) - ] - - # 4. Split chunks by max_chunk_size - split_chunks: list[list[Any]] = [] - for chunk in chunks: - for i in range(0, len(chunk), max_chunk_size): - split_chunks.append(chunk[i : i + max_chunk_size]) - - # Optional 5: Sort chunks by number of elements - if sort_chunks_by_size: - return sorted(split_chunks, key=len) - else: - return split_chunks + # 3. Group into chunks by x + chunks = [ + list(group) + for _, group in groupby( + sorted_resources, + key=lambda res: res.get_absolute_location().x, + ) + ] + + # 4. Split chunks by max_chunk_size + split_chunks: list[list[Any]] = [] + for chunk in chunks: + for i in range(0, len(chunk), max_chunk_size): + split_chunks.append(chunk[i : i + max_chunk_size]) + + # Optional 5: Sort chunks by number of elements + if sort_chunks_by_size: + return sorted(split_chunks, key=len) + else: + return split_chunks From 910f338f6b656da179f398150801523facdfa3ff Mon Sep 17 00:00:00 2001 From: Camillo Moschner <122165124+BioCam@users.noreply.github.com> Date: Fri, 12 Dec 2025 18:07:53 +0000 Subject: [PATCH 3/6] Update pylabrobot/resources/utils.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pylabrobot/resources/utils.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/pylabrobot/resources/utils.py b/pylabrobot/resources/utils.py index 01cce590c36..e1f9484f528 100644 --- a/pylabrobot/resources/utils.py +++ b/pylabrobot/resources/utils.py @@ -272,21 +272,24 @@ def sort_by_xy_and_chunk_by_x( ('H1', Coordinate(x=450.9, y=366.3, z=164.45)), ...], [('D2', Coordinate(x=459.9, y=402.3, z=164.45)), ...]] """ + # Cache absolute locations to avoid redundant calls + resources_with_loc = [(res, res.get_absolute_location()) for res in resources] + # 1. & 2.: Sort by x ascending, y descending - sorted_resources = sorted( - resources, - key=lambda res: ( - res.get_absolute_location().x, - -res.get_absolute_location().y, + sorted_resources_with_loc = sorted( + resources_with_loc, + key=lambda pair: ( + pair[1].x, + -pair[1].y, ), ) # 3. Group into chunks by x chunks = [ - list(group) + [res for res, _ in group] for _, group in groupby( - sorted_resources, - key=lambda res: res.get_absolute_location().x, + sorted_resources_with_loc, + key=lambda pair: pair[1].x, ) ] From c0c673ba03ae46f09d1f206e245ce54ea18a11e7 Mon Sep 17 00:00:00 2001 From: Camillo Moschner Date: Fri, 12 Dec 2025 18:10:21 +0000 Subject: [PATCH 4/6] update docstring --- pylabrobot/resources/utils.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pylabrobot/resources/utils.py b/pylabrobot/resources/utils.py index e1f9484f528..794a542e54c 100644 --- a/pylabrobot/resources/utils.py +++ b/pylabrobot/resources/utils.py @@ -236,20 +236,30 @@ def sort_by_xy_and_chunk_by_x( sort_chunks_by_size: bool = True, ) -> list[list[Any]]: """ - Sort resources by... - 1. x ascending - 2. y descending within each x - 3. grouped into chunks by x - 4. split into sub-chunks of size <= max_chunk_size - 5. chunks sorted by length (smallest → largest) + Sort resources spatially and partition them into chunks for channel processing. + + Procedure + --------- + 1. Sort all resources by: + - x ascending + - y descending within each x + 2. Group resources into chunks based on identical x values. + 3. Split each chunk into sub-chunks of size <= max_chunk_size. + 4. Optionally sort the resulting sub-chunks by their length + (smallest → largest). Parameters ---------- resources : list[Any] - List of resources that implement .get_absolute_location() - returning an object with x and y attributes. + List of resources that implement ``.get_absolute_location()``, + returning an object with ``x`` and ``y`` attributes. max_chunk_size : int - Maximum size for any single chunk. + Maximum allowed size for any produced chunk or sub-chunk. + sort_chunks_by_size : bool, optional + If True (default), the output list of chunks is sorted by + ascending chunk size. If False, chunks retain their original + order. + Returns ------- From 61db06e22819be256c0bd2547ecfb440c3720859 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 17 Dec 2025 20:07:24 -0800 Subject: [PATCH 5/6] tiny --- pylabrobot/resources/utils.py | 92 +++++++++++++++-------------------- 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/pylabrobot/resources/utils.py b/pylabrobot/resources/utils.py index 794a542e54c..6f4f9939655 100644 --- a/pylabrobot/resources/utils.py +++ b/pylabrobot/resources/utils.py @@ -230,11 +230,14 @@ def query( return matched +T = TypeVar("T", bound=Resource) + + def sort_by_xy_and_chunk_by_x( - resources: list[Any], + resources: list[T], max_chunk_size: int, sort_chunks_by_size: bool = True, -) -> list[list[Any]]: +) -> list[list[T]]: """ Sort resources spatially and partition them into chunks for channel processing. @@ -245,72 +248,57 @@ def sort_by_xy_and_chunk_by_x( - y descending within each x 2. Group resources into chunks based on identical x values. 3. Split each chunk into sub-chunks of size <= max_chunk_size. - 4. Optionally sort the resulting sub-chunks by their length - (smallest → largest). - - Parameters - ---------- - resources : list[Any] - List of resources that implement ``.get_absolute_location()``, - returning an object with ``x`` and ``y`` attributes. - max_chunk_size : int - Maximum allowed size for any produced chunk or sub-chunk. - sort_chunks_by_size : bool, optional - If True (default), the output list of chunks is sorted by - ascending chunk size. If False, chunks retain their original - order. - - - Returns - ------- - list[list[Any]] - A list of grouped and sorted resources. - - Example - ------- - >>> sorted_chunks = sort_by_xy_and_chunk_by_x(well_list, max_chunk_size=8) - >>> [ - ... list( - ... zip( - ... [r.get_identifier() for r in chunk], - ... [r.get_absolute_location() for r in chunk], - ... ) - ... ) - ... for chunk in sorted_chunks - ... ] - [[('D1', Coordinate(x=450.9, y=402.3, z=164.45)), - ('H1', Coordinate(x=450.9, y=366.3, z=164.45)), ...], - [('D2', Coordinate(x=459.9, y=402.3, z=164.45)), ...]] + 4. Optionally sort the resulting sub-chunks by their length (smallest -> largest). + + Args: + resources: List of resources that implement ``.get_absolute_location()``, returning an object with ``x`` and ``y`` attributes. + max_chunk_size: Maximum allowed size for any produced chunk or sub-chunk. + sort_chunks_by_size: If True (default), the output list of chunks is sorted by ascending chunk size. If False, chunks retain their original order. + + Returns: + A list of grouped and sorted resources. + + Example: + >>> sorted_chunks = sort_by_xy_and_chunk_by_x(well_list, max_chunk_size=8) + >>> [ + ... list( + ... zip( + ... [r.get_identifier() for r in chunk], + ... [r.get_absolute_location() for r in chunk], + ... ) + ... ) + ... for chunk in sorted_chunks + ... ] + + [[('D1', Coordinate(x=450.9, y=402.3, z=164.45)), + ('H1', Coordinate(x=450.9, y=366.3, z=164.45)), ...], + [('D2', Coordinate(x=459.9, y=402.3, z=164.45)), ...]] + """ - # Cache absolute locations to avoid redundant calls - resources_with_loc = [(res, res.get_absolute_location()) for res in resources] # 1. & 2.: Sort by x ascending, y descending sorted_resources_with_loc = sorted( - resources_with_loc, - key=lambda pair: ( - pair[1].x, - -pair[1].y, + resources, + key=lambda r: ( + r.get_absolute_location().x, + -r.get_absolute_location().y, ), ) # 3. Group into chunks by x - chunks = [ - [res for res, _ in group] + grouped_by_x = [ + list(group) for _, group in groupby( sorted_resources_with_loc, - key=lambda pair: pair[1].x, + key=lambda r: r.get_absolute_location().x, ) ] # 4. Split chunks by max_chunk_size split_chunks: list[list[Any]] = [] - for chunk in chunks: + for chunk in grouped_by_x: for i in range(0, len(chunk), max_chunk_size): split_chunks.append(chunk[i : i + max_chunk_size]) # Optional 5: Sort chunks by number of elements - if sort_chunks_by_size: - return sorted(split_chunks, key=len) - else: - return split_chunks + return sorted(split_chunks, key=len) if sort_chunks_by_size else split_chunks From babeb61a76b94966c0c96211bb26e29ab9b6c527 Mon Sep 17 00:00:00 2001 From: Rick Wierenga Date: Wed, 17 Dec 2025 20:09:06 -0800 Subject: [PATCH 6/6] t --- pylabrobot/resources/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pylabrobot/resources/utils.py b/pylabrobot/resources/utils.py index 6f4f9939655..7d11e870dc5 100644 --- a/pylabrobot/resources/utils.py +++ b/pylabrobot/resources/utils.py @@ -230,14 +230,14 @@ def query( return matched -T = TypeVar("T", bound=Resource) +R = TypeVar("R", bound=Resource) def sort_by_xy_and_chunk_by_x( - resources: list[T], + resources: list[R], max_chunk_size: int, sort_chunks_by_size: bool = True, -) -> list[list[T]]: +) -> list[list[R]]: """ Sort resources spatially and partition them into chunks for channel processing.