diff --git a/nexus/hardware.py b/nexus/hardware.py index 0717ea10fa4a204d5e73ce9ad741dde417f4f4cd..d2b9eeea1a027cd91be40a11fe091c3b6693c290 100644 --- a/nexus/hardware.py +++ b/nexus/hardware.py @@ -1,15 +1,16 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -''' +""" Lloyd Carothers Sensor and datalogger classes -''' +""" + from collections import OrderedDict from enum import Enum import os.path import pickle -from obspy import Inventory, read_inventory +from obspy import read_inventory from obspy.core.inventory import Response from obspy.clients.nrl import NRL @@ -19,18 +20,26 @@ NRL_ROOT = NRL_URL current_dir = os.path.dirname(os.path.abspath(__file__)) etc_dir = os.path.join(current_dir, 'etc') resp_dir = os.path.join(etc_dir, 'SOH_RESP') -defaultsfile = os.path.join(etc_dir,'nexus.hardware') +defaultsfile = os.path.join(etc_dir, 'nexus.hardware') + +def chan_name_is_geophysical(channel_code: str) -> bool: + """ + Returns true if channel code is determined to be a waveform, + so the response should be fetched from the NRL. + i.e. Not a SOH channel. -def chan_name_is_geophysical(channel_code): - ''' - Returns true if channel code is determined to be a waveform which response - should be fetched from the NRL i.e. Not a SOH channel. - ''' + Args: + channel_code (str): The channel code to check if waveform. + + Returns: + bool: True if response should be fetched from the NRL, + False if response should be used from hardware. + """ band = channel_code[0] instrument = channel_code[1] orientation = channel_code[2] - + # Band codes from SEED Manual excluding A & O if band in 'FGDCESHBMLVURPTQ': # Seismic instrument codes @@ -45,86 +54,258 @@ def chan_name_is_geophysical(channel_code): class PlaceHolder(Enum): + """ + Default values for datalogger gain and + sampling rate. Gain is set to 1 and + SR is set to 2. + """ GAIN = 1 SR = 2 -class Hardware(object): - def __init__(self, name=''): +class Hardware(): + """ + Base class for representing a hardware component. + + Attributes: + name (str): The name of the hardware component. + nrl_keys (tuple): A tuple containing NRL keys associated + with the hardware. Initializes as an empty tuple. + """ + def __init__(self, name: str = ''): + """ + Initializes an instance of Hardware with the given name + and an empty tuple of nrl_keys. + + Args: + name (str, optional): The name of the hardware. Defaults to ''. + """ self.name = name - self.NRL_keys = () + self.nrl_keys = () class Sensor(Hardware): + """ + Represents a sensor device that extends the Hardware base class. + + Attributes: + name (str): The name of the sensor. + nrl_keys (tuple): A tuple containing NRL keys associated + with the sensor. Initializes as empty. + z_is_up (bool): Indicates orientation. Used to get dip angle. + Initializes as True. + + Examples: + >>> s = Sensor('CMG-3T) + """ def __init__(self, *args, **kwargs): + """ + Initializes an instance of Sensor with `z_is_up = True`. + """ super().__init__(*args, **kwargs) self.z_is_up = True - def __repr__(self): - s = f'{self.name} {self.NRL_keys}\n' + def __repr__(self) -> str: + """ + Returns a summary of the Sensor object with its name, + nrl_keys, and vertical orientation. + + Returns: + str: A string summarizing the sensor's information and + orientation. + """ + s = f'{self.name} {self.nrl_keys}\n' if self.z_is_up: s += '\tVertical Up (Broadband)\n' else: s += '\tVertical Down (Industry)\n' return s - def get_dip(self, component): + def get_dip(self, component: str) -> float: + """ + Returns the dip angle for a given component. + + The dip angle indicates the orientation of the component in degrees. + + For component `'Z'`: + * If `z_is_up` is True, `'Z'` is oriented vertically upwards, and + the dip angle is -90.0 degrees. + * If `z_is_up` is False, `'Z'` is oriented vertically downwards, + and the dip angle is 90.0 degrees. + + For horizontal components `('N', '1', 'E', '2')`, the dip angle is 0.0 + degrees. + + For any other input, the method returns None. + + Args: + component (str): The component for which to retrieve the dip angle. + Accepted values are `'Z'`, `'N'`, `'1'`, `'E'`, or + `'2'`. + + Returns: + (float or None): The dip angle in degrees for the specified + component, or None if the component is + unrecognized. + """ if component == 'Z': if self.z_is_up: return -90.00 - else: - return 90.0 - elif component in ('N', '1', 'E', '2'): + return 90.0 + if component in ('N', '1', 'E', '2'): return 0.0 - else: - return None - - def get_azimuth(self, component): + return None + + def get_azimuth(self, component: str) -> float: + """ + Returns the azimuth angle for a given component. + + The azimuth angle defines the horizontal orientation of the component + in degrees. + * For components `'Z'`, `'N'`, and `'1'`, the azimuth angle is + 0.0 degrees. + * For components `'E'` and `'2'`, the azimuth angle is 90.0 + degrees. + + Args: + component (str): The component for which to retrieve the azimuth + angle. Accepted values are `'Z'`, `'N'`, `'1'`, + `'E'`, or `'2'`. + + Returns: + float: The azimuth angle in degrees for the specified component. + """ if component in ('Z', 'N', '1'): return 0.0 - elif component in ('E', '2'): + if component in ('E', '2'): return 90.0 + raise TypeError("Component not recognized.") - def get_nrl_keys(self): - return self.NRL_keys + def get_nrl_keys(self) -> tuple: + """ + Returns the NRL keys associated with the Sensor object. - def from_gui(self, widget): + Returns: + tuple: The sensor's NRL keys. + """ + return self.nrl_keys + + def from_gui(self, widget) -> None: + """ + Builds the Sensor object's NRL keys from the NRL Wizard. + + Args: + widget (QWizard): The NRL Wizard from the root gui. + """ answer_list = (a for q, a in widget.q_and_as) - self.NRL_keys = tuple(answer_list) + self.nrl_keys = tuple(answer_list) class DataLogger(Hardware): + """ + Represents a datalogger device that extends the Hardware base class. + + Attributes: + name (str): The name of the datalogger. + nrl_keys (tuple): A tuple containing NRL keys associated + with the datalogger. Initializes as an empty tuple. + soh_resps (dict): Dict containing channels/responses. + Initializes as empty. + + Examples: + >>> d = DataLogger('RT-130) + """ def __init__(self, *args, **kwargs): + """ + Initializes an instance of DataLogger with an empty dict of + `soh_resps`. + """ super().__init__(*args, **kwargs) - self.soh_resps = {} - - def __repr__(self): - s = f'{self.name} {self.get_nrl_keys("GAIN", "SR")}\n' + self.soh_resps = {} + + def __repr__(self) -> str: + """ + Returns a summary of the DataLogger object with its name, + nrl_keys, and channels/responses. + + Returns: + str: A string summarizing the sensor's information and + orientation. + """ + s = f'{self.name} {self.get_nrl_keys("GAIN", "SR")}\n' for chan, resp in self.soh_resps.items(): s += f'\t{chan}\n\t\t{resp}\n' return s - def requires_gain(self): - if PlaceHolder.GAIN in self.NRL_keys: + def requires_gain(self) -> bool: + """ + Determines if gain needed. + + Returns: + bool: True if `PlaceHolder.GAIN` is in the DataLogger + object's NRL keys. False if it isn't. + """ + if PlaceHolder.GAIN in self.nrl_keys: return True - else: - return False + return False + + def requires_sr(self) -> bool: + """ + Determines if sample rate needed. - def requires_sr(self): - if PlaceHolder.SR in self.NRL_keys: + Returns: + bool: True if `PlaceHolder.SR` is in the DataLogger + object's NRL keys. False if it isn't. + """ + if PlaceHolder.SR in self.nrl_keys: return True - else: - return False + return False + + def get_soh_resps(self, code: str) -> str: + """ + Returns SOH response based on the given channel code. + + If the code starts with 'VM', it returns the response associated with + 'VM' regardless of the rest of the code. Otherwise, it returns the + response associated with the exact code provided. - def get_soh_resps(self, code): + Args: + code (str): The channel code used to identify the specific SOH + response to retrieve. + + Returns: + str: The SOH response associated with the given code. + """ if code[:2] == 'VM': return self.soh_resps['VM'] - else: - return self.soh_resps[code] - - def get_nrl_keys(self, gain=None, sr=None): + return self.soh_resps[code] + + def get_nrl_keys(self, gain: int = None, sr: int = None) -> list: + """ + Returns a list of NRL keys with placeholders replaced by specified + values for gain and sample rate. + + If a placeholder is found, and the required value is `None`, the method + raises a `TypeError`. + + Args: + gain (int, optional): The gain value to replace the + `PlaceHolder.GAIN` in `nrl_keys`. + Required if `PlaceHolder.GAIN` + is present in `nrl_keys`. + sr (int, optional): The sample rate (SR) value to replace the + `PlaceHolder.SR` in `nrl_keys`. Required if + `PlaceHolder.SR` is present in `nrl_keys`. + + Returns: + list: A list of NRL keys. + + Raises: + TypeError: If a required `gain` or `sr` value is not provided when + a corresponding placeholder is present in `nrl_keys`. + """ keys = [] - for ans in self.NRL_keys: + for ans in self.nrl_keys: if ans is PlaceHolder.GAIN: if not gain: raise TypeError('Gain required') @@ -136,8 +317,14 @@ class DataLogger(Hardware): keys.append(ans) return keys - def from_gui(self, widget): - answer_list = list() + def from_gui(self, widget) -> None: + """ + Builds the DataLogger object's NRL keys from the NRL Wizard. + + Args: + widget (QWizard): The NRL Wizard from the root gui. + """ + answer_list = [] for q, a in widget.q_and_as: if q.find('gain') >= 0 and a.isdigit(): answer_list.append(PlaceHolder.GAIN) @@ -145,99 +332,301 @@ class DataLogger(Hardware): answer_list.append(PlaceHolder.SR) else: answer_list.append(a) - self.NRL_keys = tuple(answer_list) + self.nrl_keys = tuple(answer_list) class NrlDict(OrderedDict): + """ + Dictionary class to store NRL data, extends OrderedDict. + + This class allows for ordered storage and retrieval of NRL-related data, + with an optional container type attribute that defines the type of + hardware (e.g., Sensor or DataLogger) the dictionary is intended to + contain. + + Attributes: + type (Sensor | DataLogger | None): Specifies the hardware type that + the dictionary is associated with. + + Args: + container_type (Sensor | DataLogger | None, optional): + Sets the hardware type that the dictionary will contain. Defaults + to None. + + Examples: + >>> sensors = NrlDict(Sensor) + >>> dataloggers = NrlDict(DataLogger) + """ + def __init__(self, container_type=None): + """ + Initializes an instance of NrlDict with the given container type. + + Args: + container_type (Sensor | DataLogger | None, optional): + Sets the hardware + type that the dictionary + will contain. Defaults + to None. + """ super().__init__(self) self.type = container_type - def append(self, item): + + def append(self, item: Hardware) -> None: + """ + Adds given Hardware object to the dict. + + Args: + item (Hardware): A Hardware object to be added to the dict. + """ + assert isinstance(item, Hardware) - self[item.NRL_keys] = item - def names(self): - # returns all the names in dict - return [item.name for item in self.values()] - def index_name(self): - # returns (index, name) of all in dict - return [ (list(self.keys()).index(key), self[key].name) for key in self.keys()] - def index(self, name): - # returns index from name - return self.names().index(name) - def name(self, index): - # returns name from index replaces DLS[index] - return self.names()[index] - def get(self, name): - # returns hardware object from name - return self[self.keys_from_name(name)] - def keys_from_name(self, name): - return [key for key, item in self.items() if item.name == name][0] - def add_from_nrl_gui(self, widget): + self[item.nrl_keys] = item + + def names(self) -> list[str]: + """ + Returns all the names of the Hardware object stored in the + dict. + + Returns: + list[str]: A list of strings representing the + `name` attribute of each Hardware object in the dict. + + Examples: + ['Na', 'RT-130', 'Q330'] + """ + # create list of names for each item + names = [item.name for item in self.values()] + return names + + def index_name(self) -> list[tuple[int, str]]: + """ + Returns a list of tuples with (index, name) for each Hardware object + in the entire dict. + + Returns: + list[tuple[int, str]]: A list of tuples. Each tuple contains the + index of the key and the `name` attribute of + the associated Hardware object in the dict. + + Examples: + [(0, 'Na'), (1, 'RT-130'), (2, 'Q330')] + """ + # create list of keys for indexing + keys = list(self.keys()) + + # create list of tuples with index and name for every key + indexed_names = [ + (keys.index(key), self[key].name) + for key in keys + ] + return indexed_names + + def index(self, name: str) -> int: + """ + Returns the index of the Hardware object with the given name. + + Args: + name (str): The name of a Hardware object in the dict. + + Returns: + int: The index of the Hardware object with the given name. + + Examples: + >>> 'Na' + 0 + + >>> 'RT-130' + 1 + """ + index = self.names().index(name) + return index + + def name(self, index: int) -> str: + """ + Returns the name for the given Hardware object index + (replaces DLS[index]). + + Args: + index (int): The index of a Hardware object in the dict. + + Returns: + str: The name of the Hardware at the given index. + """ + name = self.names()[index] + return name + + def get(self, name: str) -> Hardware: + """ + Returns the Hardware object with the given name. + + Args: + name (str): The name of a Hardware object in the dict. + + Returns: + Hardware: A Hardware object. + """ + hardware_obj = self[self.keys_from_name(name)] + return hardware_obj + + def keys_from_name(self, name: str) -> tuple: + """ + Returns all the NRL keys for the Hardware object with + the given name. + + Args: + name (str): The name of the Hardware object. + + Returns: + tuple: All the NRL keys associated with the hardware. + """ + keys = [key for key, item in self.items() if item.name == name][0] + return keys + + def add_from_nrl_gui(self, widget) -> None: + """ + Adds Hardware object to NrlDict via the NRL Wizard. + + Args: + widget (QWizard): The NRL Wizard from the root gui. + """ name = widget.nickname o = self.type(name) o.from_gui(widget) self.append(o) save() - def __repr__(self): + def __repr__(self) -> str: + """ + Returns a summary of the Hardware stored in the dict. + + Returns: + str: A string summarizing each Hardware object stored + in the dict. + """ s = 'Hardware:\n' for o in self.values(): s += o.__repr__() return s -def get_nrl_response(dl_keys, sensor_keys): + +def get_nrl_response(dl_keys: (list[str]), + sensor_keys: (list[str])) -> Response: + """ + Returns the response using NRL. + + Args: + dl_keys (list[str]): List of datalogger keys. + sensor_keys (list[str]): List of sensor keys. + + Returns: + Response: The Channel Response. + """ nrl = NRL(NRL_ROOT) return nrl.get_response(dl_keys, sensor_keys) -sensors = NrlDict(Sensor) -dataloggers = NrlDict(DataLogger) -def save(filename = defaultsfile): +SENSORS = NrlDict(Sensor) +DATALOGGERS = NrlDict(DataLogger) + + +def save(filename: str = defaultsfile) -> None: + """ + Saves file to be used to creates sensors and dataloggers. + + Args: + filename (str, optional): File to save to later be used to create + sensors and dataloggers. Defaults to + defaultsfile. + """ with open(filename, 'wb') as f: - pickle.dump(sensors, f) - pickle.dump(dataloggers, f) + pickle.dump(SENSORS, f) + pickle.dump(DATALOGGERS, f) -def load(filename = defaultsfile): + +def load(filename: str = defaultsfile) -> None: + """ + Creates sensors and dataloggers from a given file. + + Args: + filename (str, optional): File to be used to create sensors and + dataloggers. Defaults to `defaultsfile`. + """ if os.path.isfile(filename): with open(filename, 'rb') as f: - global sensors - global dataloggers - sensors = pickle.load(f) - dataloggers = pickle.load(f) + global SENSORS + global DATALOGGERS + SENSORS = pickle.load(f) + DATALOGGERS = pickle.load(f) else: create_defaults() -def create_defaults(): + +def create_defaults() -> None: + """ + Method to manually create default sensors and dataloggers. + """ s = Sensor('Na') - sensors.append(s) + SENSORS.append(s) s = Sensor('CMG-3T') - s.NRL_keys = ('Guralp', 'CMG-3T', '120s - 50Hz', '1500') - sensors.append(s) + s.nrl_keys = ('Guralp', 'CMG-3T', '120s - 50Hz', '1500') + SENSORS.append(s) s = Sensor('Trillium 40') - s.NRL_keys = ('Nanometrics', 'Trillium 40') - sensors.append(s) + s.nrl_keys = ('Nanometrics', 'Trillium 40') + SENSORS.append(s) s = Sensor('L-22') - s.NRL_keys = ('Sercel/Mark Products', 'L-22D', '5470 Ohms', '20000 Ohms') - sensors.append(s) + s.nrl_keys = ( + 'Sercel/Mark Products', + 'L-22D', + '5470 Ohms', + '20000 Ohms' + ) + SENSORS.append(s) s = Sensor('STS-2 gen1') - s.NRL_keys = ('Streckeisen', 'STS-2', '1500', '1 - installed 01/90 to 09/94') - sensors.append(s) + s.nrl_keys = ( + 'Streckeisen', + 'STS-2', + '1500', + '1 - installed 01/90 to 09/94' + ) + SENSORS.append(s) s = Sensor('STS-2 gen2') - s.NRL_keys = ('Streckeisen', 'STS-2', '1500', '2 - installed 09/94 to 04/97') - sensors.append(s) + s.nrl_keys = ( + 'Streckeisen', + 'STS-2', + '1500', + '2 - installed 09/94 to 04/97' + ) + SENSORS.append(s) s = Sensor('STS-2 gen3') - s.NRL_keys = ('Streckeisen', 'STS-2', '1500', '3 - installed 04/97 to present') - sensors.append(s) + s.nrl_keys = ( + 'Streckeisen', + 'STS-2', + '1500', + '3 - installed 04/97 to present' + ) + SENSORS.append(s) d = DataLogger('Na') - dataloggers.append(d) + DATALOGGERS.append(d) d = DataLogger('RT-130') - d.NRL_keys = ('REF TEK', 'RT 130 & 130-SMA', PlaceHolder.GAIN, PlaceHolder.SR) + d.nrl_keys = ( + 'REF TEK', + 'RT 130 & 130-SMA', + PlaceHolder.GAIN, + PlaceHolder.SR + ) d.soh_resps['LOG'] = None d.soh_resps['VM'] = 'RT130_VM_RESP' - dataloggers.append(d) + DATALOGGERS.append(d) d = DataLogger('Q330') - d.NRL_keys = ('Quanterra', 'Q330SR', PlaceHolder.GAIN, PlaceHolder.SR, 'LINEAR AT ALL SPS') + d.nrl_keys = ( + 'Quanterra', + 'Q330SR', + PlaceHolder.GAIN, + PlaceHolder.SR, + 'LINEAR AT ALL SPS' + ) d.soh_resps['ACE'] = None d.soh_resps['LOG'] = None d.soh_resps['OCF'] = None @@ -250,15 +639,18 @@ def create_defaults(): d.soh_resps['VKI'] = 'Q330_VKI_RESP' d.soh_resps['VPB'] = 'Q330_VPB_RESP' d.soh_resps['VM'] = 'Q330_VM_RESP' - dataloggers.append(d) + DATALOGGERS.append(d) empty_response = Response() - for d in dataloggers.values(): + for d in DATALOGGERS.values(): for chan, resp_str in d.soh_resps.items(): if not resp_str: d.soh_resps[chan] = empty_response else: filename = d.soh_resps[chan] - with open(os.path.join(resp_dir, filename), 'r') as f: + with open( + os.path.join(resp_dir, filename), 'r', + encoding='utf-8' + ) as f: d.soh_resps[chan] = read_inventory( f, format='RESP')[0][0][0].response