celldetective 1.5.0b8__py3-none-any.whl → 1.5.0b10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tests/test_measure.py CHANGED
@@ -1,141 +1,243 @@
1
1
  import unittest
2
2
  import pandas as pd
3
3
  import numpy as np
4
- from celldetective.measure import measure_features, measure_isotropic_intensity, drop_tonal_features
4
+ from celldetective.measure import (
5
+ measure_features,
6
+ measure_isotropic_intensity,
7
+ drop_tonal_features,
8
+ )
5
9
 
6
- class TestFeatureMeasurement(unittest.TestCase):
7
-
8
- """
9
- To do: test spot detection, fluo normalization and peripheral measurements
10
- """
11
-
12
- @classmethod
13
- def setUpClass(self):
14
-
15
- # Simple mock data, 100px*100px, one channel, value is one, uniform
16
- # Two objects in labels map
17
-
18
- self.frame = np.ones((100,100,1), dtype=float)
19
- self.labels = np.zeros((100,100), dtype=int)
20
- self.labels[50:55,50:55] = 1
21
- self.labels[0:10,0:10] = 2
22
-
23
- self.feature_measurements = measure_features(
24
- self.frame,
25
- self.labels,
26
- features=['intensity_mean','area',],
27
- channels=['test_channel']
28
- )
29
-
30
- self.feature_measurements_no_image = measure_features(
31
- None,
32
- self.labels,
33
- features=['intensity_mean','area',],
34
- channels=None
35
- )
36
-
37
- self.feature_measurements_no_features = measure_features(
38
- self.frame,
39
- self.labels,
40
- features=None,
41
- channels=['test_channel'],
42
- )
43
-
44
- # With image
45
- def test_measure_yields_table(self):
46
- self.assertIsInstance(self.feature_measurements, pd.DataFrame)
47
-
48
- def test_two_objects(self):
49
- self.assertEqual(len(self.feature_measurements),2)
50
-
51
- def test_channel_named_correctly(self):
52
- self.assertIn('test_channel_mean',list(self.feature_measurements.columns))
53
10
 
54
- def test_intensity_is_one(self):
55
- self.assertTrue(np.all([v==1.0 for v in self.feature_measurements['test_channel_mean'].values]))
56
-
57
- def test_area_first_is_twenty_five(self):
58
- self.assertEqual(self.feature_measurements['area'].values[0],25)
59
-
60
- def test_area_second_is_hundred(self):
61
- self.assertEqual(self.feature_measurements['area'].values[1],100)
62
-
63
- # Without image
64
- def test_measure_yields_table(self):
65
- self.assertIsInstance(self.feature_measurements_no_image, pd.DataFrame)
66
-
67
- def test_two_objects(self):
68
- self.assertEqual(len(self.feature_measurements_no_image),2)
69
-
70
- def test_channel_not_in_table(self):
71
- self.assertNotIn('test_channel_mean',list(self.feature_measurements_no_image.columns))
72
-
73
- # With no features
74
- def test_only_one_measurement(self):
75
- cols = list(self.feature_measurements_no_features.columns)
76
- assert 'class_id' in cols and len(cols)==1
11
+ class TestFeatureMeasurement(unittest.TestCase):
12
+ """
13
+ To do: test spot detection, fluo normalization and peripheral measurements
14
+ """
15
+
16
+ @classmethod
17
+ def setUpClass(self):
18
+
19
+ # Simple mock data, 100px*100px, one channel, value is one, uniform
20
+ # Two objects in labels map
21
+
22
+ self.frame = np.ones((100, 100, 1), dtype=float)
23
+ self.labels = np.zeros((100, 100), dtype=int)
24
+ self.labels[50:55, 50:55] = 1
25
+ self.labels[0:10, 0:10] = 2
26
+
27
+ self.feature_measurements = measure_features(
28
+ self.frame,
29
+ self.labels,
30
+ features=[
31
+ "intensity_mean",
32
+ "area",
33
+ ],
34
+ channels=["test_channel"],
35
+ )
36
+
37
+ self.feature_measurements_no_image = measure_features(
38
+ None,
39
+ self.labels,
40
+ features=[
41
+ "intensity_mean",
42
+ "area",
43
+ ],
44
+ channels=None,
45
+ )
46
+
47
+ self.feature_measurements_no_features = measure_features(
48
+ self.frame,
49
+ self.labels,
50
+ features=None,
51
+ channels=["test_channel"],
52
+ )
53
+
54
+ # With image
55
+ def test_measure_yields_table(self):
56
+ self.assertIsInstance(self.feature_measurements, pd.DataFrame)
57
+
58
+ def test_two_objects(self):
59
+ self.assertEqual(len(self.feature_measurements), 2)
60
+
61
+ def test_channel_named_correctly(self):
62
+ self.assertIn("test_channel_mean", list(self.feature_measurements.columns))
63
+
64
+ def test_intensity_is_one(self):
65
+ self.assertTrue(
66
+ np.all(
67
+ [
68
+ v == 1.0
69
+ for v in self.feature_measurements["test_channel_mean"].values
70
+ ]
71
+ )
72
+ )
73
+
74
+ def test_area_first_is_twenty_five(self):
75
+ self.assertEqual(self.feature_measurements["area"].values[0], 25)
76
+
77
+ def test_area_second_is_hundred(self):
78
+ self.assertEqual(self.feature_measurements["area"].values[1], 100)
79
+
80
+ # Without image
81
+ def test_measure_yields_table(self):
82
+ self.assertIsInstance(self.feature_measurements_no_image, pd.DataFrame)
83
+
84
+ def test_two_objects(self):
85
+ self.assertEqual(len(self.feature_measurements_no_image), 2)
86
+
87
+ def test_channel_not_in_table(self):
88
+ self.assertNotIn(
89
+ "test_channel_mean", list(self.feature_measurements_no_image.columns)
90
+ )
91
+
92
+ # With no features
93
+ def test_only_one_measurement(self):
94
+ cols = list(self.feature_measurements_no_features.columns)
95
+ assert "class_id" in cols and len(cols) == 1
77
96
 
78
97
 
79
98
  class TestIsotropicMeasurement(unittest.TestCase):
99
+ """
100
+
101
+ Test that isotropic intensity measurements behave as expected on fake image
102
+
103
+ """
104
+
105
+ @classmethod
106
+ def setUpClass(self):
107
+
108
+ # Simple mock data, 100px*100px, one channel, value is one
109
+ # Square (21*21px) of value 0. in middle
110
+ # Two objects in labels map
111
+
112
+ self.frame = np.ones((100, 100, 1), dtype=float)
113
+ self.frame[40:61, 40:61, 0] = 0.0
114
+ self.positions = pd.DataFrame(
115
+ [
116
+ {
117
+ "TRACK_ID": 0,
118
+ "POSITION_X": 50,
119
+ "POSITION_Y": 50,
120
+ "FRAME": 0,
121
+ "class_id": 0,
122
+ }
123
+ ]
124
+ )
125
+
126
+ self.inner_radius = 9
127
+ self.upper_radius = 20
128
+ self.safe_upper_radius = int(21 // 2 * np.sqrt(2)) + 2
129
+
130
+ self.iso_measurements = measure_isotropic_intensity(
131
+ self.positions,
132
+ self.frame,
133
+ channels=["test_channel"],
134
+ intensity_measurement_radii=[self.inner_radius, self.upper_radius],
135
+ operations=["mean"],
136
+ )
137
+ self.iso_measurements_ring = measure_isotropic_intensity(
138
+ self.positions,
139
+ self.frame,
140
+ channels=["test_channel"],
141
+ intensity_measurement_radii=[
142
+ [self.safe_upper_radius, self.safe_upper_radius + 3]
143
+ ],
144
+ operations=["mean"],
145
+ )
146
+
147
+ def test_measure_yields_table(self):
148
+ self.assertIsInstance(self.iso_measurements, pd.DataFrame)
149
+
150
+ def test_intensity_zero_in_small_circle(self):
151
+ self.assertEqual(
152
+ self.iso_measurements[
153
+ f"test_channel_circle_{self.inner_radius}_mean"
154
+ ].values[0],
155
+ 0.0,
156
+ )
157
+
158
+ def test_intensity_greater_than_zero_in_intermediate_circle(self):
159
+ self.assertGreater(
160
+ self.iso_measurements[
161
+ f"test_channel_circle_{self.upper_radius}_mean"
162
+ ].values[0],
163
+ 0.0,
164
+ )
165
+
166
+ def test_ring_measurement_avoids_zero(self):
167
+ self.assertEqual(
168
+ self.iso_measurements[
169
+ f"test_channel_ring_{self.safe_upper_radius}_{self.safe_upper_radius+3}_mean"
170
+ ].values[0],
171
+ 1.0,
172
+ )
80
173
 
81
- """
82
-
83
- Test that isotropic intensity measurements behave as expected on fake image
84
-
85
- """
86
-
87
- @classmethod
88
- def setUpClass(self):
89
-
90
- # Simple mock data, 100px*100px, one channel, value is one
91
- # Square (21*21px) of value 0. in middle
92
- # Two objects in labels map
93
-
94
- self.frame = np.ones((100,100,1), dtype=float)
95
- self.frame[40:61,40:61,0] = 0.
96
- self.positions = pd.DataFrame([{'TRACK_ID': 0, 'POSITION_X': 50, 'POSITION_Y': 50, 'FRAME': 0, 'class_id': 0}])
97
-
98
- self.inner_radius = 9
99
- self.upper_radius = 20
100
- self.safe_upper_radius = int(21//2*np.sqrt(2))+2
101
-
102
- self.iso_measurements = measure_isotropic_intensity(self.positions,
103
- self.frame,
104
- channels=['test_channel'],
105
- intensity_measurement_radii=[self.inner_radius, self.upper_radius],
106
- operations = ['mean'],
107
- )
108
- self.iso_measurements_ring = measure_isotropic_intensity(
109
- self.positions,
110
- self.frame,
111
- channels=['test_channel'],
112
- intensity_measurement_radii=[[self.safe_upper_radius, self.safe_upper_radius+3]],
113
- operations = ['mean'],
114
- )
115
-
116
-
117
- def test_measure_yields_table(self):
118
- self.assertIsInstance(self.iso_measurements, pd.DataFrame)
119
-
120
- def test_intensity_zero_in_small_circle(self):
121
- self.assertEqual(self.iso_measurements[f'test_channel_circle_{self.inner_radius}_mean'].values[0],0.)
122
-
123
- def test_intensity_greater_than_zero_in_intermediate_circle(self):
124
- self.assertGreater(self.iso_measurements[f'test_channel_circle_{self.upper_radius}_mean'].values[0],0.)
125
-
126
- def test_ring_measurement_avoids_zero(self):
127
- self.assertEqual(self.iso_measurements[f'test_channel_ring_{self.safe_upper_radius}_{self.safe_upper_radius+3}_mean'].values[0],1.0)
128
174
 
129
175
  class TestDropTonal(unittest.TestCase):
130
176
 
131
- @classmethod
132
- def setUpClass(self):
133
- self.features = ['area', 'intensity_mean', 'intensity_max']
134
-
135
- def test_drop_tonal(self):
136
- self.features_processed = drop_tonal_features(self.features)
137
- self.assertEqual(self.features_processed,['area'])
138
-
139
-
140
- if __name__=="__main__":
141
- unittest.main()
177
+ @classmethod
178
+ def setUpClass(self):
179
+ self.features = ["area", "intensity_mean", "intensity_max"]
180
+
181
+ def test_drop_tonal(self):
182
+ self.features_processed = drop_tonal_features(self.features)
183
+ self.assertEqual(self.features_processed, ["area"])
184
+
185
+
186
+ class TestNumpyArrayHandling(unittest.TestCase):
187
+ """
188
+ Regression test for bug where passing channels as numpy array caused AttributeError.
189
+ Fix: 'numpy.ndarray' object has no attribute 'index'
190
+ """
191
+
192
+ @classmethod
193
+ def setUpClass(self):
194
+ self.frame = np.ones((100, 100, 2), dtype=float)
195
+ self.labels = np.zeros((100, 100), dtype=int)
196
+ self.labels[50:60, 50:60] = 1
197
+
198
+ # KEY: Pass channels as numpy array to trigger the potential bug
199
+ self.channels = np.array(["channel_1", "channel_2"])
200
+
201
+ def test_measure_features_with_numpy_channels(self):
202
+ """
203
+ Test that measure_features works when channels is a numpy array.
204
+ Prevents regression of AttributeError: 'numpy.ndarray' object has no attribute 'index'
205
+ """
206
+ try:
207
+ df = measure_features(
208
+ self.frame,
209
+ self.labels,
210
+ features=["intensity_mean"],
211
+ channels=self.channels,
212
+ )
213
+ self.assertIsInstance(df, pd.DataFrame)
214
+ self.assertIn("channel_1_mean", df.columns)
215
+ except AttributeError as e:
216
+ self.fail(f"measure_features failed with numpy array channels: {e}")
217
+
218
+ def test_spot_detection_with_numpy_channels_match(self):
219
+ """
220
+ Test spot detection logic with numpy array channels.
221
+ The bug also appeared in spot detection channel matching.
222
+ """
223
+ spot_opts = {
224
+ "channel": "channel_1", # Matches one of the channels
225
+ "diameter": 5,
226
+ "threshold": 0.1,
227
+ }
228
+
229
+ try:
230
+ # Should not raise AttributeError
231
+ df = measure_features(
232
+ self.frame,
233
+ self.labels,
234
+ channels=self.channels,
235
+ spot_detection=spot_opts,
236
+ )
237
+ self.assertIsInstance(df, pd.DataFrame)
238
+ except AttributeError as e:
239
+ self.fail(f"Spot detection failed with numpy array channels: {e}")
240
+
241
+
242
+ if __name__ == "__main__":
243
+ unittest.main()
tests/test_signals.py CHANGED
@@ -1,135 +1,154 @@
1
1
  import unittest
2
- import matplotlib.pyplot as plt
3
2
  import numpy as np
4
3
  import os
5
4
  import random
6
5
  import pandas as pd
7
6
  import shutil
8
7
 
9
- def sigmoid(t,t0,dt,A,offset):
10
- return A/(1+np.exp(-(t-t0)/dt)) + offset
8
+
9
+ def sigmoid(t, t0, dt, A, offset):
10
+ return A / (1 + np.exp(-(t - t0) / dt)) + offset
11
+
11
12
 
12
13
  def generate_fake_signal_data(n_signals):
13
-
14
- timeline = np.linspace(0,100,100)
15
- amplitudes = list(np.linspace(2000,3000,100))
16
- slopes = list(np.linspace(0.5,5,100))
17
- means = list(np.linspace(-100,200,100))
18
- random_cut = list(np.linspace(25,200,176,dtype=int))
19
- noise_levels = list(np.linspace(1,100,100,dtype=int))
20
-
21
- trajectories = []
22
- for i in range(n_signals):
23
-
24
- a = random.sample(amplitudes,k=1)[0]
25
- dt = random.sample(slopes,k=1)[0]
26
- mu = random.sample(means,k=1)[0]
27
- cut = random.sample(random_cut,k=1)[0]
28
- n = random.sample(noise_levels,k=1)[0]
29
-
30
- if mu<=0.:
31
- cclass=2
32
- t0=-1
33
- elif (mu>0)*(mu<=100):
34
- cclass=0
35
- t0=mu
36
- else:
37
- cclass=1
38
- t0=-1
39
-
40
- noise = [random.random()*n for i in range(len(timeline))]
41
- signal = sigmoid(timeline, mu, dt,a,0)+noise
42
- signal = signal[:cut]
43
- if mu>=cut:
44
- cclass=1
45
- t0=-1
46
-
47
- for j in range(len(signal)):
48
- trajectories.append({'TRACK_ID': i, 'POSITION_X': 0., 'POSITION_Y': 0., 'FRAME': j,'signal': signal[j], 't0': t0, 'cclass': cclass})
49
-
50
- trajectories = pd.DataFrame(trajectories)
51
-
52
- return trajectories
53
-
54
- def export_set(trajectories, name='set.npy', output_folder='.'):
55
-
56
- training_set = []
57
- cols = trajectories.columns
58
- tracks = np.unique(trajectories["TRACK_ID"].to_numpy())
59
-
60
- for track in tracks:
61
- signals = {}
62
- for c in cols:
63
- signals.update({c: trajectories.loc[trajectories["TRACK_ID"] == track, c].to_numpy()})
64
- time_of_interest = trajectories.loc[trajectories["TRACK_ID"] == track, "t0"].to_numpy()[0]
65
- cclass = trajectories.loc[trajectories["TRACK_ID"] == track, "cclass"].to_numpy()[0]
66
- signals.update({"time_of_interest": time_of_interest, "class": cclass})
67
- training_set.append(signals)
68
-
69
- np.save(os.sep.join([output_folder,name]), training_set)
14
+
15
+ timeline = np.linspace(0, 100, 100)
16
+ amplitudes = list(np.linspace(2000, 3000, 100))
17
+ slopes = list(np.linspace(0.5, 5, 100))
18
+ means = list(np.linspace(-100, 200, 100))
19
+ random_cut = list(np.linspace(25, 200, 176, dtype=int))
20
+ noise_levels = list(np.linspace(1, 100, 100, dtype=int))
21
+
22
+ trajectories = []
23
+ for i in range(n_signals):
24
+
25
+ a = random.sample(amplitudes, k=1)[0]
26
+ dt = random.sample(slopes, k=1)[0]
27
+ mu = random.sample(means, k=1)[0]
28
+ cut = random.sample(random_cut, k=1)[0]
29
+ n = random.sample(noise_levels, k=1)[0]
30
+
31
+ if mu <= 0.0:
32
+ cclass = 2
33
+ t0 = -1
34
+ elif (mu > 0) * (mu <= 100):
35
+ cclass = 0
36
+ t0 = mu
37
+ else:
38
+ cclass = 1
39
+ t0 = -1
40
+
41
+ noise = [random.random() * n for i in range(len(timeline))]
42
+ signal = sigmoid(timeline, mu, dt, a, 0) + noise
43
+ signal = signal[:cut]
44
+ if mu >= cut:
45
+ cclass = 1
46
+ t0 = -1
47
+
48
+ for j in range(len(signal)):
49
+ trajectories.append(
50
+ {
51
+ "TRACK_ID": i,
52
+ "POSITION_X": 0.0,
53
+ "POSITION_Y": 0.0,
54
+ "FRAME": j,
55
+ "signal": signal[j],
56
+ "t0": t0,
57
+ "cclass": cclass,
58
+ }
59
+ )
60
+
61
+ trajectories = pd.DataFrame(trajectories)
62
+
63
+ return trajectories
64
+
65
+
66
+ def export_set(trajectories, name="set.npy", output_folder="."):
67
+
68
+ training_set = []
69
+ cols = trajectories.columns
70
+ tracks = np.unique(trajectories["TRACK_ID"].to_numpy())
71
+
72
+ for track in tracks:
73
+ signals = {}
74
+ for c in cols:
75
+ signals.update(
76
+ {c: trajectories.loc[trajectories["TRACK_ID"] == track, c].to_numpy()}
77
+ )
78
+ time_of_interest = trajectories.loc[
79
+ trajectories["TRACK_ID"] == track, "t0"
80
+ ].to_numpy()[0]
81
+ cclass = trajectories.loc[
82
+ trajectories["TRACK_ID"] == track, "cclass"
83
+ ].to_numpy()[0]
84
+ signals.update({"time_of_interest": time_of_interest, "class": cclass})
85
+ training_set.append(signals)
86
+
87
+ np.save(os.sep.join([output_folder, name]), training_set)
70
88
 
71
89
 
72
90
  class TestCreateSignalModel(unittest.TestCase):
73
91
 
74
- def test_create_model(self):
92
+ def test_create_model(self):
75
93
 
76
- from celldetective.event_detection_models import SignalDetectionModel
94
+ from celldetective.event_detection_models import SignalDetectionModel
77
95
 
78
96
  model = SignalDetectionModel(
79
- channel_option=["signal"],
80
- model_signal_length=128,
81
- n_channels=1,
82
- n_conv=2,
83
- n_classes=3,
84
- dense_collection=512,
85
- dropout_rate=0.1,
86
- label='test',
87
- )
97
+ channel_option=["signal"],
98
+ model_signal_length=128,
99
+ n_channels=1,
100
+ n_conv=2,
101
+ n_classes=3,
102
+ dense_collection=512,
103
+ dropout_rate=0.1,
104
+ label="test",
105
+ )
88
106
 
89
107
 
90
108
  class TestTrainSignalModel(unittest.TestCase):
91
109
 
92
- @classmethod
93
- def setUpClass(self):
110
+ @classmethod
111
+ def setUpClass(self):
94
112
 
95
- from celldetective.event_detection_models import SignalDetectionModel
113
+ from celldetective.event_detection_models import SignalDetectionModel
96
114
 
97
115
  self.trajectories = generate_fake_signal_data(300)
98
- if not os.path.exists('temp'):
99
- os.mkdir('temp')
100
- export_set(self.trajectories, name='set.npy', output_folder='temp')
101
- self.model = SignalDetectionModel(
102
- channel_option=["signal"],
103
- model_signal_length=128,
104
- n_channels=1,
105
- n_conv=2,
106
- n_classes=3,
107
- dense_collection=512,
108
- dropout_rate=0.1,
109
- label='test',
110
- )
111
-
112
- def test_train_signal_model(self):
113
-
114
- self.model.fit_from_directory(
115
- ['temp'],
116
- normalize=True,
117
- normalization_percentile=None,
118
- normalization_values = None,
119
- normalization_clip = None,
120
- channel_option=["signal"],
121
- target_directory='temp',
122
- augment=False,
123
- model_name='None',
124
- validation_split=0.2,
125
- test_split=0.1,
126
- batch_size = 16,
127
- epochs=1,
128
- recompile_pretrained=False,
129
- learning_rate=0.01,
130
- show_plots=False,
131
- )
132
- shutil.rmtree('temp')
133
-
134
- if __name__=="__main__":
135
- unittest.main()
116
+ if not os.path.exists("temp"):
117
+ os.mkdir("temp")
118
+ export_set(self.trajectories, name="set.npy", output_folder="temp")
119
+ self.model = SignalDetectionModel(
120
+ channel_option=["signal"],
121
+ model_signal_length=128,
122
+ n_channels=1,
123
+ n_conv=2,
124
+ n_classes=3,
125
+ dense_collection=512,
126
+ dropout_rate=0.1,
127
+ label="test",
128
+ )
129
+
130
+ def test_train_signal_model(self):
131
+
132
+ self.model.fit_from_directory(
133
+ ["temp"],
134
+ normalize=True,
135
+ normalization_percentile=None,
136
+ normalization_values=None,
137
+ normalization_clip=None,
138
+ channel_option=["signal"],
139
+ target_directory="temp",
140
+ augment=False,
141
+ model_name="None",
142
+ validation_split=0.2,
143
+ test_split=0.1,
144
+ batch_size=16,
145
+ epochs=1,
146
+ recompile_pretrained=False,
147
+ learning_rate=0.01,
148
+ show_plots=False,
149
+ )
150
+ shutil.rmtree("temp")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ unittest.main()