Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/4d service #255

Merged
merged 34 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d9ae118
add accufiz_interferometer to index.rst file
steigersg Aug 12, 2024
c215c77
first pass at accufiz_interferometer service
steigersg Aug 12, 2024
a824fa0
add h5py to environment.yml for 4d
steigersg Aug 12, 2024
2f758e1
import fixes
steigersg Aug 12, 2024
79e7de1
add height and width parameters to the config
steigersg Aug 12, 2024
c9381e8
remove unused code
steigersg Aug 12, 2024
8b520a7
remove call to catkit util
steigersg Aug 12, 2024
010855c
better file path management
steigersg Aug 12, 2024
bc6241e
remove more unused code with deprecated catkit call
steigersg Aug 12, 2024
3b8539f
add accufiz_interferometer.rst doc file as a placeholder to fill in b…
steigersg Aug 12, 2024
8448ece
cleaning
steigersg Aug 12, 2024
be70bdd
add dtype when submitting data to the datastreams
steigersg Aug 12, 2024
b8e3431
remove defaults for some config items and add local and server paths
steigersg Aug 12, 2024
95cd86b
remove deprecated code
steigersg Aug 12, 2024
fa1c271
Revert "remove deprecated code"
steigersg Aug 12, 2024
216e8cc
Merge branch 'develop' into feature/4d_service
lanemeier7 Sep 17, 2024
33fbf19
Merge branch 'develop' into feature/4d_service
lanemeier7 Sep 18, 2024
ff84516
Created catkit2 utils for flip/rotate function. Added accufiz_interfe…
lanemeier7 Oct 11, 2024
075545e
Merge remote-tracking branch 'origin/develop' into feature/4d_service
lanemeier7 Oct 11, 2024
a8b3b0f
accufiz simulator generates its own data if sim_data path is not in c…
lanemeier7 Oct 18, 2024
a2170aa
fixed some documentation typos
lanemeier7 Oct 18, 2024
96f2ddb
accufiz_interferometer now following CameraProxy service interface
lanemeier7 Oct 18, 2024
6353335
accufiz_interferometer changes per linter fail
lanemeier7 Oct 18, 2024
7057d5c
accufiz_interferometer changes per flake8 linter
lanemeier7 Oct 18, 2024
0e30146
uitls.py flake8 adjustments
lanemeier7 Oct 18, 2024
e40c11e
fixed bug where accufiz save_fits config was not actually stopping FI…
lanemeier7 Oct 19, 2024
676b578
Removed utils.py added fliplr and rotate to accufiz config, used newe…
lanemeier7 Oct 23, 2024
1bb66c9
added docstrings to accufiz_interferometer and sim service
lanemeier7 Oct 23, 2024
e55f2a7
Merge branch 'develop' into feature/4d_service
lanemeier7 Oct 23, 2024
bd70a00
Merge remote-tracking branch 'origin/develop' into feature/4d_service
lanemeier7 Oct 23, 2024
dee6f8e
Merge branch 'feature/4d_service' of github.com:spacetelescope/catkit…
lanemeier7 Oct 23, 2024
c80dc29
Filled out doxygen documentation for accufiz
lanemeier7 Oct 23, 2024
afd21fc
Converted accufiz docstrings from google style to numpy style
lanemeier7 Oct 24, 2024
5de5ce6
removed unnecessary sleep from accufiz classes
lanemeier7 Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 270 additions & 0 deletions catkit2/services/accufiz_interferometer/accufiz_interferometer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
import os
import h5py
import time
import requests
import uuid
import numpy as np
from scipy import ndimage
import math
from astropy.io import fits
from glob import glob
from catkit2.testbed.service import Service
import os
import threading



def rotate_and_flip_image(data, theta, flip):
"""
Rotate and/or flip the image data.

Args:
data (numpy.ndarray): Numpy array of image data.
theta (int): Rotation in degrees.
flip (bool): If True, flip the image horizontally.

Returns:
numpy.ndarray: Modified image after rotation and/or flip.
"""
raphaelpclt marked this conversation as resolved.
Show resolved Hide resolved
data_corr = np.rot90(data, int(theta / 90))

if flip:
data_corr = np.fliplr(data_corr)

return data_corr

class AccufizInterferometer(Service):
"""
Service class for the 4D Technologies Accufiz Interferometer.
It handles image acquisition, processing, and data handling.
This requires 4D Insight Web Service is run first and that the 4Sight software is set to be listening
"""
NUM_FRAMES_IN_BUFFER = 20
instrument_lib = requests

def __init__(self):
"""
Initialize the Accufiz Interferometer Simulator with configuration and set up data streams.
"""
super().__init__('accufiz_interferometer')

# Essential configurations
self.mask = self.config['mask']
self.server_path = self.config['server_path']
self.local_path = self.config['local_path']

# Optional configurations
self.ip = self.config.get('ip_address', 'localhost:8080')
self.calibration_data_package = self.config.get('calibration_data_package', '')
self.timeout = self.config.get('timeout', 10000)
self.post_save_sleep = self.config.get('post_save_sleep', 1)
self.file_mode = self.config.get('file_mode', True)
self.image_height = self.config.get('height', 1967)
self.image_width = self.config.get('width', 1970)
self.config_id = self.config.get('config_id', 'accufiz')
self.save_h5 = self.config.get('save_h5', True)
self.save_fits = self.config.get('save_fits', False)
self.num_frames_avg = self.config.get('num_avg', 2)
self.fliplr = self.config.get('fliplr', True)
self.rotate = self.config.get('rotate', 0)

# Set the 4D timeout.
self.html_prefix = f"http://{self.ip}/WebService4D/WebService4D.asmx"
set_timeout_string = f"{self.html_prefix}/SetTimeout?timeOut={self.timeout}"
self.get(set_timeout_string)

# Set the mask
self.set_mask()

# Create data streams.
self.detector_masks = self.make_data_stream('detector_masks', 'uint8', [self.image_height, self.image_width], self.NUM_FRAMES_IN_BUFFER)
self.images = self.make_data_stream('images', 'float32', [self.image_height, self.image_width], self.NUM_FRAMES_IN_BUFFER)
self.is_acquiring = self.make_data_stream('is_acquiring', 'int8', [1], 20)
self.is_acquiring.submit_data(np.array([0], dtype='int8'))
self.should_be_acquiring = threading.Event()
self.should_be_acquiring.clear()

self.make_command('take_measurement', self.take_measurement)
self.make_command('start_acquisition', self.start_acquisition)
self.make_command('end_acquisition', self.end_acquisition)

def set_mask(self):
"""
Set the mask for the simulator. The mask must be local to the 4D computer in a specified directory.
"""
filemask = self.mask
typeofmask = "Detector"
parammask = {"maskType": typeofmask, "fileName": filemask}
set_mask_string = f"{self.html_prefix}/SetMask"

self.post(set_mask_string, data=parammask)

return True

def get(self, url, params=None, **kwargs):
"""
HTTP GET request.

Args:
url (str): URL to send the GET request to.
params (dict, optional): Parameters for the request. Defaults to None.

Returns:
resp: response object.
"""
raphaelpclt marked this conversation as resolved.
Show resolved Hide resolved
resp = self.instrument_lib.get(url, params=params, **kwargs)
if resp.status_code != 200:
raise RuntimeError(f"{self.config_id} GET error: {resp.status_code}: {resp.text}")
return resp

def post(self, url, data=None, json=None, **kwargs):
"""
HTTP POST request.

Args:
url (str): URL to send the POST request to.
data (dict, optional): Data to send in the request. Defaults to None.
json (dict, optional): JSON data to send in the request. Defaults to None.

Returns:
resp: response object.
"""
raphaelpclt marked this conversation as resolved.
Show resolved Hide resolved
resp = self.instrument_lib.post(url, data=data, json=json, **kwargs)
if resp.status_code != 200:
raise RuntimeError(f"{self.config_id} POST error: {resp.status_code}: {resp.text}")
time.sleep(self.post_save_sleep)
return resp


def take_measurement(self):
"""
Take a measurement, save the data, and return the processed image.

Returns:
numpy.ndarray: Processed image data after measurement.
"""
raphaelpclt marked this conversation as resolved.
Show resolved Hide resolved
# Send request to take data.
resp = self.post(f"{self.html_prefix}/AverageMeasure", data={"count": int(self.num_frames_avg)})

if "success" not in resp.text:
raise RuntimeError(f"{self.config_id}: Failed to take data - {resp.text}.")

filename = str(uuid.uuid4())
server_file_path = os.path.join(self.server_path, filename)
local_file_path = os.path.join(self.local_path, filename)

# This line is here because when sent through webservice slashes tend
# to disappear. If we sent in parameter a path with only one slash,
# they disappear
server_file_path = server_file_path.replace('\\', '/')
raphaelpclt marked this conversation as resolved.
Show resolved Hide resolved
server_file_path = server_file_path.replace('/', '\\\\')

# Send request to save data.
self.post(f"{self.html_prefix}/SaveMeasurement", data={"fileName": server_file_path})

if not glob(f"{local_file_path}.h5"):
raise RuntimeError(f"{self.config_id}: Failed to save measurement data to '{local_file_path}'.")

local_file_path = local_file_path if local_file_path.endswith(".h5") else f"{local_file_path}.h5"
self.log.info(f"{self.config_id}: Succeeded to save measurement data to '{local_file_path}'")

mask = np.array(h5py.File(local_file_path, 'r').get('measurement0').get('Detectormask', 1))
img = np.array(h5py.File(local_file_path, 'r').get('measurement0').get('genraw').get('data')) * mask

self.detector_masks.submit_data(mask.astype(np.uint8))

image = self.convert_h5_to_fits(local_file_path, rotate=self.rotate, fliplr=self.fliplr, mask=mask, img=img, create_fits=self.save_fits)
# remove h5 file if configuration was not set
if (not self.save_h5) and os.path.exists(local_file_path):
os.remove(local_file_path)

return image

@staticmethod
def convert_h5_to_fits(filepath, rotate, fliplr, img, mask, wavelength=632.8, create_fits=False):
"""
Convert HDF5 data to FITS format and process image data.

Args:
filepath (str): Filepath for the HDF5 data.
rotate (int): Rotation angle in degrees.
fliplr (bool): If True, flip the image horizontally.
img (numpy.ndarray): Image data to be processed.
mask (numpy.ndarray): Mask data to be applied.
wavelength (float, optional): Wavelength for scaling. Defaults to 632.8 nm.
create_fits (bool, optional): If True, save the processed image as a FITS file.

Returns:
numpy.ndarray: Processed image data.
"""
raphaelpclt marked this conversation as resolved.
Show resolved Hide resolved
filepath = filepath if filepath.endswith(".h5") else f"{filepath}.h5"
fits_filepath = f"{os.path.splitext(filepath)[0]}.fits"

mask = np.array(h5py.File(filepath, 'r').get('measurement0').get('Detectormask', 1))
img = np.array(h5py.File(filepath, 'r').get('measurement0').get('genraw').get('data')) * mask

if create_fits:
fits.PrimaryHDU(mask).writeto(fits_filepath, overwrite=True)

radiusmask = np.int64(np.sqrt(np.sum(mask) / math.pi))
center = ndimage.measurements.center_of_mass(mask)

image = np.clip(img, -10, +10)[np.int64(center[0]) - radiusmask:np.int64(center[0]) + radiusmask - 1,
np.int64(center[1]) - radiusmask: np.int64(center[1]) + radiusmask - 1]

# Apply the rotation and flips.
image = rotate_and_flip_image(image, rotate, fliplr)

# Convert waves to nanometers.
image = image * wavelength

if create_fits:
fits_hdu = fits.PrimaryHDU(image)
fits_hdu.writeto(fits_filepath, overwrite=True)

return image

def main(self):
"""
Main loop to manage data acquisition and processing.
"""
while not self.should_shut_down:
if self.should_be_acquiring.wait(0.05):
self.acquisition_loop()

def acquisition_loop(self):
"""
Handle continuous data acquisition while the service is running.
"""
try:
self.is_acquiring.submit_data(np.array([1], dtype='int8'))

while self.should_be_acquiring.is_set() and not self.should_shut_down:
img = self.take_measurement()

has_correct_parameters = np.allclose(self.images.shape, img.shape)

if not has_correct_parameters:
self.images.update_parameters('float32', img.shape, 20)

self.images.submit_data(img.astype('float32'))
time.sleep(0.05)
raphaelpclt marked this conversation as resolved.
Show resolved Hide resolved
finally:
self.is_acquiring.submit_data(np.array([0], dtype='int8'))

def start_acquisition(self):
"""
Start the data acquisition process.
"""
self.should_be_acquiring.set()

def end_acquisition(self):
"""
End the data acquisition process.
"""
self.should_be_acquiring.clear()


if __name__ == '__main__':
service = AccufizInterferometer()
service.run()
Loading
Loading