Module catmaid_publish.landmarks

Expand source code
from __future__ import annotations

import json
from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import Any, Iterable, Optional

import pandas as pd
import pymaid

from .utils import copy_cache, fill_in_dict


def get_landmarks(
    groups: Optional[list[str]],
    group_rename: dict[str, str],
    names: Optional[list[str]],
    rename: dict[str, str],
) -> tuple[pd.DataFrame, pd.DataFrame]:
    """Get locations associated with landmarks and groups.

    Parameters
    ----------
    groups : Optional[list[str]]
        List of group names of interest (None means all)
    group_rename : dict[str, str]
        Remap group names.
    names : Optional[list[str]]
        List of landmark names of interest(None means all)
    rename : dict[str, str]
        Remap landmark names.

    Returns
    -------
    tuple[pd.DataFrame, pd.DataFrame]
        Dataframes have columns location_id, x, y, z, name.

        The first element refers to landmarks, the second to groups.
        Locations are not unique as they can belong to several landmarks and/or groups.
    """
    lmark_df, lmark_loc_df = pymaid.get_landmarks()

    if names is None:
        names = list(lmark_df["name"])
    rename = fill_in_dict(rename, names)

    # location_id, x, y, z, landmark_id
    # landmark_id, name, user_id, project_id, creation_time, edition_time
    lmark_combined = lmark_loc_df.merge(lmark_df, on="landmark_id")
    lmark_reduced = lmark_combined.loc[lmark_combined["name"].isin(rename)].copy()
    lmark_reduced["name"] = [rename[old] for old in lmark_reduced["name"]]
    lmark_final = lmark_reduced.drop(
        columns=[
            "landmark_id",
            "user_id",
            "project_id",
            "creation_time",
            "edition_time",
        ],
        inplace=False,
    )

    group_df, group_loc_df, _ = pymaid.get_landmark_groups(True, False)

    if groups is None:
        groups = list(group_df["name"])
    group_rename = fill_in_dict(group_rename, groups)

    # location_id, x, y, z, group_id
    # group_id, name, user_id, project_id, creation_time, edition_time.
    group_combined = group_loc_df.merge(group_df, on="group_id")
    group_reduced = group_combined.loc[group_combined["name"].isin(group_rename)].copy()
    group_reduced["name"] = [group_rename[old] for old in group_reduced["name"]]

    group_final = group_reduced.drop(
        columns=["group_id", "user_id", "project_id", "creation_time", "edition_time"],
        inplace=False,
    )

    return lmark_final, group_final


@dataclass
class Location:
    """Location of importance to landmarks and groups.

    Attributes
    ----------
    xyz : tuple[float, float, float]
        Coordinates of location
    groups : set[str]
        Set of landmark groups this location belongs to.
    landmarks : set[str]
        Set of landmarks this location belongs to.
    """

    xyz: tuple[float, float, float]
    groups: set[str] = field(default_factory=set)
    landmarks: set[str] = field(default_factory=set)

    def to_jso(self) -> dict[str, Any]:
        """Convert to JSON-serialisable object.

        Returns
        -------
        dict[str, Any]
        """
        d = asdict(self)
        d["xyz"] = list(d["xyz"])
        d["groups"] = sorted(d["groups"])
        d["landmarks"] = sorted(d["landmarks"])
        return d

    @classmethod
    def from_jso(cls, jso: dict[str, Any]) -> Location:
        """Instantiate from JSON-like dict.

        Parameters
        ----------
        jso : dict[str, Any]
            Keys ``"xyz"`` (3-length list of float),
            ``"groups"`` (list of str),
            ``"landmarks"`` (list of str)

        Returns
        -------
        Location
        """
        return cls(
            tuple(jso["xyz"]),
            set(jso["groups"]),
            set(jso["landmarks"]),
        )


def write_landmarks(fpath: Path, landmarks: pd.DataFrame, groups: pd.DataFrame):
    if len(landmarks) + len(groups) == 0:
        return

    location_data: dict[int, Location] = dict()
    for row in landmarks.itertuples(index=False):
        d = location_data.setdefault(row.location_id, Location((row.x, row.y, row.z)))
        d.landmarks.add(row.name)

    for row in groups.itertuples(index=False):
        d = location_data.setdefault(row.location_id, Location((row.x, row.y, row.z)))
        d.groups.add(row.name)

    out = [v.to_jso() for _, v in sorted(location_data.items())]

    with open(fpath, "w") as f:
        json.dump(out, f, indent=2, sort_keys=True)


class LandmarkReader:
    """Class for reading exported landmark data."""

    def __init__(self, dpath: Path) -> None:
        """
        Parameters
        ----------
        dpath : Path
            Directory in which landmark data is saved.
        """
        self.dpath = dpath
        self.fpath = dpath / "locations.json"

    @copy_cache()
    def _locations(self):
        with open(self.fpath) as f:
            d = json.load(f)

        return [Location.from_jso(loc) for loc in d]

    def get_all(self) -> Iterable[Location]:
        """Lazily iterate through landmark locations.

        Yields
        ------
        Location
        """
        yield from self._locations()

    def get_group_names(self) -> set[str]:
        """Return all groups with locations in the dataset.

        Returns
        -------
        set[str]
            Set of group names.
        """
        out = set()
        for loc in self._locations():
            out.update(loc.groups)
        return out

    def get_landmark_names(self) -> set[str]:
        """Return all landmarks with locations in the dataset.

        Returns
        -------
        set[str]
            Set of landmark names.
        """
        out = set()
        for loc in self._locations():
            out.update(loc.landmarks)
        return out

    def get_group(self, *group: str) -> Iterable[Location]:
        """Lazily iterate through all locations from any of the given groups.

        Parameters
        ----------
        group : str
            Group name (can give multiple as *args).

        Yields
        ------
        Location
        """
        groupset = set(group)
        for loc in self._locations():
            if not loc.groups.isdisjoint(groupset):
                yield loc

    def get_landmark(self, *landmark: str) -> Iterable[Location]:
        """Lazily iterate through all locations from any of the given landmarks.

        Parameters
        ----------
        landmark : str
            Landmark name (can give multiple as *args)

        Yields
        ------
        Location
        """
        lmarkset = set(landmark)
        for loc in self._locations():
            if not loc.landmarks.isdisjoint(lmarkset):
                yield loc

    def get_paired_locations(
        self, group1: str, group2: str
    ) -> Iterable[tuple[Location, Location]]:
        """Iterate through paired locations.

        Locations are paired when both belong to the same landmark,
        and each location is the only one of that landmark to exist in that group,
        and they are not the same location.

        This is useful for creating transformations between two spaces
        (as landmark groups) by shared features (as landmarks).

        Parameters
        ----------
        group1 : str
            Group name
        group2 : str
            Group name

        Yields
        ------
        tuple[Location, Location]
        """
        la_lo1: dict[str, list[Location]] = dict()
        la_lo2: dict[str, list[Location]] = dict()
        for loc in self._locations():
            if group1 in loc.groups:
                if group2 in loc.groups:
                    continue
                for landmark in loc.landmarks:
                    la_lo1.setdefault(landmark, []).append(loc)
            elif group2 in loc.groups:
                for landmark in loc.landmarks:
                    la_lo2.setdefault(landmark, []).append(loc)

        landmarks = sorted(set(la_lo1).intersection(la_lo2))
        for la in landmarks:
            lo1 = la_lo1[la]
            if len(lo1) != 1:
                continue
            lo2 = la_lo2[la]
            if len(lo2) != 1:
                continue

            yield lo1[0], lo2[0]


README = """
# Landmarks

Landmarks represent important points in space.
A *landmark* can have multiple *locations* associated with it:
for example, one landmark can represent a neuron lineage entry point which exists on both sides of the central nervous system, or is segmentally repeated.

A landmark *group* is a collection of *landmark*s.
For example, a landmark group can represent all neuron lineage entry points in the brain.
However, not all of a *landmark*'s *location*s are necessarily associated with a *group* even if the group includes that *landmark*.
This allows for *landmark*/ *group* intersections like:

- landmark: bilateral pair of homologous neuron lineage **A** entry points
- group: all neuron lineage entry points on the **left** side of the brain

Data in this directory can be parsed into sets of `catmaid_publish.Location` objects
(which contain coordinates and landmark/group memberships)
using `catmaid_publish.LandmarkReader`.

## Files

### `locations.json`

A JSON file which is an array of objects representing locations of interest.

Each object's keys are:

- `"landmarks"`: array of names of landmarks to which this location belongs
- `"groups"`: array of names of landmark groups to which this location belongs
- `"xyz"`: 3-length array of decimals representing coordinates of location
""".lstrip()

Functions

def get_landmarks(groups: Optional[list[str]], group_rename: dict[str, str], names: Optional[list[str]], rename: dict[str, str]) ‑> tuple[pandas.core.frame.DataFrame, pandas.core.frame.DataFrame]

Get locations associated with landmarks and groups.

Parameters

groups : Optional[list[str]]
List of group names of interest (None means all)
group_rename : dict[str, str]
Remap group names.
names : Optional[list[str]]
List of landmark names of interest(None means all)
rename : dict[str, str]
Remap landmark names.

Returns

tuple[pd.DataFrame, pd.DataFrame]

Dataframes have columns location_id, x, y, z, name.

The first element refers to landmarks, the second to groups. Locations are not unique as they can belong to several landmarks and/or groups.

Expand source code
def get_landmarks(
    groups: Optional[list[str]],
    group_rename: dict[str, str],
    names: Optional[list[str]],
    rename: dict[str, str],
) -> tuple[pd.DataFrame, pd.DataFrame]:
    """Get locations associated with landmarks and groups.

    Parameters
    ----------
    groups : Optional[list[str]]
        List of group names of interest (None means all)
    group_rename : dict[str, str]
        Remap group names.
    names : Optional[list[str]]
        List of landmark names of interest(None means all)
    rename : dict[str, str]
        Remap landmark names.

    Returns
    -------
    tuple[pd.DataFrame, pd.DataFrame]
        Dataframes have columns location_id, x, y, z, name.

        The first element refers to landmarks, the second to groups.
        Locations are not unique as they can belong to several landmarks and/or groups.
    """
    lmark_df, lmark_loc_df = pymaid.get_landmarks()

    if names is None:
        names = list(lmark_df["name"])
    rename = fill_in_dict(rename, names)

    # location_id, x, y, z, landmark_id
    # landmark_id, name, user_id, project_id, creation_time, edition_time
    lmark_combined = lmark_loc_df.merge(lmark_df, on="landmark_id")
    lmark_reduced = lmark_combined.loc[lmark_combined["name"].isin(rename)].copy()
    lmark_reduced["name"] = [rename[old] for old in lmark_reduced["name"]]
    lmark_final = lmark_reduced.drop(
        columns=[
            "landmark_id",
            "user_id",
            "project_id",
            "creation_time",
            "edition_time",
        ],
        inplace=False,
    )

    group_df, group_loc_df, _ = pymaid.get_landmark_groups(True, False)

    if groups is None:
        groups = list(group_df["name"])
    group_rename = fill_in_dict(group_rename, groups)

    # location_id, x, y, z, group_id
    # group_id, name, user_id, project_id, creation_time, edition_time.
    group_combined = group_loc_df.merge(group_df, on="group_id")
    group_reduced = group_combined.loc[group_combined["name"].isin(group_rename)].copy()
    group_reduced["name"] = [group_rename[old] for old in group_reduced["name"]]

    group_final = group_reduced.drop(
        columns=["group_id", "user_id", "project_id", "creation_time", "edition_time"],
        inplace=False,
    )

    return lmark_final, group_final
def write_landmarks(fpath: Path, landmarks: pd.DataFrame, groups: pd.DataFrame)
Expand source code
def write_landmarks(fpath: Path, landmarks: pd.DataFrame, groups: pd.DataFrame):
    if len(landmarks) + len(groups) == 0:
        return

    location_data: dict[int, Location] = dict()
    for row in landmarks.itertuples(index=False):
        d = location_data.setdefault(row.location_id, Location((row.x, row.y, row.z)))
        d.landmarks.add(row.name)

    for row in groups.itertuples(index=False):
        d = location_data.setdefault(row.location_id, Location((row.x, row.y, row.z)))
        d.groups.add(row.name)

    out = [v.to_jso() for _, v in sorted(location_data.items())]

    with open(fpath, "w") as f:
        json.dump(out, f, indent=2, sort_keys=True)

Classes

class LandmarkReader (dpath: Path)

Class for reading exported landmark data.

Parameters

dpath : Path
Directory in which landmark data is saved.
Expand source code
class LandmarkReader:
    """Class for reading exported landmark data."""

    def __init__(self, dpath: Path) -> None:
        """
        Parameters
        ----------
        dpath : Path
            Directory in which landmark data is saved.
        """
        self.dpath = dpath
        self.fpath = dpath / "locations.json"

    @copy_cache()
    def _locations(self):
        with open(self.fpath) as f:
            d = json.load(f)

        return [Location.from_jso(loc) for loc in d]

    def get_all(self) -> Iterable[Location]:
        """Lazily iterate through landmark locations.

        Yields
        ------
        Location
        """
        yield from self._locations()

    def get_group_names(self) -> set[str]:
        """Return all groups with locations in the dataset.

        Returns
        -------
        set[str]
            Set of group names.
        """
        out = set()
        for loc in self._locations():
            out.update(loc.groups)
        return out

    def get_landmark_names(self) -> set[str]:
        """Return all landmarks with locations in the dataset.

        Returns
        -------
        set[str]
            Set of landmark names.
        """
        out = set()
        for loc in self._locations():
            out.update(loc.landmarks)
        return out

    def get_group(self, *group: str) -> Iterable[Location]:
        """Lazily iterate through all locations from any of the given groups.

        Parameters
        ----------
        group : str
            Group name (can give multiple as *args).

        Yields
        ------
        Location
        """
        groupset = set(group)
        for loc in self._locations():
            if not loc.groups.isdisjoint(groupset):
                yield loc

    def get_landmark(self, *landmark: str) -> Iterable[Location]:
        """Lazily iterate through all locations from any of the given landmarks.

        Parameters
        ----------
        landmark : str
            Landmark name (can give multiple as *args)

        Yields
        ------
        Location
        """
        lmarkset = set(landmark)
        for loc in self._locations():
            if not loc.landmarks.isdisjoint(lmarkset):
                yield loc

    def get_paired_locations(
        self, group1: str, group2: str
    ) -> Iterable[tuple[Location, Location]]:
        """Iterate through paired locations.

        Locations are paired when both belong to the same landmark,
        and each location is the only one of that landmark to exist in that group,
        and they are not the same location.

        This is useful for creating transformations between two spaces
        (as landmark groups) by shared features (as landmarks).

        Parameters
        ----------
        group1 : str
            Group name
        group2 : str
            Group name

        Yields
        ------
        tuple[Location, Location]
        """
        la_lo1: dict[str, list[Location]] = dict()
        la_lo2: dict[str, list[Location]] = dict()
        for loc in self._locations():
            if group1 in loc.groups:
                if group2 in loc.groups:
                    continue
                for landmark in loc.landmarks:
                    la_lo1.setdefault(landmark, []).append(loc)
            elif group2 in loc.groups:
                for landmark in loc.landmarks:
                    la_lo2.setdefault(landmark, []).append(loc)

        landmarks = sorted(set(la_lo1).intersection(la_lo2))
        for la in landmarks:
            lo1 = la_lo1[la]
            if len(lo1) != 1:
                continue
            lo2 = la_lo2[la]
            if len(lo2) != 1:
                continue

            yield lo1[0], lo2[0]

Methods

def get_all(self) ‑> Iterable[Location]

Lazily iterate through landmark locations.

Yields

Location
 
Expand source code
def get_all(self) -> Iterable[Location]:
    """Lazily iterate through landmark locations.

    Yields
    ------
    Location
    """
    yield from self._locations()
def get_group(self, *group: str) ‑> Iterable[Location]

Lazily iterate through all locations from any of the given groups.

Parameters

group : str
Group name (can give multiple as *args).

Yields

Location
 
Expand source code
def get_group(self, *group: str) -> Iterable[Location]:
    """Lazily iterate through all locations from any of the given groups.

    Parameters
    ----------
    group : str
        Group name (can give multiple as *args).

    Yields
    ------
    Location
    """
    groupset = set(group)
    for loc in self._locations():
        if not loc.groups.isdisjoint(groupset):
            yield loc
def get_group_names(self) ‑> set[str]

Return all groups with locations in the dataset.

Returns

set[str]
Set of group names.
Expand source code
def get_group_names(self) -> set[str]:
    """Return all groups with locations in the dataset.

    Returns
    -------
    set[str]
        Set of group names.
    """
    out = set()
    for loc in self._locations():
        out.update(loc.groups)
    return out
def get_landmark(self, *landmark: str) ‑> Iterable[Location]

Lazily iterate through all locations from any of the given landmarks.

Parameters

landmark : str
Landmark name (can give multiple as *args)

Yields

Location
 
Expand source code
def get_landmark(self, *landmark: str) -> Iterable[Location]:
    """Lazily iterate through all locations from any of the given landmarks.

    Parameters
    ----------
    landmark : str
        Landmark name (can give multiple as *args)

    Yields
    ------
    Location
    """
    lmarkset = set(landmark)
    for loc in self._locations():
        if not loc.landmarks.isdisjoint(lmarkset):
            yield loc
def get_landmark_names(self) ‑> set[str]

Return all landmarks with locations in the dataset.

Returns

set[str]
Set of landmark names.
Expand source code
def get_landmark_names(self) -> set[str]:
    """Return all landmarks with locations in the dataset.

    Returns
    -------
    set[str]
        Set of landmark names.
    """
    out = set()
    for loc in self._locations():
        out.update(loc.landmarks)
    return out
def get_paired_locations(self, group1: str, group2: str) ‑> Iterable[tuple[LocationLocation]]

Iterate through paired locations.

Locations are paired when both belong to the same landmark, and each location is the only one of that landmark to exist in that group, and they are not the same location.

This is useful for creating transformations between two spaces (as landmark groups) by shared features (as landmarks).

Parameters

group1 : str
Group name
group2 : str
Group name

Yields

tuple[Location, Location]
 
Expand source code
def get_paired_locations(
    self, group1: str, group2: str
) -> Iterable[tuple[Location, Location]]:
    """Iterate through paired locations.

    Locations are paired when both belong to the same landmark,
    and each location is the only one of that landmark to exist in that group,
    and they are not the same location.

    This is useful for creating transformations between two spaces
    (as landmark groups) by shared features (as landmarks).

    Parameters
    ----------
    group1 : str
        Group name
    group2 : str
        Group name

    Yields
    ------
    tuple[Location, Location]
    """
    la_lo1: dict[str, list[Location]] = dict()
    la_lo2: dict[str, list[Location]] = dict()
    for loc in self._locations():
        if group1 in loc.groups:
            if group2 in loc.groups:
                continue
            for landmark in loc.landmarks:
                la_lo1.setdefault(landmark, []).append(loc)
        elif group2 in loc.groups:
            for landmark in loc.landmarks:
                la_lo2.setdefault(landmark, []).append(loc)

    landmarks = sorted(set(la_lo1).intersection(la_lo2))
    for la in landmarks:
        lo1 = la_lo1[la]
        if len(lo1) != 1:
            continue
        lo2 = la_lo2[la]
        if len(lo2) != 1:
            continue

        yield lo1[0], lo2[0]
class Location (xyz: tuple[float, float, float], groups: set[str] = <factory>, landmarks: set[str] = <factory>)

Location of importance to landmarks and groups.

Attributes

xyz : tuple[float, float, float]
Coordinates of location
groups : set[str]
Set of landmark groups this location belongs to.
landmarks : set[str]
Set of landmarks this location belongs to.
Expand source code
@dataclass
class Location:
    """Location of importance to landmarks and groups.

    Attributes
    ----------
    xyz : tuple[float, float, float]
        Coordinates of location
    groups : set[str]
        Set of landmark groups this location belongs to.
    landmarks : set[str]
        Set of landmarks this location belongs to.
    """

    xyz: tuple[float, float, float]
    groups: set[str] = field(default_factory=set)
    landmarks: set[str] = field(default_factory=set)

    def to_jso(self) -> dict[str, Any]:
        """Convert to JSON-serialisable object.

        Returns
        -------
        dict[str, Any]
        """
        d = asdict(self)
        d["xyz"] = list(d["xyz"])
        d["groups"] = sorted(d["groups"])
        d["landmarks"] = sorted(d["landmarks"])
        return d

    @classmethod
    def from_jso(cls, jso: dict[str, Any]) -> Location:
        """Instantiate from JSON-like dict.

        Parameters
        ----------
        jso : dict[str, Any]
            Keys ``"xyz"`` (3-length list of float),
            ``"groups"`` (list of str),
            ``"landmarks"`` (list of str)

        Returns
        -------
        Location
        """
        return cls(
            tuple(jso["xyz"]),
            set(jso["groups"]),
            set(jso["landmarks"]),
        )

Class variables

var groups : set[str]
var landmarks : set[str]
var xyz : tuple[float, float, float]

Static methods

def from_jso(jso: dict[str, Any]) ‑> Location

Instantiate from JSON-like dict.

Parameters

jso : dict[str, Any]
Keys "xyz" (3-length list of float), "groups" (list of str), "landmarks" (list of str)

Returns

Location
 
Expand source code
@classmethod
def from_jso(cls, jso: dict[str, Any]) -> Location:
    """Instantiate from JSON-like dict.

    Parameters
    ----------
    jso : dict[str, Any]
        Keys ``"xyz"`` (3-length list of float),
        ``"groups"`` (list of str),
        ``"landmarks"`` (list of str)

    Returns
    -------
    Location
    """
    return cls(
        tuple(jso["xyz"]),
        set(jso["groups"]),
        set(jso["landmarks"]),
    )

Methods

def to_jso(self) ‑> dict[str, typing.Any]

Convert to JSON-serialisable object.

Returns

dict[str, Any]
 
Expand source code
def to_jso(self) -> dict[str, Any]:
    """Convert to JSON-serialisable object.

    Returns
    -------
    dict[str, Any]
    """
    d = asdict(self)
    d["xyz"] = list(d["xyz"])
    d["groups"] = sorted(d["groups"])
    d["landmarks"] = sorted(d["landmarks"])
    return d