docs: migrate to zensical
This commit was merged in pull request #329.
This commit is contained in:
@@ -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
|
||||
- { 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.
|
||||
|
||||
- { 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
@@ -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 ``); 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
|
||||

|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user