diff --git a/catkit2/services/bmc_dm/bmc_dm.py b/catkit2/services/bmc_dm/bmc_dm.py deleted file mode 100644 index 5dd9a0f0a..000000000 --- a/catkit2/services/bmc_dm/bmc_dm.py +++ /dev/null @@ -1,153 +0,0 @@ -from catkit2.testbed.service import Service -from catkit2.testbed.tracing import trace_interval - -import time -import sys -import os -import threading -import numpy as np -from astropy.io import fits - -try: - sdk_path = os.environ.get('CATKIT_BOSTON_SDK_PATH') - if sdk_path is not None: - sys.path.append(sdk_path) - - import bmc -except ImportError: - print('To use Boston DMs, you need to set the CATKIT_BOSTON_SDK_PATH environment variable.') - raise - -class BmcDm(Service): - def __init__(self): - super().__init__('bmc_dm') - - self.serial_number = self.config['serial_number'] - self.command_length = self.config['command_length'] - self.flat_map_fname = self.config['flat_map_fname'] - self.gain_map_fname = self.config['gain_map_fname'] - self.max_volts = self.config['max_volts'] - - self.startup_maps = self.config.get('startup_maps', {}) - - self.lock = threading.Lock() - - self.channels = {} - self.channel_threads = {} - for channel in self.config['channels']: - self.add_channel(channel) - - channel_names = list(channel.lower() for channel in self.config['channels']) - self.make_property('channels', lambda: channel_names) - - self.total_voltage = self.make_data_stream('total_voltage', 'float64', [self.command_length], 20) - self.total_surface = self.make_data_stream('total_surface', 'float64', [self.command_length], 20) - - def add_channel(self, channel_name): - self.channels[channel_name] = self.make_data_stream(channel_name.lower(), 'float64', [self.command_length], 20) - - # Get the right default flat map. - if channel_name in self.startup_maps: - flatmap = fits.getdata(self.startup_maps[channel_name]).astype('float64') - else: - flatmap = np.zeros(self.command_length) - - self.channels[channel_name].submit_data(flatmap) - - def main(self): - self.channel_threads = {} - - # Start channel monitoring threads - for channel_name in self.channels.keys(): - thread = threading.Thread(target=self.monitor_channel, args=(channel_name,)) - thread.start() - - self.channel_threads[channel_name] = thread - - while not self.should_shut_down: - time.sleep(0.01) - - for thread in self.channel_threads.values(): - thread.join() - self.channel_threads = {} - - def monitor_channel(self, channel_name): - while not self.should_shut_down: - try: - self.channels[channel_name].get_next_frame(10) - except Exception: - # Timed out. This is used to periodically check the shutdown flag. - continue - - self.update_dm() - - def update_dm(self): - with trace_interval('update dm surface'): - with trace_interval('compute total surface'): - # Add up all channels to get the total surface. - total_surface = 0 - for stream in self.channels.values(): - total_surface += stream.get_latest_frame().data - - # Apply the command on the DM. - self.send_surface(total_surface) - - def send_surface(self, total_surface): - with trace_interval('send surface'): - # Submit this surface to the total surface data stream. - self.total_surface.submit_data(total_surface) - - with trace_interval('compute voltages'): - # Compute the voltages from the request total surface. - voltages = self.flat_map + total_surface * self.gain_map_inv - voltages /= self.max_volts - voltages = np.clip(voltages, 0, 1) - - dac_bit_depth = self.config['dac_bit_depth'] - - discretized_voltages = voltages - if dac_bit_depth is not None: - discretized_voltages = (np.floor(voltages * (2**dac_bit_depth))) / (2**dac_bit_depth) - - with trace_interval('send data'): - with self.lock: - status = self.device.send_data(voltages) - - if status != bmc.NO_ERR: - raise RuntimeError(f'Failed to send data: {self.device.error_string(status)}.') - - # Submit these voltages to the total voltage data stream. - self.total_voltage.submit_data(discretized_voltages) - - def open(self): - self.flat_map = fits.getdata(self.flat_map_fname) - self.gain_map = fits.getdata(self.gain_map_fname) - - with np.errstate(divide='ignore', invalid='ignore'): - self.gain_map_inv = 1 / self.gain_map - self.gain_map_inv[np.abs(self.gain_map) < 1e-10] = 0 - - self.device = bmc.BmcDm() - status = self.device.open_dm(self.serial_number) - - if status != bmc.NO_ERR: - raise RuntimeError(f'Failed to connect: {self.dm.error_string(status)}.') - - command_length = self.device.num_actuators() - if self.command_length != command_length: - raise ValueError(f'Command length in config: {self.command_length}. Command length on hardware: {command_length}.') - - zeros = np.zeros(self.command_length, dtype='float64') - self.send_surface(zeros) - - def close(self): - try: - zeros = np.zeros(self.command_length, dtype='float64') - self.send_surface(zeros) - finally: - self.device.close_dm() - self.device = None - -if __name__ == '__main__': - service = BmcDm() - service.run() diff --git a/catkit2/services/bmc_dm_sim/bmc_dm_sim.py b/catkit2/services/bmc_dm_sim/bmc_dm_sim.py deleted file mode 100644 index 5a9550339..000000000 --- a/catkit2/services/bmc_dm_sim/bmc_dm_sim.py +++ /dev/null @@ -1,120 +0,0 @@ -from catkit2.testbed.service import Service - -import threading -import numpy as np -from astropy.io import fits - -class BmcDmSim(Service): - def __init__(self): - super().__init__('bmc_dm_sim') - - self.serial_number = self.config['serial_number'] - self.command_length = self.config['command_length'] - self.flat_map_fname = self.config['flat_map_fname'] - self.gain_map_fname = self.config['gain_map_fname'] - self.max_volts = self.config['max_volts'] - - self.startup_maps = self.config.get('startup_maps', {}) - - self.flat_map = np.zeros(self.command_length) - self.gain_map = np.ones(self.command_length) - - self.lock = threading.Lock() - - self.channels = {} - self.channel_threads = {} - for channel in self.config['channels']: - self.add_channel(channel) - - channel_names = [channel.lower() for channel in self.config['channels']] - self.make_property('channels', lambda: channel_names) - - self.total_voltage = self.make_data_stream('total_voltage', 'float64', [self.command_length], 20) - self.total_surface = self.make_data_stream('total_surface', 'float64', [self.command_length], 20) - - def add_channel(self, channel_name): - self.channels[channel_name] = self.make_data_stream(channel_name, 'float64', [self.command_length], 20) - - # Get the right default flat map. - if channel_name in self.startup_maps: - flatmap = fits.getdata(self.startup_maps[channel_name]).astype('float64') - else: - flatmap = np.zeros(self.command_length) - - self.channels[channel_name].submit_data(flatmap) - - def main(self): - self.channel_threads = {} - - # Start channel monitoring threads - for channel_name in self.channels.keys(): - thread = threading.Thread(target=self.monitor_channel, args=(channel_name,)) - thread.start() - - self.channel_threads[channel_name] = thread - - while not self.should_shut_down: - self.sleep(0.1) - - for thread in self.channel_threads.values(): - thread.join() - self.channel_threads = {} - - def monitor_channel(self, channel_name): - while not self.should_shut_down: - try: - self.channels[channel_name].get_next_frame(10) - except Exception: - # Timed out. This is used to periodically check the shutdown flag. - continue - - self.update_dm() - - def update_dm(self): - # Add up all channels to get the total surface. - total_surface = 0 - for stream in self.channels.values(): - total_surface += stream.get_latest_frame().data - - # Apply the command on the DM. - self.send_surface(total_surface) - - def send_surface(self, total_surface): - # Submit this surface to the total surface data stream. - self.total_surface.submit_data(total_surface) - - # Compute the voltages from the request total surface. - voltages = self.flat_map + total_surface * self.gain_map_inv - voltages /= self.max_volts - voltages = np.clip(voltages, 0, 1) - - dac_bit_depth = self.config['dac_bit_depth'] - - discretized_voltages = voltages - discretized_surface = total_surface - - if dac_bit_depth is not None: - discretized_voltages = (np.floor(voltages * (2**dac_bit_depth))) / (2**dac_bit_depth) - discretized_surface = (discretized_voltages * self.max_volts - self.flat_map) * self.gain_map - - with self.lock: - self.testbed.simulator.actuate_dm(dm_name=self.id, new_actuators=discretized_surface) - - # Submit these voltages to the total voltage data stream. - self.total_voltage.submit_data(discretized_voltages) - - def open(self): - self.flat_map = fits.getdata(self.flat_map_fname) - self.gain_map = fits.getdata(self.gain_map_fname) - - with np.errstate(divide='ignore', invalid='ignore'): - self.gain_map_inv = 1 / self.gain_map - self.gain_map_inv[np.abs(self.gain_map) < 1e-10] = 0 - - zeros = np.zeros(self.command_length, dtype='float64') - self.send_surface(zeros) - - -if __name__ == '__main__': - service = BmcDmSim() - service.run() diff --git a/catkit2/testbed/proxies/__init__.py b/catkit2/testbed/proxies/__init__.py index 068fea619..6219eec77 100644 --- a/catkit2/testbed/proxies/__init__.py +++ b/catkit2/testbed/proxies/__init__.py @@ -2,7 +2,6 @@ 'CameraProxy', 'NewportXpsQ8Proxy', 'FlipMountProxy', - 'BmcDmProxy', 'DeformableMirrorProxy', 'NewportPicomotorProxy', 'NiDaqProxy', @@ -13,7 +12,6 @@ 'OceanopticsSpectroProxy' ] -from .bmc_dm import * from .camera import * from .deformable_mirror import * from .newport_xps import * diff --git a/catkit2/testbed/proxies/bmc_dm.py b/catkit2/testbed/proxies/bmc_dm.py deleted file mode 100644 index 5a732fd74..000000000 --- a/catkit2/testbed/proxies/bmc_dm.py +++ /dev/null @@ -1,66 +0,0 @@ -from ..service_proxy import ServiceProxy - -import numpy as np -from astropy.io import fits -import hcipy - - -class BmcDmProxy(ServiceProxy): - @property - def dm_mask(self): - if not hasattr(self, '_dm_mask'): - fname = self.config['dm_mask_fname'] - - self._dm_mask = fits.getdata(fname).astype('bool') - - return self._dm_mask - - @property - def num_actuators(self): - return self.config['num_actuators'] - - @property - def actuator_grid(self): - dims = self.dm_mask.shape[::-1] - - return hcipy.make_uniform_grid(dims, dims) - - def dm_shapes_to_command(self, dm1_shape, dm2_shape=None): - command = np.zeros(2048) - - if dm2_shape is None: - command[:952] = dm1_shape[:952] - command[1024:1024 + 952] = dm1_shape[952:] - else: - command[:952] = dm1_shape[self.dm_mask] - command[1024:1024 + 952] = dm2_shape[self.dm_mask] - - return command - - def flatten_channels(self, channel_names): - summed_command = 0 - - if isinstance(channel_names, str): - channel_names = [channel_names] - - # Get commands from channels, zero each channel, and sum commands - for channel_name in channel_names: - summed_command += getattr(self, channel_name).get_latest_frame().data - self.apply_shape(channel_name, np.zeros(2 * self.num_actuators)) - - # Return summed command (note that this is not a DM shape) - return summed_command - - def command_to_dm_shapes(self, command): - dm1_shape = np.zeros((34, 34)) - dm2_shape = np.zeros((34, 34)) - - dm1_shape[self.dm_mask] = command[:952] - dm2_shape[self.dm_mask] = command[1024:1024 + 952] - - return dm1_shape, dm2_shape - - def apply_shape(self, channel, dm1_shape, dm2_shape=None): - command = self.dm_shapes_to_command(dm1_shape, dm2_shape) - - getattr(self, channel).submit_data(command) diff --git a/docs/index.rst b/docs/index.rst index 30fdedbec..5a249cf32 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,7 +22,6 @@ Catkit2 services/accufiz_interferometer services/aimtti_plp services/allied_vision_camera - services/bmc_dm services/bmc_deformable_mirror services/camera_sim services/deformable_mirror diff --git a/docs/services/bmc_dm.rst b/docs/services/bmc_dm.rst deleted file mode 100644 index 4a9874d19..000000000 --- a/docs/services/bmc_dm.rst +++ /dev/null @@ -1,58 +0,0 @@ -Boston Deformable Mirror -======================== -This service operates a pair of identical Boston Micromachines MEMS DMs controlled by the same driver. The following Boston DMs have been tested with catkit2 thus far: - -- `BMC DM Kilo-C-1.5 `_ - -This service is been superseded by the :ref:`here `. - -Configuration -------------- -.. code-block:: YAML - - boston_dm: - service_type: bmc_dm - simulated_service_type: bmc_dm_sim - interface: bmc_dm - requires_safety: true - - serial_number: 0000 - command_length: 2048 - num_actuators: 952 - dac_bit_depth: 14 - max_volts: 200 - - flat_map_fname: !path ../flat_data.fits - gain_map_fname: !path ../gain_map.fits - dm_mask_fname: !path ../dm_mask.fits - - startup_maps: - flat: !path ../flat_data.fits - - channels: - - correction_howfs - - correction_lowfs - - probe - - poke - - aberration - - atmosphere - - astrogrid - - resume - - flat - -Properties ----------- -``channels``: List of command channel names (dict). - -Commands --------- - -None. - -Datastreams ------------ -``total_voltage``: Array of the total voltage applied to each actuator of the DM. - -``total_surface``: Array of the total amplitude of each DM actuator (nanometers). - -``channels[channel_name]``: The command (nm surface) per virtual channel, identified by channel name. diff --git a/setup.py b/setup.py index 450a651f0..e7a9029c4 100644 --- a/setup.py +++ b/setup.py @@ -150,8 +150,6 @@ def build_extension(self, ext): 'allied_vision_camera = catkit2.services.allied_vision_camera.allied_vision_camera', 'bmc_deformable_mirror_hardware = catkit2.services.bmc_deformable_mirror_hardware.bmc_deformable_mirror_hardware', 'bmc_deformable_mirror_sim = catkit2.services.bmc_deformable_mirror_sim.bmc_deformable_mirror_sim', - 'bmc_dm = catkit2.services.bmc_dm.bmc_dm', - 'bmc_dm_sim = catkit2.services.bmc_dm_sim.bmc_dm_sim', 'camera_sim = catkit2.services.camera_sim.camera_sim', 'dummy_camera = catkit2.services.dummy_camera.dummy_camera', 'empty_service = catkit2.services.empty_service.empty_service', @@ -192,7 +190,6 @@ def build_extension(self, ext): 'zwo_camera = catkit2.services.zwo_camera.zwo_camera', ], 'catkit2.proxies': [ - 'bmc_dm = catkit2.testbed.proxies.bmc_dm:BmcDmProxy', 'camera = catkit2.testbed.proxies.camera:CameraProxy', 'deformable_mirror = catkit2.testbed.proxies.deformable_mirror:DeformableMirrorProxy', 'flip_mount = catkit2.testbed.proxies.flip_mount:FlipMountProxy', diff --git a/tests/config/services.yml b/tests/config/services.yml index 5de63c155..9384515c4 100644 --- a/tests/config/services.yml +++ b/tests/config/services.yml @@ -1,10 +1,10 @@ dummy_dm_service: service_type: dummy_dm_service requires_safety: false - interface: bmc_dm + interface: deformable_mirror - num_actuators: 952 - dm_shape: 2048 + device_actuator_mask_fname: !path ../data/dm_mask.fits + num_actuators_all_dms: 1904 dummy_service: service_type: dummy_service diff --git a/tests/data/dm_mask.fits b/tests/data/dm_mask.fits new file mode 100644 index 000000000..fe3cd3d8b Binary files /dev/null and b/tests/data/dm_mask.fits differ diff --git a/tests/services/dummy_dm_service/dummy_dm_service.py b/tests/services/dummy_dm_service/dummy_dm_service.py index 78b916cf2..40ddec65f 100644 --- a/tests/services/dummy_dm_service/dummy_dm_service.py +++ b/tests/services/dummy_dm_service/dummy_dm_service.py @@ -2,24 +2,25 @@ import numpy as np + class DummyDmService(Service): def __init__(self): super().__init__('dummy_dm_service') self.channel_names = ['correction_howfs', 'correction_lowfs', 'aberration', 'atmosphere'] - - self.dm_shape = self.config['dm_shape'] + self.num_actuators_all_dms = self.config['num_actuators_all_dms'] def open(self): # Make channels streamable for channel in self.channel_names: - setattr(self, channel, self.make_data_stream(channel, 'float64', [self.dm_shape], 20)) - getattr(self, channel).submit_data(np.zeros(self.dm_shape,)) + setattr(self, channel, self.make_data_stream(channel, 'float64', [self.num_actuators_all_dms], 20)) + getattr(self, channel).submit_data(np.zeros(self.num_actuators_all_dms,)) def main(self): while not self.should_shut_down: self.sleep(0.1) + if __name__ == '__main__': service = DummyDmService() service.run() diff --git a/tests/test_dm_commands.py b/tests/test_dm_commands.py index 2bfca6e56..4de4bd0cb 100644 --- a/tests/test_dm_commands.py +++ b/tests/test_dm_commands.py @@ -5,8 +5,8 @@ def test_flatten_single_channel(testbed): # Test with single channel dm_proxy = testbed.dummy_dm_service - expected_zero_command = np.zeros(dm_proxy.config['dm_shape']) - initial_command = np.ones(dm_proxy.config['dm_shape']) + expected_zero_command = np.zeros(dm_proxy.num_dms * dm_proxy.num_actuators) + initial_command = np.ones(dm_proxy.num_dms * dm_proxy.num_actuators) dm_proxy.correction_howfs.submit_data(initial_command) previous_dm_command = dm_proxy.flatten_channels('correction_howfs') @@ -19,14 +19,14 @@ def test_flatten_multiple_channels(testbed): # Flatten multiple channels dm_proxy = testbed.dummy_dm_service - initial_command = np.ones(dm_proxy.config['dm_shape']) + initial_command = np.ones(dm_proxy.num_dms * dm_proxy.num_actuators) dm_proxy.correction_lowfs.submit_data(initial_command) dm_proxy.atmosphere.submit_data(initial_command) dm_proxy.aberration.submit_data(initial_command) num_channels_summed = 3 - expected_summed_command = num_channels_summed*initial_command - expected_zero_command = np.zeros(dm_proxy.config['dm_shape']) + expected_summed_command = num_channels_summed * initial_command + expected_zero_command = np.zeros(dm_proxy.num_dms * dm_proxy.num_actuators) move_command = dm_proxy.flatten_channels(['correction_lowfs', 'atmosphere', 'aberration']) assert np.allclose(move_command, expected_summed_command)