Compare commits
9 Commits
feat/flomn
...
test_pseud
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a85599b2a | |||
| b8a32db919 | |||
| 4be5ee2347 | |||
| 3e93861407 | |||
| b290d75205 | |||
| 9ff42865f4 | |||
| 6bee207acb | |||
| 9990a70d55 | |||
|
|
3dcdbc5f63 |
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 |
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 (
|
||||
WebpageGenerator,
|
||||
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,14 +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()
|
||||
self._webpage_gen = WebpageGenerator(
|
||||
bec_client=client,
|
||||
output_dir="~/data/raw/webpage/", # adjust to your staging path
|
||||
verbosity=VERBOSITY_NORMAL,
|
||||
)
|
||||
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()
|
||||
@@ -1372,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")
|
||||
@@ -1594,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.")
|
||||
@@ -1608,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):
|
||||
@@ -1622,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":
|
||||
@@ -1635,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):
|
||||
"""
|
||||
@@ -1664,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.")
|
||||
@@ -1763,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:
|
||||
@@ -1837,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:
|
||||
@@ -1858,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
|
||||
@@ -1905,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
|
||||
@@ -1946,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(
|
||||
@@ -2005,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"""
|
||||
@@ -2109,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,
|
||||
@@ -2122,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:")
|
||||
@@ -2263,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",
|
||||
@@ -2286,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__":
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,892 +0,0 @@
|
||||
"""
|
||||
webpage_generator.py
|
||||
====================
|
||||
Background thread that reads the flOMNI tomo progress from the BEC global
|
||||
variable store and writes a self-contained status.json + status.html to a
|
||||
configurable output directory. A separate upload process can copy those
|
||||
files to the web host.
|
||||
|
||||
Usage (inside Flomni.__init__, after self._progress_proxy.reset()):
|
||||
--------------------------------------------------------------------
|
||||
self._webpage_gen = WebpageGenerator(
|
||||
bec_client=client,
|
||||
output_dir="~/data/raw/webpage/",
|
||||
)
|
||||
self._webpage_gen.start()
|
||||
|
||||
Interactive commands (optional, in the iPython session):
|
||||
---------------------------------------------------------
|
||||
flomni._webpage_gen.status() # print current status
|
||||
flomni._webpage_gen.verbosity = 2 # switch to VERBOSE mid-session
|
||||
flomni._webpage_gen.stop() # release lock, let another session take over
|
||||
flomni._webpage_gen.start() # restart after stop()
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# BEC global-var key used as a distributed singleton lock
|
||||
_LOCK_VAR_KEY = "webpage_generator_lock"
|
||||
|
||||
# Heartbeat must be refreshed at least this often (seconds) or the lock
|
||||
# is considered stale and another session may take over.
|
||||
_LOCK_STALE_AFTER_S = 45
|
||||
|
||||
# How long between generator cycles (seconds)
|
||||
_CYCLE_INTERVAL_S = 15
|
||||
|
||||
# If the tomo progress heartbeat has not been updated for this long we
|
||||
# consider the tomo loop no longer actively running.
|
||||
_TOMO_HEARTBEAT_STALE_S = 90
|
||||
|
||||
# After finishing normally, stay in IDLE_SHORT for this long before
|
||||
# switching to IDLE_LONG (which triggers the audio warning).
|
||||
_IDLE_SHORT_WINDOW_S = 300 # 5 minutes
|
||||
|
||||
# Verbosity levels
|
||||
VERBOSITY_SILENT = 0 # no output at all
|
||||
VERBOSITY_NORMAL = 1 # startup/stop messages only
|
||||
VERBOSITY_VERBOSE = 2 # each cycle summary
|
||||
VERBOSITY_DEBUG = 3 # full detail each cycle
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def _epoch() -> float:
|
||||
return time.time()
|
||||
|
||||
|
||||
def _heartbeat_age_s(iso_str) -> float:
|
||||
"""Return seconds since the ISO-format heartbeat string, or infinity."""
|
||||
if iso_str is None:
|
||||
return float("inf")
|
||||
try:
|
||||
ts = datetime.datetime.fromisoformat(iso_str)
|
||||
return (datetime.datetime.now() - ts).total_seconds()
|
||||
except Exception:
|
||||
return float("inf")
|
||||
|
||||
|
||||
def _format_duration(seconds) -> str:
|
||||
if seconds is None:
|
||||
return "N/A"
|
||||
try:
|
||||
seconds = int(float(seconds))
|
||||
except (TypeError, ValueError):
|
||||
return "N/A"
|
||||
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 _check_account_match(bec_client) -> bool:
|
||||
"""Return True if the BEC active account matches the system user."""
|
||||
try:
|
||||
active = bec_client.active_account # e.g. "p23092"
|
||||
system_user = os.getenv("USER") or os.getlogin() # e.g. "e23092"
|
||||
return active[1:] == system_user[1:]
|
||||
except Exception:
|
||||
return True # don't block on unknown accounts
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status derivation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _derive_status(progress: dict, queue_has_active_scan: bool, idle_since) -> str:
|
||||
"""
|
||||
Derive a simple status string from available signals.
|
||||
|
||||
Returns one of:
|
||||
"scanning" - tomo heartbeat is fresh (tomo loop actively running)
|
||||
"running" - a scan is active but outside the tomo heartbeat window
|
||||
(alignment, other tasks, or brief inter-scan gap)
|
||||
"idle_short" - recently finished, within IDLE_SHORT_WINDOW_S
|
||||
"idle_long" - idle longer than IDLE_SHORT_WINDOW_S (trigger warning)
|
||||
"unknown" - cannot determine yet
|
||||
"""
|
||||
hb_age = _heartbeat_age_s(progress.get("heartbeat"))
|
||||
tomo_active = hb_age < _TOMO_HEARTBEAT_STALE_S
|
||||
|
||||
if tomo_active:
|
||||
return "scanning"
|
||||
|
||||
if queue_has_active_scan:
|
||||
return "running"
|
||||
|
||||
if idle_since is not None:
|
||||
idle_s = _epoch() - idle_since
|
||||
return "idle_short" if idle_s < _IDLE_SHORT_WINDOW_S else "idle_long"
|
||||
|
||||
return "unknown"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main generator class
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class WebpageGenerator:
|
||||
"""
|
||||
Singleton-safe background thread that generates the experiment status
|
||||
page by reading BEC global variables.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
bec_client : BECClient
|
||||
The active BEC client instance (``bec`` in the iPython session).
|
||||
output_dir : str | Path
|
||||
Directory where ``status.json`` and ``status.html`` are written.
|
||||
Created if it does not exist.
|
||||
cycle_interval : float
|
||||
Seconds between update cycles. Default: 15 s.
|
||||
verbosity : int
|
||||
VERBOSITY_SILENT / VERBOSITY_NORMAL / VERBOSITY_VERBOSE / VERBOSITY_DEBUG.
|
||||
Default: VERBOSITY_NORMAL.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bec_client,
|
||||
output_dir: str = "~/data/raw/webpage/",
|
||||
cycle_interval: float = _CYCLE_INTERVAL_S,
|
||||
verbosity: int = VERBOSITY_NORMAL,
|
||||
):
|
||||
self._bec = bec_client
|
||||
self._output_dir = Path(output_dir).expanduser().resolve()
|
||||
self._cycle_interval = cycle_interval
|
||||
self._verbosity = verbosity
|
||||
|
||||
self._thread = None
|
||||
self._stop_event = threading.Event()
|
||||
|
||||
# Rolling state kept between cycles
|
||||
self._idle_since = None
|
||||
self._owner_id = f"{socket.gethostname()}:{os.getpid()}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Public API
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def start(self) -> bool:
|
||||
"""
|
||||
Start the generator thread if this session wins the singleton lock.
|
||||
Returns True if started, False if another session already owns it.
|
||||
"""
|
||||
if not _check_account_match(self._bec):
|
||||
self._log(
|
||||
VERBOSITY_NORMAL,
|
||||
"WebpageGenerator: BEC account does not match system user. "
|
||||
"Not starting to avoid writing data to the wrong account.",
|
||||
level="warning",
|
||||
)
|
||||
return False
|
||||
|
||||
if self._thread is not None and self._thread.is_alive():
|
||||
self._log(VERBOSITY_NORMAL, "WebpageGenerator already running in this session.")
|
||||
return True
|
||||
|
||||
if not self._acquire_lock():
|
||||
return False
|
||||
|
||||
self._output_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._stop_event.clear()
|
||||
self._thread = threading.Thread(
|
||||
target=self._run,
|
||||
name="WebpageGenerator",
|
||||
daemon=True,
|
||||
)
|
||||
self._thread.start()
|
||||
self._log(
|
||||
VERBOSITY_NORMAL,
|
||||
f"WebpageGenerator started (owner: {self._owner_id}, "
|
||||
f"output: {self._output_dir}, interval: {self._cycle_interval}s)",
|
||||
)
|
||||
return True
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the generator thread and release the singleton lock."""
|
||||
self._stop_event.set()
|
||||
if self._thread is not None:
|
||||
self._thread.join(timeout=self._cycle_interval + 5)
|
||||
self._release_lock()
|
||||
self._log(VERBOSITY_NORMAL, "WebpageGenerator stopped.")
|
||||
|
||||
@property
|
||||
def verbosity(self) -> int:
|
||||
return self._verbosity
|
||||
|
||||
@verbosity.setter
|
||||
def verbosity(self, val: int) -> None:
|
||||
self._verbosity = val
|
||||
self._log(VERBOSITY_NORMAL, f"WebpageGenerator verbosity set to {val}.")
|
||||
|
||||
def status(self) -> None:
|
||||
"""Print a human-readable status summary to the console."""
|
||||
lock = self._read_lock()
|
||||
running = self._thread is not None and self._thread.is_alive()
|
||||
print(
|
||||
f"WebpageGenerator\n"
|
||||
f" This session running : {running}\n"
|
||||
f" Lock owner : {lock.get('owner_id', 'none')}\n"
|
||||
f" Lock heartbeat : {lock.get('heartbeat', 'never')}\n"
|
||||
f" Output dir : {self._output_dir}\n"
|
||||
f" Cycle interval : {self._cycle_interval}s\n"
|
||||
f" Verbosity : {self._verbosity}\n"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Singleton lock helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _acquire_lock(self) -> bool:
|
||||
lock = self._read_lock()
|
||||
if lock:
|
||||
age = _heartbeat_age_s(lock.get("heartbeat"))
|
||||
if age < _LOCK_STALE_AFTER_S:
|
||||
self._log(
|
||||
VERBOSITY_NORMAL,
|
||||
f"WebpageGenerator already owned by "
|
||||
f"'{lock.get('owner_id')}' "
|
||||
f"(heartbeat {age:.0f}s ago). Not starting.",
|
||||
)
|
||||
return False
|
||||
self._log(
|
||||
VERBOSITY_NORMAL,
|
||||
f"Stale lock found (owner: '{lock.get('owner_id')}', "
|
||||
f"{age:.0f}s ago). Taking over.",
|
||||
)
|
||||
|
||||
self._write_lock()
|
||||
return True
|
||||
|
||||
def _write_lock(self) -> None:
|
||||
self._bec.set_global_var(
|
||||
_LOCK_VAR_KEY,
|
||||
{
|
||||
"owner_id": self._owner_id,
|
||||
"heartbeat": _now_iso(),
|
||||
"pid": os.getpid(),
|
||||
"hostname": socket.gethostname(),
|
||||
},
|
||||
)
|
||||
|
||||
def _read_lock(self) -> dict:
|
||||
val = self._bec.get_global_var(_LOCK_VAR_KEY)
|
||||
return val if isinstance(val, dict) else {}
|
||||
|
||||
def _release_lock(self) -> None:
|
||||
lock = self._read_lock()
|
||||
if lock.get("owner_id") == self._owner_id:
|
||||
self._bec.delete_global_var(_LOCK_VAR_KEY)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Main loop
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _run(self) -> None:
|
||||
while not self._stop_event.is_set():
|
||||
cycle_start = _epoch()
|
||||
try:
|
||||
self._cycle()
|
||||
except Exception as exc:
|
||||
self._log(
|
||||
VERBOSITY_NORMAL,
|
||||
f"WebpageGenerator cycle error: {exc}",
|
||||
level="warning",
|
||||
)
|
||||
# Refresh the singleton heartbeat
|
||||
try:
|
||||
self._write_lock()
|
||||
except Exception:
|
||||
pass
|
||||
# Sleep for the remainder of the interval
|
||||
elapsed = _epoch() - cycle_start
|
||||
sleep_time = max(0.0, self._cycle_interval - elapsed)
|
||||
self._stop_event.wait(sleep_time)
|
||||
|
||||
def _cycle(self) -> None:
|
||||
"""One generator cycle: read state -> derive status -> write outputs."""
|
||||
|
||||
# --- Read progress from global var (readable from any session) -------
|
||||
progress = self._bec.get_global_var("tomo_progress") or {}
|
||||
|
||||
# --- Read queue status -----------------------------------------------
|
||||
# NOTE: queue status is always 'RUNNING' while BEC is alive.
|
||||
# An actual scan is executing only when info is non-empty AND
|
||||
# active_request_block is set on the first entry.
|
||||
try:
|
||||
queue_info = self._bec.queue.queue_storage.current_scan_queue
|
||||
primary = queue_info.get("primary")
|
||||
queue_status = primary.status if primary is not None else "unknown"
|
||||
queue_has_active_scan = (
|
||||
primary is not None
|
||||
and len(primary.info) > 0
|
||||
and primary.info[0].active_request_block is not None
|
||||
)
|
||||
except Exception:
|
||||
queue_status = "unknown"
|
||||
queue_has_active_scan = False
|
||||
|
||||
# --- Track idle onset ------------------------------------------------
|
||||
# Use both the tomo heartbeat and the queue active-scan flag.
|
||||
# This handles the brief COMPLETED gap between individual scans
|
||||
# while a tomo is still running.
|
||||
hb_age = _heartbeat_age_s(progress.get("heartbeat"))
|
||||
tomo_active = hb_age < _TOMO_HEARTBEAT_STALE_S
|
||||
|
||||
if tomo_active or queue_has_active_scan:
|
||||
self._idle_since = None
|
||||
elif self._idle_since is None:
|
||||
self._idle_since = _epoch()
|
||||
|
||||
# --- Derive experiment status ----------------------------------------
|
||||
exp_status = _derive_status(progress, queue_has_active_scan, self._idle_since)
|
||||
|
||||
# --- Build payload ---------------------------------------------------
|
||||
idle_for_s = None if self._idle_since is None else (_epoch() - self._idle_since)
|
||||
|
||||
payload = {
|
||||
"generated_at": _now_iso(),
|
||||
"generated_at_epoch": _epoch(),
|
||||
"experiment_status": exp_status,
|
||||
"queue_status": queue_status,
|
||||
"queue_has_active_scan": queue_has_active_scan,
|
||||
"idle_for_s": idle_for_s,
|
||||
"idle_for_human": _format_duration(idle_for_s),
|
||||
"progress": {
|
||||
"tomo_type": progress.get("tomo_type", "N/A"),
|
||||
"projection": progress.get("projection", 0),
|
||||
"total_projections": progress.get("total_projections", 0),
|
||||
"subtomo": progress.get("subtomo", 0),
|
||||
"subtomo_projection": progress.get("subtomo_projection", 0),
|
||||
"subtomo_total_projections": progress.get("subtomo_total_projections", 1),
|
||||
"angle": progress.get("angle", 0),
|
||||
"tomo_start_time": progress.get("tomo_start_time"),
|
||||
"estimated_remaining_s": progress.get("estimated_remaining_time"),
|
||||
"estimated_remaining_human": _format_duration(
|
||||
progress.get("estimated_remaining_time")
|
||||
),
|
||||
"heartbeat": progress.get("heartbeat"),
|
||||
"heartbeat_age_s": round(hb_age, 1) if hb_age != float("inf") else None,
|
||||
},
|
||||
"generator": {
|
||||
"owner_id": self._owner_id,
|
||||
"cycle_interval_s": self._cycle_interval,
|
||||
},
|
||||
}
|
||||
|
||||
# --- Write outputs ---------------------------------------------------
|
||||
json_path = self._output_dir / "status.json"
|
||||
json_path.write_text(json.dumps(payload, indent=2, default=str))
|
||||
|
||||
html_path = self._output_dir / "status.html"
|
||||
html_path.write_text(_render_html())
|
||||
|
||||
# --- Console feedback ------------------------------------------------
|
||||
self._log(
|
||||
VERBOSITY_VERBOSE,
|
||||
f"[{_now_iso()}] status={exp_status} active_scan={queue_has_active_scan} "
|
||||
f"proj={payload['progress']['projection']}/"
|
||||
f"{payload['progress']['total_projections']} "
|
||||
f"hb_age={payload['progress']['heartbeat_age_s']}s "
|
||||
f"idle={_format_duration(idle_for_s)}",
|
||||
)
|
||||
self._log(
|
||||
VERBOSITY_DEBUG,
|
||||
f" full payload:\n{json.dumps(payload, indent=4, default=str)}",
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Logging helper
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _log(self, min_verbosity: int, msg: str, level: str = "info") -> None:
|
||||
if self._verbosity < min_verbosity:
|
||||
return
|
||||
if level == "warning":
|
||||
logger.warning(msg)
|
||||
elif level == "error":
|
||||
logger.error(msg)
|
||||
else:
|
||||
print(msg)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTML template (static shell - the page fetches status.json on load/refresh)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _render_html() -> str:
|
||||
"""Return the full HTML for the status page."""
|
||||
return r"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>flOMNI - Experiment Status</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;600&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #0d0f14;
|
||||
--surface: #161a23;
|
||||
--surface2: #1e2330;
|
||||
--border: #2a3045;
|
||||
--text: #cdd6f4;
|
||||
--text-dim: #6c7a9c;
|
||||
--mono: 'Space Mono', monospace;
|
||||
--sans: 'DM Sans', sans-serif;
|
||||
--c-scanning: #89dceb;
|
||||
--c-running: #a6e3a1;
|
||||
--c-idle-short: #f9e2af;
|
||||
--c-idle-long: #fab387;
|
||||
--c-error: #f38ba8;
|
||||
--c-unknown: #6c7a9c;
|
||||
--status-color: var(--c-unknown);
|
||||
}
|
||||
|
||||
body.scanning { --status-color: var(--c-scanning); }
|
||||
body.running { --status-color: var(--c-running); }
|
||||
body.idle_short { --status-color: var(--c-idle-short);}
|
||||
body.idle_long { --status-color: var(--c-idle-long); }
|
||||
body.error { --status-color: var(--c-error); }
|
||||
body.unknown { --status-color: var(--c-unknown); }
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--sans);
|
||||
font-weight: 300;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
transition: background 0.6s ease;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed; inset: 0;
|
||||
background: radial-gradient(ellipse 80% 50% at 50% -10%,
|
||||
color-mix(in srgb, var(--status-color) 8%, transparent), transparent);
|
||||
pointer-events: none;
|
||||
transition: background 0.8s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.layout {
|
||||
position: relative; z-index: 1;
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.logo {
|
||||
font-family: var(--mono);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.logo span { color: var(--status-color); transition: color 0.6s; }
|
||||
#last-update { font-family: var(--mono); font-size: 0.7rem; color: var(--text-dim); }
|
||||
|
||||
.status-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--status-color);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
transition: border-color 0.6s;
|
||||
}
|
||||
.status-pill {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--bg);
|
||||
background: var(--status-color);
|
||||
padding: 0.3rem 0.9rem;
|
||||
border-radius: 100px;
|
||||
white-space: nowrap;
|
||||
transition: background 0.6s;
|
||||
}
|
||||
.status-detail { flex: 1; font-size: 0.9rem; color: var(--text-dim); line-height: 1.6; }
|
||||
.status-detail strong { color: var(--text); font-weight: 600; }
|
||||
|
||||
.progress-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem 2rem;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 1.5rem 2.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.rings-wrap { position: relative; width: 120px; height: 120px; flex-shrink: 0; }
|
||||
.rings-wrap svg { width: 100%; height: 100%; transform: rotate(-90deg); }
|
||||
.ring-track { fill: none; stroke: var(--surface2); }
|
||||
.ring-outer {
|
||||
fill: none; stroke: var(--status-color); stroke-linecap: round; opacity: 0.9;
|
||||
transition: stroke-dashoffset 0.8s cubic-bezier(.4,0,.2,1), stroke 0.6s;
|
||||
}
|
||||
.ring-inner {
|
||||
fill: none;
|
||||
stroke: color-mix(in srgb, var(--status-color) 55%, var(--surface2));
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dashoffset 0.8s cubic-bezier(.4,0,.2,1), stroke 0.6s;
|
||||
}
|
||||
.ring-label {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: var(--mono);
|
||||
}
|
||||
.ring-label .pct { font-size: 1.3rem; font-weight: 700; color: var(--text); }
|
||||
.ring-label .sublbl { font-size: 0.6rem; color: var(--text-dim); letter-spacing: 0.06em; }
|
||||
|
||||
.progress-info { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem 2rem; }
|
||||
.info-item { display: flex; flex-direction: column; gap: 0.15rem; }
|
||||
.info-item .label {
|
||||
font-family: var(--mono); font-size: 0.65rem; color: var(--text-dim);
|
||||
letter-spacing: 0.08em; text-transform: uppercase;
|
||||
}
|
||||
.info-item .value { font-size: 0.95rem; font-weight: 600; color: var(--text); }
|
||||
|
||||
.bar-wrap { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 0.4rem; }
|
||||
.bar-label {
|
||||
display: flex; justify-content: space-between;
|
||||
font-family: var(--mono); font-size: 0.65rem; color: var(--text-dim);
|
||||
letter-spacing: 0.06em; text-transform: uppercase;
|
||||
}
|
||||
.bar-track { height: 6px; background: var(--surface2); border-radius: 99px; overflow: hidden; }
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
background: color-mix(in srgb, var(--status-color) 70%, var(--surface2));
|
||||
border-radius: 99px;
|
||||
transition: width 0.8s cubic-bezier(.4,0,.2,1), background 0.6s;
|
||||
}
|
||||
|
||||
.audio-card {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 1.25rem 2rem; display: flex; align-items: center;
|
||||
justify-content: space-between; gap: 1rem; flex-wrap: wrap;
|
||||
}
|
||||
.audio-info { display: flex; align-items: center; gap: 0.75rem; }
|
||||
.audio-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim); transition: background 0.3s; }
|
||||
.audio-dot.active { background: var(--c-scanning); box-shadow: 0 0 6px var(--c-scanning); }
|
||||
.audio-text { font-size: 0.85rem; color: var(--text-dim); }
|
||||
.audio-controls { display: flex; gap: 0.75rem; }
|
||||
|
||||
button {
|
||||
font-family: var(--mono); font-size: 0.7rem; font-weight: 700;
|
||||
letter-spacing: 0.1em; text-transform: uppercase;
|
||||
border: 1px solid var(--border); background: var(--surface2); color: var(--text);
|
||||
padding: 0.4rem 1rem; border-radius: 6px; cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
button:hover { background: var(--border); }
|
||||
button.active { background: var(--status-color); border-color: var(--status-color); color: var(--bg); }
|
||||
|
||||
footer {
|
||||
font-family: var(--mono); font-size: 0.65rem; color: var(--text-dim);
|
||||
border-top: 1px solid var(--border); padding-top: 1rem;
|
||||
display: flex; justify-content: space-between; gap: 1rem; flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#audio-gate {
|
||||
position: fixed; inset: 0; background: rgba(13,15,20,0.85);
|
||||
backdrop-filter: blur(6px); display: flex; align-items: center;
|
||||
justify-content: center; z-index: 100; cursor: pointer;
|
||||
}
|
||||
#audio-gate.hidden { display: none; }
|
||||
.gate-box {
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
||||
padding: 2.5rem 3rem; text-align: center; max-width: 400px;
|
||||
}
|
||||
.gate-box h2 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
.gate-box p { font-size: 0.85rem; color: var(--text-dim); margin-bottom: 1.5rem; }
|
||||
.gate-btn {
|
||||
font-family: var(--mono); font-size: 0.8rem; font-weight: 700;
|
||||
letter-spacing: 0.1em; text-transform: uppercase;
|
||||
background: var(--status-color); color: var(--bg); border: none;
|
||||
padding: 0.7rem 2rem; border-radius: 8px; cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="unknown">
|
||||
|
||||
<div id="audio-gate">
|
||||
<div class="gate-box">
|
||||
<h2>flOMNI Status Page</h2>
|
||||
<p>Click to enable the page. Audio warnings require a user interaction to activate.</p>
|
||||
<button class="gate-btn" onclick="unlockAudio()">Open Status Page</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
|
||||
<header>
|
||||
<div class="logo">fl<span>OMNY</span> · STATUS</div>
|
||||
<div id="last-update">updating…</div>
|
||||
</header>
|
||||
|
||||
<div class="status-card">
|
||||
<div class="status-pill" id="status-pill">-</div>
|
||||
<div class="status-detail" id="status-detail">Loading…</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-card">
|
||||
<div class="rings-wrap">
|
||||
<svg viewBox="0 0 120 120">
|
||||
<circle class="ring-track" cx="60" cy="60" r="50" stroke-width="8"/>
|
||||
<circle class="ring-outer" id="ring-outer"
|
||||
cx="60" cy="60" r="50" stroke-width="8"
|
||||
stroke-dasharray="314.16" stroke-dashoffset="314.16"/>
|
||||
<circle class="ring-track" cx="60" cy="60" r="37" stroke-width="7"/>
|
||||
<circle class="ring-inner" id="ring-inner"
|
||||
cx="60" cy="60" r="37" stroke-width="7"
|
||||
stroke-dasharray="232.48" stroke-dashoffset="232.48"/>
|
||||
</svg>
|
||||
<div class="ring-label">
|
||||
<span class="pct" id="ring-pct">0%</span>
|
||||
<span class="sublbl">OVERALL</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-info">
|
||||
<div class="info-item">
|
||||
<span class="label">Projection</span>
|
||||
<span class="value" id="pi-proj">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Sub-tomo</span>
|
||||
<span class="value" id="pi-subtomo">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Angle</span>
|
||||
<span class="value" id="pi-angle">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Tomo type</span>
|
||||
<span class="value" id="pi-type">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">ETA</span>
|
||||
<span class="value" id="pi-eta">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Started</span>
|
||||
<span class="value" id="pi-start">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bar-wrap">
|
||||
<div class="bar-label">
|
||||
<span>Sub-tomo progress</span>
|
||||
<span id="bar-sub-label">-</span>
|
||||
</div>
|
||||
<div class="bar-track">
|
||||
<div class="bar-fill" id="bar-sub-fill" style="width:0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="audio-card">
|
||||
<div class="audio-info">
|
||||
<div class="audio-dot" id="audio-dot"></div>
|
||||
<span class="audio-text" id="audio-text">Audio warnings: initialising…</span>
|
||||
</div>
|
||||
<div class="audio-controls">
|
||||
<button id="btn-toggle-audio" onclick="toggleAudio()">Enable</button>
|
||||
<button onclick="testSound()">Test sound</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<span id="footer-gen">generator: -</span>
|
||||
<span id="footer-queue">queue active: -</span>
|
||||
<span id="footer-hb">heartbeat: -</span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const STATUS_JSON = 'status.json';
|
||||
const POLL_INTERVAL = 15000;
|
||||
const WARN_STATUSES = new Set(['idle_long', 'error', 'unknown']);
|
||||
|
||||
// Audio
|
||||
let audioCtx = null;
|
||||
let audioEnabled = (localStorage.getItem('audioEnabled') !== 'false');
|
||||
let warningTimer = null;
|
||||
|
||||
function getCtx() {
|
||||
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
return audioCtx;
|
||||
}
|
||||
|
||||
function beep(freq, dur, vol) {
|
||||
try {
|
||||
const ctx = getCtx(), osc = ctx.createOscillator(), gain = ctx.createGain();
|
||||
osc.connect(gain); gain.connect(ctx.destination);
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, ctx.currentTime);
|
||||
gain.gain.setValueAtTime(vol, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + dur);
|
||||
osc.start(ctx.currentTime); osc.stop(ctx.currentTime + dur);
|
||||
} catch(e) { console.warn('Audio:', e); }
|
||||
}
|
||||
|
||||
function warningChime() { beep(660, 0.3, 0.4); setTimeout(() => beep(440, 0.4, 0.4), 350); }
|
||||
function testSound() { beep(880,0.15,0.4); setTimeout(()=>beep(1100,0.15,0.4),180); setTimeout(()=>beep(880,0.3,0.4),360); }
|
||||
|
||||
function toggleAudio() {
|
||||
audioEnabled = !audioEnabled;
|
||||
localStorage.setItem('audioEnabled', audioEnabled);
|
||||
updateAudioUI();
|
||||
if (!audioEnabled) stopWarning();
|
||||
}
|
||||
|
||||
function updateAudioUI() {
|
||||
const btn = document.getElementById('btn-toggle-audio');
|
||||
const dot = document.getElementById('audio-dot');
|
||||
const txt = document.getElementById('audio-text');
|
||||
if (audioEnabled) {
|
||||
btn.textContent = 'Disable'; btn.classList.add('active');
|
||||
dot.classList.add('active'); txt.textContent = 'Audio warnings: enabled';
|
||||
} else {
|
||||
btn.textContent = 'Enable'; btn.classList.remove('active');
|
||||
dot.classList.remove('active'); txt.textContent = 'Audio warnings: disabled';
|
||||
}
|
||||
}
|
||||
|
||||
function startWarning() {
|
||||
if (warningTimer) return;
|
||||
if (audioEnabled) warningChime();
|
||||
warningTimer = setInterval(() => { if (audioEnabled) warningChime(); }, 30000);
|
||||
}
|
||||
function stopWarning() { if (warningTimer) { clearInterval(warningTimer); warningTimer = null; } }
|
||||
|
||||
function unlockAudio() {
|
||||
getCtx().resume();
|
||||
document.getElementById('audio-gate').classList.add('hidden');
|
||||
updateAudioUI();
|
||||
poll();
|
||||
}
|
||||
|
||||
// Rendering
|
||||
const LABELS = {
|
||||
scanning: 'SCANNING',
|
||||
running: 'RUNNING',
|
||||
idle_short: 'IDLE',
|
||||
idle_long: 'IDLE - CHECK',
|
||||
error: 'STOPPED',
|
||||
unknown: 'UNKNOWN',
|
||||
};
|
||||
const DETAILS = {
|
||||
scanning: d => 'Tomo scan in progress · projection ' + d.progress.projection + ' of ' + d.progress.total_projections + ' · ' + d.progress.tomo_type,
|
||||
running: d => 'Queue active · outside tomo heartbeat window (alignment or inter-scan gap)',
|
||||
idle_short: d => 'Finished normally · idle for <strong>' + d.idle_for_human + '</strong>',
|
||||
idle_long: d => 'Idle for <strong>' + d.idle_for_human + '</strong> — no tomo scan running',
|
||||
error: d => 'Queue stopped unexpectedly · idle for <strong>' + (d.idle_for_human || '?') + '</strong>',
|
||||
unknown: d => 'Status unknown · waiting for first data…',
|
||||
};
|
||||
|
||||
function setRing(id, circ, pct) {
|
||||
document.getElementById(id).style.strokeDashoffset = circ * (1 - Math.min(pct, 1));
|
||||
}
|
||||
function fmtAngle(v) { const n = parseFloat(v); return isNaN(n) ? '-' : n.toFixed(2) + '\u00b0'; }
|
||||
function fmtTime(iso) {
|
||||
if (!iso) return '-';
|
||||
try { return new Date(iso).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}); }
|
||||
catch { return iso; }
|
||||
}
|
||||
|
||||
function render(d) {
|
||||
const s = d.experiment_status || 'unknown';
|
||||
const p = d.progress || {};
|
||||
|
||||
document.body.className = s;
|
||||
document.getElementById('status-pill').textContent = LABELS[s] || s.toUpperCase();
|
||||
document.getElementById('status-detail').innerHTML = (DETAILS[s] || (()=>s))(d);
|
||||
|
||||
const oPct = p.total_projections > 0 ? p.projection / p.total_projections : 0;
|
||||
const sPct = p.subtomo_total_projections > 0 ? p.subtomo_projection / p.subtomo_total_projections : 0;
|
||||
setRing('ring-outer', 314.16, oPct);
|
||||
setRing('ring-inner', 232.48, sPct);
|
||||
document.getElementById('ring-pct').textContent = Math.round(oPct * 100) + '%';
|
||||
|
||||
document.getElementById('pi-proj').textContent = (p.projection || 0) + ' / ' + (p.total_projections || 0);
|
||||
document.getElementById('pi-subtomo').textContent = p.subtomo || '-';
|
||||
document.getElementById('pi-angle').textContent = fmtAngle(p.angle);
|
||||
document.getElementById('pi-type').textContent = p.tomo_type || '-';
|
||||
document.getElementById('pi-eta').textContent = p.estimated_remaining_human || '-';
|
||||
document.getElementById('pi-start').textContent = fmtTime(p.tomo_start_time);
|
||||
|
||||
document.getElementById('bar-sub-label').textContent = (p.subtomo_projection || 0) + ' / ' + (p.subtomo_total_projections || 0);
|
||||
document.getElementById('bar-sub-fill').style.width = (sPct * 100).toFixed(1) + '%';
|
||||
|
||||
document.getElementById('last-update').textContent = 'updated ' + new Date(d.generated_at).toLocaleTimeString();
|
||||
document.getElementById('footer-gen').textContent = 'generator: ' + (d.generator && d.generator.owner_id || '-');
|
||||
document.getElementById('footer-queue').textContent = 'queue active: ' + d.queue_has_active_scan;
|
||||
document.getElementById('footer-hb').textContent = 'heartbeat: ' + (p.heartbeat_age_s != null ? p.heartbeat_age_s + 's ago' : 'none');
|
||||
|
||||
WARN_STATUSES.has(s) ? startWarning() : stopWarning();
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
try {
|
||||
const r = await fetch(STATUS_JSON + '?t=' + Date.now());
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
render(await r.json());
|
||||
} catch(e) {
|
||||
console.warn('Fetch failed:', e);
|
||||
document.getElementById('last-update').textContent = 'fetch failed - retrying...';
|
||||
}
|
||||
}
|
||||
|
||||
updateAudioUI();
|
||||
setInterval(poll, POLL_INTERVAL);
|
||||
// First poll triggered by unlockAudio() after the gate is dismissed
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 359 KiB |
@@ -395,15 +395,15 @@ 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
|
||||
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 ############################
|
||||
|
||||
24
csaxs_bec/device_configs/test_config.yaml
Normal file
24
csaxs_bec/device_configs/test_config.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
galilrioesxbox:
|
||||
description: Galil RIO for remote gain switching and slow reading ES XBox
|
||||
deviceClass: csaxs_bec.devices.omny.galil.galil_rio.GalilRIO
|
||||
deviceConfig:
|
||||
host: galilrioesft.psi.ch
|
||||
enabled: true
|
||||
onFailure: raise
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
connectionTimeout: 20
|
||||
bpm1:
|
||||
readoutPriority: baseline
|
||||
deviceClass: csaxs_bec.devices.pseudo_devices.bpm.BPM
|
||||
deviceConfig:
|
||||
blade_t: galilrioesxbox.analog_in.ch0
|
||||
blade_r: galilrioesxbox.analog_in.ch1
|
||||
blade_b: galilrioesxbox.analog_in.ch2
|
||||
blade_l: galilrioesxbox.analog_in.ch3
|
||||
enabled: true
|
||||
readOnly: false
|
||||
softwareTrigger: true
|
||||
needs:
|
||||
- galilrioesxbox
|
||||
|
||||
@@ -48,7 +48,6 @@ class OMNYFastShutter(PSIDeviceBase, Device):
|
||||
def fshopen(self):
|
||||
"""Open the fast shutter."""
|
||||
if self._check_if_cSAXS_shutter_exists_in_config():
|
||||
self.shutter.put(1)
|
||||
return self.device_manager.devices["fsh"].fshopen()
|
||||
else:
|
||||
self.shutter.put(1)
|
||||
@@ -56,7 +55,6 @@ class OMNYFastShutter(PSIDeviceBase, Device):
|
||||
def fshclose(self):
|
||||
"""Close the fast shutter."""
|
||||
if self._check_if_cSAXS_shutter_exists_in_config():
|
||||
self.shutter.put(0)
|
||||
return self.device_manager.devices["fsh"].fshclose()
|
||||
else:
|
||||
self.shutter.put(0)
|
||||
|
||||
0
csaxs_bec/devices/pseudo_devices/__init__.py
Normal file
0
csaxs_bec/devices/pseudo_devices/__init__.py
Normal file
117
csaxs_bec/devices/pseudo_devices/bpm.py
Normal file
117
csaxs_bec/devices/pseudo_devices/bpm.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import time
|
||||
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Kind, Signal
|
||||
from ophyd_devices.interfaces.base_classes.psi_pseudo_device_base import PSIPseudoDeviceBase
|
||||
from ophyd_devices.utils.bec_processed_signal import BECProcessedSignal
|
||||
|
||||
|
||||
class BPM(PSIPseudoDeviceBase):
|
||||
"""BPM positioner pseudo device."""
|
||||
|
||||
# Blade signals, a,b,c,d
|
||||
top = Cpt(
|
||||
BECProcessedSignal, name="top", model_config=None, kind=Kind.config, doc="... top blade"
|
||||
)
|
||||
right = Cpt(
|
||||
BECProcessedSignal, name="right", model_config=None, kind=Kind.config, doc="... right blade"
|
||||
)
|
||||
bot = Cpt(
|
||||
BECProcessedSignal, name="bot", model_config=None, kind=Kind.config, doc="... bot blade"
|
||||
)
|
||||
left = Cpt(
|
||||
BECProcessedSignal, name="left", model_config=None, kind=Kind.config, doc="... left blade"
|
||||
)
|
||||
|
||||
# Virtual signals
|
||||
pos_x = Cpt(
|
||||
BECProcessedSignal, name="pos_x", model_config=None, kind=Kind.config, doc="... pos_x"
|
||||
)
|
||||
pos_y = Cpt(
|
||||
BECProcessedSignal, name="pos_y", model_config=None, kind=Kind.config, doc="... pos_y"
|
||||
)
|
||||
diagonal = Cpt(
|
||||
BECProcessedSignal, name="diagonal", model_config=None, kind=Kind.config, doc="... diagonal"
|
||||
)
|
||||
intensity = Cpt(
|
||||
BECProcessedSignal,
|
||||
name="intensity",
|
||||
model_config=None,
|
||||
kind=Kind.config,
|
||||
doc="... intensity",
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
blade_t: str,
|
||||
blade_r: str,
|
||||
blade_b: str,
|
||||
blade_l: str,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(name=name, device_manager=device_manager, **kwargs)
|
||||
# Get all blade signal objects from utility method
|
||||
signal_t = self.top.get_device_object_from_bec(
|
||||
object_name=blade_t, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
signal_r = self.right.get_device_object_from_bec(
|
||||
object_name=blade_r, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
signal_b = self.bot.get_device_object_from_bec(
|
||||
object_name=blade_b, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
signal_l = self.left.get_device_object_from_bec(
|
||||
object_name=blade_l, signal_name=self.name, device_manager=device_manager
|
||||
)
|
||||
|
||||
# Set compute methods for blade signals and virtual signals
|
||||
self.top.set_compute_method(self._compute_blade_signal, signal=signal_t)
|
||||
self.right.set_compute_method(self._compute_blade_signal, signal=signal_r)
|
||||
self.bot.set_compute_method(self._compute_blade_signal, signal=signal_b)
|
||||
self.left.set_compute_method(self._compute_blade_signal, signal=signal_l)
|
||||
|
||||
self.intensity.set_compute_method(
|
||||
self._compute_intensity, top=self.top, right=self.right, bot=self.bot, left=self.left
|
||||
)
|
||||
self.pos_x.set_compute_method(
|
||||
self._compute_pos_x, left=self.left, top=self.top, right=self.right, bot=self.bot
|
||||
)
|
||||
self.pos_y.set_compute_method(
|
||||
self._compute_pos_y, left=self.left, top=self.top, right=self.right, bot=self.bot
|
||||
)
|
||||
self.diagonal.set_compute_method(
|
||||
self._compute_diagonal, left=self.left, top=self.top, right=self.right, bot=self.bot
|
||||
)
|
||||
|
||||
def _compute_blade_signal(self, signal: Signal) -> float:
|
||||
return signal.get()
|
||||
|
||||
def _compute_intensity(self, top: Signal, right: Signal, bot: Signal, left: Signal) -> float:
|
||||
intensity = top.get() + right.get() + bot.get() + left.get()
|
||||
return intensity
|
||||
|
||||
def _compute_pos_x(self, left: Signal, top: Signal, right: Signal, bot: Signal) -> float:
|
||||
sum_left = left.get() + top.get()
|
||||
sum_right = right.get() + bot.get()
|
||||
sum_total = sum_left + sum_right
|
||||
if sum_total == 0:
|
||||
return 0.0
|
||||
return (sum_left - sum_right) / sum_total
|
||||
|
||||
def _compute_pos_y(self, left: Signal, top: Signal, right: Signal, bot: Signal) -> float:
|
||||
sum_top = top.get() + right.get()
|
||||
sum_bot = bot.get() + left.get()
|
||||
sum_total = sum_top + sum_bot
|
||||
if sum_total == 0:
|
||||
return 0.0
|
||||
return (sum_top - sum_bot) / sum_total
|
||||
|
||||
def _compute_diagonal(self, left: Signal, top: Signal, right: Signal, bot: Signal) -> float:
|
||||
sum_diag1 = left.get() + right.get()
|
||||
sum_diag2 = top.get() + bot.get()
|
||||
sum_total = sum_diag1 + sum_diag2
|
||||
if sum_total == 0:
|
||||
return 0.0
|
||||
return (sum_diag1 - sum_diag2) / sum_total
|
||||
1
csaxs_bec/devices/pseudo_devices/dlpca200_settings.py
Normal file
1
csaxs_bec/devices/pseudo_devices/dlpca200_settings.py
Normal file
@@ -0,0 +1 @@
|
||||
# from ophyd
|
||||
Reference in New Issue
Block a user