Module catmaid_publish.annotations

Expand source code
import json
from pathlib import Path
from typing import Optional

import networkx as nx

from .utils import copy_cache, descendants, entity_graph, fill_in_dict, remove_nodes


def get_annotations(
    annotated: list[str], names: Optional[list[str]], rename: dict[str, str]
) -> tuple[dict[str, list[str]], dict[str, str]]:
    """Get annotations of interest and how they relate to each other.

    Parameters
    ----------
    annotated : list[str]
        Include all descendant annotations of these meta-annotations.
    names : Optional[list[str]]
        Include annotations with any of these names.
        If None, include all annotations in the project.
    rename : dict[str, str]
        Rename these annotations.

    Returns
    -------
    tuple[dict[str, list[str]], dict[str, str]]
        2-tuple:
        First element is a dict of a (renamed) annotation
        to its (renamed) sub-annotations.
        Second element is the complete dict of renames ``{old: new}``.
    """
    g = entity_graph()
    remove_nodes(g, lambda _, d: d["type"] != "annotation")

    if names is None:
        name_set = set(g.nodes)
    else:
        ann_set = set(annotated)
        roots = [d["id"] for _, d in g.nodes(data=True) if d["name"] in ann_set]
        desc = descendants(g, roots)
        name_set = set(names).union(g.nodes[d]["name"] for d in desc)

    rename = fill_in_dict(rename, name_set)

    sub = g.subgraph((n for n, d in g.nodes(data=True) if d["name"] in rename))
    id_to_name = {n: d["name"] for n, d in sub.nodes(data=True) if d["name"] in rename}
    out = dict()
    for n in sorted(id_to_name, key=id_to_name.get):
        out[id_to_name[n]] = sorted(
            rename[sub.nodes[s]["name"]] for s in sub.successors(n)
        )

    return out, rename


def write_annotation_graph(fpath: Path, annotations: dict[str, list[str]]):
    """Write annotation graph as JSON ``{parent: [child1, ...]}``.

    Parameters
    ----------
    fpath : Path
        Path to write to. Ancestor directories will be created.
    annotations : dict[str, list[str]]
        Parent-children annotation relationships.
    """
    fpath.parent.mkdir(exist_ok=True, parents=True)
    with open(fpath, "w") as f:
        json.dump(annotations, f, indent=2, sort_keys=True)


class AnnotationReader:
    """Class for reading exported annotation data."""

    def __init__(self, dpath: Path) -> None:
        """
        Parameters
        ----------
        dpath : Path
            Directory in which the annotation data is saved.
        """
        self.dpath = dpath

    @copy_cache()
    def get_graph(self) -> nx.DiGraph:
        """Return the saved graph of text annotations.

        Returns
        -------
        nx.DiGraph
            Directed graph of text annotations,
            where an edge denotes the source annotating the target.
            All nodes have attributes ``type="annotation``;
            all edges have attributes ``meta_annotation=True``.
        """
        with open(self.dpath / "annotation_graph.json") as f:
            d = json.load(f)

        g = nx.DiGraph()
        for u, vs in d.items():
            for v in vs:
                g.add_edge(u, v, meta_annotation=True)

        for _, d in g.nodes(data=True):
            d["type"] = "annotation"

        return g


README = """
# Annotations

Annotations are text labels applied to neurons and to other annotations.

Data in this directory can be parsed into a `networkx.DiGraph`
using `catmaid_publish.AnnotationReader`.

## Files

### `annotations.json`

A JSON file of annotations and how they annotate each other.
Every annotation of interest is a key in the JSON object:
the value is the list of annotations of interest annotated by that key.
""".lstrip()

Functions

def get_annotations(annotated: list[str], names: Optional[list[str]], rename: dict[str, str]) ‑> tuple[dict[str, list[str]], dict[str, str]]

Get annotations of interest and how they relate to each other.

Parameters

annotated : list[str]
Include all descendant annotations of these meta-annotations.
names : Optional[list[str]]
Include annotations with any of these names. If None, include all annotations in the project.
rename : dict[str, str]
Rename these annotations.

Returns

tuple[dict[str, list[str]], dict[str, str]]
2-tuple: First element is a dict of a (renamed) annotation to its (renamed) sub-annotations. Second element is the complete dict of renames {old: new}.
Expand source code
def get_annotations(
    annotated: list[str], names: Optional[list[str]], rename: dict[str, str]
) -> tuple[dict[str, list[str]], dict[str, str]]:
    """Get annotations of interest and how they relate to each other.

    Parameters
    ----------
    annotated : list[str]
        Include all descendant annotations of these meta-annotations.
    names : Optional[list[str]]
        Include annotations with any of these names.
        If None, include all annotations in the project.
    rename : dict[str, str]
        Rename these annotations.

    Returns
    -------
    tuple[dict[str, list[str]], dict[str, str]]
        2-tuple:
        First element is a dict of a (renamed) annotation
        to its (renamed) sub-annotations.
        Second element is the complete dict of renames ``{old: new}``.
    """
    g = entity_graph()
    remove_nodes(g, lambda _, d: d["type"] != "annotation")

    if names is None:
        name_set = set(g.nodes)
    else:
        ann_set = set(annotated)
        roots = [d["id"] for _, d in g.nodes(data=True) if d["name"] in ann_set]
        desc = descendants(g, roots)
        name_set = set(names).union(g.nodes[d]["name"] for d in desc)

    rename = fill_in_dict(rename, name_set)

    sub = g.subgraph((n for n, d in g.nodes(data=True) if d["name"] in rename))
    id_to_name = {n: d["name"] for n, d in sub.nodes(data=True) if d["name"] in rename}
    out = dict()
    for n in sorted(id_to_name, key=id_to_name.get):
        out[id_to_name[n]] = sorted(
            rename[sub.nodes[s]["name"]] for s in sub.successors(n)
        )

    return out, rename
def write_annotation_graph(fpath: pathlib.Path, annotations: dict[str, list[str]])

Write annotation graph as JSON {parent: [child1, ...]}.

Parameters

fpath : Path
Path to write to. Ancestor directories will be created.
annotations : dict[str, list[str]]
Parent-children annotation relationships.
Expand source code
def write_annotation_graph(fpath: Path, annotations: dict[str, list[str]]):
    """Write annotation graph as JSON ``{parent: [child1, ...]}``.

    Parameters
    ----------
    fpath : Path
        Path to write to. Ancestor directories will be created.
    annotations : dict[str, list[str]]
        Parent-children annotation relationships.
    """
    fpath.parent.mkdir(exist_ok=True, parents=True)
    with open(fpath, "w") as f:
        json.dump(annotations, f, indent=2, sort_keys=True)

Classes

class AnnotationReader (dpath: pathlib.Path)

Class for reading exported annotation data.

Parameters

dpath : Path
Directory in which the annotation data is saved.
Expand source code
class AnnotationReader:
    """Class for reading exported annotation data."""

    def __init__(self, dpath: Path) -> None:
        """
        Parameters
        ----------
        dpath : Path
            Directory in which the annotation data is saved.
        """
        self.dpath = dpath

    @copy_cache()
    def get_graph(self) -> nx.DiGraph:
        """Return the saved graph of text annotations.

        Returns
        -------
        nx.DiGraph
            Directed graph of text annotations,
            where an edge denotes the source annotating the target.
            All nodes have attributes ``type="annotation``;
            all edges have attributes ``meta_annotation=True``.
        """
        with open(self.dpath / "annotation_graph.json") as f:
            d = json.load(f)

        g = nx.DiGraph()
        for u, vs in d.items():
            for v in vs:
                g.add_edge(u, v, meta_annotation=True)

        for _, d in g.nodes(data=True):
            d["type"] = "annotation"

        return g

Methods

def get_graph(self) ‑> networkx.classes.digraph.DiGraph

Return the saved graph of text annotations.

Returns

nx.DiGraph
Directed graph of text annotations, where an edge denotes the source annotating the target. All nodes have attributes type="annotation; all edges have attributes meta_annotation=True.
Expand source code
@copy_cache()
def get_graph(self) -> nx.DiGraph:
    """Return the saved graph of text annotations.

    Returns
    -------
    nx.DiGraph
        Directed graph of text annotations,
        where an edge denotes the source annotating the target.
        All nodes have attributes ``type="annotation``;
        all edges have attributes ``meta_annotation=True``.
    """
    with open(self.dpath / "annotation_graph.json") as f:
        d = json.load(f)

    g = nx.DiGraph()
    for u, vs in d.items():
        for v in vs:
            g.add_edge(u, v, meta_annotation=True)

    for _, d in g.nodes(data=True):
        d["type"] = "annotation"

    return g