Skip to content

Commit

Permalink
tests + associated changes
Browse files Browse the repository at this point in the history
  • Loading branch information
ceesem committed Jul 9, 2024
1 parent a9b7682 commit 7f99a0f
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 38 deletions.
3 changes: 2 additions & 1 deletion src/nglui/easyviewer/ev_base/mainline.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ def add_selected_objects(
if issubdtype(type(oids), integer):
oids = [oids]
with self.txn() as s:
s.layers[segmentation_layer].segments.update(oids)
for oid in oids:
s.layers[segmentation_layer].segments.add(oid)
if colors is not None:
if isinstance(colors, dict):
self.assign_colors(segmentation_layer, colors)
Expand Down
68 changes: 34 additions & 34 deletions src/nglui/segmentprops/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __attrs_post_init__(self):

@attrs.define
class LabelProperty(SegmentPropertyBase):
id = attrs.field(init=False, default="label", type=str)
id = attrs.field(default="label", type=str, kw_only=True)
type = attrs.field(init=False, default="label", type=str)
values = attrs.field(type=list, converter=list_of_strings, kw_only=True)
description = attrs.field(type=Optional[str], default=None, kw_only=True)
Expand All @@ -65,7 +65,7 @@ def __len__(self):

@attrs.define
class DescriptionProperty(SegmentPropertyBase):
id = attrs.field(init=False, default="description", type=str)
id = attrs.field(default="description", type=str, kw_only=True)
type = attrs.field(init=False, default="description", type=str)
values = attrs.field(type=list, converter=list_of_strings, kw_only=True)
description = attrs.field(type=Optional[str], default=None, kw_only=True)
Expand Down Expand Up @@ -109,7 +109,7 @@ def __len__(self):

@attrs.define
class TagProperty(SegmentPropertyBase):
id = attrs.field(type=str, kw_only=True)
id = attrs.field(type=str, kw_only=True, default="tags")
type = attrs.field(init=False, type=str, default="tags")
tags = attrs.field(
type=list[str],
Expand Down Expand Up @@ -187,35 +187,30 @@ def _tag_descriptions(tags, tag_descriptions):
return [tag_descriptions.get(tag, tag) for tag in tags]


def _tag_property_from_columns(df, cols, tag_descriptions=None, name="tags"):
def _make_tag_property(df, value_columns, bool_columns, tag_descriptions, name="tags"):
if value_columns is None:
value_columns = []
if bool_columns is None:
bool_columns = []
tags = []
for col in cols:
for col in value_columns:
unique_tags = df[col].unique()
# df.unique works differently for categorical dtype columns and does not return an ndarray so we have to check
if isinstance(unique_tags, np.ndarray):
unique_tags = sorted(unique_tags.tolist())
unique_tags = sorted([x for x in unique_tags.tolist() if x is not None])
else:
unique_tags = unique_tags.sort_values().tolist()
if np.any(np.isin(tags, unique_tags)):
raise ValueError("Tags across columns are not unique")
tags.extend(unique_tags)
tags.extend(bool_columns)
tag_map = {tag: i for i, tag in enumerate(tags) if tag is not None}
tag_values = []
for _, row in df.iterrows():
tag_values.append([tag_map[tag] for tag in row[cols] if tag is not None])
return TagProperty(
id=name,
tags=tags,
values=tag_values,
tag_descriptions=_tag_descriptions(tags, tag_descriptions),
)


def _tag_property_from_bool_cols(df, col_list, tag_descriptions=None, name="tags"):
tags = col_list
tag_map = {tag: i for i, tag in enumerate(tags)}
tag_values = [[] for _ in range(len(df))]
for tv in tag_values:
tag_values.append(
[tag_map[tag] for tag in row[value_columns] if tag is not None]
)
for tv in bool_columns:
for loc in np.flatnonzero(df[tv]):
tag_values[loc].append(tag_map[tv])
return TagProperty(
Expand Down Expand Up @@ -276,6 +271,15 @@ def _property_list(self):
)
return single_prop_list + multi_prop_list

def __repr__(self):
return f"SegmentProperties ({len(self.ids)} segments, {len(self._property_list())} properties)"

def __str__(self):
return self.__repr__()

def property_description(self):
return [(prop.id, prop.type, len(prop)) for prop in self._property_list()]

def to_dict(self):
"Converts the segment properties to a dictionary for use in neuroglancer"
return build_segment_properties(
Expand Down Expand Up @@ -317,8 +321,8 @@ def from_dataframe(
Column (or list of columns) to generate tags based on unique values.
Each column produces one tag per row based on the value, by default None
tag_bool_cols : Optional[list[str]], optional
List of columns to generate tags based on boolean values. Each column is a tag, and each id gets the tag if it has a True in its row.
Cannot be used if `tag_value_cols` is also used. y default None
List of columns to generate tags based on boolean values where each column is a tag, and each id gets the tag if it has a True in its row.
By default None.
tag_descriptions : Optional[dict], optional
Dictionary of tag values to long-form tag descriptions, by default None.
Tags without a key/value are passed through directly.
Expand All @@ -337,10 +341,14 @@ def from_dataframe(
values=df[description_col].tolist()
)
if string_cols:
if isinstance(string_cols, str):
string_cols = [string_cols]
properties["string_properties"] = [
StringProperty(id=col, values=df[col].tolist()) for col in string_cols
]
if number_cols:
if isinstance(number_cols, str):
number_cols = [number_cols]
properties["number_properties"] = [
NumberProperty(
id=col,
Expand All @@ -349,19 +357,14 @@ def from_dataframe(
)
for col in number_cols
]
if tag_value_cols:
if tag_value_cols or tag_bool_cols:
if isinstance(tag_value_cols, str):
tag_value_cols = [tag_value_cols]
properties["tag_properties"] = _tag_property_from_columns(
if isinstance(tag_bool_cols, str):
tag_bool_cols = [tag_bool_cols]
properties["tag_properties"] = _make_tag_property(
df,
tag_value_cols,
tag_descriptions,
)
elif tag_bool_cols:
if "tag_properties" in properties:
raise ValueError("Cannot set both tag_value_cols and tag_bool_cols")
properties["tag_properties"] = _tag_property_from_bool_cols(
df,
tag_bool_cols,
tag_descriptions,
)
Expand Down Expand Up @@ -390,13 +393,11 @@ def from_dict(
for prop in props:
if prop["type"] == "label":
prop_classes["label_property"] = LabelProperty(
id=prop["id"],
values=prop["values"],
description=prop.get("description"),
)
elif prop["type"] == "description":
prop_classes["description_property"] = DescriptionProperty(
id=prop["id"],
values=prop["values"],
description=prop.get("description"),
)
Expand All @@ -423,7 +424,6 @@ def from_dict(
)
elif prop["type"] == "tags":
prop_classes["tag_properties"] = TagProperty(
id=prop["id"],
tags=prop["tags"],
values=prop["values"],
tag_descriptions=prop.get("tag_descriptions"),
Expand Down
39 changes: 39 additions & 0 deletions tests/test_segment_props.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import pytest
import pandas as pd
import numpy as np
from nglui.segmentprops import SegmentProperties


@pytest.fixture
def test_df():
return pd.DataFrame(
{
"seg_id": np.arange(0, 100),
"cell_type": 30 * ["ct_a"] + 30 * ["ct_b"] + 40 * ["ct_c"],
"category": 50 * ["cat_1"] + 40 * ["cat_2"] + 10 * [None],
"number_int": np.arange(300, 400),
"number_float": np.arange(300, 400) + 0.1,
"tag_a": 90 * [False] + 10 * [True],
"tag_b": 95 * [True] + 5 * [False],
}
)


def test_segment_props(test_df):
props = SegmentProperties.from_dataframe(
test_df,
id_col="seg_id",
label_col="seg_id",
number_cols=["number_int", "number_float"],
tag_value_cols="cell_type",
tag_bool_cols=["tag_a", "tag_b"],
tag_descriptions={"tag_a": "The first tag", "tag_b": "The second tag"},
)

assert len(props) == 100
assert len(props.property_description()) == 4
p_dict = props.to_dict()
assert p_dict["inline"]["properties"][2]["data_type"] == "int32"

rh_props = SegmentProperties.from_dict(p_dict)
assert len(rh_props) == 100
15 changes: 12 additions & 3 deletions tests/test_statebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ def test_basic_cave_explorer(image_layer, seg_layer_basic, anno_layer_basic):
assert state.data == f'<a href="{state_url}" target="_blank">Neuroglancer Link</a>'

state = sb.render_state(return_as="dict")
assert isinstance(state, OrderedDict)
assert isinstance(state, OrderedDict) or isinstance(
state, dict
) # Cave-explorer uses dict

state = sb.render_state(return_as="json")
assert type(state) is str
Expand All @@ -89,8 +91,15 @@ def test_segmentation_layer(soma_df, seg_path_precomputed, target_site):
sb = StateBuilder(layers=[seg_layer], target_site=target_site)
state = sb.render_state(soma_df, return_as="dict")
print(state["layers"])
assert 648518346349538466 in state["layers"][0]["segments"]
assert 1000 in state["layers"][0]["segments"]
assert (
648518346349538466 in state["layers"][0]["segments"]
or "648518346349538466"
in state["layers"][0]["segments"] # Cave-explorer uses strings for ids
)
assert (
1000 in state["layers"][0]["segments"]
or "1000" in state["layers"][0]["segments"]
)


@pytest.mark.parametrize("target_site", [None, "seunglab", "cave-explorer"])
Expand Down

0 comments on commit 7f99a0f

Please sign in to comment.