Compare commits
2 Commits
feat/flomn
...
fix/fixes_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a81b4c49e | ||
|
|
b60ec2927a |
@@ -1,21 +0,0 @@
|
||||
name: Read the Docs Deploy Trigger
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
trigger-rtd-webhook:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger Read the Docs webhook
|
||||
env:
|
||||
RTD_TOKEN: ${{ secrets.RTD_TOKEN }}
|
||||
run: |
|
||||
curl --fail --show-error --silent \
|
||||
-X POST \
|
||||
-d "branches=${{ github.ref_name }}" \
|
||||
-d "token=${RTD_TOKEN}" \
|
||||
"https://readthedocs.org/api/v2/webhook/sls-csaxs/270162/"
|
||||
@@ -8,14 +8,15 @@ version: 2
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
python: "3.10"
|
||||
jobs:
|
||||
pre_install:
|
||||
- pip install .
|
||||
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
configuration: docs/conf.py
|
||||
|
||||
# If using Sphinx, optionally build your docs in additional formats such as PDF
|
||||
# formats:
|
||||
@@ -23,5 +24,6 @@ sphinx:
|
||||
|
||||
# Optionally declare the Python requirements required to build your docs
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 562 KiB |
BIN
csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_logo.png
Normal file
BIN
csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -1,89 +0,0 @@
|
||||
"""
|
||||
LamNI/webpage_generator.py
|
||||
===========================
|
||||
LamNI-specific webpage generator subclass.
|
||||
|
||||
Integration (inside the LamNI __init__ / startup):
|
||||
---------------------------------------------------
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI.webpage_generator import (
|
||||
LamniWebpageGenerator,
|
||||
)
|
||||
self._webpage_gen = LamniWebpageGenerator(
|
||||
bec_client=client,
|
||||
output_dir="~/data/raw/webpage/",
|
||||
)
|
||||
self._webpage_gen.start()
|
||||
|
||||
Or use the factory (auto-selects by session name "lamni"):
|
||||
----------------------------------------------------------
|
||||
from csaxs_bec.bec_ipython_client.plugins.flomni.webpage_generator import (
|
||||
make_webpage_generator,
|
||||
)
|
||||
self._webpage_gen = make_webpage_generator(bec, output_dir="~/data/raw/webpage/")
|
||||
self._webpage_gen.start()
|
||||
|
||||
Interactive helpers:
|
||||
--------------------
|
||||
lamni._webpage_gen.status()
|
||||
lamni._webpage_gen.verbosity = 2
|
||||
lamni._webpage_gen.stop()
|
||||
lamni._webpage_gen.start()
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.flomni.webpage_generator import (
|
||||
WebpageGeneratorBase,
|
||||
_safe_get,
|
||||
_safe_float,
|
||||
_gvar,
|
||||
)
|
||||
|
||||
|
||||
class LamniWebpageGenerator(WebpageGeneratorBase):
|
||||
"""
|
||||
LamNI-specific webpage generator.
|
||||
Logo: LamNI.png from the same directory as this file.
|
||||
|
||||
Override _collect_setup_data() to add LamNI-specific temperatures,
|
||||
sample name, and measurement settings.
|
||||
"""
|
||||
|
||||
# TODO: fill in LamNI-specific device paths
|
||||
# label -> dotpath under device_manager.devices
|
||||
_TEMP_MAP = {
|
||||
# "Sample": "lamni_temphum.temperature_sample",
|
||||
# "OSA": "lamni_temphum.temperature_osa",
|
||||
}
|
||||
|
||||
def _logo_path(self):
|
||||
return Path(__file__).parent / "LamNI.png"
|
||||
|
||||
def _collect_setup_data(self) -> dict:
|
||||
# ── LamNI-specific data goes here ─────────────────────────────
|
||||
# Uncomment and adapt when device names are known:
|
||||
#
|
||||
# dm = self._bec.device_manager
|
||||
# sample_name = _safe_get(dm, "lamni_samples.sample_names.sample0") or "N/A"
|
||||
# temperatures = {
|
||||
# label: _safe_float(_safe_get(dm, path))
|
||||
# for label, path in self._TEMP_MAP.items()
|
||||
# }
|
||||
# settings = {
|
||||
# "Sample name": sample_name,
|
||||
# "FOV x / y": ...,
|
||||
# "Exposure time": _gvar(self._bec, "tomo_countingtime", ".3f", " s"),
|
||||
# "Angle step": _gvar(self._bec, "tomo_angle_stepsize", ".2f", "\u00b0"),
|
||||
# }
|
||||
# return {
|
||||
# "type": "lamni",
|
||||
# "sample_name": sample_name,
|
||||
# "temperatures": temperatures,
|
||||
# "settings": settings,
|
||||
# }
|
||||
|
||||
# Placeholder — returns minimal info until implemented
|
||||
return {
|
||||
"type": "lamni",
|
||||
# LamNI-specific data here
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 124 KiB |
@@ -21,14 +21,6 @@ from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import (
|
||||
TomoIDManager,
|
||||
)
|
||||
|
||||
# from csaxs_bec.bec_ipython_client.plugins.flomni.webpage_generator import (
|
||||
# FlomniWebpageGenerator,
|
||||
# VERBOSITY_SILENT, # 0 — no output
|
||||
# VERBOSITY_NORMAL, # 1 — startup / stop messages only (default)
|
||||
# VERBOSITY_VERBOSE, # 2 — one-line summary per cycle
|
||||
# VERBOSITY_DEBUG, # 3 — full JSON payload per cycle
|
||||
# )
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
if builtins.__dict__.get("bec") is not None:
|
||||
@@ -977,6 +969,8 @@ 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__)
|
||||
@@ -1217,76 +1211,6 @@ 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,
|
||||
@@ -1309,17 +1233,14 @@ class Flomni(
|
||||
self.corr_angle_y = []
|
||||
self.corr_pos_y_2 = []
|
||||
self.corr_angle_y_2 = []
|
||||
self._progress_proxy = _ProgressProxy(self.client)
|
||||
self._progress_proxy.reset()
|
||||
from csaxs_bec.bec_ipython_client.plugins.flomni.flomni_webpage_generator import (
|
||||
FlomniWebpageGenerator,
|
||||
)
|
||||
self._webpage_gen = FlomniWebpageGenerator(
|
||||
bec_client=client,
|
||||
output_dir="~/data/raw/webpage/",
|
||||
)
|
||||
self._webpage_gen.start()
|
||||
|
||||
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.OMNYTools = OMNYTools(self.client)
|
||||
self.reconstructor = PtychoReconstructor(self.ptycho_reconstruct_foldername)
|
||||
self.tomo_id_manager = TomoIDManager()
|
||||
@@ -1375,42 +1296,6 @@ 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")
|
||||
@@ -1597,11 +1482,21 @@ 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.")
|
||||
@@ -1611,9 +1506,11 @@ 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):
|
||||
@@ -1625,6 +1522,7 @@ 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":
|
||||
@@ -1638,27 +1536,24 @@ 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)
|
||||
alignment_scan_numbers.append(scan_nr)
|
||||
self._write_tomo_scan_number(scan_nr, angle, 0)
|
||||
|
||||
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() ."
|
||||
)
|
||||
|
||||
# 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"
|
||||
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,
|
||||
)
|
||||
print(scliog_content)
|
||||
bec.messaging.scilog.new().add_text(scilog_content.replace("\n", "<br>")).add_tags("alignmentscan").send()
|
||||
|
||||
def sub_tomo_scan(self, subtomo_number, start_angle=None):
|
||||
"""
|
||||
@@ -1667,6 +1562,18 @@ 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.")
|
||||
@@ -1766,7 +1673,6 @@ 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:
|
||||
@@ -1840,8 +1746,6 @@ 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:
|
||||
@@ -1861,6 +1765,7 @@ 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
|
||||
@@ -1908,6 +1813,7 @@ 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
|
||||
@@ -1949,42 +1855,14 @@ 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']}")
|
||||
print(f"Estimated remaining time: ........ {eta_str}\x1b[0m")
|
||||
print(f"Current projection within subtomo: {self.progress['subtomo_projection']}\x1b[0m")
|
||||
self._flomnigui_update_progress()
|
||||
|
||||
def add_sample_database(
|
||||
@@ -2008,6 +1886,7 @@ 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"""
|
||||
@@ -2112,7 +1991,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,
|
||||
@@ -2125,9 +2004,6 @@ class Flomni(
|
||||
corridor_size=corridor_size,
|
||||
)
|
||||
|
||||
self.tomo_reconstruct()
|
||||
|
||||
|
||||
def tomo_parameters(self):
|
||||
"""print and update the tomo parameters"""
|
||||
print("Current settings:")
|
||||
@@ -2266,21 +2142,19 @@ class Flomni(
|
||||
+ ' 888 888 "Y88888P" 888 888 888 Y888 8888888 \n'
|
||||
)
|
||||
padding = 20
|
||||
fovxy = f"{self.fovx:.1f}/{self.fovy:.1f}"
|
||||
stitching = f"{self.stitch_x:.0f}/{self.stitch_y:.0f}"
|
||||
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(account):>{padding}}\n",
|
||||
f"{'e-account:':<{padding}}{str(self.client.username):>{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}}To be implemented\n",
|
||||
#f"{'Current photon energy:':<{padding}}{dev.mokev.read()['mokev']['value']:>{padding}.4f}\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",
|
||||
@@ -2289,38 +2163,20 @@ class Flomni(
|
||||
f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n",
|
||||
]
|
||||
content = "".join(content)
|
||||
user_target = os.path.expanduser(f"~/data/raw/documentation/tomo_scan_ID_{self.tomo_id}.pdf")
|
||||
user_target = os.path.expanduser(f"~/Data10/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.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", "</p><p>")).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).add_text(content.replace("\n", "<br>")).add_tags("tomoscan").send()
|
||||
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", "</p><p>")).add_tag(
|
||||
["BEC", "tomo_parameters", f"dataset_id_{dataset_id}", "LamNI", self.sample_name]
|
||||
)
|
||||
self.client.logbook.send_logbook_message(msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -223,14 +223,6 @@ 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:
|
||||
@@ -243,31 +235,6 @@ 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"
|
||||
@@ -276,9 +243,7 @@ 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'])}\n"
|
||||
f" Scan started: {start_display}\n"
|
||||
f" Est. remaining: {eta_display}"
|
||||
f" Total projections per subtomo: {int(self.progress['subtomo_total_projections'])}"
|
||||
)
|
||||
self.progressbar.set_center_label(text)
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 359 KiB |
@@ -1,96 +0,0 @@
|
||||
"""
|
||||
omny/webpage_generator.py
|
||||
==========================
|
||||
OMNY-specific webpage generator subclass.
|
||||
|
||||
Integration (inside the OMNY __init__ / startup):
|
||||
--------------------------------------------------
|
||||
from csaxs_bec.bec_ipython_client.plugins.omny.webpage_generator import (
|
||||
OmnyWebpageGenerator,
|
||||
)
|
||||
self._webpage_gen = OmnyWebpageGenerator(
|
||||
bec_client=client,
|
||||
output_dir="~/data/raw/webpage/",
|
||||
)
|
||||
self._webpage_gen.start()
|
||||
|
||||
Or use the factory (auto-selects by session name "omny"):
|
||||
---------------------------------------------------------
|
||||
from csaxs_bec.bec_ipython_client.plugins.flomni.webpage_generator import (
|
||||
make_webpage_generator,
|
||||
)
|
||||
self._webpage_gen = make_webpage_generator(bec, output_dir="~/data/raw/webpage/")
|
||||
self._webpage_gen.start()
|
||||
|
||||
Interactive helpers:
|
||||
--------------------
|
||||
omny._webpage_gen.status()
|
||||
omny._webpage_gen.verbosity = 2
|
||||
omny._webpage_gen.stop()
|
||||
omny._webpage_gen.start()
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.flomni.webpage_generator import (
|
||||
WebpageGeneratorBase,
|
||||
_safe_get,
|
||||
_safe_float,
|
||||
_gvar,
|
||||
)
|
||||
|
||||
|
||||
class OmnyWebpageGenerator(WebpageGeneratorBase):
|
||||
"""
|
||||
OMNY-specific webpage generator.
|
||||
Logo: OMNY.png from the same directory as this file.
|
||||
|
||||
Override _collect_setup_data() to add OMNY-specific temperatures,
|
||||
sample name, and measurement settings.
|
||||
|
||||
The old OMNY spec webpage showed:
|
||||
- Cryo temperatures (XOMNY-TEMP-CRYO-A/B)
|
||||
- Per-channel temperatures (XOMNY-TEMP1..48)
|
||||
- Dewar pressure / LN2 flow
|
||||
- Interferometer strengths (OINTERF)
|
||||
Map these to BEC device paths below once available.
|
||||
"""
|
||||
|
||||
# TODO: fill in OMNY-specific device paths
|
||||
# label -> dotpath under device_manager.devices
|
||||
_TEMP_MAP = {
|
||||
# "Sample (cryo A)": "omny_temp.cryo_a",
|
||||
# "Cryo head (B)": "omny_temp.cryo_b",
|
||||
}
|
||||
|
||||
def _logo_path(self):
|
||||
return Path(__file__).parent / "OMNY.png"
|
||||
|
||||
def _collect_setup_data(self) -> dict:
|
||||
# ── OMNY-specific data goes here ──────────────────────────────
|
||||
# Uncomment and adapt when device names are known:
|
||||
#
|
||||
# dm = self._bec.device_manager
|
||||
# sample_name = _safe_get(dm, "omny_samples.sample_names.sample0") or "N/A"
|
||||
# temperatures = {
|
||||
# label: _safe_float(_safe_get(dm, path))
|
||||
# for label, path in self._TEMP_MAP.items()
|
||||
# }
|
||||
# settings = {
|
||||
# "Sample name": sample_name,
|
||||
# "FOV x / y": ...,
|
||||
# "Exposure time": _gvar(self._bec, "tomo_countingtime", ".3f", " s"),
|
||||
# "Angle step": _gvar(self._bec, "tomo_angle_stepsize", ".2f", "\u00b0"),
|
||||
# }
|
||||
# return {
|
||||
# "type": "omny",
|
||||
# "sample_name": sample_name,
|
||||
# "temperatures": temperatures,
|
||||
# "settings": settings,
|
||||
# }
|
||||
|
||||
# Placeholder — returns minimal info until implemented
|
||||
return {
|
||||
"type": "omny",
|
||||
# OMNY-specific data here
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
endstation:
|
||||
- !include ./bl_endstation.yaml
|
||||
|
||||
detectors:
|
||||
- !include ./bl_detectors.yaml
|
||||
# detectors:
|
||||
# - !include ./bl_detectors.yaml
|
||||
|
||||
#sastt:
|
||||
# - !include ./sastt.yaml
|
||||
|
||||
@@ -395,16 +395,6 @@ rtz:
|
||||
readoutPriority: on_request
|
||||
connectionTimeout: 20
|
||||
|
||||
rt_flyer:
|
||||
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniFlyer
|
||||
deviceConfig:
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
readoutPriority: async
|
||||
connectionTimeout: 20
|
||||
enabled: true
|
||||
readOnly: False
|
||||
|
||||
############################################################
|
||||
####################### Cameras ############################
|
||||
############################################################
|
||||
|
||||
@@ -317,6 +317,8 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
try:
|
||||
scan_done = bool(value == self._num_total_triggers)
|
||||
self.progress.put(value=value, max_value=self._num_total_triggers, done=scan_done)
|
||||
if scan_done:
|
||||
self._scan_done_event.set()
|
||||
except Exception:
|
||||
content = traceback.format_exc()
|
||||
logger.info(f"Device {self.name} error: {content}")
|
||||
@@ -391,7 +393,6 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
self._current_data_index = 0
|
||||
|
||||
# NOTE Make sure that the signal that omits mca callbacks is cleared
|
||||
# DO NOT REMOVE!!
|
||||
self._omit_mca_callbacks.clear()
|
||||
|
||||
# For a fly scan we need to start the mcs card ourselves
|
||||
@@ -562,9 +563,8 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Hook called when the device is stopped. In addition, any status that is registered through cancel_on_stop will be cancelled here."""
|
||||
with suppress_mca_callbacks(self):
|
||||
self.stop_all.put(1)
|
||||
self.erase_all.put(1)
|
||||
self.stop_all.put(1)
|
||||
self.erase_all.put(1)
|
||||
|
||||
def mcs_recovery(self, timeout: int = 1) -> None:
|
||||
"""
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import threading
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError
|
||||
from ophyd_devices import AsyncMultiSignal, DeviceStatus, ProgressSignal
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from csaxs_bec.devices.omny.rt.rt_ophyd import (
|
||||
BECConfigError,
|
||||
RtCommunicationError,
|
||||
RtError,
|
||||
RtReadbackSignal,
|
||||
@@ -430,6 +432,27 @@ class RtFlomniController(Controller):
|
||||
t.add_row([i, self.read_ssi_interferometer(i)])
|
||||
print(t)
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[4])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[7])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[2])},
|
||||
"average_x_st_fzp": {"value": float(return_table[3])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[4])},
|
||||
"target_y": {"value": float(return_table[5])},
|
||||
"average_y_st_fzp": {"value": float(return_table[6])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[7])},
|
||||
"average_rotz": {"value": float(return_table[8])},
|
||||
"stdev_rotz": {"value": float(return_table[9])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
}
|
||||
return signals
|
||||
|
||||
@threadlocked
|
||||
def start_scan(self):
|
||||
if not self.feedback_is_running():
|
||||
@@ -469,6 +492,91 @@ class RtFlomniController(Controller):
|
||||
current_position_in_scan = int(float(return_table[2]))
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
def read_positions_from_sampler(self):
|
||||
# this was for reading after the scan completed
|
||||
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
|
||||
|
||||
read_counter = 0
|
||||
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
# while scan is running
|
||||
while mode > 0:
|
||||
|
||||
# TODO here?: scan abortion if no progress in scan *raise error
|
||||
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
|
||||
time.sleep(0.01)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
# logger.info(f"{return_table}")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
time.sleep(0.05)
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter:
|
||||
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
# logger.info(f"{return_table}")
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Flomni statistics: Average of all standard deviations: x"
|
||||
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}"
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.device_manager.connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_flomni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
),
|
||||
)
|
||||
|
||||
def start_readout(self):
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler)
|
||||
readout.start()
|
||||
|
||||
def kickoff(self, metadata):
|
||||
self.readout_metadata = metadata
|
||||
while not self._min_scan_buffer_reached:
|
||||
time.sleep(0.001)
|
||||
self.start_scan()
|
||||
time.sleep(0.1)
|
||||
self.start_readout()
|
||||
|
||||
|
||||
class RtFlomniReadbackSignal(RtReadbackSignal):
|
||||
@retry_once
|
||||
@@ -736,185 +844,6 @@ class RtFlomniMotor(Device, PositionerBase):
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
class RtFlomniFlyer(Device):
|
||||
USER_ACCESS = ["controller"]
|
||||
data = Cpt(
|
||||
AsyncMultiSignal,
|
||||
name="data",
|
||||
signals=[
|
||||
"target_x",
|
||||
"average_x_st_fzp",
|
||||
"stdev_x_st_fzp",
|
||||
"target_y",
|
||||
"average_y_st_fzp",
|
||||
"stdev_y_st_fzp",
|
||||
"average_rotz",
|
||||
"stdev_rotz",
|
||||
"average_stdeviations_x_st_fzp",
|
||||
"average_stdeviations_y_st_fzp",
|
||||
],
|
||||
ndim=1,
|
||||
async_update={"type": "add", "max_shape": [None]},
|
||||
max_size=1000,
|
||||
)
|
||||
progress = Cpt(
|
||||
ProgressSignal, doc="ProgressSignal indicating the progress of the device during a scan."
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2844.psi.ch",
|
||||
port=2222,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(prefix=prefix, name=name, parent=parent, **kwargs)
|
||||
self.shutdown_event = threading.Event()
|
||||
self.controller = RtFlomniController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
|
||||
)
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
self.readout_thread = None
|
||||
self.scan_done_event = threading.Event()
|
||||
self.scan_done_event.set()
|
||||
|
||||
def read_positions_from_sampler(self, status: DeviceStatus):
|
||||
"""
|
||||
Read the positions from the sampler and update the data signal.
|
||||
This function runs in a separate thread and continuously checks the
|
||||
scan status.
|
||||
|
||||
Args:
|
||||
status (DeviceStatus): The status object to update when the readout is complete.
|
||||
"""
|
||||
read_counter = 0
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = (
|
||||
self.controller.get_scan_status()
|
||||
)
|
||||
|
||||
# while scan is running
|
||||
while mode > 0 and not self.shutdown_event.wait(0.01):
|
||||
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
|
||||
mode, number_of_positions_planned, current_position_in_scan = (
|
||||
self.controller.get_scan_status()
|
||||
)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (
|
||||
self.controller.socket_put_and_receive(f"r{read_counter}")
|
||||
).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
self.progress.put(
|
||||
value=read_counter, max_value=number_of_positions_planned, done=False
|
||||
)
|
||||
read_counter = read_counter + 1
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.data.set(signals)
|
||||
|
||||
if self.shutdown_event.wait(0.05):
|
||||
logger.info("Shutdown event set, stopping readout.")
|
||||
# if we are here, the shutdown_event is set. We can exit the readout loop.
|
||||
status.set_finished()
|
||||
return
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter and not self.shutdown_event.is_set():
|
||||
return_table = (self.controller.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
self.progress.put(value=read_counter, max_value=number_of_positions_planned, done=False)
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.data.set(signals)
|
||||
|
||||
# NOTE: No need to set the status to failed if the shutdown_event is set.
|
||||
# The stop() method will take care of that.
|
||||
status.set_finished()
|
||||
self.progress.put(value=read_counter, max_value=number_of_positions_planned, done=True)
|
||||
|
||||
logger.info(
|
||||
"Flomni statistics: Average of all standard deviations: x"
|
||||
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}"
|
||||
)
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[4])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[7])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[2])},
|
||||
"average_x_st_fzp": {"value": float(return_table[3])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[4])},
|
||||
"target_y": {"value": float(return_table[5])},
|
||||
"average_y_st_fzp": {"value": float(return_table[6])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[7])},
|
||||
"average_rotz": {"value": float(return_table[8])},
|
||||
"stdev_rotz": {"value": float(return_table[9])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
}
|
||||
return signals
|
||||
|
||||
def stage(self):
|
||||
self.shutdown_event.clear()
|
||||
self.scan_done_event.set()
|
||||
return super().stage()
|
||||
|
||||
def start_readout(self, status: DeviceStatus):
|
||||
self.readout_thread = threading.Thread(
|
||||
target=self.read_positions_from_sampler, args=(status,)
|
||||
)
|
||||
self.readout_thread.start()
|
||||
|
||||
def kickoff(self) -> DeviceStatus:
|
||||
self.shutdown_event.clear()
|
||||
self.scan_done_event.clear()
|
||||
while not self.controller._min_scan_buffer_reached and not self.shutdown_event.wait(0.001):
|
||||
...
|
||||
self.controller.start_scan()
|
||||
self.shutdown_event.wait(0.1)
|
||||
status = DeviceStatus(self)
|
||||
status.set_finished()
|
||||
return status
|
||||
|
||||
def complete(self) -> DeviceStatus:
|
||||
"""Wait until the flyer is done."""
|
||||
if self.scan_done_event.is_set():
|
||||
# if the scan_done_event is already set, we can return a finished status immediately
|
||||
status = DeviceStatus(self)
|
||||
status.set_finished()
|
||||
return status
|
||||
status = DeviceStatus(self)
|
||||
self.start_readout(status)
|
||||
status.add_callback(lambda *args, **kwargs: self.scan_done_event.set())
|
||||
return status
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.shutdown_event.set()
|
||||
self.scan_done_event.set()
|
||||
if self.readout_thread is not None:
|
||||
self.readout_thread.join()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rtcontroller = RtFlomniController(
|
||||
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222, device_manager=None
|
||||
|
||||
@@ -27,19 +27,20 @@ from bec_lib import bec_logger, messages
|
||||
from bec_lib.alarm_handler import Alarms
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_server.scan_server.errors import ScanAbortion
|
||||
from bec_server.scan_server.scans import AsyncFlyScanBase
|
||||
from bec_server.scan_server.scans import SyncFlyScanBase
|
||||
|
||||
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import TRIGGERSOURCE
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FlomniFermatScan(AsyncFlyScanBase):
|
||||
class FlomniFermatScan(SyncFlyScanBase):
|
||||
scan_name = "flomni_fermat_scan"
|
||||
scan_type = "fly"
|
||||
required_kwargs = ["fovx", "fovy", "exp_time", "step", "angle"]
|
||||
arg_input = {}
|
||||
arg_bundle_size = {"bundle": len(arg_input), "min": None, "max": None}
|
||||
use_scan_progress_report = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -103,14 +104,6 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
self.zshift = -100
|
||||
self.flomni_rotation_status = None
|
||||
|
||||
def scan_report_instructions(self):
|
||||
"""Scan report instructions for the progress bar"""
|
||||
yield from self.stubs.scan_report_instruction({"device_progress": ["rt_flyer"]})
|
||||
|
||||
@property
|
||||
def monitor_sync(self) -> str:
|
||||
return "rt_flyer"
|
||||
|
||||
def initialize(self):
|
||||
self.scan_motors = []
|
||||
self.update_readout_priority()
|
||||
@@ -120,6 +113,10 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
self.positions, corridor_size=self.optim_trajectory_corridor
|
||||
)
|
||||
|
||||
@property
|
||||
def monitor_sync(self):
|
||||
return "rt_flomni"
|
||||
|
||||
def reverse_trajectory(self):
|
||||
"""
|
||||
Reverse the trajectory. Every other scan should be reversed to
|
||||
@@ -293,18 +290,26 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
return np.array(positions)
|
||||
|
||||
def scan_core(self):
|
||||
# send off the flyer
|
||||
yield from self.stubs.kickoff(device="rt_flyer")
|
||||
# use a device message to receive the scan number and
|
||||
# scan ID before sending the message to the device server
|
||||
yield from self.stubs.kickoff(device="rtx")
|
||||
while True:
|
||||
yield from self.stubs.read(group="monitored")
|
||||
status = self.connector.get(MessageEndpoints.device_status("rt_scan"))
|
||||
if status:
|
||||
status_id = status.content.get("status", 1)
|
||||
request_id = status.metadata.get("RID")
|
||||
if status_id == 0 and self.metadata.get("RID") == request_id:
|
||||
break
|
||||
if status_id == 2 and self.metadata.get("RID") == request_id:
|
||||
raise ScanAbortion(
|
||||
"An error occured during the flomni readout:"
|
||||
f" {status.metadata.get('error')}"
|
||||
)
|
||||
|
||||
# start the readout loop of the flyer
|
||||
status = yield from self.stubs.complete(device="rt_flyer", wait=False)
|
||||
|
||||
# read the monitors until the flyer is done
|
||||
while not status.done:
|
||||
yield from self.stubs.read(group="monitored", point_id=self.point_id)
|
||||
self.point_id += 1
|
||||
time.sleep(1)
|
||||
logger.debug("reading monitors")
|
||||
# yield from self.device_rpc("rtx", "controller.kickoff")
|
||||
|
||||
def move_to_start(self):
|
||||
"""return to the start position"""
|
||||
@@ -331,7 +336,6 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
yield from self.read_scan_motors()
|
||||
self.prepare_positions()
|
||||
yield from self._prepare_setup()
|
||||
yield from self.scan_report_instructions()
|
||||
yield from self.open_scan()
|
||||
yield from self.stage()
|
||||
yield from self.run_baseline_reading()
|
||||
|
||||
@@ -217,16 +217,6 @@ def test_mcs_card_csaxs_complete_and_stop(mock_mcs_csaxs: MCSCardCSAXS):
|
||||
assert not mcs._start_monitor_async_data_emission.is_set()
|
||||
|
||||
|
||||
def test_mcs_on_stop(mock_mcs_csaxs: MCSCardCSAXS):
|
||||
"""Test that on stop sets the omit_mca_callbacks flag. Also test that on stage clears the omit_mca_callbacks flag."""
|
||||
mcs = mock_mcs_csaxs
|
||||
assert mcs._omit_mca_callbacks.is_set() is False
|
||||
mcs.stop()
|
||||
assert mcs._omit_mca_callbacks.is_set() is True
|
||||
mcs.stage()
|
||||
assert mcs._omit_mca_callbacks.is_set() is False
|
||||
|
||||
|
||||
def test_mcs_recovery(mock_mcs_csaxs: MCSCardCSAXS):
|
||||
mcs = mock_mcs_csaxs
|
||||
# Simulate ongoing acquisition
|
||||
|
||||
Reference in New Issue
Block a user