fucciphase 0.0.2__py3-none-any.whl → 0.0.3__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.
fucciphase/__init__.py CHANGED
@@ -1,4 +1,10 @@
1
- """Cell cycle analysis plugin."""
1
+ """
2
+ FUCCIphase: Analysis tools for cell-cycle estimation from FUCCI imaging.
3
+
4
+ This module exposes the main public API of the package, including the
5
+ core processing functions and the package version.
6
+
7
+ """
2
8
 
3
9
  from importlib.metadata import PackageNotFoundError, version
4
10
 
fucciphase/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ """
2
+ Module entry point for ``python -m fucciphase``.
3
+
4
+ This thin wrapper forwards execution to :func:`fucciphase.main_cli.main_cli`,
5
+ which implements the command-line interface used by the ``fucciphase``
6
+ console script.
7
+ """
8
+
9
+ from .main_cli import main_cli
10
+
11
+ if __name__ == "__main__":
12
+ main_cli()
fucciphase/fucci_phase.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from pathlib import Path
2
- from typing import List, Optional, Union
3
2
 
4
3
  import pandas as pd
5
4
 
@@ -11,20 +10,28 @@ from .utils import normalize_channels, split_trackmate_tracks
11
10
 
12
11
  def process_dataframe(
13
12
  df: pd.DataFrame,
14
- channels: List[str],
13
+ channels: list[str],
15
14
  sensor: FUCCISensor,
16
- thresholds: List[float],
15
+ thresholds: list[float],
17
16
  use_moving_average: bool = True,
18
17
  window_size: int = 7,
19
- manual_min: Optional[List[float]] = None,
20
- manual_max: Optional[List[float]] = None,
18
+ manual_min: list[float] | None = None,
19
+ manual_max: list[float] | None = None,
21
20
  generate_unique_tracks: bool = False,
22
21
  track_id_name: str = "TRACK_ID",
23
22
  label_id_name: str = "name",
24
23
  estimate_percentage: bool = True,
25
24
  ) -> None:
26
- """Process a pd.DataFrame by computing the cell cycle percentage from two FUCCI
27
- cycle reporter channels in place.
25
+ """Apply the FUCCIphase analysis pipeline to an existing dataframe.
26
+
27
+ This function assumes that tracking and fluorescence information are
28
+ already available in a pandas DataFrame with the expected column
29
+ structure. It performs the same core steps as ``process_trackmate``,
30
+ but skips the TrackMate file I/O and starts directly from tabular data.
31
+
32
+ Use this when your tracking pipeline already provides a dataframe, or
33
+ when you have manually assembled the input table and still want to use
34
+ FUCCIphase for cell-cycle analysis and visualization.
28
35
 
29
36
  The dataframe must contain ID and TRACK_ID features.
30
37
 
@@ -38,34 +45,53 @@ def process_dataframe(
38
45
  Parameters
39
46
  ----------
40
47
  df : pandas.DataFrame
41
- Dataframe
48
+ Input dataframe containing tracking and intensity features.
49
+
42
50
  channels: List[str]
43
- Names of channels holding FUCCI information
51
+ Names of columns holding FUCCI fluorescence information.
52
+
44
53
  sensor : FUCCISensor
45
- FUCCI sensor with phase specifics
54
+ FUCCI sensor with phase-specific parameters.
55
+
46
56
  thresholds: List[float]
47
- Thresholds to separate phases
57
+ Thresholds used to separate cell-cycle phases.
58
+
48
59
  use_moving_average : bool, optional
49
- Use moving average before normalization, by default True
60
+ If True, apply a moving average before normalization. Default is True.
61
+
50
62
  window_size : int, optional
51
- Window size of the moving average, by default 5
63
+ Window size of the moving average. Default is 7.
64
+
52
65
  manual_min : Optional[List[float]], optional
53
66
  Manually determined minimum for each channel, by default None
67
+
54
68
  manual_max : Optional[List[float]], optional
55
69
  Manually determined maximum for each channel, by default None
56
- estimate_percentage: bool
57
- Estimate cell cycle percentage
58
- label_id_name: str
59
- Give an indentifier for the spot name (needed for unique track ID generation)
70
+
60
71
  generate_unique_tracks: bool
61
- Assign unique track IDs to splitted tracks.
62
- Requires usage of action in TrackMate.
72
+ If True, assign unique track IDs to split tracks. This requires
73
+ using the appropriate action in TrackMate. Default is False.
74
+
63
75
  track_id_name: str
64
- Name of column with track IDs
76
+ Name of the column containing track IDs. Default is ``"TRACK_ID"``.
77
+
78
+ label_id_name: str
79
+ Column name identifying the spot label / name (used for unique
80
+ track ID generation). Default is ``"name"``.
81
+
82
+ estimate_percentage: bool, optional
83
+ If True, estimate cell-cycle percentage along each track. Default is True.
84
+
85
+ Returns
86
+ -------
87
+ None
88
+ The input dataframe is modified in-place. No value is returned.
65
89
  """
90
+ # ensure that the number of provided channels matches the sensor definition
66
91
  if len(channels) != sensor.fluorophores:
67
92
  raise ValueError(f"Need to provide {sensor.fluorophores} channel names.")
68
93
 
94
+ # optionally split TrackMate subtracks and re-label them as unique tracks
69
95
  if generate_unique_tracks:
70
96
  if "TRACK_ID" in df.columns:
71
97
  split_trackmate_tracks(df, label_id_name=label_id_name)
@@ -86,7 +112,7 @@ def process_dataframe(
86
112
  track_id_name=track_id_name,
87
113
  )
88
114
 
89
- # compute the phases
115
+ # compute the phases (and, optionally, the cycle percentage)
90
116
  generate_cycle_phases(
91
117
  df,
92
118
  sensor=sensor,
@@ -97,20 +123,29 @@ def process_dataframe(
97
123
 
98
124
 
99
125
  def process_trackmate(
100
- xml_path: Union[str, Path],
101
- channels: List[str],
126
+ xml_path: str | Path,
127
+ channels: list[str],
102
128
  sensor: FUCCISensor,
103
- thresholds: List[float],
129
+ thresholds: list[float],
104
130
  use_moving_average: bool = True,
105
131
  window_size: int = 7,
106
- manual_min: Optional[List[float]] = None,
107
- manual_max: Optional[List[float]] = None,
132
+ manual_min: list[float] | None = None,
133
+ manual_max: list[float] | None = None,
108
134
  generate_unique_tracks: bool = False,
109
135
  estimate_percentage: bool = True,
136
+ output_dir: str | Path | None = None,
110
137
  ) -> pd.DataFrame:
111
- """Process a trackmate XML file, compute cell cycle percentage from two FUCCI cycle
112
- reporter channels, save an updated copy of the XML and return the results in a
113
- dataframe.
138
+ """Run the full FUCCIphase pipeline on a TrackMate export.
139
+
140
+ This high-level helper takes tracking data exported from Fiji/TrackMate
141
+ (typically XML or CSV), converts it into a pandas DataFrame with the
142
+ expected fucciphase columns, applies basic quality checks and
143
+ preprocessing, and estimates cell-cycle phase information that can be
144
+ used for downstream analysis and plotting.
145
+
146
+ The returned table is intended to be the main entry point for
147
+ fucciphase workflows, and is compatible with the plotting and
148
+ visualization functions provided in this package.
114
149
 
115
150
  This function applies the following steps:
116
151
  - load the XML file and generate a dataframe from the spots and tracks
@@ -124,36 +159,40 @@ def process_trackmate(
124
159
  Parameters
125
160
  ----------
126
161
  xml_path : Union[str, Path]
127
- Path to the XML file
128
- channels: List[str]
129
- Names of channels holding FUCCI information
130
- generate_unique_tracks: bool
131
- Assign unique track IDs to splitted tracks.
132
- Requires usage of action in TrackMate.
162
+ Path to the TrackMate XML file.
163
+ channels : List[str]
164
+ Names of columns holding FUCCI fluorescence information.
133
165
  sensor : FUCCISensor
134
- FUCCI sensor with phase specifics
135
- thresholds: List[float]
136
- Thresholds to separate phases
166
+ FUCCI sensor with phase-specific parameters.
167
+ thresholds : List[float]
168
+ Thresholds used to separate cell-cycle phases.
137
169
  use_moving_average : bool, optional
138
- Use moving average before normalization, by default True
170
+ If True, apply a moving average before normalization. Default is True.
139
171
  window_size : int, optional
140
- Window size of the moving average, by default 5
172
+ Window size of the moving average. Default is 7.
141
173
  manual_min : Optional[List[float]], optional
142
- Manually determined minimum for each channel, by default None
174
+ Manually determined minimum for each channel, by default None.
143
175
  manual_max : Optional[List[float]], optional
144
- Manually determined maximum for each channel, by default None
145
- estimate_percentage: bool, optional
146
- Estimate cell cycle percentage
176
+ Manually determined maximum for each channel, by default None.
177
+ generate_unique_tracks : bool, optional
178
+ If True, assign unique track IDs to split tracks. This requires
179
+ using the appropriate action in TrackMate. Default is False.
180
+ estimate_percentage : bool, optional
181
+ If True, estimate cell-cycle percentage along each track. Default is True.
182
+ output_dir : Optional[Union[str, Path]], optional
183
+ Optional directory where the updated XML should be written. If None,
184
+ the file is saved next to the input XML.
147
185
 
148
186
  Returns
149
187
  -------
150
- pd.DataFrame
151
- Dataframe with the cell cycle percentage and the corresponding phases
188
+ pandas.DataFrame
189
+ Dataframe with the cell-cycle percentage and the corresponding phases.
190
+
152
191
  """
153
- # read the XML
192
+ # read the XML and extract the dataframe and XML wrapper
154
193
  df, tmxml = read_trackmate_xml(xml_path)
155
194
 
156
- # process the dataframe
195
+ # process the dataframe in-place (and also get a reference to it)
157
196
  process_dataframe(
158
197
  df,
159
198
  channels,
@@ -167,12 +206,19 @@ def process_trackmate(
167
206
  estimate_percentage=estimate_percentage,
168
207
  )
169
208
 
170
- # update the XML
209
+ # update the XML with the new features
171
210
  tmxml.update_features(df)
172
211
 
173
- # export the xml
212
+ # export the updated XML next to the original file
174
213
  new_name = Path(xml_path).stem + "_processed.xml"
175
- new_path = Path(xml_path).parent / new_name
214
+
215
+ if output_dir is not None:
216
+ output_dir = Path(output_dir)
217
+ output_dir.mkdir(parents=True, exist_ok=True)
218
+ new_path = output_dir / new_name
219
+ else:
220
+ new_path = Path(xml_path).parent / new_name
221
+
176
222
  tmxml.save_xml(new_path)
177
223
 
178
224
  return df
fucciphase/io.py CHANGED
@@ -1,25 +1,25 @@
1
1
  from pathlib import Path
2
- from typing import Tuple, Union
3
2
 
4
3
  import pandas as pd
5
4
 
6
5
  from .utils import TrackMateXML
7
6
 
8
7
 
9
- def read_trackmate_xml(xml_path: Union[Path, str]) -> Tuple[pd.DataFrame, TrackMateXML]:
10
- """Read a trackmate exported xml file.
8
+ def read_trackmate_xml(xml_path: Path | str) -> tuple[pd.DataFrame, TrackMateXML]:
9
+ """Read a TrackMate-exported XML file and return data and XML wrapper.
11
10
 
12
11
  Parameters
13
12
  ----------
14
13
  xml_path : Union[Path, str]
15
- Path to the xml file.
14
+ Path to the XML file.
16
15
 
17
16
  Returns
18
17
  -------
19
18
  df : pandas.DataFrame
20
- Dataframe containing the xml data.
19
+ Dataframe containing the spot and track data, sorted by FRAME.
21
20
  trackmate : TrackMateXML
22
- TrackMateXML object.
21
+ TrackMateXML object wrapping the original XML and allowing
22
+ feature updates / re-export.
23
23
  """
24
24
  # read in the xml file
25
25
  trackmate = TrackMateXML(xml_path)
@@ -32,27 +32,31 @@ def read_trackmate_xml(xml_path: Union[Path, str]) -> Tuple[pd.DataFrame, TrackM
32
32
  return df, trackmate
33
33
 
34
34
 
35
- def read_trackmate_csv(csv_path: Union[Path, str]) -> pd.DataFrame:
36
- """Read a trackmate exported csv file.
35
+ def read_trackmate_csv(csv_path: Path | str) -> pd.DataFrame:
36
+ """Read a TrackMate-exported CSV file.
37
37
 
38
38
  The first three rows (excluding header) of the csv file are skipped as
39
39
  they contain duplicate titles of columns and units (Trackmate specific).
40
40
 
41
+ The first three rows (excluding the header) of the CSV file are
42
+ skipped as they contain duplicate column titles and units
43
+ (TrackMate-specific).
41
44
 
42
45
  Parameters
43
46
  ----------
44
- csv_path : str
45
- Path to the csv file.
47
+ csv_path : Union[Path, str]
48
+ Path to the CSV file.
46
49
 
47
50
  Returns
48
51
  -------
49
52
  df : pandas.DataFrame
50
- Dataframe containing the csv data.
53
+ Dataframe containing the CSV data with converted dtypes.
51
54
 
52
55
  Raises
53
56
  ------
54
57
  ValueError
55
- If the csv file does not contain at least two channels.
58
+ If the CSV file does not contain both MEAN_INTENSITY_CH1 and
59
+ MEAN_INTENSITY_CH2 columns.
56
60
  """
57
61
  df = pd.read_csv(csv_path, encoding="unicode_escape", skiprows=[1, 2, 3])
58
62
 
fucciphase/main_cli.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import argparse
2
2
  import json
3
+ from pathlib import Path
3
4
 
4
5
  import pandas as pd
5
6
 
@@ -15,12 +16,37 @@ except ImportError as err:
15
16
 
16
17
 
17
18
  def main_cli() -> None:
18
- """Fucciphase CLI."""
19
+ """Fucciphase CLI: Command-line entry point for FUCCIphase.
20
+
21
+ This function is invoked by the ``fucciphase`` console script and
22
+ implements the standard command-line workflow:
23
+
24
+ 1. Parse command-line arguments describing:
25
+ - a TrackMate tracking file (XML or CSV),
26
+ - a reference cell-cycle trace in CSV format,
27
+ - an optional FUCCI sensor JSON file,
28
+ - the acquisition timestep and channel names.
29
+ 2. Load the reference data and rename its fluorescence columns to
30
+ match the user-specified channel names.
31
+ 3. Load and preprocess the tracking data using either
32
+ :func:`process_trackmate` (for XML) or
33
+ :func:`process_dataframe` (for CSV).
34
+ 4. Estimate cell-cycle percentages for each track by subsequence
35
+ alignment against the reference trace.
36
+ 5. Write the processed table to ``<tracking_file>_processed.csv`` in
37
+ the same directory as the input file.
38
+
39
+ The function is designed to be used from the command line and does
40
+ not return a value. It will raise a ``ValueError`` if the tracking
41
+ file does not have an XML or CSV extension.
42
+ """
19
43
  parser = argparse.ArgumentParser(
20
44
  prog="fucciphase",
21
45
  description="FUCCIphase tool to estimate cell cycle phases and percentages.",
22
46
  epilog="Please report bugs and errors on GitHub.",
23
47
  )
48
+
49
+ # -------------- 1. Parse command-line arguments --------------
24
50
  parser.add_argument("tracking_file", type=str, help="TrackMate XML or CSV file")
25
51
  parser.add_argument(
26
52
  "-ref",
@@ -61,13 +87,19 @@ def main_cli() -> None:
61
87
  )
62
88
 
63
89
  args = parser.parse_args()
90
+ # Decide where to store outputs (CSV and, for XML input, processed XML)
91
+ output_dir = Path("outputs")
92
+ output_dir.mkdir(exist_ok=True)
64
93
 
94
+ # ---------------- 2. Load and adapt the reference cell-cycle trace ----------------
65
95
  reference_df = pd.read_csv(args.reference_file)
96
+ # The reference file is expected to contain 'cyan' and 'magenta' columns;
97
+ # they are renamed here to match the actual channel names in the data.
66
98
  reference_df.rename(
67
99
  columns={"cyan": args.cyan_channel, "magenta": args.magenta_channel},
68
100
  inplace=True,
69
101
  )
70
-
102
+ # ---------------- 3. Build the sensor model ----------------
71
103
  if args.sensor_file is not None:
72
104
  with open(args.sensor_file) as fp:
73
105
  sensor_properties = json.load(fp)
@@ -75,15 +107,19 @@ def main_cli() -> None:
75
107
  else:
76
108
  sensor = get_fuccisa_default_sensor()
77
109
 
110
+ # ---------------- 4. Load and preprocess the tracking data ----------------
78
111
  if args.tracking_file.endswith(".xml"):
112
+ # XML: let process_trackmate handle I/O and preprocessing
79
113
  df = process_trackmate(
80
114
  args.tracking_file,
81
115
  channels=[args.cyan_channel, args.magenta_channel],
82
116
  sensor=sensor,
83
117
  thresholds=[0.1, 0.1],
84
118
  generate_unique_tracks=args.generate_unique_tracks,
119
+ output_dir=output_dir,
85
120
  )
86
121
  elif args.tracking_file.endswith(".csv"):
122
+ # CSV: read the table and then run the processing pipeline on it
87
123
  df = pd.read_csv(args.tracking_file)
88
124
  process_dataframe(
89
125
  df,
@@ -95,6 +131,7 @@ def main_cli() -> None:
95
131
  else:
96
132
  raise ValueError("Tracking file must be an XML or CSV file.")
97
133
 
134
+ # ---------------- 5. Estimate cell-cycle percentages ----------------
98
135
  track_id_name = "UNIQUE_TRACK_ID"
99
136
  if not args.generate_unique_tracks:
100
137
  track_id_name = "TRACK_ID"
@@ -104,13 +141,31 @@ def main_cli() -> None:
104
141
  dt=args.timestep,
105
142
  channels=[args.cyan_channel, args.magenta_channel],
106
143
  reference_data=reference_df,
107
- track_id_name=track_id_name
144
+ track_id_name=track_id_name,
108
145
  )
109
- df.to_csv(args.tracking_file + "_processed.csv", index=False)
146
+ # ---------------- 6. Save results ----------------
147
+ tracking_path = Path(args.tracking_file)
148
+ output_csv = output_dir / (tracking_path.stem + "_processed.csv")
149
+ df.to_csv(output_csv, index=False)
110
150
 
111
151
 
112
152
  def main_visualization() -> None:
113
- """Fucciphase visualization."""
153
+ """Fucciphase visualization.
154
+
155
+ Launch a napari-based visualization of FUCCIphase results.
156
+
157
+ This command-line entry point loads a processed FUCCIphase CSV file
158
+ together with the corresponding OME-TIFF time-lapse movie and
159
+ segmentation masks, then opens an interactive napari viewer showing:
160
+
161
+ - cyan and magenta fluorescence channels,
162
+ - segmentation masks as a labels layer,
163
+ - tracks and cell-cycle information overlaid on the image.
164
+
165
+ The function is intended to be invoked via the ``fucciphase-napari``
166
+ console script and does not return a value.
167
+
168
+ """
114
169
  parser = argparse.ArgumentParser(
115
170
  prog="fucciphase-napari",
116
171
  description="FUCCIphase napari script to launch visualization.",
@@ -142,13 +197,19 @@ def main_visualization() -> None:
142
197
  required=True,
143
198
  )
144
199
  parser.add_argument(
145
- "--pixel_size",
146
- type=float,
147
- help="Pixel size, only used if not in metadata",
148
- default=None)
200
+ "--pixel_size",
201
+ type=float,
202
+ help="Pixel size, only used if not in metadata",
203
+ default=None,
204
+ )
149
205
 
150
206
  args = parser.parse_args()
151
207
 
208
+ # Decide where to store outputs (CSV and, for XML input, processed XML)
209
+ output_dir = Path("outputs")
210
+ output_dir.mkdir(exist_ok=True)
211
+
212
+ # Try to read the video using AICSImage; fall back to bioio if needed
152
213
  AICSIMAGE = False
153
214
  BIOIMAGE = False
154
215
  try:
@@ -169,6 +230,8 @@ def main_visualization() -> None:
169
230
  image = AICSImage(args.video)
170
231
  elif BIOIMAGE:
171
232
  image = BioImage(args.video, reader=bioio_ome_tiff.Reader)
233
+
234
+ # Determine spatial scale; fall back to unit scale or user-provided pixel size
172
235
  scale = (image.physical_pixel_sizes.Y, image.physical_pixel_sizes.X)
173
236
  if None in scale:
174
237
  if args.pixel_size is not None:
@@ -1,5 +1,3 @@
1
- from typing import List, Optional
2
-
3
1
  import numpy as np
4
2
  import pandas as pd
5
3
 
@@ -15,18 +13,18 @@ def add_trackmate_data_to_viewer(
15
13
  df: pd.DataFrame,
16
14
  viewer: napari.Viewer,
17
15
  scale: tuple,
18
- image_data: List[np.ndarray],
19
- colormaps: List[str],
20
- labels: Optional[np.ndarray],
21
- cycle_percentage_id: Optional[str] = "CELL_CYCLE_PERC_POST",
16
+ image_data: list[np.ndarray],
17
+ colormaps: list[str],
18
+ labels: np.ndarray | None,
19
+ cycle_percentage_id: str | None = "CELL_CYCLE_PERC_POST",
22
20
  dim: int = 2,
23
- textkwargs: Optional[dict] = None,
24
- label_id_name: Optional[str] = "MAX_INTENSITY_CH3",
25
- track_id_name: Optional[str] = "TRACK_ID",
26
- time_id_name: Optional[str] = "POSITION_T",
27
- pos_x_id_name: Optional[str] = "POSITION_X",
28
- pos_y_id_name: Optional[str] = "POSITION_Y",
29
- crop_fov: Optional[List[float]] = None,
21
+ textkwargs: dict | None = None,
22
+ label_id_name: str | None = "MAX_INTENSITY_CH3",
23
+ track_id_name: str | None = "TRACK_ID",
24
+ time_id_name: str | None = "POSITION_T",
25
+ pos_x_id_name: str | None = "POSITION_X",
26
+ pos_y_id_name: str | None = "POSITION_Y",
27
+ crop_fov: list[float] | None = None,
30
28
  ) -> None:
31
29
  """Overlay tracking result and video.
32
30
 
@@ -73,7 +71,7 @@ def add_trackmate_data_to_viewer(
73
71
  labels_layer = viewer.add_labels(new_labels, scale=scale)
74
72
  labels_layer.contour = 10
75
73
 
76
- for image, colormap in zip(image_data, colormaps):
74
+ for image, colormap in zip(image_data, colormaps, strict=True):
77
75
  viewer.add_image(image, blending="additive", colormap=colormap, scale=scale)
78
76
  # TODO implement cropping, filter points in / outside range
79
77
  # crop_fov =
@@ -95,8 +93,8 @@ def pandas_df_to_napari_tracks(
95
93
  frame_id_name: str,
96
94
  position_x_name: str,
97
95
  position_y_name: str,
98
- feature_name: Optional[str] = None,
99
- colormaps_dict: Optional[dict] = None,
96
+ feature_name: str | None = None,
97
+ colormaps_dict: dict | None = None,
100
98
  ) -> None:
101
99
  """Add tracks to Napari track layer.
102
100
  Splitting and merging are not yet implemented.