Skip to content

Commit b6e9e1c

Browse files
authored
Merge pull request #297 from GEECS-BELLA/feat/post-analysis-cleanup
Add post-analysis cleanup hook to free per-scan memory
2 parents 478d431 + 5021eca commit b6e9e1c

File tree

6 files changed

+59
-4
lines changed

6 files changed

+59
-4
lines changed

ScanAnalysis/scan_analysis/analyzers/common/scatter_plotter_analysis.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,3 +489,7 @@ def process_to_bins(self, key_name: str) -> dict[str, np.ndarray]:
489489
logging.info(f"Shots: {shots_in_bin[0]} - {shots_in_bin[-1]}")
490490

491491
return {"bin": bins, "average": average, "sigma": sigma, "median": median}
492+
493+
def cleanup(self) -> None:
494+
"""No large data held between scans; nothing to release."""
495+
pass

ScanAnalysis/scan_analysis/analyzers/common/single_device_scan_analyzer.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,30 @@ def bin_data_from_results(self) -> dict[int, BinDataEntry]:
815815

816816
return binned_data
817817

818+
def cleanup(self) -> None:
819+
"""
820+
Free memory held by loaded and analyzed data after analysis is complete.
821+
822+
Clears per-scan attributes that may hold large numpy arrays or result objects:
823+
824+
- ``raw_data`` — loaded shot images
825+
- ``results`` — per-shot/bin ImageAnalyzerResult objects
826+
- ``_data_file_map`` — shot-to-path mapping
827+
- ``stateful_results`` — batch-analysis state dict
828+
- ``_pending_aux_updates`` — queued s-file row updates
829+
830+
Also delegates to ``renderer.cleanup()``.
831+
"""
832+
self.raw_data = {}
833+
self.results = {}
834+
self._data_file_map = {}
835+
self.stateful_results = {}
836+
self._pending_aux_updates = []
837+
838+
self.renderer.cleanup()
839+
840+
logger.debug(f"[{self.__class__.__name__}] cleanup() complete.")
841+
818842
@staticmethod
819843
def average_data(data_list: list[np.ndarray]) -> Optional[np.ndarray]:
820844
"""Return the element-wise mean of a list of data arrays."""

ScanAnalysis/scan_analysis/analyzers/renderers/base_renderer.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Abstract base class for scan analysis renderers."""
22

3+
import logging
34
from abc import ABC, abstractmethod
45
from pathlib import Path
56
from typing import List
67
from .config import RenderContext, BaseRendererConfig
78

9+
logger = logging.getLogger(__name__)
10+
811

912
class BaseRenderer(ABC):
1013
"""
@@ -113,3 +116,14 @@ def render_animation(
113116
Path to the created animation file
114117
"""
115118
pass
119+
120+
def cleanup(self) -> None:
121+
"""
122+
Free memory held by the renderer after analysis is complete.
123+
124+
Resets ``display_contents`` so paths from a previous scan are not
125+
accidentally returned on the next run. Subclasses may override to
126+
release additional cached state (e.g. large arrays held across calls).
127+
"""
128+
self.display_contents = []
129+
logger.debug(f"[{self.__class__.__name__}] cleanup() complete.")

ScanAnalysis/scan_analysis/base.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ def run_analysis(self, scan_tag: ScanTag) -> Optional[list[Union[Path, str]]]:
138138
return None
139139
return self._run_analysis_core()
140140

141+
def cleanup(self) -> None:
142+
"""Release per-scan memory after analysis completes.
143+
144+
Must be implemented by all subclasses. Called by the task runner
145+
after each analyzer finishes so that loaded data and results do not
146+
accumulate across scans. Failing to implement this will raise
147+
NotImplementedError and halt the runner — intentionally, to prevent
148+
unbounded memory growth.
149+
"""
150+
raise NotImplementedError(f"{self.__class__.__name__} must implement cleanup()")
151+
141152
def _run_analysis_core(self) -> Optional[list[Union[Path, str]]]:
142153
"""Core analysis routine to be implemented by subclasses."""
143154
"""

ScanAnalysis/scan_analysis/task_queue.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@ def run_worklist(
433433
finally:
434434
stop_event.set()
435435
hb_thread.join(timeout=HEARTBEAT_INTERVAL_SECONDS)
436+
analyzer.cleanup()
436437

437438

438439
def load_analyzers_from_config(

docs/scan_analysis/examples/live_watch.ipynb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,13 @@
4141
"scan_analysis_config.set_base_dir(ScanPaths.paths_config.scan_analysis_configs_path)\n",
4242
"\n",
4343
"\n",
44-
"date_tag = ScanTag(year=2026, month=2, day=12, number=0, experiment=\"Thomson\")\n",
44+
"date_tag = ScanTag(year=2026, month=2, day=12, number=0, experiment=\"Undulator\")\n",
4545
"runner = LiveTaskRunner(\n",
46-
" analyzer_group=\"HTT\",\n",
46+
" analyzer_group=\"Undulator\",\n",
4747
" date_tag=date_tag,\n",
4848
" config_dir=scan_analysis_config.base_dir, # optional; else env/global\n",
4949
" image_config_dir=image_analysis_config.base_dir, # optional\n",
50+
" gdoc_enabled=True,\n",
5051
")"
5152
]
5253
},
@@ -83,7 +84,7 @@
8384
],
8485
"metadata": {
8586
"kernelspec": {
86-
"display_name": "Python 3 (ipykernel)",
87+
"display_name": "Python (Poetry)",
8788
"language": "python",
8889
"name": "python3"
8990
},
@@ -97,7 +98,7 @@
9798
"name": "python",
9899
"nbconvert_exporter": "python",
99100
"pygments_lexer": "ipython3",
100-
"version": "3.10.18"
101+
"version": "3.10.11"
101102
}
102103
},
103104
"nbformat": 4,

0 commit comments

Comments
 (0)