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
8 changes: 5 additions & 3 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ jobs:
fetch-depth: 2

- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5

- name: Install dependencies
run: pip install ".[test]"
run: |
python -m pip install --upgrade pip
pip install -e ".[test]"

- name: Run tests
run: pytest --cov --cov-branch --cov-report=xml
run: pytest --cov=lineagetree --cov-report=xml

- name: Upload results to Codecov
uses: codecov/codecov-action@v5
Expand Down
12 changes: 9 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ dependencies = [
"matplotlib",
"edist",
"svgwrite",
"micromastodonreader",
"Micro-Mastodon-Reader",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -123,6 +123,12 @@ root = ["./src", ]
[tool.ty.rules]
unresolved-import = "ignore"

[tool.pytest.ini_options]
addopts = "--cov"
[tool.coverage.run]
source = ["lineagetree"]

[tool.coverage.report]
omit = [
"*/test/*",
"*/.vscode/*",
"*/site-packages/*"
]
4 changes: 4 additions & 0 deletions src/lineagetree/_assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
remove_nodes,
)
from ._basics._navigation import (
change_labels,
find_leaves,
get_all_chains_of_subtree,
get_ancestor_at_t,
get_ancestor_with_attribute,
get_available_labels,
get_chain_of_node,
get_labelled_ancestor,
get_predecessors,
Expand Down Expand Up @@ -104,6 +106,8 @@
LineageTree.calculate_dtw = calculate_dtw

# Navigation functions
LineageTree.get_available_labels = get_available_labels
LineageTree.change_labels = change_labels
LineageTree.find_leaves = find_leaves
LineageTree.get_all_chains_of_subtree = get_all_chains_of_subtree
LineageTree.get_ancestor_at_t = get_ancestor_at_t
Expand Down
99 changes: 99 additions & 0 deletions src/lineagetree/_basics/_navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from collections.abc import Iterable
from typing import TYPE_CHECKING
import warnings

if TYPE_CHECKING:
from ..lineage_tree import LineageTree
Expand Down Expand Up @@ -394,3 +395,101 @@ def get_subtree(lT: LineageTree, node_list: set[int]) -> LineageTree:
],
**{name: lT.__dict__[name] for name in lT._custom_property_list},
)


def get_available_labels(lT: LineageTree) -> list[str]:
"""Returns the list all the available label dictionaries

Returns
-------
list of string
list of the names of all the available properties
to label the nodes
"""
available_labels = []
for prop_name, prop in lT.__dict__.items():
if (
0 < len(prop_name)
and prop_name[0] != "_"
and isinstance(prop, dict)
and 0 < len(prop)
and all(isinstance(k, int) for k in prop.keys())
and all(isinstance(v, str) for v in prop.values())
):
available_labels.append(prop_name)
return available_labels


def change_labels(
lT: LineageTree,
new_labels_name: str | None = None,
new_labels_dict: dict[int, str] | None = None,
only_first_node_in_chain: bool = False,
) -> None:
"""Change the dictionary that serves at labels with
the `LineageTree` attribute `new_labels_name`.
It has to be a dictionary mapping node id to string.

If `new_labels_dict` is provided, it will be used to
label the cells.
If `new_labels_name` is not specified, the labels are reset.

One can decide to only label the first node of the chain
instead of all the nodes of the chain.
That can help readability in the napari plugin reLAX.

Parameters
----------
lT : LineageTree
new_labels_name : string, optional
The name of the dictionary to use
(the list of potential dictionaries can be found
with `lT.available_labels`)
If `new_labels_name` is not provided,
the labels are reset to `"Unlabeleld"`
new_labels_dict : dictionary mapping integers to strings, optional
The new names as a dictionary mapping each named node id to its string label
If not provided and lT has a fitting attribute named `new_labels_name`,
it will therefore be used
only_first_node_in_chain : bool, default=True
If `True` only labels the first node of the chains
"""
store_new_labels = True
if new_labels_name is not None:
lT.labels_name = new_labels_name
if new_labels_dict is None:
if new_labels_name in lT.__dict__:
new_labels_dict = lT.__dict__[new_labels_name]
store_new_labels = False
else:
raise AttributeError(
f"{new_labels_name} is not in the properties of {lT.name}"
)
if any(not isinstance(v, str) for v in new_labels_dict.values()):
raise TypeError(
"All values of new_labels dictionary should be `str`"
)

labelled_cells = lT.nodes.intersection(new_labels_dict)
if only_first_node_in_chain:
labelled_cells = labelled_cells.intersection(
{chain[0] for chain in lT.all_chains}
)

if len(labelled_cells) < 1:
warnings.warn(
"The labeling dictionary does not have any node labels.\n"
'Defaulting to the "Unlabeled" labeling'
)
else:
lT._labels = {n: new_labels_dict[n] for n in labelled_cells}
if store_new_labels:
lT.__dict__[new_labels_name] = lT._labels
else:
lT.labels_name = ""
lT._labels = {
root: "Unlabeled"
for root in lT.roots
for leaf in lT.find_leaves(root)
if abs(lT._time[leaf] - lT._time[root]) >= abs(lT.t_e - lT.t_b) / 4
}
5 changes: 4 additions & 1 deletion src/lineagetree/_basics/_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,13 @@ def labels(lT: LineageTree) -> dict[int, str]:
"""Dictionary that maps a node to its label"""
if not hasattr(lT, "_labels"):
if hasattr(lT, "node_name"):
lT.labels_name = "node_name"
lT._labels = {
i: lT.node_name.get(i, "Unlabeled") for i in lT.roots
chain[0]: lT.node_name.get(chain[0], "Unlabeled")
for chain in lT.all_chains
}
else:
lT.labels_name = ""
lT._labels = {
root: "Unlabeled"
for root in lT.roots
Expand Down
31 changes: 13 additions & 18 deletions src/lineagetree/_io/_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,16 +763,17 @@ def read_from_tgmm_xml(


def read_from_mastodon(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If understand this correctly it only reads one label set, right? If yes maybe we can read every label?

path: str, tag_set: int | None = None, name: None | str = None
path: str, tag_set: str | None = None, name: str | None = None
) -> LineageTree:
"""Read a maston lineage tree.

Parameters
----------
path : str
path to the mastodon file
tag_set : int, optional
The tag set that will be used to label.
tag_set : str, optional
If specified, `tag_set` will be used for labeling the cells
Otherwise a random tag set will be used
name : str, optional
The name attribute of the LineageTree file. If given a non-empty string, the value of the attribute
will be the name attribute, otherwise the name will be the stem of the file path.
Expand All @@ -793,20 +794,12 @@ def read_from_mastodon(
for succ, pred in zip(links[:, 1], links[:, 0]):
predecessor[int(succ)] = int(pred)

label = {}
all_tags = mr.read_tags(spots, links)
properties = {}
for tags in all_tags:
properties[tags["name"]] = {
tag["id"]: tag["label"] for tag in tags["tags"]
}
_, properties, _ = mr.read_tags()

if isinstance(tag_set, int):
tags = all_tags[tag_set]
for tag in tags["tags"]:
label[tag["id"]] = tag["label"]
elif isinstance(tag_set, str) and tag_set in properties:
label = properties[tag_set]
if isinstance(tag_set, str) and tag_set in properties:
labels = properties[tag_set]
elif 0 < len(properties):
labels_name, labels = next(iter(properties.items()))

if not name:
if isinstance(path, Path):
Expand All @@ -816,11 +809,13 @@ def read_from_mastodon(
if name == "":
warn(f"Name set to default {tmp_name}", stacklevel=2)
name = tmp_name

return LineageTree(
predecessor=predecessor,
time=time,
pos=pos,
label=label,
labels=labels,
labels_name=labels_name,
name=name,
**properties,
)
Expand All @@ -833,7 +828,7 @@ def read_from_mastodon_csv(

Parameters
----------
paths : list[str]
paths : list of strings
list of paths to the csv files
name : None or str, optional
The name attribute of the LineageTree file. If given a non-empty string, the value of the attribute
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading