style: format with ruff
This commit is contained in:
+38
-48
@@ -5,29 +5,29 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../src'))
|
||||
sys.path.insert(0, os.path.abspath('../src/aare'))
|
||||
sys.path.insert(0, os.path.abspath('../src/aare/daq'))
|
||||
sys.path.insert(0, os.path.abspath('../src/aare/common'))
|
||||
sys.path.insert(0, os.path.abspath('../src/aare/gui'))
|
||||
sys.path.insert(0, os.path.abspath("../src"))
|
||||
sys.path.insert(0, os.path.abspath("../src/aare"))
|
||||
sys.path.insert(0, os.path.abspath("../src/aare/daq"))
|
||||
sys.path.insert(0, os.path.abspath("../src/aare/common"))
|
||||
sys.path.insert(0, os.path.abspath("../src/aare/gui"))
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = 'AareDAQ'
|
||||
copyright = '2026, Paul Scherrer Institute'
|
||||
author = 'Martin Appleby, Guillaume Gotthard, Filip Leonarski and Dawn (Jiaxin) Daun'
|
||||
release = '0.3.1'
|
||||
master_doc = 'index'
|
||||
root_doc = 'index'
|
||||
project = "AareDAQ"
|
||||
copyright = "2026, Paul Scherrer Institute"
|
||||
author = "Martin Appleby, Guillaume Gotthard, Filip Leonarski and Dawn (Jiaxin) Daun"
|
||||
release = "0.3.1"
|
||||
master_doc = "index"
|
||||
root_doc = "index"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
autodoc_mock_imports = [
|
||||
'aaredb',
|
||||
'jfjoch_client',
|
||||
'numpy',
|
||||
'requests',
|
||||
"aaredb",
|
||||
"jfjoch_client",
|
||||
"numpy",
|
||||
"requests",
|
||||
"pyJW",
|
||||
"pyzmq",
|
||||
"opencv-python-headless",
|
||||
@@ -43,16 +43,16 @@ autodoc_mock_imports = [
|
||||
"psi-pshell",
|
||||
]
|
||||
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
extensions = [
|
||||
# 'fuse_search',
|
||||
'sphinx.ext.intersphinx',
|
||||
'myst_parser',
|
||||
'sphinx_immaterial',
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.napoleon',
|
||||
'sphinx.ext.viewcode',
|
||||
"sphinx.ext.intersphinx",
|
||||
"myst_parser",
|
||||
"sphinx_immaterial",
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.viewcode",
|
||||
]
|
||||
|
||||
# Optional: allow includes in Markdown
|
||||
@@ -63,11 +63,11 @@ myst_enable_extensions = [
|
||||
"html_image",
|
||||
"linkify",
|
||||
"substitution",
|
||||
'smartquotes',
|
||||
"smartquotes",
|
||||
"tasklist",
|
||||
]
|
||||
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
myst_heading_anchor = 3
|
||||
|
||||
|
||||
@@ -86,10 +86,10 @@ myst_heading_anchor = 3
|
||||
# ]
|
||||
# }
|
||||
|
||||
#Static Files
|
||||
html_static_path = ['_static']
|
||||
# Static Files
|
||||
html_static_path = ["_static"]
|
||||
html_css_files = [
|
||||
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css',
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
|
||||
# 'css/fuse-search.css',
|
||||
]
|
||||
# html_js_files = [
|
||||
@@ -100,11 +100,11 @@ html_css_files = [
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
html_theme = 'sphinx_immaterial'
|
||||
html_theme = "sphinx_immaterial"
|
||||
|
||||
html_theme_options = {
|
||||
"icon": {
|
||||
"repo": "fontawesome/brands/github", # or gitlab/gitea icon
|
||||
"repo": "fontawesome/brands/github" # or gitlab/gitea icon
|
||||
},
|
||||
"site_url": "https://gitea.psi.ch/mx/aaredaq",
|
||||
"repo_url": "https://gitea.psi.ch/mx/aaredaq",
|
||||
@@ -114,7 +114,7 @@ html_theme_options = {
|
||||
"icon": "fontawesome/brands/github",
|
||||
"link": "https://gitea.psi.ch/mx/aaredaq",
|
||||
"name": "AareDAQ on Gitea",
|
||||
},
|
||||
}
|
||||
],
|
||||
"palette": [
|
||||
{
|
||||
@@ -122,20 +122,14 @@ html_theme_options = {
|
||||
"scheme": "default",
|
||||
"primary": "indigo",
|
||||
"accent": "lime",
|
||||
"toggle": {
|
||||
"icon": "material/lightbulb-outline",
|
||||
"name": "Switch to dark mode",
|
||||
},
|
||||
"toggle": {"icon": "material/lightbulb-outline", "name": "Switch to dark mode"},
|
||||
},
|
||||
{
|
||||
"media": "(prefers-color-scheme: dark)",
|
||||
"scheme": "slate",
|
||||
"primary": "indigo",
|
||||
"accent": "lime",
|
||||
"toggle": {
|
||||
"icon": "material/lightbulb",
|
||||
"name": "Switch to light mode",
|
||||
},
|
||||
"toggle": {"icon": "material/lightbulb", "name": "Switch to light mode"},
|
||||
},
|
||||
],
|
||||
"features": [
|
||||
@@ -152,14 +146,10 @@ html_theme_options = {
|
||||
"globaltoc_collapse": False,
|
||||
"toc_title": "On this page",
|
||||
"toc_title_container": True,
|
||||
"font": {
|
||||
"code": "JetBrains Mono",
|
||||
},
|
||||
"font": {"code": "JetBrains Mono"},
|
||||
}
|
||||
html_theme_options["google_fonts"] = []
|
||||
html_sidebars = {
|
||||
"**": ["logo-text.html", "globaltoc.html", "searchbox.html"]
|
||||
}
|
||||
html_sidebars = {"**": ["logo-text.html", "globaltoc.html", "searchbox.html"]}
|
||||
|
||||
|
||||
html_show_sourcelink = False
|
||||
@@ -167,8 +157,8 @@ html_show_sourcelink = False
|
||||
|
||||
# Optional
|
||||
autodoc_default_options = {
|
||||
'members': True,
|
||||
'undoc-members': True,
|
||||
'show-inheritance': True,
|
||||
'member-order': 'bysource',
|
||||
"members": True,
|
||||
"undoc-members": True,
|
||||
"show-inheritance": True,
|
||||
"member-order": "bysource",
|
||||
}
|
||||
|
||||
@@ -65,3 +65,18 @@ markers = [
|
||||
[build-system]
|
||||
requires = ["setuptools>=75.6.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
skip-magic-trailing-comma = true
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 100
|
||||
multi_line_output = 3
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.format]
|
||||
skip-magic-trailing-comma = true
|
||||
|
||||
+19
-11
@@ -1,6 +1,9 @@
|
||||
import cv2
|
||||
#%%
|
||||
bkg_old = cv2.imread("/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_50_50_old.jpg")
|
||||
|
||||
# %%
|
||||
bkg_old = cv2.imread(
|
||||
"/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_50_50_old.jpg"
|
||||
)
|
||||
bkg_new = cv2.imread("/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg280_50_50.jpg")
|
||||
grey_old = cv2.cvtColor(bkg_old, cv2.COLOR_RGB2GRAY)
|
||||
grey_new = cv2.cvtColor(bkg_new, cv2.COLOR_RGB2GRAY)
|
||||
@@ -8,17 +11,22 @@ diff_image = cv2.absdiff(grey_old, grey_new)
|
||||
_, thresh_diff = cv2.threshold(diff_image, 1, 255, cv2.THRESH_BINARY)
|
||||
|
||||
# Display the results
|
||||
cv2.imshow('Original Image 1', bkg_old)
|
||||
cv2.imshow('Original Image 2', bkg_new)
|
||||
cv2.imshow('Grayscale 1', grey_old)
|
||||
cv2.imshow('Grayscale 2', grey_new)
|
||||
cv2.imshow('Difference Image', diff_image)
|
||||
cv2.imshow('Thresholded Difference', thresh_diff)
|
||||
cv2.imshow("Original Image 1", bkg_old)
|
||||
cv2.imshow("Original Image 2", bkg_new)
|
||||
cv2.imshow("Grayscale 1", grey_old)
|
||||
cv2.imshow("Grayscale 2", grey_new)
|
||||
cv2.imshow("Difference Image", diff_image)
|
||||
cv2.imshow("Thresholded Difference", thresh_diff)
|
||||
|
||||
# Save the difference image
|
||||
cv2.imwrite('/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg_difference_image.jpg', diff_image)
|
||||
cv2.imwrite('/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg_thresholded_difference.jpg', thresh_diff)
|
||||
cv2.imwrite(
|
||||
"/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg_difference_image.jpg", diff_image
|
||||
)
|
||||
cv2.imwrite(
|
||||
"/home/leonarski_f/aaredaq/daq/src/aaredaq/alc_test_images/bkg_thresholded_difference.jpg",
|
||||
thresh_diff,
|
||||
)
|
||||
|
||||
# Wait for a key press and close all windows
|
||||
cv2.waitKey(0)
|
||||
cv2.destroyAllWindows()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
+114
-90
@@ -58,10 +58,16 @@ class ImageStatsReceiver:
|
||||
x_coords = np.arange(len(x_projection))
|
||||
y_coords = np.arange(len(y_projection))
|
||||
|
||||
x_centroid = np.sum(x_coords * x_projection) / np.sum(x_projection) if np.sum(x_projection) > 0 else len(
|
||||
x_projection) / 2
|
||||
y_centroid = np.sum(y_coords * y_projection) / np.sum(y_projection) if np.sum(y_projection) > 0 else len(
|
||||
y_projection) / 2
|
||||
x_centroid = (
|
||||
np.sum(x_coords * x_projection) / np.sum(x_projection)
|
||||
if np.sum(x_projection) > 0
|
||||
else len(x_projection) / 2
|
||||
)
|
||||
y_centroid = (
|
||||
np.sum(y_coords * y_projection) / np.sum(y_projection)
|
||||
if np.sum(y_projection) > 0
|
||||
else len(y_projection) / 2
|
||||
)
|
||||
|
||||
# Calculate FWHM (Full Width at Half Maximum) approximation
|
||||
def calculate_fwhm(profile):
|
||||
@@ -76,20 +82,20 @@ class ImageStatsReceiver:
|
||||
y_fwhm = calculate_fwhm(y_projection)
|
||||
|
||||
return {
|
||||
'x_projection': x_projection.tolist(),
|
||||
'y_projection': y_projection.tolist(),
|
||||
'x_projection_max': x_projection_max.tolist(),
|
||||
'y_projection_max': y_projection_max.tolist(),
|
||||
'x_peak_position': int(x_peak_idx),
|
||||
'y_peak_position': int(y_peak_idx),
|
||||
'x_peak_value': float(x_peak_value),
|
||||
'y_peak_value': float(y_peak_value),
|
||||
'x_centroid': float(x_centroid),
|
||||
'y_centroid': float(y_centroid),
|
||||
'x_fwhm': float(x_fwhm),
|
||||
'y_fwhm': float(y_fwhm),
|
||||
'x_coords': list(range(len(x_projection))),
|
||||
'y_coords': list(range(len(y_projection)))
|
||||
"x_projection": x_projection.tolist(),
|
||||
"y_projection": y_projection.tolist(),
|
||||
"x_projection_max": x_projection_max.tolist(),
|
||||
"y_projection_max": y_projection_max.tolist(),
|
||||
"x_peak_position": int(x_peak_idx),
|
||||
"y_peak_position": int(y_peak_idx),
|
||||
"x_peak_value": float(x_peak_value),
|
||||
"y_peak_value": float(y_peak_value),
|
||||
"x_centroid": float(x_centroid),
|
||||
"y_centroid": float(y_centroid),
|
||||
"x_fwhm": float(x_fwhm),
|
||||
"y_fwhm": float(y_fwhm),
|
||||
"x_coords": list(range(len(x_projection))),
|
||||
"y_coords": list(range(len(y_projection))),
|
||||
}
|
||||
|
||||
def calculate_radial_integration(self, image: np.ndarray, num_bins: int = 50) -> Dict[str, Any]:
|
||||
@@ -104,7 +110,7 @@ class ImageStatsReceiver:
|
||||
center_y, center_x = np.array(gray_image.shape) // 2
|
||||
|
||||
# Create coordinate grids
|
||||
y_coords, x_coords = np.ogrid[:gray_image.shape[0], :gray_image.shape[1]]
|
||||
y_coords, x_coords = np.ogrid[: gray_image.shape[0], : gray_image.shape[1]]
|
||||
|
||||
# Calculate distance from center for each pixel
|
||||
distances = np.sqrt((x_coords - center_x) ** 2 + (y_coords - center_y) ** 2)
|
||||
@@ -137,13 +143,13 @@ class ImageStatsReceiver:
|
||||
pixel_counts.append(0)
|
||||
|
||||
return {
|
||||
'r_centers': r_centers.tolist(),
|
||||
'radial_profile': radial_profile,
|
||||
'radial_std': radial_std,
|
||||
'pixel_counts': pixel_counts,
|
||||
'max_radius': float(max_radius),
|
||||
'center': [int(center_x), int(center_y)],
|
||||
'num_bins': num_bins
|
||||
"r_centers": r_centers.tolist(),
|
||||
"radial_profile": radial_profile,
|
||||
"radial_std": radial_std,
|
||||
"pixel_counts": pixel_counts,
|
||||
"max_radius": float(max_radius),
|
||||
"center": [int(center_x), int(center_y)],
|
||||
"num_bins": num_bins,
|
||||
}
|
||||
|
||||
def calculate_image_stats(self, image: np.ndarray) -> Dict[str, Any]:
|
||||
@@ -151,30 +157,30 @@ class ImageStatsReceiver:
|
||||
image_float = image.astype(np.float64)
|
||||
|
||||
stats = {
|
||||
'timestamp': time.time(),
|
||||
'shape': image.shape,
|
||||
'dtype': str(image.dtype),
|
||||
'mean': float(np.mean(image_float)),
|
||||
'std': float(np.std(image_float)),
|
||||
'median': float(np.median(image_float)),
|
||||
'min': float(np.min(image_float)),
|
||||
'max': float(np.max(image_float)),
|
||||
'message_count': self.message_count
|
||||
"timestamp": time.time(),
|
||||
"shape": image.shape,
|
||||
"dtype": str(image.dtype),
|
||||
"mean": float(np.mean(image_float)),
|
||||
"std": float(np.std(image_float)),
|
||||
"median": float(np.median(image_float)),
|
||||
"min": float(np.min(image_float)),
|
||||
"max": float(np.max(image_float)),
|
||||
"message_count": self.message_count,
|
||||
}
|
||||
|
||||
# Calculate per-channel stats if color image
|
||||
if len(image.shape) == 3 and image.shape[2] > 1:
|
||||
for channel in range(image.shape[2]):
|
||||
channel_data = image_float[:, :, channel]
|
||||
stats[f'mean_ch{channel}'] = float(np.mean(channel_data))
|
||||
stats[f'std_ch{channel}'] = float(np.std(channel_data))
|
||||
stats[f'median_ch{channel}'] = float(np.median(channel_data))
|
||||
stats[f"mean_ch{channel}"] = float(np.mean(channel_data))
|
||||
stats[f"std_ch{channel}"] = float(np.std(channel_data))
|
||||
stats[f"median_ch{channel}"] = float(np.median(channel_data))
|
||||
|
||||
radial_data = self.calculate_radial_integration(image)
|
||||
stats['radial'] = radial_data
|
||||
stats["radial"] = radial_data
|
||||
|
||||
#projection_data = self.calculate_projections(image)
|
||||
#stats['projections'] = projection_data
|
||||
# projection_data = self.calculate_projections(image)
|
||||
# stats['projections'] = projection_data
|
||||
|
||||
return stats
|
||||
|
||||
@@ -200,7 +206,7 @@ class ImageStatsReceiver:
|
||||
rgb_image = rgb_image[:, ::-1, :].copy()
|
||||
|
||||
stats = self.calculate_image_stats(rgb_image)
|
||||
stats['image_type'] = 'rgb_from_bayer'
|
||||
stats["image_type"] = "rgb_from_bayer"
|
||||
|
||||
with self.stats_lock:
|
||||
self.latest_stats = stats
|
||||
@@ -213,7 +219,7 @@ class ImageStatsReceiver:
|
||||
rgb_image = rgb_image.reshape(header_shape)
|
||||
|
||||
stats = self.calculate_image_stats(rgb_image)
|
||||
stats['image_type'] = 'rgb_direct'
|
||||
stats["image_type"] = "rgb_direct"
|
||||
|
||||
with self.stats_lock:
|
||||
self.latest_stats = stats
|
||||
@@ -245,45 +251,53 @@ class ImageStatsReceiver:
|
||||
|
||||
def print_formatted_stats(self, stats: Dict[str, Any]):
|
||||
"""Print formatted statistics in a compact terminal format"""
|
||||
timestamp = time.strftime('%H:%M:%S', time.localtime(stats['timestamp']))
|
||||
timestamp = time.strftime("%H:%M:%S", time.localtime(stats["timestamp"]))
|
||||
|
||||
# Compact one-line format
|
||||
print(f"[{timestamp}] #{stats['message_count']:4d} | "
|
||||
f"Shape: {stats['shape']} | "
|
||||
f"Mean: {stats['mean']:6.1f} | "
|
||||
f"Std: {stats['std']:6.1f} | "
|
||||
f"Median: {stats['median']:6.1f} | "
|
||||
f"Range: [{stats['min']:3.0f}-{stats['max']:3.0f}]",
|
||||
flush=True)
|
||||
print(
|
||||
f"[{timestamp}] #{stats['message_count']:4d} | "
|
||||
f"Shape: {stats['shape']} | "
|
||||
f"Mean: {stats['mean']:6.1f} | "
|
||||
f"Std: {stats['std']:6.1f} | "
|
||||
f"Median: {stats['median']:6.1f} | "
|
||||
f"Range: [{stats['min']:3.0f}-{stats['max']:3.0f}]",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if 'radial' in stats:
|
||||
radial = stats['radial']
|
||||
center_intensity = radial['radial_profile'][0] if radial['radial_profile'] else 0
|
||||
edge_intensity = radial['radial_profile'][-1] if radial['radial_profile'] else 0
|
||||
peak_radius_idx = np.argmax(radial['radial_profile']) if radial['radial_profile'] else 0
|
||||
peak_radius = radial['r_centers'][peak_radius_idx] if radial['r_centers'] else 0
|
||||
if "radial" in stats:
|
||||
radial = stats["radial"]
|
||||
center_intensity = radial["radial_profile"][0] if radial["radial_profile"] else 0
|
||||
edge_intensity = radial["radial_profile"][-1] if radial["radial_profile"] else 0
|
||||
peak_radius_idx = np.argmax(radial["radial_profile"]) if radial["radial_profile"] else 0
|
||||
peak_radius = radial["r_centers"][peak_radius_idx] if radial["r_centers"] else 0
|
||||
|
||||
print(f"{'':21} Radial: Center={center_intensity:.1f} | "
|
||||
f"Edge={edge_intensity:.1f} | "
|
||||
f"Peak@r={peak_radius:.1f} | "
|
||||
f"Center=({radial['center'][0]},{radial['center'][1]})",
|
||||
flush=True)
|
||||
print(
|
||||
f"{'':21} Radial: Center={center_intensity:.1f} | "
|
||||
f"Edge={edge_intensity:.1f} | "
|
||||
f"Peak@r={peak_radius:.1f} | "
|
||||
f"Center=({radial['center'][0]},{radial['center'][1]})",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# Print projection summary
|
||||
if 'projections' in stats:
|
||||
proj = stats['projections']
|
||||
print(f"{'':21} X-Profile: Peak@{proj['x_peak_position']}({proj['x_peak_value']:.1f}) | "
|
||||
f"Centroid={proj['x_centroid']:.1f} | FWHM={proj['x_fwhm']:.1f}",
|
||||
flush=True)
|
||||
print(f"{'':21} Y-Profile: Peak@{proj['y_peak_position']}({proj['y_peak_value']:.1f}) | "
|
||||
f"Centroid={proj['y_centroid']:.1f} | FWHM={proj['y_fwhm']:.1f}",
|
||||
flush=True)
|
||||
if "projections" in stats:
|
||||
proj = stats["projections"]
|
||||
print(
|
||||
f"{'':21} X-Profile: Peak@{proj['x_peak_position']}({proj['x_peak_value']:.1f}) | "
|
||||
f"Centroid={proj['x_centroid']:.1f} | FWHM={proj['x_fwhm']:.1f}",
|
||||
flush=True,
|
||||
)
|
||||
print(
|
||||
f"{'':21} Y-Profile: Peak@{proj['y_peak_position']}({proj['y_peak_value']:.1f}) | "
|
||||
f"Centroid={proj['y_centroid']:.1f} | FWHM={proj['y_fwhm']:.1f}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
# Optional: Print per-channel stats if available
|
||||
if any(k.startswith('mean_ch') for k in stats.keys()):
|
||||
if any(k.startswith("mean_ch") for k in stats.keys()):
|
||||
channels = []
|
||||
i = 0
|
||||
while f'mean_ch{i}' in stats:
|
||||
while f"mean_ch{i}" in stats:
|
||||
channels.append(f"Ch{i}({stats[f'mean_ch{i}']:.1f})")
|
||||
i += 1
|
||||
if channels:
|
||||
@@ -292,16 +306,23 @@ class ImageStatsReceiver:
|
||||
def get_radial_profile_summary(self):
|
||||
"""Get a summary of the current radial profile"""
|
||||
with self.stats_lock:
|
||||
if self.latest_stats and 'radial' in self.latest_stats:
|
||||
radial = self.latest_stats['radial']
|
||||
if self.latest_stats and "radial" in self.latest_stats:
|
||||
radial = self.latest_stats["radial"]
|
||||
return {
|
||||
'r_centers': radial['r_centers'],
|
||||
'radial_profile': radial['radial_profile'],
|
||||
'center_intensity': radial['radial_profile'][0] if radial['radial_profile'] else 0,
|
||||
'edge_intensity': radial['radial_profile'][-1] if radial['radial_profile'] else 0,
|
||||
'max_intensity_radius': radial['r_centers'][np.argmax(radial['radial_profile'])] if radial[
|
||||
'radial_profile'] else 0,
|
||||
'max_intensity_value': max(radial['radial_profile']) if radial['radial_profile'] else 0
|
||||
"r_centers": radial["r_centers"],
|
||||
"radial_profile": radial["radial_profile"],
|
||||
"center_intensity": radial["radial_profile"][0]
|
||||
if radial["radial_profile"]
|
||||
else 0,
|
||||
"edge_intensity": radial["radial_profile"][-1]
|
||||
if radial["radial_profile"]
|
||||
else 0,
|
||||
"max_intensity_radius": radial["r_centers"][np.argmax(radial["radial_profile"])]
|
||||
if radial["radial_profile"]
|
||||
else 0,
|
||||
"max_intensity_value": max(radial["radial_profile"])
|
||||
if radial["radial_profile"]
|
||||
else 0,
|
||||
}
|
||||
return None
|
||||
|
||||
@@ -311,21 +332,23 @@ class ImageStatsReceiver:
|
||||
filename = f"radial_profile_{int(time.time())}.txt"
|
||||
|
||||
with self.stats_lock:
|
||||
if self.latest_stats and 'radial' in self.latest_stats:
|
||||
radial = self.latest_stats['radial']
|
||||
if self.latest_stats and "radial" in self.latest_stats:
|
||||
radial = self.latest_stats["radial"]
|
||||
|
||||
with open(filename, 'w') as f:
|
||||
with open(filename, "w") as f:
|
||||
f.write("# Radial Integration Profile\n")
|
||||
f.write(f"# Timestamp: {time.ctime(self.latest_stats['timestamp'])}\n")
|
||||
f.write(f"# Image shape: {self.latest_stats['shape']}\n")
|
||||
f.write(f"# Center: {radial['center']}\n")
|
||||
f.write("# Radius(pixels)\tMean_Intensity\tStd_Intensity\tPixel_Count\n")
|
||||
|
||||
for i in range(len(radial['r_centers'])):
|
||||
f.write(f"{radial['r_centers'][i]:.2f}\t"
|
||||
f"{radial['radial_profile'][i]:.2f}\t"
|
||||
f"{radial['radial_std'][i]:.2f}\t"
|
||||
f"{radial['pixel_counts'][i]}\n")
|
||||
for i in range(len(radial["r_centers"])):
|
||||
f.write(
|
||||
f"{radial['r_centers'][i]:.2f}\t"
|
||||
f"{radial['radial_profile'][i]:.2f}\t"
|
||||
f"{radial['radial_std'][i]:.2f}\t"
|
||||
f"{radial['pixel_counts'][i]}\n"
|
||||
)
|
||||
|
||||
print(f"Radial profile saved to {filename}")
|
||||
return filename
|
||||
@@ -419,6 +442,7 @@ def save_current_radial_profile(filename: str = None):
|
||||
return stats_receiver.save_radial_profile_to_file(filename)
|
||||
return None
|
||||
|
||||
|
||||
# # Auto-start when daq.py is imported/run
|
||||
# if __name__ == "__main__":
|
||||
# # If running daq.py directly
|
||||
@@ -444,4 +468,4 @@ def save_current_radial_profile(filename: str = None):
|
||||
#
|
||||
# else:
|
||||
# # If daq.py is imported as a module, auto-start the receiver
|
||||
# start_image_stats_receiver()
|
||||
# start_image_stats_receiver()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from aaredaq.config import BeamlineConfig
|
||||
from aaredaqlib.beamline import MXBeamline
|
||||
|
||||
c = BeamlineConfig(MXBeamline.X06DA)
|
||||
c.state_busy = False
|
||||
c.state_busy = False
|
||||
|
||||
+43
-42
@@ -1,13 +1,17 @@
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QFrame, QPushButton, QScrollArea, QStackedWidget,
|
||||
QApplication,
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QFrame,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QStackedWidget,
|
||||
QSizePolicy,
|
||||
)
|
||||
from PySide6.QtCore import Qt, QPointF, QRectF
|
||||
from PySide6.QtGui import (
|
||||
QPainter, QColor, QPen, QLinearGradient,
|
||||
QFont, QFontMetrics,
|
||||
)
|
||||
from PySide6.QtGui import QPainter, QColor, QPen, QLinearGradient, QFont, QFontMetrics
|
||||
import sys
|
||||
import math
|
||||
|
||||
@@ -15,15 +19,15 @@ import math
|
||||
# ---------------------------------------------------------------------------
|
||||
# Colour palette
|
||||
# ---------------------------------------------------------------------------
|
||||
BG = "#071018"
|
||||
CARD_BG = "#0E1A26"
|
||||
ACCENT = "#62D8C8"
|
||||
ACCENT_DIM = "#1A3A36"
|
||||
TEXT = "#F5F7FA"
|
||||
SUBTEXT = "#8A9BB0"
|
||||
BUTTON_BG = "#132131"
|
||||
LED_OFF = "#1C2E3E"
|
||||
LED_ON = ACCENT
|
||||
BG = "#071018"
|
||||
CARD_BG = "#0E1A26"
|
||||
ACCENT = "#62D8C8"
|
||||
ACCENT_DIM = "#1A3A36"
|
||||
TEXT = "#F5F7FA"
|
||||
SUBTEXT = "#8A9BB0"
|
||||
BUTTON_BG = "#132131"
|
||||
LED_OFF = "#1C2E3E"
|
||||
LED_ON = ACCENT
|
||||
ACTIVE_STEP = "#FFFFFF"
|
||||
|
||||
|
||||
@@ -147,10 +151,7 @@ class LEDStages(QWidget):
|
||||
line_color = QColor(ACCENT) if i < self._active else QColor(LED_OFF)
|
||||
pen = QPen(line_color, 2)
|
||||
p.setPen(pen)
|
||||
p.drawLine(
|
||||
QPointF(cx + led_r + 3, cy),
|
||||
QPointF(next_cx - led_r - 3, cy),
|
||||
)
|
||||
p.drawLine(QPointF(cx + led_r + 3, cy), QPointF(next_cx - led_r - 3, cy))
|
||||
|
||||
# --- LED circle ---
|
||||
p.setPen(Qt.NoPen)
|
||||
@@ -162,14 +163,8 @@ class LEDStages(QWidget):
|
||||
pen = QPen(QColor(BG), 2)
|
||||
pen.setCapStyle(Qt.RoundCap)
|
||||
p.setPen(pen)
|
||||
p.drawLine(
|
||||
QPointF(cx - 4, cy),
|
||||
QPointF(cx - 1, cy + 3),
|
||||
)
|
||||
p.drawLine(
|
||||
QPointF(cx - 1, cy + 3),
|
||||
QPointF(cx + 4, cy - 3),
|
||||
)
|
||||
p.drawLine(QPointF(cx - 4, cy), QPointF(cx - 1, cy + 3))
|
||||
p.drawLine(QPointF(cx - 1, cy + 3), QPointF(cx + 4, cy - 3))
|
||||
elif i == self._active:
|
||||
# active — bright with glow ring
|
||||
glow_pen = QPen(QColor(ACCENT + "55"), 4)
|
||||
@@ -194,9 +189,7 @@ class LEDStages(QWidget):
|
||||
fm = QFontMetrics(font)
|
||||
text_w = fm.horizontalAdvance(name)
|
||||
p.drawText(
|
||||
QRectF(cx - text_w / 2 - 4, 0, text_w + 8, label_y + 2),
|
||||
Qt.AlignCenter,
|
||||
name,
|
||||
QRectF(cx - text_w / 2 - 4, 0, text_w + 8, label_y + 2), Qt.AlignCenter, name
|
||||
)
|
||||
|
||||
|
||||
@@ -317,7 +310,9 @@ class QueueItemCard(QFrame):
|
||||
text_col.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
title_lbl = QLabel(self._title)
|
||||
title_lbl.setStyleSheet(f"color: {TEXT}; font-size: 13px; font-weight: 600; background: transparent;")
|
||||
title_lbl.setStyleSheet(
|
||||
f"color: {TEXT}; font-size: 13px; font-weight: 600; background: transparent;"
|
||||
)
|
||||
sub_lbl = QLabel(self._subtitle)
|
||||
sub_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 11px; background: transparent;")
|
||||
|
||||
@@ -399,7 +394,9 @@ class SampleCamera(QWidget):
|
||||
# ── Title ──────────────────────────────────────────────────────
|
||||
title = QLabel("S A M C A M E R A")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
title.setStyleSheet(f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 600;")
|
||||
title.setStyleSheet(
|
||||
f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 600;"
|
||||
)
|
||||
layout.addWidget(title)
|
||||
|
||||
# ── Camera card ────────────────────────────────────────────────
|
||||
@@ -422,7 +419,7 @@ class SampleCamera(QWidget):
|
||||
layout.addWidget(sub_lbl)
|
||||
|
||||
# ── LED step indicator ─────────────────────────────────────────
|
||||
self._leds = LEDStages(active_step=1) # 0=Mount done, 1=Centre active
|
||||
self._leds = LEDStages(active_step=1) # 0=Mount done, 1=Centre active
|
||||
layout.addWidget(self._leds)
|
||||
|
||||
# ── Transport controls ─────────────────────────────────────────
|
||||
@@ -435,7 +432,7 @@ class SampleCamera(QWidget):
|
||||
buttons = [
|
||||
("⏮", False),
|
||||
("⏪", False),
|
||||
("⏸", True), # primary / highlighted
|
||||
("⏸", True), # primary / highlighted
|
||||
("⏩", False),
|
||||
("⏭", False),
|
||||
]
|
||||
@@ -449,7 +446,9 @@ class SampleCamera(QWidget):
|
||||
# ── Up next header ─────────────────────────────────────────────
|
||||
up_next_row = QHBoxLayout()
|
||||
up_next_lbl = QLabel("UP NEXT")
|
||||
up_next_lbl.setStyleSheet(f"color: {ACCENT}; font-size: 11px; letter-spacing: 2px; font-weight: 700;")
|
||||
up_next_lbl.setStyleSheet(
|
||||
f"color: {ACCENT}; font-size: 11px; letter-spacing: 2px; font-weight: 700;"
|
||||
)
|
||||
samples_lbl = QLabel("5 SAMPLES")
|
||||
samples_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 11px; letter-spacing: 1px;")
|
||||
up_next_row.addWidget(up_next_lbl)
|
||||
@@ -459,11 +458,11 @@ class SampleCamera(QWidget):
|
||||
|
||||
# ── Queue preview cards ────────────────────────────────────────
|
||||
queue_items = [
|
||||
("Crystal Plate 14 · Well C8", "Serial MX · 1 kHz", 4100, 6000, True),
|
||||
("Crystal Plate 15 · Grid Scan", "Raster · 10 Hz", 0, 2000, False),
|
||||
("Crystal Plate 13 · Well A1", "Serial MX · 1 kHz", 0, 5000, False),
|
||||
("Crystal Plate 13 · Well B3", "Serial MX · 1 kHz", 0, 4500, False),
|
||||
("Crystal Plate 14 · Well D5", "Serial MX · 1 kHz", 0, 6000, False),
|
||||
("Crystal Plate 14 · Well C8", "Serial MX · 1 kHz", 4100, 6000, True),
|
||||
("Crystal Plate 15 · Grid Scan", "Raster · 10 Hz", 0, 2000, False),
|
||||
("Crystal Plate 13 · Well A1", "Serial MX · 1 kHz", 0, 5000, False),
|
||||
("Crystal Plate 13 · Well B3", "Serial MX · 1 kHz", 0, 4500, False),
|
||||
("Crystal Plate 14 · Well D5", "Serial MX · 1 kHz", 0, 6000, False),
|
||||
]
|
||||
|
||||
queue_widget = QWidget()
|
||||
@@ -502,7 +501,9 @@ class SampleCamera(QWidget):
|
||||
|
||||
title = QLabel("SAMPLE QUEUE")
|
||||
title.setAlignment(Qt.AlignCenter)
|
||||
title.setStyleSheet(f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 700;")
|
||||
title.setStyleSheet(
|
||||
f"color: {ACCENT}; font-size: 13px; letter-spacing: 3px; font-weight: 700;"
|
||||
)
|
||||
layout.addWidget(title)
|
||||
|
||||
scroll = QScrollArea()
|
||||
@@ -583,4 +584,4 @@ if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
ui = SampleCamera()
|
||||
ui.show()
|
||||
sys.exit(app.exec())
|
||||
sys.exit(app.exec())
|
||||
|
||||
@@ -175,7 +175,8 @@ class PShellClient:
|
||||
"""
|
||||
return self._get_response(
|
||||
requests.put(
|
||||
url=self.url + "/run", json={"script": script, "pars": pars, "background": background, "async": False}
|
||||
url=self.url + "/run",
|
||||
json={"script": script, "pars": pars, "background": background, "async": False},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -191,7 +192,9 @@ class PShellClient:
|
||||
Command id (int), which is used to retrieve command execution status/result (get_result).
|
||||
"""
|
||||
statement = quote(statement)
|
||||
return int(self._get_response(requests.get(url=self.url + "/evalAsync/" + statement), False))
|
||||
return int(
|
||||
self._get_response(requests.get(url=self.url + "/evalAsync/" + statement), False)
|
||||
)
|
||||
|
||||
def start_run(self, script, pars=None, background=False):
|
||||
"""Starts execution of a script in the interpreter.
|
||||
@@ -279,9 +282,13 @@ class PShellClient:
|
||||
if type == "json":
|
||||
return self._get_response(requests.get(url=self.url + "/data-json/" + path), True)
|
||||
elif type == "bin":
|
||||
return self._get_binary_response(requests.get(url=self.url + "/data-bin/" + path, stream=True))
|
||||
return self._get_binary_response(
|
||||
requests.get(url=self.url + "/data-bin/" + path, stream=True)
|
||||
)
|
||||
|
||||
return self._get_response(requests.get(url=self.url + "/data" + ("" if path is None else ("/" + path))), False)
|
||||
return self._get_response(
|
||||
requests.get(url=self.url + "/data" + ("" if path is None else ("/" + path))), False
|
||||
)
|
||||
|
||||
def print_logs(self):
|
||||
for log_line in self.get_logs():
|
||||
|
||||
@@ -99,15 +99,10 @@ def main(argv=None):
|
||||
help="Key pattern for SCAN (e.g., 'user:*'). If omitted, all keys are scanned.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scan-count",
|
||||
type=int,
|
||||
default=1000,
|
||||
help="Hint for SCAN per iteration (not a limit).",
|
||||
"--scan-count", type=int, default=1000, help="Hint for SCAN per iteration (not a limit)."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-ttl",
|
||||
action="store_true",
|
||||
help="Include TTL (seconds) for each key.",
|
||||
"--include-ttl", action="store_true", help="Include TTL (seconds) for each key."
|
||||
)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
@@ -125,4 +120,4 @@ def main(argv=None):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
sys.exit(main())
|
||||
|
||||
+17
-56
@@ -22,11 +22,7 @@ from aarecommon.models.models import (
|
||||
SampleShortInfo,
|
||||
ScanResultPayloadModel,
|
||||
)
|
||||
from aarecommon.models.raster_grid import (
|
||||
CenterOfMassModel,
|
||||
RasterGridRequest,
|
||||
RasterPayloadModel,
|
||||
)
|
||||
from aarecommon.models.raster_grid import CenterOfMassModel, RasterGridRequest, RasterPayloadModel
|
||||
from aarecommon.models.rotation_scan import RotationScanRequest
|
||||
from aareDB import (
|
||||
BeamlineParametersInput,
|
||||
@@ -48,19 +44,13 @@ logger = setup_logger("aareDAQ")
|
||||
|
||||
|
||||
class AareWrapper:
|
||||
def __init__(
|
||||
self,
|
||||
bl: MXBeamline,
|
||||
host: str = "https://mx-aaredb-dmz-01.psi.ch/dispatcher",
|
||||
):
|
||||
def __init__(self, bl: MXBeamline, host: str = "https://mx-aaredb-dmz-01.psi.ch/dispatcher"):
|
||||
configuration = aareDB.Configuration(host=host)
|
||||
|
||||
# --- mTLS & SSL CONFIGURATION ---
|
||||
# 1. Trust the Server (CA that signed mx-aaredb-dmz-01)
|
||||
configuration.verify_ssl = True
|
||||
configuration.ssl_ca_cert = (
|
||||
"/etc/ssl/certs/secrets/mx-aaredb-dmz-01_Full_Chain_CA.pem"
|
||||
)
|
||||
configuration.ssl_ca_cert = "/etc/ssl/certs/secrets/mx-aaredb-dmz-01_Full_Chain_CA.pem"
|
||||
|
||||
# 2. Present Machine Identity (The certs that worked in curl)
|
||||
beamline_name = bl.value.lower()
|
||||
@@ -75,9 +65,7 @@ class AareWrapper:
|
||||
self.client = aareDB.ApiClient(configuration)
|
||||
|
||||
# Identity Forwarding (Optional now that mTLS is active, but safe to keep)
|
||||
self.client.default_headers["X-Shared-Password"] = os.getenv(
|
||||
"AAREDB_SHARED_PASSWORD"
|
||||
)
|
||||
self.client.default_headers["X-Shared-Password"] = os.getenv("AAREDB_SHARED_PASSWORD")
|
||||
self.__host = host
|
||||
self.__tell_api = aareDB.TellsRunnerApi(self.client)
|
||||
self.__sample_api = aareDB.SamplesRunnerApi(self.client)
|
||||
@@ -95,15 +83,11 @@ class AareWrapper:
|
||||
|
||||
for i in input_list:
|
||||
t = SetTellPosition(
|
||||
puck_name=i.puck_name,
|
||||
segment=i.location.segment,
|
||||
puck_in_segment=i.location.pos,
|
||||
puck_name=i.puck_name, segment=i.location.segment, puck_in_segment=i.location.pos
|
||||
)
|
||||
o.append(t)
|
||||
payload = SetTellPositionRequest(pucks=o, tell=self.__bl.value.upper())
|
||||
ret = self.__tell_api.set_tell_positions(
|
||||
set_tell_position_request=payload,
|
||||
)
|
||||
ret = self.__tell_api.set_tell_positions(set_tell_position_request=payload)
|
||||
logger.debug(ret)
|
||||
|
||||
@log_timing(logger, "AareDB call")
|
||||
@@ -111,9 +95,7 @@ class AareWrapper:
|
||||
from aareDB.models import ManualSampleCreate
|
||||
|
||||
manual_sample = ManualSampleCreate(
|
||||
pgroup=s.user,
|
||||
sample_name=s.sample_name,
|
||||
data_collection_parameters=s.aaredb_params,
|
||||
pgroup=s.user, sample_name=s.sample_name, data_collection_parameters=s.aaredb_params
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -123,16 +105,11 @@ class AareWrapper:
|
||||
|
||||
@log_timing(logger, "AareDB call")
|
||||
def send_sample_event(
|
||||
self,
|
||||
sample_id: StrictInt,
|
||||
event_type: SampleEventType,
|
||||
comment: Optional[str] = None,
|
||||
self, sample_id: StrictInt, event_type: SampleEventType, comment: Optional[str] = None
|
||||
) -> None:
|
||||
if sample_id is None or sample_id < 0:
|
||||
if sample_id is None:
|
||||
logger.debug(
|
||||
f"Sample db_id is None, skipping sample event {event_type!s}"
|
||||
)
|
||||
logger.debug(f"Sample db_id is None, skipping sample event {event_type!s}")
|
||||
elif sample_id < 0:
|
||||
logger.debug(
|
||||
f"Sample db_id is invalid ({sample_id}), skipping sample event {event_type!s}"
|
||||
@@ -141,20 +118,14 @@ class AareWrapper:
|
||||
try:
|
||||
self.__sample_api.create_sample_event(
|
||||
sample_id=sample_id,
|
||||
sample_event_create=SampleEventCreate(
|
||||
event_type=event_type, comment=comment
|
||||
),
|
||||
sample_event_create=SampleEventCreate(event_type=event_type, comment=comment),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending sample event {event_type!s} to db: {e}")
|
||||
|
||||
@log_timing(logger, "AareDB call")
|
||||
def upload_image(
|
||||
self,
|
||||
sample_id: int,
|
||||
filename: str,
|
||||
bgr_image: np.ndarray,
|
||||
message: Optional[str] = None,
|
||||
self, sample_id: int, filename: str, bgr_image: np.ndarray, message: Optional[str] = None
|
||||
):
|
||||
_, buffer = cv2.imencode(".jpg", bgr_image)
|
||||
jpeg_bytes = io.BytesIO(buffer)
|
||||
@@ -176,9 +147,7 @@ class AareWrapper:
|
||||
logger.debug(f"Response status code: {response.status_code}")
|
||||
|
||||
@log_timing(logger, "AareDB call")
|
||||
def upload_jpg(
|
||||
self, sample_id: int, filename: str, jpg_image, message: Optional[str] = None
|
||||
):
|
||||
def upload_jpg(self, sample_id: int, filename: str, jpg_image, message: Optional[str] = None):
|
||||
logger.debug(f"jppg_image of type: {type(jpg_image)}")
|
||||
url = f"{self.__host}/protected_router/sample_runner/{sample_id}/upload-images"
|
||||
headers = {
|
||||
@@ -226,9 +195,7 @@ class AareWrapper:
|
||||
)
|
||||
characterization = None
|
||||
dataset = Datasets(
|
||||
filepath=r.file_prefix,
|
||||
status="written",
|
||||
written_at=datetime.datetime.now(),
|
||||
filepath=r.file_prefix, status="written", written_at=datetime.datetime.now()
|
||||
)
|
||||
detector_data = DetectorParameters(
|
||||
manufacturer="DECTRIS",
|
||||
@@ -265,8 +232,7 @@ class AareWrapper:
|
||||
type="standard", beamline_parameters=beamline_params, sample_id=s.db_id
|
||||
)
|
||||
response = self.__sample_api.create_experiment_parameters_for_sample(
|
||||
sample_id=s.db_id,
|
||||
experiment_parameters_create=experiment_params_payload,
|
||||
sample_id=s.db_id, experiment_parameters_create=experiment_params_payload
|
||||
)
|
||||
# logger.debug("Experiment parameters created:", response)
|
||||
except Exception as e:
|
||||
@@ -292,9 +258,7 @@ class AareWrapper:
|
||||
exposureTime_s=round(r.exp_time_s, 4),
|
||||
)
|
||||
dataset = Datasets(
|
||||
filepath=r.file_prefix,
|
||||
status="written",
|
||||
written_at=datetime.datetime.now(),
|
||||
filepath=r.file_prefix, status="written", written_at=datetime.datetime.now()
|
||||
)
|
||||
detector_data = DetectorParameters(
|
||||
manufacturer="DECTRIS",
|
||||
@@ -330,8 +294,7 @@ class AareWrapper:
|
||||
type="standard", beamline_parameters=beamline_params, sample_id=s.db_id
|
||||
)
|
||||
response = self.__sample_api.create_experiment_parameters_for_sample(
|
||||
sample_id=s.db_id,
|
||||
experiment_parameters_create=experiment_params_payload,
|
||||
sample_id=s.db_id, experiment_parameters_create=experiment_params_payload
|
||||
)
|
||||
# logger.info("Experiment parameters created:", response)
|
||||
except Exception as e:
|
||||
@@ -416,9 +379,7 @@ class AareWrapper:
|
||||
for img in raster_result.images
|
||||
]
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"raster score computation failed, sending null score: {e}"
|
||||
)
|
||||
logger.warning(f"raster score computation failed, sending null score: {e}")
|
||||
score = None
|
||||
|
||||
payload = RasterPayloadModel(
|
||||
|
||||
+12
-46
@@ -14,12 +14,7 @@ from aarecommon.errors.exception_handler import (
|
||||
AuthErrorCode,
|
||||
UserRightsException,
|
||||
)
|
||||
from aarecommon.models.auth import (
|
||||
BatonRequest,
|
||||
BatonRequestStatus,
|
||||
BatonStatus,
|
||||
BatonTransferQueue,
|
||||
)
|
||||
from aarecommon.models.auth import BatonRequest, BatonRequestStatus, BatonStatus, BatonTransferQueue
|
||||
from aarecommon.models.models import SessionsStateEnum
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
@@ -55,9 +50,7 @@ class TokenData(BaseModel):
|
||||
|
||||
def create_access_token(token: TokenData):
|
||||
to_encode = token.model_dump()
|
||||
to_encode.update(
|
||||
{"exp": datetime.now(UTC) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)}
|
||||
)
|
||||
to_encode.update({"exp": datetime.now(UTC) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@@ -82,9 +75,7 @@ def authenticate_from_proxy_header(request: Request) -> str:
|
||||
only the Apache proxy running on the same host can supply it.
|
||||
"""
|
||||
client_host = request.client.host if request.client else None
|
||||
logger.debug(
|
||||
f"[auth] /token client_host={client_host!r} request.client={request.client!r}"
|
||||
)
|
||||
logger.debug(f"[auth] /token client_host={client_host!r} request.client={request.client!r}")
|
||||
if not _is_loopback(client_host):
|
||||
logger.warning(
|
||||
f"[auth] Rejecting X-Remote-User: client_host {client_host!r} is not loopback"
|
||||
@@ -114,13 +105,9 @@ def authenticate_user(cfg: BeamlineConfig, username: str) -> str:
|
||||
super_user = username in SUPER_USERS
|
||||
pgroups = [group for group in supplementary_groups if group.startswith("p")]
|
||||
staff = (
|
||||
"unx-mxgroup" in supplementary_groups
|
||||
or "unx-sls_mx" in supplementary_groups
|
||||
or super_user
|
||||
)
|
||||
token = TokenData(
|
||||
sub=username, pgroups=pgroups, staff=staff, session=cfg.generate_session()
|
||||
"unx-mxgroup" in supplementary_groups or "unx-sls_mx" in supplementary_groups or super_user
|
||||
)
|
||||
token = TokenData(sub=username, pgroups=pgroups, staff=staff, session=cfg.generate_session())
|
||||
return create_access_token(token)
|
||||
|
||||
|
||||
@@ -169,9 +156,7 @@ def check_jwt_rw(cfg: BeamlineConfig, data: TokenData) -> None:
|
||||
def check_jwt_staff_only(data: TokenData) -> None:
|
||||
if not data.staff:
|
||||
raise UserRightsException(
|
||||
message="Not member of the MX staff.",
|
||||
status_code=403,
|
||||
code=AuthErrorCode.NOT_STAFF,
|
||||
message="Not member of the MX staff.", status_code=403, code=AuthErrorCode.NOT_STAFF
|
||||
)
|
||||
|
||||
|
||||
@@ -201,9 +186,7 @@ def force_current_sesion(cfg: BeamlineConfig, data: TokenData) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _finalize_expired_baton_request(
|
||||
cfg: BeamlineConfig, pending: BatonRequest
|
||||
) -> BatonStatus:
|
||||
def _finalize_expired_baton_request(cfg: BeamlineConfig, pending: BatonRequest) -> BatonStatus:
|
||||
"""
|
||||
Resolve an expired baton request in one place.
|
||||
|
||||
@@ -357,19 +340,10 @@ def request_baton(cfg: BeamlineConfig, data: TokenData) -> dict:
|
||||
to_pgroup=cfg.pgroup,
|
||||
expiry_sec=SESSION_EXPIRE_SECONDS,
|
||||
)
|
||||
return {
|
||||
"granted": True,
|
||||
"override": True,
|
||||
"message": "Staff override - baton acquired",
|
||||
}
|
||||
return {"granted": True, "override": True, "message": "Staff override - baton acquired"}
|
||||
|
||||
# Policy check: non-staff requesting from staff
|
||||
if (
|
||||
holder
|
||||
and holder.is_staff
|
||||
and not data.staff
|
||||
and not cfg.allow_non_staff_request_from_staff
|
||||
):
|
||||
if holder and holder.is_staff and not data.staff and not cfg.allow_non_staff_request_from_staff:
|
||||
return {
|
||||
"error": True,
|
||||
"message": "Requesting baton from staff is disabled by backend policy.",
|
||||
@@ -402,9 +376,7 @@ def request_baton(cfg: BeamlineConfig, data: TokenData) -> dict:
|
||||
|
||||
msg = f"Request sent to {holder.username if holder else 'current holder'}"
|
||||
if is_busy:
|
||||
msg += (
|
||||
" (Note: beamline is currently busy, transfer will be queued if accepted)"
|
||||
)
|
||||
msg += " (Note: beamline is currently busy, transfer will be queued if accepted)"
|
||||
|
||||
return {
|
||||
"pending": True,
|
||||
@@ -415,9 +387,7 @@ def request_baton(cfg: BeamlineConfig, data: TokenData) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def respond_to_baton_request(
|
||||
cfg: BeamlineConfig, data: TokenData, accept: bool
|
||||
) -> dict:
|
||||
def respond_to_baton_request(cfg: BeamlineConfig, data: TokenData, accept: bool) -> dict:
|
||||
"""
|
||||
Current baton holder responds to a pending request.
|
||||
"""
|
||||
@@ -441,11 +411,7 @@ def respond_to_baton_request(
|
||||
to_pgroup=cfg.pgroup,
|
||||
expiry_sec=SESSION_EXPIRE_SECONDS,
|
||||
)
|
||||
return {
|
||||
"accepted": True,
|
||||
"transferred": True,
|
||||
"message": "Baton transferred",
|
||||
}
|
||||
return {"accepted": True, "transferred": True, "message": "Baton transferred"}
|
||||
else:
|
||||
# Beamline busy, queue the transfer
|
||||
cfg.queued_baton_transfer = BatonTransferQueue(
|
||||
|
||||
@@ -2,7 +2,9 @@ import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
def calculate_focus_measure(image: np.ndarray, center_x: float, center_y: float, radius: float) -> float:
|
||||
def calculate_focus_measure(
|
||||
image: np.ndarray, center_x: float, center_y: float, radius: float
|
||||
) -> float:
|
||||
"""
|
||||
Calculate the focus measure within a circular region around a point.
|
||||
|
||||
@@ -22,7 +24,7 @@ def calculate_focus_measure(image: np.ndarray, center_x: float, center_y: float,
|
||||
# Create a circular mask
|
||||
height, width = gray.shape
|
||||
y, x = np.ogrid[:height, :width]
|
||||
mask = ((x - center_x) ** 2 + (y - center_y) ** 2 <= radius ** 2)
|
||||
mask = (x - center_x) ** 2 + (y - center_y) ** 2 <= radius**2
|
||||
|
||||
# Apply Laplacian operator
|
||||
laplacian = cv2.Laplacian(gray, cv2.CV_64F)
|
||||
|
||||
@@ -2,6 +2,7 @@ import numpy as np
|
||||
import cv2
|
||||
from scipy.optimize import curve_fit
|
||||
|
||||
|
||||
class Gaussian2Dfit:
|
||||
center_x: float
|
||||
center_y: float
|
||||
@@ -10,6 +11,7 @@ class Gaussian2Dfit:
|
||||
rotation_angle: float
|
||||
peak_intensity: float
|
||||
|
||||
|
||||
def gaussian_2d(xy, x0, y0, sigma_x, sigma_y, theta, A, offset):
|
||||
x, y = xy
|
||||
x_rot = (x - x0) * np.cos(theta) + (y - y0) * np.sin(theta)
|
||||
@@ -17,7 +19,8 @@ def gaussian_2d(xy, x0, y0, sigma_x, sigma_y, theta, A, offset):
|
||||
gaussian = A * np.exp(-(x_rot**2 / (2 * sigma_x**2) + y_rot**2 / (2 * sigma_y**2))) + offset
|
||||
return gaussian.ravel()
|
||||
|
||||
def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit:
|
||||
|
||||
def beamcenter_fit(image: np.ndarray) -> Gaussian2Dfit:
|
||||
"""
|
||||
Takes a np.ndarray image and returns a Gaussian2Dfit object containing the fitted parameters
|
||||
Returns None if the fit did not converge
|
||||
@@ -25,7 +28,9 @@ def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit:
|
||||
"""
|
||||
# Preprocess the image
|
||||
blurred_image = cv2.GaussianBlur(image, (5, 5), 0)
|
||||
normalized_image = cv2.normalize(blurred_image, None, 0, 255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U)
|
||||
normalized_image = cv2.normalize(
|
||||
blurred_image, None, 0, 255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U
|
||||
)
|
||||
|
||||
# Threshold to isolate the brightest spot (main blob)
|
||||
_, binary_mask = cv2.threshold(normalized_image, 180, 255, cv2.THRESH_BINARY)
|
||||
@@ -43,7 +48,7 @@ def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit:
|
||||
new_h = min(image.shape[0] - new_y, int(h * (1 + enlargement_factor)))
|
||||
|
||||
# Update the ROI
|
||||
roi = normalized_image[new_y:new_y + new_h, new_x:new_x + new_w]
|
||||
roi = normalized_image[new_y : new_y + new_h, new_x : new_x + new_w]
|
||||
|
||||
# Fit the 2D Gaussian
|
||||
X, Y = np.meshgrid(np.arange(roi.shape[1]), np.arange(roi.shape[0]))
|
||||
@@ -51,13 +56,13 @@ def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit:
|
||||
|
||||
# Initial guess for Gaussian parameters
|
||||
initial_guess = (
|
||||
new_w / 2, # x0
|
||||
new_h / 2, # y0
|
||||
new_w / 4, # sigma_x
|
||||
new_h / 4, # sigma_y
|
||||
0, # theta
|
||||
np.max(roi), # A (peak intensity)
|
||||
np.min(roi) # offset
|
||||
new_w / 2, # x0
|
||||
new_h / 2, # y0
|
||||
new_w / 4, # sigma_x
|
||||
new_h / 4, # sigma_y
|
||||
0, # theta
|
||||
np.max(roi), # A (peak intensity)
|
||||
np.min(roi), # offset
|
||||
)
|
||||
|
||||
# Flatten ROI for curve fitting
|
||||
@@ -71,10 +76,10 @@ def beamcenter_fit(image:np.ndarray) -> Gaussian2Dfit:
|
||||
# Extract fitted parameters
|
||||
x0, y0, sigma_x, sigma_y, ini_theta, intensity, _ = popt
|
||||
result = Gaussian2Dfit()
|
||||
result.center_x = x0+new_x
|
||||
result.center_y = y0+new_y
|
||||
result.center_x = x0 + new_x
|
||||
result.center_y = y0 + new_y
|
||||
result.sigma_x = sigma_x
|
||||
result.sigma_y = sigma_y
|
||||
result.rotation_angle = np.degrees(ini_theta)%360
|
||||
result.rotation_angle = np.degrees(ini_theta) % 360
|
||||
result.peak_intensity = intensity
|
||||
return result
|
||||
return result
|
||||
|
||||
+36
-117
@@ -130,9 +130,7 @@ class BeamlineConfig:
|
||||
return f"{self.__bl}:gui_session:{session}"
|
||||
|
||||
def _write_gui_session(
|
||||
self,
|
||||
payload: OpenGuiSessionInfo,
|
||||
expiry_sec: int | None = None,
|
||||
self, payload: OpenGuiSessionInfo, expiry_sec: int | None = None
|
||||
) -> None:
|
||||
expiry = int(expiry_sec or self.GUI_SESSION_EXPIRE_SECONDS)
|
||||
|
||||
@@ -163,12 +161,7 @@ class BeamlineConfig:
|
||||
return None
|
||||
|
||||
def touch_gui_session(
|
||||
self,
|
||||
*,
|
||||
session: int,
|
||||
username: str,
|
||||
staff: bool = False,
|
||||
expiry_sec: int,
|
||||
self, *, session: int, username: str, staff: bool = False, expiry_sec: int
|
||||
) -> OpenGuiSessionInfo:
|
||||
existing = self._read_gui_session(session)
|
||||
holder = self.baton_holder
|
||||
@@ -179,19 +172,11 @@ class BeamlineConfig:
|
||||
username=username,
|
||||
staff=staff,
|
||||
last_seen_ts=time.time(),
|
||||
last_interaction_ts=existing.last_interaction_ts
|
||||
if existing is not None
|
||||
else None,
|
||||
last_interaction_ts=existing.last_interaction_ts if existing is not None else None,
|
||||
close_requested=existing.close_requested if existing is not None else False,
|
||||
close_requested_by=existing.close_requested_by
|
||||
if existing is not None
|
||||
else None,
|
||||
close_requested_at=existing.close_requested_at
|
||||
if existing is not None
|
||||
else None,
|
||||
close_grace_seconds=existing.close_grace_seconds
|
||||
if existing is not None
|
||||
else None,
|
||||
close_requested_by=existing.close_requested_by if existing is not None else None,
|
||||
close_requested_at=existing.close_requested_at if existing is not None else None,
|
||||
close_grace_seconds=existing.close_grace_seconds if existing is not None else None,
|
||||
holds_baton=holds_baton,
|
||||
)
|
||||
|
||||
@@ -213,11 +198,7 @@ class BeamlineConfig:
|
||||
return payload
|
||||
|
||||
def request_gui_close(
|
||||
self,
|
||||
*,
|
||||
session: int,
|
||||
requested_by: str,
|
||||
grace_seconds: int = 60,
|
||||
self, *, session: int, requested_by: str, grace_seconds: int = 60
|
||||
) -> OpenGuiSessionInfo | None:
|
||||
payload = self._read_gui_session(session)
|
||||
if payload is None:
|
||||
@@ -293,9 +274,7 @@ class BeamlineConfig:
|
||||
return None
|
||||
|
||||
holder = self.baton_holder
|
||||
payload.holds_baton = bool(
|
||||
holder is not None and holder.session == payload.session
|
||||
)
|
||||
payload.holds_baton = bool(holder is not None and holder.session == payload.session)
|
||||
return payload
|
||||
|
||||
# Session and authentication management
|
||||
@@ -331,9 +310,7 @@ class BeamlineConfig:
|
||||
return baton.session
|
||||
|
||||
def session_status(self, session: int) -> SessionStatus:
|
||||
return SessionStatus(
|
||||
session=self.session_state(session), current_pgroup=self.pgroup
|
||||
)
|
||||
return SessionStatus(session=self.session_state(session), current_pgroup=self.pgroup)
|
||||
|
||||
def session_state(self, session: int) -> SessionsStateEnum:
|
||||
holder = self.baton_holder
|
||||
@@ -362,9 +339,7 @@ class BeamlineConfig:
|
||||
return SessionsStateEnum.OwnedByElse
|
||||
|
||||
def try_set_active_session(self, session: int, expiry_sec: int) -> None:
|
||||
with redis_lock.Lock(
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
):
|
||||
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
|
||||
active = self.active_session
|
||||
if active is None:
|
||||
self.__client.set(f"{self.__bl}:active_session", session)
|
||||
@@ -376,14 +351,10 @@ class BeamlineConfig:
|
||||
|
||||
# TODO finish setting this up!
|
||||
def try_extend_active_session(self, session: int, expiry_sec: int) -> None:
|
||||
with redis_lock.Lock(
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
):
|
||||
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
|
||||
active = self.active_session
|
||||
if active is None:
|
||||
raise Exception(
|
||||
"There is no active session with given id. Try again later."
|
||||
)
|
||||
raise Exception("There is no active session with given id. Try again later.")
|
||||
# if active == session:
|
||||
# self.__client.expire(f"{self.__bl}:active_session", expiry_sec, gt=True)
|
||||
|
||||
@@ -402,9 +373,7 @@ class BeamlineConfig:
|
||||
)
|
||||
|
||||
def end_active_session(self, session: int) -> None:
|
||||
with redis_lock.Lock(
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
):
|
||||
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
|
||||
active = self.active_session
|
||||
if active is None:
|
||||
return
|
||||
@@ -414,9 +383,7 @@ class BeamlineConfig:
|
||||
|
||||
def force_set_active_session(self, session: int, expiry_sec: int) -> None:
|
||||
# Ensure that there is no active try-set for active session
|
||||
with redis_lock.Lock(
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
):
|
||||
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
|
||||
self.__client.set(f"{self.__bl}:active_session", session)
|
||||
self.__client.expire(f"{self.__bl}:active_session", expiry_sec)
|
||||
|
||||
@@ -481,9 +448,7 @@ class BeamlineConfig:
|
||||
if transfer is None:
|
||||
self.__client.delete(f"{self.__bl}:baton_transfer_queue")
|
||||
else:
|
||||
self.__client.set(
|
||||
f"{self.__bl}:baton_transfer_queue", transfer.model_dump_json()
|
||||
)
|
||||
self.__client.set(f"{self.__bl}:baton_transfer_queue", transfer.model_dump_json())
|
||||
|
||||
def can_transfer_baton_now(self) -> bool:
|
||||
"""Check if baton can be transferred (beamline not mid-operation)."""
|
||||
@@ -507,16 +472,11 @@ class BeamlineConfig:
|
||||
Atomically transfer the baton to a new holder.
|
||||
Use existing active_session_lock for consistency.
|
||||
"""
|
||||
with redis_lock.Lock(
|
||||
self.__client, f"{self.__bl}:active_session_lock", expire=10
|
||||
):
|
||||
with redis_lock.Lock(self.__client, f"{self.__bl}:active_session_lock", expire=10):
|
||||
self.__client.set(f"{self.__bl}:active_session", to_session)
|
||||
self.__client.expire(f"{self.__bl}:active_session", expiry_sec)
|
||||
self.baton_holder = BatonHolderInfo(
|
||||
username=to_username,
|
||||
session=to_session,
|
||||
is_staff=to_is_staff,
|
||||
pgroup=to_pgroup,
|
||||
username=to_username, session=to_session, is_staff=to_is_staff, pgroup=to_pgroup
|
||||
)
|
||||
# Clear any pending request or queued transfer
|
||||
self.clear_pending_baton_request()
|
||||
@@ -635,14 +595,11 @@ class BeamlineConfig:
|
||||
def pixel_to_mm(self, zoom: float) -> float:
|
||||
cfg = self.settings
|
||||
base_pixel_in_mm = 1.0 / (
|
||||
cfg.camera_translation_factor_b
|
||||
* np.exp(cfg.camera_translation_factor_a * zoom)
|
||||
cfg.camera_translation_factor_b * np.exp(cfg.camera_translation_factor_a * zoom)
|
||||
)
|
||||
# Apply lens magnification correction relative to the default 10x lens.
|
||||
# A lower magnification lens (e.g. 5x) makes each pixel cover more physical space.
|
||||
lens_magnification = cfg_get(
|
||||
"daq.hardware.lens_magnification", DEFAULT_LENS_MAGNIFICATION
|
||||
)
|
||||
lens_magnification = cfg_get("daq.hardware.lens_magnification", DEFAULT_LENS_MAGNIFICATION)
|
||||
try:
|
||||
lens_magnification = float(lens_magnification)
|
||||
except (TypeError, ValueError):
|
||||
@@ -659,15 +616,11 @@ class BeamlineConfig:
|
||||
z = ln(lens_factor / (b * target)) / a
|
||||
"""
|
||||
if target_pixel_in_mm <= 0:
|
||||
raise ValueError(
|
||||
f"target_pixel_in_mm must be > 0, got {target_pixel_in_mm}"
|
||||
)
|
||||
raise ValueError(f"target_pixel_in_mm must be > 0, got {target_pixel_in_mm}")
|
||||
cfg = self.settings
|
||||
a = cfg.camera_translation_factor_a
|
||||
b = cfg.camera_translation_factor_b
|
||||
lens_magnification = cfg_get(
|
||||
"daq.hardware.lens_magnification", DEFAULT_LENS_MAGNIFICATION
|
||||
)
|
||||
lens_magnification = cfg_get("daq.hardware.lens_magnification", DEFAULT_LENS_MAGNIFICATION)
|
||||
try:
|
||||
lens_magnification = float(lens_magnification)
|
||||
except (TypeError, ValueError):
|
||||
@@ -733,9 +686,7 @@ class BeamlineConfig:
|
||||
def settings(self, data: BeamlineSettingsModel):
|
||||
with redis_lock.Lock(self.__client, f"{self.__bl}:settings_lock", expire=10):
|
||||
current = self.__get_settings()
|
||||
updated_data = current.model_copy(
|
||||
update=data.model_dump(exclude_unset=True)
|
||||
)
|
||||
updated_data = current.model_copy(update=data.model_dump(exclude_unset=True))
|
||||
self.__client.set(f"{self.__bl}:settings", updated_data.model_dump_json())
|
||||
|
||||
@property
|
||||
@@ -752,14 +703,10 @@ class BeamlineConfig:
|
||||
self.__client.set(f"{self.__bl}:cryojet_settings", data.model_dump_json())
|
||||
|
||||
def get_alc_bkg(self, zoom: float, exp: float, gain: float) -> np.ndarray | None:
|
||||
return base64_to_numpy(
|
||||
self.__client.get(f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}")
|
||||
)
|
||||
return base64_to_numpy(self.__client.get(f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}"))
|
||||
|
||||
def put_alc_bkg(self, zoom: float, exp: float, gain: float, data: np.ndarray):
|
||||
self.__client.set(
|
||||
f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}", numpy_to_base64(data)
|
||||
)
|
||||
self.__client.set(f"{self.__bl}:bkg{zoom:.1f}_{exp:.2f}_{gain:.1f}", numpy_to_base64(data))
|
||||
|
||||
@property
|
||||
def spreadsheet(self) -> SampleShortInfoList:
|
||||
@@ -881,15 +828,10 @@ class BeamlineConfig:
|
||||
mode = self.zoom_mode
|
||||
if not mode or not isinstance(mode, ZoomModeEnum):
|
||||
raise Exception("incorrect zoom settings mode used")
|
||||
self.__client.set(
|
||||
f"{self.__bl}:{self.zoom_setting_string(mode)}", data.model_dump_json()
|
||||
)
|
||||
self.__client.set(f"{self.__bl}:{self.zoom_setting_string(mode)}", data.model_dump_json())
|
||||
|
||||
def save_zoom_camera_setting(
|
||||
self,
|
||||
zoom_value: float,
|
||||
settings: SampleCameraSettings,
|
||||
mode: ZoomModeEnum | None = None,
|
||||
self, zoom_value: float, settings: SampleCameraSettings, mode: ZoomModeEnum | None = None
|
||||
) -> None:
|
||||
"""Insert/replace the camera settings for a single zoom stop in a given
|
||||
mode's table (persisted to Redis under that mode's key). Defaults to the
|
||||
@@ -897,11 +839,7 @@ class BeamlineConfig:
|
||||
mode = mode or self.zoom_mode
|
||||
key = f"{self.__bl}:{self.zoom_setting_string(mode)}"
|
||||
tmp = self.__client.get(key)
|
||||
model = (
|
||||
ZoomModel(**json.loads(tmp))
|
||||
if tmp is not None
|
||||
else zoom_manager(mode, self.__mxb)
|
||||
)
|
||||
model = ZoomModel(**json.loads(tmp)) if tmp is not None else zoom_manager(mode, self.__mxb)
|
||||
model.z[zoom_value] = settings
|
||||
self.__client.set(key, model.model_dump_json())
|
||||
|
||||
@@ -960,8 +898,7 @@ class BeamlineConfig:
|
||||
|
||||
def mark_beam(self, x_pxl: float, y_pxl: float, zoom: float):
|
||||
self.__client.hset(
|
||||
f"{self.__bl}:beam_mark",
|
||||
mapping={f"{zoom}": json.dumps({"x": x_pxl, "y": y_pxl})},
|
||||
f"{self.__bl}:beam_mark", mapping={f"{zoom}": json.dumps({"x": x_pxl, "y": y_pxl})}
|
||||
)
|
||||
vals = self.__client.hgetall(f"{self.__bl}:beam_mark")
|
||||
|
||||
@@ -1061,9 +998,7 @@ class BeamlineConfig:
|
||||
if input_params is None:
|
||||
self.__client.delete(f"{self.__bl}:simple_input_params")
|
||||
else:
|
||||
self.__client.set(
|
||||
f"{self.__bl}:simple_input_params", input_params.model_dump_json()
|
||||
)
|
||||
self.__client.set(f"{self.__bl}:simple_input_params", input_params.model_dump_json())
|
||||
|
||||
@property
|
||||
def auto_params(self) -> SimpleScanParameters | None:
|
||||
@@ -1102,20 +1037,13 @@ class BeamlineConfig:
|
||||
seq = int(seq_raw) if seq_raw is not None else 0
|
||||
progress = json.loads(payload_raw) if payload_raw else None
|
||||
|
||||
return {
|
||||
"seq": seq,
|
||||
"progress": progress,
|
||||
}
|
||||
return {"seq": seq, "progress": progress}
|
||||
|
||||
def set_automation_progress_state(
|
||||
self, progress: AutomationProgress | dict
|
||||
) -> dict:
|
||||
def set_automation_progress_state(self, progress: AutomationProgress | dict) -> dict:
|
||||
def _json_default(value):
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
raise TypeError(
|
||||
f"Object of type {type(value).__name__} is not JSON serializable"
|
||||
)
|
||||
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
|
||||
|
||||
if isinstance(progress, dict):
|
||||
payload = dict(progress)
|
||||
@@ -1124,9 +1052,7 @@ class BeamlineConfig:
|
||||
elif is_dataclass(progress):
|
||||
payload = asdict(progress)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Unsupported automation progress type: {type(progress).__name__}"
|
||||
)
|
||||
raise TypeError(f"Unsupported automation progress type: {type(progress).__name__}")
|
||||
|
||||
next_seq = int(self.__client.incr(self._automation_progress_seq_key()))
|
||||
self.__client.set(
|
||||
@@ -1134,10 +1060,7 @@ class BeamlineConfig:
|
||||
json.dumps(payload, separators=(",", ":"), default=_json_default),
|
||||
)
|
||||
|
||||
return {
|
||||
"seq": next_seq,
|
||||
"progress": payload,
|
||||
}
|
||||
return {"seq": next_seq, "progress": payload}
|
||||
|
||||
@property
|
||||
def failed_mount_count(self) -> int:
|
||||
@@ -1262,12 +1185,8 @@ class BeamlineConfig:
|
||||
|
||||
def set_detector_metadata(self, payload: dict) -> dict:
|
||||
safe_payload = dict(payload or {})
|
||||
safe_payload["dtz_low"] = self._coerce_optional_float(
|
||||
safe_payload.get("dtz_low")
|
||||
)
|
||||
safe_payload["dtz_high"] = self._coerce_optional_float(
|
||||
safe_payload.get("dtz_high")
|
||||
)
|
||||
safe_payload["dtz_low"] = self._coerce_optional_float(safe_payload.get("dtz_low"))
|
||||
safe_payload["dtz_high"] = self._coerce_optional_float(safe_payload.get("dtz_high"))
|
||||
safe_payload["pixel_size_mm"] = self._coerce_optional_float(
|
||||
safe_payload.get("pixel_size_mm")
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class LocalContactConfigModel(BaseModel):
|
||||
mount_to_center_sleep_s: float = Field(
|
||||
default=0.0,
|
||||
@@ -26,4 +27,4 @@ class LocalContactConfigModel(BaseModel):
|
||||
"the ML prediction is loop_all. 0.5 means add 50% of the detected "
|
||||
"cell count above and below."
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
+143
-453
File diff suppressed because it is too large
Load Diff
+8
-33
@@ -76,9 +76,7 @@ class BeamlineDevices:
|
||||
self.__ringcurrent = self.bec_worker.ring_current
|
||||
|
||||
self.__zoom = SetGetPV(
|
||||
name=f"zoom",
|
||||
setpv=f"{BEAMLINE}-ES-MS:ZOOM.VAL",
|
||||
getpv=f"{BEAMLINE}-ES-MS:ZOOM.RBV",
|
||||
name=f"zoom", setpv=f"{BEAMLINE}-ES-MS:ZOOM.VAL", getpv=f"{BEAMLINE}-ES-MS:ZOOM.RBV"
|
||||
)
|
||||
|
||||
self.__cryojet_pos = EnumPV(
|
||||
@@ -88,9 +86,7 @@ class BeamlineDevices:
|
||||
timeout=10.0,
|
||||
)
|
||||
|
||||
self.__cryojet_x = MyMotor(
|
||||
f"{BEAMLINE}-ES-CS:TRX"
|
||||
) # currently in is 5 out is 15?
|
||||
self.__cryojet_x = MyMotor(f"{BEAMLINE}-ES-CS:TRX") # currently in is 5 out is 15?
|
||||
|
||||
self.__cryojet_temperature_get = PV(f"{BEAMLINE}-ES-CS:TEMP_RBV")
|
||||
self.__cryojet_temperature_set = PV(f"{BEAMLINE}-ES-CS:TEMP.VAL")
|
||||
@@ -121,9 +117,7 @@ class BeamlineDevices:
|
||||
try:
|
||||
self.bec_worker.shutdown_client()
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to shutdown previous BEC worker cleanly: {e}"
|
||||
)
|
||||
logger.warning(f"Failed to shutdown previous BEC worker cleanly: {e}")
|
||||
finally:
|
||||
beamline = MXBeamline.SIMULATED if simulated else self._beamline
|
||||
logger.info(f"Restarting BEC worker with simulated={simulated}")
|
||||
@@ -251,8 +245,7 @@ class BeamlineDevices:
|
||||
@property
|
||||
def samcam_settings(self) -> SampleCameraSettings:
|
||||
return SampleCameraSettings(
|
||||
gain=self.__sample_cam.gain_rbv.value,
|
||||
exposure=self.__sample_cam.expo_rbv.value,
|
||||
gain=self.__sample_cam.gain_rbv.value, exposure=self.__sample_cam.expo_rbv.value
|
||||
)
|
||||
|
||||
@samcam_settings.setter
|
||||
@@ -314,17 +307,9 @@ class BeamlineDevices:
|
||||
|
||||
@aerotech_pos.setter
|
||||
def aerotech_pos(
|
||||
self,
|
||||
coord: AerotechCoordinate,
|
||||
/,
|
||||
wait: bool = True,
|
||||
incremental: bool = False,
|
||||
self, coord: AerotechCoordinate, /, wait: bool = True, incremental: bool = False
|
||||
):
|
||||
self.aerotech.position(
|
||||
coord,
|
||||
wait=wait,
|
||||
incremental=incremental,
|
||||
)
|
||||
self.aerotech.position(coord, wait=wait, incremental=incremental)
|
||||
|
||||
@property
|
||||
def aerotech_omega(self) -> float:
|
||||
@@ -334,19 +319,9 @@ class BeamlineDevices:
|
||||
def aerotech_omega(self, val: float):
|
||||
self.set_aerotech_omega(val, wait=True)
|
||||
|
||||
def set_aerotech_omega(
|
||||
self,
|
||||
val: float,
|
||||
/,
|
||||
wait: bool = True,
|
||||
incremental: bool = False,
|
||||
):
|
||||
def set_aerotech_omega(self, val: float, /, wait: bool = True, incremental: bool = False):
|
||||
target = AerotechCoordinate(omega_deg=val)
|
||||
return self.aerotech.position(
|
||||
target,
|
||||
wait=wait,
|
||||
incremental=incremental,
|
||||
)
|
||||
return self.aerotech.position(target, wait=wait, incremental=incremental)
|
||||
|
||||
def aerotech_stop(self):
|
||||
# TODO link cancel in GUI to cancel in aerotech if not already done
|
||||
|
||||
+23
-96
@@ -7,12 +7,7 @@ import numpy as np
|
||||
from aarecommon.config.logger import setup_logger
|
||||
from aarecommon.config.logger_events import log_timing
|
||||
from aarecommon.models.beamline import MXBeamline
|
||||
from aarecommon.models.models import (
|
||||
BoundingBoxModel,
|
||||
MLBoxModel,
|
||||
MLBoxType,
|
||||
MLOutputModel,
|
||||
)
|
||||
from aarecommon.models.models import BoundingBoxModel, MLBoxModel, MLBoxType, MLOutputModel
|
||||
from aarelcinfer_client.models import LatestPredictionModel
|
||||
|
||||
logger = setup_logger("aareDAQ")
|
||||
@@ -118,9 +113,7 @@ class MlBox:
|
||||
def _prediction_score(predictions: MLOutputModel | None) -> tuple[int, float]:
|
||||
if predictions is None or not predictions.boxes:
|
||||
return (0, 0.0)
|
||||
confs = [
|
||||
float(m.conf or 0.0) for m in predictions.boxes.values() if m is not None
|
||||
]
|
||||
confs = [float(m.conf or 0.0) for m in predictions.boxes.values() if m is not None]
|
||||
return (len(confs), max(confs) if confs else 0.0)
|
||||
|
||||
def _fetch_prediction_bundle(self):
|
||||
@@ -128,9 +121,7 @@ class MlBox:
|
||||
for attempt in range(1, self.RETRY_COUNT + 1):
|
||||
try:
|
||||
bundle = self.__wrapper.get_latest_prediction_bundle()
|
||||
logger.debug(
|
||||
f"Fetched prediction bundle on attempt {attempt}/{self.RETRY_COUNT}"
|
||||
)
|
||||
logger.debug(f"Fetched prediction bundle on attempt {attempt}/{self.RETRY_COUNT}")
|
||||
return bundle
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
@@ -165,9 +156,7 @@ class MlBox:
|
||||
if isinstance(raw, (list, tuple)) and len(raw) >= 2:
|
||||
return float(raw[0]), float(raw[1])
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to parse target_point from prediction metadata: {e}"
|
||||
)
|
||||
logger.warning(f"Failed to parse target_point from prediction metadata: {e}")
|
||||
|
||||
return None
|
||||
|
||||
@@ -182,9 +171,7 @@ class MlBox:
|
||||
if raw_focus is not None:
|
||||
focus = float(raw_focus)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to parse focus_score from prediction metadata: {e}"
|
||||
)
|
||||
logger.warning(f"Failed to parse focus_score from prediction metadata: {e}")
|
||||
|
||||
return MLBundleMeta(target_point=target_point, focus=focus)
|
||||
|
||||
@@ -242,13 +229,9 @@ class MlBox:
|
||||
if best_predictions is None:
|
||||
logger.info("No valid prediction bundle metadata was available")
|
||||
elif not best_predictions.boxes:
|
||||
logger.info(
|
||||
"Prediction bundle candidates contained no supported detections"
|
||||
)
|
||||
logger.info("Prediction bundle candidates contained no supported detections")
|
||||
|
||||
return MLBundle(
|
||||
predictions=best_predictions, image=best_image, bundle_meta=best_meta
|
||||
)
|
||||
return MLBundle(predictions=best_predictions, image=best_image, bundle_meta=best_meta)
|
||||
|
||||
def _get_latest_bundle_image(self) -> np.ndarray | None:
|
||||
bundle = self._fetch_prediction_bundle()
|
||||
@@ -264,18 +247,8 @@ class MlBox:
|
||||
# Extract (x1,y1,x2,y2) from MLBoxModel/BoundingBoxModel
|
||||
if not box0 or not box0.box or not box1 or not box1.box:
|
||||
raise ValueError("Invalid boxes passed to check_box_relation")
|
||||
x10, y10, x20, y20 = (
|
||||
box0.box.top_x,
|
||||
box0.box.top_y,
|
||||
box0.box.bottom_x,
|
||||
box0.box.bottom_y,
|
||||
)
|
||||
x11, y11, x21, y21 = (
|
||||
box1.box.top_x,
|
||||
box1.box.top_y,
|
||||
box1.box.bottom_x,
|
||||
box1.box.bottom_y,
|
||||
)
|
||||
x10, y10, x20, y20 = (box0.box.top_x, box0.box.top_y, box0.box.bottom_x, box0.box.bottom_y)
|
||||
x11, y11, x21, y21 = (box1.box.top_x, box1.box.top_y, box1.box.bottom_x, box1.box.bottom_y)
|
||||
left, top, overlap_x, overlap_y = False, False, False, False
|
||||
|
||||
# Compute centers for robustness
|
||||
@@ -298,12 +271,7 @@ class MlBox:
|
||||
else:
|
||||
overlap_y = True
|
||||
|
||||
return {
|
||||
"left": left,
|
||||
"top": top,
|
||||
"overlap_x": overlap_x,
|
||||
"overlap_y": overlap_y,
|
||||
}
|
||||
return {"left": left, "top": top, "overlap_x": overlap_x, "overlap_y": overlap_y}
|
||||
|
||||
def box_relation(self, boxes: MLOutputModel, classes: list[str] | None = None):
|
||||
pin: MLBoxModel | None = boxes.get_best_for_class(MLBoxType.PIN)
|
||||
@@ -314,11 +282,7 @@ class MlBox:
|
||||
box_relative_to_pin: dict[str, bool] = {}
|
||||
for box_type in classes:
|
||||
# Ensure the entry exists and is an MLBoxModel
|
||||
model = (
|
||||
boxes.get(box_type)
|
||||
if hasattr(boxes, "get")
|
||||
else boxes.boxes.get(box_type)
|
||||
)
|
||||
model = boxes.get(box_type) if hasattr(boxes, "get") else boxes.boxes.get(box_type)
|
||||
if model:
|
||||
box_relative_to_pin = self.check_box_relation(pin, model)
|
||||
|
||||
@@ -428,15 +392,9 @@ class MlBox:
|
||||
logger.debug(f"Failed to parse bundle detection: {e}")
|
||||
continue
|
||||
|
||||
out.add_box(
|
||||
cls=cls,
|
||||
box_tuple=(x1, y1, x2, y2),
|
||||
conf=conf,
|
||||
)
|
||||
out.add_box(cls=cls, box_tuple=(x1, y1, x2, y2), conf=conf)
|
||||
|
||||
logger.debug(
|
||||
f"Parsed {len(out.boxes)} supported detections from bundle metadata"
|
||||
)
|
||||
logger.debug(f"Parsed {len(out.boxes)} supported detections from bundle metadata")
|
||||
return out
|
||||
|
||||
def get_best_detections(self, results) -> MLOutputModel | None:
|
||||
@@ -464,21 +422,14 @@ class MlBox:
|
||||
Best MLBoxModel according to preferences
|
||||
"""
|
||||
if preferred_class is None:
|
||||
order = (
|
||||
MLBoxType.CRYSTAL,
|
||||
MLBoxType.LOOP_FACE,
|
||||
MLBoxType.LOOP_ALL,
|
||||
MLBoxType.PIN,
|
||||
)
|
||||
order = (MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN)
|
||||
else:
|
||||
if isinstance(preferred_class, MLBoxType):
|
||||
order = (preferred_class,)
|
||||
elif isinstance(preferred_class, int):
|
||||
order = (MLBoxType(preferred_class),)
|
||||
else:
|
||||
order = tuple(
|
||||
MLBoxType(c) if isinstance(c, int) else c for c in preferred_class
|
||||
)
|
||||
order = tuple(MLBoxType(c) if isinstance(c, int) else c for c in preferred_class)
|
||||
|
||||
# Get best box from each class
|
||||
best_boxes = {}
|
||||
@@ -515,25 +466,17 @@ class MlBox:
|
||||
|
||||
@staticmethod
|
||||
def get_preferred_class_box(
|
||||
boxes: MLOutputModel,
|
||||
preferred_class: Optional[Iterable[int] | int | MLBoxType] = None,
|
||||
boxes: MLOutputModel, preferred_class: Optional[Iterable[int] | int | MLBoxType] = None
|
||||
) -> Optional[MLBoxModel]:
|
||||
if preferred_class is None:
|
||||
order = (
|
||||
MLBoxType.CRYSTAL,
|
||||
MLBoxType.LOOP_FACE,
|
||||
MLBoxType.LOOP_ALL,
|
||||
MLBoxType.PIN,
|
||||
)
|
||||
order = (MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN)
|
||||
else:
|
||||
if isinstance(preferred_class, MLBoxType):
|
||||
order = (preferred_class,)
|
||||
elif isinstance(preferred_class, int):
|
||||
order = (MLBoxType(preferred_class),)
|
||||
else:
|
||||
order = tuple(
|
||||
MLBoxType(c) if isinstance(c, int) else c for c in preferred_class
|
||||
)
|
||||
order = tuple(MLBoxType(c) if isinstance(c, int) else c for c in preferred_class)
|
||||
|
||||
for cls in order:
|
||||
m = boxes.get_best_for_class(cls)
|
||||
@@ -548,12 +491,7 @@ class MlBox:
|
||||
confidence_min: float | None = None,
|
||||
return_image: bool = False,
|
||||
return_bundle_meta: bool = False,
|
||||
) -> (
|
||||
None
|
||||
| MLBoxModel
|
||||
| tuple[MLBoxModel | None, np.ndarray | None]
|
||||
| MLBoxPredictionResult
|
||||
):
|
||||
) -> None | MLBoxModel | tuple[MLBoxModel | None, np.ndarray | None] | MLBoxPredictionResult:
|
||||
ml_bundle = self._collect_best_bundle()
|
||||
predictions, image, bundle_meta = (
|
||||
ml_bundle.predictions,
|
||||
@@ -620,11 +558,7 @@ class MlBox:
|
||||
| MLBoxPredictionsResult
|
||||
):
|
||||
ml_bundle = self._collect_best_bundle()
|
||||
best, image, bundle_meta = (
|
||||
ml_bundle.predictions,
|
||||
ml_bundle.image,
|
||||
ml_bundle.bundle_meta,
|
||||
)
|
||||
best, image, bundle_meta = (ml_bundle.predictions, ml_bundle.image, ml_bundle.bundle_meta)
|
||||
if not best:
|
||||
logger.debug(f"No best predictions from ML bundle: {best}")
|
||||
if return_bundle_meta:
|
||||
@@ -654,10 +588,7 @@ class MlBox:
|
||||
overlap_parameter: float | None = None,
|
||||
confidence_filter: float | None = None,
|
||||
return_image: bool = False,
|
||||
) -> (
|
||||
dict[str, list[MLBoxModel]]
|
||||
| tuple[dict[str, list[MLBoxModel]], np.ndarray | None]
|
||||
):
|
||||
) -> dict[str, list[MLBoxModel]] | tuple[dict[str, list[MLBoxModel]], np.ndarray | None]:
|
||||
"""
|
||||
Return a dict keyed by '<MLBoxTypeName>_<ordinal>' -> [MLBoxModel, ...] for each detection.
|
||||
The ordinal is the running count per class (1-based) in the order they appear after filtering.
|
||||
@@ -673,9 +604,7 @@ class MlBox:
|
||||
return ({}, image) if return_image else {}
|
||||
|
||||
self._filter_predictions(
|
||||
grouped,
|
||||
overlap_with_pin=overlap_parameter,
|
||||
confidence_min=confidence_filter,
|
||||
grouped, overlap_with_pin=overlap_parameter, confidence_min=confidence_filter
|
||||
)
|
||||
|
||||
per_class_counter: dict[MLBoxType, int] = {}
|
||||
@@ -730,9 +659,7 @@ class MlBox:
|
||||
logger.debug(f"Frame {frame_idx + 1}/{n_frames}: no detection")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Frame {frame_idx + 1}/{n_frames}: prediction error: {e}"
|
||||
)
|
||||
logger.warning(f"Frame {frame_idx + 1}/{n_frames}: prediction error: {e}")
|
||||
continue
|
||||
|
||||
if best_box is None:
|
||||
|
||||
@@ -41,12 +41,7 @@ def _box_tuple(model) -> BoxTuple | None:
|
||||
|
||||
|
||||
def _box_extends_beyond(inner: BoxTuple, outer: BoxTuple) -> bool:
|
||||
return (
|
||||
inner[0] < outer[0]
|
||||
or inner[1] < outer[1]
|
||||
or inner[2] > outer[2]
|
||||
or inner[3] > outer[3]
|
||||
)
|
||||
return inner[0] < outer[0] or inner[1] < outer[1] or inner[2] > outer[2] or inner[3] > outer[3]
|
||||
|
||||
|
||||
def _box_union(a: BoxTuple, b: BoxTuple) -> BoxTuple:
|
||||
@@ -114,9 +109,7 @@ def get_ml_bounding_box(
|
||||
sample_id = getattr(sample, "db_id", None)
|
||||
|
||||
prediction_result: MLBoxPredictionResult = mlbox.predict(
|
||||
preferred_class=(3, 0),
|
||||
return_image=True,
|
||||
return_bundle_meta=True,
|
||||
preferred_class=(3, 0), return_image=True, return_bundle_meta=True
|
||||
)
|
||||
m = prediction_result.box
|
||||
bundle_image = prediction_result.image
|
||||
@@ -145,9 +138,7 @@ def get_ml_bounding_box(
|
||||
|
||||
if filename is not None and bundle_image is not None:
|
||||
annotated_image = bundle_image.copy()
|
||||
cv2.rectangle(
|
||||
annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2
|
||||
)
|
||||
cv2.rectangle(annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
|
||||
upload_image(sample_id, filename, annotated_image)
|
||||
|
||||
geom = sample_geometry
|
||||
@@ -155,10 +146,7 @@ def get_ml_bounding_box(
|
||||
"ML bounding box selected",
|
||||
extra=merge_log_context(
|
||||
sample_log_context(sample),
|
||||
{
|
||||
"sample_id": sample_id,
|
||||
"ml_image_name": filename,
|
||||
},
|
||||
{"sample_id": sample_id, "ml_image_name": filename},
|
||||
geom_log_context(geom),
|
||||
{
|
||||
"box_x1": x1,
|
||||
@@ -214,9 +202,7 @@ def _box_to_raster_request(
|
||||
# of the n_y scan) can be padded more than the top.
|
||||
frac_x = float(cfg_get("daq.auto_raster.grid_padding_fraction_x", 0.15))
|
||||
frac_y_top = float(cfg_get("daq.auto_raster.grid_padding_fraction_y", 0.15))
|
||||
frac_y_bottom = float(
|
||||
cfg_get("daq.auto_raster.grid_padding_fraction_y_bottom", frac_y_top)
|
||||
)
|
||||
frac_y_bottom = float(cfg_get("daq.auto_raster.grid_padding_fraction_y_bottom", frac_y_top))
|
||||
pad_x = max(1, int(ceil(frac_x * n_x)))
|
||||
pad_y_top = max(1, int(ceil(frac_y_top * n_y)))
|
||||
pad_y_bottom = max(1, int(ceil(frac_y_bottom * n_y)))
|
||||
@@ -307,9 +293,7 @@ def _box_to_raster_request(
|
||||
exp_time_s=0.01,
|
||||
transmission=1.0,
|
||||
smargon_top_left=SmargonCoordinate(
|
||||
chi_deg=geom.smargon.chi_deg,
|
||||
phi_deg=geom.smargon.phi_deg,
|
||||
sh_mm=start_coord,
|
||||
chi_deg=geom.smargon.chi_deg, phi_deg=geom.smargon.phi_deg, sh_mm=start_coord
|
||||
),
|
||||
n_x=n_x,
|
||||
n_y=n_y,
|
||||
@@ -343,8 +327,7 @@ def build_ml_raster_plan(
|
||||
sample_id = getattr(sample, "db_id", None)
|
||||
|
||||
result: MLBoxPredictionsResult = mlbox.predict_all_best(
|
||||
return_image=True,
|
||||
return_bundle_meta=True,
|
||||
return_image=True, return_bundle_meta=True
|
||||
)
|
||||
predictions = result.predictions
|
||||
bundle_image = result.image
|
||||
@@ -356,12 +339,8 @@ def build_ml_raster_plan(
|
||||
focus=result.focus,
|
||||
)
|
||||
|
||||
loop_all = (
|
||||
predictions.get_best_for_class(MLBoxType.LOOP_ALL) if predictions else None
|
||||
)
|
||||
loop_face = (
|
||||
predictions.get_best_for_class(MLBoxType.LOOP_FACE) if predictions else None
|
||||
)
|
||||
loop_all = predictions.get_best_for_class(MLBoxType.LOOP_ALL) if predictions else None
|
||||
loop_face = predictions.get_best_for_class(MLBoxType.LOOP_FACE) if predictions else None
|
||||
|
||||
# Grid box: prefer loop_face, else loop_all (matches the legacy (3, 0) order).
|
||||
grid_model = loop_face if loop_face is not None else loop_all
|
||||
@@ -398,18 +377,12 @@ def build_ml_raster_plan(
|
||||
x1, y1, x2, y2 = _box_union((x1, y1, x2, y2), cbox)
|
||||
logger.info(
|
||||
"Extended ML raster grid to include crystal outside the loop box",
|
||||
extra={
|
||||
"sample_id": sample_id,
|
||||
"ml_image_name": filename,
|
||||
"crystal_box": cbox,
|
||||
},
|
||||
extra={"sample_id": sample_id, "ml_image_name": filename, "crystal_box": cbox},
|
||||
)
|
||||
|
||||
if filename is not None and bundle_image is not None:
|
||||
annotated_image = bundle_image.copy()
|
||||
cv2.rectangle(
|
||||
annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2
|
||||
)
|
||||
cv2.rectangle(annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
|
||||
upload_image(sample_id, filename, annotated_image)
|
||||
|
||||
grid_request = _box_to_raster_request(
|
||||
|
||||
@@ -24,6 +24,7 @@ class BeamlineDependencies:
|
||||
cfg: BeamlineConfig
|
||||
devs: BeamlineDevices
|
||||
|
||||
|
||||
@dataclass
|
||||
class OperationResult:
|
||||
success: bool
|
||||
@@ -52,4 +53,4 @@ class BaseOperationContext(Generic[DepsT, SettingsT]):
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self.runtime.status
|
||||
return self.runtime.status
|
||||
|
||||
@@ -28,7 +28,9 @@ class SampleEventSender(Protocol):
|
||||
class ScanIngestor(Protocol):
|
||||
def ingest_scan(self, *, sample, result, geom, beam_mark_pxl) -> None: ...
|
||||
|
||||
def ingest_gridscan(self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl) -> None: ...
|
||||
def ingest_gridscan(
|
||||
self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class DatacollectionSetupRunner(Protocol):
|
||||
@@ -81,13 +83,12 @@ class ScanIngestionService:
|
||||
|
||||
def ingest_scan(self, *, sample, result, geom, beam_mark_pxl) -> None:
|
||||
self.ingestor.ingest_scan(
|
||||
sample=sample,
|
||||
result=result,
|
||||
geom=geom,
|
||||
beam_mark_pxl=beam_mark_pxl,
|
||||
sample=sample, result=result, geom=geom, beam_mark_pxl=beam_mark_pxl
|
||||
)
|
||||
|
||||
def ingest_gridscan(self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl) -> None:
|
||||
def ingest_gridscan(
|
||||
self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl
|
||||
) -> None:
|
||||
self.ingestor.ingest_gridscan(
|
||||
sample=sample,
|
||||
raster_result=raster_result,
|
||||
@@ -115,4 +116,4 @@ class OperationServices:
|
||||
face_detection_progress: FaceDetectionProgressEmitter | None = None
|
||||
events: SampleEventPublisher | None = None
|
||||
ingestion: ScanIngestionService | None = None
|
||||
datacollection: DataCollectionPreparer | None = None
|
||||
datacollection: DataCollectionPreparer | None = None
|
||||
|
||||
@@ -6,11 +6,7 @@ from jfjoch_client import ScanResult, ScanResultImagesInner
|
||||
|
||||
|
||||
def build_fake_scan_result(
|
||||
*,
|
||||
file_prefix: str | None,
|
||||
image_count: int,
|
||||
rotation: bool = False,
|
||||
start_angle: float = 0.0,
|
||||
*, file_prefix: str | None, image_count: int, rotation: bool = False, start_angle: float = 0.0
|
||||
) -> ScanResult:
|
||||
total_images = max(1, image_count)
|
||||
angle_step = 360.0 / total_images if rotation else 0.0
|
||||
@@ -33,9 +29,7 @@ def build_fake_scan_result(
|
||||
|
||||
|
||||
def build_fake_rotation_result(
|
||||
request: RotationScanRequest,
|
||||
*,
|
||||
start_angle: float = 0.0,
|
||||
request: RotationScanRequest, *, start_angle: float = 0.0
|
||||
) -> CompletedRotationScan:
|
||||
result = build_fake_scan_result(
|
||||
file_prefix=request.file_prefix,
|
||||
@@ -43,20 +37,13 @@ def build_fake_rotation_result(
|
||||
rotation=True,
|
||||
start_angle=start_angle,
|
||||
)
|
||||
return CompletedRotationScan(
|
||||
request=copy.deepcopy(request),
|
||||
result=result,
|
||||
)
|
||||
return CompletedRotationScan(request=copy.deepcopy(request), result=result)
|
||||
|
||||
|
||||
def build_fake_raster_result(request: RasterGridRequest) -> CompletedRasterGridElem:
|
||||
result = build_fake_scan_result(
|
||||
file_prefix=request.file_prefix,
|
||||
image_count=request.n_x * request.n_y,
|
||||
rotation=False,
|
||||
file_prefix=request.file_prefix, image_count=request.n_x * request.n_y, rotation=False
|
||||
)
|
||||
return CompletedRasterGridElem(
|
||||
request=copy.deepcopy(request),
|
||||
result=result,
|
||||
centre_of_mass=None,
|
||||
request=copy.deepcopy(request), result=result, centre_of_mass=None
|
||||
)
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
from aare.daq.operations.face_detection.models import (
|
||||
FaceDetectionContext,
|
||||
FaceDetectionResult,
|
||||
)
|
||||
from aare.daq.operations.face_detection.models import FaceDetectionContext, FaceDetectionResult
|
||||
from aare.daq.operations.face_detection.service import FaceDetectionService
|
||||
|
||||
__all__ = [
|
||||
"FaceDetectionContext",
|
||||
"FaceDetectionResult",
|
||||
"FaceDetectionService",
|
||||
]
|
||||
__all__ = ["FaceDetectionContext", "FaceDetectionResult", "FaceDetectionService"]
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aare.daq.mlbox import MlBox
|
||||
from aare.daq.operations.common.models import (
|
||||
BaseOperationContext,
|
||||
BeamlineDependencies,
|
||||
)
|
||||
from aare.daq.operations.common.models import BaseOperationContext, BeamlineDependencies
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -20,9 +17,7 @@ class FaceDetectionSettings:
|
||||
|
||||
|
||||
@dataclass
|
||||
class FaceDetectionContext(
|
||||
BaseOperationContext[FaceDetectionDependencies, FaceDetectionSettings]
|
||||
):
|
||||
class FaceDetectionContext(BaseOperationContext[FaceDetectionDependencies, FaceDetectionSettings]):
|
||||
pass
|
||||
|
||||
|
||||
@@ -31,4 +26,4 @@ class FaceDetectionResult:
|
||||
success: bool
|
||||
payload: dict
|
||||
error: Exception | None = None
|
||||
comment: str | None = None
|
||||
comment: str | None = None
|
||||
|
||||
@@ -6,10 +6,7 @@ from aarecommon.models.models import MLBoxModel, ZoomModeEnum
|
||||
|
||||
import aare.daq.operations.face_detection.utils as fd
|
||||
from aare.daq.mlbox import MLBoxPredictionResult
|
||||
from aare.daq.operations.face_detection.models import (
|
||||
FaceDetectionContext,
|
||||
FaceDetectionResult,
|
||||
)
|
||||
from aare.daq.operations.face_detection.models import FaceDetectionContext, FaceDetectionResult
|
||||
|
||||
|
||||
class FaceDetectionService:
|
||||
@@ -20,9 +17,7 @@ class FaceDetectionService:
|
||||
def _progress_emitter(self):
|
||||
emitter = self.ctx.services.face_detection_progress
|
||||
if emitter is None:
|
||||
raise RuntimeError(
|
||||
"FaceDetectionService requires services.face_detection_progress"
|
||||
)
|
||||
raise RuntimeError("FaceDetectionService requires services.face_detection_progress")
|
||||
return emitter
|
||||
|
||||
def _log_warning(self, message: str) -> None:
|
||||
@@ -36,10 +31,7 @@ class FaceDetectionService:
|
||||
fallback(message)
|
||||
|
||||
def _emit_running_progress(
|
||||
self,
|
||||
*,
|
||||
angle: int,
|
||||
boxes_face: dict[int, tuple[float, float, float, float]],
|
||||
self, *, angle: int, boxes_face: dict[int, tuple[float, float, float, float]]
|
||||
) -> None:
|
||||
self._progress_emitter().emit_progress(
|
||||
{
|
||||
@@ -52,12 +44,7 @@ class FaceDetectionService:
|
||||
)
|
||||
|
||||
def _emit_empty_result(self) -> dict:
|
||||
payload = {
|
||||
"running": False,
|
||||
"samples": [],
|
||||
"height_fit": {},
|
||||
"area_fit": {},
|
||||
}
|
||||
payload = {"running": False, "samples": [], "height_fit": {}, "area_fit": {}}
|
||||
self._progress_emitter().emit_progress(payload)
|
||||
return payload
|
||||
|
||||
@@ -87,9 +74,7 @@ class FaceDetectionService:
|
||||
steps = self.ctx.settings.steps if steps is None else steps
|
||||
step_size = self.ctx.settings.step_size if step_size is None else step_size
|
||||
face_min_ratio = (
|
||||
self.ctx.settings.face_min_ratio
|
||||
if face_min_ratio is None
|
||||
else face_min_ratio
|
||||
self.ctx.settings.face_min_ratio if face_min_ratio is None else face_min_ratio
|
||||
)
|
||||
try:
|
||||
self.ctx.deps.cfg.zoom_mode = ZoomModeEnum.LoopCenter
|
||||
@@ -119,9 +104,7 @@ class FaceDetectionService:
|
||||
)
|
||||
|
||||
prediction_result: MLBoxPredictionResult = self.ctx.deps.mlbox.predict(
|
||||
preferred_class=(3, 0),
|
||||
return_image=True,
|
||||
return_bundle_meta=True,
|
||||
preferred_class=(3, 0), return_image=True, return_bundle_meta=True
|
||||
)
|
||||
model = prediction_result.box
|
||||
log_ml_bundle_meta(
|
||||
@@ -167,9 +150,7 @@ class FaceDetectionService:
|
||||
return FaceDetectionResult(success=True, payload=payload)
|
||||
|
||||
total_detections = len(boxes_face) + len(boxes_loop)
|
||||
face_ratio = (
|
||||
len(boxes_face) / total_detections if total_detections > 0 else 0.0
|
||||
)
|
||||
face_ratio = len(boxes_face) / total_detections if total_detections > 0 else 0.0
|
||||
|
||||
if boxes_face and face_ratio >= face_min_ratio:
|
||||
boxes = boxes_face
|
||||
@@ -183,13 +164,9 @@ class FaceDetectionService:
|
||||
)
|
||||
else:
|
||||
boxes = boxes_face
|
||||
self.logger.debug(
|
||||
f"using loop_face boxes (only source, {len(boxes_face)} entries)"
|
||||
)
|
||||
self.logger.debug(f"using loop_face boxes (only source, {len(boxes_face)} entries)")
|
||||
|
||||
best_fit_angle_area, area_params = fd.get_flat_face(
|
||||
boxes, start_angle, end_angle, True
|
||||
)
|
||||
best_fit_angle_area, area_params = fd.get_flat_face(boxes, start_angle, end_angle, True)
|
||||
best_fit_angle_height, height_params = fd.get_flat_face(
|
||||
boxes, start_angle, end_angle, False
|
||||
)
|
||||
@@ -237,10 +214,7 @@ class FaceDetectionService:
|
||||
self.logger.error(f"error in face detection sequence {e}")
|
||||
payload = self._emit_empty_result()
|
||||
return FaceDetectionResult(
|
||||
success=False,
|
||||
payload=payload,
|
||||
error=e,
|
||||
comment="Face detection sequence failed",
|
||||
success=False, payload=payload, error=e, comment="Face detection sequence failed"
|
||||
)
|
||||
finally:
|
||||
self.ctx.deps.cfg.zoom_mode = ZoomModeEnum.User
|
||||
|
||||
@@ -33,15 +33,11 @@ def prepare_samples(
|
||||
return samples
|
||||
|
||||
|
||||
def cos_model(
|
||||
theta_deg: float | np.ndarray, A: float, B: float, phi_rad: float, C: float
|
||||
):
|
||||
def cos_model(theta_deg: float | np.ndarray, A: float, B: float, phi_rad: float, C: float):
|
||||
return A + B * np.cos(C * np.deg2rad(theta_deg) - phi_rad)
|
||||
|
||||
|
||||
def mad_filter(
|
||||
samples: List[Tuple[float, float]], k: float = 3.5
|
||||
) -> List[Tuple[float, float]]:
|
||||
def mad_filter(samples: List[Tuple[float, float]], k: float = 3.5) -> List[Tuple[float, float]]:
|
||||
if not samples:
|
||||
return samples
|
||||
ys = [y for _, y in samples]
|
||||
@@ -75,15 +71,7 @@ def fit_cosine(samples: List[Tuple[float, float]]) -> dict:
|
||||
samples = mad_filter(samples, k=3.5)
|
||||
if len(samples) < 3:
|
||||
A = sum(y for _, y in samples) / max(1, len(samples))
|
||||
return {
|
||||
"A": A,
|
||||
"B": 1.0,
|
||||
"phi_rad": 0.0,
|
||||
"C": 1.0,
|
||||
"rmse": None,
|
||||
"mae": None,
|
||||
"r2": None,
|
||||
}
|
||||
return {"A": A, "B": 1.0, "phi_rad": 0.0, "C": 1.0, "rmse": None, "mae": None, "r2": None}
|
||||
|
||||
degs = np.array([d for d, _ in samples], dtype=float)
|
||||
ys = np.array([y for _, y in samples], dtype=float)
|
||||
@@ -119,15 +107,7 @@ def fit_cosine(samples: List[Tuple[float, float]]) -> dict:
|
||||
B = max(0.0, B)
|
||||
yhat = cos_model(degs, A, B, phi, C)
|
||||
rmse, mae, r2 = fit_metrics(ys, yhat)
|
||||
return {
|
||||
"A": A,
|
||||
"B": B,
|
||||
"phi_rad": phi,
|
||||
"C": C,
|
||||
"rmse": rmse,
|
||||
"mae": mae,
|
||||
"r2": r2,
|
||||
}
|
||||
return {"A": A, "B": B, "phi_rad": phi, "C": C, "rmse": rmse, "mae": mae, "r2": r2}
|
||||
|
||||
except Exception as e:
|
||||
# Fallback to initial
|
||||
@@ -150,9 +130,7 @@ def get_samples_out(boxes):
|
||||
for deg, box in boxes.items():
|
||||
h = box_height_from_tuple(box)
|
||||
a = box_area_from_tuple(box)
|
||||
samples_out.append(
|
||||
{"angle_deg": float(deg), "height": float(h), "area": float(a)}
|
||||
)
|
||||
samples_out.append({"angle_deg": float(deg), "height": float(h), "area": float(a)})
|
||||
samples_out.sort(key=lambda x: x["angle_deg"])
|
||||
return samples_out
|
||||
|
||||
@@ -224,9 +202,7 @@ def get_flat_face(
|
||||
return best_fit_angle, parameters
|
||||
|
||||
|
||||
def chose_best_angle(
|
||||
boxes: dict[int, tuple[float, float, float, float]], fit_results
|
||||
) -> int:
|
||||
def chose_best_angle(boxes: dict[int, tuple[float, float, float, float]], fit_results) -> int:
|
||||
measured_angles = list(boxes.keys())
|
||||
choose_best_fit(fit_results)
|
||||
candidates = [
|
||||
@@ -236,10 +212,7 @@ def chose_best_angle(
|
||||
]
|
||||
if measured_angles and candidates:
|
||||
chosen = min(
|
||||
candidates,
|
||||
key=lambda a: min(
|
||||
abs(((a - m + 180) % 360) - 180) for m in measured_angles
|
||||
),
|
||||
candidates, key=lambda a: min(abs(((a - m + 180) % 360) - 180) for m in measured_angles)
|
||||
)
|
||||
else:
|
||||
chosen = candidates[0] if candidates else 0
|
||||
|
||||
@@ -14,4 +14,4 @@ __all__ = [
|
||||
"LoopCenteringSettings",
|
||||
"LoopCenteringService",
|
||||
"LoopCenteringAnalyzer",
|
||||
]
|
||||
]
|
||||
|
||||
@@ -2,10 +2,7 @@ from aarecommon.config.logger_events import log_ml_bundle_meta
|
||||
from aarecommon.math.coordinate import Coordinate, SmargonCoordinate
|
||||
from aarecommon.models.models import MLBoxType
|
||||
|
||||
from aare.daq.operations.loop_centering.models import (
|
||||
AngleAnalysis,
|
||||
LoopCenteringContext,
|
||||
)
|
||||
from aare.daq.operations.loop_centering.models import AngleAnalysis, LoopCenteringContext
|
||||
|
||||
|
||||
class LoopCenteringAnalyzer:
|
||||
@@ -17,11 +14,7 @@ class LoopCenteringAnalyzer:
|
||||
if not classes:
|
||||
return False
|
||||
|
||||
ignore_classes = {
|
||||
MLBoxType.PIN.value,
|
||||
MLBoxType.NEEDLE.value,
|
||||
MLBoxType.ICE.value,
|
||||
}
|
||||
ignore_classes = {MLBoxType.PIN.value, MLBoxType.NEEDLE.value, MLBoxType.ICE.value}
|
||||
found = {int(cls) for cls in classes if cls is not None}
|
||||
return bool(found) and found.issubset(ignore_classes)
|
||||
|
||||
@@ -78,18 +71,14 @@ class LoopCenteringAnalyzer:
|
||||
return calculated_target
|
||||
|
||||
def _extract_prediction_target(
|
||||
self,
|
||||
*,
|
||||
target_point: tuple[float, float] | None,
|
||||
self, *, target_point: tuple[float, float] | None
|
||||
) -> SmargonCoordinate | None:
|
||||
if target_point is None:
|
||||
return None
|
||||
|
||||
geom = self.ctx.runtime.sample_geometry
|
||||
return SmargonCoordinate(
|
||||
sh_mm=geom.picture_to_smargon(
|
||||
Coordinate(x=target_point[0], y=target_point[1])
|
||||
)
|
||||
sh_mm=geom.picture_to_smargon(Coordinate(x=target_point[0], y=target_point[1]))
|
||||
)
|
||||
|
||||
def _interpret_ml_loop_centre_box(
|
||||
@@ -109,8 +98,7 @@ class LoopCenteringAnalyzer:
|
||||
pin = box
|
||||
|
||||
best_box = self.ctx.deps.mlbox.get_preferred_class_box(
|
||||
boxes,
|
||||
(MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN),
|
||||
boxes, (MLBoxType.CRYSTAL, MLBoxType.LOOP_FACE, MLBoxType.LOOP_ALL, MLBoxType.PIN)
|
||||
)
|
||||
if best_box is None or best_box.box is None or best_box.cls is None:
|
||||
return None, None, classes
|
||||
@@ -155,11 +143,7 @@ class LoopCenteringAnalyzer:
|
||||
return SmargonCoordinate(sh_mm=coord), cls, classes
|
||||
|
||||
def analyze_angle(
|
||||
self,
|
||||
*,
|
||||
angle_deg: int,
|
||||
zoom_value: float,
|
||||
sample_id: int | None,
|
||||
self, *, angle_deg: int, zoom_value: float, sample_id: int | None
|
||||
) -> AngleAnalysis:
|
||||
if self.ctx.services.predictions is None:
|
||||
raise RuntimeError("LoopCenteringAnalyzer requires services.predictions")
|
||||
@@ -173,7 +157,7 @@ class LoopCenteringAnalyzer:
|
||||
)
|
||||
|
||||
predicted_target = self._extract_prediction_target(
|
||||
target_point=prediction_result.target_point,
|
||||
target_point=prediction_result.target_point
|
||||
)
|
||||
calculated_target, selected_class, classes = self._interpret_ml_loop_centre_box(
|
||||
prediction_result.predictions
|
||||
@@ -189,7 +173,6 @@ class LoopCenteringAnalyzer:
|
||||
analysis.ignore_only = self.is_ignore_only_classes(analysis.classes)
|
||||
analysis.has_valid_target = self.has_valid_target_classes(analysis.classes)
|
||||
analysis.final_target = self.select_smargon_target(
|
||||
calculated_target=analysis.calculated_target,
|
||||
predicted_target=analysis.predicted_target,
|
||||
calculated_target=analysis.calculated_target, predicted_target=analysis.predicted_target
|
||||
)
|
||||
return analysis
|
||||
|
||||
@@ -3,10 +3,7 @@ from dataclasses import dataclass, field
|
||||
from aarecommon.math.coordinate import SmargonCoordinate
|
||||
|
||||
from aare.daq.mlbox import MlBox
|
||||
from aare.daq.operations.common.models import (
|
||||
BaseOperationContext,
|
||||
BeamlineDependencies,
|
||||
)
|
||||
from aare.daq.operations.common.models import BaseOperationContext, BeamlineDependencies
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -45,7 +42,5 @@ class LoopCenteringSettings:
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoopCenteringContext(
|
||||
BaseOperationContext[LoopCenteringDependencies, LoopCenteringSettings]
|
||||
):
|
||||
class LoopCenteringContext(BaseOperationContext[LoopCenteringDependencies, LoopCenteringSettings]):
|
||||
pass
|
||||
|
||||
@@ -40,9 +40,7 @@ class LoopCenteringService:
|
||||
)
|
||||
|
||||
analysis = self.analyzer.analyze_angle(
|
||||
angle_deg=angle,
|
||||
zoom_value=zoom_value,
|
||||
sample_id=sample_id,
|
||||
angle_deg=angle, zoom_value=zoom_value, sample_id=sample_id
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -63,13 +61,10 @@ class LoopCenteringService:
|
||||
|
||||
if sample_id is not None and trace_all_alc_moves:
|
||||
self.ctx.services.traces.append_smargon_trace(
|
||||
sample_id=sample_id,
|
||||
event=f"alc_move_zoom_{zoom_value:.0f}_angle_{angle}",
|
||||
sample_id=sample_id, event=f"alc_move_zoom_{zoom_value:.0f}_angle_{angle}"
|
||||
)
|
||||
else:
|
||||
self.logger.debug(
|
||||
f"Skipping move at angle {angle}; classes={analysis.classes}"
|
||||
)
|
||||
self.logger.debug(f"Skipping move at angle {angle}; classes={analysis.classes}")
|
||||
|
||||
if sample_id is not None and analysis.selected_class in (
|
||||
MLBoxType.LOOP_ALL.value,
|
||||
@@ -77,9 +72,7 @@ class LoopCenteringService:
|
||||
MLBoxType.CRYSTAL.value,
|
||||
):
|
||||
self.ctx.services.screenshots.save_to_db(
|
||||
sample_id,
|
||||
f"{sample_id}_{angle}_{zoom_value:.0f}",
|
||||
wait_screenshot_sleep_sec,
|
||||
sample_id, f"{sample_id}_{angle}_{zoom_value:.0f}", wait_screenshot_sleep_sec
|
||||
)
|
||||
|
||||
return analysis
|
||||
@@ -117,9 +110,7 @@ class LoopCenteringService:
|
||||
)
|
||||
|
||||
for attempt_number in range(1, max_attempts + 1):
|
||||
self.logger.info(
|
||||
f"Starting ALC attempt {attempt_number}/{max_attempts}"
|
||||
)
|
||||
self.logger.info(f"Starting ALC attempt {attempt_number}/{max_attempts}")
|
||||
attempt = AttemptSummary(attempt_number=attempt_number)
|
||||
|
||||
for angle in angles:
|
||||
@@ -133,9 +124,7 @@ class LoopCenteringService:
|
||||
attempt.first_pass.append(result)
|
||||
|
||||
for c in result.classes:
|
||||
found_classes_count[int(c)] = (
|
||||
found_classes_count.get(int(c), 0) + 1
|
||||
)
|
||||
found_classes_count[int(c)] = found_classes_count.get(int(c), 0) + 1
|
||||
|
||||
if attempt_number == 1:
|
||||
valid_seen_first_pass = any(
|
||||
@@ -159,9 +148,7 @@ class LoopCenteringService:
|
||||
attempt.correction_pass.append(result)
|
||||
|
||||
for c in result.classes:
|
||||
found_classes_count[int(c)] = (
|
||||
found_classes_count.get(int(c), 0) + 1
|
||||
)
|
||||
found_classes_count[int(c)] = found_classes_count.get(int(c), 0) + 1
|
||||
|
||||
attempts.append(attempt)
|
||||
|
||||
@@ -219,8 +206,4 @@ class LoopCenteringService:
|
||||
self.logger.error(f"ALC exception: {alc_comment}")
|
||||
self.logger.error(traceback.format_exc())
|
||||
self.logger.error(f"Error in loop centering: {e}")
|
||||
return LoopCenteringResult(
|
||||
success=False,
|
||||
comment=alc_comment,
|
||||
error=e,
|
||||
)
|
||||
return LoopCenteringResult(success=False, comment=alc_comment, error=e)
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
from aare.daq.operations.mounting.models import MountingContext, MountingResult
|
||||
from aare.daq.operations.mounting.service import MountingService
|
||||
|
||||
__all__ = [
|
||||
"MountingContext",
|
||||
"MountingResult",
|
||||
"MountingService",
|
||||
]
|
||||
__all__ = ["MountingContext", "MountingResult", "MountingService"]
|
||||
|
||||
@@ -43,9 +43,7 @@ class MountingService:
|
||||
self.logger.error(
|
||||
"Goniometer didn't reach position based on magnet position sensor readout"
|
||||
)
|
||||
raise Exception(
|
||||
"Goniometer is not in position based on magnet position sensor readout"
|
||||
)
|
||||
raise Exception("Goniometer is not in position based on magnet position sensor readout")
|
||||
|
||||
def _handle_consecutive_mount_failure(self) -> None:
|
||||
count = self.ctx.deps.cfg.increment_mount_failure_streak()
|
||||
@@ -59,21 +57,16 @@ class MountingService:
|
||||
self.logger.exception(f"Failed to dry after mount failure: {e}")
|
||||
|
||||
if count >= STOP_AFTER_FAIL_COUNT:
|
||||
self.logger.error(
|
||||
f"Mount failed {count} times in a row, stopping automation"
|
||||
)
|
||||
self.logger.error(f"Mount failed {count} times in a row, stopping automation")
|
||||
self.logger.error("Unmounting sample and drying")
|
||||
try:
|
||||
self._unmount_current_sample(timeout=60.0)
|
||||
self.dry(park=True)
|
||||
except Exception as e:
|
||||
self.logger.exception(
|
||||
f"Failed to clean up after repeated mount failure: {e}"
|
||||
)
|
||||
self.logger.exception(f"Failed to clean up after repeated mount failure: {e}")
|
||||
|
||||
raise MountingFailed(
|
||||
f"Mount failed {count} times in a row, stopping automation.",
|
||||
critical=True,
|
||||
f"Mount failed {count} times in a row, stopping automation.", critical=True
|
||||
)
|
||||
|
||||
def _mount_handler(self, target) -> None:
|
||||
@@ -135,9 +128,7 @@ class MountingService:
|
||||
"""
|
||||
pss = self.ctx.deps.devs.pss
|
||||
if not pss.is_prohibited():
|
||||
self.logger.error(
|
||||
"Hutch is not in prohibited state; robot motion not permitted"
|
||||
)
|
||||
self.logger.error("Hutch is not in prohibited state; robot motion not permitted")
|
||||
raise DoorSafetyError(
|
||||
"Door safety could not be activated: close the hutch doors and "
|
||||
"complete the safety search before mounting."
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
from aare.daq.operations.raster.models import RasterContext, RasterBoundingBoxResult
|
||||
from aare.daq.operations.raster.service import RasterService
|
||||
|
||||
__all__ = [
|
||||
"RasterBoundingBoxResult",
|
||||
"RasterContext",
|
||||
"RasterService",
|
||||
]
|
||||
__all__ = ["RasterBoundingBoxResult", "RasterContext", "RasterService"]
|
||||
|
||||
@@ -4,10 +4,7 @@ from typing import TYPE_CHECKING
|
||||
from aarecommon.models.raster_grid import RasterGridRequest
|
||||
|
||||
from aare.daq.mlbox import MlBox
|
||||
from aare.daq.operations.common.models import (
|
||||
BaseOperationContext,
|
||||
BeamlineDependencies,
|
||||
)
|
||||
from aare.daq.operations.common.models import BaseOperationContext, BeamlineDependencies
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aare.daq.aaredb import AareWrapper
|
||||
|
||||
@@ -11,10 +11,7 @@ from aarecommon.config.logger_events import (
|
||||
raster_request_log_context,
|
||||
sample_log_context,
|
||||
)
|
||||
from aarecommon.errors.exception_handler import (
|
||||
AutoRasterSampleSkipped,
|
||||
RasterScanException,
|
||||
)
|
||||
from aarecommon.errors.exception_handler import AutoRasterSampleSkipped, RasterScanException
|
||||
from aarecommon.math.coordinate import AerotechCoordinate, Coordinate, SmargonCoordinate
|
||||
from aarecommon.math.find_xtal import (
|
||||
compute_crystal_score_array,
|
||||
@@ -57,10 +54,7 @@ class RasterService:
|
||||
|
||||
@staticmethod
|
||||
def _grid_image_id_from_centre_offset(
|
||||
*,
|
||||
x_mm: float,
|
||||
y_mm: float,
|
||||
request: RasterGridRequest,
|
||||
*, x_mm: float, y_mm: float, request: RasterGridRequest
|
||||
) -> int:
|
||||
if request.grid_size_mm.x <= 0 or request.grid_size_mm.y <= 0:
|
||||
raise ValueError("grid_size_mm must be positive")
|
||||
@@ -73,11 +67,7 @@ class RasterService:
|
||||
grid_x = min(max(1, grid_x), request.n_x)
|
||||
grid_y = min(max(1, grid_y), request.n_y)
|
||||
|
||||
return grid_to_image_id(
|
||||
grid_x=grid_x,
|
||||
grid_y=grid_y,
|
||||
number_of_cols=request.n_x,
|
||||
)
|
||||
return grid_to_image_id(grid_x=grid_x, grid_y=grid_y, number_of_cols=request.n_x)
|
||||
|
||||
def _upload_raster_diffraction_preview(
|
||||
self,
|
||||
@@ -96,10 +86,7 @@ class RasterService:
|
||||
extra=merge_log_context(
|
||||
sample_log_context(self.ctx.sample),
|
||||
raster_request_log_context(request),
|
||||
{
|
||||
"diffraction_image_id": image_id,
|
||||
"image_count": image_count,
|
||||
},
|
||||
{"diffraction_image_id": image_id, "image_count": image_count},
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -114,10 +101,7 @@ class RasterService:
|
||||
extra=merge_log_context(
|
||||
sample_log_context(self.ctx.sample),
|
||||
raster_request_log_context(request),
|
||||
{
|
||||
"diffraction_image_id": image_id,
|
||||
"image_count": image_count,
|
||||
},
|
||||
{"diffraction_image_id": image_id, "image_count": image_count},
|
||||
),
|
||||
exc_info=True,
|
||||
)
|
||||
@@ -140,14 +124,10 @@ class RasterService:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.ctx.deps.aare.upload_jpg(
|
||||
sample_id, filename, diffraction_image, message=comment
|
||||
)
|
||||
self.ctx.deps.aare.upload_jpg(sample_id, filename, diffraction_image, message=comment)
|
||||
|
||||
def ml_bounding_box(
|
||||
self,
|
||||
sample_id: int | None = None,
|
||||
filename: str | None = None,
|
||||
self, sample_id: int | None = None, filename: str | None = None
|
||||
) -> RasterGridRequest | None:
|
||||
return get_ml_bounding_box(
|
||||
mlbox=self.ctx.deps.mlbox,
|
||||
@@ -162,9 +142,7 @@ class RasterService:
|
||||
)
|
||||
|
||||
def ml_raster_plan(
|
||||
self,
|
||||
sample_id: int | None = None,
|
||||
filename: str | None = None,
|
||||
self, sample_id: int | None = None, filename: str | None = None
|
||||
) -> MLRasterPlan | None:
|
||||
return build_ml_raster_plan(
|
||||
mlbox=self.ctx.deps.mlbox,
|
||||
@@ -185,12 +163,7 @@ class RasterService:
|
||||
if width is None or height is None:
|
||||
return False
|
||||
x1, y1, x2, y2 = box
|
||||
return (
|
||||
x1 <= margin
|
||||
or y1 <= margin
|
||||
or x2 >= width - margin
|
||||
or y2 >= height - margin
|
||||
)
|
||||
return x1 <= margin or y1 <= margin or x2 >= width - margin or y2 >= height - margin
|
||||
|
||||
def _zoom_to_fit_box(self, plan: MLRasterPlan) -> tuple | None:
|
||||
"""Pick the box that drives zoom-to-fit: loop_all if detected and not
|
||||
@@ -261,15 +234,11 @@ class RasterService:
|
||||
beam_x_pxl = geom.beam_location_pxl.x
|
||||
beam_y_pxl = geom.beam_location_pxl.y
|
||||
|
||||
line_scan_centre = geom.picture_to_smargon(
|
||||
Coordinate(x=beam_x_pxl, y=beam_y_pxl)
|
||||
)
|
||||
line_scan_centre = geom.picture_to_smargon(Coordinate(x=beam_x_pxl, y=beam_y_pxl))
|
||||
n_y = default_n_y
|
||||
|
||||
prediction_result: MLBoxPredictionResult = self.ctx.deps.mlbox.predict(
|
||||
preferred_class=(3, 0),
|
||||
return_image=False,
|
||||
return_bundle_meta=True,
|
||||
preferred_class=(3, 0), return_image=False, return_bundle_meta=True
|
||||
)
|
||||
log_ml_bundle_meta(
|
||||
self.logger,
|
||||
@@ -285,7 +254,9 @@ class RasterService:
|
||||
prediction_type = getattr(prediction_box, "type", None)
|
||||
|
||||
if prediction_type == MLBoxType.LOOP_FACE:
|
||||
y_padding_fraction_each_side = local_contact_config.line_scan_loop_face_y_padding_fraction_each_side
|
||||
y_padding_fraction_each_side = (
|
||||
local_contact_config.line_scan_loop_face_y_padding_fraction_each_side
|
||||
)
|
||||
elif prediction_type == MLBoxType.LOOP_ALL:
|
||||
y_padding_fraction_each_side = (
|
||||
local_contact_config.line_scan_loop_all_y_padding_fraction_each_side
|
||||
@@ -303,9 +274,7 @@ class RasterService:
|
||||
y_retarget_threshold_mm = geom.beam_size_mm.y * 2.0
|
||||
|
||||
if y_delta_mm > y_retarget_threshold_mm:
|
||||
line_scan_centre = geom.picture_to_smargon(
|
||||
Coordinate(x=beam_x_pxl, y=target_y_pxl)
|
||||
)
|
||||
line_scan_centre = geom.picture_to_smargon(Coordinate(x=beam_x_pxl, y=target_y_pxl))
|
||||
self.logger.info(
|
||||
"Using ML target y for second auto-center raster",
|
||||
extra=merge_log_context(
|
||||
@@ -352,16 +321,10 @@ class RasterService:
|
||||
),
|
||||
)
|
||||
|
||||
if (
|
||||
prediction_box is not None
|
||||
and prediction_box.box is not None
|
||||
and grid_size_mm.y > 0
|
||||
):
|
||||
if prediction_box is not None and prediction_box.box is not None and grid_size_mm.y > 0:
|
||||
box_height_pxl = abs(prediction_box.box.bottom_y - prediction_box.box.top_y)
|
||||
padded_height_mm = (
|
||||
box_height_pxl
|
||||
* geom.pixel_in_mm
|
||||
* (1.0 + 2.0 * y_padding_fraction_each_side)
|
||||
box_height_pxl * geom.pixel_in_mm * (1.0 + 2.0 * y_padding_fraction_each_side)
|
||||
)
|
||||
n_y = max(1, int(ceil(padded_height_mm / grid_size_mm.y)))
|
||||
self.logger.info(
|
||||
@@ -396,10 +359,7 @@ class RasterService:
|
||||
),
|
||||
)
|
||||
|
||||
offset = Coordinate(
|
||||
x=-grid_size_mm.x / 2.0,
|
||||
y=-(n_y - 1) * grid_size_mm.y / 2.0,
|
||||
)
|
||||
offset = Coordinate(x=-grid_size_mm.x / 2.0, y=-(n_y - 1) * grid_size_mm.y / 2.0)
|
||||
top_left = SmargonCoordinate(
|
||||
sh_mm=line_scan_centre + geom.smargon_nudge(offset),
|
||||
phi_deg=geom.smargon.phi_deg,
|
||||
@@ -427,9 +387,7 @@ class RasterService:
|
||||
return top_left, n_y
|
||||
|
||||
def execute(
|
||||
self,
|
||||
request: RasterGridRequest,
|
||||
wait_for_screenshot: float | None = None,
|
||||
self, request: RasterGridRequest, wait_for_screenshot: float | None = None
|
||||
) -> CompletedRasterGridElem:
|
||||
total_time = request.exp_time_s * request.n_x * request.n_y + request.n_y * 0.3
|
||||
|
||||
@@ -466,9 +424,7 @@ class RasterService:
|
||||
if not self.ctx.deps.cfg.simulated_detector:
|
||||
self.ctx.deps.jfjoch.wait_till_running(timeout=60.0)
|
||||
else:
|
||||
self.logger.info(
|
||||
"Simulated detector mode enabled; faking jfjoch intilalisation."
|
||||
)
|
||||
self.logger.info("Simulated detector mode enabled; faking jfjoch intilalisation.")
|
||||
|
||||
self.logger.debug(
|
||||
f"Starting grid scan with {request.n_x}x{request.n_y} points, exp time {request.exp_time_s}s"
|
||||
@@ -496,8 +452,7 @@ class RasterService:
|
||||
else:
|
||||
coord = self.ctx.deps.cfg.abr_meas_pos.at_mm
|
||||
self.ctx.deps.devs.aerotech_pos = AerotechCoordinate(
|
||||
at_mm=coord,
|
||||
omega_deg=self.ctx.deps.devs.aerotech_omega,
|
||||
at_mm=coord, omega_deg=self.ctx.deps.devs.aerotech_omega
|
||||
)
|
||||
self.ctx.deps.devs.aerotech.wait_till_done(timeout=60)
|
||||
|
||||
@@ -505,9 +460,7 @@ class RasterService:
|
||||
y = None
|
||||
|
||||
if self.ctx.deps.cfg.simulated_detector:
|
||||
self.logger.info(
|
||||
"Simulated detector mode enabled; using fake raster result."
|
||||
)
|
||||
self.logger.info("Simulated detector mode enabled; using fake raster result.")
|
||||
scan_result = generate_no_beam_scan_result(request)
|
||||
com = raster_highest_score(scan_result.images)
|
||||
target_coor = com.get_com_mm(request)
|
||||
@@ -528,9 +481,7 @@ class RasterService:
|
||||
{"exp_time_s": request.exp_time_s},
|
||||
),
|
||||
)
|
||||
raise RasterScanException(
|
||||
"JFJoch returned no ScanResult for raster"
|
||||
)
|
||||
raise RasterScanException("JFJoch returned no ScanResult for raster")
|
||||
|
||||
com = raster_highest_score(scan_result.images)
|
||||
if com is None:
|
||||
@@ -556,24 +507,16 @@ class RasterService:
|
||||
"centre_offset_z_mm": target_coor_offset.z,
|
||||
"grid_half_width_x_mm": x,
|
||||
"grid_half_height_y_mm": y,
|
||||
"top_left_x_mm": getattr(
|
||||
request.smargon_top_left.sh_mm, "x", None
|
||||
),
|
||||
"top_left_y_mm": getattr(
|
||||
request.smargon_top_left.sh_mm, "y", None
|
||||
),
|
||||
"top_left_z_mm": getattr(
|
||||
request.smargon_top_left.sh_mm, "z", None
|
||||
),
|
||||
"top_left_x_mm": getattr(request.smargon_top_left.sh_mm, "x", None),
|
||||
"top_left_y_mm": getattr(request.smargon_top_left.sh_mm, "y", None),
|
||||
"top_left_z_mm": getattr(request.smargon_top_left.sh_mm, "z", None),
|
||||
},
|
||||
),
|
||||
)
|
||||
else:
|
||||
self.logger.info("Calcualted COM is not None, procedding")
|
||||
target_coor = com.get_com_mm(request)
|
||||
target_coor_offset = self.ctx.sample_geometry.smargon_nudge(
|
||||
target_coor
|
||||
)
|
||||
target_coor_offset = self.ctx.sample_geometry.smargon_nudge(target_coor)
|
||||
self.logger.info(
|
||||
"Calculated raster centre offset",
|
||||
extra=merge_log_context(
|
||||
@@ -585,22 +528,14 @@ class RasterService:
|
||||
"centre_offset_z_mm": target_coor_offset.z,
|
||||
"grid_half_width_x_mm": x,
|
||||
"grid_half_height_y_mm": y,
|
||||
"top_left_x_mm": getattr(
|
||||
request.smargon_top_left.sh_mm, "x", None
|
||||
),
|
||||
"top_left_y_mm": getattr(
|
||||
request.smargon_top_left.sh_mm, "y", None
|
||||
),
|
||||
"top_left_z_mm": getattr(
|
||||
request.smargon_top_left.sh_mm, "z", None
|
||||
),
|
||||
"top_left_x_mm": getattr(request.smargon_top_left.sh_mm, "x", None),
|
||||
"top_left_y_mm": getattr(request.smargon_top_left.sh_mm, "y", None),
|
||||
"top_left_z_mm": getattr(request.smargon_top_left.sh_mm, "z", None),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
f"moving Smargon to grid centre offset {target_coor_offset}"
|
||||
)
|
||||
self.logger.info(f"moving Smargon to grid centre offset {target_coor_offset}")
|
||||
target_smargon = SmargonCoordinate(
|
||||
sh_mm=request.smargon_top_left.sh_mm + target_coor_offset,
|
||||
phi_deg=request.smargon_top_left.phi_deg,
|
||||
@@ -631,9 +566,7 @@ class RasterService:
|
||||
else None
|
||||
)
|
||||
if sample_id:
|
||||
self.logger.debug(
|
||||
"moving to XtalSnapshot to take a screenshot of the sample"
|
||||
)
|
||||
self.logger.debug("moving to XtalSnapshot to take a screenshot of the sample")
|
||||
if self.ctx.services.state is None:
|
||||
raise RuntimeError("RasterService requires services.state")
|
||||
self.ctx.services.state.set_state(BeamlineStateEnum.XtalSnapshot)
|
||||
@@ -642,8 +575,7 @@ class RasterService:
|
||||
time.sleep(wait_for_screenshot)
|
||||
|
||||
self.ctx.services.screenshots.save_to_db(
|
||||
sample_id,
|
||||
f"{sample_id}_post_raster_{int(request.omega_deg)}deg",
|
||||
sample_id, f"{sample_id}_post_raster_{int(request.omega_deg)}deg"
|
||||
)
|
||||
|
||||
if self.ctx.services.ingestion is not None:
|
||||
@@ -653,9 +585,7 @@ class RasterService:
|
||||
raster_request=request,
|
||||
geom=self.ctx.sample_geometry,
|
||||
com=None,
|
||||
beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(
|
||||
self.ctx.deps.devs.zoom
|
||||
),
|
||||
beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(self.ctx.deps.devs.zoom),
|
||||
)
|
||||
else:
|
||||
self.ctx.deps.aare.ingest_gridscan(
|
||||
@@ -664,9 +594,7 @@ class RasterService:
|
||||
raster_request=request,
|
||||
geom=self.ctx.sample_geometry,
|
||||
com=None,
|
||||
beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(
|
||||
self.ctx.deps.devs.zoom
|
||||
),
|
||||
beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(self.ctx.deps.devs.zoom),
|
||||
)
|
||||
|
||||
if com is not None and com.max_image is not None:
|
||||
@@ -677,9 +605,7 @@ class RasterService:
|
||||
f"{self.ctx.sample.db_id}_diffraction_image_near_grid_scan"
|
||||
)
|
||||
diffraction_image_id = self._grid_image_id_from_centre_offset(
|
||||
x_mm=x,
|
||||
y_mm=y,
|
||||
request=request,
|
||||
x_mm=x, y_mm=y, request=request
|
||||
)
|
||||
|
||||
self._upload_raster_diffraction_preview(
|
||||
@@ -708,10 +634,7 @@ class RasterService:
|
||||
extra=merge_log_context(
|
||||
sample_log_context(self.ctx.sample),
|
||||
raster_request_log_context(request),
|
||||
{
|
||||
"scan_result_is_none": scan_result is None,
|
||||
"sample_id": sample_id,
|
||||
},
|
||||
{"scan_result_is_none": scan_result is None, "sample_id": sample_id},
|
||||
),
|
||||
)
|
||||
self.ctx.deps.cfg.crystal_size = get_xtal_size(
|
||||
@@ -719,16 +642,10 @@ class RasterService:
|
||||
compute_crystal_score_array(scan_result.images),
|
||||
r=request,
|
||||
)
|
||||
self.ctx.deps.cfg.last_best_res = get_best_res(
|
||||
result_list=scan_result.images
|
||||
)
|
||||
self.ctx.deps.cfg.last_best_b_factor = get_best_b_factor(
|
||||
result_list=scan_result.images
|
||||
)
|
||||
self.ctx.deps.cfg.last_best_res = get_best_res(result_list=scan_result.images)
|
||||
self.ctx.deps.cfg.last_best_b_factor = get_best_b_factor(result_list=scan_result.images)
|
||||
return CompletedRasterGridElem(
|
||||
request=copy.deepcopy(request),
|
||||
result=scan_result,
|
||||
centre_of_mass=com,
|
||||
request=copy.deepcopy(request), result=scan_result, centre_of_mass=com
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -742,9 +659,7 @@ class RasterService:
|
||||
)
|
||||
raise
|
||||
|
||||
def execute_auto_center(
|
||||
self, request: RasterGridRequest
|
||||
) -> CompletedRasterGrid | None:
|
||||
def execute_auto_center(self, request: RasterGridRequest) -> CompletedRasterGrid | None:
|
||||
sample = self.ctx.sample
|
||||
if sample is None:
|
||||
raise Exception("Sample must be mounted to auto center")
|
||||
@@ -787,17 +702,12 @@ class RasterService:
|
||||
"No ML bounding box found at primary angle during auto-center raster",
|
||||
extra=merge_log_context(
|
||||
sample_log_context(sample),
|
||||
{
|
||||
"omega_deg": geom.omega_deg,
|
||||
"file_prefix": old_prefix,
|
||||
},
|
||||
{"omega_deg": geom.omega_deg, "file_prefix": old_prefix},
|
||||
),
|
||||
)
|
||||
self.ctx.deps.devs.aerotech_omega = geom.omega_deg + 90.0
|
||||
time.sleep(0.2)
|
||||
plan = self.ml_raster_plan(
|
||||
sample.db_id, f"ml_{geom.omega_deg + 90.0:.2f}deg"
|
||||
)
|
||||
plan = self.ml_raster_plan(sample.db_id, f"ml_{geom.omega_deg + 90.0:.2f}deg")
|
||||
|
||||
if plan is None:
|
||||
self.logger.error(
|
||||
@@ -856,9 +766,7 @@ class RasterService:
|
||||
self.ctx.deps.jfjoch.measure_raster(grid, status)
|
||||
self.logger.info("detector initialised")
|
||||
else:
|
||||
self.logger.info(
|
||||
"Simulated detector mode enabled; using fake raster result."
|
||||
)
|
||||
self.logger.info("Simulated detector mode enabled; using fake raster result.")
|
||||
|
||||
if self.ctx.services.state is None:
|
||||
raise RuntimeError("RasterService requires services.state")
|
||||
@@ -896,18 +804,13 @@ class RasterService:
|
||||
grid.n_x = 1
|
||||
grid.file_prefix = f"{old_prefix}_{grid.omega_deg}deg"
|
||||
geom = self.ctx.sample_geometry
|
||||
grid.grid_size_mm = Coordinate(
|
||||
x=geom.beam_size_mm.x, y=geom.beam_size_mm.y * 0.25
|
||||
)
|
||||
grid.grid_size_mm = Coordinate(x=geom.beam_size_mm.x, y=geom.beam_size_mm.y * 0.25)
|
||||
grid.smargon_top_left, grid.n_y = self.auto_center_line_scan_top_left(
|
||||
omega_deg=grid.omega_deg,
|
||||
file_prefix=grid.file_prefix,
|
||||
grid_size_mm=grid.grid_size_mm,
|
||||
default_n_y=50,
|
||||
y_retarget_threshold_mm=max(
|
||||
geom.beam_size_mm.y * 2.0,
|
||||
grid.grid_size_mm.y * 4.0,
|
||||
),
|
||||
y_retarget_threshold_mm=max(geom.beam_size_mm.y * 2.0, grid.grid_size_mm.y * 4.0),
|
||||
)
|
||||
|
||||
self.logger.info(
|
||||
@@ -929,9 +832,7 @@ class RasterService:
|
||||
self.ctx.deps.jfjoch.measure_raster(grid, status)
|
||||
self.logger.info("detector initialised")
|
||||
else:
|
||||
self.logger.info(
|
||||
"Simulated detector mode enabled; using fake raster result."
|
||||
)
|
||||
self.logger.info("Simulated detector mode enabled; using fake raster result.")
|
||||
|
||||
if self.ctx.services.state is None:
|
||||
raise RuntimeError("RasterService requires services.state")
|
||||
@@ -940,8 +841,7 @@ class RasterService:
|
||||
self.logger.info(
|
||||
"Running second auto-center raster",
|
||||
extra=merge_log_context(
|
||||
sample_log_context(self.ctx.sample),
|
||||
raster_request_log_context(grid),
|
||||
sample_log_context(self.ctx.sample), raster_request_log_context(grid)
|
||||
),
|
||||
)
|
||||
res2 = self.execute(grid)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aare.daq.operations.common.models import (
|
||||
BaseOperationContext,
|
||||
BeamlineDependencies,
|
||||
)
|
||||
from aare.daq.operations.common.models import BaseOperationContext, BeamlineDependencies
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aare.daq.aaredb import AareWrapper
|
||||
@@ -24,4 +21,4 @@ class RotationSettings:
|
||||
|
||||
@dataclass
|
||||
class RotationContext(BaseOperationContext[RotationDependencies, RotationSettings]):
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -20,11 +20,7 @@ class RotationService:
|
||||
self.logger.info("Simulated detector mode enabled; skipping JFJoch start.")
|
||||
return
|
||||
|
||||
self.ctx.deps.jfjoch.measure_rotation(
|
||||
request,
|
||||
self.ctx.status,
|
||||
self.ctx.deps.cfg.xrf,
|
||||
)
|
||||
self.ctx.deps.jfjoch.measure_rotation(request, self.ctx.status, self.ctx.deps.cfg.xrf)
|
||||
|
||||
def _execute_scan(self, request: RotationScanRequest) -> CompletedRotationScan:
|
||||
omega_start = self.ctx.deps.devs.aerotech_omega
|
||||
@@ -66,9 +62,7 @@ class RotationService:
|
||||
|
||||
if request.start is not None and request.end is not None:
|
||||
smargon_time_step = request.exp_time_s / float(request.steps)
|
||||
pos_step = (request.end.sh_mm - request.start.sh_mm) * (
|
||||
1.0 / float(request.steps)
|
||||
)
|
||||
pos_step = (request.end.sh_mm - request.start.sh_mm) * (1.0 / float(request.steps))
|
||||
|
||||
for i in range(request.steps):
|
||||
self.ctx.deps.devs.smargon.target = SmargonCoordinate(
|
||||
@@ -76,22 +70,15 @@ class RotationService:
|
||||
)
|
||||
time.sleep(smargon_time_step)
|
||||
|
||||
self.ctx.deps.devs.aerotech.wait_till_done(
|
||||
timeout=int(round(total_time + 60, 0))
|
||||
)
|
||||
self.ctx.deps.devs.aerotech.wait_till_done(timeout=int(round(total_time + 60, 0)))
|
||||
self.ctx.deps.devs.aerotech_omega = omega_start
|
||||
|
||||
if self.ctx.deps.cfg.simulated_detector:
|
||||
self.logger.warning(
|
||||
"Detector in simulation mode, returning fake zero rotation result."
|
||||
)
|
||||
self.logger.warning("Detector in simulation mode, returning fake zero rotation result.")
|
||||
return build_fake_rotation_result(request, start_angle=float(omega_start))
|
||||
|
||||
scan_result = self.ctx.deps.jfjoch.wait_till_done(60)
|
||||
return CompletedRotationScan(
|
||||
request=copy.deepcopy(request),
|
||||
result=scan_result,
|
||||
)
|
||||
return CompletedRotationScan(request=copy.deepcopy(request), result=scan_result)
|
||||
|
||||
def _ingest_screening_diffraction(self, sample, scan_result) -> None:
|
||||
"""Pull each screening wedge's diffraction image from JFJoch (with spot
|
||||
@@ -110,9 +97,7 @@ class RotationService:
|
||||
)
|
||||
except Exception:
|
||||
self.logger.warning(
|
||||
"Screening diffraction image %s unavailable; skipping",
|
||||
image_id,
|
||||
exc_info=True,
|
||||
"Screening diffraction image %s unavailable; skipping", image_id, exc_info=True
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -141,11 +126,7 @@ class RotationService:
|
||||
self.ctx.services.datacollection.prepare(request)
|
||||
self._prepare_detector(request)
|
||||
|
||||
if (
|
||||
sample is not None
|
||||
and sample.db_id is not None
|
||||
and self.ctx.services.events is not None
|
||||
):
|
||||
if sample is not None and sample.db_id is not None and self.ctx.services.events is not None:
|
||||
self.ctx.services.events.send(sample.db_id, SampleEventType.COLLECTING)
|
||||
|
||||
if self.ctx.services.state is not None:
|
||||
@@ -159,16 +140,13 @@ class RotationService:
|
||||
if sample is not None and sample.db_id is not None:
|
||||
if self.ctx.services.screenshots is not None:
|
||||
self.ctx.services.screenshots.save_to_db(
|
||||
sample.db_id,
|
||||
self.ctx.settings.preview_filename,
|
||||
sample.db_id, self.ctx.settings.preview_filename
|
||||
)
|
||||
|
||||
# For screening runs, ingest the per-wedge diffraction images. Done
|
||||
# before the COLLECTED event so they pin to the same sample event the
|
||||
# run is bound to (the COLLECTING event), matching the preview above.
|
||||
self.logger.debug(
|
||||
f"Ingesting screening diffraction images: {request.screening}"
|
||||
)
|
||||
self.logger.debug(f"Ingesting screening diffraction images: {request.screening}")
|
||||
if request.screening and not self.ctx.deps.cfg.simulated_detector:
|
||||
self._ingest_screening_diffraction(sample, result.result)
|
||||
|
||||
@@ -180,9 +158,7 @@ class RotationService:
|
||||
sample=sample,
|
||||
result=result.result,
|
||||
geom=self.ctx.sample_geometry,
|
||||
beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(
|
||||
self.ctx.deps.devs.zoom
|
||||
),
|
||||
beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(self.ctx.deps.devs.zoom),
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -15,11 +15,7 @@ from aare.devices.mx_lib import clean_filename
|
||||
|
||||
class NonCriticalRunner(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
action,
|
||||
*,
|
||||
description: str,
|
||||
sample: SampleShortInfo | None = None,
|
||||
self, action, *, description: str, sample: SampleShortInfo | None = None
|
||||
) -> object | None: ...
|
||||
|
||||
|
||||
@@ -68,9 +64,7 @@ class ScreenshotService:
|
||||
self.logger.debug(f"saving screenshot {filename} from inference image")
|
||||
cv2.imwrite(f"{self.output_dir}/{filename}.jpg", bgr_image)
|
||||
|
||||
def save_to_db(
|
||||
self, sample_id: int, filename: str, settle_time_s: float = 0.2
|
||||
) -> None:
|
||||
def save_to_db(self, sample_id: int, filename: str, settle_time_s: float = 0.2) -> None:
|
||||
time.sleep(settle_time_s)
|
||||
sample = self._current_sample()
|
||||
bgr_image = self._get_inference_image()
|
||||
@@ -78,18 +72,10 @@ class ScreenshotService:
|
||||
def _upload() -> None:
|
||||
self.aare.upload_image(sample_id, filename, bgr_image)
|
||||
|
||||
self.run_noncritical(
|
||||
_upload,
|
||||
description=f"screenshot upload '{filename}'",
|
||||
sample=sample,
|
||||
)
|
||||
self.run_noncritical(_upload, description=f"screenshot upload '{filename}'", sample=sample)
|
||||
|
||||
def send_to_db(
|
||||
self,
|
||||
*,
|
||||
filename: str | None = None,
|
||||
message: str | None = None,
|
||||
default_message: str,
|
||||
self, *, filename: str | None = None, message: str | None = None, default_message: str
|
||||
) -> None:
|
||||
sample = self._current_sample()
|
||||
if sample is None or sample.db_id is None or sample.db_id < 0:
|
||||
@@ -106,9 +92,7 @@ class ScreenshotService:
|
||||
"No active pgroup set; cannot save screenshot to photos directory."
|
||||
)
|
||||
|
||||
photos_dir = (
|
||||
Path(self.photos_root) / pgroup / "raw" / "photos" / str(sample_id)
|
||||
)
|
||||
photos_dir = Path(self.photos_root) / pgroup / "raw" / "photos" / str(sample_id)
|
||||
photos_dir.mkdir(parents=True, exist_ok=True)
|
||||
photo_path = photos_dir / f"{safe_filename}.jpeg"
|
||||
cv2.imwrite(str(photo_path), bgr_image)
|
||||
@@ -119,12 +103,6 @@ class ScreenshotService:
|
||||
final_message = (message or "").strip() or default_message
|
||||
|
||||
def _upload() -> None:
|
||||
self.aare.upload_image(
|
||||
sample_id, upload_name, bgr_image, message=final_message
|
||||
)
|
||||
self.aare.upload_image(sample_id, upload_name, bgr_image, message=final_message)
|
||||
|
||||
self.run_noncritical(
|
||||
_upload,
|
||||
description=f"send screenshot '{upload_name}'",
|
||||
sample=sample,
|
||||
)
|
||||
self.run_noncritical(_upload, description=f"send screenshot '{upload_name}'", sample=sample)
|
||||
|
||||
+52
-180
@@ -77,10 +77,7 @@ _face_detection_state: dict = {
|
||||
}
|
||||
_face_detection_state_lock = asyncio.Lock()
|
||||
|
||||
_automation_progress_state: dict = {
|
||||
"seq": 0,
|
||||
"progress": None,
|
||||
}
|
||||
_automation_progress_state: dict = {"seq": 0, "progress": None}
|
||||
_automation_progress_state_lock = asyncio.Lock()
|
||||
|
||||
|
||||
@@ -172,8 +169,7 @@ def _validate_recovery_code(confirmation_code: str) -> None:
|
||||
if not hmac.compare_digest(provided, expected):
|
||||
logger.warning("Invalid recovery confirmation code.")
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_403_FORBIDDEN,
|
||||
detail="Invalid confirmation code.",
|
||||
status_code=api_status.HTTP_403_FORBIDDEN, detail="Invalid confirmation code."
|
||||
)
|
||||
|
||||
|
||||
@@ -200,10 +196,7 @@ def _push_face_detection_progress(payload: dict) -> None:
|
||||
global _face_detection_state
|
||||
try:
|
||||
next_seq = int(_face_detection_state.get("seq", 0)) + 1
|
||||
_face_detection_state = {
|
||||
"seq": next_seq,
|
||||
**payload,
|
||||
}
|
||||
_face_detection_state = {"seq": next_seq, **payload}
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to update face detection progress: {e}")
|
||||
|
||||
@@ -346,18 +339,14 @@ async def status(token: str = Depends(oauth2_scheme)) -> DAQStatusModel:
|
||||
full.sample = sample if in_ro and sample_view_allowed else None
|
||||
full.box = getattr(full, "box", None) if in_ro else None
|
||||
full.last_best_res = getattr(full, "last_best_res", None) if in_ro else None
|
||||
full.last_best_b_factor = (
|
||||
getattr(full, "last_best_b_factor", None) if in_ro else None
|
||||
)
|
||||
full.last_best_b_factor = getattr(full, "last_best_b_factor", None) if in_ro else None
|
||||
full.crystal_size = (
|
||||
getattr(full, "crystal_size", CrystalSize(x=0, y=0, z=0))
|
||||
if in_ro
|
||||
else CrystalSize(x=0, y=0, z=0)
|
||||
)
|
||||
full.session = SessionStatus(
|
||||
current_pgroup=cfg.pgroup,
|
||||
session=cfg.session_state(data.session),
|
||||
staff=data.staff,
|
||||
current_pgroup=cfg.pgroup, session=cfg.session_state(data.session), staff=data.staff
|
||||
)
|
||||
|
||||
if data.staff:
|
||||
@@ -369,9 +358,7 @@ async def status(token: str = Depends(oauth2_scheme)) -> DAQStatusModel:
|
||||
|
||||
|
||||
@app.get("/beamline/geometry")
|
||||
async def sample_geometry(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> SampleGeometryModel:
|
||||
async def sample_geometry(token: str = Depends(oauth2_scheme)) -> SampleGeometryModel:
|
||||
"""
|
||||
Get the sample geometry information.
|
||||
|
||||
@@ -494,9 +481,7 @@ async def mono_pitch_scan(plot: bool = False, token: str = Depends(oauth2_scheme
|
||||
|
||||
|
||||
@app.put("/beamline/change_energy")
|
||||
async def change_energy(
|
||||
value: float, plot: bool = False, token: str = Depends(oauth2_scheme)
|
||||
):
|
||||
async def change_energy(value: float, plot: bool = False, token: str = Depends(oauth2_scheme)):
|
||||
"""
|
||||
Change monochromator energy. Staff only.
|
||||
|
||||
@@ -534,9 +519,7 @@ async def smargon(val: SmargonCoordinate, token: str = Depends(oauth2_scheme)):
|
||||
|
||||
|
||||
@app.post("/beamline/tweak_abr_meas_pos")
|
||||
async def tweak_abr_meas_pos(
|
||||
val: AerotechCoordinate, token: str = Depends(oauth2_scheme)
|
||||
):
|
||||
async def tweak_abr_meas_pos(val: AerotechCoordinate, token: str = Depends(oauth2_scheme)):
|
||||
"""
|
||||
Tweak the Aerotech measurement position. Staff only.
|
||||
|
||||
@@ -615,10 +598,7 @@ async def initialise_smargon(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
daq.initialise_smargon()
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "Smargon initialised.",
|
||||
}
|
||||
return {"ok": True, "message": "Smargon initialised."}
|
||||
|
||||
|
||||
@app.post("/bec/load_user_macros")
|
||||
@@ -629,10 +609,7 @@ async def bec_load_user_macros(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
daq.bec_load_user_macros()
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "BEC user macros loaded.",
|
||||
}
|
||||
return {"ok": True, "message": "BEC user macros loaded."}
|
||||
|
||||
|
||||
@app.get("/bec/user_macros")
|
||||
@@ -657,8 +634,7 @@ async def bec_list_all_devices(token: str = Depends(oauth2_scheme)) -> list:
|
||||
|
||||
@app.post("/bec/reinitialise_planner_and_position_devices")
|
||||
async def bec_reinitialise_planner_and_position_devices(
|
||||
method: str = "auto",
|
||||
token: str = Depends(oauth2_scheme),
|
||||
method: str = "auto", token: str = Depends(oauth2_scheme)
|
||||
) -> dict:
|
||||
"""
|
||||
Reinitialise BEC planner and position devices. Staff only.
|
||||
@@ -689,10 +665,7 @@ async def bec_save_current_bs_pos(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
daq.bec_save_current_bs_pos()
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "Saved current BEC beamstop work position.",
|
||||
}
|
||||
return {"ok": True, "message": "Saved current BEC beamstop work position."}
|
||||
|
||||
|
||||
@app.post("/bec/save_current_collimator_pos")
|
||||
@@ -703,26 +676,18 @@ async def bec_save_current_collimator_pos(token: str = Depends(oauth2_scheme)) -
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
daq.bec_save_current_collimator_pos()
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "Saved current BEC collimator work position.",
|
||||
}
|
||||
return {"ok": True, "message": "Saved current BEC collimator work position."}
|
||||
|
||||
|
||||
@app.post("/bec/save_current_aerotech_position")
|
||||
async def bec_save_current_aerotech_position(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> dict:
|
||||
async def bec_save_current_aerotech_position(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
"""
|
||||
Save the current BEC aerotech work position and reload device config. Staff only.
|
||||
"""
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
daq.bec_save_current_aerotech_position()
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "Saved current BEC aerotech work position and reloaded devices.",
|
||||
}
|
||||
return {"ok": True, "message": "Saved current BEC aerotech work position and reloaded devices."}
|
||||
|
||||
|
||||
def initialise_aerotech(self):
|
||||
@@ -790,9 +755,7 @@ async def local_contact_links(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
|
||||
@app.post("/local_contact/simulate/{device}")
|
||||
async def local_contact_set_simulation(
|
||||
device: str,
|
||||
enabled: bool,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
device: str, enabled: bool, token: str = Depends(oauth2_scheme)
|
||||
) -> dict:
|
||||
"""
|
||||
Enable or disable runtime simulation for a backend device and restart its wrapper. Staff only.
|
||||
@@ -805,10 +768,7 @@ async def local_contact_set_simulation(
|
||||
|
||||
|
||||
@app.post("/local_contact/restart/{device}")
|
||||
async def local_contact_restart_device(
|
||||
device: str,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> dict:
|
||||
async def local_contact_restart_device(device: str, token: str = Depends(oauth2_scheme)) -> dict:
|
||||
"""
|
||||
Restart a Local Contact backend wrapper. Staff only.
|
||||
"""
|
||||
@@ -837,9 +797,7 @@ async def local_contact_restart_device(
|
||||
|
||||
|
||||
@app.post("/local_contact/resync/detector_metadata")
|
||||
async def local_contact_resync_detector_metadata(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> dict:
|
||||
async def local_contact_resync_detector_metadata(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
"""
|
||||
Refresh cached detector metadata and DTZ limits. Staff only.
|
||||
"""
|
||||
@@ -847,17 +805,11 @@ async def local_contact_resync_detector_metadata(
|
||||
auth.check_jwt_staff_only(data)
|
||||
|
||||
payload = daq.refresh_detector_metadata_cache()
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "Hardware metadata cache resynced.",
|
||||
"payload": payload,
|
||||
}
|
||||
return {"ok": True, "message": "Hardware metadata cache resynced.", "payload": payload}
|
||||
|
||||
|
||||
@app.get("/local_contact/config")
|
||||
async def local_contact_config(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> LocalContactConfigModel:
|
||||
async def local_contact_config(token: str = Depends(oauth2_scheme)) -> LocalContactConfigModel:
|
||||
"""
|
||||
Return Local Contact config values. Staff only.
|
||||
"""
|
||||
@@ -868,8 +820,7 @@ async def local_contact_config(
|
||||
|
||||
@app.put("/local_contact/config")
|
||||
async def local_contact_set_config(
|
||||
payload: LocalContactConfigModel,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
payload: LocalContactConfigModel, token: str = Depends(oauth2_scheme)
|
||||
) -> LocalContactConfigModel:
|
||||
"""
|
||||
Update Local Contact config values. Staff only.
|
||||
@@ -1054,9 +1005,7 @@ async def get_image(token: str = Depends(oauth2_scheme)):
|
||||
"""
|
||||
auth.check_jwt_ro(cfg, auth.parse_token(token))
|
||||
|
||||
_, encoded_image = cv2.imencode(
|
||||
".jpg", daq.camera_image
|
||||
) # Encodes the image in JPEG format
|
||||
_, encoded_image = cv2.imencode(".jpg", daq.camera_image) # Encodes the image in JPEG format
|
||||
image_bytes = io.BytesIO(
|
||||
encoded_image.tobytes()
|
||||
) # Convert OpenCV byte format to a file-like object
|
||||
@@ -1108,10 +1057,7 @@ async def park_and_dry(token: str = Depends(oauth2_scheme)):
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
logger.debug("Executing unmount, dry and park")
|
||||
daq.park_and_dry(park=True, unmount=True)
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "TELL has been dried and parked",
|
||||
}
|
||||
return {"ok": True, "message": "TELL has been dried and parked"}
|
||||
|
||||
|
||||
@app.post("/tell/dry")
|
||||
@@ -1129,10 +1075,7 @@ async def tell_dry(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
auth.check_jwt_rw(data)
|
||||
logger.debug("Executing dry and return to dewar")
|
||||
daq.park_and_dry(park=False, unmount=False)
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "TELL dry cycle completed.",
|
||||
}
|
||||
return {"ok": True, "message": "TELL dry cycle completed."}
|
||||
|
||||
|
||||
@app.post("/tell/toggle_blower")
|
||||
@@ -1149,16 +1092,11 @@ async def tell_toggle_blower(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
data = auth.parse_token(token)
|
||||
auth.check_jwt_staff_only(data)
|
||||
daq.blower_control()
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "TELL blower toggled.",
|
||||
}
|
||||
return {"ok": True, "message": "TELL blower toggled."}
|
||||
|
||||
|
||||
@app.post("/sample/mount")
|
||||
async def mount(
|
||||
dbid: int, token: str = Depends(oauth2_scheme), reference: bool = False
|
||||
):
|
||||
async def mount(dbid: int, token: str = Depends(oauth2_scheme), reference: bool = False):
|
||||
"""
|
||||
Mount a sample from the spreadsheet onto the goniometer.
|
||||
|
||||
@@ -1244,10 +1182,7 @@ async def sample_resync(token: str = Depends(oauth2_scheme)) -> dict:
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
daq.sync_current_sample_from_tell(force=True)
|
||||
logger.info("TELL sample cache resynced via API request.")
|
||||
return {
|
||||
"ok": True,
|
||||
"message": "TELL sample cache resynced.",
|
||||
}
|
||||
return {"ok": True, "message": "TELL sample cache resynced."}
|
||||
|
||||
|
||||
def get_spreadsheet(data: TokenData) -> SampleShortInfoList:
|
||||
@@ -1569,16 +1504,13 @@ async def take_over_beamline(
|
||||
_validate_recovery_code(payload.confirmation_code)
|
||||
auth.force_current_sesion(cfg, data)
|
||||
logger.warning(
|
||||
"Beamline session forcefully taken over.",
|
||||
extra={"session": getattr(data, "session", None)},
|
||||
"Beamline session forcefully taken over.", extra={"session": getattr(data, "session", None)}
|
||||
)
|
||||
return "OK"
|
||||
|
||||
|
||||
@app.post("/state/free_beamline")
|
||||
async def free_beamline(
|
||||
payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)
|
||||
) -> str:
|
||||
async def free_beamline(payload: RecoveryActionRequest, token: str = Depends(oauth2_scheme)) -> str:
|
||||
"""
|
||||
Clear the beamline busy flag. Staff only.
|
||||
|
||||
@@ -1680,11 +1612,7 @@ async def recovery_unmount_sample(
|
||||
|
||||
sample_mounted = _sample_is_mounted()
|
||||
if not sample_mounted:
|
||||
return {
|
||||
"ok": True,
|
||||
"sample_mounted": False,
|
||||
"message": "No sample appears to be mounted.",
|
||||
}
|
||||
return {"ok": True, "sample_mounted": False, "message": "No sample appears to be mounted."}
|
||||
|
||||
prev_state = cfg.state
|
||||
daq.recovery_unmount_sample()
|
||||
@@ -1709,9 +1637,7 @@ async def recovery_unmount_sample(
|
||||
# Scans
|
||||
@app.post("/scan/raster")
|
||||
async def raster(
|
||||
val: RasterGridRequest,
|
||||
auto_center: bool = False,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
val: RasterGridRequest, auto_center: bool = False, token: str = Depends(oauth2_scheme)
|
||||
) -> CompletedRasterGrid:
|
||||
"""
|
||||
Execute a raster scan.
|
||||
@@ -1775,9 +1701,7 @@ async def auto(s: SampleShortInfo, token: str = Depends(oauth2_scheme)):
|
||||
|
||||
|
||||
@app.post("/scan/smart_params")
|
||||
async def set_smart_params(
|
||||
p: SimpleScanParameters, token: str = Depends(oauth2_scheme)
|
||||
) -> str:
|
||||
async def set_smart_params(p: SimpleScanParameters, token: str = Depends(oauth2_scheme)) -> str:
|
||||
"""
|
||||
Set the 'smart' scan parameters.
|
||||
|
||||
@@ -1825,9 +1749,7 @@ async def alc_center_loop(token: str = Depends(oauth2_scheme)) -> str:
|
||||
|
||||
|
||||
@app.post("/alc/ml_bounding_box")
|
||||
async def alc_ml_bounding_box(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> RasterGridRequest | None:
|
||||
async def alc_ml_bounding_box(token: str = Depends(oauth2_scheme)) -> RasterGridRequest | None:
|
||||
"""
|
||||
Request an ML-based bounding box for the sample.
|
||||
|
||||
@@ -1859,13 +1781,7 @@ async def face_detection_run(
|
||||
logger.debug(f"Face detection run: {steps} steps, {step_size} step size")
|
||||
auth.check_jwt_rw(cfg, auth.parse_token(token))
|
||||
_push_face_detection_progress(
|
||||
{
|
||||
"running": True,
|
||||
"status": "starting",
|
||||
"samples": [],
|
||||
"height_fit": {},
|
||||
"area_fit": {},
|
||||
}
|
||||
{"running": True, "status": "starting", "samples": [], "height_fit": {}, "area_fit": {}}
|
||||
)
|
||||
result = daq.face_detection(steps=steps, step_size=step_size)
|
||||
return result
|
||||
@@ -2006,9 +1922,7 @@ async def get_gui_sessions(token: str = Depends(oauth2_scheme)) -> list[dict]:
|
||||
|
||||
@app.post("/admin/gui_sessions/{session_id}/request_close")
|
||||
async def request_gui_close(
|
||||
session_id: int,
|
||||
grace_seconds: int = 60,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
session_id: int, grace_seconds: int = 60, token: str = Depends(oauth2_scheme)
|
||||
) -> dict:
|
||||
"""
|
||||
Staff-only request for a remote GUI to close gracefully.
|
||||
@@ -2017,28 +1931,18 @@ async def request_gui_close(
|
||||
auth.check_jwt_staff_only(data)
|
||||
|
||||
payload = cfg.request_gui_close(
|
||||
session=session_id,
|
||||
requested_by=data.sub,
|
||||
grace_seconds=grace_seconds,
|
||||
session=session_id, requested_by=data.sub, grace_seconds=grace_seconds
|
||||
)
|
||||
if payload is None:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_404_NOT_FOUND,
|
||||
detail="GUI session not found.",
|
||||
status_code=api_status.HTTP_404_NOT_FOUND, detail="GUI session not found."
|
||||
)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"session": session_id,
|
||||
"grace_seconds": grace_seconds,
|
||||
}
|
||||
return {"ok": True, "session": session_id, "grace_seconds": grace_seconds}
|
||||
|
||||
|
||||
@app.delete("/admin/gui_sessions/{session_id}")
|
||||
async def force_remove_gui_session(
|
||||
session_id: int,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> dict:
|
||||
async def force_remove_gui_session(session_id: int, token: str = Depends(oauth2_scheme)) -> dict:
|
||||
"""
|
||||
Staff-only hard removal of a GUI session from Redis.
|
||||
|
||||
@@ -2052,18 +1956,11 @@ async def force_remove_gui_session(
|
||||
cfg.end_active_session(session_id)
|
||||
cfg.remove_gui_session(session_id)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"session": session_id,
|
||||
"message": "GUI session force removed.",
|
||||
}
|
||||
return {"ok": True, "session": session_id, "message": "GUI session force removed."}
|
||||
|
||||
|
||||
@app.post("/admin/gui_sessions/{session_id}/interaction")
|
||||
async def update_gui_interaction(
|
||||
session_id: int,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> dict:
|
||||
async def update_gui_interaction(session_id: int, token: str = Depends(oauth2_scheme)) -> dict:
|
||||
"""
|
||||
GUI-side activity heartbeat.
|
||||
This is distinct from /status so idle timeout ignores status polling.
|
||||
@@ -2075,13 +1972,10 @@ async def update_gui_interaction(
|
||||
detail="Cannot update interaction for another session.",
|
||||
)
|
||||
|
||||
payload = cfg.update_gui_interaction(
|
||||
session=session_id, last_interaction_ts=time.time()
|
||||
)
|
||||
payload = cfg.update_gui_interaction(session=session_id, last_interaction_ts=time.time())
|
||||
if payload is None:
|
||||
raise HTTPException(
|
||||
status_code=api_status.HTTP_404_NOT_FOUND,
|
||||
detail="GUI session not found.",
|
||||
status_code=api_status.HTTP_404_NOT_FOUND, detail="GUI session not found."
|
||||
)
|
||||
|
||||
return {"ok": True}
|
||||
@@ -2359,9 +2253,7 @@ async def put_settings(s: BeamlineSettingsModel, token: str = Depends(oauth2_sch
|
||||
|
||||
|
||||
@app.get("/beamline/cryo_settings")
|
||||
async def get_cryo_settings(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> CryojetSettingsModel:
|
||||
async def get_cryo_settings(token: str = Depends(oauth2_scheme)) -> CryojetSettingsModel:
|
||||
"""
|
||||
Get the current cryojet settings. Staff only.
|
||||
|
||||
@@ -2377,9 +2269,7 @@ async def get_cryo_settings(
|
||||
|
||||
|
||||
@app.put("/beamline/cryo_settings")
|
||||
async def put_cryo_settings(
|
||||
s: CryojetSettingsModel, token: str = Depends(oauth2_scheme)
|
||||
):
|
||||
async def put_cryo_settings(s: CryojetSettingsModel, token: str = Depends(oauth2_scheme)):
|
||||
"""
|
||||
Update the cryojet settings. Staff only.
|
||||
|
||||
@@ -2456,9 +2346,7 @@ async def fluorimeter_spectrum(
|
||||
|
||||
|
||||
@app.post("/fluorimeter/start")
|
||||
async def fluorimeter_start(
|
||||
erase: bool = False, token: str = Depends(oauth2_scheme)
|
||||
) -> str:
|
||||
async def fluorimeter_start(erase: bool = False, token: str = Depends(oauth2_scheme)) -> str:
|
||||
"""
|
||||
Start the fluorimeter measurement.
|
||||
|
||||
@@ -2521,9 +2409,7 @@ async def fluorimeter_data(token: str = Depends(oauth2_scheme)) -> list[int] | N
|
||||
|
||||
|
||||
@app.get("/fluorimeter/background")
|
||||
async def fluorimeter_background(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
) -> list[int] | None:
|
||||
async def fluorimeter_background(token: str = Depends(oauth2_scheme)) -> list[int] | None:
|
||||
"""
|
||||
Get the fluorimeter background data.
|
||||
|
||||
@@ -2559,24 +2445,14 @@ async def fluorimeter_stream() -> AsyncGenerator[str, None]:
|
||||
data = (
|
||||
d.tolist()
|
||||
if hasattr(d, "tolist")
|
||||
else (
|
||||
list(d)
|
||||
if isinstance(d, (tuple, list))
|
||||
else ([] if d is None else [d])
|
||||
)
|
||||
else (list(d) if isinstance(d, (tuple, list)) else ([] if d is None else [d]))
|
||||
)
|
||||
bkg = (
|
||||
b.tolist()
|
||||
if hasattr(b, "tolist")
|
||||
else (
|
||||
list(b)
|
||||
if isinstance(b, (tuple, list))
|
||||
else ([] if b is None else [b])
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
f"fluorimeter data: {data[0]}. fluorimeter background: {bkg[0]}"
|
||||
else (list(b) if isinstance(b, (tuple, list)) else ([] if b is None else [b]))
|
||||
)
|
||||
logger.debug(f"fluorimeter data: {data[0]}. fluorimeter background: {bkg[0]}")
|
||||
payload = json.dumps(
|
||||
{"status": s, "data": data, "background": bkg}, separators=(",", ":")
|
||||
)
|
||||
@@ -2622,9 +2498,7 @@ async def sse_fluorimeter(token: str = Depends(oauth2_scheme)):
|
||||
|
||||
@app.post("/samcam/send_screenshot_db")
|
||||
async def send_screenshot_db(
|
||||
filename: str | None = None,
|
||||
message: str | None = None,
|
||||
token: str = Depends(oauth2_scheme),
|
||||
filename: str | None = None, message: str | None = None, token: str = Depends(oauth2_scheme)
|
||||
) -> str:
|
||||
"""
|
||||
Capture a screenshot and send it to the database.
|
||||
@@ -2684,9 +2558,7 @@ async def maintenance(token: str = Depends(oauth2_scheme)) -> str:
|
||||
auth.check_jwt_staff(cfg, auth.parse_token(token))
|
||||
cfg.state = BeamlineStateEnum.Maintenance
|
||||
logger.warning("Beamline state set to Maintenance via protected endpoint.")
|
||||
raise MaintenanceStateException(
|
||||
"Beamline was set to Maintenance. Automation must stop."
|
||||
)
|
||||
raise MaintenanceStateException("Beamline was set to Maintenance. Automation must stop.")
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -86,10 +86,7 @@ def _extract_context(exc: Exception) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _error_body(
|
||||
exc: Exception,
|
||||
*,
|
||||
code_override: str | None = None,
|
||||
critical_override: bool | None = None,
|
||||
exc: Exception, *, code_override: str | None = None, critical_override: bool | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Build the unified error response body for any exception.
|
||||
|
||||
@@ -125,9 +122,7 @@ def register_exception_handlers(app) -> None:
|
||||
"""
|
||||
|
||||
@app.exception_handler(AutomationError)
|
||||
async def _handle_automation_error(
|
||||
request: Request, exc: AutomationError
|
||||
) -> JSONResponse:
|
||||
async def _handle_automation_error(request: Request, exc: AutomationError) -> JSONResponse:
|
||||
body = _error_body(exc)
|
||||
log = logger.error if exc.critical else logger.warning
|
||||
log(
|
||||
@@ -159,10 +154,7 @@ def register_exception_handlers(app) -> None:
|
||||
**body["context"],
|
||||
},
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_400_BAD_REQUEST,
|
||||
content=body,
|
||||
)
|
||||
return JSONResponse(status_code=api_status.HTTP_400_BAD_REQUEST, content=body)
|
||||
|
||||
@app.exception_handler(AareAuthError)
|
||||
async def _handle_auth_error(request: Request, exc: AareAuthError) -> JSONResponse:
|
||||
@@ -189,26 +181,16 @@ def register_exception_handlers(app) -> None:
|
||||
"status_code": status,
|
||||
},
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=status,
|
||||
content=body,
|
||||
headers=getattr(exc, "headers", None),
|
||||
)
|
||||
return JSONResponse(status_code=status, content=body, headers=getattr(exc, "headers", None))
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
async def _handle_http_exception(
|
||||
request: Request, exc: HTTPException
|
||||
) -> JSONResponse:
|
||||
async def _handle_http_exception(request: Request, exc: HTTPException) -> JSONResponse:
|
||||
"""HTTPException isn't in the AareException hierarchy but FastAPI raises
|
||||
it internally (e.g. validation, route-not-found) and routes raise it
|
||||
manually. Adapt to the new body shape so clients see one schema."""
|
||||
detail = exc.detail
|
||||
log = logger.error if exc.status_code >= 500 else logger.warning
|
||||
log(
|
||||
"HTTPException %s: %s",
|
||||
exc.status_code,
|
||||
detail,
|
||||
)
|
||||
log("HTTPException %s: %s", exc.status_code, detail)
|
||||
# Preserve legacy callers that passed a {code, message} dict
|
||||
if isinstance(detail, dict) and "code" in detail and "message" in detail:
|
||||
legacy_code = str(detail.get("code"))
|
||||
@@ -220,9 +202,7 @@ def register_exception_handlers(app) -> None:
|
||||
"code": legacy_code,
|
||||
"exception_class": "HTTPException",
|
||||
"message": legacy_message,
|
||||
"context": {
|
||||
k: v for k, v in detail.items() if k not in ("code", "message")
|
||||
},
|
||||
"context": {k: v for k, v in detail.items() if k not in ("code", "message")},
|
||||
},
|
||||
headers=exc.headers,
|
||||
)
|
||||
@@ -245,8 +225,6 @@ def register_exception_handlers(app) -> None:
|
||||
return JSONResponse(
|
||||
status_code=api_status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content=_error_body(
|
||||
exc,
|
||||
code_override=str(AareErrorCode.INTERNAL_ERROR),
|
||||
critical_override=True,
|
||||
exc, code_override=str(AareErrorCode.INTERNAL_ERROR), critical_override=True
|
||||
),
|
||||
)
|
||||
|
||||
@@ -27,9 +27,7 @@ def set_spreadsheet_in_redis(spreadsheet):
|
||||
"[REDIS][DEBUG] Data to write:", json.dumps(spreadsheet, indent=4)
|
||||
) # Pretty-print the data
|
||||
print("[REDIS][INFO] Writing spreadsheet to Redis...")
|
||||
config.__client.set(
|
||||
f"{config._BeamlineConfig__bl}:spreadsheet", json.dumps(spreadsheet)
|
||||
)
|
||||
config.__client.set(f"{config._BeamlineConfig__bl}:spreadsheet", json.dumps(spreadsheet))
|
||||
|
||||
|
||||
def on_message(ws, message):
|
||||
@@ -39,9 +37,7 @@ def on_message(ws, message):
|
||||
try:
|
||||
data = json.loads(message)
|
||||
|
||||
pucks_data = (
|
||||
data["samples"] if isinstance(data, dict) and "samples" in data else data
|
||||
)
|
||||
pucks_data = data["samples"] if isinstance(data, dict) and "samples" in data else data
|
||||
pucks = [PuckWithTellPosition(**item) for item in pucks_data]
|
||||
print(f"INFO: Received pucks: {[p.puck_name for p in pucks]}")
|
||||
|
||||
@@ -56,8 +52,7 @@ def on_message(ws, message):
|
||||
# Reference tool
|
||||
if isinstance(p.tell_position, str) and p.tell_position.startswith("X"):
|
||||
dewar_address = DewarAddress(
|
||||
segment="X",
|
||||
pos=int(p.tell_position[1:]) if len(p.tell_position) > 1 else 1,
|
||||
segment="X", pos=int(p.tell_position[1:]) if len(p.tell_position) > 1 else 1
|
||||
)
|
||||
target_list = reference_short_infos
|
||||
|
||||
@@ -69,9 +64,7 @@ def on_message(ws, message):
|
||||
target_list = normal_short_infos
|
||||
|
||||
else:
|
||||
print(
|
||||
f"[WARN] Skipping unknown tell_position format: {p.tell_position}"
|
||||
)
|
||||
print(f"[WARN] Skipping unknown tell_position format: {p.tell_position}")
|
||||
continue
|
||||
|
||||
info = SampleShortInfo(
|
||||
|
||||
@@ -46,9 +46,7 @@ def _update_state(
|
||||
mount_success: bool | None = None,
|
||||
mount_error: str | None = None,
|
||||
) -> TellStateModel:
|
||||
updates = {
|
||||
"last_update_ts": _utc_now_iso(),
|
||||
}
|
||||
updates = {"last_update_ts": _utc_now_iso()}
|
||||
|
||||
if activity is not None:
|
||||
updates["activity"] = activity
|
||||
@@ -72,9 +70,7 @@ def _update_state(
|
||||
return state.model_copy(update=updates)
|
||||
|
||||
|
||||
def _ready_state(
|
||||
state: TellStateModel, event_name: str, event_value: str | None
|
||||
) -> TellStateModel:
|
||||
def _ready_state(state: TellStateModel, event_name: str, event_value: str | None) -> TellStateModel:
|
||||
return _update_state(
|
||||
state,
|
||||
activity=TellActivityEnum.IDLE,
|
||||
@@ -88,9 +84,7 @@ def _ready_state(
|
||||
|
||||
|
||||
def advance_tell_state(
|
||||
state: TellStateModel,
|
||||
event_name: str,
|
||||
event_value: str | None,
|
||||
state: TellStateModel, event_name: str, event_value: str | None
|
||||
) -> TellStateModel:
|
||||
value = (event_value or "").strip()
|
||||
|
||||
@@ -120,20 +114,14 @@ def advance_tell_state(
|
||||
|
||||
if event_name == "shell":
|
||||
if value == "true":
|
||||
return _update_state(
|
||||
state,
|
||||
event_name=event_name,
|
||||
event_value=value,
|
||||
)
|
||||
return _update_state(state, event_name=event_name, event_value=value)
|
||||
|
||||
if value.startswith("mount("):
|
||||
sample_position = _parse_sample_position_from_mount_command(value)
|
||||
return _update_state(
|
||||
state,
|
||||
activity=TellActivityEnum.MOUNTING,
|
||||
message=f"Mounting {sample_position}"
|
||||
if sample_position
|
||||
else "Mounting sample",
|
||||
message=f"Mounting {sample_position}" if sample_position else "Mounting sample",
|
||||
event_name=event_name,
|
||||
event_value=value,
|
||||
operation="mount",
|
||||
@@ -201,11 +189,7 @@ def advance_tell_state(
|
||||
event_value=value,
|
||||
)
|
||||
|
||||
return _update_state(
|
||||
state,
|
||||
event_name=event_name,
|
||||
event_value=value,
|
||||
)
|
||||
return _update_state(state, event_name=event_name, event_value=value)
|
||||
|
||||
if value == "got result False":
|
||||
if state.operation == "mount" and state.phase in {
|
||||
@@ -260,11 +244,7 @@ def advance_tell_state(
|
||||
mount_error="",
|
||||
)
|
||||
|
||||
return _update_state(
|
||||
state,
|
||||
event_name=event_name,
|
||||
event_value=value,
|
||||
)
|
||||
return _update_state(state, event_name=event_name, event_value=value)
|
||||
|
||||
if event_name == "Motion Sync":
|
||||
if value == "Sample get from Gonio":
|
||||
@@ -304,9 +284,7 @@ def advance_tell_state(
|
||||
event_value=value,
|
||||
operation=next_operation,
|
||||
phase=next_phase,
|
||||
mount_success=True
|
||||
if state.operation == "unmount"
|
||||
else state.mount_success,
|
||||
mount_success=True if state.operation == "unmount" else state.mount_success,
|
||||
mount_error="" if state.operation == "unmount" else state.mount_error,
|
||||
)
|
||||
|
||||
@@ -366,15 +344,9 @@ def advance_tell_state(
|
||||
if value == "Busy":
|
||||
return _update_state(
|
||||
state,
|
||||
message=state.message
|
||||
if state.operation not in {None, "idle"}
|
||||
else "Busy",
|
||||
message=state.message if state.operation not in {None, "idle"} else "Busy",
|
||||
event_name=event_name,
|
||||
event_value=value,
|
||||
)
|
||||
|
||||
return _update_state(
|
||||
state,
|
||||
event_name=event_name,
|
||||
event_value=value,
|
||||
)
|
||||
return _update_state(state, event_name=event_name, event_value=value)
|
||||
|
||||
+15
-62
@@ -12,10 +12,7 @@ import sseclient
|
||||
import websocket
|
||||
from aarecommon.config.beamline import mx_beamline
|
||||
from aarecommon.config.logger import setup_logger
|
||||
from aarecommon.errors.exception_handler import (
|
||||
TellCommunicationError,
|
||||
TellConnectionException,
|
||||
)
|
||||
from aarecommon.errors.exception_handler import TellCommunicationError, TellConnectionException
|
||||
from aarecommon.models.beamline import MXBeamline
|
||||
from aarecommon.models.tell import TellStateModel
|
||||
from aareDB.exceptions import ApiException
|
||||
@@ -42,16 +39,10 @@ last_pucks_sent = []
|
||||
DEWAR_CONTENT_UPDATE_EVENT = "DewarContentUpdate"
|
||||
|
||||
TRACKED_TELL_EVENT_VALUES = {
|
||||
"Gripper detection": {
|
||||
"Pin still in Gripper",
|
||||
"No Pin in Gripper",
|
||||
"Pin is lost",
|
||||
},
|
||||
"Gripper detection": {"Pin still in Gripper", "No Pin in Gripper", "Pin is lost"}
|
||||
}
|
||||
|
||||
TRACKED_MOTION_EVENT_VALUES = {
|
||||
"Motion Task": {"dry"},
|
||||
}
|
||||
TRACKED_MOTION_EVENT_VALUES = {"Motion Task": {"dry"}}
|
||||
TRACKED_MOTION_SYNC_EVENTS = {
|
||||
"Motion Sync": {
|
||||
"Robot Clear after mount",
|
||||
@@ -60,7 +51,7 @@ TRACKED_MOTION_SYNC_EVENTS = {
|
||||
"Sample get from Gonio",
|
||||
"Sample put on Puck",
|
||||
"Sample get on Puck",
|
||||
},
|
||||
}
|
||||
}
|
||||
TRACKED_STATE_EVENTS = {"state": {"Ready", "Busy"}}
|
||||
|
||||
@@ -71,16 +62,8 @@ TELL_JOURNAL_PREFIX = "[TELL][JOURNAL]"
|
||||
TELL_EVENTS_REDIS_KEY_SUFFIX = "tell_events"
|
||||
TELL_STATE_REDIS_KEY_SUFFIX = "tell_state"
|
||||
SSE_RECONNECT_DELAY_S = 5
|
||||
SSE_RECOVERABLE_EXCEPTIONS = (
|
||||
RequestException,
|
||||
OSError,
|
||||
ssl.SSLError,
|
||||
)
|
||||
WS_RECOVERABLE_EXCEPTIONS = (
|
||||
OSError,
|
||||
ssl.SSLError,
|
||||
websocket.WebSocketException,
|
||||
)
|
||||
SSE_RECOVERABLE_EXCEPTIONS = (RequestException, OSError, ssl.SSLError)
|
||||
WS_RECOVERABLE_EXCEPTIONS = (OSError, ssl.SSLError, websocket.WebSocketException)
|
||||
|
||||
|
||||
class TellSsePayload(BaseModel):
|
||||
@@ -92,13 +75,7 @@ class TellSsePayload(BaseModel):
|
||||
|
||||
@property
|
||||
def normalized_value(self) -> str | None:
|
||||
for candidate in (
|
||||
self.value,
|
||||
self.eventValue,
|
||||
self.data,
|
||||
self.message,
|
||||
self.state,
|
||||
):
|
||||
for candidate in (self.value, self.eventValue, self.data, self.message, self.state):
|
||||
if candidate is None:
|
||||
continue
|
||||
stripped = candidate.strip()
|
||||
@@ -113,11 +90,7 @@ class TellEventRecord(BaseModel):
|
||||
event: str
|
||||
|
||||
def redis_dict(self) -> dict[str, str]:
|
||||
return {
|
||||
"timestamp": self.timestamp,
|
||||
"class": self.class_,
|
||||
"event": self.event,
|
||||
}
|
||||
return {"timestamp": self.timestamp, "class": self.class_, "event": self.event}
|
||||
|
||||
|
||||
def _get_redis_context() -> tuple[Any | None, str | None]:
|
||||
@@ -128,9 +101,7 @@ def _get_redis_context() -> tuple[Any | None, str | None]:
|
||||
redis_client = getattr(config, "_BeamlineConfig__client", None)
|
||||
beamline_key = getattr(config, "_BeamlineConfig__bl", None)
|
||||
if redis_client is None or beamline_key is None:
|
||||
logger.error(
|
||||
"[REDIS] BeamlineConfig internals unavailable; skipping TELL redis write"
|
||||
)
|
||||
logger.error("[REDIS] BeamlineConfig internals unavailable; skipping TELL redis write")
|
||||
return None, None
|
||||
|
||||
return cast(Any, redis_client), str(beamline_key)
|
||||
@@ -200,26 +171,16 @@ def extract_tracked_tell_event(event_name, event_data):
|
||||
|
||||
|
||||
def set_tell_events_in_redis(events: list[dict[str, str]]) -> None:
|
||||
_set_json_in_redis(
|
||||
TELL_EVENTS_REDIS_KEY_SUFFIX,
|
||||
events,
|
||||
log_label="TELL events",
|
||||
)
|
||||
_set_json_in_redis(TELL_EVENTS_REDIS_KEY_SUFFIX, events, log_label="TELL events")
|
||||
|
||||
|
||||
def set_tell_state_in_redis(state: TellStateModel) -> None:
|
||||
_set_json_in_redis(
|
||||
TELL_STATE_REDIS_KEY_SUFFIX,
|
||||
state.model_dump(),
|
||||
log_label="TELL state",
|
||||
)
|
||||
_set_json_in_redis(TELL_STATE_REDIS_KEY_SUFFIX, state.model_dump(), log_label="TELL state")
|
||||
|
||||
|
||||
def record_tell_event(event_name, event_value):
|
||||
event_record = TellEventRecord(
|
||||
timestamp=datetime.now(timezone.utc).isoformat(),
|
||||
class_=event_name,
|
||||
event=event_value,
|
||||
timestamp=datetime.now(timezone.utc).isoformat(), class_=event_name, event=event_value
|
||||
)
|
||||
|
||||
latest_tell_events[event_name] = event_value
|
||||
@@ -252,9 +213,7 @@ def listen_to_sse():
|
||||
# mTLS path
|
||||
# ca_root = "/etc/ssl/certs/secrets/mx-db-01_Full_Chain_CA.pem"
|
||||
ca_root = "/etc/ssl/certs/secrets/mx-aaredb-dmz-01_Full_Chain_CA.pem"
|
||||
response = requests.get(
|
||||
sse_url, stream=True, cert=cert_pair, verify=ca_root
|
||||
)
|
||||
response = requests.get(sse_url, stream=True, cert=cert_pair, verify=ca_root)
|
||||
response.raise_for_status()
|
||||
client = sseclient.SSEClient(response)
|
||||
else:
|
||||
@@ -271,9 +230,7 @@ def listen_to_sse():
|
||||
# print(f"event = {event.event} with data: {event.data}")
|
||||
on_sse_event(event)
|
||||
except SSE_RECOVERABLE_EXCEPTIONS:
|
||||
logger.exception(
|
||||
"[SSE] Recoverable failure while consuming TELL event stream"
|
||||
)
|
||||
logger.exception("[SSE] Recoverable failure while consuming TELL event stream")
|
||||
|
||||
logger.info(f"[SSE] Reconnecting in {SSE_RECONNECT_DELAY_S} seconds")
|
||||
time.sleep(SSE_RECONNECT_DELAY_S)
|
||||
@@ -326,11 +283,7 @@ def on_sse_event(event):
|
||||
logger.debug(f"[EVENT] SSE event={event.event} data={event.data!r}")
|
||||
normalized_data = _normalize_event_data(event.data)
|
||||
|
||||
current_tell_state = advance_tell_state(
|
||||
current_tell_state,
|
||||
event.event,
|
||||
normalized_data,
|
||||
)
|
||||
current_tell_state = advance_tell_state(current_tell_state, event.event, normalized_data)
|
||||
set_tell_state_in_redis(current_tell_state)
|
||||
|
||||
tracked_event = extract_tracked_tell_event(event.event, normalized_data)
|
||||
|
||||
@@ -13,9 +13,7 @@ logger = setup_logger("aareDAQ")
|
||||
SAFE_POSITION = 600
|
||||
|
||||
|
||||
def _move_detector_to_safe_position_if_needed(
|
||||
devs: BeamlineDevices, cfg: BeamlineConfig
|
||||
):
|
||||
def _move_detector_to_safe_position_if_needed(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
safe_position = cfg.dtz_safe_position
|
||||
current_dtz = devs.dtz
|
||||
|
||||
@@ -157,9 +155,7 @@ def sa2dc(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
start = time.perf_counter()
|
||||
det_z_pos = cfg.dtz
|
||||
status = devs.bec_worker.det_z(value=det_z_pos)
|
||||
logger.info(
|
||||
f"start detector move to {cfg.dtz} at {time.perf_counter() - start:.2f}"
|
||||
)
|
||||
logger.info(f"start detector move to {cfg.dtz} at {time.perf_counter() - start:.2f}")
|
||||
devs.samcam_auto(AutoEnum.AUTO)
|
||||
logger.info(f"sam cam auto exposure in {time.perf_counter() - start:.2f}")
|
||||
devs.bec_worker.move_to(BeamlineState.DATA_COLLECTION)
|
||||
@@ -264,9 +260,7 @@ def common2dh(devs: BeamlineDevices, cfg: BeamlineConfig):
|
||||
devs.samcam_auto(AutoEnum.ONCE)
|
||||
|
||||
if devs.tell.is_position("pPark") and devs.tell.get_mounted_sample() is None:
|
||||
logger.info(
|
||||
"TELL already in pPark; skipping dry/park preparation for dewar transfer"
|
||||
)
|
||||
logger.info("TELL already in pPark; skipping dry/park preparation for dewar transfer")
|
||||
return
|
||||
|
||||
if devs.tell.get_mounted_sample() is not None:
|
||||
|
||||
@@ -49,10 +49,7 @@ class AerotechController(object):
|
||||
self.__api = DefaultApi(self.__client)
|
||||
|
||||
def __make_aerotech_target(
|
||||
self,
|
||||
coord: AerotechCoordinate,
|
||||
wait: bool = False,
|
||||
incremental: bool = False,
|
||||
self, coord: AerotechCoordinate, wait: bool = False, incremental: bool = False
|
||||
) -> Target:
|
||||
at_mm = coord.at_mm
|
||||
|
||||
@@ -96,12 +93,7 @@ class AerotechController(object):
|
||||
def get_position(self) -> AerotechCoordinate:
|
||||
status = self.status()
|
||||
return AerotechCoordinate(
|
||||
at_mm=Coordinate(
|
||||
x=status.x.pos,
|
||||
y=status.y.pos,
|
||||
z=status.z.pos,
|
||||
),
|
||||
omega_deg=status.u.pos,
|
||||
at_mm=Coordinate(x=status.x.pos, y=status.y.pos, z=status.z.pos), omega_deg=status.u.pos
|
||||
)
|
||||
|
||||
def status(self) -> Status:
|
||||
@@ -162,10 +154,7 @@ class AerotechController(object):
|
||||
return self.__api.home_post()
|
||||
except Exception as e:
|
||||
raise AerotechCommunicationError(
|
||||
"Aerotech home failed",
|
||||
endpoint="home_post",
|
||||
base_url=self.__base,
|
||||
operation="POST",
|
||||
"Aerotech home failed", endpoint="home_post", base_url=self.__base, operation="POST"
|
||||
) from e
|
||||
|
||||
def wait_till_done(self, timeout=60):
|
||||
@@ -179,19 +168,11 @@ class AerotechController(object):
|
||||
operation="POST",
|
||||
) from e
|
||||
|
||||
def position(
|
||||
self,
|
||||
target: AerotechCoordinate,
|
||||
/,
|
||||
wait: bool = True,
|
||||
incremental: bool = False,
|
||||
):
|
||||
def position(self, target: AerotechCoordinate, /, wait: bool = True, incremental: bool = False):
|
||||
if self.__simulated:
|
||||
return self.__pos
|
||||
|
||||
payload = self.__make_aerotech_target(
|
||||
target, wait=wait, incremental=incremental
|
||||
)
|
||||
payload = self.__make_aerotech_target(target, wait=wait, incremental=incremental)
|
||||
try:
|
||||
return self.__api.position_post(payload)
|
||||
except Exception as e:
|
||||
|
||||
@@ -4,11 +4,13 @@ from enum import Enum
|
||||
import epics
|
||||
import numpy as np
|
||||
|
||||
|
||||
class AutoEnum(Enum):
|
||||
MANUAL = 0
|
||||
ONCE = 1
|
||||
AUTO = 2
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AutoExposureSettings:
|
||||
aoi_offset_x: int = 500
|
||||
@@ -18,6 +20,7 @@ class AutoExposureSettings:
|
||||
gain_max: float = 5.000
|
||||
exp_max: float = 30000.000
|
||||
|
||||
|
||||
class epicsAD(object):
|
||||
def __init__(self, prefix, cam="cam1:", image="image1:"):
|
||||
self.img = None
|
||||
@@ -70,8 +73,8 @@ class epicsAD(object):
|
||||
self.gain_max.put(settings.gain_max)
|
||||
self.exp_max.put(settings.exp_max)
|
||||
self.set_auto(AutoEnum.ONCE)
|
||||
#TODO add a white_balance button to GUI or set it so it happens with backlight in and on
|
||||
#self.auto_white_balance.put(1)
|
||||
# TODO add a white_balance button to GUI or set it so it happens with backlight in and on
|
||||
# self.auto_white_balance.put(1)
|
||||
self.acquire.put(1)
|
||||
|
||||
def get_image(self, gray=True) -> np.ndarray:
|
||||
@@ -117,4 +120,4 @@ class epicsAD(object):
|
||||
self.acquire.put(0, wait=True)
|
||||
self.gain_mode.put(state.value) # automatic gain control
|
||||
self.expo_mode.put(state.value) # automatic exposure control
|
||||
self.acquire.put(1)
|
||||
self.acquire.put(1)
|
||||
|
||||
@@ -83,9 +83,7 @@ class BECClientWorker:
|
||||
service_config.config["log_writer"]["base_path"] = "/tmp/logs"
|
||||
# service_config.config["user_macros"]["base_path"]=f'/sls/{BEAMLINE}/config/bec/production/pxiii_bec/pxiii_bec'
|
||||
# print(service_config.config)
|
||||
self.client = BECIPythonClient(
|
||||
config=service_config, mode=OperationMode.Procedure
|
||||
)
|
||||
self.client = BECIPythonClient(config=service_config, mode=OperationMode.Procedure)
|
||||
self.client.start()
|
||||
# self.client.config.update_session_with_file("/sls/x10sa/config/bec/production/bec/bec_lib/bec_lib/config_helper.py")
|
||||
self.dev = self.client.device_manager.devices
|
||||
@@ -164,11 +162,7 @@ class BECClientWorker:
|
||||
exception=exc,
|
||||
) from exc
|
||||
|
||||
raise BECCommunicationError(
|
||||
message,
|
||||
operation=operation,
|
||||
exception=exc,
|
||||
) from exc
|
||||
raise BECCommunicationError(message, operation=operation, exception=exc) from exc
|
||||
|
||||
def __set_scilog_tags(self, tags: Optional[List[str]] = None):
|
||||
try:
|
||||
@@ -243,9 +237,7 @@ class BECClientWorker:
|
||||
except Exception as e:
|
||||
self._raise_bec_error(e, operation=f"run_macro:{macro_name}")
|
||||
|
||||
def run_macro_blocked(
|
||||
self, macro_name: str, *args, queue: str = "default", **kwargs
|
||||
):
|
||||
def run_macro_blocked(self, macro_name: str, *args, queue: str = "default", **kwargs):
|
||||
if self.simulated:
|
||||
logger.debug(f"Simulating macro {macro_name}")
|
||||
return None
|
||||
@@ -308,9 +300,7 @@ class BECClientWorker:
|
||||
return []
|
||||
try:
|
||||
self.__list_all_macros()
|
||||
raw_macros = [
|
||||
name for name, _ in self.client.macros._update_handler.macros.items()
|
||||
]
|
||||
raw_macros = [name for name, _ in self.client.macros._update_handler.macros.items()]
|
||||
if raw_macros is None:
|
||||
logger.warning("BEC returned no user macros; treating as empty list")
|
||||
return []
|
||||
@@ -353,23 +343,18 @@ class BECClientWorker:
|
||||
List of position device names after reinitialisation.
|
||||
"""
|
||||
if self.simulated:
|
||||
logger.debug(
|
||||
f"Simulating reinitialise_planner_and_position_devices(method={method})"
|
||||
)
|
||||
logger.debug(f"Simulating reinitialise_planner_and_position_devices(method={method})")
|
||||
return []
|
||||
try:
|
||||
self.client.config.update_session_with_file(
|
||||
f"/sls/{self.beamline}/config/bec/production/{self._beamline_name}_bec/{self._beamline_name}_bec/device_configs/{self._beamline_name}-devices.yaml"
|
||||
)
|
||||
self.__init_beamline_environment()
|
||||
logger.info(
|
||||
f"Reinitialised BEC planner and position devices using method={method}"
|
||||
)
|
||||
logger.info(f"Reinitialised BEC planner and position devices using method={method}")
|
||||
return self.list_position_devices()
|
||||
except Exception as e:
|
||||
self._raise_bec_error(
|
||||
e,
|
||||
operation=f"reinitialise_planner_and_position_devices:{method}",
|
||||
e, operation=f"reinitialise_planner_and_position_devices:{method}"
|
||||
)
|
||||
|
||||
def shutdown_client(self):
|
||||
@@ -379,17 +364,11 @@ class BECClientWorker:
|
||||
try:
|
||||
mono_pitch_scan(plot)
|
||||
except Exception as e:
|
||||
self._raise_bec_error(
|
||||
e, operation="mono_pitch_scan", tags=["mono_pitch_scan"]
|
||||
)
|
||||
self._raise_bec_error(e, operation="mono_pitch_scan", tags=["mono_pitch_scan"])
|
||||
if self.beamline is MXBeamline.X06DA:
|
||||
addtional_text = [
|
||||
f"New dcm_pitch position: {self.dev.dcm_pitch.position:5f}"
|
||||
]
|
||||
addtional_text = [f"New dcm_pitch position: {self.dev.dcm_pitch.position:5f}"]
|
||||
else:
|
||||
addtional_text = [
|
||||
f"New dcm_theta2 position: {self.dev.dccm_theta2.position:5f}"
|
||||
]
|
||||
addtional_text = [f"New dcm_theta2 position: {self.dev.dccm_theta2.position:5f}"]
|
||||
self.scilog_msg(
|
||||
message="Mono pitch scan completed",
|
||||
bold=True,
|
||||
@@ -407,16 +386,13 @@ class BECClientWorker:
|
||||
def change_energy(self, value: float | int, plot: bool = False):
|
||||
current_energy = self.check_current_energy()
|
||||
logger.info(f"Current energy: {current_energy:.1f} eV")
|
||||
logger.info(
|
||||
f"Change energy requested: from {current_energy:.1f} to {value:.1f} eV"
|
||||
)
|
||||
logger.info(f"Change energy requested: from {current_energy:.1f} to {value:.1f} eV")
|
||||
try:
|
||||
bl_energy(value, move_gap=False, mono_scan=True, plot=plot)
|
||||
except Exception as e:
|
||||
self._raise_bec_error(
|
||||
e,
|
||||
operation=f"Requested energy change from:"
|
||||
f"{current_energy:.1f} to {value} eV",
|
||||
operation=f"Requested energy change from:{current_energy:.1f} to {value} eV",
|
||||
tags=["energy_change"],
|
||||
)
|
||||
|
||||
@@ -462,9 +438,7 @@ class BECClientWorker:
|
||||
status.wait(timeout=timeout)
|
||||
return status
|
||||
except Exception as e:
|
||||
self._raise_bec_error(
|
||||
e, operation=f"scans.mv:det_z:{value}", tags=["det_z"]
|
||||
)
|
||||
self._raise_bec_error(e, operation=f"scans.mv:det_z:{value}", tags=["det_z"])
|
||||
|
||||
def get_det_y(self):
|
||||
try:
|
||||
@@ -480,9 +454,7 @@ class BECClientWorker:
|
||||
status.wait(timeout=timeout)
|
||||
return status
|
||||
except Exception as e:
|
||||
self._raise_bec_error(
|
||||
e, operation=f"scans.mv:det_y:{value}", tags=["det_z"]
|
||||
)
|
||||
self._raise_bec_error(e, operation=f"scans.mv:det_y:{value}", tags=["det_z"])
|
||||
|
||||
@property
|
||||
def backlight_brightness(self) -> BrightnessEnum:
|
||||
@@ -506,9 +478,7 @@ class BECClientWorker:
|
||||
try:
|
||||
self.__backlight_brightness.move(value)
|
||||
except Exception as e:
|
||||
self._raise_bec_error(
|
||||
e, operation=f"backlight_brightness:{value}", tags=["backlight"]
|
||||
)
|
||||
self._raise_bec_error(e, operation=f"backlight_brightness:{value}", tags=["backlight"])
|
||||
raise
|
||||
|
||||
def get_backlight_pos(self) -> BrightnessEnum:
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Any
|
||||
|
||||
from aare.devices.set_get_pv import SetGetPV, MoveResult
|
||||
|
||||
|
||||
class EnumPV(SetGetPV):
|
||||
def __init__(self, name: str, setpv: str, getpv: str, **kwargs):
|
||||
super().__init__(name, setpv, getpv, **kwargs)
|
||||
@@ -32,4 +33,4 @@ class EnumPV(SetGetPV):
|
||||
return MoveResult(target=s, name=s)
|
||||
raise ValueError(f"'{x}' not in {list(self.setpoint_pv.enum_strs)}")
|
||||
|
||||
raise TypeError(f"Unsupported enum command type: {type(x).__name__}")
|
||||
raise TypeError(f"Unsupported enum command type: {type(x).__name__}")
|
||||
|
||||
@@ -50,6 +50,4 @@ class FilterTransmission:
|
||||
timeisup = time.time() > timeout
|
||||
poll(0.1)
|
||||
if timeisup and self._fail_on_timeout:
|
||||
raise RuntimeError(
|
||||
"timeout waiting for filters to achieve requested transmission."
|
||||
)
|
||||
raise RuntimeError("timeout waiting for filters to achieve requested transmission.")
|
||||
|
||||
@@ -13,9 +13,7 @@ class Fluorimeter(object):
|
||||
|
||||
self.__start = PV(f"{BEAMLINE}-ES-SiD:mca1Start") # 0 done ,1 start
|
||||
self.__stop = PV(f"{BEAMLINE}-ES-SiD:mca1Stop") # 0 done ,1 stop
|
||||
self.__erase_and_start = PV(
|
||||
f"{BEAMLINE}-ES-SiD:mca1EraseStart"
|
||||
) # 0 done,1 start
|
||||
self.__erase_and_start = PV(f"{BEAMLINE}-ES-SiD:mca1EraseStart") # 0 done,1 start
|
||||
self.__erase = PV(f"{BEAMLINE}-ES-SiD:mca1Erase") # 0 done,1 erase
|
||||
|
||||
self.__preset_mode = PV(f"{BEAMLINE}-ES-SiD:dxp1:PresetMode") # set mode
|
||||
@@ -25,9 +23,7 @@ class Fluorimeter(object):
|
||||
self.__live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.PLTM") # float
|
||||
self.__elapsed_real_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ERTM") # float
|
||||
self.__elapsed_live_time = PV(f"{BEAMLINE}-ES-SiD:mca1.ELTM") # float
|
||||
self.__elapsed_trigger_live_time = PV(
|
||||
f"{BEAMLINE}-ES-SiD:dxp1:ElapsedTriggerLiveTime"
|
||||
)
|
||||
self.__elapsed_trigger_live_time = PV(f"{BEAMLINE}-ES-SiD:dxp1:ElapsedTriggerLiveTime")
|
||||
|
||||
self.__instant_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.IDTIM")
|
||||
self.__average_dead_time = PV(f"{BEAMLINE}-ES-SiD:mca1.DTIM")
|
||||
|
||||
@@ -30,9 +30,7 @@ class JFJochWrapper:
|
||||
self.__api = None
|
||||
return
|
||||
|
||||
self.__client = jfjoch_client.ApiClient(
|
||||
jfjoch_client.Configuration(host=self.__url)
|
||||
)
|
||||
self.__client = jfjoch_client.ApiClient(jfjoch_client.Configuration(host=self.__url))
|
||||
self.__api = jfjoch_client.DefaultApi(self.__client)
|
||||
self.cancel()
|
||||
# if not self.is_idle():
|
||||
@@ -52,12 +50,7 @@ class JFJochWrapper:
|
||||
return None
|
||||
|
||||
def _raise_jfjoch_error(
|
||||
self,
|
||||
message: str,
|
||||
*,
|
||||
error: Exception,
|
||||
operation: str,
|
||||
endpoint: str,
|
||||
self, message: str, *, error: Exception, operation: str, endpoint: str
|
||||
) -> None:
|
||||
raise JFJochCommunicationError(
|
||||
message,
|
||||
@@ -72,10 +65,7 @@ class JFJochWrapper:
|
||||
self.__api.initialize_post()
|
||||
except Exception as e:
|
||||
self._raise_jfjoch_error(
|
||||
"JFJoch initialize failed",
|
||||
error=e,
|
||||
operation="POST",
|
||||
endpoint="initialize_post",
|
||||
"JFJoch initialize failed", error=e, operation="POST", endpoint="initialize_post"
|
||||
)
|
||||
|
||||
def cancel(self):
|
||||
@@ -83,10 +73,7 @@ class JFJochWrapper:
|
||||
self.__api.cancel_post()
|
||||
except Exception as e:
|
||||
self._raise_jfjoch_error(
|
||||
"JFJoch cancel failed",
|
||||
error=e,
|
||||
operation="POST",
|
||||
endpoint="cancel_post",
|
||||
"JFJoch cancel failed", error=e, operation="POST", endpoint="cancel_post"
|
||||
)
|
||||
|
||||
def is_idle(self) -> bool:
|
||||
@@ -213,9 +200,7 @@ class JFJochWrapper:
|
||||
f: FluorescenceSpectrumOutputModel | None = None,
|
||||
async_start: bool = True,
|
||||
):
|
||||
dataset_settings = self.__format_dataset_settings(
|
||||
r, s, f, async_start=async_start
|
||||
)
|
||||
dataset_settings = self.__format_dataset_settings(r, s, f, async_start=async_start)
|
||||
try:
|
||||
self.__api.start_post(dataset_settings=dataset_settings)
|
||||
except Exception as e:
|
||||
@@ -238,18 +223,14 @@ class JFJochWrapper:
|
||||
else:
|
||||
self.__start_scan(ScanTypeEnum.ROTATION, r, s, f, async_start=async_start)
|
||||
|
||||
def measure_raster(
|
||||
self, r: RasterGridRequest, s: DAQStatusModel, async_start: bool = True
|
||||
):
|
||||
def measure_raster(self, r: RasterGridRequest, s: DAQStatusModel, async_start: bool = True):
|
||||
self.__start_scan(ScanTypeEnum.RASTER, r, s, async_start=async_start)
|
||||
|
||||
def wait_till_running(self, timeout: int | float = 60):
|
||||
if self.__simulated:
|
||||
return None
|
||||
try:
|
||||
self.__api.wait_until_running_post_with_http_info(
|
||||
timeout=math.ceil(timeout)
|
||||
)
|
||||
self.__api.wait_until_running_post_with_http_info(timeout=math.ceil(timeout))
|
||||
return True
|
||||
except Exception as e:
|
||||
self._raise_jfjoch_error(
|
||||
@@ -259,9 +240,7 @@ class JFJochWrapper:
|
||||
endpoint="wait_until_running_post",
|
||||
)
|
||||
|
||||
def wait_till_done(
|
||||
self, timeout: int | float
|
||||
) -> jfjoch_client.models.ScanResult | None:
|
||||
def wait_till_done(self, timeout: int | float) -> jfjoch_client.models.ScanResult | None:
|
||||
if self.__simulated:
|
||||
return None
|
||||
try:
|
||||
@@ -385,9 +364,7 @@ if __name__ == "__main__":
|
||||
pixel_in_mm=0.001,
|
||||
aerotech=Coordinate(x=0, y=0),
|
||||
aerotech_meas=Coordinate(x=0, y=0),
|
||||
smargon=SmargonCoordinate(
|
||||
sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0
|
||||
),
|
||||
smargon=SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=0), phi_deg=0, chi_deg=0),
|
||||
omega_deg=10.5,
|
||||
beam_size_mm=Coordinate(x=0.02, y=0.01),
|
||||
),
|
||||
|
||||
+43
-14
@@ -4,6 +4,7 @@ from typing import Callable, Union, Any
|
||||
|
||||
from epics import PV, Motor, poll
|
||||
|
||||
|
||||
def wait_for_movement_to_finish(*motors):
|
||||
"""
|
||||
Wait for all {motors} passed in argument to finish movement.
|
||||
@@ -38,8 +39,15 @@ class ValueWaitTimeout(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def pv_wait(pv: PV | Motor, value: Any, *, timeout: float = 60.0,
|
||||
polling: float = 0.1, tolerance: float | None=None, verbose: bool =False):
|
||||
def pv_wait(
|
||||
pv: PV | Motor,
|
||||
value: Any,
|
||||
*,
|
||||
timeout: float = 60.0,
|
||||
polling: float = 0.1,
|
||||
tolerance: float | None = None,
|
||||
verbose: bool = False,
|
||||
):
|
||||
"""wait until an epics.PV reaches a value
|
||||
pv: epics.PV | epics.Motor
|
||||
the PV on which you want to wait on
|
||||
@@ -62,14 +70,16 @@ def pv_wait(pv: PV | Motor, value: Any, *, timeout: float = 60.0,
|
||||
tolerance = m.get("RDBD")
|
||||
if tolerance is None:
|
||||
print(f"WARNING: motor {m._prefix[:-1]} has no RDBD, using 0.001")
|
||||
tolerance = 0.001 #FIXME
|
||||
tolerance = 0.001 # FIXME
|
||||
if target is None:
|
||||
target = m.drive
|
||||
|
||||
diff = abs(target - m.readback)
|
||||
return m.done_moving and diff < tolerance
|
||||
|
||||
wait_motor_position(pv, checker, target=value, tolerance=tolerance, timeout=timeout, polling=polling)
|
||||
wait_motor_position(
|
||||
pv, checker, target=value, tolerance=tolerance, timeout=timeout, polling=polling
|
||||
)
|
||||
|
||||
elif is_epics_type(pv, "double") or is_epics_type(pv, "time_double"):
|
||||
wait_float_condition(pv, value, tolerance=tolerance, timeout=timeout, polling=polling)
|
||||
@@ -91,7 +101,9 @@ def is_epics_type(pv: PV, pv_type: str) -> bool:
|
||||
return pv_type == pv.type
|
||||
|
||||
|
||||
def wait_string_condition(pv: PV, target: Union[str, re.Pattern], *, timeout: float = 60.0, polling: float = 0.1):
|
||||
def wait_string_condition(
|
||||
pv: PV, target: Union[str, re.Pattern], *, timeout: float = 60.0, polling: float = 0.1
|
||||
):
|
||||
"""wait until an epics.PV of type string reaches target
|
||||
:pv: epics.PV
|
||||
PV should be of type string
|
||||
@@ -116,11 +128,19 @@ def wait_string_condition(pv: PV, target: Union[str, re.Pattern], *, timeout: fl
|
||||
poll(polling)
|
||||
|
||||
if time.time() > tout:
|
||||
raise TimeoutError(f"timeout waiting for string {pv.pvname} == {target}; actual value == {pv.char_value}")
|
||||
raise TimeoutError(
|
||||
f"timeout waiting for string {pv.pvname} == {target}; actual value == {pv.char_value}"
|
||||
)
|
||||
|
||||
|
||||
def wait_float_condition(pv: PV, value:float, *, timeout: float = 60.0,
|
||||
polling: float = 0.1, tolerance: float| None = None):
|
||||
def wait_float_condition(
|
||||
pv: PV,
|
||||
value: float,
|
||||
*,
|
||||
timeout: float = 60.0,
|
||||
polling: float = 0.1,
|
||||
tolerance: float | None = None,
|
||||
):
|
||||
"""wait until an epics.PV of type double reaches value
|
||||
pv: epics.PV
|
||||
the PV enum on which you want to wait on
|
||||
@@ -168,7 +188,9 @@ def wait_float_condition(pv: PV, value:float, *, timeout: float = 60.0,
|
||||
raise TimeoutError(f"timeout waiting for enum {pv.pvname} == {value}")
|
||||
|
||||
|
||||
def wait_motor_position(motor: Motor, tester: Callable, *, timeout: float = 50.0, polling: float = 0.1, **kwargs):
|
||||
def wait_motor_position(
|
||||
motor: Motor, tester: Callable, *, timeout: float = 50.0, polling: float = 0.1, **kwargs
|
||||
):
|
||||
"""wait until an epics.Motor reaches a condition
|
||||
motor: epics.Motor
|
||||
the motor on which you want to wait on
|
||||
@@ -202,10 +224,14 @@ def wait_motor_position(motor: Motor, tester: Callable, *, timeout: float = 50.0
|
||||
poll(polling)
|
||||
|
||||
if time.time() > tout:
|
||||
raise TimeoutError(f"timeout waiting for a condition on {motor} {motor.drive} != {motor.readback}")
|
||||
raise TimeoutError(
|
||||
f"timeout waiting for a condition on {motor} {motor.drive} != {motor.readback}"
|
||||
)
|
||||
|
||||
|
||||
def wait_enum_condition(pv: PV, value: Union[str, int, re.Pattern], *, timeout: float = 60.0, polling=0.1):
|
||||
def wait_enum_condition(
|
||||
pv: PV, value: Union[str, int, re.Pattern], *, timeout: float = 60.0, polling=0.1
|
||||
):
|
||||
"""wait until an epics.PV enum reaches value
|
||||
pv: epics.PV
|
||||
the PV enum on which you want to wait on
|
||||
@@ -244,12 +270,15 @@ def wait_enum_condition(pv: PV, value: Union[str, int, re.Pattern], *, timeout:
|
||||
poll(polling)
|
||||
|
||||
if time.time() > tout:
|
||||
raise TimeoutError(f"timeout waiting for enum {pv.pvname} == {value},"
|
||||
f"current value is {pv.get(as_string=True)}")
|
||||
raise TimeoutError(
|
||||
f"timeout waiting for enum {pv.pvname} == {value},"
|
||||
f"current value is {pv.get(as_string=True)}"
|
||||
)
|
||||
|
||||
|
||||
def clean_filename(filename: str) -> str:
|
||||
cleaned = re.sub(r"[^A-Za-z0-9._-]", "_", filename.strip())
|
||||
cleaned = cleaned.strip("._-")
|
||||
if not cleaned:
|
||||
raise ValueError("Filename is empty after sanitization.")
|
||||
return cleaned
|
||||
return cleaned
|
||||
|
||||
@@ -5,18 +5,19 @@ from epics import Motor
|
||||
|
||||
class MyMotor(Motor):
|
||||
"""Wrapper for the EPICS motor PV."""
|
||||
|
||||
def __init__(self, name, timeout=5.0):
|
||||
super().__init__(name.upper(), timeout=timeout)
|
||||
|
||||
@property
|
||||
def speed(self):
|
||||
"""Gets the current motor.slew_speed value"""
|
||||
return self.get('VELO')
|
||||
return self.get("VELO")
|
||||
|
||||
@speed.setter
|
||||
def speed(self, v):
|
||||
"""Sets the motor slew speed"""
|
||||
self.put('VELO', v)
|
||||
self.put("VELO", v)
|
||||
|
||||
@property
|
||||
def position(self):
|
||||
@@ -40,7 +41,7 @@ class MyMotor(Motor):
|
||||
@property
|
||||
def moving(self):
|
||||
"""Returns True if the motor is moving"""
|
||||
return bool(self.get('MOVN'))
|
||||
return bool(self.get("MOVN"))
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
@@ -50,14 +51,14 @@ class MyMotor(Motor):
|
||||
@property
|
||||
def limits(self):
|
||||
"""Returns (low_limit, high_limit)"""
|
||||
return self.get('HLM'), self.get('LLM')
|
||||
return self.get("HLM"), self.get("LLM")
|
||||
|
||||
@limits.setter
|
||||
def limits(self, limits):
|
||||
"""Sets (low_limit, high_limit)"""
|
||||
low, high = limits
|
||||
self.put('LLM', low)
|
||||
self.put('HLM', high)
|
||||
self.put("LLM", low)
|
||||
self.put("HLM", high)
|
||||
|
||||
def move_motor(self, val, relative=False, wait=False, timeout=300.0):
|
||||
"""
|
||||
@@ -70,12 +71,12 @@ class MyMotor(Motor):
|
||||
|
||||
return self.move(val, relative=relative, wait=wait, timeout=timeout)
|
||||
|
||||
def home(self, direction='forward', wait=False):
|
||||
def home(self, direction="forward", wait=False):
|
||||
"""
|
||||
Homes the motor.
|
||||
:param direction: 'forward' or 'reverse'
|
||||
"""
|
||||
field = 'HOMF' if direction == 'forward' else 'HOMR'
|
||||
field = "HOMF" if direction == "forward" else "HOMR"
|
||||
self.put(field, 1)
|
||||
if wait:
|
||||
self.wait_for_stop()
|
||||
@@ -95,6 +96,7 @@ class MyMotor(Motor):
|
||||
Asynchronous wait until the motor stops moving.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
start_time = time.time()
|
||||
while self.moving:
|
||||
await asyncio.sleep(poll_rate)
|
||||
|
||||
@@ -12,6 +12,7 @@ ResolverValue = Union[
|
||||
tuple[Callable[..., RawValue], tuple[Any, ...]], # (func, args) pattern you already use
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MoveResult:
|
||||
target: RawValue
|
||||
@@ -19,7 +20,15 @@ class MoveResult:
|
||||
|
||||
|
||||
class SetGetPV:
|
||||
def __init__(self, name: str, setpv: str, getpv: str, *, timeout: float = 60.0, tolerance: float | None = None):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
setpv: str,
|
||||
getpv: str,
|
||||
*,
|
||||
timeout: float = 60.0,
|
||||
tolerance: float | None = None,
|
||||
):
|
||||
self.name = name
|
||||
self.setpoint_pv = PV(setpv)
|
||||
self.readback_pv = PV(getpv)
|
||||
@@ -46,10 +55,18 @@ class SetGetPV:
|
||||
def wait(self, *, timeout: float | None = None):
|
||||
if self._last_target is None:
|
||||
return
|
||||
pv_wait(self.readback_pv, self._last_target, timeout=timeout or self.default_timeout, tolerance=self.tolerance)
|
||||
pv_wait(
|
||||
self.readback_pv,
|
||||
self._last_target,
|
||||
timeout=timeout or self.default_timeout,
|
||||
tolerance=self.tolerance,
|
||||
)
|
||||
|
||||
|
||||
class PredefinedPV(SetGetPV):
|
||||
def __init__(self, name: str, setpv: str, getpv: str, predefs: Mapping[str, ResolverValue], **kwargs):
|
||||
def __init__(
|
||||
self, name: str, setpv: str, getpv: str, predefs: Mapping[str, ResolverValue], **kwargs
|
||||
):
|
||||
super().__init__(name, setpv, getpv, **kwargs)
|
||||
self._predefs = dict(predefs)
|
||||
|
||||
|
||||
@@ -16,22 +16,16 @@ class SmargonMode(Enum):
|
||||
|
||||
|
||||
class Smargon(object):
|
||||
SMARGON_HOME = SmargonCoordinate(
|
||||
sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0
|
||||
)
|
||||
SMARGON_HOME = SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0)
|
||||
AERO_HOME = AerotechCoordinate(x=0, y=0, z=0, omega=0)
|
||||
|
||||
def __init__(self, bl: MXBeamline):
|
||||
if bl == MXBeamline.X06DA:
|
||||
self.__simulated = False
|
||||
self.__base = cfg_get(
|
||||
"daq.hardware.smargon_url", "http://x06da-smargopolo.psi.ch:3000"
|
||||
)
|
||||
self.__base = cfg_get("daq.hardware.smargon_url", "http://x06da-smargopolo.psi.ch:3000")
|
||||
elif bl == MXBeamline.X10SA:
|
||||
self.__simulated = False
|
||||
self.__base = cfg_get(
|
||||
"daq.hardware.smargon_url", "http://x10sa-smargopolo.psi.ch:3000"
|
||||
)
|
||||
self.__base = cfg_get("daq.hardware.smargon_url", "http://x10sa-smargopolo.psi.ch:3000")
|
||||
elif bl == MXBeamline.X06SA:
|
||||
raise NotImplementedError("Not implemented smargon url for X06SA")
|
||||
elif bl == MXBeamline.SIMULATED:
|
||||
@@ -158,8 +152,7 @@ class Smargon(object):
|
||||
|
||||
acs = self.gonget("readbackAEROTECH")
|
||||
return AerotechCoordinate(
|
||||
at_mm=Coordinate(x=acs["GMX"], y=acs["GMY"], z=acs["GMZ"]),
|
||||
omega_deg=acs["GMU"],
|
||||
at_mm=Coordinate(x=acs["GMX"], y=acs["GMY"], z=acs["GMZ"]), omega_deg=acs["GMU"]
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -199,8 +192,7 @@ class Smargon(object):
|
||||
|
||||
acs = self.gonget("targetAEROTECH") # targetAEROTECH, #targetOMEGA
|
||||
return AerotechCoordinate(
|
||||
at_mm=Coordinate(x=acs["GMX"], y=acs["GMY"], z=acs["GMZ"]),
|
||||
omega_deg=acs["GMU"],
|
||||
at_mm=Coordinate(x=acs["GMX"], y=acs["GMY"], z=acs["GMZ"]), omega_deg=acs["GMU"]
|
||||
)
|
||||
|
||||
@target_aerotech.setter
|
||||
|
||||
@@ -49,9 +49,7 @@ class TellBackend(Protocol):
|
||||
|
||||
def start_eval(self, expr: str) -> int: ...
|
||||
|
||||
def run(
|
||||
self, path: str, pars: list[str] | None = None, background: bool = False
|
||||
) -> None: ...
|
||||
def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None: ...
|
||||
|
||||
def abort(self) -> None: ...
|
||||
|
||||
@@ -88,9 +86,7 @@ class PShellTellBackend:
|
||||
def _resolve_url(bl: MXBeamline) -> str:
|
||||
beamline = bl.value.lower()
|
||||
if bl == MXBeamline.X06DA:
|
||||
return cfg_get(
|
||||
"daq.hardware.tell_url", f"http://{beamline}-tell.psi.ch:22222"
|
||||
)
|
||||
return cfg_get("daq.hardware.tell_url", f"http://{beamline}-tell.psi.ch:22222")
|
||||
if bl == MXBeamline.X10SA:
|
||||
return cfg_get("daq.hardware.tell_url", "http://PC17488:22222")
|
||||
if bl == MXBeamline.X06SA:
|
||||
@@ -124,11 +120,7 @@ class PShellTellBackend:
|
||||
if e.response is not None and e.response.text:
|
||||
msg = e.response.text.strip()
|
||||
raise TellCommunicationError(
|
||||
msg,
|
||||
base_url=self._url,
|
||||
endpoint=endpoint,
|
||||
operation=operation,
|
||||
critical=True,
|
||||
msg, base_url=self._url, endpoint=endpoint, operation=operation, critical=True
|
||||
) from e
|
||||
|
||||
def eval(self, expr: str):
|
||||
@@ -142,9 +134,7 @@ class PShellTellBackend:
|
||||
def start_eval(self, expr: str) -> int:
|
||||
return self._pshell.start_eval(expr)
|
||||
|
||||
def run(
|
||||
self, path: str, pars: list[str] | None = None, background: bool = False
|
||||
) -> None:
|
||||
def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None:
|
||||
self._pshell.run(path, pars=pars, background=background)
|
||||
|
||||
def abort(self) -> None:
|
||||
@@ -159,10 +149,7 @@ class SimTellBackend:
|
||||
self._mounted_sample = ""
|
||||
self._settings: dict[str, str] = {"mounted_sample_position": ""}
|
||||
self._results: dict[int, dict[str, Any]] = {}
|
||||
self._robot_status: dict[str, Any] = {
|
||||
"powered": True,
|
||||
"pos": POSITION_PARK,
|
||||
}
|
||||
self._robot_status: dict[str, Any] = {"powered": True, "pos": POSITION_PARK}
|
||||
self._current_mA = 30.0
|
||||
self._pin_offset = 0.0
|
||||
self._detected_pucks: list[dict[str, Any]] = []
|
||||
@@ -259,9 +246,7 @@ class SimTellBackend:
|
||||
return str(self._current_mA)
|
||||
|
||||
if expr.startswith("smart_magnet.set_current("):
|
||||
match = re.match(
|
||||
r"smart_magnet\.set_current\(([-+]?\d+(?:\.\d+)?)\)&", expr
|
||||
)
|
||||
match = re.match(r"smart_magnet\.set_current\(([-+]?\d+(?:\.\d+)?)\)&", expr)
|
||||
if match:
|
||||
self._current_mA = float(match.group(1))
|
||||
return None
|
||||
@@ -322,9 +307,7 @@ class SimTellBackend:
|
||||
self._set_ready_soon()
|
||||
return cmd_id
|
||||
|
||||
def run(
|
||||
self, path: str, pars: list[str] | None = None, background: bool = False
|
||||
) -> None:
|
||||
def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None:
|
||||
_ = background
|
||||
|
||||
if path == "data/set_samples_info" and pars:
|
||||
@@ -349,9 +332,7 @@ class SimTellBackend:
|
||||
|
||||
|
||||
class LazyTellBackend:
|
||||
def __init__(
|
||||
self, factory: Callable[[], TellBackend], *, retry_interval_s: float = 2.0
|
||||
):
|
||||
def __init__(self, factory: Callable[[], TellBackend], *, retry_interval_s: float = 2.0):
|
||||
self._factory = factory
|
||||
self._backend: TellBackend | None = None
|
||||
self._retry_interval_s = float(retry_interval_s)
|
||||
@@ -363,10 +344,7 @@ class LazyTellBackend:
|
||||
return self._backend
|
||||
|
||||
now = time.monotonic()
|
||||
if (
|
||||
now - self._last_attempt_ts < self._retry_interval_s
|
||||
and self._last_error is not None
|
||||
):
|
||||
if now - self._last_attempt_ts < self._retry_interval_s and self._last_error is not None:
|
||||
raise self._last_error
|
||||
|
||||
self._last_attempt_ts = now
|
||||
@@ -378,10 +356,7 @@ class LazyTellBackend:
|
||||
self._last_error = e
|
||||
raise
|
||||
except Exception as e:
|
||||
wrapped = TellCommunicationError(
|
||||
"TELL connection failed",
|
||||
operation="CONNECT",
|
||||
)
|
||||
wrapped = TellCommunicationError("TELL connection failed", operation="CONNECT")
|
||||
self._last_error = wrapped
|
||||
raise wrapped from e
|
||||
|
||||
@@ -410,9 +385,7 @@ class LazyTellBackend:
|
||||
def start_eval(self, expr: str) -> int:
|
||||
return self._get_backend().start_eval(expr)
|
||||
|
||||
def run(
|
||||
self, path: str, pars: list[str] | None = None, background: bool = False
|
||||
) -> None:
|
||||
def run(self, path: str, pars: list[str] | None = None, background: bool = False) -> None:
|
||||
self._get_backend().run(path, pars=pars, background=background)
|
||||
|
||||
def abort(self) -> None:
|
||||
|
||||
@@ -14,11 +14,7 @@ from aarecommon.errors.exception_handler import (
|
||||
TellConnectionException,
|
||||
)
|
||||
from aarecommon.models.beamline import MXBeamline
|
||||
from aarecommon.models.models import (
|
||||
DewarAddress,
|
||||
PuckLoadedInfo,
|
||||
SampleDewarAddress,
|
||||
)
|
||||
from aarecommon.models.models import DewarAddress, PuckLoadedInfo, SampleDewarAddress
|
||||
from aareDB import PuckWithTellPosition
|
||||
|
||||
from aare.devices.tell_backend import (
|
||||
@@ -150,8 +146,7 @@ class TellClient:
|
||||
|
||||
if reasons:
|
||||
raise TellCommunicationError(
|
||||
"Mount can't start: " + "; ".join(reasons),
|
||||
operation="mount_precheck",
|
||||
"Mount can't start: " + "; ".join(reasons), operation="mount_precheck"
|
||||
)
|
||||
|
||||
def validate_door_closed(self) -> None:
|
||||
@@ -162,13 +157,11 @@ class TellClient:
|
||||
door_closed = self.is_door_closed()
|
||||
except Exception as e:
|
||||
raise TellCommunicationError(
|
||||
f"Mount can't start: failed to check door status: {e}",
|
||||
operation="mount_precheck",
|
||||
f"Mount can't start: failed to check door status: {e}", operation="mount_precheck"
|
||||
)
|
||||
if not door_closed:
|
||||
raise TellCommunicationError(
|
||||
"Mount can't start: TELL doors are open",
|
||||
operation="mount_precheck",
|
||||
"Mount can't start: TELL doors are open", operation="mount_precheck"
|
||||
)
|
||||
|
||||
def set_samples_info(self, info: List[PuckWithTellPosition]):
|
||||
@@ -351,9 +344,7 @@ class TellClient:
|
||||
logger.info(f"{TellEventValueEnum.ROBOT_CLEAR_AFTER_MOUNT.value}")
|
||||
return TellEventValueEnum.ROBOT_CLEAR_AFTER_MOUNT
|
||||
else:
|
||||
logger.info(
|
||||
f"Unexpected event: {event} occurred with value: {value}"
|
||||
)
|
||||
logger.info(f"Unexpected event: {event} occurred with value: {value}")
|
||||
logger.info("Checking command completed okay anyway")
|
||||
self.check_command_ok(
|
||||
timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: "
|
||||
@@ -361,9 +352,7 @@ class TellClient:
|
||||
return TellEventValueEnum.UNKNOWN
|
||||
elif wait and segment == "X":
|
||||
logger.info("Loading an auxiliary puck")
|
||||
self.check_command_ok(
|
||||
timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: "
|
||||
)
|
||||
self.check_command_ok(timeout=wait_timeout, msg=f"Mount {segment}{puck}-{sample}: ")
|
||||
logger.info("post waiting")
|
||||
return TellEventValueEnum.SUCCESS
|
||||
except TellCommunicationError:
|
||||
@@ -371,8 +360,7 @@ class TellClient:
|
||||
except Exception as e:
|
||||
logger.error(f"Exception occurred: {e}")
|
||||
raise TellCommunicationError(
|
||||
message=f"Error during mount {segment}{puck}-{sample}: {e}",
|
||||
critical=True,
|
||||
message=f"Error during mount {segment}{puck}-{sample}: {e}", critical=True
|
||||
)
|
||||
|
||||
def unmount(self, force=False, wait=False, timeout=360.0):
|
||||
@@ -476,10 +464,9 @@ class TellClient:
|
||||
PuckLoadedInfo(
|
||||
puck_name=i["puckBarcode"],
|
||||
location=DewarAddress(
|
||||
segment=puck_address[0],
|
||||
pos=int(puck_address[1]),
|
||||
segment=puck_address[0], pos=int(puck_address[1])
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
return output
|
||||
|
||||
@@ -543,18 +530,14 @@ class TellClient:
|
||||
self.backend.eval("smart_magnet.set_supress(True)&")
|
||||
self.backend.eval("smart_magnet.state&")
|
||||
if self.get_mounted_sample() is None:
|
||||
logger.warning(
|
||||
"Check mount: A manually mounted sample is detected."
|
||||
)
|
||||
logger.warning("Check mount: A manually mounted sample is detected.")
|
||||
logger.warning("Remove before mounting with the robot.")
|
||||
raise ManualMountException
|
||||
return True
|
||||
elif state == "Ready":
|
||||
logger.debug("No sample detected, ready to mount")
|
||||
if self.get_mounted_sample():
|
||||
logger.error(
|
||||
"Check mount: No sample detected, but robot thinks is mounted"
|
||||
)
|
||||
logger.error("Check mount: No sample detected, but robot thinks is mounted")
|
||||
raise SmartMagnetFaultException
|
||||
return False
|
||||
elif state == "Paused":
|
||||
@@ -573,10 +556,7 @@ def make_tell_client(bl: MXBeamline) -> TellClient:
|
||||
if bl == MXBeamline.SIMULATED:
|
||||
backend = SimTellBackend()
|
||||
else:
|
||||
backend = LazyTellBackend(
|
||||
factory=lambda: PShellTellBackend(bl),
|
||||
retry_interval_s=2.0,
|
||||
)
|
||||
backend = LazyTellBackend(factory=lambda: PShellTellBackend(bl), retry_interval_s=2.0)
|
||||
return TellClient(bl, backend=backend)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ def wait_position(motor, target, tolerance=None, timeout=60.0):
|
||||
elif isinstance(position, (bytes, str)):
|
||||
tst = "'%s' == '%s'"
|
||||
if isinstance(position, bytes):
|
||||
ltst = lambda x, y, z: x.lower() == y.lower().encode() if isinstance(y, str) else x.lower() == y.lower()
|
||||
ltst = lambda x, y, z: (
|
||||
x.lower() == y.lower().encode() if isinstance(y, str) else x.lower() == y.lower()
|
||||
)
|
||||
else:
|
||||
ltst = lambda x, y, z: x.lower() == y.lower()
|
||||
else:
|
||||
@@ -29,7 +31,10 @@ def wait_position(motor, target, tolerance=None, timeout=60.0):
|
||||
n = n + 1
|
||||
if n > 20:
|
||||
n = 0
|
||||
print("waiting_position test: %s (%s, %s, %s)" % (tst, str(motor.readback), str(target), str(tolerance)))
|
||||
print(
|
||||
"waiting_position test: %s (%s, %s, %s)"
|
||||
% (tst, str(motor.readback), str(target), str(tolerance))
|
||||
)
|
||||
timeisup = timeout < time.time()
|
||||
condition = ltst(motor.readback, target, tolerance)
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@ logger = setup_logger("aareGUI")
|
||||
def auth(base_url: str | None, cert_path: str | None) -> str:
|
||||
curr_user = get_user()
|
||||
if base_url is None:
|
||||
token_data = TokenData(
|
||||
sub=curr_user, staff=True, session=15, pgroups=["p16371", "p22233"]
|
||||
)
|
||||
token_data = TokenData(sub=curr_user, staff=True, session=15, pgroups=["p16371", "p22233"])
|
||||
return jwt.encode(token_data.model_dump(), "ABC123")
|
||||
|
||||
# Single call: Kerberos SPNEGO through Apache, which proxies to the DAQ server.
|
||||
|
||||
+10
-30
@@ -47,12 +47,8 @@ def main():
|
||||
# TODO if zmq and pred stream come from same source, do not need images from both streams, can combine
|
||||
match mx_beamline():
|
||||
case MXBeamline.X06DA:
|
||||
default_url = cfg_get(
|
||||
"gui.daq.daq_url", "https://mx-x06da-queue-01.psi.ch"
|
||||
)
|
||||
default_cert_path = cfg_get(
|
||||
"gui.daq.cert_path", "/sls/x06da/misc/.cert/6d.crt"
|
||||
)
|
||||
default_url = cfg_get("gui.daq.daq_url", "https://mx-x06da-queue-01.psi.ch")
|
||||
default_cert_path = cfg_get("gui.daq.cert_path", "/sls/x06da/misc/.cert/6d.crt")
|
||||
default_zmq_addr = cfg_get(
|
||||
"gui.cameras.sample_camera_zmq_url", "tcp://x06da-pserv-01:9089"
|
||||
)
|
||||
@@ -67,12 +63,8 @@ def main():
|
||||
)
|
||||
default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 3))
|
||||
case MXBeamline.X10SA:
|
||||
default_url = cfg_get(
|
||||
"gui.daq.daq_url", "https://mx-x10sa-queue-01.psi.ch"
|
||||
)
|
||||
default_cert_path = cfg_get(
|
||||
"gui.daq.cert_path", "/sls/x10sa/misc/.cert/10s.crt"
|
||||
)
|
||||
default_url = cfg_get("gui.daq.daq_url", "https://mx-x10sa-queue-01.psi.ch")
|
||||
default_cert_path = cfg_get("gui.daq.cert_path", "/sls/x10sa/misc/.cert/10s.crt")
|
||||
default_zmq_addr = cfg_get(
|
||||
"gui.cameras.sample_camera_zmq_url", "tcp://x10sa-spark-01:9091"
|
||||
)
|
||||
@@ -87,17 +79,11 @@ def main():
|
||||
)
|
||||
default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 1))
|
||||
case MXBeamline.X06SA:
|
||||
default_url = cfg_get(
|
||||
"gui.daq.daq_url", "https://mx-x06sa-queue-01.psi.ch"
|
||||
)
|
||||
default_cert_path = cfg_get(
|
||||
"gui.daq.cert_path", "/sls/x06sa/misc/.cert/6s.crt"
|
||||
)
|
||||
default_url = cfg_get("gui.daq.daq_url", "https://mx-x06sa-queue-01.psi.ch")
|
||||
default_cert_path = cfg_get("gui.daq.cert_path", "/sls/x06sa/misc/.cert/6s.crt")
|
||||
default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "")
|
||||
default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "")
|
||||
default_beamline_cam_addr = cfg_get(
|
||||
"gui.cameras.beamline_camera_url", ""
|
||||
)
|
||||
default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "")
|
||||
default_gonio_cam_addr = cfg_get("gui.cameras.gonio_camera_url", "")
|
||||
default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 1))
|
||||
case _:
|
||||
@@ -105,16 +91,12 @@ def main():
|
||||
default_cert_path = cfg_get("gui.daq.cert_path", "")
|
||||
default_zmq_addr = cfg_get("gui.cameras.sample_camera_zmq_url", "")
|
||||
default_pred_zmq_addr = cfg_get("gui.cameras.prediction_zmq_url", "")
|
||||
default_beamline_cam_addr = cfg_get(
|
||||
"gui.cameras.beamline_camera_url", ""
|
||||
)
|
||||
default_beamline_cam_addr = cfg_get("gui.cameras.beamline_camera_url", "")
|
||||
default_gonio_cam_addr = cfg_get("gui.cameras.gonio_camera_url", "")
|
||||
default_gonio_camera_id = int(cfg_get("gui.cameras.gonio_camera_id", 1))
|
||||
|
||||
# Add custom options as needed
|
||||
urlOption = QCommandLineOption(
|
||||
["u", "aaredaq-url"], "Base AareDAQ URL", "url", default_url
|
||||
)
|
||||
urlOption = QCommandLineOption(["u", "aaredaq-url"], "Base AareDAQ URL", "url", default_url)
|
||||
parser.addOption(urlOption)
|
||||
|
||||
certPath = QCommandLineOption(
|
||||
@@ -126,9 +108,7 @@ def main():
|
||||
parser.addOption(certPath)
|
||||
|
||||
defaultImage = QCommandLineOption(
|
||||
["i", "image"],
|
||||
"Default image to display in absence of the ZMQ stream",
|
||||
"image",
|
||||
["i", "image"], "Default image to display in absence of the ZMQ stream", "image"
|
||||
)
|
||||
parser.addOption(defaultImage)
|
||||
|
||||
|
||||
+146
-454
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,7 @@ from PySide6.QtGui import QColor
|
||||
|
||||
class SmargonBookmark:
|
||||
coord: SmargonCoordinate
|
||||
color: Literal[
|
||||
"red", "green", "blue", "indigo", "lime"
|
||||
] # Restrict to specific colors
|
||||
color: Literal["red", "green", "blue", "indigo", "lime"] # Restrict to specific colors
|
||||
|
||||
def qt_color(self) -> QColor:
|
||||
"""Convert the color property to a QColor."""
|
||||
|
||||
@@ -21,7 +21,6 @@ class UIStateManager:
|
||||
|
||||
def save_model(self, key: str, model):
|
||||
try:
|
||||
|
||||
data = model.to_list()
|
||||
self.settings.setValue(key, json.dumps(data))
|
||||
|
||||
@@ -45,4 +44,4 @@ class UIStateManager:
|
||||
self.settings.setValue(key, value)
|
||||
|
||||
def load_value(self, key: str, default=None, value_type=None):
|
||||
return self.settings.value(key, default, type=value_type)
|
||||
return self.settings.value(key, default, type=value_type)
|
||||
|
||||
@@ -25,10 +25,7 @@ def get_entry(sample: SampleShortInfo, column: int, *, show_user: bool = False):
|
||||
|
||||
class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
samples: list[SampleShortInfo] | None = None,
|
||||
show_user: bool = False,
|
||||
self, parent=None, samples: list[SampleShortInfo] | None = None, show_user: bool = False
|
||||
):
|
||||
super().__init__(parent)
|
||||
if samples is None:
|
||||
@@ -37,9 +34,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
self._show_user = bool(show_user)
|
||||
self.samples: list[SampleShortInfo] = samples
|
||||
self.header = (
|
||||
["User", "Dewar", "Puck", "Sample"]
|
||||
if self._show_user
|
||||
else ["Dewar", "Puck", "Sample"]
|
||||
["User", "Dewar", "Puck", "Sample"] if self._show_user else ["Dewar", "Puck", "Sample"]
|
||||
)
|
||||
|
||||
def flags(self, index):
|
||||
@@ -54,9 +49,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
|
||||
def data(self, index, role=None):
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
return get_entry(
|
||||
self.samples[index.row()], index.column(), show_user=self._show_user
|
||||
)
|
||||
return get_entry(self.samples[index.row()], index.column(), show_user=self._show_user)
|
||||
elif role == Qt.ItemDataRole.TextAlignmentRole:
|
||||
return Qt.AlignmentFlag.AlignCenter
|
||||
elif role == Qt.ItemDataRole.BackgroundRole:
|
||||
@@ -76,10 +69,7 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
return str(section + 1)
|
||||
return None
|
||||
|
||||
def updateData(
|
||||
self,
|
||||
samples: list[SampleShortInfo],
|
||||
):
|
||||
def updateData(self, samples: list[SampleShortInfo]):
|
||||
self.beginResetModel()
|
||||
self.samples = list(samples)
|
||||
self.endResetModel()
|
||||
@@ -148,15 +138,11 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
self.endResetModel()
|
||||
|
||||
def to_state(self) -> dict:
|
||||
return {
|
||||
"samples": [s.model_dump() for s in self.samples],
|
||||
}
|
||||
return {"samples": [s.model_dump() for s in self.samples]}
|
||||
|
||||
def from_state(self, state: dict):
|
||||
self.beginResetModel()
|
||||
|
||||
self.samples = [
|
||||
SampleShortInfo.model_validate(d) for d in state.get("samples", [])
|
||||
]
|
||||
self.samples = [SampleShortInfo.model_validate(d) for d in state.get("samples", [])]
|
||||
|
||||
self.endResetModel()
|
||||
|
||||
@@ -304,9 +304,7 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
items = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))
|
||||
return [k for k, _ in items[:limit]]
|
||||
|
||||
def suggested_prefixes_for_location(
|
||||
self, limit: int = 200
|
||||
) -> tuple[list[str], list[str]]:
|
||||
def suggested_prefixes_for_location(self, limit: int = 200) -> tuple[list[str], list[str]]:
|
||||
"""Get location prefixes from currently filtered samples (excluding column 3 filter)."""
|
||||
# Get currently filtered samples, excluding the location filter
|
||||
temp_filter = self.__filters.pop(3, None)
|
||||
@@ -326,7 +324,5 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
if seg:
|
||||
seg_seen.add(seg)
|
||||
segs = sorted(seg_seen)
|
||||
segpos = sorted(
|
||||
segpos_seen, key=lambda x: (x[0], int(x[1:]) if x[1:].isdigit() else 0)
|
||||
)
|
||||
segpos = sorted(segpos_seen, key=lambda x: (x[0], int(x[1:]) if x[1:].isdigit() else 0))
|
||||
return (segs[:limit], segpos[:limit])
|
||||
|
||||
@@ -149,15 +149,9 @@ class RuntimeNotificationWidget(QFrame):
|
||||
timeout = 5000 if auto_clear_ms is None else int(auto_clear_ms)
|
||||
self._auto_clear_timer.start(timeout)
|
||||
|
||||
def show_waiting(
|
||||
self, *, title: str, message: str, auto_clear_ms: int | None = None
|
||||
) -> None:
|
||||
def show_waiting(self, *, title: str, message: str, auto_clear_ms: int | None = None) -> None:
|
||||
self.show_notification(
|
||||
title=title,
|
||||
message=message,
|
||||
level="warning",
|
||||
sticky=True,
|
||||
auto_clear_ms=auto_clear_ms,
|
||||
title=title, message=message, level="warning", sticky=True, auto_clear_ms=auto_clear_ms
|
||||
)
|
||||
|
||||
@Slot()
|
||||
@@ -225,11 +219,7 @@ class LogDock(QDockWidget):
|
||||
self.setVisible(True)
|
||||
self.raise_()
|
||||
self.notification.show_notification(
|
||||
title=title,
|
||||
message=message,
|
||||
level=level,
|
||||
sticky=sticky,
|
||||
auto_clear_ms=auto_clear_ms,
|
||||
title=title, message=message, level=level, sticky=sticky, auto_clear_ms=auto_clear_ms
|
||||
)
|
||||
|
||||
def show_waiting_notification(self, *, title: str, message: str) -> None:
|
||||
|
||||
@@ -5,17 +5,9 @@ import time
|
||||
from datetime import datetime
|
||||
|
||||
from aarecommon.config.logger import setup_logger
|
||||
from aarecommon.models.automation import (
|
||||
AutomationProgress,
|
||||
StepStatus,
|
||||
WorkflowStateKind,
|
||||
)
|
||||
from aarecommon.models.automation import AutomationProgress, StepStatus, WorkflowStateKind
|
||||
from PySide6.QtCore import QTimer, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QLabel,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QLabel, QVBoxLayout, QWidget
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
@@ -128,10 +120,7 @@ class AutomationProgressWidget(QWidget):
|
||||
)
|
||||
|
||||
if status == StepStatus.SUCCESS:
|
||||
return (
|
||||
base
|
||||
+ " background-color: #ECFDF3; color: #166534; border-color: #A7F3D0;"
|
||||
)
|
||||
return base + " background-color: #ECFDF3; color: #166534; border-color: #A7F3D0;"
|
||||
if status == StepStatus.RUNNING:
|
||||
return (
|
||||
base
|
||||
@@ -148,13 +137,8 @@ class AutomationProgressWidget(QWidget):
|
||||
+ " background-color: #FFF7ED; color: #C2410C; font-weight: 700; border-color: #FED7AA;"
|
||||
)
|
||||
if status == StepStatus.SKIPPED:
|
||||
return (
|
||||
base
|
||||
+ " background-color: #F8FAFC; color: #475569; border-color: #E2E8F0;"
|
||||
)
|
||||
return (
|
||||
base + " background-color: #F8FAFC; color: #64748B; border-color: #E2E8F0;"
|
||||
)
|
||||
return base + " background-color: #F8FAFC; color: #475569; border-color: #E2E8F0;"
|
||||
return base + " background-color: #F8FAFC; color: #64748B; border-color: #E2E8F0;"
|
||||
|
||||
@staticmethod
|
||||
def _format_duration(seconds: float | None) -> str:
|
||||
@@ -209,8 +193,7 @@ class AutomationProgressWidget(QWidget):
|
||||
progress = copy.deepcopy(self._progress)
|
||||
|
||||
final_step = next(
|
||||
(step for step in progress.steps if step.step == WorkflowStateKind.FINAL),
|
||||
None,
|
||||
(step for step in progress.steps if step.step == WorkflowStateKind.FINAL), None
|
||||
)
|
||||
|
||||
if self._is_paused:
|
||||
@@ -245,13 +228,9 @@ class AutomationProgressWidget(QWidget):
|
||||
|
||||
if self._stats_label:
|
||||
measured_avg_seconds = (
|
||||
progress.avg_time_per_sample
|
||||
if progress.avg_time_per_sample > 0
|
||||
else None
|
||||
)
|
||||
displayed_avg_seconds = (
|
||||
measured_avg_seconds or self.DEFAULT_SAMPLE_ESTIMATE_S
|
||||
progress.avg_time_per_sample if progress.avg_time_per_sample > 0 else None
|
||||
)
|
||||
displayed_avg_seconds = measured_avg_seconds or self.DEFAULT_SAMPLE_ESTIMATE_S
|
||||
|
||||
current_sample = progress.current_sample_name or "None"
|
||||
samples_left = (
|
||||
@@ -260,11 +239,7 @@ class AutomationProgressWidget(QWidget):
|
||||
else max(0, int(progress.samples_in_queue or 0))
|
||||
)
|
||||
|
||||
if (
|
||||
progress.finished
|
||||
or self._is_paused
|
||||
or not self._has_active_sample(progress)
|
||||
):
|
||||
if progress.finished or self._is_paused or not self._has_active_sample(progress):
|
||||
queue_remaining = displayed_avg_seconds * samples_left
|
||||
else:
|
||||
queue_remaining = displayed_avg_seconds * max(0, samples_left)
|
||||
@@ -291,9 +266,7 @@ class AutomationProgressWidget(QWidget):
|
||||
duration_str = ""
|
||||
if step_state.started_at is not None:
|
||||
end = step_state.completed_at or time.time()
|
||||
duration_str = (
|
||||
f" ({self._format_duration(end - step_state.started_at)})"
|
||||
)
|
||||
duration_str = f" ({self._format_duration(end - step_state.started_at)})"
|
||||
|
||||
if step_state.status == StepStatus.FAILED and step_state.message:
|
||||
message = f" — Failed: {step_state.message}"
|
||||
|
||||
@@ -80,12 +80,8 @@ class AxisVideoPanel(QWidget):
|
||||
else:
|
||||
self._status_label.setText(style.text)
|
||||
self._status_container.setProperty("busyState", "active")
|
||||
self._status_dot.setStyleSheet(
|
||||
f"background-color: {style.accent_dot};"
|
||||
)
|
||||
self._status_label.setStyleSheet(
|
||||
f"color: {style.badge_fg};"
|
||||
)
|
||||
self._status_dot.setStyleSheet(f"background-color: {style.accent_dot};")
|
||||
self._status_label.setStyleSheet(f"color: {style.badge_fg};")
|
||||
self._refresh_status_style()
|
||||
self._status_container.show()
|
||||
|
||||
@@ -94,4 +90,4 @@ class AxisVideoPanel(QWidget):
|
||||
view.set_busy_overlay_style(style)
|
||||
|
||||
def set_status_text(self, text: str) -> None:
|
||||
self._status_label.setText(text or "")
|
||||
self._status_label.setText(text or "")
|
||||
|
||||
@@ -15,7 +15,7 @@ from aare.gui.panels.zoom_panel import ZoomPanel
|
||||
class BeamlineControls(QFrame):
|
||||
set_width = 250
|
||||
|
||||
def __init__(self, parent=None, staff : bool = True):
|
||||
def __init__(self, parent=None, staff: bool = True):
|
||||
super().__init__(parent)
|
||||
self.setFixedWidth(self.set_width)
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
|
||||
@@ -28,8 +28,7 @@ class RecoveryPanel(QWidget):
|
||||
layout.setSpacing(8)
|
||||
|
||||
self._warning_primary = QLabel(
|
||||
"⚠ Recovery actions are staff-only and intentionally dangerous.",
|
||||
self,
|
||||
"⚠ Recovery actions are staff-only and intentionally dangerous.", self
|
||||
)
|
||||
self._warning_primary.setWordWrap(True)
|
||||
self._warning_primary.setStyleSheet(
|
||||
@@ -151,10 +150,7 @@ class RecoveryPanel(QWidget):
|
||||
|
||||
def _prompt_recovery_code(self, action_name: str) -> str | None:
|
||||
code, ok = QInputDialog.getText(
|
||||
self,
|
||||
action_name,
|
||||
"Enter recovery confirmation code:",
|
||||
QLineEdit.EchoMode.Password,
|
||||
self, action_name, "Enter recovery confirmation code:", QLineEdit.EchoMode.Password
|
||||
)
|
||||
if not ok:
|
||||
return None
|
||||
@@ -163,9 +159,7 @@ class RecoveryPanel(QWidget):
|
||||
|
||||
def _sample_appears_mounted(self) -> bool:
|
||||
try:
|
||||
return (
|
||||
self._last_status is not None and self._last_status.sample is not None
|
||||
)
|
||||
return self._last_status is not None and self._last_status.sample is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -188,15 +182,11 @@ class RecoveryPanel(QWidget):
|
||||
|
||||
self._free_beamline_btn.setEnabled(beamline_busy)
|
||||
self._free_beamline_btn.setToolTip(
|
||||
""
|
||||
if beamline_busy
|
||||
else "Disabled because beamline does not currently appear busy."
|
||||
"" if beamline_busy else "Disabled because beamline does not currently appear busy."
|
||||
)
|
||||
|
||||
self._resync_sample_btn.setEnabled(True)
|
||||
self._resync_sample_btn.setToolTip(
|
||||
"Force a one-shot sample reconciliation against TELL."
|
||||
)
|
||||
self._resync_sample_btn.setToolTip("Force a one-shot sample reconciliation against TELL.")
|
||||
|
||||
def _confirm(self, title: str, msg: str) -> bool:
|
||||
reply = QMessageBox.warning(
|
||||
@@ -229,8 +219,7 @@ class RecoveryPanel(QWidget):
|
||||
@Slot()
|
||||
def _free_beamline(self) -> None:
|
||||
if not self._confirm(
|
||||
"Free beamline",
|
||||
"This will clear the beamline busy flag.\n\nDo you want to continue?",
|
||||
"Free beamline", "This will clear the beamline busy flag.\n\nDo you want to continue?"
|
||||
):
|
||||
return
|
||||
code = self._prompt_recovery_code("Free beamline")
|
||||
|
||||
@@ -150,12 +150,7 @@ class BeamlineStatePanel(QFrame):
|
||||
"Sample centring and alignment mode",
|
||||
),
|
||||
StationSpec(
|
||||
BeamlineStateEnum.BeamLocation,
|
||||
"Beam location",
|
||||
54,
|
||||
284,
|
||||
True,
|
||||
"Beam location mode",
|
||||
BeamlineStateEnum.BeamLocation, "Beam location", 54, 284, True, "Beam location mode"
|
||||
),
|
||||
StationSpec(
|
||||
BeamlineStateEnum.BeamstopAlignment,
|
||||
@@ -190,12 +185,7 @@ class BeamlineStatePanel(QFrame):
|
||||
"Crystal snapshot mode",
|
||||
),
|
||||
StationSpec(
|
||||
BeamlineStateEnum.XrayFluorescence,
|
||||
"XRF",
|
||||
54,
|
||||
464,
|
||||
True,
|
||||
"X-ray fluorescence mode",
|
||||
BeamlineStateEnum.XrayFluorescence, "XRF", 54, 464, True, "X-ray fluorescence mode"
|
||||
),
|
||||
]
|
||||
|
||||
@@ -252,8 +242,7 @@ class BeamlineStatePanel(QFrame):
|
||||
return tuple(sorted((a, b), key=lambda state: state.value))
|
||||
|
||||
def _build_graph(
|
||||
self,
|
||||
segments: list[tuple[BeamlineStateEnum, BeamlineStateEnum]],
|
||||
self, segments: list[tuple[BeamlineStateEnum, BeamlineStateEnum]]
|
||||
) -> dict[BeamlineStateEnum, set[BeamlineStateEnum]]:
|
||||
graph: dict[BeamlineStateEnum, set[BeamlineStateEnum]] = {}
|
||||
for a, b in segments:
|
||||
@@ -265,9 +254,7 @@ class BeamlineStatePanel(QFrame):
|
||||
return {station.state: station for station in self._stations}
|
||||
|
||||
def _path_segments_between(
|
||||
self,
|
||||
start: BeamlineStateEnum | None,
|
||||
end: BeamlineStateEnum | None,
|
||||
self, start: BeamlineStateEnum | None, end: BeamlineStateEnum | None
|
||||
) -> set[tuple[BeamlineStateEnum, BeamlineStateEnum]]:
|
||||
if start is None or end is None:
|
||||
return set()
|
||||
@@ -317,9 +304,7 @@ class BeamlineStatePanel(QFrame):
|
||||
self._current_state == BeamlineStateEnum.Moving
|
||||
and self._pending_target_state is not None
|
||||
):
|
||||
return self._path_segments_between(
|
||||
self._last_stable_state, self._pending_target_state
|
||||
)
|
||||
return self._path_segments_between(self._last_stable_state, self._pending_target_state)
|
||||
|
||||
return set()
|
||||
|
||||
@@ -330,9 +315,7 @@ class BeamlineStatePanel(QFrame):
|
||||
widget.setFlat(True)
|
||||
widget.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
widget.clicked.connect(
|
||||
lambda _checked=False, state=station.state: self._emit_for_state(
|
||||
state
|
||||
)
|
||||
lambda _checked=False, state=station.state: self._emit_for_state(state)
|
||||
)
|
||||
else:
|
||||
widget = HoverableLabel(station.label, self)
|
||||
@@ -543,17 +526,14 @@ class BeamlineStatePanel(QFrame):
|
||||
return self._line_hover
|
||||
|
||||
current_path = self._path_segments_between(
|
||||
BeamlineStateEnum.DewarTransfer,
|
||||
self._last_stable_state or self._current_state,
|
||||
BeamlineStateEnum.DewarTransfer, self._last_stable_state or self._current_state
|
||||
)
|
||||
if segment in current_path:
|
||||
return self._line_current
|
||||
|
||||
return self._line_color
|
||||
|
||||
def _draw_segment(
|
||||
self, painter: QPainter, start: QPoint, end: QPoint, color: QColor
|
||||
) -> None:
|
||||
def _draw_segment(self, painter: QPainter, start: QPoint, end: QPoint, color: QColor) -> None:
|
||||
pen = QPen(color, 3)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
painter.setPen(pen)
|
||||
@@ -625,9 +605,7 @@ class BeamlineStatePanel(QFrame):
|
||||
tell_state = status.tell_state
|
||||
tell_text = f"Tell: {tell_state.activity.display_name()}"
|
||||
|
||||
tell_phase = (
|
||||
tell_state.phase.display_name() if tell_state.phase is not None else ""
|
||||
)
|
||||
tell_phase = tell_state.phase.display_name() if tell_state.phase is not None else ""
|
||||
tell_message = (tell_state.message or "").strip()
|
||||
|
||||
if tell_phase:
|
||||
@@ -637,12 +615,7 @@ class BeamlineStatePanel(QFrame):
|
||||
|
||||
if tell_state.activity.value == "error":
|
||||
tell_color = "red"
|
||||
elif tell_state.activity.value in {
|
||||
"mounting",
|
||||
"unmounting",
|
||||
"drying",
|
||||
"cooling",
|
||||
}:
|
||||
elif tell_state.activity.value in {"mounting", "unmounting", "drying", "cooling"}:
|
||||
tell_color = "orange"
|
||||
else:
|
||||
tell_color = "green"
|
||||
|
||||
@@ -103,9 +103,7 @@ class CompactAutomationPanel(QFrame):
|
||||
self.annotation_button = QToolButton(self)
|
||||
self.annotation_button.setObjectName("compactSecondaryButton")
|
||||
self.annotation_button.setText("Annotate")
|
||||
self.annotation_button.setPopupMode(
|
||||
QToolButton.ToolButtonPopupMode.InstantPopup
|
||||
)
|
||||
self.annotation_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
|
||||
|
||||
annotation_menu = QMenu(self.annotation_button)
|
||||
for label in ["Heart", "Thumbs Up", "Thumbs Down", "Eyes", "Scan Again"]:
|
||||
@@ -180,11 +178,7 @@ class CompactAutomationPanel(QFrame):
|
||||
layout.addWidget(title_label)
|
||||
layout.addWidget(value_label)
|
||||
|
||||
return {
|
||||
"frame": frame,
|
||||
"title": title_label,
|
||||
"value": value_label,
|
||||
}
|
||||
return {"frame": frame, "title": title_label, "value": value_label}
|
||||
|
||||
@Slot(bool)
|
||||
def set_running(self, running: bool) -> None:
|
||||
|
||||
@@ -2,12 +2,7 @@ from aarecommon.math.diffraction_geometry import DiffractionGeometry
|
||||
from aarecommon.math.sample_geometry import SampleGeometryModel
|
||||
from aarecommon.models.models import DAQStatusModel
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QPushButton,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
)
|
||||
from PySide6.QtWidgets import QFrame, QPushButton, QTabWidget, QVBoxLayout
|
||||
|
||||
from aare.gui.panels.file_path_panel import FilePathPanel
|
||||
from aare.gui.panels.fluorescence_data_collection import FluorescenceDataCollectionPanel
|
||||
@@ -45,9 +40,7 @@ class DataCollectionSettings(QFrame):
|
||||
)
|
||||
self.__tab_widget.addTab(self.raster, "Raster scan")
|
||||
|
||||
self.screening = RotationDataCollectionPanel(
|
||||
parent=self, diffraction=diffraction
|
||||
)
|
||||
self.screening = RotationDataCollectionPanel(parent=self, diffraction=diffraction)
|
||||
self.__tab_widget.addTab(self.screening, "Rotation")
|
||||
|
||||
self.simple = SimpleRotationSettingsPanel(parent=self)
|
||||
|
||||
@@ -57,9 +57,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
self._banner = QLabel(self)
|
||||
self._banner.setVisible(self._is_staff)
|
||||
self._banner.setWordWrap(True)
|
||||
self._banner.setTextInteractionFlags(
|
||||
Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
)
|
||||
self._banner.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
self._banner.setStyleSheet(
|
||||
"QLabel {"
|
||||
" background: #f6f6f6;"
|
||||
@@ -78,9 +76,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
top.addWidget(QLabel("Filter:", self))
|
||||
|
||||
self._filter = QLineEdit(self)
|
||||
self._filter.setPlaceholderText(
|
||||
"Type to filter (matches name, value, or help)…"
|
||||
)
|
||||
self._filter.setPlaceholderText("Type to filter (matches name, value, or help)…")
|
||||
self._filter.setClearButtonEnabled(True)
|
||||
self._filter.setMinimumHeight(28)
|
||||
self._filter.setStyleSheet(
|
||||
@@ -124,16 +120,12 @@ class DeveloperHelpDialog(QDialog):
|
||||
|
||||
self._open_gui_log_btn = QPushButton("Open GUI log", self)
|
||||
self._open_gui_log_btn.setVisible(self._is_staff)
|
||||
self._open_gui_log_btn.clicked.connect(
|
||||
lambda: self._open_log_file_for_logger("aareGUI")
|
||||
)
|
||||
self._open_gui_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareGUI"))
|
||||
top.addWidget(self._open_gui_log_btn)
|
||||
|
||||
self._open_daq_log_btn = QPushButton("Open DAQ log", self)
|
||||
self._open_daq_log_btn.setVisible(self._is_staff)
|
||||
self._open_daq_log_btn.clicked.connect(
|
||||
lambda: self._open_log_file_for_logger("aareDAQ")
|
||||
)
|
||||
self._open_daq_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareDAQ"))
|
||||
top.addWidget(self._open_daq_log_btn)
|
||||
|
||||
self._tabs = QTabWidget(self)
|
||||
@@ -144,9 +136,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
self._codes_table.setHorizontalHeaderLabels(["Name"])
|
||||
self._codes_table.setSortingEnabled(True)
|
||||
self._codes_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self._codes_table.setSelectionBehavior(
|
||||
QTableWidget.SelectionBehavior.SelectRows
|
||||
)
|
||||
self._codes_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self._codes_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
||||
self._codes_table.itemSelectionChanged.connect(self._update_code_details)
|
||||
self._codes_table.horizontalHeader().setStretchLastSection(True)
|
||||
@@ -154,11 +144,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
self._details_frame = QFrame(self)
|
||||
self._details_frame.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self._details_frame.setStyleSheet(
|
||||
"QFrame {"
|
||||
" background: #fafafa;"
|
||||
" border: 1px solid #d0d0d0;"
|
||||
" border-radius: 6px;"
|
||||
"}"
|
||||
"QFrame { background: #fafafa; border: 1px solid #d0d0d0; border-radius: 6px;}"
|
||||
)
|
||||
|
||||
details_layout = QVBoxLayout(self._details_frame)
|
||||
@@ -166,23 +152,17 @@ class DeveloperHelpDialog(QDialog):
|
||||
details_layout.setSpacing(8)
|
||||
|
||||
form = QFormLayout()
|
||||
form.setLabelAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
form.setLabelAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
details_layout.addLayout(form)
|
||||
|
||||
self._detail_name = QLabel("-", self)
|
||||
self._detail_name.setTextInteractionFlags(
|
||||
Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
)
|
||||
self._detail_name.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
form.addRow("Name:", self._detail_name)
|
||||
|
||||
value_row = QHBoxLayout()
|
||||
value_row.setSpacing(8)
|
||||
self._detail_value = QLabel("-", self)
|
||||
self._detail_value.setTextInteractionFlags(
|
||||
Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
)
|
||||
self._detail_value.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
self._copy_value_btn = QPushButton("Copy value", self)
|
||||
self._copy_value_btn.clicked.connect(self._copy_selected_value)
|
||||
value_row.addWidget(self._detail_value, 1)
|
||||
@@ -193,9 +173,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
|
||||
self._detail_help = QLabel("-", self)
|
||||
self._detail_help.setWordWrap(True)
|
||||
self._detail_help.setTextInteractionFlags(
|
||||
Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
)
|
||||
self._detail_help.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
self._detail_help.setStyleSheet(
|
||||
"QLabel {"
|
||||
" background: white;"
|
||||
@@ -219,9 +197,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
self._payload_summary = QLabel(self)
|
||||
self._payload_summary.setVisible(self._is_staff)
|
||||
self._payload_summary.setWordWrap(True)
|
||||
self._payload_summary.setTextInteractionFlags(
|
||||
Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
)
|
||||
self._payload_summary.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
|
||||
self._payload_text = QTextEdit(self)
|
||||
self._payload_text.setReadOnly(True)
|
||||
@@ -284,15 +260,9 @@ class DeveloperHelpDialog(QDialog):
|
||||
"Requested by",
|
||||
]
|
||||
)
|
||||
self._gui_sessions_table.setEditTriggers(
|
||||
QTableWidget.EditTrigger.NoEditTriggers
|
||||
)
|
||||
self._gui_sessions_table.setSelectionBehavior(
|
||||
QTableWidget.SelectionBehavior.SelectRows
|
||||
)
|
||||
self._gui_sessions_table.setSelectionMode(
|
||||
QTableWidget.SelectionMode.SingleSelection
|
||||
)
|
||||
self._gui_sessions_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self._gui_sessions_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self._gui_sessions_table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection)
|
||||
self._gui_sessions_table.setSortingEnabled(True)
|
||||
self._gui_sessions_table.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
@@ -305,9 +275,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
self._gui_close_grace_spin.setSuffix(" s")
|
||||
|
||||
self._gui_request_close_btn = QPushButton("Request Close", self)
|
||||
self._gui_request_close_btn.clicked.connect(
|
||||
self._request_close_selected_gui
|
||||
)
|
||||
self._gui_request_close_btn.clicked.connect(self._request_close_selected_gui)
|
||||
|
||||
self._gui_force_remove_btn = QPushButton("Force Remove", self)
|
||||
self._gui_force_remove_btn.clicked.connect(self._force_remove_selected_gui)
|
||||
@@ -424,9 +392,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
except Exception:
|
||||
pretty = str(p)
|
||||
blocks.append(f"#{i}\n{pretty}")
|
||||
self._payloads_text.setPlainText(
|
||||
"\n\n".join(blocks) if blocks else "(none captured yet)"
|
||||
)
|
||||
self._payloads_text.setPlainText("\n\n".join(blocks) if blocks else "(none captured yet)")
|
||||
|
||||
@Slot(str)
|
||||
def _append_error_log_line(self, line: str) -> None:
|
||||
@@ -445,10 +411,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
if isinstance(body, dict):
|
||||
code = body.get("code")
|
||||
msg = body.get("message")
|
||||
return (
|
||||
str(code) if code is not None else None,
|
||||
str(msg) if msg is not None else None,
|
||||
)
|
||||
return (str(code) if code is not None else None, str(msg) if msg is not None else None)
|
||||
return (None, None)
|
||||
|
||||
def _update_banner(self) -> None:
|
||||
@@ -520,11 +483,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
self._update_code_details()
|
||||
|
||||
# If enabled, re-try selection after the table content changes
|
||||
if (
|
||||
self._is_staff
|
||||
and self._always_highlight_last_error
|
||||
and not self._freeze_payload
|
||||
):
|
||||
if self._is_staff and self._always_highlight_last_error and not self._freeze_payload:
|
||||
self._select_code_from_last_error()
|
||||
|
||||
def _selected_code(self) -> tuple[str | None, str | None]:
|
||||
@@ -550,9 +509,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
|
||||
self._detail_name.setText(name)
|
||||
self._detail_value.setText(value)
|
||||
self._detail_help.setText(
|
||||
error_code_help(value) or "(no help text defined yet)"
|
||||
)
|
||||
self._detail_help.setText(error_code_help(value) or "(no help text defined yet)")
|
||||
self._copy_value_btn.setEnabled(True)
|
||||
|
||||
@Slot()
|
||||
@@ -599,9 +556,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
return
|
||||
|
||||
self._gui_sessions = [
|
||||
s
|
||||
if isinstance(s, OpenGuiSessionInfo)
|
||||
else OpenGuiSessionInfo.model_validate(s)
|
||||
s if isinstance(s, OpenGuiSessionInfo) else OpenGuiSessionInfo.model_validate(s)
|
||||
for s in sessions
|
||||
]
|
||||
self._gui_sessions_table.setSortingEnabled(False)
|
||||
@@ -647,9 +602,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
def _request_close_selected_gui(self) -> None:
|
||||
session_id = self._selected_gui_session_id()
|
||||
if session_id is None:
|
||||
QMessageBox.information(
|
||||
self, "Active GUIs", "Please select a GUI session first."
|
||||
)
|
||||
QMessageBox.information(self, "Active GUIs", "Please select a GUI session first.")
|
||||
return
|
||||
|
||||
grace_seconds = int(self._gui_close_grace_spin.value())
|
||||
@@ -668,9 +621,7 @@ class DeveloperHelpDialog(QDialog):
|
||||
def _force_remove_selected_gui(self) -> None:
|
||||
session_id = self._selected_gui_session_id()
|
||||
if session_id is None:
|
||||
QMessageBox.information(
|
||||
self, "Active GUIs", "Please select a GUI session first."
|
||||
)
|
||||
QMessageBox.information(self, "Active GUIs", "Please select a GUI session first.")
|
||||
return
|
||||
|
||||
answer = QMessageBox.warning(
|
||||
|
||||
@@ -27,13 +27,7 @@ class FaceDetectionPanel(QWidget):
|
||||
self.step_size = int(val)
|
||||
|
||||
self.fig = Figure(figsize=(5, 4))
|
||||
self.fig.subplots_adjust(
|
||||
left=0.18,
|
||||
right=0.97,
|
||||
bottom=0.10,
|
||||
top=0.95,
|
||||
hspace=0.45,
|
||||
)
|
||||
self.fig.subplots_adjust(left=0.18, right=0.97, bottom=0.10, top=0.95, hspace=0.45)
|
||||
self.canvas = FigureCanvas(self.fig)
|
||||
self.ax1 = self.fig.add_subplot(2, 1, 1)
|
||||
self.ax2 = self.fig.add_subplot(2, 1, 2)
|
||||
@@ -130,9 +124,7 @@ class FaceDetectionPanel(QWidget):
|
||||
height_fit = A + B * np.cos(C * np.deg2rad(ang_grid) - phi)
|
||||
self.ax1.plot(ang_grid, height_fit, color="tab:orange", label="Height fit")
|
||||
if "best_angle_deg" in hf:
|
||||
self.ax1.axvline(
|
||||
hf["best_angle_deg"], color="tab:orange", ls="--", alpha=0.6
|
||||
)
|
||||
self.ax1.axvline(hf["best_angle_deg"], color="tab:orange", ls="--", alpha=0.6)
|
||||
|
||||
af = data.get("area_fit", {}) or {}
|
||||
if {"A", "B", "phi_rad", "C"} <= af.keys():
|
||||
@@ -140,9 +132,7 @@ class FaceDetectionPanel(QWidget):
|
||||
area_fit = A2 + B2 * np.cos(C2 * np.deg2rad(ang_grid) - phi2)
|
||||
self.ax2.plot(ang_grid, area_fit, color="tab:red", label="Area fit")
|
||||
if "best_angle_deg" in af:
|
||||
self.ax2.axvline(
|
||||
af["best_angle_deg"], color="tab:red", ls="--", alpha=0.6
|
||||
)
|
||||
self.ax2.axvline(af["best_angle_deg"], color="tab:red", ls="--", alpha=0.6)
|
||||
|
||||
self.ax1.set_xlabel("Angle (deg)")
|
||||
self.ax1.set_ylabel("Height")
|
||||
|
||||
@@ -4,14 +4,7 @@ from pathlib import Path
|
||||
|
||||
from aarecommon.models.models import DAQStatusModel, SampleShortInfo
|
||||
from PySide6.QtCore import Qt, Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMessageBox,
|
||||
QSpinBox,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QGridLayout, QLabel, QLineEdit, QMessageBox, QSpinBox, QWidget
|
||||
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
@@ -203,19 +196,14 @@ class FilePathPanel(QWidget):
|
||||
self.__puck_pos = sample.pin
|
||||
self.run_number_edit.setValue(1)
|
||||
|
||||
if (
|
||||
sample.aaredb_params is not None
|
||||
and sample.aaredb_params.directory is not None
|
||||
):
|
||||
if sample.aaredb_params is not None and sample.aaredb_params.directory is not None:
|
||||
self.directory_edit.setText(f"{sample.aaredb_params.directory}")
|
||||
elif sample.location is not None:
|
||||
self.directory_edit.setText(
|
||||
f"{self.__formatted_date}/{self.__puck_name}/{self.__puck_pos:02d}"
|
||||
)
|
||||
else:
|
||||
self.directory_edit.setText(
|
||||
f"{self.__formatted_date}/manual/{self.__sample_name}"
|
||||
)
|
||||
self.directory_edit.setText(f"{self.__formatted_date}/manual/{self.__sample_name}")
|
||||
self.update_filename()
|
||||
|
||||
@Slot(DAQStatusModel)
|
||||
@@ -234,15 +222,9 @@ class FilePathPanel(QWidget):
|
||||
|
||||
def next_free_run_from(self, start_rn: int) -> tuple[int, str]:
|
||||
# Compute next free run number and updated base
|
||||
dir_name = self.directory_edit.text().replace(
|
||||
"{prefix}", self.file_prefix_edit.text()
|
||||
)
|
||||
dir_name = self.directory_edit.text().replace("{prefix}", self.file_prefix_edit.text())
|
||||
base = dir_name if dir_name.endswith("/") or dir_name == "" else dir_name + "/"
|
||||
base += (
|
||||
"run"
|
||||
if self.file_prefix_edit.text() == ""
|
||||
else self.file_prefix_edit.text()
|
||||
)
|
||||
base += "run" if self.file_prefix_edit.text() == "" else self.file_prefix_edit.text()
|
||||
|
||||
rn = start_rn
|
||||
while rn <= self.run_number_edit.maximum():
|
||||
|
||||
@@ -15,9 +15,7 @@ class FluorescenceDataCollectionPanel(QWidget):
|
||||
|
||||
# Beam transmission (0..1)
|
||||
lay.addWidget(QLabel("Beam transmission", self), 0, 0)
|
||||
self.transmission = NumberLineEdit(
|
||||
0.0, 1.0, decimals=4, default=0.1, parent=self
|
||||
)
|
||||
self.transmission = NumberLineEdit(0.0, 1.0, decimals=4, default=0.1, parent=self)
|
||||
lay.addWidget(self.transmission, 0, 1)
|
||||
|
||||
# Exposure time (seconds)
|
||||
@@ -27,9 +25,7 @@ class FluorescenceDataCollectionPanel(QWidget):
|
||||
lay.addWidget(QLabel("s", self), 1, 2)
|
||||
|
||||
# Accumulate checkbox (reverse of erase)
|
||||
self.accumulate_cb = QCheckBox(
|
||||
"Accumulate", self
|
||||
) # accumulate=True => erase=False
|
||||
self.accumulate_cb = QCheckBox("Accumulate", self) # accumulate=True => erase=False
|
||||
self.accumulate_cb.setChecked(False)
|
||||
lay.addWidget(self.accumulate_cb, 2, 0, 1, 3)
|
||||
|
||||
@@ -49,7 +45,5 @@ class FluorescenceDataCollectionPanel(QWidget):
|
||||
exp = float(self.exposure.value)
|
||||
erase = not self.accumulate_cb.isChecked()
|
||||
self.fluo_scan.emit(
|
||||
FluorescenceSpectrumParameterModel(
|
||||
acq_time_s=exp, transmission=t, erase=erase
|
||||
)
|
||||
FluorescenceSpectrumParameterModel(acq_time_s=exp, transmission=t, erase=erase)
|
||||
)
|
||||
|
||||
@@ -78,10 +78,7 @@ class FluorescencePanel(QWidget):
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
try:
|
||||
if (
|
||||
obj is self.chart_view.viewport()
|
||||
and event.type() == QEvent.Type.MouseMove
|
||||
):
|
||||
if obj is self.chart_view.viewport() and event.type() == QEvent.Type.MouseMove:
|
||||
pos = event.position() if hasattr(event, "position") else event.pos()
|
||||
p = QPointF(pos.x(), pos.y())
|
||||
plot = self.chart.plotArea()
|
||||
@@ -109,9 +106,7 @@ class FluorescencePanel(QWidget):
|
||||
best_i = i
|
||||
|
||||
pt = self.series.at(best_i)
|
||||
self.chart_view.setToolTip(
|
||||
f"Energy {pt.x():.3f} keV counts {pt.y():.3f}"
|
||||
)
|
||||
self.chart_view.setToolTip(f"Energy {pt.x():.3f} keV counts {pt.y():.3f}")
|
||||
self._update_vline(pt.x())
|
||||
return False
|
||||
except Exception as e:
|
||||
@@ -134,11 +129,7 @@ class FluorescencePanel(QWidget):
|
||||
# Prefer cached arrays; fall back to reading from series
|
||||
x_vals = self._last_x_keV
|
||||
y_vals = self._last_y_counts
|
||||
if (
|
||||
x_vals is None
|
||||
or y_vals is None
|
||||
or len(x_vals) != self.series.count()
|
||||
):
|
||||
if x_vals is None or y_vals is None or len(x_vals) != self.series.count():
|
||||
x_vals = [self.series.at(i).x() for i in range(self.series.count())]
|
||||
y_vals = [self.series.at(i).y() for i in range(self.series.count())]
|
||||
|
||||
@@ -170,9 +161,9 @@ class FluorescencePanel(QWidget):
|
||||
try:
|
||||
ymin = self.axis_y.min()
|
||||
ymax = self.axis_y.max()
|
||||
self._vline.replace(
|
||||
0, x_val, ymin
|
||||
) if self._vline.count() > 0 else self._vline.append(x_val, ymin)
|
||||
self._vline.replace(0, x_val, ymin) if self._vline.count() > 0 else self._vline.append(
|
||||
x_val, ymin
|
||||
)
|
||||
if self._vline.count() == 1:
|
||||
self._vline.append(x_val, ymax)
|
||||
else:
|
||||
|
||||
@@ -20,9 +20,7 @@ class IlluminationPanel(QWidget):
|
||||
grid_layout.addWidget(front_label, 1, 0, 1, 2)
|
||||
self.is_sliding = False
|
||||
|
||||
self.front_light_slider = QSlider(
|
||||
orientation=Qt.Orientation.Horizontal, parent=self
|
||||
)
|
||||
self.front_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self)
|
||||
self.front_light_slider.setRange(0, 100)
|
||||
self.front_light_slider.sliderPressed.connect(self.on_slider_pressed)
|
||||
self.front_light_slider.sliderReleased.connect(self.on_front_slider_released)
|
||||
@@ -32,9 +30,7 @@ class IlluminationPanel(QWidget):
|
||||
back_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
grid_layout.addWidget(back_label, 3, 0, 1, 2)
|
||||
|
||||
self.back_light_slider = QSlider(
|
||||
orientation=Qt.Orientation.Horizontal, parent=self
|
||||
)
|
||||
self.back_light_slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self)
|
||||
self.back_light_slider.setRange(0, 100)
|
||||
self.back_light_slider.sliderPressed.connect(self.on_slider_pressed)
|
||||
self.back_light_slider.sliderReleased.connect(self.on_back_slider_released)
|
||||
|
||||
@@ -98,8 +98,7 @@ class LocalContactPanel(QFrame):
|
||||
layout.addWidget(TitleLabel("Local Contact", parent=self))
|
||||
|
||||
self._info_label = QLabel(
|
||||
"Staff tools for beamline recovery and local-contact operations.",
|
||||
self,
|
||||
"Staff tools for beamline recovery and local-contact operations.", self
|
||||
)
|
||||
self._info_label.setWordWrap(True)
|
||||
layout.addWidget(self._info_label)
|
||||
@@ -141,16 +140,12 @@ class LocalContactPanel(QFrame):
|
||||
self._tabs.addTab(self._build_detector_tab(), self.TAB_DETECTOR)
|
||||
self._tabs.addTab(self._build_config_tab(), self.TAB_CONFIG)
|
||||
|
||||
self._daq.local_contact_simulation_state_loaded.connect(
|
||||
self._apply_simulation_state
|
||||
)
|
||||
self._daq.local_contact_simulation_state_loaded.connect(self._apply_simulation_state)
|
||||
self._daq.local_contact_device_state_loaded.connect(self._apply_device_state)
|
||||
self._daq.local_contact_links_loaded.connect(self._apply_links)
|
||||
self._daq.local_contact_config_loaded.connect(self._apply_local_contact_config)
|
||||
self._daq.local_contact_config_saved.connect(self._apply_local_contact_config)
|
||||
self._daq.local_contact_transfer_error.connect(
|
||||
self._show_local_contact_transfer_error
|
||||
)
|
||||
self._daq.local_contact_transfer_error.connect(self._show_local_contact_transfer_error)
|
||||
self._daq.bec_user_macros_loaded.connect(self._show_bec_user_macros)
|
||||
self._daq.bec_devices_loaded.connect(self._show_bec_devices)
|
||||
self._daq.update.connect(self._update_from_status)
|
||||
@@ -162,17 +157,12 @@ class LocalContactPanel(QFrame):
|
||||
|
||||
def set_active_tab(self, tab_name: str) -> None:
|
||||
for index in range(self._tabs.count()):
|
||||
if (
|
||||
self._tabs.tabText(index).strip().lower()
|
||||
== str(tab_name).strip().lower()
|
||||
):
|
||||
if self._tabs.tabText(index).strip().lower() == str(tab_name).strip().lower():
|
||||
self._tabs.setCurrentIndex(index)
|
||||
return
|
||||
logger.warning(f"Unknown Local Contact tab requested: {tab_name}")
|
||||
|
||||
def _register_status_widget(
|
||||
self, widget: LocalContactStatusWidget
|
||||
) -> LocalContactStatusWidget:
|
||||
def _register_status_widget(self, widget: LocalContactStatusWidget) -> LocalContactStatusWidget:
|
||||
self._status_widgets.append(widget)
|
||||
if self._last_status is not None:
|
||||
widget.set_daq_status(self._last_status)
|
||||
@@ -181,17 +171,10 @@ class LocalContactPanel(QFrame):
|
||||
return widget
|
||||
|
||||
def _make_status_widget(
|
||||
self,
|
||||
*,
|
||||
title: str,
|
||||
summary: str,
|
||||
fields: tuple[str, ...],
|
||||
self, *, title: str, summary: str, fields: tuple[str, ...]
|
||||
) -> LocalContactStatusWidget:
|
||||
widget = LocalContactStatusWidget(
|
||||
title=title,
|
||||
visible_fields=fields,
|
||||
summary=summary,
|
||||
parent=self,
|
||||
title=title, visible_fields=fields, summary=summary, parent=self
|
||||
)
|
||||
return self._register_status_widget(widget)
|
||||
|
||||
@@ -263,13 +246,7 @@ class LocalContactPanel(QFrame):
|
||||
self._make_status_widget(
|
||||
title="Recovery status",
|
||||
summary="Recovery-relevant DAQ state.",
|
||||
fields=(
|
||||
"beamline_state",
|
||||
"busy",
|
||||
"sample",
|
||||
"tell_connected",
|
||||
"tell_state",
|
||||
),
|
||||
fields=("beamline_state", "busy", "sample", "tell_connected", "tell_state"),
|
||||
)
|
||||
)
|
||||
layout.addWidget(RecoveryPanel(daq=self._daq, parent=tab), 1)
|
||||
@@ -303,37 +280,25 @@ class LocalContactPanel(QFrame):
|
||||
row += 1
|
||||
|
||||
grid.addWidget(
|
||||
self._make_button(
|
||||
"Unmount", self._daq.unmount, "Requesting sample unmount."
|
||||
),
|
||||
row,
|
||||
0,
|
||||
)
|
||||
grid.addWidget(
|
||||
self._make_button("Dry", self._daq.tell_dry, "Requesting TELL dry."), row, 1
|
||||
self._make_button("Unmount", self._daq.unmount, "Requesting sample unmount."), row, 0
|
||||
)
|
||||
grid.addWidget(self._make_button("Dry", self._daq.tell_dry, "Requesting TELL dry."), row, 1)
|
||||
row += 1
|
||||
|
||||
grid.addWidget(
|
||||
self._make_button(
|
||||
"Park and dry", self._daq.park_and_dry, "Requesting park and dry."
|
||||
),
|
||||
self._make_button("Park and dry", self._daq.park_and_dry, "Requesting park and dry."),
|
||||
row,
|
||||
0,
|
||||
)
|
||||
grid.addWidget(
|
||||
self._make_button(
|
||||
"Toggle blower", self._daq.tell_toggle_blower, "Toggling blower."
|
||||
),
|
||||
self._make_button("Toggle blower", self._daq.tell_toggle_blower, "Toggling blower."),
|
||||
row,
|
||||
1,
|
||||
)
|
||||
row += 1
|
||||
|
||||
grid.addWidget(self._make_button("Anneal", self._anneal_from_dialog), row, 0)
|
||||
grid.addWidget(
|
||||
self._make_button("TELL access info", self._show_tell_access_info), row, 1
|
||||
)
|
||||
grid.addWidget(self._make_button("TELL access info", self._show_tell_access_info), row, 1)
|
||||
|
||||
wrapper = QGroupBox("TELL actions", tab)
|
||||
wrapper_layout = QVBoxLayout(wrapper)
|
||||
@@ -356,14 +321,7 @@ class LocalContactPanel(QFrame):
|
||||
self._make_status_widget(
|
||||
title="BEC status",
|
||||
summary="BEC-focused state and related backend status.",
|
||||
fields=(
|
||||
"bec",
|
||||
"busy",
|
||||
"beamline_state",
|
||||
"detector",
|
||||
"aerotech",
|
||||
"smargon",
|
||||
),
|
||||
fields=("bec", "busy", "beamline_state", "detector", "aerotech", "smargon"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -376,9 +334,7 @@ class LocalContactPanel(QFrame):
|
||||
|
||||
tools_layout.addWidget(
|
||||
self._make_button(
|
||||
"Load BEC user macros",
|
||||
self._daq.bec_load_user_macros,
|
||||
"Loading BEC user macros.",
|
||||
"Load BEC user macros", self._daq.bec_load_user_macros, "Loading BEC user macros."
|
||||
)
|
||||
)
|
||||
tools_layout.addWidget(
|
||||
@@ -390,9 +346,7 @@ class LocalContactPanel(QFrame):
|
||||
)
|
||||
tools_layout.addWidget(
|
||||
self._make_button(
|
||||
"Show BEC position devices",
|
||||
self._daq.bec_list_all_devices,
|
||||
"Listing BEC devices.",
|
||||
"Show BEC position devices", self._daq.bec_list_all_devices, "Listing BEC devices."
|
||||
)
|
||||
)
|
||||
tools_layout.addWidget(
|
||||
@@ -445,15 +399,7 @@ class LocalContactPanel(QFrame):
|
||||
self._make_status_widget(
|
||||
title="Hardware status",
|
||||
summary="Hardware-related backend and beamline status.",
|
||||
fields=(
|
||||
"aerotech",
|
||||
"smargon",
|
||||
"detector",
|
||||
"bec",
|
||||
"tell",
|
||||
"busy",
|
||||
"beamline_state",
|
||||
),
|
||||
fields=("aerotech", "smargon", "detector", "bec", "tell", "busy", "beamline_state"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -508,9 +454,7 @@ class LocalContactPanel(QFrame):
|
||||
"Initialising detector.",
|
||||
),
|
||||
self._make_button(
|
||||
"Initialise Smargon",
|
||||
self._daq.initialise_smargon,
|
||||
"Initialising Smargon.",
|
||||
"Initialise Smargon", self._daq.initialise_smargon, "Initialising Smargon."
|
||||
),
|
||||
self._make_button(
|
||||
"Initialise Aerotech",
|
||||
@@ -555,18 +499,14 @@ class LocalContactPanel(QFrame):
|
||||
|
||||
grid.addWidget(
|
||||
self._make_button(
|
||||
"Cancel current state",
|
||||
self._daq.cancel,
|
||||
"Cancelling current detector/scan state.",
|
||||
"Cancel current state", self._daq.cancel, "Cancelling current detector/scan state."
|
||||
),
|
||||
row,
|
||||
0,
|
||||
)
|
||||
grid.addWidget(
|
||||
self._make_button(
|
||||
"Take pedestal",
|
||||
self._daq.detector_take_pedestal,
|
||||
"Requesting detector pedestal.",
|
||||
"Take pedestal", self._daq.detector_take_pedestal, "Requesting detector pedestal."
|
||||
),
|
||||
row,
|
||||
1,
|
||||
@@ -596,10 +536,7 @@ class LocalContactPanel(QFrame):
|
||||
layout.setContentsMargins(6, 6, 6, 6)
|
||||
layout.setSpacing(8)
|
||||
|
||||
description = QLabel(
|
||||
"Staff-only tuning values used by automation and recovery flows.",
|
||||
tab,
|
||||
)
|
||||
description = QLabel("Staff-only tuning values used by automation and recovery flows.", tab)
|
||||
description.setWordWrap(True)
|
||||
layout.addWidget(description)
|
||||
|
||||
@@ -622,12 +559,8 @@ class LocalContactPanel(QFrame):
|
||||
form_layout.addWidget(label, 0, 0)
|
||||
form_layout.addWidget(self._mount_to_center_sleep_s, 0, 1)
|
||||
|
||||
loop_face_padding_label = QLabel(
|
||||
"Line scan loop_face Y padding / side", form_box
|
||||
)
|
||||
self._line_scan_loop_face_y_padding_fraction_each_side = QDoubleSpinBox(
|
||||
form_box
|
||||
)
|
||||
loop_face_padding_label = QLabel("Line scan loop_face Y padding / side", form_box)
|
||||
self._line_scan_loop_face_y_padding_fraction_each_side = QDoubleSpinBox(form_box)
|
||||
self._line_scan_loop_face_y_padding_fraction_each_side.setRange(0.0, 5.0)
|
||||
self._line_scan_loop_face_y_padding_fraction_each_side.setDecimals(2)
|
||||
self._line_scan_loop_face_y_padding_fraction_each_side.setSingleStep(0.05)
|
||||
@@ -647,13 +580,9 @@ class LocalContactPanel(QFrame):
|
||||
)
|
||||
|
||||
form_layout.addWidget(loop_face_padding_label, 1, 0)
|
||||
form_layout.addWidget(
|
||||
self._line_scan_loop_face_y_padding_fraction_each_side, 1, 1
|
||||
)
|
||||
form_layout.addWidget(self._line_scan_loop_face_y_padding_fraction_each_side, 1, 1)
|
||||
form_layout.addWidget(loop_all_padding_label, 2, 0)
|
||||
form_layout.addWidget(
|
||||
self._line_scan_loop_all_y_padding_fraction_each_side, 2, 1
|
||||
)
|
||||
form_layout.addWidget(self._line_scan_loop_all_y_padding_fraction_each_side, 2, 1)
|
||||
|
||||
button_row = QWidget(tab)
|
||||
button_layout = QHBoxLayout(button_row)
|
||||
@@ -760,12 +689,9 @@ class LocalContactPanel(QFrame):
|
||||
@Slot(list)
|
||||
def _show_bec_user_macros(self, items: list) -> None:
|
||||
if self._bec_macros_dialog is None:
|
||||
self._bec_macros_dialog = TextListDialog(
|
||||
title="BEC user macros", parent=self
|
||||
)
|
||||
self._bec_macros_dialog = TextListDialog(title="BEC user macros", parent=self)
|
||||
self._bec_macros_dialog.set_items(
|
||||
[str(item) for item in items],
|
||||
empty_message="No BEC user macros found.",
|
||||
[str(item) for item in items], empty_message="No BEC user macros found."
|
||||
)
|
||||
self._bec_macros_dialog.show()
|
||||
self._bec_macros_dialog.raise_()
|
||||
@@ -774,12 +700,9 @@ class LocalContactPanel(QFrame):
|
||||
@Slot(list)
|
||||
def _show_bec_devices(self, items: list) -> None:
|
||||
if self._bec_devices_dialog is None:
|
||||
self._bec_devices_dialog = TextListDialog(
|
||||
title="BEC position devices", parent=self
|
||||
)
|
||||
self._bec_devices_dialog = TextListDialog(title="BEC position devices", parent=self)
|
||||
self._bec_devices_dialog.set_items(
|
||||
[str(item) for item in items],
|
||||
empty_message="No BEC devices found.",
|
||||
[str(item) for item in items], empty_message="No BEC devices found."
|
||||
)
|
||||
self._bec_devices_dialog.show()
|
||||
self._bec_devices_dialog.raise_()
|
||||
@@ -797,9 +720,7 @@ class LocalContactPanel(QFrame):
|
||||
url = self._links_payload.get(key)
|
||||
if not url:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Link unavailable",
|
||||
f"No configured link is available for {key}.",
|
||||
self, "Link unavailable", f"No configured link is available for {key}."
|
||||
)
|
||||
return
|
||||
QDesktopServices.openUrl(QUrl(str(url)))
|
||||
@@ -807,37 +728,23 @@ class LocalContactPanel(QFrame):
|
||||
@Slot()
|
||||
def _show_tell_access_info(self) -> None:
|
||||
message = (
|
||||
self._links_payload.get("tell_hint")
|
||||
or "Please check TELL status via Remmina / VNC."
|
||||
self._links_payload.get("tell_hint") or "Please check TELL status via Remmina / VNC."
|
||||
)
|
||||
QMessageBox.information(self, "TELL access", str(message))
|
||||
|
||||
def _make_button(
|
||||
self,
|
||||
text: str,
|
||||
callback: Callable[[], None],
|
||||
log_message: str | None = None,
|
||||
self, text: str, callback: Callable[[], None], log_message: str | None = None
|
||||
) -> QPushButton:
|
||||
button = QPushButton(text, self)
|
||||
if log_message is None:
|
||||
button.clicked.connect(callback)
|
||||
else:
|
||||
button.clicked.connect(
|
||||
lambda: self._run_logged_action(log_message, callback)
|
||||
)
|
||||
button.clicked.connect(lambda: self._run_logged_action(log_message, callback))
|
||||
return button
|
||||
|
||||
@Slot()
|
||||
def _anneal_from_dialog(self) -> None:
|
||||
seconds, ok = QInputDialog.getDouble(
|
||||
self,
|
||||
"Anneal",
|
||||
"Anneal time (s):",
|
||||
1.0,
|
||||
0.1,
|
||||
60.0,
|
||||
1,
|
||||
)
|
||||
seconds, ok = QInputDialog.getDouble(self, "Anneal", "Anneal time (s):", 1.0, 0.1, 60.0, 1)
|
||||
if not ok:
|
||||
return
|
||||
self._daq.anneal(seconds)
|
||||
@@ -853,9 +760,7 @@ class LocalContactPanel(QFrame):
|
||||
|
||||
self._mount_to_center_sleep_s.blockSignals(True)
|
||||
self._mount_to_center_sleep_s.setValue(
|
||||
float(
|
||||
self._local_contact_config_payload.get("mount_to_center_sleep_s", 0.0)
|
||||
)
|
||||
float(self._local_contact_config_payload.get("mount_to_center_sleep_s", 0.0))
|
||||
)
|
||||
self._mount_to_center_sleep_s.blockSignals(False)
|
||||
|
||||
@@ -863,8 +768,7 @@ class LocalContactPanel(QFrame):
|
||||
self._line_scan_loop_face_y_padding_fraction_each_side.setValue(
|
||||
float(
|
||||
self._local_contact_config_payload.get(
|
||||
"line_scan_loop_face_y_padding_fraction_each_side",
|
||||
0.5,
|
||||
"line_scan_loop_face_y_padding_fraction_each_side", 0.5
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -874,8 +778,7 @@ class LocalContactPanel(QFrame):
|
||||
self._line_scan_loop_all_y_padding_fraction_each_side.setValue(
|
||||
float(
|
||||
self._local_contact_config_payload.get(
|
||||
"line_scan_loop_all_y_padding_fraction_each_side",
|
||||
0.5,
|
||||
"line_scan_loop_all_y_padding_fraction_each_side", 0.5
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
from aarecommon.models.models import DAQStatusModel, SampleShortInfo
|
||||
from aareDB import DataCollectionParameters
|
||||
from PySide6.QtCore import Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QCheckBox,
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QCheckBox, QGridLayout, QLabel, QLineEdit, QPushButton, QWidget
|
||||
|
||||
from aare.gui.widgets.number_line_edit import NumberLineEdit
|
||||
from aare.gui.widgets.title_label import TitleLabel
|
||||
|
||||
@@ -3,20 +3,9 @@ from __future__ import annotations
|
||||
import math
|
||||
|
||||
from aarecommon.config.logger import setup_logger
|
||||
from aarecommon.models.automation import (
|
||||
AutomationProgress,
|
||||
StepStatus,
|
||||
WorkflowStateKind,
|
||||
)
|
||||
from aarecommon.models.automation import AutomationProgress, StepStatus, WorkflowStateKind
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt, QTimer, Signal, Slot
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
QFontMetrics,
|
||||
QLinearGradient,
|
||||
QPainter,
|
||||
QPen,
|
||||
)
|
||||
from PySide6.QtGui import QColor, QFont, QFontMetrics, QLinearGradient, QPainter, QPen
|
||||
from PySide6.QtWidgets import (
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
@@ -109,10 +98,7 @@ class LEDStages(QWidget):
|
||||
line_color = QColor(ACCENT) if i < self._active else QColor(LED_OFF)
|
||||
pen = QPen(line_color, 2)
|
||||
p.setPen(pen)
|
||||
p.drawLine(
|
||||
QPointF(cx + led_r + 3, cy),
|
||||
QPointF(next_cx - led_r - 3, cy),
|
||||
)
|
||||
p.drawLine(QPointF(cx + led_r + 3, cy), QPointF(next_cx - led_r - 3, cy))
|
||||
|
||||
p.setPen(Qt.NoPen)
|
||||
if i < self._active:
|
||||
@@ -144,9 +130,7 @@ class LEDStages(QWidget):
|
||||
fm = QFontMetrics(font)
|
||||
text_w = fm.horizontalAdvance(name)
|
||||
p.drawText(
|
||||
QRectF(cx - text_w / 2 - 4, 0, text_w + 8, label_y + 2),
|
||||
Qt.AlignCenter,
|
||||
name,
|
||||
QRectF(cx - text_w / 2 - 4, 0, text_w + 8, label_y + 2), Qt.AlignCenter, name
|
||||
)
|
||||
|
||||
|
||||
@@ -197,12 +181,7 @@ class PlayPauseButton(QPushButton):
|
||||
# ---------------------------------------------------------------------------
|
||||
class QueueItemCard(QFrame):
|
||||
def __init__(
|
||||
self,
|
||||
index: int | str,
|
||||
title: str,
|
||||
subtitle: str,
|
||||
is_next: bool = False,
|
||||
parent=None,
|
||||
self, index: int | str, title: str, subtitle: str, is_next: bool = False, parent=None
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.setFixedHeight(72)
|
||||
@@ -245,9 +224,7 @@ class QueueItemCard(QFrame):
|
||||
)
|
||||
title_lbl.setWordWrap(False)
|
||||
sub_lbl = QLabel(subtitle)
|
||||
sub_lbl.setStyleSheet(
|
||||
f"color: {SUBTEXT}; font-size: 11px; background: transparent;"
|
||||
)
|
||||
sub_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 11px; background: transparent;")
|
||||
text_col.addWidget(title_lbl)
|
||||
text_col.addWidget(sub_lbl)
|
||||
layout.addLayout(text_col, stretch=1)
|
||||
@@ -350,9 +327,7 @@ class PortraitModePanel(QWidget):
|
||||
|
||||
# Camera card — wraps the real compact_sample_camera
|
||||
cam_card = QFrame()
|
||||
cam_card.setStyleSheet(
|
||||
f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}"
|
||||
)
|
||||
cam_card.setStyleSheet(f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}")
|
||||
cam_card_layout = QVBoxLayout(cam_card)
|
||||
cam_card_layout.setContentsMargins(4, 4, 4, 4)
|
||||
cam_card_layout.setSpacing(0)
|
||||
@@ -363,9 +338,7 @@ class PortraitModePanel(QWidget):
|
||||
|
||||
# Sample name labels
|
||||
self._name_lbl = QLabel("—")
|
||||
self._name_lbl.setStyleSheet(
|
||||
f"color: {TEXT}; font-size: 18px; font-weight: 700;"
|
||||
)
|
||||
self._name_lbl.setStyleSheet(f"color: {TEXT}; font-size: 18px; font-weight: 700;")
|
||||
self._sub_lbl = QLabel("No sample queued")
|
||||
self._sub_lbl.setStyleSheet(f"color: {SUBTEXT}; font-size: 12px;")
|
||||
layout.addWidget(self._name_lbl)
|
||||
@@ -377,9 +350,7 @@ class PortraitModePanel(QWidget):
|
||||
|
||||
# Play/Pause button (single, centred)
|
||||
ctrl_frame = QFrame()
|
||||
ctrl_frame.setStyleSheet(
|
||||
f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}"
|
||||
)
|
||||
ctrl_frame.setStyleSheet(f"QFrame {{ background: {CARD_BG}; border-radius: 18px; }}")
|
||||
|
||||
ctrl_layout = QHBoxLayout(ctrl_frame)
|
||||
ctrl_layout.setContentsMargins(12, 12, 12, 12)
|
||||
@@ -497,9 +468,7 @@ class PortraitModePanel(QWidget):
|
||||
layout.addWidget(self._queue_scroll, stretch=1)
|
||||
|
||||
back_btn = self._accent_button("← BACK TO CAMERA")
|
||||
back_btn.clicked.connect(
|
||||
lambda: self._stack.setCurrentWidget(self._player_page)
|
||||
)
|
||||
back_btn.clicked.connect(lambda: self._stack.setCurrentWidget(self._player_page))
|
||||
layout.addWidget(back_btn)
|
||||
|
||||
return page
|
||||
@@ -578,12 +547,7 @@ class PortraitModePanel(QWidget):
|
||||
for i, s in enumerate(suggested):
|
||||
name = str(getattr(s, "sample_name", "") or f"Sample {i + 1}")
|
||||
puck = str(getattr(s, "puck_name", "") or "")
|
||||
card = QueueItemCard(
|
||||
index=i + 1,
|
||||
title=name,
|
||||
subtitle=puck,
|
||||
is_next=False,
|
||||
)
|
||||
card = QueueItemCard(index=i + 1, title=name, subtitle=puck, is_next=False)
|
||||
self._preview_layout.addWidget(card)
|
||||
return
|
||||
|
||||
@@ -591,10 +555,7 @@ class PortraitModePanel(QWidget):
|
||||
name = str(getattr(sample, "sample_name", "") or f"Sample {i + 1}")
|
||||
puck = str(getattr(sample, "puck_name", "") or "")
|
||||
card = QueueItemCard(
|
||||
index="▶" if i == 0 else i + 1,
|
||||
title=name,
|
||||
subtitle=puck,
|
||||
is_next=(i == 0),
|
||||
index="▶" if i == 0 else i + 1, title=name, subtitle=puck, is_next=(i == 0)
|
||||
)
|
||||
self._preview_layout.addWidget(card)
|
||||
|
||||
@@ -636,12 +597,7 @@ class PortraitModePanel(QWidget):
|
||||
for i, sample in enumerate(samples):
|
||||
name = str(getattr(sample, "sample_name", "") or f"Sample {i + 1}")
|
||||
puck = str(getattr(sample, "puck_name", "") or "")
|
||||
card = QueueItemCard(
|
||||
index=i + 1,
|
||||
title=name,
|
||||
subtitle=puck,
|
||||
is_next=False,
|
||||
)
|
||||
card = QueueItemCard(index=i + 1, title=name, subtitle=puck, is_next=False)
|
||||
self._queue_inner_layout.addWidget(card)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -727,9 +683,7 @@ class PortraitModePanel(QWidget):
|
||||
self._alert_toast.setVisible(False)
|
||||
self._alert_toast_label.clear()
|
||||
|
||||
def _flush_portrait_alerts_to_banners(
|
||||
self, primary_banner, secondary_banner
|
||||
) -> None:
|
||||
def _flush_portrait_alerts_to_banners(self, primary_banner, secondary_banner) -> None:
|
||||
"""
|
||||
Called when returning to main view — replay any error alerts that
|
||||
arrived during portrait mode so the operator doesn't miss them.
|
||||
@@ -762,27 +716,17 @@ class PortraitModePanel(QWidget):
|
||||
return
|
||||
|
||||
# Timeout protection
|
||||
if (
|
||||
self._loop_restart_deadline is not None
|
||||
and self._loop_restart_deadline.hasExpired()
|
||||
):
|
||||
if self._loop_restart_deadline is not None and self._loop_restart_deadline.hasExpired():
|
||||
self._loop_restart_timer.stop()
|
||||
|
||||
self._loop_enabled = False
|
||||
self._loop_btn.setChecked(False)
|
||||
|
||||
self.show_portrait_alert(
|
||||
"Loop stopped: beamline remained busy too long",
|
||||
True,
|
||||
)
|
||||
self.show_portrait_alert("Loop stopped: beamline remained busy too long", True)
|
||||
return
|
||||
|
||||
# Still busy, wait
|
||||
if getattr(
|
||||
self._job_list_panel,
|
||||
"_SampleQueuePanel__busy",
|
||||
False,
|
||||
):
|
||||
if getattr(self._job_list_panel, "_SampleQueuePanel__busy", False):
|
||||
return
|
||||
|
||||
# Safe to restart
|
||||
@@ -805,10 +749,7 @@ class PortraitModePanel(QWidget):
|
||||
if self._loop_remaining <= 0:
|
||||
return False
|
||||
|
||||
self._job_list_panel.queue_samples(
|
||||
list(self._loop_samples),
|
||||
replace=True,
|
||||
)
|
||||
self._job_list_panel.queue_samples(list(self._loop_samples), replace=True)
|
||||
|
||||
self._loop_remaining -= 1
|
||||
|
||||
@@ -847,7 +788,4 @@ class PortraitModePanel(QWidget):
|
||||
)
|
||||
|
||||
if ordered:
|
||||
self._job_list_panel.queue_samples(
|
||||
ordered,
|
||||
replace=True,
|
||||
)
|
||||
self._job_list_panel.queue_samples(ordered, replace=True)
|
||||
|
||||
@@ -137,8 +137,7 @@ class ConfidenceHistogramWidget(QWidget):
|
||||
self.chart.legend().setVisible(False)
|
||||
|
||||
categories = [
|
||||
f"{self.BINS[i]:.1f}-{self.BINS[i + 1]:.1f}"
|
||||
for i in range(len(self.BINS) - 1)
|
||||
f"{self.BINS[i]:.1f}-{self.BINS[i + 1]:.1f}" for i in range(len(self.BINS) - 1)
|
||||
]
|
||||
self.axis_x = QBarCategoryAxis()
|
||||
self.axis_x.append(categories)
|
||||
@@ -216,9 +215,7 @@ class ObjectCountWidget(QWidget):
|
||||
|
||||
# Count label with matching color
|
||||
count_label = QLabel("0")
|
||||
count_label.setStyleSheet(
|
||||
f"color: {color}; font-size: 14px; font-weight: bold;"
|
||||
)
|
||||
count_label.setStyleSheet(f"color: {color}; font-size: 14px; font-weight: bold;")
|
||||
count_label.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
row = i // 2
|
||||
@@ -289,11 +286,7 @@ class TimingStatsWidget(QWidget):
|
||||
layout.addWidget(group)
|
||||
|
||||
def update_stats(
|
||||
self,
|
||||
frame_time_ms: float,
|
||||
fps: float,
|
||||
detection_count: int,
|
||||
mean_confidence: float,
|
||||
self, frame_time_ms: float, fps: float, detection_count: int, mean_confidence: float
|
||||
):
|
||||
"""Update timing statistics display."""
|
||||
self.frame_time_label.setText(f"{frame_time_ms:.1f} ms")
|
||||
@@ -650,10 +643,7 @@ class PredictionMetricsPanel(QWidget):
|
||||
super().showEvent(event)
|
||||
if self._refresh_timer is not None and not self._refresh_timer.isActive():
|
||||
self._refresh_timer.start()
|
||||
if (
|
||||
self._chart_refresh_timer is not None
|
||||
and not self._chart_refresh_timer.isActive()
|
||||
):
|
||||
if self._chart_refresh_timer is not None and not self._chart_refresh_timer.isActive():
|
||||
self._chart_refresh_timer.start()
|
||||
self._dirty = True
|
||||
|
||||
@@ -758,9 +748,7 @@ class PredictionMetricsPanel(QWidget):
|
||||
|
||||
# Update timing stats
|
||||
avg_frame_time = (
|
||||
sum(self._frame_times) / len(self._frame_times)
|
||||
if self._frame_times
|
||||
else 0.0
|
||||
sum(self._frame_times) / len(self._frame_times) if self._frame_times else 0.0
|
||||
)
|
||||
fps = 1000.0 / avg_frame_time if avg_frame_time > 0 else 0.0
|
||||
|
||||
@@ -781,10 +769,7 @@ class PredictionMetricsPanel(QWidget):
|
||||
return
|
||||
|
||||
path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Export Prediction Metrics",
|
||||
"prediction_metrics.csv",
|
||||
"CSV files (*.csv)",
|
||||
self, "Export Prediction Metrics", "prediction_metrics.csv", "CSV files (*.csv)"
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
|
||||
@@ -36,9 +36,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
default_transmission: float = 1.0,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(
|
||||
diffraction, raster_mgr.active_grid.dtz, default_transmission, parent
|
||||
)
|
||||
super().__init__(diffraction, raster_mgr.active_grid.dtz, default_transmission, parent)
|
||||
|
||||
self._previous_sample_was_none_raster = True
|
||||
|
||||
@@ -71,11 +69,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
|
||||
self._layout.addWidget(QLabel("Image time", parent=self), 4, 0)
|
||||
self.image_time_enter = DbOverrideLineEdit(
|
||||
0.0005,
|
||||
10.0,
|
||||
default=raster_mgr.active_grid.exp_time_s,
|
||||
decimals=4,
|
||||
parent=self,
|
||||
0.0005, 10.0, default=raster_mgr.active_grid.exp_time_s, decimals=4, parent=self
|
||||
)
|
||||
self._layout.addWidget(self.image_time_enter, 4, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("s", parent=self), 4, 4)
|
||||
@@ -110,26 +104,18 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
self._layout.addWidget(QLabel("Metric", parent=self), 7, 0)
|
||||
|
||||
self.metric_combo.addItem("Raster score", RasterGridMetric.RASTER_SCORE)
|
||||
self.metric_combo.addItem(
|
||||
"Spot count (low res.)", RasterGridMetric.SPOTS_LOW_RES
|
||||
)
|
||||
self.metric_combo.addItem("Spot count (low res.)", RasterGridMetric.SPOTS_LOW_RES)
|
||||
self.metric_combo.addItem("Spot count", RasterGridMetric.SPOTS)
|
||||
self.metric_combo.addItem(
|
||||
"Spot count (indexed)", RasterGridMetric.SPOTS_INDEXED
|
||||
)
|
||||
self.metric_combo.addItem("Spot count (indexed)", RasterGridMetric.SPOTS_INDEXED)
|
||||
self.metric_combo.addItem("Spot count (ice)", RasterGridMetric.SPOTS_ICE)
|
||||
self.metric_combo.addItem(
|
||||
"Spot ratio (ice/low res.)", RasterGridMetric.SPOTS_ICE_LOW_RES
|
||||
)
|
||||
self.metric_combo.addItem("Spot ratio (ice/low res.)", RasterGridMetric.SPOTS_ICE_LOW_RES)
|
||||
self.metric_combo.addItem("Background estimate", RasterGridMetric.BKG)
|
||||
self.metric_combo.addItem("Indexing result", RasterGridMetric.INDEXING)
|
||||
self.metric_combo.addItem("Profile Radius", RasterGridMetric.PR)
|
||||
self.metric_combo.addItem("B-factor", RasterGridMetric.BFACTOR)
|
||||
self.metric_combo.addItem("Resolution (ML est.)", RasterGridMetric.RES)
|
||||
|
||||
self.metric_combo.setCurrentIndex(
|
||||
self.metric_combo.findData(RasterGridMetric.RASTER_SCORE)
|
||||
)
|
||||
self.metric_combo.setCurrentIndex(self.metric_combo.findData(RasterGridMetric.RASTER_SCORE))
|
||||
self.metric_combo.currentIndexChanged.connect(self.metric_changed)
|
||||
|
||||
self._layout.addWidget(self.metric_combo, 7, 1, 1, 3)
|
||||
@@ -138,9 +124,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
slider = QSlider(orientation=Qt.Orientation.Horizontal, parent=self)
|
||||
slider.setRange(0, 255)
|
||||
slider.setValue(127)
|
||||
slider.valueChanged.connect(
|
||||
lambda: self.raster_alpha_changed.emit(255 - slider.value())
|
||||
)
|
||||
slider.valueChanged.connect(lambda: self.raster_alpha_changed.emit(255 - slider.value()))
|
||||
self._layout.addWidget(slider, 8, 1, 1, 3)
|
||||
|
||||
self._table = RasterGridTable(raster_mgr)
|
||||
@@ -153,9 +137,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
|
||||
self._layout.addWidget(QLabel("Measurement time", parent=self), 11, 0)
|
||||
self.total_time = QLabel(f"{self.__total_time} min 0 s")
|
||||
self.total_time.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._layout.addWidget(self.total_time, 11, 1, 1, 3)
|
||||
|
||||
self.calculate_total_time()
|
||||
@@ -185,9 +167,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
self.calculate_total_time()
|
||||
|
||||
@Slot(int, int, float, float)
|
||||
def grid_scan_size_change(
|
||||
self, n_x: int, n_y: int, size_x_mm: float, size_y_mm: float
|
||||
):
|
||||
def grid_scan_size_change(self, n_x: int, n_y: int, size_x_mm: float, size_y_mm: float):
|
||||
self.__size_x = size_x_mm * 1000.0
|
||||
self.__size_y = size_y_mm * 1000.0
|
||||
self.__n_x = n_x
|
||||
@@ -235,7 +215,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
def get_parameter_mappings(self):
|
||||
"""Return raster-specific parameter mappings."""
|
||||
return [
|
||||
("exposure", self.image_time_enter, None),
|
||||
("exposure", self.image_time_enter, None)
|
||||
# Add other raster-specific parameters here as needed
|
||||
]
|
||||
|
||||
@@ -251,9 +231,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
if self.__n_x <= 0 or self.__n_y <= 0 or self.image_time_enter.value < 0:
|
||||
self.__total_time = 0.0
|
||||
else:
|
||||
self.__total_time = (
|
||||
self.__n_x * self.__n_y * self.image_time_enter.value * 1.3
|
||||
)
|
||||
self.__total_time = self.__n_x * self.__n_y * self.image_time_enter.value * 1.3
|
||||
# 30% buffer added
|
||||
# Show minutes
|
||||
self.update_total_time_label()
|
||||
@@ -261,12 +239,8 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
@Slot()
|
||||
def _on_evaluate_clicked(self):
|
||||
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
|
||||
logger.error(
|
||||
f"Beamline state {self.__beamline_state} is not Sample Alignment"
|
||||
)
|
||||
QMessageBox.critical(
|
||||
None, "Error", "Beamline state is not Sample Alignment"
|
||||
)
|
||||
logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment")
|
||||
QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment")
|
||||
return
|
||||
if self.check_before_run(scan_kind="raster"):
|
||||
self.evaluate_grid.emit()
|
||||
@@ -274,12 +248,8 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
@Slot()
|
||||
def _on_evaluate_auto_clicked(self):
|
||||
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
|
||||
logger.error(
|
||||
f"Beamline state {self.__beamline_state} is not Sample Alignment"
|
||||
)
|
||||
QMessageBox.critical(
|
||||
None, "Error", "Beamline state is not Sample Alignment"
|
||||
)
|
||||
logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment")
|
||||
QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment")
|
||||
return
|
||||
if self.check_before_run(scan_kind="raster"):
|
||||
self.evaluate_grid_auto.emit()
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
from typing import Optional
|
||||
|
||||
from aarecommon.config.logger import setup_logger
|
||||
from aarecommon.models.models import (
|
||||
DAQStatusModel,
|
||||
SampleShortInfo,
|
||||
SampleShortInfoList,
|
||||
)
|
||||
from aarecommon.models.models import DAQStatusModel, SampleShortInfo, SampleShortInfoList
|
||||
from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal, Slot
|
||||
from PySide6.QtGui import QBrush, QColor
|
||||
from PySide6.QtWidgets import (
|
||||
@@ -96,11 +92,7 @@ class ReferenceToolsModel(QAbstractTableModel):
|
||||
if role != Qt.ItemDataRole.DisplayRole:
|
||||
return None
|
||||
if orientation == Qt.Orientation.Horizontal:
|
||||
return (
|
||||
self.header[section]
|
||||
if section < len(self.header)
|
||||
else f"Column {section + 1}"
|
||||
)
|
||||
return self.header[section] if section < len(self.header) else f"Column {section + 1}"
|
||||
return str(section + 1)
|
||||
|
||||
def update_rows(self, rows: list[SampleShortInfo]):
|
||||
@@ -137,16 +129,12 @@ class ReferenceToolsModel(QAbstractTableModel):
|
||||
elif self.__sort_col == 2:
|
||||
# Numeric sort for Mount count; place None last on ascending, first on descending
|
||||
none_sentinel = (
|
||||
float("inf")
|
||||
if self.__sort_order == Qt.SortOrder.AscendingOrder
|
||||
else float("-inf")
|
||||
float("inf") if self.__sort_order == Qt.SortOrder.AscendingOrder else float("-inf")
|
||||
)
|
||||
self.__sorted_samples = sorted(
|
||||
self.samples,
|
||||
key=lambda row: (
|
||||
row.mount_count
|
||||
if isinstance(row.mount_count, (int, float))
|
||||
else none_sentinel
|
||||
row.mount_count if isinstance(row.mount_count, (int, float)) else none_sentinel
|
||||
),
|
||||
reverse=(self.__sort_order == Qt.SortOrder.DescendingOrder),
|
||||
)
|
||||
@@ -220,9 +208,7 @@ class ReferenceToolsPanel(QFrame):
|
||||
# initialize model with provided samples
|
||||
self.table_model = ReferenceToolsModel(rows=samples.s)
|
||||
self.table_view.setModel(self.table_model)
|
||||
self.table_view.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.Stretch
|
||||
)
|
||||
self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
self.table_view.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
|
||||
logger.debug("Setting up table header")
|
||||
@@ -237,9 +223,7 @@ class ReferenceToolsPanel(QFrame):
|
||||
logger.debug("Setting up table view context menu")
|
||||
self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table_view.customContextMenuRequested.connect(self._context_menu)
|
||||
self.table_view.setSelectionBehavior(
|
||||
QAbstractItemView.SelectionBehavior.SelectRows
|
||||
)
|
||||
self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.table_view.setSelectionMode(QTableView.SelectionMode.SingleSelection)
|
||||
|
||||
def _selected_item(self) -> Optional[SampleShortInfo]:
|
||||
|
||||
@@ -8,11 +8,7 @@ from PySide6.QtCore import Qt, Signal, Slot
|
||||
from PySide6.QtWidgets import QComboBox, QLabel, QMessageBox, QPushButton
|
||||
|
||||
from aare.gui.panels.scan_settings_panel import ScanSettingsPanel
|
||||
from aare.gui.widgets.number_line_edit import (
|
||||
CheckedLineEdit,
|
||||
DbOverrideLineEdit,
|
||||
NumberLineEdit,
|
||||
)
|
||||
from aare.gui.widgets.number_line_edit import CheckedLineEdit, DbOverrideLineEdit, NumberLineEdit
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
@@ -69,36 +65,22 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
)
|
||||
|
||||
self._layout.addWidget(QLabel("Image angle", parent=self), 5, 0)
|
||||
self.screening_image_angle = NumberLineEdit(
|
||||
0, 90.0, 0.5, decimals=3, parent=self
|
||||
)
|
||||
self.screening_image_angle = NumberLineEdit(0, 90.0, 0.5, decimals=3, parent=self)
|
||||
self._layout.addWidget(self.screening_image_angle, 5, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 5, 4)
|
||||
|
||||
self._layout.addWidget(QLabel("Image time", parent=self), 6, 0)
|
||||
self.screening_image_time_enter = NumberLineEdit(
|
||||
0.0005, 10.0, 0.1, decimals=4, parent=self
|
||||
)
|
||||
self.screening_image_time_enter = NumberLineEdit(0.0005, 10.0, 0.1, decimals=4, parent=self)
|
||||
self._layout.addWidget(self.screening_image_time_enter, 6, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("s", parent=self), 6, 4)
|
||||
|
||||
self.screening_type = QComboBox(parent=self)
|
||||
self.screening_type.addItem("1 image", {"steps": 1, "omega_step_deg": 0})
|
||||
self.screening_type.addItem(
|
||||
"2 images every 90°", {"steps": 2, "omega_step_deg": 90}
|
||||
)
|
||||
self.screening_type.addItem(
|
||||
"4 images every 90°", {"steps": 4, "omega_step_deg": 90}
|
||||
)
|
||||
self.screening_type.addItem(
|
||||
"3 images every 60°", {"steps": 3, "omega_step_deg": 60}
|
||||
)
|
||||
self.screening_type.addItem(
|
||||
"2 images every 45°", {"steps": 2, "omega_step_deg": 45}
|
||||
)
|
||||
self.screening_type.addItem(
|
||||
"4 images every 45°", {"steps": 4, "omega_step_deg": 45}
|
||||
)
|
||||
self.screening_type.addItem("2 images every 90°", {"steps": 2, "omega_step_deg": 90})
|
||||
self.screening_type.addItem("4 images every 90°", {"steps": 4, "omega_step_deg": 90})
|
||||
self.screening_type.addItem("3 images every 60°", {"steps": 3, "omega_step_deg": 60})
|
||||
self.screening_type.addItem("2 images every 45°", {"steps": 2, "omega_step_deg": 45})
|
||||
self.screening_type.addItem("4 images every 45°", {"steps": 4, "omega_step_deg": 45})
|
||||
|
||||
self._layout.addWidget(self.screening_type, 7, 0, 1, 6)
|
||||
|
||||
@@ -112,17 +94,13 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
)
|
||||
|
||||
self._layout.addWidget(QLabel("Total angle", parent=self), 10, 0)
|
||||
self.total_angle = DbOverrideLineEdit(
|
||||
0, 9999.0, default=360.0, decimals=3, parent=self
|
||||
)
|
||||
self.total_angle = DbOverrideLineEdit(0, 9999.0, default=360.0, decimals=3, parent=self)
|
||||
self._layout.addWidget(self.total_angle, 10, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 10, 4)
|
||||
self._register_override_field(self.total_angle)
|
||||
|
||||
self._layout.addWidget(QLabel("Image angle", parent=self), 11, 0)
|
||||
self.image_angle = DbOverrideLineEdit(
|
||||
0, 10.0, default=0.2, decimals=3, parent=self
|
||||
)
|
||||
self.image_angle = DbOverrideLineEdit(0, 10.0, default=0.2, decimals=3, parent=self)
|
||||
self._layout.addWidget(self.image_angle, 11, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 11, 4)
|
||||
self._register_override_field(self.image_angle)
|
||||
@@ -137,9 +115,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
|
||||
self._layout.addWidget(QLabel("Total measurement time", parent=self), 13, 0)
|
||||
self.total_time = QLabel(f"{self.__total_time} min 0 s")
|
||||
self.total_time.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._layout.addWidget(self.total_time, 13, 1, 1, 3)
|
||||
|
||||
self.total_angle.valueChanged.connect(self.calculate_measurement_time)
|
||||
@@ -150,9 +126,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
|
||||
self._layout.addWidget(QLabel("Dose", parent=self), 14, 0)
|
||||
self.dose = QLabel(f"{self.__dose_mgy}")
|
||||
self.dose.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.dose.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._layout.addWidget(self.dose, 14, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("MGy", parent=self), 14, 4)
|
||||
|
||||
@@ -168,12 +142,8 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
@Slot()
|
||||
def run_screening(self):
|
||||
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
|
||||
logger.error(
|
||||
f"Beamline state {self.__beamline_state} is not Sample Alignment"
|
||||
)
|
||||
QMessageBox.critical(
|
||||
None, "Error", "Beamline state is not Sample Alignment"
|
||||
)
|
||||
logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment")
|
||||
QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment")
|
||||
return
|
||||
if not self.check_before_run(scan_kind="screening"):
|
||||
logger.error("Cannot run measurement because of check")
|
||||
@@ -181,8 +151,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
screening_settings = self.screening_type.currentData()
|
||||
|
||||
is_single_image = (
|
||||
screening_settings["steps"] == 1
|
||||
and screening_settings["omega_step_deg"] == 0
|
||||
screening_settings["steps"] == 1 and screening_settings["omega_step_deg"] == 0
|
||||
)
|
||||
|
||||
if is_single_image:
|
||||
@@ -216,12 +185,8 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
@Slot()
|
||||
def run_measurement(self):
|
||||
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
|
||||
logger.error(
|
||||
f"Beamline state {self.__beamline_state} is not Sample Alignment"
|
||||
)
|
||||
QMessageBox.critical(
|
||||
None, "Error", "Beamline state is not Sample Alignment"
|
||||
)
|
||||
logger.error(f"Beamline state {self.__beamline_state} is not Sample Alignment")
|
||||
QMessageBox.critical(None, "Error", "Beamline state is not Sample Alignment")
|
||||
return
|
||||
if not self.check_before_run(scan_kind="rotation"):
|
||||
logger.error("Cannot run measurement because of check")
|
||||
@@ -318,9 +283,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
beam_area = s.geom.beam_size_mm.x * s.geom.beam_size_mm.y * 1e6
|
||||
time = self.image_number() * self.image_time_enter.value
|
||||
|
||||
self.__dose_mgy = (time * s.bl.flux_ph_s * self._transmission) / (
|
||||
beam_area * kdose
|
||||
)
|
||||
self.__dose_mgy = (time * s.bl.flux_ph_s * self._transmission) / (beam_area * kdose)
|
||||
self.dose.setText(f"{(self.__dose_mgy / 1e6):.1f}")
|
||||
self.__beamline_state = s.state
|
||||
|
||||
|
||||
@@ -43,9 +43,7 @@ class SamcamPanel(QWidget):
|
||||
self.exposure_spinbox = QDoubleSpinBox()
|
||||
self.exposure_spinbox.setRange(0, 1.0) # Adjust range as needed
|
||||
self.exposure_spinbox.setSingleStep(0.001)
|
||||
self.exposure_spinbox.setStyleSheet(
|
||||
"QDoubleSpinBox { background-color: white; }"
|
||||
)
|
||||
self.exposure_spinbox.setStyleSheet("QDoubleSpinBox { background-color: white; }")
|
||||
self.exposure_spinbox.setDecimals(3)
|
||||
|
||||
self.exposure_spinbox.valueChanged.connect(self.__changed)
|
||||
@@ -67,17 +65,13 @@ class SamcamPanel(QWidget):
|
||||
# Persist the current gain/exposure as the beam-location preset for the
|
||||
# current zoom (only meaningful in beam-location mode).
|
||||
self.save_beam_location_button = QPushButton("Save samcam settings (bl)")
|
||||
self.save_beam_location_button.clicked.connect(
|
||||
self.save_beam_location_setting.emit
|
||||
)
|
||||
self.save_beam_location_button.clicked.connect(self.save_beam_location_setting.emit)
|
||||
|
||||
screenshot_filename_layout = QHBoxLayout()
|
||||
screenshot_filename_label = QLabel("Filename:")
|
||||
self.screenshot_filename_edit = QLineEdit()
|
||||
self.screenshot_filename_edit.setPlaceholderText("optional")
|
||||
self.screenshot_filename_edit.setStyleSheet(
|
||||
"QLineEdit { background-color: white; }"
|
||||
)
|
||||
self.screenshot_filename_edit.setStyleSheet("QLineEdit { background-color: white; }")
|
||||
screenshot_filename_layout.addWidget(screenshot_filename_label)
|
||||
screenshot_filename_layout.addWidget(self.screenshot_filename_edit)
|
||||
|
||||
@@ -85,9 +79,7 @@ class SamcamPanel(QWidget):
|
||||
screenshot_message_label = QLabel("Message:")
|
||||
self.screenshot_message_edit = QLineEdit()
|
||||
self.screenshot_message_edit.setPlaceholderText("optional")
|
||||
self.screenshot_message_edit.setStyleSheet(
|
||||
"QLineEdit { background-color: white; }"
|
||||
)
|
||||
self.screenshot_message_edit.setStyleSheet("QLineEdit { background-color: white; }")
|
||||
screenshot_message_layout.addWidget(screenshot_message_label)
|
||||
screenshot_message_layout.addWidget(self.screenshot_message_edit)
|
||||
|
||||
@@ -114,9 +106,7 @@ class SamcamPanel(QWidget):
|
||||
target_point_layout = QHBoxLayout()
|
||||
self.show_target_point_checkbox = QCheckBox("Show target point")
|
||||
self.show_target_point_checkbox.setChecked(True)
|
||||
self.show_target_point_checkbox.toggled.connect(
|
||||
self.show_target_point_changed.emit
|
||||
)
|
||||
self.show_target_point_checkbox.toggled.connect(self.show_target_point_changed.emit)
|
||||
target_point_layout.addWidget(self.show_target_point_checkbox)
|
||||
|
||||
# Show target coordinates checkbox
|
||||
@@ -132,9 +122,7 @@ class SamcamPanel(QWidget):
|
||||
legend_layout = QHBoxLayout()
|
||||
self.show_overlay_legend_checkbox = QCheckBox("Show overlay legend")
|
||||
self.show_overlay_legend_checkbox.setChecked(True)
|
||||
self.show_overlay_legend_checkbox.toggled.connect(
|
||||
self.show_overlay_legend_changed.emit
|
||||
)
|
||||
self.show_overlay_legend_checkbox.toggled.connect(self.show_overlay_legend_changed.emit)
|
||||
legend_layout.addWidget(self.show_overlay_legend_checkbox)
|
||||
|
||||
# Compact legend checkbox
|
||||
@@ -152,9 +140,7 @@ class SamcamPanel(QWidget):
|
||||
self.target_color_combo = QComboBox()
|
||||
self.target_color_combo.addItems(["Cyan", "Dark Blue", "Dark Red"])
|
||||
self.target_color_combo.setCurrentText("Cyan")
|
||||
self.target_color_combo.currentTextChanged.connect(
|
||||
self.target_color_changed.emit
|
||||
)
|
||||
self.target_color_combo.currentTextChanged.connect(self.target_color_changed.emit)
|
||||
target_color_layout.addWidget(target_color_label)
|
||||
target_color_layout.addWidget(self.target_color_combo)
|
||||
|
||||
@@ -183,8 +169,7 @@ class SamcamPanel(QWidget):
|
||||
|
||||
def __request_screenshot(self):
|
||||
self.screenshot_requested.emit(
|
||||
self.screenshot_filename_edit.text(),
|
||||
self.screenshot_message_edit.text(),
|
||||
self.screenshot_filename_edit.text(), self.screenshot_message_edit.text()
|
||||
)
|
||||
|
||||
def apply_overlay_settings(
|
||||
|
||||
@@ -37,10 +37,7 @@ class SampleQueuePanel(QFrame):
|
||||
step_through_changed = Signal(bool)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
samples: SampleShortInfoList | None = None,
|
||||
show_user: bool = False,
|
||||
self, parent=None, samples: SampleShortInfoList | None = None, show_user: bool = False
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.__baton_holder = None
|
||||
@@ -76,25 +73,17 @@ class SampleQueuePanel(QFrame):
|
||||
header = self.table_view.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
self.table_view.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
||||
)
|
||||
self.table_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.table_view.setAcceptDrops(True)
|
||||
self.table_view.setDropIndicatorShown(True)
|
||||
self.table_view.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
|
||||
|
||||
self.delete_shortcut = QShortcut(
|
||||
QKeySequence.StandardKey.Delete, self.table_view
|
||||
)
|
||||
self.delete_shortcut = QShortcut(QKeySequence.StandardKey.Delete, self.table_view)
|
||||
self.delete_shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
||||
self.delete_shortcut.activated.connect(self.remove_selected_samples)
|
||||
|
||||
self.run_pause_shortcut = QShortcut(
|
||||
QKeySequence(Qt.Key.Key_Space), self.table_view
|
||||
)
|
||||
self.run_pause_shortcut.setContext(
|
||||
Qt.ShortcutContext.WidgetWithChildrenShortcut
|
||||
)
|
||||
self.run_pause_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Space), self.table_view)
|
||||
self.run_pause_shortcut.setContext(Qt.ShortcutContext.WidgetWithChildrenShortcut)
|
||||
self.run_pause_shortcut.activated.connect(self.run)
|
||||
|
||||
layout.addWidget(self.table_view)
|
||||
@@ -109,9 +98,7 @@ class SampleQueuePanel(QFrame):
|
||||
self.clear_button = QPushButton("✖ Clear list", self)
|
||||
self.clear_button.clicked.connect(self.clear)
|
||||
|
||||
self.park_and_dry_when_cleared = QCheckBox(
|
||||
"Park and dry when automation finishes", self
|
||||
)
|
||||
self.park_and_dry_when_cleared = QCheckBox("Park and dry when automation finishes", self)
|
||||
self.park_and_dry_when_cleared.setChecked(True)
|
||||
|
||||
self.pause_on_conditions_cb = QCheckBox(
|
||||
@@ -160,9 +147,7 @@ class SampleQueuePanel(QFrame):
|
||||
next_next_sample = samples[2] if len(samples) > 2 else None
|
||||
return current, next_sample, next_next_sample
|
||||
|
||||
def queue_samples(
|
||||
self, samples: list[SampleShortInfo], replace: bool = True
|
||||
) -> None:
|
||||
def queue_samples(self, samples: list[SampleShortInfo], replace: bool = True) -> None:
|
||||
if replace:
|
||||
self.table_model.updateData(list(samples))
|
||||
else:
|
||||
@@ -229,11 +214,7 @@ class SampleQueuePanel(QFrame):
|
||||
"""Beamline conditions that currently block/should pause automation."""
|
||||
problems: list[str] = []
|
||||
if self.ring_current is None or self.ring_current < LOW_CURRENT_THRESHOLD:
|
||||
rc = (
|
||||
"unknown"
|
||||
if self.ring_current is None
|
||||
else f"{round(self.ring_current, 2)} mA"
|
||||
)
|
||||
rc = "unknown" if self.ring_current is None else f"{round(self.ring_current, 2)} mA"
|
||||
problems.append(f"beam (ring current {rc})")
|
||||
if not self._experiment_shutter_state:
|
||||
problems.append("experiment shutter closed")
|
||||
@@ -263,9 +244,7 @@ class SampleQueuePanel(QFrame):
|
||||
self.__warning_msg_box = QMessageBox(self)
|
||||
self.__warning_msg_box.setIcon(QMessageBox.Icon.Warning)
|
||||
self.__warning_msg_box.setWindowTitle("TELL Warning")
|
||||
self.__warning_msg_box.setText(
|
||||
"TELL reported a warning. Please see console for details."
|
||||
)
|
||||
self.__warning_msg_box.setText("TELL reported a warning. Please see console for details.")
|
||||
self.__warning_msg_box.setInformativeText(
|
||||
"Automation paused for 10 minutes to allow sufficient dry time."
|
||||
"\nClick 'Continue Now' to resume immediately, or wait for auto-resume."
|
||||
@@ -340,14 +319,11 @@ class SampleQueuePanel(QFrame):
|
||||
return
|
||||
|
||||
if self.__busy:
|
||||
logger.error(
|
||||
f"Cannot run automation while beamline is busy. Busy flag = {self.__busy}"
|
||||
)
|
||||
logger.error(f"Cannot run automation while beamline is busy. Busy flag = {self.__busy}")
|
||||
self.show_error_dialog(
|
||||
title="Beamline is busy",
|
||||
msg="Cannot run automation while beamline is busy",
|
||||
info="Please wait until beamline is idle "
|
||||
"or contact your local contact for support",
|
||||
info="Please wait until beamline is idle or contact your local contact for support",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -385,9 +361,7 @@ class SampleQueuePanel(QFrame):
|
||||
if checks_enabled:
|
||||
bad = self._bad_conditions()
|
||||
if bad:
|
||||
logger.warning(
|
||||
f"Cannot start automation; beamline not ready: {bad}"
|
||||
)
|
||||
logger.warning(f"Cannot start automation; beamline not ready: {bad}")
|
||||
self.show_error_dialog(
|
||||
title="Beamline not ready",
|
||||
msg="Cannot start automation:\n- " + "\n- ".join(bad),
|
||||
@@ -450,9 +424,7 @@ class SampleQueuePanel(QFrame):
|
||||
msg = "Automation paused — beamline not ready:\n- " + "\n- ".join(bad)
|
||||
|
||||
if conditions_auto_check(self, msg, self._conditions_ok):
|
||||
logger.debug(
|
||||
"Conditions recovered or user chose to continue; resuming automation"
|
||||
)
|
||||
logger.debug("Conditions recovered or user chose to continue; resuming automation")
|
||||
self.table_model.set_running(True)
|
||||
self.__pause = False
|
||||
self.play_button.setText("⏸ Pause")
|
||||
|
||||
@@ -103,13 +103,9 @@ class ScanSettingsPanel(QWidget):
|
||||
self._register_override_field(self.transmission_enter)
|
||||
|
||||
self.reload_params_button = QPushButton("Reload DB params")
|
||||
self.reload_params_button.setToolTip(
|
||||
"Reload data collection parameters from database"
|
||||
)
|
||||
self.reload_params_button.setToolTip("Reload data collection parameters from database")
|
||||
self.reload_params_button.clicked.connect(self.reload_parameters)
|
||||
self.reload_params_button.setVisible(
|
||||
False
|
||||
) # Child classes should make it visible
|
||||
self.reload_params_button.setVisible(False) # Child classes should make it visible
|
||||
|
||||
# -- source toggle -----------------------------------------------------
|
||||
def _build_source_toggle(self) -> QWidget:
|
||||
@@ -176,8 +172,7 @@ class ScanSettingsPanel(QWidget):
|
||||
self._experiment_shutter_state = s.bl.exp_shutter_open
|
||||
self._door_prohibited = getattr(s.bl, "pss_prohibited", None)
|
||||
can_edit = (not s.busy) and (
|
||||
s.session.session
|
||||
in (SessionsStateEnum.OwnedByYou, SessionsStateEnum.PendingElseToYou)
|
||||
s.session.session in (SessionsStateEnum.OwnedByYou, SessionsStateEnum.PendingElseToYou)
|
||||
)
|
||||
self._can_edit_params = can_edit
|
||||
# Lock/unlock the override fields
|
||||
@@ -187,9 +182,7 @@ class ScanSettingsPanel(QWidget):
|
||||
# Update sample and parameters
|
||||
if s.sample is not None:
|
||||
self._sample = s.sample
|
||||
self._params = (
|
||||
s.sample.aaredb_params if hasattr(s.sample, "aaredb_params") else None
|
||||
)
|
||||
self._params = s.sample.aaredb_params if hasattr(s.sample, "aaredb_params") else None
|
||||
# Enable reload button only if sample has parameters
|
||||
self._previous_sample_was_none = False
|
||||
self.reload_params_button.setEnabled(self._params is not None)
|
||||
@@ -304,9 +297,7 @@ class ScanSettingsPanel(QWidget):
|
||||
|
||||
# Handle transmission with conversion (common to all panels)
|
||||
if (transmission := getattr(self._params, "transmission", None)) is not None:
|
||||
transmission_value = (
|
||||
transmission / 100.0 if transmission > 1.0 else transmission
|
||||
)
|
||||
transmission_value = transmission / 100.0 if transmission > 1.0 else transmission
|
||||
self._transmission = transmission_value
|
||||
self.transmission_enter.set_db_value(transmission_value)
|
||||
|
||||
|
||||
@@ -45,9 +45,7 @@ class SmargonMoveWidget(QWidget):
|
||||
|
||||
@Slot(dict)
|
||||
def smargon_button(self, payload: dict):
|
||||
self.smargon_rel.emit(
|
||||
Coordinate(x=payload["x"], y=payload["y"], z=payload["z"])
|
||||
)
|
||||
self.smargon_rel.emit(Coordinate(x=payload["x"], y=payload["y"], z=payload["z"]))
|
||||
|
||||
|
||||
class SmargonPanel(QWidget):
|
||||
@@ -96,9 +94,7 @@ class SmargonPanel(QWidget):
|
||||
@Slot()
|
||||
def home(self):
|
||||
# TODO move SMARGON_HOME to REDIS, allow GUI to read this value
|
||||
self.smargon.emit(
|
||||
SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0)
|
||||
)
|
||||
self.smargon.emit(SmargonCoordinate(sh_mm=Coordinate(x=0, y=0, z=18), phi_deg=0, chi_deg=0))
|
||||
|
||||
@Slot(float)
|
||||
def phi(self, f: float):
|
||||
@@ -120,7 +116,5 @@ class SmargonPanel(QWidget):
|
||||
return
|
||||
|
||||
self.smargon.emit(
|
||||
SmargonCoordinate(
|
||||
sh_mm=self.__geom.beamline_to_smargon(c * self.__step / 1000.0)
|
||||
)
|
||||
SmargonCoordinate(sh_mm=self.__geom.beamline_to_smargon(c * self.__step / 1000.0))
|
||||
)
|
||||
|
||||
@@ -150,8 +150,7 @@ class SmargonTracePanel(QWidget):
|
||||
self._last_distances = []
|
||||
self._last_lengths = []
|
||||
self._status.setText(
|
||||
"No trace file found. Tried: "
|
||||
+ ", ".join(str(p) for p in self._candidate_paths())
|
||||
"No trace file found. Tried: " + ", ".join(str(p) for p in self._candidate_paths())
|
||||
)
|
||||
self._summary.setText(
|
||||
f"Home position: "
|
||||
@@ -214,8 +213,7 @@ class SmargonTracePanel(QWidget):
|
||||
dz = [value * unit_scale for value in dz_mm]
|
||||
|
||||
distances_mm = [
|
||||
sqrt(dx0**2 + dy0**2 + dz0**2)
|
||||
for dx0, dy0, dz0 in zip(dx_mm, dy_mm, dz_mm)
|
||||
sqrt(dx0**2 + dy0**2 + dz0**2) for dx0, dy0, dz0 in zip(dx_mm, dy_mm, dz_mm)
|
||||
]
|
||||
distances = [value * unit_scale for value in distances_mm]
|
||||
|
||||
@@ -340,49 +338,49 @@ class SmargonTracePanel(QWidget):
|
||||
|
||||
unit_name, unit_scale = self._unit_settings()
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Export Smargon Trace Table",
|
||||
"smargon_trace_export.csv",
|
||||
"CSV Files (*.csv)",
|
||||
self, "Export Smargon Trace Table", "smargon_trace_export.csv", "CSV Files (*.csv)"
|
||||
)
|
||||
if not file_path:
|
||||
return
|
||||
|
||||
with open(file_path, "w", encoding="utf-8", newline="") as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow([
|
||||
"point",
|
||||
"timestamp",
|
||||
"event",
|
||||
"sample_id",
|
||||
f"shx_{unit_name}",
|
||||
f"shy_{unit_name}",
|
||||
f"shz_{unit_name}",
|
||||
f"distance_{unit_name}",
|
||||
f"length_{unit_name}",
|
||||
"omega_deg",
|
||||
"phi_deg",
|
||||
"chi_deg",
|
||||
])
|
||||
writer.writerow(
|
||||
[
|
||||
"point",
|
||||
"timestamp",
|
||||
"event",
|
||||
"sample_id",
|
||||
f"shx_{unit_name}",
|
||||
f"shy_{unit_name}",
|
||||
f"shz_{unit_name}",
|
||||
f"distance_{unit_name}",
|
||||
f"length_{unit_name}",
|
||||
"omega_deg",
|
||||
"phi_deg",
|
||||
"chi_deg",
|
||||
]
|
||||
)
|
||||
|
||||
for idx, (row, distance_value, length_value) in enumerate(
|
||||
zip(self._last_rows, self._last_distances, self._last_lengths),
|
||||
start=1,
|
||||
zip(self._last_rows, self._last_distances, self._last_lengths), start=1
|
||||
):
|
||||
writer.writerow([
|
||||
idx,
|
||||
row["timestamp"],
|
||||
row["event"],
|
||||
row["sample_id"],
|
||||
float(row["shx_mm"]) * unit_scale,
|
||||
float(row["shy_mm"]) * unit_scale,
|
||||
float(row["shz_mm"]) * unit_scale,
|
||||
distance_value,
|
||||
length_value,
|
||||
row["omega_deg"],
|
||||
row["phi_deg"],
|
||||
row["chi_deg"],
|
||||
])
|
||||
writer.writerow(
|
||||
[
|
||||
idx,
|
||||
row["timestamp"],
|
||||
row["event"],
|
||||
row["sample_id"],
|
||||
float(row["shx_mm"]) * unit_scale,
|
||||
float(row["shy_mm"]) * unit_scale,
|
||||
float(row["shz_mm"]) * unit_scale,
|
||||
distance_value,
|
||||
length_value,
|
||||
row["omega_deg"],
|
||||
row["phi_deg"],
|
||||
row["chi_deg"],
|
||||
]
|
||||
)
|
||||
|
||||
def _redraw_plots(
|
||||
self,
|
||||
@@ -431,7 +429,13 @@ class SmargonTracePanel(QWidget):
|
||||
ax.legend(loc="best")
|
||||
|
||||
elif plot_name == "distance":
|
||||
ax.plot(x, distances, marker="o", color=self.COLOR_DISTANCE, label=f"Total distance [{unit_name}]")
|
||||
ax.plot(
|
||||
x,
|
||||
distances,
|
||||
marker="o",
|
||||
color=self.COLOR_DISTANCE,
|
||||
label=f"Total distance [{unit_name}]",
|
||||
)
|
||||
ax.set_title("Total distance from home")
|
||||
ax.set_ylabel(f"Distance [{unit_name}]")
|
||||
ax.grid(True, alpha=0.3)
|
||||
@@ -451,9 +455,9 @@ class SmargonTracePanel(QWidget):
|
||||
unit_name: str,
|
||||
unit_scale: float,
|
||||
) -> None:
|
||||
recent_rows = rows[-self.TABLE_MAX_ROWS:]
|
||||
recent_distances = distances[-self.TABLE_MAX_ROWS:]
|
||||
recent_lengths = lengths[-self.TABLE_MAX_ROWS:]
|
||||
recent_rows = rows[-self.TABLE_MAX_ROWS :]
|
||||
recent_distances = distances[-self.TABLE_MAX_ROWS :]
|
||||
recent_lengths = lengths[-self.TABLE_MAX_ROWS :]
|
||||
|
||||
self._table.setRowCount(len(recent_rows))
|
||||
self._table.setHorizontalHeaderLabels(
|
||||
@@ -580,12 +584,7 @@ class SmargonTracePanel(QWidget):
|
||||
return sqrt(beam_x**2 + beam_y**2)
|
||||
|
||||
def _smargon_nudge_basis(
|
||||
self,
|
||||
*,
|
||||
axis: str,
|
||||
omega_deg: float,
|
||||
phi_deg: float,
|
||||
chi_deg: float,
|
||||
self, *, axis: str, omega_deg: float, phi_deg: float, chi_deg: float
|
||||
) -> np.ndarray:
|
||||
phi = np.radians(np.around(phi_deg, decimals=1))
|
||||
chi = np.radians(np.around(chi_deg, decimals=1))
|
||||
@@ -637,4 +636,4 @@ class SmargonTracePanel(QWidget):
|
||||
ax.set_xticks([])
|
||||
ax.set_yticks([])
|
||||
self._figure.tight_layout()
|
||||
self._canvas.draw_idle()
|
||||
self._canvas.draw_idle()
|
||||
|
||||
@@ -4,14 +4,7 @@ from aarecommon.config.logger import setup_logger
|
||||
from aarecommon.models.models import CrystalSize, DAQStatusModel, SimpleScanParameters
|
||||
from aarecommon.models.rotation_scan import RotationScanRequest
|
||||
from PySide6.QtCore import Qt, Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QGridLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QWidget,
|
||||
)
|
||||
from PySide6.QtWidgets import QGridLayout, QLabel, QPushButton, QSizePolicy, QSpacerItem, QWidget
|
||||
|
||||
from aare.gui.panels.rotation_data_collection import add_data_to_path
|
||||
from aare.gui.widgets.number_line_edit import NumberLineEdit
|
||||
@@ -52,17 +45,13 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
|
||||
# Visible resolution (entry)
|
||||
self._layout.addWidget(QLabel("Visible resolution", parent=self), 0, 0)
|
||||
self.visible_res_enter = NumberLineEdit(
|
||||
0.8, 10.0, decimals=2, default=2.0, parent=self
|
||||
)
|
||||
self.visible_res_enter = NumberLineEdit(0.8, 10.0, decimals=2, default=2.0, parent=self)
|
||||
self._layout.addWidget(self.visible_res_enter, 0, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("Å", parent=self), 0, 4)
|
||||
self.visible_res_enter.newValue.connect(self.set_visible_resolution)
|
||||
|
||||
self._layout.addWidget(QLabel("Start angle", parent=self), 1, 0)
|
||||
self.start_angle_enter = NumberLineEdit(
|
||||
-720, 720.0, 0.0, decimals=3, parent=self
|
||||
)
|
||||
self.start_angle_enter = NumberLineEdit(-720, 720.0, 0.0, decimals=3, parent=self)
|
||||
self._layout.addWidget(self.start_angle_enter, 1, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 1, 4)
|
||||
|
||||
@@ -81,17 +70,13 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
self.visible_res_enter.newValue.connect(self.set_total_angle)
|
||||
|
||||
self._layout.addWidget(QLabel("Image angle", parent=self), 3, 0)
|
||||
self.image_angle_enter = NumberLineEdit(
|
||||
0.001, 1.000, decimals=3, default=0.2, parent=self
|
||||
)
|
||||
self.image_angle_enter = NumberLineEdit(0.001, 1.000, decimals=3, default=0.2, parent=self)
|
||||
self._layout.addWidget(self.image_angle_enter, 3, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 3, 4)
|
||||
self.image_angle_enter.newValue.connect(self.set_image_angle)
|
||||
|
||||
self._layout.addWidget(QLabel("Temperature", parent=self), 4, 0)
|
||||
self.temp_enter = NumberLineEdit(
|
||||
80, 330, decimals=2, default=100.0, parent=self
|
||||
)
|
||||
self.temp_enter = NumberLineEdit(80, 330, decimals=2, default=100.0, parent=self)
|
||||
self._layout.addWidget(self.temp_enter, 4, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("K", parent=self), 4, 4)
|
||||
self.temp_enter.newValue.connect(self.set_temperature)
|
||||
@@ -123,9 +108,7 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
|
||||
self._layout.addWidget(QLabel("Detector distance", parent=self), 8, 0)
|
||||
self.dtz_label = QLabel(f"--", parent=self)
|
||||
self.dtz_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.dtz_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._layout.addWidget(self.dtz_label, 8, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("mm", parent=self), 8, 4)
|
||||
|
||||
@@ -155,31 +138,23 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
|
||||
self._layout.addWidget(QLabel("Crystal Size x", parent=self), 12, 0)
|
||||
self.xtal_x_label = QLabel(f"--", parent=self)
|
||||
self.xtal_x_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.xtal_x_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._layout.addWidget(self.xtal_x_label, 12, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("um", parent=self), 12, 4)
|
||||
|
||||
self._layout.addWidget(QLabel("Crystal Size y", parent=self), 13, 0)
|
||||
self.xtal_y_label = QLabel(f"--", parent=self)
|
||||
self.xtal_y_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.xtal_y_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._layout.addWidget(self.xtal_y_label, 13, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("um", parent=self), 13, 4)
|
||||
|
||||
self._layout.addWidget(QLabel("Crystal Size z", parent=self), 14, 0)
|
||||
self.xtal_z_label = QLabel(f"--", parent=self)
|
||||
self.xtal_z_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.xtal_z_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._layout.addWidget(self.xtal_z_label, 14, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("um", parent=self), 14, 4)
|
||||
|
||||
self._layout.addWidget(
|
||||
QLabel("Calculated Dose (xtal size)", parent=self), 15, 0
|
||||
)
|
||||
self._layout.addWidget(QLabel("Calculated Dose (xtal size)", parent=self), 15, 0)
|
||||
self.xtal_size_dose_label = QLabel(f"--", parent=self)
|
||||
self.xtal_size_dose_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
@@ -197,13 +172,9 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
|
||||
self._layout.addWidget(QLabel("Flux", parent=self), 17, 0)
|
||||
self.flux_label = QLabel(f"--", parent=self)
|
||||
self.flux_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.flux_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._layout.addWidget(self.flux_label, 17, 1, 1, 3)
|
||||
self._layout.addWidget(
|
||||
QLabel("x 10<sup>9</sup> ph s<sup>-1</sup>", parent=self), 17, 4
|
||||
)
|
||||
self._layout.addWidget(QLabel("x 10<sup>9</sup> ph s<sup>-1</sup>", parent=self), 17, 4)
|
||||
|
||||
self._layout.addWidget(QLabel("Beam Size", parent=self), 18, 0)
|
||||
self.beam_size_label = QLabel(f"--", parent=self)
|
||||
@@ -223,18 +194,12 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
|
||||
self._layout.addWidget(QLabel("Total measurement time", parent=self), 20, 0)
|
||||
self.total_time = QLabel(f"{self.total_time_s} min 0 s")
|
||||
self.total_time.setAlignment(
|
||||
Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
self._layout.addWidget(self.total_time, 20, 1, 1, 3)
|
||||
|
||||
# add vertical stretch between detector distance and the run button
|
||||
self._layout.addItem(
|
||||
QSpacerItem(0, 0, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding),
|
||||
21,
|
||||
0,
|
||||
1,
|
||||
6,
|
||||
QSpacerItem(0, 0, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding), 21, 0, 1, 6
|
||||
)
|
||||
|
||||
# Run rotation button
|
||||
@@ -283,9 +248,7 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
if s.diffraction.wavelength_angstrom is None:
|
||||
self.wavelength_label.setText("<b>N/A</b>")
|
||||
else:
|
||||
self.wavelength_label.setText(
|
||||
f"<b>{s.diffraction.wavelength_angstrom:.3f}</b>"
|
||||
)
|
||||
self.wavelength_label.setText(f"<b>{s.diffraction.wavelength_angstrom:.3f}</b>")
|
||||
self.update_calculated_labels()
|
||||
|
||||
@Slot(float)
|
||||
@@ -330,9 +293,7 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
total_angle = self.angular_range_enter.value
|
||||
d_vis = self.visible_res_enter.value
|
||||
image_angle = self.image_angle_enter.value
|
||||
d_vis = (
|
||||
d_vis - 0.2
|
||||
) # additional fudge factor that weights more towards high_res
|
||||
d_vis = d_vis - 0.2 # additional fudge factor that weights more towards high_res
|
||||
if d_vis <= 0.0:
|
||||
d_vis = 1.3
|
||||
d_tar = 1 / (1 / d_vis + 0.1)
|
||||
@@ -356,9 +317,7 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
multiplier_1 = max(self.xtal_y, beam_size_um_y)
|
||||
multiplier_2 = max(self.xtal_z, beam_size_um_y)
|
||||
new_beam_um_y = math.sqrt(multiplier_1 * multiplier_2)
|
||||
self.xtal_size_dose_rate_MGy_s = (
|
||||
flux / (beam_size_um_x * new_beam_um_y * Kdose)
|
||||
) / 1e6
|
||||
self.xtal_size_dose_rate_MGy_s = (flux / (beam_size_um_x * new_beam_um_y * Kdose)) / 1e6
|
||||
|
||||
else:
|
||||
self.xtal_size_dose_rate_MGy_s = self.dose_rate_MGy_s
|
||||
@@ -398,9 +357,7 @@ class SimpleRotationSettingsPanel(QWidget):
|
||||
if self.dtz <= 0.0:
|
||||
self.dtz_label.setText(f"""<span style="color: red ; ">-</span>""")
|
||||
elif self.dtz < self.__d.bl.dtz_min:
|
||||
self.dtz_label.setText(
|
||||
f"""<span style="color: red ; ">{self.dtz:.2f}</span>"""
|
||||
)
|
||||
self.dtz_label.setText(f"""<span style="color: red ; ">{self.dtz:.2f}</span>""")
|
||||
self.dtz = self.__d.bl.dtz_min
|
||||
else:
|
||||
self.dtz_label.setText(f"{self.dtz:.2f}")
|
||||
|
||||
@@ -215,52 +215,31 @@ class TargetStabilityPanel(QWidget):
|
||||
self.help_button.clicked.connect(self._show_metrics_help)
|
||||
|
||||
self.score_basis_combo = QComboBox()
|
||||
self.score_basis_combo.addItems(
|
||||
[self.SCORE_FROM_STEP_XY, self.SCORE_FROM_SIGMA_XY]
|
||||
)
|
||||
self.score_basis_combo.addItems([self.SCORE_FROM_STEP_XY, self.SCORE_FROM_SIGMA_XY])
|
||||
self.score_basis_combo.setCurrentText(self.SCORE_FROM_STEP_XY)
|
||||
self.score_basis_combo.currentTextChanged.connect(self._on_score_basis_changed)
|
||||
|
||||
self.wheel_mode_combo = QComboBox()
|
||||
self.wheel_mode_combo.addItems(
|
||||
[
|
||||
self.WHEEL_X,
|
||||
self.WHEEL_LEFT_Y,
|
||||
self.WHEEL_RIGHT_Y,
|
||||
self.WHEEL_SCORE,
|
||||
]
|
||||
[self.WHEEL_X, self.WHEEL_LEFT_Y, self.WHEEL_RIGHT_Y, self.WHEEL_SCORE]
|
||||
)
|
||||
self.wheel_mode_combo.setCurrentText(self.WHEEL_X)
|
||||
self.wheel_mode_combo.currentTextChanged.connect(
|
||||
lambda _text: self._update_status_label()
|
||||
)
|
||||
self.wheel_mode_combo.currentTextChanged.connect(lambda _text: self._update_status_label())
|
||||
|
||||
self.pan_mode_combo = QComboBox()
|
||||
self.pan_mode_combo.addItems(
|
||||
[
|
||||
self.PAN_ALL,
|
||||
self.PAN_X,
|
||||
self.PAN_LEFT_Y,
|
||||
self.PAN_RIGHT_Y,
|
||||
self.PAN_SCORE,
|
||||
]
|
||||
[self.PAN_ALL, self.PAN_X, self.PAN_LEFT_Y, self.PAN_RIGHT_Y, self.PAN_SCORE]
|
||||
)
|
||||
self.pan_mode_combo.setCurrentText(self.PAN_ALL)
|
||||
self.pan_mode_combo.currentTextChanged.connect(
|
||||
lambda _text: self._update_status_label()
|
||||
)
|
||||
self.pan_mode_combo.currentTextChanged.connect(lambda _text: self._update_status_label())
|
||||
|
||||
self.status_label = QLabel("Waiting for target-point data...")
|
||||
self.status_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
self.metrics_label = QLabel(
|
||||
"Target: (-, -) | Beam: (-, -) | Distance: - px | Std dev: - px"
|
||||
)
|
||||
self.metrics_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter
|
||||
)
|
||||
self.metrics_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
self.metrics_label.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.metrics_label.setWordWrap(True)
|
||||
|
||||
@@ -346,9 +325,7 @@ class TargetStabilityPanel(QWidget):
|
||||
self.chart.addSeries(series)
|
||||
|
||||
self.chart.legend().setVisible(True)
|
||||
self.chart.setTitle(
|
||||
"Target stability, distance, score, and step motion (last 60 s)"
|
||||
)
|
||||
self.chart.setTitle("Target stability, distance, score, and step motion (last 60 s)")
|
||||
|
||||
self.axis_x = QValueAxis()
|
||||
self.axis_x.setTitleText("Time [s ago]")
|
||||
@@ -371,12 +348,7 @@ class TargetStabilityPanel(QWidget):
|
||||
self.chart.addAxis(self.axis_y_distance, Qt.AlignmentFlag.AlignRight)
|
||||
self.chart.addAxis(self.axis_y_score, Qt.AlignmentFlag.AlignRight)
|
||||
|
||||
for series in (
|
||||
self.series,
|
||||
self.sigma_x_series,
|
||||
self.sigma_y_series,
|
||||
self.step_series,
|
||||
):
|
||||
for series in (self.series, self.sigma_x_series, self.sigma_y_series, self.step_series):
|
||||
series.attachAxis(self.axis_x)
|
||||
series.attachAxis(self.axis_y)
|
||||
|
||||
@@ -486,9 +458,7 @@ class TargetStabilityPanel(QWidget):
|
||||
)
|
||||
|
||||
def _connect_series_selection(self, series: QLineSeries, target_name: str) -> None:
|
||||
series.clicked.connect(
|
||||
lambda _point, target=target_name: self._select_trace_target(target)
|
||||
)
|
||||
series.clicked.connect(lambda _point, target=target_name: self._select_trace_target(target))
|
||||
|
||||
def _try_select_series_at_point(self, view_pos: QPoint) -> bool:
|
||||
"""Check if click is near a visible series point and select it. Returns True if found."""
|
||||
@@ -545,9 +515,7 @@ class TargetStabilityPanel(QWidget):
|
||||
def _set_paused(self, paused: bool) -> None:
|
||||
self._paused = paused
|
||||
if paused:
|
||||
self.pause_button.setText(
|
||||
"Resume (Live)" if self._collected_samples else "Resume"
|
||||
)
|
||||
self.pause_button.setText("Resume (Live)" if self._collected_samples else "Resume")
|
||||
else:
|
||||
self._unfreeze_plot()
|
||||
self.pause_button.setText("Pause")
|
||||
@@ -613,11 +581,7 @@ class TargetStabilityPanel(QWidget):
|
||||
def _current_sigma_stats(self) -> dict[str, float]:
|
||||
n = self._rolling_count
|
||||
if n < 2:
|
||||
return {
|
||||
"std_dx": 0.0,
|
||||
"std_dy": 0.0,
|
||||
"sigma": 0.0,
|
||||
}
|
||||
return {"std_dx": 0.0, "std_dy": 0.0, "sigma": 0.0}
|
||||
|
||||
mean_dx = self._rolling_sum_dx / n
|
||||
mean_dy = self._rolling_sum_dy / n
|
||||
@@ -628,30 +592,16 @@ class TargetStabilityPanel(QWidget):
|
||||
std_dy = math.sqrt(var_dy)
|
||||
sigma = math.hypot(std_dx, std_dy)
|
||||
|
||||
return {
|
||||
"std_dx": std_dx,
|
||||
"std_dy": std_dy,
|
||||
"sigma": sigma,
|
||||
}
|
||||
return {"std_dx": std_dx, "std_dy": std_dy, "sigma": sigma}
|
||||
|
||||
def _current_step_jitter_stats(self) -> dict[str, float]:
|
||||
if self._rolling_step_count <= 0:
|
||||
return {
|
||||
"rms_step_dx": 0.0,
|
||||
"rms_step_dy": 0.0,
|
||||
"rms_step_xy": 0.0,
|
||||
}
|
||||
return {"rms_step_dx": 0.0, "rms_step_dy": 0.0, "rms_step_xy": 0.0}
|
||||
|
||||
return {
|
||||
"rms_step_dx": math.sqrt(
|
||||
self._rolling_step_dx2_sum / self._rolling_step_count
|
||||
),
|
||||
"rms_step_dy": math.sqrt(
|
||||
self._rolling_step_dy2_sum / self._rolling_step_count
|
||||
),
|
||||
"rms_step_xy": math.sqrt(
|
||||
self._rolling_step_xy2_sum / self._rolling_step_count
|
||||
),
|
||||
"rms_step_dx": math.sqrt(self._rolling_step_dx2_sum / self._rolling_step_count),
|
||||
"rms_step_dy": math.sqrt(self._rolling_step_dy2_sum / self._rolling_step_count),
|
||||
"rms_step_xy": math.sqrt(self._rolling_step_xy2_sum / self._rolling_step_count),
|
||||
}
|
||||
|
||||
def _current_score_value(self) -> float:
|
||||
@@ -664,10 +614,7 @@ class TargetStabilityPanel(QWidget):
|
||||
|
||||
def _update_metrics_label(self, force: bool = False) -> None:
|
||||
now = time.monotonic()
|
||||
if (
|
||||
not force
|
||||
and (now - self._last_metrics_update_ts) < self._metrics_update_interval_s
|
||||
):
|
||||
if not force and (now - self._last_metrics_update_ts) < self._metrics_update_interval_s:
|
||||
return
|
||||
self._last_metrics_update_ts = now
|
||||
|
||||
@@ -849,12 +796,7 @@ class TargetStabilityPanel(QWidget):
|
||||
dy = ty - by
|
||||
distance = math.hypot(dx, dy)
|
||||
|
||||
sample = {
|
||||
"ts": ts,
|
||||
"dx": dx,
|
||||
"dy": dy,
|
||||
"distance": distance,
|
||||
}
|
||||
sample = {"ts": ts, "dx": dx, "dy": dy, "distance": distance}
|
||||
|
||||
should_finish_collection = False
|
||||
with self._data_lock:
|
||||
@@ -1033,9 +975,7 @@ class TargetStabilityPanel(QWidget):
|
||||
while self._plot_points and float(self._plot_points[0]["ts"]) < cutoff:
|
||||
self._plot_points.popleft()
|
||||
|
||||
def _data_limits(
|
||||
self,
|
||||
) -> tuple[float, float, float, float, float, float, float, float]:
|
||||
def _data_limits(self) -> tuple[float, float, float, float, float, float, float, float]:
|
||||
left_values: list[float] = []
|
||||
right_values: list[float] = []
|
||||
|
||||
@@ -1048,13 +988,7 @@ class TargetStabilityPanel(QWidget):
|
||||
float(point["step_xy"]),
|
||||
]
|
||||
)
|
||||
right_values.extend(
|
||||
[
|
||||
float(point["distance"]),
|
||||
float(point["dx"]),
|
||||
float(point["dy"]),
|
||||
]
|
||||
)
|
||||
right_values.extend([float(point["distance"]), float(point["dx"]), float(point["dy"])])
|
||||
|
||||
left_min = min(left_values, default=0.0)
|
||||
left_max = max(left_values, default=1.0)
|
||||
@@ -1064,12 +998,8 @@ class TargetStabilityPanel(QWidget):
|
||||
left_span = max(1.0, left_max - left_min)
|
||||
right_span = max(1.0, right_max - right_min)
|
||||
|
||||
score_min = min(
|
||||
(float(point["score"]) for point in self._plot_points), default=0.0
|
||||
)
|
||||
score_max = max(
|
||||
(float(point["score"]) for point in self._plot_points), default=100.0
|
||||
)
|
||||
score_min = min((float(point["score"]) for point in self._plot_points), default=0.0)
|
||||
score_max = max((float(point["score"]) for point in self._plot_points), default=100.0)
|
||||
score_lo = max(0.0, score_min - 5.0)
|
||||
score_hi = min(100.0, max(score_lo + 10.0, score_max + 5.0))
|
||||
|
||||
@@ -1140,9 +1070,7 @@ class TargetStabilityPanel(QWidget):
|
||||
self._clamp_axes()
|
||||
self._update_status_label()
|
||||
|
||||
def _pan_selected_axes(
|
||||
self, dx_pixels: int, dy_pixels: int, width: int, height: int
|
||||
) -> None:
|
||||
def _pan_selected_axes(self, dx_pixels: int, dy_pixels: int, width: int, height: int) -> None:
|
||||
if width <= 0 or height <= 0:
|
||||
return
|
||||
|
||||
@@ -1166,9 +1094,7 @@ class TargetStabilityPanel(QWidget):
|
||||
mode = self.pan_mode_combo.currentText()
|
||||
|
||||
if mode == self.PAN_X:
|
||||
self.axis_x.setRange(
|
||||
self.axis_x.min() + x_shift, self.axis_x.max() + x_shift
|
||||
)
|
||||
self.axis_x.setRange(self.axis_x.min() + x_shift, self.axis_x.max() + x_shift)
|
||||
|
||||
elif mode == self.PAN_LEFT_Y:
|
||||
if (
|
||||
@@ -1177,9 +1103,7 @@ class TargetStabilityPanel(QWidget):
|
||||
or self.show_sigma_y_cb.isChecked()
|
||||
or self.show_step_cb.isChecked()
|
||||
):
|
||||
self.axis_y.setRange(
|
||||
self.axis_y.min() + y_shift, self.axis_y.max() + y_shift
|
||||
)
|
||||
self.axis_y.setRange(self.axis_y.min() + y_shift, self.axis_y.max() + y_shift)
|
||||
|
||||
elif mode == self.PAN_RIGHT_Y:
|
||||
if (
|
||||
@@ -1188,21 +1112,17 @@ class TargetStabilityPanel(QWidget):
|
||||
or self.show_dy_cb.isChecked()
|
||||
):
|
||||
self.axis_y_distance.setRange(
|
||||
self.axis_y_distance.min() + y2_shift,
|
||||
self.axis_y_distance.max() + y2_shift,
|
||||
self.axis_y_distance.min() + y2_shift, self.axis_y_distance.max() + y2_shift
|
||||
)
|
||||
|
||||
elif mode == self.PAN_SCORE:
|
||||
if self.show_score_cb.isChecked():
|
||||
self.axis_y_score.setRange(
|
||||
self.axis_y_score.min() + y3_shift,
|
||||
self.axis_y_score.max() + y3_shift,
|
||||
self.axis_y_score.min() + y3_shift, self.axis_y_score.max() + y3_shift
|
||||
)
|
||||
|
||||
else:
|
||||
self.axis_x.setRange(
|
||||
self.axis_x.min() + x_shift, self.axis_x.max() + x_shift
|
||||
)
|
||||
self.axis_x.setRange(self.axis_x.min() + x_shift, self.axis_x.max() + x_shift)
|
||||
|
||||
if (
|
||||
self.show_sigma_cb.isChecked()
|
||||
@@ -1210,9 +1130,7 @@ class TargetStabilityPanel(QWidget):
|
||||
or self.show_sigma_y_cb.isChecked()
|
||||
or self.show_step_cb.isChecked()
|
||||
):
|
||||
self.axis_y.setRange(
|
||||
self.axis_y.min() + y_shift, self.axis_y.max() + y_shift
|
||||
)
|
||||
self.axis_y.setRange(self.axis_y.min() + y_shift, self.axis_y.max() + y_shift)
|
||||
|
||||
if (
|
||||
self.show_distance_cb.isChecked()
|
||||
@@ -1220,14 +1138,12 @@ class TargetStabilityPanel(QWidget):
|
||||
or self.show_dy_cb.isChecked()
|
||||
):
|
||||
self.axis_y_distance.setRange(
|
||||
self.axis_y_distance.min() + y2_shift,
|
||||
self.axis_y_distance.max() + y2_shift,
|
||||
self.axis_y_distance.min() + y2_shift, self.axis_y_distance.max() + y2_shift
|
||||
)
|
||||
|
||||
if self.show_score_cb.isChecked():
|
||||
self.axis_y_score.setRange(
|
||||
self.axis_y_score.min() + y3_shift,
|
||||
self.axis_y_score.max() + y3_shift,
|
||||
self.axis_y_score.min() + y3_shift, self.axis_y_score.max() + y3_shift
|
||||
)
|
||||
|
||||
self._clamp_axes()
|
||||
@@ -1273,11 +1189,7 @@ class TargetStabilityPanel(QWidget):
|
||||
self._update_metrics_label()
|
||||
return
|
||||
|
||||
if (
|
||||
self._paused
|
||||
and self._collecting_until is None
|
||||
and self._frozen_plot_points is None
|
||||
):
|
||||
if self._paused and self._collecting_until is None and self._frozen_plot_points is None:
|
||||
self._update_status_label()
|
||||
self._update_metrics_label()
|
||||
return
|
||||
@@ -1292,36 +1204,28 @@ class TargetStabilityPanel(QWidget):
|
||||
self._chart_dirty = False
|
||||
|
||||
sigma_points = [
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["sigma"]))
|
||||
for point in plot_data
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["sigma"])) for point in plot_data
|
||||
]
|
||||
sigma_x_points = [
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["sigma_x"]))
|
||||
for point in plot_data
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["sigma_x"])) for point in plot_data
|
||||
]
|
||||
sigma_y_points = [
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["sigma_y"]))
|
||||
for point in plot_data
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["sigma_y"])) for point in plot_data
|
||||
]
|
||||
distance_points = [
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["distance"]))
|
||||
for point in plot_data
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["distance"])) for point in plot_data
|
||||
]
|
||||
dx_points = [
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["dx"]))
|
||||
for point in plot_data
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["dx"])) for point in plot_data
|
||||
]
|
||||
dy_points = [
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["dy"]))
|
||||
for point in plot_data
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["dy"])) for point in plot_data
|
||||
]
|
||||
score_points = [
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["score"]))
|
||||
for point in plot_data
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["score"])) for point in plot_data
|
||||
]
|
||||
step_points = [
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["step_xy"]))
|
||||
for point in plot_data
|
||||
QPointF(float(point["ts"]) - ref_time, float(point["step_xy"])) for point in plot_data
|
||||
]
|
||||
|
||||
self.series.replace(sigma_points)
|
||||
@@ -1334,16 +1238,9 @@ class TargetStabilityPanel(QWidget):
|
||||
self.step_series.replace(step_points)
|
||||
|
||||
if self._auto_scale_enabled:
|
||||
(
|
||||
xmin,
|
||||
xmax,
|
||||
left_ymin,
|
||||
left_ymax,
|
||||
right_ymin,
|
||||
right_ymax,
|
||||
score_lo,
|
||||
score_hi,
|
||||
) = self._data_limits()
|
||||
(xmin, xmax, left_ymin, left_ymax, right_ymin, right_ymax, score_lo, score_hi) = (
|
||||
self._data_limits()
|
||||
)
|
||||
self.axis_x.setRange(xmin, xmax)
|
||||
self.axis_y.setRange(left_ymin, left_ymax)
|
||||
self.axis_y_distance.setRange(right_ymin, right_ymax)
|
||||
@@ -1358,9 +1255,7 @@ class TargetStabilityPanel(QWidget):
|
||||
seconds = float(self.seconds_spin.value())
|
||||
now = time.monotonic()
|
||||
rows = [s for s in self._samples if float(s["ts"]) >= now - seconds]
|
||||
self._save_rows(
|
||||
rows, suggested_name=f"target_stability_last_{self._seconds_text()}s.csv"
|
||||
)
|
||||
self._save_rows(rows, suggested_name=f"target_stability_last_{self._seconds_text()}s.csv")
|
||||
|
||||
def _collect_next_x_seconds(self) -> None:
|
||||
if self._paused:
|
||||
@@ -1413,14 +1308,8 @@ class TargetStabilityPanel(QWidget):
|
||||
if temp_rolling_count >= 2:
|
||||
mean_dx = temp_rolling_sum_dx / temp_rolling_count
|
||||
mean_dy = temp_rolling_sum_dy / temp_rolling_count
|
||||
var_dx = max(
|
||||
0.0,
|
||||
(temp_rolling_sum_dx2 / temp_rolling_count) - (mean_dx * mean_dx),
|
||||
)
|
||||
var_dy = max(
|
||||
0.0,
|
||||
(temp_rolling_sum_dy2 / temp_rolling_count) - (mean_dy * mean_dy),
|
||||
)
|
||||
var_dx = max(0.0, (temp_rolling_sum_dx2 / temp_rolling_count) - (mean_dx * mean_dx))
|
||||
var_dy = max(0.0, (temp_rolling_sum_dy2 / temp_rolling_count) - (mean_dy * mean_dy))
|
||||
std_dx = math.sqrt(var_dx)
|
||||
std_dy = math.sqrt(var_dy)
|
||||
sigma = math.hypot(std_dx, std_dy)
|
||||
@@ -1474,8 +1363,7 @@ class TargetStabilityPanel(QWidget):
|
||||
# This avoids blocking while any locks might be held
|
||||
file_name = f"target_stability_collected_{self._seconds_text()}s.csv"
|
||||
QTimer.singleShot(
|
||||
0,
|
||||
lambda: self._save_rows(self._collected_samples, suggested_name=file_name),
|
||||
0, lambda: self._save_rows(self._collected_samples, suggested_name=file_name)
|
||||
)
|
||||
self._update_status_label()
|
||||
|
||||
@@ -1491,10 +1379,7 @@ class TargetStabilityPanel(QWidget):
|
||||
return
|
||||
|
||||
path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Save Target Stability Data",
|
||||
suggested_name,
|
||||
"CSV files (*.csv)",
|
||||
self, "Save Target Stability Data", suggested_name, "CSV files (*.csv)"
|
||||
)
|
||||
if not path:
|
||||
return
|
||||
|
||||
@@ -59,19 +59,13 @@ class TellSamplePanel(QFrame):
|
||||
|
||||
self.table_model = UserSampleSpreadsheet(samples=samples.s)
|
||||
self.table_view.setModel(self.table_model)
|
||||
self.table_view.horizontalHeader().setSectionResizeMode(
|
||||
QHeaderView.ResizeMode.Stretch
|
||||
)
|
||||
self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table_view.customContextMenuRequested.connect(self.context_menu)
|
||||
self.table_view.setSelectionBehavior(
|
||||
QAbstractItemView.SelectionBehavior.SelectRows
|
||||
)
|
||||
self.table_view.setSelectionMode(
|
||||
QAbstractItemView.SelectionMode.ExtendedSelection
|
||||
)
|
||||
self.table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.table_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
self.table_view.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
|
||||
self.table_view.setDragEnabled(True)
|
||||
@@ -187,9 +181,7 @@ class TellSamplePanel(QFrame):
|
||||
toggle_all.setChecked(self.table_model.show_all_pgroups)
|
||||
|
||||
def _toggle_all():
|
||||
self.table_model.set_show_all_pgroups(
|
||||
not self.table_model.show_all_pgroups
|
||||
)
|
||||
self.table_model.set_show_all_pgroups(not self.table_model.show_all_pgroups)
|
||||
|
||||
toggle_all.triggered.connect(_toggle_all)
|
||||
|
||||
@@ -200,9 +192,7 @@ class TellSamplePanel(QFrame):
|
||||
if action == set_filter_action:
|
||||
from PySide6.QtWidgets import QInputDialog
|
||||
|
||||
text, ok = QInputDialog.getText(
|
||||
self, "Set filter", f"Filter for '{col_name}':"
|
||||
)
|
||||
text, ok = QInputDialog.getText(self, "Set filter", f"Filter for '{col_name}':")
|
||||
if ok:
|
||||
self.table_model.set_column_filter(logical_index, text)
|
||||
elif action == clear_filter_action:
|
||||
@@ -218,9 +208,7 @@ class TellSamplePanel(QFrame):
|
||||
tell_details = ""
|
||||
if tell_state is not None:
|
||||
activity = tell_state.activity.display_name()
|
||||
phase = (
|
||||
tell_state.phase.display_name() if tell_state.phase is not None else ""
|
||||
)
|
||||
phase = tell_state.phase.display_name() if tell_state.phase is not None else ""
|
||||
message = (tell_state.message or "").strip()
|
||||
|
||||
tell_parts = [activity]
|
||||
@@ -237,9 +225,7 @@ class TellSamplePanel(QFrame):
|
||||
else:
|
||||
try:
|
||||
if sample.location is None:
|
||||
base_text = (
|
||||
f"Current sample: <b>{sample.sample_name} (Manual mount)</b>"
|
||||
)
|
||||
base_text = f"Current sample: <b>{sample.sample_name} (Manual mount)</b>"
|
||||
else:
|
||||
base_text = (
|
||||
f"Current sample: <b>{sample.sample_name} "
|
||||
|
||||
@@ -43,8 +43,6 @@ class ZoomPanel(QWidget):
|
||||
self.__zoom = status.bl.zoom
|
||||
for z in self.__zoom_settings:
|
||||
if z["value"] == self.__zoom:
|
||||
self.__buttons[self.__zoom_settings.index(z)].setStyleSheet(
|
||||
"font-weight: bold;"
|
||||
)
|
||||
self.__buttons[self.__zoom_settings.index(z)].setStyleSheet("font-weight: bold;")
|
||||
else:
|
||||
self.__buttons[self.__zoom_settings.index(z)].setStyleSheet("")
|
||||
|
||||
@@ -136,9 +136,7 @@ class RasterGridManager(QObject):
|
||||
n_x=0,
|
||||
n_y=0,
|
||||
smargon_top_left=self.__geom.smargon,
|
||||
grid_size_mm=Coordinate(
|
||||
x=0.8 * self.__beam_size_mm.x, y=0.8 * self.__beam_size_mm.y
|
||||
),
|
||||
grid_size_mm=Coordinate(x=0.8 * self.__beam_size_mm.x, y=0.8 * self.__beam_size_mm.y),
|
||||
omega_deg=self.__geom.omega_deg,
|
||||
exp_time_s=0.02,
|
||||
transmission=1.0,
|
||||
@@ -150,9 +148,7 @@ class RasterGridManager(QObject):
|
||||
# Each entry is (QImage, backing ndarray); the ndarray must be kept alive
|
||||
# because QImage shares its buffer without copying. Rebuilt only when the
|
||||
# data or metric changes, not on every repaint (sample move / zoom).
|
||||
self.__heatmap_cache: dict[
|
||||
tuple[int, "RasterGridMetric"], tuple[QImage, np.ndarray]
|
||||
] = {}
|
||||
self.__heatmap_cache: dict[tuple[int, "RasterGridMetric"], tuple[QImage, np.ndarray]] = {}
|
||||
|
||||
@property
|
||||
def active_grid(self) -> RasterGridRequest:
|
||||
@@ -194,12 +190,7 @@ class RasterGridManager(QObject):
|
||||
return None
|
||||
|
||||
start_x, start_y, cell_w, cell_h = geo
|
||||
return QRectF(
|
||||
start_x,
|
||||
start_y,
|
||||
cell_w * grid.n_x,
|
||||
cell_h * grid.n_y,
|
||||
)
|
||||
return QRectF(start_x, start_y, cell_w * grid.n_x, cell_h * grid.n_y)
|
||||
|
||||
def _visible_index_range(
|
||||
self,
|
||||
@@ -214,13 +205,9 @@ class RasterGridManager(QObject):
|
||||
return 0, grid.n_x, 0, grid.n_y
|
||||
|
||||
min_x = max(0, int(math.floor((visible_rect.left() - start_x) / cell_w)) - 1)
|
||||
max_x = min(
|
||||
grid.n_x, int(math.ceil((visible_rect.right() - start_x) / cell_w)) + 1
|
||||
)
|
||||
max_x = min(grid.n_x, int(math.ceil((visible_rect.right() - start_x) / cell_w)) + 1)
|
||||
min_y = max(0, int(math.floor((visible_rect.top() - start_y) / cell_h)) - 1)
|
||||
max_y = min(
|
||||
grid.n_y, int(math.ceil((visible_rect.bottom() - start_y) / cell_h)) + 1
|
||||
)
|
||||
max_y = min(grid.n_y, int(math.ceil((visible_rect.bottom() - start_y) / cell_h)) + 1)
|
||||
|
||||
return min_x, max_x, min_y, max_y
|
||||
|
||||
@@ -246,9 +233,7 @@ class RasterGridManager(QObject):
|
||||
self.clear_active_grid()
|
||||
if self.__beam_size_mm != s.geom.beam_size_mm:
|
||||
self.__beam_size_mm = s.geom.beam_size_mm
|
||||
self.update_grid_size(
|
||||
0.8 * self.__beam_size_mm.x, 0.8 * self.__beam_size_mm.y
|
||||
)
|
||||
self.update_grid_size(0.8 * self.__beam_size_mm.x, 0.8 * self.__beam_size_mm.y)
|
||||
self.__geom = s.geom
|
||||
|
||||
def resize_active_grid(self, end_point: QPointF):
|
||||
@@ -306,15 +291,10 @@ class RasterGridManager(QObject):
|
||||
self.__active_grid.n_x = 0
|
||||
self.__active_grid.n_y = 0
|
||||
self.grid_scan_size_changed.emit(
|
||||
0,
|
||||
0,
|
||||
self.__active_grid.grid_size_mm.x,
|
||||
self.__active_grid.grid_size_mm.y,
|
||||
0, 0, self.__active_grid.grid_size_mm.x, self.__active_grid.grid_size_mm.y
|
||||
)
|
||||
|
||||
def get_grid_coord(
|
||||
self, grid: RasterGridRequest, point: QPointF
|
||||
) -> Tuple[int, int]:
|
||||
def get_grid_coord(self, grid: RasterGridRequest, point: QPointF) -> Tuple[int, int]:
|
||||
point_bl = self.__geom.picture_to_sample(Coordinate(x=point.x(), y=point.y()))
|
||||
|
||||
delta = point_bl - self.__geom.smargon_to_beamline(grid.smargon_top_left.sh_mm)
|
||||
@@ -363,9 +343,7 @@ class RasterGridManager(QObject):
|
||||
delta = point_bl - self.__geom.smargon_to_beamline(
|
||||
self.__active_grid.smargon_top_left.sh_mm
|
||||
)
|
||||
return (
|
||||
0 <= delta.x < self.__active_grid.n_x * self.__active_grid.grid_size_mm.x
|
||||
) and (
|
||||
return (0 <= delta.x < self.__active_grid.n_x * self.__active_grid.grid_size_mm.x) and (
|
||||
0 <= delta.y < self.__active_grid.n_y * self.__active_grid.grid_size_mm.y
|
||||
)
|
||||
|
||||
@@ -432,9 +410,7 @@ class RasterGridManager(QObject):
|
||||
grid.result.images[cell].pr is not None
|
||||
and grid.result.images[cell].pr >= 0
|
||||
):
|
||||
txt += (
|
||||
f"Profile Radius {grid.result.images[cell].pr:.2f}<br/>"
|
||||
)
|
||||
txt += f"Profile Radius {grid.result.images[cell].pr:.2f}<br/>"
|
||||
if (
|
||||
grid.result.images[cell].nx is not None
|
||||
and grid.result.images[cell].ny is not None
|
||||
@@ -537,15 +513,11 @@ class RasterGridManager(QObject):
|
||||
case RasterGridMetric.INDEXING:
|
||||
v = [obj.index for obj in i.result.images]
|
||||
case RasterGridMetric.PR:
|
||||
v = [
|
||||
obj.index / max(obj.spots_low_res, 1) for obj in i.result.images
|
||||
]
|
||||
v = [obj.index / max(obj.spots_low_res, 1) for obj in i.result.images]
|
||||
case RasterGridMetric.RASTER_SCORE:
|
||||
score = compute_crystal_score_array(i.result.images)
|
||||
v = [
|
||||
score[obj.nx, obj.ny]
|
||||
if obj.nx is not None and obj.ny is not None
|
||||
else None
|
||||
score[obj.nx, obj.ny] if obj.nx is not None and obj.ny is not None else None
|
||||
for obj in i.result.images
|
||||
]
|
||||
case RasterGridMetric.BFACTOR:
|
||||
@@ -558,9 +530,7 @@ class RasterGridManager(QObject):
|
||||
v = [self.spot_ice_ratio(obj) for obj in i.result.images]
|
||||
case RasterGridMetric.SPOTS_INDEXED:
|
||||
v = [obj.spots_indexed for obj in i.result.images]
|
||||
self._draw_completed_heatmap(
|
||||
painter, i, v, alpha, visible_rect, (id(i), self.__metric)
|
||||
)
|
||||
self._draw_completed_heatmap(painter, i, v, alpha, visible_rect, (id(i), self.__metric))
|
||||
|
||||
def _invalidate_heatmap_cache(self) -> None:
|
||||
"""Drop all cached heatmap bitmaps. Call whenever the completed-grid data
|
||||
@@ -568,10 +538,7 @@ class RasterGridManager(QObject):
|
||||
self.__heatmap_cache.clear()
|
||||
|
||||
def _heatmap_image(
|
||||
self,
|
||||
cache_key: tuple,
|
||||
grid: RasterGridRequest,
|
||||
values: List[float] | List[int],
|
||||
self, cache_key: tuple, grid: RasterGridRequest, values: List[float] | List[int]
|
||||
) -> QImage | None:
|
||||
"""Return a cached n_x*n_y heatmap bitmap for this grid, building it once
|
||||
on a cache miss. One pixel per cell; colours baked at full opacity with
|
||||
@@ -589,10 +556,7 @@ class RasterGridManager(QObject):
|
||||
return built[0]
|
||||
|
||||
def _build_heatmap_image(
|
||||
self,
|
||||
n_x: int,
|
||||
n_y: int,
|
||||
values: List[float] | List[int],
|
||||
self, n_x: int, n_y: int, values: List[float] | List[int]
|
||||
) -> tuple[QImage, np.ndarray] | None:
|
||||
if n_x <= 0 or n_y <= 0:
|
||||
return None
|
||||
@@ -601,10 +565,7 @@ class RasterGridManager(QObject):
|
||||
|
||||
# Cell values are laid out row-major: idx = x + y * n_x (matches the
|
||||
# per-cell loop and load_image()/get_grid_coord() hit-testing).
|
||||
flat = np.array(
|
||||
[np.nan if v is None else v for v in values[:count]],
|
||||
dtype=np.float64,
|
||||
)
|
||||
flat = np.array([np.nan if v is None else v for v in values[:count]], dtype=np.float64)
|
||||
if flat.size < count:
|
||||
flat = np.concatenate([flat, np.full(count - flat.size, np.nan)])
|
||||
|
||||
@@ -673,9 +634,7 @@ class RasterGridManager(QObject):
|
||||
painter.drawImage(bounds, image)
|
||||
painter.setOpacity(1.0)
|
||||
|
||||
painter.setPen(
|
||||
QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine)
|
||||
)
|
||||
painter.setPen(QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine))
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.drawRect(bounds)
|
||||
painter.restore()
|
||||
@@ -715,9 +674,7 @@ class RasterGridManager(QObject):
|
||||
return
|
||||
|
||||
if values is not None and (cell_w < 3.0 or cell_h < 3.0):
|
||||
if self._draw_completed_grid_fast(
|
||||
painter, grid, values, alpha, visible_rect
|
||||
):
|
||||
if self._draw_completed_grid_fast(painter, grid, values, alpha, visible_rect):
|
||||
return
|
||||
|
||||
painter.save()
|
||||
@@ -725,12 +682,8 @@ class RasterGridManager(QObject):
|
||||
|
||||
if values is not None:
|
||||
painter.setPen(Qt.PenStyle.NoPen)
|
||||
min_value = min(
|
||||
(x for x in values if x is not None and not math.isnan(x)), default=0
|
||||
)
|
||||
max_value = max(
|
||||
(x for x in values if x is not None and not math.isnan(x)), default=1
|
||||
)
|
||||
min_value = min((x for x in values if x is not None and not math.isnan(x)), default=0)
|
||||
max_value = max((x for x in values if x is not None and not math.isnan(x)), default=1)
|
||||
diff = 1 if min_value == max_value else (max_value - min_value)
|
||||
else:
|
||||
painter.setPen(QPen(QColor(114, 159, 207), 1, Qt.PenStyle.SolidLine))
|
||||
@@ -749,9 +702,7 @@ class RasterGridManager(QObject):
|
||||
|
||||
if values is None:
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.drawRect(
|
||||
QRect(round(px), round(py), round(cell_w), round(cell_h))
|
||||
)
|
||||
painter.drawRect(QRect(round(px), round(py), round(cell_w), round(cell_h)))
|
||||
continue
|
||||
|
||||
idx = x + y * grid.n_x
|
||||
@@ -763,22 +714,14 @@ class RasterGridManager(QObject):
|
||||
):
|
||||
continue
|
||||
|
||||
brush = float_to_viridis_brush(
|
||||
(values[idx] - min_value) / diff, alpha=alpha
|
||||
)
|
||||
painter.fillRect(
|
||||
QRectF(px, py, max(1.0, cell_w), max(1.0, cell_h)), brush
|
||||
)
|
||||
brush = float_to_viridis_brush((values[idx] - min_value) / diff, alpha=alpha)
|
||||
painter.fillRect(QRectF(px, py, max(1.0, cell_w), max(1.0, cell_h)), brush)
|
||||
|
||||
if values is None:
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
else:
|
||||
painter.setPen(
|
||||
QPen(
|
||||
QColor(114, 159, 207, min(255, alpha + 40)),
|
||||
1,
|
||||
Qt.PenStyle.SolidLine,
|
||||
)
|
||||
QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine)
|
||||
)
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.drawRect(bounds)
|
||||
@@ -786,10 +729,7 @@ class RasterGridManager(QObject):
|
||||
painter.restore()
|
||||
|
||||
def _draw_active_grid_fast(
|
||||
self,
|
||||
painter: QPainter,
|
||||
grid: RasterGridRequest,
|
||||
visible_rect: QRectF | None,
|
||||
self, painter: QPainter, grid: RasterGridRequest, visible_rect: QRectF | None
|
||||
) -> bool:
|
||||
geo = self._grid_pixel_geometry(grid)
|
||||
if geo is None:
|
||||
@@ -869,9 +809,7 @@ class RasterGridManager(QObject):
|
||||
):
|
||||
return True
|
||||
|
||||
valid_values = [
|
||||
x for x in values if x is not None and not math.isnan(x) and x >= 0
|
||||
]
|
||||
valid_values = [x for x in values if x is not None and not math.isnan(x) and x >= 0]
|
||||
min_value = min(valid_values, default=0)
|
||||
max_value = max(valid_values, default=1)
|
||||
diff = 1 if min_value == max_value else (max_value - min_value)
|
||||
@@ -897,12 +835,7 @@ class RasterGridManager(QObject):
|
||||
draw_w = max(1.0, cell_w * stride_x)
|
||||
|
||||
value = self._block_value(
|
||||
values,
|
||||
grid.n_x,
|
||||
x,
|
||||
min(x + stride_x, grid.n_x),
|
||||
y,
|
||||
min(y + stride_y, grid.n_y),
|
||||
values, grid.n_x, x, min(x + stride_x, grid.n_x), y, min(y + stride_y, grid.n_y)
|
||||
)
|
||||
if value is None:
|
||||
continue
|
||||
@@ -910,9 +843,7 @@ class RasterGridManager(QObject):
|
||||
brush = float_to_viridis_brush((value - min_value) / diff, alpha=alpha)
|
||||
painter.fillRect(QRectF(px, py, draw_w, draw_h), brush)
|
||||
|
||||
painter.setPen(
|
||||
QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine)
|
||||
)
|
||||
painter.setPen(QPen(QColor(114, 159, 207, min(255, alpha + 40)), 1, Qt.PenStyle.SolidLine))
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
painter.drawRect(bounds)
|
||||
|
||||
@@ -920,13 +851,7 @@ class RasterGridManager(QObject):
|
||||
return True
|
||||
|
||||
def _block_value(
|
||||
self,
|
||||
values: List[float] | List[int],
|
||||
grid_nx: int,
|
||||
x0: int,
|
||||
x1: int,
|
||||
y0: int,
|
||||
y1: int,
|
||||
self, values: List[float] | List[int], grid_nx: int, x0: int, x1: int, y0: int, y1: int
|
||||
) -> float | None:
|
||||
best = None
|
||||
for y in range(y0, y1):
|
||||
@@ -1004,12 +929,8 @@ class RasterGridManager(QObject):
|
||||
self.omega.emit(self.__completed_grids[row].request.omega_deg)
|
||||
self.smargon.emit(
|
||||
SmargonCoordinate(
|
||||
phi_deg=self.__completed_grids[
|
||||
row
|
||||
].request.smargon_top_left.phi_deg,
|
||||
chi_deg=self.__completed_grids[
|
||||
row
|
||||
].request.smargon_top_left.chi_deg,
|
||||
phi_deg=self.__completed_grids[row].request.smargon_top_left.phi_deg,
|
||||
chi_deg=self.__completed_grids[row].request.smargon_top_left.chi_deg,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1019,9 +940,7 @@ class RasterGridManager(QObject):
|
||||
src = self.__completed_grids[row].request
|
||||
self.__active_grid.n_x = src.n_x
|
||||
self.__active_grid.n_y = src.n_y
|
||||
self.__active_grid.grid_size_mm = Coordinate(
|
||||
x=src.grid_size_mm.x, y=src.grid_size_mm.y
|
||||
)
|
||||
self.__active_grid.grid_size_mm = Coordinate(x=src.grid_size_mm.x, y=src.grid_size_mm.y)
|
||||
self.__active_grid.smargon_top_left = SmargonCoordinate(
|
||||
sh_mm=Coordinate(
|
||||
x=src.smargon_top_left.sh_mm.x,
|
||||
|
||||
@@ -490,4 +490,4 @@ def _portrait_stylesheet() -> str:
|
||||
QWidget#portraitRoot QScrollBar::sub-line:vertical {
|
||||
height: 0px;
|
||||
}
|
||||
"""
|
||||
"""
|
||||
|
||||
@@ -41,13 +41,14 @@ class VideoThread(QThread):
|
||||
|
||||
if response.status_code != 200:
|
||||
self.error_occurred.emit(
|
||||
f"Could not connect to camera at {self.camera_ip}. Status: {response.status_code}")
|
||||
f"Could not connect to camera at {self.camera_ip}. Status: {response.status_code}"
|
||||
)
|
||||
return
|
||||
|
||||
self.running = True
|
||||
|
||||
# Buffer to accumulate data
|
||||
buffer = b''
|
||||
buffer = b""
|
||||
|
||||
# MJPEG boundary detection
|
||||
boundary = None
|
||||
@@ -61,9 +62,9 @@ class VideoThread(QThread):
|
||||
|
||||
# Find boundary on first iteration
|
||||
if boundary is None:
|
||||
boundary_start = buffer.find(b'--')
|
||||
boundary_start = buffer.find(b"--")
|
||||
if boundary_start != -1:
|
||||
boundary_end = buffer.find(b'\r\n', boundary_start)
|
||||
boundary_end = buffer.find(b"\r\n", boundary_start)
|
||||
if boundary_end != -1:
|
||||
boundary = buffer[boundary_start:boundary_end]
|
||||
|
||||
@@ -97,17 +98,17 @@ class VideoThread(QThread):
|
||||
continue
|
||||
|
||||
# Find the start of JPEG data (after headers)
|
||||
jpeg_start = part.find(b'\xff\xd8') # JPEG SOI marker
|
||||
jpeg_start = part.find(b"\xff\xd8") # JPEG SOI marker
|
||||
if jpeg_start == -1:
|
||||
continue
|
||||
|
||||
# Find the end of JPEG data
|
||||
jpeg_end = part.find(b'\xff\xd9', jpeg_start) # JPEG EOI marker
|
||||
jpeg_end = part.find(b"\xff\xd9", jpeg_start) # JPEG EOI marker
|
||||
if jpeg_end == -1:
|
||||
continue
|
||||
|
||||
# Extract JPEG data
|
||||
jpeg_data = part[jpeg_start:jpeg_end + 2]
|
||||
jpeg_data = part[jpeg_start : jpeg_end + 2]
|
||||
|
||||
try:
|
||||
# Decode JPEG using OpenCV
|
||||
@@ -121,7 +122,9 @@ class VideoThread(QThread):
|
||||
# Convert to QImage
|
||||
height, width, channel = rgb_frame.shape
|
||||
bytes_per_line = 3 * width
|
||||
qt_image = QImage(rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888)
|
||||
qt_image = QImage(
|
||||
rgb_frame.data, width, height, bytes_per_line, QImage.Format.Format_RGB888
|
||||
)
|
||||
|
||||
# Store as last good frame
|
||||
self.last_good_frame = qt_image
|
||||
@@ -153,4 +156,4 @@ class VideoThread(QThread):
|
||||
try:
|
||||
self.wait(5000)
|
||||
except Exception:
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -107,8 +107,7 @@ class SampleCameraThread(QThread):
|
||||
bgr = cv2.imdecode(encoded, cv2.IMREAD_COLOR)
|
||||
if bgr is None:
|
||||
self.__set_camera_available(
|
||||
False,
|
||||
"Sample camera feed unavailable: failed to decode JPEG frame",
|
||||
False, "Sample camera feed unavailable: failed to decode JPEG frame"
|
||||
)
|
||||
continue
|
||||
rgb = cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB)
|
||||
@@ -127,10 +126,7 @@ class SampleCameraThread(QThread):
|
||||
|
||||
if self.__measure_focus:
|
||||
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
|
||||
if (
|
||||
self.__focus_mask is None
|
||||
or self.__focus_mask.shape != gray.shape
|
||||
):
|
||||
if self.__focus_mask is None or self.__focus_mask.shape != gray.shape:
|
||||
height, width = gray.shape
|
||||
y, x = np.ogrid[:height, :width]
|
||||
self.__focus_mask = (x - self.__beam_x) ** 2 + (
|
||||
@@ -141,16 +137,12 @@ class SampleCameraThread(QThread):
|
||||
self.focus_measure.emit(sharpness)
|
||||
|
||||
qimage = QImage(
|
||||
rgb.data,
|
||||
rgb.shape[1],
|
||||
rgb.shape[0],
|
||||
QImage.Format.Format_RGB888,
|
||||
rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format.Format_RGB888
|
||||
).copy()
|
||||
self.camera_image.emit(QPixmap.fromImage(qimage))
|
||||
else:
|
||||
self.__set_camera_available(
|
||||
False,
|
||||
"Sample camera feed unavailable: no frame header in zmq stream",
|
||||
False, "Sample camera feed unavailable: no frame header in zmq stream"
|
||||
)
|
||||
except zmq.Again: # Timeout occurred
|
||||
now = time.perf_counter()
|
||||
@@ -165,14 +157,10 @@ class SampleCameraThread(QThread):
|
||||
self.__fps_frame_count = 0
|
||||
|
||||
if no_frames_long:
|
||||
self.__set_camera_available(
|
||||
False, "Sample camera feed unavailable"
|
||||
)
|
||||
self.__set_camera_available(False, "Sample camera feed unavailable")
|
||||
continue # Check self.running again
|
||||
except Exception as e:
|
||||
self.__set_camera_available(
|
||||
False, f"Sample camera feed unavailable: {e}"
|
||||
)
|
||||
self.__set_camera_available(False, f"Sample camera feed unavailable: {e}")
|
||||
self.running = False
|
||||
|
||||
def stop(self):
|
||||
|
||||
@@ -48,12 +48,7 @@ from aarecommon.recurrence_watcher import (
|
||||
)
|
||||
from jfjoch_client import ScanResult, ScanResultImagesInner
|
||||
from PySide6.QtCore import QByteArray, QObject, QTimer, QUrl, Signal, Slot
|
||||
from PySide6.QtNetwork import (
|
||||
QNetworkAccessManager,
|
||||
QNetworkReply,
|
||||
QNetworkRequest,
|
||||
QSslError,
|
||||
)
|
||||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest, QSslError
|
||||
|
||||
logger = setup_logger("aareGUI")
|
||||
|
||||
@@ -168,10 +163,7 @@ class DAQWorker(QObject):
|
||||
|
||||
self._device_error_log_min_interval_s = 10.0
|
||||
self._last_device_error_log_ts: dict[str, float] = {"tell": 0.0, "smargon": 0.0}
|
||||
self._last_device_error_log_key: dict[str, str | None] = {
|
||||
"tell": None,
|
||||
"smargon": None,
|
||||
}
|
||||
self._last_device_error_log_key: dict[str, str | None] = {"tell": None, "smargon": None}
|
||||
|
||||
self._last_status_error = None
|
||||
self._smargon_error_active = False
|
||||
@@ -256,9 +248,7 @@ class DAQWorker(QObject):
|
||||
self._last_detector_is_error = is_error
|
||||
self.detector_error.emit(msg, is_error)
|
||||
|
||||
def _emit_status_if_changed(
|
||||
self, key: str | None, message: str | None, is_error: bool
|
||||
) -> None:
|
||||
def _emit_status_if_changed(self, key: str | None, message: str | None, is_error: bool) -> None:
|
||||
"""
|
||||
Emit polled device status to the primary alert banner.
|
||||
Used for Server/Tell/Smargon/Aerotech connection status.
|
||||
@@ -428,11 +418,7 @@ class DAQWorker(QObject):
|
||||
|
||||
if disconnected:
|
||||
if len(disconnected) == 3:
|
||||
return (
|
||||
"all-devices-down",
|
||||
"TELL, Smargon, and Aerotech disconnected.",
|
||||
True,
|
||||
)
|
||||
return ("all-devices-down", "TELL, Smargon, and Aerotech disconnected.", True)
|
||||
if len(disconnected) == 2:
|
||||
return (
|
||||
"+".join(sorted(d.lower() for d in disconnected)) + "-down",
|
||||
@@ -440,11 +426,7 @@ class DAQWorker(QObject):
|
||||
True,
|
||||
)
|
||||
device = disconnected[0]
|
||||
return (
|
||||
f"{device.lower()}-down",
|
||||
f"{device} disconnected.",
|
||||
True,
|
||||
)
|
||||
return (f"{device.lower()}-down", f"{device} disconnected.", True)
|
||||
|
||||
if restored:
|
||||
if len(restored) == 3:
|
||||
@@ -468,9 +450,7 @@ class DAQWorker(QObject):
|
||||
parsed_response = DAQStatusModel.model_validate_json(response_data)
|
||||
self.update.emit(parsed_response)
|
||||
|
||||
pss_alarm = bool(
|
||||
getattr(getattr(parsed_response, "bl", None), "pss_alarm", False)
|
||||
)
|
||||
pss_alarm = bool(getattr(getattr(parsed_response, "bl", None), "pss_alarm", False))
|
||||
if pss_alarm != self._last_pss_alarm:
|
||||
self._last_pss_alarm = pss_alarm
|
||||
self.pss_alarm_changed.emit(pss_alarm)
|
||||
@@ -499,9 +479,7 @@ class DAQWorker(QObject):
|
||||
|
||||
tell_err_text = None if tell_err is None else str(tell_err).strip()
|
||||
smargon_err_text = None if smargon_err is None else str(smargon_err).strip()
|
||||
aerotech_err_text = (
|
||||
None if aerotech_err is None else str(aerotech_err).strip()
|
||||
)
|
||||
aerotech_err_text = None if aerotech_err is None else str(aerotech_err).strip()
|
||||
|
||||
# Skip device status processing if we just reconnected from server down
|
||||
# (we already showed "Server reconnected")
|
||||
@@ -520,8 +498,7 @@ class DAQWorker(QObject):
|
||||
return
|
||||
|
||||
tell_changed = (
|
||||
self._last_tell_connected != tell_conn
|
||||
or self._last_tell_error != tell_err_text
|
||||
self._last_tell_connected != tell_conn or self._last_tell_error != tell_err_text
|
||||
)
|
||||
smargon_changed = (
|
||||
self._last_smargon_connected != smargon_conn
|
||||
@@ -554,27 +531,19 @@ class DAQWorker(QObject):
|
||||
if not smargon_conn:
|
||||
self._log_device_error_throttled(device="smargon", message=smargon_err)
|
||||
if not aerotech_conn:
|
||||
self._log_device_error_throttled(
|
||||
device="aerotech", message=aerotech_err
|
||||
)
|
||||
self._log_device_error_throttled(device="aerotech", message=aerotech_err)
|
||||
|
||||
except Exception as e:
|
||||
status = None
|
||||
err_msg = str(e)
|
||||
|
||||
try:
|
||||
status = reply.attribute(
|
||||
QNetworkRequest.Attribute.HttpStatusCodeAttribute
|
||||
)
|
||||
status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
|
||||
raw_body = reply.readAll().data().decode("utf-8")
|
||||
if raw_body:
|
||||
body_json = json.loads(raw_body)
|
||||
if isinstance(body_json, dict):
|
||||
err_msg = (
|
||||
body_json.get("message")
|
||||
or body_json.get("detail")
|
||||
or err_msg
|
||||
)
|
||||
err_msg = body_json.get("message") or body_json.get("detail") or err_msg
|
||||
if body_json.get("code") == "AEROTECH_UNAVAILABLE":
|
||||
extra = body_json.get("extra") or {}
|
||||
if isinstance(extra, dict):
|
||||
@@ -594,18 +563,14 @@ class DAQWorker(QObject):
|
||||
self._has_seen_disconnection = True
|
||||
self._server_was_disconnected = True
|
||||
self._emit_status_if_changed(
|
||||
"server-down",
|
||||
f"Aerotech unavailable: {err_msg}",
|
||||
True,
|
||||
"server-down", f"Aerotech unavailable: {err_msg}", True
|
||||
)
|
||||
else:
|
||||
if self._server_connected is not False:
|
||||
self._has_seen_disconnection = True
|
||||
self._server_was_disconnected = True
|
||||
self._emit_status_if_changed(
|
||||
"server-down",
|
||||
"Server disconnected. Reconnecting...",
|
||||
True,
|
||||
"server-down", "Server disconnected. Reconnecting...", True
|
||||
)
|
||||
|
||||
self._server_connected = False
|
||||
@@ -686,9 +651,7 @@ class DAQWorker(QObject):
|
||||
else:
|
||||
logger.error(f"{error_info.message}")
|
||||
title = self._operation_error_title(error_info.exception_class)
|
||||
self.operation_failed.emit(
|
||||
title, error_info.message, error_info.critical
|
||||
)
|
||||
self.operation_failed.emit(title, error_info.message, error_info.critical)
|
||||
|
||||
reply.deleteLater()
|
||||
|
||||
@@ -732,9 +695,7 @@ class DAQWorker(QObject):
|
||||
logger.error(f"Hardware metadata resync failed: {e}")
|
||||
self.http_error.emit(str(e))
|
||||
|
||||
def _handle_recovery_action_response(
|
||||
self, reply: QNetworkReply, default_message: str
|
||||
):
|
||||
def _handle_recovery_action_response(self, reply: QNetworkReply, default_message: str):
|
||||
try:
|
||||
response_data = self.handle_response(reply)
|
||||
payload = json.loads(response_data) if response_data else {}
|
||||
@@ -905,9 +866,7 @@ class DAQWorker(QObject):
|
||||
body = json.dumps({"confirmation_code": confirmation_code})
|
||||
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_recovery_action_response(
|
||||
reply, "Beamline busy flag cleared."
|
||||
)
|
||||
lambda: self._handle_recovery_action_response(reply, "Beamline busy flag cleared.")
|
||||
)
|
||||
|
||||
@Slot(str)
|
||||
@@ -922,9 +881,7 @@ class DAQWorker(QObject):
|
||||
body = json.dumps({"confirmation_code": confirmation_code})
|
||||
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_recovery_action_response(
|
||||
reply, "Beamline session taken over."
|
||||
)
|
||||
lambda: self._handle_recovery_action_response(reply, "Beamline session taken over.")
|
||||
)
|
||||
|
||||
@Slot(str)
|
||||
@@ -956,9 +913,7 @@ class DAQWorker(QObject):
|
||||
body = json.dumps({"confirmation_code": confirmation_code})
|
||||
reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8")))
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_recovery_action_response(
|
||||
reply, "Recovery unmount completed."
|
||||
)
|
||||
lambda: self._handle_recovery_action_response(reply, "Recovery unmount completed.")
|
||||
)
|
||||
|
||||
@Slot(str)
|
||||
@@ -1022,13 +977,8 @@ class DAQWorker(QObject):
|
||||
if self._is_detector_state_failure_message(err_msg):
|
||||
self._emit_detector_message(f"JFJoch: {err_msg}", is_error=True)
|
||||
else:
|
||||
code = (
|
||||
body_json.get("code", "") if isinstance(body_json, dict) else ""
|
||||
)
|
||||
if (
|
||||
code == AareErrorCode.JF_JOCH_COMMUNICATION_ERROR
|
||||
or status == 503
|
||||
):
|
||||
code = body_json.get("code", "") if isinstance(body_json, dict) else ""
|
||||
if code == AareErrorCode.JF_JOCH_COMMUNICATION_ERROR or status == 503:
|
||||
self._emit_detector_message(f"JFJoch: {err_msg}", is_error=True)
|
||||
|
||||
if self._is_critical_detector_failure(status, body_json, err_msg):
|
||||
@@ -1038,15 +988,12 @@ class DAQWorker(QObject):
|
||||
f"Details: {err_msg}"
|
||||
)
|
||||
self._emit_detector_message(
|
||||
f"Detector error during manual collection: {err_msg}",
|
||||
is_error=True,
|
||||
f"Detector error during manual collection: {err_msg}", is_error=True
|
||||
)
|
||||
self._emit_manual_collection_critical_failure(critical_msg)
|
||||
|
||||
short_msg = (
|
||||
err_msg.split("input':", 1)[0].strip()
|
||||
if "input':" in err_msg
|
||||
else err_msg
|
||||
err_msg.split("input':", 1)[0].strip() if "input':" in err_msg else err_msg
|
||||
)
|
||||
logger.error(f"Rotation scan failed: {short_msg}")
|
||||
self.http_error.emit(short_msg)
|
||||
@@ -1088,13 +1035,8 @@ class DAQWorker(QObject):
|
||||
if self._is_detector_state_failure_message(err_msg):
|
||||
self._emit_detector_message(f"JFJoch: {err_msg}", is_error=True)
|
||||
else:
|
||||
code = (
|
||||
body_json.get("code", "") if isinstance(body_json, dict) else ""
|
||||
)
|
||||
if (
|
||||
code == AareErrorCode.JF_JOCH_COMMUNICATION_ERROR
|
||||
or status == 503
|
||||
):
|
||||
code = body_json.get("code", "") if isinstance(body_json, dict) else ""
|
||||
if code == AareErrorCode.JF_JOCH_COMMUNICATION_ERROR or status == 503:
|
||||
self._emit_detector_message(f"JFJoch: {err_msg}", is_error=True)
|
||||
|
||||
if self._is_critical_detector_failure(status, body_json, err_msg):
|
||||
@@ -1104,8 +1046,7 @@ class DAQWorker(QObject):
|
||||
f"Details: {err_msg}"
|
||||
)
|
||||
self._emit_detector_message(
|
||||
f"Detector error during manual collection: {err_msg}",
|
||||
is_error=True,
|
||||
f"Detector error during manual collection: {err_msg}", is_error=True
|
||||
)
|
||||
self._emit_manual_collection_critical_failure(critical_msg)
|
||||
|
||||
@@ -1159,9 +1100,7 @@ class DAQWorker(QObject):
|
||||
self.raster_scan_completed.emit(reply)
|
||||
return
|
||||
|
||||
request = QNetworkRequest(
|
||||
QUrl(f"{self.__base_url}/scan/raster?auto_center=false")
|
||||
)
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/raster?auto_center=false"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
body = r.model_dump_json()
|
||||
@@ -1203,9 +1142,7 @@ class DAQWorker(QObject):
|
||||
self.raster_scan_completed.emit(reply)
|
||||
return
|
||||
|
||||
request = QNetworkRequest(
|
||||
QUrl(f"{self.__base_url}/scan/raster?auto_center=true")
|
||||
)
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/raster?auto_center=true"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
body = r.model_dump_json()
|
||||
@@ -1256,9 +1193,7 @@ class DAQWorker(QObject):
|
||||
for watcher in self._recurrence_watchers:
|
||||
watcher.reset()
|
||||
|
||||
def _observe_recurrence(
|
||||
self, exception_class_name: str | None
|
||||
) -> WatcherTrip | None:
|
||||
def _observe_recurrence(self, exception_class_name: str | None) -> WatcherTrip | None:
|
||||
exception_class = resolve_exception_class(exception_class_name)
|
||||
for watcher in self._recurrence_watchers:
|
||||
trip = watcher.maybe_trip(exception_class)
|
||||
@@ -1273,9 +1208,7 @@ class DAQWorker(QObject):
|
||||
self.automation_critical_failure.emit(message)
|
||||
|
||||
@staticmethod
|
||||
def _extract_reply_error_details(
|
||||
reply: QNetworkReply,
|
||||
) -> tuple[int | None, str, dict | None]:
|
||||
def _extract_reply_error_details(reply: QNetworkReply) -> tuple[int | None, str, dict | None]:
|
||||
status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
|
||||
err_msg = reply.errorString()
|
||||
body_json = None
|
||||
@@ -1310,23 +1243,17 @@ class DAQWorker(QObject):
|
||||
|
||||
@staticmethod
|
||||
def _classify_reply_error(
|
||||
status: int | None,
|
||||
err_msg: str,
|
||||
body_json: dict | None,
|
||||
status: int | None, err_msg: str, body_json: dict | None
|
||||
) -> ErrorInfo:
|
||||
code = body_json.get("code") if isinstance(body_json, dict) else None
|
||||
exception_class = (
|
||||
body_json.get("exception_class") if isinstance(body_json, dict) else None
|
||||
)
|
||||
exception_class = body_json.get("exception_class") if isinstance(body_json, dict) else None
|
||||
context = body_json.get("context") if isinstance(body_json, dict) else {}
|
||||
if not isinstance(context, dict):
|
||||
context = {}
|
||||
return ErrorInfo(
|
||||
critical=DAQWorker._is_critical(body_json),
|
||||
code=str(code) if code is not None else None,
|
||||
exception_class=str(exception_class)
|
||||
if exception_class is not None
|
||||
else None,
|
||||
exception_class=str(exception_class) if exception_class is not None else None,
|
||||
message=str(err_msg or ""),
|
||||
context=context,
|
||||
)
|
||||
@@ -1351,9 +1278,7 @@ class DAQWorker(QObject):
|
||||
|
||||
@staticmethod
|
||||
def _is_critical_detector_failure(
|
||||
status: int | None,
|
||||
body_json: dict | None = None,
|
||||
err_msg: str | None = None,
|
||||
status: int | None, body_json: dict | None = None, err_msg: str | None = None
|
||||
) -> bool:
|
||||
try:
|
||||
status_int = int(status) if status is not None else None
|
||||
@@ -1397,9 +1322,7 @@ class DAQWorker(QObject):
|
||||
watcher_trip = self._observe_recurrence(error_info.exception_class)
|
||||
|
||||
if self._is_detector_state_failure_message(error_info.message):
|
||||
self._emit_detector_message(
|
||||
f"JFJoch: {error_info.message}", is_error=True
|
||||
)
|
||||
self._emit_detector_message(f"JFJoch: {error_info.message}", is_error=True)
|
||||
|
||||
if self._is_auth_error_code(error_info.code) or status == 401:
|
||||
logger.error(f"Error in auto scan: {error_info.message}")
|
||||
@@ -1500,15 +1423,11 @@ class DAQWorker(QObject):
|
||||
logger.info("POST /local_contact/resync/detector_metadata")
|
||||
return
|
||||
|
||||
request = QNetworkRequest(
|
||||
QUrl(f"{self.__base_url}/local_contact/resync/detector_metadata")
|
||||
)
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/resync/detector_metadata"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
reply = self.__net_manager.post(request, QByteArray(b""))
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_detector_metadata_resync_response(reply)
|
||||
)
|
||||
reply.finished.connect(lambda: self._handle_detector_metadata_resync_response(reply))
|
||||
|
||||
@Slot(float)
|
||||
def anneal(self, time_s: float):
|
||||
@@ -1566,14 +1485,10 @@ class DAQWorker(QObject):
|
||||
)
|
||||
return
|
||||
|
||||
request = QNetworkRequest(
|
||||
QUrl(f"{self.__base_url}/local_contact/simulation_state")
|
||||
)
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/simulation_state"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.get(request)
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_local_contact_simulation_state_response(reply)
|
||||
)
|
||||
reply.finished.connect(lambda: self._handle_local_contact_simulation_state_response(reply))
|
||||
|
||||
def _handle_local_contact_simulation_state_response(self, reply: QNetworkReply):
|
||||
try:
|
||||
@@ -1602,9 +1517,7 @@ class DAQWorker(QObject):
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/device_state"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.get(request)
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_local_contact_device_state_response(reply)
|
||||
)
|
||||
reply.finished.connect(lambda: self._handle_local_contact_device_state_response(reply))
|
||||
|
||||
def _handle_local_contact_device_state_response(self, reply: QNetworkReply):
|
||||
try:
|
||||
@@ -1665,9 +1578,7 @@ class DAQWorker(QObject):
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/local_contact/config"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.get(request)
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_local_contact_config_response(reply)
|
||||
)
|
||||
reply.finished.connect(lambda: self._handle_local_contact_config_response(reply))
|
||||
|
||||
def _handle_local_contact_config_response(self, reply: QNetworkReply):
|
||||
try:
|
||||
@@ -1695,9 +1606,7 @@ class DAQWorker(QObject):
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
body = QByteArray(json.dumps(payload).encode("utf-8"))
|
||||
reply = self.__net_manager.put(request, body)
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_set_local_contact_config_response(reply)
|
||||
)
|
||||
reply.finished.connect(lambda: self._handle_set_local_contact_config_response(reply))
|
||||
|
||||
def _handle_set_local_contact_config_response(self, reply: QNetworkReply):
|
||||
try:
|
||||
@@ -1710,17 +1619,14 @@ class DAQWorker(QObject):
|
||||
self.status_message.emit("Local Contact config saved.", False)
|
||||
except Exception as e:
|
||||
message = (
|
||||
"Error transferring information from DAQ while saving Local Contact config.\n\n"
|
||||
f"{e}"
|
||||
f"Error transferring information from DAQ while saving Local Contact config.\n\n{e}"
|
||||
)
|
||||
logger.error(message)
|
||||
self.local_contact_transfer_error.emit(message)
|
||||
|
||||
@Slot(str, bool)
|
||||
def set_local_contact_simulation(self, device: str, enabled: bool):
|
||||
self.generic_post(
|
||||
f"local_contact/simulate/{device}?enabled={str(enabled).lower()}"
|
||||
)
|
||||
self.generic_post(f"local_contact/simulate/{device}?enabled={str(enabled).lower()}")
|
||||
|
||||
@Slot(str)
|
||||
def restart_local_contact_device(self, device: str):
|
||||
@@ -1778,9 +1684,7 @@ class DAQWorker(QObject):
|
||||
|
||||
@Slot(str)
|
||||
def bec_reinitialise_planner_and_position_devices(self, method: str = "auto"):
|
||||
self.generic_post(
|
||||
f"bec/reinitialise_planner_and_position_devices?method={method}"
|
||||
)
|
||||
self.generic_post(f"bec/reinitialise_planner_and_position_devices?method={method}")
|
||||
|
||||
@Slot()
|
||||
def bec_save_current_bs_pos(self):
|
||||
@@ -1804,9 +1708,7 @@ class DAQWorker(QObject):
|
||||
|
||||
@Slot()
|
||||
def initialise_aerotech(self):
|
||||
logger.info(
|
||||
"initisalisation does not initisalise aareSCAN but runs homing script"
|
||||
)
|
||||
logger.info("initisalisation does not initisalise aareSCAN but runs homing script")
|
||||
self.generic_post("aerotech/initialize")
|
||||
|
||||
@Slot()
|
||||
@@ -1892,9 +1794,7 @@ class DAQWorker(QObject):
|
||||
status = None
|
||||
if reply is not None:
|
||||
try:
|
||||
status = reply.attribute(
|
||||
QNetworkRequest.Attribute.HttpStatusCodeAttribute
|
||||
)
|
||||
status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
|
||||
except Exception:
|
||||
status = None
|
||||
|
||||
@@ -1972,19 +1872,15 @@ class DAQWorker(QObject):
|
||||
finished=bool(progress_payload.get("finished", False)),
|
||||
success=progress_payload.get("success"),
|
||||
samples_in_queue=int(progress_payload.get("samples_in_queue", 0) or 0),
|
||||
avg_time_per_sample=float(
|
||||
progress_payload.get("avg_time_per_sample", 0.0) or 0.0
|
||||
),
|
||||
avg_time_per_sample=float(progress_payload.get("avg_time_per_sample", 0.0) or 0.0),
|
||||
current_sample_name=str(progress_payload.get("current_sample_name") or ""),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _log_level_for_event(level: str) -> int:
|
||||
return {
|
||||
"ERROR": logging.ERROR,
|
||||
"WARNING": logging.WARNING,
|
||||
"INFO": logging.INFO,
|
||||
}.get(str(level).upper(), logging.INFO)
|
||||
return {"ERROR": logging.ERROR, "WARNING": logging.WARNING, "INFO": logging.INFO}.get(
|
||||
str(level).upper(), logging.INFO
|
||||
)
|
||||
|
||||
def _emit_automation_progress_events(self, progress: AutomationProgress) -> None:
|
||||
for event in progress.events:
|
||||
@@ -2036,8 +1932,8 @@ class DAQWorker(QObject):
|
||||
|
||||
def _process_automation_progress_buffer(self) -> None:
|
||||
while "\n\n" in self._automation_progress_buffer:
|
||||
event_data, self._automation_progress_buffer = (
|
||||
self._automation_progress_buffer.split("\n\n", 1)
|
||||
event_data, self._automation_progress_buffer = self._automation_progress_buffer.split(
|
||||
"\n\n", 1
|
||||
)
|
||||
|
||||
data_lines: list[str] = []
|
||||
@@ -2070,9 +1966,7 @@ class DAQWorker(QObject):
|
||||
status = None
|
||||
if reply is not None:
|
||||
try:
|
||||
status = reply.attribute(
|
||||
QNetworkRequest.Attribute.HttpStatusCodeAttribute
|
||||
)
|
||||
status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
|
||||
except Exception:
|
||||
status = None
|
||||
|
||||
@@ -2136,9 +2030,7 @@ class DAQWorker(QObject):
|
||||
return
|
||||
|
||||
request = QNetworkRequest(
|
||||
QUrl(
|
||||
f"{self.__base_url}/face_detection/run?steps={steps}&step_size={step_size}"
|
||||
)
|
||||
QUrl(f"{self.__base_url}/face_detection/run?steps={steps}&step_size={step_size}")
|
||||
)
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
@@ -2165,12 +2057,8 @@ class DAQWorker(QObject):
|
||||
data = json.loads(response_data) if response_data else []
|
||||
if emit_status:
|
||||
# fetch status and bkg in parallel (simple sequential here)
|
||||
status_req = QNetworkRequest(
|
||||
QUrl(f"{self.__base_url}/fluorimeter/status")
|
||||
)
|
||||
status_req.setRawHeader(
|
||||
b"Authorization", f"Bearer {self.__token}".encode("utf-8")
|
||||
)
|
||||
status_req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/status"))
|
||||
status_req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
status_reply = self.__net_manager.get(status_req)
|
||||
status_reply.finished.connect(
|
||||
lambda: self._handle_fluorimeter_status_and_emit(data, status_reply)
|
||||
@@ -2186,13 +2074,9 @@ class DAQWorker(QObject):
|
||||
s_payload = self.handle_response(status_reply)
|
||||
s = int(s_payload) if s_payload not in ("", "null") else -1
|
||||
b_req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/background"))
|
||||
b_req.setRawHeader(
|
||||
b"Authorization", f"Bearer {self.__token}".encode("utf-8")
|
||||
)
|
||||
b_req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
b_reply = self.__net_manager.get(b_req)
|
||||
b_reply.finished.connect(
|
||||
lambda: self._emit_fluorimeter_with_bkg(data, s, b_reply)
|
||||
)
|
||||
b_reply.finished.connect(lambda: self._emit_fluorimeter_with_bkg(data, s, b_reply))
|
||||
except Exception as e:
|
||||
logger.error(f"Fluorimeter status error: {e}")
|
||||
self.http_error.emit(str(e))
|
||||
@@ -2222,9 +2106,7 @@ class DAQWorker(QObject):
|
||||
def _handle_fluorimeter_spectrum(self, reply: QNetworkReply):
|
||||
try:
|
||||
response_data = self.handle_response(reply)
|
||||
parsed_response = FluorescenceSpectrumOutputModel.model_validate_json(
|
||||
response_data
|
||||
)
|
||||
parsed_response = FluorescenceSpectrumOutputModel.model_validate_json(response_data)
|
||||
self.fluorimeter_spectrum_update.emit(parsed_response)
|
||||
except Exception as e:
|
||||
logger.error(f"Exception from fluorimeter spectrum: {e}")
|
||||
@@ -2237,9 +2119,7 @@ class DAQWorker(QObject):
|
||||
req = QNetworkRequest(QUrl(f"{self.__base_url}/fluorimeter/data"))
|
||||
req.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.get(req)
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_fluorimeter_data(reply, emit_status=True)
|
||||
)
|
||||
reply.finished.connect(lambda: self._handle_fluorimeter_data(reply, emit_status=True))
|
||||
|
||||
# Optional: SSE listener for live updates
|
||||
def start_fluorimeter_stream(self):
|
||||
@@ -2336,9 +2216,7 @@ class DAQWorker(QObject):
|
||||
@Slot(str, str)
|
||||
def send_screenshot_db(self, filename: str = "", message: str = ""):
|
||||
if self.__base_url is None:
|
||||
logger.info(
|
||||
f"POST /samcam/send_screenshot_db?filename={filename}&message={message}"
|
||||
)
|
||||
logger.info(f"POST /samcam/send_screenshot_db?filename={filename}&message={message}")
|
||||
return
|
||||
|
||||
from urllib.parse import quote
|
||||
@@ -2530,10 +2408,7 @@ class DAQWorker(QObject):
|
||||
- clear the active beamline session if this GUI owns it
|
||||
"""
|
||||
try:
|
||||
if (
|
||||
hasattr(self, "_baton_timeout_timer")
|
||||
and self._baton_timeout_timer is not None
|
||||
):
|
||||
if hasattr(self, "_baton_timeout_timer") and self._baton_timeout_timer is not None:
|
||||
self._baton_timeout_timer.stop()
|
||||
|
||||
self.end_session()
|
||||
@@ -2585,9 +2460,7 @@ class DAQWorker(QObject):
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
request.setRawHeader(b"Content-Type", b"application/json")
|
||||
reply = self.__net_manager.post(request, QByteArray(b""))
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_gui_session_mutation_response(reply)
|
||||
)
|
||||
reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply))
|
||||
|
||||
@Slot(int)
|
||||
def force_remove_gui_session(self, session_id: int):
|
||||
@@ -2595,14 +2468,10 @@ class DAQWorker(QObject):
|
||||
logger.info(f"DELETE /admin/gui_sessions/{session_id}")
|
||||
return
|
||||
|
||||
request = QNetworkRequest(
|
||||
QUrl(f"{self.__base_url}/admin/gui_sessions/{session_id}")
|
||||
)
|
||||
request = QNetworkRequest(QUrl(f"{self.__base_url}/admin/gui_sessions/{session_id}"))
|
||||
request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8"))
|
||||
reply = self.__net_manager.deleteResource(request)
|
||||
reply.finished.connect(
|
||||
lambda: self._handle_gui_session_mutation_response(reply)
|
||||
)
|
||||
reply.finished.connect(lambda: self._handle_gui_session_mutation_response(reply))
|
||||
|
||||
def _handle_gui_session_mutation_response(self, reply: QNetworkReply):
|
||||
try:
|
||||
@@ -2637,10 +2506,7 @@ class DAQWorker(QObject):
|
||||
self._cleanup_done = True
|
||||
|
||||
try:
|
||||
if (
|
||||
hasattr(self, "_baton_timeout_timer")
|
||||
and self._baton_timeout_timer is not None
|
||||
):
|
||||
if hasattr(self, "_baton_timeout_timer") and self._baton_timeout_timer is not None:
|
||||
self._baton_timeout_timer.stop()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to stop _baton_timeout_timer: {e}")
|
||||
|
||||
@@ -126,12 +126,7 @@ class PredictionSubscriber(QThread):
|
||||
return None
|
||||
|
||||
def _rgb_to_pixmap(self, rgb: np.ndarray) -> QPixmap:
|
||||
qimage = QImage(
|
||||
rgb.data,
|
||||
rgb.shape[1],
|
||||
rgb.shape[0],
|
||||
QImage.Format.Format_RGB888,
|
||||
).copy()
|
||||
qimage = QImage(rgb.data, rgb.shape[1], rgb.shape[0], QImage.Format.Format_RGB888).copy()
|
||||
return QPixmap.fromImage(qimage)
|
||||
|
||||
def _emit_focus_measure_if_enabled(self, rgb: np.ndarray) -> None:
|
||||
@@ -169,9 +164,7 @@ class PredictionSubscriber(QThread):
|
||||
self._fps_frame_count = 0
|
||||
|
||||
if no_frames_long:
|
||||
self._set_camera_available(
|
||||
False, "Sample camera feed unavailable"
|
||||
)
|
||||
self._set_camera_available(False, "Sample camera feed unavailable")
|
||||
continue
|
||||
|
||||
if not parts:
|
||||
@@ -215,8 +208,7 @@ class PredictionSubscriber(QThread):
|
||||
rgb = self._decode_rgb_image(header, image_bytes)
|
||||
if rgb is None:
|
||||
self._set_camera_available(
|
||||
False,
|
||||
"Sample camera feed unavailable: failed to decode frame",
|
||||
False, "Sample camera feed unavailable: failed to decode frame"
|
||||
)
|
||||
else:
|
||||
self._set_camera_available(True)
|
||||
@@ -225,8 +217,7 @@ class PredictionSubscriber(QThread):
|
||||
self.image.emit(self._rgb_to_pixmap(rgb))
|
||||
elif self._emit_images:
|
||||
self._set_camera_available(
|
||||
False,
|
||||
"Sample camera feed unavailable: no frame header in zmq stream",
|
||||
False, "Sample camera feed unavailable: no frame header in zmq stream"
|
||||
)
|
||||
|
||||
if detections and self.running:
|
||||
@@ -237,9 +228,7 @@ class PredictionSubscriber(QThread):
|
||||
|
||||
except Exception as e:
|
||||
if self.running:
|
||||
self._set_camera_available(
|
||||
False, f"Sample camera feed unavailable: {e}"
|
||||
)
|
||||
self._set_camera_available(False, f"Sample camera feed unavailable: {e}")
|
||||
logger.exception(f"PredictionSubscriber error: {e}")
|
||||
finally:
|
||||
try:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user