Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a2feb24
Save instrument configuration to YAML following to a set of given pro…
YagoDel Oct 15, 2020
f4ed033
Removing hardcoded parameters and moving them to configuration files
YagoDel Oct 15, 2020
0b2a15c
Default Andor parameters
YagoDel Oct 16, 2020
5caed41
Corrected defaults
YagoDel Oct 16, 2020
86181db
Utility PyQt message boxes following pymsgbox
YagoDel Oct 22, 2020
fd44e10
Saving config to JSON. Using PyQt for warning message boxes
YagoDel Oct 22, 2020
7911fc9
Default Andor JSON
YagoDel Oct 23, 2020
b54b267
Debugged some of the amplifier and channel handling. Removed channel …
YagoDel Oct 23, 2020
be95189
Merge branch 'master' of https://github.com/nanophotonics/nplab into …
YagoDel Oct 28, 2020
ab56ef0
CameraHandle debug
YagoDel Oct 28, 2020
e5fe15e
First pass at integrating old configuration code with HDF5 files with…
YagoDel Oct 28, 2020
31ef641
Debug
YagoDel Oct 28, 2020
7b50dc2
Might need reverting. Removing old config code
YagoDel Oct 28, 2020
b80cb99
config_file for ease of use
YagoDel Oct 28, 2020
1b3fcc1
Might need reverting. Updated spectrometer config and tested with Sea…
YagoDel Oct 28, 2020
fa7c63e
Debugged update_config. No prompt if file exists already. Handle empt…
YagoDel Oct 28, 2020
b894eff
Cleanup
YagoDel Oct 28, 2020
e9e7d6d
Merge branch 'master' into issue124
YagoDel Oct 28, 2020
54cbfdb
Andor configuration loading using properties
YagoDel Nov 18, 2020
cdfc880
Merge from master
YagoDel Feb 26, 2021
4ca63bb
Default config always points to the correct location
YagoDel Mar 28, 2021
ad1fde5
Bug
YagoDel Mar 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 202 additions & 29 deletions nplab/instrument/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,34 +12,46 @@
"""

from builtins import str
from nplab.utils.thread_utils import locked_action_decorator, background_action_decorator
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

locked_action and background_action are nicer aliases IMO

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Could agree, not an issue for this branch :P (it's been like that since the start of nplab)

from nplab.utils.thread_utils import locked_action_decorator
import nplab
from weakref import WeakSet
import nplab.utils.log
from nplab.utils.array_with_attrs import ArrayWithAttrs
from nplab.utils.show_gui_mixin import ShowGUIMixin
from nplab.ui.widgets.msgbox import prompt_box
import logging
from nplab.utils.log import create_logger
import inspect
import os
import h5py
import datetime
# <<<<<<< HEAD
import json
import numpy as np
import tempfile


# =======
from contextlib import contextmanager
# >>>>>>> 642d2633a1fc31a24d017cb97b8427f0125f9387
LOGGER = create_logger('Instrument')
LOGGER.setLevel('INFO')


class Instrument(ShowGUIMixin):
"""Base class for all instrument-control classes.

This class takes care of management of instruments, saving data, etc.
"""
__instances = None
metadata_property_names = () #"Tuple of names of properties that should be automatically saved as HDF5 metadata
metadata_property_names = () # tuple of names of properties that should be automatically saved as HDF5 metadata
config_property_names = () # tuple of names of properties that are saved and loaded for default configuration
_CONFIG_EXTENSION = '.json'

def __init__(self):
"""Create an instrument object."""
super(Instrument, self).__init__()
Instrument.instances_set().add(self) #keep track of instances (should this be in __new__?)
Instrument.instances_set().add(self) # keep track of instances (should this be in __new__?)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think using new is necessary

self._logger = logging.getLogger('Instrument.' + str(type(self)).split('.')[-1].split('\'')[0])

@classmethod
Expand All @@ -60,7 +72,7 @@ def get_instance(cls, create=True, exceptions=True, *args, **kwargs):
Usually returns the first available instance.
"""
instances = cls.get_instances()
if len(instances)>0:
if len(instances) > 0:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if instances:

return instances[0]
else:
if create:
Expand Down Expand Up @@ -99,6 +111,7 @@ def create_dataset(cls, name, flush=True, *args, **kwargs):

:param name: should be a noun describing what the reading is (image,
spectrum, etc.)
:param flush: bool

Other arguments are passed to `nplab.datafile.Group.create_dataset`.
"""
Expand All @@ -107,17 +120,17 @@ def create_dataset(cls, name, flush=True, *args, **kwargs):
df = cls.get_root_data_folder()
dset = df.create_dataset(name, *args, **kwargs)
if 'data' in kwargs and flush:
dset.file.flush() #make sure it's in the file if we wrote data
dset.file.flush() # make sure it's in the file if we wrote data
return dset

def log(self, message,level = 'info'):
def log(self, message, level='info'):
"""Save a log message to the current datafile.

This is the preferred way to output debug/informational messages. They
will be saved in the current HDF5 file and optionally shown in the
nplab console.
"""
nplab.utils.log.log(message, from_object=self,level = level)
nplab.utils.log.log(message, from_object=self, level=level)

def get_metadata(self,
property_names=[],
Expand Down Expand Up @@ -177,31 +190,190 @@ def bundle_metadata(self, data, enable=True, **kwargs):
else:
return data

def open_config_file(self):
"""Open the config file for the current spectrometer and return it, creating if it's not there"""
if not hasattr(self,'_config_file'):
def get_config(self, mode='named'):
"""Returns the configuration dictionary

:param mode: str. Either 'named' or 'all'
If 'named' it only iterates over self.config_property_names.
If 'all' it iterates over the whole __dir__, ignoring hidden attributes/methods, and attempts to get the
values. Currently only returns values if they are one of the following:
bool, dict, float, int, list, str, tuple, np.array
:return:
"""
configuration = dict()
if mode == 'named':
for name in self.config_property_names:
try:
configuration[name] = getattr(self, name)
except Exception as e:
self._logger.debug('Failed getting configuration for key: %s' % name)
elif mode == 'all':
for name in dir(self):
if not name.startswith('_'): # ignores hidden attributes/methods
try:
value = getattr(self, name)
if type(value) in [bool, dict, float, int, list, str, tuple, np.array]:
try:
# check whether value can be saved to JSON. Check is done here rather than in
# self.save_config because self.save_config dumps the whole config at once
with tempfile.TemporaryFile('w') as f:
json.dump(value, f)
configuration[name] = value
except Exception as e:
self._logger.info('Configuration value for key: %s cannot be saved to json' % name)
except Exception as e:
self._logger.debug('Failed getting configuration for key: %s' % name)
else:
raise ValueError("Unrecognised configuration mode: %s. Needs to be 'named' or 'all'" % mode)
return configuration

def set_config(self, configuration):
"""Sets attributes according to configuration
:param configuration: dict
:return:
"""
for key, value in configuration.items():
try:
setattr(self, key, value)
except Exception as e:
self._logger.info('Configuration could not be set for: %s = %s' % (key, value))

config = property(get_config, set_config)

def _config_filename(self, name=None, extension=None):
"""Returns valid file path

:param name: str. Can be just the filename, a filename with/out an extension, or a relative/absolute path
:param extension: str
:return: str. Absolute path
"""
if name is None:
name = 'config' # default name
if extension is None:
extension = self._CONFIG_EXTENSION # default extension. Can be changed in subclasses
# Ensure name has expected extension or adds it if not there
root, ext = os.path.splitext(name)
if not ext:
ext = extension
else:
assert ext == extension
filename = root + ext

# Default location for configuration file is wherever the instance's Python definition is
if not os.path.isabs(filename):
f = inspect.getfile(self.__class__)
d = os.path.dirname(f)
self._config_file = nplab.datafile.DataFile(h5py.File(os.path.join(d, 'config.h5')), mode='a')
self._config_file.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y")
return self._config_file

config_file = property(open_config_file)

def update_config(self, name, data, attrs= None):
"""Update the configuration file for this spectrometer.

A file is created in the nplab directory that holds configuration
data for the spectrometer, including reference/background. This
function allows values to be stored in that file."""
f = self.config_file
if name in f:
try: del f[name]
except:
f[name][...] = data
f.flush()
# <<<<<<< HEAD
filename = os.path.join(d, filename)
return filename

def save_config(self, config=None, filename=None, mode='named'):
"""Saves instrument configuration to file
:param config: dict or None
:param filename: str. Passed to self._config_filename
:param mode: str. If config dictionary not given, passed to self.get_config
"""
# Get filename
filename = self._config_filename(filename)
if config is None:
config = self.get_config(mode)

# If the file exists, checks whether the user wants to overwrite it
if os.path.exists(filename):
reply = prompt_box(text='That configuration file exists. Do you want to overwrite it?', default=filename)
if not reply:
return
filename = reply

_, ext = os.path.splitext(filename)
if ext == '.json':
# Dumps the configuration dictionary to a JSON
with open(filename, 'w') as config_file:
json.dump(config, config_file, indent=4)
elif ext == '.h5':
with h5py.File(filename, 'w') as dfile:
dfile.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y")
for name, value in config.items():
try:
dfile.create_dataset(name, data=value)
except Exception as e:
self._logger.info('Configuration value for key: %s cannot be saved to HDF5' % name)
else:
raise ValueError('Unrecognised extension: %s' % ext)

def load_config(self, filename=None):
"""Loads configuration from file
:param filename: str. Passed to self._config_filename
:return: dict
"""
# Get filename
filename = self._config_filename(filename)
_, ext = os.path.splitext(filename)

# Loads and sets the configuration
if ext == '.json':
with open(filename, 'r') as config_file:
config = json.load(config_file)
elif ext == '.h5':
with h5py.File(filename, 'r') as dfile:
config = dict()
for key, value in dfile.items():
config[key] = value
else:
raise ValueError('Unrecognised extension: %s' % ext)
return config

config_file = property(load_config, save_config)

def update_config(self, name, data, filename=None):
"""Edits configuration file
:param name: str
:param data: anything that can be parsed by JSON or directly saved to HDF5
:param filename: str. Passed to self._config_filename
"""
# Get filename
filename = self._config_filename(filename)

_, ext = os.path.splitext(filename)
if ext == '.json':
# Need to read the whole JSON first, modify it, and then re-write the file
with open(filename, 'a') as config_file:
try:
config = json.load(config_file)
except: # would fail if config_file is empty
config = dict()
config[name] = data
config_file.seek(0)
json.dump(config, config_file)
config_file.truncate()
elif ext == '.h5':
with h5py.File(filename, 'a') as f:
if name in f:
del f[name]
f.create_dataset(name, data=data)
else:
f.create_dataset(name, data=data ,attrs = attrs)
raise ValueError('Unrecognised extension: %s' % ext)
# =======
# self._config_file = nplab.datafile.DataFile(h5py.File(os.path.join(d, 'config.h5')), mode='a')
# self._config_file.attrs['date'] = datetime.datetime.now().strftime("%H:%M %d/%m/%y")
# return self._config_file
#
# config_file = property(open_config_file)
#
# def update_config(self, name, data, attrs= None):
# """Update the configuration file for this spectrometer.
#
# A file is created in the nplab directory that holds configuration
# data for the spectrometer, including reference/background. This
# function allows values to be stored in that file."""
# f = self.config_file
# if name in f:
# try: del f[name]
# except:
# f[name][...] = data
# f.flush()
# else:
# f.create_dataset(name, data=data ,attrs = attrs)

@contextmanager
def temporarily_set(self, **kwargs):
Expand All @@ -223,3 +395,4 @@ def temporarily_set(self, **kwargs):
finally:
for key, value in original_settings.items():
setattr(self, key, value)
# >>>>>>> 642d2633a1fc31a24d017cb97b8427f0125f9387
15 changes: 14 additions & 1 deletion nplab/instrument/camera/Andor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
from nplab.ui.ui_tools import UiTools
from weakref import WeakSet
import time
import inspect


class Andor(CameraRoiScale, AndorBase):
metadata_property_names = ('Exposure', 'x_axis', 'CurrentTemperature',)
config_property_names = CameraRoiScale.config_property_names + ('ReadMode', 'AcquisitionMode', 'TriggerMode',
'Exposure', 'Shutter', 'SetTemperature',
'CoolerMode', 'FanMode', 'cooler')

def __init__(self, settings_filepath=None, camera_index=None, **kwargs):
def __init__(self, settings_filepath=None, camera_index=None, config_file=None, **kwargs):
super(Andor, self).__init__()
self.start(camera_index)

Expand All @@ -30,6 +34,15 @@ def __init__(self, settings_filepath=None, camera_index=None, **kwargs):
self.isAborted = False

self.detector_shape = self.DetectorShape # passing the Andor parameter to the CameraRoiScale class
if config_file is not None:
self.config = self.load_config(config_file)
else:
filename = 'default_config'
if not os.path.isabs(filename):
f = inspect.getfile(Andor)
d = os.path.dirname(f)
filename = os.path.join(d, filename)
self.config = self.load_config(filename)

def __del__(self):
# Need to explicitly call this method so that the shutdown procedure is followed correctly
Expand Down
Loading