Merge branch 'main' into flomni_commissioning
This commit is contained in:
@@ -75,58 +75,21 @@ class ContGrid(ScanBase):
|
||||
}
|
||||
|
||||
def __init__(
|
||||
# fmt: off
|
||||
self,
|
||||
fast_axis: Annotated[
|
||||
DeviceBase,
|
||||
ScanArgument(display_name="Fast Axis", description="Axis with continuous motion."),
|
||||
],
|
||||
fast_start: Annotated[
|
||||
float,
|
||||
ScanArgument(
|
||||
display_name="Fast Start",
|
||||
description="Start position for measurement points of the fast axis.",
|
||||
),
|
||||
],
|
||||
fast_end: Annotated[
|
||||
float,
|
||||
ScanArgument(
|
||||
display_name="Fast End",
|
||||
description="End position for measurement points of the fast axis.",
|
||||
),
|
||||
],
|
||||
fast_step_size: Annotated[
|
||||
float,
|
||||
ScanArgument(
|
||||
display_name="Fast Step Size",
|
||||
description="Step size for points of the continuous motion axis.",
|
||||
),
|
||||
],
|
||||
stepper_axis: Annotated[
|
||||
DeviceBase,
|
||||
ScanArgument(
|
||||
display_name="Step Axis",
|
||||
description="Step axis of the grid scan, stepping through the lines.",
|
||||
),
|
||||
],
|
||||
stepper_start: Annotated[
|
||||
float,
|
||||
ScanArgument(display_name="Step Start", description="Start position of the step axis."),
|
||||
],
|
||||
stepper_stop: Annotated[
|
||||
float,
|
||||
ScanArgument(display_name="Step Stop", description="End position of the step axis."),
|
||||
],
|
||||
stepper_step_size: Annotated[
|
||||
float,
|
||||
ScanArgument(
|
||||
display_name="Step Step Size",
|
||||
description="Step size of the step axis in units of the motor.",
|
||||
),
|
||||
],
|
||||
fast_axis: Annotated[DeviceBase, ScanArgument(display_name="Fast Axis", description="Axis with continuous motion.")],
|
||||
fast_start: Annotated[float, ScanArgument(display_name="Fast Start", description="Start position for measurement points of the fast axis.")],
|
||||
fast_end: Annotated[float, ScanArgument(display_name="Fast End", description="End position for measurement points of the fast axis.")],
|
||||
fast_step_size: Annotated[float, ScanArgument(display_name="Fast Step Size", description="Step size for points of the continuous motion axis.")],
|
||||
stepper_axis: Annotated[DeviceBase, ScanArgument(display_name="Step Axis", description="Step axis of the grid scan, stepping through the lines.")],
|
||||
stepper_start: Annotated[float, ScanArgument(display_name="Step Start", description="Start position of the step axis.")],
|
||||
stepper_stop: Annotated[float, ScanArgument(display_name="Step Stop", description="End position of the step axis.")],
|
||||
stepper_step_size: Annotated[float, ScanArgument(display_name="Step Step Size", description="Step size of the step axis in units of the motor.")],
|
||||
exp_time: DefaultArgType.ExposureTime,
|
||||
relative: DefaultArgType.Relative = False,
|
||||
fast_axis_always_in_pos_dir: bool = True,
|
||||
**kwargs,
|
||||
# fmt: on
|
||||
):
|
||||
"""
|
||||
Continuous grid scan with 2-axis. The scan requires the fast axis to properly implement base velocity as well as high velocity and high acceleration time
|
||||
@@ -186,14 +149,13 @@ class ContGrid(ScanBase):
|
||||
)
|
||||
positions = position_generators.nd_grid_positions(
|
||||
[
|
||||
(self.stepper_start, self.stepper_stop, self._cont_motor_params["num_lines"]),
|
||||
(self.fast_start, self.fast_end, frames_per_trigger),
|
||||
(self.stepper_start, self.stepper_stop, self._cont_motor_params["num_lines"]),
|
||||
],
|
||||
snaked=False,
|
||||
)
|
||||
# Count only the end point of each line as a valid position, as the fast axis is continuously moving and only triggered at
|
||||
# the beginning of the line moving to the end point.
|
||||
positions = positions[:, ::-1]
|
||||
# Get device specific parameters
|
||||
self._fetch_device_params()
|
||||
|
||||
|
||||
@@ -1,3 +1,33 @@
|
||||
(developer)=
|
||||
# Developer
|
||||
# Developer
|
||||
Information for developers and maintainers of the cSAXS beamline documentation and software.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
hidden: true
|
||||
---
|
||||
|
||||
editing_docs
|
||||
|
||||
```
|
||||
|
||||
|
||||
***
|
||||
|
||||
````{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.
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
(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.
|
||||
:::
|
||||
|
||||
## How a change goes live
|
||||
|
||||
There are two ways to propose an edit:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Anatomy of a page
|
||||
|
||||
Every content page starts with a **cross-reference label** followed by a single
|
||||
top-level heading:
|
||||
|
||||
```md
|
||||
(user.saxs.data_analysis)=
|
||||
# Data analysis
|
||||
```
|
||||
|
||||
- The label `(user.saxs.data_analysis)=` 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.
|
||||
|
||||
## Conventions
|
||||
|
||||
### Code blocks — always tag the language
|
||||
|
||||
Use a language on every fenced block so syntax highlighting and the copy button work:
|
||||
|
||||
````md
|
||||
```bash
|
||||
module load Python/3.11.11
|
||||
pip install "pyFAI[gui]"
|
||||
```
|
||||
````
|
||||
|
||||
Use ```` ```text ```` for plain output (e.g. error messages) and ```` ```python ````
|
||||
for Python.
|
||||
|
||||
### Call-outs — use admonitions, not bold "Note:"
|
||||
|
||||
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.
|
||||
:::
|
||||
```
|
||||
|
||||
Available types include `note`, `tip`, `important`, `warning`, `caution`, `danger`.
|
||||
|
||||
### Links and cross-references
|
||||
|
||||
- **To another page in this documentation**, link by its label, not by file path:
|
||||
|
||||
```md
|
||||
See {ref}`the data analysis guide <user.saxs.data_analysis>`.
|
||||
```
|
||||
|
||||
- **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
|
||||
---
|
||||
|
||||
data_analysis
|
||||
<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
|
||||
```
|
||||
|
||||
Open `docs/_build/html/index.html` in a browser.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **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.
|
||||
Binary file not shown.
@@ -0,0 +1,149 @@
|
||||
(user.saxs.data_analysis)=
|
||||
# Data analysis
|
||||
|
||||
We can access the online computing resources from the beamline consoles when logged in as the e-account.
|
||||
|
||||
## Allocation of nodes
|
||||
|
||||
Check available nodes and allocate one:
|
||||
|
||||
```bash
|
||||
sinfo
|
||||
salloc -A csaxs -p csaxs -w [ra-c-110]
|
||||
ssh -Y [ra-c-110]
|
||||
```
|
||||
|
||||
Or for a GPU node:
|
||||
|
||||
```bash
|
||||
salloc -A csaxs -p gpu-csaxs --gpus=4 [-w ra-gpu-003]
|
||||
ssh -Y [ra-gpu-003]
|
||||
```
|
||||
|
||||
(note that we need to ask them to explicitly use one of our GPU nodes there, and this needs a couple of days to set up)
|
||||
|
||||
**The home directory is the p group.** To go to the raw directory we need to navigate there:
|
||||
|
||||
```bash
|
||||
cd /sls/x12sa/data/[p22598]/raw
|
||||
```
|
||||
|
||||
## Python tools
|
||||
|
||||
### Python package installation
|
||||
|
||||
Anaconda is no longer supported. Packages should now be installed directly using `pip`.
|
||||
|
||||
For example, `pyFAI` can be installed as follows:
|
||||
|
||||
```bash
|
||||
module load Python/3.11.11
|
||||
pip install "pyFAI[gui]"
|
||||
```
|
||||
|
||||
To launch the `pyFAI` applications:
|
||||
|
||||
```bash
|
||||
# Detector calibration
|
||||
python -m pyFAI.app.calib2
|
||||
|
||||
# Batch integration
|
||||
python -m pyFAI.app.integrate
|
||||
```
|
||||
|
||||
### Recommended: using a Python virtual environment
|
||||
|
||||
For long-term maintainability and reproducibility, it is recommended to use a dedicated Python virtual environment.
|
||||
|
||||
#### Create a virtual environment
|
||||
|
||||
```bash
|
||||
cd /sls/x12sa/data/[p-group]/raw
|
||||
|
||||
module load Python/3.11.11
|
||||
mkdir python-env
|
||||
cd python-env
|
||||
|
||||
# Create a virtual environment named ".venv"
|
||||
python3 -m venv .venv
|
||||
|
||||
# Activate the environment
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install required packages
|
||||
pip install "pyFAI[gui]"
|
||||
```
|
||||
|
||||
#### Use an existing virtual environment
|
||||
|
||||
```bash
|
||||
cd /sls/x12sa/data/[p-group]/raw/python-env
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you encounter the following error when launching GUI applications (e.g. `pyFAI-calib2`):
|
||||
|
||||
```text
|
||||
X11 connection rejected
|
||||
```
|
||||
|
||||
reconnect to the analysis node with X11 forwarding enabled:
|
||||
|
||||
```bash
|
||||
ssh -X [ra-c-110]
|
||||
```
|
||||
|
||||
and then reactivate the virtual environment before launching the application.
|
||||
|
||||
## Use Jupyter Notebook / Lab
|
||||
|
||||
Create a virtual environment following the instructions above, then:
|
||||
|
||||
```bash
|
||||
pip install jupyter
|
||||
```
|
||||
|
||||
To run Jupyter Lab:
|
||||
|
||||
```bash
|
||||
jupyter lab --port 6006 --ip $(hostname) --no-browser
|
||||
```
|
||||
|
||||
## Setup online radial integration on Jungfraujoch
|
||||
|
||||
1. **Calibrate the detector using pyFAI.**
|
||||
|
||||
Analyze a calibration scan carefully with `pyFAI` and save the resulting **PONI** file and **pixel mask**. Also determine the beam center position.
|
||||
|
||||
:::{note}
|
||||
As of 2026-06-18, Jungfraujoch only accepts `uint32` TIFF mask files. If the mask generated by `pyFAI` is stored as `uint8`, it needs to be converted to the required format before uploading.
|
||||
To do this (from the home directory `/sls/x12sa/data/[p-group]/raw/`):
|
||||
|
||||
- Activate the python environment: `source mask/.mask/bin/activate`
|
||||
- Run `python convert_mask_format.py -i <input .tif file path> -o <output .tiff file path>`
|
||||
:::
|
||||
|
||||
2. **Update the BEC configuration.**
|
||||
|
||||
Update the detector distance and beam center coordinates in the BEC configuration files using the calibration results.
|
||||
|
||||
3. **Configure Jungfraujoch.**
|
||||
|
||||
Open the **Jungfraujoch Expert Configuration** panel in the frontend interface, for example:
|
||||
|
||||
```text
|
||||
http://sls-jfjoch-001:8080/frontend
|
||||
```
|
||||
|
||||
- Upload the pixel mask under **Pixel Mask**.
|
||||
- Fill in the required parameters under **Azimuthal Integration Settings**.
|
||||
- Make sure to enable **Force CPU calculation in FPGA workflow**.
|
||||
- After uploading a new configuration, do not press "initialize", as this will remove all the previously uploaded configuration.
|
||||
|
||||
4. **Detector rotation (if required).**
|
||||
|
||||
:::{note}
|
||||
Adding rotation of the detector still needs to be documented (likely configured in the BEC configuration).
|
||||
:::
|
||||
+36
-2
@@ -1,5 +1,39 @@
|
||||
(user.saxs)=
|
||||
# Saxs
|
||||
# SAXS
|
||||
Welcome to the SAXS section of the cSAXS beamline.
|
||||
|
||||
```{toctree}
|
||||
---
|
||||
maxdepth: 2
|
||||
hidden: true
|
||||
---
|
||||
|
||||
data_analysis
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Beamtime allocation
|
||||
***
|
||||
|
||||
````{grid} 2
|
||||
:gutter: 5
|
||||
|
||||
```{grid-item-card}
|
||||
:link: user.saxs.data_analysis
|
||||
:link-type: ref
|
||||
:img-top: /assets/biotech.svg
|
||||
:text-align: center
|
||||
:class-item: index-card
|
||||
|
||||
## Data analysis
|
||||
|
||||
Allocating compute nodes, pyFAI installation and usage, and online radial integration on Jungfraujoch.
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
|
||||
## Reference documents
|
||||
|
||||
- {download}`SAXS standards plate (PDF) <SAXS_standards_plate.pdf>`
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from bec_server.scan_server.tests.scan_fixtures import *
|
||||
@@ -0,0 +1,125 @@
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from bec_server.scan_server.tests.scan_fixtures import MockCustomDevice
|
||||
from bec_server.scan_server.tests.scan_hook_tests import (
|
||||
DEFAULT_HOOK_TESTS,
|
||||
PREMOVE_HOOK_TESTS,
|
||||
run_scan_tests,
|
||||
)
|
||||
|
||||
CONT_GRID_HOOK_TESTS = [*DEFAULT_HOOK_TESTS, *PREMOVE_HOOK_TESTS]
|
||||
|
||||
|
||||
def _assemble_cont_grid_scan(v4_scan_assembler, device_manager):
|
||||
custom_samx = MockCustomDevice(
|
||||
"samx",
|
||||
device_info={
|
||||
"signals": {
|
||||
"readback": {
|
||||
"obj_name": "samx",
|
||||
"kind_str": "hinted",
|
||||
"describe": {"precision": 3},
|
||||
},
|
||||
"velocity": {
|
||||
"obj_name": "samx_velocity",
|
||||
"kind_str": "config",
|
||||
"describe": {"precision": 3},
|
||||
},
|
||||
"acceleration": {
|
||||
"obj_name": "samx_acceleration",
|
||||
"kind_str": "config",
|
||||
"describe": {"precision": 3},
|
||||
},
|
||||
"base_velocity": {
|
||||
"obj_name": "samx_base_velocity",
|
||||
"kind_str": "config",
|
||||
"describe": {"precision": 3},
|
||||
},
|
||||
}
|
||||
},
|
||||
signal_read_values={
|
||||
"samx": 0.0,
|
||||
"samx_velocity": 10.0,
|
||||
"samx_acceleration": 2.0,
|
||||
"samx_base_velocity": 0.0,
|
||||
},
|
||||
)
|
||||
custom_samy = MockCustomDevice(
|
||||
"samy",
|
||||
device_info={
|
||||
"signals": {
|
||||
"readback": {"obj_name": "samy", "kind_str": "hinted", "describe": {"precision": 3}}
|
||||
}
|
||||
},
|
||||
signal_read_values={"samy": 0.0},
|
||||
)
|
||||
custom_ddg1 = MockCustomDevice(
|
||||
"ddg1",
|
||||
device_info={
|
||||
"signals": {
|
||||
"readback": {"obj_name": "ddg1", "kind_str": "hinted", "describe": {"precision": 3}}
|
||||
}
|
||||
},
|
||||
signal_read_values={"ddg1": 0.0},
|
||||
)
|
||||
custom_ddg1.get_shutter_to_open_delay = mock.MagicMock(return_value=2e-3)
|
||||
custom_mcs = MockCustomDevice(
|
||||
"mcs",
|
||||
device_info={
|
||||
"signals": {
|
||||
"readback": {"obj_name": "mcs", "kind_str": "hinted", "describe": {"precision": 3}}
|
||||
}
|
||||
},
|
||||
signal_read_values={"mcs": 0.0},
|
||||
)
|
||||
device_manager.add_device(custom_samx, replace=True)
|
||||
device_manager.add_device(custom_samy, replace=True)
|
||||
device_manager.add_device(custom_mcs, replace=True)
|
||||
device_manager.add_device(custom_ddg1, replace=True)
|
||||
return v4_scan_assembler("cont_grid", "samx", -1.0, 1.0, 1, "samy", -2.0, 2.0, 2, exp_time=0.1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("hook_name", "hook_tests"), CONT_GRID_HOOK_TESTS)
|
||||
def test_cont_grid_default_hooks(
|
||||
v4_scan_assembler, device_manager, nth_done_status_mock, hook_name, hook_tests
|
||||
):
|
||||
scan = _assemble_cont_grid_scan(v4_scan_assembler, device_manager)
|
||||
|
||||
run_scan_tests(scan, [(hook_name, hook_tests)], nth_done_status_mock=nth_done_status_mock)
|
||||
|
||||
|
||||
def test_cont_grid_post_scan_waits_for_completion_and_moves_back_when_relative(
|
||||
v4_scan_assembler, device_manager, nth_done_status_mock
|
||||
):
|
||||
scan = _assemble_cont_grid_scan(v4_scan_assembler, device_manager)
|
||||
|
||||
completion_status = nth_done_status_mock(resolve_after=3)
|
||||
scan.relative = True
|
||||
scan.start_positions = [1.2, -0.7]
|
||||
scan.actions.complete_all_devices = mock.MagicMock(return_value=completion_status)
|
||||
scan.components.move_and_wait = mock.MagicMock()
|
||||
scan._restore_motor_properties = mock.MagicMock()
|
||||
|
||||
scan.post_scan()
|
||||
|
||||
scan._restore_motor_properties.assert_called_once_with()
|
||||
scan.actions.complete_all_devices.assert_called_once_with(wait=False)
|
||||
scan.components.move_and_wait.assert_called_once_with(scan.motors, scan.start_positions)
|
||||
assert completion_status.wait_calls == 1
|
||||
|
||||
|
||||
def test_cont_grid_prepare_scan_keeps_generated_positions_stable(v4_scan_assembler, device_manager):
|
||||
scan = _assemble_cont_grid_scan(v4_scan_assembler, device_manager)
|
||||
|
||||
scan.prepare_scan()
|
||||
|
||||
assert np.array_equal(scan.positions, np.array([[1.0, -2.0], [1.0, 2.0]]))
|
||||
assert scan.scan_info.frames_per_trigger == 2
|
||||
assert scan._cont_motor_params["num_lines"] == 2
|
||||
assert scan.scan_info.additional_scan_parameters["num_lines"] == 2
|
||||
assert np.array_equal(
|
||||
scan.scan_info.additional_scan_parameters["computed_positions"],
|
||||
np.array([[-1.0, -2.0], [1.0, -2.0], [-1.0, 2.0], [1.0, 2.0]]),
|
||||
)
|
||||
Reference in New Issue
Block a user