docs: migrate to zensical
CI for csaxs_bec / test (pull_request) Successful in 3m6s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 3m3s

This commit was merged in pull request #329.
This commit is contained in:
2026-10-14 09:43:31 +02:00
parent ca96f41a45
commit bf8036b72d
34 changed files with 658 additions and 1518 deletions
+8 -37
View File
@@ -1,45 +1,16 @@
(developer)=
# Developer
Information for developers and maintainers of the cSAXS beamline documentation and software.
```{toctree}
---
maxdepth: 2
hidden: true
---
<div class="grid cards section-grid" markdown>
editing_docs
lamni_smear_architecture
- ![](../assets/index_contribute.svg){ width="32" height="32" .middle .card-icon } **[Editing the documentation](editing_docs.md)**
{ .card-heading }
```
Conventions for writing these Zensical docs and how changes go live on Read the Docs.
- ![](../assets/biotech.svg){ width="32" height="32" .middle .card-icon } **[LamNI smear aid architecture](lamni_smear_architecture.md)**
{ .card-heading }
***
Device/ipython-client/GUI widget interfaces for the smear rotation-center calibration aid, the min/max composite with opaque-pixel masking, and the `bw-generate-cli` regeneration pitfall.
````{grid} 2
:gutter: 5
```{grid-item-card}
:link: developer.editing_docs
:link-type: ref
:img-top: /assets/index_contribute.svg
:text-align: center
:class-item: index-card
## Editing the documentation
Conventions for writing these MyST/Sphinx docs and how changes go live on Read the Docs.
```
```{grid-item-card}
:link: developer.lamni_smear_architecture
:link-type: ref
:text-align: center
:class-item: index-card
## LamNI smear aid architecture
Device/ipython-client/GUI widget interfaces for the smear rotation-center calibration aid, the min/max composite with opaque-pixel masking, and the `bw-generate-cli` regeneration pitfall.
```
````
</div>
+130 -108
View File
@@ -1,143 +1,165 @@
(developer.editing_docs)=
# Editing the documentation
This documentation is built with [Sphinx](https://www.sphinx-doc.org) using the
[MyST](https://myst-parser.readthedocs.io) Markdown parser, and hosted on
[Read the Docs](https://sls-csaxs.readthedocs.io). The source lives in `docs/` of
the `csaxs_bec` repository.
:::{important}
The pages use **MyST Markdown**, which is a superset of CommonMark — *not*
Gitea/GitHub-flavored Markdown. Most plain Markdown works as expected, but the
conventions below are specific to this build and need to be followed for links,
admonitions, and navigation to render correctly.
:::
This site is built with [Zensical](https://zensical.org/) and hosted on
[Read the Docs](https://sls-csaxs.readthedocs.io). Pages live in `docs/`;
`zensical.toml` at the repository root controls navigation and rendering.
## How a change goes live
There are two ways to propose an edit:
- **In the browser.** Open a page in Gitea and click **Edit** to propose a change
through a branch and pull request (or a personal fork if required).
- **In the browser (easiest, no git required).** Open the page in Gitea and click
**Edit**. Gitea commits your change to a new branch and opens a pull request for you;
if you do not have write access it first creates a personal fork automatically.
- **Locally.** Create a branch, edit the `.md` files under `docs/`, push, and open a
pull request.
- **Locally.** Create a branch, edit the Markdown pages, push, and open a pull request.
A documentation maintainer then reviews and merges the pull request into `main`.
Merging is restricted to a few people by design — opening the pull request is all that
is expected of an author.
A documentation maintainer reviews and merges the pull request into `main`.
The `.gitea/workflows/rtd_deploy.yml` action then triggers a Read the Docs build
of `latest`. The `.readthedocs.yaml` configuration installs the documentation
requirements, builds the site, and copies it to
Read the Docs' HTML output directory.
Once merged, the `.gitea/workflows/rtd_deploy.yml` action fires on the push to `main`
and triggers a Read the Docs build of the `latest` version; the site updates a minute
or two later. Nothing needs to be built or uploaded by hand.
## Preview locally
On Linux or macOS, run these commands from the repository root:
```bash
python -m venv .venv-docs
source .venv-docs/bin/activate
python -m pip install -e '.[docs]'
zensical serve
```
Open the local URL printed by Zensical. The preview reloads automatically after
edits; stop it with ++ctrl+c++.
To generate the static site:
```bash
zensical build --clean
```
Output is written to `site/`.
The `docs` extra in `pyproject.toml` installs Zensical and PyMdown Extensions
alongside the repository's regular dependencies. Read the Docs uses the same editable installation command.
Do not commit the generated `site/` directory. The API reference is currently
omitted.
## Anatomy of a page
Every content page starts with a **cross-reference label** followed by a single
top-level heading:
Start with one H1 heading:
```md
(user.saxs.online_integration)=
```markdown
# Setup online radial integration on Jungfraujoch
```
- The label `(user.saxs.online_integration)=` is what other pages, the section landing
pages, and the `{ref}` role link to. **Do not delete or rename it** without
updating every reference, or links silently break.
- The `# Title` is the page's H1. Sphinx needs exactly one; section pages do not
inherit a title from the file name the way the old wiki did.
Headings receive native Zensical anchors automatically. Use these anchors for
section links, or link directly to the Markdown file when referring to a whole
page. Copy a heading's permalink in the preview to get its anchor.
For an explicit, stable anchor, attach an ID directly to the heading:
```markdown
## Online integration { #online-integration }
```
Reference it with `[Online integration](page.md#online-integration)`. Use heading
anchors rather than separate HTML `<a>` tags.
## Conventions
### Code blocks — always tag the language
### Lists and tables
Use a language on every fenced block so syntax highlighting and the copy button work:
Leave a blank line before a list or table that follows a paragraph. Indent
paragraphs, nested lists, callouts, and code blocks inside a list item by four
spaces. These rules keep procedures numbered continuously and prevent tables
and list markers from appearing as plain text.
````md
### Code blocks
Tag fenced code blocks with `python`, `bash`, or `text` so highlighting and
copy buttons work:
````markdown
```bash
module load Python/3.11.11
pip install "pyFAI[gui]"
```
````
Use ```` ```text ```` for plain output (e.g. error messages) and ```` ```python ````
for Python.
### Callouts
### Call-outs — use admonitions, not bold "Note:"
Use an admonition with its contents indented by four spaces:
Instead of `**Note:** …`, use a [colon-fence admonition](https://myst-parser.readthedocs.io/en/latest/syntax/admonitions.html):
```md
:::{note}
Jungfraujoch currently only accepts `uint32` TIFF mask files.
:::
```markdown
!!! note
Jungfraujoch currently only accepts `uint32` TIFF mask files.
```
Available types include `note`, `tip`, `important`, `warning`, `caution`, `danger`.
Use `note`, `tip`, `warning`, or `danger`; a custom title follows the type:
### Links and cross-references
- **To another page in this documentation**, link by its label, not by file path:
```md
See {ref}`the Jungfraujoch setup guide <user.saxs.online_integration>`.
```
- **External links** use ordinary Markdown: `[pyFAI](https://pyfai.readthedocs.io)`.
- **Downloadable files** (PDFs, etc.) must use the `{download}` role — a plain link
to a non-image file is *not* copied into the build and will 404:
```md
- {download}`SAXS standards plate (PDF) <SAXS_standards_plate.pdf>`
```
- **Images** use the `{figure}` directive (or standard `![alt](file.png)`); place the
image file next to the page that uses it:
````md
```{figure} omny_shuttle.png
The OMNY sample shuttle.
```
````
### Adding a new page
1. Create `docs/<section>/<name>.md` with a label and H1 as above.
2. Add it to the parent section's `{toctree}` so it appears in the navigation, e.g.
in `docs/user/saxs/saxs.md`:
````md
```{toctree}
---
maxdepth: 2
hidden: true
---
online_integration
<name>
```
````
List entries by file name **without** the `.md` extension.
## Preview locally
```bash
cd docs
pip install -r requirements.txt # plus `pip install .` from the repo root for the API reference
make html
```markdown
!!! danger "In an emergency"
Call 3333.
```
Open `docs/_build/html/index.html` in a browser.
### Keyboard shortcuts and tabs
## Pitfalls
Use `++ctrl+c++` for ++ctrl+c++ and `++tab++` for ++tab++. Use content tabs for
alternative workflows, such as previewing or building the site:
- **Broken cross-references are warnings, not errors.** The build still succeeds and
publishes, but the link won't work. Check the Read the Docs build log (or the local
`make html` output) for `WARNING` lines after editing.
- **Everything here is public.** The site is world-readable. Avoid credentials, license
keys, personal logins, and anything not meant for a general audience. Internal
hostnames and `/sls/...` paths are fine if you intend them to be public.
- **Placeholders.** Bracketed values such as `[p-group]` or `[ra-c-110]` are meant to be
substituted by the reader; keep them consistent across a page.
````markdown
=== "Preview"
```bash
zensical serve
```
=== "Build"
```bash
zensical build --clean
```
````
Keep required procedure steps and warnings outside tabs so readers see them
regardless of which alternative they select.
### Links, downloads, and images
Link to Markdown source paths relative to the current page. Zensical rewrites them
to the generated HTML URLs. For example, from this page:
```markdown
[Online integration](../user/saxs/online_integration.md)
[Angle files](../user/ptychography/flomni.md#angles-from-a-file-tomo_type-4)
[Safety checklist (PDF)](../assets/safety-at-csaxs.pdf)
```
PDFs and other assets under `docs/` are copied automatically. Place images next to
the page that uses them and use ordinary Markdown:
```markdown
![The OMNY sample shuttle](omny_shuttle.png)
```
Use `[label](https://example.org)` for external links. Put placeholder values such
as `<position>` inside backticks so they are displayed as text.
### Adding a page
1. Create `docs/<section>/<name>.md` with an H1.
2. Add the page to `project.nav` in `zensical.toml`.
3. Add a link from the relevant section overview where useful.
4. Build and check the resulting page and its links.
The `docs/plans/` directory contains working implementation plans and is excluded
from the published site. The P-touch implementation plan is also excluded from publication. Navigation entries alone do not exclude other pages from publication.
## Before opening a pull request
- [ ] Check the build output.
- [ ] Click through changed links; a successful build alone does not prove every external link works.
- [ ] Confirm that callouts, images, tables, and downloads render correctly.
The documentation is public. Keep credentials, license keys, and private personal
information out of pages and downloadable files.
+74 -68
View File
@@ -1,9 +1,7 @@
(developer.lamni_smear_architecture)=
# LamNI smear rotation-center aid: architecture notes
The experimental continuous-rotation "smear" calibration aid (see
{ref}`the user-facing description <user.ptychography.lamni>`) touches all
[the user-facing description](../user/ptychography/lamni.md)) touches all
three layers of a typical BEC GUI feature — a **device**, an **ipython-client
plugin**, and a **GUI widget** — running as three separate processes talking
over BEC's Redis-backed message bus. It's a useful worked example for how
@@ -27,15 +25,17 @@ ipython client (XrayEyeAlign, LamNI)
```
- **`IDSCamera`** (`csaxs_bec/devices/ids_cameras/ids_camera.py`, simulated
counterpart `csaxs_bec/devices/sim/sim_cameras.py`) runs in the device
server process and owns the actual camera hardware (or its simulation).
counterpart `csaxs_bec/devices/sim/sim_cameras.py`) runs in the device
server process and owns the actual camera hardware (or its simulation).
- **`XrayEyeAlign`** / **`LamNI`** (`csaxs_bec/bec_ipython_client/plugins/LamNI/`)
run in the operator's ipython client process and orchestrate the
calibration procedure — rotating the sample, grabbing frames, driving the
GUI.
run in the operator's ipython client process and orchestrate the
calibration procedure — rotating the sample, grabbing frames, driving the
GUI.
- **`XRayEye`** (`csaxs_bec/bec_widgets/widgets/xray_eye/x_ray_eye.py`) is a
Qt widget running in its own GUI server process, displaying the live image
and collecting the operator's click.
Qt widget running in its own GUI server process, displaying the live image
and collecting the operator's click.
Devices and widgets each expose a `USER_ACCESS` list of methods callable
remotely; the ipython client reaches them as `dev.cam_xeye.<method>(...)` and
@@ -48,13 +48,14 @@ calls.
`IDSCamera` exposes two `PreviewSignal` components:
- **`image`** — the live-acquisition channel. A background thread
(`_live_mode_loop`) continuously grabs frames and `put()`s them here at
~5 Hz while `live_mode_enabled` is `True`. Configured per-device with
`num_rotation_90`/`transpose` (e.g. `num_rotation_90=3` for `cam_xeye`),
applied automatically inside `PreviewSignal.put()`.
(`_live_mode_loop`) continuously grabs frames and `put()`s them here at
~5 Hz while `live_mode_enabled` is `True`. Configured per-device with
`num_rotation_90`/`transpose` (e.g. `num_rotation_90=3` for `cam_xeye`),
applied automatically inside `PreviewSignal.put()`.
- **`smear_preview`** — a second, independent channel, added for this
feature, with **no** rotation/transpose configured. Nothing else writes to
it, so publishing here never races with the live thread.
feature, with **no** rotation/transpose configured. Nothing else writes to
it, so publishing here never races with the live thread.
`PreviewSignal` (and every `BECMessageSignal` subclass) has `rpc_access`
hardcoded off, so it is **not** reachable as a plain attribute from client
@@ -64,16 +65,18 @@ method that does the `.put()` server-side, where the signal *is* a normal
ophyd attribute:
- `get_last_image()` — read side; returns `self.image.get().data`, i.e.
already display-oriented (rotation/transpose already applied by whichever
`.put()` call stored it).
already display-oriented (rotation/transpose already applied by whichever
`.put()` call stored it).
- `push_preview_image(data)` — write side for `image`. Since data built from
`get_last_image()` frames has already been transformed once, this method
undoes the transform first so `self.image.put()`'s own transform doesn't
apply it a second time (compensation is reverse-order: transpose first,
then rotate by `-num_rotation_90`).
`get_last_image()` frames has already been transformed once, this method
undoes the transform first so `self.image.put()`'s own transform doesn't
apply it a second time (compensation is reverse-order: transpose first,
then rotate by `-num_rotation_90`).
- `push_smear_preview(data)` — write side for `smear_preview`. No
compensation needed or applied, since that channel has no transform
configured.
compensation needed or applied, since that channel has no transform
configured.
**Why two channels, not one:** the first working version pushed composites
through `push_preview_image` (the `image` channel), which meant briefly
@@ -118,58 +121,61 @@ lands after the sweep but before the click completes.
stepping the motor:
1. `scans.mv(dev.lsamrot, target)` (the file's `mv()` wrapper, sibling of the
existing blocking `umv()`) issues the rotation **non-blocking** — it
returns a `ScanReport` immediately rather than waiting for the move to
finish.
existing blocking `umv()`) issues the rotation **non-blocking** — it
returns a `ScanReport` immediately rather than waiting for the move to
finish.
2. A plain loop polls `report.status` (exactly what `ScanReport.wait()` does
internally) and, while it isn't `"COMPLETED"`, grabs a frame via
`get_last_image()` and folds it into a running composite —
all single-threaded, since the rotation is progressing concurrently on
the server regardless of what the client does between polls. The
composite is `np.minimum` for `projection="min"` (default; absorption
contrast, dark features on a bright background) or `np.maximum` for
`projection="max"` (bright features on a dark background). A
max-projection is useless for absorption: the bright background passes
every pixel at some point during the rotation and washes the dark
features out. Taking the minimum is equivalent to inverting the frames,
taking the maximum and inverting back, without needing the camera's
full-scale value. The accumulation lives in `SmearComposite`
(`x_ray_eye_align.py`). For `"min"` it also skips opaque pixels: in each
frame, pixels darker than `opaque_level` (default 0.25) times the frame's
99th-percentile value count as opaque and are left out, so the sample
holder rotating through the view does not black out every pixel it ever
covers. It keeps the plain minimum alongside the masked one and falls
back to it for pixels that were opaque in every frame; `opaque_level=0`
is the plain minimum. Both values are validated up front (`ValueError`
before any motion) and stored as `projection`/`opaque_level` attributes
in the calibration HDF5.
internally) and, while it isn't `"COMPLETED"`, grabs a frame via
`get_last_image()` and folds it into a running composite —
all single-threaded, since the rotation is progressing concurrently on
the server regardless of what the client does between polls. The
composite is `np.minimum` for `projection="min"` (default; absorption
contrast, dark features on a bright background) or `np.maximum` for
`projection="max"` (bright features on a dark background). A
max-projection is useless for absorption: the bright background passes
every pixel at some point during the rotation and washes the dark
features out. Taking the minimum is equivalent to inverting the frames,
taking the maximum and inverting back, without needing the camera's
full-scale value. The accumulation lives in `SmearComposite`
(`x_ray_eye_align.py`). For `"min"` it also skips opaque pixels: in each
frame, pixels darker than `opaque_level` (default 0.25) times the frame's
99th-percentile value count as opaque and are left out, so the sample
holder rotating through the view does not black out every pixel it ever
covers. It keeps the plain minimum alongside the masked one and falls
back to it for pixels that were opaque in every frame; `opaque_level=0`
is the plain minimum. Both values are validated up front (`ValueError`
before any motion) and stored as `projection`/`opaque_level` attributes
in the calibration HDF5.
3. Every `display_update_interval_s`, the current composite is pushed to
`smear_preview` so the operator watches it grow; off-axis features trace
arcs as the sample turns, and the common center of curvature of those
arcs is the rotation axis.
`smear_preview` so the operator watches it grow; off-axis features trace
arcs as the sample turns, and the common center of curvature of those
arcs is the rotation axis.
4. Once the move completes, a final push leaves the finished composite
displayed, the shutter is closed (unless `keep_shutter_open`), and the
operator submits a click — reusing the exact same `_collect_click()` /
`_compute_shift_to_fzp()` / `_apply_rotation_center_shift()` machinery and
cumulative-shift accounting as the production `..._extended()` path.
displayed, the shutter is closed (unless `keep_shutter_open`), and the
operator submits a click — reusing the exact same `_collect_click()` /
`_compute_shift_to_fzp()` / `_apply_rotation_center_shift()` machinery and
cumulative-shift accounting as the production `..._extended()` path.
## Pitfall: regenerate the RPC client stub after changing `USER_ACCESS`
:::{important}
Whenever you add or rename a method in a plugin widget's `USER_ACCESS` list
(e.g. `XRayEye.USER_ACCESS` in `x_ray_eye.py`), you must run:
!!! warning "Important"
Whenever you add or rename a method in a plugin widget's `USER_ACCESS` list
(e.g. `XRayEye.USER_ACCESS` in `x_ray_eye.py`), you must run:
```bash
bw-generate-cli --target csaxs_bec
```
```bash
bw-generate-cli --target csaxs_bec
```
and commit the resulting changes to `csaxs_bec/bec_widgets/widgets/client.py`
(and `designer_plugins.py`). Skipping this step does **not** raise an error
at import time or widget-construction time — it fails much later, and
confusingly, as an `AttributeError` raised from deep inside
`bec_widgets`'s RPC plumbing, even after fully restarting both the ipython
client and the GUI server process.
and commit the resulting changes to `csaxs_bec/bec_widgets/widgets/client.py`
(and `designer_plugins.py`). Skipping this step does **not** raise an error
at import time or widget-construction time — it fails much later, and
confusingly, as an `AttributeError` raised from deep inside
`bec_widgets`'s RPC plumbing, even after fully restarting both the ipython
client and the GUI server process.
:::
The reason: `csaxs_bec/bec_widgets/widgets/client.py` is a **checked-in
generated file** containing one RPC stub class per plugin widget, mirroring
@@ -42,7 +42,8 @@ explicit "scan point index" field is captured by the PandA itself.
## What this does *not* include
- No on-the-fly or post-scan computation of Mean/Min/Max/stdev from the raw
samples — the raw stream is the complete record; deriving the previous
per-point statistics from it is a separate future task.
samples — the raw stream is the complete record; deriving the previous
per-point statistics from it is a separate future task.
- No changes to how Mode A (legacy) data is written — this schema only
applies when `raw_stream_mode: true`.
applies when `raw_stream_mode: true`.
+59 -52
View File
@@ -58,63 +58,68 @@ The blocks already configured and aliased today, for reference / so this
layout can be restored unambiguously (see `ptycho_flomni.yaml`):
- `FMC_IN.VAL1`/`FMC_IN.VAL2` (capacitive sensor analog inputs) — `Min`,
`Max`, `Mean` capture fields.
`Max`, `Mean` capture fields.
- `INENC1`-`INENC4.VAL` (position-capture encoder inputs) — `Min`, `Max`,
`Mean` capture fields.
`Mean` capture fields.
- `PCAP.GATE_DURATION` — `Value`.
- PCAP gate is pulsed once per point / sub-frame by the existing external
trigger electronics (unchanged by this work).
trigger electronics (unchanged by this work).
### Mode B (new) — free-running raw capture
- **Gate-bit input**: the detector-trigger signal, wired into `TTLIN1` (see
"Physical wiring" above). `TTLIN1.VAL` is captured into **`PCAP.BITS0`,
bit offset 0** — confirmed 2026-09-15 against the omny-panda hardware via:
"Physical wiring" above). `TTLIN1.VAL` is captured into **`PCAP.BITS0`,
bit offset 0** — confirmed 2026-09-15 against the omny-panda hardware via:
```
TTLIN1.VAL.CAPTURE_WORD? -> OK =PCAP.BITS0
TTLIN1.VAL.OFFSET? -> OK =0
```
```text
TTLIN1.VAL.CAPTURE_WORD? -> OK =PCAP.BITS0
TTLIN1.VAL.OFFSET? -> OK =0
```
`PCAP.BITS0` is a shared 32-bit capture word — other captured `bit_out`
signals (several TTLIN/LVDSIN/encoder bits are also flagged for capture on
this layout) may occupy other bit positions of the same word, so the raw
`PCAP.BITS0.Value` is **not** itself a clean 0/1. `PandaBoxOMNY` masks it
down to bit 0 in `raw_stream_mode` (see `_GATE_BIT_OFFSET` in
`panda_box_omny.py`) before publishing it as `gate_detector_active` — treat
that masking as required, not optional, unless it's confirmed nothing else
shares `BITS0`. Re-run the query above (read-only, safe against live
hardware) if the PandA layout is ever rebuilt, since bit assignment is
fixed by the FPGA build and not guaranteed stable across rebuilds.
`PCAP.BITS0` is a shared 32-bit capture word — other captured `bit_out`
signals (several TTLIN/LVDSIN/encoder bits are also flagged for capture on
this layout) may occupy other bit positions of the same word, so the raw
`PCAP.BITS0.Value` is **not** itself a clean 0/1. `PandaBoxOMNY` masks it
down to bit 0 in `raw_stream_mode` (see `_GATE_BIT_OFFSET` in
`panda_box_omny.py`) before publishing it as `gate_detector_active` — treat
that masking as required, not optional, unless it's confirmed nothing else
shares `BITS0`. Re-run the query above (read-only, safe against live
hardware) if the PandA layout is ever rebuilt, since bit assignment is
fixed by the FPGA build and not guaranteed stable across rebuilds.
- **Frame counter**: `COUNTER1`, triggered off `TTLIN1.VAL`'s **falling
edge** (see layout below) — i.e. it increments once per *completed*
detector exposure, not once per PCAP capture tick as originally guessed
here. Captured with `CAPTURE=Value`; this is both the data-completeness
signal and the completion-detection signal (`frame_counter` in
`signal_alias`, see below, and "Completion check" in the mode-comparison
table above) — once the last expected exposure's trigger pulse has fallen,
`COUNTER1.OUT` reaches `num_points * frames_per_trigger` exactly.
edge** (see layout below) — i.e. it increments once per *completed*
detector exposure, not once per PCAP capture tick as originally guessed
here. Captured with `CAPTURE=Value`; this is both the data-completeness
signal and the completion-detection signal (`frame_counter` in
`signal_alias`, see below, and "Completion check" in the mode-comparison
table above) — once the last expected exposure's trigger pulse has fallen,
`COUNTER1.OUT` reaches `num_points * frames_per_trigger` exactly.
- **Raw analog/position capture**: `FMC_IN.VAL1`, `FMC_IN.VAL2` and all four
`INENC1`-`4.VAL` are switched from aggregate capture to `CAPTURE=Value`
on this layout (confirmed 2026-09-15) — i.e. every analog/encoder channel
goes raw together, not a subset. This means the current PandA layout
cannot simultaneously produce Mode A's aggregate (Min/Max/Mean) data — see
"Open question" below.
`INENC1`-`4.VAL` are switched from aggregate capture to `CAPTURE=Value`
on this layout (confirmed 2026-09-15) — i.e. every analog/encoder channel
goes raw together, not a subset. This means the current PandA layout
cannot simultaneously produce Mode A's aggregate (Min/Max/Mean) data — see
"Open question" below.
- **Continuous gate/arm**: `CLOCK1`, enabled by the constant `ONE`, drives
`PCAP`'s `gate` and `trig` inputs continuously (instead of the external
per-point trigger used in Mode A), so PCAP free-runs at `CLOCK1`'s
configured rate. Testing started at **100 Hz** on 2026-09-15 and will be
ramped up; there is no PandA-side layout change needed to change the
rate, only `CLOCK1`'s period.
`PCAP`'s `gate` and `trig` inputs continuously (instead of the external
per-point trigger used in Mode A), so PCAP free-runs at `CLOCK1`'s
configured rate. Testing started at **100 Hz** on 2026-09-15 and will be
ramped up; there is no PandA-side layout change needed to change the
rate, only `CLOCK1`'s period.
```
TTLIN1 (TTL input 1, detector trigger)
├──▶ COUNTER1.trig, falling edge (Up/Down pulse counter) ──▶ COUNTER1.OUT = frame_counter (CAPTURE=Value)
│ = completion signal too: reaches num_points * frames_per_trigger when the last exposure finishes
└──▶ captured into PCAP.BITS0 bit 0 ──▶ masked in software = gate_detector_active
```text
TTLIN1 (TTL input 1, detector trigger)
├──▶ COUNTER1.trig, falling edge (Up/Down pulse counter) ──▶ COUNTER1.OUT = frame_counter (CAPTURE=Value)
│ = completion signal too: reaches num_points * frames_per_trigger when the last exposure finishes
└──▶ captured into PCAP.BITS0 bit 0 ──▶ masked in software = gate_detector_active
CLOCK1 (enabled by constant ONE) ──▶ PCAP.gate, PCAP.trig (continuous arm/capture)
```
CLOCK1 (enabled by constant ONE) ──▶ PCAP.gate, PCAP.trig (continuous arm/capture)
```
### Possible variant: gate continuous capture by exposure-active (proposed, not implemented)
@@ -124,7 +129,7 @@ free-runs continuously at `CLOCK1`'s rate regardless of whether the detector
is actually exposing -- including idle/dead time between exposures. The
proposal is to split the two roles so they're driven by different sources:
```
```text
CLOCK1 (enabled by constant ONE) ──▶ PCAP.trig (sample-rate clock, unchanged)
TTLIN1.VAL (detector-trigger / exposure-active) ──▶ PCAP.gate (only capture while exposing)
```
@@ -139,15 +144,17 @@ This should be a pure PandA-layout change -- no `ptycho_flomni.yaml` or
BEC signal names exist or what they mean:
- `TTLIN1.VAL` is already proven usable as a `PCAP.gate` source -- it's what
Mode A already uses (`PCAP.GATE`/`PCAP.TRIG` = `TTLIN1.VAL` there).
Mode A already uses (`PCAP.GATE`/`PCAP.TRIG` = `TTLIN1.VAL` there).
- `frame_counter` (`COUNTER1`, clocked directly off `TTLIN1.VAL`'s falling
edge, not off `PCAP`) is unaffected -- still counts every completed
exposure and still works as the completion/grouping signal exactly as
today.
edge, not off `PCAP`) is unaffected -- still counts every completed
exposure and still works as the completion/grouping signal exactly as
today.
- `gate_detector_active` (`PCAP.BITS0` bit 0, also `TTLIN1.VAL`) would become
trivially always `1` on every captured row, since a row is only ever
captured while that signal is high -- harmless, since `frame_counter`
already does the actual per-exposure grouping, not the gate bit.
trivially always `1` on every captured row, since a row is only ever
captured while that signal is high -- harmless, since `frame_counter`
already does the actual per-exposure grouping, not the gate bit.
Motivation: at 20 kHz continuous (current, ungated behavior), the
free-running raw stream produces roughly 67-86 MB/minute (~4-5 GB/hour) of
@@ -174,7 +181,7 @@ repo under `csaxs_bec/devices/panda_box/layouts/` — `omny.ini` (Mode A) and
design named `OMNY`/`omny_freerun` respectively (switch the PandA's active
Design first, then save):
```
```text
python ophyd_devices/devices/panda_box/utility_scripts.py \
--host omny-panda.psi.ch --save-layout csaxs_bec/devices/panda_box/layouts/omny.ini