Data Collection and Analysis
Data Collection at SESAME
Data Collection and Analysis at SESAME is was activate begging of 2021
Their tasks
smooth and reliable data collection
data analysis
data storage
provding essential pre and post processing tools
Experimental Data Management Policy of SESAME
Policy Statement
The purpose of this Policy is to provide information and guidance to users conducting peer-reviewed and Director Discretionary time research on experimental data ownership, storage, access, and management.
Coverage
This policy applies to raw data and metadata generated from peer-reviewed and Director Discretionary time research proposals and in-house experiments.
It does not cover proprietary and industrial research conducted by Participatory Research Teams
General Principles
Acceptance of this Policy is a precondition for obtaining beam time for peer-reviewed and Director Discretionary time research proposals and in-house experiments.
SESAME will strive to maximize long-term curation of data for a minimum of 5 years, with a target of 10 years.
Definitions
Experimental Data: Umbrella term for raw data and all associated metadata.
Metadata: Information related to the experiment and experimental conditions.
Data Ownership and Access
Raw data and associated metadata will become open access after a 3-year embargo period,
Data and metadata will be curated in well-defined formats for long-term access.
Results
Ownership of results derived from data analysis will be determined by contractual obligations of individuals performing the analysis.
SESAME will provide best effort curation of results and act as the long-term custodian.
Publication
Publications related to SESAME experiments must cite the persistent identifier of the experiment and data.
References of related publications must be deposited in a publications database within 3 months of the publication date or during new beam time applications.
Compliance
Acceptance of the Experimental Data Management Policy is a precondition for obtaining beam time.
Deliberate infringement may result in denial of data access and rejection of future beam time applications.
Tomoscan
it is a python based software for tomography data collection and analysis. which is used to collect data from SESAME tomography beamline.
Data Acquisition Testing Bench
The testing bench consists of five motors, from A to E, that can be controlled using EPICS. These motors are used to test various control programs that are to be used at SESAME beamlines.
Python-Based Client for Collection of Data from Testing Bench
Rotational Motion Box
A Python script was developed in order to home each of the motors in the testing bench. This is necessary in order to ensure the motors are calibrated correctly.
The script works using a JSON file containing parameters for each motor in the testing bench. Specifically, these parameters are:
pv_root: The root of all of the PVs associated with the motor.lo_lim: The soft-coded lower limit on the position of the motor.hi_lim: The soft-coded higher limit on the position of the motor.center: The “default” position of the motor, where it will be taken after the homing is complete (often takes on the value of 0).
Additionally, the JSON file contains general configuration parameters. For now, the only parameter is the tolerance, which is the difference between the hard limits and the soft limits on the motor positions.
This script is controlled via the terminal, and has five commands that can be executed:
help: Used to get usage information regarding a given command.Usage:
help command_namecommand_name: The name of the command that the user would like help with.
add: Used to add a new motor to the JSON file.Usage:
add json_path motor_name pv_root lo_lim center hi_limjson_path: The path to the JSON file containing the general and motor configurations.motor_name: The name of the motor to be added.pv_root: The PV root name used to access the motor.lo_lim: The soft-coded lower positional limit for the motor.center: The center position for the motor where it will be directed after homing.hi_lim: The soft-coded higher positional limit for the motor.
change: Used to change a preexisting motor in the JSON file.Usage:
change json_path motor_name pv_root lo_lim center hi_limjson_path: The path to the JSON file containing the general and motor configurations.motor_name: The name of the motor to be changed.pv_root: The PV root name used to access the motor.lo_lim: The soft-coded lower positional limit for the motor.center: The center position for the motor where it will be directed after homing.hi_lim: The soft-coded higher positional limit for the motor.
remove: Used to remove a preexisting motor from the JSON file.Usage:
remove json_path motor_namejson_path: The path to the JSON file containing the general and motor configurations.motor_name: The name of the motor to be removed, orallto remove all available motors.
home: Used to begin homing for one or all of the motors.Usage:
home json_path motor_name homing_modejson_path: The path to the JSON file containing the general and motor configurations.motor_name: The name of the motor to be homed, orallto home all available motors.homing_mode: The method to be used in homing, eitherhito use the higher limit orloto use the lower limit.
For example, in order to home all of the motors using the higher limit, navigate to the directory containing the Python script in the terminal and run:
Where test.py is the Python script file name and test.json is the JSON file name.
Detector
Detector Step-Scan Data Acquistion and Analysis BEATS Beamline
The BEAmline for Tomography at SESAME (BEATS) works by propagating a hard X-ray full field tomogoraphy beam through small sample structures. The diffracted X-rays are measured by the detector and acquired as single 2D frames of the sample. Therefore, there would be a need to rotate the sample in order to get a full digital 3D reconstruction of the sample.
Main Task:
We must write a program that acquires frames of the cross-section of the sample and store them appropriately in a HDF5 file in dX style.
These are the main parameters to start implementing the step scan technique (to simulate more or less BEATS DAQ step scan):
For the detector: (FLIR camera)
Exposure time: FLIR5:cam5:AcquireTime (R/W)
Num of images: FLIR5:cam5:NumImages (R/W)
Acquiring mode: FLIR5:cam5:ImageMode (0:Single, 1:Multiple, 2:Continuous) (R/W)
Start Acquiring: FLIR5:cam5:Acquire (0:Done, 1:Acquire) (R/W)
Acquiring status: FLIR5:cam5:AcquireBusy (0:Done, 1:Acquire) (Read only)
Image counter: FLIR5:cam5:ArrayCounter_RBV (Read only)
Image sizeX: FLIR5:cam5:ArraySizeX_RBV (Read only)
Image sizeY: FLIR5:cam5:ArraySizeY_RBV (Read only)
Total image size: FLIR5:cam5:ArraySize_RBV (Read only)
Trigger Mode: FLIR5:cam5:TriggerMode (0:off, 1:on) (R/W)
Trigger Source: FLIR5:cam5:TriggerSource (0:software) (R/W)
Trigger Software: FLIR5:cam5:TriggerSoftware (0: Done, 1:Trigger) (R/W) // this pv mainly used for software triggering
Enable NdArray: FLIR5:image1:EnableCallbacks (0:Disable, 1:Enable) (R/W)
Image Array Data: Flir5:image1:ArrayData (Read only)
For motion stage (MICOS stage):
Current position feedback: micos:m1.RBV (Read only)
Move motor: micos:m1.VAL (R/W) // used for moving the motor (absolute value)
Objectives of the Program
Implementing the step scan by taking the args from the user (exposure time, overall distance, step size)
Go to the home position (Position 0 abs. value)
For Each step you have to move the motor to the target position, you have to use the Motor record class to move and check if the motor movement is done, more info is found on these links: (https://epics.anl.gov/docs/APS2014/07-CA-Python.pdf , https://github.com/pyepics/pyepics/blob/master/epics/motor.py ).
Reshape the images according to the predefined size, and save it in a H5 file format in dxFile layout.
Store the data (current position vs. timestamp in a .xdi file (file is available and attached as a part of the experimental data files for XAFS Beamline)
import h5py
import numpy as np
import time
import argparse
import epics
import json
import os
from PIL import Image
import threading
class StepScan:
def __init__(self, exposure_time, overall_distance, step_size, detector_pv, motion_stage_pv, camera_acq_pv,
image_size_x, image_size_y, image_counter, num_images, acq_mode, start_acq, acq_status,
trigger_mode, trigger_source, trigger_software, image_data ,exposure_time_pv):
self.exposure_time = exposure_time
self.overall_distance = overall_distance
self.step_size = step_size
self.detector = epics.PV(detector_pv)
self.motion_stage = epics.Motor(motion_stage_pv)
self.camera_acq_pv = camera_acq_pv
self.image_size_x = int(epics.caget(image_size_x))
self.image_size_y = int(epics.caget(image_size_y))
self.image_counter = image_counter
self.num_images = num_images
self.acq_mode = acq_mode
self.start_acq = start_acq
self.acq_status = acq_status
self.trigger_mode = trigger_mode
self.trigger_source = trigger_source
self.trigger_software = trigger_software
self.image_data = image_data
self.exposure_time_pv = exposure_time_pv
# Set the exposure time
epics.caput(self.exposure_time_pv, self.exposure_time)
# Set the acquisition mode to multiple
epics.caput(self.acq_mode, 1)
# Enable the trigger mode to start the acquisition
epics.caput(self.trigger_mode, 1)
epics.caput(self.camera_acq_pv, 1)
# Set the trigger source to 0 (software triggering)
epics.caput(self.trigger_source, 0)
steps_array = np.arange(0, overall_distance + step_size, step_size)
print(f"{overall_distance} {step_size} type of overall distance: {type(overall_distance)} type of step size: {type(step_size)}")
print(f"steps array: {steps_array}")
num_step= len(steps_array) - 1
print(f"num steps: {num_step}")
epics.caput(self.num_images, num_step)
print(f"num images: {epics.caget(self.num_images)}")
def move_motor_to_position(self, position):
self.motion_stage.move(position)
while not self.motion_stage.done_moving:
time.sleep(0.1)
def save_image(self, image_data, file_name, image_size_x, image_size_y):
if not os.path.exists("images"):
os.makedirs("images")
image_reshaped = np.reshape(image_data, (image_size_y, image_size_x))
file_path = os.path.join("images", file_name.replace("npy", "png"))
image_pil = Image.fromarray(image_reshaped)
image_pil.save(file_path)
print(f"Saved image to {file_path}")
def acquire_image(self, trigger_software, image_counter, image_data, image_size_x, image_size_, num_steps):
# Wait for the image counter to change, indicating a new image has been acquired
initial_counter = epics.caget(image_counter)
# Trigger the software trigger to initiate image acquisition
epics.caput(trigger_software, 1)
while True:
time.sleep(0.1)
current_counter = epics.caget(image_counter)
if current_counter != initial_counter:
print(f"Image acquired with counter {current_counter}")
break
# Retrieve the image data
image_data = epics.caget(image_data)
image_data = np.reshape(image_data, (self.image_size_y, self.image_size_x))
return image_data
def start_step_scan(self):
f = h5py.File('step_scan.hdf5', 'w')
num_steps = int(self.overall_distance / self.step_size)
# Create detector and data groups
detector_group = f.create_group('exchange/detector')
data_group = f.create_group('exchange/data')
# Add detector metadata
detector_group.attrs['exposure_time'] = self.exposure_time
detector_group.attrs['image_size_x'] = self.image_size_x
detector_group.attrs['image_size_y'] = self.image_size_y
detector_group.attrs['Num_of_image'] = num_steps
detector_group.attrs['local_name'] = "SESAME Detector"
detector_group.attrs['pixel_size'] = 20E-6 # example
for step in range(num_steps):
# Move stage and acquire image
target_position = step * self.step_size
self.move_motor_to_position(target_position)
image_data = self.acquire_image(self.trigger_software, self.image_counter, self.image_data ,self.image_size_x, self.image_size_y,num_steps)
# Create dataset
img_dataset = data_group.create_dataset(f'image_{step}', data=image_data)
# Add metadata
img_dataset.attrs['distance'] = target_position
img_dataset.attrs['timestamp'] = time.strftime("%Y-%m-%d %H:%M:%S")
# Add scan metadata
data_group.attrs['num_images'] = num_steps
data_group.attrs['step_size'] = self.step_size
f.close()
def main(args):
with open(args.config_file) as json_file:
config = json.load(json_file)
detector_pv = config.get("detector_pv")
motion_stage_pv = config.get("motion_stage_pv")
camera_acq_pv = config.get("camera_acq_pv")
image_size_x = config.get("image_size_x")
image_size_y = config.get("image_size_y")
image_counter = config.get("image_counter")
num_images = config.get("num_images")
acq_mode = config.get("acq_mode")
start_acq = config.get("start_acq")
acq_status = config.get("acq_status")
trigger_mode = config.get("trigger_mode")
trigger_source = config.get("trigger_source")
trigger_software = config.get("trigger_software")
image_data = config.get("image_data")
exposure_time_pv = config.get("exposure_time_pv")
step_scan = StepScan(
args.exposure_time,
args.overall_distance,
args.step_size,
detector_pv,
motion_stage_pv,
camera_acq_pv,
image_size_x,
image_size_y,
image_counter,
num_images,
acq_mode,
start_acq,
acq_status,
trigger_mode,
trigger_source,
trigger_software,
image_data,
exposure_time_pv
)
step_scan.move_motor_to_position(0) # Move to the home position (position 0)
step_scan.start_step_scan()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Step Scan using FLIR camera and MICOS stage.")
parser.add_argument("exposure_time", type=float, help="Exposure time for the FLIR camera.")
parser.add_argument("overall_distance", type=float, help="Overall distance to scan with the MICOS stage.")
parser.add_argument("step_size", type=float, help="Step size for each scan step.")
parser.add_argument("--config_file", default="config.json", help="JSON file containing PV names. (Default: config.json)")
args = parser.parse_args()
main(args)
EPICS-Qt-Based Client for Collection of Data from Testing Bench
Motion Box
A GUI was developed using Qt in order to more easily home the motors, as in Python-Based Client for Collection of Data from Testing Bench. This GUI consists of:
- A QComboBox (like a dropdown menu) for selecting one or all of the motors.
- Four QLineEdits (like textboxes) for viewing and modifying the pv_root, lo_lim, center, and hi_lim for the selected motor in the JSON file (is blank when all motors are selected).
- A QPushButton for adding a new motor to the JSON file.
- A QPushButton for removing the selected motor(s).
- Two QPushButtons for homing the selected motor(s) using either the higher limit or the lower limit.
v
This GUI operates by sending commands to the Python script described in Python-Based Client for Collection of Data from Testing Bench via the terminal. It is therefore fairly finicky, as there isn’t much feedback displayed in the GUI, and the directories for the Python script and the JSON file are hardcoded into the GUI.
Synchronization between BEATS Beameline Detector and Rotational Motor
Software and Hardware Synchronization
The goal of the software-based synchronization task was to emulate the BEATS Beamline detector’s process of continuously capturing frames from the samples. In essence, this entails repeating the acquisition process throughout the full rotation of the sample.
This particular synchronization method’s implementation is grounded in a kinematic and physical framework, where both components are linked by a common time-scale. We can extract the time variable from the angular velocity of the rotary motor and determine the number of frames per second captured by the detector.
Including the PV’s in the previous exercise, the following are the extra PV’s that should be taken into consideration in relation to the continuous scan method.
For the detector: (FLIR camera)
Frame rate: FLIR5:cam5:GC_AcqResFrameRate_RBV (Read only)
Enable NdArray Array: FLIR5:image1:ArrayCallbacks (0:Disable, 1:Enable) (R/W)
Enable NdArray Callbaks: FLIR5:image1:EnableCallbacks (0:Disable, 1:Enable) (R/W)
Enable ZMQ Array: FLIR5:ZMQ1:ArrayCallbacks (0:Disable, 1:Enable) (R/W)
Enable ZMQ Callbaks: FLIR5:ZMQ1:EnableCallbacks (0:Disable, 1:Enable) (R/W)
*: you should enable them before start acquiring
For motion stage (MICOS stage):
Velocity: micos:m1.VELO (R/W)
Max. velocity: micos:m1.VMAX (R/W)
Acceleration time: micos:m1.ACCL (Read only)
Objectives of the Program:
Implementing the continuous scan by taking the args from the user (exposure time, overall distance, step size, #of projections).
Synchronization: calculating the motor speed based on FPS (as I explained it to you).
Implementing any other kinematics equations related to the motion.
Receiving the frames using ZMQ socket.
ZMQ reference: https://zeromq.org/languages/python/
Connection parameters to ZMQ socket: IP: 127.0.0.1, Port: 1234
Implement parallel processing technique to handle the process (here you will deal with low FPS). https://docs.python.org/3/library/multiprocessing.html
Store the data in HDF5 file.
import numpy as np
import h5py
import argparse
import time
import epics
import zmq
import multiprocessing
from stepscan import StepScan
from config import *
class ContinuousScan:
def __init__(self, exposure_time, total_distance, step_size, detector_pv, motion_stage_pv, camera_acq_pv, image_size_x, image_size_y, image_counter, acq_mode, start_acq, acq_status, trigger_mode, trigger_source, trigger_software, image_data, exposure_time_pv, frame_rate_pv, accelaration_time_pv, enable_ndarray, enable_ndarray_callbacks, enable_ZMQ_Array, enable_ZMQ_callbacks, zmq_port, zmq_host, num_images):
self.exposure_time = exposure_time
self.total_distance = total_distance
self.step_size = step_size
self.num_steps = int(np.ceil(self.total_distance / self.step_size))
self.hdf_file = "scan_data.hdf5"
self.exposure_time_pv = exposure_time_pv
self.motion_stage_pv = motion_stage_pv
self.fps_pv = frame_rate_pv
self.camera_acq_pv = camera_acq_pv
self.image_size_x = int(epics.caget(image_size_x))
self.image_size_y = int(epics.caget(image_size_y))
self.image_counter = image_counter
self.num_images = num_images
self.image_data = image_data
self.acq_mode = acq_mode
self.start_acq = start_acq
self.acq_status = acq_status
self.trigger_mode = trigger_mode
self.trigger_source = trigger_source
self.trigger_software = trigger_software
self.acceleration_time = float(epics.caget(accelaration_time_pv))
self.motion_stage = None
self.velocity = None
self.accel_distance = None
self.deccel_distance = None
self.constant_distance = None
self.acceleration_time_pv = accelaration_time_pv
self.fps = epics.caget(self.fps_pv)
self.enable_ndarray = epics.caput(enable_ndarray, 1)
self.enable_ndarray_callbacks = epics.caput(
enable_ndarray_callbacks, 1)
self.enable_ZMQ_Array = epics.caput(enable_ZMQ_Array, 1)
self.enable_ZMQ_callbacks = epics.caput(enable_ZMQ_callbacks, 1)
self.num_images = int(np.ceil(self.total_distance / self.step_size))
self.context = zmq.Context()
self.socket = self.context.socket(zmq.PULL)
self.socket.bind(f"tcp://127.0.0.1:1234")
self.queue = multiprocessing.Queue()
def receive_data_via_zmq(self):
while True:
data = self.socket.recv_pyobj()
if data is None:
break
self.queue.put(data)
def calculate_total_time(self, fps):
time_per_frame = 1/fps
self.total_time = time_per_frame * self.total_distance
return self.total_time
def calculate_velocity(self, fps):
self.calculate_total_time(fps)
print(f"FPS: {fps}")
self.velocity = self.total_distance / self.total_time
return float(self.velocity)
def calculate_accel_distance(self):
self.calculate_total_time(self.fps)
self.accel_distance = (self.total_distance *
self.acceleration_time) / self.total_time
self.deccel_distance = self.accel_distance
return float(self.accel_distance)
def calculate_constant_distance(self):
self.calculate_accel_distance()
self.constant_distance = self.total_distance - \
(self.accel_distance + self.deccel_distance)
return float(self.constant_distance)
def move_epics_motor(self, position):
self.motion_stage.move(position)
while not self.motion_stage.done_moving:
time.sleep(0.1)
def setup_camera(self):
epics.caput(self.acq_mode, 1)
epics.caput(self.trigger_mode, 0)
epics.caput(self.trigger_source, 0)
epics.caput(self.camera_acq_pv, 0)
def save_to_hdf5(self, data):
with h5py.File(self.hdf_file, 'a') as hdf:
group_name = f'image_{self.image_counter}'
hdf.create_group(group_name)
hdf[group_name]['image_data'] = data
def process_image_data(self):
while True:
if self.queue.empty():
time.sleep(0.1)
continue
data = self.queue.get()
# Save the acquired image data to HDF5
self.save_to_hdf5(data)
def perform_continuous_scan(self):
# Connect to the motion stage and get the fps value and setup the camera
self.setup_camera()
self.motion_stage = epics.Motor(self.motion_stage_pv)
fps = epics.caget(self.fps_pv)
self.calculate_velocity(fps)
accel_d = self.calculate_accel_distance()
print(f"accel_d: {accel_d}, type: {type(accel_d)}")
self.move_epics_motor(0 - float(accel_d))
print(f"Accelerating to steady speed...")
zmq_process = multiprocessing.Process(target=self.receive_data_via_zmq)
zmq_process.start()
processing_process = multiprocessing.Process(
target=self.process_image_data)
processing_process.start()
for _ in range(self.num_images):
epics.caput(self.start_acq, 1) # Trigger image acquisition
time.sleep(self.exposure_time) # Wait for exposure to complete
# Capture and process the acquired image data
image_data = self.image_data # Replace with actual image data retrieval
self.queue.put(image_data)
# Stop the ZMQ and processing processes
zmq_process.terminate()
processing_process.terminate()
self.move_epics_motor(self.total_distance + float(accel_d))
def main(args):
Config(args.config_file)
continuous_scan = ContinuousScan(
args.exposure_time,
args.overall_distance,
args.step_size,
detector_pv,
motion_stage_pv,
camera_acq_pv,
image_size_x,
image_size_y,
image_counter,
num_images,
acq_mode,
start_acq,
acq_status,
trigger_mode,
trigger_source,
trigger_software,
image_data,
exposure_time_pv,
frame_rate_pv,
accelaration_time_pv,
enable_ndarray,
enable_ndarray_callbacks,
enable_ZMQ_Array,
enable_ZMQ_Callbacks,
zmq_port,
zmq_host)
continuous_scan.setup_camera()
continuous_scan.perform_continuous_scan(1234, "localhost")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Step Scan using FLIR camera and MICOS stage.")
parser.add_argument("exposure_time", type=float,
help="Exposure time for the FLIR camera.")
parser.add_argument("overall_distance", type=float,
help="Overall distance to scan with the MICOS stage.")
parser.add_argument("step_size", type=float,
help="Step size for each scan step.")
parser.add_argument("--config_file", default="config.json",
help="JSON file containing PV names. (Default: config.json)")
args = parser.parse_args()
main(args)
PandABlocks: Hardware Synchronization Mechanisms and Resource Offerings
PandABlocks, an integrated position, acquisition, and control system, emerges as a groundbreaking solution tailored to fulfill the demands of contemporary experiments. Capable of processing absolute encoder inputs, PandABlocks effortlessly orchestrates the synchronized calculation, control, and recording of multiple hardware component positions. Born out of a collaborative effort between SOLEIL and DIAMOND, PandABlocks redefines position and acquisition processing platforms, ushering in a new era of scientific exploration. Its foundation rests on the powerful Zynq 7030 architecture, known for its versatility and reliability, while accommodating a range of encoder standards. This state-of-the-art platform is armed with synchronous triggering, data capture, and data manipulation capabilities, making it an indispensable asset for cutting-edge experiments.
A Collaborative project:
The culmination of synergistic efforts between SOLEIL and DIAMOND, PandABlocks epitomizes the fusion of innovation and expertise. Built upon the foundation of Diamond’s ‘Zebra’ and SOLEIL’s ‘SPEITBOX,’ PandABlocks inherits and enhances the strengths of its predecessors. Notably, the platform leverages absolute encoder protocols, enabling precise and accurate positional information. Furthermore, PandABlocks ensures seamless data transfer at higher bandwidths, thus eliminating data bottlenecks that could hinder experimental progress. One of its defining features is the augmentation of FPGA resources, endowing the system with unprecedented computational power, setting the stage for tackling the complex challenges of future experiments.
Sans-IO Library:
The decision to create a Sans-IO library for PandABlocks stems from the imperative of reusability and adaptability. By encapsulating the protocol in a distinct class separate from I/O operations, PandABlocks aligns with a variety of concurrency frameworks, facilitating integration into diverse operational environments. This design philosophy finds practical manifestation in the creation of both a BlockingClient and an AsyncioClient, two distinct implementations that draw upon the shared protocol encapsulation, aptly named a Connection. This approach ensures that the protocol remains agnostic to the underlying I/O mechanics, providing the freedom to seamlessly transition between different concurrency models without compromising performance or functionality.
Optimizing Data Transfer:
A paramount consideration in the PandABlocks ecosystem is the optimization of HDF file writing speed. Numerous factors influence the rate at which data can be efficiently written, ranging from trigger frequency to CPU load on the PandA. The library’s robust architecture addresses these factors comprehensively, providing strategies to maximize data throughput and ensure smooth operation. By averaging data, scaling it on the client side, and adopting a judicious flush rate, PandABlocks strikes a harmonious balance between real-time data capture and efficient utilization of resources. Additionally, the removal of the panda-webcontrol package contributes to streamlined performance, enhancing the overall efficiency of the system.
In conclusion, PandABlocks stands as a pinnacle of innovation, seamlessly merging advanced synchronization mechanisms with a rich array of resources. Its collaborative origins, rooted in the collaboration between SOLEIL and DIAMOND, underscore its commitment to excellence. By embodying principles of flexibility and adaptability through the Sans-IO library, PandABlocks empowers users to harness its capabilities within diverse operational contexts. Through strategic optimization strategies, PandABlocks champions efficient data transfer and processing, ensuring that the platform remains at the forefront of experimental endeavors, poised to shape the future of scientific exploration.