Source code for openmdao.vectors.default_vector

"""Define the default Vector class."""
from collections import defaultdict
import hashlib
import numpy as np

from openmdao.vectors.vector import Vector, _VecData, _full_slice
from openmdao.vectors.default_transfer import DefaultTransfer
from openmdao.utils.array_utils import array_hash, shape_to_len


[docs]class DefaultVector(Vector): """ Default NumPy vector. Parameters ---------- name : str The name of the vector: 'nonlinear' or 'linear'. kind : str The kind of vector, 'input', 'output', or 'residual'. system : <System> Pointer to the owning system. parent_vector : <Vector> Parent vector. alloc_complex : bool Whether to allocate any imaginary storage to perform complex step. Default is False. """ TRANSFER = DefaultTransfer def _get_data(self): """ Return either the data array or its real part. Note that this is intended to return only the _data array and not, for example, to return a combined array in the case of an input vector that shares entries with a connected output vector (for no-copy transfers). Returns ------- ndarray The data array or its real part. """ return self._data if self._under_complex_step else self._data.real def _initialize_data(self, parent_vector, system): """ Set up internal data structures. Parameters ---------- parent_vector : <Vector> or None Parent vector. system : <System> The owning system. """ self._views = {} start = end = 0 for name, shape in system._name_shape_iter(self._kind): end += shape_to_len(shape) self._views[name] = _VecData(shape, (start, end)) start = end if parent_vector is None: # this is a root vector self._parent_slice = slice(0, end) self._data = np.zeros(end, dtype=complex if self._alloc_complex else float) else: for name in self._views: # just get our first name to get the starting index in the parent vector start = parent_vector._views[name].range[0] self._parent_slice = slice(start, start + end) self._data = parent_vector._data[self._parent_slice] break else: self._parent_slice = slice(0, 0) self._data = np.zeros(0, dtype=complex if self._alloc_complex else float) if parent_vector._scaling: parent_scaler, parent_adder = parent_vector._scaling if parent_adder is not None: parent_adder = parent_adder[self._parent_slice] self._scaling = (parent_scaler[self._parent_slice], parent_adder) def _set_scaling(self, system, do_adder, nlvec=None): """ Set the scaling vectors. Parameters ---------- system : <System> The system to set the scaling for. do_adder : bool Whether to initialize with an additive term. nlvec : <Vector> or None Nonlinear vector if this is a linear vector. """ kind = self._kind islinear = self._name == 'linear' isinput = kind == 'input' factors = system._scale_factors # If we define 'ref' on an output, then we will need to allocate a separate scaling ndarray # for the linear and nonlinear input vectors. self._has_solver_ref = system._has_output_scaling and isinput and islinear self._nlvec = nlvec # if root, allocate space for scaling vectors if system.pathname == '': # root system self._allocate_scaling_data(do_adder, nlvec) scaler_array, adder_array = self._scaling start = end = 0 for abs_name, vinfo in self._views.items(): end += vinfo.size if abs_name in factors: factor = factors[abs_name] if kind in factor: a0, a1, factor, offset = factor[kind] if factor is not None: # Linear input vectors need to be able to handle the unit and solver scaling # in opposite directions in reverse mode. if islinear: scale0 = None scale1 = factor / a1 else: scale0 = (a0 + offset) * factor scale1 = a1 * factor else: if islinear and isinput: scale0 = None scale1 = 1.0 / a1 else: scale0 = a0 scale1 = a1 if adder_array is not None: adder_array[start:end] = scale0 scaler_array[start:end] = scale1 start = end def _allocate_scaling_data(self, do_adder, nlvec): """ Allocate root scaling arrays. Parameters ---------- do_adder : bool Whether to initialize with an additive term. nlvec : <Vector> or None Nonlinear vector if this is a linear vector. """ data = self._data if self._name == 'nonlinear': if do_adder: self._scaling = (np.ones(data.size), np.zeros(data.size)) else: self._scaling = (np.ones(data.size), None) elif self._name == 'linear': if self._has_solver_ref: # We only allocate an extra scaling vector when we have output scaling # somewhere in the model. self._scaling = (np.ones(data.size), None) else: # Reuse the nonlinear scaling vecs since they're the same as ours. # The nonlinear vectors are created before the linear vectors self._scaling = (nlvec._scaling[0], None) else: raise NameError(f"Invalid vector name: {self._name}.") def _initialize_views(self, parent_vector, system): """ Internally assemble views onto the vectors. """ islinear = self._name == 'linear' views = self._views start = end = 0 for vinfo in self._views.values(): end += vinfo.size vflat = v = self._data[start:end] if vinfo.shape != vflat.shape and vinfo.shape != (): v = vflat.view().reshape(vinfo.shape) vinfo.view = v vinfo.flat = vflat start = end self._names = frozenset(views) if islinear else views def __len__(self): """ Return the flattened length of this Vector. Returns ------- int Total flattened length of this vector. """ return self._data.size def _in_matvec_context(self): """ Return True if this vector is inside of a matvec_context. Returns ------- bool Whether or not this vector is in a matvec_context. """ return len(self._names) != len(self._views) def __iadd__(self, vec): """ Perform in-place vector addition. Parameters ---------- vec : <Vector> vector to add to self. Returns ------- <Vector> self + vec """ if isinstance(vec, Vector): self.iadd(vec.asarray()) else: data = self.asarray() data += vec return self def __isub__(self, vec): """ Perform in-place vector substraction. Parameters ---------- vec : <Vector> vector to subtract from self. Returns ------- <Vector> self - vec """ if isinstance(vec, Vector): self.isub(vec.asarray()) else: data = self.asarray() data -= vec return self def __imul__(self, vec): """ Perform in-place multiplication. Parameters ---------- vec : Vector, int, float or ndarray Value to multiply self. Returns ------- <Vector> self * vec """ if isinstance(vec, Vector): self.imul(vec.asarray()) else: data = self.asarray() data *= vec return self
[docs] def add_scal_vec(self, val, vec): """ Perform in-place addition of a vector times a scalar. Parameters ---------- val : int or float Scalar. vec : <Vector> This vector times val is added to self. """ data = self.asarray() data += (val * vec.asarray())
[docs] def set_vec(self, vec): """ Set the value of this vector to that of the incoming vector. Parameters ---------- vec : <Vector> The vector whose values self is set to. """ self.set_val(vec.asarray())
[docs] def set_val(self, val, idxs=_full_slice): """ Set the data array of this vector to a value, with optional indexing. Parameters ---------- val : float or ndarray Scalar or array to set data array to. idxs : int or slice or tuple of ints and/or slices The locations where the data array should be updated. """ # we use _data here specifically so that imaginary part # will get properly reset, e.g. when the array is zeroed out. self._data[idxs] = val
[docs] def scale_to_norm(self, mode='fwd'): """ Scale this vector to normalized form. Parameters ---------- mode : str Derivative direction. """ if mode == 'rev': self._scale_reverse(*self._scaling) else: if self._has_solver_ref: self._scale_forward(self._nlvec._scaling[0], None) else: self._scale_forward(*self._scaling)
[docs] def scale_to_phys(self, mode='fwd'): """ Scale this vector to physical form. Parameters ---------- mode : str Derivative direction. """ if mode == 'rev': self._scale_forward(*self._scaling) else: if self._has_solver_ref: self._scale_reverse(self._nlvec._scaling[0], None) else: self._scale_reverse(*self._scaling)
def _scale_forward(self, scaler, adder): """ Scale this vector by subtracting the adder and dividing by the scaler. Parameters ---------- scaler : darray Vector of multiplicative scaling factors. adder : darray Vector of additive scaling factors. """ data = self.asarray() if adder is not None: # nonlinear only data -= adder data /= scaler def _scale_reverse(self, scaler, adder): """ Scale this vector by multiplying by the scaler ahd adding the adder. Parameters ---------- scaler : darray Vector of multiplicative scaling factors. adder : darray Vector of additive scaling factors. """ data = self.asarray() data *= scaler if adder is not None: # nonlinear only data += adder
[docs] def asarray(self, copy=False): """ Return an array representation of this vector. If copy is True, return a copy. Parameters ---------- copy : bool If True, return a copy of the array. Returns ------- ndarray Array representation of this vector. """ if self._under_complex_step: arr = self._data else: arr = self._data.real if copy: return arr.copy() return arr
[docs] def iscomplex(self): """ Return True if this vector contains complex values. This checks the type of the values, not whether they have a nonzero imaginary part. Returns ------- bool True if this vector contains complex values. """ return np.iscomplexobj(self._get_data())
[docs] def iadd(self, val, idxs=_full_slice): """ Add the value to the data array at the specified indices or slice(s). Parameters ---------- val : ndarray Value to set into the data array. idxs : int or slice or tuple of ints and/or slices The locations where the data array should be updated. """ data = self.asarray() data[idxs] += val
[docs] def isub(self, val, idxs=_full_slice): """ Subtract the value from the data array at the specified indices or slice(s). Parameters ---------- val : ndarray Value to set into the data array. idxs : int or slice or tuple of ints and/or slices The locations where the data array should be updated. """ data = self.asarray() data[idxs] -= val
[docs] def imul(self, val, idxs=_full_slice): """ Multiply the value to the data array at the specified indices or slice(s). Parameters ---------- val : ndarray Value to set into the data array. idxs : int or slice or tuple of ints and/or slices The locations where the data array should be updated. """ data = self.asarray() data[idxs] *= val
[docs] def dot(self, vec): """ Compute the dot product of the real parts of the current vec and the incoming vec. Parameters ---------- vec : <Vector> The incoming vector being dotted with self. Returns ------- float The computed dot product value. """ return np.dot(self.asarray(), vec.asarray())
[docs] def get_norm(self): """ Return the norm of this vector. Returns ------- float Norm of this vector. """ return np.linalg.norm(self.asarray())
[docs] def get_range(self, name): """ Return the range of the variable in the local data array. Parameters ---------- name : str Name of the variable. Returns ------- tuple Start and stop indices of the variable in the local data array. """ return self._views[name].range
[docs] def idxs2nameloc(self, idxs): """ Given some indices, return a dict mapping variable name to corresponding local indices. This is slow and is meant to be used only for debugging or maybe error messages. Parameters ---------- idxs : list of int Vector indices to be converted to local indices for each corresponding variable. Returns ------- dict Mapping of variable name to a list of local indices into that variable. """ name2inds = defaultdict(list) start = end = 0 for name, vinfo in self._views.items(): start, end = vinfo.range for idx in idxs: if start <= idx < end: name2inds[name].append(idx - start) start = end return name2inds
[docs] def get_hash(self, alg=hashlib.sha1): """ Return a hash string for the array contained in this Vector. Parameters ---------- alg : function Algorithm used to generate the hash. Default is hashlib.sha1. Returns ------- str The hash string. """ if self._data.size == 0: return '' # we must use self._data here because the hashing alg requires array to be C-contiguous return array_hash(self._data, alg)