Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement older monocot pipeline #54

Merged
merged 50 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
3e841ec
Fix docstring and `get_network_distribution_ratio` arguments
eberrigan Aug 17, 2023
92dae9e
Add `join_pts` and `get_count` functions
eberrigan Aug 20, 2023
9325a1b
Refactor `get_network_distribution`
eberrigan Aug 21, 2023
1621efa
Generalize functions for concatenating and joining points
eberrigan Aug 21, 2023
cb0f13b
Refactor scanline function
eberrigan Aug 21, 2023
e23af0f
Make `OlderMonocotPipeline`
eberrigan Aug 21, 2023
4bc568f
Refactor tests and fix arguments
eberrigan Aug 21, 2023
ff1fb10
Refactor `get_network_length` to take arbitrary number of lengths
eberrigan Aug 23, 2023
57b8fed
Refactor `get_all_pts_array` to simplify
eberrigan Aug 23, 2023
25391b4
Refactor `Series` class to take a arbitrary number of labels
eberrigan Aug 23, 2023
dfabf07
Add 10 DO rice fixture
eberrigan Aug 23, 2023
890df7f
Test refactored functions
eberrigan Aug 23, 2023
4843f08
Add 10 DO rice fixture
eberrigan Aug 23, 2023
0c42ecb
Add test for `OlderMonocot_pipeline`
eberrigan Aug 27, 2023
f33f0e0
Modify `get_grav_index` to take float and array inputs
eberrigan Aug 31, 2023
797561b
Fix docstring in `Series` class
eberrigan Aug 31, 2023
aa59e2b
Test `OlderMonocotPipeline` and `get_grav_index`
eberrigan Aug 31, 2023
bd2621b
Lint
eberrigan Aug 31, 2023
045a7b4
Add folder of 10 do rice test data and test with `find_all_series`
eberrigan Sep 1, 2023
2364b89
Refactor series class and related functions to include crown labels
eberrigan Feb 20, 2024
a92be32
Refactor tests and fixtures
eberrigan Feb 20, 2024
1fceb01
Get rid of uncessary monocots flag
eberrigan Feb 21, 2024
1ab5a39
Refactor tips and bases
eberrigan Feb 21, 2024
11df884
Refactor tips and bases tests
eberrigan Feb 21, 2024
606af51
Merge main with `older_monocot_pipeline` branch
eberrigan Feb 21, 2024
3d1109f
Add standalone get root angle function
eberrigan Feb 27, 2024
57cfbd1
Fix typing
eberrigan Feb 27, 2024
c063a9f
Add checks to convex hull function
eberrigan Feb 27, 2024
29cdb7a
Get rid of obsolete function
eberrigan Feb 27, 2024
113ee3e
Fix network distribution
eberrigan Feb 27, 2024
534d9e6
Add points related functions
eberrigan Feb 27, 2024
b372567
Rename test data
eberrigan Feb 27, 2024
93cfe87
Test new angle functions
eberrigan Feb 27, 2024
7464680
Test new convex hull functions
eberrigan Feb 27, 2024
dd06872
Test new points functions
eberrigan Feb 27, 2024
2279ac0
Rename test data
eberrigan Feb 27, 2024
655d9cc
Fix tests
eberrigan Feb 27, 2024
ef271da
Match shapes
eberrigan Feb 27, 2024
a9054e3
Update trait pipelines
eberrigan Feb 28, 2024
d175680
Test trait pipelines
eberrigan Mar 1, 2024
5efb4fb
Test trait pipelines
eberrigan Mar 1, 2024
2416fd8
Update version
eberrigan Mar 1, 2024
9838314
Resolve conflicts
eberrigan Mar 1, 2024
0c4b4df
Resolve conflicts
eberrigan Mar 1, 2024
1e692b5
resolve
eberrigan Mar 1, 2024
10eb050
Merge branch 'main' into elizabeth/older_monocot_pipeline
eberrigan Mar 1, 2024
264dcca
Fix tips and bases and tests
eberrigan Mar 2, 2024
e38b289
Remove notebooks
eberrigan Mar 2, 2024
880817d
Reformat
eberrigan Mar 2, 2024
83b6ddd
Lint
eberrigan Mar 2, 2024
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
690 changes: 690 additions & 0 deletions notebooks/convhull_tests.ipynb

Large diffs are not rendered by default.

414 changes: 414 additions & 0 deletions notebooks/pipeline_tests.ipynb

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions sleap_roots/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@
import sleap_roots.series
import sleap_roots.summary
import sleap_roots.trait_pipelines
from sleap_roots.trait_pipelines import DicotPipeline, TraitDef, YoungerMonocotPipeline
from sleap_roots.trait_pipelines import (
DicotPipeline,
TraitDef,
YoungerMonocotPipeline,
OlderMonocotPipeline,
)
from sleap_roots.series import Series, find_all_series

# Define package version.
# This is read dynamically by setuptools in pyproject.toml to determine the release version.
__version__ = "0.0.5"
__version__ = "0.0.6"
31 changes: 30 additions & 1 deletion sleap_roots/angle.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ def get_root_angle(
pts = np.expand_dims(pts, axis=0)

angs_root = []
for i in range(len(node_ind)):
# Calculate the angle for each instance
for i in range(pts.shape[0]):
# if the node_ind is 0, do NOT calculate angs
if node_ind[i] == 0:
angs = np.nan
Expand All @@ -90,3 +91,31 @@ def get_root_angle(
if angs_root.shape[0] == 1:
return angs_root[0]
return angs_root


def get_vector_angles_from_gravity(vectors: np.ndarray) -> np.ndarray:
"""Calculate the angle of given vectors from the gravity vector, assuming the gravity
vector points downwards along the positive y-axis.

Args:
vectors: An array of vectorss with shape (instances, 2), each representing a vector
from start to end in an instance.

Returns:
An array of angles in degrees with shape (instances,), representing the angle
between each vector and the downward-pointing gravity vector.
"""
gravity_vector = np.array([0, 1]) # Downwards along the positive y-axis
# Calculate the angle between the vectors and the gravity vectors
angles = np.arctan2(vectors[:, 1], vectors[:, 0]) - np.arctan2(
gravity_vector[1], gravity_vector[0]
)
angles = np.degrees(angles)
# Normalize angles to the range [0, 180] since direction doesn't matter
angles = np.abs(angles)
angles[angles > 180] = 360 - angles[angles > 180]

# If only one root, return a scalar instead of a single-element array
if angles.shape[0] == 1:
return angles[0]
return angles
96 changes: 25 additions & 71 deletions sleap_roots/bases.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Trait calculations that rely on bases (i.e., dicot-only)."""
"""Trait calculations that rely on bases."""

import numpy as np
from shapely.geometry import LineString, Point
Expand All @@ -7,20 +7,16 @@
from typing import Union, Tuple


def get_bases(pts: np.ndarray, monocots: bool = False) -> np.ndarray:
def get_bases(pts: np.ndarray) -> np.ndarray:
"""Return bases (r1) from each root.

Args:
pts: Root landmarks as array of shape `(instances, nodes, 2)` or `(nodes, 2)`.
monocots: Boolean value, where false is dicot (default), true is rice.

Returns:
Array of bases `(instances, (x, y))`. If the input is `(nodes, 2)`, an array of
shape `(2,)` will be returned.
shape `(2,)` will be returned.
"""
if monocots:
return np.nan

# If the input has shape `(nodes, 2)`, reshape it for consistency
if pts.ndim == 2:
pts = pts[np.newaxis, ...]
Expand Down Expand Up @@ -73,57 +69,33 @@ def get_base_tip_dist(
return distances


def get_lateral_count(pts: np.ndarray):
"""Get number of lateral roots.

Args:
pts: lateral root landmarks as array of shape `(instance, node, 2)`.

Return:
Scalar of number of lateral roots.
"""
lateral_count = pts.shape[0]
return lateral_count


def get_base_xs(base_pts: np.ndarray) -> np.ndarray:
"""Get x coordinates of the base of each lateral root.

Args:
base_pts: Array of bases as returned by `get_bases`, shape `(instances, 2)` or
`(2,)`.
base_pts: root bases as array of shape `(instances, 2)` or `(2)` when there is
only one root, as is the case for primary roots.

Returns:
An array of the x-coordinates of bases `(instances,)` or a single x-coordinate.
Return:
An array of base x-coordinates (instances,) or (1,) when there is only one root.
"""
# If the input is a single number (float or integer), return np.nan
if isinstance(base_pts, (np.floating, float, np.integer, int)):
return np.nan

# If the base points array has shape `(2,)`, return the first element (x)
if base_pts.ndim == 1 and base_pts.shape[0] == 2:
return base_pts[0]

# If the base points array doesn't have exactly 2 dimensions or
# the second dimension is not of size 2, raise an error
elif base_pts.ndim != 2 or base_pts.shape[1] != 2:
if base_pts.ndim not in (1, 2):
raise ValueError(
"Array of base points must be 2-dimensional with shape (instances, 2)."
"Input array must be 2-dimensional (instances, 2) or 1-dimensional (2,)."
)
if base_pts.shape[-1] != 2:
raise ValueError("Last dimension must be (x, y).")

# If everything is fine, extract and return the x-coordinates of the base points
else:
base_xs = base_pts[:, 0]
return base_xs
base_xs = base_pts[..., 0]
return base_xs


def get_base_ys(base_pts: np.ndarray, monocots: bool = False) -> np.ndarray:
def get_base_ys(base_pts: np.ndarray) -> np.ndarray:
"""Get y coordinates of the base of each root.

Args:
base_pts: root bases as array of shape `(instances, 2)` or `(2)`
when there is only one root, as is the case for primary roots.
monocots: Boolean value, where false is dicot (default), true is rice.

Return:
An array of the y-coordinates of bases (instances,).
Expand All @@ -144,25 +116,19 @@ def get_base_ys(base_pts: np.ndarray, monocots: bool = False) -> np.ndarray:
return base_ys


def get_base_length(lateral_base_ys: np.ndarray, monocots: bool = False) -> float:
def get_base_length(lateral_base_ys: np.ndarray) -> float:
"""Get the y-axis difference from the top lateral base to the bottom lateral base.

Args:
lateral_base_ys: y-coordinates of the base points of lateral roots of shape
`(instances,)`.
monocots: Boolean value, where false is dicot (default), true is rice.

Return:
The distance between the top base y-coordinate and the deepest
base y-coordinate.
"""
# If the roots are monocots, return NaN
if monocots:
return np.nan

# Compute the difference between the maximum and minimum y-coordinates
base_length = np.nanmax(lateral_base_ys) - np.nanmin(lateral_base_ys)

return base_length


Expand All @@ -179,7 +145,7 @@ def get_base_ct_density(
Return:
Scalar of base count density.
"""
# Check if the input is invalid
# Check if the input is valid for lateral_base_pts
if (
isinstance(lateral_base_pts, (np.floating, float, np.integer, int))
or np.isnan(lateral_base_pts).all()
Expand All @@ -204,22 +170,19 @@ def get_base_ct_density(
return base_ct_density


def get_base_length_ratio(
primary_length: float, base_length: float, monocots: bool = False
) -> float:
def get_base_length_ratio(primary_length: float, base_length: float) -> float:
"""Calculate the ratio of the length of the bases to the primary root length.

Args:
primary_length (float): Length of the primary root.
base_length (float): Length of the bases along the primary root.
monocots (bool): True if the roots are monocots, False if they are dicots.

Returns:
Ratio of the length of the bases along the primary root to the primary root
length.
"""
# If roots are monocots or either of the lengths are NaN, return NaN
if monocots or np.isnan(primary_length) or np.isnan(base_length):
# If either of the lengths are NaN, return NaN
if np.isnan(primary_length) or np.isnan(base_length):
return np.nan

# Handle case where primary length is zero to avoid division by zero
Expand All @@ -231,24 +194,19 @@ def get_base_length_ratio(
return base_length_ratio


def get_base_median_ratio(lateral_base_ys, primary_tip_pt_y, monocots: bool = False):
def get_base_median_ratio(lateral_base_ys, primary_tip_pt_y):
"""Get ratio of median value in all base points to tip of primary root in y axis.

Args:
lateral_base_ys: y-coordinates of the base points of lateral roots of shape
lateral_base_ys: Y-coordinates of the base points of lateral roots of shape
`(instances,)`.
primary_tip_pt_y: y-coordinate of the tip point of the primary root of shape
primary_tip_pt_y: Y-coordinate of the tip point of the primary root of shape
`(1)`.
monocots: Boolean value, where false is dicot (default), true is rice.

Return:
Scalar of base median ratio. If all y-coordinates of the lateral root bases are
NaN, the function returns NaN.
NaN, the function returns NaN.
"""
# Check if the roots are monocots, if so return NaN
if monocots:
return np.nan

# Check if all y-coordinates of lateral root bases are NaN, if so return NaN
if np.isnan(lateral_base_ys).all():
return np.nan
Expand All @@ -271,9 +229,8 @@ def get_root_widths(
primary_max_length_pts: np.ndarray,
lateral_pts: np.ndarray,
tolerance: float = 0.02,
monocots: bool = False,
return_inds: bool = False,
) -> (np.ndarray, list, np.ndarray, np.ndarray):
) -> Tuple[np.ndarray, list, np.ndarray, np.ndarray]:
"""Estimate root width using bases of lateral roots.

Args:
Expand All @@ -283,8 +240,6 @@ def get_root_widths(
shape (n, nodes, 2).
tolerance: Tolerance level for the projection difference between matched roots.
Defaults to 0.02.
monocots: Indicates the type of plant. Set to False for dicots (default) or
True for monocots like rice.
return_inds: Flag to indicate whether to return matched indices along with
distances. Defaults to False.

Expand Down Expand Up @@ -326,11 +281,10 @@ def get_root_widths(
default_left_bases = np.empty((0, 2))
default_right_bases = np.empty((0, 2))

# Check for minimum length, monocots, or all NaNs in arrays
# Check for minimum length, or all NaNs in arrays
if (
len(primary_max_length_pts) < 2
or len(lateral_pts) < 2
or monocots
or np.isnan(primary_max_length_pts).all()
or np.isnan(lateral_pts).all()
):
Expand Down
Loading
Loading