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
108 changes: 66 additions & 42 deletions deeptrack/backend/core.py
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
-------
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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))
Expand All @@ -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)")
Expand All @@ -387,24 +386,29 @@ 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)

>>> data_dict[(0, 0)].current_value()
'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
Expand All @@ -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
Expand All @@ -429,19 +434,21 @@ class DeepTrackDataDict:
True

Check if a given `_ID` exists:

>>> (1, 0) in data_dict
True

>>> (2, 2) in data_dict
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)
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -956,32 +965,37 @@ 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=<lambda>)
DeepTrackNode(name='child', len=0, action=<lambda>)
DeepTrackNode(name='grandchild', len=0, action=<lambda>)

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=<lambda>)
DeepTrackNode(name='child', len=0, action=<lambda>)
DeepTrackNode(name='parent', len=0, action=<lambda>)

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,))
Expand All @@ -990,6 +1004,7 @@ class DeepTrackNode:
20

Compute and retrieve the value for the child and grandchild node:

>>> child(_ID=(0,))
30
>>> child(_ID=(1,))
Expand All @@ -1000,6 +1015,7 @@ class DeepTrackNode:
120

Validation and invalidation:

>>> parent.is_valid((0,))
True
>>> child.is_valid((0,))
Expand All @@ -1024,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
Expand All @@ -1038,6 +1055,7 @@ class DeepTrackNode:
252

Resetting all data in the dependency tree (recomputation required):

>>> grandchild.update()
>>> grandchild()
60
Expand All @@ -1047,6 +1065,7 @@ class DeepTrackNode:
60

Operator overloading—arithmetic and comparison:

>>> node_a = DeepTrackNode(lambda: 5)
>>> node_b = DeepTrackNode(lambda: 3)

Expand Down Expand Up @@ -1079,26 +1098,31 @@ 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)
>>> child((0, 3)) # Equivalent to parent((0,)) + 3
8

Citations for a node and its dependencies:

>>> parent.get_citations() # Set of citation strings
{...}

Expand Down
Loading
Loading