From 660e3ce549f709b871d3e28b4549fc63c62159cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:12:56 +0000 Subject: [PATCH 01/12] Initial plan From ccf38df3014eda10ec9ebee21ab756ce3ede4b25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:24:53 +0000 Subject: [PATCH 02/12] Create NKT SuperK base class and refactor EVO/FIANIUM services Co-authored-by: ivalaginja <29508965+ivalaginja@users.noreply.github.com> --- catkit2/services/nkt_superk_base.py | 252 ++++++++++++++++++ .../services/nkt_superk_evo/nkt_superk_evo.py | 189 +------------ .../nkt_superk_fianium/nkt_superk_fianium.py | 191 +------------ tests/test_nkt_superk_base.py | 132 +++++++++ 4 files changed, 412 insertions(+), 352 deletions(-) create mode 100644 catkit2/services/nkt_superk_base.py create mode 100644 tests/test_nkt_superk_base.py diff --git a/catkit2/services/nkt_superk_base.py b/catkit2/services/nkt_superk_base.py new file mode 100644 index 000000000..0e97cb366 --- /dev/null +++ b/catkit2/services/nkt_superk_base.py @@ -0,0 +1,252 @@ +"""Base class for NKT SuperK white light sources. + +This module provides a common base class for NKT SuperK devices, +eliminating code duplication between EVO and FIANIUM services. +""" + +from catkit2.testbed.service import Service + +import numpy as np +from concurrent.futures import ThreadPoolExecutor +import threading +from enum import Enum +import os +import sys +from abc import ABC, abstractmethod + + +try: + sdk_path = os.path.join(os.environ.get('NKTP_SDK_PATH'), 'Examples', 'DLL_Example_Python') + if sdk_path is not None: + sys.path.append(sdk_path) + + from NKTP_DLL import * +except ImportError: + print('To use NKT SDK, you need to install the SDK and check the NKTP_SDK_PATH environment variable.') + raise + + +class Varia(Enum): + """Registers for the NKT SuperK VARIA device.""" + DEVICE_ID = 16 + + # SuperK VARIA registers + REG_MONITOR_INPUT = 0x13 + + REG_ND_SETPOINT = 0x32 + REG_SWP_SETPOINT = 0x33 + REG_LWP_SETPOINT = 0x34 + + REG_STATUS_BITS = 0x66 + + +def read_register(read_func, register, *, ratio=1, index=-1): + """Helper function to create register read methods.""" + def getter(self): + device_id = register.__class__.DEVICE_ID + + future = self.pool.submit(read_func, self.port, device_id.value, register.value, index) + result, value = future.result() + + self.check_result(result) + + return value * ratio + + return getter + + +def write_register(write_func, register, *, ratio=1, index=-1): + """Helper function to create register write methods.""" + def setter(self, value): + device_id = register.__class__.DEVICE_ID + + # Convert the value to the register value. This assumes integer types. + register_value = int(value / ratio) + + self.log.debug(f'Writing value {register_value} to {register}.') + + future = self.pool.submit(write_func, self.port, device_id.value, register.value, register_value, index) + result = future.result() + + self.check_result(result) + + return setter + + +class NktSuperkBase(Service, ABC): + """Abstract base class for NKT SuperK white light sources. + + This class contains common functionality shared between different + NKT SuperK devices like EVO and FIANIUM. Both the SuperK source and + VARIA are controlled through this service due to the need for a + single open port to the device. + """ + + def __init__(self, service_type): + """Initialize the NKT SuperK base service. + + Args: + service_type: The specific service type (e.g., 'nkt_superk_evo') + """ + super().__init__(service_type) + + self.threads = {} + self.port = self.config['port'] + + def open(self): + """Open the service and initialize data streams.""" + # Common data streams for VARIA + self.monitor_input = self.make_data_stream('monitor_input', 'float32', [1], 20) + + self.nd_setpoint = self.make_data_stream('nd_setpoint', 'float32', [1], 20) + self.swp_setpoint = self.make_data_stream('swp_setpoint', 'float32', [1], 20) + self.lwp_setpoint = self.make_data_stream('lwp_setpoint', 'float32', [1], 20) + + self.nd_filter_moving = self.make_data_stream('nd_filter_moving', 'uint8', [1], 20) + self.swp_filter_moving = self.make_data_stream('swp_filter_moving', 'uint8', [1], 20) + self.lwp_filter_moving = self.make_data_stream('lwp_filter_moving', 'uint8', [1], 20) + + # Common data streams for SuperK source + self.emission = self.make_data_stream('emission', 'uint8', [1], 20) + + # Set initial VARIA setpoints from config + self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) + self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) + self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) + + # Set initial emission from config + self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) + + # Create device-specific data streams + self._create_device_specific_streams() + + # Define common thread functions + common_funcs = { + 'nd_setpoint': self.monitor_func(self.nd_setpoint, self.set_nd_setpoint), + 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), + 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), + 'emission': self.monitor_func(self.emission, self.set_emission), + 'varia_status': self.update_func(self.update_varia_status) + } + + # Get device-specific thread functions and merge + device_funcs = self._get_device_specific_funcs() + funcs = {**common_funcs, **device_funcs} + + # Create a pool with a single worker to perform communication with the device + self.pool = ThreadPoolExecutor(max_workers=1) + + # Open port + future = self.pool.submit(openPorts, self.port, autoMode=0, liveMode=0) + self.check_result(future.result()) + + # Start all threads + for key, func in funcs.items(): + thread = threading.Thread(target=func) + thread.start() + self.threads[key] = thread + + def main(self): + """Main service loop.""" + while not self.should_shut_down: + self.sleep(1) + + def close(self): + """Close the service and cleanup resources.""" + # Turn off the source + self.set_emission(0) + + # Device-specific cleanup + self._device_specific_cleanup() + + # Join all threads + for thread in self.threads.values(): + thread.join() + + # Close port + future = self.pool.submit(closePorts, self.port) + self.check_result(future.result()) + + # Close pool + self.pool.shutdown() + + def check_result(self, result): + """Check if an NKT SDK operation was successful.""" + if result != 0: + self.log.error('NKT error: ' + RegisterResultTypes(result)) + raise RuntimeError(RegisterResultTypes(result)) + + def update_varia_status(self): + """Update VARIA status information.""" + status = self.get_varia_status_bits() + + # Extract moving filters from status + nd_filter_moving = (status & (2 << 12)) > 0 + swp_filter_moving = (status & (2 << 13)) > 0 + lwp_filter_moving = (status & (2 << 14)) > 0 + + # Submit results to their respective datastreams + self.nd_filter_moving.submit_data(np.array([nd_filter_moving], dtype='uint8')) + self.swp_filter_moving.submit_data(np.array([swp_filter_moving], dtype='uint8')) + self.lwp_filter_moving.submit_data(np.array([lwp_filter_moving], dtype='uint8')) + + # Update input monitor + monitor_input = self.get_monitor_input() + self.monitor_input.submit_data(np.array([monitor_input], dtype='float32')) + + def monitor_func(self, stream, setter): + """Create a monitoring function for a data stream.""" + def func(): + while not self.should_shut_down: + try: + frame = stream.get_next_frame(1) + except Exception: + continue + + setter(frame.data[0]) + + return func + + def update_func(self, updater): + """Create an update function that runs periodically.""" + def func(): + while not self.should_shut_down: + updater() + self.sleep(1) + + return func + + # Common VARIA register operations + get_monitor_input = read_register(registerReadU16, Varia.REG_MONITOR_INPUT, ratio=0.1) + + get_nd_setpoint = read_register(registerReadU16, Varia.REG_ND_SETPOINT, ratio=0.1) + get_swp_setpoint = read_register(registerReadU16, Varia.REG_SWP_SETPOINT, ratio=0.1) + get_lwp_setpoint = read_register(registerReadU16, Varia.REG_LWP_SETPOINT, ratio=0.1) + + set_nd_setpoint = write_register(registerWriteU16, Varia.REG_ND_SETPOINT, ratio=0.1) + set_swp_setpoint = write_register(registerWriteU16, Varia.REG_SWP_SETPOINT, ratio=0.1) + set_lwp_setpoint = write_register(registerWriteU16, Varia.REG_LWP_SETPOINT, ratio=0.1) + + get_varia_status_bits = read_register(registerReadU16, Varia.REG_STATUS_BITS) + + # Abstract methods that must be implemented by subclasses + @abstractmethod + def _create_device_specific_streams(self): + """Create device-specific data streams.""" + pass + + @abstractmethod + def _get_device_specific_funcs(self): + """Get device-specific thread functions.""" + pass + + @abstractmethod + def _device_specific_cleanup(self): + """Perform device-specific cleanup.""" + pass + + # Abstract register operations that must be implemented by subclasses + @abstractmethod + def set_emission(self, emission): + """Set emission state for the device.""" + pass diff --git a/catkit2/services/nkt_superk_evo/nkt_superk_evo.py b/catkit2/services/nkt_superk_evo/nkt_superk_evo.py index 4f1472f34..ff569a02b 100644 --- a/catkit2/services/nkt_superk_evo/nkt_superk_evo.py +++ b/catkit2/services/nkt_superk_evo/nkt_superk_evo.py @@ -1,18 +1,9 @@ -from catkit2.testbed.service import Service +from catkit2.services.nkt_superk_base import NktSuperkBase, read_register, write_register import numpy as np -from concurrent.futures import ThreadPoolExecutor -import threading from enum import Enum -import os -import sys - try: - sdk_path = os.path.join(os.environ.get('NKTP_SDK_PATH'), 'Examples', 'DLL_Example_Python') - if sdk_path is not None: - sys.path.append(sdk_path) - from NKTP_DLL import * except ImportError: print('To use NKT SDK, you need to install the SDK and check the NKTP_SDK_PATH environment variable.') @@ -46,51 +37,7 @@ class Evo(Enum): REG_MAC_ADDRESS = 0xB3 -class Varia(Enum): - DEVICE_ID = 16 - - # SuperK VARIA registers - REG_MONITOR_INPUT = 0x13 - - REG_ND_SETPOINT = 0x32 - REG_SWP_SETPOINT = 0x33 - REG_LWP_SETPOINT = 0x34 - - REG_STATUS_BITS = 0x66 - - -def read_register(read_func, register, *, ratio=1, index=-1): - def getter(self): - device_id = register.__class__.DEVICE_ID - - future = self.pool.submit(read_func, self.port, device_id.value, register.value, index) - result, value = future.result() - - self.check_result(result) - - return value * ratio - - return getter - - -def write_register(write_func, register, *, ratio=1, index=-1): - def setter(self, value): - device_id = register.__class__.DEVICE_ID - - # Convert the value to the register value. This assumes integer types. - register_value = int(value / ratio) - - self.log.debug(f'Writing value {register_value} to {register}.') - - future = self.pool.submit(write_func, self.port, device_id.value, register.value, register_value, index) - result = future.result() - - self.check_result(result) - - return setter - - -class NktSuperkEvo(Service): +class NktSuperkEvo(NktSuperkBase): '''The service for both the NKT SuperK EVO and NKT SuperK VARIA. Both devices are combined into a single service due to the need for @@ -100,92 +47,35 @@ class NktSuperkEvo(Service): def __init__(self): super().__init__('nkt_superk_evo') - self.threads = {} - self.port = self.config['port'] - - def open(self): - # Make datastreams. + def _create_device_specific_streams(self): + """Create EVO-specific data streams.""" + # EVO-specific streams self.base_temperature = self.make_data_stream('base_temperature', 'float32', [1], 20) self.supply_voltage = self.make_data_stream('supply_voltage', 'float32', [1], 20) self.external_control_input = self.make_data_stream('external_control_input', 'float32', [1], 20) - self.emission = self.make_data_stream('emission', 'uint8', [1], 20) self.power_setpoint = self.make_data_stream('power_setpoint', 'float32', [1], 20) self.current_setpoint = self.make_data_stream('current_setpoint', 'float32', [1], 20) - self.monitor_input = self.make_data_stream('monitor_input', 'float32', [1], 20) - - self.nd_setpoint = self.make_data_stream('nd_setpoint', 'float32', [1], 20) - self.swp_setpoint = self.make_data_stream('swp_setpoint', 'float32', [1], 20) - self.lwp_setpoint = self.make_data_stream('lwp_setpoint', 'float32', [1], 20) - - self.nd_filter_moving = self.make_data_stream('nd_filter_moving', 'uint8', [1], 20) - self.swp_filter_moving = self.make_data_stream('swp_filter_moving', 'uint8', [1], 20) - self.lwp_filter_moving = self.make_data_stream('lwp_filter_moving', 'uint8', [1], 20) - - # Set current setpoints. These will be actually set on the device - # once the monitor threads have started. - self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) + # Set initial EVO setpoints from config self.power_setpoint.submit_data(np.array([self.config['power_setpoint']], dtype='float32')) self.current_setpoint.submit_data(np.array([self.config['current_setpoint']], dtype='float32')) - self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) - self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) - self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) - - # Define thread functions. - funcs = { - 'nd_setpoint': self.monitor_func(self.nd_setpoint, self.set_nd_setpoint), - 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), - 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), - 'emission': self.monitor_func(self.emission, self.set_emission), + def _get_device_specific_funcs(self): + """Get EVO-specific thread functions.""" + return { 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), 'current_setpoint': self.monitor_func(self.current_setpoint, self.set_current_setpoint), - 'varia_status': self.update_func(self.update_varia_status), 'evo_status': self.update_func(self.update_evo_status) } - # Create a pool with a single worker to perform communication with the device. - self.pool = ThreadPoolExecutor(max_workers=1) - - # Open port. - # Make sure that the device is available (ie. not being used by someone else). - # This is not strictly necessary according to the SDK, but speeds up reading/writing. - future = self.pool.submit(openPorts, self.port, autoMode=0, liveMode=0) - self.check_result(future.result()) - - # Start all threads. - for key, func in funcs.items(): - thread = threading.Thread(target=func) - thread.start() - - self.threads[key] = thread - - def main(self): - while not self.should_shut_down: - self.sleep(1) - - def close(self): - # Turn off the source - self.set_emission(0) - - # Join all threads. - for thread in self.threads.values(): - thread.join() - - # Close port. - future = self.pool.submit(closePorts, self.port) - self.check_result(future.result()) - - # Close pool. - self.pool.shutdown() - - def check_result(self, result): - if result != 0: - self.log.error('NKT error: ' + RegisterResultTypes(result)) - raise RuntimeError(RegisterResultTypes(result)) + def _device_specific_cleanup(self): + """Perform EVO-specific cleanup.""" + # No specific cleanup needed for EVO + pass def update_evo_status(self): + """Update EVO-specific status information.""" temperature = self.get_base_temperature() self.base_temperature.submit_data(np.array([temperature], dtype='float32')) @@ -195,44 +85,6 @@ def update_evo_status(self): control_input = self.get_external_control_input() self.external_control_input.submit_data(np.array([control_input], dtype='float32')) - def update_varia_status(self): - status = self.get_varia_status_bits() - - # Extract moving filters from status. - nd_filter_moving = (status & (2 << 12)) > 0 - swp_filter_moving = (status & (2 << 13)) > 0 - lwp_filter_moving = (status & (2 << 14)) > 0 - - # Submit results to their respective datastreams. - self.nd_filter_moving.submit_data(np.array([nd_filter_moving], dtype='uint8')) - self.swp_filter_moving.submit_data(np.array([swp_filter_moving], dtype='uint8')) - self.lwp_filter_moving.submit_data(np.array([lwp_filter_moving], dtype='uint8')) - - # Update input monitor. - monitor_input = self.get_monitor_input() - self.monitor_input.submit_data(np.array([monitor_input], dtype='float32')) - - def monitor_func(self, stream, setter): - def func(): - while not self.should_shut_down: - try: - frame = stream.get_next_frame(1) - except Exception: - continue - - setter(frame.data[0]) - - return func - - def update_func(self, updater): - def func(): - while not self.should_shut_down: - updater() - - self.sleep(1) - - return func - # Functions for the SuperK EVO get_base_temperature = read_register(registerReadS16, Evo.REG_BASE_TEMPERATURE, ratio=0.1) get_supply_voltage = read_register(registerReadU16, Evo.REG_SUPPLY_VOLTAGE, ratio=0.001) @@ -258,19 +110,6 @@ def func(): get_watchdog_timer = read_register(registerReadU8, Evo.REG_WATCHDOG_TIMER) set_watchdog_timer = write_register(registerWriteU8, Evo.REG_WATCHDOG_TIMER) - # Functions for the SuperK VARIA - get_monitor_input = read_register(registerReadU16, Varia.REG_MONITOR_INPUT, ratio=0.1) - - get_nd_setpoint = read_register(registerReadU16, Varia.REG_ND_SETPOINT, ratio=0.1) - get_swp_setpoint = read_register(registerReadU16, Varia.REG_SWP_SETPOINT, ratio=0.1) - get_lwp_setpoint = read_register(registerReadU16, Varia.REG_LWP_SETPOINT, ratio=0.1) - - set_nd_setpoint = write_register(registerWriteU16, Varia.REG_ND_SETPOINT, ratio=0.1) - set_swp_setpoint = write_register(registerWriteU16, Varia.REG_SWP_SETPOINT, ratio=0.1) - set_lwp_setpoint = write_register(registerWriteU16, Varia.REG_LWP_SETPOINT, ratio=0.1) - - get_varia_status_bits = read_register(registerReadU16, Varia.REG_STATUS_BITS) - if __name__ == '__main__': service = NktSuperkEvo() diff --git a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py b/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py index 634b47fdb..8a51db6d2 100644 --- a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py +++ b/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py @@ -1,18 +1,9 @@ -from catkit2.testbed.service import Service +from catkit2.services.nkt_superk_base import NktSuperkBase, read_register, write_register import numpy as np -from concurrent.futures import ThreadPoolExecutor -import threading from enum import Enum -import os -import sys - try: - sdk_path = os.path.join(os.environ.get('NKTP_SDK_PATH'), 'Examples', 'DLL_Example_Python') - if sdk_path is not None: - sys.path.append(sdk_path) - from NKTP_DLL import * except ImportError: print('To use NKT SDK, you need to install the SDK and check the NKTP_SDK_PATH environment variable.') @@ -35,51 +26,7 @@ class Fianium(Enum): REG_ERROR_CODE = 0x67 -class Varia(Enum): - DEVICE_ID = 16 - - # SuperK VARIA registers - REG_MONITOR_INPUT = 0x13 - - REG_ND_SETPOINT = 0x32 - REG_SWP_SETPOINT = 0x33 - REG_LWP_SETPOINT = 0x34 - - REG_STATUS_BITS = 0x66 - - -def read_register(read_func, register, *, ratio=1, index=-1): - def getter(self): - device_id = register.__class__.DEVICE_ID - - future = self.pool.submit(read_func, self.port, device_id.value, register.value, index) - result, value = future.result() - - self.check_result(result) - - return value * ratio - - return getter - - -def write_register(write_func, register, *, ratio=1, index=-1): - def setter(self, value): - device_id = register.__class__.DEVICE_ID - - # Convert the value to the register value. This assumes integer types. - register_value = int(value / ratio) - - self.log.debug(f'Writing value {register_value} to {register}.') - - future = self.pool.submit(write_func, self.port, device_id.value, register.value, register_value, index) - result = future.result() - - self.check_result(result) - - return setter - - -class NktSuperkFianium(Service): +class NktSuperkFianium(NktSuperkBase): '''The service for both the NKT SuperK FIANIUM and NKT SuperK VARIA. Both devices are combined into a single service due to the need for @@ -88,120 +35,32 @@ class NktSuperkFianium(Service): ''' def __init__(self): super().__init__('nkt_superk_fianium') - - self.threads = {} - self.port = self.config['port'] self.pulse_picker_safety = self.config.get('pulse_picker_safety') - def open(self): - # Make datastreams. - self.emission = self.make_data_stream('emission', 'uint8', [1], 20) + def _create_device_specific_streams(self): + """Create FIANIUM-specific data streams.""" + # FIANIUM-specific streams self.power_setpoint = self.make_data_stream('power_setpoint', 'float32', [1], 20) self.pulse_picker_ratio = self.make_data_stream('pulse_picker_ratio', 'uint16', [1], 20) - self.monitor_input = self.make_data_stream('monitor_input', 'float32', [1], 20) - - self.nd_setpoint = self.make_data_stream('nd_setpoint', 'float32', [1], 20) - self.swp_setpoint = self.make_data_stream('swp_setpoint', 'float32', [1], 20) - self.lwp_setpoint = self.make_data_stream('lwp_setpoint', 'float32', [1], 20) - - self.nd_filter_moving = self.make_data_stream('nd_filter_moving', 'uint8', [1], 20) - self.swp_filter_moving = self.make_data_stream('swp_filter_moving', 'uint8', [1], 20) - self.lwp_filter_moving = self.make_data_stream('lwp_filter_moving', 'uint8', [1], 20) - - # Set current setpoints. These will be actually set on the device - # once the monitor threads have started. - self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) + # Set initial FIANIUM setpoints from config self.power_setpoint.submit_data(np.array([self.config['power_setpoint']], dtype='float32')) self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) # Setting a safe default value of 100. - self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) - self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) - self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) - - # Define thread functions. - funcs = { - 'nd_setpoint': self.monitor_func(self.nd_setpoint, self.set_nd_setpoint), - 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), - 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), - 'emission': self.monitor_func(self.emission, self.set_emission), + def _get_device_specific_funcs(self): + """Get FIANIUM-specific thread functions.""" + return { 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), - 'pulse_picker_ratio': self.monitor_pulse_picker_ratio(self.pulse_picker_ratio, self.set_pulse_picker_ratio), - 'varia_status': self.update_func(self.update_varia_status) + 'pulse_picker_ratio': self.monitor_pulse_picker_ratio(self.pulse_picker_ratio, self.set_pulse_picker_ratio) } - # Create a pool with a single worker to perform communication with the device. - self.pool = ThreadPoolExecutor(max_workers=1) - - # Open port. - # Make sure that the device is available (ie. not being used by someone else). - # This is not strictly necessary according to the SDK, but speeds up reading/writing. - future = self.pool.submit(openPorts, self.port, autoMode=0, liveMode=0) - self.check_result(future.result()) - - # Start all threads. - for key, func in funcs.items(): - thread = threading.Thread(target=func) - thread.start() - - self.threads[key] = thread - - def main(self): - while not self.should_shut_down: - self.sleep(1) - - def close(self): + def _device_specific_cleanup(self): + """Perform FIANIUM-specific cleanup.""" + # Set pulse picker ratio to safe value self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) - # Turn off the source - self.set_emission(0) - - # Join all threads. - for thread in self.threads.values(): - thread.join() - - # Close port. - future = self.pool.submit(closePorts, self.port) - self.check_result(future.result()) - - # Close pool. - self.pool.shutdown() - - def check_result(self, result): - if result != 0: - self.log.error('NKT error: ' + RegisterResultTypes(result)) - raise RuntimeError(RegisterResultTypes(result)) - - def update_varia_status(self): - status = self.get_varia_status_bits() - - # Extract moving filters from status. - nd_filter_moving = (status & (2 << 12)) > 0 - swp_filter_moving = (status & (2 << 13)) > 0 - lwp_filter_moving = (status & (2 << 14)) > 0 - - # Submit results to their respective datastreams. - self.nd_filter_moving.submit_data(np.array([nd_filter_moving], dtype='uint8')) - self.swp_filter_moving.submit_data(np.array([swp_filter_moving], dtype='uint8')) - self.lwp_filter_moving.submit_data(np.array([lwp_filter_moving], dtype='uint8')) - - # Update input monitor. - monitor_input = self.get_monitor_input() - self.monitor_input.submit_data(np.array([monitor_input], dtype='float32')) - - def monitor_func(self, stream, setter): - def func(): - while not self.should_shut_down: - try: - frame = stream.get_next_frame(1) - except Exception: - continue - - setter(frame.data[0]) - - return func - def monitor_pulse_picker_ratio(self, stream, setter): + """Monitor pulse picker ratio with safety checks.""" def func(): while not self.should_shut_down: try: @@ -217,15 +76,6 @@ def func(): return func - def update_func(self, updater): - def func(): - while not self.should_shut_down: - updater() - - self.sleep(1) - - return func - # Functions for the SuperK FIANIUM get_power_setpoint = read_register(registerReadU16, Fianium.REG_OUTPUT_LEVEL, ratio=0.1) set_power_setpoint = write_register(registerWriteU16, Fianium.REG_OUTPUT_LEVEL, ratio=0.1) @@ -247,19 +97,6 @@ def func(): get_pulse_picker_ratio = read_register(registerReadU16, Fianium.REG_PULSE_PICKER_RATIO, ratio=1) set_pulse_picker_ratio = write_register(registerWriteU16, Fianium.REG_PULSE_PICKER_RATIO, ratio=1) - # Functions for the SuperK VARIA - get_monitor_input = read_register(registerReadU16, Varia.REG_MONITOR_INPUT, ratio=0.1) - - get_nd_setpoint = read_register(registerReadU16, Varia.REG_ND_SETPOINT, ratio=0.1) - get_swp_setpoint = read_register(registerReadU16, Varia.REG_SWP_SETPOINT, ratio=0.1) - get_lwp_setpoint = read_register(registerReadU16, Varia.REG_LWP_SETPOINT, ratio=0.1) - - set_nd_setpoint = write_register(registerWriteU16, Varia.REG_ND_SETPOINT, ratio=0.1) - set_swp_setpoint = write_register(registerWriteU16, Varia.REG_SWP_SETPOINT, ratio=0.1) - set_lwp_setpoint = write_register(registerWriteU16, Varia.REG_LWP_SETPOINT, ratio=0.1) - - get_varia_status_bits = read_register(registerReadU16, Varia.REG_STATUS_BITS) - if __name__ == '__main__': service = NktSuperkFianium() diff --git a/tests/test_nkt_superk_base.py b/tests/test_nkt_superk_base.py new file mode 100644 index 000000000..2b461e5dd --- /dev/null +++ b/tests/test_nkt_superk_base.py @@ -0,0 +1,132 @@ +"""Unit tests for the NKT SuperK base class.""" + +import unittest +from unittest.mock import Mock, patch, MagicMock +import numpy as np + + +class TestNktSuperkBase(unittest.TestCase): + """Test the NktSuperkBase abstract class.""" + + def setUp(self): + """Set up test fixtures.""" + # Mock the Service base class and NKT SDK + self.mock_service = Mock() + self.mock_service.config = { + 'port': 'COM1', + 'emission': 1, + 'nd_setpoint': 1.0, + 'swp_setpoint': 500.0, + 'lwp_setpoint': 600.0 + } + + # Mock data streams + self.mock_stream = Mock() + self.mock_stream.get.return_value = [0] + self.mock_stream.get_next_frame.return_value = Mock(data=[0]) + + self.mock_service.make_data_stream.return_value = self.mock_stream + + @patch('catkit2.services.nkt_superk_base.Service') + @patch('catkit2.services.nkt_superk_base.ThreadPoolExecutor') + def test_cannot_instantiate_abstract_base(self, mock_executor, mock_service): + """Test that the abstract base class cannot be instantiated.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + from catkit2.services.nkt_superk_base import NktSuperkBase + + with self.assertRaises(TypeError): + # Should fail because abstract methods are not implemented + NktSuperkBase('test') + + @patch('catkit2.services.nkt_superk_base.Service') + def test_varia_enum(self, mock_service): + """Test that the Varia enum is properly defined.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + from catkit2.services.nkt_superk_base import Varia + + # Test that required attributes exist + self.assertEqual(Varia.DEVICE_ID, 16) + self.assertTrue(hasattr(Varia, 'REG_MONITOR_INPUT')) + self.assertTrue(hasattr(Varia, 'REG_ND_SETPOINT')) + self.assertTrue(hasattr(Varia, 'REG_SWP_SETPOINT')) + self.assertTrue(hasattr(Varia, 'REG_LWP_SETPOINT')) + self.assertTrue(hasattr(Varia, 'REG_STATUS_BITS')) + + @patch('catkit2.services.nkt_superk_base.Service') + def test_register_helpers(self, mock_service): + """Test that the register helper functions work correctly.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + from catkit2.services.nkt_superk_base import read_register, write_register, Varia + + # Mock the register read/write functions + mock_read_func = Mock() + mock_write_func = Mock() + + # Test read_register + getter = read_register(mock_read_func, Varia.REG_MONITOR_INPUT, ratio=0.1) + self.assertTrue(callable(getter)) + + # Test write_register + setter = write_register(mock_write_func, Varia.REG_MONITOR_INPUT, ratio=0.1) + self.assertTrue(callable(setter)) + + def test_concrete_implementation_structure(self): + """Test that concrete implementations have the required structure.""" + # Mock everything we need + with patch.dict('sys.modules', { + 'catkit2.testbed.service': Mock(), + 'NKTP_DLL': Mock() + }): + with patch('catkit2.services.nkt_superk_base.Service'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU16'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU8'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU8'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadS16'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.openPorts'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.closePorts'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.RegisterResultTypes'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU16'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU8'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU8'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadS16'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.openPorts'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.closePorts'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.RegisterResultTypes'): + + from catkit2.services.nkt_superk_base import NktSuperkBase + from catkit2.services.nkt_superk_evo.nkt_superk_evo import NktSuperkEvo + from catkit2.services.nkt_superk_fianium.nkt_superk_fianium import NktSuperkFianium + + # Test inheritance + self.assertTrue(issubclass(NktSuperkEvo, NktSuperkBase)) + self.assertTrue(issubclass(NktSuperkFianium, NktSuperkBase)) + + # Test that abstract methods are implemented + required_methods = [ + '_create_device_specific_streams', + '_get_device_specific_funcs', + '_device_specific_cleanup', + 'set_emission' + ] + + for method in required_methods: + self.assertTrue(hasattr(NktSuperkEvo, method)) + self.assertTrue(hasattr(NktSuperkFianium, method)) + + # Test that common methods are available + common_methods = [ + 'get_monitor_input', 'get_nd_setpoint', 'get_swp_setpoint', + 'get_lwp_setpoint', 'set_nd_setpoint', 'set_swp_setpoint', + 'set_lwp_setpoint', 'get_varia_status_bits', 'check_result', + 'update_varia_status', 'monitor_func', 'update_func' + ] + + for method in common_methods: + self.assertTrue(hasattr(NktSuperkEvo, method)) + self.assertTrue(hasattr(NktSuperkFianium, method)) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From 68ecba7d2a253922420b3374ea4b6ea3a951c637 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:32:30 +0000 Subject: [PATCH 03/12] Move NKT SuperK base class to base_services directory Co-authored-by: ivalaginja <29508965+ivalaginja@users.noreply.github.com> --- catkit2/{services => base_services}/nkt_superk_base.py | 0 catkit2/services/nkt_superk_evo/nkt_superk_evo.py | 2 +- catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py | 2 +- tests/test_nkt_superk_base.py | 8 ++++---- 4 files changed, 6 insertions(+), 6 deletions(-) rename catkit2/{services => base_services}/nkt_superk_base.py (100%) diff --git a/catkit2/services/nkt_superk_base.py b/catkit2/base_services/nkt_superk_base.py similarity index 100% rename from catkit2/services/nkt_superk_base.py rename to catkit2/base_services/nkt_superk_base.py diff --git a/catkit2/services/nkt_superk_evo/nkt_superk_evo.py b/catkit2/services/nkt_superk_evo/nkt_superk_evo.py index ff569a02b..f799f1a21 100644 --- a/catkit2/services/nkt_superk_evo/nkt_superk_evo.py +++ b/catkit2/services/nkt_superk_evo/nkt_superk_evo.py @@ -1,4 +1,4 @@ -from catkit2.services.nkt_superk_base import NktSuperkBase, read_register, write_register +from catkit2.base_services.nkt_superk_base import NktSuperkBase, read_register, write_register import numpy as np from enum import Enum diff --git a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py b/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py index 8a51db6d2..c0dc422d2 100644 --- a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py +++ b/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py @@ -1,4 +1,4 @@ -from catkit2.services.nkt_superk_base import NktSuperkBase, read_register, write_register +from catkit2.base_services.nkt_superk_base import NktSuperkBase, read_register, write_register import numpy as np from enum import Enum diff --git a/tests/test_nkt_superk_base.py b/tests/test_nkt_superk_base.py index 2b461e5dd..25ed80d40 100644 --- a/tests/test_nkt_superk_base.py +++ b/tests/test_nkt_superk_base.py @@ -27,12 +27,12 @@ def setUp(self): self.mock_service.make_data_stream.return_value = self.mock_stream - @patch('catkit2.services.nkt_superk_base.Service') - @patch('catkit2.services.nkt_superk_base.ThreadPoolExecutor') + @patch('catkit2.base_services.nkt_superk_base.Service') + @patch('catkit2.base_services.nkt_superk_base.ThreadPoolExecutor') def test_cannot_instantiate_abstract_base(self, mock_executor, mock_service): """Test that the abstract base class cannot be instantiated.""" with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): - from catkit2.services.nkt_superk_base import NktSuperkBase + from catkit2.base_services.nkt_superk_base import NktSuperkBase with self.assertRaises(TypeError): # Should fail because abstract methods are not implemented @@ -95,7 +95,7 @@ def test_concrete_implementation_structure(self): patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.closePorts'), \ patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.RegisterResultTypes'): - from catkit2.services.nkt_superk_base import NktSuperkBase + from catkit2.base_services.nkt_superk_base import NktSuperkBase from catkit2.services.nkt_superk_evo.nkt_superk_evo import NktSuperkEvo from catkit2.services.nkt_superk_fianium.nkt_superk_fianium import NktSuperkFianium From d63bcb78746c6656773449cd29f9284c1e61a698 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:38:28 +0000 Subject: [PATCH 04/12] Consolidate NKT SuperK documentation into single file Co-authored-by: ivalaginja <29508965+ivalaginja@users.noreply.github.com> --- docs/index.rst | 3 +- docs/services/nkt_superk.rst | 110 +++++++++++++++++++++++++++ docs/services/nkt_superk_evo.rst | 70 ----------------- docs/services/nkt_superk_fianium.rst | 65 ---------------- 4 files changed, 111 insertions(+), 137 deletions(-) create mode 100644 docs/services/nkt_superk.rst delete mode 100644 docs/services/nkt_superk_evo.rst delete mode 100644 docs/services/nkt_superk_fianium.rst diff --git a/docs/index.rst b/docs/index.rst index 43fd682b1..0ec39711e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,8 +36,7 @@ Catkit2 services/newport_picomotor services/newport_xps_q8 services/ni_daq - services/nkt_superk_evo - services/nkt_superk_fianium + services/nkt_superk services/oceanoptics_spectrometer services/omega_ithx_w3 services/phasics_cam diff --git a/docs/services/nkt_superk.rst b/docs/services/nkt_superk.rst new file mode 100644 index 000000000..6803c6e06 --- /dev/null +++ b/docs/services/nkt_superk.rst @@ -0,0 +1,110 @@ +NKT SuperK Compact Tunable Laser +================================= + +The NKT SuperK services contain software for controlling NKT SuperK supercontinuum white light lasers +and the `NKT SuperK VARIA Variable Bandpass Filter `_. +This is done because a single open port to the device is needed that cannot be shared between multiple services. + +Two service variants are available: + +- **NKT SuperK EVO**: Controls the `NKT SuperK EVO Supercontinuum White Light Laser `_ +- **NKT SuperK FIANIUM**: Controls the `NKT SuperK FIANIUM Supercontinuum White Light Laser `_ + +Both services inherit from a common base class (``NktSuperkBase``) that provides shared functionality for VARIA filter control +and common device operations. Associated drivers for the laser and VARIA (found in the linked manuals) need to be installed. + +Configuration +------------- + +**NKT SuperK EVO Configuration:** + +.. code-block:: YAML + + nkt_superk: + service_type: nkt_superk_evo + simulated_service_type: nkt_superk_evo_sim + interface: nkt_superk_evo + requires_safety: false + + port: COM4 + emission: 1 + power_setpoint: 100 + current_setpoint: 100 + nd_setpoint: 100 + lwp_setpoint: 633 + swp_setpoint: 643 + sleep_time_per_nm: 0.013 + base_sleep_time: 0.05 + +**NKT SuperK FIANIUM Configuration:** + +.. code-block:: YAML + + nkt_superk: + service_type: nkt_superk_fianium + simulated_service_type: nkt_superk_fianium_sim + interface: nkt_superk_fianium + requires_safety: false + + port: COM4 + pulse_picker_safety: 5 + + emission: 3 + power_setpoint: 100 + pulse_picker_ratio: 1 + nd_setpoint: 100 + lwp_setpoint: 638 + swp_setpoint: 643 + sleep_time_per_nm: 0.013 + base_sleep_time: 0.05 + +Properties +---------- + +None. + +Commands +-------- + +None. + +Datastreams +----------- + +**Common VARIA Filter Datastreams (available in both EVO and FIANIUM):** + +``monitor_input``: Monitors the input to the VARIA from the laser. + +``nd_setpoint``: Set point for the VARIA ND filter. + +``swp_setpoint``: Upper bandwidth limit for the VARIA (in nm). + +``lwp_setpoint``: Lower bandwidth limit for the VARIA (in nm). + +``nd_filter_moving``: Whether the ND filter is moving for the VARIA. + +``swp_filter_moving``: Whether the short wavelength (high-pass) filter is moving for the VARIA. + +``lwp_filter_moving``: Whether the long wavelength (low-pass) filter is moving for the VARIA. + +**EVO-Specific Datastreams:** + +``base_temperature``: Base temperature output by the EVO (Celsius). + +``supply_voltage``: DC supply voltage to the EVO. + +``external_control_input``: Level of external feedback control for the EVO (Volts, DC). + +``emission``: Output emission of the EVO (int) - 0 is OFF, 1 is ON. + +``power_setpoint``: Output emission power level of the EVO (in percent). + +``current_setpoint``: Output current level of the EVO (in percent). + +**FIANIUM-Specific Datastreams:** + +``emission``: Output emission of the FIANIUM (int) - 0 is OFF, 3 is ON. + +``power_setpoint``: Output emission power level of the FIANIUM (in percent). + +``pulse_picker_ratio``: Pulse picker ratio for the FIANIUM. \ No newline at end of file diff --git a/docs/services/nkt_superk_evo.rst b/docs/services/nkt_superk_evo.rst deleted file mode 100644 index 3725e3db1..000000000 --- a/docs/services/nkt_superk_evo.rst +++ /dev/null @@ -1,70 +0,0 @@ -NKT Super K EVO Compact Tunable Laser -===================================== -The NKT Super K service contains software for controlling both the `NKT SuperK EVO Supercontinuum White Light Laser `_ -and the `NKT SuperK VARIA Variable Bandpass Filter `_. -This is done because a single open port to the device is needed that cannot be shared between multiple services. - -Associated drivers for both the EVO and VARIA (found in the linked manuals) also need to be installed. - -.. note:: - There is a separate service for the NKT SuperK FIANIUM. - -Configuration -------------- - -.. code-block:: YAML - - nkt_superk: - service_type: nkt_superk_evo - simulated_service_type: nkt_superk_evo_sim - interface: nkt_superk_evo - requires_safety: false - - port: COM4 - emission: 1 - power_setpoint: 100 - current_setpoint: 100 - nd_setpoint: 100 - lwp_setpoint: 633 - swp_setpoint: 643 - sleep_time_per_nm: 0.013 - base_sleep_time: 0.05 - -Properties ----------- - -None. - -Commands --------- - -None. - -Datastreams ------------ -``base_temperature``: Base temperature output by the EVO (Celsius). - -``supply_voltage``: DC supply voltage to the EVO. - -``external_control_input``: Level of external feedback control for the EVO (Volts, DC). - -``emission``: Output emission of the EVO (int) - 0 is OFF, 1 is ON. - -``power_setpoint``: Output emission power level of the EVO (in percent). - -``current_setpoint``: Output current level of the EVO (in percent). - -``monitor_input``: Monitors the input to the VARIA from the EVO. - -``nd_setpoint``: Set point for the VARIA ND filter. - -``swp_setpoint``: Upper bandwidth limit for the VARIA (in nm). - -``lwp_setpoint``: Lower bandwidth limit for the VARIA (in nm). - -``nd_filter_moving``: Whether the ND filer is moving for the VARIA. - -``swp_filter_moving``: Whether the short wavelength (high-pass) filter is moving for the VARIA. - -``lwp_filter_moving``: Whether the long wavelength (low-pass) filter is moving for the VARIA. - diff --git a/docs/services/nkt_superk_fianium.rst b/docs/services/nkt_superk_fianium.rst deleted file mode 100644 index 488db7e82..000000000 --- a/docs/services/nkt_superk_fianium.rst +++ /dev/null @@ -1,65 +0,0 @@ -NKT Super K FIANIUM Compact Tunable Laser -========================================= -The NKT Super K service contains software for controlling both the `NKT SuperK FIANIUM Supercontinuum White Light Laser `_ -and the `NKT SuperK VARIA Variable Bandpass Filter `_. -This is done because a single open port to the device is needed that cannot be shared between multiple services. - -Associated drivers for both the FIANIUM and VARIA also need to be installed. - -.. note:: - There is a separate service for the NKT SuperK EVO. - -Configuration -------------- - -.. code-block:: YAML - - nkt_superk: - service_type: nkt_superk_fianium - simulated_service_type: nkt_superk_fianium_sim - interface: nkt_superk_fianium - requires_safety: false - - port: COM4 - pulse_picker_safety: 5 - - emission: 3 - power_setpoint: 100 - pulse_picker_ratio: 1 - nd_setpoint: 100 - lwp_setpoint: 638 - swp_setpoint: 643 - sleep_time_per_nm: 0.013 - base_sleep_time: 0.05 - -Properties ----------- - -None. - -Commands --------- - -None. - -Datastreams ------------ -``emission``: Output emission of the FIANIUM (int) - 0 is OFF, 3 is ON. - -``power_setpoint``: Output emission power level of the FIANIUM (in percent). - -``pulse_picker_ratio``: Pulse picker ratio for the FIANIUM. - -``monitor_input``: Monitors the input to the VARIA from the FIANIUM. - -``nd_setpoint``: Set point for the VARIA ND filter. - -``swp_setpoint``: Upper bandwidth limit for the VARIA (in nm). - -``lwp_setpoint``: Lower bandwidth limit for the VARIA (in nm). - -``nd_filter_moving``: Whether the ND filer is moving for the VARIA. - -``swp_filter_moving``: Whether the short wavelength (high-pass) filter is moving for the VARIA. - -``lwp_filter_moving``: Whether the long wavelength (low-pass) filter is moving for the VARIA. From de0f9c23dcdcd2e46eeded97fadfdd5b0ed4f752 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:47:16 +0000 Subject: [PATCH 05/12] Refactor NKT SuperK simulation services to inherit from common base class - Created NktSuperkSimBase abstract base class in catkit2/base_services/ - Contains all shared simulation functionality (VARIA controls, thread management, data streams) - Provides abstract methods for device-specific implementations - Refactored NktSuperkEvoSim to inherit from base class (81 lines removed) - Refactored NktSuperkFianiumSim to inherit from base class (81 lines removed) - Added comprehensive unit tests for simulation base class - Eliminates 162 lines of duplicated code between simulation services - Maintains all original functionality and interfaces Co-authored-by: ivalaginja <29508965+ivalaginja@users.noreply.github.com> --- catkit2/base_services/nkt_superk_sim_base.py | 169 ++++++++++++++++++ .../nkt_superk_evo_sim/nkt_superk_evo_sim.py | 113 ++---------- .../nkt_superk_fianium_sim.py | 115 ++---------- tests/test_nkt_superk_sim_base.py | 102 +++++++++++ 4 files changed, 304 insertions(+), 195 deletions(-) create mode 100644 catkit2/base_services/nkt_superk_sim_base.py create mode 100644 tests/test_nkt_superk_sim_base.py diff --git a/catkit2/base_services/nkt_superk_sim_base.py b/catkit2/base_services/nkt_superk_sim_base.py new file mode 100644 index 000000000..e4f980961 --- /dev/null +++ b/catkit2/base_services/nkt_superk_sim_base.py @@ -0,0 +1,169 @@ +"""Base class for NKT SuperK simulated white light sources. + +This module provides a common base class for NKT SuperK simulated devices, +eliminating code duplication between EVO and FIANIUM simulation services. +""" + +from catkit2.testbed.service import Service + +import numpy as np +import threading +from abc import ABC, abstractmethod + + +class NktSuperkSimBase(Service, ABC): + """Abstract base class for NKT SuperK simulated white light sources. + + This class contains common functionality shared between different + NKT SuperK simulated devices like EVO and FIANIUM. Both the SuperK source and + VARIA simulation are controlled through this service. + """ + + def __init__(self, service_type): + """Initialize the NKT SuperK simulation base service. + + Args: + service_type: The specific service type (e.g., 'nkt_superk_evo_sim') + """ + super().__init__(service_type) + + self.threads = {} + self.port = self.config['port'] + + def open(self): + """Open the service and initialize data streams.""" + # Common data streams for VARIA + self.monitor_input = self.make_data_stream('monitor_input', 'float32', [1], 20) + + self.nd_setpoint = self.make_data_stream('nd_setpoint', 'float32', [1], 20) + self.swp_setpoint = self.make_data_stream('swp_setpoint', 'float32', [1], 20) + self.lwp_setpoint = self.make_data_stream('lwp_setpoint', 'float32', [1], 20) + + self.nd_filter_moving = self.make_data_stream('nd_filter_moving', 'uint8', [1], 20) + self.swp_filter_moving = self.make_data_stream('swp_filter_moving', 'uint8', [1], 20) + self.lwp_filter_moving = self.make_data_stream('lwp_filter_moving', 'uint8', [1], 20) + + # Common data streams for SuperK source + self.emission = self.make_data_stream('emission', 'uint8', [1], 20) + + # Set initial VARIA setpoints from config + self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) + self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) + self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) + + # Set initial emission from config + self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) + + # Create device-specific data streams + self._create_device_specific_streams() + + # Define common thread functions + common_funcs = { + 'nd_setpoint': self.monitor_func(self.nd_setpoint, self.set_nd_setpoint), + 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), + 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), + 'emission': self.monitor_func(self.emission, self.set_emission), + 'varia_status': self.update_func(self.update_varia_status) + } + + # Get device-specific thread functions and merge + device_funcs = self._get_device_specific_funcs() + funcs = {**common_funcs, **device_funcs} + + # Start all threads + for key, func in funcs.items(): + thread = threading.Thread(target=func) + thread.start() + self.threads[key] = thread + + def main(self): + """Main service loop.""" + while not self.should_shut_down: + self.sleep(1) + + def close(self): + """Close the service and cleanup resources.""" + # Turn off the source + self.set_emission(0) + + # Device-specific cleanup + self._device_specific_cleanup() + + # Join all threads + for thread in self.threads.values(): + thread.join() + + def update_varia_status(self): + """Update VARIA status information.""" + # Submit simulated results to their respective datastreams + self.nd_filter_moving.submit_data(np.array([0], dtype='uint8')) + self.swp_filter_moving.submit_data(np.array([0], dtype='uint8')) + self.lwp_filter_moving.submit_data(np.array([0], dtype='uint8')) + + self.monitor_input.submit_data(np.array([1], dtype='float32')) + + def monitor_func(self, stream, setter): + """Create a monitoring function for a data stream.""" + def func(): + while not self.should_shut_down: + try: + frame = stream.get_next_frame(1) + except Exception: + continue + + setter(frame.data[0]) + + return func + + def update_func(self, updater): + """Create an update function that runs periodically.""" + def func(): + while not self.should_shut_down: + updater() + self.sleep(1) + + return func + + # Common VARIA simulator operations + def set_nd_setpoint(self, nd_setpoint): + """Set ND filter setpoint in simulator.""" + self.testbed.simulator.move_filter( + filter_wheel_name=self.id + '_nd', + new_filter_position=nd_setpoint + ) + + def set_swp_setpoint(self, swp_setpoint): + """Set SWP filter setpoint in simulator.""" + self.testbed.simulator.move_filter( + filter_wheel_name=self.id + '_swp', + new_filter_position=swp_setpoint + ) + + def set_lwp_setpoint(self, lwp_setpoint): + """Set LWP filter setpoint in simulator.""" + self.testbed.simulator.move_filter( + filter_wheel_name=self.id + '_lwp', + new_filter_position=lwp_setpoint + ) + + # Abstract methods that must be implemented by subclasses + @abstractmethod + def _create_device_specific_streams(self): + """Create device-specific data streams.""" + pass + + @abstractmethod + def _get_device_specific_funcs(self): + """Get device-specific thread functions.""" + pass + + @abstractmethod + def _device_specific_cleanup(self): + """Perform device-specific cleanup.""" + pass + + # Abstract method that must be implemented by subclasses + @abstractmethod + def set_emission(self, emission): + """Set emission state for the device in simulator.""" + pass \ No newline at end of file diff --git a/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py b/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py index a8e284668..322270c95 100644 --- a/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py +++ b/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py @@ -1,143 +1,62 @@ -from catkit2.testbed.service import Service +from catkit2.base_services.nkt_superk_sim_base import NktSuperkSimBase import numpy as np -import threading -class NktSuperkEvoSim(Service): +class NktSuperkEvoSim(NktSuperkSimBase): def __init__(self): super().__init__('nkt_superk_evo_sim') - self.threads = {} - self.port = self.config['port'] - - def open(self): - # Make datastreams. + def _create_device_specific_streams(self): + """Create EVO-specific data streams.""" + # EVO-specific streams self.base_temperature = self.make_data_stream('base_temperature', 'float32', [1], 20) self.supply_voltage = self.make_data_stream('supply_voltage', 'float32', [1], 20) self.external_control_input = self.make_data_stream('external_control_input', 'float32', [1], 20) - self.emission = self.make_data_stream('emission', 'uint8', [1], 20) self.power_setpoint = self.make_data_stream('power_setpoint', 'float32', [1], 20) self.current_setpoint = self.make_data_stream('current_setpoint', 'float32', [1], 20) - self.monitor_input = self.make_data_stream('monitor_input', 'float32', [1], 20) - - self.nd_setpoint = self.make_data_stream('nd_setpoint', 'float32', [1], 20) - self.swp_setpoint = self.make_data_stream('swp_setpoint', 'float32', [1], 20) - self.lwp_setpoint = self.make_data_stream('lwp_setpoint', 'float32', [1], 20) - - self.nd_filter_moving = self.make_data_stream('nd_filter_moving', 'uint8', [1], 20) - self.swp_filter_moving = self.make_data_stream('swp_filter_moving', 'uint8', [1], 20) - self.lwp_filter_moving = self.make_data_stream('lwp_filter_moving', 'uint8', [1], 20) - - # Set current setpoints. These will be actually set on the device - # once the monitor threads have started. - self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) + # Set initial EVO setpoints from config self.power_setpoint.submit_data(np.array([self.config['power_setpoint']], dtype='float32')) self.current_setpoint.submit_data(np.array([self.config['current_setpoint']], dtype='float32')) - self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) - self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) - self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) - - # Define thread functions. - funcs = { - 'nd_setpoint': self.monitor_func(self.nd_setpoint, self.set_nd_setpoint), - 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), - 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), - 'emission': self.monitor_func(self.emission, self.set_emission), + def _get_device_specific_funcs(self): + """Get EVO-specific thread functions.""" + return { 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), 'current_setpoint': self.monitor_func(self.current_setpoint, self.set_current_setpoint), - 'varia_status': self.update_func(self.update_varia_status), 'evo_status': self.update_func(self.update_evo_status) } - # Start all threads. - for key, func in funcs.items(): - thread = threading.Thread(target=func) - thread.start() - - self.threads[key] = thread - - def main(self): - while not self.should_shut_down: - self.sleep(1) - - def close(self): - # Stop emission - self.set_emission(0) - # Join all threads. - for thread in self.threads.values(): - thread.join() + def _device_specific_cleanup(self): + """Perform EVO-specific cleanup.""" + pass def update_evo_status(self): + """Update EVO-specific status information.""" self.base_temperature.submit_data(np.array([28.5], dtype='float32')) self.supply_voltage.submit_data(np.array([24.1], dtype='float32')) self.external_control_input.submit_data(np.array([4.2], dtype='float32')) - def update_varia_status(self): - # Submit bogus results to their respective datastreams. - self.nd_filter_moving.submit_data(np.array([0], dtype='uint8')) - self.swp_filter_moving.submit_data(np.array([0], dtype='uint8')) - self.lwp_filter_moving.submit_data(np.array([0], dtype='uint8')) - - self.monitor_input.submit_data(np.array([1], dtype='float32')) - - def monitor_func(self, stream, setter): - def func(): - while not self.should_shut_down: - try: - frame = stream.get_next_frame(1) - except Exception: - continue - - setter(frame.data[0]) - - return func - - def update_func(self, updater): - def func(): - while not self.should_shut_down: - updater() - - self.sleep(1) - - return func - def set_emission(self, emission): + """Set emission state for EVO device in simulator.""" self.testbed.simulator.set_source_power( source_name=self.id, power=emission * self.power_setpoint.get()[0] * 1e-2 ) def set_power_setpoint(self, power_setpoint): + """Set power setpoint for EVO device in simulator.""" self.testbed.simulator.set_source_power( source_name=self.id, power=self.emission.get()[0] * power_setpoint * 1e-2 ) def set_current_setpoint(self, current_setpoint): + """Set current setpoint for EVO device (no-op in simulation).""" pass - def set_nd_setpoint(self, nd_setpoint): - self.testbed.simulator.move_filter( - filter_wheel_name=self.id + '_nd', - new_filter_position=nd_setpoint - ) - - def set_swp_setpoint(self, swp_setpoint): - self.testbed.simulator.move_filter( - filter_wheel_name=self.id + '_swp', - new_filter_position=swp_setpoint - ) - - def set_lwp_setpoint(self, lwp_setpoint): - self.testbed.simulator.move_filter( - filter_wheel_name=self.id + '_lwp', - new_filter_position=lwp_setpoint - ) - if __name__ == '__main__': service = NktSuperkEvoSim() diff --git a/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py b/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py index 06478c2a5..4895b7d3c 100644 --- a/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py +++ b/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py @@ -1,93 +1,36 @@ -from catkit2.testbed.service import Service +from catkit2.base_services.nkt_superk_sim_base import NktSuperkSimBase import numpy as np -import threading -class NktSuperkFianiumSim(Service): +class NktSuperkFianiumSim(NktSuperkSimBase): def __init__(self): super().__init__('nkt_superk_fianium_sim') + self.pulse_picker_safety = self.config.get('pulse_picker_safety') - self.threads = {} - self.port = self.config['port'] - - def open(self): - # Make datastreams. - self.emission = self.make_data_stream('emission', 'uint8', [1], 20) + def _create_device_specific_streams(self): + """Create FIANIUM-specific data streams.""" + # FIANIUM-specific streams self.power_setpoint = self.make_data_stream('power_setpoint', 'float32', [1], 20) self.pulse_picker_ratio = self.make_data_stream('pulse_picker_ratio', 'uint16', [1], 20) - self.monitor_input = self.make_data_stream('monitor_input', 'float32', [1], 20) - - self.nd_setpoint = self.make_data_stream('nd_setpoint', 'float32', [1], 20) - self.swp_setpoint = self.make_data_stream('swp_setpoint', 'float32', [1], 20) - self.lwp_setpoint = self.make_data_stream('lwp_setpoint', 'float32', [1], 20) - - self.nd_filter_moving = self.make_data_stream('nd_filter_moving', 'uint8', [1], 20) - self.swp_filter_moving = self.make_data_stream('swp_filter_moving', 'uint8', [1], 20) - self.lwp_filter_moving = self.make_data_stream('lwp_filter_moving', 'uint8', [1], 20) - - # Set current setpoints. These will be actually set on the device - # once the monitor threads have started. - self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) + # Set initial FIANIUM setpoints from config self.power_setpoint.submit_data(np.array([self.config['power_setpoint']], dtype='float32')) self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) # Setting a safe default value of 100. - self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) - self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) - self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) - - # Define thread functions. - funcs = { - 'nd_setpoint': self.monitor_func(self.nd_setpoint, self.set_nd_setpoint), - 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), - 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), - 'emission': self.monitor_func(self.emission, self.set_emission), + def _get_device_specific_funcs(self): + """Get FIANIUM-specific thread functions.""" + return { 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), - 'pulse_picker_ratio': self.monitor_pulse_picker_ratio(self.pulse_picker_ratio, self.set_pulse_picker_ratio), - 'varia_status': self.update_func(self.update_varia_status) + 'pulse_picker_ratio': self.monitor_pulse_picker_ratio(self.pulse_picker_ratio, self.set_pulse_picker_ratio) } - # Start all threads. - for key, func in funcs.items(): - thread = threading.Thread(target=func) - thread.start() - - self.threads[key] = thread - - def main(self): - while not self.should_shut_down: - self.sleep(1) - - def close(self): + def _device_specific_cleanup(self): + """Perform FIANIUM-specific cleanup.""" self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) - # Stop emission - self.set_emission(0) - # Join all threads. - for thread in self.threads.values(): - thread.join() - - def update_varia_status(self): - # Submit bogus results to their respective datastreams. - self.nd_filter_moving.submit_data(np.array([0], dtype='uint8')) - self.swp_filter_moving.submit_data(np.array([0], dtype='uint8')) - self.lwp_filter_moving.submit_data(np.array([0], dtype='uint8')) - - self.monitor_input.submit_data(np.array([1], dtype='float32')) - - def monitor_func(self, stream, setter): - def func(): - while not self.should_shut_down: - try: - frame = stream.get_next_frame(1) - except Exception: - continue - - setter(frame.data[0]) - - return func def monitor_pulse_picker_ratio(self, stream, setter): + """Create a monitoring function for pulse picker ratio with safety checks.""" def func(): while not self.should_shut_down: try: @@ -103,16 +46,8 @@ def func(): return func - def update_func(self, updater): - def func(): - while not self.should_shut_down: - updater() - - self.sleep(1) - - return func - def set_emission(self, emission): + """Set emission state for FIANIUM device in simulator.""" onoff = 0 if emission == 0 else 1 self.testbed.simulator.set_source_power( source_name=self.id, @@ -120,6 +55,7 @@ def set_emission(self, emission): ) def set_power_setpoint(self, power_setpoint): + """Set power setpoint for FIANIUM device in simulator.""" onoff = 0 if self.emission.get()[0] == 0 else 1 self.testbed.simulator.set_source_power( source_name=self.id, @@ -127,30 +63,13 @@ def set_power_setpoint(self, power_setpoint): ) def set_pulse_picker_ratio(self, pulse_picker_ratio): + """Set pulse picker ratio for FIANIUM device in simulator.""" onoff = 0 if self.emission.get()[0] == 0 else 1 self.testbed.simulator.set_source_power( source_name=self.id, power=onoff * self.power_setpoint.get()[0] * 1e-2 * pulse_picker_ratio # TODO: add model conversion for pulse picker ratio ) - def set_nd_setpoint(self, nd_setpoint): - self.testbed.simulator.move_filter( - filter_wheel_name=self.id + '_nd', - new_filter_position=nd_setpoint - ) - - def set_swp_setpoint(self, swp_setpoint): - self.testbed.simulator.move_filter( - filter_wheel_name=self.id + '_swp', - new_filter_position=swp_setpoint - ) - - def set_lwp_setpoint(self, lwp_setpoint): - self.testbed.simulator.move_filter( - filter_wheel_name=self.id + '_lwp', - new_filter_position=lwp_setpoint - ) - if __name__ == '__main__': service = NktSuperkFianiumSim() diff --git a/tests/test_nkt_superk_sim_base.py b/tests/test_nkt_superk_sim_base.py new file mode 100644 index 000000000..387ffea42 --- /dev/null +++ b/tests/test_nkt_superk_sim_base.py @@ -0,0 +1,102 @@ +"""Unit tests for the NKT SuperK simulation base class.""" + +import unittest +from unittest.mock import Mock, patch, MagicMock +import numpy as np + + +class TestNktSuperkSimBase(unittest.TestCase): + """Test the NktSuperkSimBase abstract class.""" + + def setUp(self): + """Set up test fixtures.""" + # Mock the Service base class + self.mock_service = Mock() + self.mock_service.config = { + 'port': 'COM1', + 'emission': 1, + 'nd_setpoint': 1.0, + 'swp_setpoint': 500.0, + 'lwp_setpoint': 600.0 + } + + # Mock data streams + self.mock_stream = Mock() + self.mock_stream.get.return_value = [0] + self.mock_stream.get_next_frame.return_value = Mock(data=[0]) + + self.mock_service.make_data_stream.return_value = self.mock_stream + + @patch('catkit2.base_services.nkt_superk_sim_base.Service') + def test_cannot_instantiate_abstract_base(self, mock_service): + """Test that the abstract base class cannot be instantiated.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + from catkit2.base_services.nkt_superk_sim_base import NktSuperkSimBase + + with self.assertRaises(TypeError): + # Should fail because abstract methods are not implemented + NktSuperkSimBase('test') + + def test_concrete_simulation_implementation_structure(self): + """Test that concrete simulation implementations have the required structure.""" + # Mock everything we need + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + with patch('catkit2.base_services.nkt_superk_sim_base.Service'): + + from catkit2.base_services.nkt_superk_sim_base import NktSuperkSimBase + from catkit2.services.nkt_superk_evo_sim.nkt_superk_evo_sim import NktSuperkEvoSim + from catkit2.services.nkt_superk_fianium_sim.nkt_superk_fianium_sim import NktSuperkFianiumSim + + # Test inheritance + self.assertTrue(issubclass(NktSuperkEvoSim, NktSuperkSimBase)) + self.assertTrue(issubclass(NktSuperkFianiumSim, NktSuperkSimBase)) + + # Test that abstract methods are implemented + required_methods = [ + '_create_device_specific_streams', + '_get_device_specific_funcs', + '_device_specific_cleanup', + 'set_emission' + ] + + for method in required_methods: + self.assertTrue(hasattr(NktSuperkEvoSim, method)) + self.assertTrue(hasattr(NktSuperkFianiumSim, method)) + + # Test that common methods are available + common_methods = [ + 'set_nd_setpoint', 'set_swp_setpoint', 'set_lwp_setpoint', + 'update_varia_status', 'monitor_func', 'update_func' + ] + + for method in common_methods: + self.assertTrue(hasattr(NktSuperkEvoSim, method)) + self.assertTrue(hasattr(NktSuperkFianiumSim, method)) + + def test_evo_sim_specific_methods(self): + """Test that EVO simulation has device-specific methods.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + with patch('catkit2.base_services.nkt_superk_sim_base.Service'): + from catkit2.services.nkt_superk_evo_sim.nkt_superk_evo_sim import NktSuperkEvoSim + + # Test EVO-specific methods + evo_methods = ['update_evo_status', 'set_power_setpoint', 'set_current_setpoint'] + + for method in evo_methods: + self.assertTrue(hasattr(NktSuperkEvoSim, method)) + + def test_fianium_sim_specific_methods(self): + """Test that FIANIUM simulation has device-specific methods.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + with patch('catkit2.base_services.nkt_superk_sim_base.Service'): + from catkit2.services.nkt_superk_fianium_sim.nkt_superk_fianium_sim import NktSuperkFianiumSim + + # Test FIANIUM-specific methods + fianium_methods = ['set_power_setpoint', 'set_pulse_picker_ratio', 'monitor_pulse_picker_ratio'] + + for method in fianium_methods: + self.assertTrue(hasattr(NktSuperkFianiumSim, method)) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From eb798b799922647ccd1875e14a40f07dca3b9586 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:02:42 +0000 Subject: [PATCH 06/12] Consolidate NKT SuperK base classes into single unified base class Co-authored-by: ivalaginja <29508965+ivalaginja@users.noreply.github.com> --- catkit2/base_services/nkt_superk.py | 390 ++++++++++++++++++ catkit2/base_services/nkt_superk_base.py | 252 ----------- catkit2/base_services/nkt_superk_sim_base.py | 169 -------- .../services/nkt_superk_evo/nkt_superk_evo.py | 4 +- .../nkt_superk_evo_sim/nkt_superk_evo_sim.py | 4 +- .../nkt_superk_fianium/nkt_superk_fianium.py | 4 +- .../nkt_superk_fianium_sim.py | 4 +- tests/test_nkt_superk.py | 213 ++++++++++ tests/test_nkt_superk_base.py | 132 ------ tests/test_nkt_superk_sim_base.py | 102 ----- 10 files changed, 611 insertions(+), 663 deletions(-) create mode 100644 catkit2/base_services/nkt_superk.py delete mode 100644 catkit2/base_services/nkt_superk_base.py delete mode 100644 catkit2/base_services/nkt_superk_sim_base.py create mode 100644 tests/test_nkt_superk.py delete mode 100644 tests/test_nkt_superk_base.py delete mode 100644 tests/test_nkt_superk_sim_base.py diff --git a/catkit2/base_services/nkt_superk.py b/catkit2/base_services/nkt_superk.py new file mode 100644 index 000000000..5db27782c --- /dev/null +++ b/catkit2/base_services/nkt_superk.py @@ -0,0 +1,390 @@ +"""Base class for NKT SuperK white light sources. + +This module provides a common base class for both hardware and simulation +NKT SuperK devices, eliminating code duplication between all services. +""" + +from catkit2.testbed.service import Service + +import numpy as np +from concurrent.futures import ThreadPoolExecutor +import threading +from enum import Enum +import os +import sys +from abc import ABC, abstractmethod + + +# Try to import NKT SDK - this will only succeed for hardware services +try: + sdk_path = os.path.join(os.environ.get('NKTP_SDK_PATH'), 'Examples', 'DLL_Example_Python') + if sdk_path is not None: + sys.path.append(sdk_path) + + from NKTP_DLL import * + NKT_SDK_AVAILABLE = True +except ImportError: + NKT_SDK_AVAILABLE = False + + +class Varia(Enum): + """Registers for the NKT SuperK VARIA device.""" + DEVICE_ID = 16 + + # SuperK VARIA registers + REG_MONITOR_INPUT = 0x13 + + REG_ND_SETPOINT = 0x32 + REG_SWP_SETPOINT = 0x33 + REG_LWP_SETPOINT = 0x34 + + REG_STATUS_BITS = 0x66 + + +def read_register(read_func, register, *, ratio=1, index=-1): + """Helper function to create register read methods.""" + def getter(self): + device_id = register.__class__.DEVICE_ID + + future = self.pool.submit(read_func, self.port, device_id.value, register.value, index) + result, value = future.result() + + self.check_result(result) + + return value * ratio + + return getter + + +def write_register(write_func, register, *, ratio=1, index=-1): + """Helper function to create register write methods.""" + def setter(self, value): + device_id = register.__class__.DEVICE_ID + + # Convert the value to the register value. This assumes integer types. + register_value = int(value / ratio) + + self.log.debug(f'Writing value {register_value} to {register}.') + + future = self.pool.submit(write_func, self.port, device_id.value, register.value, register_value, index) + result = future.result() + + self.check_result(result) + + return setter + + +class NktSuperk(Service, ABC): + """Abstract base class for NKT SuperK white light sources. + + This class contains common functionality shared between different + NKT SuperK devices (both hardware and simulation). Both the SuperK source and + VARIA are controlled through this service due to the need for a + single open port to the device (for hardware) or consistent simulation behavior. + """ + + def __init__(self, service_type): + """Initialize the NKT SuperK service. + + Args: + service_type: The specific service type (e.g., 'nkt_superk_evo') + """ + super().__init__(service_type) + + self.threads = {} + self.port = self.config['port'] + self.is_simulation = 'sim' in service_type + + def open(self): + """Open the service and initialize data streams.""" + # Common data streams for VARIA + self.monitor_input = self.make_data_stream('monitor_input', 'float32', [1], 20) + + self.nd_setpoint = self.make_data_stream('nd_setpoint', 'float32', [1], 20) + self.swp_setpoint = self.make_data_stream('swp_setpoint', 'float32', [1], 20) + self.lwp_setpoint = self.make_data_stream('lwp_setpoint', 'float32', [1], 20) + + self.nd_filter_moving = self.make_data_stream('nd_filter_moving', 'uint8', [1], 20) + self.swp_filter_moving = self.make_data_stream('swp_filter_moving', 'uint8', [1], 20) + self.lwp_filter_moving = self.make_data_stream('lwp_filter_moving', 'uint8', [1], 20) + + # Common data streams for SuperK source + self.emission = self.make_data_stream('emission', 'uint8', [1], 20) + + # Set initial VARIA setpoints from config + self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) + self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) + self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) + + # Set initial emission from config + self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) + + # Create device-specific data streams + self._create_device_specific_streams() + + # Define common thread functions + common_funcs = { + 'nd_setpoint': self.monitor_func(self.nd_setpoint, self.set_nd_setpoint), + 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), + 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), + 'emission': self.monitor_func(self.emission, self.set_emission), + 'varia_status': self.update_func(self.update_varia_status) + } + + # Get device-specific thread functions and merge + device_funcs = self._get_device_specific_funcs() + funcs = {**common_funcs, **device_funcs} + + # Initialize hardware communication if not simulation + if not self.is_simulation: + if not NKT_SDK_AVAILABLE: + raise RuntimeError('NKT SDK is required for hardware services but not available. ' + 'Install the SDK and check the NKTP_SDK_PATH environment variable.') + + # Create a pool with a single worker to perform communication with the device + self.pool = ThreadPoolExecutor(max_workers=1) + + # Open port + future = self.pool.submit(openPorts, self.port, autoMode=0, liveMode=0) + self.check_result(future.result()) + + # Start all threads + for key, func in funcs.items(): + thread = threading.Thread(target=func) + thread.start() + self.threads[key] = thread + + def main(self): + """Main service loop.""" + while not self.should_shut_down: + self.sleep(1) + + def close(self): + """Close the service and cleanup resources.""" + # Turn off the source + self.set_emission(0) + + # Device-specific cleanup + self._device_specific_cleanup() + + # Join all threads + for thread in self.threads.values(): + thread.join() + + # Close hardware resources if not simulation + if not self.is_simulation: + # Close port + future = self.pool.submit(closePorts, self.port) + self.check_result(future.result()) + + # Close pool + self.pool.shutdown() + + def check_result(self, result): + """Check if an NKT SDK operation was successful. + + Only used for hardware services. + """ + if not self.is_simulation and result != 0: + self.log.error('NKT error: ' + RegisterResultTypes(result)) + raise RuntimeError(RegisterResultTypes(result)) + + def update_varia_status(self): + """Update VARIA status information.""" + if self.is_simulation: + # Submit simulated results to their respective datastreams + self.nd_filter_moving.submit_data(np.array([0], dtype='uint8')) + self.swp_filter_moving.submit_data(np.array([0], dtype='uint8')) + self.lwp_filter_moving.submit_data(np.array([0], dtype='uint8')) + + self.monitor_input.submit_data(np.array([1], dtype='float32')) + else: + # Hardware status update + status = self.get_varia_status_bits() + + # Extract moving filters from status + nd_filter_moving = (status & (2 << 12)) > 0 + swp_filter_moving = (status & (2 << 13)) > 0 + lwp_filter_moving = (status & (2 << 14)) > 0 + + # Submit results to their respective datastreams + self.nd_filter_moving.submit_data(np.array([nd_filter_moving], dtype='uint8')) + self.swp_filter_moving.submit_data(np.array([swp_filter_moving], dtype='uint8')) + self.lwp_filter_moving.submit_data(np.array([lwp_filter_moving], dtype='uint8')) + + # Update input monitor + monitor_input = self.get_monitor_input() + self.monitor_input.submit_data(np.array([monitor_input], dtype='float32')) + + def monitor_func(self, stream, setter): + """Create a monitoring function for a data stream.""" + def func(): + while not self.should_shut_down: + try: + frame = stream.get_next_frame(1) + except Exception: + continue + + setter(frame.data[0]) + + return func + + def update_func(self, updater): + """Create an update function that runs periodically.""" + def func(): + while not self.should_shut_down: + updater() + self.sleep(1) + + return func + + # VARIA setpoint methods - different implementation for hardware vs simulation + def set_nd_setpoint(self, nd_setpoint): + """Set ND filter setpoint.""" + if self.is_simulation: + self.testbed.simulator.move_filter( + filter_wheel_name=self.id + '_nd', + new_filter_position=nd_setpoint + ) + else: + self._set_nd_setpoint_hardware(nd_setpoint) + + def set_swp_setpoint(self, swp_setpoint): + """Set SWP filter setpoint.""" + if self.is_simulation: + self.testbed.simulator.move_filter( + filter_wheel_name=self.id + '_swp', + new_filter_position=swp_setpoint + ) + else: + self._set_swp_setpoint_hardware(swp_setpoint) + + def set_lwp_setpoint(self, lwp_setpoint): + """Set LWP filter setpoint.""" + if self.is_simulation: + self.testbed.simulator.move_filter( + filter_wheel_name=self.id + '_lwp', + new_filter_position=lwp_setpoint + ) + else: + self._set_lwp_setpoint_hardware(lwp_setpoint) + + # Hardware register operations (only used by hardware services) + def _set_nd_setpoint_hardware(self, nd_setpoint): + """Set ND filter setpoint using hardware register.""" + device_id = Varia.DEVICE_ID + register_value = int(nd_setpoint / 0.1) + + self.log.debug(f'Writing value {register_value} to {Varia.REG_ND_SETPOINT}.') + + future = self.pool.submit(registerWriteU16, self.port, device_id.value, + Varia.REG_ND_SETPOINT.value, register_value, -1) + result = future.result() + self.check_result(result) + + def _set_swp_setpoint_hardware(self, swp_setpoint): + """Set SWP filter setpoint using hardware register.""" + device_id = Varia.DEVICE_ID + register_value = int(swp_setpoint / 0.1) + + self.log.debug(f'Writing value {register_value} to {Varia.REG_SWP_SETPOINT}.') + + future = self.pool.submit(registerWriteU16, self.port, device_id.value, + Varia.REG_SWP_SETPOINT.value, register_value, -1) + result = future.result() + self.check_result(result) + + def _set_lwp_setpoint_hardware(self, lwp_setpoint): + """Set LWP filter setpoint using hardware register.""" + device_id = Varia.DEVICE_ID + register_value = int(lwp_setpoint / 0.1) + + self.log.debug(f'Writing value {register_value} to {Varia.REG_LWP_SETPOINT}.') + + future = self.pool.submit(registerWriteU16, self.port, device_id.value, + Varia.REG_LWP_SETPOINT.value, register_value, -1) + result = future.result() + self.check_result(result) + + # Hardware register read operations (only defined for hardware services) + def get_monitor_input(self): + """Get monitor input value from hardware.""" + if self.is_simulation: + return 1.0 # Return simulated value + + device_id = Varia.DEVICE_ID + future = self.pool.submit(registerReadU16, self.port, device_id.value, + Varia.REG_MONITOR_INPUT.value, -1) + result, value = future.result() + self.check_result(result) + return value * 0.1 + + def get_nd_setpoint(self): + """Get ND filter setpoint from hardware.""" + if self.is_simulation: + return self.nd_setpoint.get_last_frame().data[0] + + device_id = Varia.DEVICE_ID + future = self.pool.submit(registerReadU16, self.port, device_id.value, + Varia.REG_ND_SETPOINT.value, -1) + result, value = future.result() + self.check_result(result) + return value * 0.1 + + def get_swp_setpoint(self): + """Get SWP filter setpoint from hardware.""" + if self.is_simulation: + return self.swp_setpoint.get_last_frame().data[0] + + device_id = Varia.DEVICE_ID + future = self.pool.submit(registerReadU16, self.port, device_id.value, + Varia.REG_SWP_SETPOINT.value, -1) + result, value = future.result() + self.check_result(result) + return value * 0.1 + + def get_lwp_setpoint(self): + """Get LWP filter setpoint from hardware.""" + if self.is_simulation: + return self.lwp_setpoint.get_last_frame().data[0] + + device_id = Varia.DEVICE_ID + future = self.pool.submit(registerReadU16, self.port, device_id.value, + Varia.REG_LWP_SETPOINT.value, -1) + result, value = future.result() + self.check_result(result) + return value * 0.1 + + def get_varia_status_bits(self): + """Get VARIA status bits from hardware.""" + if self.is_simulation: + return 0 # Return simulated status + + device_id = Varia.DEVICE_ID + future = self.pool.submit(registerReadU16, self.port, device_id.value, + Varia.REG_STATUS_BITS.value, -1) + result, value = future.result() + self.check_result(result) + return value + + # Abstract methods that must be implemented by subclasses + @abstractmethod + def _create_device_specific_streams(self): + """Create device-specific data streams.""" + pass + + @abstractmethod + def _get_device_specific_funcs(self): + """Get device-specific thread functions.""" + pass + + @abstractmethod + def _device_specific_cleanup(self): + """Perform device-specific cleanup.""" + pass + + # Abstract register operations that must be implemented by subclasses + @abstractmethod + def set_emission(self, emission): + """Set emission state for the device.""" + pass diff --git a/catkit2/base_services/nkt_superk_base.py b/catkit2/base_services/nkt_superk_base.py deleted file mode 100644 index 0e97cb366..000000000 --- a/catkit2/base_services/nkt_superk_base.py +++ /dev/null @@ -1,252 +0,0 @@ -"""Base class for NKT SuperK white light sources. - -This module provides a common base class for NKT SuperK devices, -eliminating code duplication between EVO and FIANIUM services. -""" - -from catkit2.testbed.service import Service - -import numpy as np -from concurrent.futures import ThreadPoolExecutor -import threading -from enum import Enum -import os -import sys -from abc import ABC, abstractmethod - - -try: - sdk_path = os.path.join(os.environ.get('NKTP_SDK_PATH'), 'Examples', 'DLL_Example_Python') - if sdk_path is not None: - sys.path.append(sdk_path) - - from NKTP_DLL import * -except ImportError: - print('To use NKT SDK, you need to install the SDK and check the NKTP_SDK_PATH environment variable.') - raise - - -class Varia(Enum): - """Registers for the NKT SuperK VARIA device.""" - DEVICE_ID = 16 - - # SuperK VARIA registers - REG_MONITOR_INPUT = 0x13 - - REG_ND_SETPOINT = 0x32 - REG_SWP_SETPOINT = 0x33 - REG_LWP_SETPOINT = 0x34 - - REG_STATUS_BITS = 0x66 - - -def read_register(read_func, register, *, ratio=1, index=-1): - """Helper function to create register read methods.""" - def getter(self): - device_id = register.__class__.DEVICE_ID - - future = self.pool.submit(read_func, self.port, device_id.value, register.value, index) - result, value = future.result() - - self.check_result(result) - - return value * ratio - - return getter - - -def write_register(write_func, register, *, ratio=1, index=-1): - """Helper function to create register write methods.""" - def setter(self, value): - device_id = register.__class__.DEVICE_ID - - # Convert the value to the register value. This assumes integer types. - register_value = int(value / ratio) - - self.log.debug(f'Writing value {register_value} to {register}.') - - future = self.pool.submit(write_func, self.port, device_id.value, register.value, register_value, index) - result = future.result() - - self.check_result(result) - - return setter - - -class NktSuperkBase(Service, ABC): - """Abstract base class for NKT SuperK white light sources. - - This class contains common functionality shared between different - NKT SuperK devices like EVO and FIANIUM. Both the SuperK source and - VARIA are controlled through this service due to the need for a - single open port to the device. - """ - - def __init__(self, service_type): - """Initialize the NKT SuperK base service. - - Args: - service_type: The specific service type (e.g., 'nkt_superk_evo') - """ - super().__init__(service_type) - - self.threads = {} - self.port = self.config['port'] - - def open(self): - """Open the service and initialize data streams.""" - # Common data streams for VARIA - self.monitor_input = self.make_data_stream('monitor_input', 'float32', [1], 20) - - self.nd_setpoint = self.make_data_stream('nd_setpoint', 'float32', [1], 20) - self.swp_setpoint = self.make_data_stream('swp_setpoint', 'float32', [1], 20) - self.lwp_setpoint = self.make_data_stream('lwp_setpoint', 'float32', [1], 20) - - self.nd_filter_moving = self.make_data_stream('nd_filter_moving', 'uint8', [1], 20) - self.swp_filter_moving = self.make_data_stream('swp_filter_moving', 'uint8', [1], 20) - self.lwp_filter_moving = self.make_data_stream('lwp_filter_moving', 'uint8', [1], 20) - - # Common data streams for SuperK source - self.emission = self.make_data_stream('emission', 'uint8', [1], 20) - - # Set initial VARIA setpoints from config - self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) - self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) - self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) - - # Set initial emission from config - self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) - - # Create device-specific data streams - self._create_device_specific_streams() - - # Define common thread functions - common_funcs = { - 'nd_setpoint': self.monitor_func(self.nd_setpoint, self.set_nd_setpoint), - 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), - 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), - 'emission': self.monitor_func(self.emission, self.set_emission), - 'varia_status': self.update_func(self.update_varia_status) - } - - # Get device-specific thread functions and merge - device_funcs = self._get_device_specific_funcs() - funcs = {**common_funcs, **device_funcs} - - # Create a pool with a single worker to perform communication with the device - self.pool = ThreadPoolExecutor(max_workers=1) - - # Open port - future = self.pool.submit(openPorts, self.port, autoMode=0, liveMode=0) - self.check_result(future.result()) - - # Start all threads - for key, func in funcs.items(): - thread = threading.Thread(target=func) - thread.start() - self.threads[key] = thread - - def main(self): - """Main service loop.""" - while not self.should_shut_down: - self.sleep(1) - - def close(self): - """Close the service and cleanup resources.""" - # Turn off the source - self.set_emission(0) - - # Device-specific cleanup - self._device_specific_cleanup() - - # Join all threads - for thread in self.threads.values(): - thread.join() - - # Close port - future = self.pool.submit(closePorts, self.port) - self.check_result(future.result()) - - # Close pool - self.pool.shutdown() - - def check_result(self, result): - """Check if an NKT SDK operation was successful.""" - if result != 0: - self.log.error('NKT error: ' + RegisterResultTypes(result)) - raise RuntimeError(RegisterResultTypes(result)) - - def update_varia_status(self): - """Update VARIA status information.""" - status = self.get_varia_status_bits() - - # Extract moving filters from status - nd_filter_moving = (status & (2 << 12)) > 0 - swp_filter_moving = (status & (2 << 13)) > 0 - lwp_filter_moving = (status & (2 << 14)) > 0 - - # Submit results to their respective datastreams - self.nd_filter_moving.submit_data(np.array([nd_filter_moving], dtype='uint8')) - self.swp_filter_moving.submit_data(np.array([swp_filter_moving], dtype='uint8')) - self.lwp_filter_moving.submit_data(np.array([lwp_filter_moving], dtype='uint8')) - - # Update input monitor - monitor_input = self.get_monitor_input() - self.monitor_input.submit_data(np.array([monitor_input], dtype='float32')) - - def monitor_func(self, stream, setter): - """Create a monitoring function for a data stream.""" - def func(): - while not self.should_shut_down: - try: - frame = stream.get_next_frame(1) - except Exception: - continue - - setter(frame.data[0]) - - return func - - def update_func(self, updater): - """Create an update function that runs periodically.""" - def func(): - while not self.should_shut_down: - updater() - self.sleep(1) - - return func - - # Common VARIA register operations - get_monitor_input = read_register(registerReadU16, Varia.REG_MONITOR_INPUT, ratio=0.1) - - get_nd_setpoint = read_register(registerReadU16, Varia.REG_ND_SETPOINT, ratio=0.1) - get_swp_setpoint = read_register(registerReadU16, Varia.REG_SWP_SETPOINT, ratio=0.1) - get_lwp_setpoint = read_register(registerReadU16, Varia.REG_LWP_SETPOINT, ratio=0.1) - - set_nd_setpoint = write_register(registerWriteU16, Varia.REG_ND_SETPOINT, ratio=0.1) - set_swp_setpoint = write_register(registerWriteU16, Varia.REG_SWP_SETPOINT, ratio=0.1) - set_lwp_setpoint = write_register(registerWriteU16, Varia.REG_LWP_SETPOINT, ratio=0.1) - - get_varia_status_bits = read_register(registerReadU16, Varia.REG_STATUS_BITS) - - # Abstract methods that must be implemented by subclasses - @abstractmethod - def _create_device_specific_streams(self): - """Create device-specific data streams.""" - pass - - @abstractmethod - def _get_device_specific_funcs(self): - """Get device-specific thread functions.""" - pass - - @abstractmethod - def _device_specific_cleanup(self): - """Perform device-specific cleanup.""" - pass - - # Abstract register operations that must be implemented by subclasses - @abstractmethod - def set_emission(self, emission): - """Set emission state for the device.""" - pass diff --git a/catkit2/base_services/nkt_superk_sim_base.py b/catkit2/base_services/nkt_superk_sim_base.py deleted file mode 100644 index e4f980961..000000000 --- a/catkit2/base_services/nkt_superk_sim_base.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Base class for NKT SuperK simulated white light sources. - -This module provides a common base class for NKT SuperK simulated devices, -eliminating code duplication between EVO and FIANIUM simulation services. -""" - -from catkit2.testbed.service import Service - -import numpy as np -import threading -from abc import ABC, abstractmethod - - -class NktSuperkSimBase(Service, ABC): - """Abstract base class for NKT SuperK simulated white light sources. - - This class contains common functionality shared between different - NKT SuperK simulated devices like EVO and FIANIUM. Both the SuperK source and - VARIA simulation are controlled through this service. - """ - - def __init__(self, service_type): - """Initialize the NKT SuperK simulation base service. - - Args: - service_type: The specific service type (e.g., 'nkt_superk_evo_sim') - """ - super().__init__(service_type) - - self.threads = {} - self.port = self.config['port'] - - def open(self): - """Open the service and initialize data streams.""" - # Common data streams for VARIA - self.monitor_input = self.make_data_stream('monitor_input', 'float32', [1], 20) - - self.nd_setpoint = self.make_data_stream('nd_setpoint', 'float32', [1], 20) - self.swp_setpoint = self.make_data_stream('swp_setpoint', 'float32', [1], 20) - self.lwp_setpoint = self.make_data_stream('lwp_setpoint', 'float32', [1], 20) - - self.nd_filter_moving = self.make_data_stream('nd_filter_moving', 'uint8', [1], 20) - self.swp_filter_moving = self.make_data_stream('swp_filter_moving', 'uint8', [1], 20) - self.lwp_filter_moving = self.make_data_stream('lwp_filter_moving', 'uint8', [1], 20) - - # Common data streams for SuperK source - self.emission = self.make_data_stream('emission', 'uint8', [1], 20) - - # Set initial VARIA setpoints from config - self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) - self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) - self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) - - # Set initial emission from config - self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) - - # Create device-specific data streams - self._create_device_specific_streams() - - # Define common thread functions - common_funcs = { - 'nd_setpoint': self.monitor_func(self.nd_setpoint, self.set_nd_setpoint), - 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), - 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), - 'emission': self.monitor_func(self.emission, self.set_emission), - 'varia_status': self.update_func(self.update_varia_status) - } - - # Get device-specific thread functions and merge - device_funcs = self._get_device_specific_funcs() - funcs = {**common_funcs, **device_funcs} - - # Start all threads - for key, func in funcs.items(): - thread = threading.Thread(target=func) - thread.start() - self.threads[key] = thread - - def main(self): - """Main service loop.""" - while not self.should_shut_down: - self.sleep(1) - - def close(self): - """Close the service and cleanup resources.""" - # Turn off the source - self.set_emission(0) - - # Device-specific cleanup - self._device_specific_cleanup() - - # Join all threads - for thread in self.threads.values(): - thread.join() - - def update_varia_status(self): - """Update VARIA status information.""" - # Submit simulated results to their respective datastreams - self.nd_filter_moving.submit_data(np.array([0], dtype='uint8')) - self.swp_filter_moving.submit_data(np.array([0], dtype='uint8')) - self.lwp_filter_moving.submit_data(np.array([0], dtype='uint8')) - - self.monitor_input.submit_data(np.array([1], dtype='float32')) - - def monitor_func(self, stream, setter): - """Create a monitoring function for a data stream.""" - def func(): - while not self.should_shut_down: - try: - frame = stream.get_next_frame(1) - except Exception: - continue - - setter(frame.data[0]) - - return func - - def update_func(self, updater): - """Create an update function that runs periodically.""" - def func(): - while not self.should_shut_down: - updater() - self.sleep(1) - - return func - - # Common VARIA simulator operations - def set_nd_setpoint(self, nd_setpoint): - """Set ND filter setpoint in simulator.""" - self.testbed.simulator.move_filter( - filter_wheel_name=self.id + '_nd', - new_filter_position=nd_setpoint - ) - - def set_swp_setpoint(self, swp_setpoint): - """Set SWP filter setpoint in simulator.""" - self.testbed.simulator.move_filter( - filter_wheel_name=self.id + '_swp', - new_filter_position=swp_setpoint - ) - - def set_lwp_setpoint(self, lwp_setpoint): - """Set LWP filter setpoint in simulator.""" - self.testbed.simulator.move_filter( - filter_wheel_name=self.id + '_lwp', - new_filter_position=lwp_setpoint - ) - - # Abstract methods that must be implemented by subclasses - @abstractmethod - def _create_device_specific_streams(self): - """Create device-specific data streams.""" - pass - - @abstractmethod - def _get_device_specific_funcs(self): - """Get device-specific thread functions.""" - pass - - @abstractmethod - def _device_specific_cleanup(self): - """Perform device-specific cleanup.""" - pass - - # Abstract method that must be implemented by subclasses - @abstractmethod - def set_emission(self, emission): - """Set emission state for the device in simulator.""" - pass \ No newline at end of file diff --git a/catkit2/services/nkt_superk_evo/nkt_superk_evo.py b/catkit2/services/nkt_superk_evo/nkt_superk_evo.py index f799f1a21..9e65887ec 100644 --- a/catkit2/services/nkt_superk_evo/nkt_superk_evo.py +++ b/catkit2/services/nkt_superk_evo/nkt_superk_evo.py @@ -1,4 +1,4 @@ -from catkit2.base_services.nkt_superk_base import NktSuperkBase, read_register, write_register +from catkit2.base_services.nkt_superk import NktSuperk, read_register, write_register import numpy as np from enum import Enum @@ -37,7 +37,7 @@ class Evo(Enum): REG_MAC_ADDRESS = 0xB3 -class NktSuperkEvo(NktSuperkBase): +class NktSuperkEvo(NktSuperk): '''The service for both the NKT SuperK EVO and NKT SuperK VARIA. Both devices are combined into a single service due to the need for diff --git a/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py b/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py index 322270c95..04aae20f5 100644 --- a/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py +++ b/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py @@ -1,9 +1,9 @@ -from catkit2.base_services.nkt_superk_sim_base import NktSuperkSimBase +from catkit2.base_services.nkt_superk import NktSuperk import numpy as np -class NktSuperkEvoSim(NktSuperkSimBase): +class NktSuperkEvoSim(NktSuperk): def __init__(self): super().__init__('nkt_superk_evo_sim') diff --git a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py b/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py index c0dc422d2..00a5b9a5e 100644 --- a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py +++ b/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py @@ -1,4 +1,4 @@ -from catkit2.base_services.nkt_superk_base import NktSuperkBase, read_register, write_register +from catkit2.base_services.nkt_superk import NktSuperk, read_register, write_register import numpy as np from enum import Enum @@ -26,7 +26,7 @@ class Fianium(Enum): REG_ERROR_CODE = 0x67 -class NktSuperkFianium(NktSuperkBase): +class NktSuperkFianium(NktSuperk): '''The service for both the NKT SuperK FIANIUM and NKT SuperK VARIA. Both devices are combined into a single service due to the need for diff --git a/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py b/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py index 4895b7d3c..99e8f83da 100644 --- a/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py +++ b/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py @@ -1,9 +1,9 @@ -from catkit2.base_services.nkt_superk_sim_base import NktSuperkSimBase +from catkit2.base_services.nkt_superk import NktSuperk import numpy as np -class NktSuperkFianiumSim(NktSuperkSimBase): +class NktSuperkFianiumSim(NktSuperk): def __init__(self): super().__init__('nkt_superk_fianium_sim') self.pulse_picker_safety = self.config.get('pulse_picker_safety') diff --git a/tests/test_nkt_superk.py b/tests/test_nkt_superk.py new file mode 100644 index 000000000..06c3b866a --- /dev/null +++ b/tests/test_nkt_superk.py @@ -0,0 +1,213 @@ +"""Unit tests for the consolidated NKT SuperK base class.""" + +import unittest +from unittest.mock import Mock, patch + + +class TestNktSuperk(unittest.TestCase): + """Test the NktSuperk consolidated base class.""" + + def setUp(self): + """Set up test fixtures.""" + # Mock the Service base class + self.mock_service = Mock() + self.mock_service.config = { + 'port': 'COM1', + 'emission': 1, + 'nd_setpoint': 1.0, + 'swp_setpoint': 500.0, + 'lwp_setpoint': 600.0 + } + + # Mock data streams + self.mock_stream = Mock() + self.mock_stream.get.return_value = [0] + self.mock_stream.get_next_frame.return_value = Mock(data=[0]) + + self.mock_service.make_data_stream.return_value = self.mock_stream + + @patch('catkit2.base_services.nkt_superk.Service') + def test_cannot_instantiate_abstract_base(self, mock_service): + """Test that the abstract base class cannot be instantiated.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + from catkit2.base_services.nkt_superk import NktSuperk + + with self.assertRaises(TypeError): + # Should fail because abstract methods are not implemented + NktSuperk('test') + + @patch('catkit2.base_services.nkt_superk.Service') + def test_varia_enum(self, mock_service): + """Test that the Varia enum is properly defined.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + from catkit2.base_services.nkt_superk import Varia + + # Test that required attributes exist + self.assertEqual(Varia.DEVICE_ID, 16) + self.assertTrue(hasattr(Varia, 'REG_MONITOR_INPUT')) + self.assertTrue(hasattr(Varia, 'REG_ND_SETPOINT')) + self.assertTrue(hasattr(Varia, 'REG_SWP_SETPOINT')) + self.assertTrue(hasattr(Varia, 'REG_LWP_SETPOINT')) + self.assertTrue(hasattr(Varia, 'REG_STATUS_BITS')) + + @patch('catkit2.base_services.nkt_superk.Service') + def test_register_helpers(self, mock_service): + """Test that the register helper functions work correctly.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + from catkit2.base_services.nkt_superk import read_register, write_register, Varia + + # Mock the register read/write functions + mock_read_func = Mock() + mock_write_func = Mock() + + # Test read_register + getter = read_register(mock_read_func, Varia.REG_MONITOR_INPUT, ratio=0.1) + self.assertTrue(callable(getter)) + + # Test write_register + setter = write_register(mock_write_func, Varia.REG_MONITOR_INPUT, ratio=0.1) + self.assertTrue(callable(setter)) + + def test_simulation_detection(self): + """Test that simulation services are properly detected.""" + with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): + with patch('catkit2.base_services.nkt_superk.Service'): + from catkit2.base_services.nkt_superk import NktSuperk + + # Create a concrete test implementation + class TestNktSuperk(NktSuperk): + def _create_device_specific_streams(self): + pass + def _get_device_specific_funcs(self): + return {} + def _device_specific_cleanup(self): + pass + def set_emission(self, emission): + pass + + # Test hardware service detection + hw_service = TestNktSuperk('nkt_superk_evo') + self.assertFalse(hw_service.is_simulation) + + # Test simulation service detection + sim_service = TestNktSuperk('nkt_superk_evo_sim') + self.assertTrue(sim_service.is_simulation) + + def test_concrete_implementation_structure(self): + """Test that concrete implementations have the required structure.""" + # Mock everything we need + with patch.dict('sys.modules', { + 'catkit2.testbed.service': Mock(), + 'NKTP_DLL': Mock() + }): + with patch('catkit2.base_services.nkt_superk.Service'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU16'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU8'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU8'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadS16'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.openPorts'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.closePorts'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.RegisterResultTypes'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU16'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU8'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU8'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadS16'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.openPorts'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.closePorts'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.RegisterResultTypes'): + + from catkit2.base_services.nkt_superk import NktSuperk + from catkit2.services.nkt_superk_evo.nkt_superk_evo import NktSuperkEvo + from catkit2.services.nkt_superk_fianium.nkt_superk_fianium import NktSuperkFianium + from catkit2.services.nkt_superk_evo_sim.nkt_superk_evo_sim import NktSuperkEvoSim + from catkit2.services.nkt_superk_fianium_sim.nkt_superk_fianium_sim import NktSuperkFianiumSim + + # Test inheritance for all services + all_services = [NktSuperkEvo, NktSuperkFianium, NktSuperkEvoSim, NktSuperkFianiumSim] + for service_class in all_services: + self.assertTrue(issubclass(service_class, NktSuperk)) + + # Test that abstract methods are implemented + required_methods = [ + '_create_device_specific_streams', + '_get_device_specific_funcs', + '_device_specific_cleanup', + 'set_emission' + ] + + for service_class in all_services: + for method in required_methods: + self.assertTrue(hasattr(service_class, method)) + + # Test that common methods are available for all services + common_methods = [ + 'update_varia_status', 'monitor_func', 'update_func', + 'set_nd_setpoint', 'set_swp_setpoint', 'set_lwp_setpoint' + ] + + for service_class in all_services: + for method in common_methods: + self.assertTrue(hasattr(service_class, method)) + + # Test that hardware-specific methods are available for hardware services + hardware_methods = [ + 'get_monitor_input', 'get_nd_setpoint', 'get_swp_setpoint', + 'get_lwp_setpoint', 'get_varia_status_bits', 'check_result' + ] + + for service_class in [NktSuperkEvo, NktSuperkFianium]: + for method in hardware_methods: + self.assertTrue(hasattr(service_class, method)) + + def test_evo_specific_methods(self): + """Test that EVO services have device-specific methods.""" + with patch.dict('sys.modules', { + 'catkit2.testbed.service': Mock(), + 'NKTP_DLL': Mock() + }): + with patch('catkit2.base_services.nkt_superk.Service'): + # Mock necessary register functions + with patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU16'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU8'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU8'), \ + patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadS16'): + + from catkit2.services.nkt_superk_evo.nkt_superk_evo import NktSuperkEvo + from catkit2.services.nkt_superk_evo_sim.nkt_superk_evo_sim import NktSuperkEvoSim + + # Test EVO-specific methods for both hardware and simulation + evo_methods = ['update_evo_status', 'set_power_setpoint', 'set_current_setpoint'] + + for method in evo_methods: + self.assertTrue(hasattr(NktSuperkEvo, method)) + self.assertTrue(hasattr(NktSuperkEvoSim, method)) + + def test_fianium_specific_methods(self): + """Test that FIANIUM services have device-specific methods.""" + with patch.dict('sys.modules', { + 'catkit2.testbed.service': Mock(), + 'NKTP_DLL': Mock() + }): + with patch('catkit2.base_services.nkt_superk.Service'): + # Mock necessary register functions + with patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU16'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU8'), \ + patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU8'): + + from catkit2.services.nkt_superk_fianium.nkt_superk_fianium import NktSuperkFianium + from catkit2.services.nkt_superk_fianium_sim.nkt_superk_fianium_sim import NktSuperkFianiumSim + + # Test FIANIUM-specific methods for both hardware and simulation + fianium_methods = ['set_power_setpoint', 'set_pulse_picker_ratio', 'monitor_pulse_picker_ratio'] + + for method in fianium_methods: + self.assertTrue(hasattr(NktSuperkFianium, method)) + self.assertTrue(hasattr(NktSuperkFianiumSim, method)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_nkt_superk_base.py b/tests/test_nkt_superk_base.py deleted file mode 100644 index 25ed80d40..000000000 --- a/tests/test_nkt_superk_base.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Unit tests for the NKT SuperK base class.""" - -import unittest -from unittest.mock import Mock, patch, MagicMock -import numpy as np - - -class TestNktSuperkBase(unittest.TestCase): - """Test the NktSuperkBase abstract class.""" - - def setUp(self): - """Set up test fixtures.""" - # Mock the Service base class and NKT SDK - self.mock_service = Mock() - self.mock_service.config = { - 'port': 'COM1', - 'emission': 1, - 'nd_setpoint': 1.0, - 'swp_setpoint': 500.0, - 'lwp_setpoint': 600.0 - } - - # Mock data streams - self.mock_stream = Mock() - self.mock_stream.get.return_value = [0] - self.mock_stream.get_next_frame.return_value = Mock(data=[0]) - - self.mock_service.make_data_stream.return_value = self.mock_stream - - @patch('catkit2.base_services.nkt_superk_base.Service') - @patch('catkit2.base_services.nkt_superk_base.ThreadPoolExecutor') - def test_cannot_instantiate_abstract_base(self, mock_executor, mock_service): - """Test that the abstract base class cannot be instantiated.""" - with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): - from catkit2.base_services.nkt_superk_base import NktSuperkBase - - with self.assertRaises(TypeError): - # Should fail because abstract methods are not implemented - NktSuperkBase('test') - - @patch('catkit2.services.nkt_superk_base.Service') - def test_varia_enum(self, mock_service): - """Test that the Varia enum is properly defined.""" - with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): - from catkit2.services.nkt_superk_base import Varia - - # Test that required attributes exist - self.assertEqual(Varia.DEVICE_ID, 16) - self.assertTrue(hasattr(Varia, 'REG_MONITOR_INPUT')) - self.assertTrue(hasattr(Varia, 'REG_ND_SETPOINT')) - self.assertTrue(hasattr(Varia, 'REG_SWP_SETPOINT')) - self.assertTrue(hasattr(Varia, 'REG_LWP_SETPOINT')) - self.assertTrue(hasattr(Varia, 'REG_STATUS_BITS')) - - @patch('catkit2.services.nkt_superk_base.Service') - def test_register_helpers(self, mock_service): - """Test that the register helper functions work correctly.""" - with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): - from catkit2.services.nkt_superk_base import read_register, write_register, Varia - - # Mock the register read/write functions - mock_read_func = Mock() - mock_write_func = Mock() - - # Test read_register - getter = read_register(mock_read_func, Varia.REG_MONITOR_INPUT, ratio=0.1) - self.assertTrue(callable(getter)) - - # Test write_register - setter = write_register(mock_write_func, Varia.REG_MONITOR_INPUT, ratio=0.1) - self.assertTrue(callable(setter)) - - def test_concrete_implementation_structure(self): - """Test that concrete implementations have the required structure.""" - # Mock everything we need - with patch.dict('sys.modules', { - 'catkit2.testbed.service': Mock(), - 'NKTP_DLL': Mock() - }): - with patch('catkit2.services.nkt_superk_base.Service'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU16'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU16'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU8'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU8'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadS16'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.openPorts'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.closePorts'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.RegisterResultTypes'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU16'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU16'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU8'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU8'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadS16'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.openPorts'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.closePorts'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.RegisterResultTypes'): - - from catkit2.base_services.nkt_superk_base import NktSuperkBase - from catkit2.services.nkt_superk_evo.nkt_superk_evo import NktSuperkEvo - from catkit2.services.nkt_superk_fianium.nkt_superk_fianium import NktSuperkFianium - - # Test inheritance - self.assertTrue(issubclass(NktSuperkEvo, NktSuperkBase)) - self.assertTrue(issubclass(NktSuperkFianium, NktSuperkBase)) - - # Test that abstract methods are implemented - required_methods = [ - '_create_device_specific_streams', - '_get_device_specific_funcs', - '_device_specific_cleanup', - 'set_emission' - ] - - for method in required_methods: - self.assertTrue(hasattr(NktSuperkEvo, method)) - self.assertTrue(hasattr(NktSuperkFianium, method)) - - # Test that common methods are available - common_methods = [ - 'get_monitor_input', 'get_nd_setpoint', 'get_swp_setpoint', - 'get_lwp_setpoint', 'set_nd_setpoint', 'set_swp_setpoint', - 'set_lwp_setpoint', 'get_varia_status_bits', 'check_result', - 'update_varia_status', 'monitor_func', 'update_func' - ] - - for method in common_methods: - self.assertTrue(hasattr(NktSuperkEvo, method)) - self.assertTrue(hasattr(NktSuperkFianium, method)) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/test_nkt_superk_sim_base.py b/tests/test_nkt_superk_sim_base.py deleted file mode 100644 index 387ffea42..000000000 --- a/tests/test_nkt_superk_sim_base.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Unit tests for the NKT SuperK simulation base class.""" - -import unittest -from unittest.mock import Mock, patch, MagicMock -import numpy as np - - -class TestNktSuperkSimBase(unittest.TestCase): - """Test the NktSuperkSimBase abstract class.""" - - def setUp(self): - """Set up test fixtures.""" - # Mock the Service base class - self.mock_service = Mock() - self.mock_service.config = { - 'port': 'COM1', - 'emission': 1, - 'nd_setpoint': 1.0, - 'swp_setpoint': 500.0, - 'lwp_setpoint': 600.0 - } - - # Mock data streams - self.mock_stream = Mock() - self.mock_stream.get.return_value = [0] - self.mock_stream.get_next_frame.return_value = Mock(data=[0]) - - self.mock_service.make_data_stream.return_value = self.mock_stream - - @patch('catkit2.base_services.nkt_superk_sim_base.Service') - def test_cannot_instantiate_abstract_base(self, mock_service): - """Test that the abstract base class cannot be instantiated.""" - with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): - from catkit2.base_services.nkt_superk_sim_base import NktSuperkSimBase - - with self.assertRaises(TypeError): - # Should fail because abstract methods are not implemented - NktSuperkSimBase('test') - - def test_concrete_simulation_implementation_structure(self): - """Test that concrete simulation implementations have the required structure.""" - # Mock everything we need - with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): - with patch('catkit2.base_services.nkt_superk_sim_base.Service'): - - from catkit2.base_services.nkt_superk_sim_base import NktSuperkSimBase - from catkit2.services.nkt_superk_evo_sim.nkt_superk_evo_sim import NktSuperkEvoSim - from catkit2.services.nkt_superk_fianium_sim.nkt_superk_fianium_sim import NktSuperkFianiumSim - - # Test inheritance - self.assertTrue(issubclass(NktSuperkEvoSim, NktSuperkSimBase)) - self.assertTrue(issubclass(NktSuperkFianiumSim, NktSuperkSimBase)) - - # Test that abstract methods are implemented - required_methods = [ - '_create_device_specific_streams', - '_get_device_specific_funcs', - '_device_specific_cleanup', - 'set_emission' - ] - - for method in required_methods: - self.assertTrue(hasattr(NktSuperkEvoSim, method)) - self.assertTrue(hasattr(NktSuperkFianiumSim, method)) - - # Test that common methods are available - common_methods = [ - 'set_nd_setpoint', 'set_swp_setpoint', 'set_lwp_setpoint', - 'update_varia_status', 'monitor_func', 'update_func' - ] - - for method in common_methods: - self.assertTrue(hasattr(NktSuperkEvoSim, method)) - self.assertTrue(hasattr(NktSuperkFianiumSim, method)) - - def test_evo_sim_specific_methods(self): - """Test that EVO simulation has device-specific methods.""" - with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): - with patch('catkit2.base_services.nkt_superk_sim_base.Service'): - from catkit2.services.nkt_superk_evo_sim.nkt_superk_evo_sim import NktSuperkEvoSim - - # Test EVO-specific methods - evo_methods = ['update_evo_status', 'set_power_setpoint', 'set_current_setpoint'] - - for method in evo_methods: - self.assertTrue(hasattr(NktSuperkEvoSim, method)) - - def test_fianium_sim_specific_methods(self): - """Test that FIANIUM simulation has device-specific methods.""" - with patch.dict('sys.modules', {'catkit2.testbed.service': Mock()}): - with patch('catkit2.base_services.nkt_superk_sim_base.Service'): - from catkit2.services.nkt_superk_fianium_sim.nkt_superk_fianium_sim import NktSuperkFianiumSim - - # Test FIANIUM-specific methods - fianium_methods = ['set_power_setpoint', 'set_pulse_picker_ratio', 'monitor_pulse_picker_ratio'] - - for method in fianium_methods: - self.assertTrue(hasattr(NktSuperkFianiumSim, method)) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file From e10490575f845763069e18a402cb1a9a99e8da1d Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Tue, 2 Sep 2025 14:14:07 +0200 Subject: [PATCH 07/12] Power setpoint data stream is common to SuperK --- catkit2/base_services/nkt_superk.py | 4 +++- catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py | 2 -- .../services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/catkit2/base_services/nkt_superk.py b/catkit2/base_services/nkt_superk.py index 5db27782c..07ab7a44b 100644 --- a/catkit2/base_services/nkt_superk.py +++ b/catkit2/base_services/nkt_superk.py @@ -110,14 +110,16 @@ def open(self): # Common data streams for SuperK source self.emission = self.make_data_stream('emission', 'uint8', [1], 20) + self.power_setpoint = self.make_data_stream('power_setpoint', 'float32', [1], 20) # Set initial VARIA setpoints from config self.nd_setpoint.submit_data(np.array([self.config['nd_setpoint']], dtype='float32')) self.swp_setpoint.submit_data(np.array([self.config['swp_setpoint']], dtype='float32')) self.lwp_setpoint.submit_data(np.array([self.config['lwp_setpoint']], dtype='float32')) - # Set initial emission from config + # Set initial emission and power from config self.emission.submit_data(np.array([self.config['emission']], dtype='uint8')) + self.power_setpoint.submit_data(np.array([self.config['power_setpoint']], dtype='float32')) # Create device-specific data streams self._create_device_specific_streams() diff --git a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py b/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py index 00a5b9a5e..731fc615e 100644 --- a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py +++ b/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py @@ -40,11 +40,9 @@ def __init__(self): def _create_device_specific_streams(self): """Create FIANIUM-specific data streams.""" # FIANIUM-specific streams - self.power_setpoint = self.make_data_stream('power_setpoint', 'float32', [1], 20) self.pulse_picker_ratio = self.make_data_stream('pulse_picker_ratio', 'uint16', [1], 20) # Set initial FIANIUM setpoints from config - self.power_setpoint.submit_data(np.array([self.config['power_setpoint']], dtype='float32')) self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) # Setting a safe default value of 100. def _get_device_specific_funcs(self): diff --git a/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py b/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py index 99e8f83da..1b3adc019 100644 --- a/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py +++ b/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py @@ -11,11 +11,9 @@ def __init__(self): def _create_device_specific_streams(self): """Create FIANIUM-specific data streams.""" # FIANIUM-specific streams - self.power_setpoint = self.make_data_stream('power_setpoint', 'float32', [1], 20) self.pulse_picker_ratio = self.make_data_stream('pulse_picker_ratio', 'uint16', [1], 20) # Set initial FIANIUM setpoints from config - self.power_setpoint.submit_data(np.array([self.config['power_setpoint']], dtype='float32')) self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) # Setting a safe default value of 100. def _get_device_specific_funcs(self): From e7af9b4cf26fc258294bb61313d2f2b03a30d72f Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Tue, 2 Sep 2025 14:31:32 +0200 Subject: [PATCH 08/12] Create base classes for Evo and Fianium respectively --- catkit2/base_services/nkt_superk.py | 2 +- catkit2/base_services/nkt_superk_evo.py | 38 +++++++ catkit2/base_services/nkt_superk_fianium.py | 52 +++++++++ .../nkt_superk_evo_hardware.py} | 42 ++------ .../nkt_superk_evo_sim/nkt_superk_evo_sim.py | 27 +---- .../nkt_superk_fianium/nkt_superk_fianium.py | 101 ------------------ .../nkt_superk_fianium_hardware.py | 64 +++++++++++ .../nkt_superk_fianium_sim.py | 40 +------ tests/test_nkt_superk.py | 68 ++++++------ 9 files changed, 199 insertions(+), 235 deletions(-) create mode 100644 catkit2/base_services/nkt_superk_evo.py create mode 100644 catkit2/base_services/nkt_superk_fianium.py rename catkit2/services/{nkt_superk_evo/nkt_superk_evo.py => nkt_superk_evo_hardware/nkt_superk_evo_hardware.py} (61%) delete mode 100644 catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py create mode 100644 catkit2/services/nkt_superk_fianium_hardware/nkt_superk_fianium_hardware.py diff --git a/catkit2/base_services/nkt_superk.py b/catkit2/base_services/nkt_superk.py index 07ab7a44b..5ecdb0ed6 100644 --- a/catkit2/base_services/nkt_superk.py +++ b/catkit2/base_services/nkt_superk.py @@ -87,7 +87,7 @@ def __init__(self, service_type): """Initialize the NKT SuperK service. Args: - service_type: The specific service type (e.g., 'nkt_superk_evo') + service_type: The specific service type (e.g., 'nkt_superk_evo_hardware') """ super().__init__(service_type) diff --git a/catkit2/base_services/nkt_superk_evo.py b/catkit2/base_services/nkt_superk_evo.py new file mode 100644 index 000000000..8f2e2292c --- /dev/null +++ b/catkit2/base_services/nkt_superk_evo.py @@ -0,0 +1,38 @@ +from catkit2.base_services.nkt_superk import NktSuperk + +import numpy as np + + +class NktSuperkEvo(NktSuperk): + '''The base service for both the NKT SuperK EVO and NKT SuperK VARIA. + + Both devices are combined into a single service due to the need for + a single open port to the device that cannot be shared between + multiple services. + ''' + def __init__(self, service_type): + super().__init__(service_type) + + def _create_device_specific_streams(self): + """Create EVO-specific data streams.""" + # EVO-specific streams + self.base_temperature = self.make_data_stream('base_temperature', 'float32', [1], 20) + self.supply_voltage = self.make_data_stream('supply_voltage', 'float32', [1], 20) + self.external_control_input = self.make_data_stream('external_control_input', 'float32', [1], 20) + self.current_setpoint = self.make_data_stream('current_setpoint', 'float32', [1], 20) + + # Set initial EVO setpoints from config + self.current_setpoint.submit_data(np.array([self.config['current_setpoint']], dtype='float32')) + + def _get_device_specific_funcs(self): + """Get EVO-specific thread functions.""" + return { + 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), + 'current_setpoint': self.monitor_func(self.current_setpoint, self.set_current_setpoint), + 'evo_status': self.update_func(self.update_evo_status) + } + + def _device_specific_cleanup(self): + """Perform EVO-specific cleanup.""" + # No specific cleanup needed for EVO + pass diff --git a/catkit2/base_services/nkt_superk_fianium.py b/catkit2/base_services/nkt_superk_fianium.py new file mode 100644 index 000000000..acd6929ea --- /dev/null +++ b/catkit2/base_services/nkt_superk_fianium.py @@ -0,0 +1,52 @@ +from catkit2.base_services.nkt_superk import NktSuperk + +import numpy as np + + +class NktSuperkFianium(NktSuperk): + '''The service for both the NKT SuperK FIANIUM and NKT SuperK VARIA. + + Both devices are combined into a single service due to the need for + a single open port to the device that cannot be shared between + multiple services. + ''' + def __init__(self, service_type): + super().__init__(service_type) + self.pulse_picker_safety = self.config.get('pulse_picker_safety') + + def _create_device_specific_streams(self): + """Create FIANIUM-specific data streams.""" + # FIANIUM-specific streams + self.pulse_picker_ratio = self.make_data_stream('pulse_picker_ratio', 'uint16', [1], 20) + + # Set initial FIANIUM setpoints from config + self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) # Setting a safe default value of 100. + + def _get_device_specific_funcs(self): + """Get FIANIUM-specific thread functions.""" + return { + 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), + 'pulse_picker_ratio': self.monitor_pulse_picker_ratio(self.pulse_picker_ratio, self.set_pulse_picker_ratio) + } + + def _device_specific_cleanup(self): + """Perform FIANIUM-specific cleanup.""" + self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) + + def monitor_pulse_picker_ratio(self, stream, setter): + """Monitor pulse picker ratio with safety checks.""" + def func(): + while not self.should_shut_down: + try: + frame = stream.get_next_frame(1) + except Exception: + continue + + bandwidth = self.swp_setpoint.get()[0] - self.lwp_setpoint.get()[0] + if frame.data[0] >= max(int(bandwidth / self.pulse_picker_safety), 1): + setter(frame.data[0]) + else: + self.log.warning(f'Pulse picker ratio {frame.data[0]} is too low for the current bandwidth {bandwidth}. Not setting it.') + + return func + diff --git a/catkit2/services/nkt_superk_evo/nkt_superk_evo.py b/catkit2/services/nkt_superk_evo_hardware/nkt_superk_evo_hardware.py similarity index 61% rename from catkit2/services/nkt_superk_evo/nkt_superk_evo.py rename to catkit2/services/nkt_superk_evo_hardware/nkt_superk_evo_hardware.py index 9e65887ec..aac5c4947 100644 --- a/catkit2/services/nkt_superk_evo/nkt_superk_evo.py +++ b/catkit2/services/nkt_superk_evo_hardware/nkt_superk_evo_hardware.py @@ -1,4 +1,5 @@ -from catkit2.base_services.nkt_superk import NktSuperk, read_register, write_register +from catkit2.base_services.nkt_superk import read_register, write_register +from catkit2.base_services.nkt_superk_evo import NktSuperkEvo import numpy as np from enum import Enum @@ -6,8 +7,8 @@ try: from NKTP_DLL import * except ImportError: - print('To use NKT SDK, you need to install the SDK and check the NKTP_SDK_PATH environment variable.') - raise + raise RuntimeError('NKT SDK is required for hardware services but not available. ' + 'Install the SDK and check the NKTP_SDK_PATH environment variable.') class Evo(Enum): @@ -37,42 +38,15 @@ class Evo(Enum): REG_MAC_ADDRESS = 0xB3 -class NktSuperkEvo(NktSuperk): - '''The service for both the NKT SuperK EVO and NKT SuperK VARIA. +class NktSuperkEvoHardware(NktSuperkEvo): + '''The hardware service for both the NKT SuperK EVO and NKT SuperK VARIA. Both devices are combined into a single service due to the need for a single open port to the device that cannot be shared between multiple services. ''' def __init__(self): - super().__init__('nkt_superk_evo') - - def _create_device_specific_streams(self): - """Create EVO-specific data streams.""" - # EVO-specific streams - self.base_temperature = self.make_data_stream('base_temperature', 'float32', [1], 20) - self.supply_voltage = self.make_data_stream('supply_voltage', 'float32', [1], 20) - self.external_control_input = self.make_data_stream('external_control_input', 'float32', [1], 20) - - self.power_setpoint = self.make_data_stream('power_setpoint', 'float32', [1], 20) - self.current_setpoint = self.make_data_stream('current_setpoint', 'float32', [1], 20) - - # Set initial EVO setpoints from config - self.power_setpoint.submit_data(np.array([self.config['power_setpoint']], dtype='float32')) - self.current_setpoint.submit_data(np.array([self.config['current_setpoint']], dtype='float32')) - - def _get_device_specific_funcs(self): - """Get EVO-specific thread functions.""" - return { - 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), - 'current_setpoint': self.monitor_func(self.current_setpoint, self.set_current_setpoint), - 'evo_status': self.update_func(self.update_evo_status) - } - - def _device_specific_cleanup(self): - """Perform EVO-specific cleanup.""" - # No specific cleanup needed for EVO - pass + super().__init__('nkt_superk_evo_hardware') def update_evo_status(self): """Update EVO-specific status information.""" @@ -112,5 +86,5 @@ def update_evo_status(self): if __name__ == '__main__': - service = NktSuperkEvo() + service = NktSuperkEvoHardware() service.run() diff --git a/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py b/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py index 04aae20f5..3504ee8fb 100644 --- a/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py +++ b/catkit2/services/nkt_superk_evo_sim/nkt_superk_evo_sim.py @@ -4,35 +4,10 @@ class NktSuperkEvoSim(NktSuperk): + """The simulated service for both the NKT SuperK EVO and NKT SuperK VARIA.""" def __init__(self): super().__init__('nkt_superk_evo_sim') - def _create_device_specific_streams(self): - """Create EVO-specific data streams.""" - # EVO-specific streams - self.base_temperature = self.make_data_stream('base_temperature', 'float32', [1], 20) - self.supply_voltage = self.make_data_stream('supply_voltage', 'float32', [1], 20) - self.external_control_input = self.make_data_stream('external_control_input', 'float32', [1], 20) - - self.power_setpoint = self.make_data_stream('power_setpoint', 'float32', [1], 20) - self.current_setpoint = self.make_data_stream('current_setpoint', 'float32', [1], 20) - - # Set initial EVO setpoints from config - self.power_setpoint.submit_data(np.array([self.config['power_setpoint']], dtype='float32')) - self.current_setpoint.submit_data(np.array([self.config['current_setpoint']], dtype='float32')) - - def _get_device_specific_funcs(self): - """Get EVO-specific thread functions.""" - return { - 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), - 'current_setpoint': self.monitor_func(self.current_setpoint, self.set_current_setpoint), - 'evo_status': self.update_func(self.update_evo_status) - } - - def _device_specific_cleanup(self): - """Perform EVO-specific cleanup.""" - pass - def update_evo_status(self): """Update EVO-specific status information.""" self.base_temperature.submit_data(np.array([28.5], dtype='float32')) diff --git a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py b/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py deleted file mode 100644 index 731fc615e..000000000 --- a/catkit2/services/nkt_superk_fianium/nkt_superk_fianium.py +++ /dev/null @@ -1,101 +0,0 @@ -from catkit2.base_services.nkt_superk import NktSuperk, read_register, write_register - -import numpy as np -from enum import Enum - -try: - from NKTP_DLL import * -except ImportError: - print('To use NKT SDK, you need to install the SDK and check the NKTP_SDK_PATH environment variable.') - raise - - -class Fianium(Enum): - """Registers for the NKT SuperK FIANIUM device.""" - DEVICE_ID = 15 - REG_EMISSION = 0x30 - REG_SETUP_BITS = 0x31 - REG_INTERLOCK = 0x32 - REG_PULSE_PICKER_RATIO = 0x34 - REG_WATCHDOG_TIMER = 0x36 - REG_OUTPUT_LEVEL = 0x37 - REG_NIM_DELAY = 0x39 - REG_MODULE_TYPE = 0x61 - REG_MODEL_SERIAL_NUMBER = 0x65 - REG_STATUS_BITS = 0x66 - REG_ERROR_CODE = 0x67 - - -class NktSuperkFianium(NktSuperk): - '''The service for both the NKT SuperK FIANIUM and NKT SuperK VARIA. - - Both devices are combined into a single service due to the need for - a single open port to the device that cannot be shared between - multiple services. - ''' - def __init__(self): - super().__init__('nkt_superk_fianium') - self.pulse_picker_safety = self.config.get('pulse_picker_safety') - - def _create_device_specific_streams(self): - """Create FIANIUM-specific data streams.""" - # FIANIUM-specific streams - self.pulse_picker_ratio = self.make_data_stream('pulse_picker_ratio', 'uint16', [1], 20) - - # Set initial FIANIUM setpoints from config - self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) # Setting a safe default value of 100. - - def _get_device_specific_funcs(self): - """Get FIANIUM-specific thread functions.""" - return { - 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), - 'pulse_picker_ratio': self.monitor_pulse_picker_ratio(self.pulse_picker_ratio, self.set_pulse_picker_ratio) - } - - def _device_specific_cleanup(self): - """Perform FIANIUM-specific cleanup.""" - # Set pulse picker ratio to safe value - self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) - - def monitor_pulse_picker_ratio(self, stream, setter): - """Monitor pulse picker ratio with safety checks.""" - def func(): - while not self.should_shut_down: - try: - frame = stream.get_next_frame(1) - except Exception: - continue - - bandwidth = self.swp_setpoint.get()[0] - self.lwp_setpoint.get()[0] - if frame.data[0] >= max(int(bandwidth / self.pulse_picker_safety), 1): - setter(frame.data[0]) - else: - self.log.warning(f'Pulse picker ratio {frame.data[0]} is too low for the current bandwidth {bandwidth}. Not setting it.') - - return func - - # Functions for the SuperK FIANIUM - get_power_setpoint = read_register(registerReadU16, Fianium.REG_OUTPUT_LEVEL, ratio=0.1) - set_power_setpoint = write_register(registerWriteU16, Fianium.REG_OUTPUT_LEVEL, ratio=0.1) - - get_emission = read_register(registerReadU8, Fianium.REG_EMISSION, ratio=1) - set_emission = write_register(registerWriteU8, Fianium.REG_EMISSION, ratio=1) - - get_setup_bits = read_register(registerReadU8, Fianium.REG_SETUP_BITS) - set_setup_bits = write_register(registerWriteU8, Fianium.REG_SETUP_BITS) - - get_interlock_msb = read_register(registerReadU8, Fianium.REG_INTERLOCK, index=0) - get_interlock_lsb = read_register(registerReadU8, Fianium.REG_INTERLOCK, index=1) - - get_fianium_status_bits = read_register(registerReadU16, Fianium.REG_STATUS_BITS) - - get_watchdog_timer = read_register(registerReadU8, Fianium.REG_WATCHDOG_TIMER) - set_watchdog_timer = write_register(registerWriteU8, Fianium.REG_WATCHDOG_TIMER) - - get_pulse_picker_ratio = read_register(registerReadU16, Fianium.REG_PULSE_PICKER_RATIO, ratio=1) - set_pulse_picker_ratio = write_register(registerWriteU16, Fianium.REG_PULSE_PICKER_RATIO, ratio=1) - - -if __name__ == '__main__': - service = NktSuperkFianium() - service.run() diff --git a/catkit2/services/nkt_superk_fianium_hardware/nkt_superk_fianium_hardware.py b/catkit2/services/nkt_superk_fianium_hardware/nkt_superk_fianium_hardware.py new file mode 100644 index 000000000..1f3f81564 --- /dev/null +++ b/catkit2/services/nkt_superk_fianium_hardware/nkt_superk_fianium_hardware.py @@ -0,0 +1,64 @@ +from catkit2.base_services.nkt_superk import read_register, write_register +from catkit2.base_services.nkt_superk_fianium import NktSuperkFianium + +import numpy as np +from enum import Enum + +try: + from NKTP_DLL import * +except ImportError: + raise RuntimeError('NKT SDK is required for hardware services but not available. ' + 'Install the SDK and check the NKTP_SDK_PATH environment variable.') + + +class Fianium(Enum): + """Registers for the NKT SuperK FIANIUM device.""" + DEVICE_ID = 15 + REG_EMISSION = 0x30 + REG_SETUP_BITS = 0x31 + REG_INTERLOCK = 0x32 + REG_PULSE_PICKER_RATIO = 0x34 + REG_WATCHDOG_TIMER = 0x36 + REG_OUTPUT_LEVEL = 0x37 + REG_NIM_DELAY = 0x39 + REG_MODULE_TYPE = 0x61 + REG_MODEL_SERIAL_NUMBER = 0x65 + REG_STATUS_BITS = 0x66 + REG_ERROR_CODE = 0x67 + + +class NktSuperkFianiumHardware(NktSuperkFianium): + '''The hardware service for both the NKT SuperK FIANIUM and NKT SuperK VARIA. + + Both devices are combined into a single service due to the need for + a single open port to the device that cannot be shared between + multiple services. + ''' + def __init__(self): + super().__init__('nkt_superk_fianium_hardware') + + # Functions for the SuperK FIANIUM + get_power_setpoint = read_register(registerReadU16, Fianium.REG_OUTPUT_LEVEL, ratio=0.1) + set_power_setpoint = write_register(registerWriteU16, Fianium.REG_OUTPUT_LEVEL, ratio=0.1) + + get_emission = read_register(registerReadU8, Fianium.REG_EMISSION, ratio=1) + set_emission = write_register(registerWriteU8, Fianium.REG_EMISSION, ratio=1) + + get_setup_bits = read_register(registerReadU8, Fianium.REG_SETUP_BITS) + set_setup_bits = write_register(registerWriteU8, Fianium.REG_SETUP_BITS) + + get_interlock_msb = read_register(registerReadU8, Fianium.REG_INTERLOCK, index=0) + get_interlock_lsb = read_register(registerReadU8, Fianium.REG_INTERLOCK, index=1) + + get_fianium_status_bits = read_register(registerReadU16, Fianium.REG_STATUS_BITS) + + get_watchdog_timer = read_register(registerReadU8, Fianium.REG_WATCHDOG_TIMER) + set_watchdog_timer = write_register(registerWriteU8, Fianium.REG_WATCHDOG_TIMER) + + get_pulse_picker_ratio = read_register(registerReadU16, Fianium.REG_PULSE_PICKER_RATIO, ratio=1) + set_pulse_picker_ratio = write_register(registerWriteU16, Fianium.REG_PULSE_PICKER_RATIO, ratio=1) + + +if __name__ == '__main__': + service = NktSuperkFianiumHardware() + service.run() diff --git a/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py b/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py index 1b3adc019..1c38715eb 100644 --- a/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py +++ b/catkit2/services/nkt_superk_fianium_sim/nkt_superk_fianium_sim.py @@ -1,48 +1,10 @@ from catkit2.base_services.nkt_superk import NktSuperk -import numpy as np - class NktSuperkFianiumSim(NktSuperk): + """The simulated service for both the NKT SuperK FIANIUM and NKT SuperK VARIA.""" def __init__(self): super().__init__('nkt_superk_fianium_sim') - self.pulse_picker_safety = self.config.get('pulse_picker_safety') - - def _create_device_specific_streams(self): - """Create FIANIUM-specific data streams.""" - # FIANIUM-specific streams - self.pulse_picker_ratio = self.make_data_stream('pulse_picker_ratio', 'uint16', [1], 20) - - # Set initial FIANIUM setpoints from config - self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) # Setting a safe default value of 100. - - def _get_device_specific_funcs(self): - """Get FIANIUM-specific thread functions.""" - return { - 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), - 'pulse_picker_ratio': self.monitor_pulse_picker_ratio(self.pulse_picker_ratio, self.set_pulse_picker_ratio) - } - - def _device_specific_cleanup(self): - """Perform FIANIUM-specific cleanup.""" - self.pulse_picker_ratio.submit_data(np.array([100], dtype='uint16')) - - def monitor_pulse_picker_ratio(self, stream, setter): - """Create a monitoring function for pulse picker ratio with safety checks.""" - def func(): - while not self.should_shut_down: - try: - frame = stream.get_next_frame(1) - except Exception: - continue - - bandwidth = self.swp_setpoint.get()[0] - self.lwp_setpoint.get()[0] - if frame.data[0] >= max(int(bandwidth / self.pulse_picker_safety), 1): - setter(frame.data[0]) - else: - self.log.warning(f'Pulse picker ratio {frame.data[0]} is too low for the current bandwidth {bandwidth}. Not setting it.') - - return func def set_emission(self, emission): """Set emission state for FIANIUM device in simulator.""" diff --git a/tests/test_nkt_superk.py b/tests/test_nkt_superk.py index 06c3b866a..25ca197bc 100644 --- a/tests/test_nkt_superk.py +++ b/tests/test_nkt_superk.py @@ -86,7 +86,7 @@ def set_emission(self, emission): pass # Test hardware service detection - hw_service = TestNktSuperk('nkt_superk_evo') + hw_service = TestNktSuperk('nkt_superk_evo_hardware') self.assertFalse(hw_service.is_simulation) # Test simulation service detection @@ -101,31 +101,31 @@ def test_concrete_implementation_structure(self): 'NKTP_DLL': Mock() }): with patch('catkit2.base_services.nkt_superk.Service'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU16'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU16'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU8'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU8'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadS16'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.openPorts'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.closePorts'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.RegisterResultTypes'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU16'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU16'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU8'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU8'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadS16'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.openPorts'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.closePorts'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.RegisterResultTypes'): + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerReadU16'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerReadU8'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerWriteU8'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerReadS16'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.openPorts'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.closePorts'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.RegisterResultTypes'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.registerReadU16'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.registerReadU8'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.registerWriteU8'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.registerReadS16'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.openPorts'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.closePorts'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.RegisterResultTypes'): from catkit2.base_services.nkt_superk import NktSuperk - from catkit2.services.nkt_superk_evo.nkt_superk_evo import NktSuperkEvo - from catkit2.services.nkt_superk_fianium.nkt_superk_fianium import NktSuperkFianium + from catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware import NktSuperkEvoHardware + from catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware import NktSuperkFianiumHardware from catkit2.services.nkt_superk_evo_sim.nkt_superk_evo_sim import NktSuperkEvoSim from catkit2.services.nkt_superk_fianium_sim.nkt_superk_fianium_sim import NktSuperkFianiumSim # Test inheritance for all services - all_services = [NktSuperkEvo, NktSuperkFianium, NktSuperkEvoSim, NktSuperkFianiumSim] + all_services = [NktSuperkEvoHardware, NktSuperkFianiumHardware, NktSuperkEvoSim, NktSuperkFianiumSim] for service_class in all_services: self.assertTrue(issubclass(service_class, NktSuperk)) @@ -157,7 +157,7 @@ def test_concrete_implementation_structure(self): 'get_lwp_setpoint', 'get_varia_status_bits', 'check_result' ] - for service_class in [NktSuperkEvo, NktSuperkFianium]: + for service_class in [NktSuperkEvoHardware, NktSuperkFianiumHardware]: for method in hardware_methods: self.assertTrue(hasattr(service_class, method)) @@ -169,20 +169,20 @@ def test_evo_specific_methods(self): }): with patch('catkit2.base_services.nkt_superk.Service'): # Mock necessary register functions - with patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU16'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU16'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadU8'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerWriteU8'), \ - patch('catkit2.services.nkt_superk_evo.nkt_superk_evo.registerReadS16'): + with patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerReadU16'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerReadU8'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerWriteU8'), \ + patch('catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware.registerReadS16'): - from catkit2.services.nkt_superk_evo.nkt_superk_evo import NktSuperkEvo + from catkit2.services.nkt_superk_evo_hardware.nkt_superk_evo_hardware import NktSuperkEvoHardware from catkit2.services.nkt_superk_evo_sim.nkt_superk_evo_sim import NktSuperkEvoSim # Test EVO-specific methods for both hardware and simulation evo_methods = ['update_evo_status', 'set_power_setpoint', 'set_current_setpoint'] for method in evo_methods: - self.assertTrue(hasattr(NktSuperkEvo, method)) + self.assertTrue(hasattr(NktSuperkEvoHardware, method)) self.assertTrue(hasattr(NktSuperkEvoSim, method)) def test_fianium_specific_methods(self): @@ -193,19 +193,19 @@ def test_fianium_specific_methods(self): }): with patch('catkit2.base_services.nkt_superk.Service'): # Mock necessary register functions - with patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU16'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU16'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerReadU8'), \ - patch('catkit2.services.nkt_superk_fianium.nkt_superk_fianium.registerWriteU8'): + with patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.registerReadU16'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.registerWriteU16'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.registerReadU8'), \ + patch('catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware.registerWriteU8'): - from catkit2.services.nkt_superk_fianium.nkt_superk_fianium import NktSuperkFianium + from catkit2.services.nkt_superk_fianium_hardware.nkt_superk_fianium_hardware import NktSuperkFianiumHardware from catkit2.services.nkt_superk_fianium_sim.nkt_superk_fianium_sim import NktSuperkFianiumSim # Test FIANIUM-specific methods for both hardware and simulation fianium_methods = ['set_power_setpoint', 'set_pulse_picker_ratio', 'monitor_pulse_picker_ratio'] for method in fianium_methods: - self.assertTrue(hasattr(NktSuperkFianium, method)) + self.assertTrue(hasattr(NktSuperkFianiumHardware, method)) self.assertTrue(hasattr(NktSuperkFianiumSim, method)) From 4ab9b2009bc5ad5fa9a6b0a0f10a02d7c0d40a53 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Tue, 2 Sep 2025 14:38:42 +0200 Subject: [PATCH 09/12] Move power_setpoint to common_funcs in base base class --- catkit2/base_services/nkt_superk.py | 1 + catkit2/base_services/nkt_superk_evo.py | 1 - catkit2/base_services/nkt_superk_fianium.py | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/catkit2/base_services/nkt_superk.py b/catkit2/base_services/nkt_superk.py index 5ecdb0ed6..633d34bd2 100644 --- a/catkit2/base_services/nkt_superk.py +++ b/catkit2/base_services/nkt_superk.py @@ -130,6 +130,7 @@ def open(self): 'swp_setpoint': self.monitor_func(self.swp_setpoint, self.set_swp_setpoint), 'lwp_setpoint': self.monitor_func(self.lwp_setpoint, self.set_lwp_setpoint), 'emission': self.monitor_func(self.emission, self.set_emission), + 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), 'varia_status': self.update_func(self.update_varia_status) } diff --git a/catkit2/base_services/nkt_superk_evo.py b/catkit2/base_services/nkt_superk_evo.py index 8f2e2292c..8786dfffe 100644 --- a/catkit2/base_services/nkt_superk_evo.py +++ b/catkit2/base_services/nkt_superk_evo.py @@ -27,7 +27,6 @@ def _create_device_specific_streams(self): def _get_device_specific_funcs(self): """Get EVO-specific thread functions.""" return { - 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), 'current_setpoint': self.monitor_func(self.current_setpoint, self.set_current_setpoint), 'evo_status': self.update_func(self.update_evo_status) } diff --git a/catkit2/base_services/nkt_superk_fianium.py b/catkit2/base_services/nkt_superk_fianium.py index acd6929ea..dc5b94cc4 100644 --- a/catkit2/base_services/nkt_superk_fianium.py +++ b/catkit2/base_services/nkt_superk_fianium.py @@ -25,7 +25,6 @@ def _create_device_specific_streams(self): def _get_device_specific_funcs(self): """Get FIANIUM-specific thread functions.""" return { - 'power_setpoint': self.monitor_func(self.power_setpoint, self.set_power_setpoint), 'pulse_picker_ratio': self.monitor_pulse_picker_ratio(self.pulse_picker_ratio, self.set_pulse_picker_ratio) } From aae6a7c212ca6a8017dc6908cafedc26e58aed1b Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Tue, 2 Sep 2025 14:51:38 +0200 Subject: [PATCH 10/12] Remove unused import --- .../nkt_superk_fianium_hardware/nkt_superk_fianium_hardware.py | 1 - 1 file changed, 1 deletion(-) diff --git a/catkit2/services/nkt_superk_fianium_hardware/nkt_superk_fianium_hardware.py b/catkit2/services/nkt_superk_fianium_hardware/nkt_superk_fianium_hardware.py index 1f3f81564..c842d21db 100644 --- a/catkit2/services/nkt_superk_fianium_hardware/nkt_superk_fianium_hardware.py +++ b/catkit2/services/nkt_superk_fianium_hardware/nkt_superk_fianium_hardware.py @@ -1,7 +1,6 @@ from catkit2.base_services.nkt_superk import read_register, write_register from catkit2.base_services.nkt_superk_fianium import NktSuperkFianium -import numpy as np from enum import Enum try: From 0b3bec36f6035b7e03e78692383019463ec7c660 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Tue, 2 Sep 2025 14:56:28 +0200 Subject: [PATCH 11/12] Resolve hardware-specific functions --- catkit2/base_services/nkt_superk.py | 62 ++++++++++++----------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/catkit2/base_services/nkt_superk.py b/catkit2/base_services/nkt_superk.py index 633d34bd2..36f489597 100644 --- a/catkit2/base_services/nkt_superk.py +++ b/catkit2/base_services/nkt_superk.py @@ -250,7 +250,15 @@ def set_nd_setpoint(self, nd_setpoint): new_filter_position=nd_setpoint ) else: - self._set_nd_setpoint_hardware(nd_setpoint) + device_id = Varia.DEVICE_ID + register_value = int(nd_setpoint / 0.1) + + self.log.debug(f'Writing value {register_value} to {Varia.REG_ND_SETPOINT}.') + + future = self.pool.submit(registerWriteU16, self.port, device_id.value, + Varia.REG_ND_SETPOINT.value, register_value, -1) + result = future.result() + self.check_result(result) def set_swp_setpoint(self, swp_setpoint): """Set SWP filter setpoint.""" @@ -260,7 +268,15 @@ def set_swp_setpoint(self, swp_setpoint): new_filter_position=swp_setpoint ) else: - self._set_swp_setpoint_hardware(swp_setpoint) + device_id = Varia.DEVICE_ID + register_value = int(swp_setpoint / 0.1) + + self.log.debug(f'Writing value {register_value} to {Varia.REG_SWP_SETPOINT}.') + + future = self.pool.submit(registerWriteU16, self.port, device_id.value, + Varia.REG_SWP_SETPOINT.value, register_value, -1) + result = future.result() + self.check_result(result) def set_lwp_setpoint(self, lwp_setpoint): """Set LWP filter setpoint.""" @@ -270,44 +286,16 @@ def set_lwp_setpoint(self, lwp_setpoint): new_filter_position=lwp_setpoint ) else: - self._set_lwp_setpoint_hardware(lwp_setpoint) - - # Hardware register operations (only used by hardware services) - def _set_nd_setpoint_hardware(self, nd_setpoint): - """Set ND filter setpoint using hardware register.""" - device_id = Varia.DEVICE_ID - register_value = int(nd_setpoint / 0.1) + device_id = Varia.DEVICE_ID + register_value = int(lwp_setpoint / 0.1) - self.log.debug(f'Writing value {register_value} to {Varia.REG_ND_SETPOINT}.') + self.log.debug(f'Writing value {register_value} to {Varia.REG_LWP_SETPOINT}.') - future = self.pool.submit(registerWriteU16, self.port, device_id.value, - Varia.REG_ND_SETPOINT.value, register_value, -1) - result = future.result() - self.check_result(result) - - def _set_swp_setpoint_hardware(self, swp_setpoint): - """Set SWP filter setpoint using hardware register.""" - device_id = Varia.DEVICE_ID - register_value = int(swp_setpoint / 0.1) + future = self.pool.submit(registerWriteU16, self.port, device_id.value, + Varia.REG_LWP_SETPOINT.value, register_value, -1) + result = future.result() + self.check_result(result) - self.log.debug(f'Writing value {register_value} to {Varia.REG_SWP_SETPOINT}.') - - future = self.pool.submit(registerWriteU16, self.port, device_id.value, - Varia.REG_SWP_SETPOINT.value, register_value, -1) - result = future.result() - self.check_result(result) - - def _set_lwp_setpoint_hardware(self, lwp_setpoint): - """Set LWP filter setpoint using hardware register.""" - device_id = Varia.DEVICE_ID - register_value = int(lwp_setpoint / 0.1) - - self.log.debug(f'Writing value {register_value} to {Varia.REG_LWP_SETPOINT}.') - - future = self.pool.submit(registerWriteU16, self.port, device_id.value, - Varia.REG_LWP_SETPOINT.value, register_value, -1) - result = future.result() - self.check_result(result) # Hardware register read operations (only defined for hardware services) def get_monitor_input(self): From c3affd66810f536cc0e481f34cf0e656f236d816 Mon Sep 17 00:00:00 2001 From: Iva Laginja Date: Tue, 2 Sep 2025 15:03:48 +0200 Subject: [PATCH 12/12] Update docs --- docs/services/nkt_superk.rst | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/services/nkt_superk.rst b/docs/services/nkt_superk.rst index 6803c6e06..65cb18934 100644 --- a/docs/services/nkt_superk.rst +++ b/docs/services/nkt_superk.rst @@ -21,7 +21,7 @@ Configuration .. code-block:: YAML nkt_superk: - service_type: nkt_superk_evo + service_type: nkt_superk_evo_hardware simulated_service_type: nkt_superk_evo_sim interface: nkt_superk_evo requires_safety: false @@ -41,7 +41,7 @@ Configuration .. code-block:: YAML nkt_superk: - service_type: nkt_superk_fianium + service_type: nkt_superk_fianium_hardware simulated_service_type: nkt_superk_fianium_sim interface: nkt_superk_fianium requires_safety: false @@ -87,6 +87,8 @@ Datastreams ``lwp_filter_moving``: Whether the long wavelength (low-pass) filter is moving for the VARIA. +``power_setpoint``: Output emission power level of the EVO/FIANIUM (in percent). + **EVO-Specific Datastreams:** ``base_temperature``: Base temperature output by the EVO (Celsius). @@ -97,14 +99,10 @@ Datastreams ``emission``: Output emission of the EVO (int) - 0 is OFF, 1 is ON. -``power_setpoint``: Output emission power level of the EVO (in percent). - ``current_setpoint``: Output current level of the EVO (in percent). **FIANIUM-Specific Datastreams:** ``emission``: Output emission of the FIANIUM (int) - 0 is OFF, 3 is ON. -``power_setpoint``: Output emission power level of the FIANIUM (in percent). - ``pulse_picker_ratio``: Pulse picker ratio for the FIANIUM. \ No newline at end of file