"""Define the base Vector and Transfer classes."""
from copy import deepcopy
import weakref
import hashlib
import numpy as np
from openmdao.utils.name_maps import prom_name2abs_name
from openmdao.utils.indexer import Indexer, indexer
_full_slice = slice(None)
_flat_full_indexer = indexer(_full_slice, flat_src=True)
_full_indexer = indexer(_full_slice, flat_src=False)
_type_map = {
'input': 'input',
'output': 'output',
'residual': 'output'
}
[docs]class Vector(object):
"""
Base Vector class.
This class is instantiated for inputs, outputs, and residuals.
It provides a dictionary interface and an arithmetic operations interface.
Implementations:
- <DefaultVector>
- <PETScVector>
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.
root_vector : <Vector>
Pointer to the vector owned by the root system.
alloc_complex : bool
Whether to allocate any imaginary storage to perform complex step. Default is False.
Attributes
----------
_name : str
The name of the vector: 'nonlinear' or 'linear'.
_typ : str
Type: 'input' for input vectors; 'output' for output/residual vectors.
_kind : str
Specific kind of vector, either 'input', 'output', or 'residual'.
_system : System
Weak ref to the owning system.
_views : dict
Dictionary mapping absolute variable names to the ndarray views.
_views_flat : dict
Dictionary mapping absolute variable names to the flattened ndarray views.
_names : set([str, ...])
Set of variables that are relevant in the current context.
_root_vector : Vector
Pointer to the vector owned by the root system.
_root_offset : int
Offset of this vector into the root vector.
_alloc_complex : bool
If True, then space for the complex vector is also allocated.
_data : ndarray
Actual allocated data.
_slices : dict
Mapping of var name to slice.
_under_complex_step : bool
When True, this vector is under complex step, and data is swapped with the complex data.
_do_scaling : bool
True if this vector performs scaling.
_do_adder : bool
True if this vector's scaling includes an additive term.
_scaling : dict
Contains scale factors to convert data arrays.
_scaling_nl_vec : dict
Reference to the scaling factors in the nonlinear vector. Only used for linear input
vectors.
read_only : bool
When True, values in the vector cannot be changed via the user __setitem__ API.
_len : int
Total length of data vector (including shared memory parts).
_has_solver_ref : bool
This is set to True only when a ref is defined on a solver.
"""
# Listing of relevant citations
cite = ""
# Indicator whether a vector class is MPI-distributed
distributed = False
[docs] def __init__(self, name, kind, system, root_vector=None, alloc_complex=False):
"""
Initialize all attributes.
"""
self._name = name
self._typ = _type_map[kind]
self._kind = kind
self._len = 0
self._system = weakref.ref(system)
self._views = {}
self._views_flat = {}
# self._names will either contain the same names as self._views or to the
# set of variables relevant to the current matvec product.
self._names = self._views
self._root_vector = None
self._data = None
self._slices = None
self._root_offset = 0
# Support for Complex Step
self._alloc_complex = alloc_complex
self._under_complex_step = False
self._do_scaling = ((kind == 'input' and system._has_input_scaling) or
(kind == 'output' and system._has_output_scaling) or
(kind == 'residual' and system._has_resid_scaling))
self._do_adder = ((kind == 'input' and system._has_input_adder) or
(kind == 'output' and system._has_output_adder) or
(kind == 'residual' and system._has_resid_scaling))
self._scaling = None
self._scaling_nl_vec = None
# 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 kind == 'input' and name == 'linear'
if root_vector is None:
self._root_vector = self
else:
self._root_vector = root_vector
self._initialize_data(root_vector)
self._initialize_views()
self.read_only = False
def __str__(self):
"""
Return a string representation of the Vector object.
Returns
-------
str
String rep of this object.
"""
return str(self.asarray())
def __len__(self):
"""
Return the flattened length of this Vector.
Returns
-------
int
Total flattened length of this vector.
"""
return self._len
[docs] def nvars(self):
"""
Return the number of variables in this Vector.
Returns
-------
int
Number of variables in this Vector.
"""
return len(self._views)
def _copy_views(self):
"""
Return a dictionary containing just the views.
Returns
-------
dict
Dictionary containing the _views.
"""
return deepcopy(self._views)
[docs] def keys(self):
"""
Return variable names of variables contained in this vector (relative names).
Returns
-------
listiterator (Python 3.x) or list (Python 2.x)
The variable names.
"""
return self.__iter__()
[docs] def values(self):
"""
Return values of variables contained in this vector.
Yields
------
ndarray or float
Value of each variable.
"""
if self._under_complex_step:
for n, v in self._views.items():
if n in self._names:
yield v
else:
yield np.zeros_like(v)
else:
for n, v in self._views.items():
if n in self._names:
yield v.real
else:
yield np.zeros_like(v.real)
[docs] def items(self):
"""
Return (name, value) for variables contained in this vector.
Yields
------
str
Relative name of each variable.
ndarray or float
Value of each variable.
"""
if self._system().pathname:
plen = len(self._system().pathname) + 1
else:
plen = 0
if self._under_complex_step:
for n, v in self._views.items():
if n in self._names:
yield n[plen:], v
else:
for n, v in self._views.items():
if n in self._names:
yield n[plen:], v.real
def _name2abs_name(self, name):
"""
Map the given promoted or relative name to the absolute name.
This is only valid when the name is unique; otherwise, a KeyError is thrown.
Parameters
----------
name : str
Promoted or relative variable name in the owning system's namespace.
Returns
-------
str or None
Absolute variable name if unique abs_name found or None otherwise.
"""
system = self._system()
# try relative name first
abs_name = system.pathname + '.' + name if system.pathname else name
if abs_name in self._views:
return abs_name
abs_name = prom_name2abs_name(system, name, self._typ)
if abs_name in self._views:
return abs_name
[docs] def __iter__(self):
"""
Return an iterator over variables involved in the current mat-vec product (relative names).
Returns
-------
listiterator
iterator over the variable names.
"""
system = self._system()
path = system.pathname
idx = len(path) + 1 if path else 0
return (n[idx:] for n in self._views if n in self._names)
def _abs_item_iter(self, flat=True):
"""
Iterate over the items in the vector, using absolute names.
Parameters
----------
flat : bool
If True, return the flattened values.
Yields
------
str
Name of each variable.
ndarray or float
Value of each variable.
"""
arrs = self._views_flat if flat else self._views
if self._under_complex_step:
yield from arrs.items()
else:
for name, val in arrs.items():
yield name, val.real
def _abs_iter(self):
"""
Iterate over the absolute names in the vector.
Yields
------
str
Name of each variable.
"""
yield from self._views
[docs] def __contains__(self, name):
"""
Check if the variable is found in this vector.
Parameters
----------
name : str
Promoted or relative variable name in the owning system's namespace.
Returns
-------
bool
True or False.
"""
return self._name2abs_name(name) in self._names
def _contains_abs(self, name):
"""
Check if the variable is found in this vector.
Parameters
----------
name : str
Absolute variable name.
Returns
-------
bool
True or False.
"""
return name in self._names
[docs] def __getitem__(self, name):
"""
Get the variable value.
Parameters
----------
name : str
Promoted or relative variable name in the owning system's namespace.
Returns
-------
float or ndarray
variable value.
"""
abs_name = self._name2abs_name(name)
if abs_name is not None:
return self._abs_get_val(abs_name, flat=False)
else:
raise KeyError(f"{self._system().msginfo}: Variable name '{name}' not found.")
def _abs_get_val(self, name, flat=True):
"""
Get the variable value using the absolute name.
No error checking is performed on the name.
Parameters
----------
name : str
Absolute name in the owning system's namespace.
flat : bool
If True, return the flat value.
Returns
-------
float or ndarray
variable value.
"""
if flat:
if self._under_complex_step:
return self._views_flat[name]
else:
return self._views_flat[name].real
if self._under_complex_step:
return self._views[name]
else:
return self._views[name].real
def _abs_set_val(self, name, val):
"""
Set the variable value using the absolute name.
No error checking is performed on the name.
Parameters
----------
name : str
Absolute name in the owning system's namespace.
val : float or ndarray
Value to set.
"""
if self._under_complex_step:
self._views[name][:] = val
else:
self._views[name].real[:] = val
[docs] def __setitem__(self, name, value):
"""
Set the variable value.
Parameters
----------
name : str
Promoted or relative variable name in the owning system's namespace.
value : float or list or tuple or ndarray
variable value to set
"""
self.set_var(name, value)
def _initialize_data(self, root_vector):
"""
Internally allocate vectors.
Must be implemented by the subclass.
Parameters
----------
root_vector : <Vector> or None
the root's vector instance or None, if we are at the root.
"""
raise NotImplementedError('_initialize_data not defined for vector type '
f'{type(self).__name__}')
def _initialize_views(self):
"""
Internally assemble views onto the vectors.
Must be implemented by the subclass.
"""
raise NotImplementedError('_initialize_views not defined for vector type '
f'{type(self).__name__}')
def __iadd__(self, vec):
"""
Perform in-place vector addition.
Must be implemented by the subclass.
Parameters
----------
vec : <Vector>
vector to add to self.
"""
raise NotImplementedError(f'__iadd__ not defined for vector type {type(self).__name__}')
def __isub__(self, vec):
"""
Perform in-place vector substraction.
Must be implemented by the subclass.
Parameters
----------
vec : <Vector>
vector to subtract from self.
"""
raise NotImplementedError(f'__isub__ not defined for vector type {type(self).__name__}')
def __imul__(self, val):
"""
Perform in-place scalar multiplication.
Must be implemented by the subclass.
Parameters
----------
val : int or float
scalar to multiply self.
"""
raise NotImplementedError(f'__imul__ not defined for vector type {type(self).__name__}')
[docs] def add_scal_vec(self, val, vec):
"""
Perform in-place addition of a vector times a scalar.
Must be implemented by the subclass.
Parameters
----------
val : int or float
Scalar.
vec : <Vector>
This vector times val is added to self.
"""
raise NotImplementedError('add_scale_vec not defined for vector type '
f'{type(self).__name__}')
[docs] def asarray(self, copy=False):
"""
Return a flat array representation of this vector.
If copy is True, return a copy. Otherwise, try to avoid it.
Parameters
----------
copy : bool
If True, return a copy of the array.
Returns
-------
ndarray
Array representation of this vector.
"""
raise NotImplementedError(f'asarray not defined for vector type {type(self).__name__}')
return None # silence lint warning
[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.
"""
raise NotImplementedError(f'iscomplex not defined for vector type {type(self).__name__}')
return False # silence lint warning
[docs] def set_vec(self, vec):
"""
Set the value of this vector to that of the incoming vector.
Must be implemented by the subclass.
Parameters
----------
vec : <Vector>
The vector whose values self is set to.
"""
raise NotImplementedError(f'set_vec not defined for vector type {type(self).__name__}')
[docs] def set_val(self, val, idxs=_full_slice):
"""
Set the data array of this vector to a scalar or array value, with optional indices.
Must be implemented by the subclass.
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.
"""
raise NotImplementedError(f'set_arr not defined for vector type {type(self).__name__}')
[docs] def set_vals(self, vals):
"""
Set the data array of this vector using a value or iter of values, one for each variable.
The values must be in the same order as the variables appear in this Vector.
Parameters
----------
vals : iter of ndarrays
Values for each variable contained in this vector, in the proper order.
"""
arr = self.asarray()
start = end = 0
for v in vals:
try:
end += v.size
except AttributeError: # assume a plain float
arr[start] = v
end += 1
else:
arr[start:end] = v.ravel()
start = end
[docs] def set_var(self, name, val, idxs=_full_slice, flat=False, var_name=None):
"""
Set the array view corresponding to the named variable, with optional indexing.
Parameters
----------
name : str
The name of the variable.
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.
flat : bool
If True, set into flattened variable.
var_name : str or None
If specified, the variable name to use when reporting errors. This is useful
when setting an AutoIVC value that the user only knows by a connected input name.
"""
abs_name = self._name2abs_name(name)
if abs_name is None:
raise KeyError(f"{self._system().msginfo}: Variable name "
f"'{var_name if var_name else name}' not found.")
if self.read_only:
raise ValueError(f"{self._system().msginfo}: Attempt to set value of "
f"'{var_name if var_name else name}' in "
f"{self._kind} vector when it is read only.")
if idxs is _full_slice:
if flat:
idxs = _flat_full_indexer
else:
idxs = _full_indexer
elif not isinstance(idxs, Indexer):
idxs = indexer(idxs, flat_src=flat)
if flat:
if isinstance(val, float):
self._views_flat[abs_name][idxs.flat()] = val
else:
self._views_flat[abs_name][idxs.flat()] = np.asarray(val).flat
else:
value = np.asarray(val)
view = self._views[abs_name]
try:
if view.shape:
view[idxs()] = value
else:
# view is a scalar so we can't update it without breaking its connection
# to the underlying array, so set the value into the
# array using the flat view, which is an array of size 1.
self._views_flat[abs_name][0] = value
except Exception as err:
try:
value = value.reshape(view[idxs()].shape)
except Exception:
raise ValueError(f"{self._system().msginfo}: Failed to set value of "
f"'{var_name if var_name else name}': {str(err)}.")
view[idxs()] = value
[docs] def dot(self, vec):
"""
Compute the dot product of the current vec and the incoming vec.
Must be implemented by the subclass.
Parameters
----------
vec : <Vector>
The incoming vector being dotted with self.
"""
raise NotImplementedError(f'dot not defined for vector type {type(self).__name__}')
[docs] def get_norm(self):
"""
Return the norm of this vector.
Must be implemented by the subclass.
Returns
-------
float
Norm of this vector.
"""
raise NotImplementedError(f'get_norm not defined for vector type {type(self).__name__}')
return None # silence lint warning about missing return value.
def _in_matvec_context(self):
"""
Return True if this vector is inside of a matvec_context.
"""
raise NotImplementedError('_in_matvec_context not defined for vector type '
f'{type(self).__name__}')
[docs] def set_complex_step_mode(self, active):
"""
Turn on or off complex stepping mode.
Parameters
----------
active : bool
Complex mode flag; set to True prior to commencing complex step.
"""
self._under_complex_step = active
[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.
"""
raise NotImplementedError(f'get_hash not defined for vector type {type(self).__name__}')
return '' # silence lint warning about missing return value.
def _get_local_views(self, arr=None):
"""
Return a dict of views into an array using local names.
If arr is not supplied, use our existing internal data array.
Note that if arr is not specified, the array used will depend upon the value of
_under_complex_step.
Parameters
----------
arr : ndarray or None
If not None, create views into this array.
Returns
-------
dict
A dict of views into the data array keyed using local names.
"""
if arr is None:
arr = self.asarray(copy=False)
elif len(self) != arr.size:
raise RuntimeError(f"{self._system().msginfo}: can't create local view dict because "
f"given array is size {arr.size} but expected size is {len(self)}.")
dct = {}
path = self._system().pathname
pathlen = len(path) + 1 if path else 0
start = end = 0
for name, val in self._abs_item_iter(flat=False):
end += val.size
view = arr[start:end]
view.shape = val.shape
dct[name[pathlen:]] = view
start = end
return dct