-
Notifications
You must be signed in to change notification settings - Fork 3
Feature/4d service #255
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
Merged
Merged
Feature/4d service #255
Changes from all 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 c215c77
first pass at accufiz_interferometer service
steigersg a824fa0
add h5py to environment.yml for 4d
steigersg 2f758e1
import fixes
steigersg 79e7de1
add height and width parameters to the config
steigersg c9381e8
remove unused code
steigersg 8b520a7
remove call to catkit util
steigersg 010855c
better file path management
steigersg bc6241e
remove more unused code with deprecated catkit call
steigersg 3b8539f
add accufiz_interferometer.rst doc file as a placeholder to fill in b…
steigersg 8448ece
cleaning
steigersg be70bdd
add dtype when submitting data to the datastreams
steigersg b8e3431
remove defaults for some config items and add local and server paths
steigersg 95cd86b
remove deprecated code
steigersg fa1c271
Revert "remove deprecated code"
steigersg 216e8cc
Merge branch 'develop' into feature/4d_service
lanemeier7 33fbf19
Merge branch 'develop' into feature/4d_service
lanemeier7 ff84516
Created catkit2 utils for flip/rotate function. Added accufiz_interfe…
lanemeier7 075545e
Merge remote-tracking branch 'origin/develop' into feature/4d_service
lanemeier7 a8b3b0f
accufiz simulator generates its own data if sim_data path is not in c…
lanemeier7 a2170aa
fixed some documentation typos
lanemeier7 96f2ddb
accufiz_interferometer now following CameraProxy service interface
lanemeier7 6353335
accufiz_interferometer changes per linter fail
lanemeier7 7057d5c
accufiz_interferometer changes per flake8 linter
lanemeier7 0e30146
uitls.py flake8 adjustments
lanemeier7 e40c11e
fixed bug where accufiz save_fits config was not actually stopping FI…
lanemeier7 676b578
Removed utils.py added fliplr and rotate to accufiz config, used newe…
lanemeier7 1bb66c9
added docstrings to accufiz_interferometer and sim service
lanemeier7 e55f2a7
Merge branch 'develop' into feature/4d_service
lanemeier7 bd70a00
Merge remote-tracking branch 'origin/develop' into feature/4d_service
lanemeier7 dee6f8e
Merge branch 'feature/4d_service' of github.com:spacetelescope/catkit…
lanemeier7 c80dc29
Filled out doxygen documentation for accufiz
lanemeier7 afd21fc
Converted accufiz docstrings from google style to numpy style
lanemeier7 5de5ce6
removed unnecessary sleep from accufiz classes
lanemeier7 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
320 changes: 320 additions & 0 deletions
320
catkit2/services/accufiz_interferometer/accufiz_interferometer.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,320 @@ | ||
| 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. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| data : numpy.ndarray | ||
| Numpy array of image data. | ||
| theta : int | ||
| Rotation angle in degrees. | ||
| flip : bool | ||
| If True, flip the image horizontally. | ||
|
|
||
| Returns | ||
| ------- | ||
| numpy.ndarray | ||
| Modified image after rotation and/or flip. | ||
| """ | ||
| 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 to be running, and the 4Sight software to be set to 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. | ||
|
|
||
| Returns | ||
| ------- | ||
| bool | ||
| True if the mask is successfully set. | ||
| """ | ||
| 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. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| url : str | ||
| URL to send the GET request to. | ||
| params : dict, optional | ||
| Parameters for the request. Defaults to None. | ||
|
|
||
| Returns | ||
| ------- | ||
| resp | ||
| Response object. | ||
|
|
||
| Raises | ||
| ------ | ||
| RuntimeError | ||
| If the GET request fails. | ||
| """ | ||
| 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. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| 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. | ||
|
|
||
| Raises | ||
| ------ | ||
| RuntimeError | ||
| If the POST request fails. | ||
| """ | ||
| 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. | ||
|
|
||
| Raises | ||
| ------ | ||
| RuntimeError | ||
| If data acquisition or saving fails. | ||
| """ | ||
| # 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('\\', '/') | ||
| 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 HDF5 file if not required | ||
| 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. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| 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, default is 632.8 nm. | ||
| create_fits : bool, optional | ||
| If True, save the processed image as a FITS file. | ||
|
|
||
| Returns | ||
| ------- | ||
| numpy.ndarray | ||
| Processed image data. | ||
| """ | ||
| 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')) | ||
| 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() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.