From fad6df11f9d8f0a1c2d63df8424a40988c420201 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 13:16:00 +0200 Subject: [PATCH 01/11] Update core.py --- deeptrack/backend/core.py | 96 ++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/deeptrack/backend/core.py b/deeptrack/backend/core.py index d871add89..94f49ba3d 100644 --- a/deeptrack/backend/core.py +++ b/deeptrack/backend/core.py @@ -1,21 +1,21 @@ """Core data structures for DeepTrack2. -This module defines the foundational data structures used throughout -DeepTrack2 for constructing, managing, and evaluating computational graphs -with flexible data storage and dependency management. +This module defines the foundational data structures used throughout DeepTrack2 +for constructing, managing, and evaluating computational graphs with flexible +data storage and dependency management. Key Features ------------ - **Hierarchical Data Management** - Provides validated, hierarchical data containers (`DeepTrackDataObject` - and `DeepTrackDataDict`) for storing data and managing complex, nested - data structures. Supports dependency tracking and flexible indexing. + Provides validated, hierarchical data containers (`DeepTrackDataObject` and + `DeepTrackDataDict`) for storing data and managing complex, nested data + structures. Supports dependency tracking and flexible indexing. - **Computation Graphs with Lazy Evaluation** - Implements the `DeepTrackNode` class, the core abstraction for nodes in - a computational graph. Supports lazy evaluation, caching, dependency + Implements the `DeepTrackNode` class, the core abstraction for nodes in a + computational graph. Supports lazy evaluation, caching, dependency tracking, and operator overloading for intuitive composition of complex computational pipelines. @@ -40,28 +40,20 @@ - `DeepTrackNode`: Node in a computation graph with operator overloading. - Represents a node in a computation graph, capable of storing and - computing values based on dependencies, with full support for lazy - evaluation, dependency tracking, and operator overloading. + Represents a node in a computation graph, capable of storing and computing + values based on dependencies, with full support for lazy evaluation, + dependency tracking, and operator overloading. Functions: -- `_equivalent(a, b)` +- `_equivalent(a, b) -> bool` - def _equivalent(a: Any, b: Any) -> bool + Determines whether two objects should be considered equivalent, according + to DeepTrack2's internal rules (identity, empty lists, etc). - Determines whether two objects should be considered equivalent, - according to DeepTrack2's internal rules (identity, empty lists, etc). +- `_create_node_with_operator(op, a, b) -> DeepTrackNode` -- `_create_node_with_operator(op, a, b)` - - def _create_node_with_operator( - op: Callable, - a: Any, - b: Any, - ) -> DeepTrackNode - - Internal helper to create a new computation node by applying a + Internal helper function to create a new computation node by applying a specified operator to two operands, establishing correct graph relationships and supporting operator overloading. @@ -120,9 +112,9 @@ def _create_node_with_operator( from __future__ import annotations from collections.abc import ItemsView, KeysView, ValuesView -import operator # Operator overloading for computation nodes. -from weakref import WeakSet # Manages relationships between nodes without - # creating circular dependencies. +import operator # Operator overloading for computation nodes +from weakref import WeakSet # To manage relationships between nodes without + # creating circular dependencies from typing import Any, Callable, Iterator from deeptrack.utils import get_kwarg_names @@ -183,23 +175,29 @@ class DeepTrackDataObject: >>> import deeptrack as dt Create a `DeepTrackDataObject`: + >>> data_obj = dt.DeepTrackDataObject() >>> data_obj + DeepTrackDataObject(data=None, valid=False) Store a value in this container: + >>> data_obj.store(42) >>> data_obj DeepTrackDataObject(data=42, valid=True) Access the currently stored value: + >>> data_obj.current_value() 42 Check if the stored data is valid: + >>> data_obj.is_valid() True Invalidate the stored data: + >>> data_obj.invalidate() >>> data_obj DeepTrackDataObject(data=42, valid=False) @@ -208,6 +206,7 @@ class DeepTrackDataObject: False Validate the data to restore its valid status: + >>> data_obj.validate() >>> data_obj DeepTrackDataObject(data=42, valid=True) @@ -284,8 +283,7 @@ def __repr__(self: DeepTrackDataObject) -> str: """Return the string representation of the object. Provides a concise representation of the data object, including the - stored data and its validity flag. It is useful for debugging and - logging purposes. + stored data and its validity flag. Useful for debugging and logging. Returns ------- @@ -307,15 +305,13 @@ class DeepTrackDataDict: `DeepTrackDataDict` can store multiple `DeepTrackDataObject` instances, each associated with a unique tuple of integers (its `_ID`). - **Use of _IDs** - The default `_ID` is an empty tuple, `_ID = ()`. - Once the first entry is created, all `_ID`s must match the set key length. + Once the first entry is created, all `_ID`s must match the set key-length. When retrieving the data associated to an `_ID`: - - If an `_ID` longer than the set key length is requested, it is trimmed. - - If an `_ID` shorter than the set key length is requested, a dictionary + - If an `_ID` longer than the set key-length is requested, it is trimmed. + - If an `_ID` shorter than the set key-length is requested, a dictionary slice containing all matching entries is returned. NOTE: The `_ID`s are specifically used in the `Repeat` feature to allow it @@ -325,8 +321,8 @@ class DeepTrackDataDict: ---------- keylength: int or None Read-only property exposing the internal variable with the length of - the `_ID`s set when the first entry is created. If `None`, no entries - have been created, and any `_ID` length is valid. + the `_ID`s set when the first entry is created. If `None`, no entry has + been created, and any `_ID` length is valid. dict: dict[tuple[int, ...], DeepTrackDataObject] or {} Read-only property exposing the internal dictionary of stored data, `_dict`. This is a dictionary mapping tuples of integers (`_ID`s) to @@ -341,7 +337,7 @@ class DeepTrackDataDict: `validate() -> None` Mark all stored data objects as valid. `valid_index(_ID) -> bool` - Check if the given _ID is valid for the current configuration. + Check if the given `_ID` is valid for the current configuration. `__getitem__(_ID) -> DeepTrackDataObject or dict[_ID, DeepTrackDataObject]` Retrieve data associated with the `_ID`. Can return a `DeepTrackDataObject`, or a dict of `DeepTrackDataObject`s if `_ID` is @@ -366,11 +362,13 @@ class DeepTrackDataDict: >>> import deeptrack as dt Create a structure to store multiple, indexed instances of data: + >>> data_dict = dt.DeepTrackDataDict() >>> data_dict DeepTrackDataDict(0 entries, keylength=None) Create the entries: + >>> data_dict.create_index((0, 0)) >>> data_dict.create_index((0, 1)) >>> data_dict.create_index((1, 0)) @@ -379,6 +377,7 @@ class DeepTrackDataDict: DeepTrackDataDict(4 entries, keylength=2) Store the values associated with each `_ID`: + >>> data_dict[(0, 0)].store("Data at (0, 0)") >>> data_dict[(0, 1)].store("Data at (0, 1)") >>> data_dict[(1, 0)].store("Data at (1, 0)") @@ -387,6 +386,7 @@ class DeepTrackDataDict: DeepTrackDataDict(4 entries, keylength=2) Retrieve values based on their `_ID`s: + >>> data_dict[(0, 0)] DeepTrackDataObject(data='Data at (0, 0)', valid=True) @@ -394,17 +394,21 @@ class DeepTrackDataDict: 'Data at (0, 0)' >>> data_dict[(1, 1)] + DeepTrackDataObject(data='Data at (1, 1)', valid=True) >>> data_dict[(1, 1)].current_value() + 'Data at (1, 1)' If requesting a shorter `_ID`, it returns all matching nested entries: + >>> data_dict[(0,)] {(0, 0): DeepTrackDataObject(data='Data at (0, 0)', valid=True), (0, 1): DeepTrackDataObject(data='Data at (0, 1)', valid=True)} Validate and invalidate all entries at once: + >>> data_dict.invalidate() >>> data_dict[(0, 0)].is_valid() False @@ -420,6 +424,7 @@ class DeepTrackDataDict: True Invalidate and validate a single entry: + >>> data_dict[(0, 1)].invalidate() >>> data_dict[(0, 1)].is_valid() False @@ -429,6 +434,7 @@ class DeepTrackDataDict: True Check if a given `_ID` exists: + >>> (1, 0) in data_dict True @@ -436,12 +442,13 @@ class DeepTrackDataDict: False Iterate over all entries: + >>> for key, value in data_dict.items(): ... print(key, value.current_value()) - (0, 0) DeepTrackDataObject(data='Data at (0, 0)', valid=True) - (0, 1) DeepTrackDataObject(data='Data at (0, 1)', valid=True) - (1, 0) DeepTrackDataObject(data='Data at (1, 0)', valid=True) - (1, 1) DeepTrackDataObject(data='Data at (1, 1)', valid=True) + (0, 0) Data at (0, 0) + (0, 1) Data at (0, 1) + (1, 0) Data at (1, 0) + (1, 1) Data at (1, 1) >>> for key in data_dict.keys(): ... print(key) @@ -458,6 +465,7 @@ class DeepTrackDataDict: DeepTrackDataObject(data='Data at (1, 1)', valid=True) Check if an `_ID` is valid according to current keylength: + >>> data_dict.valid_index((0, 1)) True @@ -848,7 +856,7 @@ class DeepTrackNode: garbage collection of nodes that are no longer used. dependencies: WeakSet[DeepTrackNode] Read-only property exposing the internal weak set `_dependencies` - containign the nodes on which this node depends (its parents). + containing the nodes on which this node depends (its parents). This is a weakref.WeakSet, for efficient memory management. _action: Callable[..., Any] The function or lambda-function to compute the node value. @@ -940,6 +948,7 @@ class DeepTrackNode: >>> from deeptrack.backend.core import DeepTrackNode Create three `DeepTrackNode` objects, as parent, child, and grandchild: + >>> parent = DeepTrackNode( ... node_name="parent", ... action=lambda: 10, @@ -956,6 +965,7 @@ class DeepTrackNode: >>> child.add_child(grandchild) Check all children of `parent` (includes `parent` itself): + >>> for node in parent.recurse_children(): ... print(node) DeepTrackNode(name='parent', len=0, action=) @@ -963,12 +973,14 @@ class DeepTrackNode: DeepTrackNode(name='grandchild', len=0, action=) Print the children tree: + >>> parent.print_children_tree() - DeepTrackNode 'parent' at 0x334202650 - DeepTrackNode 'child' at 0x334201cf0 - DeepTrackNode 'grandchild' at 0x334201ea0 Check all dependencies of `grandchild` (includes `grandchild` itself): + >>> for node in grandchild.recurse_dependencies(): ... print(node) DeepTrackNode(name='grandchild', len=0, action=) From b1e41ab8ab4e73440d2bd7cd38c68f65316cd56e Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 13:25:03 +0200 Subject: [PATCH 02/11] Update DTAT399A_backend.core.ipynb --- .../3-advanced-topics/DTAT399A_backend.core.ipynb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tutorials/3-advanced-topics/DTAT399A_backend.core.ipynb b/tutorials/3-advanced-topics/DTAT399A_backend.core.ipynb index 5a20ef492..761111862 100644 --- a/tutorials/3-advanced-topics/DTAT399A_backend.core.ipynb +++ b/tutorials/3-advanced-topics/DTAT399A_backend.core.ipynb @@ -239,7 +239,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 2.5. Validation and Invalidation\n", + "### 2.5. Validating and Invalidating\n", "\n", "When you invalidate the parent for a particular `_ID`, the child’s value for that `_ID` will also be marked as invalid (since it depends on the parent). This ensures that downstream computations are never out of sync." ] @@ -433,7 +433,7 @@ "source": [ "### 2.8. Evaluating, Resetting, and Recomputing Node Values\n", "\n", - "The value of a `DeepTrackNode` is evaluated and stored when the node is called with `.__call__()` (e.g., `node()`). Wehn you call a node multiple times, you will always get the same value as the node value is retrieved from memory (and not recomputed) each time.\n", + "The value of a `DeepTrackNode` is evaluated and stored when the node is called with `.__call__()` (e.g., `node()`). When you call a node multiple times, you will always get the same value as the node value is retrieved from memory (and not recomputed) each time.\n", "\n", "The state of the node (and its dependencies) can be reset using the `.update()` method. After this, calling the node will result in a recomputation of its value (e.g., `node.update()()`).\n", "\n", @@ -765,7 +765,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 3. Lazy Evaluation and Caching\n", + "## 3. Performing Lazy Evaluation and Caching\n", "\n", "A powerful feature of DeepTrack2 nodes is lazy evaluation: the node’s value is only computed when it is needed, and the result is cached until the node (or its dependencies) is invalidated. This avoids redundant computations and ensures high efficiency, especially in large graphs.\n", "\n", @@ -892,7 +892,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 3.3 Invalidation Forces Recalculation\n", + "### 3.3 Forcing Recalculation though Invalidation\n", "\n", "If you invalidate the node, the cache is cleared and the next call will recompute the value:" ] @@ -992,7 +992,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 4. Data Management with IDs\n", + "## 4. Managing Data with IDs\n", "\n", "The `DeepTrackDataDict` class provides an efficient, validated way to manage multiple data objects, each indexed by a unique tuple of integers.\n", "\n", @@ -1124,7 +1124,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 5. Operator Overloading and Pipeline Composition\n", + "## 5. Overloading Operators and Composing Pipeline\n", "\n", "A unique and powerful feature of `DeepTrackNode` is its support for operator overloading. This allows you to build complex computational pipelines by composing nodes using familiar arithmetic and comparison operators, making your code both expressive and readable.\n", "\n", @@ -1291,7 +1291,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 5.3. Comparison Operators for Graphs\n", + "### 5.3. Using Comparison Operators for Graphs\n", "\n", "Comparison operators also work on nodes, returning new nodes that compute boolean results." ] From c1787fe241c29e4c9844f024703e777f002bff55 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 13:29:49 +0200 Subject: [PATCH 03/11] Update DTDV411_style.ipynb --- tutorials/4-developers/DTDV411_style.ipynb | 33 +++++++++++----------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/tutorials/4-developers/DTDV411_style.ipynb b/tutorials/4-developers/DTDV411_style.ipynb index 79f3be12a..1096ed093 100644 --- a/tutorials/4-developers/DTDV411_style.ipynb +++ b/tutorials/4-developers/DTDV411_style.ipynb @@ -67,7 +67,7 @@ "\n", "from typing import Literal\n", "\n", - "def process_values(mode: Literal[\"sum\", \"average\"]) -> float:\n", + "def process_values(mode: Literal[\"sum\", \"average\"]) -> None:\n", " pass" ] }, @@ -86,7 +86,7 @@ "source": [ "# Incorrect\n", "\n", - "def process_values(mode: str) -> float:\n", + "def process_values(mode: str) -> None:\n", " pass" ] }, @@ -114,7 +114,7 @@ "source": [ "# Correct\n", "\n", - "def calculate_average(numbers: list[int]) -> float:\n", + "def calculate_average(numbers: list[int]) -> None:\n", " pass" ] }, @@ -133,7 +133,7 @@ "source": [ "# Incorrect\n", "\n", - "def calculate_average(numbers: list) -> float:\n", + "def calculate_average(numbers: list) -> None:\n", " pass" ] }, @@ -192,7 +192,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**NOTE:** Not all imports need to be placed at the top of the module. You can import libraries inside a class if they are only used within that class, or inside conditional statements if necessary." + "**NOTE:** Not all imports need to be placed at the top of the module. You can import libraries inside a class if they are only used within that class, or even inside conditional statements if necessary." ] }, { @@ -226,7 +226,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**NOTE:** The NumpyDoc guide specifies using phrases such as `int or list of int` rather than shorthand notations such as `int | list[int]`. We also prefer the phrases in the docstrings." + "**NOTE:** The NumpyDoc guide specifies using phrases such as `int or float` rather than shorthand notations such as `int | float`. We also prefer the phrases in the docstrings." ] }, { @@ -245,7 +245,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -275,12 +275,12 @@ " param3: \"numpy\" or \"torch\", optional\n", " This is a description of the third parameter. When the parameter is\n", " optional, its default value should be provided; in this case, \"numpy\".\n", - " param4: list of int or None, optional\n", + " param4: list[int] or None, optional\n", " This is a description of the fourth parameter. It defaults to None.\n", "\n", " Returns\n", " -------\n", - " list of int\n", + " list[int]\n", " This is a description of the return value. This can also be on multiple\n", " lines.\n", "\n", @@ -292,6 +292,10 @@ "\n", " Examples\n", " --------\n", + " >>> import deeptrack as dt\n", + "\n", + " This line of code uses the function:\n", + " \n", " >>> my_function(1, \"a\")\n", " [1, 2, 3]\n", "\n", @@ -309,7 +313,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -425,7 +429,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -466,12 +470,7 @@ "\n", "Functions:\n", "\n", - "- `my_function(param1, param2)`\n", - "\n", - " def my_function(\n", - " param1: int,\n", - " param2: str,\n", - " ) -> list[int]\n", + "- `my_function(param1, param2) -> list[int]`\n", "\n", " Short description of the function.\n", "\n", From b9697a01e6ca7f269f074ff280392e5d3ed5798d Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 13:31:40 +0200 Subject: [PATCH 04/11] Update core.py --- deeptrack/backend/core.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/deeptrack/backend/core.py b/deeptrack/backend/core.py index 94f49ba3d..63669e38a 100644 --- a/deeptrack/backend/core.py +++ b/deeptrack/backend/core.py @@ -988,12 +988,14 @@ class DeepTrackNode: DeepTrackNode(name='parent', len=0, action=) Print the dependency tree: + >>> grandchild.print_dependencies_tree() - DeepTrackNode 'grandchild' at 0x334201ea0 - DeepTrackNode 'child' at 0x334201cf0 - DeepTrackNode 'parent' at 0x334202650 Store and retrieve data for specific _IDs: + >>> parent.store(15, _ID=(0,)) >>> parent.store(20, _ID=(1,)) >>> parent.current_value((0,)) @@ -1002,6 +1004,7 @@ class DeepTrackNode: 20 Compute and retrieve the value for the child and grandchild node: + >>> child(_ID=(0,)) 30 >>> child(_ID=(1,)) @@ -1012,6 +1015,7 @@ class DeepTrackNode: 120 Validation and invalidation: + >>> parent.is_valid((0,)) True >>> child.is_valid((0,)) @@ -1036,6 +1040,7 @@ class DeepTrackNode: False Setting a value and automatic invalidation: + >>> parent.current_value((0,)) 15 >>> grandchild((0,)) # Computes and stores the value in grandchild @@ -1050,6 +1055,7 @@ class DeepTrackNode: 252 Resetting all data in the dependency tree (recomputation required): + >>> grandchild.update() >>> grandchild() 60 @@ -1059,6 +1065,7 @@ class DeepTrackNode: 60 Operator overloading—arithmetic and comparison: + >>> node_a = DeepTrackNode(lambda: 5) >>> node_b = DeepTrackNode(lambda: 3) @@ -1091,19 +1098,23 @@ class DeepTrackNode: True Indexing into computed data: + >>> vector_node = DeepTrackNode(lambda: [10, 20, 30]) >>> first_element = vector_node[0] >>> first_element() 10 Accessing a value before computing it raises an error: + >>> new_node = DeepTrackNode(lambda: 123) >>> new_node.is_valid((42,)) False + >>> new_node.current_value((42,)) KeyError: 'Attempting to index an empty dict.' Working with nested _ID slicing: + >>> parent = DeepTrackNode(lambda: 5) >>> child = DeepTrackNode(lambda _ID=None: parent(_ID[:1]) + _ID[1]) >>> parent.add_child(child) @@ -1111,6 +1122,7 @@ class DeepTrackNode: 8 Citations for a node and its dependencies: + >>> parent.get_citations() # Set of citation strings {...} From 44d1aca53e425d55606e327122823cc474b28360 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 13:31:42 +0200 Subject: [PATCH 05/11] Update test_core.py --- deeptrack/tests/backend/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/tests/backend/test_core.py b/deeptrack/tests/backend/test_core.py index 4ba8bae64..b4bc24f1a 100644 --- a/deeptrack/tests/backend/test_core.py +++ b/deeptrack/tests/backend/test_core.py @@ -1,7 +1,7 @@ # pylint: disable=C0115:missing-class-docstring # pylint: disable=C0116:missing-function-docstring # pylint: disable=C0103:invalid-name - + # Use this only when running the test locally. # import sys # sys.path.append(".") # Adds the module to path. From a207705072d2afccb9b4ed3248078550868bc37c Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 13:42:31 +0200 Subject: [PATCH 06/11] Update DTDV401_overview.ipynb --- tutorials/4-developers/DTDV401_overview.ipynb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tutorials/4-developers/DTDV401_overview.ipynb b/tutorials/4-developers/DTDV401_overview.ipynb index 4f7923154..49080d5bf 100644 --- a/tutorials/4-developers/DTDV401_overview.ipynb +++ b/tutorials/4-developers/DTDV401_overview.ipynb @@ -21,12 +21,10 @@ "\n", "- Module structure\n", "\n", - "- Low-level modules, such as `core` and `image`.\n", + "- Low-level modules, in particular `core`.\n", "\n", "- The `features` module.\n", "\n", - "- The `image` module.\n", - "\n", "- High-level modules, such as `holography` and `optics`.\n", "\n", "- Utility and handle modules." @@ -48,9 +46,9 @@ "source": [ "## 2. Module Structure\n", "\n", - "Each module should be clearly documented in the beginning.\n", + "Each module is extensively documented.\n", "\n", - "Often modules contain some **abstract classes** to provide a standardized implementation of core functionalities (for example, image transformation) to ensure consistent output formatting across all derived classes in the module (for example, to ensure that the format of the output image is the same)." + "Often modules contain some **abstract classes** to provide a standardized implementation of core functionalities (for example, image transformation) to ensure consistent output formatting across all derived classes in the module." ] }, { @@ -103,7 +101,7 @@ "source": [ "## 3. Type Hints for Code Readability\n", "\n", - "As type hints are used extensively to improve code readability in accordance with the [style guide](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/tutorials/4-developers/DTDV411_style.ipynb), DeepTrack2 introduces a few custom type hints for internal use, which are declared in the [types.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/types.py) module." + "Since type hints are used extensively to improve code readability in accordance with [DTDT411. Style Guide](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/tutorials/4-developers/DTDV411_style.ipynb), DeepTrack2 introduces a few custom type hints for internal use, which are declared in the [types.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/types.py) module." ] }, { @@ -124,7 +122,7 @@ "```python\n", "from __future__ import annotations\n", "\n", - "from .types import ArrayLike, PropertyLike\n", + "from deeptrack.backend.types import ArrayLike, PropertyLike\n", "\n", "def ClassName():\n", "\n", From 2e81db5ecbddaf56146e36bb24d80418bbafb02b Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 13:48:07 +0200 Subject: [PATCH 07/11] Update DTDV401_overview.ipynb --- tutorials/4-developers/DTDV401_overview.ipynb | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/tutorials/4-developers/DTDV401_overview.ipynb b/tutorials/4-developers/DTDV401_overview.ipynb index 49080d5bf..05484f3e5 100644 --- a/tutorials/4-developers/DTDV401_overview.ipynb +++ b/tutorials/4-developers/DTDV401_overview.ipynb @@ -101,7 +101,7 @@ "source": [ "## 3. Type Hints for Code Readability\n", "\n", - "Since type hints are used extensively to improve code readability in accordance with [DTDT411. Style Guide](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/tutorials/4-developers/DTDV411_style.ipynb), DeepTrack2 introduces a few custom type hints for internal use, which are declared in the [types.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/types.py) module." + "Since type hints are used extensively to improve code readability in accordance with [DTDV411_style.ipynb](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/tutorials/4-developers/DTDV411_style.ipynb), DeepTrack2 introduces a few custom type hints for internal use, which are declared in the [types.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/types.py) module." ] }, { @@ -169,13 +169,13 @@ "\n", " In particular, the [core.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/backend/core.py) module contains the `DeepTrackNode` class which is used to represent a node in a computation graph, which when used together with the `DeepTrackDataObject` class can store data and compute new data based on its dependencies and child nodes. These classes track dependencies and validate data with ID and index addresses. \n", "\n", - " The [core.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/backend/core.py) module also provides the base class for the [features.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/features.py) module, which is the largest module in DeepTrack2 in terms of code volume, and provides the base class for all other modules in the deeptrack directory; the only exceptions are [image.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/image.py) and [properties.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/properties.py).\n", + " The [core.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/backend/core.py) module also provides the base class for the [features.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/features.py) module, which is the largest module in DeepTrack2 in terms of code volume, and provides the base class for all other modules in the deeptrack directory; the only exceptions is [properties.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/properties.py).\n", "\n", "- [mie.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/backend/mie.py) provides functions to perform Mie scattering calculations often used in simulations.\n", "\n", " In particular, it provides methods to compute coefficients for both spherical and stratified spherical harmonics, and to calculate the spherical harmonics of the Mie field with an iterative method.\n", "\n", - "- [pint_definition.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/backend/pint_definition.py)extends Pint's default definitions by introducing project-specific constants and unit modifications for flexible calculations in the context of DeepTrack2.\n", + "- [pint_definition.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/backend/pint_definition.py) extends Pint's default definitions by introducing project-specific constants and unit modifications for flexible calculations in the context of DeepTrack2.\n", "\n", "- [polynomials.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/backend/polynomials.py) provides a set of functions which compute Bessel and Riccati-Bessel polynomials and their derivatives.\n", "\n", @@ -242,7 +242,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The value of a `Property` has no datatype restrictions and can represent a constant, a function, a list, an `Image`, etc." + "The value of a `Property` has no datatype restrictions and can represent a constant, a function, a list, etc." ] }, { @@ -263,19 +263,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 7. Containers for Array-Like Structures\n", - "\n", - "The [image.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/image.py) module only contains a single class, namely `Image`. This class serves as a wrapper for array-like data and provides a unified interface for array operations and property management with NumPy and PyTorch compatibility.\n", - "\n", - "Several utility functions are also included, which can be used to manipulate `Image` objects within pipelines, such as image coercion to ensure a consistent type across a series of images, or padding to optimize Fast Fourier Transform performance." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "## 8. High-Level Modules\n", + "## 7. High-Level Modules\n", "\n", "The remaoning modules jointly implement the main functionality of DeepTrack2, which is synthetic data generation using simulations." ] @@ -303,8 +291,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "\n", - "## 9. Handle and Utility Modules\n", + "## 8. Handle and Utility Modules\n", "\n", "The following modules play a mediating role between external libraries and DeepTrack2, such as integrating Pytorch classes or Numpy functions, as well as utilities for type consistency and radial center calculations." ] @@ -313,7 +300,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 9.1. Numpy\n", + "### 8.1. Numpy\n", "The following modules provide handles to enable the use of NumPy functions with DeepTrack2 objects as well as mathematical utilities:\n", "\n", "- [elementwise.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/elementwise.py) provides handles to perform various elementary NumPy functions elementwise and sequentially when using `Feature` objects. For example, some of these functions include trigonometric, hyperbolic, rounding, exponents.\n", @@ -327,7 +314,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 9.2. Pytorch\n", + "### 8.2. Pytorch\n", "Located in the [pytorch](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/pytorch/) directory, there are two modules to facilitate PyTorch integration with DeepTrack2 objects:\n", "\n", "- [pytorch.data.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/pytorch/data.py) extends the PyTorch [Dataset](https://pytorch.org/tutorials/beginner/basics/data_tutorial.html) class to work with DeepTrack2 `Image` objects.\n", @@ -339,7 +326,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 9.3. Deeplay\n", + "### 8.3. Deeplay\n", "\n", "The init file in [deeplay/\\_\\_init__.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/deeplay/__init__.py) enables users to import Deeplay from DeepTrack2 with:\n", "\n", @@ -352,7 +339,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 9.4. Other Modules\n", + "### 8.4. Other Modules\n", "\n", "- [utils.py](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/utils.py) provides various utilities to streamline common operations, ensuring type and argument consistency with various check methods and safe call.\n", "\n", @@ -363,7 +350,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 10. Unit testing\n", + "## 9. Unit testing\n", "\n", "The unit testing scripts are found in the [tests](https://github.com/DeepTrackAI/DeepTrack2/blob/develop/deeptrack/tests/) directory, and follow the same structure as the [deeptrack](https://github.com/DeepTrackAI/DeepTrack2/tree/develop/deeptrack) directory with a test script for each module." ] From 916ea1d2fb58426f86dae72a9112f23ed53d78e3 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 13:52:05 +0200 Subject: [PATCH 08/11] Update DTAT306_properties.ipynb --- .../DTAT306_properties.ipynb | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/tutorials/3-advanced-topics/DTAT306_properties.ipynb b/tutorials/3-advanced-topics/DTAT306_properties.ipynb index 03ab37a46..852a31120 100644 --- a/tutorials/3-advanced-topics/DTAT306_properties.ipynb +++ b/tutorials/3-advanced-topics/DTAT306_properties.ipynb @@ -22,7 +22,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This advanced tutorial introduces the module deeptrack.properties." + "This advanced tutorial introduces the module `properties.py`." ] }, { @@ -31,15 +31,7 @@ "source": [ "## 1. What is `properties.py`?\n", "\n", - "The `properties.py` module in DeepTrack2 defines the mechanisms for \n", - "managing the dynamic and hierarchical properties of features. Unlike \n", - "standard Python attributes, these properties support lazy evaluation, \n", - "inheritance, dependency tracking, and value caching. This makes them \n", - "ideal for defining complex, data-driven feature graphs where values \n", - "may depend on other values—either statically or dynamically. The \n", - "properties' system is tightly integrated with the `Feature` class, \n", - "serving as the foundation for the flexible and declarative interface \n", - "used to build DeepTrack pipelines." + "The `properties.py` module in DeepTrack2 defines the mechanisms for managing the dynamic and hierarchical properties of features. Unlike standard Python attributes, these properties support lazy evaluation, inheritance, dependency tracking, and value caching. This makes them ideal for defining complex, data-driven feature graphs where values may depend on other values—either statically or dynamically. The properties' system is tightly integrated with the `Feature` class, serving as the foundation for the flexible and declarative interface used to build DeepTrack2 pipelines." ] }, { @@ -49,33 +41,22 @@ "The key roles of `properties.py` are:\n", "\n", "- **Lazy-Evaluated Properties:**\n", - " Properties can be set as static values or as callables, allowing for \n", - " on-demand evaluation. This is critical for dynamic data pipelines \n", - " where some values depend on runtime conditions.\n", + " Properties can be set as static values or as callables, allowing for on-demand evaluation. This is critical for dynamic data pipelines where some values depend on runtime conditions.\n", "\n", "- **Tracking of Dependencies Between Properties:**\n", - " Properties can depend on other properties. When a dependency is \n", - " updated, dependent properties are automatically re-evaluated. This \n", - " enables consistent and efficient parameter propagation.\n", + " Properties can depend on other properties. When a dependency is updated, dependent properties are automatically re-evaluated. This enables consistent and efficient parameter propagation.\n", "\n", "- **Hierarchical Property Inheritance:**\n", - " Sub-features can inherit and override properties from their parent \n", - " features, enabling modular and composable configurations.\n", + " Sub-features can inherit and override properties from their parent features, enabling modular and composable configurations.\n", "\n", "- **Introspection and Documentation:**\n", - " Properties can be introspected to extract their default values, \n", - " types, and documentation, which supports automated documentation \n", - " generation and better developer experience.\n", + " Properties can be introspected to extract their default values, types, and documentation, which supports automated documentation generation and better developer experience.\n", "\n", "- **Evaluated Values Caching:**\n", - " Once evaluated, a property's value is cached until its dependencies \n", - " change. This prevents unnecessary recomputation while ensuring \n", - " correctness.\n", + " Once evaluated, a property's value is cached until its dependencies change. This prevents unnecessary recomputation while ensuring correctness.\n", "\n", "- **Declarative Syntax for Pipeline Specification:**\n", - " The use of properties allows DeepTrack users to define image \n", - " generation pipelines in a high-level, declarative manner while \n", - " retaining low-level control when needed." + " The use of properties allows DeepTrack users to define image generation pipelines in a high-level, declarative manner while retaining low-level control when needed." ] }, { @@ -87,7 +68,7 @@ "Each feature (instance of the class `Feature`, see [DTAT301_features.ipynb](DTAT301_features.ipynb)) can have several properties (instances of the class `Property`). These properties can be constants, functions, lists, dictionaries, iterators, or slices, providing flexibility in defining and controlling different aspects of the system being modelled.\n", "\n", "A propety has a value accessible through the field `current_value`, whose data type is not restricted. \n", - "This value is updated through a sampling rule (method `.update()`), which is passed to the class constructor on initialization. " + "This value is updated through a sampling rule (used by the `.update()` method), which is passed to the class constructor on initialization. " ] }, { @@ -476,7 +457,7 @@ "\n", "Another class contained in the module deeptrack.properties is `PropertyDict`. This is a dictionary of properties (keys: name of properties; values: properties) complemented by utility methods to manage collections of properties. These include:\n", "\n", - "- `.current_value_dict()`, which creates and returns a dictionary with the current value of all properties in the PropertyDict (keys: name of properties; values: current values of the properties).\n", + "- `.current_value_dict()`, which creates and returns a dictionary with the current value of all properties in the `PropertyDict` (keys: name of properties; values: current values of the properties).\n", "\n", "- `.update()`, which calls the method `.update()` on all properties in the PropertyDict.\n", "\n", @@ -601,7 +582,7 @@ "source": [ "### 4.2. Simulating a Random Walker with a Markov Chain\n", "\n", - "We will now simulate a random walker in one dimension by iterating the `SequentialProperty` with random number sampling rule as the displacements in each time step and updating the walkers position as a cummulative sum of the sampled displacements." + "You will now simulate a random walker in one dimension by iterating the `SequentialProperty` with random number sampling rule as the displacements in each time step and updating the walkers position as a cummulative sum of the sampled displacements." ] }, { From 04630170b4c9b38e0dba7d716079a7f64c1e2fad Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 15:10:00 +0200 Subject: [PATCH 09/11] Update properties.py --- deeptrack/properties.py | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/deeptrack/properties.py b/deeptrack/properties.py index eb54e0870..a03b3262a 100644 --- a/deeptrack/properties.py +++ b/deeptrack/properties.py @@ -43,17 +43,20 @@ >>> import deeptrack as dt Create and use a constant property: + >>> const_prop = dt.Property(42) >>> const_prop() 42 Define a dynamic property dependent on another: + >>> const_prop = dt.Property(5) >>> dynamic_prop = dt.Property(lambda x: x * 2, x=const_prop) >>> dynamic_prop() 10 Create a dictionary of properties: + >>> import numpy as np >>> >>> prop_dict = dt.PropertyDict( @@ -63,12 +66,15 @@ ... ) >>> prop_dict["constant"]() 42 + >>> prop_dict["dependent"]() 52 + >>> prop_dict["random"]() 52.35065943710633 Handle sequential properties: + >>> seq_prop = dt.SequentialProperty( ... sampling_rule=lambda: np.random.randint(10, 20), ... ) @@ -136,7 +142,7 @@ class Property(DeepTrackNode): sampling_rule: Any The rule for sampling values. Can be a constant, function, list, dictionary, iterator, tuple, NumPy array, PyTorch tensor, slice, - or DeepTrackNode. + or `DeepTrackNode`. **dependencies: Property Additional dependencies passed as named arguments. These dependencies can be used as inputs to functions or other dynamic components of the @@ -144,7 +150,7 @@ class Property(DeepTrackNode): Methods ------- - create_action(sampling_rule: Any, **dependencies: Property) -> Callable[..., Any] + `create_action(sampling_rule, **dependencies) -> Callable[..., Any]` Creates an action that defines how the property is evaluated. The behavior of the action depends on the type of `sampling_rule`. @@ -189,6 +195,8 @@ class Property(DeepTrackNode): >>> dynamic_prop.update() # Updates the value >>> dynamic_prop() # Returns different random value 0.5862725216547282 + >>> dynamic_prop.new() # Returns different random value + 0.36122033451938484 >>> const_prop = dt.Property(5) >>> dynamic_prop = dt.Property(lambda x: 2 * x, x=const_prop) @@ -216,14 +224,11 @@ class Property(DeepTrackNode): >>> iter_prop = dt.Property(iter([1, 2, 3])) >>> iter_prop() 1 - >>> iter_prop.update() - >>> iter_prop() + >>> iter_prop.new() # equivalent to iter_prop.update()() 2 - >>> iter_prop.update() - >>> iter_prop() + >>> iter_prop.new() 3 - >>> iter_prop.update() - >>> iter_prop() # Last value repeats + >>> iter_prop.new() # Last value repeats 3 Lists and dictionaries can contain properties, functions, or constants: @@ -252,7 +257,7 @@ class Property(DeepTrackNode): 100 >>> node = dt.DeepTrackNode(lambda _ID=(): np.random.rand()) - >>> node_prop = Property(node) + >>> node_prop = dt.Property(node) >>> node_prop() 0.5065650298607408 @@ -362,7 +367,7 @@ def create_action( or tuple or np.ndarray or torch.Tensor or slice or DeepTrackNode or Any The rule to sample values for the property. - **dependencies: dict[str, Property] + **dependencies: Property Dependencies to be used in the sampling rule. Returns @@ -479,7 +484,7 @@ class PropertyDict(DeepTrackNode, dict): Methods ------- - __getitem__(key: str) -> Any + `__getitem__(key) -> Any` Retrieves a value from the dictionary using a key. Examples @@ -487,6 +492,7 @@ class PropertyDict(DeepTrackNode, dict): >>> import deeptrack as dt Initialize a `PropertyDict` with different types of properties: + >>> import numpy as np >>> >>> prop_dict = dt.PropertyDict( @@ -496,10 +502,13 @@ class PropertyDict(DeepTrackNode, dict): ... ) Access the properties: + >>> prop_dict["constant"]() 42 + >>> prop_dict["dependent"]() 52 + >>> prop_dict["random"]() 0.33112452108057056 @@ -536,8 +545,10 @@ def __init__( try: # Create a Property instance for the key, # resolving dependencies. - dependencies[key] = Property(value, - **{**dependencies, **kwargs}) + dependencies[key] = Property( + value, + **{**dependencies, **kwargs}, + ) # Remove the key from the input dictionary once resolved. kwargs.pop(key) except AttributeError: From 791fc82638727591088994f1e2acf6af1b5e6bc0 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 15:50:11 +0200 Subject: [PATCH 10/11] Update scatterers.py --- deeptrack/scatterers.py | 52 +++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/deeptrack/scatterers.py b/deeptrack/scatterers.py index bfecaed45..04a7c5eae 100644 --- a/deeptrack/scatterers.py +++ b/deeptrack/scatterers.py @@ -160,11 +160,12 @@ from __future__ import annotations -from typing import Callable +from typing import Any, TYPE_CHECKING import warnings -from pint import Quantity import numpy as np +from numpy.typing import NDArray +from pint import Quantity from deeptrack.holography import get_propagation_matrix from deeptrack.backend.units import ( @@ -179,6 +180,22 @@ from deeptrack import units_registry as u +__all__ = [ + "Scatterer", + "PointParticle", + "Ellipse", + "Sphere", + "Ellipsoid", + "MieScatterer", + "MieSphere", + "MieStratifiedSphere", +] + + +if TYPE_CHECKING: + import torch + + #TODO ***??*** revise Scatterer - torch, typing, docstring, unit test class Scatterer(Feature): """Base abstract class for scatterers. @@ -248,6 +265,7 @@ def __init__( ) self._processed_properties = False + super().__init__( position=position, z=z, @@ -264,7 +282,7 @@ def _process_properties( self, properties: dict ) -> dict: - + # Rescales the position property. properties = super()._process_properties(properties) self._processed_properties = True @@ -323,14 +341,14 @@ def _no_wrap_format_input( **kwargs ) -> list: return self._image_wrapped_format_input(*args, **kwargs) - + def _no_wrap_process_and_get( self, *args, **feature_input ) -> list: return self._image_wrapped_process_and_get(*args, **feature_input) - + def _no_wrap_process_output( self, *args, @@ -341,15 +359,15 @@ def _no_wrap_process_output( #TODO ***??*** revise PointParticle - torch, typing, docstring, unit test class PointParticle(Scatterer): - """Generates a point particle + """Generate a diffraction-limited point particle. - A point particle is approximated by the size of a pixel. For subpixel - positioning, the position is interpolated linearly. + A point particle is approximated by the size of a single pixel or voxel. + For subpixel positioning, the position is interpolated linearly. Parameters ---------- position: ArrayLike[float, float (, float)] - The position of the particle, length 2 or 3. Third index is optional, + Particle position in 2D or 3D. Third index is optional, and represents the position in the direction normal to the camera plane. @@ -365,17 +383,21 @@ class PointParticle(Scatterer): """ def __init__( - self, - **kwargs + self: PointParticle, + **kwargs: Any, ): + """ + + """ + super().__init__(upsample=1, upsample_axes=(), **kwargs) def get( - self, + self: PointParticle, image: Image | np.ndarray, - **kwarg - ) -> ArrayLike[float]: - """Abstract method to initialize the point scatterer""" + **kwarg: Any, + ) -> NDArray[Any] | torch.Tensor: + """Evaluate and return the scatterer volume.""" scale = get_active_scale() From 690195ada16653632f36a10a199cb1167333d8f0 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 6 Sep 2025 15:50:13 +0200 Subject: [PATCH 11/11] Update units.py --- deeptrack/backend/units.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/deeptrack/backend/units.py b/deeptrack/backend/units.py index a36a4b5b8..5d40397da 100644 --- a/deeptrack/backend/units.py +++ b/deeptrack/backend/units.py @@ -27,28 +27,15 @@ ---------------- Functions: -- `get_active_voxel_size()` - - def get_active_voxel_size() -> tuple[float, float, float] +- `get_active_voxel_size() -> tuple[float, float, float]` Get the active voxel size in meters along the x, y, and z axes. -- `get_active_scale()` - - def get_active_scale() -> tuple[float, float, float] +- `get_active_scale() -> tuple[float, float, float]` Get the scaling factors between simulation and optical pixels. -- `create_context(xpixel, ypixel, zpixel, xscale, yscale, zscale)` - - def create_context( - xpixel: float | None = None, - ypixel: float | None = None, - zpixel: float | None = None, - xscale: int | None = None, - yscale: int | None = None, - zscale: int | None = None, - ) -> Context +- `create_context(xpixel, ypixel, zpixel, xscale, yscale, zscale) -> Context` Create a unit context that defines how pixels map to meters. @@ -64,14 +51,17 @@ def create_context( >>> from deeptrack.backend import units Retrieve the active voxel size in meters: + >>> units.get_active_voxel_size() (1e-06, 1e-06, 1e-06) Retrieve the scaling factors between simulation and optical pixels: + >>> units.get_active_scale() (1.0, 1.0, 1.0) Create a custom unit context and use it to convert simulation pixels: + >>> from deeptrack import units_registry as u >>> >>> ctx = units.create_context( @@ -90,6 +80,7 @@ def create_context( 1e-06 meter Use the ConversionTable to convert physical quantities to target units: + >>> conversion_table = units.ConversionTable( ... length=(u.meter, u.micrometer), ... time=(u.second, u.millisecond), @@ -99,6 +90,7 @@ def create_context( 'time': 500.0 } Support for PyTorch tensors: + >>> import torch >>> >>> conversion_table.convert(length=torch.tensor([1.0, 2.0])) @@ -106,6 +98,7 @@ def create_context( """ + from __future__ import annotations from typing import Any