diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI.png b/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI.png new file mode 100644 index 0000000..e8aaba2 Binary files /dev/null and b/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI.png differ diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_logo.png b/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_logo.png deleted file mode 100644 index 965195c..0000000 Binary files a/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_logo.png and /dev/null differ diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flOMNI.png b/csaxs_bec/bec_ipython_client/plugins/flomni/flOMNI.png new file mode 100644 index 0000000..b6972b0 Binary files /dev/null and b/csaxs_bec/bec_ipython_client/plugins/flomni/flOMNI.png differ diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py index f36a346..f2da35d 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -969,8 +969,6 @@ class FlomniSampleTransferMixin: class FlomniAlignmentMixin: import csaxs_bec - import os - from pathlib import Path # Ensure this is a Path object, not a string csaxs_bec_basepath = Path(csaxs_bec.__file__) @@ -1211,6 +1209,76 @@ class FlomniAlignmentMixin: return additional_correction_shift +class _ProgressProxy: + """Dict-like proxy that persists the flOMNI progress dict as a BEC global variable. + + Every read (`proxy["key"]`) fetches the current dict from the global var store, + and every write (`proxy["key"] = val`) fetches, updates, and saves it back. + This makes the progress state visible to all BEC client sessions via + ``client.get_global_var("tomo_progress")``. + """ + + _GLOBAL_VAR_KEY = "tomo_progress" + _DEFAULTS: dict = { + "subtomo": 0, + "subtomo_projection": 0, + "subtomo_total_projections": 1, + "projection": 0, + "total_projections": 1, + "angle": 0, + "tomo_type": 0, + "tomo_start_time": None, + "estimated_remaining_time": None, + "heartbeat": None, + } + + def __init__(self, client): + self._client = client + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + def _load(self) -> dict: + val = self._client.get_global_var(self._GLOBAL_VAR_KEY) + if val is None: + return dict(self._DEFAULTS) + return val + + def _save(self, data: dict) -> None: + self._client.set_global_var(self._GLOBAL_VAR_KEY, data) + + # ------------------------------------------------------------------ + # Dict-like interface + # ------------------------------------------------------------------ + def __getitem__(self, key): + return self._load()[key] + + def __setitem__(self, key, value) -> None: + data = self._load() + data[key] = value + self._save(data) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self._load()!r})" + + def get(self, key, default=None): + return self._load().get(key, default) + + def update(self, *args, **kwargs) -> None: + """Update multiple fields in a single round-trip.""" + data = self._load() + data.update(*args, **kwargs) + self._save(data) + + def reset(self) -> None: + """Reset all progress fields to their default values.""" + self._save(dict(self._DEFAULTS)) + + def as_dict(self) -> dict: + """Return a plain copy of the current progress state.""" + return self._load() + + class Flomni( FlomniInitStagesMixin, FlomniSampleTransferMixin, @@ -1233,14 +1301,8 @@ class Flomni( self.corr_angle_y = [] self.corr_pos_y_2 = [] self.corr_angle_y_2 = [] - self.progress = {} - self.progress["subtomo"] = 0 - self.progress["subtomo_projection"] = 0 - self.progress["subtomo_total_projections"] = 1 - self.progress["projection"] = 0 - self.progress["total_projections"] = 1 - self.progress["angle"] = 0 - self.progress["tomo_type"] = 0 + self._progress_proxy = _ProgressProxy(self.client) + self._progress_proxy.reset() self.OMNYTools = OMNYTools(self.client) self.reconstructor = PtychoReconstructor(self.ptycho_reconstruct_foldername) self.tomo_id_manager = TomoIDManager() @@ -1296,6 +1358,42 @@ class Flomni( self.special_angles = [] self.special_angle_repeats = 1 + @property + def progress(self) -> _ProgressProxy: + """Proxy dict backed by the BEC global variable ``tomo_progress``. + + Readable from any BEC client session via:: + + client.get_global_var("tomo_progress") + + Individual fields can be read and written just like a regular dict:: + + flomni.progress["projection"] # read + flomni.progress["projection"] = 42 # write (persists immediately) + + To update multiple fields atomically use :py:meth:`_ProgressProxy.update`:: + + flomni.progress.update(projection=42, angle=90.0) + + To reset all fields to their defaults:: + + flomni.progress.reset() + """ + return self._progress_proxy + + @progress.setter + def progress(self, val: dict) -> None: + """Replace the entire progress dict. + + Accepts a plain :class:`dict` and persists it to the global var store. + Example:: + + flomni.progress = {"projection": 0, "total_projections": 100, ...} + """ + if not isinstance(val, dict): + raise TypeError(f"progress must be a dict, got {type(val).__name__!r}") + self._progress_proxy._save(val) + @property def tomo_shellstep(self): val = self.client.get_global_var("tomo_shellstep") @@ -1482,21 +1580,11 @@ class Flomni( def sample_name(self): return self.sample_get_name(0) - def write_to_scilog(self, content, tags: list = None): - try: - if tags is not None: - tags.append("BEC") - else: - tags = ["BEC"] - msg = bec.logbook.LogbookMessage() - msg.add_text(content).add_tag(tags) - self.client.logbook.send_logbook_message(msg) - except Exception: - logger.warning("Failed to write to scilog.") - def tomo_alignment_scan(self): """ Performs a tomogram alignment scan. + Collects all scan numbers acquired during the alignment, prints them at the end, + and creates a BEC scilog text entry summarising the alignment scan numbers. """ if self.get_alignment_offset(0) == (0, 0, 0): print("It appears that the xrayeye alignemtn was not performend or loaded. Aborting.") @@ -1506,11 +1594,9 @@ class Flomni( self.feye_out() tags = ["BEC_alignment_tomo", self.sample_name] - self.write_to_scilog( - f"Starting alignment scan. First scan number: {bec.queue.next_scan_number}.", tags - ) start_angle = 0 + alignment_scan_numbers = [] angle_end = start_angle + 180 for angle in np.linspace(start_angle, angle_end, num=int(180 / 45) + 1, endpoint=True): @@ -1522,7 +1608,6 @@ class Flomni( try: start_scan_number = bec.queue.next_scan_number self.tomo_scan_projection(angle) - self.tomo_reconstruct() error_caught = False except AlarmBase as exc: if exc.alarm_type == "TimeoutError": @@ -1536,24 +1621,27 @@ class Flomni( end_scan_number = bec.queue.next_scan_number for scan_nr in range(start_scan_number, end_scan_number): - self._write_tomo_scan_number(scan_nr, angle, 0) + #self._write_tomo_scan_number(scan_nr, angle, 0) + alignment_scan_numbers.append(scan_nr) umv(dev.fsamroy, 0) self.OMNYTools.printgreenbold( "\n\nAlignment scan finished. Please run SPEC_ptycho_align and load the new fit by flomni.read_alignment_offset() ." ) - def _write_subtomo_to_scilog(self, subtomo_number): - dev = builtins.__dict__.get("dev") - bec = builtins.__dict__.get("bec") - if self.tomo_id > 0: - tags = ["BEC_subtomo", self.sample_name, f"tomo_id_{self.tomo_id}"] - else: - tags = ["BEC_subtomo", self.sample_name] - self.write_to_scilog( - f"Starting subtomo: {subtomo_number}. First scan number: {bec.queue.next_scan_number}.", - tags, + # summary of alignment scan numbers + scan_list_str = ", ".join(str(s) for s in alignment_scan_numbers) + #print(f"\nAlignment scan numbers ({len(alignment_scan_numbers)} total): {scan_list_str}") + + # BEC scilog entry (no logo) + scilog_content = ( + f"Alignment scan finished.\n" + f"Sample: {self.sample_name}\n" + f"Number of alignment scans: {len(alignment_scan_numbers)}\n" + f"Alignment scan numbers: {scan_list_str}\n" ) + print(scliog_content) + bec.messaging.scilog.new().add_text(scilog_content.replace("\n", "
")).add_tags("alignmentscan").send() def sub_tomo_scan(self, subtomo_number, start_angle=None): """ @@ -1562,18 +1650,6 @@ class Flomni( subtomo_number (int): The sub tomogram number. start_angle (float, optional): The start angle of the scan. Defaults to None. """ - # dev = builtins.__dict__.get("dev") - # bec = builtins.__dict__.get("bec") - # if self.tomo_id > 0: - # tags = ["BEC_subtomo", self.sample_name, f"tomo_id_{self.tomo_id}"] - # else: - # tags = ["BEC_subtomo", self.sample_name] - # self.write_to_scilog( - # f"Starting subtomo: {subtomo_number}. First scan number: {bec.queue.next_scan_number}.", - # tags, - # ) - - self._write_subtomo_to_scilog(subtomo_number) if start_angle is not None: print(f"Sub tomo scan with start angle {start_angle} requested.") @@ -1673,6 +1749,7 @@ class Flomni( successful = False error_caught = False if 0 <= angle < 180.05: + self.progress["heartbeat"] = datetime.datetime.now().isoformat() print(f"Starting flOMNI scan for angle {angle} in subtomo {subtomo_number}") self._print_progress() while not successful: @@ -1746,6 +1823,8 @@ class Flomni( # self.write_pdf_report() # else: self.tomo_id = 0 + self.write_pdf_report() + self.progress["tomo_start_time"] = datetime.datetime.now().isoformat() with scans.dataset_id_on_hold: if self.tomo_type == 1: @@ -1765,7 +1844,6 @@ class Flomni( while True: angle, subtomo_number = self._golden(ii, self.golden_ratio_bunch_size, 180, 1) if previous_subtomo_number != subtomo_number: - self._write_subtomo_to_scilog(subtomo_number) if ( subtomo_number % 2 == 1 and ii > 10 @@ -1813,7 +1891,6 @@ class Flomni( ii, int(180 / self.tomo_angle_stepsize), 180, 1, 0 ) if previous_subtomo_number != subtomo_number: - self._write_subtomo_to_scilog(subtomo_number) if ( subtomo_number % 2 == 1 and ii > 10 @@ -1855,14 +1932,42 @@ class Flomni( self._print_progress() self.OMNYTools.printgreenbold("Tomoscan finished") + @staticmethod + def _format_duration(seconds: float) -> str: + """Format a duration in seconds as a human-readable string, e.g. '2h 03m 15s'.""" + seconds = int(seconds) + h, remainder = divmod(seconds, 3600) + m, s = divmod(remainder, 60) + if h > 0: + return f"{h}h {m:02d}m {s:02d}s" + if m > 0: + return f"{m}m {s:02d}s" + return f"{s}s" + def _print_progress(self): + # --- compute and store estimated remaining time ----------------------- + start_str = self.progress.get("tomo_start_time") + projection = self.progress["projection"] + total = self.progress["total_projections"] + if start_str is not None and total > 0 and projection > 9: + elapsed = ( + datetime.datetime.now() - datetime.datetime.fromisoformat(start_str) + ).total_seconds() + rate = projection / elapsed # projections per second + remaining_s = (total - projection) / rate + self.progress["estimated_remaining_time"] = remaining_s + eta_str = self._format_duration(remaining_s) + else: + eta_str = "N/A" + # ---------------------------------------------------------------------- print("\x1b[95mProgress report:") print(f"Tomo type: ....................... {self.progress['tomo_type']}") print(f"Projection: ...................... {self.progress['projection']:.0f}") print(f"Total projections expected ....... {self.progress['total_projections']}") print(f"Angle: ........................... {self.progress['angle']}") print(f"Current subtomo: ................. {self.progress['subtomo']}") - print(f"Current projection within subtomo: {self.progress['subtomo_projection']}\x1b[0m") + print(f"Current projection within subtomo: {self.progress['subtomo_projection']}") + print(f"Estimated remaining time: ........ {eta_str}\x1b[0m") self._flomnigui_update_progress() def add_sample_database( @@ -1886,7 +1991,6 @@ class Flomni( return self.tomo_scan_projection(angle) - self.tomo_reconstruct() def _golden(self, ii, howmany_sorted, maxangle, reverse=False): """returns the iis golden ratio angle of sorted bunches of howmany_sorted and its subtomo number""" @@ -1991,7 +2095,7 @@ class Flomni( f"{str(datetime.datetime.now())}: flomni scan projection at angle {angle}, scan" f" number {bec.queue.next_scan_number}.\n" ) - # self.write_to_scilog(log_message, ["BEC_scans", self.sample_name]) + scans.flomni_fermat_scan( fovx=self.fovx, fovy=self.fovy, @@ -2004,6 +2108,9 @@ class Flomni( corridor_size=corridor_size, ) + self.tomo_reconstruct() + + def tomo_parameters(self): """print and update the tomo parameters""" print("Current settings:") @@ -2145,38 +2252,58 @@ class Flomni( fovxy = f"{self.fovx:.2f}/{self.fovy:.2f}" stitching = f"{self.stitch_x:.2f}/{self.stitch_y:.2f}" dataset_id = str(self.client.queue.next_dataset_number) + account = bec.active_account content = [ f"{'Sample Name:':<{padding}}{self.sample_name:>{padding}}\n", f"{'Measurement ID:':<{padding}}{str(self.tomo_id):>{padding}}\n", f"{'Dataset ID:':<{padding}}{dataset_id:>{padding}}\n", f"{'Sample Info:':<{padding}}{'Sample Info':>{padding}}\n", - f"{'e-account:':<{padding}}{str(self.client.username):>{padding}}\n", + f"{'e-account:':<{padding}}{str(account):>{padding}}\n", f"{'Number of projections:':<{padding}}{int(180 / self.tomo_angle_stepsize * 8):>{padding}}\n", f"{'First scan number:':<{padding}}{self.client.queue.next_scan_number:>{padding}}\n", f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(180 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n", - f"{'Current photon energy:':<{padding}}{dev.mokev.read()['mokev']['value']:>{padding}.4f}\n", + f"{'Current photon energy:':<{padding}}To be implemented\n", + #f"{'Current photon energy:':<{padding}}{dev.mokev.read()['mokev']['value']:>{padding}.4f}\n", f"{'Exposure time:':<{padding}}{self.tomo_countingtime:>{padding}.2f}\n", f"{'Fermat spiral step size:':<{padding}}{self.tomo_shellstep:>{padding}.2f}\n", f"{'FOV:':<{padding}}{fovxy:>{padding}}\n", - f"{'Stitching:':<{padding}}{stitching:>{padding}}\n", + f"{'Stitching:':<{padding}}{stitching:>{padding}.0f}\n", f"{'Number of individual sub-tomograms:':<{padding}}{8:>{padding}}\n", f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n", ] content = "".join(content) - user_target = os.path.expanduser(f"~/Data10/documentation/tomo_scan_ID_{self.tomo_id}.pdf") + user_target = os.path.expanduser(f"~/data/raw/documentation/tomo_scan_ID_{self.tomo_id}.pdf") with PDFWriter(user_target) as file: file.write(header) file.write(content) - subprocess.run( - "xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &", shell=True - ) + # subprocess.run( + # "xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &", shell=True + # ) # status = subprocess.run(f"cp /tmp/spec-e20131-specES1.pdf {user_target}", shell=True) - msg = bec.logbook.LogbookMessage() - logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LamNI_logo.png") - msg.add_file(logo_path).add_text("".join(content).replace("\n", "

")).add_tag( - ["BEC", "tomo_parameters", f"dataset_id_{dataset_id}", "LamNI", self.sample_name] - ) - self.client.logbook.send_logbook_message(msg) + # msg = bec.tomo_progress.tomo_progressMessage() + # logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LamNI_logo.png") + # msg.add_file(logo_path).add_text("".join(content).replace("\n", "

")).add_tag( + # ["BEC", "tomo_parameters", f"dataset_id_{dataset_id}", "flOMNI", self.sample_name] + # ) + # self.client.tomo_progress.send_tomo_progress_message("~/data/raw/documentation/tomo_scan_ID_{self.tomo_id}.pdf").send() + import csaxs_bec + + + # Ensure this is a Path object, not a string + csaxs_bec_basepath = Path(csaxs_bec.__file__) + + logo_file_rel = "flOMNI.png" + + # Build the absolute path correctly + logo_file = ( + csaxs_bec_basepath.parent + / "bec_ipython_client" + / "plugins" + / "flomni" + / logo_file_rel + ).resolve() + print(logo_file) + bec.messaging.scilog.new().add_attachment(logo_file, width=200).add_text(content.replace("\n", "
")).add_tags("tomoscan").send() if __name__ == "__main__": diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py b/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py index 653883f..5d03537 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/gui_tools.py @@ -223,6 +223,14 @@ class flomniGuiTools: self._flomnigui_update_progress() def _flomnigui_update_progress(self): + """Update the progress ring bar and center label from the current progress state. + + ``self.progress`` is backed by the BEC global variable ``tomo_progress`` + (see :class:`_ProgressProxy` in ``flomni.py``), so this method reflects + the live state that is also accessible from other BEC client sessions via:: + + client.get_global_var("tomo_progress") + """ main_progress_ring = self.progressbar.rings[0] subtomo_progress_ring = self.progressbar.rings[1] if self.progressbar is not None: @@ -235,6 +243,31 @@ class flomniGuiTools: main_progress_ring.set_value(progress) subtomo_progress_ring.set_value(subtomo_progress) + # --- format start time for display -------------------------------- + start_str = self.progress.get("tomo_start_time") + if start_str is not None: + import datetime as _dt + start_display = _dt.datetime.fromisoformat(start_str).strftime("%Y-%m-%d %H:%M:%S") + else: + start_display = "N/A" + + # --- format estimated remaining time ------------------------------ + remaining_s = self.progress.get("estimated_remaining_time") + if remaining_s is not None and remaining_s >= 0: + import datetime as _dt + remaining_s = int(remaining_s) + h, rem = divmod(remaining_s, 3600) + m, s = divmod(rem, 60) + if h > 0: + eta_display = f"{h}h {m:02d}m {s:02d}s" + elif m > 0: + eta_display = f"{m}m {s:02d}s" + else: + eta_display = f"{s}s" + else: + eta_display = "N/A" + # ------------------------------------------------------------------ + text = ( f"Progress report:\n" f" Tomo type: {self.progress['tomo_type']}\n" @@ -243,7 +276,9 @@ class flomniGuiTools: f" Angle: {self.progress['angle']:.1f}\n" f" Current subtomo: {self.progress['subtomo']}\n" f" Current projection within subtomo: {self.progress['subtomo_projection']}\n" - f" Total projections per subtomo: {int(self.progress['subtomo_total_projections'])}" + f" Total projections per subtomo: {int(self.progress['subtomo_total_projections'])}\n" + f" Scan started: {start_display}\n" + f" Est. remaining: {eta_display}" ) self.progressbar.set_center_label(text) diff --git a/csaxs_bec/bec_ipython_client/plugins/omny/OMNY.png b/csaxs_bec/bec_ipython_client/plugins/omny/OMNY.png new file mode 100644 index 0000000..bf00a11 Binary files /dev/null and b/csaxs_bec/bec_ipython_client/plugins/omny/OMNY.png differ