Show screening, permit screening distance to be changed
Browse files- app.py +7 -4
- path_analysis/analyse.py +50 -26
- path_analysis/data_preprocess.py +40 -40
- tests/test_analyse.py +77 -20
- tests/test_preprocess.py +2 -2
app.py
CHANGED
@@ -9,6 +9,7 @@ import numpy as np
|
|
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:
|
@@ -41,6 +42,7 @@ with gr.Blocks() as demo:
|
|
41 |
|
42 |
threshold_type = gr.Radio(["per-trace", "per-cell"], label="Threshold-type", value="per-trace", interactive=True)
|
43 |
use_corrected_positions = gr.Checkbox(label="Correct foci position measurements", value=True, interactive=True)
|
|
|
44 |
|
45 |
|
46 |
# The output column showing the result of processing
|
@@ -52,15 +54,16 @@ with gr.Blocks() as demo:
|
|
52 |
data_file_output=gr.File(label="Output data file (.csv)")
|
53 |
|
54 |
|
55 |
-
def process(cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type, use_corrected_positions):
|
56 |
|
57 |
config = { 'sphere_radius': sphere_radius,
|
58 |
'peak_threshold': peak_threshold,
|
59 |
'xy_res': xy_res,
|
60 |
'z_res': z_res,
|
61 |
'threshold_type': threshold_type,
|
62 |
-
'use_corrected_positions': use_corrected_positions
|
63 |
-
|
|
|
64 |
|
65 |
|
66 |
paths, traces, fig, extracted_peaks = analyse_paths(cellid_input, image_input.name, path_input.name, config)
|
@@ -71,7 +74,7 @@ with gr.Blocks() as demo:
|
|
71 |
|
72 |
with gr.Row():
|
73 |
greet_btn = gr.Button("Process")
|
74 |
-
greet_btn.click(fn=process, inputs=[cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type, use_corrected_positions], outputs=[trace_output, image_output, plot_output, data_output, data_file_output], api_name="process")
|
75 |
|
76 |
|
77 |
if __name__ == "__main__":
|
|
|
9 |
# Function to preview the imported image
|
10 |
def preview_image(file1):
|
11 |
if file1:
|
12 |
+
print('Uploading image', file1.name)
|
13 |
im = imread(file1.name)
|
14 |
print(im.ndim, im.shape)
|
15 |
if im.ndim>2:
|
|
|
42 |
|
43 |
threshold_type = gr.Radio(["per-trace", "per-cell"], label="Threshold-type", value="per-trace", interactive=True)
|
44 |
use_corrected_positions = gr.Checkbox(label="Correct foci position measurements", value=True, interactive=True)
|
45 |
+
screening_distance = gr.Number(label='Screening distance (voxels)', value=10, interactive=True)
|
46 |
|
47 |
|
48 |
# The output column showing the result of processing
|
|
|
54 |
data_file_output=gr.File(label="Output data file (.csv)")
|
55 |
|
56 |
|
57 |
+
def process(cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type, use_corrected_positions, screening_distance):
|
58 |
|
59 |
config = { 'sphere_radius': sphere_radius,
|
60 |
'peak_threshold': peak_threshold,
|
61 |
'xy_res': xy_res,
|
62 |
'z_res': z_res,
|
63 |
'threshold_type': threshold_type,
|
64 |
+
'use_corrected_positions': use_corrected_positions,
|
65 |
+
'screening_distance': screening_distance,
|
66 |
+
}
|
67 |
|
68 |
|
69 |
paths, traces, fig, extracted_peaks = analyse_paths(cellid_input, image_input.name, path_input.name, config)
|
|
|
74 |
|
75 |
with gr.Row():
|
76 |
greet_btn = gr.Button("Process")
|
77 |
+
greet_btn.click(fn=process, inputs=[cellid_input, image_input, path_input, sphere_radius, peak_threshold, xy_res, z_res, threshold_type, use_corrected_positions, screening_distance], outputs=[trace_output, image_output, plot_output, data_output, data_file_output], api_name="process")
|
78 |
|
79 |
|
80 |
if __name__ == "__main__":
|
path_analysis/analyse.py
CHANGED
@@ -53,6 +53,7 @@ def calculate_path_length_partials(point_list, voxel_size=(1,1,1)):
|
|
53 |
section_lengths = [0.0]
|
54 |
s = np.array(voxel_size)
|
55 |
for i in range(len(point_list)-1):
|
|
|
56 |
section_lengths.append(la.norm(s * (np.array(point_list[i+1]) - np.array(point_list[i]))))
|
57 |
return np.cumsum(section_lengths)
|
58 |
|
@@ -89,7 +90,7 @@ def visualise_ordering(points_list, dim, wr=5, wc=5):
|
|
89 |
col_map = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255), (0,255,255),
|
90 |
(255,127,0), (255, 0, 127), (127, 255, 0), (0, 255, 127), (127,0,255), (0,127,255)]
|
91 |
|
92 |
-
def draw_paths(all_paths, foci_stack, foci_index=None, r=3):
|
93 |
"""
|
94 |
Draws paths on the provided image stack and overlays markers for the foci
|
95 |
|
@@ -98,7 +99,7 @@ def draw_paths(all_paths, foci_stack, foci_index=None, r=3):
|
|
98 |
foci_stack (np.array): 3D numpy array representing the image stack.
|
99 |
foci_index (list, optional): List of list of focus indices (along each path). Defaults to None.
|
100 |
r (int, optional): Radius for the ellipse or line drawing around the focus. Defaults to 3.
|
101 |
-
|
102 |
Returns:
|
103 |
PIL.Image.Image: An image with the drawn paths.
|
104 |
"""
|
@@ -110,13 +111,20 @@ def draw_paths(all_paths, foci_stack, foci_index=None, r=3):
|
|
110 |
for i, (p, col) in enumerate(zip(all_paths, cycle(col_map))):
|
111 |
draw.line([(u[0], u[1]) for u in p], fill=col)
|
112 |
draw.text((p[0][0], p[0][1]), str(i+1), fill=col)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
if foci_index is not None:
|
114 |
for i, (idx, p, col) in enumerate(zip(foci_index, all_paths, cycle(col_map))):
|
115 |
if len(idx):
|
116 |
for j in idx:
|
117 |
draw.line((int(p[j][0]-r), int(p[j][1]), int(p[j][0]+r), int(p[j][1])), fill=col, width=2)
|
118 |
draw.line((int(p[j][0]), int(p[j][1]-r), int(p[j][0]), int(p[j][1]+r)), fill=col, width=2)
|
119 |
-
|
120 |
return im
|
121 |
|
122 |
|
@@ -164,8 +172,7 @@ def make_mask_s(p, melem, measure_stack):
|
|
164 |
#
|
165 |
|
166 |
R = [u//2 for u in melem.shape]
|
167 |
-
|
168 |
-
|
169 |
r, c, z = p
|
170 |
|
171 |
mask = np.zeros(melem.shape)
|
@@ -210,7 +217,8 @@ def make_sphere(R=5, z_scale_ratio=2.3):
|
|
210 |
Generate a binary representation of a sphere in 3D space.
|
211 |
|
212 |
Args:
|
213 |
-
R (int, optional): Radius of the sphere. Default is 5.
|
|
|
214 |
z_scale_ratio (float, optional): Scaling factor for the z-axis. Default is 2.3.
|
215 |
|
216 |
Returns:
|
@@ -243,25 +251,26 @@ def measure_all_with_sphere(points_list, measure_stack, op='mean', R=5, z_scale_
|
|
243 |
|
244 |
|
245 |
# Measure fluorescence levels along ordered skeleton
|
246 |
-
def measure_chrom2(path,
|
247 |
"""
|
248 |
Measure fluorescence levels along an ordered skeleton.
|
249 |
|
250 |
Args:
|
251 |
path (list): List of ordered path points (r, c, z).
|
252 |
-
|
253 |
config (dict): Configuration dictionary containing 'z_res', 'xy_res', and 'sphere_radius' values.
|
254 |
|
255 |
Returns:
|
256 |
tuple: A tuple containing the visualization, mean measurements, and max measurements along the path.
|
257 |
"""
|
|
|
258 |
scale_ratio = config['z_res']/config['xy_res']
|
259 |
sphere_xy_radius = int(math.ceil(config['sphere_radius']/config['xy_res']))
|
260 |
|
261 |
-
vis = visualise_ordering(path, dim=
|
262 |
|
263 |
-
measurements = measure_all_with_sphere(path,
|
264 |
-
measurements_max = measure_all_with_sphere(path,
|
265 |
|
266 |
|
267 |
return vis, measurements, measurements_max
|
@@ -290,20 +299,22 @@ def extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config):
|
|
290 |
n_paths = len(all_paths)
|
291 |
|
292 |
data = []
|
293 |
-
foci_absolute_intensity, foci_position, foci_position_index,
|
294 |
|
|
|
295 |
foci_intensities = []
|
296 |
for path_foci_abs_int, tmi in zip(foci_absolute_intensity, trace_median_intensities):
|
297 |
foci_intensities.extend(list(path_foci_abs_int - tmi))
|
298 |
-
|
|
|
299 |
mean_intensity = np.mean(foci_intensities)
|
300 |
trace_positions = []
|
301 |
|
302 |
for i in range(n_paths):
|
303 |
|
|
|
304 |
pl = calculate_path_length_partials(all_paths[i], (config['xy_res'], config['xy_res'], config['z_res']))
|
305 |
|
306 |
-
print(i, len(all_paths[i]), len(pl))
|
307 |
|
308 |
path_data = { 'Cell_ID':cell_id,
|
309 |
'Trace': i+1,
|
@@ -311,17 +322,23 @@ def extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config):
|
|
311 |
'Measured_trace_length(um)': pl[-1],
|
312 |
'Trace_median_intensity': trace_median_intensities[i],
|
313 |
'Detection_sphere_radius(um)': config['sphere_radius'],
|
314 |
-
'
|
|
|
|
|
315 |
for j, (idx, u,v) in enumerate(zip(foci_position_index[i], foci_position[i], foci_absolute_intensity[i])):
|
316 |
if config['use_corrected_positions']:
|
|
|
317 |
path_data[f'Foci_{j+1}_position(um)'] = pl[idx]
|
318 |
else:
|
|
|
319 |
path_data[f'Foci_{j+1}_position(um)'] = u
|
|
|
320 |
path_data[f'Foci_{j+1}_absolute_intensity'] = v
|
|
|
321 |
path_data[f'Foci_{j+1}_relative_intensity'] = (v - trace_median_intensities[i])/mean_intensity
|
322 |
data.append(path_data)
|
323 |
trace_positions.append(pl)
|
324 |
-
return pd.DataFrame(data), foci_absolute_intensity, foci_position_index,
|
325 |
|
326 |
|
327 |
def analyse_paths(cell_id,
|
@@ -344,24 +361,29 @@ def analyse_paths(cell_id,
|
|
344 |
"""
|
345 |
|
346 |
|
|
|
|
|
347 |
foci_stack = tifffile.imread(foci_file)
|
348 |
|
|
|
349 |
if foci_stack.ndim==2:
|
350 |
foci_stack = foci_stack[None,:,:]
|
351 |
|
352 |
all_paths, path_lengths = get_paths_from_traces_file(traces_file)
|
353 |
|
354 |
-
all_trace_vis = []
|
355 |
-
all_m = []
|
356 |
for p in all_paths:
|
|
|
357 |
vis, m, _ = measure_chrom2(p,foci_stack.transpose(2,1,0), config)
|
358 |
all_trace_vis.append(vis)
|
359 |
all_m.append(m)
|
360 |
|
361 |
|
362 |
-
|
|
|
363 |
|
364 |
-
|
365 |
n_cols = 2
|
366 |
n_rows = (len(all_paths)+n_cols-1)//n_cols
|
367 |
fig, ax = plt.subplots(n_rows,n_cols, figsize=(5*n_cols, 3*n_rows))
|
@@ -371,22 +393,24 @@ def analyse_paths(cell_id,
|
|
371 |
ax[i].set_title(f'Trace {i+1}')
|
372 |
ax[i].plot(trace_positions[i], m)
|
373 |
if len(foci_pos_index[i]):
|
|
|
374 |
ax[i].plot(trace_positions[i][foci_pos_index[i]], np.array(m)[foci_pos_index[i]], 'rx')
|
375 |
|
376 |
-
if len(
|
377 |
-
|
378 |
-
|
379 |
-
ax[i].plot(trace_positions[i][
|
380 |
|
381 |
-
|
382 |
if trace_thresholds[i] is not None:
|
383 |
ax[i].axhline(trace_thresholds[i], c='r', ls=':')
|
384 |
ax[i].set_xlabel('Distance from start (um)')
|
385 |
ax[i].set_ylabel('Intensity')
|
|
|
386 |
for i in range(len(all_m), n_cols*n_rows):
|
387 |
ax[i].axis('off')
|
388 |
|
389 |
plt.tight_layout()
|
390 |
-
trace_overlay = draw_paths(all_paths, foci_stack, foci_index=foci_pos_index)
|
391 |
|
392 |
return trace_overlay, all_trace_vis, fig, extracted_peaks
|
|
|
53 |
section_lengths = [0.0]
|
54 |
s = np.array(voxel_size)
|
55 |
for i in range(len(point_list)-1):
|
56 |
+
# Euclidean distance between successive points
|
57 |
section_lengths.append(la.norm(s * (np.array(point_list[i+1]) - np.array(point_list[i]))))
|
58 |
return np.cumsum(section_lengths)
|
59 |
|
|
|
90 |
col_map = [(255,0,0), (0,255,0), (0,0,255), (255,255,0), (255,0,255), (0,255,255),
|
91 |
(255,127,0), (255, 0, 127), (127, 255, 0), (0, 255, 127), (127,0,255), (0,127,255)]
|
92 |
|
93 |
+
def draw_paths(all_paths, foci_stack, foci_index=None, r=3, screened_foci_data=None):
|
94 |
"""
|
95 |
Draws paths on the provided image stack and overlays markers for the foci
|
96 |
|
|
|
99 |
foci_stack (np.array): 3D numpy array representing the image stack.
|
100 |
foci_index (list, optional): List of list of focus indices (along each path). Defaults to None.
|
101 |
r (int, optional): Radius for the ellipse or line drawing around the focus. Defaults to 3.
|
102 |
+
screened_foci_data (list, optional): List of RemovedPeakData for screened foci
|
103 |
Returns:
|
104 |
PIL.Image.Image: An image with the drawn paths.
|
105 |
"""
|
|
|
111 |
for i, (p, col) in enumerate(zip(all_paths, cycle(col_map))):
|
112 |
draw.line([(u[0], u[1]) for u in p], fill=col)
|
113 |
draw.text((p[0][0], p[0][1]), str(i+1), fill=col)
|
114 |
+
|
115 |
+
if screened_foci_data is not None:
|
116 |
+
for i, removed_peaks in enumerate(screened_foci_data):
|
117 |
+
for p in removed_peaks:
|
118 |
+
u = all_paths[i][p.idx]
|
119 |
+
v = all_paths[p.screening_peak[0]][p.screening_peak[1]]
|
120 |
+
draw.line((int(u[0]), int(u[1]), int(v[0]), int(v[1])), fill=(127,127,127), width=2)
|
121 |
+
|
122 |
if foci_index is not None:
|
123 |
for i, (idx, p, col) in enumerate(zip(foci_index, all_paths, cycle(col_map))):
|
124 |
if len(idx):
|
125 |
for j in idx:
|
126 |
draw.line((int(p[j][0]-r), int(p[j][1]), int(p[j][0]+r), int(p[j][1])), fill=col, width=2)
|
127 |
draw.line((int(p[j][0]), int(p[j][1]-r), int(p[j][0]), int(p[j][1]+r)), fill=col, width=2)
|
|
|
128 |
return im
|
129 |
|
130 |
|
|
|
172 |
#
|
173 |
|
174 |
R = [u//2 for u in melem.shape]
|
175 |
+
|
|
|
176 |
r, c, z = p
|
177 |
|
178 |
mask = np.zeros(melem.shape)
|
|
|
217 |
Generate a binary representation of a sphere in 3D space.
|
218 |
|
219 |
Args:
|
220 |
+
R (int, optional): Radius of the sphere. Default is 5. Centred on the centre of the middle voxel.
|
221 |
+
Includes all voxels whose centre is precisely R from the middle voxel.
|
222 |
z_scale_ratio (float, optional): Scaling factor for the z-axis. Default is 2.3.
|
223 |
|
224 |
Returns:
|
|
|
251 |
|
252 |
|
253 |
# Measure fluorescence levels along ordered skeleton
|
254 |
+
def measure_chrom2(path, intensity, config):
|
255 |
"""
|
256 |
Measure fluorescence levels along an ordered skeleton.
|
257 |
|
258 |
Args:
|
259 |
path (list): List of ordered path points (r, c, z).
|
260 |
+
intensity (numpy.ndarray): 3D fluorescence data.
|
261 |
config (dict): Configuration dictionary containing 'z_res', 'xy_res', and 'sphere_radius' values.
|
262 |
|
263 |
Returns:
|
264 |
tuple: A tuple containing the visualization, mean measurements, and max measurements along the path.
|
265 |
"""
|
266 |
+
# Calculate size of spheroid used for measurement
|
267 |
scale_ratio = config['z_res']/config['xy_res']
|
268 |
sphere_xy_radius = int(math.ceil(config['sphere_radius']/config['xy_res']))
|
269 |
|
270 |
+
vis = visualise_ordering(path, dim=intensity.shape, wr=sphere_xy_radius, wc=sphere_xy_radius)
|
271 |
|
272 |
+
measurements = measure_all_with_sphere(path, intensity, op='mean', R=sphere_xy_radius, z_scale_ratio=scale_ratio)
|
273 |
+
measurements_max = measure_all_with_sphere(path, intensity, op='max', R=sphere_xy_radius, z_scale_ratio=scale_ratio)
|
274 |
|
275 |
|
276 |
return vis, measurements, measurements_max
|
|
|
299 |
n_paths = len(all_paths)
|
300 |
|
301 |
data = []
|
302 |
+
foci_absolute_intensity, foci_position, foci_position_index, screened_foci_data, trace_median_intensities, trace_thresholds = analyse_traces(all_paths, path_lengths, measured_traces, config)
|
303 |
|
304 |
+
# Normalize foci intensities (for quantification) using trace medians as estimates of background
|
305 |
foci_intensities = []
|
306 |
for path_foci_abs_int, tmi in zip(foci_absolute_intensity, trace_median_intensities):
|
307 |
foci_intensities.extend(list(path_foci_abs_int - tmi))
|
308 |
+
|
309 |
+
# Divide all foci intensities by the mean within the cell
|
310 |
mean_intensity = np.mean(foci_intensities)
|
311 |
trace_positions = []
|
312 |
|
313 |
for i in range(n_paths):
|
314 |
|
315 |
+
# Calculate real (Euclidean) distance of each point along the traced path
|
316 |
pl = calculate_path_length_partials(all_paths[i], (config['xy_res'], config['xy_res'], config['z_res']))
|
317 |
|
|
|
318 |
|
319 |
path_data = { 'Cell_ID':cell_id,
|
320 |
'Trace': i+1,
|
|
|
322 |
'Measured_trace_length(um)': pl[-1],
|
323 |
'Trace_median_intensity': trace_median_intensities[i],
|
324 |
'Detection_sphere_radius(um)': config['sphere_radius'],
|
325 |
+
'Screening_distance(voxels)': config['screening_distance'],
|
326 |
+
'Foci_ID_threshold': config['peak_threshold'],
|
327 |
+
'Trace_foci_number': len(foci_position_index[i]) }
|
328 |
for j, (idx, u,v) in enumerate(zip(foci_position_index[i], foci_position[i], foci_absolute_intensity[i])):
|
329 |
if config['use_corrected_positions']:
|
330 |
+
# Use the calculated position along the traced path
|
331 |
path_data[f'Foci_{j+1}_position(um)'] = pl[idx]
|
332 |
else:
|
333 |
+
# Use the measured trace length (from SNT), and assume all steps of path are approximately the same length
|
334 |
path_data[f'Foci_{j+1}_position(um)'] = u
|
335 |
+
# The original measured intensity (mean in spheroid around detected peak)
|
336 |
path_data[f'Foci_{j+1}_absolute_intensity'] = v
|
337 |
+
# Measure relative intensity by removing per-trace background and dividing by cell total
|
338 |
path_data[f'Foci_{j+1}_relative_intensity'] = (v - trace_median_intensities[i])/mean_intensity
|
339 |
data.append(path_data)
|
340 |
trace_positions.append(pl)
|
341 |
+
return pd.DataFrame(data), foci_absolute_intensity, foci_position_index, screened_foci_data, trace_thresholds, trace_positions
|
342 |
|
343 |
|
344 |
def analyse_paths(cell_id,
|
|
|
361 |
"""
|
362 |
|
363 |
|
364 |
+
# Read stack
|
365 |
+
|
366 |
foci_stack = tifffile.imread(foci_file)
|
367 |
|
368 |
+
# If 2D add additional (z) dimension
|
369 |
if foci_stack.ndim==2:
|
370 |
foci_stack = foci_stack[None,:,:]
|
371 |
|
372 |
all_paths, path_lengths = get_paths_from_traces_file(traces_file)
|
373 |
|
374 |
+
all_trace_vis = [] # Per-path visualizations
|
375 |
+
all_m = [] # Per-path measured intensities
|
376 |
for p in all_paths:
|
377 |
+
# Measure intensity along path - transpose the stack ZYX -> XYZ
|
378 |
vis, m, _ = measure_chrom2(p,foci_stack.transpose(2,1,0), config)
|
379 |
all_trace_vis.append(vis)
|
380 |
all_m.append(m)
|
381 |
|
382 |
|
383 |
+
# Extract all data from paths and traces
|
384 |
+
extracted_peaks, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, all_m, config)
|
385 |
|
386 |
+
# Plot per-path measured intensities and indicate foci
|
387 |
n_cols = 2
|
388 |
n_rows = (len(all_paths)+n_cols-1)//n_cols
|
389 |
fig, ax = plt.subplots(n_rows,n_cols, figsize=(5*n_cols, 3*n_rows))
|
|
|
393 |
ax[i].set_title(f'Trace {i+1}')
|
394 |
ax[i].plot(trace_positions[i], m)
|
395 |
if len(foci_pos_index[i]):
|
396 |
+
# Plot detected foci
|
397 |
ax[i].plot(trace_positions[i][foci_pos_index[i]], np.array(m)[foci_pos_index[i]], 'rx')
|
398 |
|
399 |
+
if len(screened_foci_data[i]):
|
400 |
+
# Indicate screened foci by gray circles on plots
|
401 |
+
screened_foci_pos_index = [u.idx for u in screened_foci_data[i]]
|
402 |
+
ax[i].plot(trace_positions[i][screened_foci_pos_index], np.array(m)[screened_foci_pos_index], color=(0.5,0.5,0.5), marker='o', linestyle='None')
|
403 |
|
404 |
+
# Show per-trace intensity thresholds with red dotted lines
|
405 |
if trace_thresholds[i] is not None:
|
406 |
ax[i].axhline(trace_thresholds[i], c='r', ls=':')
|
407 |
ax[i].set_xlabel('Distance from start (um)')
|
408 |
ax[i].set_ylabel('Intensity')
|
409 |
+
# Hide excess plots
|
410 |
for i in range(len(all_m), n_cols*n_rows):
|
411 |
ax[i].axis('off')
|
412 |
|
413 |
plt.tight_layout()
|
414 |
+
trace_overlay = draw_paths(all_paths, foci_stack, foci_index=foci_pos_index, screened_foci_data=screened_foci_data)
|
415 |
|
416 |
return trace_overlay, all_trace_vis, fig, extracted_peaks
|
path_analysis/data_preprocess.py
CHANGED
@@ -74,10 +74,10 @@ class RemovedPeakData(object):
|
|
74 |
|
75 |
Attributes:
|
76 |
idx (int): Index of peak along path
|
77 |
-
|
78 |
"""
|
79 |
idx: int
|
80 |
-
|
81 |
|
82 |
@dataclass
|
83 |
class PathData(object):
|
@@ -86,17 +86,17 @@ class PathData(object):
|
|
86 |
This dataclass encapsulates information about the peaks,
|
87 |
the defining points, the fluorescence values, and the path length of a specific path.
|
88 |
|
89 |
-
Attributes: peaks (list): List of peaks in the path (indicies of positions in points,
|
90 |
removed_peaks (list): List of peaks in the path which have been removed because of a nearby larger peak
|
91 |
points (list): List of points defining the path.
|
92 |
-
|
93 |
SC_length (float): Length of the path.
|
94 |
|
95 |
"""
|
96 |
peaks: list
|
97 |
removed_peaks: list
|
98 |
points: list
|
99 |
-
|
100 |
SC_length: float
|
101 |
|
102 |
@dataclass
|
@@ -138,7 +138,7 @@ def find_peaks2(v, distance=5, prominence=0.5):
|
|
138 |
return n_peaks, _
|
139 |
|
140 |
|
141 |
-
def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
142 |
"""
|
143 |
Process traces of cells to extract peak information and organize the data.
|
144 |
|
@@ -152,6 +152,7 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
152 |
path_lengths (list of float): List of path lengths corresponding to the provided paths.
|
153 |
measured_trace_fluorescence (list of list of float): A list containing fluorescence
|
154 |
data corresponding to each path point.
|
|
|
155 |
|
156 |
Returns:
|
157 |
CellData: An object containing organized peak and path data for a given cell.
|
@@ -163,17 +164,17 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
163 |
|
164 |
cell_peaks = []
|
165 |
|
166 |
-
for points,
|
167 |
|
168 |
# For peak determination normalize each trace to have mean zero and s.d. 1
|
169 |
-
|
170 |
|
171 |
# Find peaks - these will be further refined later
|
172 |
-
p,_ = find_peaks2(
|
173 |
peaks = np.array(p, dtype=np.int32)
|
174 |
|
175 |
# Store peak data - using original values, not normalized ones
|
176 |
-
peak_mean_heights = [
|
177 |
peak_points = [ points[u] for u in peaks ]
|
178 |
|
179 |
cell_peaks.append((peaks, peak_points, peak_mean_heights))
|
@@ -188,7 +189,7 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
188 |
to_thin.append(PeakData(pos=cell_peaks[k][1][u], intensity=cell_peaks[k][2][u], key=(k, u)))
|
189 |
|
190 |
# Exclude any peak with a nearby brighter peak (on any SC)
|
191 |
-
removed_peaks, removed_larger_peaks = thin_peaks(to_thin, return_larger_peaks=True)
|
192 |
|
193 |
# Clean up and remove these peaks
|
194 |
new_cell_peaks = []
|
@@ -206,7 +207,7 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
206 |
# What's the larger point?
|
207 |
idx = removed_peaks.index((path_idx, peak_idx))
|
208 |
larger_path, larger_idx = removed_larger_peaks[idx]
|
209 |
-
path_removed_peaks.append(RemovedPeakData(idx=path_peaks[peak_idx],
|
210 |
###
|
211 |
|
212 |
new_cell_peaks.append(path_retained_peaks)
|
@@ -215,15 +216,15 @@ def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence):
|
|
215 |
cell_peaks = new_cell_peaks
|
216 |
pd_list = []
|
217 |
|
218 |
-
# Save peak positions, absolute
|
219 |
for k in range(len(all_paths)):
|
220 |
|
221 |
-
points,
|
222 |
|
223 |
peaks = cell_peaks[k]
|
224 |
removed_peaks = removed_cell_peaks[k]
|
225 |
|
226 |
-
pd = PathData(peaks=peaks, removed_peaks=removed_peaks, points=points,
|
227 |
pd_list.append(pd)
|
228 |
|
229 |
cd = CellData(pathdata_list=pd_list)
|
@@ -235,7 +236,7 @@ alpha_max = 0.4
|
|
235 |
|
236 |
|
237 |
# Criterion used for identifying peak as a focus - normalized (with mean and s.d.)
|
238 |
-
#
|
239 |
def focus_criterion(pos, v, alpha=alpha_max):
|
240 |
"""
|
241 |
Identify and return positions where values in the array `v` exceed a certain threshold.
|
@@ -271,14 +272,14 @@ def analyse_celldata(cell_data, config):
|
|
271 |
- foci_rel_intensity (list): List of relative intensities for the detected foci.
|
272 |
- foci_pos (list): List of absolute positions of the detected foci.
|
273 |
- foci_pos_index (list): List of indices of the detected foci.
|
274 |
-
-
|
275 |
- trace_median_intensities (list): Per-trace median intensity
|
276 |
- trace_thresholds (list): Per-trace absolute threshold for calling peaks as foci
|
277 |
"""
|
278 |
foci_abs_intensity = []
|
279 |
foci_pos = []
|
280 |
foci_pos_index = []
|
281 |
-
|
282 |
trace_median_intensities = []
|
283 |
trace_thresholds = []
|
284 |
|
@@ -296,12 +297,11 @@ def analyse_celldata(cell_data, config):
|
|
296 |
|
297 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
298 |
# by standard deviation - note that the latter should have no effect on the results).
|
299 |
-
h = np.array(path_data.
|
300 |
h = h - np.mean(h)
|
301 |
h = h/np.std(h)
|
302 |
# Extract foci according to criterion
|
303 |
foci_idx = focus_criterion(peaks, h[peaks], peak_threshold)
|
304 |
-
print('peaks', peaks, h[peaks], foci_idx, np.mean(path_data.o_hei10))
|
305 |
|
306 |
#
|
307 |
removed_peaks = path_data.removed_peaks
|
@@ -309,30 +309,30 @@ def analyse_celldata(cell_data, config):
|
|
309 |
|
310 |
|
311 |
if len(peaks):
|
312 |
-
trace_thresholds.append((1-peak_threshold)*np.mean(path_data.
|
313 |
else:
|
314 |
trace_thresholds.append(None)
|
315 |
|
316 |
if len(removed_peaks):
|
317 |
if len(peaks):
|
318 |
-
threshold = (1-peak_threshold)*np.mean(path_data.
|
319 |
else:
|
320 |
threshold = float('-inf')
|
321 |
|
322 |
|
323 |
-
removed_peak_heights = np.array(path_data.
|
324 |
-
|
325 |
|
326 |
-
|
327 |
else:
|
328 |
-
|
329 |
|
330 |
pos_abs = (foci_idx/len(path_data.points))*path_data.SC_length
|
331 |
foci_pos.append(pos_abs)
|
332 |
-
foci_abs_intensity.append(np.array(path_data.
|
333 |
|
334 |
foci_pos_index.append(foci_idx)
|
335 |
-
trace_median_intensities.append(np.median(path_data.
|
336 |
|
337 |
elif threshold_type == 'per-cell':
|
338 |
"""
|
@@ -343,7 +343,7 @@ def analyse_celldata(cell_data, config):
|
|
343 |
|
344 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
345 |
# by standard deviation - note that the latter should have no effect on the results).
|
346 |
-
h = np.array(path_data.
|
347 |
h = h - np.mean(h)
|
348 |
max_cell_intensity = max(max_cell_intensity, np.max(h))
|
349 |
|
@@ -352,7 +352,7 @@ def analyse_celldata(cell_data, config):
|
|
352 |
|
353 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
354 |
# by standard deviation - note that the latter should have no effect on the results).
|
355 |
-
h = np.array(path_data.
|
356 |
h = h - np.mean(h)
|
357 |
|
358 |
foci_idx = peaks[h[peaks]>peak_threshold*max_cell_intensity]
|
@@ -360,33 +360,33 @@ def analyse_celldata(cell_data, config):
|
|
360 |
removed_peaks = path_data.removed_peaks
|
361 |
removed_peaks_idx = np.array([u.idx for u in removed_peaks], dtype=np.int32)
|
362 |
|
363 |
-
trace_thresholds.append(np.mean(path_data.
|
364 |
|
365 |
if len(removed_peaks):
|
366 |
-
threshold = np.mean(path_data.
|
367 |
|
368 |
-
removed_peak_heights = np.array(path_data.
|
369 |
-
|
370 |
|
371 |
-
|
372 |
else:
|
373 |
-
|
374 |
|
375 |
pos_abs = (foci_idx/len(path_data.points))*path_data.SC_length
|
376 |
foci_pos.append(pos_abs)
|
377 |
-
foci_abs_intensity.append(np.array(path_data.
|
378 |
|
379 |
foci_pos_index.append(foci_idx)
|
380 |
-
trace_median_intensities.append(np.median(path_data.
|
381 |
|
382 |
else:
|
383 |
raise NotImplementedError
|
384 |
|
385 |
-
return foci_abs_intensity, foci_pos, foci_pos_index,
|
386 |
|
387 |
def analyse_traces(all_paths, path_lengths, measured_trace_fluorescence, config):
|
388 |
|
389 |
-
cd = process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence)
|
390 |
|
391 |
return analyse_celldata(cd, config)
|
392 |
|
|
|
74 |
|
75 |
Attributes:
|
76 |
idx (int): Index of peak along path
|
77 |
+
screening_peak (tuple): (path_idx, position along path) for screening peak
|
78 |
"""
|
79 |
idx: int
|
80 |
+
screening_peak: tuple
|
81 |
|
82 |
@dataclass
|
83 |
class PathData(object):
|
|
|
86 |
This dataclass encapsulates information about the peaks,
|
87 |
the defining points, the fluorescence values, and the path length of a specific path.
|
88 |
|
89 |
+
Attributes: peaks (list): List of peaks in the path (indicies of positions in points, o_intensity).
|
90 |
removed_peaks (list): List of peaks in the path which have been removed because of a nearby larger peak
|
91 |
points (list): List of points defining the path.
|
92 |
+
o_intensity (list): List of (unnormalized) fluorescence intensity values along the path
|
93 |
SC_length (float): Length of the path.
|
94 |
|
95 |
"""
|
96 |
peaks: list
|
97 |
removed_peaks: list
|
98 |
points: list
|
99 |
+
o_intensity: list
|
100 |
SC_length: float
|
101 |
|
102 |
@dataclass
|
|
|
138 |
return n_peaks, _
|
139 |
|
140 |
|
141 |
+
def process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence, dmin=10):
|
142 |
"""
|
143 |
Process traces of cells to extract peak information and organize the data.
|
144 |
|
|
|
152 |
path_lengths (list of float): List of path lengths corresponding to the provided paths.
|
153 |
measured_trace_fluorescence (list of list of float): A list containing fluorescence
|
154 |
data corresponding to each path point.
|
155 |
+
dmin (float): Distance below which brighter peaks screen less bright ones.
|
156 |
|
157 |
Returns:
|
158 |
CellData: An object containing organized peak and path data for a given cell.
|
|
|
164 |
|
165 |
cell_peaks = []
|
166 |
|
167 |
+
for points, o_intensity in zip(all_paths, measured_trace_fluorescence):
|
168 |
|
169 |
# For peak determination normalize each trace to have mean zero and s.d. 1
|
170 |
+
intensity_normalized = (o_intensity - np.mean(o_intensity))/np.std(o_intensity)
|
171 |
|
172 |
# Find peaks - these will be further refined later
|
173 |
+
p,_ = find_peaks2(intensity_normalized, distance=5, prominence=0.5*np.std(intensity_normalized))
|
174 |
peaks = np.array(p, dtype=np.int32)
|
175 |
|
176 |
# Store peak data - using original values, not normalized ones
|
177 |
+
peak_mean_heights = [ o_intensity[u] for u in peaks ]
|
178 |
peak_points = [ points[u] for u in peaks ]
|
179 |
|
180 |
cell_peaks.append((peaks, peak_points, peak_mean_heights))
|
|
|
189 |
to_thin.append(PeakData(pos=cell_peaks[k][1][u], intensity=cell_peaks[k][2][u], key=(k, u)))
|
190 |
|
191 |
# Exclude any peak with a nearby brighter peak (on any SC)
|
192 |
+
removed_peaks, removed_larger_peaks = thin_peaks(to_thin, return_larger_peaks=True, dmin=dmin)
|
193 |
|
194 |
# Clean up and remove these peaks
|
195 |
new_cell_peaks = []
|
|
|
207 |
# What's the larger point?
|
208 |
idx = removed_peaks.index((path_idx, peak_idx))
|
209 |
larger_path, larger_idx = removed_larger_peaks[idx]
|
210 |
+
path_removed_peaks.append(RemovedPeakData(idx=path_peaks[peak_idx], screening_peak=(larger_path, cell_peaks[larger_path][0][larger_idx])))
|
211 |
###
|
212 |
|
213 |
new_cell_peaks.append(path_retained_peaks)
|
|
|
216 |
cell_peaks = new_cell_peaks
|
217 |
pd_list = []
|
218 |
|
219 |
+
# Save peak positions, absolute intensity intensities, and length for each SC
|
220 |
for k in range(len(all_paths)):
|
221 |
|
222 |
+
points, o_intensity = all_paths[k], measured_trace_fluorescence[k]
|
223 |
|
224 |
peaks = cell_peaks[k]
|
225 |
removed_peaks = removed_cell_peaks[k]
|
226 |
|
227 |
+
pd = PathData(peaks=peaks, removed_peaks=removed_peaks, points=points, o_intensity=o_intensity, SC_length=path_lengths[k])
|
228 |
pd_list.append(pd)
|
229 |
|
230 |
cd = CellData(pathdata_list=pd_list)
|
|
|
236 |
|
237 |
|
238 |
# Criterion used for identifying peak as a focus - normalized (with mean and s.d.)
|
239 |
+
# intensity levels being above 0.4 time maximum peak level
|
240 |
def focus_criterion(pos, v, alpha=alpha_max):
|
241 |
"""
|
242 |
Identify and return positions where values in the array `v` exceed a certain threshold.
|
|
|
272 |
- foci_rel_intensity (list): List of relative intensities for the detected foci.
|
273 |
- foci_pos (list): List of absolute positions of the detected foci.
|
274 |
- foci_pos_index (list): List of indices of the detected foci.
|
275 |
+
- screened_foci_data (list): List of RemovedPeakData indicating positions of removed peaks and the index of the larger peak
|
276 |
- trace_median_intensities (list): Per-trace median intensity
|
277 |
- trace_thresholds (list): Per-trace absolute threshold for calling peaks as foci
|
278 |
"""
|
279 |
foci_abs_intensity = []
|
280 |
foci_pos = []
|
281 |
foci_pos_index = []
|
282 |
+
screened_foci_data = []
|
283 |
trace_median_intensities = []
|
284 |
trace_thresholds = []
|
285 |
|
|
|
297 |
|
298 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
299 |
# by standard deviation - note that the latter should have no effect on the results).
|
300 |
+
h = np.array(path_data.o_intensity)
|
301 |
h = h - np.mean(h)
|
302 |
h = h/np.std(h)
|
303 |
# Extract foci according to criterion
|
304 |
foci_idx = focus_criterion(peaks, h[peaks], peak_threshold)
|
|
|
305 |
|
306 |
#
|
307 |
removed_peaks = path_data.removed_peaks
|
|
|
309 |
|
310 |
|
311 |
if len(peaks):
|
312 |
+
trace_thresholds.append((1-peak_threshold)*np.mean(path_data.o_intensity) + peak_threshold*np.max(np.array(path_data.o_intensity)[peaks]))
|
313 |
else:
|
314 |
trace_thresholds.append(None)
|
315 |
|
316 |
if len(removed_peaks):
|
317 |
if len(peaks):
|
318 |
+
threshold = (1-peak_threshold)*np.mean(path_data.o_intensity) + peak_threshold*np.max(np.array(path_data.o_intensity)[peaks])
|
319 |
else:
|
320 |
threshold = float('-inf')
|
321 |
|
322 |
|
323 |
+
removed_peak_heights = np.array(path_data.o_intensity)[removed_peaks_idx]
|
324 |
+
screened_foci_idx = np.where(removed_peak_heights>threshold)[0]
|
325 |
|
326 |
+
screened_foci_data.append([removed_peaks[i] for i in screened_foci_idx])
|
327 |
else:
|
328 |
+
screened_foci_data.append([])
|
329 |
|
330 |
pos_abs = (foci_idx/len(path_data.points))*path_data.SC_length
|
331 |
foci_pos.append(pos_abs)
|
332 |
+
foci_abs_intensity.append(np.array(path_data.o_intensity)[foci_idx])
|
333 |
|
334 |
foci_pos_index.append(foci_idx)
|
335 |
+
trace_median_intensities.append(np.median(path_data.o_intensity))
|
336 |
|
337 |
elif threshold_type == 'per-cell':
|
338 |
"""
|
|
|
343 |
|
344 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
345 |
# by standard deviation - note that the latter should have no effect on the results).
|
346 |
+
h = np.array(path_data.o_intensity)
|
347 |
h = h - np.mean(h)
|
348 |
max_cell_intensity = max(max_cell_intensity, np.max(h))
|
349 |
|
|
|
352 |
|
353 |
# Normalize extracted fluorescent intensities by subtracting mean (and dividing
|
354 |
# by standard deviation - note that the latter should have no effect on the results).
|
355 |
+
h = np.array(path_data.o_intensity)
|
356 |
h = h - np.mean(h)
|
357 |
|
358 |
foci_idx = peaks[h[peaks]>peak_threshold*max_cell_intensity]
|
|
|
360 |
removed_peaks = path_data.removed_peaks
|
361 |
removed_peaks_idx = np.array([u.idx for u in removed_peaks], dtype=np.int32)
|
362 |
|
363 |
+
trace_thresholds.append(np.mean(path_data.o_intensity) + peak_threshold*max_cell_intensity)
|
364 |
|
365 |
if len(removed_peaks):
|
366 |
+
threshold = np.mean(path_data.o_intensity) + peak_threshold*max_cell_intensity
|
367 |
|
368 |
+
removed_peak_heights = np.array(path_data.o_intensity)[removed_peaks_idx]
|
369 |
+
screened_foci_idx = np.where(removed_peak_heights>threshold)[0]
|
370 |
|
371 |
+
screened_foci_data.append([removed_peaks[i] for i in screened_foci_idx])
|
372 |
else:
|
373 |
+
screened_foci_data.append([])
|
374 |
|
375 |
pos_abs = (foci_idx/len(path_data.points))*path_data.SC_length
|
376 |
foci_pos.append(pos_abs)
|
377 |
+
foci_abs_intensity.append(np.array(path_data.o_intensity)[foci_idx])
|
378 |
|
379 |
foci_pos_index.append(foci_idx)
|
380 |
+
trace_median_intensities.append(np.median(path_data.o_intensity))
|
381 |
|
382 |
else:
|
383 |
raise NotImplementedError
|
384 |
|
385 |
+
return foci_abs_intensity, foci_pos, foci_pos_index, screened_foci_data, trace_median_intensities, trace_thresholds
|
386 |
|
387 |
def analyse_traces(all_paths, path_lengths, measured_trace_fluorescence, config):
|
388 |
|
389 |
+
cd = process_cell_traces(all_paths, path_lengths, measured_trace_fluorescence, dmin=config['screening_distance'])
|
390 |
|
391 |
return analyse_celldata(cd, config)
|
392 |
|
tests/test_analyse.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
|
2 |
import pytest
|
3 |
from path_analysis.analyse import *
|
|
|
4 |
import numpy as np
|
5 |
from math import pi
|
6 |
import xml.etree.ElementTree as ET
|
@@ -99,7 +100,7 @@ def test_get_paths_from_traces_file():
|
|
99 |
def test_measure_chrom2():
|
100 |
# Mock data
|
101 |
path = [(2, 3, 4), (4, 5, 6), (9, 9, 9)] # Sample ordered path points
|
102 |
-
|
103 |
config = {
|
104 |
'z_res': 1,
|
105 |
'xy_res': 0.5,
|
@@ -107,7 +108,7 @@ def test_measure_chrom2():
|
|
107 |
}
|
108 |
|
109 |
# Function call
|
110 |
-
_, measurements, measurements_max = measure_chrom2(path,
|
111 |
|
112 |
# Assertions
|
113 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
@@ -118,7 +119,7 @@ def test_measure_chrom2():
|
|
118 |
def test_measure_chrom2_z():
|
119 |
# Mock data
|
120 |
path = [(2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
121 |
-
_,_,
|
122 |
config = {
|
123 |
'z_res': 1,
|
124 |
'xy_res': 0.5,
|
@@ -126,7 +127,7 @@ def test_measure_chrom2_z():
|
|
126 |
}
|
127 |
|
128 |
# Function call
|
129 |
-
_, measurements, measurements_max = measure_chrom2(path,
|
130 |
|
131 |
# Assertions
|
132 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
@@ -137,7 +138,7 @@ def test_measure_chrom2_z():
|
|
137 |
def test_measure_chrom2_z2():
|
138 |
# Mock data
|
139 |
path = [(0,0,0), (2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
140 |
-
_,_,
|
141 |
config = {
|
142 |
'z_res': 0.25,
|
143 |
'xy_res': 0.5,
|
@@ -145,7 +146,7 @@ def test_measure_chrom2_z2():
|
|
145 |
}
|
146 |
|
147 |
# Function call
|
148 |
-
_, measurements, measurements_max = measure_chrom2(path,
|
149 |
|
150 |
# Assertions
|
151 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
@@ -283,31 +284,87 @@ def test_make_sphere_equal():
|
|
283 |
|
284 |
import pandas as pd
|
285 |
|
286 |
-
|
287 |
-
# 1. Test basic functionality
|
288 |
def test_extract_peaks_basic():
|
289 |
-
cell_id = 1
|
290 |
-
all_paths = [[[0, 0], [1, 1]]]
|
291 |
path_lengths = [1.41] # length of the above path
|
292 |
measured_traces = [[100, 200]] # fluorescence along the path
|
293 |
-
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'use_corrected_positions': True}
|
294 |
|
295 |
-
df,
|
296 |
-
|
297 |
-
# Now add your assertions to validate the result
|
298 |
assert len(df) == 1, "Expected one row in DataFrame"
|
299 |
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
301 |
|
302 |
-
# 2. Test multiple paths
|
303 |
def test_extract_peaks_multiple_paths():
|
304 |
cell_id = 1
|
305 |
-
all_paths = [[[0, 0], [1, 1]], [[1, 1], [2, 2]]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
306 |
path_lengths = [1.41, 1.41]
|
307 |
measured_traces = [[100, 200], [100, 150]]
|
308 |
-
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'use_corrected_positions': True}
|
309 |
|
310 |
-
df,
|
311 |
|
|
|
|
|
312 |
assert len(df) == 2, "Expected two rows in DataFrame"
|
313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
|
2 |
import pytest
|
3 |
from path_analysis.analyse import *
|
4 |
+
from path_analysis.data_preprocess import RemovedPeakData
|
5 |
import numpy as np
|
6 |
from math import pi
|
7 |
import xml.etree.ElementTree as ET
|
|
|
100 |
def test_measure_chrom2():
|
101 |
# Mock data
|
102 |
path = [(2, 3, 4), (4, 5, 6), (9, 9, 9)] # Sample ordered path points
|
103 |
+
intensity = np.random.rand(10, 10, 10) # Random 3D fluorescence data
|
104 |
config = {
|
105 |
'z_res': 1,
|
106 |
'xy_res': 0.5,
|
|
|
108 |
}
|
109 |
|
110 |
# Function call
|
111 |
+
_, measurements, measurements_max = measure_chrom2(path, intensity, config)
|
112 |
|
113 |
# Assertions
|
114 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
|
|
119 |
def test_measure_chrom2_z():
|
120 |
# Mock data
|
121 |
path = [(2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
122 |
+
_,_,intensity = np.meshgrid(np.arange(10), np.arange(10), np.arange(10)) # 3D fluorescence data - z dependent
|
123 |
config = {
|
124 |
'z_res': 1,
|
125 |
'xy_res': 0.5,
|
|
|
127 |
}
|
128 |
|
129 |
# Function call
|
130 |
+
_, measurements, measurements_max = measure_chrom2(path, intensity, config)
|
131 |
|
132 |
# Assertions
|
133 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
|
|
138 |
def test_measure_chrom2_z2():
|
139 |
# Mock data
|
140 |
path = [(0,0,0), (2, 3, 4), (4, 5, 6)] # Sample ordered path points
|
141 |
+
_,_,intensity = np.meshgrid(np.arange(10), np.arange(10), np.arange(10)) # 3D fluorescence data - z dependent
|
142 |
config = {
|
143 |
'z_res': 0.25,
|
144 |
'xy_res': 0.5,
|
|
|
146 |
}
|
147 |
|
148 |
# Function call
|
149 |
+
_, measurements, measurements_max = measure_chrom2(path, intensity, config)
|
150 |
|
151 |
# Assertions
|
152 |
assert len(measurements) == len(path), "Measurements length should match path length"
|
|
|
284 |
|
285 |
import pandas as pd
|
286 |
|
|
|
|
|
287 |
def test_extract_peaks_basic():
|
288 |
+
cell_id = 1 # Simple per-cell tag
|
289 |
+
all_paths = [[[0, 0, 0], [1, 1, 0]]] # Single, simple path
|
290 |
path_lengths = [1.41] # length of the above path
|
291 |
measured_traces = [[100, 200]] # fluorescence along the path
|
292 |
+
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'threshold_type':'per-cell', 'use_corrected_positions': True, 'screening_distance':10 }
|
293 |
|
294 |
+
df, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config)
|
295 |
+
|
|
|
296 |
assert len(df) == 1, "Expected one row in DataFrame"
|
297 |
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
298 |
+
assert list(df['Trace_foci_number']) == [1], "Wrong foci number"
|
299 |
+
assert df['Foci_1_position(um)'].iloc[0] == np.sqrt(2)
|
300 |
+
assert foci_pos_index == [[1]]
|
301 |
+
assert foci_absolute_intensity == [[200]]
|
302 |
+
assert screened_foci_data == [[]]
|
303 |
+
assert trace_thresholds == [ [ 150+0.4*50] ]
|
304 |
+
assert np.all(trace_positions[0] == np.array([0, np.sqrt(2)]))
|
305 |
|
|
|
306 |
def test_extract_peaks_multiple_paths():
|
307 |
cell_id = 1
|
308 |
+
all_paths = [[[0, 0, 0], [1, 1, 0]], [[1, 1, 200], [2, 2, 200]]]
|
309 |
+
path_lengths = [1.41, 1.41]
|
310 |
+
measured_traces = [[100, 200], [100, 140]]
|
311 |
+
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'threshold_type':'per-trace', 'use_corrected_positions': True, 'screening_distance':10 }
|
312 |
+
|
313 |
+
df, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config)
|
314 |
+
|
315 |
+
|
316 |
+
|
317 |
+
assert len(df) == 2, "Expected two rows in DataFrame"
|
318 |
+
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
319 |
+
assert list(df['Trace_foci_number']) == [1,1], "Wrong foci number"
|
320 |
+
assert df['Foci_1_position(um)'].iloc[0] == np.sqrt(2)
|
321 |
+
print(foci_pos_index)
|
322 |
+
assert list(map(list, foci_pos_index)) == [[1],[1]]
|
323 |
+
assert list(map(list, foci_absolute_intensity)) == [[200],[140]]
|
324 |
+
assert trace_thresholds == [ 150+0.4*50, 120+0.4*20 ]
|
325 |
+
assert np.all(trace_positions[0] == np.array([0, np.sqrt(2)]))
|
326 |
+
assert screened_foci_data == [[],[]]
|
327 |
+
|
328 |
+
def test_extract_peaks_multiple_paths_screened():
|
329 |
+
cell_id = 1
|
330 |
+
all_paths = [[[0, 0, 0], [1, 1, 0]], [[1, 1, 2], [2, 2, 2]]]
|
331 |
path_lengths = [1.41, 1.41]
|
332 |
measured_traces = [[100, 200], [100, 150]]
|
333 |
+
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'threshold_type':'per-trace', 'use_corrected_positions': True, 'screening_distance':10 }
|
334 |
|
335 |
+
df, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config)
|
336 |
|
337 |
+
|
338 |
+
|
339 |
assert len(df) == 2, "Expected two rows in DataFrame"
|
340 |
+
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
341 |
+
assert list(df['Trace_foci_number']) == [1,0], "Wrong foci number"
|
342 |
+
assert df['Foci_1_position(um)'].iloc[0] == np.sqrt(2)
|
343 |
+
print(foci_pos_index)
|
344 |
+
assert list(map(list, foci_pos_index)) == [[1],[]]
|
345 |
+
assert list(map(list, foci_absolute_intensity)) == [[200],[]]
|
346 |
+
assert trace_thresholds == [ 150+0.4*50, None ]
|
347 |
+
assert np.all(trace_positions[0] == np.array([0, np.sqrt(2)]))
|
348 |
+
assert screened_foci_data == [[],[RemovedPeakData(idx=1, screening_peak=(0,1))]]
|
349 |
+
|
350 |
+
|
351 |
+
def test_extract_peaks_multiple_paths_per_cell():
|
352 |
+
cell_id = 1
|
353 |
+
all_paths = [[[0, 0, 0], [1, 1, 0]], [[1, 1, 200], [2, 2, 200]]]
|
354 |
+
path_lengths = [1.41, 1.41]
|
355 |
+
measured_traces = [[100, 200], [100, 140]]
|
356 |
+
config = {'peak_threshold': 0.4, 'sphere_radius': 2, 'xy_res': 1, 'z_res': 1, 'threshold_type':'per-cell', 'use_corrected_positions': True, 'screening_distance':10 }
|
357 |
+
|
358 |
+
df, foci_absolute_intensity, foci_pos_index, screened_foci_data, trace_thresholds, trace_positions = extract_peaks(cell_id, all_paths, path_lengths, measured_traces, config)
|
359 |
+
|
360 |
+
|
361 |
+
|
362 |
+
assert len(df) == 2, "Expected two rows in DataFrame"
|
363 |
+
assert df['Cell_ID'].iloc[0] == cell_id, "Unexpected cell_id"
|
364 |
+
assert list(df['Trace_foci_number']) == [1,0], "Wrong foci number"
|
365 |
+
assert df['Foci_1_position(um)'].iloc[0] == np.sqrt(2)
|
366 |
+
assert list(map(list, foci_pos_index)) == [[1],[]]
|
367 |
+
assert list(map(list, foci_absolute_intensity)) == [[200],[]]
|
368 |
+
assert trace_thresholds == [ 150+0.4*50, 120+0.4*50 ]
|
369 |
+
assert np.all(trace_positions[0] == np.array([0, np.sqrt(2)]))
|
370 |
+
assert screened_foci_data == [[],[]]
|
tests/test_preprocess.py
CHANGED
@@ -128,8 +128,8 @@ def test_process_cell_traces_peaks(mock_data):
|
|
128 |
# Mock data
|
129 |
@pytest.fixture
|
130 |
def mock_celldata():
|
131 |
-
pathdata1 = PathData(peaks=[0, 5], points=[(0,0,0), (0,2,0), (0,5,0), (0,10,0), (0,15,0), (0,20,0)], removed_peaks=[],
|
132 |
-
pathdata2 = PathData(peaks=[2], points=[(1,20,0), (1,20,10), (1,20,20) ], removed_peaks=[RemovedPeakData(0, (0,5))],
|
133 |
return CellData(pathdata_list=[pathdata1, pathdata2])
|
134 |
|
135 |
def test_analyse_celldata(mock_celldata):
|
|
|
128 |
# Mock data
|
129 |
@pytest.fixture
|
130 |
def mock_celldata():
|
131 |
+
pathdata1 = PathData(peaks=[0, 5], points=[(0,0,0), (0,2,0), (0,5,0), (0,10,0), (0,15,0), (0,20,0)], removed_peaks=[], o_intensity=[100, 8, 3, 2, 3, 69], SC_length=2.2)
|
132 |
+
pathdata2 = PathData(peaks=[2], points=[(1,20,0), (1,20,10), (1,20,20) ], removed_peaks=[RemovedPeakData(0, (0,5))], o_intensity=[38, 2, 20], SC_length=2.3)
|
133 |
return CellData(pathdata_list=[pathdata1, pathdata2])
|
134 |
|
135 |
def test_analyse_celldata(mock_celldata):
|