phenome 1.4.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.
Files changed (105) hide show
  1. phenome/__init__.py +180 -0
  2. phenome/_logging.py +99 -0
  3. phenome/core/__init__.py +107 -0
  4. phenome/core/_processing_pipeline_persist.py +112 -0
  5. phenome/core/batch_correction.py +384 -0
  6. phenome/core/dataframe_contract.py +175 -0
  7. phenome/core/math/__init__.py +38 -0
  8. phenome/core/math/combined_features.py +99 -0
  9. phenome/core/math/correlation.py +496 -0
  10. phenome/core/math/dimensionality_reduction.py +489 -0
  11. phenome/core/math/interpretability.py +361 -0
  12. phenome/core/pipeline_results.py +238 -0
  13. phenome/core/processing_pipeline.py +1679 -0
  14. phenome/core/property_utils.py +143 -0
  15. phenome/core/protocols.py +79 -0
  16. phenome/core/results_export.py +142 -0
  17. phenome/core/results_metadata.py +451 -0
  18. phenome/core/results_validation.py +49 -0
  19. phenome/core/run_log.py +227 -0
  20. phenome/io/__init__.py +50 -0
  21. phenome/io/_checkpoint_ops.py +523 -0
  22. phenome/io/_discovery_ops.py +106 -0
  23. phenome/io/_path_utils.py +42 -0
  24. phenome/io/batch_correction_ops.py +100 -0
  25. phenome/io/checkpoint.py +1878 -0
  26. phenome/io/checkpoint_alignment.py +502 -0
  27. phenome/io/discovery.py +629 -0
  28. phenome/mcp/__init__.py +5 -0
  29. phenome/mcp/docs_search.py +104 -0
  30. phenome/mcp/recipes.py +92 -0
  31. phenome/mcp/server.py +137 -0
  32. phenome/metadata/__init__.py +50 -0
  33. phenome/metadata/base.py +255 -0
  34. phenome/metadata/sources.py +247 -0
  35. phenome/mixins/__init__.py +79 -0
  36. phenome/mixins/_helpers.py +136 -0
  37. phenome/mixins/analysis.py +1885 -0
  38. phenome/mixins/batch_correction.py +357 -0
  39. phenome/mixins/dataset.py +219 -0
  40. phenome/mixins/distances.py +400 -0
  41. phenome/mixins/embedding_extractor.py +237 -0
  42. phenome/mixins/interactive/__init__.py +18 -0
  43. phenome/mixins/interactive/_html_components.py +333 -0
  44. phenome/mixins/interactive/_protocol.py +54 -0
  45. phenome/mixins/interactive/_utils.py +247 -0
  46. phenome/mixins/interactive/explorer.py +3500 -0
  47. phenome/mixins/properties/__init__.py +10 -0
  48. phenome/mixins/properties/_compute.py +1194 -0
  49. phenome/mixins/properties/_grouping_table.py +173 -0
  50. phenome/mixins/properties/_paths.py +270 -0
  51. phenome/mixins/properties/_report.py +157 -0
  52. phenome/mixins/properties/_workers.py +303 -0
  53. phenome/mixins/properties/grouping.py +516 -0
  54. phenome/mixins/properties/main.py +568 -0
  55. phenome/mixins/properties/matrix.py +383 -0
  56. phenome/mixins/visualization/__init__.py +10 -0
  57. phenome/mixins/visualization/_component_correlation_plots.py +254 -0
  58. phenome/mixins/visualization/_core.py +167 -0
  59. phenome/mixins/visualization/_distance_plots.py +278 -0
  60. phenome/mixins/visualization/_dr_plots.py +609 -0
  61. phenome/mixins/visualization/_group_enrichment_plots.py +271 -0
  62. phenome/mixins/visualization/_helpers.py +224 -0
  63. phenome/mixins/visualization/_image_display.py +568 -0
  64. phenome/mixins/visualization/_interpretability_plots.py +187 -0
  65. phenome/pipeline.py +2832 -0
  66. phenome/plotly_display.py +98 -0
  67. phenome/plugins/__init__.py +78 -0
  68. phenome/plugins/builtin_properties.py +174 -0
  69. phenome/plugins/registry.py +104 -0
  70. phenome/py.typed +0 -0
  71. phenome/report/__init__.py +63 -0
  72. phenome/report/_components.py +317 -0
  73. phenome/report/_section_helpers.py +127 -0
  74. phenome/report/context.py +53 -0
  75. phenome/report/generator.py +544 -0
  76. phenome/report/helpers.py +223 -0
  77. phenome/report/scripts.py +332 -0
  78. phenome/report/sections/__init__.py +38 -0
  79. phenome/report/sections/clustering.py +449 -0
  80. phenome/report/sections/correlation.py +75 -0
  81. phenome/report/sections/distance.py +159 -0
  82. phenome/report/sections/export.py +96 -0
  83. phenome/report/sections/gallery.py +232 -0
  84. phenome/report/sections/interpretability.py +125 -0
  85. phenome/report/sections/outlier.py +164 -0
  86. phenome/report/sections/overview.py +119 -0
  87. phenome/report/sections/property_stats.py +160 -0
  88. phenome/report/sections/run_settings.py +80 -0
  89. phenome/report/sections/visualization.py +121 -0
  90. phenome/report/styles.py +928 -0
  91. phenome/utils/__init__.py +119 -0
  92. phenome/utils/device.py +63 -0
  93. phenome/utils/display_names.py +65 -0
  94. phenome/utils/metadata.py +206 -0
  95. phenome/utils/model_wrapper.py +211 -0
  96. phenome/utils/path_utils.py +170 -0
  97. phenome/utils/progress.py +35 -0
  98. phenome/utils/property_factories.py +671 -0
  99. phenome/utils/transforms.py +319 -0
  100. phenome-1.4.1.dist-info/METADATA +165 -0
  101. phenome-1.4.1.dist-info/RECORD +105 -0
  102. phenome-1.4.1.dist-info/WHEEL +5 -0
  103. phenome-1.4.1.dist-info/entry_points.txt +2 -0
  104. phenome-1.4.1.dist-info/licenses/LICENSE +21 -0
  105. phenome-1.4.1.dist-info/top_level.txt +1 -0
phenome/__init__.py ADDED
@@ -0,0 +1,180 @@
1
+ """
2
+ @section Core Pipeline
3
+
4
+ PhenoMe Package
5
+
6
+ A modular, **dataset-agnostic** and **model-agnostic** pipeline for phenotyping
7
+ analysis using deep learning embeddings.
8
+
9
+ **Overview:**
10
+ PhenoMe orchestrates the full phenotyping workflow: discover images, extract embeddings
11
+ with custom or pretrained models, compute cell/object properties, perform statistical
12
+ analysis, and generate interactive visualizations and reports.
13
+
14
+ **Public API:**
15
+
16
+ Core orchestration:
17
+ - `PhenoMe`: Main pipeline class; find images, process embeddings, compute properties, analyze.
18
+ - `PhenoMeResults`: Container for embeddings, properties, metadata, and analysis results.
19
+
20
+ Metadata helpers:
21
+ - `DefaultMetadata`: Simple key-value metadata.
22
+ - `PathTemplateMetadata`: Extract metadata from file paths using templates.
23
+ - `DataFrameMetadata`: Use a DataFrame as metadata source.
24
+ - `default_metadata_from_path`, `get_metadata_from_path`, `make_dataframe_metadata_fn`:
25
+ Metadata factory functions.
26
+
27
+ Model loading:
28
+ - `load_dinov2_model`: Load pretrained DINOv2 model.
29
+
30
+ Property factories (create custom cell/object properties):
31
+ - `create_regionprops_function`, `create_intensity_function`,
32
+ `create_masked_intensity_function`, `create_texture_function`,
33
+ `create_concentric_ring_function`, `create_blur_effect_function`,
34
+ `create_entropy_function`: Built-in property creators.
35
+ - `get_preset_property_functions`: List all available preset properties.
36
+
37
+ **Data flow:**
38
+
39
+ ```
40
+ find_files()
41
+ ↓ returns DataFrame of image paths
42
+ set_file_df() or directly via DataFrame
43
+ ↓ store file locations
44
+ process_images(model)
45
+ ↓ extract embeddings, populate results.embeddings
46
+ compute_properties(property_functions)
47
+ ↓ compute morphological/biological features
48
+ compute_clustering() / detect_outliers() / ...
49
+ ↓ statistical analysis
50
+ plot_pca() / plot_tsne() / plot_umap() / ...
51
+ ↓ visualization
52
+ generate_report()
53
+ ↓ HTML report
54
+ ```
55
+
56
+ **Model compatibility:**
57
+ - **DINOv2:** Use `load_dinov2_model()` or wrap with `DinoV2ModelWrapper`.
58
+ - **Custom:** Subclass `ModelWrapper`, implement `_get_embeddings(images) -> (B, D)`.
59
+
60
+ **For advanced usage, import from submodules:**
61
+ - `phenome.core`: Math utilities (DR, correlation, validation, export).
62
+ - `phenome.io`: File I/O (`read_image`, `FileDiscovery`, `CheckpointManager`).
63
+ - `phenome.mixins`: Composition classes (dataset, properties, analysis, visualization).
64
+ - `phenome.plugins`: Property registry and custom property registration.
65
+ - `phenome.report`: HTML report generation.
66
+ - `phenome.utils`: Device management, transforms, metadata and property factories.
67
+
68
+ **Logging:**
69
+
70
+ Progress messages are visible at INFO level by default:
71
+
72
+ ```python
73
+ import logging
74
+ logging.getLogger("phenome").setLevel(logging.WARNING) # Suppress
75
+ ```
76
+
77
+ **Example:**
78
+
79
+ Basic phenotyping workflow:
80
+
81
+ ```python
82
+ from phenome import PhenoMe, load_dinov2_model
83
+
84
+ # Initialize pipeline
85
+ pm = PhenoMe(device="cuda", seed=42)
86
+
87
+ # Find images
88
+ pm.find_files("path/to/images/")
89
+
90
+ # Extract embeddings
91
+ model = load_dinov2_model(model_name="dinov2_vitb14")
92
+ pm.process_images(model, batch_size=32)
93
+
94
+ # Compute properties
95
+ pm.compute_properties(property_preset="basic")
96
+
97
+ # Analyze
98
+ pm.compute_clustering(method="kmeans", n_clusters=5)
99
+ pm.compute_pca()
100
+
101
+ # Visualize
102
+ pm.plot_pca(color_by="clustering")
103
+ ```
104
+
105
+ **See Also:**
106
+ For architecture, design patterns, and extensibility, see the [Architecture guide](https://AAitorG.github.io/PhenoMe/guides/architecture/).
107
+ """
108
+
109
+ # Initialise notebook-safe logging on first import
110
+ from importlib.metadata import PackageNotFoundError, version
111
+
112
+ from ._logging import get_logger as _get_logger
113
+ from .core import PhenoMeResults
114
+ from .metadata import (
115
+ DataFrameMetadata,
116
+ DefaultMetadata,
117
+ MetadataBase,
118
+ PathTemplateMetadata,
119
+ )
120
+ from .pipeline import PhenoMe
121
+ from .plotly_display import register_plotly_display_defaults
122
+
123
+ # Metadata functions and classes
124
+ from .utils.metadata import (
125
+ default_metadata_from_path,
126
+ get_metadata_from_path,
127
+ make_dataframe_metadata_fn,
128
+ )
129
+
130
+ # Core workflow helpers
131
+ from .utils.model_wrapper import load_dinov2_model
132
+
133
+ # Property factories
134
+ from .utils.property_factories import (
135
+ create_blur_effect_function,
136
+ create_concentric_ring_function,
137
+ create_entropy_function,
138
+ create_intensity_function,
139
+ create_masked_intensity_function,
140
+ create_regionprops_function,
141
+ create_texture_function,
142
+ get_preset_property_functions,
143
+ )
144
+
145
+ try:
146
+ __version__ = version("phenome")
147
+ except PackageNotFoundError:
148
+ __version__ = "1.4.1"
149
+
150
+ __all__ = [
151
+ "DataFrameMetadata",
152
+ "DefaultMetadata",
153
+ "MetadataBase",
154
+ "PathTemplateMetadata",
155
+ "PhenoMe",
156
+ "PhenoMeResults",
157
+ "__version__",
158
+ # Property factories
159
+ "create_blur_effect_function",
160
+ "create_concentric_ring_function",
161
+ "create_entropy_function",
162
+ "create_intensity_function",
163
+ "create_masked_intensity_function",
164
+ "create_regionprops_function",
165
+ "create_texture_function",
166
+ # Metadata functions
167
+ "default_metadata_from_path",
168
+ "get_metadata_from_path",
169
+ # Property factories
170
+ "get_preset_property_functions",
171
+ # Default models
172
+ "load_dinov2_model",
173
+ "make_dataframe_metadata_fn",
174
+ ]
175
+
176
+ _get_logger(__name__)
177
+ __author__ = "Aitor González-Marfil"
178
+ __author_github__ = "AAitorG"
179
+
180
+ register_plotly_display_defaults()
phenome/_logging.py ADDED
@@ -0,0 +1,99 @@
1
+ """
2
+ Notebook-safe logging configuration for phenome.
3
+
4
+ Problem
5
+ -------
6
+ The standard ``logging.getLogger(__name__)`` pattern silently drops INFO
7
+ messages in Jupyter notebooks because Python's root logger defaults to
8
+ WARNING level and has no handler attached. If the user manually adds a
9
+ handler, duplicate output appears on every cell re-run.
10
+
11
+ Solution
12
+ --------
13
+ We configure a single package-level logger (``phenome``) with:
14
+
15
+ * A ``StreamHandler`` writing to ``sys.stderr`` (rendered by Jupyter as
16
+ plain text below the cell).
17
+ * INFO as default level, so progress messages are always visible.
18
+ * A guard that prevents adding duplicate handlers on module reload
19
+ (``%autoreload`` / repeated imports).
20
+
21
+ All sub-modules should call::
22
+
23
+ from ._logging import get_logger
24
+ logger = get_logger(__name__)
25
+
26
+ instead of ``logging.getLogger(__name__)``.
27
+
28
+ Users who want to silence the output can do::
29
+
30
+ import logging
31
+ logging.getLogger("phenome").setLevel(logging.WARNING)
32
+ """
33
+
34
+ import logging
35
+ import sys
36
+
37
+ # Package-level logger name
38
+ _PKG_LOGGER_NAME = "phenome"
39
+
40
+ # Sentinel attribute on the package logger so we configure at most once.
41
+ _CONFIGURED_ATTR = "_phenome_configured"
42
+
43
+
44
+ def get_logger(name: str) -> logging.Logger:
45
+ """Return a logger configured for notebook-safe output.
46
+
47
+ Typical usage at the top of each module::
48
+
49
+ from ._logging import get_logger
50
+ logger = get_logger(__name__)
51
+
52
+ Parameters
53
+ ----------
54
+ name : str
55
+ Usually ``__name__`` of the calling module.
56
+ """
57
+ _ensure_configured()
58
+ return logging.getLogger(name)
59
+
60
+
61
+ def _ensure_configured() -> None:
62
+ """Attach a StreamHandler to the package logger if not done already."""
63
+ pkg_logger = logging.getLogger(_PKG_LOGGER_NAME)
64
+
65
+ if getattr(pkg_logger, _CONFIGURED_ATTR, False):
66
+ return
67
+
68
+ # Library best-practice: add NullHandler so that if the user never
69
+ # configures logging at all, nothing breaks.
70
+ pkg_logger.addHandler(logging.NullHandler())
71
+
72
+ # Only add our StreamHandler if the package logger (or its parents)
73
+ # have no *effective* handlers yet. This avoids fighting with a
74
+ # user who already configured logging before importing us.
75
+ if not _has_effective_handler(pkg_logger):
76
+ handler = logging.StreamHandler(sys.stderr)
77
+ handler.setFormatter(logging.Formatter("%(message)s"))
78
+ pkg_logger.addHandler(handler)
79
+
80
+ # Set INFO as the default level for the package tree.
81
+ # Only override if the logger is still at NOTSET (i.e. the user hasn't
82
+ # explicitly set a level).
83
+ if pkg_logger.level == logging.NOTSET:
84
+ pkg_logger.setLevel(logging.INFO)
85
+
86
+ setattr(pkg_logger, _CONFIGURED_ATTR, True)
87
+
88
+
89
+ def _has_effective_handler(logger: logging.Logger) -> bool:
90
+ """Check if *logger* or any ancestor already has a non-NullHandler."""
91
+ current: logging.Logger | None = logger
92
+ while current:
93
+ for h in current.handlers:
94
+ if not isinstance(h, logging.NullHandler):
95
+ return True
96
+ if not current.propagate:
97
+ break
98
+ current = current.parent
99
+ return False
@@ -0,0 +1,107 @@
1
+ """
2
+ @section Core Pipeline
3
+
4
+ Core utilities for math, validation, export, and results management.
5
+
6
+ This package provides the mathematical algorithms and data structures that underpin
7
+ the phenotyping pipeline. Key components:
8
+
9
+ **Results container:**
10
+ - `PhenoMeResults`: Typed container for embeddings, properties, metadata, and analysis results.
11
+
12
+ **Mathematical functions:**
13
+ - `run_dimensionality_reduction`: PCA, t-SNE, UMAP.
14
+ - `compute_pearson_correlation`, `compute_spearman_correlation`: Feature correlations.
15
+ - `compute_distance_correlation`: Distance correlation between features.
16
+ - `compute_mutual_info`: Mutual information between features.
17
+ - `compute_entropy`: Entropy of features.
18
+ - `build_combined_features`: Combine multiple features into single representation.
19
+
20
+ **Validation and export:**
21
+ - `validate_results`: Check results integrity (embeddings, properties, shapes).
22
+ - `build_export_dataframe`: Format results for export to CSV/Excel.
23
+ - `prepare_embedding_dataframe`: Prepare embeddings with metadata for analysis.
24
+
25
+ **Metadata handling:**
26
+ - `build_metadata_columns`: Extract and format metadata columns.
27
+ - `filter_indices`: Select rows by metadata criteria.
28
+ - `get_metadata_value`: Retrieve metadata value for a row.
29
+ - `get_all_metadata_keys`: List all available metadata keys.
30
+ - `metadata_to_stable_key`: Convert metadata to file-safe string.
31
+
32
+ **Protocols:**
33
+ - `PhenoMeProtocol`: Protocol (duck-type interface) for objects implementing the phenotyping API.
34
+
35
+ **See Also:**
36
+ For top-level analysis pipeline, see `phenome.PhenoMe`.
37
+ """
38
+
39
+ from .dataframe_contract import (
40
+ IMAGE_INDEX,
41
+ IMAGE_NAME,
42
+ IMAGE_PATH,
43
+ TIER_A_EXCLUDE_FROM_PROPERTIES,
44
+ append_property_columns,
45
+ is_per_image_df,
46
+ per_image_dataframe,
47
+ reorder_tier_a_columns,
48
+ )
49
+ from .math import (
50
+ build_combined_features,
51
+ clean_correlation_inputs,
52
+ compute_distance_correlation,
53
+ compute_entropy,
54
+ compute_lasso_interpretability,
55
+ compute_mutual_info,
56
+ compute_pearson_correlation,
57
+ compute_rf_interpretability,
58
+ compute_spearman_correlation,
59
+ run_dimensionality_reduction,
60
+ run_dimensionality_reduction_matrix,
61
+ )
62
+ from .pipeline_results import PhenoMeResults
63
+ from .property_utils import metadata_to_stable_key, optimize_property_types
64
+ from .protocols import PhenoMeProtocol
65
+ from .results_export import build_export_dataframe, prepare_embedding_dataframe
66
+ from .results_metadata import (
67
+ build_metadata_columns,
68
+ filter_indices,
69
+ get_all_metadata_keys,
70
+ get_metadata_value,
71
+ get_metadata_value_from_dict,
72
+ )
73
+ from .results_validation import validate_results
74
+
75
+ __all__ = [
76
+ "IMAGE_INDEX",
77
+ "IMAGE_NAME",
78
+ "IMAGE_PATH",
79
+ "TIER_A_EXCLUDE_FROM_PROPERTIES",
80
+ "PhenoMeProtocol",
81
+ "PhenoMeResults",
82
+ "append_property_columns",
83
+ "build_combined_features",
84
+ "build_export_dataframe",
85
+ "build_metadata_columns",
86
+ "clean_correlation_inputs",
87
+ "compute_distance_correlation",
88
+ "compute_entropy",
89
+ "compute_lasso_interpretability",
90
+ "compute_mutual_info",
91
+ "compute_pearson_correlation",
92
+ "compute_rf_interpretability",
93
+ "compute_spearman_correlation",
94
+ "filter_indices",
95
+ "get_all_metadata_keys",
96
+ "get_metadata_value",
97
+ "get_metadata_value_from_dict",
98
+ "is_per_image_df",
99
+ "metadata_to_stable_key",
100
+ "optimize_property_types",
101
+ "per_image_dataframe",
102
+ "prepare_embedding_dataframe",
103
+ "reorder_tier_a_columns",
104
+ "run_dimensionality_reduction",
105
+ "run_dimensionality_reduction_matrix",
106
+ "validate_results",
107
+ ]
@@ -0,0 +1,112 @@
1
+ """Slim title/detail step payloads for checkpoint persistence.
2
+
3
+ Kept separate from ``processing_pipeline`` so HDF5 I/O can slim steps without
4
+ importing image preflight (which would cycle through ``phenome.io``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping
10
+ from typing import Any
11
+
12
+ __all__ = ["coalesce_pipeline_payload", "slim_pipeline_payload"]
13
+
14
+
15
+ def _printed_step_kind(step: Mapping[str, Any]) -> str | None:
16
+ """Return a persisted pipeline kind, or None when the step has none."""
17
+ kind = str(step.get("kind") or "").strip().lower()
18
+ if kind:
19
+ return kind
20
+ if str(step.get("id") or "").startswith("mask_"):
21
+ return "mask"
22
+ return None
23
+
24
+
25
+ def _is_mask_step(step: Mapping[str, Any]) -> bool:
26
+ """Return True when *step* is a mask-only pipeline step."""
27
+ return _printed_step_kind(step) == "mask"
28
+
29
+
30
+ def _printed_step_fields(step: Any) -> dict[str, str] | None:
31
+ """Keep title, detail, and kind for printing or persistence."""
32
+ if not isinstance(step, Mapping):
33
+ return None
34
+ title = str(step.get("title") or "").strip()
35
+ detail = str(step.get("detail") or "").strip()
36
+ if not title and not detail:
37
+ return None
38
+ out: dict[str, str] = {}
39
+ if title:
40
+ out["title"] = title
41
+ if detail:
42
+ out["detail"] = detail
43
+ kind = _printed_step_kind(step)
44
+ if kind:
45
+ out["kind"] = kind
46
+ return out
47
+
48
+
49
+ def _steps_from_value(value: Any) -> list[dict[str, str]] | None:
50
+ """Extract ordered title/detail steps from a slim list or a full segment."""
51
+ raw: Any = value
52
+ if isinstance(value, Mapping) and "steps" in value:
53
+ raw = value.get("steps")
54
+ if not isinstance(raw, list):
55
+ return None
56
+ kept = [step for item in raw if (step := _printed_step_fields(item))]
57
+ return kept or None
58
+
59
+
60
+ def slim_pipeline_payload(payload: Mapping[str, Any] | None) -> dict[str, Any] | None:
61
+ """Return only embeddings/properties step lists suitable for HDF5.
62
+
63
+ Accepts a session dict, a slim ``{embeddings, properties}`` payload, a
64
+ full segment with ``steps``, or a bare step list.
65
+
66
+ Args:
67
+ payload: Session pipeline, slim checkpoint payload, or None.
68
+
69
+ Returns:
70
+ ``{embeddings, properties}`` lists of printed step fields, or None.
71
+ """
72
+ if not payload:
73
+ return None
74
+ embeddings = _steps_from_value(payload.get("embeddings"))
75
+ if embeddings is None:
76
+ embeddings = _steps_from_value(payload.get("base"))
77
+ properties = _steps_from_value(payload.get("properties"))
78
+ if embeddings is None and properties is None:
79
+ embeddings = _steps_from_value(payload)
80
+ out: dict[str, Any] = {}
81
+ if embeddings:
82
+ out["embeddings"] = embeddings
83
+ if properties:
84
+ out["properties"] = properties
85
+ return out or None
86
+
87
+
88
+ def coalesce_pipeline_payload(
89
+ incoming: Mapping[str, Any] | None,
90
+ existing: Mapping[str, Any] | None,
91
+ ) -> dict[str, Any] | None:
92
+ """Fill missing embeddings/properties steps from *existing*.
93
+
94
+ Used when a write has only one segment (for example ``process_images``
95
+ after ``reset()``) so the other stored segment is not deleted.
96
+
97
+ Args:
98
+ incoming: Session pipeline or slim payload being written.
99
+ existing: Payload already stored on the checkpoint, or None.
100
+
101
+ Returns:
102
+ Merged slim payload, or None when both sides are empty.
103
+ """
104
+ slim_in = slim_pipeline_payload(incoming)
105
+ slim_ex = slim_pipeline_payload(existing)
106
+ if slim_in is None:
107
+ return slim_ex
108
+ if slim_ex is None:
109
+ return slim_in
110
+ merged = dict(slim_ex)
111
+ merged.update(slim_in)
112
+ return merged