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 21 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
151 changes: 151 additions & 0 deletions catkit2/services/accufiz_interferometer/accufiz_interferometer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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
from catkit2.testbed.service_proxy import ServiceProxy
from catkit2.utils import rotate_and_flip_image

@ServiceProxy.register_service_interface('accufiz_interferometer')
raphaelpclt marked this conversation as resolved.
Show resolved Hide resolved
class AccufizInterferometer(Service):
NUM_FRAMES_IN_BUFFER = 20

def __init__(self):
super().__init__('accufiz_interferometer')

# mask, server path, local path are required
self.mask = self.config['mask']
self.server_path = self.config['server_path']
self.local_path = self.config['local_path']

# these are the optional configurations and will automatically default to something for convenience
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')

# 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}"

# set 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.make_command('take_measurement', self.take_measurement)


instrument_lib = requests

def set_mask(self):
# Set the Mask. This mask has to be local to the 4D computer in this 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):
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):
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, num_frames=2):
# Send request to take data.
resp = self.post(f"{self.html_prefix}/AverageMeasure", data={"count": int(num_frames)})

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))
self.images.submit_data(img.astype(np.float32))

image = self.convert_h5_to_fits(local_file_path, rotate=0, fliplr=True, mask=mask, img=img)
return image


@staticmethod
def convert_h5_to_fits(filepath, rotate, fliplr, img, mask, wavelength=632.8):

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

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

fits_hdu = fits.PrimaryHDU(image)
fits_hdu.writeto(fits_filepath, overwrite=True)
return image

def main(self):
while not self.should_shut_down:
self.sleep(0.1)


if __name__ == '__main__':
service = AccufizInterferometer()
service.run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
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
from catkit2.testbed.service_proxy import ServiceProxy
from catkit2.utils import rotate_and_flip_image
import tempfile
import os

class sim_response:
text = 'success'

import numpy as np

def generate_circle_array(radius=1, h=256, w=256):
# radius is the proportion of the circle (0.5 is half the size)
# h is the height of the rectangle
# w is the width of the rectangle array with circle
# the purpose of this function is to generate a dummy mask

# Create a grid of points with shape (grid_size, grid_size)
x = np.linspace(-1, 1, w)
y = np.linspace(-1, 1, h)
xx, yy = np.meshgrid(x, y)

# Calculate the distance from the origin (0,0) for each point in the grid
distances = np.sqrt(xx**2 + yy**2)

# Create the array: 1 inside the circle, 0 outside
circle_array = np.where(distances <= radius, 1, 0)

return circle_array.astype('uint8')


@ServiceProxy.register_service_interface('accufiz_interferometer_sim')
class AccufizInterferometerSim(Service):
NUM_FRAMES_IN_BUFFER = 20

def __init__(self):
super().__init__('accufiz_interferometer_sim')

# mask, server path, local path are required
self.mask = self.config['mask']
self.server_path = self.config['server_path']
self.local_path = self.config['local_path']

# these are the optional configurations and will automatically default to something for convenience
self.ip = self.config.get('ip_address', 'localhost:8080')
self.calibration_data_package = self.config.get('calibration_data_package', '')
self.sim_data = self.config.get('sim_data', None)
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')

# 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}"

# set 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.make_command('take_measurement', self.take_measurement)


instrument_lib = requests

def set_mask(self):
# Set the Mask. This mask has to be local to the 4D computer in this 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


# TODO: could store a realistic list of POST/GET response expectations. But need to store the commands as retreived from the hardware.
def get(self, url, params=None, **kwargs):
return sim_response()

def post(self, url, data=None, json=None, **kwargs):
time.sleep(self.post_save_sleep)
return sim_response()


def take_measurement(self, num_frames=2):
# Send request to take data.
resp = self.post(f"{self.html_prefix}/AverageMeasure", data={"count": int(num_frames)})
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)
temp_file = None
if self.sim_data is None:
# Create a temporary file with fake data
# this way we don't need to store data in the catkit repo
temp_file = tempfile.mkdtemp()
local_file_path = temp_file
fname = local_file_path + '.h5'
tmph5f = h5py.File(fname, 'w')
tmph5f['measurement0/Detectormask'] = generate_circle_array(radius=1, h=self.image_height, w=self.image_width)
tmph5f['measurement0/genraw/data'] = np.random.rand(self.image_height, self.image_width)
tmph5f.close()

else:
local_file_path = self.sim_data.replace('.h5', '')

# 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))
self.images.submit_data(img.astype(np.float32))

image = self.convert_h5_to_fits(local_file_path, rotate=0, fliplr=True, mask=mask, img=img)
if temp_file:
os.remove(local_file_path)
os.remove(local_file_path.replace('.h5', '.fits'))
self.log.info('cleaning up temporary simulated files')
return image


@staticmethod
def convert_h5_to_fits(filepath, rotate, fliplr, img, mask, wavelength=632.8):

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

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

fits_hdu = fits.PrimaryHDU(image)
fits_hdu.writeto(fits_filepath, overwrite=True)
return image

def main(self):
while not self.should_shut_down:
self.sleep(0.1)


if __name__ == '__main__':
service = AccufizInterferometerSim()
service.run()
16 changes: 16 additions & 0 deletions catkit2/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import numpy as np

def rotate_and_flip_image(data, theta, flip):
"""
Converts an image based on rotation and flip parameters.
:param data: Numpy array of image data.
:param theta: Rotation in degrees of the mounted camera, only these discrete values {0, 90, 180, 270}
:param flip: Boolean for whether to flip the data using np.fliplr.
:return: Converted numpy array.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use the numpy docstring format (some non-compliant docstrings might exist in the repos, but we try to get rid of them) - could you change to that format?
https://numpydoc.readthedocs.io/en/latest/format.html

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do!

"""
data_corr = np.rot90(data, int(theta / 90))

if flip:
data_corr = np.fliplr(data_corr)

return data_corr
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Catkit2
:maxdepth: 1
:caption: Built-In Services

services/accufiz_interferometer
services/aimtti_plp
services/allied_vision_camera
services/bmc_dm
Expand Down
Empty file.
1 change: 1 addition & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies:
- networkx
- pytest
- flake8
- h5py
- pip:
- dcps
- zwoasi>=0.0.21
Expand Down
Loading