Repository: fdrgsp/micromanager-gui
Pull request #48: Iei
Author: login: annayang26
Created: 2025-02-28T21:31:36Z | Merged: 2025-03-06T04:59:17Z

merge after #45 

Add back inter-event intervals to ROIData. 


Diff:
diff --git a/src/micromanager_gui/_plate_viewer/_analysis.py b/src/micromanager_gui/_plate_viewer/_analysis.py
index 37e3e81c..2c43f47d 100644
--- a/src/micromanager_gui/_plate_viewer/_analysis.py
+++ b/src/micromanager_gui/_plate_viewer/_analysis.py
@@ -45,6 +45,7 @@
     _WaitingProgressBarWidget,
     calculate_dff,
     get_cubic_phase,
+    get_iei,
     get_linear_phase,
     parse_lineedit_text,
     show_error_dialog,
@@ -537,28 +538,31 @@ def _extract_trace_per_position(self, p: int) -> None:
 
         seq = cast(useq.MDASequence, self.data.sequence)
         timepoints = seq.sizes["t"]
-        exp_time = meta[0][event_key].get("exposure")
-        elapsed_time: list = []
+        exp_time = meta[0][event_key].get("exposure", 0)
+        elapsed_time_list: list[float] = []
 
         # get the elapsed time for each timepoint to calculate tot_time_sec
         if (cam_key := CAMERA_KEY) in meta[0]:  # new metadata format
             for m in meta:
                 et = m[cam_key].get(ELAPSED_TIME_KEY)
                 if et is not None:
-                    elapsed_time.append(float(et))
+                    elapsed_time_list.append(float(et))
         else:  # old metadata format
             for m in meta:
                 et = m.get(ELAPSED_TIME_KEY)
                 if et is not None:
-                    elapsed_time.append(float(et))
+                    elapsed_time_list.append(float(et))
+
         # if the len of elapsed time is not equal to the number of timepoints,
         # use exposure time and the number of timepoints to calculate tot_time_sec
-        if len(elapsed_time) != timepoints:
+        if len(elapsed_time_list) != timepoints:
             tot_time_sec = exp_time * timepoints / 1000
         else:
             # otherwise, calculate the total time in seconds using the elapsed time.
             # NOTE: adding the exposure time to consider the first frame
-            tot_time_sec = (elapsed_time[-1] - elapsed_time[0] + exp_time) / 1000
+            tot_time_sec = (
+                elapsed_time_list[-1] - elapsed_time_list[0] + exp_time
+            ) / 1000
 
         roi_trace: np.ndarray
 
@@ -645,6 +649,12 @@ def _extract_trace_per_position(self, p: int) -> None:
                 linear_phase = get_linear_phase(timepoints, peaks_dec_dff)
                 cubic_phase = get_cubic_phase(timepoints, peaks_dec_dff)
 
+            # if the elapsed time is not available or for any reason is different from
+            # the number of timepoints, set it as list of timepoints every exp_time
+            if len(elapsed_time_list) != timepoints:
+                elapsed_time_list = [i * exp_time for i in range(timepoints)]
+            iei = get_iei(peaks_dec_dff, elapsed_time_list)  # s
+
             # store the analysis data
             self._analysis_data[well][str(label_value)] = ROIData(
                 raw_trace=cast(list[float], roi_trace.tolist()),
@@ -663,6 +673,7 @@ def _extract_trace_per_position(self, p: int) -> None:
                 active=len(peaks_dec_dff) > 0,
                 linear_phase=linear_phase,
                 cubic_phase=cubic_phase,
+                iei=iei,
             )
 
         # save json file
diff --git a/src/micromanager_gui/_plate_viewer/_plate_map.py b/src/micromanager_gui/_plate_viewer/_plate_map.py
index 7e624081..58dac555 100644
--- a/src/micromanager_gui/_plate_viewer/_plate_map.py
+++ b/src/micromanager_gui/_plate_viewer/_plate_map.py
@@ -307,7 +307,6 @@ def clear(self) -> None:
 
     def clear_condition(self) -> None:
         """Clear the condition of the selected wells."""
-        print("clear condition")
         wells: tuple[tuple[int, int]] = self._plate_view.selectedIndices()
         for well in wells:
             r, c = well
diff --git a/src/micromanager_gui/_plate_viewer/_plate_viewer.py b/src/micromanager_gui/_plate_viewer/_plate_viewer.py
index b9044813..1156e0ee 100644
--- a/src/micromanager_gui/_plate_viewer/_plate_viewer.py
+++ b/src/micromanager_gui/_plate_viewer/_plate_viewer.py
@@ -526,6 +526,8 @@ def _init_widget(
         """Initialize the widget with the given datastore."""
         # clear the image viewer cache
         self._image_viewer._viewer._contour_cache.clear()
+        self._plate_map_genotype.clear()
+        self._plate_map_treatment.clear()
 
         # load analysis json file if the path is not None
         if self._analysis_file_path:
diff --git a/src/micromanager_gui/_plate_viewer/_plot_methods.py b/src/micromanager_gui/_plate_viewer/_plot_methods.py
index b4060655..8d426888 100644
--- a/src/micromanager_gui/_plate_viewer/_plot_methods.py
+++ b/src/micromanager_gui/_plate_viewer/_plot_methods.py
@@ -10,6 +10,7 @@
     DEC_DFF_AMPLITUDE,
     DEC_DFF_AMPLITUDE_VS_FREQUENCY,
     DEC_DFF_FREQUENCY,
+    DEC_DFF_IEI,
     DEC_DFF_NORMALIZED,
     DEC_DFF_NORMALIZED_WITH_PEAKS,
     DEC_DFF_WITH_PEAKS,
@@ -37,12 +38,14 @@
     DEC_DFF_AMPLITUDE: {"dec": True, "amp": True},
     DEC_DFF_FREQUENCY: {"dec": True, "freq": True},
     DEC_DFF_AMPLITUDE_VS_FREQUENCY: {"dec": True, "amp": True, "freq": True},
+    DEC_DFF_IEI: {"dec": True, "iei": True},
 }
 
 MULTI_WELL_GRAPHS_OPTIONS: dict[str, dict[str, bool]] = {
     DEC_DFF_AMPLITUDE_VS_FREQUENCY: {"amp": True, "freq": True},
     DEC_DFF_AMPLITUDE: {"amp": True},
     DEC_DFF_FREQUENCY: {"freq": True},
+    DEC_DFF_IEI: {"iei": True},
 }
 
 
@@ -86,6 +89,7 @@ def _plot_single_well_traces(
     with_peaks: bool = False,
     amp: bool = False,
     freq: bool = False,
+    iei: bool = False,
 ) -> None:
     """Plot various types of traces."""
     # Clear the figure
@@ -139,14 +143,19 @@ def _plot_single_well_traces(
 
         elif freq:
             # plot frequency
+            ax.plot(int(key), roi_data.dec_dff_frequency, "o", label=f"ROI {key}")
+            ax.set_xlabel("ROIs")
+            ax.set_ylabel("Frequency")
+
+        elif iei:
+            # plot inter-event intervals
+            if roi_data.iei is None:
+                continue
             ax.plot(
-                int(key),
-                roi_data.dec_dff_frequency,
-                "o",
-                label=f"ROI {key}",
+                [int(key)] * len(roi_data.iei), roi_data.iei, "o", label=f"ROI {key}"
             )
             ax.set_xlabel("ROIs")
-            ax.set_ylabel("Frequency")
+            ax.set_ylabel("Inter-event intervals (sec)")
 
         else:
             # normalize if the flag is set
@@ -251,51 +260,78 @@ def _plot_multi_well_data(
     positions: list[int] | None = None,
     amp: bool = False,
     freq: bool = False,
+    iei: bool = False,
 ) -> None:
     """Plot the multi-well data."""
     widget.figure.clear()
     ax = widget.figure.add_subplot(111)
 
+    well_count: int = 0
+    wells_names: list[str] = []
     # loop over the data and plot the data
-    for key, position_data in data.items():
-        if positions is not None and int(key) not in positions:
+    for well_and_fov, roi_data in data.items():
+        if positions is not None and int(well_and_fov) not in positions:
             continue
 
-        for well in position_data:
+        # this is to store the well names to then use them as x axis labels
+        if (well := well_and_fov.split("_")[0]) not in wells_names:
+            wells_names.append(well)
+            # we increment the well count only when we find a new well
+            well_count += 1
+
+        for roi in roi_data:
+            r_data = roi_data[roi]
+
             if amp and freq:
-                well_data = position_data[well]
-                if well_data.peaks_amplitudes_dec_dff is None:
+                if r_data.peaks_amplitudes_dec_dff is None:
                     continue
-                amp_list = well_data.peaks_amplitudes_dec_dff
-                well_freq_list = [well_data.dec_dff_frequency] * len(amp_list)
-                ax.plot(amp_list, well_freq_list, "o", label=f"{key} - pos{well}")
+                amp_list = r_data.peaks_amplitudes_dec_dff
+                well_freq_list = [r_data.dec_dff_frequency] * len(amp_list)
+                ax.plot(
+                    amp_list, well_freq_list, "o", label=f"{well_and_fov} - roi{roi}"
+                )
                 ax.set_xlabel("Amplitude")
                 ax.set_ylabel("Frequency")
 
             elif amp:
-                well_data = position_data[well]
-                if well_data.peaks_amplitudes_dec_dff is None:
+                if r_data.peaks_amplitudes_dec_dff is None:
                     continue
                 ax.plot(
-                    [int(well)] * len(well_data.peaks_amplitudes_dec_dff),
-                    well_data.peaks_amplitudes_dec_dff,
+                    [well_count] * len(r_data.peaks_amplitudes_dec_dff),
+                    r_data.peaks_amplitudes_dec_dff,
                     "o",
-                    label=f"{key} - pos{well}",
+                    label=f"{well_and_fov} - roi{roi}",
                 )
                 ax.set_xlabel("Wells")
                 ax.set_ylabel("Amplitude")
 
             elif freq:
-                well_data = position_data[well]
                 ax.plot(
-                    int(well),
-                    well_data.dec_dff_frequency,
+                    well_count,
+                    r_data.dec_dff_frequency,
                     "o",
-                    label=f"{key} - pos{well}",
+                    label=f"{well_and_fov} - roi{roi}",
                 )
                 ax.set_xlabel("Wells")
                 ax.set_ylabel("Frequency")
 
+            elif iei:
+                if r_data.iei is None:
+                    continue
+                ax.plot(
+                    [well_count] * len(r_data.iei),
+                    r_data.iei,
+                    "o",
+                    label=f"{well_and_fov} - roi{roi}",
+                )
+                ax.set_xlabel("Wells")
+                ax.set_ylabel("Inter-event intervals (sec)")
+
+    # set ticks labels as well names
+    if (amp or freq or iei) and not (amp and freq):
+        ax.set_xticks(range(1, len(wells_names) + 1))
+        ax.set_xticklabels(wells_names)
+
     widget.figure.tight_layout()
 
     # add hover functionality to display the well position
diff --git a/src/micromanager_gui/_plate_viewer/_util.py b/src/micromanager_gui/_plate_viewer/_util.py
index 186477a2..942eb637 100644
--- a/src/micromanager_gui/_plate_viewer/_util.py
+++ b/src/micromanager_gui/_plate_viewer/_util.py
@@ -43,10 +43,12 @@
 DEC_DFF_AMPLITUDE = "Deconvolved DeltaF/F0 Amplitudes"
 DEC_DFF_FREQUENCY = "Deconvolved DeltaF/F0 Frequencies"
 DEC_DFF_AMPLITUDE_VS_FREQUENCY = "Deconvolved DeltaF/F0 Amplitudes vs Frequencies"
+DEC_DFF_IEI = "Deconvolved DeltaF/F0 Inter-event Interval"
 
 DEC_DFF_AMPLITUDE_VS_FREQUENCY_ALL = "Deconvolved DeltaF/F0 Amplitudes vs Frequencies"
 DEC_DFF_AMPLITUDE_ALL = "Deconvolved DeltaF/F0 Amplitudes"
 DEC_DFF_FREQUENCY_ALL = "Deconvolved DeltaF/F0 Frequencies"
+DEC_DFF_IEI_ALL = "Deconvolved DeltaF/F0 Inter-event Interval"
 
 SINGLE_WELL_COMBO_OPTIONS = [
     RAW_TRACES,
@@ -60,12 +62,14 @@
     DEC_DFF_AMPLITUDE,
     DEC_DFF_FREQUENCY,
     DEC_DFF_AMPLITUDE_VS_FREQUENCY,
+    DEC_DFF_IEI,
 ]
 
 MULTI_WELL_COMBO_OPTIONS = [
     DEC_DFF_AMPLITUDE_VS_FREQUENCY_ALL,
     DEC_DFF_AMPLITUDE_ALL,
     DEC_DFF_FREQUENCY_ALL,
+    DEC_DFF_IEI_ALL,
 ]
 # ------------------------------------------------------------------------------------
 
@@ -99,6 +103,7 @@ class ROIData(BaseClass):
     active: bool | None = None
     linear_phase: list[float] | None = None
     cubic_phase: list[float] | None = None
+    iei: list[float] | None = None  # interevent interval
     # ... add whatever other data we need
 
 
@@ -364,9 +369,7 @@ def get_cubic_phase(total_frames: int, peaks: np.ndarray) -> list[float]:
     phases = np.clip(phases, 0, None)
     phases = np.mod(phases, 2 * np.pi)
 
-    phase_list = [float(phase) for phase in phases]
-
-    return phase_list
+    return [float(phase) for phase in phases]
 
 
 def get_connectivity(phase_dict: dict[str, list[float]]) -> float | None:
@@ -382,14 +385,11 @@ def get_connectivity(phase_dict: dict[str, list[float]]) -> float | None:
     ):
         return None
 
-    # Compute mean connectivity
-    mean_connect = float(
+    return float(
         np.median(np.sum(connection_matrix, axis=0) - 1)
         / (connection_matrix.shape[0] - 1)
     )
 
-    return mean_connect
-
 
 def _get_connectivity_matrix(phase_dict: dict[str, list[float]]) -> np.ndarray | None:
     """Calculate global connectivity using vectorized operations."""
@@ -413,7 +413,20 @@ def _get_connectivity_matrix(phase_dict: dict[str, list[float]]) -> np.ndarray |
     cos_mean = np.mean(np.cos(phase_diff), axis=2)  # Shape: (N, N)
     sin_mean = np.mean(np.sin(phase_diff), axis=2)  # Shape: (N, N)
 
-    # Compute synchronization index (vectorized)
-    connect_matrix = np.array(np.sqrt(cos_mean**2 + sin_mean**2))
+    return np.array(np.sqrt(cos_mean**2 + sin_mean**2))
+
+
+def get_iei(peaks: list[int], elapsed_time_list: list[float]) -> list[float] | None:
+    """Calculate the interevent interval."""
+    # if less than 2 peaks or framerate is negative
+    if len(peaks) < 2 or len(elapsed_time_list) <= 1:
+        return None
+
+    peaks_time_stamps = [elapsed_time_list[i] for i in peaks]
+
+    # calculate the difference in time between two consecutive peaks
+    iei_ms = np.diff(np.array(peaks_time_stamps))
+
+    iei = [float(iei_peak / 1000) for iei_peak in iei_ms]  # convert from ms to s
 
-    return connect_matrix
+    return iei


Reviews:
user: login: fdrgsp
state: COMMENTED
author_association: OWNER
submitted_at: 2025-03-04T02:04:14Z
user: login: annayang26
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T02:09:58Z

Review comments:
diff_hunk: @@ -645,6 +648,8 @@ def _extract_trace_per_position(self, p: int) -> None:
                 linear_phase = get_linear_phase(timepoints, peaks_dec_dff)
                 cubic_phase = get_cubic_phase(timepoints, peaks_dec_dff)
 
+            iei = get_iei(peaks_dec_dff, exp_time)  # s
path: src/micromanager_gui/_plate_viewer/_analysis.py
user: login: fdrgsp
body: Hey @annayang26 , can you try to use the actual interval values from the metadata since we have them (`elapsed_time_list`)? Something like below...basically the `elapsed_time_list` contains the actual timestamp of each frame so I think we can use it to get the iei. I imagine that the `get_iei` method will have a  `elapsed_time_list` as argument instead of the `exposure_time`...

```suggestion
            # if the elapsed time is not available or for any reason is different from
            # the number of timepoints, set it as list of timepoints every exp_time
            if len(elapsed_time_list) != timepoints:
                elapsed_time_list = [i * exp_time for i in range(timepoints)]
            # otherwise, use the elapsed time
            else:
                iei = get_iei(peaks_dec_dff, elapsed_time_list)  # s
```
created_at: 2025-03-04T02:04:14Z
author_association: OWNER
diff_hunk: @@ -645,6 +648,8 @@ def _extract_trace_per_position(self, p: int) -> None:
                 linear_phase = get_linear_phase(timepoints, peaks_dec_dff)
                 cubic_phase = get_cubic_phase(timepoints, peaks_dec_dff)
 
+            iei = get_iei(peaks_dec_dff, exp_time)  # s
path: src/micromanager_gui/_plate_viewer/_analysis.py
user: login: annayang26
body: Will do! I will change the code tomorrow.
created_at: 2025-03-04T02:09:58Z
author_association: COLLABORATOR

PR comments:
user: login: annayang26
created_at: 2025-03-03T16:12:34Z
body: @fdrgsp Done! :D
author_association: COLLABORATOR
user: login: annayang26
created_at: 2025-03-04T19:42:24Z
body: Hi @fdrgsp, I changed the code to use the elapsed time between two peaks to calculate IEI as you suggested. I also checked the IEI generated between the new and the old code, and they are similar (the results from the new code are more precise with more decimal places). 
author_association: COLLABORATOR
user: login: fdrgsp
created_at: 2025-03-06T04:59:10Z
body: Thanks @annayang26!
author_association: OWNER