foz
commited on
Commit
•
0dab632
1
Parent(s):
020d824
Add more options and tests
Browse files- app.py +39 -9
- path_analysis/__init__.py +0 -0
- path_analysis/analyse.py +375 -0
- path_analysis/data_preprocess.py +314 -0
- setup.py +29 -0
- tests/__init__.py +0 -0
- tests/test_analyse.py +223 -0
- tests/test_preprocess.py +139 -0
app.py
CHANGED
@@ -2,20 +2,19 @@
|
|
2 |
import gradio as gr
|
3 |
from tifffile import imread
|
4 |
from PIL import Image
|
5 |
-
|
6 |
-
from analyse import analyse_paths
|
7 |
import numpy as np
|
8 |
|
9 |
-
def process(cell_id, foci_file, traces_file):
|
10 |
-
paths, traces, fig, extracted_peaks = analyse_paths(cell_id, foci_file.name, traces_file.name)
|
11 |
-
extracted_peaks.to_csv('tmp')
|
12 |
-
return paths, [Image.fromarray(im) for im in traces], fig, extracted_peaks, 'tmp'
|
13 |
|
|
|
14 |
def preview_image(file1):
|
15 |
if file1:
|
16 |
im = imread(file1.name)
|
17 |
-
print(im.shape)
|
18 |
-
|
|
|
|
|
|
|
19 |
else:
|
20 |
return None
|
21 |
|
@@ -23,12 +22,27 @@ def preview_image(file1):
|
|
23 |
with gr.Blocks() as demo:
|
24 |
with gr.Row():
|
25 |
with gr.Column():
|
|
|
26 |
cellid_input = gr.Textbox(label="Cell ID", placeholder="Image_1")
|
27 |
image_input = gr.File(label="Input foci image")
|
28 |
image_preview = gr.Image(label="Max projection of foci image")
|
29 |
image_input.change(fn=preview_image, inputs=image_input, outputs=image_preview)
|
30 |
path_input = gr.File(label="SNT traces file")
|
31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
with gr.Column():
|
33 |
trace_output = gr.Image(label="Overlayed paths")
|
34 |
image_output=gr.Gallery(label="Traced paths")
|
@@ -36,9 +50,25 @@ with gr.Blocks() as demo:
|
|
36 |
data_output=gr.DataFrame(label="Detected peak data")#, "Peak 1 pos", "Peak 1 int"])
|
37 |
data_file_output=gr.File(label="Output data file (.csv)")
|
38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
with gr.Row():
|
40 |
greet_btn = gr.Button("Process")
|
41 |
-
greet_btn.click(fn=process, inputs=[cellid_input, image_input, path_input], outputs=[trace_output, image_output, plot_output, data_output, data_file_output], api_name="process")
|
42 |
|
43 |
|
44 |
if __name__ == "__main__":
|
|
|
2 |
import gradio as gr
|
3 |
from tifffile import imread
|
4 |
from PIL import Image
|
5 |
+
from path_analysis.analyse import analyse_paths
|
|
|
6 |
import numpy as np
|
7 |
|
|
|
|
|
|
|
|
|
8 |
|
9 |
+
# Function to preview the imported image
|
10 |
def preview_image(file1):
|
11 |
if file1:
|
12 |
im = imread(file1.name)
|
13 |
+
print(im.ndim, im.shape)
|
14 |
+
if im.ndim>2:
|
15 |
+
return Image.fromarray(np.max(im, axis=0))
|
16 |
+
else:
|
17 |
+
return Image.fromarray(im)
|
18 |
else:
|
19 |
return None
|
20 |
|
|
|
22 |
with gr.Blocks() as demo:
|
23 |
with gr.Row():
|
24 |
with gr.Column():
|
25 |
+
# Inputs for cell ID, image, and path
|
26 |
cellid_input = gr.Textbox(label="Cell ID", placeholder="Image_1")
|
27 |
image_input = gr.File(label="Input foci image")
|
28 |
image_preview = gr.Image(label="Max projection of foci image")
|
29 |
image_input.change(fn=preview_image, inputs=image_input, outputs=image_preview)
|
30 |
path_input = gr.File(label="SNT traces file")
|
31 |
|
32 |
+
# Additional options wrapped in an accordion for better UI experience
|
33 |
+
with gr.Accordion("Additional options ..."):
|
34 |
+
sphere_radius = gr.Number(label="Trace sphere radius (um)", value=0.1984125, interactive=True)
|
35 |
+
peak_threshold = gr.Number(label="Peak relative threshold", value=0.4, interactive=True)
|
36 |
+
# Resolutions for xy and z axis
|
37 |
+
with gr.Row():
|
38 |
+
xy_res = gr.Number(label='xy-yesolution (um)', value=0.0396825, interactive=True)
|
39 |
+
z_res = gr.Number(label='z resolution (um)', value=0.0909184, interactive=True)
|
40 |
+
# Resolutions for xy and z axis
|
41 |
+
|
42 |
+
threshold_type = gr.Radio(["per-trace", "per-cell"], label="Threshold-type", value="per-trace", interactive=True)
|
43 |
+
|
44 |
+
|
45 |
+
# The output column showing the result of processing
|
46 |
with gr.Column():
|
47 |
trace_output = gr.Image(label="Overlayed paths")
|
48 |
image_output=gr.Gallery(label="Traced paths")
|
|
|
50 |
data_output=gr.DataFrame(label="Detected peak data")#, "Peak 1 pos", "Peak 1 int"])
|
51 |
data_file_output=gr.File(label="Output data file (.csv)")
|
52 |
|
53 |
+
|
54 |
+
def process(cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type):
|
55 |
+
|
56 |
+
config = { 'sphere_radius': sphere_radius,
|
57 |
+
'peak_threshold': peak_threshold,
|
58 |
+
'xy_res': xy_res,
|
59 |
+
'z_res': z_res,
|
60 |
+
'threshold_type': threshold_type }
|
61 |
+
|
62 |
+
|
63 |
+
paths, traces, fig, extracted_peaks = analyse_paths(cellid_input, image_input.name, path_input.name, config)
|
64 |
+
extracted_peaks.to_csv('output.csv')
|
65 |
+
print('extracted', extracted_peaks)
|
66 |
+
return paths, [Image.fromarray(im) for im in traces], fig, extracted_peaks, 'output.csv'
|
67 |
+
|
68 |
+
|
69 |
with gr.Row():
|
70 |
greet_btn = gr.Button("Process")
|
71 |
+
greet_btn.click(fn=process, inputs=[cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type], outputs=[trace_output, image_output, plot_output, data_output, data_file_output], api_name="process")
|
72 |
|
73 |
|
74 |
if __name__ == "__main__":
|
path_analysis/__init__.py
ADDED
File without changes
|
path_analysis/analyse.py
ADDED
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import lxml.etree as ET
|
3 |
+
import gzip
|
4 |
+
import tifffile
|
5 |
+
import matplotlib.pyplot as plt
|
6 |
+
import numpy as np
|
7 |
+
from PIL import Image, ImageDraw
|
8 |
+
import pandas as pd
|
9 |
+
from itertools import cycle
|
10 |
+
from .data_preprocess import analyse_traces
|
11 |
+
import math
|
12 |
+
import scipy.linalg as la
|
13 |
+
|
14 |
+
|
15 |
+
def get_paths_from_traces_file(traces_file):
|
16 |
+
"""
|
17 |
+
Parses the specified traces file and extracts paths and their lengths.
|
18 |
+
|
19 |
+
Args:
|
20 |
+
traces_file (str): Path to the XML traces file.
|
21 |
+
|
22 |
+
Returns:
|
23 |
+
tuple: A tuple containing a list of paths (each path is a list of tuples representing points)
|
24 |
+
and a list of corresponding path lengths.
|
25 |
+
"""
|
26 |
+
tree = ET.parse(traces_file)
|
27 |
+
root = tree.getroot()
|
28 |
+
all_paths = []
|
29 |
+
path_lengths = []
|
30 |
+
for path in root.findall('path'):
|
31 |
+
length=path.get('reallength')
|
32 |
+
path_points = []
|
33 |
+
for point in path:
|
34 |
+
path_points.append((int(point.get('x')), int(point.get('y')), int(point.get('z'))))
|
35 |
+
all_paths.append(path_points)
|
36 |
+
path_lengths.append(float(length))
|
37 |
+
return all_paths, path_lengths
|
38 |
+
|
39 |
+
def calculate_path_length(point_list, voxel_size=(1,1,1)):
|
40 |
+
# Simple calculation
|
41 |
+
l = 0
|
42 |
+
s = np.array(voxel_size)
|
43 |
+
for i in range(len(point_list)-1):
|
44 |
+
l += la.norm(s * (np.array(point_list[i+1]) - np.array(point_list[i])))
|
45 |
+
return l
|
46 |
+
|
47 |
+
|
48 |
+
def calculate_path_length_partials(point_list, voxel_size=(1,1,1)):
|
49 |
+
# Simple calculation
|
50 |
+
l = [0.0]
|
51 |
+
s = np.array(voxel_size)
|
52 |
+
for i in range(len(point_list)-1):
|
53 |
+
l.append(la.norm(s * (np.array(point_list[i+1]) - np.array(point_list[i]))))
|
54 |
+
return np.cumsum(l)
|
55 |
+
|
56 |
+
|
57 |
+
def visualise_ordering(points_list, dim, wr=5, wc=5):
|
58 |
+
"""
|
59 |
+
Visualize the ordering of points in an image.
|
60 |
+
|
61 |
+
Args:
|
62 |
+
points_list (list): List of points to be visualized.
|
63 |
+
dim (tuple): Dimensions of the image (rows, columns, channels).
|
64 |
+
wr (int, optional): Width of the region to visualize around the point in the row direction. Defaults to 5.
|
65 |
+
wc (int, optional): Width of the region to visualize around the point in the column direction. Defaults to 5.
|
66 |
+
|
67 |
+
Returns:
|
68 |
+
np.array: An image array with visualized points.
|
69 |
+
"""
|
70 |
+
# Visualizes the ordering of the points in the list on a blank image.
|
71 |
+
rdim, cdim, _ = dim
|
72 |
+
vis = np.zeros((rdim, cdim, 3), dtype=np.uint8)
|
73 |
+
|
74 |
+
def get_col(i):
|
75 |
+
r = int(255 * i/len(points_list))
|
76 |
+
g = 255 - r
|
77 |
+
return r, g, 0
|
78 |
+
|
79 |
+
for n, p in enumerate(points_list):
|
80 |
+
c, r, _ = map(int, p)
|
81 |
+
vis[max(0,r-wr):min(rdim,r+wr+1),max(0,c-wc):min(cdim,c+wc+1)] = get_col(n)
|
82 |
+
|
83 |
+
return vis
|
84 |
+
|
85 |
+
# A color map for paths
|
86 |
+
col_map = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255), (0,255,255),
|
87 |
+
(255,127,0), (255, 0, 127), (127, 255, 0), (0, 255, 127), (127,0,255), (0,127,255)]
|
88 |
+
|
89 |
+
def draw_paths(all_paths, foci_stack, foci_index=None, r=3):
|
90 |
+
"""
|
91 |
+
Draws paths on the provided image stack and overlays markers for the foci
|
92 |
+
|
93 |
+
Args:
|
94 |
+
all_paths (list): List of paths where each path is a list of points.
|
95 |
+
foci_stack (np.array): 3D numpy array representing the image stack.
|
96 |
+
foci_index (list, optional): List of list of focus indices (along each path). Defaults to None.
|
97 |
+
r (int, optional): Radius for the ellipse or line drawing around the focus. Defaults to 3.
|
98 |
+
|
99 |
+
Returns:
|
100 |
+
PIL.Image.Image: An image with the drawn paths.
|
101 |
+
"""
|
102 |
+
im = np.max(foci_stack, axis=0)
|
103 |
+
im = (im/np.max(im)*255).astype(np.uint8)
|
104 |
+
im = np.dstack((im,)*3)
|
105 |
+
im = Image.fromarray(im)
|
106 |
+
draw = ImageDraw.Draw(im)
|
107 |
+
for i, (p, col) in enumerate(zip(all_paths, cycle(col_map))):
|
108 |
+
draw.line([(u[0], u[1]) for u in p], fill=col)
|
109 |
+
draw.text((p[0][0], p[0][1]), str(i+1), fill=col)
|
110 |
+
if foci_index is not None:
|
111 |
+
for i, (idx, p, col) in enumerate(zip(foci_index, all_paths, cycle(col_map))):
|
112 |
+
if len(idx):
|
113 |
+
for j in idx:
|
114 |
+
draw.line((int(p[j][0]-r), int(p[j][1]), int(p[j][0]+r), int(p[j][1])), fill=col, width=2)
|
115 |
+
draw.line((int(p[j][0]), int(p[j][1]-r), int(p[j][0]), int(p[j][1]+r)), fill=col, width=2)
|
116 |
+
|
117 |
+
return im
|
118 |
+
|
119 |
+
|
120 |
+
def measure_from_mask(mask, measure_stack):
|
121 |
+
"""
|
122 |
+
Compute the sum of measure_stack values where the mask is equal to 1.
|
123 |
+
|
124 |
+
Args:
|
125 |
+
mask (numpy.ndarray): Binary mask where the measurement should be applied.
|
126 |
+
measure_stack (numpy.ndarray): Stack of measurements.
|
127 |
+
|
128 |
+
Returns:
|
129 |
+
measure_stack.dtype: Sum of measure_stack values where the mask is 1.
|
130 |
+
"""
|
131 |
+
return np.sum(mask * measure_stack)
|
132 |
+
|
133 |
+
# Max of measure_stack over region where mask==1
|
134 |
+
def max_from_mask(mask, measure_stack):
|
135 |
+
"""
|
136 |
+
Compute the maximum of measure_stack values where the mask is equal to 1.
|
137 |
+
|
138 |
+
Args:
|
139 |
+
mask (numpy.ndarray): Binary mask where the measurement should be applied.
|
140 |
+
measure_stack (numpy.ndarray): Stack of measurements.
|
141 |
+
|
142 |
+
Returns:
|
143 |
+
measure_stack.dtype: Maximum value of measure_stack where the mask is 1.
|
144 |
+
"""
|
145 |
+
return np.max(mask * measure_stack)
|
146 |
+
|
147 |
+
def make_mask_s(p, melem, measure_stack):
|
148 |
+
"""
|
149 |
+
Translate a mask to point p, ensuring correct treatment near the edges of the measure_stack.
|
150 |
+
|
151 |
+
Args:
|
152 |
+
p (tuple): Target point (r, c, z).
|
153 |
+
melem (numpy.ndarray): Structuring element for the mask.
|
154 |
+
measure_stack (numpy.ndarray): Stack of measurements.
|
155 |
+
|
156 |
+
Returns:
|
157 |
+
tuple: A tuple containing the translated mask and a section of the measure_stack.
|
158 |
+
"""
|
159 |
+
|
160 |
+
|
161 |
+
#
|
162 |
+
|
163 |
+
R = [u//2 for u in melem.shape]
|
164 |
+
|
165 |
+
|
166 |
+
r, c, z = p
|
167 |
+
|
168 |
+
mask = np.zeros(melem.shape)
|
169 |
+
|
170 |
+
m_data = np.zeros(melem.shape)
|
171 |
+
s = measure_stack.shape
|
172 |
+
o_1, o_2, o_3 = max(R[0]-r, 0), max(R[1]-c, 0), max(R[2]-z,0)
|
173 |
+
e_1, e_2, e_3 = min(R[0]-r+s[0], 2*R[0]+1), min(R[1]-c+s[1], 2*R[1]+1), min(R[2]-z+s[2], 2*R[2]+1)
|
174 |
+
m_data[o_1:e_1,o_2:e_2,o_3:e_3] = measure_stack[max(r-R[0],0):min(r+R[0]+1,s[0]),max(c-R[1],0):min(c+R[1]+1,s[1]),max(z-R[2],0):min(z+R[2]+1, s[2])]
|
175 |
+
mask[o_1:e_1,o_2:e_2,o_3:e_3] = melem[o_1:e_1,o_2:e_2,o_3:e_3]
|
176 |
+
|
177 |
+
|
178 |
+
return mask, m_data
|
179 |
+
|
180 |
+
|
181 |
+
def measure_at_point(p, melem, measure_stack, op='mean'):
|
182 |
+
"""
|
183 |
+
Measure the mean or max value of measure_stack around a specific point using a structuring element.
|
184 |
+
|
185 |
+
Args:
|
186 |
+
p (tuple): Target point (r, c, z).
|
187 |
+
melem (numpy.ndarray): Structuring element for the mask.
|
188 |
+
measure_stack (numpy.ndarray): Stack of measurements.
|
189 |
+
op (str, optional): Operation to be applied; either 'mean' or 'max'. Default is 'mean'.
|
190 |
+
|
191 |
+
Returns:
|
192 |
+
float: Measured value based on the specified operation.
|
193 |
+
"""
|
194 |
+
|
195 |
+
p = map(int, p)
|
196 |
+
if op=='mean':
|
197 |
+
mask, m_data = make_mask_s(p, melem, measure_stack)
|
198 |
+
melem_size = np.sum(mask)
|
199 |
+
return float(measure_from_mask(mask, m_data) / melem_size)
|
200 |
+
else:
|
201 |
+
mask, m_data = make_mask_s(p, melem, measure_stack)
|
202 |
+
return float(max_from_mask(mask, m_data))
|
203 |
+
|
204 |
+
# Generate spherical region
|
205 |
+
def make_sphere(R=5, z_scale_ratio=2.3):
|
206 |
+
"""
|
207 |
+
Generate a binary representation of a sphere in 3D space.
|
208 |
+
|
209 |
+
Args:
|
210 |
+
R (int, optional): Radius of the sphere. Default is 5.
|
211 |
+
z_scale_ratio (float, optional): Scaling factor for the z-axis. Default is 2.3.
|
212 |
+
|
213 |
+
Returns:
|
214 |
+
numpy.ndarray: Binary representation of the sphere.
|
215 |
+
"""
|
216 |
+
R_z = int(math.ceil(R/z_scale_ratio))
|
217 |
+
x, y, z = np.ogrid[-R:R+1, -R:R+1, -R_z:R_z+1]
|
218 |
+
sphere = x**2 + y**2 + (z_scale_ratio * z)**2 <= R**2
|
219 |
+
return sphere
|
220 |
+
|
221 |
+
# Measure the values of measure_stack at each of the points of points_list in turn.
|
222 |
+
# Measurement is the mean / max (specified by op) on the spherical region about each point
|
223 |
+
def measure_all_with_sphere(points_list, measure_stack, op='mean', R=5, z_scale_ratio=2.3):
|
224 |
+
"""
|
225 |
+
Measure the values of measure_stack at each point in a list using a spherical region.
|
226 |
+
|
227 |
+
Args:
|
228 |
+
points_list (list): List of points (r, c, z) to be measured.
|
229 |
+
measure_stack (numpy.ndarray): Stack of measurements.
|
230 |
+
op (str, optional): Operation to be applied; either 'mean' or 'max'. Default is 'mean'.
|
231 |
+
R (int, optional): Radius of the sphere. Default is 5.
|
232 |
+
z_scale_ratio (float, optional): Scaling factor for the z-axis. Default is 2.3.
|
233 |
+
|
234 |
+
Returns:
|
235 |
+
list: List of measured values for each point.
|
236 |
+
"""
|
237 |
+
melem = make_sphere(R, z_scale_ratio)
|
238 |
+
measure_func = lambda p: measure_at_point(p, melem, measure_stack, op)
|
239 |
+
return list(map(measure_func, points_list))
|
240 |
+
|
241 |
+
|
242 |
+
# Measure fluorescence levels along ordered skeleton
|
243 |
+
def measure_chrom2(path, hei10, config):
|
244 |
+
"""
|
245 |
+
Measure fluorescence levels along an ordered skeleton.
|
246 |
+
|
247 |
+
Args:
|
248 |
+
path (list): List of ordered path points (r, c, z).
|
249 |
+
hei10 (numpy.ndarray): 3D fluorescence data.
|
250 |
+
config (dict): Configuration dictionary containing 'z_res', 'xy_res', and 'sphere_radius' values.
|
251 |
+
|
252 |
+
Returns:
|
253 |
+
tuple: A tuple containing the visualization, mean measurements, and max measurements along the path.
|
254 |
+
"""
|
255 |
+
scale_ratio = config['z_res']/config['xy_res']
|
256 |
+
sphere_xy_radius = int(math.ceil(config['sphere_radius']/config['xy_res']))
|
257 |
+
|
258 |
+
vis = visualise_ordering(path, dim=hei10.shape, wr=sphere_xy_radius, wc=sphere_xy_radius)
|
259 |
+
|
260 |
+
measurements = measure_all_with_sphere(path, hei10, op='mean', R=sphere_xy_radius, z_scale_ratio=scale_ratio)
|
261 |
+
measurements_max = measure_all_with_sphere(path, hei10, op='max', R=sphere_xy_radius, z_scale_ratio=scale_ratio)
|
262 |
+
|
263 |
+
return vis, measurements, measurements_max
|
264 |
+
|
265 |
+
def extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config):
|
266 |
+
"""
|
267 |
+
Extract peak information from given traces and compile them into a DataFrame.
|
268 |
+
|
269 |
+
Args:
|
270 |
+
- cell_id (int or str): Identifier for the cell being analyzed.
|
271 |
+
- all_paths (list of lists): Contains ordered path points for multiple paths.
|
272 |
+
- path_lengths (list of floats): List containing lengths of each path in all_paths.
|
273 |
+
- measured_traces (list of lists): Contains fluorescence measurement values along the paths.
|
274 |
+
- config (dict): Configuration dictionary containing:
|
275 |
+
- 'peak_threshold': Threshold value to determine a peak in the trace.
|
276 |
+
- 'sphere_radius': Radius of the sphere used in fluorescence measurement.
|
277 |
+
|
278 |
+
Returns:
|
279 |
+
- pd.DataFrame: DataFrame containing peak information for each path.
|
280 |
+
- list of lists: Absolute intensities of the detected foci.
|
281 |
+
- list of lists: Index positions of the detected foci.
|
282 |
+
- list of lists: Absolute focus intensity threshold for each trace.
|
283 |
+
- list of numpy.ndarray: For each trace, distances of each point from start of trace in microns
|
284 |
+
"""
|
285 |
+
|
286 |
+
n_paths = len(all_paths)
|
287 |
+
|
288 |
+
data = []
|
289 |
+
foci_absolute_intensity, foci_position, foci_position_index, trace_median_intensities, trace_thresholds = analyse_traces(all_paths, path_lengths, measured_traces, config)
|
290 |
+
|
291 |
+
total_intensity = sum(sum(path_foci_abs_int - tmi) for path_foci_abs_int, tmi in zip(foci_absolute_intensity, trace_median_intensities))
|
292 |
+
trace_positions = []
|
293 |
+
|
294 |
+
for i in range(n_paths):
|
295 |
+
|
296 |
+
pl = calculate_path_length_partials(all_paths[i], (config['xy_res'], config['xy_res'], config['z_res']))
|
297 |
+
|
298 |
+
path_data = { 'Cell_ID':cell_id,
|
299 |
+
'Trace': i+1,
|
300 |
+
'SNT_trace_length(um)': path_lengths[i],
|
301 |
+
'Measured_trace_length(um)': pl[-1],
|
302 |
+
'Trace_median_intensity': trace_median_intensities[i],
|
303 |
+
'Detection_sphere_radius(um)': config['sphere_radius'],
|
304 |
+
'Foci_ID_threshold': config['peak_threshold'] }
|
305 |
+
for j, (idx, u,v) in enumerate(zip(foci_position_index[i], foci_position[i], foci_absolute_intensity[i])):
|
306 |
+
if config['use_corrected_positions']:
|
307 |
+
path_data[f'Foci_{j+1}_position(um)'] = pl[idx]
|
308 |
+
else:
|
309 |
+
path_data[f'Foci_{j+1}_position(um)'] = u
|
310 |
+
path_data[f'Foci_{j+1}_absolute_intensity'] = v
|
311 |
+
path_data[f'Foci_{j+1}_relative_intensity'] = (v - trace_median_intensities[i])/total_intensity
|
312 |
+
data.append(path_data)
|
313 |
+
trace_positions.append(pl)
|
314 |
+
return pd.DataFrame(data), foci_absolute_intensity, foci_position_index, trace_thresholds, trace_positions
|
315 |
+
|
316 |
+
|
317 |
+
def analyse_paths(cell_id,
|
318 |
+
foci_file,
|
319 |
+
traces_file,
|
320 |
+
config
|
321 |
+
):
|
322 |
+
"""
|
323 |
+
Analyzes paths for the given cell ID using provided foci and trace files.
|
324 |
+
|
325 |
+
Args:
|
326 |
+
cell_id (int/str): Identifier for the cell.
|
327 |
+
foci_file (str): Path to the foci image file.
|
328 |
+
traces_file (str): Path to the XML traces file.
|
329 |
+
config (dict): Configuration dictionary containing necessary parameters such as resolutions and thresholds.
|
330 |
+
|
331 |
+
Returns:
|
332 |
+
tuple: A tuple containing an overlay image of the traces, visualization images for each trace,
|
333 |
+
a figure with plotted measurements, and a dataframe with extracted peaks.
|
334 |
+
"""
|
335 |
+
|
336 |
+
|
337 |
+
foci_stack = tifffile.imread(foci_file)
|
338 |
+
|
339 |
+
if foci_stack.ndim==2:
|
340 |
+
foci_stack = foci_stack[None,:,:]
|
341 |
+
|
342 |
+
all_paths, path_lengths = get_paths_from_traces_file(traces_file)
|
343 |
+
|
344 |
+
all_trace_vis = []
|
345 |
+
all_m = []
|
346 |
+
for p in all_paths:
|
347 |
+
vis, m, _ = measure_chrom2(p,foci_stack.transpose(2,1,0), config)
|
348 |
+
all_trace_vis.append(vis)
|
349 |
+
all_m.append(m)
|
350 |
+
|
351 |
+
|
352 |
+
extracted_peaks, foci_absolute_intensity, foci_pos_index, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, all_m, config)
|
353 |
+
|
354 |
+
|
355 |
+
n_cols = 2
|
356 |
+
n_rows = (len(all_paths)+n_cols-1)//n_cols
|
357 |
+
fig, ax = plt.subplots(n_rows,n_cols)
|
358 |
+
ax = ax.flatten()
|
359 |
+
|
360 |
+
for i, m in enumerate(all_m):
|
361 |
+
ax[i].set_title(f'Trace {i+1}')
|
362 |
+
ax[i].plot(trace_positions[i], m)
|
363 |
+
print(foci_pos_index)
|
364 |
+
if len(foci_pos_index[i]):
|
365 |
+
ax[i].plot(trace_positions[i][foci_pos_index[i]], np.array(m)[foci_pos_index[i]], 'rx')
|
366 |
+
ax[i].set_xlabel('Distance from start (um)')
|
367 |
+
ax[i].set_ylabel('Intensity')
|
368 |
+
ax[i].axhline(trace_thresholds[i], c='r', ls=':')
|
369 |
+
for i in range(len(all_m), n_cols*n_rows):
|
370 |
+
ax[i].axis('off')
|
371 |
+
|
372 |
+
plt.tight_layout()
|
373 |
+
trace_overlay = draw_paths(all_paths, foci_stack, foci_index=foci_pos_index)
|
374 |
+
|
375 |
+
return trace_overlay, all_trace_vis, fig, extracted_peaks
|
path_analysis/data_preprocess.py
ADDED
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
|
3 |
+
from dataclasses import dataclass
|
4 |
+
import numpy as np
|
5 |
+
import scipy.linalg as la
|
6 |
+
from scipy.signal import find_peaks
|
7 |
+
from math import ceil
|
8 |
+
|
9 |
+
|
10 |
+
|
11 |
+
|
12 |
+
def thin_points(point_list, dmin=10, voxel_size=(1,1,1)):
|
13 |
+
"""
|
14 |
+
Remove points within a specified distance of each other, retaining the point with the highest intensity.
|
15 |
+
|
16 |
+
Args:
|
17 |
+
- point_list (list of tuples): Each tuple contains:
|
18 |
+
- x (list of float): 3D coordinates of the point.
|
19 |
+
- intensity (float): The intensity value of the point.
|
20 |
+
- idx (int): A unique identifier or index for the point.
|
21 |
+
- dmin (float, optional): Minimum distance between points. Points closer than this threshold will be thinned. Defaults to 10.
|
22 |
+
|
23 |
+
Returns:
|
24 |
+
- list of int: A list containing indices of the removed points.
|
25 |
+
|
26 |
+
Notes:
|
27 |
+
- The function uses the L2 norm (Euclidean distance) to compute the distance between points.
|
28 |
+
- When two points are within `dmin` distance, the point with the lower intensity is removed.
|
29 |
+
"""
|
30 |
+
removed_points = []
|
31 |
+
for i in range(len(point_list)):
|
32 |
+
if point_list[i][2] in removed_points:
|
33 |
+
continue
|
34 |
+
for j in range(len(point_list)):
|
35 |
+
if i==j:
|
36 |
+
continue
|
37 |
+
if point_list[j][2] in removed_points:
|
38 |
+
continue
|
39 |
+
d = (np.array(point_list[i][0]) - np.array(point_list[j][0]))*np.array(voxel_size)
|
40 |
+
d = la.norm(d)
|
41 |
+
if d<dmin:
|
42 |
+
hi = point_list[i][1]
|
43 |
+
hj = point_list[j][1]
|
44 |
+
if hi<hj:
|
45 |
+
removed_points.append(point_list[i][2])
|
46 |
+
break
|
47 |
+
else:
|
48 |
+
removed_points.append(point_list[j][2])
|
49 |
+
|
50 |
+
return removed_points
|
51 |
+
|
52 |
+
|
53 |
+
@dataclass
|
54 |
+
class CellData(object):
|
55 |
+
"""Represents data related to a single cell.
|
56 |
+
|
57 |
+
Attributes:
|
58 |
+
pathdata_list (list): A list of PathData objects representing the various paths associated with the cell.
|
59 |
+
"""
|
60 |
+
pathdata_list: list
|
61 |
+
|
62 |
+
@dataclass
|
63 |
+
class PathData(object):
|
64 |
+
"""Represents data related to a specific path in the cell.
|
65 |
+
|
66 |
+
This dataclass encapsulates information about the peaks,
|
67 |
+
the defining points, the fluorescence values, and the path length of a specific path.
|
68 |
+
|
69 |
+
Attributes: peaks (list): List of peaks in the path (indicies of positions in points, o_hei10).
|
70 |
+
points (list): List of points defining the path.
|
71 |
+
o_hei10 (list): List of (unnormalized) fluorescence intensity values along the path
|
72 |
+
SC_length (float): Length of the path.
|
73 |
+
|
74 |
+
"""
|
75 |
+
peaks: list
|
76 |
+
points: list
|
77 |
+
o_hei10: list
|
78 |
+
SC_length: float
|
79 |
+
|
80 |
+
|
81 |
+
|
82 |
+
def find_peaks2(v, distance=5, prominence=0.5):
|
83 |
+
"""
|
84 |
+
Find peaks in a 1D array with extended boundary handling.
|
85 |
+
|
86 |
+
The function pads the input array at both ends to handle boundary peaks. It then identifies peaks in the extended array
|
87 |
+
and maps them back to the original input array.
|
88 |
+
|
89 |
+
Args:
|
90 |
+
- v (numpy.ndarray): 1D input array in which to find peaks.
|
91 |
+
- distance (int, optional): Minimum number of array elements that separate two peaks. Defaults to 5.
|
92 |
+
- prominence (float, optional): Minimum prominence required for a peak to be identified. Defaults to 0.5.
|
93 |
+
|
94 |
+
Returns:
|
95 |
+
- list of int: List containing the indices of the identified peaks in the original input array.
|
96 |
+
- dict: Information about the properties of the identified peaks (as returned by scipy.signal.find_peaks).
|
97 |
+
|
98 |
+
"""
|
99 |
+
pad = int(ceil(distance))+1
|
100 |
+
v_ext = np.concatenate([np.ones((pad,), dtype=v.dtype)*np.min(v), v, np.ones((pad,), dtype=v.dtype)*np.min(v)])
|
101 |
+
|
102 |
+
assert(len(v_ext) == len(v)+2*pad)
|
103 |
+
peaks, _ = find_peaks(v_ext, distance=distance, prominence=prominence)
|
104 |
+
peaks = peaks - pad
|
105 |
+
n_peaks = []
|
106 |
+
for i in peaks:
|
107 |
+
if 0<=i<len(v):
|
108 |
+
n_peaks.append(i)
|
109 |
+
else:
|
110 |
+
raise Exception
|
111 |
+
return n_peaks, _
|
112 |
+
|
113 |
+
|
114 |
+
def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
115 |
+
"""
|
116 |
+
Process traces of cells to extract peak information and organize the data.
|
117 |
+
|
118 |
+
The function normalizes fluorescence data, finds peaks, refines peak information,
|
119 |
+
removes unwanted peaks that might be due to close proximity of bright peaks from
|
120 |
+
other paths, and organizes all the information into a structured data format.
|
121 |
+
|
122 |
+
Args:
|
123 |
+
all_paths (list of list of tuples): A list containing paths, where each path is
|
124 |
+
represented as a list of 3D coordinate tuples.
|
125 |
+
path_lengths (list of float): List of path lengths corresponding to the provided paths.
|
126 |
+
measured_trace_fluorescence (list of list of float): A list containing fluorescence
|
127 |
+
data corresponding to each path point.
|
128 |
+
|
129 |
+
Returns:
|
130 |
+
CellData: An object containing organized peak and path data for a given cell.
|
131 |
+
|
132 |
+
Note:
|
133 |
+
- The function assumes that each path and its corresponding length and fluorescence data
|
134 |
+
are positioned at the same index in their respective lists.
|
135 |
+
"""
|
136 |
+
|
137 |
+
cell_peaks = []
|
138 |
+
|
139 |
+
for points, path_length, o_hei10 in zip(all_paths, path_lengths, measured_trace_fluorescence):
|
140 |
+
|
141 |
+
# For peak determination normalize each trace to have mean zero and s.d. 1
|
142 |
+
hei10_normalized = (o_hei10 - np.mean(o_hei10))/np.std(o_hei10)
|
143 |
+
|
144 |
+
# Find peaks - these will be further refined later
|
145 |
+
p,_ = find_peaks2(hei10_normalized, distance=5, prominence=0.5*np.std(hei10_normalized))
|
146 |
+
peaks = np.array(p, dtype=np.int32)
|
147 |
+
|
148 |
+
# Store peak data - using original values, not normalized ones
|
149 |
+
peak_mean_heights = [ o_hei10[u] for u in peaks ]
|
150 |
+
peak_points = [ points[u] for u in peaks ]
|
151 |
+
|
152 |
+
cell_peaks.append((peaks, peak_points, peak_mean_heights))
|
153 |
+
|
154 |
+
# Eliminate peaks which have another larger peak nearby (in 3D space, on any chromosome).
|
155 |
+
# This aims to remove small peaks in the mean intensity generated when an SC passes close
|
156 |
+
# to a bright peak on another SC - this is nearby in space, but brighter.
|
157 |
+
|
158 |
+
to_thin = []
|
159 |
+
for k in range(len(cell_peaks)):
|
160 |
+
for u in range(len(cell_peaks[k][0])):
|
161 |
+
to_thin.append((cell_peaks[k][1][u], cell_peaks[k][2][u], (k, u)))
|
162 |
+
|
163 |
+
# Exclude any peak with a nearby brighter peak (on any SC)
|
164 |
+
removed_points = thin_points(to_thin)
|
165 |
+
|
166 |
+
|
167 |
+
# Clean up and remove these peaks
|
168 |
+
new_cell_peaks = []
|
169 |
+
for k in range(len(cell_peaks)):
|
170 |
+
cc = []
|
171 |
+
pp = cell_peaks[k][0]
|
172 |
+
for u in range(len(pp)):
|
173 |
+
if (k,u) not in removed_points:
|
174 |
+
cc.append(pp[u])
|
175 |
+
new_cell_peaks.append(cc)
|
176 |
+
|
177 |
+
cell_peaks = new_cell_peaks
|
178 |
+
|
179 |
+
pd_list = []
|
180 |
+
|
181 |
+
# Save peak positions, absolute HEI10 intensities, and length for each SC
|
182 |
+
for k in range(len(all_paths)):
|
183 |
+
|
184 |
+
points, o_hei10 = all_paths[k], measured_trace_fluorescence[k]
|
185 |
+
|
186 |
+
peaks = cell_peaks[k]
|
187 |
+
|
188 |
+
pd = PathData(peaks=peaks, points=points, o_hei10=o_hei10, SC_length=path_lengths[k])
|
189 |
+
pd_list.append(pd)
|
190 |
+
|
191 |
+
cd = CellData(pathdata_list=pd_list)
|
192 |
+
|
193 |
+
return cd
|
194 |
+
|
195 |
+
|
196 |
+
alpha_max = 0.4
|
197 |
+
|
198 |
+
|
199 |
+
# Criterion used for identifying peak as a CO - normalized (with mean and s.d.)
|
200 |
+
# hei10 levels being above 0.4 time maximum peak level
|
201 |
+
def pc(pos, v, alpha=alpha_max):
|
202 |
+
"""
|
203 |
+
Identify and return positions where values in the array `v` exceed a certain threshold.
|
204 |
+
|
205 |
+
The threshold is computed as `alpha` times the maximum value in `v`.
|
206 |
+
|
207 |
+
Args:
|
208 |
+
- pos (numpy.ndarray): Array of positions.
|
209 |
+
- v (numpy.ndarray): 1D array of values, e.g., intensities.
|
210 |
+
- alpha (float, optional): A scaling factor for the threshold. Defaults to `alpha_max`.
|
211 |
+
|
212 |
+
Returns:
|
213 |
+
- numpy.ndarray: Array of positions where corresponding values in `v` exceed the threshold.
|
214 |
+
"""
|
215 |
+
idx = (v>=alpha*np.max(v))
|
216 |
+
return np.array(pos[idx])
|
217 |
+
|
218 |
+
def analyse_celldata(cell_data, config):
|
219 |
+
"""
|
220 |
+
Analyse the provided cell data to extract focus-related information.
|
221 |
+
|
222 |
+
Args:
|
223 |
+
cd (CellData): An instance of the CellData class containing path data information.
|
224 |
+
config (dictionary): Configuration dictionary containing 'peak_threshold' and 'threshold_type'
|
225 |
+
'peak_threshold' (float) - threshold for calling peaks as foci
|
226 |
+
'threshold_type' (str) = 'per-trace', 'per-foci'
|
227 |
+
|
228 |
+
Returns:
|
229 |
+
tuple: A tuple containing three lists:
|
230 |
+
- foci_rel_intensity (list): List of relative intensities for the detected foci.
|
231 |
+
- foci_pos (list): List of absolute positions of the detected foci.
|
232 |
+
- foci_pos_index (list): List of indices of the detected foci.
|
233 |
+
"""
|
234 |
+
foci_abs_intensity = []
|
235 |
+
foci_pos = []
|
236 |
+
foci_pos_index = []
|
237 |
+
trace_median_intensities = []
|
238 |
+
trace_thresholds = []
|
239 |
+
|
240 |
+
peak_threshold = config['peak_threshold']
|
241 |
+
|
242 |
+
threshold_type = config['threshold_type']
|
243 |
+
|
244 |
+
if threshold_type == 'per-trace':
|
245 |
+
"""
|
246 |
+
Call extracted peaks as foci if intensity - trace_mean > peak_threshold * (trace_max_foci_intensity - trace_mean)
|
247 |
+
"""
|
248 |
+
|
249 |
+
for path_data in cell_data.pathdata_list:
|
250 |
+
peaks = np.array(path_data.peaks, dtype=np.int32)
|
251 |
+
|
252 |
+
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
253 |
+
# by standard deviation - note that the latter should have no effect on the results).
|
254 |
+
h = np.array(path_data.o_hei10)
|
255 |
+
h = h - np.mean(h)
|
256 |
+
h = h/np.std(h)
|
257 |
+
# Extract peaks according to criterion
|
258 |
+
sig_peak_idx = pc(peaks, h[peaks], peak_threshold)
|
259 |
+
trace_thresholds.append((1-peak_threshold)*np.mean(path_data.o_hei10) + peak_threshold*np.max(np.array(path_data.o_hei10)[peaks]))
|
260 |
+
|
261 |
+
pos_abs = (sig_peak_idx/len(path_data.points))*path_data.SC_length
|
262 |
+
foci_pos.append(pos_abs)
|
263 |
+
foci_abs_intensity.append(np.array(path_data.o_hei10)[sig_peak_idx])
|
264 |
+
|
265 |
+
foci_pos_index.append(sig_peak_idx)
|
266 |
+
trace_median_intensities.append(np.median(path_data.o_hei10))
|
267 |
+
|
268 |
+
elif threshold_type == 'per-cell':
|
269 |
+
"""
|
270 |
+
Call extracted peaks as foci if intensity - trace_mean > peak_threshold * max(intensity - trace_mean)
|
271 |
+
"""
|
272 |
+
max_cell_intensity = float("-inf")
|
273 |
+
for path_data in cell_data.pathdata_list:
|
274 |
+
|
275 |
+
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
276 |
+
# by standard deviation - note that the latter should have no effect on the results).
|
277 |
+
h = np.array(path_data.o_hei10)
|
278 |
+
h = h - np.mean(h)
|
279 |
+
max_cell_intensity = max(max_cell_intensity, np.max(h))
|
280 |
+
|
281 |
+
for path_data in cell_data.pathdata_list:
|
282 |
+
peaks = np.array(path_data.peaks, dtype=np.int32)
|
283 |
+
|
284 |
+
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
285 |
+
# by standard deviation - note that the latter should have no effect on the results).
|
286 |
+
h = np.array(path_data.o_hei10)
|
287 |
+
h = h - np.mean(h)
|
288 |
+
|
289 |
+
sig_peak_idx = peaks[h[peaks]>peak_threshold*max_cell_intensity]
|
290 |
+
|
291 |
+
trace_thresholds.append(np.mean(path_data.o_hei10) + peak_threshold*max_cell_intensity)
|
292 |
+
|
293 |
+
|
294 |
+
pos_abs = (sig_peak_idx/len(path_data.points))*path_data.SC_length
|
295 |
+
foci_pos.append(pos_abs)
|
296 |
+
foci_abs_intensity.append(np.array(path_data.o_hei10)[sig_peak_idx])
|
297 |
+
|
298 |
+
foci_pos_index.append(sig_peak_idx)
|
299 |
+
trace_median_intensities.append(np.median(path_data.o_hei10))
|
300 |
+
|
301 |
+
else:
|
302 |
+
raise NotImplementedError
|
303 |
+
|
304 |
+
return foci_abs_intensity, foci_pos, foci_pos_index, trace_median_intensities, trace_thresholds
|
305 |
+
|
306 |
+
def analyse_traces(all_paths, path_lengths, measured_trace_fluorescence, config):
|
307 |
+
|
308 |
+
cd = process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence)
|
309 |
+
|
310 |
+
return analyse_celldata(cd, config)
|
311 |
+
|
312 |
+
|
313 |
+
|
314 |
+
|
setup.py
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from setuptools import setup, find_packages
|
2 |
+
|
3 |
+
setup(
|
4 |
+
name='path_analysis',
|
5 |
+
version='0.1.0',
|
6 |
+
description='A brief description of your package',
|
7 |
+
author='Your Name',
|
8 |
+
author_email='[email protected]',
|
9 |
+
url='https://github.com/yourusername/yourrepository', # if you have a repo for the project
|
10 |
+
packages=find_packages(), # or specify manually: ['your_package', 'your_package.submodule', ...]
|
11 |
+
install_requires=[
|
12 |
+
'numpy', # for example, if your package needs numpy
|
13 |
+
'gradio',
|
14 |
+
# ... other dependencies
|
15 |
+
],
|
16 |
+
classifiers=[
|
17 |
+
'Development Status :: 3 - Alpha',
|
18 |
+
'Intended Audience :: Developers',
|
19 |
+
'Programming Language :: Python :: 3',
|
20 |
+
'Programming Language :: Python :: 3.6',
|
21 |
+
'Programming Language :: Python :: 3.7',
|
22 |
+
'Programming Language :: Python :: 3.8',
|
23 |
+
'Programming Language :: Python :: 3.9',
|
24 |
+
# ... other classifiers
|
25 |
+
],
|
26 |
+
python_requires='>=3.6', # your project's Python version requirement
|
27 |
+
keywords='some keywords related to your project',
|
28 |
+
# ... other parameters
|
29 |
+
)
|
tests/__init__.py
ADDED
File without changes
|
tests/test_analyse.py
ADDED
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
from path_analysis.analyse import *
|
3 |
+
import numpy as np
|
4 |
+
from math import pi
|
5 |
+
import xml.etree.ElementTree as ET
|
6 |
+
|
7 |
+
|
8 |
+
def test_get_paths_from_traces_file():
|
9 |
+
# Mock the XML traces file content
|
10 |
+
xml_content = '''<?xml version="1.0"?>
|
11 |
+
<root>
|
12 |
+
<path reallength="5.0">
|
13 |
+
<point x="1" y="2" z="3"/>
|
14 |
+
<point x="4" y="5" z="6"/>
|
15 |
+
</path>
|
16 |
+
<path reallength="10.0">
|
17 |
+
<point x="7" y="8" z="9"/>
|
18 |
+
<point x="10" y="11" z="12"/>
|
19 |
+
</path>
|
20 |
+
</root>
|
21 |
+
'''
|
22 |
+
|
23 |
+
# Create a temporary XML file
|
24 |
+
with open("temp_traces.xml", "w") as f:
|
25 |
+
f.write(xml_content)
|
26 |
+
|
27 |
+
all_paths, path_lengths = get_paths_from_traces_file("temp_traces.xml")
|
28 |
+
|
29 |
+
expected_paths = [[(1, 2, 3), (4, 5, 6)], [(7, 8, 9), (10, 11, 12)]]
|
30 |
+
expected_lengths = [5.0, 10.0]
|
31 |
+
|
32 |
+
assert all_paths == expected_paths, f"Expected paths {expected_paths}, but got {all_paths}"
|
33 |
+
assert path_lengths == expected_lengths, f"Expected lengths {expected_lengths}, but got {path_lengths}"
|
34 |
+
|
35 |
+
# Clean up temporary file
|
36 |
+
import os
|
37 |
+
os.remove("temp_traces.xml")
|
38 |
+
|
39 |
+
|
40 |
+
def test_measure_chrom2():
|
41 |
+
# Mock data
|
42 |
+
path = [(2, 3, 4), (4, 5, 6), (9, 9, 9)] # Sample ordered path points
|
43 |
+
hei10 = np.random.rand(10, 10, 10) # Random 3D fluorescence data
|
44 |
+
config = {
|
45 |
+
'z_res': 1,
|
46 |
+
'xy_res': 0.5,
|
47 |
+
'sphere_radius': 2.5
|
48 |
+
}
|
49 |
+
|
50 |
+
# Function call
|
51 |
+
_, measurements, measurements_max = measure_chrom2(path, hei10, config)
|
52 |
+
|
53 |
+
# Assertions
|
54 |
+
assert len(measurements) == len(path), "Measurements length should match path length"
|
55 |
+
assert len(measurements_max) == len(path), "Max measurements length should match path length"
|
56 |
+
assert all(0 <= val <= 1 for val in measurements), "All mean measurements should be between 0 and 1 for this mock data"
|
57 |
+
assert all(0 <= val <= 1 for val in measurements_max), "All max measurements should be between 0 and 1 for this mock data"
|
58 |
+
|
59 |
+
def test_measure_chrom2_z():
|
60 |
+
# Mock data
|
61 |
+
path = [(2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
62 |
+
_,_,hei10 = np.meshgrid(np.arange(10), np.arange(10), np.arange(10)) # 3D fluorescence data - z dependent
|
63 |
+
config = {
|
64 |
+
'z_res': 1,
|
65 |
+
'xy_res': 0.5,
|
66 |
+
'sphere_radius': 2.5
|
67 |
+
}
|
68 |
+
|
69 |
+
# Function call
|
70 |
+
_, measurements, measurements_max = measure_chrom2(path, hei10, config)
|
71 |
+
|
72 |
+
# Assertions
|
73 |
+
assert len(measurements) == len(path), "Measurements length should match path length"
|
74 |
+
assert len(measurements_max) == len(path), "Max measurements length should match path length"
|
75 |
+
assert all(measurements == np.array([4,6]))
|
76 |
+
assert all(measurements_max == np.array([6,8]))
|
77 |
+
|
78 |
+
def test_measure_chrom2_z2():
|
79 |
+
# Mock data
|
80 |
+
path = [(0,0,0), (2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
81 |
+
_,_,hei10 = np.meshgrid(np.arange(10), np.arange(10), np.arange(10)) # 3D fluorescence data - z dependent
|
82 |
+
config = {
|
83 |
+
'z_res': 0.25,
|
84 |
+
'xy_res': 0.5,
|
85 |
+
'sphere_radius': 2.5
|
86 |
+
}
|
87 |
+
|
88 |
+
# Function call
|
89 |
+
_, measurements, measurements_max = measure_chrom2(path, hei10, config)
|
90 |
+
|
91 |
+
# Assertions
|
92 |
+
assert len(measurements) == len(path), "Measurements length should match path length"
|
93 |
+
assert len(measurements_max) == len(path), "Max measurements length should match path length"
|
94 |
+
assert all(measurements_max == np.array([9,9,9]))
|
95 |
+
|
96 |
+
|
97 |
+
def test_measure_from_mask():
|
98 |
+
mask = np.array([
|
99 |
+
[0, 1, 0],
|
100 |
+
[1, 1, 1],
|
101 |
+
[0, 1, 0]
|
102 |
+
])
|
103 |
+
measure_stack = np.array([
|
104 |
+
[2, 4, 2],
|
105 |
+
[4, 8, 4],
|
106 |
+
[2, 4, 2]
|
107 |
+
])
|
108 |
+
result = measure_from_mask(mask, measure_stack)
|
109 |
+
assert result == 24 # Expected sum: 4+4+8+4+4
|
110 |
+
|
111 |
+
def test_max_from_mask():
|
112 |
+
mask = np.array([
|
113 |
+
[0, 1, 0],
|
114 |
+
[1, 1, 1],
|
115 |
+
[0, 1, 0]
|
116 |
+
])
|
117 |
+
measure_stack = np.array([
|
118 |
+
[2, 5, 2],
|
119 |
+
[4, 8, 3],
|
120 |
+
[2, 7, 2]
|
121 |
+
])
|
122 |
+
result = max_from_mask(mask, measure_stack)
|
123 |
+
assert result == 8 # Expected max: 8
|
124 |
+
|
125 |
+
|
126 |
+
def test_measure_at_point_mean():
|
127 |
+
measure_stack = np.array([
|
128 |
+
[[2, 2, 2, 0], [4, 4, 6, 0], [3, 3, 2, 0], [0, 0, 0, 0]],
|
129 |
+
[[4, 4, 4, 0], [8, 8, 8, 0], [4, 4, 4, 0], [0, 0, 0, 0]],
|
130 |
+
[[3, 3, 3, 0], [6, 6, 4, 0], [3, 2, 2, 0], [0, 0, 0, 0]],
|
131 |
+
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
132 |
+
])
|
133 |
+
p = (1, 1, 1)
|
134 |
+
melem = np.ones((3, 3, 3))
|
135 |
+
result = measure_at_point(p, melem, measure_stack, op='mean')
|
136 |
+
assert result == 4, "Expected mean: 4"
|
137 |
+
|
138 |
+
def test_measure_at_point_mean_off1():
|
139 |
+
measure_stack = np.array([
|
140 |
+
[[2, 2, 2, 0], [4, 4, 6, 0], [5, 5, 2, 0], [0, 0, 0, 0]],
|
141 |
+
[[4, 4, 4, 0], [8, 8, 8, 0], [4, 4, 4, 0], [0, 0, 0, 0]],
|
142 |
+
[[3, 3, 3, 0], [6, 6, 4, 0], [3, 2, 2, 0], [0, 0, 0, 0]],
|
143 |
+
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
144 |
+
])
|
145 |
+
p = (0, 0, 0)
|
146 |
+
melem = np.ones((3, 3, 3))
|
147 |
+
result = measure_at_point(p, melem, measure_stack, op='mean')
|
148 |
+
assert result == 4.5, "Expected mean: 4.5"
|
149 |
+
|
150 |
+
def test_measure_at_point_mean_off2():
|
151 |
+
measure_stack = np.array([
|
152 |
+
[[2, 2, 2, 0], [4, 4, 6, 0], [5, 5, 2, 0], [0, 0, 0, 0]],
|
153 |
+
[[4, 4, 4, 0], [8, 8, 8, 0], [4, 4, 4, 0], [0, 0, 0, 0]],
|
154 |
+
[[3, 3, 3, 0], [6, 6, 4, 0], [3, 2, 2, 0], [0, 0, 0, 0]],
|
155 |
+
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
156 |
+
])
|
157 |
+
p = (3, 1, 1)
|
158 |
+
melem = np.ones((3, 3, 3))
|
159 |
+
print(measure_stack[p[0], p[1], p[2]])
|
160 |
+
|
161 |
+
result = measure_at_point(p, melem, measure_stack, op='mean')
|
162 |
+
assert result == 32/18 # Expected mean: 4.5
|
163 |
+
|
164 |
+
def test_measure_at_point_mean_off3():
|
165 |
+
measure_stack = np.array([
|
166 |
+
[[2, 2, 2, 0], [4, 4, 6, 0], [5, 5, 2, 0], [0, 0, 0, 0]],
|
167 |
+
[[4, 4, 4, 0], [8, 8, 8, 0], [4, 4, 4, 0], [0, 0, 0, 0]],
|
168 |
+
[[3, 3, 3, 0], [6, 6, 4, 0], [3, 2, 2, 0], [0, 0, 0, 0]],
|
169 |
+
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
170 |
+
])
|
171 |
+
p = (3, 1, 1)
|
172 |
+
melem = np.ones((1, 1, 3))
|
173 |
+
print(measure_stack[p[0], p[1], p[2]])
|
174 |
+
|
175 |
+
result = measure_at_point(p, melem, measure_stack, op='mean')
|
176 |
+
assert result == 0, "Expected mean: 4.5"
|
177 |
+
|
178 |
+
def test_measure_at_point_mean_off3():
|
179 |
+
measure_stack = np.array([
|
180 |
+
[[2, 2, 2, 0], [4, 4, 6, 0], [5, 5, 2, 0], [0, 0, 0, 0]],
|
181 |
+
[[4, 4, 4, 0], [8, 8, 8, 0], [4, 4, 4, 0], [0, 0, 0, 0]],
|
182 |
+
[[3, 3, 3, 0], [6, 6, 4, 0], [3, 2, 2, 0], [0, 0, 0, 0]],
|
183 |
+
[[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
|
184 |
+
])
|
185 |
+
p = (3, 1, 1)
|
186 |
+
melem = np.ones((3, 1, 1))
|
187 |
+
print(measure_stack[p[0], p[1], p[2]])
|
188 |
+
|
189 |
+
result = measure_at_point(p, melem, measure_stack, op='mean')
|
190 |
+
assert result == 3, "Expected mean: 4.5"
|
191 |
+
|
192 |
+
|
193 |
+
def test_measure_at_point_max():
|
194 |
+
measure_stack = np.array([
|
195 |
+
[[2, 2, 2], [4, 4, 4], [2, 2, 2]],
|
196 |
+
[[4, 5, 4], [8, 7, 9], [4, 4, 4]],
|
197 |
+
[[2, 2, 2], [4, 4, 4], [2, 2, 2]]
|
198 |
+
])
|
199 |
+
p = (1, 1, 1)
|
200 |
+
melem = np.ones((3, 3, 3))
|
201 |
+
result = measure_at_point(p, melem, measure_stack, op='max')
|
202 |
+
assert result == 9, "Expected max: 9"
|
203 |
+
|
204 |
+
|
205 |
+
def test_make_sphere_equal():
|
206 |
+
R = 5
|
207 |
+
z_scale_ratio = 1.0
|
208 |
+
|
209 |
+
sphere = make_sphere(R, z_scale_ratio)
|
210 |
+
|
211 |
+
# Check the returned type
|
212 |
+
assert isinstance(sphere, np.ndarray), "Output should be a numpy ndarray"
|
213 |
+
|
214 |
+
# Check the shape
|
215 |
+
expected_shape = (2*R+1, 2*R+1, 2*R+1)
|
216 |
+
assert sphere.shape == expected_shape, f"Expected shape {expected_shape}, but got {sphere.shape}"
|
217 |
+
|
218 |
+
assert (sphere[:,:,::-1] == sphere).all(), f"Expected symmetrical mask"
|
219 |
+
assert (sphere[:,::-1,:] == sphere).all(), f"Expected symmetrical mask"
|
220 |
+
assert (sphere[::-1,:,:] == sphere).all(), f"Expected symmetrical mask"
|
221 |
+
assert abs(np.sum(sphere)-4/3*pi*R**3)<10, f"Expected approximate volume to be correct"
|
222 |
+
assert (sphere[R,R,0] == 1), f"Expected centre point on top plane to be within sphere"
|
223 |
+
assert (sphere[R+1,R,0] == 0), f"Expected point next to centre on top plane to be outside sphere"
|
tests/test_preprocess.py
ADDED
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from path_analysis.data_preprocess import *
|
2 |
+
import numpy as np
|
3 |
+
import pytest
|
4 |
+
|
5 |
+
def test_thin_points():
|
6 |
+
# Define a sample point list
|
7 |
+
points = [
|
8 |
+
([0, 0, 0], 10, 0),
|
9 |
+
([1, 1, 1], 8, 1),
|
10 |
+
([10, 10, 10], 12, 2),
|
11 |
+
([10.5, 10.5, 10.5], 5, 3),
|
12 |
+
([20, 20, 20], 15, 4)
|
13 |
+
]
|
14 |
+
|
15 |
+
# Call the thin_points function with dmin=5 (for example)
|
16 |
+
removed_indices = thin_points(points, dmin=5)
|
17 |
+
|
18 |
+
# Check results
|
19 |
+
# Point at index 1 ([1, 1, 1]) should be removed since it's within 5 units distance of point at index 0 and has lower intensity.
|
20 |
+
# Similarly, point at index 3 ([10.5, 10.5, 10.5]) should be removed as it's close to point at index 2 and has lower intensity.
|
21 |
+
assert set(removed_indices) == {1, 3}
|
22 |
+
|
23 |
+
# Another simple test to check if function does nothing when points are far apart
|
24 |
+
far_points = [
|
25 |
+
([0, 0, 0], 10, 0),
|
26 |
+
([100, 100, 100], 12, 1),
|
27 |
+
([200, 200, 200], 15, 2)
|
28 |
+
]
|
29 |
+
|
30 |
+
removed_indices_far = thin_points(far_points, dmin=5)
|
31 |
+
assert len(removed_indices_far) == 0 # Expect no points to be removed
|
32 |
+
|
33 |
+
|
34 |
+
def test_find_peaks2():
|
35 |
+
|
36 |
+
# Basic test
|
37 |
+
data = np.array([0, 0, 0, 0, 0, 0, 5, 0, 3, 0])
|
38 |
+
peaks, _ = find_peaks2(data)
|
39 |
+
assert set(peaks) == {6} # Expected peaks at positions 6
|
40 |
+
|
41 |
+
# Basic test
|
42 |
+
data = np.array([0, 2, 0, 0, 0, 0, 0, 0, 0, 0])
|
43 |
+
peaks, _ = find_peaks2(data)
|
44 |
+
assert set(peaks) == {1} # Expected peaks at positions 1
|
45 |
+
|
46 |
+
|
47 |
+
# Test with padding impacting peak detection
|
48 |
+
data = np.array([3, 2.9, 0, 0, 0, 3])
|
49 |
+
peaks, _ = find_peaks2(data)
|
50 |
+
assert set(peaks) == {0,5} # Peaks at both ends
|
51 |
+
|
52 |
+
# Test with close peaks
|
53 |
+
data = np.array([3, 0, 3])
|
54 |
+
peaks, _ = find_peaks2(data)
|
55 |
+
assert set(peaks) == {2} # Peak at right end only
|
56 |
+
# Test with close peaks
|
57 |
+
|
58 |
+
|
59 |
+
# Test with close peaks
|
60 |
+
data = np.array([3, 0, 3])
|
61 |
+
peaks, _ = find_peaks2(data, distance=1)
|
62 |
+
assert set(peaks) == {0,2} # Peaks at both ends
|
63 |
+
|
64 |
+
# Test with close peaks
|
65 |
+
data = np.array([0, 3, 3, 3, 0, 3, 3, 3, 3, 3, 3])
|
66 |
+
peaks, _ = find_peaks2(data, distance=1)
|
67 |
+
assert set(peaks) == {2,7} # Peak at centre (rounded to the left) of groups of maximum values
|
68 |
+
|
69 |
+
# Test with prominence threshold
|
70 |
+
data = np.array([0, 1, 0, 0.4, 0])
|
71 |
+
peaks, _ = find_peaks2(data, prominence=0.5)
|
72 |
+
assert peaks == [1] # Only the peak at position 1 meets the prominence threshold
|
73 |
+
|
74 |
+
|
75 |
+
def test_pc():
|
76 |
+
pos = np.array([0, 1, 2, 3, 4, 6])
|
77 |
+
values = np.array([0.1, 0.5, 0.2, 0.8, 0.3, 0.9])
|
78 |
+
|
79 |
+
# Basic test
|
80 |
+
assert np.array_equal(pc(pos, values), np.array([1, 3, 6])) # only values 0.8 and 0.9 exceed 0.4 times the max (which is 0.9)
|
81 |
+
|
82 |
+
# Test with custom alpha
|
83 |
+
assert np.array_equal(pc(pos, values, alpha=0.5), np.array([1, 3, 6]))
|
84 |
+
|
85 |
+
# Test with a larger alpha
|
86 |
+
assert np.array_equal(pc(pos, values, alpha=1.0), [6]) # No values exceed the maximum value itself
|
87 |
+
|
88 |
+
# Test with all values below threshold
|
89 |
+
values = np.array([0.1, 0.2, 0.3, 0.4])
|
90 |
+
|
91 |
+
assert np.array_equal(pc(pos[:4], values), [1,2,3]) # All values are below 0.4 times the max (which is 0.4)
|
92 |
+
|
93 |
+
@pytest.fixture
|
94 |
+
def mock_data():
|
95 |
+
all_paths = [ [ (0,0,0), (0,2,0), (0,5,0), (0,10,0), (0,15,0), (0,20,0)], [ (1,20,0), (1,20,10), (1,20,20) ] ] # Mock paths
|
96 |
+
path_lengths = [ 2.2, 2.3 ] # Mock path lengths
|
97 |
+
measured_trace_fluorescence = [ [100, 8, 3, 2, 3, 39], [38, 2, 20] ] # Mock fluorescence data
|
98 |
+
return all_paths, path_lengths, measured_trace_fluorescence
|
99 |
+
|
100 |
+
def test_process_cell_traces_return_type(mock_data):
|
101 |
+
all_paths, path_lengths, measured_trace_fluorescence = mock_data
|
102 |
+
result = process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence)
|
103 |
+
assert isinstance(result, CellData), f"Expected CellData but got {type(result)}"
|
104 |
+
|
105 |
+
def test_process_cell_traces_pathdata_list_length(mock_data):
|
106 |
+
all_paths, path_lengths, measured_trace_fluorescence = mock_data
|
107 |
+
result = process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence)
|
108 |
+
assert len(result.pathdata_list) == len(all_paths), f"Expected {len(all_paths)} but got {len(result.pathdata_list)}"
|
109 |
+
|
110 |
+
def test_process_cell_traces_pathdata_path_lengths(mock_data):
|
111 |
+
all_paths, path_lengths, measured_trace_fluorescence = mock_data
|
112 |
+
result = process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence)
|
113 |
+
path_lengths = [p.SC_length for p in result.pathdata_list]
|
114 |
+
expected_path_lengths = [2.2, 2.3]
|
115 |
+
assert path_lengths == expected_path_lengths, f"Expected {expected_path_lengths} but got {path_lengths}"
|
116 |
+
|
117 |
+
def test_process_cell_traces_peaks(mock_data):
|
118 |
+
all_paths, path_lengths, measured_trace_fluorescence = mock_data
|
119 |
+
result = process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence)
|
120 |
+
peaks = [p.peaks for p in result.pathdata_list]
|
121 |
+
assert peaks == [[0,5],[]]
|
122 |
+
|
123 |
+
# Mock data
|
124 |
+
@pytest.fixture
|
125 |
+
def mock_celldata():
|
126 |
+
pathdata1 = PathData(peaks=[0, 5], points=[(0,0,0), (0,2,0), (0,5,0), (0,10,0), (0,15,0), (0,20,0)], o_hei10=[100, 8, 3, 2, 3, 39], SC_length=2.2)
|
127 |
+
pathdata2 = PathData(peaks=[0], points=[(1,20,0), (1,20,10), (1,20,20) ], o_hei10=[38, 2, 20], SC_length=2.3)
|
128 |
+
return CellData(pathdata_list=[pathdata1, pathdata2])
|
129 |
+
|
130 |
+
def test_analyse_celldata_output_length(mock_celldata):
|
131 |
+
rel_intensity, pos, pos_index, trace_median_intensity, trace_thresholds = analyse_celldata(mock_celldata, {'peak_threshold': 0.4, 'threshold_type':'per-trace'})
|
132 |
+
assert len(rel_intensity) == len(mock_celldata.pathdata_list), "Mismatch in relative intensities length"
|
133 |
+
assert len(pos) == len(mock_celldata.pathdata_list), "Mismatch in positions length"
|
134 |
+
assert len(pos_index) == len(mock_celldata.pathdata_list), "Mismatch in position indices length"
|
135 |
+
|
136 |
+
|
137 |
+
|
138 |
+
|
139 |
+
|