6 Commits

Author SHA1 Message Date
d1e035e01e Update repo with template version v1.2.8
All checks were successful
CI for addams_bec / test (pull_request) Successful in 35s
CI for addams_bec / test (push) Successful in 32s
2026-02-27 15:49:26 +01:00
d49d9cef33 Update repo with template version v1.2.7
Some checks failed
CI for addams_bec / test (push) Failing after 1s
CI for addams_bec / test (pull_request) Failing after 0s
2026-02-27 12:11:40 +01:00
a62fe77b56 fix: ipython startup
All checks were successful
CI for addams_bec / test (pull_request) Successful in 30s
CI for addams_bec / test (push) Successful in 28s
2026-01-17 18:24:39 +01:00
e642cbaae8 refactor: upgrade copier to v1-2-2
All checks were successful
CI for addams_bec / test (pull_request) Successful in 35s
CI for addams_bec / test (push) Successful in 29s
2025-09-11 18:24:35 +02:00
37bfc1c2a2 Merge branch 'update_copier_template' into 'main'
feat: update repository with copier changes for gitea migration

See merge request bec/addams_bec!4
2025-09-11 15:16:27 +02:00
63ded2bf7d feat: update repository with copier changes for gitea migration 2025-09-11 15:10:02 +02:00
10 changed files with 342 additions and 157 deletions

View File

@@ -2,8 +2,8 @@
# It is needed to track the repo template version, and editing may break things.
# This file will be overwritten by copier on template updates.
_commit: v1.0.3
_src_path: https://gitea.psi.ch/bec/bec_plugin_copier_template.git
make_commit: true
_commit: v1.2.8
_src_path: https://github.com/bec-project/plugin_copier_template.git
make_commit: false
project_name: addams_bec
widget_plugins_input: []

102
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,102 @@
name: CI for addams_bec
on:
push:
pull_request:
workflow_dispatch:
inputs:
BEC_WIDGETS_BRANCH:
description: "Branch of BEC Widgets to install"
required: false
type: string
default: "main"
BEC_CORE_BRANCH:
description: "Branch of BEC Core to install"
required: false
type: string
default: "main"
OPHYD_DEVICES_BRANCH:
description: "Branch of Ophyd Devices to install"
required: false
type: string
default: "main"
BEC_PLUGIN_REPO_BRANCH:
description: "Branch of the BEC Plugin Repository to install"
required: false
type: string
default: "main"
PYTHON_VERSION:
description: "Python version to use"
required: false
type: string
default: "3.12"
permissions:
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
env:
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "${{ inputs.PYTHON_VERSION || '3.12' }}"
- name: Checkout BEC Plugin Repository
uses: actions/checkout@v4
with:
repository: bec/addams_bec
ref: "${{ inputs.BEC_PLUGIN_REPO_BRANCH || github.head_ref || github.sha }}"
path: ./addams_bec
- name: Lint for merge conflicts from template updates
shell: bash
# Find all Copier conflicts except this line
run: '! grep -r "<<<<<<< before updating" | grep -v "grep -r \"<<<<<<< before updating"'
- name: Checkout BEC Core
uses: actions/checkout@v4
with:
repository: bec/bec
ref: "${{ inputs.BEC_CORE_BRANCH || 'main' }}"
path: ./bec
- name: Checkout Ophyd Devices
uses: actions/checkout@v4
with:
repository: bec/ophyd_devices
ref: "${{ inputs.OPHYD_DEVICES_BRANCH || 'main' }}"
path: ./ophyd_devices
- name: Checkout BEC Widgets
uses: actions/checkout@v4
with:
repository: bec/bec_widgets
ref: "${{ inputs.BEC_WIDGETS_BRANCH || 'main' }}"
path: ./bec_widgets
- name: Install dependencies
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
- name: Install Python dependencies
shell: bash
run: |
pip install uv
uv pip install --system -e ./ophyd_devices
uv pip install --system -e ./bec/bec_lib[dev]
uv pip install --system -e ./bec/bec_ipython_client
uv pip install --system -e ./bec/bec_server[dev]
uv pip install --system -e ./bec_widgets[dev,pyside6]
uv pip install --system -e ./addams_bec
- name: Run Pytest with Coverage
id: coverage
run: pytest --random-order --cov=./addams_bec --cov-config=./addams_bec/pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail ./addams_bec/tests/ || test $? -eq 5

View File

@@ -0,0 +1,62 @@
name: Create template upgrade PR for addams_bec
on:
workflow_dispatch:
permissions:
pull-requests: write
jobs:
create_update_branch_and_pr:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install tools
run: |
pip install copier PySide6
- name: Checkout
uses: actions/checkout@v4
- name: Perform update
run: |
git config --global user.email "bec_ci_staging@psi.ch"
git config --global user.name "BEC automated CI"
branch="chore/update-template-$(python -m uuid)"
echo "switching to branch $branch"
git checkout -b $branch
echo "Running copier update..."
output="$(copier update --trust --defaults --conflict inline 2>&1)"
echo "$output"
msg="$(printf '%s\n' "$output" | head -n 1)"
if ! grep -q "make_commit: true" .copier-answers.yml ; then
echo "Autocommit not made, committing..."
git add -A
git commit -a -m "$msg"
fi
if diff-index --quiet HEAD ; then
echo "No changes detected"
exit 0
fi
git push -u origin $branch
curl -X POST "https://gitea.psi.ch/api/v1/repos/${{ gitea.repository }}/pulls" \
-H "Authorization: token ${{ secrets.CI_REPO_WRITE }}" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Template: $(echo $msg)\",
\"body\": \"This PR was created by Gitea Actions\",
\"head\": \"$(echo $branch)\",
\"base\": \"main\"
}"

View File

@@ -1,7 +0,0 @@
include:
- file: /templates/plugin-repo-template.yml
inputs:
name: addams_bec
target: addams_bec
branch: $CHILD_PIPELINE_BRANCH
project: bec/awi_utils

View File

@@ -1,75 +1,57 @@
import builtins
import collections
import functools
import json
import math
import pathlib
import numpy
from bec_ipython_client.main import BECClientPrompt
from bec_ipython_client.prettytable import PrettyTable
__all__ = [
'setlat',
'setlambda',
'setmode',
'freeze',
'unfreeze',
'br',
'ubr',
'mvhkl',
'umvhkl',
'ca',
'wh',
'pa',
'orientAdd',
'orientRemove',
'orientShow',
'orientFit',
'ct'
"setlat",
"setlambda",
"setmode",
"freeze",
"unfreeze",
"br",
"ubr",
"mvhkl",
"umvhkl",
"ca",
"wh",
"pa",
"orientAdd",
"orientRemove",
"orientShow",
"orientFit",
"ct",
]
bec = builtins.__dict__.get("bec")
dev = builtins.__dict__.get("dev")
scans = builtins.__dict__.get("scans")
class BECClientPromptDiffractometer(BECClientPrompt):
@property
def username(self):
"""current username"""
if "x04v" in dev:
return "x04v"
if "x04h" in dev:
return "x04h"
return "demo"
bec._ip.prompts = BECClientPromptDiffractometer(ip=bec._ip, username="demo", client=bec._client, status=1)
# check for diffractometer device
diffract = None
if dev is not None:
if 'x04h' in dev:
if "x04h" in dev:
diffract = dev.x04h
elif 'x04v' in dev:
elif "x04v" in dev:
diffract = dev.x04v
if diffract is not None:
RealPosition = collections.namedtuple('RealPosition', ' '.join(diffract.get_real_positioners()))
RealPosition = collections.namedtuple("RealPosition", " ".join(diffract.get_real_positioners()))
def freeze(
angle: float | None
):
def freeze(angle: float | None):
"""
Freeze the value of the mode dependent angle, so when calculating motor positions
corresponding to an arbitrary (H, K, L ), the angle will be reset to the frozen value
Freeze the value of the mode dependent angle, so when calculating motor positions
corresponding to an arbitrary (H, K, L ), the angle will be reset to the frozen value
before the calculation no matter what the current position of the diffractometer.
"""
diffract.freeze(angle)
def unfreeze():
"""
Subsequent angle calculations will use whatever the current value of the associated
@@ -77,74 +59,69 @@ def unfreeze():
"""
diffract.unfreeze()
def setlat(
a: float, b: float, c: float, alpha: float, beta: float, gamma: float
):
def setlat(a: float, b: float, c: float, alpha: float, beta: float, gamma: float):
"""
Set sample lattice parameters
"""
diffract.set_lattice((a, b, c, alpha, beta, gamma))
def setlambda(
wavelength: float
):
def setlambda(wavelength: float):
"""
Set the x-ray wavelength (in Angstroms)
"""
if wavelength <= 0:
print('Invalid input: wavelength <=0!')
print("Invalid input: wavelength <=0!")
return
current_wavelength = diffract.get_wavelength()
if math.isclose(wavelength, current_wavelength):
print(f'Still using {current_wavelength} A')
print(f"Still using {current_wavelength} A")
else:
diffract.set_wavelength(wavelength)
print(f'Lambda reset from {current_wavelength} to {wavelength} A')
print(f"Lambda reset from {current_wavelength} to {wavelength} A")
def setmode(
mode: int
):
def setmode(mode: int):
"""
Set the geometry mode
"""
if mode < 0 or mode > 2:
print('Valid mode is from 0 to 2')
print("Valid mode is from 0 to 2")
return
current_mode = diffract.get_mode()
if mode == current_mode:
print(f'Still using mode {current_mode}')
print(f"Still using mode {current_mode}")
else:
diffract.set_mode(mode)
print(f'Mode reset from {current_mode} to {mode}')
print(f"Mode reset from {current_mode} to {mode}")
def mvhkl(
h: float, k: float, l: float, auto=False
):
def mvhkl(h: float, k: float, l: float, auto=False):
"""
Move to the reciprocol space coordinates
"""
try:
angles = diffract.forward(h, k, l)[:-2]
except Exception as exc:
print(f'{h} {k} {l} is not obtainable: {exc}')
print(f"{h} {k} {l} is not obtainable: {exc}")
return
if not auto:
for axis, current, target in zip(RealPosition._fields, _currentPosition(), angles):
print('%7s = %9.4f --> %9.4f' % (axis, current, target))
print("%7s = %9.4f --> %9.4f" % (axis, current, target))
answer = input('Move to these values? [Y/n]: ')
if answer.startswith(('N', 'n')):
print('Move abandoned.')
answer = input("Move to these values? [Y/n]: ")
if answer.startswith(("N", "n")):
print("Move abandoned.")
return
br(h, k, l)
def br(
h: float, k: float, l: float
):
def br(h: float, k: float, l: float):
"""
Move to the reciprocol space coordinates
"""
@@ -156,9 +133,8 @@ def br(
scans.mv(*args, relative=False)
def ubr(
h: float, k: float, l: float
):
def ubr(h: float, k: float, l: float):
"""
Move to the reciprocol space coordinates with updates
"""
@@ -170,44 +146,43 @@ def ubr(
scans.umv(*args, relative=False)
def umvhkl(
h: float, k: float, l: float, auto=False
):
def umvhkl(h: float, k: float, l: float, auto=False):
"""
Move to the reciprocol space coordinates with updates
"""
try:
angles = diffract.forward(h, k, l)[:-2]
except Exception as exc:
print(f'{h} {k} {l} is not obtainable: {exc}')
print(f"{h} {k} {l} is not obtainable: {exc}")
return
if not auto:
for axis, current, target in zip(RealPosition._fields, _currentPosition(), angles):
print('%7s = %9.4f --> %9.4f' % (axis, current, target))
print("%7s = %9.4f --> %9.4f" % (axis, current, target))
answer = input('Move to these values? [Y/n]: ')
if answer.startswith(('N', 'n')):
print('Move abandoned.')
answer = input("Move to these values? [Y/n]: ")
if answer.startswith(("N", "n")):
print("Move abandoned.")
return
ubr(h, k, l)
def ca(
h: float, k: float, l: float
):
def ca(h: float, k: float, l: float):
"""
Calculate angle positions for a given point in reciprocol space
"""
angles = diffract.forward(h, k, l)
print("\nCalculated positions:\n")
print(f'H K L = {h} {k} {l}')
print('BetaIn = %.5f BetaOut = %.5f' %(angles[-2], angles[-1]))
print('Lambda = %.3f' % diffract.get_wavelength())
print(f"H K L = {h} {k} {l}")
print("BetaIn = %.5f BetaOut = %.5f" % (angles[-2], angles[-1]))
print("Lambda = %.3f" % diffract.get_wavelength())
print()
_showAngles(angles[:-2])
def wh():
"""
Show where principal axes and reciprocal space
@@ -218,41 +193,45 @@ def wh():
betaIn = diffract.betaIn.position
betaOut = diffract.betaOut.position
print(f'H K L = {h:.4f} {k:.4f} {l:.4f}')
print('BetaIn = %.5f BetaOut = %.5f' %(betaIn, betaOut))
print('Lambda = %.3f' % diffract.get_wavelength())
print(f"H K L = {h:.4f} {k:.4f} {l:.4f}")
print("BetaIn = %.5f BetaOut = %.5f" % (betaIn, betaOut))
print("Lambda = %.3f" % diffract.get_wavelength())
print()
_showAngles()
def pa():
"""
Show geometry parameters
"""
if diffract.name == 'x04v':
print('x04v (Newport Microcontrols 2+3 at SLS) vertical geometry')
elif diffract.name == 'x04h':
print('x04h (Newport Microcontrols 2+3 at SLS) horizontal geometry')
if diffract.name == "x04v":
print("x04v (Newport Microcontrols 2+3 at SLS) vertical geometry")
elif diffract.name == "x04h":
print("x04h (Newport Microcontrols 2+3 at SLS) horizontal geometry")
match mode := diffract.get_mode():
case 0:
print(f' BetaIn Fixed (mode {mode})')
print(f" BetaIn Fixed (mode {mode})")
case 1:
print(f' BetaOut Fixed (mode {mode})')
print(f" BetaOut Fixed (mode {mode})")
case 2:
print(f' BetaIn equals BetaOut (mode {mode})')
print(f" BetaIn equals BetaOut (mode {mode})")
if beta_frozen := diffract.get_frozen():
print(f' Frozen coordinate: {beta_frozen}')
print(f" Frozen coordinate: {beta_frozen}")
def orientShow():
"""
Display list of measured reflections
"""
print('\n(Using lattice constants:)')
print("\n(Using lattice constants:)")
lattice = diffract.get_lattice()
print('a = %.4g, b = %.4g, b = %.4g, alpha = %.6g, beta = %.6g, gamma = %.6g' %
(lattice[0], lattice[1], lattice[2], lattice[3], lattice[4], lattice[5]))
print(
"a = %.4g, b = %.4g, b = %.4g, alpha = %.6g, beta = %.6g, gamma = %.6g"
% (lattice[0], lattice[1], lattice[2], lattice[3], lattice[4], lattice[5])
)
print("\n------------------------------------------------\n")
@@ -263,17 +242,15 @@ def orientShow():
_showUB()
def orientRemove(
h: float, k: float, l: float
):
def orientRemove(h: float, k: float, l: float):
"""
Remove a measured reflection from the list
"""
diffract.remove_reflection(h, k, l)
def orientAdd(
h: float, k: float, l: float, *args
):
def orientAdd(h: float, k: float, l: float, *args):
"""
Add a reflection to the list of measured reflections
"""
@@ -282,63 +259,67 @@ def orientAdd(
response = diffract.real_position
# The original return value is of namedtuple type,
# which gets serialized to a dictionary by the device server.
angles = tuple(response['values'][axis] for axis in response['fields'] if axis != 'nu')
angles = tuple(response["values"][axis] for axis in response["fields"] if axis != "nu")
if len(angles) < 4:
print('Please specify all angles')
print("Please specify all angles")
return
diffract.add_reflection(h, k, l, angles)
def orientSave(
filename: str
):
def orientSave(filename: str):
"""
Save the current reflections
"""
configuration = {}
configuration['geometry'] = diffract.name
configuration['wavelength'] = diffract.get_wavelength()
configuration['lattice'] = diffract.get_lattice()
configuration['reflections'] = diffract.get_reflections()
configuration["geometry"] = diffract.name
configuration["wavelength"] = diffract.get_wavelength()
configuration["lattice"] = diffract.get_lattice()
configuration["reflections"] = diffract.get_reflections()
filepath = pathlib.Path(filename)
if filepath.exists():
answer = input('File "%s" already exists. Do you want to overwrite it? [y/N]: ' %(filepath.absolute()))
if not answer.startswith(('Y', 'y')):
answer = input(
'File "%s" already exists. Do you want to overwrite it? [y/N]: ' % (filepath.absolute())
)
if not answer.startswith(("Y", "y")):
return
with open(filepath, 'w') as f:
with open(filepath, "w") as f:
json.dump(configuration, f)
def orientLoad(
filename: str
):
def orientLoad(filename: str):
"""
Load relfections from file
"""
with open(filename, 'r') as f:
with open(filename, "r") as f:
configuration = json.load(f)
if configuration['geometry'] != diffract.name:
print('Saved orientation is for a different geometry "%s", current is "%s".' % configuration['geometry'], diffract.name)
if configuration["geometry"] != diffract.name:
print(
'Saved orientation is for a different geometry "%s", current is "%s".'
% configuration["geometry"],
diffract.name,
)
return
# save current wavelength, lattice and reflections
saved_wavelength = diffract.get_wavelength()
saved_lattice = diffract.get_lattice()
saved_reflections = diffract.get_reflections()
try:
diffract.set_lattice(configuration['lattice'])
diffract.set_lattice(configuration["lattice"])
diffract.clear_reflections()
for reflection in configuration['reflections']:
for reflection in configuration["reflections"]:
diffract.add_reflection(*reflection)
_showReflections(configuration['reflections'])
_showReflections(configuration["reflections"])
print("\n------------------------------------------------\n")
# set wavelength temporarily for orientFit and restore later
diffract.set_wavelength(configuration['wavelength'])
diffract.set_wavelength(configuration["wavelength"])
orientFit()
except Exception as exc:
# restore saved lattice and reflections
@@ -352,6 +333,7 @@ def orientLoad(
# restore wavelength
diffract.set_wavelength(saved_wavelength)
def orientFit():
"""
Fit UB matrix from given reflections
@@ -364,19 +346,22 @@ def orientFit():
diffract.compute_UB()
_showUB()
def ct(exp_time: float):
"""
Acquire all detectors
"""
scans.acquire(exp_time=exp_time)
def _showUB():
UB = diffract.get_UB()
print('Orientation matrix by row:')
print(' Row 1: %8.5f %8.5f %8.5f' % (UB[0,0], UB[0,1], UB[0,2]))
print(' Row 2: %8.5f %8.5f %8.5f' % (UB[1,0], UB[1,1], UB[1,2]))
print(' Row 3: %8.5f %8.5f %8.5f' % (UB[2,0], UB[2,1], UB[2,2]))
print("Orientation matrix by row:")
print(" Row 1: %8.5f %8.5f %8.5f" % (UB[0, 0], UB[0, 1], UB[0, 2]))
print(" Row 2: %8.5f %8.5f %8.5f" % (UB[1, 0], UB[1, 1], UB[1, 2]))
print(" Row 3: %8.5f %8.5f %8.5f" % (UB[2, 0], UB[2, 1], UB[2, 2]))
def _showAngles(angles=None):
if angles is None:
@@ -384,23 +369,25 @@ def _showAngles(angles=None):
table = PrettyTable(RealPosition._fields, padding=12)
print(table.get_header())
text = tuple(f'{x:9.4f}' for x in angles)
text = tuple(f"{x:9.4f}" for x in angles)
print(table.get_row(*text))
def _currentPosition():
response = diffract.real_position
# The original return value is of namedtuple type,
# which gets serialized to a dictionary by the device server.
angles = RealPosition(*(response['values'][axis] for axis in response['fields']))
angles = RealPosition(*(response["values"][axis] for axis in response["fields"]))
return angles
def _showReflections(reflections):
print('The defined reflections are:')
header = ['h', 'k', 'l'] + diffract.real_position['fields'][:-1]
print("The defined reflections are:")
header = ["h", "k", "l"] + diffract.real_position["fields"][:-1]
table = PrettyTable(header, padding=12)
print(' ', table.get_header())
print(" ", table.get_header())
for reflection in reflections:
h, k, l, angles = reflection
text = [f'{h:9.4f}', f'{k:9.4f}', f'{l:9.4f}'] + [f'{x:9.4f}' for x in angles]
print(' ', table.get_row(*text))
text = [f"{h:9.4f}", f"{k:9.4f}", f"{l:9.4f}"] + [f"{x:9.4f}" for x in angles]
print(" ", table.get_row(*text))

View File

@@ -34,7 +34,27 @@ to setup the prompts.
"""
# pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module
import builtins
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from bec_ipython_client.main import BECIPythonClient
bec: BECIPythonClient = BECIPythonClient()
dev = bec.device_manager.devices
scans = bec.scans
else:
bec = builtins.__dict__.get("bec")
dev = builtins.__dict__.get("dev")
scans = builtins.__dict__.get("scans")
bec.load_high_level_interface("bec_hli")
bec.load_high_level_interface("spec_hli")
bec.load_high_level_interface("hkl_hli")
if "x04v" in dev:
bec._ip.prompts.session_name = "x04v"
elif "x04h" in dev:
bec._ip.prompts.session_name = "x04h"

View File

@@ -3,8 +3,12 @@ Pre-startup script for BEC client. This script is executed before the BEC client
is started. It can be used to add additional command line arguments.
"""
import os
from bec_lib.service_config import ServiceConfig
import addams_bec
def extend_command_line_args(parser):
"""
@@ -14,3 +18,14 @@ def extend_command_line_args(parser):
# parser.add_argument("--session", help="Session name", type=str, default="cSAXS")
return parser
def get_config() -> ServiceConfig:
"""
Create and return the ServiceConfig for the plugin repository
"""
deployment_path = os.path.dirname(os.path.dirname(os.path.dirname(addams_bec.__file__)))
files = os.listdir(deployment_path)
if "bec_config.yaml" in files:
return ServiceConfig(config_path=os.path.join(deployment_path, "bec_config.yaml"))
else:
return ServiceConfig(redis={"host": "localhost", "port": 6379})

View File

@@ -0,0 +1,6 @@
# Macros
This directory is intended to store macros which will be loaded automatically when starting BEC.
Macros are small functions to make repetitive tasks easier. Functions defined in python files in this directory will be accessible from the BEC console.
Please do not put any code outside of function definitions here. If you wish for code to be automatically run when starting BEC, see the startup script at addams_bec/bec_ipython_client/startup/post_startup.py
For a guide on writing macros, please see: https://bec.readthedocs.io/en/latest/user/command_line_interface.html#how-to-write-a-macro

View File

View File

@@ -5,8 +5,8 @@ build-backend = "hatchling.build"
[project]
name = "addams_bec"
version = "0.0.0"
description = "A plugin repository for BEC for the ADDAMS beamline"
requires-python = ">=3.10"
description = "A plugin repository for BEC"
requires-python = ">=3.11"
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",