itasc 0.2.0.dev0__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.
Files changed (162) hide show
  1. itasc/__init__.py +16 -0
  2. itasc/cellpose/__init__.py +31 -0
  3. itasc/cellpose/cellpose_runner.py +334 -0
  4. itasc/cellpose/divergence_maps.py +380 -0
  5. itasc/cellpose/flow_following.py +283 -0
  6. itasc/cellpose/joint.py +98 -0
  7. itasc/cellpose/napari.yaml +10 -0
  8. itasc/cellpose/native_masks.py +313 -0
  9. itasc/cellpose/retrack.py +232 -0
  10. itasc/cellpose/shape.py +51 -0
  11. itasc/cellpose/track_laptrack.py +279 -0
  12. itasc/contact_analysis/__init__.py +61 -0
  13. itasc/contact_analysis/_provenance.py +29 -0
  14. itasc/contact_analysis/catalog.py +393 -0
  15. itasc/contact_analysis/config.py +155 -0
  16. itasc/contact_analysis/contacts/__init__.py +24 -0
  17. itasc/contact_analysis/contacts/batch.py +158 -0
  18. itasc/contact_analysis/contacts/build.py +837 -0
  19. itasc/contact_analysis/contacts/contact_labels.py +136 -0
  20. itasc/contact_analysis/contacts/neighborhood.py +146 -0
  21. itasc/contact_analysis/contacts/reader.py +58 -0
  22. itasc/contact_analysis/contacts/signed_contact_length.py +143 -0
  23. itasc/contact_analysis/curation.py +185 -0
  24. itasc/contact_analysis/dynamics/__init__.py +43 -0
  25. itasc/contact_analysis/dynamics/collective.py +206 -0
  26. itasc/contact_analysis/dynamics/kinematics.py +278 -0
  27. itasc/contact_analysis/dynamics/msd.py +165 -0
  28. itasc/contact_analysis/dynamics/store.py +251 -0
  29. itasc/contact_analysis/dynamics/trajectories.py +100 -0
  30. itasc/contact_analysis/frame_interval.py +85 -0
  31. itasc/contact_analysis/napari.yaml +10 -0
  32. itasc/contact_analysis/pipeline.py +313 -0
  33. itasc/contact_analysis/pixel_size.py +166 -0
  34. itasc/contact_analysis/quantifier.py +204 -0
  35. itasc/contact_analysis/quantifiers/__init__.py +7 -0
  36. itasc/contact_analysis/quantifiers/_contacts_derived.py +42 -0
  37. itasc/contact_analysis/quantifiers/cell_density.py +65 -0
  38. itasc/contact_analysis/quantifiers/cell_dynamics.py +81 -0
  39. itasc/contact_analysis/quantifiers/cell_shape.py +32 -0
  40. itasc/contact_analysis/quantifiers/contacts.py +72 -0
  41. itasc/contact_analysis/quantifiers/neighbor_count.py +26 -0
  42. itasc/contact_analysis/quantifiers/nucleus_dynamics.py +77 -0
  43. itasc/contact_analysis/quantifiers/nucleus_shape.py +35 -0
  44. itasc/contact_analysis/quantifiers/shape_relational.py +34 -0
  45. itasc/contact_analysis/quantifiers/signed_contact_length.py +44 -0
  46. itasc/contact_analysis/records.py +129 -0
  47. itasc/contact_analysis/shape/__init__.py +19 -0
  48. itasc/contact_analysis/shape/core.py +141 -0
  49. itasc/contact_analysis/shape/relational.py +134 -0
  50. itasc/contact_analysis/shape_tables.py +387 -0
  51. itasc/core/__init__.py +0 -0
  52. itasc/core/cancellation.py +15 -0
  53. itasc/core/commit.py +61 -0
  54. itasc/core/imageops.py +34 -0
  55. itasc/core/label_store.py +53 -0
  56. itasc/core/lineage.py +113 -0
  57. itasc/core/logging.py +51 -0
  58. itasc/core/paths.py +34 -0
  59. itasc/core/tiff.py +13 -0
  60. itasc/correction/__init__.py +25 -0
  61. itasc/correction/labels.py +983 -0
  62. itasc/napari/__init__.py +7 -0
  63. itasc/napari/_experiments_panel.py +1091 -0
  64. itasc/napari/_flow_layout.py +98 -0
  65. itasc/napari/_icons.py +126 -0
  66. itasc/napari/_napari_compat.py +118 -0
  67. itasc/napari/_paths.py +231 -0
  68. itasc/napari/_preview_cache.py +60 -0
  69. itasc/napari/_spotlight.py +46 -0
  70. itasc/napari/_stage_loader.py +146 -0
  71. itasc/napari/_stage_status.py +110 -0
  72. itasc/napari/_standalone_paths.py +89 -0
  73. itasc/napari/_state.py +146 -0
  74. itasc/napari/_status_rail.py +128 -0
  75. itasc/napari/_track_render.py +114 -0
  76. itasc/napari/_widget_helpers.py +236 -0
  77. itasc/napari/aggregate_widget.py +339 -0
  78. itasc/napari/candidate_gallery_controller.py +285 -0
  79. itasc/napari/cell_workflow_widget.py +1405 -0
  80. itasc/napari/cellpose_segment_track_widget.py +1253 -0
  81. itasc/napari/cellpose_widget.py +1034 -0
  82. itasc/napari/contact_analysis/__init__.py +6 -0
  83. itasc/napari/contact_analysis/curation_controller.py +94 -0
  84. itasc/napari/contact_analysis/plugins/__init__.py +102 -0
  85. itasc/napari/contact_analysis/plugins/catalog_summary.py +56 -0
  86. itasc/napari/contact_analysis/plugins/curation.py +247 -0
  87. itasc/napari/contact_analysis/plugins/visualize_contacts.py +64 -0
  88. itasc/napari/contact_analysis_widget.py +951 -0
  89. itasc/napari/contact_visualization.py +895 -0
  90. itasc/napari/correction/__init__.py +8 -0
  91. itasc/napari/correction/_correction_anchor.py +54 -0
  92. itasc/napari/correction/_correction_candidate_panel.py +259 -0
  93. itasc/napari/correction/_correction_candidates.py +169 -0
  94. itasc/napari/correction/_correction_centroids.py +132 -0
  95. itasc/napari/correction/_correction_commit.py +71 -0
  96. itasc/napari/correction/_correction_events.py +39 -0
  97. itasc/napari/correction/_correction_keymap.py +80 -0
  98. itasc/napari/correction/_correction_layer_lifecycle.py +137 -0
  99. itasc/napari/correction/_correction_layer_loader.py +86 -0
  100. itasc/napari/correction/_correction_navigation.py +97 -0
  101. itasc/napari/correction/_correction_paint.py +46 -0
  102. itasc/napari/correction/_correction_playback.py +68 -0
  103. itasc/napari/correction/_correction_protection.py +44 -0
  104. itasc/napari/correction/_correction_takeover.py +64 -0
  105. itasc/napari/correction/_correction_track_accordion.py +566 -0
  106. itasc/napari/correction/_correction_track_path.py +388 -0
  107. itasc/napari/correction/_correction_ui.py +367 -0
  108. itasc/napari/correction/_correction_ui_nucleus.py +393 -0
  109. itasc/napari/correction/_correction_utils.py +204 -0
  110. itasc/napari/correction/_correction_validation.py +83 -0
  111. itasc/napari/correction/cell_correction_widget.py +1190 -0
  112. itasc/napari/correction/correction_widget.py +1363 -0
  113. itasc/napari/correction/nucleus_correction_widget.py +2185 -0
  114. itasc/napari/divergence_maps_widget.py +795 -0
  115. itasc/napari/lineage_canvas_controller.py +351 -0
  116. itasc/napari/main_widget.py +1184 -0
  117. itasc/napari/nucleus_atom_extraction_widget.py +648 -0
  118. itasc/napari/nucleus_db_browser_widget.py +1442 -0
  119. itasc/napari/nucleus_pipeline_widget.py +579 -0
  120. itasc/napari/nucleus_segmentation_inputs_widget.py +21 -0
  121. itasc/napari/nucleus_tracking_inputs_widget.py +248 -0
  122. itasc/napari/nucleus_workflow_widget.py +963 -0
  123. itasc/napari/track_path_controller.py +387 -0
  124. itasc/napari/ui_gate.py +266 -0
  125. itasc/napari/ui_style.py +503 -0
  126. itasc/napari/validated_overlay_controller.py +254 -0
  127. itasc/napari/widgets.py +616 -0
  128. itasc/napari.yaml +45 -0
  129. itasc/segmentation/__init__.py +60 -0
  130. itasc/segmentation/cell_divergence_segmentation.py +433 -0
  131. itasc/segmentation/cell_label_icm.py +637 -0
  132. itasc/segmentation/contour_filtering.py +128 -0
  133. itasc/segmentation/lineage.py +18 -0
  134. itasc/segmentation/nucleus_segmentation.py +35 -0
  135. itasc/tracking_ultrack/__init__.py +12 -0
  136. itasc/tracking_ultrack/_node_geometry.py +191 -0
  137. itasc/tracking_ultrack/atoms.py +470 -0
  138. itasc/tracking_ultrack/config.py +76 -0
  139. itasc/tracking_ultrack/corrections.py +1213 -0
  140. itasc/tracking_ultrack/db_build.py +492 -0
  141. itasc/tracking_ultrack/db_query.py +952 -0
  142. itasc/tracking_ultrack/export.py +133 -0
  143. itasc/tracking_ultrack/extend.py +243 -0
  144. itasc/tracking_ultrack/ingest.py +83 -0
  145. itasc/tracking_ultrack/linking.py +286 -0
  146. itasc/tracking_ultrack/multi_threshold.py +46 -0
  147. itasc/tracking_ultrack/napari.yaml +10 -0
  148. itasc/tracking_ultrack/reseed.py +456 -0
  149. itasc/tracking_ultrack/retracker.py +153 -0
  150. itasc/tracking_ultrack/scoring.py +74 -0
  151. itasc/tracking_ultrack/seed_prior.py +175 -0
  152. itasc/tracking_ultrack/solve.py +52 -0
  153. itasc/tracking_ultrack/swap_candidate.py +171 -0
  154. itasc/tracking_ultrack/track_quality.py +142 -0
  155. itasc/tracking_ultrack/validation_nodes.py +315 -0
  156. itasc/tracking_ultrack/validation_state.py +430 -0
  157. itasc-0.2.0.dev0.data/data/itasc/napari.yaml +45 -0
  158. itasc-0.2.0.dev0.dist-info/METADATA +186 -0
  159. itasc-0.2.0.dev0.dist-info/RECORD +162 -0
  160. itasc-0.2.0.dev0.dist-info/WHEEL +4 -0
  161. itasc-0.2.0.dev0.dist-info/entry_points.txt +2 -0
  162. itasc-0.2.0.dev0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,7 @@
1
+ """Quantifier plugins.
2
+
3
+ Dropping a ``*.py`` module here whose class subclasses
4
+ :class:`itasc.contact_analysis.quantifier.Quantifier` (with a non-empty
5
+ ``quantity_id``) registers a new per-position quantity. ``available_quantifiers``
6
+ imports every module here so they self-register.
7
+ """
@@ -0,0 +1,42 @@
1
+ """Shared plumbing for the contacts-*derived* quantifiers.
2
+
3
+ The neighbor count / density / signed-contact-length quantities are all computed
4
+ from a position's already-built ``contact_analysis.h5``. They are **pooled**
5
+ quantities: each runs its compute function over the contacts artifact at aggregate
6
+ time (in :meth:`Quantifier.compute_object_table`) and the tidy table goes straight
7
+ into the pooled output — no per-position artifact is persisted.
8
+
9
+ This module factors out the one thing those quantifiers share: loading the
10
+ contacts artifact for a position. All of it is **label-agnostic**.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ from itasc.contact_analysis.contacts.reader import (
17
+ PositionContactAnalysis,
18
+ read_position_contacts,
19
+ )
20
+ from itasc.contact_analysis.quantifier import PositionInputs
21
+
22
+ __all__ = [
23
+ "PositionContactAnalysis",
24
+ "load_analysis",
25
+ ]
26
+
27
+
28
+ def load_analysis(inputs: PositionInputs) -> PositionContactAnalysis:
29
+ """Read the position's contacts artifact, or raise a clear error.
30
+
31
+ The derived quantifiers ``require`` ``contact_analysis_path``, so a missing /
32
+ not-yet-built file is a real precondition failure (a position lacking its
33
+ ``contact_analysis.h5`` is scoped out before pooling) rather than a silent
34
+ skip.
35
+ """
36
+ path = inputs.contact_analysis_path
37
+ if path is None or not Path(path).is_file():
38
+ raise FileNotFoundError(
39
+ "contact_analysis.h5 not found — build 'Cell–cell contacts' for this "
40
+ f"position first (looked for {path!r})."
41
+ )
42
+ return read_position_contacts(path)
@@ -0,0 +1,65 @@
1
+ """Cell-density quantifier — per-frame cell counts per field-of-view area.
2
+
3
+ Counts cells straight off the tracked **cell labels** (unique non-zero labels per
4
+ frame), so it is a cell-labels quantity, not a contacts-derived one. The
5
+ field-of-view area is **required** — it comes from the shared params bar and there
6
+ is no silent image-area fallback; ``density = n_cells / fov_area_mm2`` in
7
+ cells/mm². One ``all`` total row per frame (label-agnostic). A **pooled** quantity:
8
+ :meth:`compute_object_table` builds the tidy table in memory (no per-position
9
+ artifact).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Mapping
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+
18
+ from itasc.contact_analysis.contacts.neighborhood import cell_density
19
+ from itasc.contact_analysis.quantifier import PositionInputs, Quantifier
20
+ from itasc.contact_analysis.shape.core import read_label_stack
21
+
22
+
23
+ class CellDensityQuantifier(Quantifier):
24
+ """Per ``(frame, label)`` cell count and density (cells/mm²)."""
25
+
26
+ quantity_id = "cell_density"
27
+ display_name = "Cell density"
28
+ # Cell labels are the only hard input — counts come straight off them. The
29
+ # field-of-view area is a required build *param* (validated in
30
+ # ``compute_object_table``), not a PositionInputs field.
31
+ requires = ("cell_labels_path",)
32
+ # Density is keyed per (frame, label); ``label`` is always ``"all"``.
33
+ table_keys = ("frame", "label")
34
+ wants_build_params = True
35
+ # The field-of-view area has no image-area fallback, so it is a hard build
36
+ # requirement: the pooling loop skips Cell density (and the UI greys it out)
37
+ # until it is set, instead of letting the compute raise.
38
+ required_build_params = {"fov_area_mm2": "FOV area (mm²)"}
39
+
40
+ def compute_object_table(
41
+ self, inputs: PositionInputs, *, params: dict | None = None
42
+ ) -> Mapping[str, np.ndarray]:
43
+ fov = (params or {}).get("fov_area_mm2")
44
+ if not (isinstance(fov, (int, float)) and fov > 0):
45
+ raise ValueError(
46
+ "Cell density requires a field-of-view area — set 'FOV area (mm²)' "
47
+ "in Parameters before building."
48
+ )
49
+ frame_cells = _frame_cells_from_labels(inputs.cell_labels_path)
50
+ return cell_density(frame_cells, fov_area_mm2=float(fov))
51
+
52
+
53
+ def _frame_cells_from_labels(labels_path: Path | None) -> dict[int, list[int]]:
54
+ """``frame -> [cell_id, …]`` from a tracked cell-label TIFF.
55
+
56
+ The unique non-zero labels of frame *i* are the cells present in it — the same
57
+ set the contacts ``cells`` table holds (it is one ``regionprops`` row per
58
+ label), so this reproduces the old per-frame counts without the artifact.
59
+ """
60
+ stack = read_label_stack(Path(labels_path))
61
+ out: dict[int, list[int]] = {}
62
+ for frame_idx, frame in enumerate(stack):
63
+ ids = np.unique(frame)
64
+ out[frame_idx] = [int(i) for i in ids if i != 0]
65
+ return out
@@ -0,0 +1,81 @@
1
+ """Cell-dynamics quantifier — the registry adapter over the dynamics core.
2
+
3
+ Wraps :mod:`itasc.contact_analysis.dynamics` so the studio can build
4
+ and read per-cell motion through the generic :class:`Quantifier` interface. Its
5
+ persistence is a multi-table ``4_contact_analysis/cell_dynamics.h5``;
6
+ :meth:`object_table` exposes the per-frame instantaneous table (the only
7
+ ``(frame, cell_id)`` table) to the plotting backend. The cell twin of
8
+ :mod:`.nucleus_dynamics` — they share the label-agnostic core and differ only by
9
+ which label field they read and their output filename.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable, Mapping
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+
18
+ from itasc.contact_analysis.dynamics import (
19
+ TrackDynamics,
20
+ build_track_dynamics,
21
+ read_instantaneous_table,
22
+ read_track_dynamics,
23
+ )
24
+ from itasc.contact_analysis.dynamics.kinematics import instantaneous_table
25
+ from itasc.contact_analysis.dynamics.trajectories import extract_trajectories
26
+ from itasc.contact_analysis.quantifier import PositionInputs, Quantifier
27
+
28
+
29
+ class CellDynamicsQuantifier(Quantifier):
30
+ """Quantifies per-cell motion (speed, persistence, MSD, collective) from cell labels."""
31
+
32
+ quantity_id = "cell_dynamics"
33
+ display_name = "Cell dynamics"
34
+ requires = ("cell_labels_path",)
35
+ # Pixel size (µm/px) + frame interval (s/frame) are global build params, set
36
+ # once in the Parameters panel and applied to every position, so each output
37
+ # lands in physical units. They gate the build (unset ⇒ not buildable) and
38
+ # reach :meth:`build` via the stamped ``PositionInputs``, not the params dict.
39
+ required_build_params = {
40
+ "pixel_size_um": "pixel size (µm/px)",
41
+ "time_interval_s": "frame interval (s)",
42
+ }
43
+
44
+ default_output_name = "cell_dynamics.h5"
45
+ # The object_table is the per-(frame, cell_id) instantaneous motion table. The
46
+ # per-track / per-tissue / curve sub-tables are separate views (tracks / frames
47
+ # / dac_curves) and are not this quantifier's object_table.
48
+ table_keys = ("frame", "cell_id")
49
+
50
+ def build(
51
+ self,
52
+ inputs: PositionInputs,
53
+ output_path: Path,
54
+ *,
55
+ params: dict | None = None,
56
+ progress_cb: Callable[[int, int, str], None] | None = None,
57
+ ) -> Path:
58
+ return build_track_dynamics(
59
+ inputs.cell_labels_path,
60
+ output_path,
61
+ pixel_size_um=inputs.pixel_size_um,
62
+ time_interval_s=inputs.time_interval_s,
63
+ source_path=inputs.position_dir,
64
+ params=params,
65
+ quantity_id=self.quantity_id,
66
+ progress_cb=progress_cb,
67
+ )
68
+
69
+ def read(self, output_path: Path) -> TrackDynamics:
70
+ return read_track_dynamics(output_path)
71
+
72
+ def object_table(self, output_path: Path) -> Mapping[str, np.ndarray]:
73
+ return read_instantaneous_table(output_path)
74
+
75
+ def compute_object_table(
76
+ self, inputs: PositionInputs, *, params: dict | None = None
77
+ ) -> Mapping[str, np.ndarray]:
78
+ trajectories = extract_trajectories(
79
+ inputs.cell_labels_path, pixel_size_um=inputs.pixel_size_um
80
+ )
81
+ return instantaneous_table(trajectories, time_interval_s=inputs.time_interval_s)
@@ -0,0 +1,32 @@
1
+ """Cell-shape quantifier — the registry adapter over the shape core.
2
+
3
+ Exposes per-cell morphology through the generic :class:`Quantifier` interface as
4
+ a **pooled** quantity: :meth:`compute_object_table` runs the shape core over the
5
+ cell label stack in memory and hands the tidy table straight to the pooling layer
6
+ (no per-position artifact is persisted). The object-key column is ``cell_id`` (the
7
+ shared track id).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from itasc.contact_analysis.quantifier import Quantifier
12
+ from itasc.contact_analysis.shape import compute_object_shape
13
+
14
+
15
+ class CellShapeQuantifier(Quantifier):
16
+ """Quantifies per-cell, per-frame shape descriptors from cell labels."""
17
+
18
+ quantity_id = "cell_shape"
19
+ display_name = "Cell shape"
20
+ # Cell labels only; no nucleus / contacts dependency. Pixel size (to emit
21
+ # physical µm / µm²) is a global build param set in the Parameters panel.
22
+ requires = ("cell_labels_path",)
23
+ required_build_params = {"pixel_size_um": "pixel size (µm/px)"}
24
+ #: Per-cell, per-frame shape descriptors, keyed (frame, cell_id).
25
+ table_keys = ("frame", "cell_id")
26
+
27
+ def compute_object_table(self, inputs, *, params=None):
28
+ return compute_object_shape(
29
+ inputs.cell_labels_path,
30
+ pixel_size_um=inputs.pixel_size_um,
31
+ object_key="cell_id",
32
+ )
@@ -0,0 +1,72 @@
1
+ """Cell-cell contacts quantifier — the registry adapter over the contacts code.
2
+
3
+ Wraps :mod:`itasc.contact_analysis.contacts` so the studio can build
4
+ and read contacts through the generic :class:`Quantifier` interface. Its chosen
5
+ persistence is the ``contact_analysis.h5`` schema, unchanged from before the
6
+ rename.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable, Mapping
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import numpy as np
15
+
16
+ from itasc.contact_analysis.contacts.build import build_contacts
17
+ from itasc.contact_analysis.contacts.reader import (
18
+ read_position_contacts,
19
+ )
20
+ from itasc.contact_analysis.quantifier import PositionInputs, Quantifier
21
+
22
+
23
+ class ContactsQuantifier(Quantifier):
24
+ """Quantifies cell-cell contacts: edges, T1 events, NLS classes."""
25
+
26
+ quantity_id = "contacts"
27
+ display_name = "Cell–cell contacts"
28
+ # Cell labels are mandatory; nucleus labels are optional (when present they
29
+ # enable the cell_id == nucleus_id invariant check and NLS classification).
30
+ requires = ("cell_labels_path",)
31
+ #: The contacts artifact is the input the contacts-derived quantifiers
32
+ #: (neighbor count / enrichment / z-score / density / signed contact length) consume.
33
+ produces = "contact_analysis_path"
34
+
35
+ #: Default artifact name when a position does not dictate one.
36
+ default_output_name = "contact_analysis.h5"
37
+
38
+ def build(
39
+ self,
40
+ inputs: PositionInputs,
41
+ output_path: Path,
42
+ *,
43
+ params: dict | None = None,
44
+ progress_cb: Callable[[int, int, str], None] | None = None,
45
+ ) -> Path:
46
+ return build_contacts(
47
+ cell_labels_path=inputs.cell_labels_path,
48
+ nucleus_labels_path=inputs.nucleus_labels_path,
49
+ output_path=output_path,
50
+ source_path=inputs.position_dir,
51
+ edge_extraction_params=params,
52
+ progress_cb=progress_cb,
53
+ )
54
+
55
+ def default_output(self, inputs: PositionInputs) -> Path:
56
+ """The contacts ``.h5`` lives in the position base folder, beside the
57
+ committed ``cell_labels.tif`` / ``nucleus_labels.tif`` — not under the
58
+ shared :data:`~itasc.contact_analysis.quantifier.OUTPUT_SUBDIR` that the
59
+ other (dynamics) quantifiers persist into. One homogeneous per-position
60
+ layout for the downstream-stable inputs and this derived-from-them graph.
61
+ """
62
+ return inputs.position_dir / self.default_output_name
63
+
64
+ def read(self, output_path: Path) -> Any:
65
+ return read_position_contacts(output_path)
66
+
67
+ def object_table(self, output_path: Path) -> Mapping[str, np.ndarray]:
68
+ """The per-cell ``cells`` table (``frame`` · ``cell_id`` · morphometry).
69
+
70
+ The subpopulation label is no longer carried here — it lives in the NLS
71
+ sidecar CSV and is joined by ``cell_id`` at pool time."""
72
+ return read_position_contacts(output_path).cells
@@ -0,0 +1,26 @@
1
+ """Neighbor-count quantifier — per-cell adjacency degree from the contact graph.
2
+
3
+ A contacts-derived **pooled** quantity: :meth:`compute_object_table` reads the
4
+ position's ``contact_analysis.h5`` and returns the per ``(frame, cell_id)``
5
+ neighbor count in memory (no per-position artifact). The NLS subpopulation label
6
+ is *not* stored here — it is left-joined by ``cell_id`` at pool time, exactly as
7
+ the shape family does.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from itasc.contact_analysis.contacts.neighborhood import cell_neighbor_counts
12
+ from itasc.contact_analysis.quantifier import Quantifier
13
+ from itasc.contact_analysis.quantifiers import _contacts_derived as derived
14
+
15
+
16
+ class NeighborCountQuantifier(Quantifier):
17
+ """Per ``(frame, cell_id)`` number of distinct cell-cell neighbors."""
18
+
19
+ quantity_id = "neighbor_count"
20
+ display_name = "Neighbor count"
21
+ requires = ("contact_analysis_path",)
22
+ # Per-cell adjacency degree, keyed (frame, cell_id).
23
+ table_keys = ("frame", "cell_id")
24
+
25
+ def compute_object_table(self, inputs, *, params=None):
26
+ return dict(cell_neighbor_counts(derived.load_analysis(inputs)))
@@ -0,0 +1,77 @@
1
+ """Nucleus-dynamics quantifier — the registry adapter over the dynamics core.
2
+
3
+ The nucleus twin of :mod:`.cell_dynamics`: it runs the same label-agnostic
4
+ :func:`build_track_dynamics` over the **nucleus** label stack instead of the cell
5
+ one, persisting ``4_contact_analysis/nucleus_dynamics.h5``. Nuclei are
6
+ compact, point-like centroids — the robust default for motility / MSD. The
7
+ object-key column stays ``cell_id`` (a nucleus is nucleus-seeded so it carries
8
+ its cell's shared track id), so the pooling / plotting layer treats it no
9
+ differently.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable, Mapping
14
+ from pathlib import Path
15
+
16
+ import numpy as np
17
+
18
+ from itasc.contact_analysis.dynamics import (
19
+ TrackDynamics,
20
+ build_track_dynamics,
21
+ read_instantaneous_table,
22
+ read_track_dynamics,
23
+ )
24
+ from itasc.contact_analysis.dynamics.kinematics import instantaneous_table
25
+ from itasc.contact_analysis.dynamics.trajectories import extract_trajectories
26
+ from itasc.contact_analysis.quantifier import PositionInputs, Quantifier
27
+
28
+
29
+ class NucleusDynamicsQuantifier(Quantifier):
30
+ """Quantifies per-nucleus motion (speed, persistence, MSD, collective) from nucleus labels."""
31
+
32
+ quantity_id = "nucleus_dynamics"
33
+ display_name = "Nucleus dynamics"
34
+ requires = ("nucleus_labels_path",)
35
+ # Pixel size + frame interval are global build params (see CellDynamics).
36
+ required_build_params = {
37
+ "pixel_size_um": "pixel size (µm/px)",
38
+ "time_interval_s": "frame interval (s)",
39
+ }
40
+
41
+ default_output_name = "nucleus_dynamics.h5"
42
+ # Instantaneous nucleus motion is keyed on the shared cell track id
43
+ # (frame, cell_id).
44
+ table_keys = ("frame", "cell_id")
45
+
46
+ def build(
47
+ self,
48
+ inputs: PositionInputs,
49
+ output_path: Path,
50
+ *,
51
+ params: dict | None = None,
52
+ progress_cb: Callable[[int, int, str], None] | None = None,
53
+ ) -> Path:
54
+ return build_track_dynamics(
55
+ inputs.nucleus_labels_path,
56
+ output_path,
57
+ pixel_size_um=inputs.pixel_size_um,
58
+ time_interval_s=inputs.time_interval_s,
59
+ source_path=inputs.position_dir,
60
+ params=params,
61
+ quantity_id=self.quantity_id,
62
+ progress_cb=progress_cb,
63
+ )
64
+
65
+ def read(self, output_path: Path) -> TrackDynamics:
66
+ return read_track_dynamics(output_path)
67
+
68
+ def object_table(self, output_path: Path) -> Mapping[str, np.ndarray]:
69
+ return read_instantaneous_table(output_path)
70
+
71
+ def compute_object_table(
72
+ self, inputs: PositionInputs, *, params: dict | None = None
73
+ ) -> Mapping[str, np.ndarray]:
74
+ trajectories = extract_trajectories(
75
+ inputs.nucleus_labels_path, pixel_size_um=inputs.pixel_size_um
76
+ )
77
+ return instantaneous_table(trajectories, time_interval_s=inputs.time_interval_s)
@@ -0,0 +1,35 @@
1
+ """Nucleus-shape quantifier — the registry adapter over the shape core.
2
+
3
+ The nucleus twin of :mod:`.cell_shape`: :meth:`compute_object_table` runs the same
4
+ label-agnostic :func:`compute_object_shape` over the **nucleus** label stack
5
+ instead of the cell one and pools the tidy table in memory (no per-position
6
+ artifact). The object-key column stays ``cell_id`` — a nucleus is nucleus-seeded
7
+ so it carries its cell's shared track id — so nothing in the pooling layer treats
8
+ it differently.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from itasc.contact_analysis.quantifier import Quantifier
13
+ from itasc.contact_analysis.shape import compute_object_shape
14
+
15
+
16
+ class NucleusShapeQuantifier(Quantifier):
17
+ """Quantifies per-nucleus, per-frame shape descriptors from nucleus labels."""
18
+
19
+ quantity_id = "nucleus_shape"
20
+ display_name = "Nucleus shape"
21
+ # Nucleus labels only; pixel size (to emit physical µm / µm²) is a global
22
+ # build param set in the Parameters panel.
23
+ requires = ("nucleus_labels_path",)
24
+ required_build_params = {"pixel_size_um": "pixel size (µm/px)"}
25
+ # Nucleus shape is keyed on its cell's shared track id (frame, cell_id); its
26
+ # descriptors are namespaced by quantity_id (so the nucleus ``area`` never
27
+ # collides with the cell ``area`` in a joined view).
28
+ table_keys = ("frame", "cell_id")
29
+
30
+ def compute_object_table(self, inputs, *, params=None):
31
+ return compute_object_shape(
32
+ inputs.nucleus_labels_path,
33
+ pixel_size_um=inputs.pixel_size_um,
34
+ object_key="cell_id",
35
+ )
@@ -0,0 +1,34 @@
1
+ """Relational shape quantifier — nucleus-vs-cell paired morphology.
2
+
3
+ Wraps :func:`itasc.contact_analysis.shape.compute_relational_table`, which
4
+ pairs each nucleus with its cell on the shared ``(frame, id)`` key and emits
5
+ relational quantities (nuclear:cell area ratio, centroid offset, orientation
6
+ delta, …). Needs **both** label stacks plus a pixel size. A **pooled** quantity:
7
+ :meth:`compute_object_table` builds the tidy table in memory (no per-position
8
+ artifact). The object-key column is ``cell_id``, so the relational table pools
9
+ through the same path as the per-source shape tables.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from itasc.contact_analysis.quantifier import Quantifier
14
+ from itasc.contact_analysis.shape import compute_relational_table
15
+
16
+
17
+ class ShapeRelationalQuantifier(Quantifier):
18
+ """Quantifies per-(frame, cell) relational nucleus-vs-cell shape."""
19
+
20
+ quantity_id = "shape_relational"
21
+ display_name = "Nucleus–cell shape"
22
+ # Both label stacks (to pair); pixel size (to emit physical units) is a
23
+ # global build param set in the Parameters panel.
24
+ requires = ("cell_labels_path", "nucleus_labels_path")
25
+ required_build_params = {"pixel_size_um": "pixel size (µm/px)"}
26
+ # Per-cell nucleus↔cell relational descriptors, keyed (frame, cell_id).
27
+ table_keys = ("frame", "cell_id")
28
+
29
+ def compute_object_table(self, inputs, *, params=None):
30
+ return compute_relational_table(
31
+ inputs.cell_labels_path,
32
+ inputs.nucleus_labels_path,
33
+ pixel_size_um=inputs.pixel_size_um,
34
+ )
@@ -0,0 +1,44 @@
1
+ """Signed-contact-length quantifier — signed T1 junction lengths for the potential.
2
+
3
+ A contacts-derived **pooled** quantity: :meth:`compute_object_table` returns the
4
+ signed central-junction length of each T1 event (negative on the losing side,
5
+ positive on the gaining side) in memory (no per-position artifact). Pooled and
6
+ Boltzmann-inverted downstream these reproduce the double-well potential. Lengths
7
+ are in µm when the position's pixel size resolves, else pixels. The
8
+ ``contact_type`` transition label is normalized here (blank → ``"unlabelled"``) so
9
+ a group axis never sees an empty string.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import numpy as np
14
+
15
+ from itasc.contact_analysis.contacts.signed_contact_length import (
16
+ signed_central_junction_lengths,
17
+ )
18
+ from itasc.contact_analysis.quantifier import Quantifier
19
+ from itasc.contact_analysis.quantifiers import _contacts_derived as derived
20
+
21
+
22
+ class SignedContactLengthQuantifier(Quantifier):
23
+ """Per-T1-event signed central junction length (the potential's samples)."""
24
+
25
+ quantity_id = "signed_contact_length"
26
+ display_name = "Signed contact length"
27
+ requires = ("contact_analysis_path",)
28
+ # One row per T1 event; ``contact_type`` / ``role`` are categorical axes that
29
+ # key the table alongside the event id (only ``signed_length`` is a value
30
+ # column).
31
+ table_keys = ("frame", "t1_event_id", "role", "contact_type")
32
+
33
+ def compute_object_table(self, inputs, *, params=None):
34
+ analysis = derived.load_analysis(inputs)
35
+ table = dict(
36
+ signed_central_junction_lengths(
37
+ analysis, pixel_size_um=inputs.pixel_size_um
38
+ )
39
+ )
40
+ if "contact_type" in table:
41
+ ct = np.asarray(table["contact_type"], dtype=object)
42
+ ct[ct == ""] = "unlabelled"
43
+ table["contact_type"] = ct
44
+ return table
@@ -0,0 +1,129 @@
1
+ """Qt-free bridge from catalogue *records* to a quantifier's per-position files.
2
+
3
+ The studio carries positions as normalized catalogue ``record`` dicts (see
4
+ :mod:`itasc.contact_analysis.catalog`). Turning one into the
5
+ :class:`~itasc.contact_analysis.quantifier.PositionInputs` a quantifier
6
+ builds from — and resolving where that quantifier's artifact lives — is pure path
7
+ logic with no Qt. It lives here (rather than in the napari studio layer) so the
8
+ headless aggregation backend (:mod:`.shape_tables`) and the standalone
9
+ ``itasc-aggregate`` wheel can resolve the same paths the pipeline does.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Mapping, Sequence
14
+ from dataclasses import fields as _dataclass_fields
15
+ from pathlib import Path
16
+
17
+ from .quantifier import PositionInputs, Quantifier, available_quantifiers
18
+
19
+ __all__ = [
20
+ "position_inputs_from_record",
21
+ "output_for_record",
22
+ "available_fields",
23
+ "record_build_params",
24
+ "supported_quantities",
25
+ ]
26
+
27
+
28
+ def position_inputs_from_record(record: dict) -> PositionInputs:
29
+ """Build :class:`PositionInputs` from a normalized catalogue record.
30
+
31
+ ``pixel_size_um`` / ``time_interval_s`` are **global** build params: they are
32
+ taken only from the value stamped on the record (the Parameters panel's
33
+ px/Δt), never auto-resolved from a position's config or label TIFF tags.
34
+ Absent ⇒ ``None``.
35
+ """
36
+ out = record.get("contact_analysis_path")
37
+ cell = record.get("cell_tracked_labels_path")
38
+ nucleus = record.get("nucleus_tracked_labels_path")
39
+ position_dir = record.get("position_path") or (Path(out).parent if out else Path("."))
40
+ cell_path = Path(cell) if cell else None
41
+ nucleus_path = Path(nucleus) if nucleus else None
42
+ return PositionInputs(
43
+ position_dir=Path(position_dir),
44
+ cell_labels_path=cell_path,
45
+ nucleus_labels_path=nucleus_path,
46
+ pixel_size_um=_positive_float(record.get("pixel_size_um")),
47
+ time_interval_s=_positive_float(record.get("time_interval_s")),
48
+ # The contacts artifact is a *produced* input — the contacts quantifier
49
+ # writes it. The catalogue stamps its expected path on every position
50
+ # whether or not it has been built yet, so gate on the file actually
51
+ # existing: an unbuilt contacts product is not an available input.
52
+ contact_analysis_path=Path(out) if out and Path(out).is_file() else None,
53
+ )
54
+
55
+
56
+ def _positive_float(value: object) -> float | None:
57
+ try:
58
+ result = float(value) # type: ignore[arg-type]
59
+ except (TypeError, ValueError):
60
+ return None
61
+ return result if result > 0 else None
62
+
63
+
64
+ def output_for_record(quantifier: Quantifier, record: dict) -> Path:
65
+ """Where *quantifier*'s artifact lives for a catalogue *record*.
66
+
67
+ Contacts keeps using the catalogue's explicit ``contact_analysis_path``
68
+ column — it predates the quantifier seam and may hold a custom *nested* path
69
+ that the per-position read/visualize paths also rely on, so building must
70
+ target the same file. Every other quantifier derives its destination from
71
+ :meth:`Quantifier.default_output`.
72
+ """
73
+ if quantifier.quantity_id == "contacts":
74
+ explicit = record.get("contact_analysis_path")
75
+ if explicit:
76
+ return Path(explicit)
77
+ return quantifier.default_output(position_inputs_from_record(record))
78
+
79
+
80
+ def available_fields(inputs: PositionInputs) -> set[str]:
81
+ """The populated (non-``None``) ``PositionInputs`` field names — the satisfied
82
+ prerequisites a quantifier's ``requires`` is checked against."""
83
+ return {f.name for f in _dataclass_fields(inputs) if getattr(inputs, f.name) is not None}
84
+
85
+
86
+ def record_build_params(
87
+ quantifier: Quantifier, record: dict, params: Mapping[str, object] | None
88
+ ) -> dict:
89
+ """Shared *params* overlaid with the record's own required-build-param values.
90
+
91
+ A param like pixel size can be set per-position on the record (the value the
92
+ build actually reads via ``PositionInputs``) instead of in the shared bar, so
93
+ the build-param gate must see both. The record's own value wins where present,
94
+ mirroring ``run()``'s per-record stamping.
95
+ """
96
+ merged = dict(params or {})
97
+ for key in quantifier.required_build_params:
98
+ value = record.get(key)
99
+ if value is not None:
100
+ merged[key] = value
101
+ return merged
102
+
103
+
104
+ def supported_quantities(
105
+ records: Sequence[dict], *, params: Mapping[str, object] | None = None
106
+ ) -> set[str]:
107
+ """``quantity_id``\\ s of the pooled quantifiers *records* can actually produce.
108
+
109
+ A pooled quantifier (one declaring ``table_keys``) is *supported* when at least
110
+ one record satisfies **both** gates :func:`shape_tables.build_table` applies per
111
+ position: its ``requires`` inputs are present (:meth:`Quantifier.can_build`) and
112
+ its ``required_build_params`` are satisfied (pixel size, FOV area, …). That makes
113
+ "supported" ⟺ "this table would pool at least one non-empty row" — the exact
114
+ predicate the Aggregate UI greys its checkboxes against, so an enabled box never
115
+ promises a table the run would silently skip. Producers (no ``table_keys``, e.g.
116
+ ``contacts``) are never pooled and so never reported.
117
+ """
118
+ supported: set[str] = set()
119
+ for quantifier in (cls() for cls in available_quantifiers()):
120
+ if not quantifier.table_keys:
121
+ continue
122
+ for record in records:
123
+ inputs = position_inputs_from_record(record)
124
+ if quantifier.can_build(inputs) and not quantifier.missing_build_params(
125
+ record_build_params(quantifier, record, params)
126
+ ):
127
+ supported.add(quantifier.quantity_id)
128
+ break
129
+ return supported
@@ -0,0 +1,19 @@
1
+ """Shape compute core: per-object morphology + relational nucleus-vs-cell shape.
2
+
3
+ The headless backend for the shape quantifiers. :func:`compute_object_shape` runs
4
+ :func:`skimage.measure.regionprops` over any tracked-label stack (cell *or*
5
+ nucleus) and returns a tidy column-major table; :func:`compute_relational_table`
6
+ pairs each nucleus with its cell and emits relational quantities. Both compute in
7
+ memory — the aggregate stage pools the tables, nothing is persisted per position.
8
+ No Qt / napari import, so scripts and the standalone wheel can use it.
9
+ """
10
+
11
+ from .core import DESCRIPTOR_COLUMNS, compute_object_shape
12
+ from .relational import RELATIONAL_COLUMNS, compute_relational_table
13
+
14
+ __all__ = [
15
+ "DESCRIPTOR_COLUMNS",
16
+ "RELATIONAL_COLUMNS",
17
+ "compute_object_shape",
18
+ "compute_relational_table",
19
+ ]