fucciphase 0.0.1__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.
@@ -0,0 +1,295 @@
1
+ import re
2
+ import xml.etree.ElementTree as et
3
+ from pathlib import Path
4
+ from typing import Any, Dict, List, Optional, Tuple, Union
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ # TrackMate XML tags
10
+ MODEL = "Model"
11
+ FEATURES = "FeatureDeclarations"
12
+ SPOT_FEATURES = "SpotFeatures"
13
+ ALL_SPOTS = "AllSpots"
14
+ N_SPOTS = "nspots"
15
+ ID = "ID"
16
+ SUBTRACK_REGEX = r"Track_[0-9]+\.[a-z]+"
17
+
18
+
19
+ # TODO test on very large trackmate files, since this is potentially a bottleneck here
20
+ class TrackMateXML:
21
+ """Class to handle TrackMate xml files.
22
+
23
+ TrackMate xml files are structured as follows:
24
+ root
25
+ ├── Log
26
+ ├── Model
27
+ │ ├── FeatureDeclarations
28
+ │ ├── AllSpots
29
+ │ │ └── SpotsInFrame
30
+ │ │ └── Spot
31
+ │ ├── AllTracks
32
+ │ └── FilteredTracks
33
+ ├── Settings
34
+ ├── GUIState
35
+ └── DisplaySettings
36
+
37
+ This class allows reading in the tree and converting the spots to a
38
+ pandas dataframe. The features (columns) can also be updated and the
39
+ xml file be saved.
40
+
41
+ Attributes
42
+ ----------
43
+ nspots : int
44
+ Number of spots in the xml file.
45
+ features : Dict[str, type]
46
+ List of all features in the xml file, and whether they are integer features.
47
+ """
48
+
49
+ def __init__(self, xml_path: Union[str, Path]) -> None:
50
+ """Initialize the TrackMateXML object.
51
+
52
+ The xml file is parsed and the model and all spots are imported.
53
+
54
+ Parameters
55
+ ----------
56
+ xml_path : Union[str, Path]
57
+ Path to the xml file.
58
+ """
59
+ # parse tree
60
+ self._tree: et.ElementTree[et.Element[str]] = et.parse(xml_path)
61
+ self._root: et.Element | Any = self._tree.getroot()
62
+
63
+ # placeholders
64
+ self._model: Optional[et.Element] = None
65
+ self._allspots: Optional[et.Element] = None
66
+
67
+ self.nspots: int = 0 # number of spots
68
+ self.features: Dict[str, type] = {} # features and their types
69
+ self.spot_features: List[str] = [] # list of spot features
70
+
71
+ # import model and all spots
72
+ self._import_data()
73
+
74
+ def _get_spot_features(self) -> None:
75
+ """Get the spot features from the tree."""
76
+ if self._allspots is not None:
77
+ spot_features: List[str] = []
78
+ for frame in self._allspots:
79
+ for spot in frame:
80
+ spot_features.extend(spot.attrib.keys())
81
+ break
82
+ break
83
+
84
+ self.spot_features = spot_features
85
+
86
+ def _get_features(self) -> None:
87
+ """Compare spot features and features declaration, keep the intersection and
88
+ register the dtypes in a public member.
89
+ """
90
+ if self._model is not None:
91
+ features = {}
92
+ for element in self._model:
93
+ if element.tag == FEATURES:
94
+ for feature in element:
95
+ if feature.tag == SPOT_FEATURES:
96
+ for spot_feature in feature:
97
+ # get feature name
98
+ feature_name = spot_feature.attrib["feature"]
99
+
100
+ # check if feature is integer
101
+ is_integer = spot_feature.attrib["isint"] == "true"
102
+
103
+ # add feature to dictionary
104
+ features[feature_name] = int if is_integer else float
105
+
106
+ # get spot features
107
+ self._get_spot_features()
108
+
109
+ # keep only features that are in both spot features and features
110
+ features_key = set(features.keys())
111
+ features_to_keep = features_key - (features_key - set(self.spot_features))
112
+ self.features = {feature: features[feature] for feature in features_to_keep}
113
+
114
+ def _import_data(self) -> None:
115
+ """Import the model and all spots from the xml file.
116
+
117
+ Raises
118
+ ------
119
+ ValueError
120
+ If the xml file does not contain a "Model" tag.
121
+ ValueError
122
+ If the "Model" tag does not contain an "AllSpots" tag.
123
+ """
124
+ # get model
125
+ for element in self._root:
126
+ if element.tag == MODEL:
127
+ self._model = element
128
+
129
+ if self._model is None:
130
+ raise ValueError('"Model" tag not found in xml file.')
131
+
132
+ # get allspots
133
+ for element in self._model:
134
+ if element.tag == ALL_SPOTS:
135
+ self._allspots = element
136
+ self.nspots = int(element.attrib[N_SPOTS])
137
+
138
+ if self._allspots is None:
139
+ raise ValueError('"AllSpots" tag not found in xml file.')
140
+
141
+ # get feature declarations
142
+ self._get_features()
143
+
144
+ def _add_track_ids(self, df: pd.DataFrame) -> None:
145
+ # extract track IDs (if there are spots)
146
+ if len(df) > 0:
147
+ # some spots do not have tracks associated
148
+ track_ids = np.full_like(df[ID].values, -1)
149
+ if self._model is not None:
150
+ for element in self._model:
151
+ if element.tag == "AllTracks":
152
+ for track in element:
153
+ track_id = int(track.attrib["TRACK_ID"])
154
+
155
+ for edge in track:
156
+ spot_source = edge.attrib["SPOT_SOURCE_ID"]
157
+ spot_target = edge.attrib["SPOT_TARGET_ID"]
158
+
159
+ # get row index of the source and target spots
160
+ source_index = df[df[ID] == spot_source].index[0]
161
+ target_index = df[df[ID] == spot_target].index[0]
162
+
163
+ # update track IDs
164
+ track_ids[source_index] = track_id
165
+ track_ids[target_index] = track_id
166
+
167
+ # update dataframe
168
+ df["TRACK_ID"] = track_ids
169
+ else:
170
+ df["TRACK_ID"] = []
171
+
172
+ # add track id to the spot features (this avoids exporting it to the xml later)
173
+ self.spot_features.append("TRACK_ID")
174
+
175
+ def to_pandas(self) -> pd.DataFrame:
176
+ """Export the spots as a pandas dataframe.
177
+
178
+ Returns
179
+ -------
180
+ pd.DataFrame
181
+ Dataframe containing the spots.
182
+ """
183
+ df = pd.DataFrame()
184
+
185
+ # loop over all frames and add spots to a dataframe
186
+ if self._allspots is not None:
187
+ spot_count = 0
188
+ for frame in self._allspots:
189
+ # only run on frames with spots
190
+ if len(frame) > 0:
191
+ # for each spot in the frame
192
+ for spot in frame:
193
+ # if this is the first spot, initialize dataframe
194
+ if spot_count == 0:
195
+ df = pd.DataFrame(columns=spot.attrib.keys())
196
+
197
+ # add the spot to the dataframe
198
+ df.loc[spot_count] = spot.attrib
199
+ spot_count += 1
200
+
201
+ # add tracks IDs
202
+ self._add_track_ids(df)
203
+
204
+ # fix type of TRACK_ID
205
+ self.features["TRACK_ID"] = int
206
+ # convert features to their declared types
207
+ return df.astype(self.features)
208
+
209
+ def update_features(self, df: pd.DataFrame) -> None:
210
+ """Update the xml tree with new features, where features are columns of the
211
+ dataset.
212
+
213
+ Parameters
214
+ ----------
215
+ df : pd.DataFrame
216
+ Dataframe containing the new features.
217
+ """
218
+ # compare number of spots
219
+ if len(df) != self.nspots:
220
+ raise ValueError(
221
+ f"Number of spots in the dataframe ({len(df)}) does not match number "
222
+ f"of spots in xml file ({self.nspots})."
223
+ )
224
+
225
+ # check if ID column is in the dataframe
226
+ if ID not in df.columns:
227
+ raise ValueError(f"Column {ID} not found in dataframe.")
228
+
229
+ # new features
230
+ new_features = set(df.columns) - set(self.spot_features)
231
+
232
+ if self._allspots is not None:
233
+ # update features
234
+ for feature in new_features:
235
+ self.features[feature] = df[feature].dtype
236
+
237
+ # if there are spots and features
238
+ if len(df) > 0 and len(new_features) > 0:
239
+ # loop over the frames and spots
240
+ for frame in self._allspots:
241
+ for spot in frame:
242
+ # get ID
243
+ spot_id = spot.attrib[ID]
244
+
245
+ # get spot
246
+ spot_df = df[df[ID] == spot_id]
247
+
248
+ # add the new feature
249
+ for feature in new_features:
250
+ spot.attrib[feature] = str(spot_df[feature].values[0])
251
+
252
+ def save_xml(self, xml_path: Union[str, Path]) -> None:
253
+ """Save the xml file.
254
+
255
+ Parameters
256
+ ----------
257
+ xml_path : Union[str, Path]
258
+ Path to the xml file.
259
+ """
260
+ with open(xml_path, "wb") as f:
261
+ self._tree.write(f)
262
+
263
+ def get_full_tracks(
264
+ df: pd.DataFrame,
265
+ channels: List[str],
266
+ track_id_name: str = "UNIQUE_TRACK_ID",
267
+ spot_name: str = "name",
268
+ frame_name: str = "FRAME",
269
+ min_length: int = 40,
270
+ ) -> Tuple[List[pd.DataFrame], List[pd.DataFrame]]:
271
+ """Locate all tracks that may describe a full cycle.
272
+ Tracks need to be auto-named by TrackMate.
273
+ For example, Track_1a, Track_1aa, Track_1b etc.
274
+ This can be done in TrackMate by executing an action after the tracking.
275
+ In addition, tracks longer than a certain minimum length can be selected.
276
+ """
277
+ regex = "Track_[0-9]+.[a-z]+"
278
+ candidate_tracks: List[pd.DataFrame] = []
279
+ save_tracks: List[pd.DataFrame] = []
280
+ track_ids = df[track_id_name].unique()
281
+ for track_id in track_ids:
282
+ track = df[df[track_id_name] == track_id]
283
+ name = track[spot_name].iloc[0]
284
+ last_frame = track[frame_name].max()
285
+ # is the track a subtrack
286
+ match = re.match(regex, name)
287
+ # is there a subtrack
288
+ next_match = any(df[spot_name].str.match(name + "[a-z]+").unique())
289
+ if match is not None and last_frame < df[frame_name].max():
290
+ if next_match:
291
+ save_tracks.append(track[[frame_name, *channels]])
292
+ else:
293
+ if len(track) > min_length:
294
+ candidate_tracks.append(track[[frame_name, *channels]])
295
+ return save_tracks, candidate_tracks
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: fucciphase
3
+ Version: 0.0.1
4
+ Summary: Cell cycle analysis plugin.
5
+ Project-URL: homepage, https://github.com/nobias-ht/fucciphase
6
+ Project-URL: repository, https://github.com/nobias-ht/fucciphase
7
+ Author-email: Joran Deschamps <joran.deschamps@fht.org>
8
+ License: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Requires-Python: >=3.8
18
+ Requires-Dist: dtaidistance
19
+ Requires-Dist: lineagetree<1.5.0
20
+ Requires-Dist: matplotlib
21
+ Requires-Dist: monotonic-derivative
22
+ Requires-Dist: numpy
23
+ Requires-Dist: openpyxl
24
+ Requires-Dist: pandas
25
+ Requires-Dist: scipy
26
+ Requires-Dist: svgwrite
27
+ Provides-Extra: dev
28
+ Requires-Dist: ipython; extra == 'dev'
29
+ Requires-Dist: mypy; extra == 'dev'
30
+ Requires-Dist: pdbpp; extra == 'dev'
31
+ Requires-Dist: pre-commit; extra == 'dev'
32
+ Requires-Dist: rich; extra == 'dev'
33
+ Requires-Dist: ruff; extra == 'dev'
34
+ Provides-Extra: doc
35
+ Requires-Dist: sphinx; extra == 'doc'
36
+ Provides-Extra: jupyter
37
+ Requires-Dist: jupyter; extra == 'jupyter'
38
+ Provides-Extra: test
39
+ Requires-Dist: pytest; extra == 'test'
40
+ Requires-Dist: pytest-cov; extra == 'test'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # fucciphase
44
+
45
+ [![License](https://img.shields.io/pypi/l/fucciphase.svg?color=green)](https://github.com/Synthetic-Physiology-Lab/fucciphase/raw/main/LICENSE)
46
+ [![PyPI](https://img.shields.io/pypi/v/fucciphase.svg?color=green)](https://pypi.org/project/fucciphase)
47
+ [![Python Version](https://img.shields.io/pypi/pyversions/fucciphase.svg?color=green)](https://python.org)
48
+ [![CI](https://github.com/Synthetic-Physiology-Lab/fucciphase/actions/workflows/ci.yml/badge.svg)](https://github.com/Synthetic-Physiology-Lab/fucciphase/actions/workflows/ci.yml)
49
+ [![codecov](https://codecov.io/gh/Synthetic-Physiology-Lab/fucciphase/branch/main/graph/badge.svg)](https://codecov.io/gh/Synthetic-Physiology-Lab/fucciphase)
50
+ [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Synthetic-Physiology-Lab/fucciphase/main.svg)](https://results.pre-commit.ci/latest/github/Synthetic-Physiology-Lab/fucciphase/main)
51
+
52
+ FUCCI cell cycle analysis plugin.
53
+ Obtain cell cycle information from FUCCI fluorescence intensities.
54
+
55
+ ## Installation
56
+
57
+ The best way to run fucciphase is to install it in a virtual conda environment.
58
+ Make sure that git is installed and can be called from the command line.
59
+
60
+ (SOON) To install from pip:
61
+
62
+ ```bash
63
+ pip install fucciphase
64
+ ```
65
+
66
+ If you wish to install it from source:
67
+
68
+ ```bash
69
+ git clone https://github.com/Synthetic-Physiology-Lab/fucciphase
70
+ cd fucciphase
71
+ pip install -e .
72
+ ```
73
+
74
+ The installation should not take longer than a few seconds (depending on your internet connection).
75
+
76
+ To use the notebooks, also install jupyter:
77
+
78
+ ```bash
79
+ pip install jupyter
80
+ ```
81
+
82
+ ## Usage
83
+
84
+ Fucci phase currently supports loading a
85
+ [TrackMate](https://imagej.net/plugins/trackmate/) XML file:
86
+
87
+ ```python
88
+ from fucciphase import process_trackmate
89
+ from fucciphase.sensor import get_fuccisa_default_sensor
90
+
91
+ trackmate_xml = "path/to/trackmate.xml"
92
+ channel1 = "MEAN_INTENSITY_CH3"
93
+ channel2 = "MEAN_INTENSITY_CH4"
94
+
95
+ sensor = get_fuccisa_default_sensor()
96
+
97
+ df = process_trackmate(trackmate_xml,
98
+ channels=[channel1, channel2],
99
+ sensor=sensor,
100
+ thresholds=[0.1, 0.1])
101
+ print(df)
102
+ ```
103
+
104
+ The TrackMate XML is converted to a [Pandas](https://pandas.pydata.org/) DataFrame.
105
+ Thus, in general data (e.g., stored in a CSV or XLSX file) that can be parsed into
106
+ a DataFrame is supported.
107
+
108
+ Have a look at the examples in the `example` folder to get more information!
109
+
110
+ The runtime of the scripts depends on your datasize. 2D samples with a few hundred to a few thousand cells
111
+ can be processed in a few minutes. Visualization in Napari can take a bit longer.
112
+ Standard processing does not require a powerful computer.
113
+ Make sure that you have sufficient RAM to load videos for visualization in Napari.
114
+
115
+ ## Development
116
+
117
+ To develop fucciphase, clone the repository, install fucciphase in your environment
118
+ and install the pre-commit hooks:
119
+
120
+ ```bash
121
+ git clone https://github.com/Synthetic-Physiology-Lab/fucciphase
122
+ cd fucciphase
123
+ pip install -e ".[test, dev]"
124
+ pre-commit install
125
+ ```
126
+
127
+ If you want to build the documentation, replace the abovementioned pip install by:
128
+ ```bash
129
+ pip install -e ".[test, dev, doc]"
130
+ ```
131
+
132
+ ## Cite us
133
+
134
+ Di Sante, M., Pezzotti, M., Zimmermann, J., Enrico, A., Deschamps, J., Balmas, E.,
135
+ Becca, S., Solito, S., Reali, A., Bertero, A., Jug, F. and Pasqualini, F.S., 2025.
136
+ CALIPERS: Cell cycle-aware live imaging for phenotyping experiments and regeneration studies.
137
+ bioRxiv, https://doi.org/10.1101/2024.12.19.629259
@@ -0,0 +1,22 @@
1
+ fucciphase/__init__.py,sha256=8PA6UNaYjycfPpOg7e5ArP3k9Lj9H15Lmw-Mnd14azY,375
2
+ fucciphase/fucci_phase.py,sha256=VgydHVN0nWzC6Eeh0LRaPHdScL2w7C5X7IhGSvyp4qw,6134
3
+ fucciphase/io.py,sha256=ELcoxsPHzcg9tf48MaMNnuSMxe0Fd3Nc9LCWbQ6p4I8,1715
4
+ fucciphase/phase.py,sha256=jy2l9LD81FTk6TIlyWyYPbVnwClGP9N4RrYyqtiMStg,17130
5
+ fucciphase/plot.py,sha256=Obfv2VOHdDN4Az_JUErJ22ESAgHhdsBaavdsS0f6xZ4,18593
6
+ fucciphase/py.typed,sha256=esB4cHc6c07uVkGtqf8at7ttEnprwRxwk8obY8Qumq4,187
7
+ fucciphase/sensor.py,sha256=6-WEI8viI5fSVyKHnECmKYKaROW4tQaPaNW4hHYVAcA,14788
8
+ fucciphase/tracking_utilities.py,sha256=IfKH2fyPo7fkW3PBvQrCz-UcfrdkOj7D-iLJabynvfw,2776
9
+ fucciphase/napari/__init__.py,sha256=At9Shk6HfDf6obtQaM0yKG4NOZVO6YxD2-J1M2ZGm7w,198
10
+ fucciphase/napari/tracks_to_napari.py,sha256=US9uAzGVJi5wAJ4CbUi9XztRFbrMRK7t0oJrQ40aDEg,4094
11
+ fucciphase/utils/__init__.py,sha256=E4wk7ygjHQQ-8vj4VNHseMLE-PQ1bZK2gvDFaO5Z1eU,1011
12
+ fucciphase/utils/checks.py,sha256=ZTe6cq11Y2e3suM3vECqCvvFxQTMOJqXMPS8gdCP7qc,651
13
+ fucciphase/utils/dtw.py,sha256=MDJEJUT9oSz8Iu6SeWFSBhVgZmLnE3CXE_2cbEl6TE8,1682
14
+ fucciphase/utils/normalize.py,sha256=eGytBDjmWcr1OY88McCJNcmy4zioM2DbtQConDoIVRw,6037
15
+ fucciphase/utils/phase_fit.py,sha256=Ht_dEyuLYonv6is9qQ-Xd95pQR7IR-8C8mv0ckDcp4E,1743
16
+ fucciphase/utils/simulator.py,sha256=7bmrO0IWUqsk4CyM-PlVpG2QTJxjTsY1h6buxmTs1iM,2322
17
+ fucciphase/utils/track_postprocessing.py,sha256=cTe3OOCR4dxFBZwN7XdbzDbnsgouoJql8rv4WwvmbM8,14438
18
+ fucciphase/utils/trackmate.py,sha256=dir4ayS1Fl-gtK8NTbP7t7CBLnrumC8LgPiCwxsBf-c,10666
19
+ fucciphase-0.0.1.dist-info/METADATA,sha256=TL-dyIUGTeLdahG5YiQiWhq3i8o3esSpvujY47rLP14,4900
20
+ fucciphase-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ fucciphase-0.0.1.dist-info/licenses/LICENSE,sha256=pQGrOGpOTwikEzkZ8Zc9XLQwbaZ85TMJP-GaWCNZciw,1554
22
+ fucciphase-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Joran Deschamps
4
+ Copyright (c) 2023, Julius Zimmermann (contributor)
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.