-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #172 from spacetelescope/feature/ocean_optics_usb4000
Add services for Ocean Optics spectrometers
- Loading branch information
Showing
7 changed files
with
412 additions
and
1 deletion.
There are no files selected for viewing
193 changes: 193 additions & 0 deletions
193
catkit2/services/oceanoptics_spectrometer/oceanoptics_spectrometer.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
''' | ||
This module contains a service for Ocean Optics Spectrometers. | ||
''' | ||
import numpy as np | ||
|
||
from seabreeze.spectrometers import Spectrometer | ||
from seabreeze.spectrometers import SeaBreezeError | ||
|
||
from catkit2.testbed.service import Service | ||
|
||
|
||
class OceanOpticsSpectrometer(Service): | ||
''' | ||
Service for Ocean Optics Spectrometer. | ||
This service is a wrapper around the python seabreeze package. | ||
It provides a simple interface to control the spectrometer and acquire spectra. | ||
Attributes | ||
---------- | ||
spectrometer : Spectrometer | ||
The spectrometer to control. | ||
serial_number : str | ||
The serial number of the spectrometer. | ||
model : str | ||
The model of the spectrometer. | ||
pixels_number : int | ||
number of pixels in the spectrum. | ||
interval : float | ||
The interval between spectrum acquisitions in seconds. | ||
spectra : DataStream | ||
A data stream to submit spectra from the spectrometer. | ||
is_saturating : DataStream | ||
A data stream to submit whether the spectrometer is currently saturating. | ||
NUM_FRAMES : int | ||
The number of frames to allocate for the data streams. | ||
Methods | ||
------- | ||
open() | ||
Open the service. | ||
main() | ||
The main function of the service. | ||
close() | ||
Close the service. | ||
take_one_spectrum() | ||
Get a spectrum from the spectrometer. | ||
''' | ||
NUM_FRAMES = 20 | ||
|
||
def __init__(self): | ||
''' | ||
Create a new OceanOpticsSpectrometer service. | ||
''' | ||
super().__init__('oceanoptics_spectrometer') | ||
|
||
self.serial_number = str(self.config['serial_number']) | ||
self.interval = self.config.get('interval', 1) | ||
self._exposure_time = self.config.get('exposure_time', 1000) | ||
|
||
self.spectrometer = None | ||
self.model = None | ||
self.pixels_number = None | ||
|
||
def open(self): | ||
''' | ||
Open the service. | ||
This function is called when the service is opened. | ||
It initializes the spectrometer and creates the data streams and properties. | ||
Raises | ||
------ | ||
ImportError | ||
If the spectrometer cannot be found. | ||
''' | ||
|
||
# Find the spectrometer | ||
try: | ||
self.spectrometer = Spectrometer.from_serial_number(self.serial_number) | ||
except SeaBreezeError: | ||
raise ImportError(f'OceanOptics: Could not find spectrometer with serial number {self.serial_number}') | ||
|
||
# exctract attributes | ||
self.model = self.spectrometer.model | ||
self.pixels_number = self.spectrometer.pixels | ||
|
||
# Define and set default exposure time. | ||
self.exposure_time = self._exposure_time | ||
|
||
# Create datastreams | ||
self.spectra = self.make_data_stream('spectra', 'float32', [self.pixels_number], self.NUM_FRAMES) | ||
|
||
self.is_saturating = self.make_data_stream('is_saturating', 'int8', [1], self.NUM_FRAMES) | ||
self.is_saturating.submit_data(np.array([0], dtype='int8')) | ||
|
||
# Create properties | ||
def make_property_helper(name, read_only=False): | ||
if read_only: | ||
self.make_property(name, lambda: getattr(self, name)) | ||
else: | ||
self.make_property(name, lambda: getattr(self, name), lambda val: setattr(self, name, val)) | ||
|
||
make_property_helper('exposure_time') | ||
make_property_helper('wavelengths', read_only=True) | ||
|
||
def main(self): | ||
''' | ||
The main function of the service. | ||
This function is called when the service is started. | ||
''' | ||
while not self.should_shut_down: | ||
intensities = self.take_one_spectrum() | ||
self.spectra.submit_data(np.array(intensities, dtype='float32')) | ||
|
||
if np.max(intensities) == self.spectrometer.max_intensity: | ||
self.is_saturating.submit_data(np.array([1], dtype='int8')) | ||
else: | ||
self.is_saturating.submit_data(np.array([0], dtype='int8')) | ||
|
||
self.sleep(self.interval) | ||
|
||
def take_one_spectrum(self): | ||
''' | ||
Measure and return one spectrum. | ||
''' | ||
return self.spectrometer.intensities() | ||
|
||
@property | ||
def wavelengths(self): | ||
''' | ||
The wavelengths of the spectrometer in nm. | ||
Returns: | ||
-------- | ||
ndarray of float: | ||
wavelengths of the spectrometer. | ||
''' | ||
return self.spectrometer.wavelengths() | ||
|
||
@property | ||
def exposure_time(self): | ||
''' | ||
The exposure time in microseconds. | ||
Returns: | ||
-------- | ||
int: | ||
The exposure time in microseconds. | ||
''' | ||
return self._exposure_time | ||
|
||
@exposure_time.setter | ||
def exposure_time(self, exposure_time: int): | ||
''' | ||
Set the exposure time in microseconds. | ||
This property can be used to set the exposure time of the spectrometer. | ||
If the `exposure_time` parameter is inferior to the minimum allowed exposure time for this | ||
spectrometer (set by Ocean Optics), it is set to the minimum allowed exposure time. | ||
If the `exposure_time` is larger than the maximum allowed exposure time, it is set to the | ||
maximum allowed exposure time. | ||
Parameters | ||
---------- | ||
exposure_time : int | ||
The exposure time in microseconds. | ||
''' | ||
int_time_range = self.spectrometer.integration_time_micros_limits | ||
if exposure_time < int_time_range[0]: | ||
exposure_time = int_time_range[0] | ||
if exposure_time > int_time_range[1]: | ||
exposure_time = int_time_range[1] | ||
|
||
self._exposure_time = exposure_time | ||
self.spectrometer.integration_time_micros(exposure_time) | ||
|
||
def close(self): | ||
''' | ||
Close the service. | ||
This function is called when the service is closed. | ||
It closes the spectrometer. | ||
''' | ||
self.spectrometer.close() | ||
self.spectrometer = None | ||
|
||
|
||
if __name__ == '__main__': | ||
service = OceanOpticsSpectrometer() | ||
service.run() |
150 changes: 150 additions & 0 deletions
150
catkit2/services/oceanoptics_spectrometer_sim/oceanoptics_spectrometer_sim.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
''' | ||
This module contains a service for simulation Ocean Optics Spectrometers. | ||
''' | ||
import numpy as np | ||
from catkit2.testbed.service import Service | ||
|
||
|
||
class OceanOpticsSpectrometerSim(Service): | ||
''' | ||
Service for simulated Ocean Optics Spectrometer. | ||
Attributes | ||
---------- | ||
spectrometer : Spectrometer | ||
The spectrometer to control. | ||
serial_number : str | ||
The serial number of the spectrometer. | ||
pixels_number : int | ||
Number of pixels in the spectrum. | ||
interval : float | ||
The interval between spectrum acquisitions in seconds. | ||
spectra : DataStream | ||
A data stream to submit spectra from the spectrometer. | ||
is_saturating : DataStream | ||
A data stream to submit whether the spectrometer is currently saturating. | ||
NUM_FRAMES : int | ||
The number of frames to allocate for the data streams. | ||
Methods | ||
------- | ||
open() | ||
Open the service. | ||
main() | ||
The main function of the service. | ||
close() | ||
Close the service. | ||
take_one_spectrum() | ||
Get a spectrum from the spectrometer. | ||
''' | ||
NUM_FRAMES = 20 | ||
|
||
def __init__(self): | ||
''' | ||
Create a new OceanOpticsSpectrometer service. | ||
''' | ||
super().__init__('oceanoptics_spectrometer_sim') | ||
|
||
self.serial_number = str(self.config['serial_number']) | ||
self.interval = self.config.get('interval', 1) | ||
self._exposure_time = self.config.get('exposure_time', 1000) | ||
|
||
|
||
self.pixels_number = 3000 | ||
|
||
self.spectra = None | ||
self.is_saturating = None | ||
|
||
def open(self): | ||
''' | ||
Open the service. | ||
This function is called when the service is opened. | ||
It initializes the spectrometer and creates the data streams and properties. | ||
Raises | ||
------ | ||
RuntimeError | ||
If the spectrometer cannot be found. | ||
''' | ||
|
||
# Define and set defaut exposure time | ||
self.exposure_time = self._exposure_time | ||
|
||
# Create datastreams | ||
self.spectra = self.make_data_stream('spectra', 'float32', [self.pixels_number], self.NUM_FRAMES) | ||
|
||
self.is_saturating = self.make_data_stream('is_saturating', 'int8', [1], self.NUM_FRAMES) | ||
self.is_saturating.submit_data(np.array([0], dtype='int8')) | ||
|
||
# Create properties | ||
def make_property_helper(name, read_only=False): | ||
if read_only: | ||
self.make_property(name, lambda: getattr(self, name)) | ||
else: | ||
self.make_property(name, lambda: getattr(self, name), lambda val: setattr(self, name, val)) | ||
|
||
make_property_helper('exposure_time') | ||
make_property_helper('wavelengths', read_only=True) | ||
|
||
def main(self): | ||
''' | ||
The main function of the service. | ||
This function is called when the service is started. | ||
''' | ||
while not self.should_shut_down: | ||
intensities = self.take_one_spectrum(self.pixels_number) | ||
self.spectra.submit_data(np.array(intensities, dtype='float32')) | ||
self.is_saturating.submit_data(np.array([0], dtype='int8')) | ||
|
||
self.sleep(self.interval) | ||
|
||
def take_one_spectrum(self): | ||
''' | ||
Measure and return one spectrum. | ||
Simulated service: random numpy array. | ||
''' | ||
return np.random.random(self.pixels_number) + 1000 | ||
|
||
@property | ||
def wavelengths(self): | ||
''' | ||
The wavelengths of the spectrometer in nm. | ||
Simulated service: linear vector evenly spaced bewtween 350 and 1000 nm. | ||
Returns: | ||
-------- | ||
ndarray of float: | ||
wavelengths of the spectrometer. | ||
''' | ||
return np.linspace(350, 1000, num=self.pixels_number) | ||
|
||
@property | ||
def exposure_time(self): | ||
''' | ||
The exposure time in microseconds. | ||
Returns: | ||
-------- | ||
int: | ||
The exposure time in microseconds. | ||
''' | ||
return self._exposure_time | ||
|
||
@exposure_time.setter | ||
def exposure_time(self, exposure_time: int): | ||
''' | ||
Set the exposure time in microseconds. | ||
Parameters | ||
---------- | ||
exposure_time : int | ||
The exposure time in microseconds. | ||
''' | ||
self._exposure_time = exposure_time | ||
|
||
|
||
if __name__ == '__main__': | ||
service = OceanOpticsSpectrometerSim() | ||
service.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
from ..service_proxy import ServiceProxy | ||
|
||
@ServiceProxy.register_service_interface('oceanoptics_spectrometer') | ||
class OceanopticsSpectroProxy(ServiceProxy): | ||
|
||
def take_raw_exposures(self, num_exposures): | ||
first_frame_id = self.spectra.newest_available_frame_id | ||
|
||
i = 0 | ||
num_exposures_remaining = num_exposures | ||
|
||
while num_exposures_remaining >= 1: | ||
try: | ||
frame = self.spectra.get_frame(first_frame_id + i, 1000) | ||
except RuntimeError: | ||
# The frame wasn't available anymore because we were waiting too long. | ||
continue | ||
finally: | ||
i += 1 | ||
|
||
yield frame.data.copy() | ||
num_exposures_remaining -= 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.