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:
@@ -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">⋮⋮</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">⋮⋮</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">⋮⋮</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>';
|
||||
}}
|
||||
}});
|
||||
|
||||
Reference in New Issue
Block a user