Compare commits

..

2 Commits

Author SHA1 Message Date
dc4a1ee9e6 Merge branch '2509.rc.87' into 'main'
Some checks failed
Build Packages / build:rpm (rocky8_nocuda) (push) Failing after 31m23s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Failing after 29m0s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Failing after 29m18s
Build Packages / build:rpm (rocky8) (push) Failing after 28m36s
Build Packages / build:rpm (rocky8_sls9) (push) Failing after 29m45s
Build Packages / build:rpm (rocky9_nocuda) (push) Failing after 32m11s
Build Packages / Generate python client (push) Failing after 51s
Build Packages / Build documentation (push) Successful in 1m10s
Build Packages / build:rpm (rocky9) (push) Failing after 5m44s
Build Packages / build:rpm (ubuntu2204) (push) Failing after 9m50s
Build Packages / build:rpm (ubuntu2404) (push) Failing after 9m17s
Build Packages / Unit tests (push) Successful in 1h13m5s
1.0.0-rc.87

See merge request jungfraujoch/nextgendcu!176
2025-09-30 20:43:54 +02:00
99b7dc07f7 1.0.0-rc.87 2025-09-30 20:43:53 +02:00
25 changed files with 276 additions and 332 deletions

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
*.mcs filter=lfs diff=lfs merge=lfs -text
*.mcs.gz filter=lfs diff=lfs merge=lfs -text

View File

@@ -12,7 +12,6 @@ on:
jobs:
build-rpm:
name: build:rpm (${{ matrix.distro }})
if: github.ref_type == 'tag'
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
@@ -21,30 +20,48 @@ jobs:
- runner: jfjoch_rocky8
distro: rocky8
cmake_flags: -DJFJOCH_USE_CUDA=ON
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet8-cuda12/upload
- runner: jfjoch_rocky8
distro: rocky8_sls9
cmake_flags: -DJFJOCH_USE_CUDA=ON -DSLS9=ON
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet9-cuda12/upload
- runner: jfjoch_rocky8
distro: rocky8_nocuda
cmake_flags: -DJFJOCH_USE_CUDA=OFF
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet8-nocuda/upload
- runner: jfjoch_rocky9
distro: rocky9
cmake_flags: -DJFJOCH_USE_CUDA=ON
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el9/slsdet8-cuda13/upload
- runner: jfjoch_rocky9
distro: rocky9_nocuda
cmake_flags: -DJFJOCH_USE_CUDA=OFF
pkg_glob: "*.rpm"
upload_url: https://gitea.psi.ch/api/packages/mx/rpm/centos/el9/slsdet8-nocuda/upload
- runner: jfjoch_ubuntu2204
distro: ubuntu2204
cmake_flags: -DJFJOCH_USE_CUDA=ON
pkg_glob: "*.deb"
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/jammy/cuda13/upload
- runner: jfjoch_ubuntu2204
distro: ubuntu2204_nocuda
cmake_flags: -DJFJOCH_USE_CUDA=OFF
pkg_glob: "*.deb"
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/jammy/nocuda/upload
- runner: jfjoch_ubuntu2404
distro: ubuntu2404
cmake_flags: -DJFJOCH_USE_CUDA=ON
pkg_glob: "*.deb"
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/noble/cuda13/upload
- runner: jfjoch_ubuntu2404
distro: ubuntu2404_nocuda
cmake_flags: -DJFJOCH_USE_CUDA=OFF
pkg_glob: "*.deb"
upload_url: https://gitea.psi.ch/api/packages/mx/debian/pool/noble/nocuda/upload
steps:
- uses: actions/checkout@v4
- name: Build packages
@@ -58,74 +75,21 @@ jobs:
- name: Upload packages
if: github.ref_type == 'tag'
shell: bash
env:
TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
set -euo pipefail
cd build
if [ "${{ matrix.distro }}" = "rocky8" ]; then
for file in *.rpm; do
if [ -f "$file" ]; then
curl --user __token__:${{ secrets.PIP_REPOSITORY_API_TOKEN }} \
--upload-file "$file" \
https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet8-cuda12/upload
fi
done
elif [ "${{ matrix.distro }}" = "rocky8_nocuda" ]; then
for file in *.rpm; do
if [ -f "$file" ]; then
curl --user __token__:${{ secrets.PIP_REPOSITORY_API_TOKEN }} \
--upload-file "$file" \
https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet8-nocuda/upload
fi
done
elif [ "${{ matrix.distro }}" = "rocky8_sls9" ]; then
for file in *.rpm; do
if [ -f "$file" ]; then
curl --user __token__:${{ secrets.PIP_REPOSITORY_API_TOKEN }} \
--upload-file "$file" \
https://gitea.psi.ch/api/packages/mx/rpm/centos/el8/slsdet9-cuda12/upload
fi
done
elif [ "${{ matrix.distro }}" = "rocky9_nocuda" ]; then
for file in *.rpm; do
if [ -f "$file" ]; then
curl --user __token__:${{ secrets.PIP_REPOSITORY_API_TOKEN }} \
--upload-file "$file" \
https://gitea.psi.ch/api/packages/mx/rpm/centos/el9/slsdet8-nocuda/upload
fi
done
elif [ "${{ matrix.distro }}" = "ubuntu2204_nocuda" ]; then
for file in *.deb; do
if [ -f "$file" ]; then
curl --user __token__:${{ secrets.PIP_REPOSITORY_API_TOKEN }} \
--upload-file "$file" \
https://gitea.psi.ch/api/packages/mx/debian/pool/jammy/nocuda/upload
fi
done
elif [ "${{ matrix.distro }}" = "ubuntu2204" ]; then
for file in *.deb; do
if [ -f "$file" ]; then
curl --user __token__:${{ secrets.PIP_REPOSITORY_API_TOKEN }} \
--upload-file "$file" \
https://gitea.psi.ch/api/packages/mx/debian/pool/jammy/cuda13/upload
fi
done
elif [ "${{ matrix.distro }}" = "ubuntu2404" ]; then
for file in *.deb; do
if [ -f "$file" ]; then
curl --user __token__:${{ secrets.PIP_REPOSITORY_API_TOKEN }} \
--upload-file "$file" \
https://gitea.psi.ch/api/packages/mx/debian/pool/noble/cuda13/upload
fi
done
fi
elif [ "${{ matrix.distro }}" = "ubuntu2404_nocuda" ]; then
for file in *.deb; do
if [ -f "$file" ]; then
curl --user __token__:${{ secrets.PIP_REPOSITORY_API_TOKEN }} \
--upload-file "$file" \
https://gitea.psi.ch/api/packages/mx/debian/pool/noble/nocuda/upload
fi
done
shopt -s nullglob
files=(${{ matrix.pkg_glob }})
if [ ${#files[@]} -eq 0 ]; then
echo "No package files found for pattern: ${{ matrix.pkg_glob }}"
exit 1
fi
for file in "${files[@]}"; do
echo "Uploading $file -> ${{ matrix.upload_url }}"
curl --fail --user __token__:"$TOKEN" --upload-file "$file" "${{ matrix.upload_url }}"
done
python-client:
name: Generate python client
runs-on: jfjoch_rocky8
@@ -139,30 +103,7 @@ jobs:
run: |
twine upload -u __token__ -p ${{ secrets.CI_PYPI_TOKEN }} --skip-existing dist/*
twine upload -u __token__ -p ${{ secrets.PIP_REPOSITORY_API_TOKEN }} --repository-url https://gitea.psi.ch/api/packages/mx/pypi dist/*
create-release:
name: Create release
runs-on: jfjoch_rocky8
# if: github.ref_type == 'tag'
steps:
- uses: actions/checkout@v4
with:
persist-credentials: 'true' # Optional; should be the default
- name: Configure auth and fetch LFS
shell: bash
env:
GITEA_TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
run: |
git lfs install --local
AUTH=$(git config --local http.${{ github.server_url }}/.extraheader)
git config --local --unset http.${{ github.server_url }}/.extraheader
git config --local http.${{ github.server_url }}/${{ github.repository }}.git/info/lfs/objects/batch.extraheader "$AUTH"
git lfs pull
- name: Create release
env:
GITEA_TOKEN: ${{ secrets.PIP_REPOSITORY_API_TOKEN }}
shell: bash
run: |
python3 gitea_create_release.py
documentation:
name: Build documentation
runs-on: jfjoch_rocky8
@@ -192,7 +133,6 @@ jobs:
unit-tests:
name: Unit tests
runs-on: jfjoch_rocky8
if: github.ref_type == 'tag'
container:
image: gitea.psi.ch/leonarski_f/jfjoch_rocky8:2509
options: --gpus all

View File

@@ -133,9 +133,11 @@ struct DataMessage {
std::optional<int64_t> original_number;
std::vector<Reflection> reflections;
std::vector<float> integration_logI;
std::vector<float> integration_one_over_d;
std::vector<float> integration_B_logI;
std::vector<float> integration_B_one_over_d;
std::vector<float> integration_Isigma;
std::vector<float> integration_Isigma_one_over_d;
std::optional<float> beam_corr_x;
std::optional<float> beam_corr_y;

View File

@@ -14,7 +14,6 @@ RUN dnf -y update && \
dnf-plugins-core \
epel-release \
git \
git-lfs \
which \
ca-certificates \
wget \
@@ -61,15 +60,12 @@ RUN dnf -y update && \
python3-pip \
python3-setuptools \
python3-wheel \
python3-requests \
twine \
openblas \
openblas-devel \
freetype-devel && \
dnf clean all && rm -rf /var/cache/dnf
RUN git lfs install
RUN set -eux; \
cd /tmp; \
curl -LO https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz; \

View File

@@ -15,7 +15,6 @@ RUN dnf -y update && \
dnf-plugins-core \
epel-release \
git \
git-lfs \
which \
ca-certificates \
wget \
@@ -33,7 +32,6 @@ RUN dnf -y update && \
redhat-rpm-config \
ninja-build \
python3 \
python3-requests \
perl \
pkgconf-pkg-config \
libxcb-devel \
@@ -63,8 +61,6 @@ RUN dnf -y update && \
freetype-devel && \
dnf clean all && rm -rf /var/cache/dnf
RUN git lfs install
RUN set -eux; \
cd /tmp; \
curl -LO https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz; \

View File

@@ -1,4 +1,11 @@
# Changelog
## 1.0.0.rc.87
This is an UNSTABLE release.
* jfjoch_viewer: Display more image metadata (angle / exposure time)
* jfjoch_viewer: Improve I/sigma and B-factor plots
* jfjoch_broker: Estimate resolution based on visible spots
## 1.0.0.rc.86
This is an UNSTABLE release.

Binary file not shown.

Binary file not shown.

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env python3
import os
import sys
from typing import Optional
try:
import requests
except ModuleNotFoundError:
print("ERROR: Python 'requests' package is required. Please install it (e.g., pip install requests).")
sys.exit(1)
gitea_api_url = "https://gitea.psi.ch/api/v1/"
repo_owner = "mx"
repo_name = "jungfraujoch"
gitea_token = os.getenv('GITEA_TOKEN')
if gitea_token is None:
print("ERROR: Required environment variables not set")
sys.exit(1)
def read_version():
try:
with open('VERSION', 'r') as f:
return f.readline().strip()
except FileNotFoundError:
print("ERROR: VERSION file not found")
sys.exit(1)
def create_release(version):
headers = {
'Authorization': f'token {gitea_token}',
'Content-Type': 'application/json',
}
data = {
'tag_name': version,
'name': f'Release {version}',
'draft': False,
'prerelease': '-rc.' in version or '-alpha.' in version or '-beta.' in version
}
url = f'{gitea_api_url}/repos/{repo_owner}/{repo_name}/releases'
response = requests.post(url, headers=headers, json=data)
if response.status_code != 201:
print(f"ERROR: Failed to create release. Status code: {response.status_code}")
print(response.text)
sys.exit(1)
release = response.json()
print(f"Successfully created release {version}")
return release
def get_release_by_tag(version: str):
"""
Fetch release information by tag name.
Returns the release object (dict) or exits on error.
"""
headers = {
'Authorization': f'token {gitea_token}',
'Content-Type': 'application/json',
}
url = f'{gitea_api_url}/repos/{repo_owner}/{repo_name}/releases/tags/{version}'
resp = requests.get(url, headers=headers)
if resp.status_code != 200:
print(f"ERROR: Failed to fetch release {version}. Status code: {resp.status_code}")
print(resp.text)
sys.exit(1)
return resp.json()
def upload_file_to_release(version: str, file_path: str, name: Optional[str] = None, label: Optional[str] = None):
"""
Upload a file as an asset to the release identified by the given version tag.
:param version: Tag name of the release (e.g., '1.2.3' or '1.2.3-rc.1')
:param file_path: Local file path to upload
:param name: Optional asset name to display in release (defaults to filename)
:param label: Optional asset label (display name)
"""
if not os.path.isfile(file_path):
print(f"ERROR: File not found: {file_path}")
sys.exit(1)
# Ensure the release exists and get its ID
release = get_release_by_tag(version)
release_id = release.get('id')
if release_id is None:
print("ERROR: Release ID not found in response")
sys.exit(1)
headers = {
'Authorization': f'token {gitea_token}',
}
asset_name = name if name else os.path.basename(file_path)
data = {'name': asset_name}
if label:
data['label'] = label
url = f'{gitea_api_url}/repos/{repo_owner}/{repo_name}/releases/{release_id}/assets'
with open(file_path, 'rb') as f:
files = {
'attachment': (asset_name, f, 'application/octet-stream'),
}
resp = requests.post(url, headers=headers, data=data, files=files)
if resp.status_code not in (201, 200):
print(f"ERROR: Failed to upload asset. Status code: {resp.status_code}")
print(resp.text)
sys.exit(1)
asset = resp.json()
browser_download_url = asset.get('browser_download_url') or asset.get('browser_url') or asset.get('url')
print(f"Uploaded asset '{asset_name}' to release {version}: {browser_download_url}")
if __name__ == '__main__':
version = read_version()
release = create_release(version)
upload_file_to_release(version, 'fpga/images/jfjoch_fpga_pcie_8x10g.mcs.gz', 'jfjoch_fpga_pcie_8x10g.mcs.gz', '8x10G FPGA image')
upload_file_to_release(version, 'fpga/images/jfjoch_fpga_pcie_100g.mcs.gz', 'jfjoch_fpga_pcie_100g.mcs.gz', '100G FPGA image')

View File

@@ -8,7 +8,7 @@
#include "spot_finding/StrongPixelSet.h"
#include "bragg_integration/BraggIntegrate2D.h"
#include "indexing/AnalyzeIndexing.h"
#include "bragg_integration/CalcISigma.h"
void SpotAnalyze(const DiffractionExperiment &experiment,
const SpotFindingSettings &spot_finding_settings,
@@ -86,16 +86,13 @@ void SpotAnalyze(const DiffractionExperiment &experiment,
ewald_dist_cutoff, output.number);
constexpr size_t kMaxReflections = 10000;
if (res.reflections.size() > kMaxReflections) {
output.reflections.assign(res.reflections.begin(),
res.reflections.begin() + kMaxReflections);
if (res.size() > kMaxReflections) {
output.reflections.assign(res.begin(), res.begin() + kMaxReflections);
} else
output.reflections = res.reflections;
output.reflections = res;
output.b_factor = res.b_factor;
output.integration_Isigma = res.Isigma;
output.integration_logI = res.logI;
output.integration_one_over_d = res.one_over_d;
CalcISigma(output);
CalcWilsonBFactor(output);
}
}
}

View File

@@ -4,13 +4,10 @@
#include "BraggIntegrate2D.h"
#include "BraggPrediction.h"
#include "BraggIntegrationStats.h"
template<class T>
bool IntegrateReflection(Reflection &r, const T *image, size_t xpixel, size_t ypixel,
int64_t special_value, int64_t saturation,
float r_3, float r_1_sq, float r_2_sq, float r_3_sq) {
r.I = 0;
r.bkg = 0;
@@ -68,24 +65,23 @@ bool IntegrateReflection(Reflection &r, const T *image, size_t xpixel, size_t yp
template<class T>
QuickIntegrateResult IntegrateInternal(const DiffractionExperiment &experiment,
const CompressedImage &image,
const CrystalLattice &latt,
float dist_from_ewald_sphere,
int64_t special_value,
int64_t saturation,
int64_t image_number) {
std::vector<Reflection> IntegrateInternal(const DiffractionExperiment &experiment,
const CompressedImage &image,
const CrystalLattice &latt,
float dist_from_ewald_sphere,
int64_t special_value,
int64_t saturation,
int64_t image_number) {
auto settings = experiment.GetBraggIntegrationSettings();
auto geom = experiment.GetDiffractionGeometry();
auto start = std::chrono::high_resolution_clock::now();
std::vector<uint8_t> buffer;
auto ptr = reinterpret_cast<const T*>(image.GetUncompressedPtr(buffer));
auto ptr = reinterpret_cast<const T *>(image.GetUncompressedPtr(buffer));
auto refl = CalcBraggPredictions(experiment, latt, settings.GetDMinLimit_A(), dist_from_ewald_sphere);
QuickIntegrateResult ret;
BraggIntegrationStats stats(6.0, settings.GetDMinLimit_A(), 20); // Wilson statistics is only linear in certain resolution range
std::vector<Reflection> ret;
float r_3 = settings.GetR3();
float r_1_sq = settings.GetR1() * settings.GetR1();
float r_2_sq = settings.GetR2() * settings.GetR2();
@@ -94,51 +90,52 @@ QuickIntegrateResult IntegrateInternal(const DiffractionExperiment &experiment,
for (const auto &[x, r_in]: refl) {
Reflection r = r_in;
if (IntegrateReflection(r, ptr, image.GetWidth(), image.GetHeight(), special_value, saturation,
r_3, r_1_sq, r_2_sq, r_3_sq)) {
r_3, r_1_sq, r_2_sq, r_3_sq)) {
if (experiment.GetPolarizationFactor()) {
float pol = geom.CalcAzIntPolarizationCorr(r.predicted_x, r.predicted_y, experiment.GetPolarizationFactor().value());
float pol = geom.CalcAzIntPolarizationCorr(r.predicted_x, r.predicted_y,
experiment.GetPolarizationFactor().value());
r.I /= pol;
r.bkg /= pol;
r.sigma /= pol;
}
r.image_number = static_cast<int>(image_number);
ret.reflections.push_back(r);
stats.AddReflection(r);
ret.push_back(r);
}
}
ret.b_factor = stats.BFactor();
ret.logI = stats.GetWilsonPlot();
ret.one_over_d = stats.GetWilsonPlotLegend();
ret.Isigma = stats.GetISigmaPlot();
return ret;
}
QuickIntegrateResult BraggIntegrate2D(const DiffractionExperiment &experiment,
const CompressedImage &image,
const CrystalLattice &latt,
float dist_from_ewald_sphere,
int64_t image_number) {
std::vector<Reflection> BraggIntegrate2D(const DiffractionExperiment &experiment,
const CompressedImage &image,
const CrystalLattice &latt,
float dist_from_ewald_sphere,
int64_t image_number) {
if (image.GetCompressedSize() == 0)
return {};
switch (image.GetMode()) {
case CompressedImageMode::Int8:
return IntegrateInternal<int8_t>(experiment, image, latt, dist_from_ewald_sphere, INT8_MIN, INT8_MAX, image_number);
return IntegrateInternal<int8_t>(experiment, image, latt, dist_from_ewald_sphere, INT8_MIN, INT8_MAX,
image_number);
case CompressedImageMode::Int16:
return IntegrateInternal<int16_t>(experiment, image, latt, dist_from_ewald_sphere, INT16_MIN, INT16_MAX, image_number);
return IntegrateInternal<int16_t>(experiment, image, latt, dist_from_ewald_sphere, INT16_MIN, INT16_MAX,
image_number);
case CompressedImageMode::Int32:
return IntegrateInternal<int32_t>(experiment, image, latt, dist_from_ewald_sphere, INT32_MIN, INT32_MAX, image_number);
return IntegrateInternal<int32_t>(experiment, image, latt, dist_from_ewald_sphere, INT32_MIN, INT32_MAX,
image_number);
case CompressedImageMode::Uint8:
return IntegrateInternal<uint8_t>(experiment, image, latt, dist_from_ewald_sphere, UINT8_MAX, UINT8_MAX, image_number);
return IntegrateInternal<uint8_t>(experiment, image, latt, dist_from_ewald_sphere, UINT8_MAX, UINT8_MAX,
image_number);
case CompressedImageMode::Uint16:
return IntegrateInternal<uint16_t>(experiment, image, latt, dist_from_ewald_sphere, UINT16_MAX, UINT16_MAX, image_number);
return IntegrateInternal<uint16_t>(experiment, image, latt, dist_from_ewald_sphere, UINT16_MAX, UINT16_MAX,
image_number);
case CompressedImageMode::Uint32:
return IntegrateInternal<uint16_t>(experiment, image, latt, dist_from_ewald_sphere, UINT32_MAX, UINT32_MAX, image_number);
return IntegrateInternal<uint16_t>(experiment, image, latt, dist_from_ewald_sphere, UINT32_MAX, UINT32_MAX,
image_number);
default:
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Image mode not supported");
}
}

View File

@@ -9,18 +9,10 @@
#include "../../common/CrystalLattice.h"
#include "../../common/Reflection.h"
struct QuickIntegrateResult {
std::vector<Reflection> reflections;
std::optional<float> b_factor;
std::vector<float> logI;
std::vector<float> one_over_d;
std::vector<float> Isigma;
};
QuickIntegrateResult BraggIntegrate2D(const DiffractionExperiment &experiment,
const CompressedImage &image,
const CrystalLattice &latt,
float dist_from_ewald_sphere,
int64_t image_number);
std::vector<Reflection> BraggIntegrate2D(const DiffractionExperiment &experiment,
const CompressedImage &image,
const CrystalLattice &latt,
float dist_from_ewald_sphere,
int64_t image_number);
#endif //JFJOCH_BRAGGINTEGRATE2D_H

View File

@@ -39,12 +39,12 @@ std::optional<float> BraggIntegrationStats::BFactor() {
+ (static_cast<float>(i) + 0.5) * (one_over_dmin - one_over_dmax) / static_cast<float>(nshells);
wilson_plot_legend.push_back(one_over_d);
if (count[i] == 0 )
plot_Isigma.push_back(NAN);
plot_Isigma.push_back(0);
else
plot_Isigma.push_back(I_sigma[i] / count[i]);
if (count[i] == 0 || I[i] < 0.0 || I_sigma[i] < 0.2f * count[i])
wilson_plot.push_back(NAN);
wilson_plot.push_back(0);
else
wilson_plot.push_back(log(I[i] / count[i]));
}
@@ -60,7 +60,7 @@ std::vector<float> BraggIntegrationStats::GetWilsonPlot() {
return wilson_plot;
}
std::vector<float> BraggIntegrationStats::GetWilsonPlotLegend() {
std::vector<float> BraggIntegrationStats::GetOneOverDPlot() {
return wilson_plot_legend;
}

View File

@@ -23,11 +23,11 @@ class BraggIntegrationStats {
std::vector<float> wilson_plot_legend;
std::vector<float> plot_Isigma;
public:
explicit BraggIntegrationStats(float d_max_A = 6.0, float d_min_A = 1.5, int32_t nshells = 20);
explicit BraggIntegrationStats(float d_max_A, float d_min_A, int32_t nshells);
void AddReflection(const Reflection &r);
std::optional<float> BFactor();
std::vector<float> GetWilsonPlot();
std::vector<float> GetWilsonPlotLegend();
std::vector<float> GetOneOverDPlot();
std::vector<float> GetISigmaPlot();
};

View File

@@ -6,6 +6,8 @@ ADD_LIBRARY(JFJochBraggIntegration STATIC
BraggIntegrationStats.cpp
BraggIntegrationStats.h
Regression.h
CalcISigma.cpp
CalcISigma.h
)
TARGET_LINK_LIBRARIES(JFJochBraggIntegration JFJochCommon)

View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "CalcISigma.h"
#include "BraggIntegrationStats.h"
void CalcISigma(DataMessage &msg) {
if (msg.reflections.empty())
return;
BraggIntegrationStats stats(50.0, 1.5, 20);
for (const auto &r: msg.reflections)
stats.AddReflection(r);
stats.BFactor(); // need to do it to generate plots...
msg.integration_Isigma = stats.GetISigmaPlot();
msg.integration_Isigma_one_over_d = stats.GetOneOverDPlot();
}
void CalcWilsonBFactor(DataMessage &msg) {
if (msg.reflections.empty())
return;
BraggIntegrationStats stats(6.0, 3.0, 10); // Wilson statistics is only linear in certain resolution range
for (const auto &r: msg.reflections)
stats.AddReflection(r);
msg.b_factor = stats.BFactor();
msg.integration_B_logI = stats.GetWilsonPlot();
msg.integration_B_one_over_d = stats.GetOneOverDPlot();
}

View File

@@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#ifndef JFJOCH_CALCISIGMA_H
#define JFJOCH_CALCISIGMA_H
#include "../../common/JFJochMessages.h"
void CalcISigma(DataMessage &msg);
void CalcWilsonBFactor(DataMessage &msg);
#endif //JFJOCH_CALCISIGMA_H

View File

@@ -19,7 +19,7 @@ struct SpotFindingSettings {
float ice_ring_width_Q_recipA = 0.02;
bool indexing = true;
bool quick_integration = false;
bool quick_integration = true;
};
#endif //JUNGFRAUJOCH_SPOTFINDINGSETTINGS_H

View File

@@ -4,6 +4,7 @@
#include "JFJochHDF5Reader.h"
#include "spdlog/fmt/fmt.h"
#include "../image_analysis/bragg_integration/BraggIntegrationStats.h"
#include "../image_analysis/bragg_integration/CalcISigma.h"
std::vector<hsize_t> GetDimension(HDF5Object& object, const std::string& path) {
const auto dim = object.GetDimension(path);
@@ -542,8 +543,6 @@ bool JFJochHDF5Reader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset
|| h.size() != predicted_x.size() || h.size() != predicted_y.size()
|| h.size() != int_sum.size() || h.size() != int_err.size() || h.size() != bkg.size())
throw JFJochException(JFJochExceptionCategory::HDF5, "Wrong size of reflections dataset");
BraggIntegrationSettings settings;
BraggIntegrationStats stats(6.0, settings.GetDMinLimit_A(), 10); // Wilson statistics is only linear in certain resolution range
for (size_t i = 0; i < h.size(); i++) {
Reflection r{
@@ -557,13 +556,10 @@ bool JFJochHDF5Reader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset
.bkg = bkg.at(i),
.sigma = int_err.at(i)
};
stats.AddReflection(r);
message.reflections.emplace_back(r);
}
message.integration_logI = stats.GetWilsonPlot();
message.integration_one_over_d = stats.GetWilsonPlotLegend();
message.integration_Isigma = stats.GetISigmaPlot();
CalcISigma(message);
CalcWilsonBFactor(message);
}
return true;

View File

@@ -8,6 +8,7 @@
#include "../frame_serialize/CBORStream2Deserializer.h"
#include "../broker/gen/model/Image_buffer_status.h"
#include "../broker/gen/model/Plots.h"
#include "../image_analysis/bragg_integration/CalcISigma.h"
void JFJochHttpReader::Close_i() {
addr = "";
@@ -155,6 +156,10 @@ bool JFJochHttpReader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset
return false;
message = *msg->data_message;
CalcISigma(message);
CalcWilsonBFactor(message);
return true;
} catch (std::exception &e) {
return false;

View File

@@ -3,6 +3,7 @@
#include "JFJochViewerImageStatistics.h"
#include <QFormLayout>
#include <QRegularExpression>
static QString mkSourceInstrumentText(const DiffractionExperiment& exp) {
QString src, inst;
@@ -61,11 +62,11 @@ JFJochViewerImageStatistics::JFJochViewerImageStatistics(QWidget *parent) : QWid
sample_name = new QLabel(this);
layout->addRow("Sample:", sample_name);
error_pixels = new QLabel(this);
layout->addRow(new QLabel("Error pixels:"), error_pixels);
exposure_time = new QLabel(this);
layout->addRow(new QLabel("Exposure Time:"), exposure_time);
sat_pixels = new QLabel(this);
layout->addRow(new QLabel("Sat. pixels:"), sat_pixels);
rotation_angle = new QLabel(this);
layout->addRow(new QLabel("Image angle:"), rotation_angle);
valid_values = new QLabel(this);
layout->addRow(new QLabel("Valid values:"), valid_values);
@@ -99,6 +100,17 @@ JFJochViewerImageStatistics::JFJochViewerImageStatistics(QWidget *parent) : QWid
}
QString TrimZeros(double number, int precision) {
auto s = QString::number(number, 'f', precision);
s.remove(QRegularExpression("\\.?0+$"));
if (s.isEmpty()) s = "0";
return s;
}
QString FormatTime(std::chrono::microseconds time) {
return TrimZeros(time.count()/1e6, 6);
}
void JFJochViewerImageStatistics::loadImage(std::shared_ptr<const JFJochReaderImage> image) {
if (!image) {
source_name->setText("");
@@ -107,9 +119,12 @@ void JFJochViewerImageStatistics::loadImage(std::shared_ptr<const JFJochReaderIm
dataset_name->setToolTip("");
detector_name->setText("");
detector_name->setToolTip("");
error_pixels->setText("");
sat_pixels->setText("");
exposure_time->setText("");
exposure_time->setToolTip("");
rotation_angle->setText("");
rotation_angle->setToolTip("");
valid_values->setText("");
valid_values->setToolTip("");
spots->setText("");
bkg_estimate->setText("");
indexed->setText("");
@@ -146,16 +161,24 @@ void JFJochViewerImageStatistics::loadImage(std::shared_ptr<const JFJochReaderIm
detector_name->setText(text);
source_name->setText(mkSourceInstrumentText(exp));
source_name->setToolTip(mkSampleTooltip(exp));
source_name->setToolTip(mkSourceInstrumentTooltip(exp));
sample_name->setText(mkSampleText(exp));
sample_name->setToolTip(mkSampleTooltip(exp));
text = QString("<b>%1</b>").arg(image->ErrorPixels().size());
error_pixels->setText(text);
text = QString("<b>%1</b>").arg(image->SaturatedPixels().size());
sat_pixels->setText(text);
if (exp.GetGoniometer()) {
rotation_angle->setText(QString("<b>%1°</b>")
.arg(TrimZeros(exp.GetGoniometer()->GetIncrement_deg(), 3)));
rotation_angle->setToolTip(QString("Start angle: <b>%1°</b><br/>This image: <b>%2°</b>")
.arg(TrimZeros(exp.GetGoniometer()->GetStart_deg(), 3))
.arg(TrimZeros(exp.GetGoniometer()->GetAngle_deg(image->ImageData().number), 3))
);
} else {
rotation_angle->setText(QString("-"));
rotation_angle->setToolTip("");
}
exposure_time->setText(QString("<b>%1</b> s").arg(FormatTime(exp.GetImageTime())));
exposure_time->setToolTip(QString("Count time: <b>%1</b> s<br/>").arg(FormatTime(exp.GetImageCountTime())));
text = QString("<b>%1</b>").arg(image->ImageData().spots.size());
spots->setText(text);
@@ -170,6 +193,8 @@ void JFJochViewerImageStatistics::loadImage(std::shared_ptr<const JFJochReaderIm
.arg(image->ValidPixels().begin()->first)
.arg(image->ValidPixels().rbegin()->first);
valid_values->setText(text);
valid_values->setToolTip(QString("Error pixels: <b>%1</b><br/>Saturated pixels: <b>%2</b>")
.arg(image->ErrorPixels().size()).arg(image->SaturatedPixels().size()));
if (!image->Dataset().bkg_estimate.empty()) {
text = QString("<b>%1</b>").arg(image->ImageData().bkg_estimate.value_or(0));

View File

@@ -15,8 +15,8 @@ class JFJochViewerImageStatistics : public QWidget {
QLabel *sample_name;
QLabel *dataset_name;
QLabel *detector_name;
QLabel *error_pixels;
QLabel *sat_pixels;
QLabel *exposure_time;
QLabel *rotation_angle;
QLabel *spots;
QLabel *valid_values;
QLabel *indexed;

View File

@@ -24,8 +24,8 @@ JFJochViewerSidePanelChart::JFJochViewerSidePanelChart(QWidget *parent) : QWidge
azint_image = new JFJochAzIntImageView(this);
stack = new QStackedWidget(this);
stack->addWidget(azint_plot); // index 0
stack->addWidget(azint_image); // index 1
stack->addWidget(azint_plot); // index 0
stack->addWidget(azint_image); // index 1
layout->addWidget(stack);
setLayout(layout);
@@ -44,7 +44,8 @@ void JFJochViewerSidePanelChart::comboBoxSelected(int val) {
}
void JFJochViewerSidePanelChart::redrawPlot() {
std::vector<float> x,y;
std::vector<float> x, y;
bool one_over_x = false;
if (image) {
auto index = combo_box->currentIndex();
switch (combo_box->itemData(index).toInt()) {
@@ -53,12 +54,14 @@ void JFJochViewerSidePanelChart::redrawPlot() {
y = image->GetAzInt1D();
break;
case 1:
x = image->ImageData().integration_one_over_d;
y = image->ImageData().integration_logI;
x = image->ImageData().integration_B_one_over_d;
y = image->ImageData().integration_B_logI;
one_over_x = true;
break;
case 2:
x = image->ImageData().integration_one_over_d;
x = image->ImageData().integration_Isigma_one_over_d;
y = image->ImageData().integration_Isigma;
one_over_x = true;
break;
case 4:
x = image->Dataset().experiment.GetFluorescenceSpectrum().GetEnergy_eV();
@@ -83,7 +86,7 @@ void JFJochViewerSidePanelChart::redrawPlot() {
}
}
if (!x.empty() && !y.empty()) {
azint_plot->UpdateData(x,y);
azint_plot->UpdateData(x, y, one_over_x);
} else {
azint_plot->ClearData();
}
@@ -92,4 +95,4 @@ void JFJochViewerSidePanelChart::redrawPlot() {
void JFJochViewerSidePanelChart::loadImage(std::shared_ptr<const JFJochReaderImage> in_image) {
image = in_image;
redrawPlot();
}
}

View File

@@ -6,6 +6,7 @@
#include <QMenu>
#include <QClipboard>
#include <QApplication>
#include <QCategoryAxis>
JFJochSimpleChartView::JFJochSimpleChartView(QWidget *parent)
: QChartView(new QChart(), parent) {
@@ -15,16 +16,58 @@ JFJochSimpleChartView::JFJochSimpleChartView(QWidget *parent)
//setRubberBand(QChartView::RubberBand::HorizontalRubberBand);
}
void JFJochSimpleChartView::UpdateData(const std::vector<float> &in_x, const std::vector<float> &in_y) {
x = in_x;
y = in_y;
void JFJochSimpleChartView::UpdateData(const std::vector<float> &in_x, const std::vector<float> &in_y, bool one_over_x) {
x = in_x; y = in_y;
chart()->removeAllSeries();
if (!x.empty() && x.size() == y.size()) {
series = new QLineSeries(this);
for (int i = 0; i < x.size(); i++)
series->append(x[i], in_y[i]);
chart()->addSeries(series);
chart()->createDefaultAxes();
// Remove all axes to avoid duplicates
for (auto ax : chart()->axes()) chart()->removeAxis(ax);
if (x.empty() || x.size() != y.size()) return;
auto* series = new QLineSeries(this);
for (size_t i = 0; i < x.size(); ++i) series->append(x[i], y[i]);
chart()->addSeries(series);
auto* axY = new QValueAxis();
chart()->addAxis(axY, Qt::AlignLeft);
series->attachAxis(axY);
// Build X range
auto [minXIt, maxXIt] = std::minmax_element(x.begin(), x.end());
const double xmin = *minXIt, xmax = *maxXIt;
if (one_over_x) {
// Hidden X value axis on top for grid/range
auto* axXTop = new QValueAxis();
axXTop->setRange(xmin, xmax);
axXTop->setTickCount(5);
axXTop->setLabelsVisible(false);
chart()->addAxis(axXTop, Qt::AlignTop);
series->attachAxis(axXTop);
// Visible category axis at bottom for 1/x labels
auto* axXcat = new QCategoryAxis();
axXcat->setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue);
axXcat->setGridLineVisible(false);
axXcat->setMinorGridLineVisible(false);
const int tickCount = axXTop->tickCount();
const double step = (tickCount > 1) ? (xmax - xmin) / (tickCount - 1) : 0.0;
for (int i = 0; i < tickCount; ++i) {
const double xv = xmin + i * step;
const QString lab = (std::abs(xv) < 1e-12)
? QStringLiteral("")
: QString::number(1.0 / xv, 'g', 4);
axXcat->append(lab, xv);
}
chart()->addAxis(axXcat, Qt::AlignBottom);
series->attachAxis(axXcat);
} else {
// Normal value axis at bottom
auto* axX = new QValueAxis();
axX->setRange(xmin, xmax);
chart()->addAxis(axX, Qt::AlignBottom);
series->attachAxis(axX);
}
}
@@ -51,3 +94,31 @@ void JFJochSimpleChartView::contextMenuEvent(QContextMenuEvent *event) {
cb->setText(out);
}
}
void JFJochSimpleChartView::applyInverseXLabels(QValueAxis* axX, QLineSeries* s) {
axX->setTickCount(5); // adjust as needed
axX->setLabelsVisible(false); // hide default numeric labels
auto* axXcat = new QCategoryAxis();
axXcat->setLabelsPosition(QCategoryAxis::AxisLabelsPositionOnValue);
axXcat->setGridLineVisible(false);
axXcat->setMinorGridLineVisible(false);
const int tickCount = axX->tickCount();
const double min = axX->min();
const double max = axX->max();
const double step = (tickCount > 1) ? (max - min) / (tickCount - 1) : 0.0;
for (int i = 0; i < tickCount; ++i) {
const double xv = min + i * step;
if (std::abs(xv) < 1e-12) {
axXcat->append("", xv);
} else {
const double inv = 1.0 / xv;
axXcat->append(QString::number(inv, 'g', 4), xv);
}
}
chart()->addAxis(axXcat, Qt::AlignBottom);
s->attachAxis(axXcat);
}

View File

@@ -11,15 +11,16 @@
class JFJochSimpleChartView : public QChartView {
Q_OBJECT
QLineSeries *series = nullptr;
std::vector<float> x, y;
void applyInverseXLabels(QValueAxis* axX, QLineSeries* s);
void Plot();
protected:
void contextMenuEvent(QContextMenuEvent *event) override;
public:
JFJochSimpleChartView(QWidget *parent = nullptr);
void UpdateData(const std::vector<float> &in_x, const std::vector<float> &in_y);
void UpdateData(const std::vector<float> &in_x, const std::vector<float> &in_y, bool one_over_x);
void ClearData();
};