diff --git a/pyproject.toml b/pyproject.toml index 0ddc0be..8b8b28f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jabs-postprocess" -version = "0.5.0" +version = "0.5.1" description = "A python library for JABS postprocessing utilities." readme = "README.md" license = "LicenseRef-PLATFORM-LICENSE-AGREEMENT-FOR-NON-COMMERCIAL-USE" diff --git a/src/jabs_postprocess/heuristic_classifiers/locomotion_corner.yaml b/src/jabs_postprocess/heuristic_classifiers/locomotion_corner.yaml new file mode 100644 index 0000000..a117371 --- /dev/null +++ b/src/jabs_postprocess/heuristic_classifiers/locomotion_corner.yaml @@ -0,0 +1,43 @@ +# Locomotion in corner +# Within 20% distance of any 2 walls +# Mouse is moving > 5cm/s +# This is really slow for a mouse, but essentially just removes noise from micromovements +behavior: + Locomotion In Corner + +interpolate: + 5 + +stitch: + 5 + +min_bout: + 15 + +definition: + and: + - minimum: + - 2 + - less than: + - wall_distances/wall_0 + - divide: + - avg_wall_length + - 5 + - less than: + - wall_distances/wall_1 + - divide: + - avg_wall_length + - 5 + - less than: + - wall_distances/wall_2 + - divide: + - avg_wall_length + - 5 + - less than: + - wall_distances/wall_3 + - divide: + - avg_wall_length + - 5 + - greater than: + - features/per_frame/centroid_velocity_mag centroid_velocity_mag + - 5.0 diff --git a/src/jabs_postprocess/heuristic_classifiers/locomotion_periphery.yaml b/src/jabs_postprocess/heuristic_classifiers/locomotion_periphery.yaml new file mode 100644 index 0000000..eadb49c --- /dev/null +++ b/src/jabs_postprocess/heuristic_classifiers/locomotion_periphery.yaml @@ -0,0 +1,26 @@ +# Locomotion in periphery +# Within 20% distance of any wall +# Mouse is moving > 5cm/s +# This is really slow for a mouse, but essentially just removes noise from micromovements +behavior: + Locomotion In Periphery + +interpolate: + 5 + +stitch: + 5 + +min_bout: + 15 + +definition: + and: + - less than: + - features/per_frame/corner_distances distance to wall + - divide: + - avg_wall_length + - 5 + - greater than: + - features/per_frame/centroid_velocity_mag centroid_velocity_mag + - 5.0 diff --git a/src/jabs_postprocess/utils/project_utils.py b/src/jabs_postprocess/utils/project_utils.py index e7b77d2..38d2fbe 100644 --- a/src/jabs_postprocess/utils/project_utils.py +++ b/src/jabs_postprocess/utils/project_utils.py @@ -509,6 +509,8 @@ def __init__(self, settings: ClassifierSettings, data: pd.DataFrame): "exp_prefix", "time", "distance", + "distance_threshold", + "distance_seg", "closest_id", "closest_lixit", "closest_corner", @@ -677,6 +679,8 @@ def __init__(self, settings: ClassifierSettings, data: pd.DataFrame): "exp_prefix", "time", "distance", + "distance_threshold", + "distance_seg", "closest_id", "closest_lixit", "closest_corner", @@ -770,6 +774,16 @@ def add_bout_features(self, feature_file: Path): "features/per_frame/centroid_velocity_mag centroid_velocity_mag", lambda x: np.nansum(x, initial=0) / 30, ), + FeatureInEvent( + "distance_threshold", + "features/per_frame/centroid_velocity_mag centroid_velocity_mag", + lambda x: np.nansum(x[x > 5], initial=0) / 30, + ), + FeatureInEvent( + "distance_seg", + "features/per_frame/shape_descriptor centroid_speed", + lambda x: np.nansum(x, initial=0) / 30, + ), FeatureInEvent("closest_id", "closest_identities", np.median), FeatureInEvent("closest_lixit", "closest_lixit", np.median), FeatureInEvent("closest_corner", "closest_corners", np.median), @@ -1003,6 +1017,14 @@ def bouts_to_bins( bins_to_summarize["calc_dist"] = ( bins_to_summarize["distance"] * bins_to_summarize["percent_bout"] ) + bins_to_summarize["calc_dist_threshold"] = ( + bins_to_summarize["distance_threshold"] + * bins_to_summarize["percent_bout"] + ) + bins_to_summarize["calc_dist_seg"] = ( + bins_to_summarize["distance_seg"] + * bins_to_summarize["percent_bout"] + ) else: pass pd.options.mode.chained_assignment = "warn" @@ -1024,12 +1046,10 @@ def bouts_to_bins( # We use a weighted statistic definitions here # Weights are the proportion of bout contained in the bin (percent_bout) if results["bout_behavior"] > 0: - results["avg_bout_duration"] = ( - np.sum( - behavior_bins["duration"].values - * behavior_bins["percent_bout"].values - ) - / results["bout_behavior"] + results["avg_bout_duration"] = np.average( + behavior_bins["duration"].values + / behavior_bins["percent_bout"].values, + weights=behavior_bins["percent_bout"].values, ) results["latency_to_first_prediction"] = behavior_bins["start"].min() results["latency_to_last_prediction"] = ( @@ -1072,6 +1092,12 @@ def bouts_to_bins( results["behavior_dist"] = bins_to_summarize.loc[ bins_to_summarize["is_behavior"] == 1, "calc_dist" ].sum() + results["behavior_dist_threshold"] = bins_to_summarize.loc[ + bins_to_summarize["is_behavior"] == 1, "calc_dist_threshold" + ].sum() + results["behavior_dist_seg"] = bins_to_summarize.loc[ + bins_to_summarize["is_behavior"] == 1, "calc_dist_seg" + ].sum() results_df_list.append(pd.DataFrame(results)) # Remove an non-informative rows @@ -1153,6 +1179,8 @@ def __init__(self, settings: ClassifierSettings, data: pd.DataFrame): "time", "not_behavior_dist", "behavior_dist", + "behavior_dist_threshold", + "behavior_dist_seg", "avg_bout_duration", "_stats_sample_count", "bout_duration_std", diff --git a/tests/utils/test_project_utils.py b/tests/utils/test_project_utils.py index 9009370..af6ce54 100644 --- a/tests/utils/test_project_utils.py +++ b/tests/utils/test_project_utils.py @@ -759,15 +759,15 @@ def test_bouts_to_bins_split_bout_weighted_statistics(self): # Both bins should have: # - bout_behavior = 0.5 (half the bout counted) # - time_behavior = 300 (half the frames) - # - avg_bout_duration = 300 (the actual duration in each bin) + # - avg_bout_duration = 600 (duration remains same, average of 1 sample) # - variance = NaN (only one bout, even if split) assert len(result) == 2 for i in range(2): assert abs(result.iloc[i]["bout_behavior"] - 0.5) < 0.01 assert result.iloc[i]["time_behavior"] == 300 - # When a bout is split, the avg_bout_duration is the fractional duration - assert abs(result.iloc[i]["avg_bout_duration"] - 300) < 0.1 + # When a bout is split, the avg_bout_duration is still the total duration + assert abs(result.iloc[i]["avg_bout_duration"] - 600) < 0.1 assert pd.isna(result.iloc[i]["bout_duration_var"]) def test_bouts_to_bins_multiple_split_bouts_variance(self): diff --git a/uv.lock b/uv.lock index 3d517ae..a774e8f 100644 --- a/uv.lock +++ b/uv.lock @@ -340,7 +340,7 @@ wheels = [ [[package]] name = "jabs-postprocess" -version = "0.5.0" +version = "0.5.1" source = { editable = "." } dependencies = [ { name = "black" }, diff --git a/vm/jabs-postprocess.def b/vm/jabs-postprocess.def index be1aa11..7a58cd4 100644 --- a/vm/jabs-postprocess.def +++ b/vm/jabs-postprocess.def @@ -62,5 +62,5 @@ From: ghcr.io/astral-sh/uv:python3.10-bookworm-slim %labels Author "The Kumar Lab" - Version "0.1.0" + Version "0.5.1" Description "JABS post-processing container with Python 3.10"