├── catkit2 ├── tui │ └── __init__.py ├── testbed │ ├── proxies │ │ ├── ni_daq.py │ │ ├── thorlabs_mcls1.py │ │ ├── web_power_switch.py │ │ ├── oceanoptics_spectrometer.py │ │ ├── __init__.py │ │ ├── thorlabs_cube_motor_kinesis.py │ │ ├── optical_fiber_switch.py │ │ ├── flip_mount.py │ │ └── camera.py │ ├── __init__.py │ ├── testbed_proxy.py │ └── service.py ├── simulator │ └── __init__.py ├── proto │ └── __init__.py ├── services │ ├── empty_service │ │ └── empty_service.py │ ├── bmc_deformable_mirror_sim │ │ └── bmc_deformable_mirror_sim.py │ ├── omega_ithx_w3_sim │ │ └── omega_ithx_w3_sim.py │ ├── thorlabs_pm_sim │ │ └── thorlabs_pm_sim.py │ ├── safety_manual_check │ │ └── safety_manual_check.py │ ├── snmp_ups_sim │ │ └── snmp_ups_sim.py │ ├── thorlabs_mff101_sim │ │ └── thorlabs_mff101_sim.py │ ├── simple_simulator │ │ └── simple_simulator.py │ ├── web_power_switch_sim │ │ └── web_power_switch_sim.py │ ├── thorlabs_tsp01_sim │ │ └── thorlabs_tsp01_sim.py │ └── thorlabs_cld101x_sim │ │ └── thorlabs_cld101x_sim.py ├── version.py ├── __init__.py ├── CMakeLists.txt └── config.py ├── cookiecutter-testbed ├── .python-version ├── {{cookiecutter.pypi_package_name}} │ ├── tests │ │ ├── __init__.py │ │ └── test_{{cookiecutter.project_slug}}.py │ ├── HISTORY.md │ ├── src │ │ └── {{cookiecutter.project_slug}} │ │ │ ├── utils.py │ │ │ ├── config │ │ │ ├── simulator.yml │ │ │ ├── services.yml │ │ │ ├── testbed.yml │ │ │ └── __init__.py │ │ │ ├── __init__.py │ │ │ ├── services │ │ │ └── {{cookiecutter.project_slug}}_simulator │ │ │ │ └── {{cookiecutter.project_slug}}_simulator.py │ │ │ ├── {{cookiecutter.project_slug}}_optical_model.py │ │ │ └── cli.py │ ├── docs │ │ ├── usage.md │ │ ├── index.md │ │ └── installation.md │ ├── MANIFEST.in │ ├── .github │ │ ├── ISSUE_TEMPLATE.md │ │ └── workflows │ │ │ └── test.yml │ ├── .editorconfig │ ├── README.md │ ├── LICENSE │ └── pyproject.toml ├── hooks │ ├── post_gen_project.py │ └── pre_gen_project.py ├── cookiecutter.json ├── .readthedocs.yaml ├── .editorconfig └── pyproject.toml ├── pytest.ini ├── docs ├── _static │ └── dummy.txt ├── catkit_core.rst ├── services_flowchart.png ├── services_flowchart.pptx ├── benchmarks.rst ├── services │ ├── empty_service.rst │ ├── thorlabs_pm.rst │ ├── omega_ithx_w3.rst │ ├── optical_fiber_switch.rst │ ├── safety_manual_check.rst │ ├── snmp_ups.rst │ ├── thorlabs_fw102c.rst │ ├── thorlabs_tsp01.rst │ ├── thorlabs_mff101.rst │ ├── thorlabs_cld101x.rst │ ├── ni_daq.rst │ ├── oceanoptics_spectrometer.rst │ ├── web_power_switch.rst │ ├── safety_monitor.rst │ ├── newport_picomotor.rst │ ├── camera_sim.rst │ ├── thorlabs_cube_motor_kinesis.rst │ ├── physik_stage_controller.rst │ ├── bmc_deformable_mirror.rst │ ├── nkt_superk_fianium.rst │ ├── phasics_cam.rst │ └── deformable_mirror.rst ├── catkit2.rst ├── Makefile ├── testbed_implementation.rst ├── index.rst ├── acknowledging_catkit2.rst └── conf.py ├── tests ├── data │ └── dm_mask.fits ├── test_process_stats.py ├── test_uuid.py ├── config │ ├── testbed.yml │ └── services.yml ├── test_optical_model.py ├── services │ ├── dummy_dm_service │ │ └── dummy_dm_service.py │ └── dummy_service │ │ └── dummy_service.py ├── test_service.py ├── conftest.py ├── test_dm_commands.py ├── test_event.py ├── test_allocator.py ├── test_shared_memory.py ├── test_datastream.py └── test_server_client.py ├── catkit_core ├── cmake │ └── CatkitCoreConfig.cmake.in ├── HostName.h ├── ServiceState.cpp ├── ComplexTraits.h ├── LogFile.h ├── Finally.h ├── StructStream.cpp ├── LogConsole.h ├── Timing.h ├── ServiceState.h ├── Command.cpp ├── LoggingProxy.h ├── Uuid.h ├── Command.h ├── Memory.h ├── StructStream.h ├── FitsFile.h ├── LogFile.cpp ├── StructStream.inl ├── ProcessStats.h ├── CudaSharedMemory.h ├── Util.h ├── LogForwarder.h ├── ConcurrentVector.h ├── Client.h ├── Server.h ├── ArrayView.h ├── LocalMemory.h ├── Timing.cpp ├── DeformableMirrorService.h ├── ArrayView.cpp ├── PoolAllocator.h ├── Types.h ├── Util.cpp ├── Shareable.cpp ├── HostName.cpp ├── RefCounter.h ├── BuddyAllocator.h ├── Log.cpp ├── Event.h ├── Uuid.cpp ├── EventBase.h ├── LogConsole.cpp ├── EventSpinLock.inl ├── HashMap.h ├── Shareable.h ├── EventBase.inl ├── Util.inl ├── FitsFile.cpp ├── ServiceProxy.h ├── SharedMemory.h ├── LocalMemory.cpp ├── HybridPoolAllocator.h └── Log.h ├── .gitignore ├── benchmarks ├── timestamp.cpp ├── uuid_generator.cpp ├── latency_histogram.py ├── message_broker.cpp ├── pool_allocator.cpp ├── datastream_submit.cpp └── hash_map.cpp ├── .flake8 ├── .github ├── workflows │ ├── linting.yml │ ├── testing.yml │ └── docs.yml └── flake8_problem_matcher.json ├── proto ├── core.proto ├── service.proto ├── tracing.proto └── logging.proto ├── CMakeLists.txt ├── environment.yml ├── LICENSE.rst └── README.md /catkit2/tui/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cookiecutter-testbed/.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | -------------------------------------------------------------------------------- /docs/_static/dummy.txt: -------------------------------------------------------------------------------- 1 | This file exists to suppress a Sphinx warning about a missing directory. -------------------------------------------------------------------------------- /tests/data/dm_mask.fits: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacetelescope/catkit2/develop/tests/data/dm_mask.fits -------------------------------------------------------------------------------- /docs/catkit_core.rst: -------------------------------------------------------------------------------- 1 | catkit_core 2 | =========== 3 | 4 | .. doxygenindex:: 5 | :project: catkit_core 6 | -------------------------------------------------------------------------------- /docs/services_flowchart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacetelescope/catkit2/develop/docs/services_flowchart.png -------------------------------------------------------------------------------- /docs/services_flowchart.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spacetelescope/catkit2/develop/docs/services_flowchart.pptx -------------------------------------------------------------------------------- /catkit_core/cmake/CatkitCoreConfig.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | include("${CMAKE_CURRENT_LIST_DIR}/CatkitCoreTargets.cmake") 3 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit test package for {{ cookiecutter.project_slug }}.""" 2 | -------------------------------------------------------------------------------- /catkit_core/HostName.h: -------------------------------------------------------------------------------- 1 | #ifndef HOSTNAME_H 2 | #define HOSTNAME_H 3 | 4 | #include 5 | 6 | std::string_view GetHostName(); 7 | 8 | #endif // HOSTNAME_H 9 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | ## {{ cookiecutter.first_version }} ({% now 'local' %}) 4 | 5 | * First release on PyPI. 6 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/src/{{cookiecutter.project_slug}}/utils.py: -------------------------------------------------------------------------------- 1 | def do_something_useful(): 2 | print("Replace this with a utility function") 3 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | To use {{ cookiecutter.project_name }} in a project: 4 | 5 | ```python 6 | import {{ cookiecutter.project_slug }} 7 | ``` 8 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/src/{{cookiecutter.project_slug}}/config/simulator.yml: -------------------------------------------------------------------------------- 1 | pupil_mask: 2 | diameter: 1 3 | grid_size: 1.1 4 | dimensions: 400 5 | 6 | detector: 7 | pixel_size: .1 8 | roi: 400 9 | -------------------------------------------------------------------------------- /catkit2/testbed/proxies/ni_daq.py: -------------------------------------------------------------------------------- 1 | from ..service_proxy import ServiceProxy 2 | 3 | 4 | class NiDaqProxy(ServiceProxy): 5 | def apply_voltage(self, channel, voltage, timeout=None): 6 | getattr(self, channel).submit_data(voltage) 7 | -------------------------------------------------------------------------------- /catkit_core/ServiceState.cpp: -------------------------------------------------------------------------------- 1 | #include "ServiceState.h" 2 | 3 | bool IsAliveState(const ServiceState &state) 4 | { 5 | return state == ServiceState::INITIALIZING 6 | || state == ServiceState::OPENING 7 | || state == ServiceState::RUNNING; 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_process_stats.py: -------------------------------------------------------------------------------- 1 | from catkit2.catkit_bindings import ProcessStats 2 | 3 | 4 | def test_process_stats(): 5 | stats = ProcessStats() 6 | stats.update() 7 | 8 | assert stats.memory_usage > 0 9 | assert stats.cpu_usage >= 0 10 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/src/{{cookiecutter.project_slug}}/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level package for {{ cookiecutter.project_name }}.""" 2 | 3 | __author__ = """{{ cookiecutter.full_name }}""" 4 | __email__ = '{{ cookiecutter.email }}' 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | *.egg-info/ 3 | build/ 4 | *.so 5 | *.pyd 6 | *.pyc 7 | __pycache__/ 8 | *CMakeFiles 9 | *CMakeCache.txt 10 | extern/*/ 11 | *_pb2.py 12 | *.pb.cc 13 | *.pb.h 14 | docs/doxygen/xml/ 15 | docs/_build 16 | docs/api 17 | .idea/ 18 | .vscode 19 | -------------------------------------------------------------------------------- /catkit2/simulator/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'OpticalModel', 3 | 'property_with_logic', 4 | 'with_cached_result', 5 | 'Simulator', 6 | 'SimpleOpticalModel' 7 | ] 8 | 9 | from .optical_model import * 10 | from .simulator import * 11 | from .simple_optical_model import * 12 | -------------------------------------------------------------------------------- /catkit_core/ComplexTraits.h: -------------------------------------------------------------------------------- 1 | #ifndef COMPLEXTRAITS_H 2 | #define COMPLEXTRAITS_H 3 | 4 | #include 5 | #include 6 | 7 | template struct is_complex : std::false_type {}; 8 | template struct is_complex> : std::true_type {}; 9 | 10 | #endif // COMPLEXTRAITS_H 11 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTING.md 2 | include HISTORY.md 3 | include LICENSE 4 | include README.md 5 | 6 | recursive-include tests * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | 10 | recursive-include docs *.md Makefile *.jpg *.png *.gif 11 | -------------------------------------------------------------------------------- /catkit_core/LogFile.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGFILE_H 2 | #define LOGFILE_H 3 | 4 | #include "Log.h" 5 | 6 | #include 7 | 8 | class LogFile : public LogListener 9 | { 10 | public: 11 | LogFile(const char *filename); 12 | 13 | void AddLogEntry(const LogEntry &entry); 14 | 15 | private: 16 | std::ofstream m_File; 17 | }; 18 | 19 | #endif // LOGFILE_H -------------------------------------------------------------------------------- /catkit2/proto/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # This is HORRIBLE. Protoc produces files with absolute imports. 5 | # This is apparently intended behaviour. I do not understand how 6 | # protoc is supposed to work with multiple languages. This is 7 | # a simple but horrible solution to absolute imports. 8 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) 9 | -------------------------------------------------------------------------------- /catkit_core/Finally.h: -------------------------------------------------------------------------------- 1 | #ifndef FINALLY_H 2 | #define FINALLY_H 3 | 4 | #include 5 | 6 | class Finally 7 | { 8 | public: 9 | inline Finally(std::function func) 10 | : m_Func(func) 11 | { 12 | } 13 | 14 | inline ~Finally() 15 | { 16 | m_Func(); 17 | } 18 | 19 | private: 20 | std::function m_Func; 21 | }; 22 | 23 | #endif // FINALLY_H 24 | -------------------------------------------------------------------------------- /catkit_core/StructStream.cpp: -------------------------------------------------------------------------------- 1 | #include "StructStream.h" 2 | 3 | StructStream::StructStream(std::shared_ptr buffer, std::size_t offset) 4 | : m_Buffer(buffer), m_Offset(offset) 5 | { 6 | } 7 | 8 | std::size_t StructStream::GetOffset() 9 | { 10 | return m_Offset; 11 | } 12 | 13 | std::shared_ptr StructStream::GetBuffer() 14 | { 15 | return m_Buffer; 16 | } 17 | -------------------------------------------------------------------------------- /cookiecutter-testbed/hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | if __name__ == "__main__": 4 | print("Your Python package project has been created successfully!") 5 | print("Note: Your Python package will be generated with defaults " 6 | "(including licenses!). Make sure to check all files before making " 7 | "your package publicly available.") 8 | -------------------------------------------------------------------------------- /catkit_core/LogConsole.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGCONSOLE_H 2 | #define LOGCONSOLE_H 3 | 4 | #include "Log.h" 5 | 6 | class LogConsole : public LogListener 7 | { 8 | public: 9 | LogConsole(bool use_color = true, bool print_context = true); 10 | 11 | void AddLogEntry(const LogEntry &entry); 12 | 13 | private: 14 | bool m_UseColor; 15 | bool m_PrintContext; 16 | }; 17 | 18 | #endif // LOGCONSOLE_H -------------------------------------------------------------------------------- /catkit_core/Timing.h: -------------------------------------------------------------------------------- 1 | #ifndef TIME_H 2 | #define TIME_H 3 | 4 | #include 5 | #include 6 | 7 | uint64_t GetTimeStamp(); 8 | std::string ConvertTimestampToString(uint64_t timestamp); 9 | 10 | class Timer 11 | { 12 | public: 13 | Timer(); 14 | 15 | double GetTime(); 16 | 17 | private: 18 | std::chrono::steady_clock::time_point m_StartTime; 19 | }; 20 | 21 | #endif // TIME_H 22 | -------------------------------------------------------------------------------- /docs/benchmarks.rst: -------------------------------------------------------------------------------- 1 | Benchmarks 2 | ========== 3 | 4 | .. _benchmarks_data_streams: 5 | 6 | Data streams 7 | ------------ 8 | 9 | Framerate 10 | ~~~~~~~~~ 11 | 12 | Latency 13 | ~~~~~~~ 14 | 15 | Server communication 16 | -------------------- 17 | 18 | Getting the full configuration 19 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 20 | 21 | Sending requests to a service 22 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 23 | -------------------------------------------------------------------------------- /catkit_core/ServiceState.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVICE_STATE_H 2 | #define SERVICE_STATE_H 3 | 4 | #include "testbed.pb.h" 5 | 6 | enum ServiceState 7 | { 8 | CLOSED = 0, 9 | INITIALIZING = 1, 10 | OPENING = 2, 11 | RUNNING = 3, 12 | CLOSING = 4, 13 | UNRESPONSIVE = 5, 14 | CRASHED = 6, 15 | FAIL_SAFE = 7 16 | }; 17 | 18 | bool IsAliveState(const ServiceState &state); 19 | 20 | #endif // SERVICE_STATE_H 21 | -------------------------------------------------------------------------------- /catkit_core/Command.cpp: -------------------------------------------------------------------------------- 1 | #include "Command.h" 2 | 3 | #include 4 | 5 | Command::Command(std::string name, CommandFunction command) 6 | : m_Name(name), m_CommandFunction(command) 7 | { 8 | } 9 | 10 | Command::~Command() 11 | { 12 | } 13 | 14 | Value Command::Execute(const Dict &arguments) 15 | { 16 | return m_CommandFunction(arguments); 17 | } 18 | 19 | std::string Command::GetName() 20 | { 21 | return m_Name; 22 | } 23 | -------------------------------------------------------------------------------- /catkit2/testbed/proxies/thorlabs_mcls1.py: -------------------------------------------------------------------------------- 1 | from ..service_proxy import ServiceProxy 2 | 3 | 4 | class ThorlabsMcls1(ServiceProxy): 5 | @property 6 | def channel(self): 7 | return self.config['channel'] 8 | 9 | @property 10 | def center_wavelength(self): 11 | return self.config['channels'][str(self.channel)] 12 | 13 | @property 14 | def bandwidth(self): 15 | return self.config['bandwidth'] 16 | -------------------------------------------------------------------------------- /catkit_core/LoggingProxy.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGGING_PROXY_H 2 | #define LOGGING_PROXY_H 3 | 4 | #include "Log.h" 5 | #include "TestbedProxy.h" 6 | 7 | #include 8 | 9 | class LoggingProxy 10 | { 11 | public: 12 | LoggingProxy(std::shared_ptr testbed); 13 | 14 | LogEntry GetNextEntry(double wait_time_in_seconds); 15 | 16 | private: 17 | std::string m_Host; 18 | int m_Port; 19 | }; 20 | 21 | #endif // LOGGING_PROXY_H 22 | -------------------------------------------------------------------------------- /catkit_core/Uuid.h: -------------------------------------------------------------------------------- 1 | #ifndef UUID_GENERATOR_H 2 | #define UUID_GENERATOR_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | struct Uuid 9 | { 10 | std::array data; 11 | 12 | static void Generate(Uuid *uuid); 13 | 14 | bool operator==(const Uuid &other) const; 15 | bool operator!=(const Uuid &other) const; 16 | 17 | std::string to_string() const; 18 | }; 19 | 20 | #endif // UUID_GENERATOR_H 21 | -------------------------------------------------------------------------------- /cookiecutter-testbed/hooks/pre_gen_project.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | 4 | MODULE_REGEX = r"^[_a-zA-Z][_a-zA-Z0-9]+$" 5 | 6 | module_name = "{{ cookiecutter.project_slug}}" 7 | 8 | if not re.match(MODULE_REGEX, module_name): 9 | print( 10 | "ERROR: The project slug (%s) is not a valid Python module name. " 11 | "Please do not use a - and use _ instead" % module_name 12 | ) 13 | # Exit to cancel project 14 | sys.exit(1) 15 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to {{ cookiecutter.project_name }}'s documentation! 2 | 3 | ## Contents 4 | 5 | - [Readme](readme.md) 6 | - [Installation](installation.md) 7 | - [Usage](usage.md) 8 | - [Modules](modules.md) 9 | - [Contributing](contributing.md) 10 | - [History](history.md) 11 | 12 | ## Indices and tables 13 | 14 | - [Index](genindex) 15 | - [Module Index](modindex) 16 | - [Search](search) 17 | -------------------------------------------------------------------------------- /catkit2/services/empty_service/empty_service.py: -------------------------------------------------------------------------------- 1 | from catkit2.testbed.service import Service 2 | 3 | 4 | class EmptyService(Service): 5 | def __init__(self): 6 | super().__init__('empty_service') 7 | 8 | def main(self): 9 | # Just wait until we're being shut down. 10 | while not self.should_shut_down: 11 | self.sleep(1) 12 | 13 | 14 | if __name__ == '__main__': 15 | service = EmptyService() 16 | service.run() 17 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * {{ cookiecutter.project_name }} version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /benchmarks/timestamp.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "Timing.h" 4 | 5 | const size_t NUM_ITERATIONS = 100000000; 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | auto start = GetTimeStamp(); 10 | 11 | for (size_t i = 0; i < NUM_ITERATIONS; ++i) 12 | { 13 | GetTimeStamp(); 14 | } 15 | 16 | auto end = GetTimeStamp(); 17 | 18 | std::cout << double(end - start) / NUM_ITERATIONS << " ns per timestamp" << std::endl; 19 | 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /tests/test_uuid.py: -------------------------------------------------------------------------------- 1 | from catkit2.catkit_bindings import Uuid 2 | import uuid 3 | 4 | def test_uuid_generation(): 5 | # Generate a UUID using the catkit2 library 6 | catkit_uuid = Uuid.generate() 7 | 8 | # Validate if the generated UUID is a valid UUID 9 | uuid_obj = uuid.UUID(str(catkit_uuid)) 10 | assert uuid_obj.version == 4, "Generated UUID is not a valid UUID4" 11 | assert str(catkit_uuid) == str(uuid_obj), "Generated UUID does not match the expected format" 12 | -------------------------------------------------------------------------------- /tests/config/testbed.yml: -------------------------------------------------------------------------------- 1 | default_port: 6346 2 | safety: 3 | service_id: safety 4 | check_interval: 60 5 | safe_interval: 180 6 | 7 | base_data_path: 8 | default: !path "~/temp_data" 9 | support_data_path: 10 | default: !path "~/temp_support" 11 | 12 | base_experiment_path: "{simulator_or_hardware}/{date_and_time}_{experiment_name}/" 13 | sub_experiment_path: "{experiment_id:03d}_{experiment_name}/" 14 | 15 | long_term_monitoring_path: "{simulator_or_hardware}/long_term_monitoring/" 16 | -------------------------------------------------------------------------------- /tests/config/services.yml: -------------------------------------------------------------------------------- 1 | dummy_dm_service: 2 | service_type: dummy_dm_service 3 | requires_safety: false 4 | interface: deformable_mirror 5 | 6 | device_actuator_mask_fname: !path ../data/dm_mask.fits 7 | num_actuators_all_dms: 1904 8 | 9 | dummy_service: 10 | service_type: dummy_service 11 | requires_safety: false 12 | 13 | readonly_property: 5 14 | 15 | safety: 16 | service_type: safety_monitor 17 | requires_safety: false 18 | 19 | check_interval: 5 20 | 21 | safeties: [] 22 | -------------------------------------------------------------------------------- /catkit2/version.py: -------------------------------------------------------------------------------- 1 | def get_version(): 2 | '''Return the version of this package. 3 | 4 | Returns 5 | ------- 6 | string 7 | The version of the catkit2 package. 8 | ''' 9 | if get_version._version is None: 10 | from pkg_resources import get_distribution, DistributionNotFound 11 | 12 | try: 13 | get_version._version = get_distribution('catkit2').version 14 | except DistributionNotFound: 15 | # package is not installed 16 | pass 17 | 18 | return get_version._version 19 | 20 | get_version._version = None 21 | -------------------------------------------------------------------------------- /catkit_core/Command.h: -------------------------------------------------------------------------------- 1 | #ifndef COMMAND_H 2 | #define COMMAND_H 3 | 4 | #include "Types.h" 5 | 6 | #include 7 | #include 8 | 9 | class Command 10 | { 11 | public: 12 | typedef std::function CommandFunction; 13 | 14 | Command(std::string name, CommandFunction command); 15 | ~Command(); 16 | 17 | Value Execute(const Dict &arguments); 18 | std::string GetName(); 19 | 20 | private: 21 | std::string m_Name; 22 | CommandFunction m_CommandFunction; 23 | }; 24 | 25 | #endif // COMMAND_H 26 | -------------------------------------------------------------------------------- /catkit_core/Memory.h: -------------------------------------------------------------------------------- 1 | #ifndef MEMORY_H 2 | #define MEMORY_H 3 | 4 | #include 5 | 6 | class StructStream; 7 | 8 | enum class MemoryType 9 | { 10 | SharedMemory, 11 | LocalMemory 12 | }; 13 | 14 | class Memory 15 | { 16 | public: 17 | virtual ~Memory() = default; 18 | 19 | virtual void *GetAddress(std::size_t offset = 0) = 0; 20 | virtual std::size_t GetCapacity() const = 0; 21 | 22 | virtual MemoryType GetMemoryType() const = 0; 23 | 24 | virtual void WriteReference(StructStream *stream) = 0; 25 | }; 26 | 27 | #endif // MEMORY_H 28 | -------------------------------------------------------------------------------- /docs/services/empty_service.rst: -------------------------------------------------------------------------------- 1 | Empty Service 2 | ============= 3 | 4 | This service doesn't do anything, but is useful when you want to create a proxy-only service. 5 | 6 | Configuration 7 | ------------- 8 | 9 | .. code-block:: YAML 10 | 11 | light_source: 12 | service_type: empty_service 13 | interface: my_proxy 14 | requires_safety: false 15 | 16 | source_type: thorlabs_diode_group 17 | 18 | Properties 19 | ---------- 20 | None. 21 | 22 | Commands 23 | -------- 24 | None. 25 | 26 | Datastreams 27 | ----------- 28 | None. 29 | -------------------------------------------------------------------------------- /catkit2/testbed/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'Testbed', 3 | 'Service', 4 | 'parse_service_args', 5 | 'CatkitLogHandler', 6 | 'TestbedProxy', 7 | 'ServiceProxy', 8 | 'Experiment', 9 | 'TraceWriter', 10 | 'trace_interval', 11 | 'trace_instant', 12 | 'trace_counter', 13 | 'ZmqDistributor', 14 | ] 15 | 16 | from .testbed import * 17 | from .experiment import * 18 | from .service import * 19 | from .logging import * 20 | from .tracing import * 21 | from .distributor import * 22 | from .testbed_proxy import * 23 | from .service_proxy import * 24 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/src/{{cookiecutter.project_slug}}/config/services.yml: -------------------------------------------------------------------------------- 1 | detector: 2 | service_type: allied_vision_camera 3 | simulated_service_type: camera_sim 4 | interface: camera 5 | requires_safety: false 6 | 7 | camera_id: "DEV_1AB22C050456" 8 | device_name: Allied Alvium 1800 U-500m 9 | 10 | offset_x: 1200 11 | offset_y: 600 12 | width: 400 13 | height: 400 14 | sensor_width: 2592 15 | sensor_height: 1944 16 | exposure_time: 200 17 | gain: 0 18 | 19 | simulator: 20 | service_type: {{cookiecutter.project_slug}}_simulator 21 | requires_safety: false 22 | -------------------------------------------------------------------------------- /tests/test_optical_model.py: -------------------------------------------------------------------------------- 1 | from catkit2.catkit_bindings import DataStream 2 | from catkit2.simulator import SimpleOpticalModel 3 | 4 | import hcipy 5 | import numpy as np 6 | 7 | 8 | def test_optical_model(): 9 | model = SimpleOpticalModel() 10 | pupil_grid = model.pupil_grid 11 | 12 | wf_in = hcipy.Wavefront(pupil_grid.ones()) 13 | model.set_wavefronts('pre_pupil', wf_in) 14 | 15 | for i in range(5): 16 | model.atmosphere.t += 0.01 17 | model.purge_plane('pre_coro') 18 | 19 | wf_out = model.get_wavefronts('science_camera')[0] 20 | 21 | assert np.sum(wf_out.intensity) > 0 22 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E,W,F,N 3 | 4 | select = 5 | E101 6 | E128 7 | E131 8 | E201 9 | E202 10 | E203 11 | E225 12 | E226 13 | E231 14 | E241 15 | E261 16 | E262 17 | E265 18 | E271 19 | E4 20 | E7 21 | E9 22 | W291 23 | W292 24 | W293 25 | W6 26 | F401 27 | F821 28 | F822 29 | F841 30 | N801 31 | N802 32 | N804 33 | N805 34 | 35 | exclude = 36 | .git 37 | __pycache__ 38 | docs 39 | build 40 | dist 41 | .eggs 42 | tests 43 | proto 44 | extern 45 | NKTP_DLL.py 46 | cookiecutter-testbed 47 | -------------------------------------------------------------------------------- /cookiecutter-testbed/cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "full_name": "Audrey M. Roy Greenfeld", 3 | "email": "audreyfeldroy@example.com", 4 | "github_username": "audreyfeldroy", 5 | "pypi_package_name": "python-boilerplate", 6 | "project_name": "Python Boilerplate", 7 | "project_slug": "{{ cookiecutter.pypi_package_name.replace('-', '_') }}", 8 | "project_short_description": "Python Boilerplate contains all the boilerplate you need to create a Python package.", 9 | "pypi_username": "{{ cookiecutter.github_username }}", 10 | "first_version": "0.1.0", 11 | "__gh_slug": "{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}" 12 | } 13 | -------------------------------------------------------------------------------- /catkit2/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | # Setting to ensure CTRL-C commands are caught, which allows services to exit properly. 4 | import os 5 | os.environ['FOR_DISABLE_CONSOLE_CTRL_HANDLER'] = '1' 6 | 7 | # Enable printing of stacktrace upon segfault. 8 | import faulthandler 9 | faulthandler.enable() 10 | 11 | from . import testbed 12 | from . import simulator 13 | from . import config 14 | 15 | from .testbed import * 16 | from .simulator import * 17 | from .config import * 18 | 19 | from .version import get_version 20 | __version__ = get_version() 21 | 22 | __all__ = [] 23 | __all__.extend(testbed.__all__) 24 | __all__.extend(simulator.__all__) 25 | -------------------------------------------------------------------------------- /docs/services/thorlabs_pm.rst: -------------------------------------------------------------------------------- 1 | Thorlabs Power Meter 2 | ==================== 3 | 4 | This services periodically checks the measured power of a Thorlabs power meter. 5 | 6 | Configuration 7 | ------------- 8 | 9 | .. code-block:: YAML 10 | 11 | power_meter: 12 | service_type: thorlabs_pm 13 | simulated_service_type: thorlabs_pm_sim 14 | requires_safety: false 15 | 16 | serial_number: 111000222 17 | interval: 0.5 18 | 19 | Properties 20 | ---------- 21 | None. 22 | 23 | Commands 24 | -------- 25 | None. 26 | 27 | Datastreams 28 | ----------- 29 | ``power``: The measured power in W. 30 | 31 | ``dark``: The measured dark level in W. 32 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/src/{{cookiecutter.project_slug}}/config/testbed.yml: -------------------------------------------------------------------------------- 1 | default_port: 1234 2 | simulator: 3 | service_id: {{cookiecutter.project_slug}}_simulator 4 | service_paths: 5 | - !path ../services/ 6 | startup_services: [] 7 | base_data_path: 8 | default: !path "~/{{cookiecutter.project_slug}}_data" 9 | support_data_path: 10 | default: !path "~/{{cookiecutter.project_slug}}_data" 11 | 12 | base_experiment_path: "{simulator_or_hardware}/{date_and_time}_{experiment_name}/" 13 | sub_experiment_path: "{experiment_id:03d}_{experiment_name}/" 14 | 15 | long_term_monitoring_path: "{simulator_or_hardware}/long_term_monitoring/" 16 | -------------------------------------------------------------------------------- /catkit2/services/bmc_deformable_mirror_sim/bmc_deformable_mirror_sim.py: -------------------------------------------------------------------------------- 1 | from catkit2.base_services.bmc_deformable_mirror import BmcDeformableMirror 2 | from catkit2.testbed.tracing import trace_interval 3 | 4 | 5 | class BmcDeformableMirrorSim(BmcDeformableMirror): 6 | def __init__(self, service_type='bmc_deformable_mirror_sim'): 7 | super().__init__(service_type) 8 | 9 | def send_to_device(self): 10 | with trace_interval('send data'): 11 | self.testbed.simulator.actuate_dm(dm_name=self.id, new_actuators=self.discretized_surface) 12 | 13 | 14 | if __name__ == '__main__': 15 | service = BmcDeformableMirrorSim() 16 | service.run() 17 | -------------------------------------------------------------------------------- /.github/workflows/linting.yml: -------------------------------------------------------------------------------- 1 | name: Linting 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | types: [opened, synchronize, ready_for_review] 7 | branches: 8 | - develop 9 | 10 | jobs: 11 | linter: 12 | name: Flake8 13 | runs-on: ubuntu-22.04 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.7.16' 21 | - name: Install flake8 22 | run: | 23 | python -m pip install flake8 24 | shell: bash 25 | - name: Lint with flake8 26 | run: flake8 . --max-line-length=127 --count --statistics 27 | shell: bash 28 | -------------------------------------------------------------------------------- /docs/catkit2.rst: -------------------------------------------------------------------------------- 1 | catkit2 2 | ======= 3 | 4 | Testbed 5 | ------- 6 | 7 | .. automodapi:: catkit2.testbed 8 | :no-inheritance-diagram: 9 | :include-all-objects: 10 | :no-heading: 11 | 12 | Proxies 13 | ------- 14 | 15 | .. automodapi:: catkit2.testbed.proxies 16 | :no-inheritance-diagram: 17 | :include-all-objects: 18 | :no-heading: 19 | 20 | Simulator 21 | --------- 22 | 23 | .. automodapi:: catkit2.simulator 24 | :no-inheritance-diagram: 25 | :include-all-objects: 26 | :no-heading: 27 | 28 | Configuration 29 | ------------- 30 | 31 | .. automodapi:: catkit2.config 32 | :no-inheritance-diagram: 33 | :include-all-objects: 34 | :no-heading: 35 | -------------------------------------------------------------------------------- /benchmarks/uuid_generator.cpp: -------------------------------------------------------------------------------- 1 | #include "Uuid.h" 2 | #include "Timing.h" 3 | 4 | #include 5 | 6 | int main() 7 | { 8 | const size_t N = 100000000; 9 | 10 | Uuid uuid; 11 | 12 | std::cout << std::hex; 13 | 14 | auto start = GetTimeStamp(); 15 | 16 | for (size_t i = 0; i < N; ++i) 17 | { 18 | Uuid::Generate(&uuid); 19 | } 20 | 21 | auto end = GetTimeStamp(); 22 | 23 | std::cout << std::dec; 24 | 25 | std::cout << "Time: " << (end - start) / 1e9 << " sec" << std::endl; 26 | std::cout << "Throughput: " << N / ((end - start) / 1e9) << " ops/s" << std::endl; 27 | std::cout << "Time per operation: " << (end - start) / N << " ns" << std::endl; 28 | 29 | return 0; 30 | } 31 | -------------------------------------------------------------------------------- /catkit_core/StructStream.h: -------------------------------------------------------------------------------- 1 | #ifndef STRUCT_STREAM_H 2 | #define STRUCT_STREAM_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class Memory; 9 | 10 | class StructStream 11 | { 12 | public: 13 | StructStream(std::shared_ptr buffer, std::size_t offset = 0); 14 | 15 | template 16 | T *Extract(std::size_t num_elements = 1); 17 | 18 | template 19 | inline void AddPadding(); 20 | 21 | std::size_t GetOffset(); 22 | 23 | std::shared_ptr GetBuffer(); 24 | 25 | private: 26 | std::shared_ptr m_Buffer; 27 | std::size_t m_Offset; 28 | }; 29 | 30 | #include "StructStream.inl" 31 | 32 | #endif // STRUCT_STREAM_H 33 | -------------------------------------------------------------------------------- /cookiecutter-testbed/.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | # python: 21 | # install: 22 | # - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /benchmarks/latency_histogram.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | latencies = [] 5 | 6 | with open('results.txt') as f: 7 | for line in f.readlines(): 8 | latencies.append(float(line) / 1000) 9 | 10 | print(len(latencies)) 11 | 12 | plt.hist(latencies, bins=np.arange(0, 100)) 13 | plt.yscale('log') 14 | 15 | quantiles = [0.5, 0.99, 0.999, 0.9999] 16 | for q in quantiles: 17 | p = np.quantile(latencies, q) 18 | plt.axvline(p, c='k') 19 | plt.text(p, 1e6, f'{float(q * 100)}%', horizontalalignment='right', verticalalignment='bottom', rotation=90) 20 | plt.ylim(5e-1, 2e7) 21 | 22 | plt.xlabel('Latency [us]') 23 | plt.ylabel('Occurance') 24 | plt.show() 25 | -------------------------------------------------------------------------------- /catkit_core/FitsFile.h: -------------------------------------------------------------------------------- 1 | #ifndef FITS_FILE_H 2 | #define FITS_FILE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class FitsFile 11 | { 12 | public: 13 | FitsFile(std::string fname, std::string hdu_name = ""); 14 | ~FitsFile(); 15 | 16 | int GetNDim(); 17 | std::vector GetShape(); 18 | long GetSize(); 19 | int GetDataType(); 20 | 21 | template 22 | std::vector GetData(); 23 | 24 | template 25 | std::vector GetDataCasted(); 26 | 27 | private: 28 | std::string GetFitsError(); 29 | 30 | fitsfile *m_File; 31 | }; 32 | 33 | #include "FitsFile.inl" 34 | 35 | #endif // FITS_FILE_H 36 | -------------------------------------------------------------------------------- /catkit_core/LogFile.cpp: -------------------------------------------------------------------------------- 1 | #include "LogFile.h" 2 | 3 | using namespace std; 4 | 5 | LogFile::LogFile(const char *filename) 6 | { 7 | m_File.open(filename); 8 | } 9 | 10 | void LogFile::AddLogEntry(const LogEntry &entry) 11 | { 12 | m_File << entry.time << " "; 13 | m_File << entry.function << "@" << entry.filename << ":" << " "; 14 | 15 | switch (entry.severity) 16 | { 17 | case S_CRITICAL: 18 | m_File << "Critical: "; 19 | break; 20 | case S_ERROR: 21 | m_File << "Error: "; 22 | break; 23 | case S_WARNING: 24 | m_File << "Warning: "; 25 | break; 26 | case S_INFO: 27 | m_File << "Info: "; 28 | break; 29 | case S_DEBUG: 30 | m_File << "Debug: "; 31 | break; 32 | } 33 | 34 | m_File << entry.message << endl; 35 | } -------------------------------------------------------------------------------- /catkit_core/StructStream.inl: -------------------------------------------------------------------------------- 1 | #include "StructStream.h" 2 | 3 | #include "Memory.h" 4 | 5 | template 6 | T *StructStream::Extract(std::size_t num_elements) 7 | { 8 | AddPadding(); 9 | 10 | // Link element to the buffer. 11 | T *res = reinterpret_cast(m_Buffer->GetAddress(m_Offset)); 12 | 13 | // Consume bytes from the buffer. 14 | m_Offset += num_elements * sizeof(T); 15 | 16 | return res; 17 | } 18 | 19 | template 20 | void StructStream::AddPadding() 21 | { 22 | // Get alignment requirement of the type. 23 | constexpr std::size_t align = std::max({alignof(Ts)...}); 24 | 25 | // Pad the buffer to satisfy alignment requirement. 26 | m_Offset += (align - (m_Offset % align)) % align; 27 | } 28 | -------------------------------------------------------------------------------- /catkit_core/ProcessStats.h: -------------------------------------------------------------------------------- 1 | #ifndef PROCESS_STATS_H 2 | #define PROCESS_STATS_H 3 | 4 | #include 5 | 6 | class ProcessStats 7 | { 8 | public: 9 | ProcessStats(); 10 | 11 | void Update(); 12 | 13 | double GetCpuUsage() const; 14 | uint64_t GetMemoryUsage() const; 15 | 16 | private: 17 | double m_CpuUsage = 0.0; 18 | uint64_t m_MemoryUsage = 0; 19 | 20 | #if defined(_WIN32) 21 | void *m_ProcessHandle; 22 | 23 | unsigned long long m_LastProcTime = 0; 24 | unsigned long long m_LastSysTime = 0; 25 | #elif defined(__APPLE__) 26 | uint64_t m_LastTotalTime = 0; 27 | uint64_t m_LastTaskTime = 0; 28 | #else 29 | long m_LastProcJiffies = 0; 30 | long m_LastSysJiffies = 0; 31 | #endif 32 | }; 33 | 34 | #endif // PROCESS_STATS_H 35 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | cd doxygen && doxygen 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /proto/core.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package catkit_proto; 4 | 5 | message List 6 | { 7 | repeated Value items = 1; 8 | } 9 | 10 | message Dict 11 | { 12 | map items = 5; 13 | } 14 | 15 | message Tensor 16 | { 17 | string dtype = 1; 18 | repeated int64 dimensions = 2; 19 | bytes data = 3; 20 | } 21 | 22 | message Value 23 | { 24 | oneof kind 25 | { 26 | NoneValue none_value = 1; 27 | int64 int_value = 2; 28 | double double_value = 3; 29 | string string_value = 4; 30 | bool bool_value = 5; 31 | Dict dict_value = 6; 32 | List list_value = 7; 33 | Tensor tensor_value = 8; 34 | } 35 | } 36 | 37 | enum NoneValue 38 | { 39 | NONE = 0; 40 | } 41 | -------------------------------------------------------------------------------- /proto/service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "core.proto"; 4 | 5 | package catkit_proto.service; 6 | 7 | message GetInfoRequest 8 | { 9 | } 10 | 11 | message GetInfoReply 12 | { 13 | string service_id = 1; 14 | string service_type = 2; 15 | string config = 3; 16 | 17 | repeated string property_names = 4; 18 | repeated string command_names = 5; 19 | map datastream_ids = 6; 20 | 21 | string heartbeat_stream_id = 7; 22 | } 23 | 24 | message ExecuteCommandRequest 25 | { 26 | string command_name = 1; 27 | Dict arguments = 2; 28 | } 29 | 30 | message ExecuteCommandReply 31 | { 32 | Value result = 1; 33 | } 34 | 35 | message ShutDownRequest 36 | { 37 | } 38 | 39 | message ShutDownReply 40 | { 41 | } 42 | -------------------------------------------------------------------------------- /catkit2/testbed/proxies/web_power_switch.py: -------------------------------------------------------------------------------- 1 | from ..service_proxy import ServiceProxy 2 | 3 | import numpy as np 4 | 5 | 6 | class WebPowerSwitchProxy(ServiceProxy): 7 | def switch(self, outlet_name, on): 8 | if outlet_name.lower() not in self.outlets: 9 | raise ValueError(f'\"{outlet_name}\" is not one of the outlets.') 10 | 11 | channel = getattr(self, outlet_name.lower()) 12 | channel.submit_data(np.ones(1, dtype='int8') * on) 13 | 14 | def turn_on(self, outlet_name): 15 | self.switch(outlet_name, True) 16 | 17 | def turn_off(self, outlet_name): 18 | self.switch(outlet_name, False) 19 | 20 | @property 21 | def outlets(self): 22 | return [key.lower() for key in self.config['outlets'].keys()] 23 | -------------------------------------------------------------------------------- /docs/services/omega_ithx_w3.rst: -------------------------------------------------------------------------------- 1 | Omega Temperature Sensor 2 | ======================== 3 | 4 | This service operates an Omega ITHX-W3 temperature and humidity sensor. Both temperature and humidity are checked every ``time_interval``. 5 | 6 | Configuration 7 | ------------- 8 | 9 | .. code-block:: YAML 10 | 11 | omega_dm1: 12 | service_type: omega_ithx_w3 13 | simulated_service_type: omega_ithx_w3_sim 14 | 15 | ip_address: xxx.xxx.xxx.xxx 16 | time_interval: 30 # seconds 17 | 18 | Properties 19 | ---------- 20 | None. 21 | 22 | Commands 23 | -------- 24 | None. 25 | 26 | Datastreams 27 | ----------- 28 | ``temperature``: The temperature as measured by this sensor in Celsius. 29 | 30 | ``humidity``: The humidity as measured by this sensor in percent. 31 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/tests/test_{{cookiecutter.project_slug}}.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import pytest 3 | 4 | """Tests for `{{ cookiecutter.project_slug }}` package.""" 5 | 6 | # from {{ cookiecutter.project_slug }} import {{ cookiecutter.project_slug }} 7 | 8 | 9 | @pytest.fixture 10 | def response(): 11 | """Sample pytest fixture. 12 | 13 | See more at: http://doc.pytest.org/en/latest/fixture.html 14 | """ 15 | # import requests 16 | # return requests.get('https://github.com/audreyfeldroy/cookiecutter-pypackage') 17 | 18 | 19 | def test_content(response): 20 | """Sample pytest test function with the pytest fixture as an argument.""" 21 | # from bs4 import BeautifulSoup 22 | # assert 'GitHub' in BeautifulSoup(response.content).title.string 23 | -------------------------------------------------------------------------------- /catkit2/testbed/proxies/oceanoptics_spectrometer.py: -------------------------------------------------------------------------------- 1 | from ..service_proxy import ServiceProxy 2 | 3 | 4 | class OceanopticsSpectroProxy(ServiceProxy): 5 | 6 | def take_raw_exposures(self, num_exposures): 7 | first_frame_id = self.spectra.newest_available_frame_id 8 | 9 | i = 0 10 | num_exposures_remaining = num_exposures 11 | 12 | while num_exposures_remaining >= 1: 13 | try: 14 | frame = self.spectra.get_frame(first_frame_id + i, 1000) 15 | except RuntimeError: 16 | # The frame wasn't available anymore because we were waiting too long. 17 | continue 18 | finally: 19 | i += 1 20 | 21 | yield frame.data.copy() 22 | num_exposures_remaining -= 1 23 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | 3 | project(catkit2) 4 | 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED on) 7 | 8 | # Add debug symbols for all builds. 9 | if (MSVC) 10 | add_compile_options("$<$:/Zi>") 11 | add_link_options("$<$:/DEBUG>") 12 | elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") 13 | add_compile_options(-g) 14 | endif() 15 | 16 | set(CMAKE_MACOSX_RPATH ON) 17 | list(APPEND CMAKE_INSTALL_RPATH $ENV{CONDA_PREFIX}/lib) 18 | 19 | # Ensure that conda-installed libraries take precedence over 20 | # system installed libraries. 21 | list(APPEND CMAKE_PREFIX_PATH "$ENV{CONDA_PREFIX}") 22 | list(APPEND CMAKE_PREFIX_PATH "$ENV{CONDA_PREFIX}/Library") 23 | 24 | add_subdirectory(catkit_core) 25 | add_subdirectory(catkit2) 26 | add_subdirectory(benchmarks) 27 | -------------------------------------------------------------------------------- /cookiecutter-testbed/.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{html,css,js,json,sh,yml,yaml}] 13 | indent_size = 2 14 | 15 | [*.bat] 16 | indent_style = tab 17 | end_of_line = crlf 18 | 19 | [LICENSE] 20 | insert_final_newline = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | indent_size = unset 25 | 26 | # Ignore binary or generated files 27 | [*.{png,jpg,gif,ico,woff,woff2,ttf,eot,svg,pdf}] 28 | charset = unset 29 | end_of_line = unset 30 | indent_style = unset 31 | indent_size = unset 32 | trim_trailing_whitespace = unset 33 | insert_final_newline = unset 34 | max_line_length = unset 35 | 36 | [*.{diff,patch}] 37 | trim_trailing_whitespace = false 38 | -------------------------------------------------------------------------------- /catkit_core/CudaSharedMemory.h: -------------------------------------------------------------------------------- 1 | #ifndef CUDA_SHARED_MEMORY_H 2 | #define CUDA_SHARED_MEMORY_H 3 | 4 | #include "Memory.h" 5 | 6 | #include 7 | 8 | #ifdef HAVE_CUDA 9 | #include 10 | typedef cudaIpcMemHandle_t CudaIpcHandle; 11 | #else 12 | // CUDA cudaIpcMemHandle_t is a struct of 64 bytes. 13 | typedef char CudaIpcHandle[64]; 14 | #endif 15 | 16 | class CudaSharedMemory : public Memory 17 | { 18 | private: 19 | CudaSharedMemory(const CudaIpcHandle &ipc_handle, void *device_pointer=nullptr); 20 | 21 | public: 22 | ~CudaSharedMemory(); 23 | 24 | static std::shared_ptr Create(size_t num_bytes_in_buffer); 25 | static std::shared_ptr Open(const CudaIpcHandle &ipc_handle); 26 | 27 | void *GetAddress(std::size_t offset = 0) override; 28 | }; 29 | 30 | #endif // CUDA_SHARED_MEMORY_H 31 | -------------------------------------------------------------------------------- /catkit_core/Util.h: -------------------------------------------------------------------------------- 1 | #ifndef UTIL_H 2 | #define UTIL_H 3 | 4 | #include 5 | #include 6 | 7 | int GetProcessId(); 8 | int GetThreadId(); 9 | 10 | template 11 | std::string Serialize(const ProtoClass &obj); 12 | 13 | template 14 | ProtoClass Deserialize(const std::string &data); 15 | 16 | void Sleep(double sleep_time_in_sec, std::function cancellation_callback = nullptr); 17 | 18 | template 19 | constexpr UnsignedType round_up_to_power_of_2(UnsignedType v); 20 | 21 | template 22 | constexpr UnsignedType round_down_to_power_of_2(UnsignedType v); 23 | 24 | // Cross-platform implementation of std::bit_width() (in absence of C++20) 25 | template 26 | constexpr int bit_width(T x); 27 | 28 | #include "Util.inl" 29 | 30 | #endif // UTIL_H 31 | -------------------------------------------------------------------------------- /catkit_core/LogForwarder.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGFORWARDER_H 2 | #define LOGFORWARDER_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include "Log.h" 14 | 15 | class LogForwarder : LogListener 16 | { 17 | public: 18 | LogForwarder(); 19 | ~LogForwarder(); 20 | 21 | void Connect(std::string service_id, std::string host); 22 | 23 | void AddLogEntry(const LogEntry &entry); 24 | 25 | private: 26 | void MessageLoop(); 27 | void ShutDown(); 28 | 29 | std::thread m_MessageLoopThread; 30 | std::atomic_bool m_ShutDown; 31 | 32 | std::queue m_LogMessages; 33 | std::mutex m_Mutex; 34 | std::condition_variable m_ConditionVariable; 35 | 36 | std::string m_ServiceId; 37 | std::string m_Host; 38 | }; 39 | 40 | #endif // LOGFORWARDER_H 41 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{html,css,js,json,sh,yml,yaml}] 13 | indent_size = 2 14 | 15 | [*.bat] 16 | indent_style = tab 17 | end_of_line = crlf 18 | 19 | [LICENSE] 20 | insert_final_newline = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | indent_size = unset 25 | 26 | # Ignore binary or generated files 27 | [*.{png,jpg,gif,ico,woff,woff2,ttf,eot,svg,pdf}] 28 | charset = unset 29 | end_of_line = unset 30 | indent_style = unset 31 | indent_size = unset 32 | trim_trailing_whitespace = unset 33 | insert_final_newline = unset 34 | max_line_length = unset 35 | 36 | [*.{diff,patch}] 37 | trim_trailing_whitespace = false 38 | -------------------------------------------------------------------------------- /catkit_core/ConcurrentVector.h: -------------------------------------------------------------------------------- 1 | #ifndef CONCURRENT_VECTOR_H 2 | #define CONCURRENT_VECTOR_H 3 | 4 | #include "Util.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | template 14 | class ConcurrentVector 15 | { 16 | public: 17 | ConcurrentVector(); 18 | ~ConcurrentVector(); 19 | 20 | size_t PushBack(const T& value); 21 | T &operator[](size_t index) const; 22 | 23 | size_t Size() const; 24 | private: 25 | std::atomic m_Size; 26 | std::atomic m_Segments[MaxSegments]; 27 | 28 | void EnsureSegmentExists(size_t seg); 29 | 30 | static constexpr size_t SegmentSize(size_t seg); 31 | static constexpr std::pair Locate(size_t index); 32 | }; 33 | 34 | #include "ConcurrentVector.inl" 35 | 36 | #endif // CONCURRENT_VECTOR_H 37 | -------------------------------------------------------------------------------- /catkit_core/Client.h: -------------------------------------------------------------------------------- 1 | #ifndef CLIENT_H 2 | #define CLIENT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | class Client 13 | { 14 | public: 15 | Client(std::string host, int port); 16 | virtual ~Client(); 17 | 18 | std::string GetHost(); 19 | int GetPort(); 20 | 21 | std::string MakeRequest(const std::string &what, const std::string &request); 22 | 23 | private: 24 | std::string m_Host; 25 | int m_Port; 26 | 27 | zmq::context_t m_Context; 28 | 29 | typedef std::unique_ptr> socket_ptr; 30 | socket_ptr GetSocket(); 31 | 32 | std::mutex m_Mutex; 33 | std::stack> m_Sockets; 34 | }; 35 | 36 | template 37 | std::string Serialize(const ProtoRequest &request); 38 | 39 | #endif // CLIENT_H 40 | -------------------------------------------------------------------------------- /catkit2/testbed/proxies/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'CameraProxy', 3 | 'NewportXpsQ8Proxy', 4 | 'FlipMountProxy', 5 | 'DeformableMirrorProxy', 6 | 'NewportPicomotorProxy', 7 | 'NiDaqProxy', 8 | 'NktSuperkEvoProxy', 9 | 'NktSuperkFianiumProxy', 10 | 'ThorlabsCubeMotorKinesisProxy', 11 | 'ThorlabsMcls1', 12 | 'WebPowerSwitchProxy', 13 | 'OceanopticsSpectroProxy', 14 | 'OpticalFiberSwitchProxy' 15 | ] 16 | 17 | from .camera import * 18 | from .deformable_mirror import * 19 | from .newport_xps import * 20 | from .flip_mount import * 21 | from .newport_picomotor import * 22 | from .ni_daq import * 23 | from .nkt_superk_evo import * 24 | from .nkt_superk_fianium import * 25 | from .oceanoptics_spectrometer import * 26 | from .optical_fiber_switch import * 27 | from .thorlabs_cube_motor_kinesis import * 28 | from .thorlabs_mcls1 import * 29 | from .web_power_switch import * 30 | -------------------------------------------------------------------------------- /tests/services/dummy_dm_service/dummy_dm_service.py: -------------------------------------------------------------------------------- 1 | from catkit2 import Service 2 | 3 | import numpy as np 4 | 5 | 6 | class DummyDmService(Service): 7 | def __init__(self): 8 | super().__init__('dummy_dm_service') 9 | 10 | self.channel_names = ['correction_howfs', 'correction_lowfs', 'aberration', 'atmosphere'] 11 | self.num_actuators_all_dms = self.config['num_actuators_all_dms'] 12 | 13 | def open(self): 14 | # Make channels streamable 15 | for channel in self.channel_names: 16 | setattr(self, channel, self.make_data_stream(channel, 'float64', [self.num_actuators_all_dms], 20)) 17 | getattr(self, channel).submit_data(np.zeros(self.num_actuators_all_dms,)) 18 | 19 | def main(self): 20 | while not self.should_shut_down: 21 | self.sleep(0.1) 22 | 23 | 24 | if __name__ == '__main__': 25 | service = DummyDmService() 26 | service.run() 27 | -------------------------------------------------------------------------------- /catkit_core/Server.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVER_H 2 | #define SERVER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | class Server 11 | { 12 | public: 13 | Server(int port); 14 | virtual ~Server(); 15 | 16 | typedef std::function RequestHandler; 17 | 18 | void RegisterRequestHandler(std::string type, RequestHandler func); 19 | 20 | void Start(); 21 | void Stop(); 22 | 23 | bool IsRunning(); 24 | 25 | int GetPort(); 26 | 27 | void Sleep(double sleep_time_in_sec, void (*error_check)()=nullptr); 28 | 29 | void CleanupRequestHandlers(); 30 | 31 | protected: 32 | int m_Port; 33 | 34 | private: 35 | void RunInternal(); 36 | 37 | std::thread m_RunThread; 38 | 39 | std::map m_RequestHandlers; 40 | 41 | std::atomic_bool m_IsRunning; 42 | std::atomic_bool m_ShouldShutDown; 43 | }; 44 | 45 | #endif // SERVER_H 46 | -------------------------------------------------------------------------------- /.github/flake8_problem_matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "flake8-error", 5 | "severity": "error", 6 | "pattern": [ 7 | { 8 | "regexp": "^([^:]*):(\\d+):(\\d+): ([E]\\d\\d\\d) (.*)$", 9 | "file": 1, 10 | "line": 2, 11 | "column": 3, 12 | "code": 4, 13 | "message": 5 14 | } 15 | ] 16 | }, 17 | { 18 | "owner": "flake8-warning", 19 | "severity": "warning", 20 | "pattern": [ 21 | { 22 | "regexp": "^([^:]*):(\\d+):(\\d+): ([FWN]\\d\\d\\d) (.*)$", 23 | "file": 1, 24 | "line": 2, 25 | "column": 3, 26 | "code": 4, 27 | "message": 5 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /catkit2/services/omega_ithx_w3_sim/omega_ithx_w3_sim.py: -------------------------------------------------------------------------------- 1 | from catkit2.testbed.service import Service 2 | 3 | import numpy as np 4 | 5 | class OmegaIthxW3Sim(Service): 6 | def __init__(self): 7 | super().__init__('omega_ithx_w3_sim') 8 | 9 | self.time_interval = self.config['time_interval'] 10 | 11 | self.temperature = self.make_data_stream('temperature', 'float64', [1], 20) 12 | self.humidity = self.make_data_stream('humidity', 'float64', [1], 20) 13 | 14 | def main(self): 15 | while not self.should_shut_down: 16 | temp, hum = self.get_temperature_and_humidity() 17 | 18 | self.temperature.submit_data(np.array([temp])) 19 | self.humidity.submit_data(np.array([hum])) 20 | 21 | self.sleep(self.time_interval) 22 | 23 | def get_temperature_and_humidity(self): 24 | return 25.0, 10.0 25 | 26 | if __name__ == '__main__': 27 | service = OmegaIthxW3Sim() 28 | service.run() 29 | -------------------------------------------------------------------------------- /catkit_core/ArrayView.h: -------------------------------------------------------------------------------- 1 | #ifndef ARRAY_VIEW_H 2 | #define ARRAY_VIEW_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | const size_t MAX_NUM_DIMENSIONS = 4; 9 | 10 | struct ArrayInfo 11 | { 12 | char data_type; // b, i, u, f, c 13 | char byte_order; // < > = ! 14 | std::uint8_t item_size; // 1, 2, 4, 8, 16 15 | std::uint8_t ndim; // 0, 1, 2, 3, ..., MAX_NUM_DIMENSIONS 16 | std::array shape; // in elements 17 | std::array strides; // in bytes 18 | 19 | bool IsCContiguous() const; 20 | bool IsFContiguous() const; 21 | std::size_t GetSize() const; 22 | std::size_t GetSizeInBytes() const; 23 | }; 24 | 25 | // A mathematical array class. 26 | // This follows the NumPy convention. 27 | struct ArrayView 28 | { 29 | ArrayInfo info; 30 | void *data = nullptr; 31 | 32 | bool IsAligned() const; 33 | bool IsCContiguous() const; 34 | bool IsFContiguous() const; 35 | }; 36 | 37 | #endif // ARRAY_VIEW_H 38 | -------------------------------------------------------------------------------- /catkit2/services/thorlabs_pm_sim/thorlabs_pm_sim.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from catkit2.testbed.service import Service 4 | 5 | 6 | class ThorlabsPMSim(Service): 7 | def __init__(self): 8 | super().__init__('thorlabs_pm_sim') 9 | 10 | self.interval = self.config.get('interval', 10) 11 | 12 | self.power = self.make_data_stream('power', 'float64', [1], 20) 13 | self.dark = self.make_data_stream('dark', 'float64', [1], 20) 14 | 15 | def main(self): 16 | while not self.should_shut_down: 17 | power = self.get_power() 18 | self.power.submit_data(np.array([power], dtype='float64')) 19 | 20 | # Keep dark data stream updating with zero in simulation. 21 | self.dark.submit_data(np.array([0], dtype='float64')) 22 | 23 | self.sleep(self.interval) 24 | 25 | def get_power(self): 26 | return 1 # Watt 27 | 28 | 29 | if __name__ == '__main__': 30 | service = ThorlabsPMSim() 31 | service.run() 32 | -------------------------------------------------------------------------------- /proto/tracing.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package catkit_proto.tracing; 4 | 5 | message TraceEventInterval 6 | { 7 | string name = 1; 8 | string category = 2; 9 | uint32 process_id = 3; 10 | string process_name = 4; 11 | uint32 thread_id = 5; 12 | string thread_name = 6; 13 | uint64 timestamp = 7; 14 | uint64 duration = 8; 15 | } 16 | 17 | message TraceEventInstant 18 | { 19 | string name = 1; 20 | uint32 process_id = 2; 21 | string process_name = 3; 22 | uint32 thread_id = 4; 23 | string thread_name = 5; 24 | uint64 timestamp = 6; 25 | } 26 | 27 | message TraceEventCounter 28 | { 29 | string name = 1; 30 | string series = 2; 31 | uint32 process_id = 3; 32 | string process_name = 4; 33 | uint64 timestamp = 5; 34 | double counter = 6; 35 | } 36 | 37 | message TraceEvent 38 | { 39 | oneof event 40 | { 41 | TraceEventInterval interval = 1; 42 | TraceEventInstant instant = 2; 43 | TraceEventCounter counter = 3; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /catkit_core/LocalMemory.h: -------------------------------------------------------------------------------- 1 | #ifndef LOCAL_MEMORY_H 2 | #define LOCAL_MEMORY_H 3 | 4 | #include "Memory.h" 5 | #include "Shareable.h" 6 | #include "Util.h" 7 | 8 | class LocalMemory : public Memory, public Shareable 9 | { 10 | public: 11 | virtual ~LocalMemory(); 12 | 13 | static std::shared_ptr Create(StructStream &stream, std::size_t num_bytes); 14 | static std::shared_ptr Create(std::size_t num_bytes); 15 | static std::shared_ptr Open(StructStream &stream); 16 | 17 | virtual void *GetAddress(std::size_t offset = 0) override; 18 | virtual std::size_t GetCapacity() const override; 19 | virtual void WriteReference(StructStream *stream) override; 20 | 21 | virtual ShareableType GetType() const override; 22 | virtual MemoryType GetMemoryType() const override; 23 | 24 | private: 25 | LocalMemory(char *memory, std::size_t num_bytes, bool is_owner); 26 | 27 | char *m_Memory; 28 | const std::size_t m_Capacity; 29 | bool m_IsOwner; 30 | }; 31 | 32 | #endif // LOCAL_MEMORY_H 33 | -------------------------------------------------------------------------------- /docs/testbed_implementation.rst: -------------------------------------------------------------------------------- 1 | Testbed Implementation 2 | ====================== 3 | 4 | catkit2 includes code to generate a sample catkit2-based testbed package from the command line using `cookiecutter `__. 5 | This is meant to provide a starting place from which to build out your own full testbed repository. Cookiecutter automatically generates all configuration and installation files, a 6 | pyproject.toml, template docs, and a sample simulated service to be able to start running your own testbed server. 7 | 8 | To generate the testbed repository, from the command line run: 9 | 10 | .. code-block:: shell 11 | cookiecutter catkit2/cookiecutter-testbed 12 | 13 | You will then be prompted with a series of questions answered by typing directly in the console. One of these will specify the testbed name (e.g. ``my_testbed``). 14 | 15 | You can then start the server for ``my_testbed`` by running: 16 | 17 | .. code-block:: shell 18 | cd my_testbed 19 | pip install -e . 20 | my_testbed start server --simulated -------------------------------------------------------------------------------- /catkit2/testbed/proxies/thorlabs_cube_motor_kinesis.py: -------------------------------------------------------------------------------- 1 | from ..service_proxy import ServiceProxy 2 | 3 | import numpy as np 4 | 5 | 6 | class ThorlabsCubeMotorKinesisProxy(ServiceProxy): 7 | def move_absolute(self, position): 8 | position = self.resolve_position(position) 9 | self.command.submit_data(np.array([position], dtype='float64')) 10 | 11 | def move_relative(self, distance): 12 | current_position = self.current_position.get()[0] 13 | new_position = current_position + distance 14 | self.move_absolute(new_position) 15 | 16 | def resolve_position(self, position_name): 17 | if type(position_name) == str: 18 | # The position is a named position. 19 | position = self.positions[position_name] 20 | # The position may still be a named position, so try to resolve deeper. 21 | return self.resolve_position(position) 22 | else: 23 | return position_name 24 | 25 | @property 26 | def positions(self): 27 | return self.config['positions'] 28 | -------------------------------------------------------------------------------- /catkit2/services/safety_manual_check/safety_manual_check.py: -------------------------------------------------------------------------------- 1 | from catkit2.testbed import Service 2 | 3 | import numpy as np 4 | 5 | 6 | class SafetyManualCheck(Service): 7 | def __init__(self): 8 | super().__init__('safety_manual_check') 9 | 10 | self.dtype = self.config['dtype'] 11 | self.value = np.array([self.config['initial_value']], dtype=self.dtype) 12 | 13 | self.make_property('value', self.get_value, self.set_value) 14 | 15 | self.check_stream = self.make_data_stream('check', self.dtype, [1], 20) 16 | 17 | def main(self): 18 | while not self.should_shut_down: 19 | self.check_stream.submit_data(self.value) 20 | 21 | self.sleep(0.1) 22 | 23 | def get_value(self): 24 | return self.value[0] 25 | 26 | def set_value(self, new_value): 27 | # This makes sure that the new value is actually convertable to a self.dtype. 28 | self.value = np.array([new_value], dtype=self.dtype) 29 | 30 | 31 | if __name__ == '__main__': 32 | service = SafetyManualCheck() 33 | service.run() 34 | -------------------------------------------------------------------------------- /docs/services/optical_fiber_switch.rst: -------------------------------------------------------------------------------- 1 | Optical Fiber Switch 2 | ==================== 3 | 4 | This service controls an optical fibers switch through a serial connection. 5 | 6 | On a 2x2 switch, the input channel is either 1 or two, depending on whether the switch does a direct or crossed connection. 7 | 8 | Configuration 9 | ------------- 10 | 11 | .. code-block:: YAML 12 | 13 | optical_switch_8x1: 14 | service_type: optical_fiber_switch 15 | simulated_service_type: optical_fiber_switch_sim 16 | requires_safety: false 17 | 18 | port: 'COM5' 19 | baudrate: 9600 20 | 21 | min_channel: 1 22 | max_channel: 8 23 | 24 | Properties 25 | ---------- 26 | None. 27 | 28 | Commands 29 | -------- 30 | None. 31 | 32 | Datastreams 33 | ----------- 34 | ``input_channel``: The data stream being monitored for input channel changes. This is an int that sets the current input channel of the switch. Must be between the `min_channel` and `max_channel` defined in the configuration. 35 | 36 | ``current_input``: The current input channel of the switch. 37 | -------------------------------------------------------------------------------- /docs/services/safety_manual_check.rst: -------------------------------------------------------------------------------- 1 | Safety Manual Check 2 | =================== 3 | 4 | A service that allows the user to manually check the safety of the testbed through the :ref:`safety_monitor` service. 5 | 6 | This service has a property called ``value`` whose initial value is read from the configuration file, and it can be set to any value by the user. 7 | It is continuously submitted to the only datastream ``check``. 8 | 9 | By setting the value to something new, the safety monitor picks up on the new value on the datastream ``check`` and it should react accordingly. 10 | 11 | Configuration 12 | ------------- 13 | 14 | .. code-block:: YAML 15 | 16 | safety_manual_check: 17 | service_type: safety_manual_check 18 | requires_safety: true 19 | 20 | dtype: int 21 | initial_value: 5 22 | 23 | Properties 24 | ---------- 25 | ``value``: The value that is continuously submitted to the datastream ``check``. 26 | 27 | 28 | Commands 29 | -------- 30 | None. 31 | 32 | Datastreams 33 | ----------- 34 | ``check``: The data stream that is continuously checked for safety. -------------------------------------------------------------------------------- /catkit2/services/snmp_ups_sim/snmp_ups_sim.py: -------------------------------------------------------------------------------- 1 | from catkit2.testbed.service import Service 2 | 3 | import numpy as np 4 | 5 | class SnmpUpsSim(Service): 6 | def __init__(self): 7 | super().__init__('snmp_ups_sim') 8 | 9 | self.check_interval = self.config.get('check_interval', 30) 10 | 11 | self.power_ok = self.make_data_stream('power_ok', 'int8', [1], 20) 12 | 13 | def get_power_ok(self): 14 | return True 15 | 16 | def open(self): 17 | # Do one check on power safety during opening. 18 | # This is to make sure that we always have at least one power check in the datastream. 19 | power_ok = self.get_power_ok() 20 | self.power_ok.submit_data(np.array([power_ok], dtype='int8')) 21 | 22 | def main(self): 23 | while not self.should_shut_down: 24 | power_ok = self.get_power_ok() 25 | self.power_ok.submit_data(np.array([power_ok], dtype='int8')) 26 | 27 | self.sleep(self.check_interval) 28 | 29 | if __name__ == '__main__': 30 | service = SnmpUpsSim() 31 | service.run() 32 | -------------------------------------------------------------------------------- /docs/services/snmp_ups.rst: -------------------------------------------------------------------------------- 1 | SNMP UPS 2 | ======== 3 | 4 | This services periodically checks a status value of a UPS operating under the SNMP protocol. Every ``check_interval`` a request is made to the UPS, and the resulting response is checked against known a known ``pass_status`` that indicates whether power is ok or not. The result of this check is submitted to the ``power_ok`` data stream. 5 | 6 | Configuration 7 | ------------- 8 | 9 | .. code-block:: YAML 10 | 11 | lab_ups: 12 | service_type: snmp_ups 13 | simulated_service_type: snmp_ups_sim 14 | 15 | ip_address: xxx.xxx.xxx.xx 16 | port: yyy 17 | snmp_oid: 1.2.3.4.5.6 # The OID of the UPS. 18 | community: string # The community string of the UPS. 19 | pass_status: 64 # The status of a passing check. 20 | check_interval: 30 # seconds 21 | 22 | Properties 23 | ---------- 24 | None. 25 | 26 | Commands 27 | -------- 28 | None. 29 | 30 | Datastreams 31 | ----------- 32 | ``power_ok``: This indicates whether the UPS passed its power check or not. This is 1 if passing, and 0 if not passing. 33 | -------------------------------------------------------------------------------- /catkit2/testbed/proxies/optical_fiber_switch.py: -------------------------------------------------------------------------------- 1 | from ..service_proxy import ServiceProxy 2 | import numpy as np 3 | import time 4 | 5 | 6 | class OpticalFiberSwitchProxy(ServiceProxy): 7 | wait_time = 0.1 8 | 9 | def set_channel(self, channel, wait=True): 10 | channel = self.resolve_channel(channel) 11 | self.input_channel.submit_data(np.array([channel], dtype='int8')) 12 | 13 | if wait: 14 | time.sleep(self.wait_time) 15 | 16 | def resolve_channel(self, channel): 17 | if isinstance(channel, str): 18 | # The channel is a named channel. 19 | channel = self.channels[channel] 20 | 21 | # The channel may still be a named channel, so try to resolve deeper. 22 | return self.resolve_channel(channel) 23 | else: 24 | return channel 25 | 26 | @property 27 | def channels(self): 28 | return self.config['channels'] 29 | 30 | def get_named_channel(self, channel): 31 | named_channel = [str(i) for i in self.channels if self.channels[i] == channel][0] 32 | return named_channel 33 | -------------------------------------------------------------------------------- /docs/services/thorlabs_fw102c.rst: -------------------------------------------------------------------------------- 1 | Thorlabs Filter Wheel 2 | ===================== 3 | This service operates a Thorlabs filter wheel. 4 | 5 | Successfully tested with the following devices: 6 | 7 | - `ThorLabs FW 102C `_ 8 | 9 | Configuration 10 | ------------- 11 | .. code-block:: YAML 12 | 13 | filter_wheel: 14 | service_type: thorlabs_fw102c 15 | simulated_service_type: thorlabs_fw102c_sim 16 | requires_safety: false 17 | 18 | visa_id: ASRL6::INSTR 19 | 20 | filters: # named positions resolved by the proxy. 21 | clear: 1 22 | 2.8_percent: 2 23 | 12_percent: 3 24 | 9_percent: 4 25 | 26 | Properties 27 | ---------- 28 | None. 29 | 30 | Commands 31 | -------- 32 | None. 33 | 34 | Datastreams 35 | ----------- 36 | ``position``: The commanded position of the filter wheel. This can have the values specified by the service configuration. 37 | 38 | ``current_position``: The current position of the filter wheel. This can have the values specified by the service configuration. -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/README.md: -------------------------------------------------------------------------------- 1 | # {{ cookiecutter.project_name }} 2 | 3 | ![PyPI version](https://img.shields.io/pypi/v/{{ cookiecutter.pypi_package_name }}.svg) 4 | [![Documentation Status](https://readthedocs.org/projects/{{ cookiecutter.pypi_package_name }}/badge/?version=latest)](https://{{ cookiecutter.pypi_package_name }}.readthedocs.io/en/latest/?version=latest) 5 | 6 | {{ cookiecutter.project_short_description }} 7 | 8 | * PyPI package: https://pypi.org/project/{{ cookiecutter.pypi_package_name }}/ 9 | * Free software: MIT License 10 | * Documentation: https://{{ cookiecutter.pypi_package_name }}.readthedocs.io. 11 | 12 | ## Features 13 | 14 | * TODO 15 | 16 | ## Credits 17 | 18 | This package makes use of the Control and Automation for Testbeds Kit 2 (CATKit2) package [catkit2](https://github.com/spacetelescope/catkit2) [(Por et. al. 2024)](https://doi.org/10.5281/zenodo.11395554). 19 | 20 | This package was created with [Cookiecutter](https://github.com/audreyfeldroy/cookiecutter) and the [audreyfeldroy/cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage) project template. 21 | -------------------------------------------------------------------------------- /catkit_core/Timing.cpp: -------------------------------------------------------------------------------- 1 | #include "Timing.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace std; 8 | using namespace std::chrono; 9 | 10 | uint64_t GetTimeStamp() 11 | { 12 | auto time_since_epoch = system_clock::now().time_since_epoch(); 13 | return duration_cast(time_since_epoch).count(); 14 | } 15 | 16 | string ConvertTimestampToString(uint64_t timestamp) 17 | { 18 | const auto time_since = duration_cast(nanoseconds(timestamp)); 19 | auto tp = system_clock::time_point(time_since); 20 | 21 | auto c = system_clock::to_time_t(tp); 22 | auto tm_local = *std::localtime(&c); 23 | 24 | std::stringstream ss; 25 | ss << std::put_time(&tm_local, "%F %T"); 26 | ss << "." << std::setw(9) << std::setfill('0') << (timestamp % 1000000000) << " "; 27 | ss << std::put_time(&tm_local, "UTC%z"); 28 | 29 | return ss.str(); 30 | } 31 | 32 | Timer::Timer() 33 | { 34 | m_StartTime = steady_clock::now(); 35 | } 36 | 37 | double Timer::GetTime() 38 | { 39 | auto now = steady_clock::now(); 40 | 41 | return duration_cast>(now - m_StartTime).count(); 42 | } 43 | -------------------------------------------------------------------------------- /docs/services/thorlabs_tsp01.rst: -------------------------------------------------------------------------------- 1 | Thorlabs Temperature Sensor 2 | =========================== 3 | 4 | This service retrieves data from a `Thorlabs TSP01 `_ 5 | temperature and humidity sensor. 6 | 7 | Configuration 8 | ------------- 9 | 10 | .. code-block:: YAML 11 | 12 | tsp01_1: 13 | service_type: thorlabs_tsp01 14 | simulated_service_type: thorlabs_tsp01_sim 15 | 16 | serial_number: M01234567 17 | num_averaging: 25 # optional 18 | interval: 60 # seconds between measurements, optional, default 10s 19 | 20 | Properties 21 | ---------- 22 | None. 23 | 24 | Commands 25 | -------- 26 | None. 27 | 28 | Datastreams 29 | ----------- 30 | ``temperature_internal``: The temperature of the sensor embedded inside the housing in Celsius. 31 | 32 | ``humidity_internal``: The humidity as measured by the sensor embedded inside the housing in percent. 33 | 34 | ``temperature_header_1``: The temperature of the external temperature probe connected to port 1 in Celsius. 35 | 36 | ``temperature_header_2``: The temperature of the external temperature probe connected to port 2 in Celsius. 37 | -------------------------------------------------------------------------------- /benchmarks/message_broker.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "LocalMemory.h" 4 | #include "LocalMessageBroker.h" 5 | 6 | const size_t NUM_MESSAGES = 10000000; 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | auto buffer = LocalMemory::Create(1024 * 1024 * 512); 11 | auto stream = StructStream(buffer); 12 | 13 | std::vector> memory_blocks; 14 | memory_blocks.push_back(LocalMemory::Create(stream, 1024 * 1024 * 128)); 15 | 16 | auto message_broker = LocalMessageBroker::Create(stream, memory_blocks); 17 | 18 | std::cout << "Message broker size: " << stream.GetOffset() << std::endl; 19 | 20 | std::cout << "Message broker created." << std::endl; 21 | 22 | auto start = GetTimeStamp(); 23 | 24 | for (int i = 0; i < NUM_MESSAGES; ++i) 25 | { 26 | Message message = message_broker->PrepareMessage("abc/def/ghi/set", 16); 27 | 28 | message_broker->PublishMessage(message); 29 | } 30 | 31 | auto end = GetTimeStamp(); 32 | 33 | std::cout << NUM_MESSAGES / ((end - start) * 1e-9) << " messages per second" << std::endl; 34 | std::cout << (end - start) / NUM_MESSAGES << " ns per message" << std::endl; 35 | 36 | return 0; 37 | } 38 | -------------------------------------------------------------------------------- /catkit_core/DeformableMirrorService.h: -------------------------------------------------------------------------------- 1 | #ifndef DEFORMABLE_MIRROR_SERVICE_H 2 | #define DEFORMABLE_MIRROR_SERVICE_H 3 | 4 | #include "Service.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | class DeformableMirrorService : public Service 12 | { 13 | public: 14 | DeformableMirrorService(std::string service_type, std::string service_id, int service_port, int testbed_port); 15 | virtual ~DeformableMirrorService(); 16 | 17 | virtual void Start(); 18 | virtual void Main(); 19 | virtual void Stop(); 20 | 21 | virtual void ApplySurface(double *surface) = 0; 22 | 23 | private: 24 | void MonitorChannel(std::string channel_id); 25 | void ApplySurfaceDelta(double *surface_delta); 26 | 27 | std::mutex m_Mutex; 28 | double *m_CurrentSurface; 29 | 30 | std::vector m_ActuatorMask; 31 | std::vector m_Shape; 32 | 33 | std::size_t m_NumActuators; 34 | 35 | std::vector m_ChannelIds; 36 | std::map m_ChannelThreads; 37 | std::map> m_ChannelStreams; 38 | 39 | std::shared_ptr m_TotalSurface; 40 | }; 41 | 42 | #endif // DEFORMABLE_MIRROR_SERVICE_H 43 | -------------------------------------------------------------------------------- /tests/test_service.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | def test_service_property(dummy_service): 4 | # We should be able to read and write to a property. 5 | dummy_service.readwrite_property = 2 6 | assert dummy_service.readwrite_property == 2 7 | 8 | def test_service_property_readonly(dummy_service): 9 | expected_value = dummy_service.config['readonly_property'] 10 | 11 | # We should be able to read from a readonly property. 12 | assert dummy_service.readonly_property == expected_value 13 | 14 | # Writing to a readonly property should yield an exception. 15 | with pytest.raises(RuntimeError): 16 | dummy_service.readonly_property = 3 17 | 18 | def test_service_command(dummy_service): 19 | a = 'a' 20 | b = 'b' 21 | 22 | assert dummy_service.add(a=a, b=b) == a + b 23 | 24 | def test_service_datastream(dummy_service): 25 | assert dummy_service.stream.dtype == 'float64' 26 | 27 | # Check that push_on_stream() submits something to the datastream. 28 | before_id = dummy_service.stream.get_latest_frame().id 29 | dummy_service.push_on_stream() 30 | after_id = dummy_service.stream.get_latest_frame().id 31 | 32 | assert after_id == before_id + 1 33 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Stable release 4 | 5 | To install {{ cookiecutter.project_name }}, run this command in your terminal: 6 | 7 | ```sh 8 | uv add {{ cookiecutter.pypi_package_name }} 9 | ``` 10 | 11 | Or if you prefer to use `pip`: 12 | 13 | ```sh 14 | pip install {{ cookiecutter.pypi_package_name }} 15 | ``` 16 | 17 | ## From source 18 | 19 | The source files for {{ cookiecutter.project_name }} can be downloaded from the [Github repo](https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}). 20 | 21 | You can either clone the public repository: 22 | 23 | ```sh 24 | git clone git://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }} 25 | ``` 26 | 27 | Or download the [tarball](https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/tarball/master): 28 | 29 | ```sh 30 | curl -OJL https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug }}/tarball/master 31 | ``` 32 | 33 | Once you have a copy of the source, you can install it with: 34 | 35 | ```sh 36 | cd {{ cookiecutter.project_slug }} 37 | uv pip install . 38 | ``` 39 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.full_name }} 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/src/{{cookiecutter.project_slug}}/config/__init__.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import os 3 | import importlib.resources 4 | 5 | import catkit2.config 6 | 7 | class _Path: 8 | def __init__(self, package, resource): 9 | self.package = package 10 | self.resource = resource 11 | self.name = resource 12 | self.parent = os.path.dirname(__file__) 13 | 14 | def read_text(self, encoding=None, errors=None): 15 | return importlib.resources.read_text( 16 | self.package, 17 | self.resource, 18 | encoding=encoding, 19 | errors=errors 20 | ) 21 | 22 | def read_config(additional_config_paths=None): 23 | files = [] 24 | 25 | if additional_config_paths is None: 26 | additional_config_paths = [] 27 | 28 | for fname in importlib.resources.contents(__package__): 29 | if fname.endswith('.yml'): 30 | files.append(_Path(__package__, fname)) 31 | 32 | for dirname in additional_config_paths: 33 | path = pathlib.Path(dirname) 34 | path = path.resolve() 35 | 36 | files.extend(path.glob('*.yml')) 37 | 38 | return catkit2.config.read_config_files(files) 39 | -------------------------------------------------------------------------------- /docs/services/thorlabs_mff101.rst: -------------------------------------------------------------------------------- 1 | Thorlabs Motorized Flip Mount 2 | ============================= 3 | 4 | This service operates a `Thorlabs MFF101 `_ 5 | motorized flip mount. 6 | 7 | Configuration 8 | ------------- 9 | 10 | .. code-block:: YAML 11 | 12 | beam_dump: 13 | service_type: thorlabs_mff101 14 | simulated_service_type: thorlabs_mff101_sim 15 | interface: flip_mount 16 | 17 | serial_number: 12345678 18 | positions: # named positions resolved by the proxy. 19 | in_beam: 1 20 | out_of_beam: 2 21 | nominal: out_of_beam 22 | 23 | Properties 24 | ---------- 25 | None. 26 | 27 | Commands 28 | -------- 29 | ``blink_led()``: This blinks the LED on the front panel of the flip mount for a short period. This can be used to identify the right flip mount on the bench if required. 30 | 31 | Datastreams 32 | ----------- 33 | ``current_position``: The current position of the flip mount. This can be set to either 1 or 2, indicating position 1 or 2. Other values will be silently ignored. 34 | 35 | ``commanded_position``: The commanded position of the flip mount. This can be set to either 1 or 2, indicating position 1 or 2. Other values will be silently ignored. 36 | -------------------------------------------------------------------------------- /benchmarks/pool_allocator.cpp: -------------------------------------------------------------------------------- 1 | #include "PoolAllocator.h" 2 | #include "Timing.h" 3 | #include "StructStream.h" 4 | #include "LocalMemory.h" 5 | #include 6 | 7 | void benchmark_linux_scalability() 8 | { 9 | const size_t N = 10000000; 10 | const size_t CAPACITY = 2 * N; 11 | 12 | auto buffer = LocalMemory::Create(PoolAllocator::GetSharedStateSize(CAPACITY)); 13 | auto stream = StructStream(buffer); 14 | 15 | auto allocator = PoolAllocator::Create(stream, CAPACITY); 16 | 17 | auto *handles = new PoolAllocator::BlockHandle[N]; 18 | 19 | auto start = GetTimeStamp(); 20 | 21 | for (size_t i = 0; i < N; ++i) 22 | { 23 | handles[i] = allocator->Allocate(); 24 | } 25 | 26 | for (size_t i = 0; i < N; ++i) 27 | { 28 | allocator->Release(handles[i]); 29 | } 30 | 31 | auto end = GetTimeStamp(); 32 | 33 | std::cout << "Linux Scalability:" << std::endl; 34 | std::cout << "Time: " << (end - start) / 1e9 << " sec" << std::endl; 35 | std::cout << "Throughput: " << 2 * N / ((end - start) / 1e9) << " ops/s" << std::endl; 36 | std::cout << "Time per operation: " << (end - start) / (2 * N) << " ns" << std::endl; 37 | 38 | delete[] handles; 39 | } 40 | 41 | int main(int argc, char **argv) 42 | { 43 | benchmark_linux_scalability(); 44 | 45 | return 0; 46 | } 47 | -------------------------------------------------------------------------------- /docs/services/thorlabs_cld101x.rst: -------------------------------------------------------------------------------- 1 | Thorlabs Compact Laser Diode 2 | ============================ 3 | 4 | Controls a Thorlabs Compact Laser Diode (CLD) using ``pyvisa``. This service requires the VXIpnp VISA Instrument Driver to be 5 | installed on the host system. The driver can be downloaded from the 6 | `Thorlabs website `_ and is also included 7 | on the CD that is shipped with the device. 8 | 9 | Successfully tested with the following devices: 10 | 11 | - Thorlabs CLD1010LP 12 | - Thorlabs CLD1011 13 | - Thorlabs CLD1015 14 | 15 | Configuration 16 | ------------- 17 | 18 | .. code-block:: YAML 19 | 20 | thorlabs_laser_diode_1: 21 | service_type: thorlabs_cld101x 22 | simulated_service_type: thorlabs_cld101x_sim 23 | requires_safety: false 24 | 25 | visa_id: USB::0x1313::0x804F::M00441199::INSTR 26 | wavelength: 640 27 | function_mode: current 28 | max_current: 0.255 29 | 30 | Properties 31 | ---------- 32 | None. 33 | 34 | Commands 35 | -------- 36 | None. 37 | 38 | Datastreams 39 | ----------- 40 | ``current_setpoint``: The current the laser diode is set to in A. 41 | 42 | ``current_percent``: The current the laser diode is set to in percent of its maximum current. -------------------------------------------------------------------------------- /catkit_core/ArrayView.cpp: -------------------------------------------------------------------------------- 1 | #include "ArrayView.h" 2 | 3 | #include 4 | 5 | bool ArrayInfo::IsCContiguous() const 6 | { 7 | size_t expected_stride = item_size; 8 | 9 | for (std::size_t i = 0; i < ndim; ++i) 10 | { 11 | if (strides[ndim - 1 - i] != expected_stride) 12 | return false; 13 | 14 | expected_stride *= shape[ndim - 1 - i]; 15 | } 16 | 17 | return true; 18 | } 19 | 20 | bool ArrayInfo::IsFContiguous() const 21 | { 22 | size_t expected_stride = item_size; 23 | 24 | for (std::size_t i = 0; i < ndim; ++i) 25 | { 26 | if (strides[i] != expected_stride) 27 | return false; 28 | 29 | expected_stride *= shape[i]; 30 | } 31 | 32 | return true; 33 | } 34 | 35 | std::size_t ArrayInfo::GetSize() const 36 | { 37 | std::size_t num_items = 1; 38 | 39 | for (std::size_t i = 0; i < ndim; ++i) 40 | { 41 | num_items *= shape[i]; 42 | } 43 | 44 | return num_items; 45 | } 46 | 47 | std::size_t ArrayInfo::GetSizeInBytes() const 48 | { 49 | return GetSize() * item_size; 50 | } 51 | 52 | bool ArrayView::IsAligned() const 53 | { 54 | return uintptr_t(data) % 128 == 0; 55 | } 56 | 57 | bool ArrayView::IsCContiguous() const 58 | { 59 | return info.IsCContiguous(); 60 | } 61 | 62 | bool ArrayView::IsFContiguous() const 63 | { 64 | return info.IsFContiguous(); 65 | } 66 | -------------------------------------------------------------------------------- /catkit2/testbed/proxies/flip_mount.py: -------------------------------------------------------------------------------- 1 | from ..service_proxy import ServiceProxy 2 | 3 | import numpy as np 4 | 5 | 6 | class FlipMountProxy(ServiceProxy): 7 | def move_to(self, position, wait=True): 8 | position = self.resolve_position(position) 9 | 10 | self.commanded_position.submit_data(np.array([position], dtype='int8')) 11 | 12 | if wait: 13 | while self.current_position.get()[0] != position: 14 | try: 15 | frame = self.current_position.get_next_frame(10) 16 | if frame.data[0] == position: 17 | break 18 | except Exception: 19 | # Timed out. No problem. 20 | pass 21 | 22 | def resolve_position(self, position): 23 | if isinstance(position, str): 24 | # The position is a named position. 25 | position = self.positions[position] 26 | 27 | # The position may still be a named position, so try to resolve deeper. 28 | return self.resolve_position(position) 29 | else: 30 | return position 31 | 32 | @property 33 | def positions(self): 34 | return self.config['positions'] 35 | 36 | def is_at(self, position): 37 | return self.current_position.get()[0] == self.resolve_position(position) 38 | -------------------------------------------------------------------------------- /catkit_core/PoolAllocator.h: -------------------------------------------------------------------------------- 1 | #ifndef POOL_ALLOCATOR_H 2 | #define POOL_ALLOCATOR_H 3 | 4 | #include "Shareable.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | // A simple lock-free pool allocator. 14 | class PoolAllocator : public Shareable 15 | { 16 | public: 17 | using BlockHandle = std::uint32_t; 18 | static const BlockHandle INVALID_HANDLE = std::numeric_limits::max(); 19 | 20 | static std::size_t GetSharedStateSize(std::uint32_t capacity); 21 | 22 | static std::shared_ptr Create(StructStream &stream, std::uint32_t capacity); 23 | static std::shared_ptr Open(StructStream &stream); 24 | 25 | BlockHandle Allocate(); 26 | bool Acquire(BlockHandle index); 27 | bool Release(BlockHandle index); 28 | 29 | ShareableType GetType() const override; 30 | 31 | private: 32 | PoolAllocator(std::uint32_t capacity, std::atomic *head, std::atomic *next, std::atomic_size_t *ref_count, std::shared_ptr memory_block); 33 | 34 | std::uint32_t m_Capacity; 35 | std::atomic *m_Head; 36 | std::atomic *m_Next; 37 | std::atomic_size_t *m_RefCount; 38 | 39 | static_assert(std::atomic::is_always_lock_free); 40 | }; 41 | 42 | #endif // POOL_ALLOCATOR_H 43 | -------------------------------------------------------------------------------- /catkit_core/Types.h: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_H 2 | #define TYPES_H 3 | 4 | #include "Tensor.h" 5 | #include "core.pb.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | class List; 14 | class Dict; 15 | 16 | class NoneValue 17 | { 18 | }; 19 | 20 | using Value = std::variant< 21 | NoneValue, 22 | std::int64_t, 23 | double, 24 | std::string, 25 | bool, 26 | Dict, 27 | List, 28 | Tensor>; 29 | 30 | class List : public std::list 31 | { 32 | }; 33 | 34 | class Dict : public std::map 35 | { 36 | }; 37 | 38 | void ToProto(const Value &value, catkit_proto::Value *proto_value); 39 | void ToProto(const List &list, catkit_proto::List *proto_list); 40 | void ToProto(const Dict &dict, catkit_proto::Dict *proto_dict); 41 | 42 | void FromProto(const catkit_proto::Value *proto_value, Value &value); 43 | void FromProto(const catkit_proto::List *proto_list, List &list); 44 | void FromProto(const catkit_proto::Dict *proto_dict, Dict &dict); 45 | 46 | template 47 | T CastTo(const Value &val) 48 | { 49 | return std::visit([](auto &&val) 50 | { 51 | if constexpr(std::is_convertible_v) 52 | { 53 | return T(val); 54 | } 55 | else 56 | { 57 | throw std::bad_variant_access{}; 58 | return T{}; 59 | } 60 | }, val); 61 | } 62 | 63 | #endif // TYPES_H 64 | -------------------------------------------------------------------------------- /docs/services/ni_daq.rst: -------------------------------------------------------------------------------- 1 | NI DAQ 2 | ====== 3 | 4 | This service controls a National Instruments DAQ card. It is implemented similarly to a deformable mirror in such that 5 | it has a set of virtual channels that can be controlled independently. The total voltage output by the NI DAQ is the sum 6 | of all the channels. 7 | 8 | The service requires installation of the NI-DAQmx driver. The driver can be downloaded from the National Instruments website. 9 | It uses the Python API provided by the ``nidaqmx`` package. 10 | 11 | Configuration 12 | ------------- 13 | 14 | .. code-block:: YAML 15 | 16 | piezo_tip_tilt: 17 | service_type: ni_daq 18 | simulated_service_type: ni_daq_sim 19 | interface: ni_daq 20 | requires_safety: false 21 | 22 | device_name: Dev1 23 | daq_input_channels: [] 24 | daq_output_channels: [ao0, ao1] 25 | volt_limit_min: -2. 26 | volt_limit_max: 2. 27 | 28 | channels: 29 | - target_acquisition 30 | - aberration 31 | - correction 32 | 33 | Properties 34 | ---------- 35 | None. 36 | 37 | Commands 38 | -------- 39 | None. 40 | 41 | Datastreams 42 | ----------- 43 | ``total_voltage``: The total voltage output by the NI DAQ. This is the sum of the voltages output by each virtual channel. 44 | 45 | ``channels[channel_name]``: The command per virtual channel, identified by channel name. 46 | -------------------------------------------------------------------------------- /catkit2/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | 3 | project(catkit_bindings) 4 | 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED on) 7 | 8 | set(SOURCES "bindings.cpp") 9 | 10 | find_package(pybind11 REQUIRED) 11 | pybind11_add_module(catkit_bindings ${SOURCES}) 12 | 13 | find_package(pybind11_json REQUIRED) 14 | target_include_directories(catkit_bindings PUBLIC pybind11_json_INCLUDE_DIRS) 15 | 16 | target_include_directories(catkit_bindings PUBLIC ../catkit_core) 17 | 18 | target_link_libraries(catkit_bindings PUBLIC catkit_core) 19 | 20 | # Compile protobuf files. 21 | find_package(Protobuf REQUIRED) 22 | 23 | set(PROTO_FILES 24 | ${CMAKE_CURRENT_SOURCE_DIR}/../proto/core.proto 25 | ${CMAKE_CURRENT_SOURCE_DIR}/../proto/logging.proto 26 | ${CMAKE_CURRENT_SOURCE_DIR}/../proto/service.proto 27 | ${CMAKE_CURRENT_SOURCE_DIR}/../proto/testbed.proto 28 | ${CMAKE_CURRENT_SOURCE_DIR}/../proto/tracing.proto 29 | ) 30 | 31 | set(PYTHON_OUT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/./proto/) 32 | 33 | add_custom_target( 34 | catkit2_python_protobuf ALL 35 | COMMAND ${Protobuf_PROTOC_EXECUTABLE} 36 | --proto_path=${CMAKE_CURRENT_SOURCE_DIR}/../proto/ 37 | --python_out=${PYTHON_OUT_DIR} 38 | ${PROTO_FILES} 39 | DEPENDS ${PROTO_FILES} 40 | ) 41 | 42 | install(TARGETS catkit_bindings LIBRARY DESTINATION "${SKBUILD_PLATLIB_DIR}/catkit2/") 43 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | # conda env create --file environment.yml 2 | # conda env update --file environment.yml --prune 3 | # or, if you're not wanting to use the default env name: 4 | # conda env create --name --file environment.yml 5 | # conda env update --name =4.0.0 16 | - conda-forge::pyvisa>=1.10 17 | - conda-forge::pyvisa-py 18 | - conda-forge::nidaqmx-python 19 | - conda-forge::seabreeze 20 | - pyserial 21 | - pysnmp 22 | - requests 23 | - ftd2xx 24 | - conda-forge::asdf 25 | - pyyaml 26 | - psutil 27 | - protobuf==3.20.3 28 | - libprotobuf==3.20.3 29 | - conda-forge::hcipy 30 | - conda-forge::cppzmq==4.8.1 31 | - conda-forge::pybind11==2.13.6 32 | - conda-forge::eigen==3.4.0 33 | - conda-forge::nlohmann_json==3.9.1 34 | - conda-forge::pybind11_json 35 | - conda-forge::cfitsio 36 | - pip 37 | - cmake>=3.0.0 38 | - doxygen 39 | - sphinx 40 | - conda-forge::sphinx-automodapi 41 | - conda-forge::breathe==4.32.0 42 | - conda-forge::sphinx_rtd_theme 43 | - networkx 44 | - pytest 45 | - flake8 46 | - h5py 47 | - importlib_metadata 48 | - fasteners 49 | - textual 50 | - pip: 51 | - dcps 52 | - zwoasi>=0.0.21 53 | - cookiecutter 54 | -------------------------------------------------------------------------------- /benchmarks/datastream_submit.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "DataStream.h" 6 | #include "Timing.h" 7 | 8 | const size_t NUM_ITERATIONS = 10000; 9 | const size_t NUM_FRAMES_IN_BUFFER = 20; 10 | 11 | void benchmark(size_t N, bool with_data) 12 | { 13 | auto stream_name = std::to_string(GetTimeStamp()); 14 | auto stream = DataStream::Create(stream_name, "benchmark", DataType::DT_FLOAT64, {N, N}, NUM_FRAMES_IN_BUFFER); 15 | 16 | char *data = new char[N * N * 8]; 17 | 18 | auto start = GetTimeStamp(); 19 | 20 | for (size_t j = 0; j < NUM_ITERATIONS; ++j) 21 | { 22 | if (with_data) 23 | { 24 | stream->SubmitData(data); 25 | } 26 | else 27 | { 28 | auto frame = stream->RequestNewFrame(); 29 | stream->SubmitFrame(frame.m_Id); 30 | } 31 | } 32 | 33 | auto end = GetTimeStamp(); 34 | auto time_per_iteration = double(end - start) / NUM_ITERATIONS; 35 | 36 | delete[] data; 37 | 38 | std::cout << N << "x" << N << ": " << time_per_iteration << " ns per submit" << std::endl; 39 | } 40 | 41 | int main(int argc, char *argv[]) 42 | { 43 | std::vector Ns = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048}; 44 | 45 | for (auto &with_data : {true, false}) 46 | { 47 | std::cout << (with_data ? "With" : "Without") << " copying data:" << std::endl; 48 | 49 | for (auto &N : Ns) 50 | { 51 | benchmark(N, with_data); 52 | } 53 | } 54 | 55 | return 0; 56 | } 57 | -------------------------------------------------------------------------------- /catkit_core/Util.cpp: -------------------------------------------------------------------------------- 1 | #include "Util.h" 2 | 3 | #include "Timing.h" 4 | 5 | #ifdef _WIN32 6 | #define WIN32_LEAN_AND_MEAN 7 | #include 8 | #else 9 | #include 10 | #endif // _WIN32 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | int GetProcessId() 18 | { 19 | #ifdef _WIN32 20 | static int process_id(GetCurrentProcessId()); 21 | return process_id; 22 | #else 23 | static int process_id(getpid()); 24 | return process_id; 25 | #endif // _WIN32 26 | } 27 | 28 | int GetThreadId() 29 | { 30 | static std::atomic_int next_thread_id(0); 31 | thread_local static int thread_id(next_thread_id++); 32 | 33 | return thread_id; 34 | } 35 | 36 | void Sleep(double sleep_time_in_sec, std::function cancellation_callback) 37 | { 38 | Timer timer; 39 | 40 | while (true) 41 | { 42 | double sleep_remaining = sleep_time_in_sec - timer.GetTime(); 43 | 44 | // Sleep is over when timer has expired. 45 | if (sleep_remaining < 0) 46 | break; 47 | 48 | // Sleep is over when cancellation is requested. 49 | if (cancellation_callback) 50 | { 51 | if (cancellation_callback()) 52 | break; 53 | } 54 | 55 | // Use brackets around std::min to avoid the macro from windows.h. Sigh. 56 | double this_sleep_time = (std::min)(double(0.001), sleep_remaining); 57 | 58 | std::this_thread::sleep_for(std::chrono::duration(this_sleep_time)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /catkit_core/Shareable.cpp: -------------------------------------------------------------------------------- 1 | #include "Shareable.h" 2 | 3 | #include 4 | 5 | #include "DataStream.h" 6 | #include "Event.h" 7 | #include "PoolAllocator.h" 8 | #include "SharedMemory.h" 9 | #include "LocalMemory.h" 10 | #include "HashMap.h" 11 | #include "LocalMessageBroker.h" 12 | #include "BuddyAllocator.h" 13 | #include "HybridPoolAllocator.h" 14 | 15 | Shareable::Shareable(std::shared_ptr memory_block) 16 | : m_MemoryBlock(memory_block) 17 | { 18 | } 19 | 20 | std::shared_ptr Shareable::Open(StructStream &stream) 21 | { 22 | ShareableType type = *stream.Extract(); 23 | 24 | switch (type) 25 | { 26 | case ShareableType::DataStream: 27 | return DataStream::Open(stream); 28 | case ShareableType::Event: 29 | return Event::Open(stream); 30 | case ShareableType::PoolAllocator: 31 | return PoolAllocator::Open(stream); 32 | case ShareableType::SharedMemory: 33 | return SharedMemory::Open(stream); 34 | case ShareableType::LocalMemory: 35 | return LocalMemory::Open(stream); 36 | case ShareableType::HashMap: 37 | return HashMap::Open(stream); 38 | case ShareableType::LocalMessageBroker: 39 | return LocalMessageBroker::Open(stream); 40 | case ShareableType::BuddyAllocator: 41 | return BuddyAllocator::Open(stream); 42 | case ShareableType::HybridPoolAllocator: 43 | return HybridPoolAllocator::Open(stream); 44 | default: 45 | throw std::runtime_error("Unknown shareable type."); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/services/dummy_service/dummy_service.py: -------------------------------------------------------------------------------- 1 | from catkit2 import Service 2 | 3 | import numpy as np 4 | 5 | N = 16 6 | 7 | class DummyService(Service): 8 | def __init__(self): 9 | super().__init__('dummy_service') 10 | 11 | self.readonly_property = self.config['readonly_property'] 12 | self.readwrite_property = 1 13 | 14 | def open(self): 15 | self.make_property('readonly_property', self.get_readonly) 16 | self.make_property('readwrite_property', self.get_readwrite, self.set_readwrite) 17 | 18 | self.make_command('add', self.add) 19 | self.make_command('push_on_stream', self.push_on_stream) 20 | 21 | self.stream = self.make_data_stream('stream', 'float64', [N, N], 20) 22 | self.push_on_stream() 23 | 24 | def main(self): 25 | while not self.should_shut_down: 26 | self.sleep(0.1) 27 | 28 | def close(self): 29 | pass 30 | 31 | def get_readonly(self): 32 | return self.readonly_property 33 | 34 | def get_readwrite(self): 35 | return self.readwrite_property 36 | 37 | def set_readwrite(self, value): 38 | self.readwrite_property = value 39 | 40 | def add(self, a, b): 41 | return a + b 42 | 43 | def push_on_stream(self): 44 | arr = np.random.randn(N, N).astype('float64') 45 | self.stream.submit_data(arr) 46 | 47 | if __name__ == '__main__': 48 | service = DummyService() 49 | service.run() 50 | -------------------------------------------------------------------------------- /docs/services/oceanoptics_spectrometer.rst: -------------------------------------------------------------------------------- 1 | Ocean Optics Spectrometer 2 | ========================= 3 | 4 | This service controls an Ocean Optics Spectrometer. It is a wrapper around the python-seabreeze package. 5 | The python-seabreeze package needs to be installed first, and the page also explain how to install the 6 | spectrometer drivers needed for windows. 7 | 8 | python-seabreeze package: `https://python-seabreeze.readthedocs.io/ `_ 9 | 10 | The service has been successfully tested with the following ocean optics spectrometer: 11 | 12 | - USB4000 Spectrometer 13 | 14 | Configuration 15 | ------------- 16 | 17 | .. code-block:: YAML 18 | 19 | spectrometer: 20 | service_type: oceanoptics_spectrometer 21 | simulated_service_type: oceanoptics_spectrometer_sim 22 | interface: oceanoptics_spectrometer 23 | requires_safety: false 24 | 25 | serial_number: USB4C01580 # Serial number of the spectrometer. 26 | exposure_time: 1000 # Exposure time of the spectrometer in microseconds. 27 | interval: 0.01 # Interval between measurements, in seconds. 28 | 29 | Properties 30 | ---------- 31 | ``exposure_time``: Exposure time of the spectrometer in microseconds. 32 | 33 | ``wavelengths``: Wavelengths in (nm) corresponding to each pixel of the spectrometer 34 | 35 | Commands 36 | -------- 37 | 38 | Datastreams 39 | ----------- 40 | ``spectra``: Spectra acquired by the camera. 41 | 42 | ``is_saturating``: If the intensity as reached the maximum value of the spectrometer. -------------------------------------------------------------------------------- /catkit_core/HostName.cpp: -------------------------------------------------------------------------------- 1 | #include "HostName.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifdef _WIN32 10 | #define WIN32_LEAN_AND_MEAN 11 | #include 12 | #include 13 | #else 14 | #include 15 | #endif 16 | 17 | std::string GetHostNameImpl() 18 | { 19 | #ifdef _WIN32 20 | TCHAR info_buf[MAX_COMPUTERNAME_LENGTH + 1]; 21 | DWORD buf_char_count = MAX_COMPUTERNAME_LENGTH + 1; 22 | 23 | if (GetComputerName(info_buf, &buf_char_count)) 24 | { 25 | #ifdef UNICODE 26 | std::vector buffer; 27 | 28 | int size = WideCharToMultiByte(CP_UTF8, 0, info_buf, -1, NULL, 0, NULL, NULL); 29 | 30 | if (size > 0) 31 | { 32 | buffer.resize(size); 33 | WideCharToMultiByte(CP_UTF8, 0, info_buf, -1, static_cast(&buffer[0]), buffer.size(), NULL, NULL); 34 | } 35 | else 36 | { 37 | throw std::runtime_error("Host name cannot be converted to ASCII."); 38 | } 39 | 40 | return std::string(&buffer[0]); 41 | #else 42 | return std::string(info_buf); 43 | #endif 44 | } 45 | else 46 | { 47 | return "unknown"; 48 | } 49 | #else 50 | char name[150]; 51 | memset(name, 0, 150); 52 | 53 | // Get a hostname, but ensure that it's zero terminated at all costs. 54 | if (gethostname(name, 150 - 1)) 55 | throw std::runtime_error("Host name could not be retrieved."); 56 | 57 | return name; 58 | #endif 59 | } 60 | 61 | std::string_view GetHostName() 62 | { 63 | static std::string host_name = GetHostNameImpl(); 64 | 65 | return host_name; 66 | } 67 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Catkit2 2 | ======= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | :caption: Getting Started 7 | 8 | installation 9 | overview 10 | acknowledging_catkit2 11 | contribution 12 | testbed_implementation 13 | 14 | .. toctree:: 15 | :maxdepth: 1 16 | :caption: User Guide 17 | 18 | configuration 19 | protocol 20 | services 21 | benchmarks 22 | safety 23 | 24 | .. toctree:: 25 | :maxdepth: 1 26 | :caption: Built-In Services 27 | 28 | services/accufiz_interferometer 29 | services/aimtti_plp 30 | services/allied_vision_camera 31 | services/bmc_deformable_mirror 32 | services/camera_sim 33 | services/deformable_mirror 34 | services/empty_service 35 | services/flir_camera 36 | services/hamamatsu_camera 37 | services/newport_picomotor 38 | services/newport_xps_q8 39 | services/ni_daq 40 | services/nkt_superk_evo 41 | services/nkt_superk_fianium 42 | services/oceanoptics_spectrometer 43 | services/omega_ithx_w3 44 | services/phasics_cam 45 | services/physik_stage_controller 46 | services/safety_manual_check 47 | services/safety_monitor 48 | services/snmp_ups 49 | services/thorlabs_cld101x 50 | services/thorlabs_cube_motor_kinesis 51 | services/thorlabs_fw102c 52 | services/thorlabs_mff101 53 | services/thorlabs_pm 54 | services/thorlabs_tsp01 55 | services/watchdog 56 | services/web_power_switch 57 | services/zwo_camera 58 | 59 | .. toctree:: 60 | :maxdepth: 1 61 | :caption: API Documentation 62 | 63 | catkit2 64 | catkit_core 65 | -------------------------------------------------------------------------------- /LICENSE.rst: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, AURA 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: 6 | - develop 7 | pull_request: 8 | types: [opened, synchronize, ready_for_review] 9 | branches: 10 | - develop 11 | 12 | jobs: 13 | pytest: 14 | name: Pytest on ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: [ubuntu-22.04, windows-latest, macos-latest] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: conda-incubator/setup-miniconda@v2 24 | with: 25 | miniconda-version: "latest" 26 | auto-update-conda: true 27 | auto-activate-base: false 28 | - name: Switch to gcc-10 on linux 29 | if: runner.os == 'Linux' 30 | run: | 31 | sudo apt install gcc-10 g++-10 cpp-10 32 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 33 | - name: Install dependencies 34 | run: | 35 | conda env create --name ci-env --file environment.yml 36 | shell: bash -l {0} 37 | - name: Print debugging information 38 | run: | 39 | conda activate ci-env 40 | conda list 41 | shell: bash -l {0} 42 | - name: Install catkit2 43 | run: | 44 | conda activate ci-env 45 | pip install -v -e . 46 | shell: bash -l {0} 47 | - name: Run tests 48 | run: | 49 | conda activate ci-env 50 | pytest -v 51 | shell: bash -l {0} 52 | -------------------------------------------------------------------------------- /catkit2/testbed/proxies/camera.py: -------------------------------------------------------------------------------- 1 | from ..service_proxy import ServiceProxy 2 | 3 | import warnings 4 | 5 | 6 | class CameraProxy(ServiceProxy): 7 | def take_raw_exposures(self, num_exposures): 8 | was_acquiring = self.is_acquiring.get()[0] 9 | 10 | if not was_acquiring: 11 | self.start_acquisition() 12 | 13 | first_frame_id = self.images.newest_available_frame_id 14 | 15 | if was_acquiring: 16 | # Ignore first two frames to ensure the frames were taken _after_ the 17 | # call to this function. This is not necessary if the acquisition just 18 | # started. 19 | first_frame_id += 2 20 | 21 | try: 22 | i = 0 23 | num_exposures_remaining = num_exposures 24 | 25 | while num_exposures_remaining >= 1: 26 | try: 27 | frame = self.images.get_frame(first_frame_id + i, 100000) 28 | except RuntimeError: 29 | # The frame wasn't available anymore because we were waiting too long. 30 | continue 31 | finally: 32 | i += 1 33 | 34 | yield frame.data.copy() 35 | num_exposures_remaining -= 1 36 | finally: 37 | if not was_acquiring: 38 | self.end_acquisition() 39 | 40 | def take_exposures(self, *args, **kwargs): 41 | warnings.warn('Please use camera.take_raw_exposures() instead.', DeprecationWarning) 42 | yield from self.take_raw_exposures(*args, **kwargs) 43 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint. 2 | # For more information see: https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-python 3 | 4 | name: Test Python application 5 | 6 | on: 7 | push: 8 | branches: [ "main", "master" ] 9 | pull_request: 10 | branches: [ "main", "master" ] 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | build: 17 | strategy: 18 | matrix: 19 | # Test all supported Python versions under Ubuntu 20 | os: [ubuntu-latest] 21 | python-version: ['3.12', '3.13'] 22 | 23 | runs-on: {% raw %}${{ matrix.os }}{% endraw %} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: Set up Python {% raw %}${{ matrix.python-version }}{% endraw %} 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: {% raw %}${{ matrix.python-version }}{% endraw %} 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install -r requirements_dev.txt 35 | - name: Lint with ruff 36 | run: | 37 | # stop the build if there are Python syntax errors or undefined names 38 | ruff check --select=E9,F63,F7,F82 39 | # exit-zero treats all errors as warnings 40 | ruff check --exit-zero --statistics 41 | - name: Install project 42 | run: | 43 | pip install . 44 | - name: Run tests 45 | run: | 46 | pytest 47 | -------------------------------------------------------------------------------- /catkit_core/RefCounter.h: -------------------------------------------------------------------------------- 1 | #ifndef REF_COUNTER_H 2 | #define REF_COUNTER_H 3 | 4 | #include 5 | #include 6 | 7 | // A wait-free reference counter. 8 | // 9 | // This implementation is based on Daniel Anderson's CppCon 2024 talk. 10 | template 11 | class RefCounter 12 | { 13 | static_assert(std::is_integral_v, "RefCounter can only be used with integral types."); 14 | static_assert(std::is_unsigned_v, "RefCounter can only be used with unsigned types."); 15 | static_assert(std::atomic::is_always_lock_free, "RefCounter requires atomic operations to be lock-free."); 16 | 17 | private: 18 | // MSB of the counter indicates whether it's zero or not. 19 | static constexpr T is_zero = T(1) << (8 * sizeof(T) - 1); 20 | 21 | std::atomic m_Counter; 22 | 23 | public: 24 | inline RefCounter() : m_Counter(1) 25 | { 26 | } 27 | 28 | inline bool Increment() 29 | { 30 | // Check if the MSB was set before incrementing. 31 | return (m_Counter.fetch_add(1) & is_zero) == 0; 32 | } 33 | 34 | inline bool Decrement() 35 | { 36 | if (m_Counter.fetch_sub(1) == 1) 37 | { 38 | // The counter is now zero. We need to set the MSB to indicate this. 39 | T e = 0; 40 | 41 | // If we fail, it means that someone else incremented the ref counter before us. 42 | // Increment linearizes before decrement, so the counter wasn't "actually" zero. 43 | return m_Counter.compare_exchange_strong(e, is_zero); 44 | } 45 | 46 | return false; 47 | } 48 | 49 | inline void Reset() 50 | { 51 | m_Counter.store(1, std::memory_order_relaxed); 52 | } 53 | }; 54 | 55 | #endif // REF_COUNTER_H 56 | -------------------------------------------------------------------------------- /catkit_core/BuddyAllocator.h: -------------------------------------------------------------------------------- 1 | #ifndef BUDDY_ALLOCATOR_H 2 | #define BUDDY_ALLOCATOR_H 3 | 4 | #include "Shareable.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | class BuddyAllocator : public Shareable 13 | { 14 | public: 15 | virtual ~BuddyAllocator() = default; 16 | 17 | static std::shared_ptr Create(StructStream &stream, std::size_t max_size, std::size_t min_size); 18 | static std::shared_ptr Open(StructStream &stream); 19 | 20 | ShareableType GetType() const override; 21 | static std::size_t GetSharedStateSize(std::size_t max_size, std::size_t min_size); 22 | 23 | using Handle = std::size_t; 24 | 25 | static constexpr Handle INVALID_HANDLE = 0; 26 | 27 | Handle Allocate(std::size_t size); 28 | bool Acquire(Handle handle); 29 | bool Release(Handle handle); 30 | 31 | std::size_t GetOffset(Handle handle) const; 32 | 33 | void PrintState() const; 34 | 35 | private: 36 | BuddyAllocator(std::size_t capacity, std::size_t min_size, std::atomic_uint16_t *tree, std::atomic_size_t *last_success, std::shared_ptr memory_block); 37 | 38 | Handle TryAllocate(Handle index); 39 | 40 | void FreeNode(Handle handle, std::size_t upper_bound); 41 | void Unmark(Handle handle, std::size_t upper_bound); 42 | 43 | std::size_t GetLevel(Handle n) const; 44 | std::size_t GetSize(Handle n) const; 45 | 46 | std::size_t m_Capacity; 47 | std::size_t m_MinSize; 48 | std::size_t m_Depth; 49 | 50 | std::atomic_uint16_t *m_Tree; 51 | std::atomic_size_t *m_LastSuccessfulAllocation; 52 | }; 53 | 54 | #endif // BUDDY_ALLOCATOR_H 55 | -------------------------------------------------------------------------------- /docs/acknowledging_catkit2.rst: -------------------------------------------------------------------------------- 1 | Acknowledging Catkit2 2 | ====================== 3 | 4 | If you use catkit2 for your own research, we ask you to acknowledge the use of catkit2 in your publications, 5 | presentations, and other materials. 6 | 7 | For presentations, we ask you to mention at least: 8 | 9 | *catkit2 (Por et al. 2024)* 10 | 11 | For written publications, we ask you to cite the `Zenodo DOI `__. If there is no appropriate place in the 12 | body text to cite it, you can include something along the lines of the following in your acknowledgements: 13 | 14 | *This research made use of catkit2, an open-source package for controlling testbed hardware* (`Por et al. 2024 `__). 15 | 16 | The Zenodo citation can be found below: 17 | 18 | .. code-block:: bib 19 | 20 | @software{por_2024_11395554, 21 | author = {Por, Emiel, H. and 22 | Laginja, Iva and 23 | Pourcelot, Raphaël and 24 | Soummer, Rémi and 25 | Sevin, Arnaud and 26 | Sahoo, Ananya and 27 | Nguyen, Meiji and 28 | Fowler, Jules and 29 | Egger, Léo and 30 | Pougheon, Erin and 31 | Demagny, Augustin}, 32 | title = {spacetelescope/catkit2}, 33 | month = may, 34 | year = 2024, 35 | publisher = {Zenodo}, 36 | version = {v0.6.1}, 37 | doi = {10.5281/zenodo.11395554}, 38 | url = {https://doi.org/10.5281/zenodo.11395554}, 39 | } 40 | -------------------------------------------------------------------------------- /docs/services/web_power_switch.rst: -------------------------------------------------------------------------------- 1 | Web Power Switch 2 | ================ 3 | 4 | This service controls a power switch that is controllable over the internet. 5 | 6 | So far catkit2 has been tested on the following web power switches: 7 | 8 | - `LPC7-PRO from TeleDynamics `_ (for more information, see the `Spec Sheet `_ and `User Manual `_. Note: no software driver installation required) 9 | 10 | Configuration 11 | ------------- 12 | 13 | .. code-block:: YAML 14 | 15 | web_power_switch1: 16 | service_type: web_power_switch 17 | simulated_service_type: web_power_switch_sim 18 | interface: web_power_switch 19 | requires_safety: false 20 | 21 | user: username 22 | password: password 23 | ip_address: 000.000.000.00 24 | dns: domain_name_system 25 | 26 | # Plugged-in devices with their outlet number: 27 | outlets: 28 | npoint_tiptilt_lc_400: 1 29 | iris_usb_hub: 2 30 | newport_xps_q8: 3 31 | iris_dm: 4 32 | quad_cell: 5 33 | air_valve: 6 34 | pupil_led: 7 35 | fpm_led: 8 36 | 37 | Properties 38 | ---------- 39 | None. 40 | 41 | Commands 42 | -------- 43 | None. 44 | 45 | Datastreams 46 | ----------- 47 | ``{outlet_name}``: The name of an outlet. These are defined by the config file (see the sample Configuration section above where the names of eight example outlets are given). 48 | -------------------------------------------------------------------------------- /catkit_core/Log.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | #include "Timing.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | using namespace std; 9 | 10 | LogEntry::LogEntry(std::string filename, unsigned int line, std::string function, Severity severity, std::string message, uint64_t timestamp) 11 | : filename(filename), line(line), function(function), severity(severity), message(message), timestamp(timestamp) 12 | { 13 | time = ConvertTimestampToString(timestamp); 14 | } 15 | 16 | vector log_listeners; 17 | 18 | LogListener::LogListener() 19 | { 20 | // Add myself to the log_listeners vector. 21 | log_listeners.push_back(this); 22 | } 23 | 24 | LogListener::~LogListener() 25 | { 26 | // Remove myself from the log_listeners vector (with erase-remove). 27 | log_listeners.erase(std::remove(log_listeners.begin(), log_listeners.end(), this), log_listeners.end()); 28 | } 29 | 30 | void LogListener::AddLogEntry(const LogEntry &entry) 31 | { 32 | } 33 | 34 | void SubmitLogEntry(std::string filename, unsigned int line, std::string function, Severity severity, std::string message) 35 | { 36 | auto entry = LogEntry(filename, line, function, severity, message, GetTimeStamp()); 37 | for (auto listener : log_listeners) 38 | { 39 | listener->AddLogEntry(entry); 40 | } 41 | } 42 | 43 | std::string ConvertSeverityToString(Severity severity) 44 | { 45 | switch (severity) 46 | { 47 | case S_CRITICAL: 48 | return "critical"; 49 | case S_ERROR: 50 | return "error"; 51 | case S_WARNING: 52 | return "warning"; 53 | case S_INFO: 54 | return "info"; 55 | case S_DEBUG: 56 | return "debug"; 57 | default: 58 | return "undefined"; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /catkit_core/Event.h: -------------------------------------------------------------------------------- 1 | #ifndef EVENT_H 2 | #define EVENT_H 3 | 4 | #include "EventBase.h" 5 | #include "Shareable.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | enum class EventWaitMethod 12 | { 13 | ConditionVariable, 14 | Futex, 15 | Semaphore, 16 | SpinLock, 17 | #ifdef _WIN32 18 | Default = Semaphore 19 | #elif defined(__linux__) 20 | Default = Futex 21 | #elif defined(__APPLE__) 22 | Default = ConditionVariable 23 | #endif 24 | }; 25 | 26 | class Event : public Shareable 27 | { 28 | private: 29 | Event(std::shared_ptr memory_block); 30 | 31 | static const int EVENT_ID_MAX_SIZE = 256; 32 | 33 | struct Header 34 | { 35 | std::array m_Id; 36 | 37 | EventConditionVariable::SharedState m_ConditionVariable; 38 | EventFutex::SharedState m_Futex; 39 | EventSemaphore::SharedState m_Semaphore; 40 | EventSpinLock::SharedState m_SpinLock; 41 | }; 42 | 43 | public: 44 | void Wait(double timeout_in_sec, std::function condition, EventWaitMethod wait_method = EventWaitMethod::Default, void (*error_check)() = nullptr); 45 | void Signal(); 46 | 47 | static std::shared_ptr Create(StructStream &stream, std::string_view id); 48 | static std::shared_ptr Open(StructStream &stream); 49 | 50 | static constexpr std::size_t GetSharedStateSize() 51 | { 52 | return sizeof(Header); 53 | } 54 | 55 | ShareableType GetType() const override; 56 | 57 | private: 58 | std::shared_ptr m_ConditionVariable; 59 | std::shared_ptr m_Futex; 60 | std::shared_ptr m_Semaphore; 61 | std::shared_ptr m_SpinLock; 62 | }; 63 | 64 | #endif // EVENT_H 65 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from catkit2 import TestbedProxy, Testbed, read_config_files 2 | 3 | import pytest 4 | import socket 5 | import pathlib 6 | import os 7 | import multiprocessing 8 | 9 | @pytest.fixture() 10 | def unused_port(): 11 | def get(): 12 | with socket.socket() as sock: 13 | sock.bind(('', 0)) 14 | return sock.getsockname()[1] 15 | 16 | return get 17 | 18 | def run_testbed(port, config): 19 | is_simulated = False 20 | 21 | testbed = Testbed(port, is_simulated, config) 22 | 23 | # Add test services manually for testing purposes. 24 | base_path = os.path.dirname(__file__) 25 | testbed.register_service_type('dummy_service', os.path.join(base_path, 'services/dummy_service/dummy_service.py')) 26 | testbed.register_service_type('dummy_dm_service', os.path.join(base_path, 'services/dummy_dm_service/dummy_dm_service.py')) 27 | 28 | testbed.run() 29 | 30 | @pytest.fixture(scope='session') 31 | def testbed(): 32 | config_path = pathlib.Path(os.path.join(os.path.dirname(__file__), 'config')) 33 | config_files = config_path.resolve().glob('*.yml') 34 | config = read_config_files(config_files) 35 | 36 | port = config['testbed']['default_port'] 37 | 38 | process = multiprocessing.Process(target=run_testbed, args=(port, config)) 39 | process.start() 40 | 41 | testbed = TestbedProxy('127.0.0.1', port) 42 | 43 | yield testbed 44 | 45 | testbed.shut_down() 46 | process.join() 47 | 48 | @pytest.fixture(scope='session') 49 | def dummy_service(testbed): 50 | testbed.start_service('dummy_service') 51 | 52 | yield testbed.dummy_service 53 | 54 | testbed.stop_service('dummy_service') 55 | -------------------------------------------------------------------------------- /docs/services/safety_monitor.rst: -------------------------------------------------------------------------------- 1 | Safety Monitor 2 | ============== 3 | 4 | This service periodically checks datastreams from other services and makes sure these are within specified bounds, and are not stale (were submitted within a certain timeframe before the check). Only the zeroth element of the datastream is checked. Each check interval, an update is submitted to the ``is_safe`` datastream. 5 | 6 | Configuration 7 | ------------- 8 | 9 | .. code-block:: YAML 10 | 11 | safety: 12 | service_type: safety_monitor 13 | 14 | check_interval: 60 # seconds 15 | 16 | safeties: 17 | humidity_dm: 18 | service_id: omega_dm 19 | stream_name: humidity 20 | minimum_value: 1 21 | maximum_value: 28 22 | safe_interval: 60 # seconds 23 | temperature_dm: 24 | service_id: omega_dm 25 | stream_name: temperature 26 | minimum_value: 0 27 | maximum_value: 29 28 | safe_interval: 60 # seconds 29 | lab_ups: 30 | service_id: lab_ups 31 | stream_name: power_ok 32 | minimum_value: 0.5 33 | maximum_value: 1.5 34 | safe_interval: 60 # seconds 35 | 36 | Properties 37 | ---------- 38 | 39 | ``checked_safeties`` A list of safety names as described by the configuration file. The ``i``-th element indicates the safety of the ``i``-th element in the ``is_safe`` datastream. 40 | 41 | Commands 42 | ---------- 43 | None. 44 | 45 | Datastreams 46 | ----------- 47 | 48 | ``is_safe``: Whether the checked safety is safe or not. This datastream includes one element for each checked safety, with the ordering the same as the ``checked_safeties`` list property. 49 | -------------------------------------------------------------------------------- /proto/logging.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "core.proto"; 4 | 5 | package catkit_proto.logging; 6 | 7 | // Wraps two Numpy arrays to form a, for example, contrast curve. 8 | message Curve 9 | { 10 | Tensor x = 1; 11 | Tensor y = 2; 12 | } 13 | 14 | // Wraps a Figure from Matplotlib, saved in png format. 15 | message Figure 16 | { 17 | bytes png = 1; 18 | } 19 | 20 | // Wraps an external reference to a file. 21 | message File 22 | { 23 | // This URI is relative to the experiment directory. 24 | string uri = 1; 25 | } 26 | 27 | message DataLogEntry 28 | { 29 | // The wall clock time as a Unix time stamp. 30 | double wall_time = 1; 31 | 32 | // The tag of this event. This describes what is contained in 33 | // the value. Example: "contrast" or "dark_zone_SNR". 34 | string tag = 2; 35 | 36 | // The type of value that is wrapped. This can be either 37 | // the type of value in the binary file, or, otherwise, 38 | // an arbitrary string. In these cases, the value is not 39 | // stored inside the binary file. 40 | string value_type = 3; 41 | 42 | // The value in this event. 43 | oneof value 44 | { 45 | float scalar = 8; 46 | Tensor tensor = 9; 47 | Curve curve = 10; 48 | Figure figure = 11; 49 | File file = 12; 50 | } 51 | } 52 | 53 | message LogEntry 54 | { 55 | string filename = 1; 56 | uint32 line = 2; 57 | string function = 3; 58 | Severity severity = 4; 59 | string message = 5; 60 | uint64 timestamp = 6; 61 | } 62 | 63 | enum Severity 64 | { 65 | UNSPECIFIED = 0; 66 | DEBUG = 10; 67 | INFO = 20; 68 | WARNING = 30; 69 | ERROR = 40; 70 | CRITICAL = 50; 71 | } 72 | -------------------------------------------------------------------------------- /cookiecutter-testbed/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "cookiecutter-pypackage" 3 | version = "0.2.0" 4 | description = "Cookiecutter template for a Python package" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | license = { text = "MIT" } 8 | authors = [ 9 | { name = "Audrey M. Roy Greenfeld", email = "audrey@feldroy.com" } 10 | ] 11 | keywords = ["cookiecutter", "template", "package"] 12 | classifiers = [ 13 | "Development Status :: 4 - Beta", 14 | "Environment :: Console", 15 | "Intended Audience :: Developers", 16 | "Natural Language :: English", 17 | "License :: OSI Approved :: MIT License", 18 | "Programming Language :: Python", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | "Programming Language :: Python :: Implementation :: CPython", 25 | "Topic :: Software Development", 26 | ] 27 | dependencies = [ 28 | "cookiecutter>=2.6.0", 29 | "typer>=0.16.0", 30 | "alabaster>=1.0.0", 31 | "watchdog>=6.0.0", 32 | ] 33 | 34 | [project.optional-dependencies] 35 | test = [ 36 | "coverage", # testing 37 | "pytest", # testing 38 | "pytest-cookies", # testing 39 | "ruff", # linting 40 | "ty", # checking types 41 | "ipdb" 42 | ] 43 | 44 | [project.urls] 45 | homepage = "https://github.com/audreyfeldroy/cookiecutter-pypackage" 46 | 47 | [tool.ruff] 48 | exclude = [ 49 | "*cookiecutter.pypi_package_name*" 50 | ] 51 | 52 | [tool.pytest.ini_options] 53 | testpaths = [ 54 | "tests", 55 | ] 56 | 57 | [dependency-groups] 58 | dev = [ 59 | "rust-just>=1.42.4", 60 | ] -------------------------------------------------------------------------------- /catkit_core/Uuid.cpp: -------------------------------------------------------------------------------- 1 | #include "Uuid.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | class UuidGenerator 8 | { 9 | public: 10 | UuidGenerator() 11 | { 12 | std::random_device random_device; 13 | 14 | for (size_t i = 0; i < 2; ++i) 15 | { 16 | std::seed_seq seed{random_device(), random_device()}; 17 | m_Engines[i].seed(seed); 18 | } 19 | } 20 | 21 | void Generate(Uuid *uuid) 22 | { 23 | // Fill all bytes with random data. 24 | for (size_t i = 0; i < 2; ++i) 25 | { 26 | std::uint64_t value = m_Engines[i](); 27 | std::memcpy(uuid->data.data() + i * 8, &value, sizeof(value)); 28 | } 29 | 30 | // Ensure we are compliant with RFC 4122. 31 | // Set the version and variant bits to conform to version 4. 32 | uuid->data[6] = (uuid->data[6] & 0x0F) | 0x40; 33 | uuid->data[8] = (uuid->data[8] & 0x3F) | 0x80; 34 | } 35 | 36 | private: 37 | std::mt19937_64 m_Engines[2]; 38 | }; 39 | 40 | void Uuid::Generate(Uuid *uuid) 41 | { 42 | static thread_local UuidGenerator generator; 43 | 44 | generator.Generate(uuid); 45 | } 46 | 47 | bool Uuid::operator==(const Uuid &other) const 48 | { 49 | return std::equal(data.begin(), data.end(), other.data.begin()); 50 | } 51 | 52 | bool Uuid::operator!=(const Uuid &other) const 53 | { 54 | return !(*this == other); 55 | } 56 | 57 | std::string Uuid::to_string() const 58 | { 59 | std::ostringstream oss; 60 | oss << std::hex << std::setfill('0'); 61 | 62 | for (size_t i = 0; i < 16; ++i) 63 | { 64 | // Insert hex value of the current byte. 65 | oss << std::setw(2) << static_cast(data[i]); 66 | 67 | // Insert dashes at the correct positions. 68 | if (i == 3 || i == 5 || i == 7 || i == 9) 69 | oss << '-'; 70 | } 71 | 72 | return oss.str(); 73 | } 74 | -------------------------------------------------------------------------------- /docs/services/newport_picomotor.rst: -------------------------------------------------------------------------------- 1 | Newport Picomotor 2 | ================= 3 | 4 | This service controls a Newport Picomotor Motion Controller. 5 | The following Newport Picomotors have been tested and used with catkit2 so far: 6 | 7 | - `Model 8742 `_ (comes with a USB Flash Drive that contains communication drivers and software necessary for operating the controller, see page 42 of the `user manual `_) 8 | 9 | Configuration 10 | ------------- 11 | 12 | .. code-block:: YAML 13 | 14 | picomotor1: 15 | service_type: newport_picomotor 16 | simulated_service_type: newport_picomotor_sim 17 | interface: newport_picomotor 18 | requires_safety: false 19 | 20 | ip_address: 000.000.000.000 21 | max_step: 2147483647 22 | timeout: 60 23 | atol: 1 24 | daisy: 0 # use this when daisy chaining multiple picomotor controllers 25 | axes: 26 | x: 1 27 | y: 2 28 | z: 3 29 | sleep_per_step: 0.0005 # sleep time (s) between every step 30 | sleep_base: 0.1 # base sleep time (s) for every move command 31 | 32 | Properties 33 | ---------- 34 | None. 35 | 36 | Commands 37 | -------- 38 | None. 39 | 40 | Datastreams 41 | ----------- 42 | ``{axis_name}_command``: A movement command along the axis corresponding to axis_name (where axis_name can be x, y, or z). The axis names are defined by the config file. 43 | 44 | ``{axis_name}_current_position``: The current position of the picomotor along the axis corresponding to axis_name (where axis_name can be x, y, or z). The axis names are defined by the config file. 45 | 46 | -------------------------------------------------------------------------------- /benchmarks/hash_map.cpp: -------------------------------------------------------------------------------- 1 | #include "HashMap.h" 2 | #include "Timing.h" 3 | #include "LocalMemory.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | int main(int argc, char **argv) 10 | { 11 | std::size_t buffer_size = HashMap::GetSharedStateSize(65536, 13, sizeof(std::int16_t)); 12 | std::cout << "Buffer size: " << buffer_size << " bytes" << std::endl; 13 | 14 | auto buffer = LocalMemory::Create(buffer_size); 15 | auto stream = StructStream(buffer); 16 | 17 | auto map = HashMap::Create(stream, 65536, 13, sizeof(std::int16_t)); 18 | 19 | std::uint64_t total_time = 0; 20 | std::size_t N = 5000; 21 | 22 | for (std::int16_t i = 0; i < N; ++i) 23 | { 24 | std::string key = "key" + std::to_string(i); 25 | 26 | auto start = GetTimeStamp(); 27 | auto value = map->Insert(key, &i); 28 | auto end = GetTimeStamp(); 29 | 30 | if (value == nullptr) 31 | { 32 | std::cout << "Insertion failed." << std::endl; 33 | } 34 | 35 | total_time += end - start; 36 | } 37 | 38 | std::cout << "Insertion time: " << total_time / N << " ns" << std::endl; 39 | 40 | total_time = 0; 41 | 42 | for (std::int16_t i = 0; i < N; ++i) 43 | { 44 | std::string key = "key" + std::to_string(i); 45 | 46 | auto start = GetTimeStamp(); 47 | auto *value = map->Find(key); 48 | auto end = GetTimeStamp(); 49 | 50 | if (value == nullptr || *((std::uint16_t *)value) != i) 51 | { 52 | std::cout << "Key not found." << std::endl; 53 | } 54 | 55 | total_time += end - start; 56 | } 57 | 58 | std::cout << "Lookup time: " << total_time / N << " ns" << std::endl; 59 | 60 | return 0; 61 | } 62 | -------------------------------------------------------------------------------- /catkit_core/EventBase.h: -------------------------------------------------------------------------------- 1 | #ifndef EVENT_BASE_H 2 | #define EVENT_BASE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | enum class EventImplementationType 10 | { 11 | ConditionVariable, 12 | Futex, 13 | Semaphore, 14 | SpinLock 15 | }; 16 | 17 | template 18 | struct EventSharedState 19 | { 20 | }; 21 | 22 | template 23 | struct EventLocalState 24 | { 25 | }; 26 | 27 | template 28 | class EventImpl 29 | { 30 | public: 31 | using SharedState = EventSharedState; 32 | using LocalState = EventLocalState; 33 | 34 | protected: 35 | EventImpl(); 36 | 37 | public: 38 | ~EventImpl(); 39 | 40 | // Note: do not implement these functions for specific implementations. 41 | static std::shared_ptr> Create(std::string_view id, SharedState *shared_state); 42 | static std::shared_ptr> Open(std::string_view id, SharedState *shared_state); 43 | 44 | // Note: implement the following functions for specific implementations. 45 | inline void Wait(double timeout_in_sec, std::function condition, void (*error_check)()); 46 | inline void Signal(); 47 | 48 | protected: 49 | inline void CreateImpl(std::string_view id, SharedState *shared_state); 50 | inline void OpenImpl(std::string_view id, SharedState *shared_state); 51 | 52 | bool m_IsOwner; 53 | 54 | SharedState *m_SharedState; 55 | LocalState m_LocalState; 56 | }; 57 | 58 | template 59 | struct is_event_implemented : std::false_type 60 | { 61 | }; 62 | 63 | #include "EventBase.inl" 64 | #include "EventConditionVariable.inl" 65 | #include "EventFutex.inl" 66 | #include "EventSemaphore.inl" 67 | #include "EventSpinLock.inl" 68 | 69 | #endif // EVENT_BASE_H 70 | -------------------------------------------------------------------------------- /catkit2/services/thorlabs_mff101_sim/thorlabs_mff101_sim.py: -------------------------------------------------------------------------------- 1 | from catkit2.testbed.service import Service 2 | 3 | import numpy as np 4 | 5 | class ThorlabsMFF101Sim(Service): 6 | def __init__(self): 7 | super().__init__('thorlabs_mff101_sim') 8 | 9 | self.out_of_beam_position = self.config['positions']['out_of_beam'] 10 | 11 | self.commanded_position = self.make_data_stream('commanded_position', 'int8', [1], 20) 12 | self.current_position = self.make_data_stream('current_position', 'int8', [1], 20) 13 | 14 | self.current_position.submit_data(np.array([-1], dtype='int8')) 15 | 16 | self.make_command('blink_led', self.blink_led) 17 | 18 | def main(self): 19 | while not self.should_shut_down: 20 | try: 21 | frame = self.commanded_position.get_next_frame(10) 22 | except Exception: 23 | # Timed out. This is used to periodically check the shutdown flag. 24 | continue 25 | 26 | position = frame.data[0] 27 | 28 | self.set_position(position) 29 | 30 | def open(self): 31 | self.set_position(self.out_of_beam_position) 32 | 33 | def set_position(self, position): 34 | if position == self.current_position.get()[0]: 35 | return 36 | 37 | self.current_position.submit_data(np.array([-1], dtype='int8')) 38 | 39 | # Send the command to the simulator. The simulator will in turn set the current position on 40 | # our data stream once the flip mount has been moved. 41 | self.testbed.simulator.move_flip_mount(flip_mount_name=self.id, new_flip_mount_position=position) 42 | 43 | def blink_led(self, args=None): 44 | self.log.info('Blinking LED.') 45 | 46 | if __name__ == '__main__': 47 | service = ThorlabsMFF101Sim() 48 | service.run() 49 | -------------------------------------------------------------------------------- /catkit_core/LogConsole.cpp: -------------------------------------------------------------------------------- 1 | #include "LogConsole.h" 2 | 3 | #include 4 | #include 5 | 6 | using namespace std; 7 | 8 | enum ColorCode { 9 | FG_BLACK = 30, 10 | FG_RED = 31, 11 | FG_GREEN = 32, 12 | FG_BROWN = 33, 13 | FG_BLUE = 34, 14 | FG_MAGENTA = 35, 15 | FG_CYAN = 36, 16 | FG_GRAY = 37, 17 | FG_DEFAULT = 39, 18 | BG_RED = 41, 19 | BG_GREEN = 42, 20 | BG_YELLOW = 43, 21 | BG_BLUE = 44, 22 | BG_MAGENTA = 45, 23 | BG_CYAN = 46, 24 | BG_GRAY = 47, 25 | BG_DEFAULT = 49 26 | }; 27 | 28 | ostream &operator<<(ostream &os, ColorCode code) { 29 | return os << "\033[" << static_cast(code) << "m"; 30 | } 31 | 32 | LogConsole::LogConsole(bool use_color, bool print_context) 33 | : m_UseColor(use_color), m_PrintContext(print_context) 34 | { 35 | } 36 | 37 | void LogConsole::AddLogEntry(const LogEntry &entry) 38 | { 39 | if (m_PrintContext) 40 | cout << "Function " << entry.function << " in " << entry.filename << ":" << entry.line << "\n "; 41 | 42 | if (m_UseColor) 43 | { 44 | switch (entry.severity) 45 | { 46 | case S_CRITICAL: 47 | cout << BG_RED; 48 | break; 49 | case S_ERROR: 50 | cout << FG_RED; 51 | break; 52 | case S_WARNING: 53 | cout << FG_BROWN; 54 | break; 55 | case S_INFO: 56 | cout << FG_BLUE; 57 | break; 58 | case S_DEBUG: 59 | cout << FG_GREEN; 60 | break; 61 | } 62 | } 63 | 64 | switch (entry.severity) 65 | { 66 | case S_CRITICAL: 67 | cout << "Critical Error: "; 68 | break; 69 | case S_ERROR: 70 | cout << "Error: "; 71 | break; 72 | case S_WARNING: 73 | cout << "Warning: "; 74 | break; 75 | case S_INFO: 76 | cout << "Info: "; 77 | break; 78 | case S_DEBUG: 79 | cout << "Debug: "; 80 | break; 81 | } 82 | 83 | cout << entry.message; 84 | 85 | if (m_UseColor) 86 | cout << FG_DEFAULT << BG_DEFAULT; 87 | 88 | cout << endl; 89 | } 90 | -------------------------------------------------------------------------------- /catkit_core/EventSpinLock.inl: -------------------------------------------------------------------------------- 1 | #include "EventBase.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using EventSpinLock = EventImpl; 8 | 9 | const std::size_t NUM_ITERATIONS_BETWEEN_CHECKS = 16; 10 | 11 | template<> 12 | struct is_event_implemented : std::true_type 13 | { 14 | }; 15 | 16 | template<> 17 | struct EventSharedState 18 | { 19 | std::atomic_size_t m_Counter; 20 | }; 21 | 22 | template<> 23 | inline void EventSpinLock::Wait(double timeout_in_sec, std::function condition, void (*error_check)()) 24 | { 25 | Timer timer; 26 | 27 | std::size_t current_counter = m_SharedState->m_Counter.load(std::memory_order_acquire); 28 | std::size_t i = 0; 29 | 30 | while (!condition()) 31 | { 32 | while (true) 33 | { 34 | std::size_t new_counter = m_SharedState->m_Counter.load(std::memory_order_acquire); 35 | 36 | if (new_counter != current_counter) 37 | { 38 | current_counter = new_counter; 39 | break; 40 | } 41 | 42 | if (++i == NUM_ITERATIONS_BETWEEN_CHECKS) 43 | { 44 | // If we've been waiting for a long time, then the lock is probably deadlocked. 45 | if (error_check != nullptr) 46 | error_check(); 47 | 48 | if (timer.GetTime() > timeout_in_sec) 49 | throw std::runtime_error("Waiting time has expired."); 50 | 51 | i = 0; 52 | 53 | // Yield the thread to allow other threads to run. 54 | std::this_thread::yield(); 55 | } 56 | } 57 | } 58 | } 59 | 60 | template<> 61 | inline void EventSpinLock::Signal() 62 | { 63 | m_SharedState->m_Counter.fetch_add(1, std::memory_order_release); 64 | } 65 | 66 | template<> 67 | inline void EventSpinLock::CreateImpl(std::string_view id, SharedState *shared_state) 68 | { 69 | shared_state->m_Counter.store(0); 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: 6 | - develop 7 | pull_request: 8 | types: [opened, synchronize, ready_for_review] 9 | branches: 10 | - develop 11 | push: 12 | branches: 13 | - develop 14 | 15 | jobs: 16 | build_docs: 17 | name: Build 18 | runs-on: ubuntu-22.04 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: conda-incubator/setup-miniconda@v2 23 | with: 24 | auto-update-conda: true 25 | auto-activate-base: false 26 | - name: Switch to gcc-10 on linux 27 | if: runner.os == 'Linux' 28 | run: | 29 | sudo apt install gcc-10 g++-10 cpp-10 30 | sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-10 100 --slave /usr/bin/g++ g++ /usr/bin/g++-10 --slave /usr/bin/gcov gcov /usr/bin/gcov-10 31 | - name: Install dependencies 32 | run: | 33 | conda env create --name build-env --file environment.yml 34 | conda activate build-env 35 | pip install -e . 36 | shell: bash -l {0} 37 | - name: Build documentation 38 | run: | 39 | conda activate build-env 40 | cd docs 41 | make html 42 | shell: bash -l {0} 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: ./docs/_build/html 47 | 48 | deploy: 49 | name: Deploy 50 | if: ${{ github.event_name == 'push' }} 51 | 52 | permissions: 53 | pages: write 54 | id-token: write 55 | 56 | environment: 57 | name: github-pages 58 | url: ${{ steps.deployment.outputs.page_url }} 59 | 60 | runs-on: ubuntu-22.04 61 | needs: build_docs 62 | steps: 63 | - name: Deploy to GitHub Pages 64 | id: deployment 65 | uses: actions/deploy-pages@v4 66 | -------------------------------------------------------------------------------- /docs/services/camera_sim.rst: -------------------------------------------------------------------------------- 1 | Camera Simulator 2 | ================ 3 | This service operates a simulated camera. This service is meant to mimic, but does not actually 4 | control, a hardware camera. 5 | 6 | This service can be used to simulate any hardware camera service since they are all written consistently. 7 | 8 | Configuration 9 | ------------- 10 | .. code-block:: YAML 11 | 12 | camera1: 13 | service_type: my_camera 14 | simulated_service_type: camera_sim 15 | interface: camera 16 | requires_safety: false 17 | 18 | # Keys used only by hardware service. 19 | device_name: ZWO ASI178MM 20 | device_id: 4 21 | well_depth_percentage_target: 0.65 22 | 23 | # Keys used by simulated and hardware service. 24 | exposure_time: 1000 25 | width: 1680 26 | height: 1680 27 | offset_x: 336 28 | offset_y: 204 29 | gain: 0 30 | 31 | Properties 32 | ---------- 33 | ``exposure_time``: Simulated exposure time (in microseconds) of the camera. 34 | 35 | ``gain``: Simulated gain of the camera. 36 | 37 | ``width``: The width of the camera frames. 38 | 39 | ``height``: The height of the camera frames. 40 | 41 | ``offset_x``: The x offset of the camera frames on the sensor. 42 | 43 | ``offset_y``: The y offset of the camera frames on the sensor. 44 | 45 | ``sensor_width``: The width of the simulated sensor. 46 | 47 | ``sensor_height``: The height of the simulated sensor. 48 | 49 | Commands 50 | -------- 51 | ``start_acquisition()``: This starts the acquisition of images from the camera. 52 | 53 | ``end_acquisition()``: This ends the acquisition of images from the camera. 54 | 55 | Datastreams 56 | ----------- 57 | ``temperature``: The simulated temperature (in Celsius) as measured by the camera. 58 | 59 | ``images``: The images acquired by the camera. 60 | 61 | ``is_acquiring``: Whether the camera is currently acquiring images. 62 | -------------------------------------------------------------------------------- /catkit_core/HashMap.h: -------------------------------------------------------------------------------- 1 | #ifndef HASH_MAP_H 2 | #define HASH_MAP_H 3 | 4 | #include "Shareable.h" 5 | #include "StructStream.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // A hash map with the following limitations: 15 | // * entries cannot be removed. 16 | // * key is string type of fixed size. 17 | class HashMap : public Shareable 18 | { 19 | private: 20 | enum EntryFlags : uint8_t 21 | { 22 | UNOCCUPIED = 0, 23 | INITIALIZING = 1, 24 | OCCUPIED = 2 25 | }; 26 | 27 | char *m_Data; 28 | 29 | std::size_t m_NumEntries; 30 | std::size_t m_MaxKeySize; 31 | std::size_t m_ValueSize; 32 | std::size_t m_EntrySize; 33 | 34 | std::uint32_t GetHash(std::string_view key) const; 35 | std::size_t GetIndex(std::string_view key) const; 36 | 37 | std::string_view GetKey(std::size_t entry) const; 38 | void SetKey(std::size_t entry, std::string_view key); 39 | std::atomic *GetFlagsRef(std::size_t entry) const; 40 | void *GetValue(std::size_t entry) const; 41 | 42 | static std::size_t CalculateEntrySize(std::size_t max_key_size, std::size_t value_size); 43 | 44 | public: 45 | HashMap(char *data, std::size_t num_entries, std::size_t max_key_size, std::size_t value_size, std::shared_ptr memory_block); 46 | 47 | static std::size_t GetSharedStateSize(std::size_t num_entries, std::size_t max_key_size, std::size_t value_size); 48 | 49 | static std::shared_ptr Create(StructStream &stream, std::size_t num_entries, std::size_t max_key_size, std::size_t value_size); 50 | static std::shared_ptr Open(StructStream &stream); 51 | 52 | void *Insert(std::string_view key, const void *value = nullptr); 53 | void *Find(std::string_view key) const; 54 | 55 | std::vector GetAllKeys() const; 56 | 57 | ShareableType GetType() const override; 58 | }; 59 | 60 | #endif // HASH_MAP_H 61 | -------------------------------------------------------------------------------- /tests/test_dm_commands.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | 4 | def test_flatten_single_channel(testbed): 5 | # Test with single channel 6 | dm_proxy = testbed.dummy_dm_service 7 | 8 | expected_zero_command = np.zeros(dm_proxy.num_dms * dm_proxy.num_actuators) 9 | initial_command = np.ones(dm_proxy.num_dms * dm_proxy.num_actuators) 10 | dm_proxy.correction_howfs.submit_data(initial_command) 11 | 12 | previous_dm_command = dm_proxy.flatten_channels('correction_howfs') 13 | assert np.allclose(previous_dm_command, initial_command) 14 | 15 | current_dm_command = dm_proxy.correction_howfs.get_latest_frame().data 16 | assert np.allclose(current_dm_command, expected_zero_command) 17 | 18 | def test_flatten_multiple_channels(testbed): 19 | # Flatten multiple channels 20 | dm_proxy = testbed.dummy_dm_service 21 | 22 | initial_command = np.ones(dm_proxy.num_dms * dm_proxy.num_actuators) 23 | dm_proxy.correction_lowfs.submit_data(initial_command) 24 | dm_proxy.atmosphere.submit_data(initial_command) 25 | dm_proxy.aberration.submit_data(initial_command) 26 | num_channels_summed = 3 27 | 28 | expected_summed_command = num_channels_summed * initial_command 29 | expected_zero_command = np.zeros(dm_proxy.num_dms * dm_proxy.num_actuators) 30 | 31 | move_command = dm_proxy.flatten_channels(['correction_lowfs', 'atmosphere', 'aberration']) 32 | assert np.allclose(move_command, expected_summed_command) 33 | 34 | current_lowfs_command = dm_proxy.correction_lowfs.get_latest_frame().data 35 | assert np.allclose(current_lowfs_command, expected_zero_command) 36 | 37 | current_atmosphere_command = dm_proxy.atmosphere.get_latest_frame().data 38 | assert np.allclose(current_atmosphere_command, expected_zero_command) 39 | 40 | current_aberration_command = dm_proxy.aberration.get_latest_frame().data 41 | assert np.allclose(current_aberration_command, expected_zero_command) 42 | 43 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "{{cookiecutter.pypi_package_name}}" 3 | version = "{{ cookiecutter.first_version }}" 4 | description = "{{cookiecutter.project_short_description}}" 5 | readme = "README.md" 6 | authors = [ 7 | {name = "{{cookiecutter.full_name}}", email = "{{cookiecutter.email}}"} 8 | ] 9 | maintainers = [ 10 | {name = "{{cookiecutter.full_name}}", email = "{{cookiecutter.email}}"} 11 | ] 12 | classifiers = [ 13 | # TODO 14 | ] 15 | license = {text = "MIT"} 16 | dependencies = [ 17 | "typer" 18 | ] 19 | requires-python = ">=3.7" 20 | 21 | [project.optional-dependencies] 22 | test = [ 23 | "coverage", # testing 24 | "pytest", # testing 25 | "ruff", # linting 26 | "ty", # checking types 27 | "ipdb", # debugging 28 | ] 29 | 30 | [project.urls] 31 | bugs = "https://github.com/{{cookiecutter.__gh_slug}}/issues" 32 | changelog = "https://github.com/{{cookiecutter.__gh_slug}}/blob/master/changelog.md" 33 | homepage = "https://github.com/{{cookiecutter.__gh_slug}}" 34 | 35 | [project.scripts] 36 | {{cookiecutter.project_slug}} = "{{cookiecutter.project_slug}}.cli:main" 37 | 38 | [project.entry-points."catkit2.services"] 39 | {{cookiecutter.project_slug}}_simulator = "{{cookiecutter.project_slug}}.services.{{cookiecutter.project_slug}}_simulator.{{cookiecutter.project_slug}}_simulator" 40 | 41 | [tool.ty] 42 | # All rules are enabled as "error" by default; no need to specify unless overriding. 43 | # Example override: relax a rule for the entire project (uncomment if needed). 44 | # rules.TY015 = "warn" # For invalid-argument-type, warn instead of error. 45 | 46 | [tool.ruff] 47 | line-length = 120 48 | 49 | [tool.ruff.lint] 50 | select = [ 51 | "E", # pycodestyle errors 52 | "W", # pycodestyle warnings 53 | "F", # Pyflakes 54 | "I", # isort 55 | "B", # flake8-bugbear 56 | "UP", # pyupgrade 57 | ] 58 | 59 | [tool.uv] 60 | package = true 61 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/src/{{cookiecutter.project_slug}}/services/{{cookiecutter.project_slug}}_simulator/{{cookiecutter.project_slug}}_simulator.py: -------------------------------------------------------------------------------- 1 | from catkit2.simulator import Simulator 2 | from {{cookiecutter.project_slug}}.{{cookiecutter.project_slug}}_optical_model import {{cookiecutter.project_slug.capitalize()}}OpticalModel 3 | import hcipy 4 | 5 | 6 | class {{cookiecutter.project_slug.capitalize()}}Simulator(Simulator): 7 | """A very simple example simulator for the example testbed "{{cookiecutter.project_slug.capitalize()}}". 8 | This service provides the simulator interface to the connected optical model. 9 | """ 10 | def __init__(self): 11 | super().__init__('{{cookiecutter.project_slug}}_simulator') 12 | self.light_source_data = {} 13 | 14 | def open(self): 15 | self.model = {{cookiecutter.project_slug.capitalize()}}OpticalModel(self.testbed.config['simulator']) 16 | wavefronts = [hcipy.Wavefront(self.model.pupil_grid.ones() * 1e5)] 17 | self.model.set_wavefronts('light_source', wavefronts) 18 | self.images = self.make_data_stream('images', 'float64', self.model.detector_grid.shape, 20) 19 | 20 | def camera_readout(self, camera_name, power): 21 | image = power.shaped 22 | image = hcipy.large_poisson(image) 23 | image[image > 2**16] = 2**16 24 | image = image.astype('float32') 25 | 26 | try: 27 | self.testbed.detector.images.update_parameters('float32', image.shape, 20) 28 | self.testbed.detector.images.submit_data(image) 29 | except Exception as e: 30 | self.log.error(str(e)) 31 | 32 | def get_camera_power(self, camera_name): 33 | wavefronts = self.model.get_wavefronts(camera_name) 34 | return sum(wf.power for wf in wavefronts) 35 | 36 | 37 | if __name__ == '__main__': 38 | service = {{cookiecutter.project_slug.capitalize()}}Simulator() 39 | service.run() 40 | -------------------------------------------------------------------------------- /catkit_core/Shareable.h: -------------------------------------------------------------------------------- 1 | #ifndef SHAREABLE_H 2 | #define SHAREABLE_H 3 | 4 | #include "StructStream.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | enum class ShareableType 11 | { 12 | DataStream, 13 | Event, 14 | LocalMemory, 15 | PoolAllocator, 16 | SharedMemory, 17 | HashMap, 18 | LocalMessageBroker, 19 | BuddyAllocator, 20 | HybridPoolAllocator 21 | }; 22 | 23 | /* 24 | * A base class for all shareable objects. 25 | * 26 | * A shareable object is a object that operates on a shared memory block. 27 | * It can use the shared state to communicate with itself on other processes 28 | * or other threads. The attributes of a shareable object are stored locally. 29 | * A shareable object should be able to be opened from its shared state. 30 | */ 31 | class Shareable 32 | { 33 | protected: 34 | // Constructor. This stores a reference to the memory block, 35 | // ensuring it stays alive at least until this object gets deallocated. 36 | Shareable(std::shared_ptr memory_block); 37 | 38 | public: 39 | virtual ~Shareable() = default; 40 | 41 | // Disallow copy and move semantics. 42 | Shareable(const Shareable &) = delete; 43 | Shareable &operator=(const Shareable &) = delete; 44 | Shareable(Shareable &&) = delete; 45 | Shareable &operator=(Shareable &&) = delete; 46 | 47 | static std::shared_ptr Open(StructStream &stream); 48 | 49 | virtual ShareableType GetType() const = 0; 50 | 51 | private: 52 | // The memory object containing our shared state. 53 | std::shared_ptr m_MemoryBlock; 54 | }; 55 | 56 | template 57 | inline bool CheckVersion(StructStream &stream, const std::array expected_version) 58 | { 59 | // Get the version from the stream; 60 | T *our_version = stream.Extract(N); 61 | 62 | // Return if the version strings are the same. 63 | return std::equal(expected_version.begin(), expected_version.end(), our_version); 64 | } 65 | 66 | #endif // SHAREABLE_H 67 | -------------------------------------------------------------------------------- /tests/test_event.py: -------------------------------------------------------------------------------- 1 | from catkit2.catkit_bindings import Event, EventWaitMethod, LocalMemory, is_wait_method_implemented 2 | import threading 3 | import pytest 4 | import time 5 | 6 | def event_wait(event, signaled, waiting, condition): 7 | waiting.set() 8 | 9 | event.wait(lambda: condition[0] != 0, 2) 10 | 11 | signaled.set() 12 | 13 | @pytest.mark.parametrize("wait_method", [ 14 | EventWaitMethod.Default, 15 | EventWaitMethod.Semaphore, 16 | EventWaitMethod.ConditionVariable, 17 | EventWaitMethod.Futex, 18 | EventWaitMethod.SpinLock]) 19 | def test_event(wait_method): 20 | if not is_wait_method_implemented(wait_method): 21 | pytest.skip(f"Wait method {wait_method} is not implemented.") 22 | 23 | memory = LocalMemory.create(1024) 24 | event = Event.create(memory, 'test_event') 25 | 26 | signaled = threading.Event() 27 | waiting = threading.Event() 28 | 29 | # The condition that the threads will wait on. This needs to be a mutable object. 30 | condition = [0] 31 | 32 | thread = threading.Thread(target=event_wait, args=(event, signaled, waiting, condition)) 33 | thread.start() 34 | 35 | # Ensure that the waiting thread has started waiting. 36 | waiting.wait(1) 37 | assert waiting.is_set(), 'Something went wrong starting the waiting thread.' 38 | 39 | # Ensure the event is not triggered unless signaled. 40 | start = time.perf_counter() 41 | with pytest.raises(RuntimeError): 42 | event.wait(lambda: condition[0] != 0, 0.1) 43 | end = time.perf_counter() 44 | 45 | # Ensure that we at least waited for the timeout duration. 46 | assert (end - start) >= 0.1 47 | 48 | # Signal the event. 49 | condition[0] = 1 50 | event.signal() 51 | 52 | # Wait for the wait to end. 53 | signaled.wait(1) 54 | 55 | # Ensure that the wait actually ended and was triggered. 56 | assert signaled.is_set(), 'The waiting thread was not signaled.' 57 | 58 | thread.join() 59 | -------------------------------------------------------------------------------- /catkit2/services/simple_simulator/simple_simulator.py: -------------------------------------------------------------------------------- 1 | from catkit2.simulator import SimpleOpticalModel, Simulator 2 | import hcipy 3 | 4 | 5 | class SimpleSimulator(Simulator): 6 | '''A simple simulator for the SimpleOpticalModel. 7 | 8 | This service is not meant to be used, but rather serves as an 9 | example on how to use the Simulator base class. 10 | ''' 11 | def __init__(self): 12 | super().__init__('simple_simulator') 13 | 14 | self.alpha = 1 15 | 16 | def open(self): 17 | self.model = SimpleOpticalModel() 18 | wavefronts = [hcipy.Wavefront(self.model.pupil_grid.ones() * 1e6)] 19 | self.model.set_wavefronts('pre_pupil', wavefronts) 20 | 21 | self.images = self.make_data_stream('images', 'float64', self.model.focal_grid.shape, 20) 22 | 23 | self.update_atmosphere() 24 | 25 | def actuate_dm(self, at_time, dm_name, new_actuators): 26 | def callback(): 27 | self.model.dm.actuators = new_actuators 28 | self.model.purge_plane('pre_turbulence') 29 | 30 | self.add_callback(at_time, callback) 31 | 32 | def update_atmosphere(self): 33 | self.log.info('Updating atmosphere.') 34 | 35 | t = self.time.get()[0] 36 | 37 | self.model.atmosphere.t = t 38 | self.model.purge_plane('pre_coro') 39 | 40 | self.add_callback(t + 0.01, self.update_atmosphere) 41 | 42 | def camera_readout(self, camera_name, power): 43 | image = power.shaped 44 | image = hcipy.large_poisson(image) 45 | image[image > 2**16] = 2**16 46 | image = image.astype('float32') 47 | 48 | try: 49 | self.testbed.science_camera.images.update_parameters('float32', image.shape, 20) 50 | self.testbed.science_camera.images.submit_data(image) 51 | except Exception as e: 52 | self.log.error(str(e)) 53 | 54 | if __name__ == '__main__': 55 | service = SimpleSimulator() 56 | service.run() 57 | -------------------------------------------------------------------------------- /docs/services/thorlabs_cube_motor_kinesis.rst: -------------------------------------------------------------------------------- 1 | Thorlabs Cube Motors 2 | ==================== 3 | 4 | This service connects to Thorlabs TDC001 and Thorlabs KDC101 controllers in order to operate a motor. 5 | 6 | This service uses pieces of the official vendor Python library: 7 | 8 | `https://github.com/Thorlabs/Motion_Control_Examples/tree/main/Python `_ 9 | 10 | The service also requires the installation of the Thorlabs Kinesis software: 11 | `https://www.thorlabs.com/software_pages/viewsoftwarepage.cfm?code=Motion_Control# `_ 12 | 13 | Then, to use Thorlabs cube motors with Kinesis, you need to set the ``THORLABS_KINESIS_DLL_PATH`` environment variable. 14 | 15 | Successfully tested with the following devices: 16 | 17 | - Thorlabs TDC001 18 | - Thorlabs KDC101 19 | 20 | Successfully tested with the motor stages: 21 | 22 | - MTS25-Z8 23 | - MTS50-Z8 24 | - Z825B 25 | 26 | Configuration 27 | ------------- 28 | 29 | .. code-block:: YAML 30 | 31 | motor: 32 | service_type: thorlabs_cube_motor_kinesis 33 | simulated_service_type: thorlabs_cube_motor_kinesis_sim 34 | interface: thorlabs_cube_motor_kinesis 35 | 36 | cube_model: TDC001 37 | serial_number: 12345678 38 | stage_model: MTS50-Z8 39 | unit: mm 40 | min_position: 0.00 41 | max_position: 50.0 42 | 43 | positions: # named positions resolved by the proxy. 44 | nominal: arbitrary_2 45 | arbitrary_1: 10 46 | arbitrary_2: 25 47 | 48 | Properties 49 | ---------- 50 | None. 51 | 52 | Commands 53 | -------- 54 | ``home()``: This will home the motor and block until the motor has finished homing. 55 | 56 | Datastreams 57 | ----------- 58 | ``command``: the current command sent to the motor. 59 | ``current_position``: the current (commanded) position of the motor. -------------------------------------------------------------------------------- /catkit2/services/web_power_switch_sim/web_power_switch_sim.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | from catkit2.testbed.service import Service 4 | 5 | class WebPowerSwitchSim(Service): 6 | def __init__(self): 7 | super().__init__('web_power_switch_sim') 8 | 9 | self.outlet_ids = self.config['outlets'] 10 | 11 | self.outlets = {} 12 | for outlet_name in self.outlet_ids.keys(): 13 | self.add_outlet(outlet_name) 14 | 15 | def add_outlet(self, outlet_name): 16 | self.outlets[outlet_name] = self.make_data_stream(outlet_name.lower(), 'int8', [1], 20) 17 | 18 | def monitor_outlet(self, outlet_name): 19 | while not self.should_shut_down: 20 | try: 21 | frame = self.outlets[outlet_name].get_next_frame(10) 22 | except Exception: 23 | # Timed out. This is used to periodically check the shutdown flag. 24 | continue 25 | 26 | # Turn the outlet on or off 27 | on = frame.data[0] != 0 28 | self.switch_outlet(outlet_name, on) 29 | 30 | def switch_outlet(self, outlet_name, on): 31 | # Contact simulator. 32 | self.testbed.simulator.switch_power(outlet_name=outlet_name, powered=1 if on else 0) 33 | 34 | def open(self): 35 | # Start the outlet threads 36 | self.outlet_threads = {} 37 | 38 | for outlet_name in self.outlet_ids.keys(): 39 | thread = threading.Thread(target=self.monitor_outlet, args=(outlet_name,)) 40 | thread.start() 41 | 42 | self.outlet_threads[outlet_name] = thread 43 | 44 | def main(self): 45 | while not self.should_shut_down: 46 | self.sleep(1) 47 | 48 | def close(self): 49 | # Stop the outlet threads. 50 | for thread in self.outlet_threads.values(): 51 | thread.join() 52 | 53 | self.outlet_threads = {} 54 | 55 | if __name__ == '__main__': 56 | service = WebPowerSwitchSim() 57 | service.run() 58 | -------------------------------------------------------------------------------- /docs/services/physik_stage_controller.rst: -------------------------------------------------------------------------------- 1 | Physik Instrumente Stage Controller 2 | ===================================== 3 | 4 | This service controls Physik Instrumente (PI) motion stages using the `pipython` library. It has been tested with: 5 | 6 | - E-727.3SDA controller with S-330.2SH stages 7 | 8 | For controller specs, see the PI website. 9 | Note that using PI controllers requires manual installation of the GCS DLL drivers from `physikinstrumente.com `_ 10 | 11 | Configuration 12 | ------------- 13 | 14 | .. code-block:: YAML 15 | 16 | physik_stage_controller: 17 | service_type: physik_stage_controller 18 | simulated_service_type: physik_stage_controller_sim 19 | requires_safety: false 20 | 21 | controller_name: 'E-727.3SDA' 22 | stages: 'S-330.2SH' 23 | refmodes: 'FNL' 24 | SN: '123456' 25 | 26 | axis_map: 27 | x: 1 28 | y: 2 29 | 30 | initial_position: 31 | x: 1350 32 | y: 1685 33 | 34 | Properties 35 | ---------- 36 | ``position_{axis_name}``: Get or set the position for each configured axis (e.g., ``position_x``, ``position_y``). 37 | 38 | Commands 39 | -------- 40 | ``move_to(positions)``: Move to absolute positions. ``positions`` is a dict mapping axis names to target positions (e.g., ``{'x': 1350.0, 'y': 1685.0}``). 41 | 42 | ``move_relative(deltas)``: Move relative to current positions. ``deltas`` is a dict mapping axis names to relative movements (e.g., ``{'x': 10.0, 'y': -5.0}``). 43 | 44 | ``get_positions()``: Get current positions of all axes as a dict. 45 | 46 | ``stop_motion()``: Emergency stop all motion immediately. 47 | 48 | Datastreams 49 | ----------- 50 | ``positions``: Current positions of all axes (float64 array, updated at 10 Hz). 51 | 52 | ``target_positions``: Target positions when moves are commanded (float64 array). 53 | 54 | ``is_moving`` (sim only): Whether any axis is currently in motion (int8). 55 | -------------------------------------------------------------------------------- /catkit_core/EventBase.inl: -------------------------------------------------------------------------------- 1 | #include "EventBase.h" 2 | 3 | template 4 | EventImpl::EventImpl() 5 | : m_IsOwner(false), m_SharedState(nullptr) 6 | { 7 | } 8 | 9 | template 10 | EventImpl::~EventImpl() 11 | { 12 | } 13 | 14 | template 15 | void EventImpl::Wait(double timeout_in_sec, std::function condition, void (*error_check)()) 16 | { 17 | throw std::runtime_error("This type of event implementation wasn't implemented."); 18 | } 19 | 20 | template 21 | void EventImpl::Signal() 22 | { 23 | } 24 | 25 | template 26 | std::shared_ptr> EventImpl::Create(std::string_view id, EventImpl::SharedState *shared_state) 27 | { 28 | if (!shared_state) 29 | throw std::runtime_error("The passed shared data was a nullptr."); 30 | 31 | auto obj = std::shared_ptr>(new EventImpl()); 32 | 33 | obj->CreateImpl(id, shared_state); 34 | 35 | obj->m_IsOwner = true; 36 | obj->m_SharedState = shared_state; 37 | 38 | return obj; 39 | } 40 | 41 | template 42 | std::shared_ptr> EventImpl::Open(std::string_view id, EventImpl::SharedState *shared_state) 43 | { 44 | if (!shared_state) 45 | throw std::runtime_error("The passed shared data was a nullptr."); 46 | 47 | auto obj = std::shared_ptr>(new EventImpl()); 48 | 49 | obj->OpenImpl(id, shared_state); 50 | 51 | obj->m_IsOwner = false; 52 | obj->m_SharedState = shared_state; 53 | 54 | return obj; 55 | } 56 | 57 | template 58 | void EventImpl::CreateImpl(std::string_view id, EventImpl::SharedState *shared_state) 59 | { 60 | // Do nothing. 61 | } 62 | 63 | template 64 | void EventImpl::OpenImpl(std::string_view id, EventImpl::SharedState *shared_state) 65 | { 66 | // Do nothing. 67 | } 68 | -------------------------------------------------------------------------------- /catkit_core/Util.inl: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | template 4 | std::string Serialize(const ProtoClass &obj) 5 | { 6 | std::string data; 7 | obj.SerializeToString(&data); 8 | 9 | return data; 10 | } 11 | 12 | template 13 | ProtoClass Deserialize(const std::string &data) 14 | { 15 | ProtoClass obj; 16 | obj.ParseFromString(data); 17 | 18 | return obj; 19 | } 20 | 21 | template 22 | constexpr UnsignedType round_up_to_power_of_2(UnsignedType v) 23 | { 24 | static_assert(std::is_unsigned_v); 25 | 26 | v--; 27 | 28 | for (std::size_t i = 1; i < sizeof(v) * 8; i *= 2) 29 | { 30 | v |= v >> i; 31 | } 32 | 33 | return ++v; 34 | } 35 | 36 | template 37 | constexpr UnsignedType round_down_to_power_of_2(UnsignedType v) 38 | { 39 | static_assert(std::is_unsigned_v); 40 | 41 | for (size_t i = 1; i < sizeof(v) * 8; i *= 2) 42 | v |= v >> i; 43 | 44 | return v - (v >> 1); 45 | } 46 | 47 | // Note: this function is defined in C++20, but we need to support C++17. 48 | template 49 | constexpr int bit_width(T x) 50 | { 51 | static_assert(std::is_integral_v && std::is_unsigned_v, "bit_width requires an unsigned integral type"); 52 | 53 | if (x == 0) 54 | return 0; 55 | 56 | #if defined(__GNUC__) || defined(__clang__) 57 | if constexpr (sizeof(T) == 8) 58 | { 59 | return std::numeric_limits::digits - __builtin_clzll(x); 60 | } 61 | else 62 | { 63 | return std::numeric_limits::digits - __builtin_clz((unsigned int) x); 64 | } 65 | #elif defined(_MSC_VER) 66 | unsigned long index; 67 | 68 | if constexpr (sizeof(T) == 8) 69 | { 70 | _BitScanReverse64(&index, x); 71 | return index + 1; 72 | } 73 | else 74 | { 75 | _BitScanReverse(&index, (unsigned int) x); 76 | return index + 1; 77 | } 78 | #else 79 | // Portable fallback 80 | int width = 0; 81 | 82 | while (x) 83 | { 84 | x >>= 1; 85 | ++width; 86 | } 87 | 88 | return width; 89 | #endif 90 | } 91 | -------------------------------------------------------------------------------- /catkit_core/FitsFile.cpp: -------------------------------------------------------------------------------- 1 | #include "FitsFile.h" 2 | 3 | #include 4 | 5 | FitsFile::FitsFile(std::string fname, std::string hdu_name) 6 | { 7 | int status = 0; 8 | 9 | if (fits_open_file(&m_File, fname.c_str(), READONLY, &status)) 10 | { 11 | fits_report_error(stderr, status); 12 | throw std::runtime_error("Failed to open FITS file."); 13 | } 14 | 15 | if (fits_movnam_hdu(m_File, IMAGE_HDU, const_cast(hdu_name.c_str()), 0, &status)) 16 | { 17 | fits_report_error(stderr, status); 18 | fits_close_file(m_File, &status); 19 | throw std::runtime_error("Failed to move to HDU."); 20 | } 21 | } 22 | 23 | FitsFile::~FitsFile() 24 | { 25 | int status = 0; 26 | fits_close_file(m_File, &status); 27 | } 28 | 29 | int FitsFile::GetNDim() 30 | { 31 | int status = 0; 32 | int naxis; 33 | if (fits_get_img_dim(m_File, &naxis, &status)) 34 | { 35 | fits_report_error(stderr, status); 36 | throw std::runtime_error("Failed to get image dimension."); 37 | } 38 | 39 | return naxis; 40 | } 41 | 42 | std::vector FitsFile::GetShape() 43 | { 44 | int status = 0; 45 | 46 | std::vector shape; 47 | shape.resize(GetNDim()); 48 | 49 | if (fits_get_img_size(m_File, shape.size(), shape.data(), &status)) 50 | { 51 | fits_report_error(stderr, status); 52 | throw std::runtime_error("Failed to get image shape."); 53 | } 54 | 55 | return shape; 56 | } 57 | 58 | long FitsFile::GetSize() 59 | { 60 | long size = 1; 61 | for (auto s : GetShape()) 62 | size *= s; 63 | 64 | return size; 65 | } 66 | 67 | int FitsFile::GetDataType() 68 | { 69 | int status = 0; 70 | int bitpix; 71 | 72 | if (fits_get_img_type(m_File, &bitpix, &status)) 73 | { 74 | fits_report_error(stderr, status); 75 | throw std::runtime_error("Failed to get image type."); 76 | } 77 | 78 | return bitpix; 79 | } 80 | 81 | std::string FitsFile::GetFitsError() 82 | { 83 | std::string error; 84 | char error_message[80]; 85 | 86 | while (fits_read_errmsg(error_message)) 87 | error += error_message; 88 | error += "\n"; 89 | 90 | return error; 91 | } 92 | -------------------------------------------------------------------------------- /tests/test_allocator.py: -------------------------------------------------------------------------------- 1 | from catkit2.catkit_bindings import LocalMemory, HybridPoolAllocator, BuddyAllocator, PoolAllocator 2 | import pytest 3 | 4 | DYNAMIC_CAPACITY = 1024 * 1024 5 | MIN_SIZE = 16 6 | MIN_SIZE_POOL = 1024 7 | POOL_CAPACITY = 16 8 | 9 | @pytest.mark.parametrize("allocator_constructor", [ 10 | lambda header: BuddyAllocator.create(header, DYNAMIC_CAPACITY, MIN_SIZE), 11 | lambda header: HybridPoolAllocator.create(header, DYNAMIC_CAPACITY, MIN_SIZE, MIN_SIZE_POOL), 12 | ]) 13 | def test_dynamic_size_allocator(allocator_constructor): 14 | header = LocalMemory.create(1024 * 1024 * 512) 15 | allocator = allocator_constructor(header) 16 | 17 | handle = allocator.allocate(128) 18 | 19 | assert allocator.acquire(handle) is True, "Memory block should still be allocated." 20 | assert allocator.release(handle) is False, "Memory block was unexpectedly deallocated while ref count == 1." 21 | assert allocator.release(handle) is True, "Memory block was not deallocated when ref count == 0." 22 | 23 | # Allocating a block too large should raise an error. 24 | with pytest.raises(RuntimeError): 25 | allocator.allocate(1024 * 1024 * 1024) 26 | 27 | @pytest.mark.parametrize("allocator_constructor", [ 28 | lambda header: PoolAllocator.create(header, POOL_CAPACITY), 29 | ]) 30 | def test_fixed_size_allocator(allocator_constructor): 31 | header = LocalMemory.create(1024 * 1024 * 512) 32 | allocator = allocator_constructor(header) 33 | 34 | handle = allocator.allocate() 35 | 36 | assert allocator.acquire(handle) is True, "Memory block should still be allocated." 37 | assert allocator.release(handle) is False, "Memory block was unexpectedly deallocated while ref count == 1." 38 | assert allocator.release(handle) is True, "Memory block was not deallocated when ref count == 0." 39 | 40 | for _ in range(POOL_CAPACITY): 41 | allocator.allocate() 42 | 43 | # Allocating more blocks than the pool capacity should raise an error. 44 | with pytest.raises(RuntimeError): 45 | allocator.allocate() 46 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/src/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}_optical_model.py: -------------------------------------------------------------------------------- 1 | from catkit2.simulator import OpticalModel, with_cached_result 2 | import hcipy 3 | import numpy as np 4 | 5 | 6 | class {{cookiecutter.project_slug.capitalize()}}OpticalModel(OpticalModel): 7 | """An example optical model. 8 | This class simulates a simple pupil mask and simple science camera. All parameters are read from 9 | the simulator configuration file, {{cookiecutter.project_slug}}/config/simulator.yml. 10 | """ 11 | def __init__(self, config, wavelength=700e-9): 12 | super().__init__() 13 | 14 | self.config = config 15 | self.wavelength = wavelength 16 | 17 | @self.register_plane('detector', 'pupil') 18 | def detector(wf): 19 | return self.prop(wf) 20 | 21 | @self.register_plane('pupil', 'light_source') 22 | def pupil(wf): 23 | return self.pupil_mask(wf) 24 | 25 | self.set_wavefronts('light_source', hcipy.Wavefront(self.pupil_grid.ones(), self.wavelength)) 26 | 27 | @property 28 | def pupil_grid(self): 29 | dimensions = self.config['pupil_mask']['dimensions'] 30 | dims = np.array([dimensions, dimensions]) 31 | size = self.config['pupil_mask']['grid_size'] 32 | 33 | return hcipy.make_uniform_grid(dims, size) 34 | 35 | @property 36 | def detector_grid(self): 37 | roi = self.config['detector']['roi'] 38 | dims = np.array([roi, roi]) 39 | pixel_size = self.config['detector']['pixel_size'] 40 | 41 | return hcipy.make_uniform_grid(dims, dims * pixel_size) 42 | 43 | @property 44 | @with_cached_result 45 | def prop(self): 46 | return hcipy.FraunhoferPropagator(self.pupil_grid, self.detector_grid) 47 | 48 | @property 49 | @with_cached_result 50 | def pupil_mask(self): 51 | diameter = self.config['pupil_mask']['diameter'] 52 | mask = hcipy.circular_aperture(diameter)(self.pupil_grid) 53 | 54 | return hcipy.Apodizer(mask) 55 | -------------------------------------------------------------------------------- /catkit2/config.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import os 3 | import collections.abc 4 | 5 | def _deep_update(original, update): 6 | if not isinstance(original, collections.abc.Mapping): 7 | return update 8 | 9 | for key, value in update.items(): 10 | if isinstance(value, collections.abc.Mapping): 11 | original[key] = _deep_update(original.get(key, {}), value) 12 | else: 13 | original[key] = value 14 | 15 | return original 16 | 17 | def _get_yaml_loader(config_path): 18 | def path_constructor(loader, node): 19 | path = loader.construct_scalar(node) 20 | path = os.path.expanduser(path) 21 | 22 | if not os.path.isabs(path): 23 | path = os.path.abspath(os.path.join(config_path, path)) 24 | 25 | return path 26 | 27 | class Loader(yaml.SafeLoader): 28 | pass 29 | 30 | Loader.add_constructor('!path', path_constructor) 31 | return Loader 32 | 33 | def _read_config_file(config_file): 34 | config = {} 35 | 36 | contents = config_file.read_text() 37 | loader = _get_yaml_loader(config_file.parent) 38 | 39 | conf = yaml.load(contents, Loader=loader) 40 | config[os.path.splitext(config_file.name)[0]] = conf 41 | 42 | return config 43 | 44 | def read_config_files(config_files): 45 | '''Read all configuration files and return a single configuration. 46 | 47 | `config_files` is an ordered list. Files later in this list will overwrite 48 | the read in values from files earlier in the list. Each file creates its own 49 | section in the returned configuration dictionary, named after its filename 50 | without file extension. 51 | 52 | Parameters 53 | ---------- 54 | config_files : list of Path objects 55 | A list of configuration files to be read in. 56 | 57 | Returns 58 | ------- 59 | dict 60 | A dictionary containing all configuration as read in from the files. 61 | ''' 62 | config = {} 63 | 64 | for config_file in config_files: 65 | config = _deep_update(config, _read_config_file(config_file)) 66 | 67 | return config 68 | -------------------------------------------------------------------------------- /catkit2/services/thorlabs_tsp01_sim/thorlabs_tsp01_sim.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import time 3 | 4 | from catkit2.testbed.service import Service 5 | 6 | class ThorlabsTSP01Sim(Service): 7 | def __init__(self): 8 | super().__init__('thorlabs_tsp01_sim') 9 | 10 | self.interval = self.config.get('interval', 10) 11 | 12 | self.temperature_internal = self.make_data_stream('temperature_internal', 'float64', [1], 20) 13 | self.temperature_header_1 = self.make_data_stream('temperature_header_1', 'float64', [1], 20) 14 | self.temperature_header_2 = self.make_data_stream('temperature_header_2', 'float64', [1], 20) 15 | self.humidity_internal = self.make_data_stream('humidity_internal', 'float64', [1], 20) 16 | 17 | self.shifts = np.random.uniform(0, 2 * np.pi, size=3) 18 | self.periods = np.random.uniform(300, 1800, size=3) 19 | self.amplitudes = np.random.uniform(0.1, 1, size=3) 20 | self.offsets = np.random.uniform(20, 21, size=3) 21 | 22 | def main(self): 23 | while not self.should_shut_down: 24 | t1 = self.get_temperature(1) 25 | t2 = self.get_temperature(2) 26 | t3 = self.get_temperature(3) 27 | h = self.get_humidity() 28 | 29 | self.temperature_internal.submit_data(np.array([t1], dtype='float64')) 30 | self.temperature_header_1.submit_data(np.array([t2], dtype='float64')) 31 | self.temperature_header_2.submit_data(np.array([t3], dtype='float64')) 32 | self.humidity_internal.submit_data(np.array([h], dtype='float64')) 33 | 34 | self.sleep(self.interval) 35 | 36 | def get_temperature(self, channel): 37 | period = self.periods[channel - 1] 38 | shift = self.shifts[channel - 1] 39 | amplitude = self.amplitudes[channel - 1] 40 | offset = self.offsets[channel - 1] 41 | 42 | return np.sin(time.time() * 2 * np.pi / period + shift) * amplitude + offset 43 | 44 | def get_humidity(self): 45 | return 20 46 | 47 | if __name__ == '__main__': 48 | service = ThorlabsTSP01Sim() 49 | service.run() 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Control and Automation for Testbeds Kit 2 (CATKit2) 2 | --------------------- 3 | CATKit2 is a toolkit for hardware controls that has been developed at the Space Telescope Science Institute. 4 | It provides a general infrastructure to control hardware and synchronize devices. 5 | 6 | This package was developed for use on the High-contrast Imager for Complex Apertures Testbed (HiCAT) for 7 | developing technologies relevant to direct imaging of exoplanets in astronomy in the laboratory. 8 | 9 | This is an open-source package, but it is not actively supported. Use at your own risk. 10 | 11 | For installation instructions, see the official documentation: 12 | https://spacetelescope.github.io/catkit2/ 13 | 14 | Python permissions 15 | ------------------ 16 | 17 | On MacOS, even after having allowed your firewall to receive incoming connections from Python applications while running catkit2, 18 | it might keep popping up windows asking you to accept incoming connections every single time you start a server or service. 19 | To prevent this, you can create a self-signed certificate in your keychain. The instructions for that are below, 20 | found on: https://stackoverflow.com/a/59186900/10112569 21 | 22 | ``` 23 | With the OS X firewall enabled, you can remove the "Do you want the application "python" to accept incoming network connections?" message. 24 | 25 | Create a self-signed certificate. 26 | 27 | Open Keychain Access. Applications > Utilities > Keychain Access. 28 | Keychain Access menu > Certificate Assistant > Create a Certificate... 29 | Enter a Name like "My Certificate". 30 | Select Identity Type: Self Signed Root 31 | Select Certificate Type: Code Signing 32 | Check the Let me override defaults box 33 | Click Continue 34 | Enter a unique Serial Number 35 | Enter 7300 for Validity Period. 36 | Click Continue 37 | Click Continue for the rest of the dialogs 38 | Now sign your application 39 | 40 | codesign -s "My Certificate" -f $(which python) 41 | 42 | In the dialog that appears, click "Allow". 43 | 44 | Note that when using a virtual environment, you need to activate the virtual environment before running this command. 45 | ``` 46 | -------------------------------------------------------------------------------- /catkit_core/ServiceProxy.h: -------------------------------------------------------------------------------- 1 | #ifndef SERVICE_PROXY_H 2 | #define SERVICE_PROXY_H 3 | 4 | #include "Types.h" 5 | #include "DataStream.h" 6 | #include "ServiceState.h" 7 | #include "Client.h" 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | class TestbedProxy; 17 | 18 | class ServiceProxy 19 | { 20 | public: 21 | ServiceProxy(std::shared_ptr testbed, std::string service_id); 22 | virtual ~ServiceProxy(); 23 | 24 | Value GetProperty(const std::string &name, void (*error_check)() = nullptr); 25 | Value SetProperty(const std::string &name, const Value &value, void (*error_check)() = nullptr); 26 | 27 | Value ExecuteCommand(const std::string &name, const Dict &arguments, void (*error_check)() = nullptr); 28 | 29 | std::shared_ptr GetDataStream(const std::string &name, void (*error_check)() = nullptr); 30 | 31 | std::shared_ptr GetHeartbeat(); 32 | 33 | ServiceState GetState(); 34 | bool IsRunning(); 35 | bool IsAlive(); 36 | 37 | void Start(double timeout_in_sec = -1, void (*error_check)() = nullptr); 38 | void Stop(); 39 | void Interrupt(); 40 | void Terminate(); 41 | 42 | std::vector GetPropertyNames(void (*error_check)() = nullptr); 43 | std::vector GetCommandNames(void (*error_check)() = nullptr); 44 | std::vector GetDataStreamNames(void (*error_check)() = nullptr); 45 | 46 | nlohmann::json GetConfig(); 47 | std::string GetId(); 48 | std::shared_ptr GetTestbed(); 49 | 50 | private: 51 | void Connect(); 52 | void Disconnect(); 53 | 54 | std::shared_ptr m_Testbed; 55 | std::string m_ServiceId; 56 | 57 | std::shared_ptr m_Client; 58 | 59 | std::vector m_PropertyNames; 60 | std::vector m_CommandNames; 61 | std::map m_DataStreamIds; 62 | 63 | std::map> m_DataStreams; 64 | 65 | std::shared_ptr m_Heartbeat; 66 | std::shared_ptr m_State; 67 | std::uint64_t m_TimeLastConnect; 68 | }; 69 | 70 | #endif // SERVICE_PROXY_H 71 | -------------------------------------------------------------------------------- /catkit_core/SharedMemory.h: -------------------------------------------------------------------------------- 1 | #ifndef SHARED_MEMORY_H 2 | #define SHARED_MEMORY_H 3 | 4 | #include "Memory.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "Shareable.h" 13 | 14 | #ifdef _WIN32 15 | #define WIN32_LEAN_AND_MEAN 16 | #define NOMINMAX 17 | #include 18 | #else 19 | #include 20 | #include 21 | #include 22 | #include 23 | #endif // _WIN32 24 | 25 | const int SHARED_MEMORY_FNAME_SIZE = 256; 26 | 27 | class SharedMemory : public Memory, public Shareable 28 | { 29 | public: 30 | #ifdef _WIN32 31 | typedef HANDLE FileObject; 32 | #else 33 | typedef int FileObject; 34 | #endif 35 | 36 | private: 37 | SharedMemory(std::string_view fname, FileObject file, bool is_owner); 38 | 39 | struct Header 40 | { 41 | std::uint64_t capacity; 42 | 43 | // Pad to 128bytes. 44 | char padding[128 - sizeof(capacity)]; 45 | }; 46 | 47 | public: 48 | virtual ~SharedMemory(); 49 | 50 | std::size_t GetSharedStateSize(); 51 | 52 | static std::shared_ptr Create(StructStream &stream, std::string_view fname, size_t num_bytes_in_buffer); 53 | static std::shared_ptr Create(StructStream &stream, size_t num_bytes_in_buffer); 54 | static std::shared_ptr Create(std::string_view fname, size_t num_bytes_in_buffer); 55 | static std::shared_ptr Open(StructStream &stream); 56 | static std::shared_ptr Open(std::string_view fname); 57 | 58 | // Destroy the shared memory object, even if we are not the owner. 59 | void Destroy(); 60 | 61 | virtual void *GetAddress(std::size_t offset = 0) override; 62 | virtual std::size_t GetCapacity() const override; 63 | virtual void WriteReference(StructStream *stream) override; 64 | 65 | std::string GetFileName(); 66 | 67 | virtual ShareableType GetType() const override; 68 | virtual MemoryType GetMemoryType() const override; 69 | 70 | private: 71 | std::string m_FileName; 72 | bool m_IsOwner; 73 | 74 | FileObject m_File; 75 | void *m_Buffer; 76 | 77 | std::size_t m_Capacity; 78 | }; 79 | 80 | #endif // SHARED_MEMORY_H 81 | -------------------------------------------------------------------------------- /catkit_core/LocalMemory.cpp: -------------------------------------------------------------------------------- 1 | #include "LocalMemory.h" 2 | 3 | #include 4 | 5 | LocalMemory::LocalMemory(char *memory, std::size_t num_bytes, bool is_owner) 6 | : Shareable(nullptr), m_Memory(memory), m_Capacity(num_bytes), m_IsOwner(is_owner) 7 | { 8 | } 9 | 10 | LocalMemory::~LocalMemory() 11 | { 12 | if (m_IsOwner) 13 | delete[] m_Memory; 14 | } 15 | 16 | void *LocalMemory::GetAddress(std::size_t offset) 17 | { 18 | return m_Memory + offset; 19 | } 20 | 21 | std::size_t LocalMemory::GetCapacity() const 22 | { 23 | return m_Capacity; 24 | } 25 | 26 | void LocalMemory::WriteReference(StructStream *stream) 27 | { 28 | *stream->Extract() = m_Memory; 29 | *stream->Extract() = m_Capacity; 30 | *stream->Extract() = GetProcessId(); 31 | } 32 | 33 | std::shared_ptr LocalMemory::Create(StructStream &stream, std::size_t num_bytes) 34 | { 35 | // Allocate the memory. 36 | char *memory = new char[num_bytes]; 37 | auto res = std::shared_ptr(new LocalMemory(memory, num_bytes, true)); 38 | 39 | // Write metadata to stream. 40 | *stream.Extract() = res->m_Memory; 41 | *stream.Extract() = num_bytes; 42 | *stream.Extract() = GetProcessId(); 43 | 44 | return std::move(res); 45 | } 46 | 47 | std::shared_ptr LocalMemory::Create(std::size_t num_bytes) 48 | { 49 | // Allocate the memory. 50 | char *memory = new char[num_bytes]; 51 | return std::shared_ptr(new LocalMemory(memory, num_bytes, true)); 52 | } 53 | 54 | std::shared_ptr LocalMemory::Open(StructStream &stream) 55 | { 56 | // Read metadata from stream. 57 | auto memory = *stream.Extract(); 58 | auto num_bytes = *stream.Extract(); 59 | auto pid = *stream.Extract(); 60 | 61 | if (pid != GetProcessId()) 62 | throw std::runtime_error("This local memory was created on a different process."); 63 | 64 | return std::shared_ptr(new LocalMemory(memory, num_bytes, false)); 65 | } 66 | 67 | ShareableType LocalMemory::GetType() const 68 | { 69 | return ShareableType::LocalMemory; 70 | } 71 | 72 | MemoryType LocalMemory::GetMemoryType() const 73 | { 74 | return MemoryType::LocalMemory; 75 | } 76 | -------------------------------------------------------------------------------- /cookiecutter-testbed/{{cookiecutter.pypi_package_name}}/src/{{cookiecutter.project_slug}}/cli.py: -------------------------------------------------------------------------------- 1 | ''' 2 | The control software for an example testbed called {{cookiecutter.project_slug}}. 3 | 4 | Usage: 5 | {{cookiecutter.project_slug}} start server [--port PORT_ID] [--simulated] [--config_path PATH]... 6 | {{cookiecutter.project_slug}} (-h | --help) 7 | {{cookiecutter.project_slug}} --version 8 | 9 | Options: 10 | -p, --port PORT_ID The port number on which the testbed server operates. 11 | Defaults to the port specified in the config file. 12 | --simulated Whether the testbed server should be run in simulated mode or not. 13 | -c, --config_path PATH A path were additional config files can be found. By default, 14 | the config directory of this example is prepended. 15 | -h, --help Show this help message and exit. 16 | --version Show version and exit. 17 | ''' 18 | 19 | from catkit2.testbed.testbed import Testbed 20 | from docopt import docopt 21 | from . import config 22 | 23 | from {{cookiecutter.project_slug}} import utils 24 | 25 | def get_port(arguments, config): 26 | if arguments['--port'] is None: 27 | # Load port from config file. 28 | return config['testbed']['default_port'] 29 | else: 30 | # Return specified port. 31 | try: 32 | return int(arguments['--port']) 33 | except ValueError: 34 | raise RuntimeError( 35 | 'The supplied port number must be an integer.') 36 | 37 | 38 | def main(): 39 | arguments = docopt(__doc__, version='0.1') 40 | 41 | if arguments['start']: 42 | configuration = config.read_config(arguments['--config_path']) 43 | port = get_port(arguments, configuration) 44 | 45 | if arguments['server']: 46 | print( 47 | 'Starting the {{cookiecutter.project_slug}} testbed on {}...'.format( 48 | port)) 49 | server = Testbed(port, arguments['--simulated'], configuration) 50 | 51 | print( 52 | 'Use Ctrl-C to terminate the server and close all modules.') 53 | server.run() 54 | -------------------------------------------------------------------------------- /catkit_core/HybridPoolAllocator.h: -------------------------------------------------------------------------------- 1 | #ifndef HYBRID_POOL_ALLOCATOR_H 2 | #define HYBRID_POOL_ALLOCATOR_H 3 | 4 | #include "BuddyAllocator.h" 5 | #include "PoolAllocator.h" 6 | #include "Shareable.h" 7 | #include "ConcurrentVector.h" 8 | #include "RefCounter.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | class HybridPoolAllocator : public Shareable 19 | { 20 | private: 21 | struct Pool 22 | { 23 | std::uint64_t map; 24 | std::array, 64> ref_counts; 25 | }; 26 | 27 | public: 28 | using Handle = BuddyAllocator::Handle; 29 | static const Handle INVALID_HANDLE = BuddyAllocator::INVALID_HANDLE; 30 | 31 | static std::shared_ptr Create(StructStream &stream, std::size_t capacity, std::size_t min_size, std::size_t min_size_pool); 32 | static std::shared_ptr Open(StructStream &stream); 33 | 34 | ShareableType GetType() const override; 35 | static std::size_t GetSharedStateSize(std::size_t capacity, std::size_t min_size); 36 | 37 | Handle Allocate(std::size_t size); 38 | bool Acquire(Handle handle); 39 | bool Release(Handle handle); 40 | 41 | std::size_t GetOffset(Handle handle) const; 42 | 43 | public: 44 | HybridPoolAllocator(std::size_t capacity, std::size_t min_size, std::size_t min_size_pool, std::shared_ptr allocator, Pool *pools, std::shared_ptr memory_block); 45 | ~HybridPoolAllocator(); 46 | 47 | std::size_t GetLevelFromHandle(Handle handle) const; 48 | std::size_t GetLevelFromSize(std::size_t size) const; 49 | std::size_t GetSizeFromHandle(Handle handle) const; 50 | 51 | std::size_t m_Capacity; 52 | std::size_t m_MinSize; 53 | std::size_t m_MinSizePool; 54 | 55 | std::shared_ptr m_Allocator; 56 | 57 | Pool *m_Pools; 58 | 59 | using Bucket = std::vector; 60 | static constexpr std::size_t MAX_NUM_LEVELS = 64; 61 | 62 | // An array of buckets for each thread. 63 | ConcurrentVector> m_Buckets; 64 | 65 | Bucket &GetBucket(std::size_t level); 66 | }; 67 | 68 | #endif // HYBRID_POOL_ALLOCATOR_LOCAL_H 69 | -------------------------------------------------------------------------------- /docs/services/bmc_deformable_mirror.rst: -------------------------------------------------------------------------------- 1 | BMC Deformable Mirror (Inherits from DeformableMirror) 2 | ====================================================== 3 | 4 | This is a base class for Boston Micromachines DMs, which inherits from the general DM base class. This class 5 | abstracts away the discretization of the voltage and total surface, as well as the handling of the flat maps and gain 6 | maps. 7 | 8 | .. note:: 9 | The provided flat maps and gain maps need to be FITS files in DM map format. 10 | 11 | The child hardware service is ``BmcDeformableMirrorHardware``, and the child simulated service is 12 | ``BmcDeformableMirrorSim``. While the simulated service talks directly to a simulator, the hardware service talks to the 13 | actual hardware. The latter performs a conversion to the actual hardware device command by reading an optional command 14 | starting index ``device_command_index`` from the service configuration. This parameter has three value options: 15 | 16 | - *Undefined*: Zero will be assumed as the hardware command index. 17 | - *Integer*: If only one device is controlled by the service, using a non-zero starting index for its hardware command. 18 | - *List of integers*: If multiple devices with the same number of actuators are controlled by the service. 19 | 20 | Configuration 21 | ------------- 22 | 23 | .. code-block:: YAML 24 | 25 | boston_dm: 26 | service_type: bmc_deformable_mirror_hardware 27 | simulated_service_type: bmc_deformable_mirror_sim 28 | interface: deformable_mirror 29 | requires_safety: true 30 | 31 | serial_number: 00XX000#000 32 | dac_bit_depth: 33 | max_volts: 200 34 | 35 | device_actuator_mask_fname: !path ../data/boston_dms/DM_mask.fits 36 | flat_map_fname: !path ../data/boston_dms/flat_map.fits 37 | gain_map_fname: !path ../data/boston_dms/gain_map.fits 38 | 39 | channels: 40 | - correction_howfs 41 | - correction_lowfs 42 | - probe 43 | - poke 44 | - aberration 45 | - atmosphere 46 | - astrogrid 47 | - resume 48 | 49 | Properties 50 | ---------- 51 | See ``DeformableMirror``. 52 | 53 | Commands 54 | -------- 55 | None. 56 | 57 | Datastreams 58 | ----------- 59 | See ``DeformableMirror``. 60 | -------------------------------------------------------------------------------- /catkit2/services/thorlabs_cld101x_sim/thorlabs_cld101x_sim.py: -------------------------------------------------------------------------------- 1 | from catkit2.testbed.service import Service 2 | 3 | import numpy as np 4 | 5 | 6 | class ThorlabsCLD101XSim(Service): 7 | def __init__(self): 8 | super().__init__('thorlabs_cld101x_sim') 9 | 10 | self.visa_id = self.config['visa_id'] 11 | self.wavelength = self.config['wavelength'] 12 | self.function_mode = self.config['function_mode'] # 'current' or 'power' 13 | self.max_current = self.config['max_current'] # in Ampere 14 | 15 | self.current_setpoint = self.make_data_stream(f'current_setpoint_{self.wavelength}', 'float32', [1], 20) 16 | self.current_percent = self.make_data_stream(f'current_percent_{self.wavelength}', 'float32', [1], 20) 17 | 18 | def open(self): 19 | self.current_percent.submit_data(np.array([0.0], dtype='float32')) 20 | 21 | def main(self): 22 | while not self.should_shut_down: 23 | try: 24 | # Get an update for this channel 25 | frame = self.current_percent.get_next_frame(10) 26 | # Set to new current 27 | self.set_current_setpoint(frame.data[0]) 28 | 29 | except Exception: 30 | # Timed out. This is used to periodically check the shutdown flag. 31 | continue 32 | 33 | def close(self): 34 | pass 35 | 36 | def set_current_setpoint(self, current_percent): 37 | """ 38 | Set the current setpoint of the laser, controlled as percent of its max current setpoint. 39 | 40 | Parameters 41 | ---------- 42 | current_percent : int 43 | Limited to range 0-100, in percent of the max current setpoint in Ampere. 44 | """ 45 | if current_percent < 0 or current_percent > 100: 46 | raise ValueError("Current_percent must be between 0 and 100.") 47 | 48 | current_setpoint = current_percent / 100 * self.max_current 49 | self.testbed.simulator.set_source_power(source_name=self.id, power=current_percent) 50 | 51 | self.current_setpoint.submit_data(np.array([current_setpoint], dtype='float32')) 52 | 53 | 54 | if __name__ == '__main__': 55 | service = ThorlabsCLD101XSim() 56 | service.run() 57 | -------------------------------------------------------------------------------- /catkit2/testbed/testbed_proxy.py: -------------------------------------------------------------------------------- 1 | from .. import catkit_bindings 2 | 3 | from .service_proxy import ServiceProxy 4 | from .proxies import * # noqa 5 | 6 | class TestbedProxy(catkit_bindings.TestbedProxy): 7 | '''A client for connecting to a testbed server. 8 | 9 | This object acts as a proxy for the Testbed object. 10 | ''' 11 | def __init__(self, host, port): 12 | super().__init__(host, port) 13 | 14 | self._services = {} 15 | 16 | def get_service(self, service_id): 17 | '''Get a ServiceProxy object for the service `service_id`. 18 | 19 | Parameters 20 | ---------- 21 | service_id : string 22 | The identifier for the service for which to return the ServiceProxy. 23 | 24 | Returns 25 | ------- 26 | ServiceProxy or derived class object. 27 | A ServiceProxy for the named service. 28 | ''' 29 | # If we already created a proxy, return it. 30 | if service_id in self._services: 31 | return self._services[service_id] 32 | 33 | # Get the service interface class. 34 | interface_name = self.config['services'][service_id].get('interface') 35 | service_proxy_class = ServiceProxy.get_service_interface(interface_name) 36 | 37 | # Create proxy and store it in the cache. 38 | proxy = service_proxy_class(self, service_id) 39 | self._services[service_id] = proxy 40 | 41 | return proxy 42 | 43 | def __getattr__(self, item): 44 | '''Get the ServiceProxy named after the attribute. 45 | 46 | This acts as a shortcut for :func:`~catkit2.testbed.TestbedProxy.get_service`. 47 | 48 | Parameters 49 | ---------- 50 | item : string 51 | The identifier for the service for which to return the ServiceProxy. 52 | 53 | Returns 54 | ------- 55 | ServiceProxy or derived class object. 56 | A ServiceProxy for the named service. 57 | ''' 58 | try: 59 | service = self.get_service(item) 60 | 61 | # Remember the service for next time. 62 | setattr(self, item, service) 63 | 64 | return service 65 | except Exception as e: 66 | raise AttributeError(str(e)) 67 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Catkit2' 21 | copyright = '2021, Emiel Por' 22 | author = 'Emiel Por' 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | 'sphinx.ext.autodoc', 32 | 'sphinx.ext.mathjax', 33 | 'sphinx_automodapi.automodapi', 34 | 'breathe' 35 | ] 36 | 37 | breathe_projects = {"catkit_core": "./doxygen/xml/"} 38 | breathe_default_project = "catkit_core" 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # List of patterns, relative to source directory, that match files and 44 | # directories to ignore when looking for source files. 45 | # This pattern also affects html_static_path and html_extra_path. 46 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'doxygen'] 47 | 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | html_theme = 'sphinx_rtd_theme' 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | html_static_path = ['_static'] 60 | -------------------------------------------------------------------------------- /catkit_core/Log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H 2 | #define LOG_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifndef LOG_LEVEL 10 | #define LOG_LEVEL 5 11 | #endif 12 | 13 | #if LOG_LEVEL > 0 14 | #define LOG_CRITICAL(message) SubmitLogEntry(__FILE__, __LINE__, __func__, S_CRITICAL, message) 15 | #else 16 | #define LOG_CRITICAL(message) ((void) 0) 17 | #endif 18 | 19 | #if LOG_LEVEL > 1 20 | #define LOG_ERROR(message) SubmitLogEntry(__FILE__, __LINE__, __func__, S_ERROR, message) 21 | #else 22 | #define LOG_ERROR(message) ((void) 0) 23 | #endif 24 | 25 | #if LOG_LEVEL > 2 26 | #define LOG_WARNING(message) SubmitLogEntry(__FILE__, __LINE__, __func__, S_WARNING, message) 27 | #else 28 | #define LOG_WARNING(message) ((void) 0) 29 | #endif 30 | 31 | #if LOG_LEVEL > 3 32 | #define LOG_INFO(message) SubmitLogEntry(__FILE__, __LINE__, __func__, S_INFO, message) 33 | #else 34 | #define LOG_INFO(message) ((void) 0) 35 | #endif 36 | 37 | #if LOG_LEVEL > 4 38 | #define LOG_DEBUG(message) SubmitLogEntry(__FILE__, __LINE__, __func__, S_DEBUG, message) 39 | #else 40 | #define LOG_DEBUG(message) ((void) 0) 41 | #endif 42 | 43 | #ifdef NDEBUG 44 | #define LOG_ASSERT(condition, message) if (!(condition)) LOG_ERROR("Assertion failed: "s + (message)) 45 | #else 46 | #define LOG_ASSERT(condition, message) ((void) 0) 47 | #endif 48 | 49 | // Define the same numeric values of log levels as Python. 50 | enum Severity 51 | { 52 | S_CRITICAL = 50, 53 | S_ERROR = 40, 54 | S_WARNING = 30, 55 | S_INFO = 20, 56 | S_DEBUG = 10 57 | }; 58 | 59 | typedef struct LogEntry 60 | { 61 | std::string filename; 62 | unsigned int line; 63 | std::string function; 64 | Severity severity; 65 | std::string message; 66 | std::uint64_t timestamp; 67 | std::string time; 68 | 69 | LogEntry(std::string filename, unsigned int line, std::string function, Severity severity, std::string message, std::uint64_t timestamp); 70 | } LogEntry; 71 | 72 | class LogListener 73 | { 74 | public: 75 | LogListener(); 76 | virtual ~LogListener(); 77 | 78 | virtual void AddLogEntry(const LogEntry &entry); 79 | }; 80 | 81 | void SubmitLogEntry(std::string filename, unsigned int line, std::string function, Severity severity, std::string message); 82 | std::string ConvertSeverityToString(Severity severity); 83 | 84 | #endif // LOG_H 85 | -------------------------------------------------------------------------------- /docs/services/nkt_superk_fianium.rst: -------------------------------------------------------------------------------- 1 | NKT Super K FIANIUM Compact Tunable Laser 2 | ========================================= 3 | The NKT Super K service contains software for controlling both the `NKT SuperK FIANIUM Supercontinuum White Light Laser `_ 4 | and the `NKT SuperK VARIA Variable Bandpass Filter `_. 5 | This is done because a single open port to the device is needed that cannot be shared between multiple services. 6 | 7 | Associated drivers for both the FIANIUM and VARIA also need to be installed. 8 | 9 | .. note:: 10 | There is a separate service for the NKT SuperK EVO. 11 | 12 | Configuration 13 | ------------- 14 | 15 | .. code-block:: YAML 16 | 17 | nkt_superk: 18 | service_type: nkt_superk_fianium 19 | simulated_service_type: nkt_superk_fianium_sim 20 | interface: nkt_superk_fianium 21 | requires_safety: false 22 | 23 | port: COM4 24 | pulse_picker_safety: 5 25 | 26 | emission: 3 27 | power_setpoint: 100 28 | pulse_picker_ratio: 1 29 | nd_setpoint: 100 30 | lwp_setpoint: 638 31 | swp_setpoint: 643 32 | sleep_time_per_nm: 0.013 33 | base_sleep_time: 0.05 34 | 35 | Properties 36 | ---------- 37 | 38 | None. 39 | 40 | Commands 41 | -------- 42 | 43 | None. 44 | 45 | Datastreams 46 | ----------- 47 | ``emission``: Output emission of the FIANIUM (int) - 0 is OFF, 3 is ON. 48 | 49 | ``power_setpoint``: Output emission power level of the FIANIUM (in percent). 50 | 51 | ``pulse_picker_ratio``: Pulse picker ratio for the FIANIUM. 52 | 53 | ``monitor_input``: Monitors the input to the VARIA from the FIANIUM. 54 | 55 | ``nd_setpoint``: Set point for the VARIA ND filter. 56 | 57 | ``swp_setpoint``: Upper bandwidth limit for the VARIA (in nm). 58 | 59 | ``lwp_setpoint``: Lower bandwidth limit for the VARIA (in nm). 60 | 61 | ``nd_filter_moving``: Whether the ND filer is moving for the VARIA. 62 | 63 | ``swp_filter_moving``: Whether the short wavelength (high-pass) filter is moving for the VARIA. 64 | 65 | ``lwp_filter_moving``: Whether the long wavelength (low-pass) filter is moving for the VARIA. 66 | -------------------------------------------------------------------------------- /catkit2/testbed/service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from .. import catkit_bindings 5 | from .logging import CatkitLogHandler 6 | from .testbed_proxy import TestbedProxy 7 | 8 | doc = ''' 9 | Usage: 10 | service --id=ID --port=PORT --testbed_port=TESTBED_PORT 11 | 12 | Options: 13 | --id=ID The ID of the service. This should correspond to a value in the testbed configuration. 14 | --port=PORT The port for this service. 15 | --testbed_port=TESTBED_PORT The port where the testbed is running. 16 | ''' 17 | 18 | def parse_service_args(argv=None): 19 | '''Parse the command line arguments for a launched service. 20 | 21 | Parameters 22 | ---------- 23 | argv : list of strings or None 24 | The command line arguments. If this is None (default), 25 | sys.argv will be used instead. 26 | 27 | Returns 28 | ------- 29 | service_id : string 30 | The name of the service that was launched. 31 | service_port : integer 32 | The port of the service to start on. 33 | testbed_port : integer 34 | The port of the testbed server to connect to. 35 | ''' 36 | if argv is None: 37 | argv = sys.argv 38 | 39 | arguments = catkit_bindings.parse_service_args(argv) 40 | 41 | res = { 42 | 'service_id': arguments[0], 43 | 'service_port': arguments[1], 44 | 'testbed_port': arguments[2], 45 | } 46 | 47 | return res 48 | 49 | class Service(catkit_bindings.Service): 50 | log = logging.getLogger(__name__) 51 | 52 | def __init__(self, service_type, **kwargs): 53 | # Parse service arguments from argv, and update with overridden arguments. 54 | service_args = parse_service_args() 55 | service_args.update(kwargs) 56 | 57 | super().__init__(service_type, **service_args) 58 | 59 | # Override the testbed attribute with the extended Python version. 60 | self._testbed = TestbedProxy(getattr(super(), 'testbed').host, getattr(super(), 'testbed').port) 61 | 62 | # Set up log handler. 63 | self._log_handler = CatkitLogHandler() 64 | logging.getLogger(__name__).addHandler(self._log_handler) 65 | logging.getLogger(__name__).setLevel(logging.DEBUG) 66 | 67 | @property 68 | def testbed(self): 69 | return self._testbed 70 | -------------------------------------------------------------------------------- /docs/services/phasics_cam.rst: -------------------------------------------------------------------------------- 1 | Phasics Camera 2 | ========================= 3 | 4 | This service operates a Phasics SID4 camera using the Phasics SDK for communication and PySID4x python library. It handles image acquisition, and by default returns Phase Maps as images. For raw intensity values see the 'intensity' data stream. 5 | 6 | .. note:: 7 | The Phasics Camera requires the Phasics SDK and PySID4x Python package to be installed and configured correctly. To get a copy of the Phasics SDK and PySID4x python package please contact Phasics directly for customer support. 8 | 9 | 10 | Configuration 11 | ------------- 12 | 13 | .. code-block:: YAML 14 | 15 | phasics_cam: 16 | service_type: phasics_cam 17 | simulated_service_type: phasics_cam_sim 18 | interface: camera 19 | mode: normal 20 | mask: !path "../../data/phasics/2025-01-28 full aperture.msk" 21 | usr_profile_path: C:/path/to/SID4_SDK_x86_64/Examples/UserProfiles/SID4-1481/SID4-1481.txt 22 | wavelength: 633.0 23 | buffer_frames: 16 24 | requires_safety: false 25 | exposure_time: 504 26 | 27 | ``exposure_time``: Exposure time in milliseconds. 28 | 29 | ``mode``: 'normal' mode operates the camera. 'dummy' mode generates fake data for testing signal chain. 30 | 31 | ``wavelength``: The wavelength in (nm) of the camera 32 | 33 | ``mask``: the location of the camera mask 34 | 35 | ``usr_profile_path``: the location of hte SID4 SDK user profile 36 | 37 | ``buffer frames``: number of buffered frames in the catkit2 datastream 38 | 39 | Properties 40 | ---------- 41 | None. 42 | 43 | 44 | Commands 45 | ----------- 46 | ``take_measurement``: Acquires a single measurement from the Phasics camera. 47 | 48 | ``start_acquisition``: Begins continuous image acquisition from the camera. 49 | 50 | ``end_acquisition``: Stops the continuous image acquisition. 51 | 52 | ``phase_filtering``: Applies phase filtering to the acquired images using supplied zernike projection coefficients 53 | 54 | ``phase_projection``: Performs zernike phase projection on the acquired images. 55 | 56 | 57 | Datastreams 58 | ----------- 59 | ``images``: The acquired (Phase map) images from the Phasics camera. 60 | 61 | ``is_acquiring``: Indicates whether the service is currently acquiring data (True for acquiring, False for not acquiring). 62 | 63 | ``intensity``: The acquired intensity images from Phasics camera. 64 | -------------------------------------------------------------------------------- /docs/services/deformable_mirror.rst: -------------------------------------------------------------------------------- 1 | Deformable Mirror (Base class) 2 | ============================== 3 | 4 | This is the deformable mirror base class. It abstracts away the virtual channels, device actuator map and all data 5 | streams. It is meant to be subclassed by the specific DM implementation, both for hardware and software devices. 6 | 7 | This service controls any number of DMs as long as they are all controlled by the same driver, and they all use the same 8 | shape (optionally zero-padded). 9 | 10 | .. note:: 11 | This service and all its subclasses allow for exactly two types of commands: 12 | 1. "DM command". A 1D array containing all individual DM actuator commands concatenated together. 13 | 2. "DM map". A multidimensional stack of DM actuator maps, where the first dimension is the DM index. 14 | 15 | DM commands are used extensively in various experiments and are easier to create, while DM maps are used for plotting purposes. 16 | This division makes it easier to distinguish between them since one is a 1D array and 17 | the other one is not. Any conversion to a device-specific hardware command should be done in the subclass, as 18 | it would only be used for communicating with the hardware devices. 19 | 20 | The **device actuator mask file needs to be a FITS file in DM map format**. The actual device actuators are identified 21 | by the non-zero pixels in the mask. The mask is used to determine the number of actuators and their positions on the 22 | device. 23 | 24 | The **data streams hold exclusively 1D DM commands**, where all device actuators from all DMs are concatenated in sequence. 25 | 26 | At startup, each channel will have an optional startup map applied. 27 | 28 | Configuration 29 | ------------- 30 | This service cannot be used as-is since it lacks an implementation of the ``send_surface()`` method. It is meant to be 31 | subclassed by the specific DM implementation, both for hardware and software devices. 32 | 33 | Properties 34 | ---------- 35 | ``channels``: List of command channel names (dict). 36 | 37 | Commands 38 | -------- 39 | None. 40 | 41 | Datastreams 42 | ----------- 43 | ``total_voltage``: Array of the total voltage applied to each actuator of the DM. 44 | 45 | ``total_surface``: Array of the total amplitude of each DM actuator (nm). 46 | 47 | ``channels[channel_name]``: The command per virtual channel, identified by channel name, in nm surface. 48 | -------------------------------------------------------------------------------- /tests/test_shared_memory.py: -------------------------------------------------------------------------------- 1 | from catkit2.catkit_bindings import SharedMemory 2 | import pytest 3 | 4 | 5 | def test_shared_memory_lifetime(): 6 | stream_id = 'test_memory' 7 | memory = SharedMemory.create(stream_id, 1024) 8 | 9 | # Creating shared memory with the same ID should raise an error. 10 | with pytest.raises(RuntimeError, match="Something went wrong while creating shared memory"): 11 | SharedMemory.create(stream_id, 1024) 12 | 13 | # We should be able to access the shared memory when opening. 14 | memory2 = SharedMemory.open(stream_id) 15 | 16 | # After closing the opened memory, we should still be able to reopen it. 17 | del memory2 18 | 19 | memory2 = SharedMemory.open(stream_id) 20 | 21 | # Delete both the created and opened memory. 22 | del memory 23 | del memory2 24 | 25 | # Now we should not be able to open the shared memory anymore. 26 | with pytest.raises(RuntimeError, match="Something went wrong while opening shared memory"): 27 | SharedMemory.open(stream_id) 28 | 29 | # However, we should be able to create a new shared memory with the same ID. 30 | memory = SharedMemory.create(stream_id, 1024) 31 | del memory 32 | 33 | def test_large_shared_memory_size(): 34 | large_size = 1024 * 1024 * 1024 # 1GB 35 | 36 | stream_id = 'large_memory' 37 | 38 | memory = SharedMemory.create(stream_id, large_size) 39 | assert memory is not None 40 | 41 | del memory 42 | 43 | 44 | def test_shared_memory_open_non_existent(): 45 | stream_id = 'non_existent_memory' 46 | 47 | # Opening a non-existent shared memory should raise an error. 48 | with pytest.raises(RuntimeError, match="Something went wrong while opening shared memory"): 49 | SharedMemory.open(stream_id) 50 | 51 | def test_shared_memory_wrong_type(): 52 | # Opening a shared memory with a float for the memory size should raise an error. 53 | with pytest.raises(TypeError) as excinfo: 54 | SharedMemory.create('some_id', 1024.1) # Invalid type 55 | assert "create(): incompatible function arguments" in str(excinfo.value) 56 | 57 | # Opening a shared memory with a string for the memory size should raise an error. 58 | with pytest.raises(TypeError) as excinfo: 59 | SharedMemory.create('some_id', 'abc') # Invalid type 60 | assert "create(): incompatible function arguments" in str(excinfo.value) 61 | -------------------------------------------------------------------------------- /tests/test_datastream.py: -------------------------------------------------------------------------------- 1 | from catkit2.catkit_bindings import DataStream 2 | import numpy as np 3 | import pytest 4 | import sys 5 | 6 | dtypes = ['int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'int64', 'uint64', 'float32', 'float64', 'complex64', 'complex128'] 7 | shapes = [[10], [10, 10], [10, 10, 10], [10, 10, 10, 10]] 8 | 9 | @pytest.mark.parametrize("shape", shapes) 10 | @pytest.mark.parametrize("dtype", dtypes) 11 | def test_data_stream(shape, dtype): 12 | # Use a unique name for each created stream. 13 | created_stream = DataStream.create(f'{dtype}_{len(shape)}_stream', 'service', dtype, shape, 20) 14 | 15 | # Created stream should have the right dtype and shape. 16 | assert created_stream.dtype == dtype 17 | assert np.allclose(created_stream.shape, shape) 18 | 19 | opened_stream = DataStream.open(created_stream.stream_id) 20 | 21 | # The opened stream should have the right dtype and shape. 22 | assert opened_stream.dtype == dtype 23 | assert np.allclose(opened_stream.shape, shape) 24 | 25 | data = np.abs(np.random.randn(*shape)).astype(dtype) 26 | created_stream.submit_data(data) 27 | 28 | frame = opened_stream.get_latest_frame() 29 | 30 | # This should be the first frame on this datastream. 31 | assert frame.id == 0 32 | 33 | # The data should match with what we put in. 34 | assert np.allclose(frame.data, data) 35 | assert np.allclose(opened_stream.get(), data) 36 | 37 | # We should get an error if we submit the wrong dtype on a data stream. 38 | for send_dtype in dtypes: 39 | if send_dtype == dtype: 40 | continue 41 | 42 | data = np.abs(np.random.randn(*shape)).astype(send_dtype) 43 | 44 | with pytest.raises(RuntimeError): 45 | created_stream.submit_data(data) 46 | 47 | # We should get an error if we submit data with the wrong shape. 48 | wrong_shape = np.copy(shape) 49 | wrong_shape[-1] += 1 50 | 51 | data = np.abs(np.random.randn(*wrong_shape)).astype(dtype) 52 | 53 | with pytest.raises(RuntimeError): 54 | created_stream.submit_data(data) 55 | 56 | # We should get an error if we submit a non-contiguous array. 57 | if len(shape) >= 2: 58 | data = np.abs(np.random.randn(*wrong_shape)).astype(dtype) 59 | data = data[..., :-1] 60 | 61 | with pytest.raises(RuntimeError): 62 | created_stream.submit_data(data) 63 | -------------------------------------------------------------------------------- /tests/test_server_client.py: -------------------------------------------------------------------------------- 1 | from catkit2.catkit_bindings import Server, Client 2 | import time 3 | import pytest 4 | import weakref 5 | 6 | class OurServer(Server): 7 | def __init__(self, port): 8 | super().__init__(port) 9 | 10 | self.register_request_handler('foo', self.foo) 11 | self.register_request_handler('bar', self.bar) 12 | 13 | def foo(self, data): 14 | return ('foo:' + data.decode('ascii')).encode('ascii') 15 | 16 | def bar(self, data): 17 | raise ValueError("Data is incorrect.") 18 | 19 | class OurClient(Client): 20 | def __init__(self, port): 21 | super().__init__('127.0.0.1', port) 22 | 23 | def foo(self, data): 24 | return self.make_request('foo', data) 25 | 26 | def bar(self): 27 | return self.make_request('bar', b'other_data') 28 | 29 | def baz(self): 30 | return self.make_request('baz', b'even_other_data') 31 | 32 | def test_server_client_communication(unused_port): 33 | port = unused_port() 34 | 35 | server = OurServer(port) 36 | client = OurClient(port) 37 | 38 | server.start() 39 | 40 | assert client.foo(b'abcd') == b'foo:abcd' 41 | 42 | with pytest.raises(RuntimeError, match='Data is incorrect'): 43 | client.bar() 44 | 45 | with pytest.raises(RuntimeError, match='Unknown request type'): 46 | client.baz() 47 | 48 | server.stop() 49 | 50 | def test_server_cleanup(unused_port): 51 | port = unused_port() 52 | server = OurServer(port) 53 | 54 | # Use a weak reference to the server, to check if actually gets deleted. 55 | server_ref = weakref.ref(server) 56 | assert server_ref() is not None 57 | 58 | # Run the server. 59 | server.start() 60 | time.sleep(0.5) 61 | server.stop() 62 | 63 | # Delete the server. 64 | del server 65 | 66 | # The server should now have been deleted. 67 | assert server_ref() is None 68 | 69 | def test_server_cleanup_manual(unused_port): 70 | port = unused_port() 71 | server = OurServer(port) 72 | 73 | # Use a weak reference to the server, to check if actually gets deleted. 74 | server_ref = weakref.ref(server) 75 | assert server_ref() is not None 76 | 77 | # Cleanup the request handlers and delete the server. 78 | server.cleanup_request_handlers() 79 | del server 80 | 81 | # This should have deleted the server object itself. 82 | assert server_ref() is None 83 | --------------------------------------------------------------------------------