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).
This commit is contained in:
x12sa
2026-07-07 10:41:40 +02:00
committed by holler
co-authored by holler
parent 2f95f44b15
commit af00c64c49
2 changed files with 107 additions and 33 deletions
@@ -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
}
}
@@ -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 <html>.
# ---------------------------------------------------------------------------
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' <div class="phone-row">'
f'<span class="phone-label">{label}</span>'
@@ -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 = """
<!-- 5. Tomo queue -->
<div class="card draggable-card" data-card-id="tomo-queue">
<div class="drag-handle" title="Drag to reorder">&#8942;&#8942;</div>
<div class="card-title">Tomo queue</div>
<div id="tomo-queue-content"></div>
</div>
"""
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"""<!DOCTYPE html>
<html lang="en" data-theme="auto">
<head>
@@ -2139,13 +2194,7 @@ def _render_html(phone_numbers: list) -> str:
</table>
</div>
<!-- 5. Tomo queue -->
<div class="card draggable-card" data-card-id="tomo-queue">
<div class="drag-handle" title="Drag to reorder">&#8942;&#8942;</div>
<div class="card-title">Tomo queue</div>
<div id="tomo-queue-content"></div>
</div>
{tomo_queue_card_html}
<!-- 6. Contacts -->
<div class="card draggable-card" data-card-id="contacts">
<div class="drag-handle" title="Drag to reorder">&#8942;&#8942;</div>
@@ -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+='<tr><td>'+dispLabel+'</td><td>'+esc(fmtTqParam(key,params[key]))+'</td></tr>';
}}
// 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+='<tr><td>Projections</td><td>'+nproj+'</td></tr>';
}}
}});