Source code for aviary.utils.aviary_values

'''
Define utilities for using aviary values with associated units and testing
for compatibility with aviary metadata dictionary.

Utilities
---------
Units : type alias
    define a type hint for associated units

ValueAndUnits : type alias
    define a type hint for a single value paired with its associated units

OptionalValueAndUnits : type alias
    define a type hint for an optional single value paired with its associated units

class AviaryValues
    define a collection of named values with associated units
'''
from enum import EnumMeta

import numpy as np
from openmdao.utils.units import convert_units as _convert_units

from aviary.utils.named_values import (NamedValues, get_items, get_keys,
                                       get_values)
from aviary.variable_info.variable_meta_data import _MetaData


[docs] class AviaryValues(NamedValues): ''' Define a collection of aviary values with associated units and aviary tests. '''
[docs] def set_val(self, key, val, units='unitless', meta_data=_MetaData): ''' Update the named value and its associated units. Note, specifying units of `None` or units of any type other than `str` will raise `Typerror`. Parameters ---------- key : str the name of the item val : Any the new value of the item units : str ('unitless') the units associated with the new value, if any Raises ------ TypeError if units of `None` were specified or units of any type other than `str` ''' # Special handling to access an Enum member from either the member name or its value. my_val = val if key in _MetaData.keys(): expected_types = _MetaData[key]['types'] if type(expected_types) is EnumMeta: if self._is_iterable(val): my_val = [self._convert_to_enum( item, expected_types) for item in val] else: my_val = self._convert_to_enum(val, expected_types) # Special handling if the variable is supposed to be an array if key in _MetaData.keys(): default_value = _MetaData[key]['default_value'] # if the item is supposed to be an iterable... if self._is_iterable(default_value): # but the provided value is not... if not self._is_iterable(my_val): # make object the correct iterable if isinstance(default_value, tuple): my_val = (my_val,) else: my_val = np.array([my_val], dtype=type(default_value[0])) self._check_type(key, my_val, meta_data=meta_data) self._check_units_compatability(key, my_val, units, meta_data=meta_data) super().set_val(key=key, val=my_val, units=units)
def _check_type(self, key, val, meta_data=_MetaData): if key in meta_data.keys(): expected_types = meta_data[key]['types'] if expected_types is not None: if self._is_iterable(expected_types): expected_types = tuple(expected_types) # if val is not iterable, add it to a list (length 1), checks assume # val is iterable if not self._is_iterable(val): val = [val] # numpy arrays have special typings. Extract item of equivalent built-in python type # numpy arrays do not allow mixed types, only have to check first entry # empty arrays do not need this check if isinstance(val, np.ndarray) and len(val) > 0: # NoneType numpy arrays do not need to be "converted" to built-in python types if val.dtype == type(None): val = [val[0]] else: # item() gets us native Python equivalent object (i.e. int vs. numpy.int64) # wrap first index in np array to ensures works on any dtype val = [np.array(val[0]).item()] for item in val: has_bool = False # needs some fancy shenanigans because bools will register as ints if (isinstance(expected_types, type)): if expected_types is bool: has_bool = True elif bool in expected_types: has_bool = True if (not isinstance(item, expected_types)) or ( (has_bool == False) and (isinstance(item, bool))): raise TypeError( f'{key} is of type(s) {meta_data[key]["types"]} but you ' f'have provided a value of type {type(item)}.') def _check_units_compatability(self, key, val, units, meta_data=_MetaData): if key in meta_data.keys(): expected_units = meta_data[key]['units'] try: # NOTE the value here is unimportant, we only care if OpenMDAO will # convert the units _convert_units(10, expected_units, units) except ValueError: raise ValueError( f'The units {units} which you have provided for {key} are invalid.') except TypeError: raise TypeError( f'The base units of {key} are {expected_units}, and you have tried to set {key} with units of {units}, which are not compatible.') except BaseException: raise KeyError('There is an unknown error with your units.') def _is_iterable(self, val): return isinstance(val, _valid_iterables) def _convert_to_enum(self, val, enum_type): if isinstance(val, str): try: # see if str maps to ENUM value return enum_type(val) except ValueError: # str instead maps to ENUM name return enum_type[val.upper()] else: return enum_type(val)
_valid_iterables = (list, np.ndarray, tuple)