Source code for sofar.utils

import os
import glob
import numpy as np
import numpy.testing as npt
import warnings
import sofar as sf


[docs] def version(): """Return version of sofar and SOFA conventions""" sofa_conventions = os.path.join( os.path.dirname(__file__), "sofa_conventions", 'VERSION') with open(sofa_conventions) as file: sofa_conventions = file.readline().strip() return (f"sofar v{sf.__version__} implementing " f"SOFA standard {sofa_conventions}")
def _verify_convention_and_version(version, convention): """ Verify if convention and version exist. Raise a Value error if it does not. Parameters ---------- version : str The version to be checked convention : str The name of the convention to be checked """ # check if the convention exists if convention not in _get_conventions("name"): raise ValueError( f"Convention '{convention}' does not exist") name_version = _get_conventions("name_version") # check which version is wanted version_exists = False for versions in name_version: # check if convention and version match if versions[0] == convention \ and str(float(versions[1])) == version: version_exists = True if not version_exists: raise ValueError(( f"{convention} v{version} is not a valid SOFA Convention." "If you are trying to read the data use " "sofar.read_sofa_as_netcdf(). Call sofar.list_conventions() for a " "list of valid Conventions"))
[docs] def list_conventions(): """ List available SOFA conventions by printing to the console. """ print(_get_conventions("string"))
def _get_conventions(return_type, conventions_path=None): """ Get available SOFA conventions. Parameters ---------- return_type : string, optional ``'path'`` Return a list with the full paths and filenames of the convention files (json files) ``'path_source'`` Return a list with the full paths and filenames of the source convention files from API_MO (csv files) ``'name'`` Return a list of the convention names without version ``'name_version'`` Return a list of tuples containing the convention name and version. ``'string'`` Returns a string that lists the names and versions of all conventions. conventions_path : str, optional The path to the the `conventions` folder containing the csv and json files. Returns ------- See parameter `return_type`. """ # directory containing the SOFA conventions if conventions_path is None: conventions_path = os.path.join( os.path.dirname(__file__), "sofa_conventions", 'conventions') reg_str = "*.csv" if return_type == "path_source" else "*.json" # SOFA convention files standardized = list(glob.glob(os.path.join(conventions_path, reg_str))) deprecated = list( glob.glob(os.path.join(conventions_path, "deprecated", reg_str))) paths = standardized + deprecated conventions_str = "Available SOFA conventions:\n" conventions = [] versions = [] for path in paths: fileparts = os.path.basename(path).split(sep="_") conventions += [fileparts[0]] versions += [fileparts[1][:-5]] conventions_str += f"{conventions[-1]} (Version {versions[-1]})\n" if return_type is None: return elif return_type.startswith("path"): return paths elif return_type == "name": return conventions elif return_type == "name_version": return list(zip(conventions, versions)) elif return_type == "string": return conventions_str else: raise ValueError(f"return_type {return_type} is invalid")
[docs] def equals(sofa_a, sofa_b, verbose=True, exclude=None): """ Compare two SOFA objects against each other. Parameters ---------- sofa_a : Sofa SOFA object sofa_b : Sofa SOFA object verbose : bool, optional Print differences to the console. The default is True. exclude : str, optional Specify what fields should be excluded from the comparison ``'GLOBAL'`` Exclude all global attributes, i.e., fields starting with 'GLOBAL:' ``'DATE'`` Exclude date attributes, i.e., fields that contain 'Date' ``'ATTR'`` Exclude all attributes, i.e., fields that contain ':' The default is None, which does not exclude anything. Returns ------- is_identical : bool ``True`` if sofa_a and sofa_b are identical, ``False`` otherwise. """ is_identical = True # get and filter keys # ('_*' are SOFA object private variables, '__' are netCDF attributes) keys_a = [k for k in sofa_a.__dict__.keys() if not k.startswith("_")] keys_b = [k for k in sofa_b.__dict__.keys() if not k.startswith("_")] if exclude is not None: if exclude.upper() == "GLOBAL": keys_a = [k for k in keys_a if not k.startswith("GLOBAL_")] keys_b = [k for k in keys_b if not k.startswith("GLOBAL_")] elif exclude.upper() == "ATTR": keys_a = [k for k in keys_a if sofa_a._convention[k]["type"] != "attribute"] keys_b = [k for k in keys_b if sofa_b._convention[k]["type"] != "attribute"] elif exclude.upper() == "DATE": keys_a = [k for k in keys_a if "Date" not in k] keys_b = [k for k in keys_b if "Date" not in k] else: raise ValueError( f"exclude is {exclude} but must be GLOBAL, DATE, or ATTR") # check for equal length if len(keys_a) != len(keys_b): return _equals_raise_warning(( f"not identical: sofa_a has {len(keys_a)} attributes for " f"comparison and sofa_b has {len(keys_b)}."), verbose) # check if the keys match if set(keys_a) != set(keys_b): return _equals_raise_warning( "not identical: sofa_a and sofa_b do not have the ame attributes", verbose) # compare the data inside the SOFA object for key in keys_a: # get data and types a = getattr(sofa_a, key) b = getattr(sofa_b, key) type_a = sofa_a._convention[key]["type"] type_b = sofa_b._convention[key]["type"] # compare attributes if type_a == "attribute" and type_b == "attribute": # compare if a != b: is_identical = _equals_raise_warning( f"not identical: different values for {key}", verbose) # compare double variables elif type_a == "double" and type_b == "double": try: npt.assert_allclose(np.squeeze(a), np.squeeze(b)) except AssertionError: is_identical = _equals_raise_warning( "not identical: different values for {key}", verbose) # compare string variables elif type_a == "string" and type_b == "string": try: assert np.all( np.squeeze(a).astype("S") == np.squeeze(b).astype("S")) except AssertionError: is_identical = _equals_raise_warning( "not identical: different values for {key}", verbose) else: is_identical = _equals_raise_warning( (f"not identical: {key} has different data types " f"({type_a}, {type_b})"), verbose) return is_identical
def _equals_raise_warning(message, verbose): if verbose: warnings.warn(message) return False def _atleast_nd(array, ndim): """ Get numpy array with specified number of dimensions. Dimensions are appended at the end if ndim > 3. """ try: array = array.copy() except AttributeError: array = array if ndim == 1: array = np.atleast_1d(array) if ndim == 2: array = np.atleast_2d(array) if ndim >= 3: array = np.atleast_3d(array) for _ in range(ndim - array.ndim): array = array[..., np.newaxis] return array def _nd_newaxis(array, ndim): """Append dimensions to the end of an array until array.ndim == ndim""" array = np.array(array) for _ in range(ndim - array.ndim): array = array[..., np.newaxis] return array def _complete_sofa(convention="GeneralTF"): """ Generate SOFA file with all required data for testing verification rules. """ sofa = sf.Sofa(convention) # Listener meta data sofa.add_variable("ListenerView", [1, 0, 0], "double", "IC") sofa.add_attribute("ListenerView_Type", "cartesian") sofa.add_attribute("ListenerView_Units", "metre") sofa.add_variable("ListenerUp", [0, 0, 1], "double", "IC") # Receiver meta data sofa.add_variable("ReceiverView", [1, 0, 0], "double", "IC") sofa.add_attribute("ReceiverView_Type", "cartesian") sofa.add_attribute("ReceiverView_Units", "metre") sofa.add_variable("ReceiverUp", [0, 0, 1], "double", "IC") # Source meta data sofa.add_variable("SourceView", [1, 0, 0], "double", "IC") sofa.add_attribute("SourceView_Type", "cartesian") sofa.add_attribute("SourceView_Units", "metre") sofa.add_variable("SourceUp", [0, 0, 1], "double", "IC") # Emitter meta data sofa.add_variable("EmitterView", [1, 0, 0], "double", "IC") sofa.add_attribute("EmitterView_Type", "cartesian") sofa.add_attribute("EmitterView_Units", "metre") sofa.add_variable("EmitterUp", [0, 0, 1], "double", "IC") sofa.add_attribute("GLOBAL_EmitterDescription", "what an emitter") sofa.add_variable("EmitterDescriptions", ["emitter array"], "string", "MS") # Room meta data sofa.add_attribute("GLOBAL_RoomShortName", "Hall") sofa.add_attribute("GLOBAL_RoomDescription", "Wooden floor") sofa.add_attribute("GLOBAL_RoomLocation", "some where nice") sofa.add_variable("RoomTemperature", 0, "double", "I") sofa.add_attribute("RoomTemperature_Units", "kelvin") sofa.add_attribute("GLOBAL_RoomGeometry", "some/file") sofa.add_variable("RoomVolume", 200, "double", "I") sofa.add_attribute("RoomVolume_Units", "cubic metre") sofa.add_variable("RoomCornerA", [0, 0, 0], "double", "IC") sofa.add_variable("RoomCornerB", [1, 1, 1], "double", "IC") sofa.add_variable("RoomCorners", 0, "double", "I") sofa.add_attribute("RoomCorners_Type", "cartesian") sofa.add_attribute("RoomCorners_Units", "metre") sofa.verify() return sofa