From af00c64c494ed6ca8477bf2e16eb2158fafcce4a Mon Sep 17 00:00:00 2001 From: x12sa Date: Sat, 4 Jul 2026 07:50:01 +0200 Subject: [PATCH] feat/webpage_generator: make tomo-queue and tomo-type formula setup-configurable - Add HAS_TOMO_QUEUE and TOMO_TYPES capability flags to WebpageGeneratorBase, defaulting to flomni's current behavior (type1 grid-8, type2/3 golden-capped) so this is a no-op for flomni. - Gate the tomo_queue global-var read in _cycle() behind HAS_TOMO_QUEUE; unaffected: queue_status/queue_locks/beamline_states, which come from BEC's own primary scan queue and stay common to all setups. - _render_html() now conditionally emits the 'Tomo queue' card and its DEFAULT_ORDER slot, and drives the projection-count formula from a TOMO_TYPES JSON blob (generic calcProjections()) instead of a hardcoded per-instrument JS branch. - LamniWebpageGenerator: HAS_TOMO_QUEUE=False, TOMO_TYPES with 2-subtomo and 8-subtomo equally_spaced_grid entries (placeholder keys pending real tomo_type values from the LamNI producer side). --- .../plugins/LamNI/LamNI_webpage_generator.py | 18 ++- .../flomni/flomni_webpage_generator.py | 122 +++++++++++++----- 2 files changed, 107 insertions(+), 33 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_webpage_generator.py b/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_webpage_generator.py index a77e77c..a5a1708 100644 --- a/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_webpage_generator.py +++ b/csaxs_bec/bec_ipython_client/plugins/LamNI/LamNI_webpage_generator.py @@ -49,6 +49,22 @@ class LamniWebpageGenerator(WebpageGeneratorBase): sample name, and measurement settings. """ + # LamNI has no persisted parameter-snapshot job queue (unlike flomni + # and, later, omny) — the "Tomo queue" card and its global-var read + # are skipped entirely for this setup. + HAS_TOMO_QUEUE = False + + # LamNI's tomo scans come in two flavours: a 2-subtomo and an + # 8-subtomo variant, both using the same equally-spaced-grid formula + # as flomni's type 1 (just with a different sub-tomo count). + # TODO: replace the placeholder keys (1, 2) with whatever values + # progress["tomo_type"] actually takes for LamNI once the producer + # side (LamNI equivalent of flomni.py) defines them. + TOMO_TYPES = { + 1: {"kind": "equally_spaced_grid", "n_subtomos": 2}, + 2: {"kind": "equally_spaced_grid", "n_subtomos": 8}, + } + # TODO: fill in LamNI-specific device paths # label -> dotpath under device_manager.devices _TEMP_MAP = { @@ -86,4 +102,4 @@ class LamniWebpageGenerator(WebpageGeneratorBase): return { "type": "lamni", # LamNI-specific data here - } + } \ No newline at end of file diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_webpage_generator.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_webpage_generator.py index 6825d73..103acaf 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_webpage_generator.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_webpage_generator.py @@ -574,8 +574,32 @@ class WebpageGeneratorBase: Common webpage generator. Subclass and override: _collect_setup_data() -- return dict of instrument-specific data _logo_path() -- return Path to logo PNG, or None for text fallback + + Setup capability flags (override on subclass as needed): + HAS_TOMO_QUEUE -- whether this instrument exposes the persisted + parameter-snapshot job queue (global var + "tomo_queue" + the "Tomo queue" UI card). + NOT to be confused with BEC's own primary scan + queue (queue_status / queue_locks in _cycle()), + which every setup has and which stays common. + TOMO_TYPES -- dict describing this instrument's tomography + scan types, keyed by the value progress["tomo_type"] + takes. Used by the UI to compute total-projection + counts for queued jobs without hardcoding a + per-instrument formula in JS. Recognised "kind"s: + "equally_spaced_grid" (params: n_subtomos) + total = floor(angle_range / angle_stepsize) * n_subtomos + "golden_capped" + total = golden_max_number_of_projections """ + HAS_TOMO_QUEUE = True + TOMO_TYPES = { + 1: {"kind": "equally_spaced_grid", "n_subtomos": 8}, + 2: {"kind": "golden_capped"}, + 3: {"kind": "golden_capped"}, + } + def __init__( self, bec_client, @@ -647,7 +671,9 @@ class WebpageGeneratorBase: # Copy logo once at startup — HTML is also written once here, # not every cycle, since it is a static shell that only loads status.json. self._copy_logo() - (self._output_dir / "status.html").write_text(_render_html(_PHONE_NUMBERS)) + (self._output_dir / "status.html").write_text( + _render_html(_PHONE_NUMBERS, self.HAS_TOMO_QUEUE, self.TOMO_TYPES) + ) # Check whether the active BEC account matches the session user on the # remote server. If not, clear session.htpasswd so the old user loses @@ -1028,11 +1054,16 @@ class WebpageGeneratorBase: else max(0.0, _epoch() - self._last_active_time) ) - # ── Tomo queue ─────────────────────────────────────────────── + # ── Tomo queue (setup-specific; see HAS_TOMO_QUEUE) ───────────── # Persisted list of parameter-snapshot jobs (global var "tomo_queue"). # Each job: {"label": str, "params": {...}, # "status": pending|running|incomplete|done, "added_at": ISO str} - tomo_queue = self._bec.get_global_var("tomo_queue") or [] + # Instruments without this feature (e.g. LamNI) never touch the + # global var and always report an empty queue. + if self.HAS_TOMO_QUEUE: + tomo_queue = self._bec.get_global_var("tomo_queue") or [] + else: + tomo_queue = [] # ── Reconstruction queue ────────────────────────────────────── recon = self._collect_recon_data() @@ -1453,7 +1484,7 @@ def make_webpage_generator(bec_client, **kwargs): # Theme is stored in localStorage and applied via data-theme on . # --------------------------------------------------------------------------- -def _render_html(phone_numbers: list) -> str: +def _render_html(phone_numbers: list, has_tomo_queue: bool = True, tomo_types: dict = None) -> str: phones_html = "\n".join( f'
' f'{label}' @@ -1461,6 +1492,30 @@ def _render_html(phone_numbers: list) -> str: for label, num in phone_numbers ) + # Tomo queue card + its slot in the default card order are only emitted + # for setups that actually have the feature (see HAS_TOMO_QUEUE). + if has_tomo_queue: + tomo_queue_card_html = """ + +
+
⋮⋮
+
Tomo queue
+
+
+""" + else: + tomo_queue_card_html = "" + + default_order = ['audio', 'recon-queue', 'ptycho', 'instrument', 'blstates', 'contacts'] + if has_tomo_queue: + default_order.insert(default_order.index('contacts'), 'tomo-queue') + default_order_json = json.dumps(default_order) + + # Declarative tomo-type -> projection-count formula, consumed by the + # generic calcProjections() in JS instead of a hardcoded per-instrument + # branch. See WebpageGeneratorBase.TOMO_TYPES for the schema. + tomo_types_json = json.dumps(tomo_types or {}) + return f""" @@ -2139,13 +2194,7 @@ def _render_html(phone_numbers: list) -> str:
- -
-
⋮⋮
-
Tomo queue
-
-
- +{tomo_queue_card_html}
⋮⋮
@@ -2184,7 +2233,7 @@ function setTheme(t) {{ // ── Drag-and-drop card ordering ────────────────────────────────────────── const CARD_ORDER_KEY = 'cardOrder'; -const DEFAULT_ORDER = ['audio','recon-queue','ptycho','instrument','blstates','tomo-queue','contacts']; +const DEFAULT_ORDER = {default_order_json}; let _dragSrc = null; function savedOrder() {{ @@ -2642,8 +2691,32 @@ function fmtTqParam(key, val){{ return String(val); }} +// Declarative per-instrument tomo-type definitions (see +// WebpageGeneratorBase.TOMO_TYPES on the Python side for the schema). +// Adding or changing a tomo type on any setup is a config change here, +// not a new branch of formula code. +const TOMO_TYPES = {tomo_types_json}; + +function calcProjections(params){{ + const def=TOMO_TYPES[params.tomo_type]; + if(!def) return null; + if(def.kind==='golden_capped'){{ + const gmax=params.golden_max_number_of_projections; + return (gmax!=null&&gmax>0)?gmax:null; + }} + if(def.kind==='equally_spaced_grid'){{ + const range=params.tomo_angle_range, step=params.tomo_angle_stepsize; + if(range>0&&step>0){{ + const N=Math.floor(range/step); + return N*(def.n_subtomos||1); + }} + }} + return null; +}} + function renderTomoQueue(jobs){{ const el=document.getElementById('tomo-queue-content'); + if(!el) return; // setup has no tomo-queue card (HAS_TOMO_QUEUE=False) // Snapshot which job indices are currently open before replacing innerHTML const openIndices=new Set(); el.querySelectorAll('details.tq-job').forEach((det,i)=>{{ @@ -2665,27 +2738,12 @@ function renderTomoQueue(jobs){{ if(key in params){{ paramRows+=''+dispLabel+''+esc(fmtTqParam(key,params[key]))+''; }} - // After angle step, inject a computed projection count row. - // Formula must match _tomo_type1_actual_grid() / tomo_parameters() exactly: - // type 1: N = int(range / stepsize), corrected_step = range / N, total = N * 8 - // type 2/3: golden_max_number_of_projections directly + // After angle step, inject a computed projection count row, driven by + // this instrument's TOMO_TYPES config (must match the corresponding + // Python-side formula, e.g. _tomo_type1_actual_grid() for flomni's + // "equally_spaced_grid" type). if(key==='tomo_angle_stepsize'){{ - const ttype=params.tomo_type; - let nproj=null; - if(ttype===2||ttype===3){{ - // golden ratio / equally-spaced golden: capped at golden_max_number_of_projections - const gmax=params.golden_max_number_of_projections; - nproj=(gmax!=null&&gmax>0)?gmax:null; - }}else{{ - // type 1: 8 equally-spaced sub-tomograms. - // int(range/stepsize) = N per sub-tomo; total = N * 8 - // (mirrors _tomo_type1_actual_grid: N=int(range/step), total=N*8) - const range=params.tomo_angle_range, step=params.tomo_angle_stepsize; - if(range>0&&step>0){{ - const N=Math.floor(range/step); - nproj=N*8; - }} - }} + const nproj=calcProjections(params); if(nproj!=null) paramRows+='Projections'+nproj+''; }} }});