"""Define the base System class."""
import sys
import os
import hashlib
import time

from contextlib import contextmanager
from collections import OrderedDict, defaultdict
from itertools import chain
from enum import IntEnum

import re
from fnmatch import fnmatchcase

from numbers import Integral

import numpy as np
import networkx as nx

from openmdao.core.configinfo import _ConfigInfo
from openmdao.core.constants import _DEFAULT_OUT_STREAM, _UNDEFINED, INT_DTYPE, INF_BOUND, \
    _SetupStatus
from openmdao.jacobians.assembled_jacobian import DenseJacobian, CSCJacobian
from openmdao.recorders.recording_manager import RecordingManager
from openmdao.vectors.vector import _full_slice
from openmdao.utils.mpi import MPI, multi_proc_exception_check
from openmdao.utils.options_dictionary import OptionsDictionary
from openmdao.utils.record_util import create_local_meta, check_path
from openmdao.utils.units import is_compatible, unit_conversion, simplify_unit
from openmdao.utils.variable_table import write_var_table
from openmdao.utils.array_utils import evenly_distrib_idxs
from openmdao.utils.name_maps import name2abs_name, name2abs_names
from openmdao.utils.coloring import _compute_coloring, Coloring, \
    _STD_COLORING_FNAME, _DEF_COMP_SPARSITY_ARGS, _ColSparsityJac
import openmdao.utils.coloring as coloring_mod
from openmdao.utils.indexer import indexer
from openmdao.utils.om_warnings import issue_warning, DerivativesWarning, PromotionWarning,\
    UnusedOptionWarning, warn_deprecation
from openmdao.utils.general_utils import determine_adder_scaler, \
    format_as_float_or_array, ContainsAll, all_ancestors, make_set, match_prom_or_abs, \
        _is_slicer_op
from openmdao.approximation_schemes.complex_step import ComplexStep
from openmdao.approximation_schemes.finite_difference import FiniteDifference


_empty_frozen_set = frozenset()

_asm_jac_types = {
    'csc': CSCJacobian,
    'dense': DenseJacobian,
}

# Suppored methods for derivatives
_supported_methods = {
    'fd': FiniteDifference,
    'cs': ComplexStep,
    'exact': None,
    'jax': None
}

_DEFAULT_COLORING_META = {
    'wrt_patterns': ('*',),  # patterns used to match wrt variables
    'method': 'fd',          # finite differencing method  ('fd' or 'cs')
    'wrt_matches': None,     # where matched wrt names are stored
    'per_instance': True,    # assume each instance can have a different coloring
    'coloring': None,        # this will contain the actual Coloring object
    'dynamic': False,        # True if dynamic coloring is being used
    'static': None,          # either _STD_COLORING_FNAME, a filename, or a Coloring object
                             # if use_fixed_coloring was called
}

_DEFAULT_COLORING_META.update(_DEF_COMP_SPARSITY_ARGS)

_recordable_funcs = frozenset(['_apply_linear', '_apply_nonlinear', '_solve_linear',
                               '_solve_nonlinear'])

# the following are local metadata that will also be accessible for vars on all procs
global_meta_names = {
    'input': ('units', 'shape', 'size', 'distributed', 'tags', 'desc', 'shape_by_conn',
              'copy_shape'),
    'output': ('units', 'shape', 'size', 'desc',
               'ref', 'ref0', 'res_ref', 'distributed', 'lower', 'upper', 'tags', 'shape_by_conn',
               'copy_shape'),
}

allowed_meta_names = {
    'val',
    'global_shape',
    'global_size',
    'src_indices',
    'flat_src_indices',
    'type',
    'res_units',
}
allowed_meta_names.update(global_meta_names['input'])
allowed_meta_names.update(global_meta_names['output'])

resp_size_checks = {
    'con': ['ref', 'ref0', 'scaler', 'adder', 'upper', 'lower', 'equals'],
    'obj': ['ref', 'ref0', 'scaler', 'adder']
}
resp_types = {'con': 'constraint', 'obj': 'objective'}


class _MatchType(IntEnum):
    """
    Class used to define different types of promoted name matches.

    Attributes
    ----------
    NAME : int
        Literal name match.
    RENAME : int
        Rename match.
    PATTERN : int
        Glob pattern match.
    """

    NAME = 0
    RENAME = 1
    PATTERN = 2


value_deprecated_msg = "The metadata key 'value' will be deprecated in 4.0. Please use 'val'."


class _MetadataDict(dict):
    """
    A dict wrapper for a dict of metadata, to throw deprecation if a user indexes in using value.
    """

    def __getitem__(self, key):
        if key == 'value':
            warn_deprecation(value_deprecated_msg)
            key = 'val'
        val = dict.__getitem__(self, key)
        return val

    def __setitem__(self, key, val):
        if key == 'value':
            warn_deprecation(value_deprecated_msg)
            key = 'val'
        dict.__setitem__(self, key, val)


class System(object):
    """
    Base class for all systems in OpenMDAO.

    Never instantiated; subclassed by <Group> or <Component>.
    All subclasses have their attributes defined here.

    In attribute names:
        abs / abs_name: absolute, unpromoted variable name, seen from root (unique).
        rel / rel_name: relative, unpromoted variable name, seen from current system (unique).
        prom / prom_name: relative, promoted variable name, seen from current system (non-unique).
        idx: global variable index among variables on all procs (I/O indices separate).
        my_idx: index among variables in this system, on this processor (I/O indices separate).
        io: indicates explicitly that input and output variables are combined in the same dict.

    Parameters
    ----------
    num_par_fd : int
        If FD is active, number of concurrent FD solves.
    **kwargs : dict of keyword arguments
        Keyword arguments that will be mapped into the System options.

    Attributes
    ----------
    name : str
        Name of the system, must be different from siblings.
    pathname : str
        Global name of the system, including the path.
    comm : MPI.Comm or <FakeComm>
        MPI communicator object.
    options : OptionsDictionary
        options dictionary
    recording_options : OptionsDictionary
        Recording options dictionary
    _problem_meta : dict
        Problem level metadata.
    under_complex_step : bool
        When True, this system is undergoing complex step.
    under_finite_difference : bool
        When True, this system is undergoing finite differencing.
    under_approx : bool
        When True, this system is undergoing approximation.
    iter_count : int
        Counts the number of times this system has called _solve_nonlinear. This also
        corresponds to the number of times that the system's outputs are recorded if a recorder
        is present.
    iter_count_apply : int
        Counts the number of times the system has called _apply_nonlinear. For ExplicitComponent,
        calls to apply_nonlinear also call compute, so number of executions can be found by adding
        this and iter_count together. Recorders do no record calls to apply_nonlinear.
    iter_count_without_approx : int
        Counts the number of times the system has iterated but excludes any that occur during
        approximation of derivatives.
    cite : str
        Listing of relevant citations that should be referenced when
        publishing work that uses this class.
    _full_comm : MPI.Comm or None
        MPI communicator object used when System's comm is split for parallel FD.
    _solver_print_cache : list
        Allows solver iprints to be set to requested values after setup calls.
    _subsystems_allprocs : OrderedDict
        Dict mapping subsystem name to SysInfo(system, index) for children of this system.
    _subsystems_myproc : [<System>, ...]
        List of local subsystems that exist on this proc.
    _var_promotes : { 'any': [], 'input': [], 'output': [] }
        Dictionary of lists of variable names/wildcards specifying promotion
        (used to calculate promoted names)
    _var_prom2inds : dict
        Maps promoted name to src_indices in scope of system.
    _var_allprocs_prom2abs_list : {'input': dict, 'output': dict}
        Dictionary mapping promoted names to list of all absolute names.
        For outputs, the list will have length one since promoted output names are unique.
    _var_abs2prom : {'input': dict, 'output': dict}
        Dictionary mapping absolute names to promoted names, on current proc.
    _var_allprocs_abs2prom : {'input': dict, 'output': dict}
        Dictionary mapping absolute names to promoted names, on all procs.
    _var_allprocs_abs2meta : dict
        Dictionary mapping absolute names to metadata dictionaries for allprocs variables.
        The keys are
        ('units', 'shape', 'size') for inputs and
        ('units', 'shape', 'size', 'ref', 'ref0', 'res_ref', 'distributed') for outputs.
    _var_abs2meta : dict
        Dictionary mapping absolute names to metadata dictionaries for myproc variables.
    _var_discrete : dict
        Dictionary of discrete var metadata and values local to this process.
    _var_allprocs_discrete : dict
        Dictionary of discrete var metadata and values for all processes.
    _discrete_inputs : dict-like or None
        Storage for discrete input values.
    _discrete_outputs : dict-like or None
        Storage for discrete output values.
    _var_allprocs_abs2idx : dict
        Dictionary mapping absolute names to their indices among this system's allprocs variables.
        Therefore, the indices range from 0 to the total number of this system's variables.
    _var_sizes : {'input': ndarray, 'output': ndarray}
        Array of local sizes of this system's allprocs variables.
        The array has size nproc x num_var where nproc is the number of processors
        owned by this system and num_var is the number of allprocs variables.
    _owned_sizes : ndarray
        Array of local sizes for 'owned' or distributed vars only.
    _var_offsets : {<vecname>: {'input': dict of ndarray, 'output': dict of ndarray}, ...} or None
        Dict of distributed offsets, keyed by var name.  Offsets are stored in an array
        of size nproc x num_var where nproc is the number of processors
        in this System's communicator and num_var is the number of allprocs variables
        in the given system.  This is only defined in a Group that owns one or more interprocess
        connections or a top level Group or System that is used to compute total derivatives
        across multiple processes.
    _vars_to_gather : dict
        Contains names of non-distributed variables that are remote on at least one proc in the comm
    _conn_global_abs_in2out : {'abs_in': 'abs_out'}
        Dictionary containing all explicit & implicit connections (continuous and discrete)
        owned by this system or any descendant system. The data is the same across all processors.
    _vectors : {'input': dict, 'output': dict, 'residual': dict}
        Dictionaries of vectors keyed by vec_name.
    _inputs : <Vector>
        The inputs vector; points to _vectors['input']['nonlinear'].
    _outputs : <Vector>
        The outputs vector; points to _vectors['output']['nonlinear'].
    _residuals : <Vector>
        The residuals vector; points to _vectors['residual']['nonlinear'].
    _nonlinear_solver : <NonlinearSolver>
        Nonlinear solver to be used for solve_nonlinear.
    _linear_solver : <LinearSolver>
        Linear solver to be used for solve_linear; not the Newton system.
    _approx_schemes : OrderedDict
        A mapping of approximation types to the associated ApproximationScheme.
    _jacobian : <Jacobian>
        <Jacobian> object to be used in apply_linear.
    _owns_approx_jac : bool
        If True, this system approximated its Jacobian
    _owns_approx_jac_meta : dict
        Stores approximation metadata (e.g., step_size) from calls to approx_totals
    _owns_approx_of : list or None
        Overrides aproximation outputs. This is set when calculating system derivatives, and serves
        as a way to communicate the driver's output quantities to the approximation objects so that
        we only take derivatives of variables that the driver needs.
    _owns_approx_of_idx : dict
        Index for override 'of' approximations if declared. When the user calls  `add_objective`
        or `add_constraint`, they may optionally specify an "indices" argument. This argument must
        also be communicated to the approximations when they are set up so that 1) the Jacobian is
        the correct size, and 2) we don't perform any extra unnecessary calculations.
    _owns_approx_wrt : list or None
        Overrides aproximation inputs. This is set when calculating system derivatives, and serves
        as a way to communicate the driver's input quantities to the approximation objects so that
        we only take derivatives with respect to variables that the driver needs.
    _owns_approx_wrt_idx : dict
        Index for override 'wrt' approximations if declared. When the user calls  `add_designvar`
        they may optionally specify an "indices" argument. This argument must also be communicated
        to the approximations when they are set up so that 1) the Jacobian is the correct size, and
        2) we don't perform any extra unnecessary calculations.
    _subjacs_info : dict of dict
        Sub-jacobian metadata for each (output, input) pair added using
        declare_partials. Members of each pair may be glob patterns.
    _approx_subjac_keys : list
        List of subjacobian keys used for approximated derivatives.
    _design_vars : dict of dict
        dict of all driver design vars added to the system.
    _responses : dict of dict
        dict of all driver responses added to the system.
    _rec_mgr : <RecordingManager>
        object that manages all recorders added to this system.
    _static_subsystems_allprocs : OrderedDict
        Dict of SysInfo(subsys, index) that stores all subsystems added outside of setup.
    _static_design_vars : dict of dict
        Driver design variables added outside of setup.
    _static_responses : dict of dict
        Driver responses added outside of setup.
    matrix_free : bool
        This is set to True if the component overrides the appropriate function with a user-defined
        matrix vector product with the Jacobian or any of its subsystems do.
    _relevant : dict
        Mapping of a VOI to a tuple containing dependent inputs, dependent outputs,
        and dependent systems.
    _vois : dict
        Either design vars or responses metadata, depending on the direction of
        derivatives.
    _mode : str
        Indicates derivative direction for the model, either 'fwd' or 'rev'.
    _scope_cache : dict
        Cache for variables in the scope of various mat-vec products.
    _has_guess : bool
        True if this system has or contains a system with a `guess_nonlinear` method defined.
    _has_output_scaling : bool
        True if this system has output scaling.
    _has_output_adder : bool
        True if this system has scaling that includes an adder term.
    _has_resid_scaling : bool
        True if this system has resid scaling.
    _has_input_scaling : bool
        True if this system has input scaling.
    _has_input_adder : bool
        True if this system has scaling that includes an adder term.
    _has_bounds : bool
        True if this system has upper or lower bounds on outputs.
    _has_distrib_vars : bool
        If True, this System contains at least one distributed variable. Used to determine if a
        parallel group or distributed component is below a DirectSolver so that we can raise an
        exception.
    _owning_rank : dict
        Dict mapping var name to the lowest rank where that variable is local.
    _filtered_vars_to_record : Dict
        Dict of list of var names to record
    _vector_class : class
        Class to use for data vectors.  After setup will contain the value of either
        _problem_meta['distributed_vector_class'] or _problem_meta['local_vector_class'].
    _assembled_jac : AssembledJacobian or None
        If not None, this is the AssembledJacobian owned by this system's linear_solver.
    _num_par_fd : int
        If FD is active, and the value is > 1, turns on parallel FD and specifies the number of
        concurrent FD solves.
    _par_fd_id : int
        ID used to determine which columns in the jacobian will be computed when using parallel FD.
    _has_approx : bool
        If True, this system or its descendent has declared approximated partial or semi-total
        derivatives.
    _coloring_info : tuple
        Metadata that defines how to perform coloring of this System's approx jacobian. Not
        used if this System does no partial or semi-total coloring.
    _first_call_to_linearize : bool
        If True, this is the first call to _linearize.
    _is_local : bool
        If True, this system is local to this mpi process.
    _tot_jac : __TotalJacInfo or None
        If a total jacobian is being computed and this is the top level System, this will
        be a reference to the _TotalJacInfo object.
    """

    def __init__(self, num_par_fd=1, **kwargs):
        """
        Initialize all attributes.
        """
        self.name = ''
        self.pathname = None
        self.comm = None
        self._is_local = False

        # System options
        self.options = OptionsDictionary(parent_name=type(self).__name__)

        self.options.declare('assembled_jac_type', values=['csc', 'dense'], default='csc',
                             desc='Linear solver(s) in this group or implicit component, '
                                  'if using an assembled jacobian, will use this type.')

        # Case recording options
        self.recording_options = OptionsDictionary(parent_name=type(self).__name__)
        self.recording_options.declare('record_inputs', types=bool, default=True,
                                       desc='Set to True to record inputs at the system level')
        self.recording_options.declare('record_outputs', types=bool, default=True,
                                       desc='Set to True to record outputs at the system level')
        self.recording_options.declare('record_residuals', types=bool, default=True,
                                       desc='Set to True to record residuals at the system level')
        self.recording_options.declare('record_metadata', types=bool,
                                       desc='Deprecated. Recording of metadata will always be done',
                                       default=True,
                                       deprecation="The recording option, record_metadata, "
                                       "on System is "
                                       "deprecated. Recording of metadata will always be done")
        self.recording_options.declare('record_model_metadata', types=bool,
                                       desc='Deprecated. Recording of model metadata will always '
                                       'be done',
                                       deprecation="The recording option, record_model_metadata, "
                                       "on System is deprecated. Recording of model metadata will "
                                       "always be done",
                                       default=True)
        self.recording_options.declare('includes', types=list, default=['*'],
                                       desc='Patterns for variables to include in recording. \
                                       Uses fnmatch wildcards')
        self.recording_options.declare('excludes', types=list, default=[],
                                       desc='Patterns for vars to exclude in recording '
                                       '(processed post-includes). Uses fnmatch wildcards')
        self.recording_options.declare('options_excludes', types=list, default=[],
                                       desc='User-defined metadata to exclude in recording')

        self._problem_meta = None

        # Counting iterations.
        self.iter_count = 0
        self.iter_count_apply = 0
        self.iter_count_without_approx = 0

        self.cite = ""

        self._solver_print_cache = []

        self._subsystems_allprocs = {}
        self._subsystems_myproc = []
        self._vars_to_gather = {}

        self._var_promotes = {'input': [], 'output': [], 'any': []}

        self._var_allprocs_prom2abs_list = None
        self._var_prom2inds = {}
        self._var_abs2prom = {'input': {}, 'output': {}}
        self._var_allprocs_abs2prom = {'input': {}, 'output': {}}
        self._var_allprocs_abs2meta = {'input': {}, 'output': {}}
        self._var_abs2meta = {'input': {}, 'output': {}}
        self._var_discrete = {'input': {}, 'output': {}}
        self._var_allprocs_discrete = {'input': {}, 'output': {}}

        self._var_allprocs_abs2idx = {}

        self._var_sizes = None
        self._owned_sizes = None
        self._var_offsets = None

        self._full_comm = None

        self._vectors = {'input': {}, 'output': {}, 'residual': {}}

        self._inputs = None
        self._outputs = None
        self._residuals = None
        self._discrete_inputs = None
        self._discrete_outputs = None

        self._nonlinear_solver = None
        self._linear_solver = None

        self._jacobian = None
        self._approx_schemes = OrderedDict()
        self._subjacs_info = {}
        self._approx_subjac_keys = None
        self.matrix_free = False

        self.under_approx = False
        self._owns_approx_jac = False
        self._owns_approx_jac_meta = {}
        self._owns_approx_wrt = None
        self._owns_approx_of = None
        self._owns_approx_wrt_idx = {}
        self._owns_approx_of_idx = {}

        self.under_complex_step = False
        self.under_finite_difference = False

        self._design_vars = OrderedDict()
        self._responses = OrderedDict()
        self._rec_mgr = RecordingManager()

        self._conn_global_abs_in2out = {}

        self._static_subsystems_allprocs = {}
        self._static_design_vars = OrderedDict()
        self._static_responses = OrderedDict()

        self._mode = None

        self._scope_cache = {}

        self._num_par_fd = num_par_fd

        self._declare_options()
        self.initialize()

        self.options.update(kwargs)

        self._has_guess = False
        self._has_output_scaling = False
        self._has_output_adder = False
        self._has_resid_scaling = False
        self._has_input_scaling = False
        self._has_input_adder = False
        self._has_bounds = False
        self._has_distrib_vars = False
        self._has_approx = False

        self._vector_class = None

        self._assembled_jac = None

        self._par_fd_id = 0

        self._filtered_vars_to_record = {}
        self._owning_rank = None
        self._coloring_info = _DEFAULT_COLORING_META.copy()
        self._first_call_to_linearize = True   # will check in first call to _linearize
        self._tot_jac = None

    @property
    def msginfo(self):
        """
        Our instance pathname, if available, or our class name.  For use in error messages.

        Returns
        -------
        str
            Either our instance pathname or class name.
        """
        if self.pathname is not None:
            if self.pathname == '':
                return f"<model> <class {type(self).__name__}>"
            return f"'{self.pathname}' <class {type(self).__name__}>"
        if self.name:
            return f"'{self.name}' <class {type(self).__name__}>"
        return f"<class {type(self).__name__}>"

    def _get_inst_id(self):
        return self.pathname

    def abs_name_iter(self, iotype, local=True, cont=True, discrete=False):
        """
        Iterate over absolute variable names for this System.

        By setting appropriate values for 'cont' and 'discrete', yielded variable
        names can be continuous only, discrete only, or both.

        Parameters
        ----------
        iotype : str
            Either 'input' or 'output'.
        local : bool
            If True, include only names of local variables. Default is True.
        cont : bool
            If True, include names of continuous variables.  Default is True.
        discrete : bool
            If True, include names of discrete variables.  Default is False.

        Yields
        ------
        str
        """
        if cont:
            if local:
                yield from self._var_abs2meta[iotype]
            else:
                yield from self._var_allprocs_abs2meta[iotype]

        if discrete:
            if local:
                prefix = self.pathname + '.' if self.pathname else ''
                for name in self._var_discrete[iotype]:
                    yield prefix + name
            else:
                yield from self._var_allprocs_discrete[iotype]

    def _jac_of_iter(self):
        """
        Iterate over (name, offset, end, slice, dist_sizes) for each 'of' (row) var in the jacobian.

        The slice is internal to the given variable in the result, and this is always a full
        slice except possible for groups where _owns_approx_of_idx is defined.

        Yields
        ------
        str
            Name of 'of' variable.
        int
            Starting index.
        int
            Ending index.
        slice or ndarray
            A full slice or indices for the 'of' variable.
        ndarray or None
            Distributed sizes if var is distributed else None
        """
        toidx = self._var_allprocs_abs2idx
        sizes = self._var_sizes['output']
        total = self.pathname == ''
        szname = 'global_size' if total else 'size'
        start = end = 0
        for of, meta in self._var_abs2meta['output'].items():
            end += meta[szname]
            yield of, start, end, _full_slice, sizes[:, toidx[of]] if meta['distributed'] else None
            start = end

    def _jac_wrt_iter(self, wrt_matches=None):
        """
        Iterate over (name, offset, end, vec, slc, dist_sizes) for each column var in the jacobian.

        Parameters
        ----------
        wrt_matches : set or None
            Only include row vars that are contained in this set.  This will determine what
            the actual offsets are, i.e. the offsets will be into a reduced jacobian
            containing only the matching columns.

        Yields
        ------
        str
            Name of 'wrt' variable.
        int
            Starting index.
        int
            Ending index.
        Vector
            Either the _outputs or _inputs vector.
        slice
            A full slice.
        ndarray or None
            Distributed sizes if var is distributed else None
        """
        toidx = self._var_allprocs_abs2idx
        sizes_in = self._var_sizes['input']
        sizes_out = self._var_sizes['output']

        tometa_in = self._var_allprocs_abs2meta['input']

        local_ins = self._var_abs2meta['input']
        local_outs = self._var_abs2meta['output']

        total = self.pathname == ''
        szname = 'global_size' if total else 'size'

        start = end = 0
        for of, _start, _end, _, dist_sizes in self._jac_of_iter():
            if wrt_matches is None or of in wrt_matches:
                end += (_end - _start)
                vec = self._outputs if of in local_outs else None
                yield of, start, end, vec, _full_slice, dist_sizes
                start = end

        for wrt, meta in self._var_abs2meta['input'].items():
            if wrt_matches is None or wrt in wrt_matches:
                end += meta[szname]
                vec = self._inputs if wrt in local_ins else None
                dist_sizes = sizes_in[:, toidx[wrt]] if tometa_in[wrt]['distributed'] else None
                yield wrt, start, end, vec, _full_slice, dist_sizes
                start = end

    def _declare_options(self):
        """
        Declare options before kwargs are processed in the init method.

        This is optionally implemented by subclasses of Component or Group
        that themselves are intended to be subclassed by the end user. The
        options of the intermediate class are declared here leaving the
        `initialize` method available for user-defined options.
        """
        pass

    def initialize(self):
        """
        Perform any one-time initialization run at instantiation.
        """
        pass

    def _configure(self):
        """
        Configure this system to assign children settings.
        """
        pass

    def _get_root_vectors(self):
        """
        Get the root vectors for the nonlinear and linear vectors for the model.

        Returns
        -------
        dict of dict of Vector
            Root vectors: first key is 'input', 'output', or 'residual'; second key is vec_name.
        """
        # save root vecs as an attribute so that we can reuse the nonlinear scaling vecs in the
        # linear root vec
        self._root_vecs = root_vectors = {'input': OrderedDict(),
                                          'output': OrderedDict(),
                                          'residual': OrderedDict()}

        force_alloc_complex = self._problem_meta['force_alloc_complex']

        # Check for complex step to set vectors up appropriately.
        # If any subsystem needs complex step, then we need to allocate it everywhere.
        nl_alloc_complex = force_alloc_complex
        for sub in self.system_iter(include_self=True, recurse=True):
            nl_alloc_complex |= 'cs' in sub._approx_schemes
            if nl_alloc_complex:
                break

        # Linear vectors allocated complex only if subsolvers require derivatives.
        if nl_alloc_complex:
            from openmdao.error_checking.check_config import check_allocate_complex_ln
            ln_alloc_complex = check_allocate_complex_ln(self, force_alloc_complex)
        else:
            ln_alloc_complex = False

        if self._has_input_scaling or self._has_output_scaling or self._has_resid_scaling:
            self._scale_factors = self._compute_root_scale_factors()
        else:
            self._scale_factors = None

        if self._vector_class is None:
            self._vector_class = self._local_vector_class

        for vec_name in ('nonlinear', 'linear'):
            sizes = self._var_sizes['output']
            ncol = 1
            if vec_name == 'nonlinear':
                alloc_complex = nl_alloc_complex
            else:
                alloc_complex = ln_alloc_complex

            for key in ['input', 'output', 'residual']:
                root_vectors[key][vec_name] = self._vector_class(vec_name, key, self,
                                                                 alloc_complex=alloc_complex)

        if 'linear' in root_vectors['input']:
            root_vectors['input']['linear']._scaling_nl_vec = \
                root_vectors['input']['nonlinear']._scaling

        return root_vectors

    def _get_approx_scheme(self, method):
        """
        Return the approximation scheme associated with the given method, creating one if needed.

        Parameters
        ----------
        method : str
            Name of the type of approxmation scheme.

        Returns
        -------
        ApproximationScheme
            The ApproximationScheme associated with the given method.
        """
        if method == 'exact':
            return None
        if method not in _supported_methods:
            msg = '{}: Method "{}" is not supported, method must be one of {}'
            raise ValueError(msg.format(self.msginfo, method,
                             [m for m in _supported_methods if m != 'exact']))
        if method not in self._approx_schemes:
            self._approx_schemes[method] = _supported_methods[method]()
        return self._approx_schemes[method]

    def get_source(self, name):
        """
        Return the source variable connected to the given named variable.

        The name can be a promoted name or an absolute name.
        If the given variable is an input, the absolute name of the connected source will
        be returned.  If the given variable itself is a source, its own absolute name will
        be returned.

        Parameters
        ----------
        name : str
            Absolute or promoted name of the variable.

        Returns
        -------
        str
            The absolute name of the source variable.
        """
        try:
            prom2abs = self._problem_meta['prom2abs']
        except Exception:
            raise RuntimeError(f"{self.msginfo}: get_source cannot be called for variable {name} "
                               "before Problem.setup has been called.")

        if name in prom2abs['output']:
            return prom2abs['output'][name][0]

        if name in prom2abs['input']:
            name = prom2abs['input'][name][0]

        model = self._problem_meta['model_ref']()
        if name in model._conn_global_abs_in2out:
            return model._conn_global_abs_in2out[name]

        raise KeyError(f"{self.msginfo}: source for '{name}' not found.")

    def _setup(self, comm, mode, prob_meta):
        """
        Perform setup for this system and its descendant systems.

        Parameters
        ----------
        comm : MPI.Comm or <FakeComm> or None
            The global communicator.
        mode : str
            Derivative direction, either 'fwd', or 'rev', or 'auto'
        prob_meta : dict
            Problem level metadata dictionary.
        """
        # save a ref to the problem level options.
        self._problem_meta = prob_meta

        # reset any coloring if a Coloring object was not set explicitly
        if self._coloring_info['dynamic'] or self._coloring_info['static'] is not None:
            self._coloring_info['coloring'] = None

        self.pathname = ''
        self.comm = comm
        self._mode = mode

        # Besides setting up the processors, this method also builds the model hierarchy.
        self._setup_procs(self.pathname, comm, mode, self._problem_meta)

        prob_meta['config_info'] = _ConfigInfo()

        try:
            # Recurse model from the bottom to the top for configuring.
            self._configure()
        finally:
            prob_meta['config_info'] = None

        self._configure_check()

        self._setup_var_data()

        # promoted names must be known to determine implicit connections so this must be
        # called after _setup_var_data, and _setup_var_data will have to be partially redone
        # after auto_ivcs have been added, but auto_ivcs can't be added until after we know all of
        # the connections.
        self._setup_global_connections()
        self._setup_dynamic_shapes()

        self._top_level_post_connections(mode)

        self._problem_meta['relevant'] = self._init_relevance(mode)
        self._setup_var_sizes()

        self._top_level_post_sizes()

        # determine which connections are managed by which group, and check validity of connections
        self._setup_connections()

    def _top_level_post_connections(self, mode):
        # this runs after all connections are known, and only if this is the top level system
        self._problem_meta['prom2abs'] = self._get_all_promotes()

    def _get_all_promotes(self):
        """
        Create the top level mapping of all promoted names to absolute names for all local systems.

        Note that this will only be called on a component that is the top level model.

        Returns
        -------
        dict
            Mapping of all promoted names to absolute names.
        """
        prom2abs = {}
        for typ in ('input', 'output'):
            t_prom2abs = prom2abs[typ] = defaultdict(list)
            for prom, abslist in self._var_allprocs_prom2abs_list[typ].items():
                t_prom2abs[prom] = abslist

        return prom2abs

    def _top_level_post_sizes(self):
        # this runs after the variable sizes are known
        self._setup_global_shapes()

    def _setup_check(self):
        """
        Do any error checking on user's setup, before any other recursion happens.
        """
        pass

    def _configure_check(self):
        """
        Do any error checking on i/o and connections.
        """
        pass

    def _setup_dynamic_shapes(self):
        pass

    def _final_setup(self, comm):
        """
        Perform final setup for this system and its descendant systems.

        This part of setup is called automatically at the start of run_model or run_driver.

        Parameters
        ----------
        comm : MPI.Comm or <FakeComm> or None
            The global communicator.
        """
        if self._use_derivatives:
            # must call this before vector setup because it determines if we need to alloc commplex
            self._setup_partials()

        self._setup_vectors(self._get_root_vectors())

        # Transfers do not require recursion, but they have to be set up after the vector setup.
        self._setup_transfers()

        # Same situation with solvers, partials, and Jacobians.
        # If we're updating, we just need to re-run setup on these, but no recursion necessary.
        self._setup_solvers()
        self._setup_solver_print()
        if self._use_derivatives:
            self._setup_jacobians()

        self._setup_recording()

        self.set_initial_values()

    def _get_approx_subjac_keys(self):
        """
        Return a list of (of, wrt) keys needed for approx derivs for this group.

        Returns
        -------
        list
            List of approx derivative subjacobian keys.
        """
        if self._approx_subjac_keys is None:
            self._approx_subjac_keys = list(self._approx_subjac_keys_iter())

        return self._approx_subjac_keys

    def use_fixed_coloring(self, coloring=_STD_COLORING_FNAME, recurse=True):
        """
        Use a precomputed coloring for this System.

        Parameters
        ----------
        coloring : str
            A coloring filename.  If no arg is passed, filename will be determined
            automatically.
        recurse : bool
            If True, set fixed coloring in all subsystems that declare a coloring. Ignored
            if a specific coloring is passed in.
        """
        if coloring_mod._force_dyn_coloring and coloring is _STD_COLORING_FNAME:
            self._coloring_info['dynamic'] = True
            return  # don't use static this time

        self._coloring_info['static'] = coloring
        self._coloring_info['dynamic'] = False

        if coloring is not _STD_COLORING_FNAME:
            if recurse:
                issue_warning('recurse was passed to use_fixed_coloring but a specific coloring '
                              'was set, so recurse was ignored.',
                              prefix=self.pathname,
                              category=UnusedOptionWarning)
            if isinstance(coloring, Coloring):
                approx = self._get_approx_scheme(coloring._meta['method'])
                # force regen of approx groups on next call to compute_approximations
                approx._reset()
            return

        if recurse:
            for s in self._subsystems_myproc:
                s.use_fixed_coloring(coloring, recurse)

    def declare_coloring(self,
                         wrt=_DEFAULT_COLORING_META['wrt_patterns'],
                         method=_DEFAULT_COLORING_META['method'],
                         form=None,
                         step=None,
                         per_instance=_DEFAULT_COLORING_META['per_instance'],
                         num_full_jacs=_DEFAULT_COLORING_META['num_full_jacs'],
                         tol=_DEFAULT_COLORING_META['tol'],
                         orders=_DEFAULT_COLORING_META['orders'],
                         perturb_size=_DEFAULT_COLORING_META['perturb_size'],
                         min_improve_pct=_DEFAULT_COLORING_META['min_improve_pct'],
                         show_summary=_DEFAULT_COLORING_META['show_summary'],
                         show_sparsity=_DEFAULT_COLORING_META['show_sparsity']):
        """
        Set options for deriv coloring of a set of wrt vars matching the given pattern(s).

        Parameters
        ----------
        wrt : str or list of str
            The name or names of the variables that derivatives are taken with respect to.
            This can contain input names, output names, or glob patterns.
        method : str
            Method used to compute derivative: "fd" for finite difference, "cs" for complex step.
        form : str
            Finite difference form, can be "forward", "central", or "backward". Leave
            undeclared to keep unchanged from previous or default value.
        step : float
            Step size for finite difference. Leave undeclared to keep unchanged from previous
            or default value.
        per_instance : bool
            If True, a separate coloring will be generated for each instance of a given class.
            Otherwise, only one coloring for a given class will be generated and all instances
            of that class will use it.
        num_full_jacs : int
            Number of times to repeat partial jacobian computation when computing sparsity.
        tol : float
            Tolerance used to determine if an array entry is nonzero during sparsity determination.
        orders : int
            Number of orders above and below the tolerance to check during the tolerance sweep.
        perturb_size : float
            Size of input/output perturbation during generation of sparsity.
        min_improve_pct : float
            If coloring does not improve (decrease) the number of solves more than the given
            percentage, coloring will not be used.
        show_summary : bool
            If True, display summary information after generating coloring.
        show_sparsity : bool
            If True, display sparsity with coloring info after generating coloring.
        """
        if method not in ('fd', 'cs', 'jax'):
            raise RuntimeError(
                "{}: method must be one of ['fd', 'cs', 'jax'].".format(self.msginfo))

        self._has_approx = True

        # start with defaults
        options = _DEFAULT_COLORING_META.copy()

        if method != 'jax':
            approx = self._get_approx_scheme(method)
            options.update(approx.DEFAULT_OPTIONS)

        if self._coloring_info['static'] is None:
            options['dynamic'] = True
        else:
            options['dynamic'] = False
            options['static'] = self._coloring_info['static']

        options['wrt_patterns'] = [wrt] if isinstance(wrt, str) else wrt
        options['method'] = method
        options['per_instance'] = per_instance
        options['num_full_jacs'] = num_full_jacs
        options['tol'] = tol
        options['orders'] = orders
        options['perturb_size'] = perturb_size
        options['min_improve_pct'] = min_improve_pct
        options['show_summary'] = show_summary
        options['show_sparsity'] = show_sparsity
        options['coloring'] = self._coloring_info['coloring']
        if form is not None:
            options['form'] = form
        if step is not None:
            options['step'] = step

        self._coloring_info = options

    def _compute_coloring(self, recurse=False, **overrides):
        """
        Compute a coloring of the partial jacobian.

        This assumes that the current System is in a proper state for computing derivatives.

        Parameters
        ----------
        recurse : bool
            If True, recurse from this system down the system hierarchy.  Whenever a group
            is encountered that has specified its coloring metadata, we don't recurse below
            that group unless that group has a subsystem that has a nonlinear solver that uses
            gradients.
        **overrides : dict
            Any args that will override either default coloring settings or coloring settings
            resulting from an earlier call to declare_coloring.

        Returns
        -------
        list of Coloring
            The computed colorings.
        """
        if recurse:
            colorings = []
            my_coloring = self._coloring_info['coloring']
            grad_systems = self._get_gradient_nl_solver_systems()
            for s in self.system_iter(include_self=True, recurse=True):
                if my_coloring is None or s in grad_systems:
                    if s._coloring_info['coloring'] is not None:
                        coloring = s._compute_coloring(recurse=False, **overrides)[0]
                        colorings.append(coloring)
                        if coloring is not None:
                            coloring._meta['pathname'] = s.pathname
                            coloring._meta['class'] = type(s).__name__
            return [c for c in colorings if c is not None] or [None]

        info = self._coloring_info

        use_jax = False
        try:
            if self.options['use_jax']:
                info['method'] = 'jax'
                use_jax = True
        except KeyError:
            pass

        info.update(**overrides)
        if isinstance(info['wrt_patterns'], str):
            info['wrt_patterns'] = [info['wrt_patterns']]

        if info['method'] is None and self._approx_schemes:
            info['method'] = list(self._approx_schemes)[0]

        if info['coloring'] is None:
            # check to see if any approx or jax derivs have been declared
            for meta in self._subjacs_info.values():
                if 'method' in meta and meta['method']:
                    break
            else:  # no approx derivs found
                if not (self._owns_approx_of or self._owns_approx_wrt):
                    issue_warning("No partials found but coloring was requested.  "
                                  "Declaring ALL partials as dense "
                                  "(method='{}')".format(info['method']),
                                  prefix=self.msginfo, category=DerivativesWarning)
                    try:
                        self.declare_partials('*', '*', method=info['method'])
                    except AttributeError:  # this system must be a group
                        from openmdao.core.component import Component
                        from openmdao.core.indepvarcomp import IndepVarComp
                        from openmdao.components.exec_comp import ExecComp
                        for s in self.system_iter(recurse=True, typ=Component):
                            if not isinstance(s, ExecComp) and not isinstance(s, IndepVarComp):
                                s.declare_partials('*', '*', method=info['method'])
                    self._setup_partials()

        if not use_jax:
            approx_scheme = self._get_approx_scheme(info['method'])

        if info['coloring'] is None and info['static'] is None:
            info['dynamic'] = True

        coloring_fname = self.get_coloring_fname()

        # if we find a previously computed class coloring for our class, just use that
        # instead of regenerating a coloring.
        if not info['per_instance'] and coloring_fname in coloring_mod._CLASS_COLORINGS:
            info['coloring'] = coloring = coloring_mod._CLASS_COLORINGS[coloring_fname]
            if coloring is None:
                print("\nClass coloring for class '{}' wasn't good enough, "
                      "so skipping for '{}'".format(type(self).__name__, self.pathname))
                info['static'] = None
            else:
                print("\n{} using class coloring for class '{}'".format(self.pathname,
                                                                        type(self).__name__))
                info.update(coloring._meta)
                # force regen of approx groups during next compute_approximations
                if not use_jax:
                    approx_scheme._reset()
            return [coloring]

        # compute perturbations
        starting_inputs = self._inputs.asarray(copy=True)
        in_offsets = starting_inputs.copy()
        in_offsets[in_offsets == 0.0] = 1.0
        in_offsets *= info['perturb_size']

        starting_outputs = self._outputs.asarray(copy=True)
        out_offsets = starting_outputs.copy()
        out_offsets[out_offsets == 0.0] = 1.0
        out_offsets *= info['perturb_size']

        starting_resids = self._residuals.asarray(copy=True)

        save_first_call = self._first_call_to_linearize
        self._first_call_to_linearize = False
        sparsity_start_time = time.time()

        # for groups, this does some setup of approximations
        self._setup_approx_coloring()

        # tell approx scheme to limit itself to only colored columns
        if not use_jax:
            approx_scheme._reset()
            approx_scheme._during_sparsity_comp = True

        self._update_wrt_matches(info)

        save_jac = self._jacobian

        # use special sparse jacobian to collect sparsity info
        self._jacobian = _ColSparsityJac(self, info)

        from openmdao.core.group import Group
        is_total = isinstance(self, Group)

        for i in range(info['num_full_jacs']):
            # randomize inputs (and outputs if implicit)
            if i > 0:
                self._inputs.set_val(starting_inputs +
                                     in_offsets * np.random.random(in_offsets.size))
                self._outputs.set_val(starting_outputs +
                                      out_offsets * np.random.random(out_offsets.size))
                if is_total:
                    self._solve_nonlinear()
                else:
                    self._apply_nonlinear()

                if not use_jax:
                    for scheme in self._approx_schemes.values():
                        scheme._reset()  # force a re-initialization of approx
                        scheme._during_sparsity_comp = True

            if use_jax:
                self._jax_linearize()
            else:
                self.run_linearize(sub_do_ln=False)

        sparsity, sp_info = self._jacobian.get_sparsity(self)

        self._jacobian = save_jac

        if not use_jax:
            # revert uncolored approx back to normal
            for scheme in self._approx_schemes.values():
                scheme._reset()

        sparsity_time = time.time() - sparsity_start_time

        ordered_wrt_info = list(self._jac_wrt_iter(info['wrt_matches']))
        ordered_of_info = list(self._jac_of_iter())

        sp_info['sparsity_time'] = sparsity_time
        sp_info['pathname'] = self.pathname
        sp_info['class'] = type(self).__name__
        sp_info['type'] = 'semi-total' if self._subsystems_allprocs else 'partial'

        if self.pathname:
            ordered_of_info = self._jac_var_info_abs2prom(ordered_of_info)
            ordered_wrt_info = self._jac_var_info_abs2prom(ordered_wrt_info)

        if use_jax:
            direction = self._mode
        else:
            direction = 'fwd'
        coloring = _compute_coloring(sparsity, direction)

        # if the improvement wasn't large enough, don't use coloring
        pct = coloring._solves_info()[-1]
        if info['min_improve_pct'] > pct:
            info['coloring'] = info['static'] = None
            msg = f"Coloring was deactivated.  Improvement of {pct:.1f}% was less than min " \
                  f"allowed ({info['min_improve_pct']:.1f}%)."
            issue_warning(msg, prefix=self.msginfo, category=DerivativesWarning)
            if not info['per_instance']:
                coloring_mod._CLASS_COLORINGS[coloring_fname] = None

            # make sure we have no leftover garbage from sparsity/coloring computations
            self._inputs.set_val(starting_inputs)
            self._outputs.set_val(starting_outputs)
            self._residuals.set_val(starting_resids)
            return [None]

        coloring._row_vars = [t[0] for t in ordered_of_info]
        coloring._col_vars = [t[0] for t in ordered_wrt_info]
        coloring._row_var_sizes = [t[2] - t[1] for t in ordered_of_info]
        coloring._col_var_sizes = [t[2] - t[1] for t in ordered_wrt_info]

        coloring._meta.update(info)  # save metadata we used to create the coloring
        del coloring._meta['coloring']
        coloring._meta.update(sp_info)

        info['coloring'] = coloring

        if not use_jax:
            approx = self._get_approx_scheme(coloring._meta['method'])
            # force regen of approx groups during next compute_approximations
            approx._reset()

        if info['show_sparsity'] or info['show_summary']:
            print("\nColoring for '%s' (class %s)" % (self.pathname, type(self).__name__))

        if info['show_sparsity']:
            coloring.display_txt()
        if info['show_summary']:
            coloring.summary()

        self._save_coloring(coloring)

        if not info['per_instance']:
            # save the class coloring for other instances of this class to use
            coloring_mod._CLASS_COLORINGS[coloring_fname] = coloring

        # restore original inputs/outputs
        self._inputs.set_val(starting_inputs)
        self._outputs.set_val(starting_outputs)
        self._residuals.set_val(starting_resids)

        self._first_call_to_linearize = save_first_call

        return [coloring]

    def _setup_approx_coloring(self):
        pass

    def get_coloring_fname(self):
        """
        Return the full pathname to a coloring file.

        Returns
        -------
        str
            Full pathname of the coloring file.
        """
        directory = self._problem_meta['coloring_dir']
        if not self.pathname:
            # total coloring
            return os.path.join(directory, 'total_coloring.pkl')

        if self._coloring_info.get('per_instance'):
            # base the name on the instance pathname
            fname = 'coloring_' + self.pathname.replace('.', '_') + '.pkl'
        else:
            # base the name on the class name
            fname = 'coloring_' + '_'.join(
                [self.__class__.__module__.replace('.', '_'), self.__class__.__name__]) + '.pkl'

        return os.path.join(directory, fname)

    def _save_coloring(self, coloring):
        """
        Save the coloring to a file based on this system's class or pathname.

        Parameters
        ----------
        coloring : Coloring
            See Coloring class docstring.
        """
        # under MPI, only save on proc 0
        if ((self._full_comm is not None and self._full_comm.rank == 0) or
                (self._full_comm is None and self.comm.rank == 0)):
            coloring.save(self.get_coloring_fname())

    def _get_static_coloring(self):
        """
        Get the Coloring for this system.

        If necessary, load the Coloring from a file.

        Returns
        -------
        Coloring or None
            Coloring object, possible loaded from a file, or None
        """
        info = self._coloring_info
        coloring = info['coloring']
        if coloring is not None:
            return coloring

        static = info['static']
        if static is _STD_COLORING_FNAME or isinstance(static, str):
            if static is _STD_COLORING_FNAME:
                fname = self.get_coloring_fname()
            else:
                fname = static
            print("%s: loading coloring from file %s" % (self.msginfo, fname))
            info['coloring'] = coloring = Coloring.load(fname)
            if info['wrt_patterns'] != coloring._meta['wrt_patterns']:
                raise RuntimeError("%s: Loaded coloring has different wrt_patterns (%s) than "
                                   "declared ones (%s)." %
                                   (self.msginfo, coloring._meta['wrt_patterns'],
                                    info['wrt_patterns']))
            info.update(info['coloring']._meta)
            approx = self._get_approx_scheme(info['method'])
            # force regen of approx groups during next compute_approximations
            approx._reset()
        elif isinstance(static, coloring_mod.Coloring):
            info['coloring'] = coloring = static

        if coloring is not None:
            info['dynamic'] = False

        info['static'] = coloring

        return coloring

    def _get_coloring(self):
        """
        Get the Coloring for this system.

        If necessary, load the Coloring from a file or dynamically generate it.

        Returns
        -------
        Coloring or None
            Coloring object, possible loaded from a file or dynamically generated, or None
        """
        coloring = self._get_static_coloring()
        if coloring is None and self._coloring_info['dynamic']:
            self._coloring_info['coloring'] = coloring = self._compute_coloring()[0]
            if coloring is not None:
                self._coloring_info.update(coloring._meta)

        return coloring

    def _setup_par_fd_procs(self, comm):
        """
        Split up the comm for use in parallel FD.

        Parameters
        ----------
        comm : MPI.Comm or <FakeComm>
            MPI communicator object.

        Returns
        -------
        MPI.Comm or <FakeComm>
            MPI communicator object.
        """
        num_par_fd = self._num_par_fd
        if comm.size < num_par_fd:
            raise ValueError("%s: num_par_fd must be <= communicator size (%d)" %
                             (self.msginfo, comm.size))

        self._full_comm = comm

        if num_par_fd > 1:
            sizes, offsets = evenly_distrib_idxs(num_par_fd, comm.size)

            # a 'color' is assigned to each subsystem, with
            # an entry for each processor it will be given
            # e.g. [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]
            color = np.empty(comm.size, dtype=INT_DTYPE)
            for i in range(num_par_fd):
                color[offsets[i]:offsets[i] + sizes[i]] = i

            self._par_fd_id = color[comm.rank]

            comm = self._full_comm.Split(self._par_fd_id)

        return comm

    def _setup_recording(self):
        if self._rec_mgr._recorders:
            myinputs = myoutputs = myresiduals = []

            options = self.recording_options
            incl = options['includes']
            excl = options['excludes']

            # includes and excludes for inputs are specified using _absolute_ names
            # vectors are keyed on absolute name, discretes on relative/promoted name
            if options['record_inputs']:
                myinputs = sorted([n for n in self._var_abs2prom['input']
                                   if check_path(n, incl, excl)])

            # includes and excludes for outputs are specified using _promoted_ names
            # vectors are keyed on absolute name, discretes on relative/promoted name
            if options['record_outputs']:
                myoutputs = sorted([n for n, prom in self._var_abs2prom['output'].items()
                                    if check_path(prom, incl, excl)])

                if self._var_discrete['output']:
                    # if we have discrete outputs then residual name set doesn't match output one
                    if options['record_residuals']:
                        contains = self._residuals._contains_abs
                        myresiduals = [n for n in myoutputs if contains(n)]
                elif options['record_residuals']:
                    myresiduals = myoutputs

            elif options['record_residuals']:
                abs2prom = self._var_abs2prom['output']
                myresiduals = [n for n in self._residuals._abs_iter()
                               if check_path(abs2prom[n], incl, excl)]

            self._filtered_vars_to_record = {
                'input': myinputs,
                'output': myoutputs,
                'residual': myresiduals
            }

            self._rec_mgr.startup(self)

        for subsys in self._subsystems_myproc:
            subsys._setup_recording()

    def _setup_procs(self, pathname, comm, mode, prob_meta):
        """
        Execute first phase of the setup process.

        Distribute processors, assign pathnames, and call setup on the component.
        Also reset internal data structures.

        Parameters
        ----------
        pathname : str
            Global name of the system, including the path.
        comm : MPI.Comm or <FakeComm>
            MPI communicator object.
        mode : str
            Derivatives calculation mode, 'fwd' for forward, and 'rev' for
            reverse (adjoint). Default is 'rev'.
        prob_meta : dict
            Problem level options.
        """
        self.pathname = pathname
        self._problem_meta = prob_meta
        self._first_call_to_linearize = True
        self._is_local = True
        self._vectors = {}
        self._full_comm = None
        self._approx_subjac_keys = None

        self.options._parent_name = self.msginfo
        self.recording_options._parent_name = self.msginfo
        self._mode = mode
        self._design_vars = OrderedDict()
        self._responses = OrderedDict()
        self._design_vars.update(self._static_design_vars)
        self._responses.update(self._static_responses)

    def _setup_var_data(self):
        """
        Compute the list of abs var names, abs/prom name maps, and metadata dictionaries.
        """
        self._var_prom2inds = {}
        self._var_allprocs_prom2abs_list = {'input': OrderedDict(), 'output': OrderedDict()}
        self._var_abs2prom = {'input': {}, 'output': {}}
        self._var_allprocs_abs2prom = {'input': {}, 'output': {}}
        self._var_allprocs_abs2meta = {'input': {}, 'output': {}}
        self._var_abs2meta = {'input': {}, 'output': {}}
        self._var_allprocs_discrete = {'input': {}, 'output': {}}
        self._var_allprocs_abs2idx = {}
        self._owning_rank = defaultdict(int)
        self._var_sizes = {}
        self._owned_sizes = None

        cfginfo = self._problem_meta['config_info']
        if cfginfo and self.pathname in cfginfo._modified_systems:
            cfginfo._modified_systems.remove(self.pathname)

    def _setup_global_shapes(self):
        """
        Compute the global size and shape of all variables on this system.
        """
        loc_meta = self._var_abs2meta

        for io in ('input', 'output'):
            # now set global sizes and shapes into metadata for distributed variables
            sizes = self._var_sizes[io]
            for idx, (abs_name, mymeta) in enumerate(self._var_allprocs_abs2meta[io].items()):
                local_shape = mymeta['shape']
                if mymeta['distributed']:
                    global_size = np.sum(sizes[:, idx])
                    mymeta['global_size'] = global_size

                    # assume that all but the first dimension of the shape of a
                    # distributed variable is the same on all procs
                    mymeta['global_shape'] = self._get_full_dist_shape(abs_name, local_shape)
                else:
                    # not distributed, just use local shape and size
                    mymeta['global_size'] = mymeta['size']
                    mymeta['global_shape'] = local_shape

                if abs_name in loc_meta[io]:
                    loc_meta[io][abs_name]['global_shape'] = mymeta['global_shape']
                    loc_meta[io][abs_name]['global_size'] = mymeta['global_size']

    def _setup_global_connections(self, conns=None):
        """
        Compute dict of all connections between this system's inputs and outputs.

        The connections come from 4 sources:
        1. Implicit connections owned by the current system
        2. Explicit connections declared by the current system
        3. Explicit connections declared by parent systems
        4. Implicit / explicit from subsystems

        Parameters
        ----------
        conns : dict
            Dictionary of connections passed down from parent group.
        """
        pass

    def _init_relevance(self, mode):
        """
        Create the relevance dictionary.

        This is only called on the top level System.

        Parameters
        ----------
        mode : str
            Derivative direction, either 'fwd' or 'rev'.

        Returns
        -------
        dict
            The relevance dictionary.
        """
        if self._use_derivatives:
            desvars = self.get_design_vars(recurse=True, get_sizes=False, use_prom_ivc=False)
            responses = self.get_responses(recurse=True, get_sizes=False, use_prom_ivc=False)
            return self.get_relevant_vars(desvars, responses, mode)

        return {'@all': ({'input': ContainsAll(), 'output': ContainsAll()}, ContainsAll())}

    def _setup_driver_units(self):
        """
        Compute unit conversions for driver variables.
        """
        abs2meta = self._var_allprocs_abs2meta['output']
        pro2abs = self._var_allprocs_prom2abs_list['output']
        pro2abs_in = self._var_allprocs_prom2abs_list['input']

        dv = self._design_vars
        for name, meta in dv.items():

            units = meta['units']
            dv[name]['total_adder'] = dv[name]['adder']
            dv[name]['total_scaler'] = dv[name]['scaler']

            if units is not None:
                # If derivatives are not being calculated, then you reach here before ivc_source
                # is placed in the meta.
                try:
                    units_src = meta['ivc_source']
                except KeyError:
                    units_src = self.get_source(name)

                var_units = abs2meta[units_src]['units']

                if var_units == units:
                    continue

                if var_units is None:
                    msg = "{}: Target for design variable {} has no units, but '{}' units " + \
                          "were specified."
                    raise RuntimeError(msg.format(self.msginfo, name, units))

                if not is_compatible(var_units, units):
                    msg = "{}: Target for design variable {} has '{}' units, but '{}' units " + \
                          "were specified."
                    raise RuntimeError(msg.format(self.msginfo, name, var_units, units))

                factor, offset = unit_conversion(var_units, units)
                base_adder, base_scaler = determine_adder_scaler(None, None,
                                                                 dv[name]['adder'],
                                                                 dv[name]['scaler'])

                dv[name]['total_adder'] = offset + base_adder / factor
                dv[name]['total_scaler'] = base_scaler * factor

        resp = self._responses
        type_dict = {'con': 'constraint', 'obj': 'objective'}
        for name, meta in resp.items():

            units = meta['units']
            resp[name]['total_scaler'] = resp[name]['scaler']
            resp[name]['total_adder'] = resp[name]['adder']

            if units is not None:
                # If derivatives are not being calculated, then you reach here before ivc_source
                # is placed in the meta.
                try:
                    units_src = meta['ivc_source']
                except KeyError:
                    units_src = self.get_source(name)

                var_units = abs2meta[units_src]['units']

                if var_units == units:
                    continue

                if var_units is None:
                    msg = "{}: Target for {} {} has no units, but '{}' units " + \
                          "were specified."
                    raise RuntimeError(msg.format(self.msginfo, type_dict[meta['type']],
                                                  name, units))

                if not is_compatible(var_units, units):
                    msg = "{}: Target for {} {} has '{}' units, but '{}' units " + \
                          "were specified."
                    raise RuntimeError(msg.format(self.msginfo, type_dict[meta['type']],
                                                  name, var_units, units))

                factor, offset = unit_conversion(var_units, units)
                base_adder, base_scaler = determine_adder_scaler(None, None,
                                                                 resp[name]['adder'],
                                                                 resp[name]['scaler'])

                resp[name]['total_scaler'] = base_scaler * factor
                resp[name]['total_adder'] = offset + base_adder / factor

        for s in self._subsystems_myproc:
            s._setup_driver_units()

    def _setup_connections(self):
        """
        Compute dict of all connections owned by this system.
        """
        pass

    def _setup_vectors(self, root_vectors):
        """
        Compute all vectors for all vec names and assign excluded variables lists.

        Parameters
        ----------
        root_vectors : dict of dict of Vector
            Root vectors: first key is 'input', 'output', or 'residual'; second key is vec_name.
        """
        self._vectors = vectors = {'input': OrderedDict(),
                                   'output': OrderedDict(),
                                   'residual': OrderedDict()}

        # Allocate complex if root vector was allocated complex.
        alloc_complex = root_vectors['output']['nonlinear']._alloc_complex

        # This happens if you reconfigure and switch to 'cs' without forcing the vectors to be
        # initially allocated as complex.
        if not alloc_complex and 'cs' in self._approx_schemes:
            raise RuntimeError("{}: In order to activate complex step during reconfiguration, "
                               "you need to set 'force_alloc_complex' to True during setup. e.g. "
                               "'problem.setup(force_alloc_complex=True)'".format(self.msginfo))

        if self._vector_class is None:
            self._vector_class = self._local_vector_class

        vector_class = self._vector_class

        for vec_name in ('nonlinear', 'linear'):

            # Only allocate complex in the vectors we need.
            vec_alloc_complex = root_vectors['output'][vec_name]._alloc_complex

            for kind in ['input', 'output', 'residual']:
                rootvec = root_vectors[kind][vec_name]
                vectors[kind][vec_name] = vector_class(
                    vec_name, kind, self, rootvec, alloc_complex=vec_alloc_complex)

        if 'linear' in vectors['input']:
            vectors['input']['linear']._scaling_nl_vec = vectors['input']['nonlinear']._scaling

        self._inputs = vectors['input']['nonlinear']
        self._outputs = vectors['output']['nonlinear']
        self._residuals = vectors['residual']['nonlinear']

        for subsys in self._subsystems_myproc:
            subsys._scale_factors = self._scale_factors
            subsys._setup_vectors(root_vectors)

    def _compute_root_scale_factors(self):
        """
        Compute scale factors for all variables.

        Returns
        -------
        dict
            Mapping of each absoute var name to its corresponding scaling factor tuple.
        """
        # make this a defaultdict to handle the case of access using unconnected inputs
        scale_factors = defaultdict(lambda: {
            'input': (0.0, 1.0),
        })

        for abs_name, meta in self._var_allprocs_abs2meta['output'].items():
            ref0 = meta['ref0']
            res_ref = meta['res_ref']
            a0 = ref0
            a1 = meta['ref'] - ref0
            scale_factors[abs_name] = {
                'output': (a0, a1),
                'residual': (0.0, res_ref),
            }
        return scale_factors

    def _setup_transfers(self):
        """
        Compute all transfers that are owned by this system.
        """
        pass

    def _setup_solvers(self):
        """
        Perform setup in all solvers.
        """
        # remove old solver error files if they exist
        if self.pathname == '':
            rank = MPI.COMM_WORLD.rank if MPI is not None else 0
            if rank == 0:
                for f in os.listdir('.'):
                    if fnmatchcase(f, 'solver_errors.*.out'):
                        os.remove(f)

        if self._nonlinear_solver is not None:
            self._nonlinear_solver._setup_solvers(self, 0)
        if self._linear_solver is not None:
            self._linear_solver._setup_solvers(self, 0)

        for subsys in self._subsystems_myproc:
            subsys._setup_solvers()

    def _setup_jacobians(self, recurse=True):
        """
        Set and populate jacobians down through the system tree.

        Parameters
        ----------
        recurse : bool
            If True, setup jacobians in all descendants.
        """
        asm_jac_solvers = set()
        if self._linear_solver is not None:
            asm_jac_solvers.update(self._linear_solver._assembled_jac_solver_iter())

        nl_asm_jac_solvers = set()
        if self.nonlinear_solver is not None:
            nl_asm_jac_solvers.update(self.nonlinear_solver._assembled_jac_solver_iter())

        asm_jac = None
        if asm_jac_solvers:
            asm_jac = _asm_jac_types[self.options['assembled_jac_type']](system=self)
            self._assembled_jac = asm_jac
            for s in asm_jac_solvers:
                s._assembled_jac = asm_jac

        if nl_asm_jac_solvers:
            if asm_jac is None:
                asm_jac = _asm_jac_types[self.options['assembled_jac_type']](system=self)
            for s in nl_asm_jac_solvers:
                s._assembled_jac = asm_jac

        if self._has_approx:
            self._set_approx_partials_meta()

        # At present, we don't support a AssembledJacobian in a group
        # if any subcomponents are matrix-free.
        if asm_jac is not None:
            if self.matrix_free:
                raise RuntimeError("%s: AssembledJacobian not supported for matrix-free "
                                   "subcomponent." % self.msginfo)

        if recurse:
            for subsys in self._subsystems_myproc:
                subsys._setup_jacobians()

    def set_initial_values(self):
        """
        Set all input and output variables to their declared initial values.
        """
        for abs_name, meta in self._var_abs2meta['input'].items():
            self._inputs.set_var(abs_name, meta['val'])

        for abs_name, meta in self._var_abs2meta['output'].items():
            self._outputs.set_var(abs_name, meta['val'])

    def _get_promotion_maps(self):
        """
        Define variable maps based on promotes lists.

        Returns
        -------
        dict of {'input': {str:(str, info), ...}, 'output': {str:(str, info), ...}}
            dictionary mapping input/output variable names
            to (promoted name, promotion_info) tuple.
        """
        from openmdao.core.group import Group
        prom_names = self._var_allprocs_prom2abs_list
        gname = self.name + '.' if self.name else ''

        def split_list(lst):
            """
            Yield match type, name/pattern/tuple info, and src_indices info.

            Parameters
            ----------
            lst : list
                List of names, patterns and/or tuples specifying promotes.

            Yields
            ------
            Enum
                match type
            str
                name or pattern string
            (str, _PromotesInfo)
                name/rename/pattern, promotion info (src_indices, etc.)
            """
            for entry in lst:
                key, pinfo = entry
                if isinstance(key, str):
                    # note, conditional here is faster than using precompiled regex
                    if '*' in key or '?' in key or '[' in key:
                        yield _MatchType.PATTERN, key, entry
                    else:
                        yield _MatchType.NAME, key, entry
                elif isinstance(key, tuple) and len(key) == 2:
                    yield _MatchType.RENAME, key[0], (key[1], pinfo)
                else:
                    raise TypeError(f"when adding subsystem '{self.pathname}', entry '{key}'"
                                    " is not a string or tuple of size 2.")

        def _dup(io, matches, match_type, name, tup):
            """
            Report error or warning when attempting to promote a variable twice.

            Parameters
            ----------
            matches : dict {'input': ..., 'output': ...}
                Dict of promoted names and associated info.
            match_type : intEnum
                Indicates whether match is an explicit name, rename, or pattern match.
            name : str
                Name of promoted variable that is specified multiple times.
            tup : tuple (?, _PromotesInfo)
                First entry can be name, rename, or pattern depending on the match type.

            Returns
            -------
            bool
                If True, ignore the new match, else replace the old with the new.
            """
            old_name, old_key, old_info, old_match_type = matches[io][name]
            _, info = tup
            if old_match_type == _MatchType.RENAME:
                old_key = (old_name, old_key)
            else:
                old_using = f"'{old_key}'"
            if match_type == _MatchType.RENAME:
                new_using = (name, tup[0])
            else:
                new_using = f"'{tup[0]}'"

            mismatch = info.compare(old_info) if info is not None and old_info is not None else ()
            if mismatch:
                raise RuntimeError(f"{self.msginfo}: {io} variable '{name}', promoted using "
                                   f"{new_using}, was already promoted using {old_using} with "
                                   f"different values for {mismatch}.")

            if old_match_type != _MatchType.PATTERN:
                if old_key != tup[0]:
                    raise RuntimeError(f"{self.msginfo}: Can't alias promoted {io} '{name}' to "
                                       f"'{tup[0]}' because '{name}' has already been promoted as "
                                       f"'{old_key}'.")

            if old_key != '*':
                msg = f"{io} variable '{name}', promoted using {new_using}, " \
                      f"was already promoted using {old_using}."
                issue_warning(msg, prefix=self.msginfo, category=PromotionWarning)

            return match_type == _MatchType.PATTERN

        def resolve(to_match, io_types, matches, proms):
            """
            Determine the mapping of promoted names to the parent scope for a promotion type.

            This is called once for promotes or separately for promotes_inputs and promotes_outputs.
            """
            if not to_match:
                return

            # always add '*' so we won't report if it matches nothing (in the case where the
            # system has no variables of that io type)
            found = {'*'}

            for match_type, key, tup in split_list(to_match):
                s, pinfo = tup
                if match_type == _MatchType.PATTERN:
                    for io in io_types:
                        if io == 'output':
                            pinfo = None
                        if key == '*' and not matches[io]:  # special case. add everything
                            matches[io] = pmap = {n: (n, key, pinfo, match_type) for n in proms[io]}
                        else:
                            pmap = matches[io]
                            nmatch = len(pmap)
                            for n in proms[io]:
                                if fnmatchcase(n, key):
                                    if not (n in pmap and _dup(io, matches, match_type, n, tup)):
                                        pmap[n] = (n, key, pinfo, match_type)
                            if len(pmap) > nmatch:
                                found.add(key)
                else:  # NAME or RENAME
                    for io in io_types:
                        if io == 'output':
                            pinfo = None
                        pmap = matches[io]
                        if key in proms[io]:
                            if key in pmap:
                                _dup(io, matches, match_type, key, tup)
                            pmap[key] = (s, key, pinfo, match_type)
                            if match_type == _MatchType.NAME:
                                found.add(key)
                            else:
                                found.add((key, s))

            not_found = set(n for n, _ in to_match) - found
            if not_found:
                if (not self._var_abs2meta['input'] and not self._var_abs2meta['output'] and
                        isinstance(self, Group)):
                    empty_group_msg = ' Group contains no variables.'
                else:
                    empty_group_msg = ''
                if len(io_types) == 2:
                    call = 'promotes'
                else:
                    call = 'promotes_%ss' % io_types[0]

                not_found = sorted(not_found, key=lambda x: x if isinstance(x, str) else x[0])
                raise RuntimeError(f"{self.msginfo}: '{call}' failed to find any matches for the "
                                   f"following names or patterns: {not_found}.{empty_group_msg}")

        maps = {'input': {}, 'output': {}}

        if self._var_promotes['input'] or self._var_promotes['output']:
            if self._var_promotes['any']:
                raise RuntimeError("%s: 'promotes' cannot be used at the same time as "
                                   "'promotes_inputs' or 'promotes_outputs'." % self.msginfo)
            resolve(self._var_promotes['input'], ('input',), maps, prom_names)
            resolve(self._var_promotes['output'], ('output',), maps, prom_names)
        else:
            resolve(self._var_promotes['any'], ('input', 'output'), maps, prom_names)

        return maps

    def _get_scope(self):
        """
        Find the input and output variables that are needed for a particular matvec product.

        Returns
        -------
        (set, set)
            Sets of output and input variables.
        """
        try:
            return self._scope_cache[None]
        except KeyError:
            self._scope_cache[None] = (frozenset(self._var_abs2meta['output']), _empty_frozen_set)
            return self._scope_cache[None]

    @contextmanager
    def _unscaled_context(self, outputs=(), residuals=()):
        """
        Context manager for units and scaling for vectors.

        Temporarily puts vectors in a physical and unscaled state, because
        internally, vectors are nominally in a dimensionless and scaled state.

        Parameters
        ----------
        outputs : list of output <Vector> objects
            List of output vectors to apply the unit and scaling conversions.
        residuals : list of residual <Vector> objects
            List of residual vectors to apply the unit and scaling conversions.
        """
        if self._has_output_scaling:
            for vec in outputs:
                vec.scale_to_phys()
        if self._has_resid_scaling:
            for vec in residuals:
                vec.scale_to_phys()

        try:

            yield

        finally:

            if self._has_output_scaling:
                for vec in outputs:
                    vec.scale_to_norm()

            if self._has_resid_scaling:
                for vec in residuals:
                    vec.scale_to_norm()

    @contextmanager
    def _scaled_context_all(self):
        """
        Context manager that temporarily puts all vectors in a scaled state.
        """
        if self._has_output_scaling:
            for vec in self._vectors['output'].values():
                vec.scale_to_norm()
        if self._has_resid_scaling:
            for vec in self._vectors['residual'].values():
                vec.scale_to_norm()

        try:

            yield

        finally:

            if self._has_output_scaling:
                for vec in self._vectors['output'].values():
                    vec.scale_to_phys()
            if self._has_resid_scaling:
                for vec in self._vectors['residual'].values():
                    vec.scale_to_phys()

    @contextmanager
    def _matvec_context(self, scope_out, scope_in, mode, clear=True):
        """
        Context manager for vectors.

        For the given vec_name, return vectors that use a set of
        internal variables that are relevant to the current matrix-vector
        product.  This is called only from _apply_linear.

        Parameters
        ----------
        scope_out : frozenset or None
            Set of absolute output names in the scope of this mat-vec product.
            If None, all are in the scope.
        scope_in : frozenset or None
            Set of absolute input names in the scope of this mat-vec product.
            If None, all are in the scope.
        mode : str
            Key for specifying derivative direction. Values are 'fwd'
            or 'rev'.
        clear : bool(True)
            If True, zero out residuals (in fwd mode) or inputs and outputs
            (in rev mode).

        Yields
        ------
        (d_inputs, d_outputs, d_residuals) : tuple of Vectors
            Yields the three Vectors configured internally to deal only
            with variables relevant to the current matrix vector product.

        """
        d_inputs = self._vectors['input']['linear']
        d_outputs = self._vectors['output']['linear']
        d_residuals = self._vectors['residual']['linear']

        if clear:
            if mode == 'fwd':
                d_residuals.set_val(0.0)
            else:  # rev
                d_inputs.set_val(0.0)
                d_outputs.set_val(0.0)

        if scope_out is None and scope_in is None:
            yield d_inputs, d_outputs, d_residuals
        else:
            old_ins = d_inputs._names
            old_outs = d_outputs._names

            if scope_out is not None:
                d_outputs._names = scope_out.intersection(d_outputs._abs_iter())
            if scope_in is not None:
                d_inputs._names = scope_in.intersection(d_inputs._abs_iter())

            try:
                yield d_inputs, d_outputs, d_residuals
            finally:
                # reset _names so users will see full vector contents
                d_inputs._names = old_ins
                d_outputs._names = old_outs

    @contextmanager
    def _call_user_function(self, fname, protect_inputs=True,
                            protect_outputs=False, protect_residuals=False):
        """
        Context manager that wraps a call to a user defined function.

        Protect any vectors that should not be modified to help prevent user error
        and add information about the system to any errors that don't have it already.

        Parameters
        ----------
        fname : str
            Name of the user defined function.
        protect_inputs : bool
            If True, then set the inputs vector to be read only
        protect_outputs : bool
            If True, then set the outputs vector to be read only
        protect_residuals : bool
            If True, then set the residuals vector to be read only
        """
        self._inputs.read_only = protect_inputs
        self._outputs.read_only = protect_outputs
        self._residuals.read_only = protect_residuals

        try:
            yield
        except Exception:
            err_type, err, trace = sys.exc_info()
            if str(err).startswith(self.msginfo):
                raise
            else:
                raise err_type(
                    f"{self.msginfo}: Error calling {fname}(), {err}").with_traceback(trace)
        finally:
            self._inputs.read_only = False
            self._outputs.read_only = False
            self._residuals.read_only = False

    def get_nonlinear_vectors(self):
        """
        Return the inputs, outputs, and residuals vectors.

        Returns
        -------
        (inputs, outputs, residuals)
            Yields the inputs, outputs, and residuals nonlinear vectors.
        """
        if self._inputs is None:
            raise RuntimeError("{}: Cannot get vectors because setup has not yet been "
                               "called.".format(self.msginfo))

        return self._inputs, self._outputs, self._residuals

    def get_linear_vectors(self):
        """
        Return the linear inputs, outputs, and residuals vectors.

        Returns
        -------
        (inputs, outputs, residuals): tuple of <Vector> instances
            Yields the linear inputs, outputs, and residuals vectors.
        """
        if self._inputs is None:
            raise RuntimeError("{}: Cannot get vectors because setup has not yet been "
                               "called.".format(self.msginfo))

        return (self._vectors['input']['linear'],
                self._vectors['output']['linear'],
                self._vectors['residual']['linear'])

    def _get_var_offsets(self):
        """
        Compute global offsets for variables.

        Returns
        -------
        dict
            Arrays of global offsets keyed by vec_name and deriv direction.
        """
        if self._var_offsets is None:
            offsets = self._var_offsets = {}
            for type_ in ['input', 'output']:
                vsizes = self._var_sizes[type_]
                if vsizes.size > 0:
                    csum = np.empty(vsizes.size, dtype=INT_DTYPE)
                    csum[0] = 0
                    csum[1:] = np.cumsum(vsizes)[:-1]
                    offsets[type_] = csum.reshape(vsizes.shape)
                else:
                    offsets[type_] = np.zeros(0, dtype=INT_DTYPE).reshape((1, 0))

        return self._var_offsets

    @property
    def nonlinear_solver(self):
        """
        Get the nonlinear solver for this system.
        """
        return self._nonlinear_solver

    @nonlinear_solver.setter
    def nonlinear_solver(self, solver):
        """
        Set this system's nonlinear solver.
        """
        self._nonlinear_solver = solver

    @property
    def linear_solver(self):
        """
        Get the linear solver for this system.
        """
        return self._linear_solver

    @linear_solver.setter
    def linear_solver(self, solver):
        """
        Set this system's linear solver.
        """
        self._linear_solver = solver

    @property
    def _force_alloc_complex(self):
        return self._problem_meta['force_alloc_complex']

    @property
    def _use_derivatives(self):
        return self._problem_meta['use_derivatives']

    @property
    def _local_vector_class(self):
        return self._problem_meta['local_vector_class']

    @property
    def _distributed_vector_class(self):
        return self._problem_meta['distributed_vector_class']

    @property
    def _recording_iter(self):
        return self._problem_meta['recording_iter']

    @property
    def _relevant(self):
        return self._problem_meta['relevant']

    @property
    def _static_mode(self):
        """
        Return True if we are outside of setup.

        In this case, add_input, add_output, and add_subsystem all add to the
        '_static' versions of the respective data structures.
        These data structures are never reset during setup.

        Returns
        -------
        True if outside of setup.
        """
        return self._problem_meta is None or self._problem_meta['static_mode']

    def _set_solver_print(self, level=2, depth=1e99, type_='all'):
        """
        Apply the given print settings to the internal solvers, recursively.

        Parameters
        ----------
        level : int
            iprint level. Set to 2 to print residuals each iteration; set to 1
            to print just the iteration totals; set to 0 to disable all printing
            except for failures, and set to -1 to disable all printing including failures.
        depth : int
            How deep to recurse. For example, you can set this to 0 if you only want
            to print the top level linear and nonlinear solver messages. Default
            prints everything.
        type_ : str
            Type of solver to set: 'LN' for linear, 'NL' for nonlinear, or 'all' for all.
        """
        if self._linear_solver is not None and type_ != 'NL':
            self._linear_solver._set_solver_print(level=level, type_=type_)
        if self.nonlinear_solver is not None and type_ != 'LN':
            self.nonlinear_solver._set_solver_print(level=level, type_=type_)

        for subsys, _ in self._subsystems_allprocs.values():

            current_depth = subsys.pathname.count('.')
            if current_depth >= depth:
                continue

            subsys._set_solver_print(level=level, depth=depth - current_depth, type_=type_)

            if subsys._linear_solver is not None and type_ != 'NL':
                subsys._linear_solver._set_solver_print(level=level, type_=type_)
            if subsys.nonlinear_solver is not None and type_ != 'LN':
                subsys.nonlinear_solver._set_solver_print(level=level, type_=type_)

    def _setup_solver_print(self, recurse=True):
        """
        Apply the cached solver print settings during setup.

        Parameters
        ----------
        recurse : bool
            Whether to call this method in subsystems.
        """
        for level, depth, type_ in self._solver_print_cache:
            self._set_solver_print(level, depth, type_)

        if recurse:
            for subsys in self._subsystems_myproc:
                subsys._setup_solver_print(recurse=recurse)

    def set_solver_print(self, level=2, depth=1e99, type_='all'):
        """
        Control printing for solvers and subsolvers in the model.

        Parameters
        ----------
        level : int
            Iprint level. Set to 2 to print residuals each iteration; set to 1
            to print just the iteration totals; set to 0 to disable all printing
            except for failures, and set to -1 to disable all printing including failures.
        depth : int
            How deep to recurse. For example, you can set this to 0 if you only want
            to print the top level linear and nonlinear solver messages. Default
            prints everything.
        type_ : str
            Type of solver to set: 'LN' for linear, 'NL' for nonlinear, or 'all' for all.
        """
        if (level, depth, type_) not in self._solver_print_cache:
            self._solver_print_cache.append((level, depth, type_))

    def _set_approx_partials_meta(self):
        # this will load a static coloring (if any) and will populate wrt_matches if
        # there is any coloring (static or dynamic).
        self._get_static_wrt_matches()

    def _get_static_wrt_matches(self):
        """
        Return wrt_matches for static coloring if there is one.

        Returns
        -------
        list of str or ()
            List of wrt_matches for a static coloring or () if there isn't one.
        """
        if (self._coloring_info['coloring'] is not None and
                self._coloring_info['wrt_matches'] is None):
            self._update_wrt_matches(self._coloring_info)

        # if coloring has been specified, we don't want to have multiple
        # approximations for the same subjac, so don't register any new
        # approximations when the wrt matches those used in the coloring.
        if self._get_static_coloring() is not None:  # static coloring has been specified
            return self._coloring_info['wrt_matches']

        return ()  # for dynamic coloring or no coloring

    def system_iter(self, include_self=False, recurse=True, typ=None):
        """
        Yield a generator of local subsystems of this system.

        Parameters
        ----------
        include_self : bool
            If True, include this system in the iteration.
        recurse : bool
            If True, iterate over the whole tree under this system.
        typ : type
            If not None, only yield Systems that match that are instances of the
            given type.

        Yields
        ------
        type or None
        """
        if include_self and (typ is None or isinstance(self, typ)):
            yield self

        for s in self._subsystems_myproc:
            if typ is None or isinstance(s, typ):
                yield s
            if recurse:
                for sub in s.system_iter(recurse=True, typ=typ):
                    yield sub

    def _create_indexer(self, indices, typename, vname, flat_src=False):
        """
        Return an Indexer instance and it's size if possible.

        Parameters
        ----------
        indices : ndarray or sequence of ints
            The indices used to create the Indexer.
        typename : str
            Type name of the variable.  Could be 'design var', 'objective' or 'constraint'.
        vname : str
            Name of the variable.
        flat_src : bool
            If True, indices index into a flat array.

        Returns
        -------
        Indexer
            The newly created Indexer
        int or None
            The size of the indices, if known.
        """
        if not (indices is None or _is_slicer_op(indices)):
            arr = np.asarray(indices)
            if arr.dtype.kind not in ('i', 'u') or len(arr.shape) == 0:
                raise ValueError(f"{self.msginfo}: If specified, {typename} '{vname}' indices "
                                 "must be a sequence of integers.")
        try:
            idxer = indexer(indices, flat_src=flat_src)
        except Exception as err:
            raise err.__class__(f"{self.msginfo}: Invalid indices {indices} for {typename} "
                                f"'{vname}'.")

        # size may not be available at this point, but get it if we can in order to allow
        # some earlier error checking
        try:
            size = idxer.indexed_src_size
        except Exception:
            size = None

        return idxer, size

    def add_design_var(self, name, lower=None, upper=None, ref=None, ref0=None, indices=None,
                       adder=None, scaler=None, units=None,
                       parallel_deriv_color=None, cache_linear_solution=False, flat_indices=False):
        r"""
        Add a design variable to this system.

        Parameters
        ----------
        name : str
            Name of the design variable in the system.
        lower : float or ndarray, optional
            Lower boundary for the input.
        upper : upper or ndarray, optional
            Upper boundary for the input.
        ref : float or ndarray, optional
            Value of design var that scales to 1.0 in the driver.
        ref0 : float or ndarray, optional
            Value of design var that scales to 0.0 in the driver.
        indices : iter of int, optional
            If an input is an array, these indicate which entries are of
            interest for this particular design variable.  These may be
            positive or negative integers.
        adder : float or ndarray, optional
            Value to add to the model value to get the scaled value for the driver. adder
            is first in precedence.  adder and scaler are an alterantive to using ref
            and ref0.
        scaler : float or ndarray, optional
            Value to multiply the model value to get the scaled value for the driver. scaler
            is second in precedence. adder and scaler are an alterantive to using ref
            and ref0.
        units : str, optional
            Units to convert to before applying scaling.
        parallel_deriv_color : str
            If specified, this design var will be grouped for parallel derivative
            calculations with other variables sharing the same parallel_deriv_color.
        cache_linear_solution : bool
            If True, store the linear solution vectors for this variable so they can
            be used to start the next linear solution with an initial guess equal to the
            solution from the previous linear solve.
        flat_indices : bool
            If True, interpret specified indices as being indices into a flat source array.

        Notes
        -----
        The response can be scaled using ref and ref0.
        The argument :code:`ref0` represents the physical value when the scaled value is 0.
        The argument :code:`ref` represents the physical value when the scaled value is 1.
        """
        if name in self._design_vars or name in self._static_design_vars:
            msg = "{}: Design Variable '{}' already exists."
            raise RuntimeError(msg.format(self.msginfo, name))

        # Name must be a string
        if not isinstance(name, str):
            raise TypeError('{}: The name argument should be a string, got {}'.format(self.msginfo,
                                                                                      name))

        if units is not None:
            if not isinstance(units, str):
                raise TypeError(f"{self.msginfo}: The units argument should be a str or None for "
                                f"design_var '{name}'.")
            try:
                units = simplify_unit(units, msginfo=self.msginfo)
            except ValueError as e:
                raise(ValueError(f"{str(e)[:-1]} for design_var '{name}'."))

        # Convert ref/ref0 to ndarray/float as necessary
        ref = format_as_float_or_array('ref', ref, val_if_none=None, flatten=True)
        ref0 = format_as_float_or_array('ref0', ref0, val_if_none=None, flatten=True)

        # determine adder and scaler based on args
        adder, scaler = determine_adder_scaler(ref0, ref, adder, scaler)

        if lower is None:
            # if not set, set lower to -INF_BOUND and don't apply adder/scaler
            lower = -INF_BOUND
        else:
            # Convert lower to ndarray/float as necessary
            lower = format_as_float_or_array('lower', lower, flatten=True)
            # Apply scaler/adder
            lower = (lower + adder) * scaler

        if upper is None:
            # if not set, set upper to INF_BOUND and don't apply adder/scaler
            upper = INF_BOUND
        else:
            # Convert upper to ndarray/float as necessary
            upper = format_as_float_or_array('upper', upper, flatten=True)
            # Apply scaler/adder
            upper = (upper + adder) * scaler

        if self._static_mode:
            design_vars = self._static_design_vars
        else:
            design_vars = self._design_vars

        dv = OrderedDict()

        if isinstance(scaler, np.ndarray):
            if np.all(scaler == 1.0):
                scaler = None
        elif scaler == 1.0:
            scaler = None
        dv['scaler'] = scaler

        if isinstance(adder, np.ndarray):
            if not np.any(adder):
                adder = None
        elif adder == 0.0:
            adder = None
        dv['adder'] = adder

        dv['name'] = name
        dv['upper'] = upper
        dv['lower'] = lower
        dv['ref'] = ref
        dv['ref0'] = ref0
        dv['units'] = units
        dv['cache_linear_solution'] = cache_linear_solution

        if indices is not None:
            indices, size = self._create_indexer(indices, 'design var', name, flat_src=flat_indices)
            if size is not None:
                dv['size'] = size

        dv['indices'] = indices
        dv['flat_indices'] = flat_indices
        dv['parallel_deriv_color'] = parallel_deriv_color

        design_vars[name] = dv

    def add_response(self, name, type_, lower=None, upper=None, equals=None,
                     ref=None, ref0=None, indices=None, index=None, units=None,
                     adder=None, scaler=None, linear=False, parallel_deriv_color=None,
                     cache_linear_solution=False, flat_indices=None):
        r"""
        Add a response variable to this system.

        The response can be scaled using ref and ref0.
        The argument :code:`ref0` represents the physical value when the scaled value is 0.
        The argument :code:`ref` represents the physical value when the scaled value is 1.

        Parameters
        ----------
        name : str
            Name of the response variable in the system.
        type_ : str
            The type of response. Supported values are 'con' and 'obj'.
        lower : float or ndarray, optional
            Lower boundary for the variable.
        upper : upper or ndarray, optional
            Upper boundary for the variable.
        equals : equals or ndarray, optional
            Equality constraint value for the variable.
        ref : float or ndarray, optional
            Value of response variable that scales to 1.0 in the driver.
        ref0 : upper or ndarray, optional
            Value of response variable that scales to 0.0 in the driver.
        indices : sequence of int, optional
            If variable is an array, these indicate which entries are of
            interest for this particular response.
        index : int, optional
            If variable is an array, this indicates which entry is of
            interest for this particular response.
        units : str, optional
            Units to convert to before applying scaling.
        adder : float or ndarray, optional
            Value to add to the model value to get the scaled value for the driver. adder
            is first in precedence.  adder and scaler are an alterantive to using ref
            and ref0.
        scaler : float or ndarray, optional
            Value to multiply the model value to get the scaled value for the driver. scaler
            is second in precedence. adder and scaler are an alterantive to using ref
            and ref0.
        linear : bool
            Set to True if constraint is linear. Default is False.
        parallel_deriv_color : str
            If specified, this design var will be grouped for parallel derivative
            calculations with other variables sharing the same parallel_deriv_color.
        cache_linear_solution : bool
            If True, store the linear solution vectors for this variable so they can
            be used to start the next linear solution with an initial guess equal to the
            solution from the previous linear solve.
        flat_indices : bool
            If True, interpret specified indices as being indices into a flat source array.
        """
        # Name must be a string
        if not isinstance(name, str):
            raise TypeError('{}: The name argument should be a string, '
                            'got {}'.format(self.msginfo, name))

        # Type must be a string and one of 'con' or 'obj'
        if not isinstance(type_, str):
            raise TypeError('{}: The type argument should be a string'.format(self.msginfo))
        elif type_ not in ('con', 'obj'):
            raise ValueError('{}: The type must be one of \'con\' or \'obj\': '
                             'Got \'{}\' instead'.format(self.msginfo, name))

        if units is not None:
            if not isinstance(units, str):
                raise TypeError(f"{self.msginfo}: The units argument should be a str or None for "
                                f"response '{name}'.")
            try:
                units = simplify_unit(units, msginfo=self.msginfo)
            except ValueError as e:
                raise(ValueError(f"{str(e)[:-1]} for response '{name}'."))

        typemap = {'con': 'Constraint', 'obj': 'Objective'}
        if name in self._responses or name in self._static_responses:
            msg = "{}: {} '{}' already exists.".format(self.msginfo, typemap[type_], name)
            raise RuntimeError(msg.format(name))

        # Convert ref/ref0 to ndarray/float as necessary
        ref = format_as_float_or_array('ref', ref, val_if_none=None, flatten=True)
        ref0 = format_as_float_or_array('ref0', ref0, val_if_none=None, flatten=True)

        # determine adder and scaler based on args
        adder, scaler = determine_adder_scaler(ref0, ref, adder, scaler)

        # A constraint cannot be an equality and inequality constraint
        if equals is not None and (lower is not None or upper is not None):
            msg = "{}: Constraint '{}' cannot be both equality and inequality."
            raise ValueError(msg.format(self.msginfo, name))

        if self._static_mode:
            responses = self._static_responses
        else:
            responses = self._responses

        resp = OrderedDict()

        if type_ == 'con':

            # Convert lower to ndarray/float as necessary
            try:
                if lower is None:
                    # don't apply adder/scaler if lower not set
                    lower = -INF_BOUND
                else:
                    lower = format_as_float_or_array('lower', lower, flatten=True)
                    lower = (lower + adder) * scaler
            except (TypeError, ValueError):
                raise TypeError("Argument 'lower' can not be a string ('{}' given). You can not "
                                "specify a variable as lower bound. You can only provide constant "
                                "float values".format(lower))

            # Convert upper to ndarray/float as necessary
            try:
                if upper is None:
                    # don't apply adder/scaler if upper not set
                    upper = INF_BOUND
                else:
                    upper = format_as_float_or_array('upper', upper, flatten=True)
                    upper = (upper + adder) * scaler
            except (TypeError, ValueError):
                raise TypeError("Argument 'upper' can not be a string ('{}' given). You can not "
                                "specify a variable as upper bound. You can only provide constant "
                                "float values".format(upper))
            # Convert equals to ndarray/float as necessary
            if equals is not None:
                try:
                    equals = format_as_float_or_array('equals', equals, flatten=True)
                except (TypeError, ValueError):
                    raise TypeError("Argument 'equals' can not be a string ('{}' given). You can "
                                    "not specify a variable as equals bound. You can only provide "
                                    "constant float values".format(equals))
                equals = (equals + adder) * scaler

            resp['lower'] = lower
            resp['upper'] = upper
            resp['equals'] = equals
            resp['linear'] = linear
            if indices is not None:
                indices, size = self._create_indexer(indices, resp_types[type_], name,
                                                     flat_src=flat_indices)
                if size is not None:
                    resp['size'] = size
            resp['indices'] = indices
        else:  # 'obj'
            if index is not None:
                if not isinstance(index, Integral):
                    raise TypeError(f"{self.msginfo}: index must be of integral type, but type is "
                                    f"{type(index).__name__}")
                index = indexer(index, flat_src=flat_indices)
                resp['size'] = 1
            resp['indices'] = index

        if isinstance(scaler, np.ndarray):
            if np.all(scaler == 1.0):
                scaler = None
        elif scaler == 1.0:
            scaler = None
        resp['scaler'] = scaler

        if isinstance(adder, np.ndarray):
            if not np.any(adder):
                adder = None
        elif adder == 0.0:
            adder = None
        resp['adder'] = adder

        resp['name'] = name
        resp['ref'] = ref
        resp['ref0'] = ref0
        resp['type'] = type_
        resp['units'] = units
        resp['cache_linear_solution'] = cache_linear_solution
        resp['parallel_deriv_color'] = parallel_deriv_color
        resp['flat_indices'] = flat_indices

        responses[name] = resp

    def add_constraint(self, name, lower=None, upper=None, equals=None,
                       ref=None, ref0=None, adder=None, scaler=None, units=None,
                       indices=None, linear=False, parallel_deriv_color=None,
                       cache_linear_solution=False, flat_indices=False):
        r"""
        Add a constraint variable to this system.

        Parameters
        ----------
        name : str
            Name of the response variable in the system.
        lower : float or ndarray, optional
            Lower boundary for the variable.
        upper : float or ndarray, optional
            Upper boundary for the variable.
        equals : float or ndarray, optional
            Equality constraint value for the variable.
        ref : float or ndarray, optional
            Value of response variable that scales to 1.0 in the driver.
        ref0 : float or ndarray, optional
            Value of response variable that scales to 0.0 in the driver.
        adder : float or ndarray, optional
            Value to add to the model value to get the scaled value for the driver. adder
            is first in precedence.  adder and scaler are an alterantive to using ref
            and ref0.
        scaler : float or ndarray, optional
            Value to multiply the model value to get the scaled value for the driver. scaler
            is second in precedence. adder and scaler are an alterantive to using ref
            and ref0.
        units : str, optional
            Units to convert to before applying scaling.
        indices : sequence of int, optional
            If variable is an array, these indicate which entries are of
            interest for this particular response.  These may be positive or
            negative integers.
        linear : bool
            Set to True if constraint is linear. Default is False.
        parallel_deriv_color : str
            If specified, this design var will be grouped for parallel derivative
            calculations with other variables sharing the same parallel_deriv_color.
        cache_linear_solution : bool
            If True, store the linear solution vectors for this variable so they can
            be used to start the next linear solution with an initial guess equal to the
            solution from the previous linear solve.
        flat_indices : bool
            If True, interpret specified indices as being indices into a flat source array.

        Notes
        -----
        The response can be scaled using ref and ref0.
        The argument :code:`ref0` represents the physical value when the scaled value is 0.
        The argument :code:`ref` represents the physical value when the scaled value is 1.
        The arguments (:code:`lower`, :code:`upper`, :code:`equals`) can not be strings or variable
        names.
        """
        self.add_response(name=name, type_='con', lower=lower, upper=upper,
                          equals=equals, scaler=scaler, adder=adder, ref=ref,
                          ref0=ref0, indices=indices, linear=linear, units=units,
                          parallel_deriv_color=parallel_deriv_color,
                          cache_linear_solution=cache_linear_solution,
                          flat_indices=flat_indices)

    def add_objective(self, name, ref=None, ref0=None, index=None, units=None,
                      adder=None, scaler=None, parallel_deriv_color=None,
                      cache_linear_solution=False, flat_indices=False):
        r"""
        Add a response variable to this system.

        Parameters
        ----------
        name : str
            Name of the response variable in the system.
        ref : float or ndarray, optional
            Value of response variable that scales to 1.0 in the driver.
        ref0 : float or ndarray, optional
            Value of response variable that scales to 0.0 in the driver.
        index : int, optional
            If variable is an array, this indicates which entry is of
            interest for this particular response. This may be a positive
            or negative integer.
        units : str, optional
            Units to convert to before applying scaling.
        adder : float or ndarray, optional
            Value to add to the model value to get the scaled value for the driver. adder
            is first in precedence.  adder and scaler are an alterantive to using ref
            and ref0.
        scaler : float or ndarray, optional
            Value to multiply the model value to get the scaled value for the driver. scaler
            is second in precedence. adder and scaler are an alterantive to using ref
            and ref0.
        parallel_deriv_color : str
            If specified, this design var will be grouped for parallel derivative
            calculations with other variables sharing the same parallel_deriv_color.
        cache_linear_solution : bool
            If True, store the linear solution vectors for this variable so they can
            be used to start the next linear solution with an initial guess equal to the
            solution from the previous linear solve.
        flat_indices : bool
            If True, interpret specified indices as being indices into a flat source array.

        Notes
        -----
        The objective can be scaled using scaler and adder, where

        .. math::

            x_{scaled} = scaler(x + adder)

        or through the use of ref/ref0, which map to scaler and adder through
        the equations:

        .. math::

            0 = scaler(ref_0 + adder)

            1 = scaler(ref + adder)

        which results in:

        .. math::

            adder = -ref_0

            scaler = \frac{1}{ref + adder}
        """
        if index is not None and not isinstance(index, int):
            raise TypeError('{}: If specified, objective index must be '
                            'an int.'.format(self.msginfo))
        self.add_response(name, type_='obj', scaler=scaler, adder=adder,
                          ref=ref, ref0=ref0, index=index, units=units,
                          parallel_deriv_color=parallel_deriv_color,
                          cache_linear_solution=cache_linear_solution,
                          flat_indices=flat_indices)

    def _check_voi_meta_sizes(self, typename, meta, names):
        """
        Check that sizes of named metadata agree with meta['size'].

        Parameters
        ----------
        typename : str
            'design var', 'objective', or 'constraint'
        meta : dict
            Metadata dictionary.
        names : list of str
            The metadata entries at each of these names must match meta['size'].
        """
        if 'size' in meta and meta['size'] is not None:
            size = meta['size']
            for mname in names:
                val = meta[mname]
                if isinstance(val, np.ndarray) and size != val.size:
                    raise ValueError(f"{self.msginfo}: When adding {typename} '{meta['name']}',"
                                     f" {mname} should have size {size} but instead has size "
                                     f"{val.size}.")

    def get_design_vars(self, recurse=True, get_sizes=True, use_prom_ivc=True):
        """
        Get the DesignVariable settings from this system.

        Retrieve all design variable settings from the system and, if recurse
        is True, all of its subsystems.

        Parameters
        ----------
        recurse : bool
            If True, recurse through the subsystems and return the path of
            all design vars relative to the this system.
        get_sizes : bool, optional
            If True, compute the size of each design variable.
        use_prom_ivc : bool
            Translate auto_ivc_names to their promoted input names.

        Returns
        -------
        dict
            The design variables defined in the current system and, if
            recurse=True, its subsystems.
        """
        pro2abs_out = self._var_allprocs_prom2abs_list['output']
        pro2abs_in = self._var_allprocs_prom2abs_list['input']
        model = self._problem_meta['model_ref']()
        conns = model._conn_global_abs_in2out
        abs2meta_out = model._var_allprocs_abs2meta['output']

        # Human readable error message during Driver setup.
        out = OrderedDict()
        try:
            for name, data in self._design_vars.items():
                if 'parallel_deriv_color' in data and data['parallel_deriv_color'] is not None:
                    self._problem_meta['using_par_deriv_color'] = True

                if name in pro2abs_out:

                    # This is an output name, most likely a manual indepvarcomp.
                    abs_name = pro2abs_out[name][0]
                    out[abs_name] = data
                    out[abs_name]['ivc_source'] = abs_name
                    out[abs_name]['distributed'] = \
                        abs_name in abs2meta_out and abs2meta_out[abs_name]['distributed']

                else:  # assume an input name else KeyError

                    # Design variable on an auto_ivc input, so use connected output name.
                    in_abs = pro2abs_in[name][0]
                    ivc_path = conns[in_abs]
                    distrib = ivc_path in abs2meta_out and abs2meta_out[ivc_path]['distributed']
                    if use_prom_ivc:
                        out[name] = data
                        out[name]['ivc_source'] = ivc_path
                        out[name]['distributed'] = distrib
                    else:
                        out[ivc_path] = data
                        out[ivc_path]['ivc_source'] = ivc_path
                        out[ivc_path]['distributed'] = distrib

        except KeyError as err:
            msg = "{}: Output not found for design variable {}."
            raise RuntimeError(msg.format(self.msginfo, str(err)))

        if get_sizes:
            # Size them all
            sizes = model._var_sizes['output']
            abs2idx = model._var_allprocs_abs2idx
            owning_rank = model._owning_rank

            for name, meta in out.items():

                src_name = name
                if meta['ivc_source'] is not None:
                    src_name = meta['ivc_source']

                if 'size' not in meta:
                    if src_name in abs2idx:
                        if meta['distributed']:
                            meta['size'] = sizes[model.comm.rank, abs2idx[src_name]]
                        else:
                            meta['size'] = sizes[owning_rank[src_name], abs2idx[src_name]]
                    else:
                        meta['size'] = 0  # discrete var, don't know size
                meta['size'] = int(meta['size'])  # make default int so will be json serializable

                if src_name in abs2idx:  # var is continuous
                    indices = meta['indices']
                    vmeta = abs2meta_out[src_name]
                    meta['distributed'] = vmeta['distributed']
                    if indices is not None:
                        # Index defined in this design var.
                        # update src shapes for Indexer objects
                        indices.set_src_shape(vmeta['global_shape'])
                        indices = indices.shaped_instance()
                        meta['size'] = meta['global_size'] = indices.indexed_src_size
                    else:
                        meta['global_size'] = vmeta['global_size']

                    self._check_voi_meta_sizes('design var', meta,
                                               ['ref', 'ref0', 'scaler', 'adder', 'upper', 'lower'])
                else:
                    meta['global_size'] = 0  # discrete var

        if recurse:
            abs2prom_in = self._var_allprocs_abs2prom['input']
            for subsys in self._subsystems_myproc:
                dvs = subsys.get_design_vars(recurse=recurse, get_sizes=get_sizes,
                                             use_prom_ivc=use_prom_ivc)
                if use_prom_ivc:
                    # have to promote subsystem prom name to this level
                    sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input']
                    for dv, meta in dvs.items():
                        if dv in sub_pro2abs_in:
                            abs_dv = sub_pro2abs_in[dv][0]
                            out[abs2prom_in[abs_dv]] = meta
                        else:
                            out[dv] = meta
                else:
                    out.update(dvs)

            if self.comm.size > 1 and self._subsystems_allprocs:
                my_out = out
                allouts = self.comm.allgather(out)
                out = OrderedDict()
                for rank, all_out in enumerate(allouts):
                    for name, meta in all_out.items():
                        if name not in out:
                            if name in my_out:
                                out[name] = my_out[name]
                            else:
                                out[name] = meta

        if out and self is model:
            for var in out:
                if var in abs2meta_out:
                    if "openmdao:allow_desvar" not in abs2meta_out[var]['tags']:
                        raise RuntimeError(f"Design variable '{var}' is not an IndepVarComp or "
                                           f"ImplicitComp output.")

        return out

    def get_responses(self, recurse=True, get_sizes=True, use_prom_ivc=False):
        """
        Get the response variable settings from this system.

        Retrieve all response variable settings from the system as a dict,
        keyed by variable name.

        Parameters
        ----------
        recurse : bool, optional
            If True, recurse through the subsystems and return the path of
            all responses relative to the this system.
        get_sizes : bool, optional
            If True, compute the size of each response.
        use_prom_ivc : bool
            Translate auto_ivc_names to their promoted input names.

        Returns
        -------
        dict
            The responses defined in the current system and, if
            recurse=True, its subsystems.
        """
        prom2abs_out = self._var_allprocs_prom2abs_list['output']
        prom2abs_in = self._var_allprocs_prom2abs_list['input']
        model = self._problem_meta['model_ref']()
        conns = model._conn_global_abs_in2out
        abs2meta_out = model._var_allprocs_abs2meta['output']

        # Human readable error message during Driver setup.
        try:
            out = {}
            for name, data in self._responses.items():
                if 'parallel_deriv_color' in data and data['parallel_deriv_color'] is not None:
                    self._problem_meta['using_par_deriv_color'] = True

                if name in prom2abs_out:
                    abs_name = prom2abs_out[name][0]
                    out[abs_name] = data
                    out[abs_name]['ivc_source'] = abs_name
                    out[abs_name]['distributed'] = \
                        abs_name in abs2meta_out and abs2meta_out[abs_name]['distributed']

                else:
                    # A constraint can be on an auto_ivc input, so use connected
                    # output name.
                    in_abs = prom2abs_in[name][0]
                    ivc_path = conns[in_abs]
                    distrib = ivc_path in abs2meta_out and abs2meta_out[ivc_path]['distributed']
                    if use_prom_ivc:
                        out[name] = data
                        out[name]['ivc_source'] = ivc_path
                        out[name]['distributed'] = distrib
                    else:
                        out[ivc_path] = data
                        out[ivc_path]['ivc_source'] = ivc_path
                        out[ivc_path]['distributed'] = distrib

        except KeyError as err:
            msg = "{}: Output not found for response {}."
            raise RuntimeError(msg.format(self.msginfo, str(err)))

        if get_sizes:
            # Size them all
            sizes = model._var_sizes['output']
            abs2idx = model._var_allprocs_abs2idx
            owning_rank = model._owning_rank
            for response in out.values():
                name = response['ivc_source']

                # Discrete vars
                if name not in abs2idx:
                    response['size'] = response['global_size'] = 0  # discrete var, don't know size
                    continue

                meta = abs2meta_out[name]
                response['distributed'] = meta['distributed']

                if response['indices'] is not None:
                    indices = response['indices']
                    indices.set_src_shape(meta['global_shape'])
                    indices = indices.shaped_instance()
                    response['size'] = response['global_size'] = indices.indexed_src_size
                else:
                    response['size'] = sizes[owning_rank[name], abs2idx[name]]
                    response['global_size'] = meta['global_size']

                self._check_voi_meta_sizes(resp_types[response['type']], response,
                                           resp_size_checks[response['type']])

        if recurse:
            abs2prom_in = self._var_allprocs_abs2prom['input']
            for subsys in self._subsystems_myproc:
                resps = subsys.get_responses(recurse=recurse, get_sizes=get_sizes,
                                             use_prom_ivc=use_prom_ivc)
                if use_prom_ivc:
                    # have to promote subsystem prom name to this level
                    sub_pro2abs_in = subsys._var_allprocs_prom2abs_list['input']
                    for dv, meta in resps.items():
                        if dv in sub_pro2abs_in:
                            abs_resp = sub_pro2abs_in[dv][0]
                            out[abs2prom_in[abs_resp]] = meta
                        else:
                            out[dv] = meta
                else:
                    out.update(resps)

            if self.comm.size > 1 and self._subsystems_allprocs:
                all_outs = self.comm.allgather(out)
                out = OrderedDict()
                for rank, all_out in enumerate(all_outs):
                    out.update(all_out)

        return out

    def get_constraints(self, recurse=True):
        """
        Get the Constraint settings from this system.

        Retrieve the constraint settings for the current system as a dict,
        keyed by variable name.

        Parameters
        ----------
        recurse : bool, optional
            If True, recurse through the subsystems and return the path of
            all constraints relative to the this system.

        Returns
        -------
        dict
            The constraints defined in the current system.
        """
        return OrderedDict((key, response) for (key, response) in
                           self.get_responses(recurse=recurse).items()
                           if response['type'] == 'con')

    def get_objectives(self, recurse=True):
        """
        Get the Objective settings from this system.

        Retrieve all objectives settings from the system as a dict, keyed
        by variable name.

        Parameters
        ----------
        recurse : bool, optional
            If True, recurse through the subsystems and return the path of
            all objective relative to the this system.

        Returns
        -------
        dict
            The objectives defined in the current system.
        """
        return OrderedDict((key, response) for (key, response) in
                           self.get_responses(recurse=recurse).items()
                           if response['type'] == 'obj')

    def run_apply_nonlinear(self):
        """
        Compute residuals.

        This calls _apply_nonlinear, but with the model assumed to be in an unscaled state.
        """
        with self._scaled_context_all():
            self._apply_nonlinear()

    def get_io_metadata(self, iotypes=('input', 'output'), metadata_keys=None,
                        includes=None, excludes=None, tags=(), get_remote=False, rank=None,
                        return_rel_names=True):
        """
        Retrieve metdata for a filtered list of variables.

        Parameters
        ----------
        iotypes : str or iter of str
            Will contain either 'input', 'output', or both.  Defaults to both.
        metadata_keys : iter of str or None
            Names of metadata entries to be retrieved or None, meaning retrieve all
            available 'allprocs' metadata.  If 'val' or 'src_indices' are required,
            their keys must be provided explicitly since they are not found in the 'allprocs'
            metadata and must be retrieved from local metadata located in each process.
        includes : str, iter of str or None
            Collection of glob patterns for pathnames of variables to include. Default is None,
            which includes all variables.
        excludes : str, iter of str or None
            Collection of glob patterns for pathnames of variables to exclude. Default is None.
        tags : str or iter of strs
            User defined tags that can be used to filter what gets listed. Only inputs with the
            given tags will be listed.
            Default is None, which means there will be no filtering based on tags.
        get_remote : bool
            If True, retrieve variables from other MPI processes as well.
        rank : int or None
            If None, and get_remote is True, retrieve values from all MPI process to all other
            MPI processes.  Otherwise, if get_remote is True, retrieve values from all MPI
            processes only to the specified rank.
        return_rel_names : bool
            If True, the names returned will be relative to the scope of this System. Otherwise
            they will be absolute names.

        Returns
        -------
        dict
            A dict of metadata keyed on name, where name is either absolute or relative
            based on the value of the `return_rel_names` arg, and metadata is a dict containing
            entries based on the value of the metadata_keys arg.  Every metadata dict will
            always contain two entries, 'promoted_name' and 'discrete', to indicate a given
            variable's promoted name and whether or not it is discrete.
        """
        prefix = self.pathname + '.' if self.pathname else ''
        rel_idx = len(prefix)

        # Setup any modified subsystems so the metadata dicts are up-to-date.
        if self._problem_meta:
            conf_info = self._problem_meta['config_info']
            if conf_info:
                if self._subsystems_allprocs:
                    conf_info._update_modified_systems(self)
                if self.pathname in conf_info._modified_systems:
                    self._setup_var_data()

        if isinstance(iotypes, str):
            iotypes = (iotypes,)
        if isinstance(includes, str):
            includes = (includes,)
        if isinstance(excludes, str):
            excludes = (excludes,)

        loc2meta = self._var_abs2meta
        all2meta = self._var_allprocs_abs2meta

        dynset = set(('shape', 'size', 'val'))
        gather_keys = {'val', 'src_indices'}
        need_gather = get_remote and self.comm.size > 1
        if metadata_keys is not None:
            keyset = set(metadata_keys)
            try:
                # DEPRECATION: if 'value' in keyset, replace with 'val'
                keyset.remove('value')
                keyset.add('val')
                warn_deprecation(value_deprecated_msg)
            except KeyError:
                pass
            diff = keyset - allowed_meta_names
            if diff:
                raise RuntimeError(f"{self.msginfo}: {sorted(diff)} are not valid metadata entry "
                                   "names.")
        need_local_meta = metadata_keys is not None and len(gather_keys.intersection(keyset)) > 0
        nodyn = metadata_keys is None or keyset.intersection(dynset)

        if need_local_meta:
            metadict = loc2meta
            disc_metadict = self._var_discrete
        else:
            metadict = all2meta
            disc_metadict = self._var_allprocs_discrete
            need_gather = False  # we can get everything from 'allprocs' dict without gathering

        if tags:
            tagset = make_set(tags)

        result = {}

        it = self._var_allprocs_abs2prom if get_remote else self._var_abs2prom

        for iotype in iotypes:
            cont2meta = metadict[iotype]
            disc2meta = disc_metadict[iotype]

            for abs_name, prom in it[iotype].items():
                if not match_prom_or_abs(abs_name, prom, includes, excludes):
                    continue

                rel_name = abs_name[rel_idx:]

                if abs_name in all2meta[iotype]:  # continuous
                    meta = cont2meta[abs_name] if abs_name in cont2meta else None
                    distrib = all2meta[iotype][abs_name]['distributed']
                    if nodyn:
                        a2m = all2meta[iotype][abs_name]
                        if a2m['shape'] is None and (a2m['shape_by_conn'] or a2m['copy_shape']):
                            raise RuntimeError(f"{self.msginfo}: Can't retrieve shape, size, or "
                                               f"value for dynamically sized variable '{prom}' "
                                               "because they aren't known yet.")
                else:  # discrete
                    if need_local_meta:  # use relative name for discretes
                        meta = disc2meta[rel_name] if rel_name in disc2meta else None
                    else:
                        meta = disc2meta[abs_name]
                    distrib = False

                if meta is None:
                    ret_meta = None
                else:
                    if metadata_keys is None:
                        ret_meta = _MetadataDict(meta)
                    else:
                        ret_meta = _MetadataDict()
                        for key in metadata_keys:
                            try:
                                ret_meta[key] = meta[key]
                            except KeyError:
                                ret_meta[key] = 'Unavailable'

                if need_gather:
                    if distrib or abs_name in self._vars_to_gather:
                        if rank is None:
                            allproc_metas = self.comm.allgather(ret_meta)
                        else:
                            allproc_metas = self.comm.gather(ret_meta, root=rank)

                        if rank is None or self.comm.rank == rank:
                            if not ret_meta:
                                ret_meta = _MetadataDict()
                            if distrib:
                                if 'val' in metadata_keys:
                                    # assemble the full distributed value
                                    dist_vals = [m['val'] for m in allproc_metas
                                                 if m is not None and m['val'].size > 0]
                                    if dist_vals:
                                        ret_meta['val'] = np.concatenate(dist_vals)
                                    else:
                                        ret_meta['val'] = np.zeros(0)
                                if 'src_indices' in metadata_keys:
                                    # assemble full src_indices
                                    dist_src_inds = [m['src_indices'] for m in allproc_metas
                                                     if m is not None and m['src_indices'].size > 0]
                                    if dist_src_inds:
                                        ret_meta['src_indices'] = np.concatenate(dist_src_inds)
                                    else:
                                        ret_meta['src_indices'] = np.zeros(0, dtype=INT_DTYPE)

                            elif abs_name in self._vars_to_gather:
                                for m in allproc_metas:
                                    if m is not None:
                                        ret_meta = m
                                        break
                        else:
                            ret_meta = None

                if ret_meta is not None:
                    ret_meta['prom_name'] = prom
                    ret_meta['discrete'] = abs_name not in all2meta

                    vname = rel_name if return_rel_names else abs_name

                    if tags and not tagset & ret_meta['tags']:
                        continue

                    result[vname] = ret_meta

        return result

    def list_inputs(self,
                    val=True,
                    prom_name=False,
                    units=False,
                    shape=False,
                    global_shape=False,
                    desc=False,
                    hierarchical=True,
                    print_arrays=False,
                    tags=None,
                    includes=None,
                    excludes=None,
                    all_procs=False,
                    out_stream=_DEFAULT_OUT_STREAM,
                    values=None,
                    print_min=False,
                    print_max=False):
        """
        Write a list of input names and other optional information to a specified stream.

        Parameters
        ----------
        val : bool, optional
            When True, display/return input values. Default is True.
        prom_name : bool, optional
            When True, display/return the promoted name of the variable.
            Default is False.
        units : bool, optional
            When True, display/return units. Default is False.
        shape : bool, optional
            When True, display/return the shape of the value. Default is False.
        global_shape : bool, optional
            When True, display/return the global shape of the value. Default is False.
        desc : bool, optional
            When True, display/return description. Default is False.
        hierarchical : bool, optional
            When True, human readable output shows variables in hierarchical format.
        print_arrays : bool, optional
            When False, in the columnar display, just display norm of any ndarrays with size > 1.
            The norm is surrounded by vertical bars to indicate that it is a norm.
            When True, also display full values of the ndarray below the row. Format is affected
            by the values set with numpy.set_printoptions
            Default is False.
        tags : str or list of strs
            User defined tags that can be used to filter what gets listed. Only inputs with the
            given tags will be listed.
            Default is None, which means there will be no filtering based on tags.
        includes : None, str, or iter of str
            Collection of glob patterns for pathnames of variables to include. Default is None,
            which includes all input variables.
        excludes : None, str, or iter of str
            Collection of glob patterns for pathnames of variables to exclude. Default is None.
        all_procs : bool, optional
            When True, display output on all ranks. Default is False, which will display
            output only from rank 0.
        out_stream : file-like object
            Where to send human readable output. Default is sys.stdout.
            Set to None to suppress.
        values : bool, optional
            This argument has been deprecated and will be removed in 4.0.
        print_min : bool
            When true, if the input value is an array, print its smallest value.
        print_max : bool
            When true, if the input value is an array, print its largest value.

        Returns
        -------
        list of (name, metadata)
            List of input names and other optional information about those inputs.
        """
        if values is not None:
            warn_deprecation(f"{self.msginfo}: The 'values' argument to 'list_inputs()' is "
                             "deprecated and will be removed in 4.0. Please use 'val' instead.")
        elif not val and values:
            values = True
        else:
            values = val

        if self._problem_meta['setup_status'] < _SetupStatus.POST_FINAL_SETUP:
            issue_warning("Calling `list_inputs` before `final_setup` will only "
                          "display the default values of variables and will not show the result of "
                          "any `set_val` calls.")

        metavalues = values and self._inputs is None

        keynames = ['val', 'units', 'shape', 'global_shape', 'desc', 'tags']
        keyvals = [metavalues, units, shape, global_shape, desc, tags is not None]
        keys = [n for i, n in enumerate(keynames) if keyvals[i]]

        inputs = self.get_io_metadata(('input',), keys, includes, excludes, tags,
                                      get_remote=True,
                                      rank=None if all_procs or val else 0,
                                      return_rel_names=False)

        if inputs:
            to_remove = ['discrete']
            if tags:
                to_remove.append('tags')
            if not prom_name:
                to_remove.append('prom_name')
            for _, meta in inputs.items():
                for key in to_remove:
                    del meta[key]

        if values and self._inputs is not None:
            # we want value from the input vector, not from the metadata
            print_options = np.get_printoptions()
            np_precision = print_options['precision']

            for n, meta in inputs.items():
                meta['val'] = self._abs_get_val(n, get_remote=True,
                                                rank=None if all_procs else 0, kind='input')
                if isinstance(meta['val'], np.ndarray):
                    if print_min:
                        meta['min'] = np.round(np.min(meta['val']), np_precision)

                    if print_max:
                        meta['max'] = np.round(np.max(meta['val']), np_precision)

        if not inputs or (not all_procs and self.comm.rank != 0):
            return []

        if out_stream:
            self._write_table('input', inputs, hierarchical, print_arrays, all_procs,
                              out_stream)

        if self.pathname:
            # convert to relative names
            rel_idx = len(self.pathname) + 1
            inputs = [(n[rel_idx:], meta) for n, meta in inputs.items()]
        else:
            inputs = list(inputs.items())

        return inputs

    def list_outputs(self,
                     explicit=True, implicit=True,
                     val=True,
                     prom_name=False,
                     residuals=False,
                     residuals_tol=None,
                     units=False,
                     shape=False,
                     global_shape=False,
                     bounds=False,
                     scaling=False,
                     desc=False,
                     hierarchical=True,
                     print_arrays=False,
                     tags=None,
                     includes=None,
                     excludes=None,
                     all_procs=False,
                     list_autoivcs=False,
                     out_stream=_DEFAULT_OUT_STREAM,
                     values=None,
                     print_min=False,
                     print_max=False):
        """
        Write a list of output names and other optional information to a specified stream.

        Parameters
        ----------
        explicit : bool, optional
            Include outputs from explicit components. Default is True.
        implicit : bool, optional
            Include outputs from implicit components. Default is True.
        val : bool, optional
            When True, display output values. Default is True.
        prom_name : bool, optional
            When True, display the promoted name of the variable.
            Default is False.
        residuals : bool, optional
            When True, display residual values. Default is False.
        residuals_tol : float, optional
            If set, limits the output of list_outputs to only variables where
            the norm of the resids array is greater than the given 'residuals_tol'.
            Default is None.
        units : bool, optional
            When True, display units. Default is False.
        shape : bool, optional
            When True, display/return the shape of the value. Default is False.
        global_shape : bool, optional
            When True, display/return the global shape of the value. Default is False.
        bounds : bool, optional
            When True, display/return bounds (lower and upper). Default is False.
        scaling : bool, optional
            When True, display/return scaling (ref, ref0, and res_ref). Default is False.
        desc : bool, optional
            When True, display/return description. Default is False.
        hierarchical : bool, optional
            When True, human readable output shows variables in hierarchical format.
        print_arrays : bool, optional
            When False, in the columnar display, just display norm of any ndarrays with size > 1.
            The norm is surrounded by vertical bars to indicate that it is a norm.
            When True, also display full values of the ndarray below the row. Format  is affected
            by the values set with numpy.set_printoptions
            Default is False.
        tags : str or list of strs
            User defined tags that can be used to filter what gets listed. Only outputs with the
            given tags will be listed.
            Default is None, which means there will be no filtering based on tags.
        includes : None, str, or iter of str
            Collection of glob patterns for pathnames of variables to include. Default is None,
            which includes all output variables.
        excludes : None, str, or iter of str
            Collection of glob patterns for pathnames of variables to exclude. Default is None.
        all_procs : bool, optional
            When True, display output on all processors. Default is False.
        list_autoivcs : bool
            If True, include auto_ivc outputs in the listing.  Defaults to False.
        out_stream : file-like
            Where to send human readable output. Default is sys.stdout.
            Set to None to suppress.
        values : bool, optional
            This argument has been deprecated and will be removed in 4.0.
        print_min : bool
            When true, if the output value is an array, print its smallest value.
        print_max : bool
            When true, if the output value is an array, print its largest value.

        Returns
        -------
        list of (name, metadata)
            List of output names and other optional information about those outputs.
        """
        if values is not None:
            warn_deprecation(f"{self.msginfo}: The 'values' argument to 'list_outputs()' is "
                             "deprecated and will be removed in 4.0. Please use 'val' instead.")
        elif not val and values:
            values = True
        else:
            values = val

        keynames = ['val', 'units', 'shape', 'global_shape', 'desc', 'tags']
        keyflags = [values, units, shape, global_shape, desc, tags]

        keys = [name for i, name in enumerate(keynames) if keyflags[i]]

        if bounds:
            keys.extend(('lower', 'upper'))
        if scaling:
            keys.extend(('ref', 'ref0', 'res_ref'))

        outputs = self.get_io_metadata(('output',), keys, includes, excludes, tags,
                                       get_remote=True,
                                       rank=None if all_procs or val or residuals else 0,
                                       return_rel_names=False)

        # filter auto_ivcs if requested
        if outputs and not list_autoivcs:
            outputs = {n: m for n, m in outputs.items() if not n.startswith('_auto_ivc.')}

        # get values & resids
        if self._outputs is not None and (values or residuals or residuals_tol):
            to_remove = []
            print_options = np.get_printoptions()
            np_precision = print_options['precision']

            for name, meta in outputs.items():
                if values:
                    # we want value from the input vector, not from the metadata
                    meta['val'] = self._abs_get_val(name, get_remote=True,
                                                    rank=None if all_procs else 0, kind='output')

                    if isinstance(meta['val'], np.ndarray):
                        if print_min:
                            meta['min'] = np.round(np.min(meta['val']), np_precision)

                        if print_max:
                            meta['max'] = np.round(np.max(meta['val']), np_precision)

                if residuals or residuals_tol:
                    resids = self._abs_get_val(name, get_remote=True,
                                               rank=None if all_procs else 0,
                                               kind='residual')
                    if residuals_tol and np.linalg.norm(resids) < residuals_tol:
                        to_remove.append(name)
                    elif residuals:
                        meta['resids'] = resids

            # remove any outputs that don't pass the residuals_tol filter
            for name in to_remove:
                del outputs[name]

        # NOTE: calls to _abs_get_val() above are collective calls and must be done on all procs
        if not outputs or (not all_procs and self.comm.rank != 0):
            return []

        # remove metadata we don't want to show/return
        to_remove = ['discrete']
        if tags:
            to_remove.append('tags')
        if not prom_name:
            to_remove.append('prom_name')
        for _, meta in outputs.items():
            for key in to_remove:
                del meta[key]

        rel_idx = len(self.pathname) + 1 if self.pathname else 0

        states = set(self._list_states())
        if explicit:
            expl_outputs = {n: m for n, m in outputs.items() if n not in states}
            if out_stream:
                self._write_table('explicit', expl_outputs, hierarchical, print_arrays,
                                  all_procs, out_stream)

            if self.name:  # convert to relative name
                expl_outputs = [(n[rel_idx:], meta) for n, meta in expl_outputs.items()]
            else:
                expl_outputs = list(expl_outputs.items())

        if implicit:
            impl_outputs = {}
            if residuals_tol:
                for n, m in outputs.items():
                    if n in states:
                        if residuals_tol and 'resids' in m:
                            if np.linalg.norm(m['resids']) >= residuals_tol:
                                impl_outputs[n] = m
                        else:
                            impl_outputs[n] = m
            else:
                impl_outputs = {n: m for n, m in outputs.items() if n in states}
            if out_stream:
                self._write_table('implicit', impl_outputs, hierarchical, print_arrays,
                                  all_procs, out_stream)
            if self.name:  # convert to relative name
                impl_outputs = [(n[rel_idx:], meta) for n, meta in impl_outputs.items()]
            else:
                impl_outputs = list(impl_outputs.items())

        if explicit:
            if implicit:
                return expl_outputs + impl_outputs
            return expl_outputs
        elif implicit:
            return impl_outputs
        else:
            raise RuntimeError(self.msginfo +
                               ': You have excluded both Explicit and Implicit components.')

    def _write_table(self, var_type, var_data, hierarchical, print_arrays, all_procs, out_stream):
        """
        Write table of variable names, values, residuals, and metadata to out_stream.

        Parameters
        ----------
        var_type : 'input', 'explicit' or 'implicit'
            Indicates type of variables, input or explicit/implicit output.
        var_data : dict
            dict of name and metadata.
        hierarchical : bool
            When True, human readable output shows variables in hierarchical format.
        print_arrays : bool
            When False, in the columnar display, just display norm of any ndarrays with size > 1.
            The norm is surrounded by vertical bars to indicate that it is a norm.
            When True, also display full values of the ndarray below the row. Format  is affected
            by the values set with numpy.set_printoptions
            Default is False.
        all_procs : bool, optional
            When True, display output on all processors.
        out_stream : file-like object
            Where to send human readable output.
            Set to None to suppress.
        """
        if out_stream is None:
            return

        if self._outputs is None:
            var_list = var_data.keys()
            top_name = self.name
        else:
            inputs = var_type == 'input'
            outputs = not inputs
            var_list = self._get_vars_exec_order(inputs=inputs, outputs=outputs, variables=var_data)
            top_name = self.name if self.name else 'model'

        if all_procs or self.comm.rank == 0:
            write_var_table(self.pathname, var_list, var_type, var_data,
                            hierarchical, top_name, print_arrays, out_stream)

    def _get_vars_exec_order(self, inputs=False, outputs=False, variables=None):
        """
        Get list of variable names in execution order, based on the order subsystems were setup.

        Parameters
        ----------
        outputs : bool, optional
            Get names of output variables. Default is False.
        inputs : bool, optional
            Get names of input variables. Default is False.
        variables : Collection (list or dict)
            Absolute path names of the subset of variables to include.
            If None then all variables will be included. Default is None.

        Returns
        -------
        list
            list of variable names in execution order
        """
        var_list = []

        real_vars = self._var_allprocs_abs2meta
        disc_vars = self._var_allprocs_discrete

        in_or_out = []
        if inputs:
            in_or_out.append('input')
        if outputs:
            in_or_out.append('output')

        if self._subsystems_allprocs:
            for subsys, _ in self._subsystems_allprocs.values():
                prefix = subsys.pathname + '.'
                for io in in_or_out:
                    for var_name in chain(real_vars[io], disc_vars[io]):
                        if variables is None or var_name in variables:
                            if var_name.startswith(prefix):
                                var_list.append(var_name)
        else:
            # For components with no children, self._subsystems_allprocs is empty.
            for io in in_or_out:
                for var_name in chain(real_vars[io], disc_vars[io]):
                    if not variables or var_name in variables:
                        var_list.append(var_name)

        return var_list

    def run_solve_nonlinear(self):
        """
        Compute outputs.

        This calls _solve_nonlinear, but with the model assumed to be in an unscaled state.
        """
        with self._scaled_context_all():
            self._solve_nonlinear()

    def run_apply_linear(self, mode, scope_out=None, scope_in=None):
        """
        Compute jac-vec product.

        This calls _apply_linear, but with the model assumed to be in an unscaled state.

        Parameters
        ----------
        mode : str
            'fwd' or 'rev'.
        scope_out : set or None
            Set of absolute output names in the scope of this mat-vec product.
            If None, all are in the scope.
        scope_in : set or None
            Set of absolute input names in the scope of this mat-vec product.
            If None, all are in the scope.
        """
        with self._scaled_context_all():
            self._apply_linear(None, ContainsAll(), mode, scope_out, scope_in)

    def run_solve_linear(self, mode):
        """
        Apply inverse jac product.

        This calls _solve_linear, but with the model assumed to be in an unscaled state.

        Parameters
        ----------
        mode : str
            'fwd' or 'rev'.
        """
        with self._scaled_context_all():
            self._solve_linear(mode, ContainsAll())

    def run_linearize(self, sub_do_ln=True):
        """
        Compute jacobian / factorization.

        This calls _linearize, but with the model assumed to be in an unscaled state.

        Parameters
        ----------
        sub_do_ln : bool
            Flag indicating if the children should call linearize on their linear solvers.
        """
        with self._scaled_context_all():
            do_ln = self._linear_solver is not None and self._linear_solver._linearize_children()
            self._linearize(self._assembled_jac, sub_do_ln=do_ln)
            if self._linear_solver is not None and sub_do_ln:
                self._linear_solver._linearize()

    def _apply_nonlinear(self):
        """
        Compute residuals. The model is assumed to be in a scaled state.
        """
        pass

    def check_config(self, logger):
        """
        Perform optional error checks.

        Parameters
        ----------
        logger : object
            The object that manages logging output.
        """
        pass

    def _apply_linear(self, jac, rel_systems, mode, scope_in=None, scope_out=None):
        """
        Compute jac-vec product. The model is assumed to be in a scaled state.

        Parameters
        ----------
        jac : Jacobian or None
            If None, use local jacobian, else use assembled jacobian jac.
        rel_systems : set of str
            Set of names of relevant systems based on the current linear solve.
        mode : str
            'fwd' or 'rev'.
        scope_out : set or None
            Set of absolute output names in the scope of this mat-vec product.
            If None, all are in the scope.
        scope_in : set or None
            Set of absolute input names in the scope of this mat-vec product.
            If None, all are in the scope.
        """
        raise NotImplementedError(self.msginfo + ": _apply_linear has not been overridden")

    def _solve_linear(self, mode, rel_systems):
        """
        Apply inverse jac product. The model is assumed to be in a scaled state.

        Parameters
        ----------
        mode : str
            'fwd' or 'rev'.
        rel_systems : set of str
            Set of names of relevant systems based on the current linear solve.
        """
        pass

    def _linearize(self, jac, sub_do_ln=True):
        """
        Compute jacobian / factorization. The model is assumed to be in a scaled state.

        Parameters
        ----------
        jac : Jacobian or None
            If None, use local jacobian, else use assembled jacobian jac.
        sub_do_ln : bool
            Flag indicating if the children should call linearize on their linear solvers.
        """
        pass

    def _list_states(self):
        """
        Return list of all states at and below this system.

        Returns
        -------
        list
            List of all states.
        """
        return []

    def _list_states_allprocs(self):
        """
        Return list of all states at and below this system across all procs.

        Returns
        -------
        list
            List of all states.
        """
        return []

    def add_recorder(self, recorder, recurse=False):
        """
        Add a recorder to the system.

        Parameters
        ----------
        recorder : <CaseRecorder>
           A recorder instance.
        recurse : bool
            Flag indicating if the recorder should be added to all the subsystems.
        """
        if MPI:
            raise RuntimeError(self.msginfo + ": Recording of Systems when running parallel "
                               "code is not supported yet")

        self._rec_mgr.append(recorder)

        if recurse:
            for s in self.system_iter(include_self=False, recurse=recurse):
                s._rec_mgr.append(recorder)

    def record_iteration(self):
        """
        Record an iteration of the current System.
        """
        global _recordable_funcs

        if self._rec_mgr._recorders:
            parallel = self._rec_mgr._check_parallel() if self.comm.size > 1 else False
            options = self.recording_options
            metadata = create_local_meta(self.pathname)

            # Get the data to record
            if self._recording_iter.stack:
                stack_top = self._recording_iter.stack[-1][0]
                method = stack_top.rsplit('.', 1)[-1]

                if method not in _recordable_funcs:
                    raise ValueError(f"{self.msginfo}: {method} must be one of: "
                                     f"{sorted(_recordable_funcs)}")

                if 'nonlinear' in method:
                    inputs, outputs, residuals = self.get_nonlinear_vectors()
                    vec_name = 'nonlinear'
                else:
                    inputs, outputs, residuals = self.get_linear_vectors()
                    vec_name = 'linear'
            else:
                # outside of a run, just record nonlinear vectors
                inputs, outputs, residuals = self.get_nonlinear_vectors()
                vec_name = 'nonlinear'

            discrete_inputs = self._discrete_inputs
            discrete_outputs = self._discrete_outputs
            filt = self._filtered_vars_to_record

            data = {'input': {}, 'output': {}, 'residual': {}}
            if options['record_inputs'] and (inputs._names or len(discrete_inputs) > 0):
                data['input'] = self._retrieve_data_of_kind(filt, 'input', vec_name, parallel)

            if options['record_outputs'] and (outputs._names or len(discrete_outputs) > 0):
                data['output'] = self._retrieve_data_of_kind(filt, 'output', vec_name, parallel)

            if options['record_residuals'] and residuals._names:
                data['residual'] = self._retrieve_data_of_kind(filt, 'residual', vec_name, parallel)

            self._rec_mgr.record_iteration(self, data, metadata)

        # All calls to _solve_nonlinear are recorded, The counter is incremented after recording.
        self.iter_count += 1
        if not self.under_approx:
            self.iter_count_without_approx += 1

    def is_active(self):
        """
        Determine if the system is active on this rank.

        Returns
        -------
        bool
            If running under MPI, returns True if this `System` has a valid
            communicator. Always returns True if not running under MPI.
        """
        return MPI is None or not (self.comm is None or
                                   self.comm == MPI.COMM_NULL)

    def _clear_iprint(self):
        """
        Clear out the iprint stack from the solvers.
        """
        self.nonlinear_solver._solver_info.clear()

    def _reset_iter_counts(self):
        """
        Recursively reset iteration counter for all systems and solvers.
        """
        for s in self.system_iter(include_self=True, recurse=True):
            s.iter_count = 0
            s.iter_count_apply = 0
            s.iter_count_without_approx = 0

            if s._linear_solver:
                s._linear_solver._iter_count = 0
            if s._nonlinear_solver:
                nl = s._nonlinear_solver
                nl._iter_count = 0
                if hasattr(nl, 'linesearch') and nl.linesearch:
                    nl.linesearch._iter_count = 0

    def _set_finite_difference_mode(self, active):
        """
        Turn on or off finite difference mode.

        Recurses to turn on or off finite difference mode in all subsystems.

        Parameters
        ----------
        active : bool
            Finite difference flag; set to True prior to commencing finite difference.
        """
        for sub in self.system_iter(include_self=True, recurse=True):
            sub.under_finite_difference = active

    def _set_complex_step_mode(self, active):
        """
        Turn on or off complex stepping mode.

        Recurses to turn on or off complex stepping mode in all subsystems and their vectors.

        Parameters
        ----------
        active : bool
            Complex mode flag; set to True prior to commencing complex step.
        """
        for sub in self.system_iter(include_self=True, recurse=True):
            sub.under_complex_step = active
            sub._inputs.set_complex_step_mode(active)
            sub._outputs.set_complex_step_mode(active)
            sub._residuals.set_complex_step_mode(active)

            if sub._vectors['output']['linear']._alloc_complex:
                sub._vectors['output']['linear'].set_complex_step_mode(active)
                sub._vectors['input']['linear'].set_complex_step_mode(active)
                sub._vectors['residual']['linear'].set_complex_step_mode(active)

                if sub.linear_solver:
                    sub.linear_solver._set_complex_step_mode(active)

                if sub.nonlinear_solver:
                    sub.nonlinear_solver._set_complex_step_mode(active)

                if sub._owns_approx_jac:
                    sub._jacobian.set_complex_step_mode(active)

                if sub._assembled_jac:
                    sub._assembled_jac.set_complex_step_mode(active)

    def _set_approx_mode(self, active):
        """
        Turn on or off approx mode flag.

        Recurses to turn on or off approx mode flag in all subsystems.

        Parameters
        ----------
        active : bool
            Approx mode flag; set to True prior to commencing approximation.
        """
        for sub in self.system_iter(include_self=True, recurse=True):
            sub.under_approx = active

    def cleanup(self):
        """
        Clean up resources prior to exit.
        """
        # shut down all recorders
        self._rec_mgr.shutdown()

        # do any required cleanup on solvers
        if self._nonlinear_solver:
            self._nonlinear_solver.cleanup()
        if self._linear_solver:
            self._linear_solver.cleanup()

    def _get_partials_varlists(self):
        """
        Get lists of 'of' and 'wrt' variables that form the partial jacobian.

        Returns
        -------
        tuple(list, list)
            'of' and 'wrt' variable lists.
        """
        of = list(self._var_allprocs_prom2abs_list['output'])
        wrt = list(self._var_allprocs_prom2abs_list['input'])

        # filter out any discrete inputs or outputs
        if self._discrete_outputs:
            of = [n for n in of if n not in self._discrete_outputs]
        if self._discrete_inputs:
            wrt = [n for n in wrt if n not in self._discrete_inputs]

        # wrt should include implicit states
        return of, of + wrt

    def _get_gradient_nl_solver_systems(self):
        """
        Return a set of all Systems, including this one, that have a gradient nonlinear solver.

        Returns
        -------
        set
            Set of Systems containing nonlinear solvers that compute gradients.
        """
        return set(s for s in self.system_iter(include_self=True, recurse=True)
                   if s.nonlinear_solver and s.nonlinear_solver.supports['gradients'])

    def _jac_var_info_abs2prom(self, var_info):
        """
        Return a new list with tuples' [0] entry converted from absolute to promoted names.

        Parameters
        ----------
        var_info : list of (name, offset, end, idxs)
            The list that uses absolute names.

        Returns
        -------
        list
            The new list with promoted names.
        """
        new_list = []
        abs2prom_in = self._var_allprocs_abs2prom['input']
        abs2prom_out = self._var_allprocs_abs2prom['output']
        for tup in var_info:
            lst = list(tup)
            if tup[0] in abs2prom_out:
                lst[0] = abs2prom_out[tup[0]]
            else:
                lst[0] = abs2prom_in[tup[0]]
            new_list.append(lst)
        return new_list

    def _abs_get_val(self, abs_name, get_remote=False, rank=None, vec_name=None, kind=None,
                     flat=False, from_root=False):
        """
        Return the value of the variable specified by the given absolute name.

        Parameters
        ----------
        abs_name : str
            The absolute name of the variable.
        get_remote : bool or None
            If True, return the value even if the variable is remote. NOTE: This function must be
            called in all procs in the Problem's MPI communicator.
            If False, only retrieve the value if it is on the current process, or only the part
            of the value that's on the current process for a distributed variable.
            If None and the variable is remote or distributed, a RuntimeError will be raised.
        rank : int or None
            If not None, specifies that the value is to be gathered to the given rank only.
            Otherwise, if get_remote is specified, the value will be broadcast to all procs
            in the MPI communicator.
        vec_name : str
            Name of the vector to use.
        kind : str or None
            Kind of variable ('input', 'output', or 'residual').  If None, returned value
            will be either an input or output.
        flat : bool
            If True, return the flattened version of the value.
        from_root : bool
            If True, resolve variables from top level scope.

        Returns
        -------
        object or None
            The value of the requested output/input/resid variable.  None if variable is not found.
        """
        discrete = distrib = False
        val = _UNDEFINED
        if from_root:
            all_meta = self._problem_meta['model_ref']()._var_allprocs_abs2meta
            my_meta = self._problem_meta['model_ref']()._var_abs2meta
            io = 'output' if abs_name in all_meta['output'] else 'input'
            all_meta = all_meta[io]
            my_meta = my_meta[io]
        else:
            io = 'output' if abs_name in self._var_allprocs_abs2meta['output'] else 'input'
            all_meta = self._var_allprocs_abs2meta[io]
            my_meta = self._var_abs2meta[io]

        vars_to_gather = self._problem_meta['vars_to_gather']

        # if abs_name is non-discrete it should be found in all_meta
        if abs_name in all_meta:
            if get_remote:
                meta = all_meta[abs_name]
                distrib = meta['distributed']
            elif self.comm.size > 1:
                if abs_name in vars_to_gather and vars_to_gather[abs_name] != self.comm.rank:
                    raise RuntimeError(f"{self.msginfo}: Variable '{abs_name}' is not local to "
                                       f"rank {self.comm.rank}. You can retrieve values from "
                                       "other processes using `get_val(<name>, get_remote=True)`.")

                meta = my_meta[abs_name]
                distrib = meta['distributed']
                if distrib and get_remote is None:
                    raise RuntimeError(f"{self.msginfo}: Variable '{abs_name}' is a distributed "
                                       "variable. You can retrieve values from all processes "
                                       "using `get_val(<name>, get_remote=True)` or from the "
                                       "local process using `get_val(<name>, get_remote=False)`.")
        else:
            discrete = True
            relname = abs_name[len(self.pathname) + 1:] if self.pathname else abs_name
            if relname in self._discrete_outputs:
                val = self._discrete_outputs[relname]
            elif relname in self._discrete_inputs:
                val = self._discrete_inputs[relname]
            elif abs_name in self._var_allprocs_discrete['output']:
                pass  # non-local discrete output
            elif abs_name in self._var_allprocs_discrete['input']:
                pass  # non-local discrete input
            elif get_remote:
                raise ValueError(f"{self.msginfo}: Can't find variable named '{abs_name}'.")
            else:
                return _UNDEFINED

        typ = 'output' if abs_name in self._var_allprocs_abs2prom['output'] else 'input'
        if kind is None:
            kind = typ
        if vec_name is None:
            vec_name = 'nonlinear'

        if not discrete:
            try:
                vec = self._vectors[kind][vec_name]
            except KeyError:
                if abs_name in my_meta:
                    if vec_name != 'nonlinear':
                        raise ValueError(f"{self.msginfo}: Can't get variable named '{abs_name}' "
                                         "because linear vectors are not available before "
                                         "final_setup.")
                    val = my_meta[abs_name]['val']
            else:
                if from_root:
                    vec = vec._root_vector
                if vec._contains_abs(abs_name):
                    val = vec._abs_get_val(abs_name, flat)

        if get_remote and (distrib or abs_name in vars_to_gather) and self.comm.size > 1:
            owner = self._owning_rank[abs_name]
            myrank = self.comm.rank
            if rank is None:   # bcast
                if distrib:
                    idx = self._var_allprocs_abs2idx[abs_name]
                    sizes = self._var_sizes[typ][:, idx]
                    # TODO: could cache these offsets
                    offsets = np.zeros(sizes.size, dtype=INT_DTYPE)
                    offsets[1:] = np.cumsum(sizes[:-1])
                    if val is _UNDEFINED:
                        loc_val = np.zeros(sizes[myrank])
                    else:
                        loc_val = np.ascontiguousarray(val)
                    val = np.zeros(np.sum(sizes))
                    self.comm.Allgatherv(loc_val, [val, sizes, offsets, MPI.DOUBLE])
                    if not flat:
                        val.shape = meta['global_shape'] if get_remote else meta['shape']
                else:
                    if owner != self.comm.rank:
                        val = None
                    # TODO: use Bcast if not discrete for speed
                    new_val = self.comm.bcast(val, root=owner)
                    val = new_val
            else:   # retrieve to rank
                if distrib:
                    idx = self._var_allprocs_abs2idx[abs_name]
                    sizes = self._var_sizes[typ][:, idx]
                    # TODO: could cache these offsets
                    offsets = np.zeros(sizes.size, dtype=INT_DTYPE)
                    offsets[1:] = np.cumsum(sizes[:-1])
                    if val is _UNDEFINED:
                        loc_val = np.zeros(sizes[idx])
                    else:
                        loc_val = np.ascontiguousarray(val)
                    val = np.zeros(np.sum(sizes))
                    self.comm.Gatherv(loc_val, [val, sizes, offsets, MPI.DOUBLE], root=rank)
                    if not flat:
                        val.shape = meta['global_shape'] if get_remote else meta['shape']
                else:
                    if rank != owner:
                        tag = self._var_allprocs_abs2idx[abs_name]
                        # avoid tag collisions between inputs, outputs, and resids
                        if kind != 'output':
                            tag += len(self._var_allprocs_abs2meta['output'])
                            if kind == 'residual':
                                tag += len(self._var_allprocs_abs2meta['input'])
                        if self.comm.rank == owner:
                            self.comm.send(val, dest=rank, tag=tag)
                        elif self.comm.rank == rank:
                            val = self.comm.recv(source=owner, tag=tag)

        return val

    def get_val(self, name, units=None, indices=None, get_remote=False, rank=None,
                vec_name='nonlinear', kind=None, flat=False, from_src=True):
        """
        Get an output/input/residual variable.

        Function is used if you want to specify display units.

        Parameters
        ----------
        name : str
            Promoted or relative variable name in the root system's namespace.
        units : str, optional
            Units to convert to before return.
        indices : int or list of ints or tuple of ints or int ndarray or Iterable or None, optional
            Indices or slice to return.
        get_remote : bool or None
            If True, retrieve the value even if it is on a remote process.  Note that if the
            variable is remote on ANY process, this function must be called on EVERY process
            in the Problem's MPI communicator.
            If False, only retrieve the value if it is on the current process, or only the part
            of the value that's on the current process for a distributed variable.
            If None and the variable is remote or distributed, a RuntimeError will be raised.
        rank : int or None
            If not None, only gather the value to this rank.
        vec_name : str
            Name of the vector to use.   Defaults to 'nonlinear'.
        kind : str or None
            Kind of variable ('input', 'output', or 'residual').  If None, returned value
            will be either an input or output.
        flat : bool
            If True, return the flattened version of the value.
        from_src : bool
            If True, retrieve value of an input variable from its connected source.

        Returns
        -------
        object
            The value of the requested output/input variable.
        """
        abs_names = name2abs_names(self, name)
        if not abs_names:
            raise KeyError('{}: Variable "{}" not found.'.format(self.msginfo, name))
        simp_units = simplify_unit(units)

        if from_src:
            conns = self._problem_meta['model_ref']()._conn_global_abs_in2out
        else:
            conns = []
        if from_src and abs_names[0] in conns:  # pull input from source
            src = conns[abs_names[0]]
            if src in self._var_allprocs_abs2prom['output']:
                caller = self
            else:
                # src is outside of this system so get the value from the model
                caller = self._problem_meta['model_ref']()
            return caller._get_input_from_src(name, abs_names, conns, units=simp_units,
                                              indices=indices, get_remote=get_remote, rank=rank,
                                              vec_name='nonlinear', flat=flat, scope_sys=self)
        else:
            val = self._abs_get_val(abs_names[0], get_remote, rank, vec_name, kind, flat)

            if indices is not None:
                val = val[indices]

            if units is not None:
                val = self.convert2units(abs_names[0], val, simp_units)

        return val

    def _get_input_from_src(self, name, abs_ins, conns, units=None, indices=None,
                            get_remote=False, rank=None, vec_name='nonlinear', flat=False,
                            scope_sys=None):
        """
        Given an input name, retrieve the value from its source output.

        Parameters
        ----------
        name : str
            Promoted or relative variable name in the root system's namespace.
        abs_ins : list of str
            List of absolute input names.
        conns : dict
            Mapping of absolute names of each input to its connected output across the whole model.
        units : str, optional
            Units to convert to before return.
        indices : int or list of ints or tuple of ints or int ndarray or Iterable or None, optional
            Indices or slice to return.
        get_remote : bool
            If True, retrieve the value even if it is on a remote process.  Note that if the
            variable is remote on ANY process, this function must be called on EVERY process
            in the Problem's MPI communicator.
            If False, only retrieve the value if it is on the current process, or only the part
            of the value that's on the current process for a distributed variable.
            If None and the variable is remote or distributed, a RuntimeError will be raised.
        rank : int or None
            If not None, only gather the value to this rank.
        vec_name : str
            Name of the vector to use.   Defaults to 'nonlinear'.
        flat : bool
            If True, return the flattened version of the value.
        scope_sys : <System> or None
            If not None, the System where the original get_val was called.  This situation
            happens when get_val is called on an input, and the source connected to that input
            resides in a different scope.

        Returns
        -------
        object
            The value of the requested variable.
        """
        abs_name = abs_ins[0]
        src = conns[abs_name]
        if src in self._var_allprocs_discrete['output']:
            return self._abs_get_val(src, get_remote, rank, vec_name, 'output', flat,
                                     from_root=True)

        if scope_sys is None:
            scope_sys = self

        # if we have multiple promoted inputs that are explicitly connected to an output and units
        # have not been specified, look for group input to disambiguate
        if units is None and len(abs_ins) > 1:
            if abs_name not in self._var_allprocs_discrete['input']:
                # can't get here unless Group because len(abs_ins) always == 1 for comp
                try:
                    units = scope_sys._group_inputs[name][0]['units']
                except (KeyError, IndexError):
                    unit0 = self._var_allprocs_abs2meta['input'][abs_ins[0]]['units']
                    for n in abs_ins[1:]:
                        if unit0 != self._var_allprocs_abs2meta['input'][n]['units']:
                            self._show_ambiguity_msg(name, ('units',), abs_ins)
                            break

        if abs_name in self._var_abs2meta['input']:  # input is local
            vmeta = self._var_abs2meta['input'][abs_name]
            src_indices = vmeta['src_indices']
        else:
            vmeta = self._var_allprocs_abs2meta['input'][abs_name]
            src_indices = None  # FIXME: remote var could have src_indices

        distrib = vmeta['distributed']
        vshape = vmeta['shape']
        vdynshape = vmeta['shape_by_conn']
        has_src_indices = any(self._var_allprocs_abs2meta['input'][n]['has_src_indices']
                              for n in abs_ins)

        # see if we have any 'intermediate' level src_indices when using a promoted name
        if name in scope_sys._var_prom2inds:
            src_shape, inds, _ = scope_sys._var_prom2inds[name]
            if inds is None:
                if len(abs_ins) > 1 or name != abs_ins[0]:  # using a promoted lookup
                    src_indices = None
                    vshape = None
                    has_src_indices = False
            else:
                shp = inds.indexed_src_shape
                src_indices = inds
                has_src_indices = True
                if len(abs_ins) > 1 or name != abs_name:
                    vshape = shp

        if self.comm.size > 1 and get_remote:
            if self.comm.rank == self._owning_rank[abs_name]:
                self.comm.bcast(has_src_indices, root=self.comm.rank)
            else:
                has_src_indices = self.comm.bcast(None, root=self._owning_rank[abs_name])

        model_ref = self._problem_meta['model_ref']()
        smeta = model_ref._var_allprocs_abs2meta['output'][src]
        sdistrib = smeta['distributed']
        dynshape = vdynshape or smeta['shape_by_conn']
        slocal = src in model_ref._var_abs2meta['output']

        if self.comm.size > 1:
            if distrib and get_remote is None:
                raise RuntimeError(f"{self.msginfo}: Variable '{abs_name}' is a distributed "
                                   "variable. You can retrieve values from all processes "
                                   "using `get_val(<name>, get_remote=True)` or from the "
                                   "local process using `get_val(<name>, get_remote=False)`.")

            if sdistrib and not distrib and not get_remote:
                raise RuntimeError(f"{self.msginfo}: Non-distributed variable '{abs_name}' has "
                                   f"a distributed source, '{src}', so you must retrieve its value "
                                   "using 'get_remote=True'.")

        # get value of the source
        val = self._abs_get_val(src, get_remote, rank, vec_name, 'output', flat, from_root=True)

        if has_src_indices:
            if src_indices is None:  # input is remote
                val = np.zeros(0)
            else:
                if distrib and (sdistrib or dynshape or not slocal) and not get_remote:
                    var_idx = self._var_allprocs_abs2idx[src]
                    # sizes for src var in each proc
                    sizes = self._var_sizes['output'][:, var_idx]
                    start = np.sum(sizes[:self.comm.rank])
                    end = start + sizes[self.comm.rank]
                    src_indices = src_indices.shaped_array(copy=True)
                    if np.all(np.logical_and(src_indices >= start, src_indices < end)):
                        if src_indices.size > 0:
                            src_indices = src_indices - start
                        val = val.ravel()[src_indices]
                        fail = 0
                    else:
                        fail = 1
                    if self.comm.allreduce(fail) > 0:
                        raise RuntimeError(f"{self.msginfo}: Can't retrieve distributed variable "
                                           f"'{abs_name}' because its src_indices reference "
                                           "entries from other processes. You can retrieve values "
                                           "from all processes using "
                                           "`get_val(<name>, get_remote=True)`.")
                else:
                    if src_indices._flat_src:
                        val = val.ravel()[src_indices.flat()]
                    else:
                        val = val[src_indices()]

            if get_remote and self.comm.size > 1:
                if distrib:
                    if rank is None:
                        parts = self.comm.allgather(val)
                        parts = [p for p in parts if p.size > 0]
                        val = np.hstack(parts)
                    else:
                        parts = self.comm.gather(val, root=rank)
                        if rank == self.comm.rank:
                            parts = [p for p in parts if p.size > 0]
                            val = np.hstack(parts)
                        else:
                            val = None
                else:  # non-distrib input
                    if self.comm.rank == self._owning_rank[abs_name]:
                        self.comm.bcast(val, root=self.comm.rank)
                    else:
                        val = self.comm.bcast(None, root=self._owning_rank[abs_name])

            if distrib and get_remote:
                val.shape = self._var_allprocs_abs2meta['input'][abs_name]['global_shape']
            elif not flat and val.size > 0:
                val.shape = vshape
        elif vshape is not None:
            val = val.reshape(vshape)

        if indices is not None:
            val = val[indices]

        if units is not None:
            if smeta['units'] is not None:
                try:
                    val = self.convert2units(src, val, units)
                except TypeError:  # just call this to get the right error message
                    self.convert2units(abs_name, val, units)
            else:
                val = self.convert2units(abs_name, val, units)
        elif (vmeta['units'] is not None and smeta['units'] is not None and
                vmeta['units'] != smeta['units']):
            val = self.convert2units(src, val, vmeta['units'])

        return val

    def _retrieve_data_of_kind(self, filtered_vars, kind, vec_name, parallel=False):
        """
        Retrieve variables, either local or remote, in the filtered_vars list.

        Parameters
        ----------
        filtered_vars : dict
            Dictionary containing entries for 'input', 'output', and/or 'residual'.
        kind : str
            Either 'input', 'output', or 'residual'.
        vec_name : str
            Either 'nonlinear' or 'linear'.
        parallel : bool
            If True, recorders are parallel, so only local values should be saved in each proc.

        Returns
        -------
        dict
            Variable values keyed on absolute name.
        """
        prom2abs_in = self._var_allprocs_prom2abs_list['input']
        conns = self._problem_meta['model_ref']()._conn_global_abs_in2out
        vdict = {}
        variables = filtered_vars.get(kind)
        if variables:
            vec = self._vectors[kind][vec_name]
            rank = self.comm.rank
            discrete_vec = () if kind == 'residual' else self._var_discrete[kind]
            offset = len(self.pathname) + 1 if self.pathname else 0

            if self.comm.size == 1:
                get = vec._abs_get_val
                srcget = self._vectors['output'][vec_name]._abs_get_val
                vdict = {}
                if discrete_vec:
                    for n in variables:
                        if vec._contains_abs(n):
                            vdict[n] = get(n, False)
                        elif n[offset:] in discrete_vec:
                            vdict[n] = discrete_vec[n[offset:]]['val']
                        else:
                            ivc_path = conns[prom2abs_in[n][0]]
                            if vec._contains_abs(ivc_path):
                                vdict[ivc_path] = srcget(ivc_path, False)
                            elif ivc_path[offset:] in discrete_vec:
                                vdict[ivc_path] = discrete_vec[ivc_path[offset:]]['val']
                else:
                    for name in variables:
                        if vec._contains_abs(name):
                            vdict[name] = get(name, False)
                        else:
                            ivc_path = conns[prom2abs_in[name][0]]
                            vdict[ivc_path] = srcget(ivc_path, False)
            elif parallel:
                get = self._abs_get_val
                vdict = {}
                if discrete_vec:
                    for name in variables:
                        if vec._contains_abs(name):
                            vdict[name] = get(name, get_remote=True, rank=0,
                                              vec_name=vec_name, kind=kind)
                        elif name[offset:] in discrete_vec and self._owning_rank[name] == rank:
                            vdict[name] = discrete_vec[name[offset:]]['val']
                else:
                    for name in variables:
                        if vec._contains_abs(name):
                            vdict[name] = get(name, get_remote=True, rank=0,
                                              vec_name=vec_name, kind=kind)
                        else:
                            ivc_path = conns[prom2abs_in[name][0]]
                            vdict[name] = get(ivc_path, get_remote=True, rank=0,
                                              vec_name=vec_name, kind='output')
            else:
                io = 'input' if kind == 'input' else 'output'
                meta = self._var_allprocs_abs2meta[io]
                for name in variables:
                    if self._owning_rank[name] == 0 and not meta[name]['distributed']:
                        # if using a serial recorder and rank 0 owns the variable,
                        # use local value on rank 0 and do nothing on other ranks.
                        if rank == 0:
                            if vec._contains_abs(name):
                                vdict[name] = vec._abs_get_val(name, flat=False)
                            elif name[offset:] in discrete_vec:
                                vdict[name] = discrete_vec[name[offset:]]['val']
                    else:
                        vdict[name] = self.get_val(name, get_remote=True, rank=0,
                                                   vec_name=vec_name, kind=kind, from_src=False)

        return vdict

    def convert2units(self, name, val, units):
        """
        Convert the given value to the specified units.

        Parameters
        ----------
        name : str
            Name of the variable.
        val : float or ndarray of float
            The value of the variable.
        units : str
            The units to convert to.

        Returns
        -------
        float or ndarray of float
            The value converted to the specified units.
        """
        base_units = self._get_var_meta(name, 'units')

        if base_units == units:
            return val

        try:
            scale, offset = unit_conversion(base_units, units)
        except Exception:
            msg = "{}: Can't express variable '{}' with units of '{}' in units of '{}'."
            raise TypeError(msg.format(self.msginfo, name, base_units, units))

        return (val + offset) * scale

    def convert_from_units(self, name, val, units):
        """
        Convert the given value from the specified units to those of the named variable.

        Parameters
        ----------
        name : str
            Name of the variable.
        val : float or ndarray of float
            The value of the variable.
        units : str
            The units to convert to.

        Returns
        -------
        float or ndarray of float
            The value converted to the specified units.
        """
        base_units = self._get_var_meta(name, 'units')

        if base_units == units:
            return val

        try:
            scale, offset = unit_conversion(units, base_units)
        except Exception:
            msg = "{}: Can't express variable '{}' with units of '{}' in units of '{}'."
            raise TypeError(msg.format(self.msginfo, name, base_units, units))

        return (val + offset) * scale

    def convert_units(self, name, val, units_from, units_to):
        """
        Wrap the utility convert_units and give a good error message.

        Parameters
        ----------
        name : str
            Name of the variable.
        val : float or ndarray of float
            The value of the variable.
        units_from : str
            The units to convert from.
        units_to : str
            The units to convert to.

        Returns
        -------
        float or ndarray of float
            The value converted to the specified units.
        """
        if units_from == units_to:
            return val

        try:
            scale, offset = unit_conversion(units_from, units_to)
        except Exception:
            raise TypeError(f"{self.msginfo}: Can't set variable '{name}' with units "
                            f"'{units_from}' to value with units '{units_to}'.")

        return (val + offset) * scale

    def _get_var_meta(self, name, key):
        """
        Get metadata for a variable.

        Parameters
        ----------
        name : str
            Variable name (promoted, relative, or absolute) in the root system's namespace.
        key : str
            Key into the metadata dict for the given variable.

        Returns
        -------
        object
            The value stored under key in the metadata dictionary for the named variable.
        """
        if self._problem_meta is not None:
            model_ref = self._problem_meta['model_ref']()
        else:
            model_ref = None

        if model_ref is not None:
            meta_all = model_ref._var_allprocs_abs2meta
            meta_loc = model_ref._var_abs2meta
        else:
            meta_all = self._var_allprocs_abs2meta
            meta_loc = self._var_abs2meta

        meta = None
        if name in meta_all['output']:
            abs_name = name
            meta = meta_all['output'][name]
        elif name in meta_all['input']:
            abs_name = name
            meta = meta_all['input'][name]

        if meta is None:
            abs_name = name2abs_name(self, name)
            if abs_name is not None:
                if abs_name in meta_all['output']:
                    meta = meta_all['output'][abs_name]
                elif abs_name in meta_all['input']:
                    meta = meta_all['input'][abs_name]

        if meta:
            if key in meta:
                return meta[key]
            else:
                # key is either bogus or a key into the local metadata dict
                # (like 'val' or 'src_indices'). If MPI is active, this val may be remote
                # on some procs
                if self.comm.size > 1 and abs_name in self._vars_to_gather:
                    # TODO: fix this
                    # cause a failure in all procs to avoid a hang
                    raise RuntimeError(f"{self.msgifo}: No support yet for retrieving local "
                                       f"metadata key '{key}' from a remote proc.")
                elif abs_name in meta_loc['output']:
                    try:
                        return meta_loc['output'][abs_name][key]
                    except KeyError:
                        raise KeyError(f"{self.msginfo}: Metadata key '{key}' not found for "
                                       f"variable '{name}'.")
                elif abs_name in meta_loc['input']:
                    try:
                        return meta_loc['input'][abs_name][key]
                    except KeyError:
                        raise KeyError(f"{self.msginfo}: Metadata key '{key}' not found for "
                                       f"variable '{name}'.")

        if abs_name is not None:
            if abs_name in self._var_allprocs_discrete['output']:
                meta = self._var_allprocs_discrete['output'][abs_name]
            elif abs_name in self._var_allprocs_discrete['input']:
                meta = self._var_allprocs_discrete['input'][abs_name]

            if meta and key in meta:
                return meta[key]

            rel_idx = len(self.pathname) + 1 if self.pathname else 0
            relname = abs_name[rel_idx:]
            if relname in self._var_discrete['output']:
                meta = self._var_discrete['output'][relname]
            elif relname in self._var_discrete['input']:
                meta = self._var_discrete['input'][relname]

            if meta:
                try:
                    return meta[key]
                except KeyError:
                    raise KeyError(f"{self.msginfo}: Metadata key '{key}' not found for "
                                   f"variable '{name}'.")

        raise KeyError(f"{self.msginfo}: Metadata for variable '{name}' not found.")

    def _resolve_ambiguous_input_meta(self):
        pass

    def get_relevant_vars(self, desvars, responses, mode):
        """
        Find all relevant vars between desvars and responses.

        Both vars are assumed to be outputs (either design vars or responses).

        Parameters
        ----------
        desvars : dict
            Dictionary of design variable metadata.
        responses : dict
            Dictionary of response variable metadata.
        mode : str
            Direction of derivatives, either 'fwd' or 'rev'.

        Returns
        -------
        dict
            Dict of ({'outputs': dep_outputs, 'inputs': dep_inputs}, dep_systems)
            keyed by design vars and responses.
        """
        conns = self._conn_global_abs_in2out
        relevant = defaultdict(dict)

        # Create a hybrid graph with components and all connected vars.  If a var is connected,
        # also connect it to its corresponding component.  This results in a smaller graph
        # (fewer edges) than would be the case for a pure variable graph where all inputs
        # to a particular component would have to be connected to all outputs from that component.
        graph = nx.DiGraph()
        for tgt, src in conns.items():
            if src not in graph:
                graph.add_node(src, type_='out')
            graph.add_node(tgt, type_='in')

            src_sys = src.rsplit('.', 1)[0]
            graph.add_edge(src_sys, src)

            tgt_sys = tgt.rsplit('.', 1)[0]
            graph.add_edge(tgt, tgt_sys)

            graph.add_edge(src, tgt)

        for dv in desvars:
            if dv not in graph:
                graph.add_node(dv, type_='out')
                parts = dv.rsplit('.', 1)
                if len(parts) == 1:
                    system = ''  # this happens when a component is the model
                    graph.add_edge(dv, system)
                else:
                    system = parts[0]
                    graph.add_edge(system, dv)

        for res in responses:
            if res not in graph:
                graph.add_node(res, type_='out')
                parts = res.rsplit('.', 1)
                if len(parts) == 1:
                    system = ''  # this happens when a component is the model
                else:
                    system = parts[0]
                graph.add_edge(system, res)

        nodes = graph.nodes
        grev = graph.reverse(copy=False)
        rescache = {}
        pd_dv_locs = {}  # local nodes dependent on a par deriv desvar
        pd_res_locs = {}  # local nodes dependent on a par deriv response
        pd_common = defaultdict(dict)
        # for each par deriv color, keep list of all local dep nodes for each var
        pd_err_chk = defaultdict(dict)

        for desvar, dvmeta in desvars.items():
            dvset = set(self.all_connected_nodes(graph, desvar))
            parallel_deriv_color = dvmeta.get('parallel_deriv_color')
            if parallel_deriv_color:
                pd_dv_locs[desvar] = set(self.all_connected_nodes(graph, desvar, local=True))
                pd_err_chk[parallel_deriv_color][desvar] = pd_dv_locs[desvar]

            for response, resmeta in responses.items():
                if response not in rescache:
                    rescache[response] = set(self.all_connected_nodes(grev, response))
                    parallel_deriv_color = resmeta.get('parallel_deriv_color')
                    if parallel_deriv_color:
                        pd_res_locs[response] = set(self.all_connected_nodes(grev, response,
                                                                             local=True))
                        pd_err_chk[parallel_deriv_color][response] = pd_res_locs[response]

                common = dvset.intersection(rescache[response])

                if common:
                    dv = conns[desvar] if desvar in conns else desvar
                    r = conns[response] if response in conns else response
                    if desvar in pd_dv_locs and pd_dv_locs[desvar]:
                        pd_common[dv][r] = pd_dv_locs[desvar].intersection(rescache[response])
                    elif response in pd_res_locs and pd_res_locs[response]:
                        pd_common[r][dv] = pd_res_locs[response].intersection(dvset)

                    input_deps = set()
                    output_deps = set()
                    sys_deps = set()
                    for node in common:
                        if 'type_' in nodes[node]:
                            typ = nodes[node]['type_']
                            parts = node.rsplit('.', 1)
                            if len(parts) == 1:
                                system = ''
                            else:
                                system = parts[0]
                            if typ == 'in':  # input var
                                input_deps.add(node)
                                if system not in sys_deps:
                                    sys_deps.update(all_ancestors(system))
                            else:  # output var
                                output_deps.add(node)
                                if system not in sys_deps:
                                    sys_deps.update(all_ancestors(system))

                elif desvar == response:
                    input_deps = set()
                    output_deps = set([response])
                    parts = desvar.rsplit('.', 1)
                    sys_deps = set(all_ancestors('' if len(parts) == 1 else parts[0]))

                if common or desvar == response:
                    desvar = conns[desvar] if desvar in conns else desvar
                    response = conns[response] if response in conns else response
                    if mode != 'rev':  # fwd or auto
                        relevant[desvar][response] = ({'input': input_deps,
                                                       'output': output_deps}, sys_deps)
                    if mode != 'fwd':  # rev or auto
                        relevant[response][desvar] = ({'input': input_deps,
                                                       'output': output_deps}, sys_deps)

                    sys_deps.add('')  # top level Group is always relevant

        rescache = None

        if pd_dv_locs or pd_res_locs:
            # check to make sure we don't have any overlapping dependencies between vars of the
            # same color
            vtype = 'design variable' if mode == 'fwd' else 'response'
            err = (None, None)
            for pdcolor, dct in pd_err_chk.items():
                seen = set()
                for vname, nodes in dct.items():
                    if seen.intersection(nodes):
                        err = (vname, pdcolor)
                        break
                    seen.update(nodes)

            all_errs = self.comm.allgather(err)
            for n, color in all_errs:
                if n is not None:
                    raise RuntimeError(f"{self.msginfo}: {vtype} '{n}' has overlapping dependencies"
                                       f" on the same rank with other {vtype}s in "
                                       f"parallel_deriv_color '{color}'.")

            # we have some parallel deriv colors, so update relevance entries to throw out
            # any dependencies that aren't on the same rank.
            if pd_common:
                for inp, sub in relevant.items():
                    for out, tup in sub.items():
                        meta = tup[0]
                        if inp in pd_common:
                            meta['input'] = meta['input'].intersection(pd_common[inp][out])
                            meta['output'] = meta['output'].intersection(pd_common[inp][out])
                            if out not in meta['output']:
                                meta['input'] = set()
                                meta['output'] = set()

        voi_lists = []
        if mode != 'rev':
            voi_lists.append((desvars, responses))
        if mode != 'fwd':
            voi_lists.append((responses, desvars))

        # now calculate dependencies between each VOI and all other VOIs of the
        # other type, e.g for each input VOI wrt all output VOIs.  This is only
        # done for design vars in fwd mode or responses in rev mode. In auto mode,
        # we combine the results for fwd and rev modes.
        for inputs, outputs in voi_lists:
            for inp in inputs:
                if inp in conns:
                    inp = conns[inp]
                relinp = relevant[inp]
                if relinp:
                    if '@all' in relinp:
                        dct, total_systems = relinp['@all']
                        total_inps = dct['input']
                        total_outs = dct['output']
                    else:
                        total_inps = set()
                        total_outs = set()
                        total_systems = set()

                    for out in outputs:
                        if out in relinp:
                            dct, systems = relinp[out]
                            total_inps.update(dct['input'])
                            total_outs.update(dct['output'])
                            total_systems.update(systems)

                    relinp['@all'] = ({'input': total_inps, 'output': total_outs},
                                      total_systems)
                else:
                    relinp['@all'] = ({'input': set(), 'output': set()}, set())

        return relevant

    def all_connected_nodes(self, graph, start, local=False):
        """
        Yield all downstream nodes starting at the given node.

        Parameters
        ----------
        graph : network.DiGraph
            Graph being traversed.
        start : hashable object
            Identifier of the starting node.
        local : bool
            If True and a non-local node is encountered in the traversal, the traversal
            ends on that branch.

        Yields
        ------
        str
            Each node found when traversal starts at start.
        """
        if local:
            abs2meta_in = self._var_abs2meta['input']
            abs2meta_out = self._var_abs2meta['output']
            all_abs2meta_in = self._var_allprocs_abs2meta['input']
            all_abs2meta_out = self._var_allprocs_abs2meta['output']

            def is_local(name):
                return (name in abs2meta_in or name in abs2meta_out or
                        (name not in all_abs2meta_in and name not in all_abs2meta_out))

        stack = [start]
        visited = set(stack)
        if not local or is_local(start):
            yield start
        else:
            return

        while stack:
            src = stack.pop()
            for tgt in graph[src]:
                if not local or is_local(tgt):
                    yield tgt
                else:
                    continue
                if tgt not in visited:
                    visited.add(tgt)
                    stack.append(tgt)

    def _generate_md5_hash(self):
        """
        Generate an md5 hash for the data structure of this model.

        The hash is generated from an encoded string containing the physical model hiearchy,
        including all component and variable names, and all connection information.

        The hash is used by the n2 viewer to determine if a saved view can be reused. It is not
        intended to accurately track whether a model has been changed, so no options/settings are
        tracked.

        Returns
        -------
        str
            The md5 hash string for the model.
        """
        data = []

        # Model Hierarchy.
        for sys_name in self.system_iter(include_self=True, recurse=True):

            # System name and depth.
            pathname = sys_name.pathname
            if pathname:
                name_parts = pathname.split('.')
                depth = len(name_parts)

                data.append((name_parts[-1], depth))

            else:
                data.append(('model', 0))

            # Local (relative) names for Component inputs and outputs.
            try:
                data.append(sorted(sys_name._var_rel_names['input']))
                data.append(sorted(sys_name._var_rel_names['output']))
            except AttributeError:
                continue

        # All Connections.
        # Note: dictionary can be in any order, so we have to sort.
        for key in sorted(self._conn_global_abs_in2out):
            data.append(self._conn_global_abs_in2out[key])

        return hashlib.md5(str(data).encode()).hexdigest()  # nosec: content not sensitive

    def _get_full_dist_shape(self, abs_name, local_shape):
        """
        Get the full 'distributed' shape for a variable.

        Variable name is absolute and variable is assumed to be continuous.

        Parameters
        ----------
        abs_name : str
            Absolute name of the variable.

        local_shape : tuple
            Local shape of the variable, used in error reporting.

        Returns
        -------
        tuple
            The distributed shape for the given variable.
        """
        io = 'output' if abs_name in self._var_allprocs_abs2meta['output'] else 'input'
        meta = self._var_allprocs_abs2meta[io][abs_name]
        var_idx = self._var_allprocs_abs2idx[abs_name]
        global_size = np.sum(self._var_sizes[io][:, var_idx])

        # assume that all but the first dimension of the shape of a
        # distributed variable is the same on all procs
        high_dims = meta['shape'][1:]
        with multi_proc_exception_check(self.comm):
            if high_dims:
                high_size = np.prod(high_dims)

                dim_size_match = bool(global_size % high_size == 0)
                if dim_size_match is False:
                    raise RuntimeError(f"{self.msginfo}: All but the first dimension of the "
                                       "shape's local parts in a distributed variable must match "
                                       f"across processes. For output '{abs_name}', local shape "
                                       f"{local_shape} in MPI rank {self.comm.rank} has a "
                                       "higher dimension that differs in another rank.")

                dim1 = global_size // high_size
                return tuple([dim1] + list(high_dims))

        return (global_size,)

    def _get_jac_col_scatter(self):
        """
        Return source and target indices for a scatter from the output vector to a jacobian column.

        If the transfer involves remote or distributed variables, the indices will be global.
        Otherwise they will be converted to local.

        Returns
        -------
        ndarray
            Source indices.
        ndarray
            Target indices.
        int
            Size of jacobian column.
        bool
            True if remote or distributed vars are present.
        """
        myrank = self.comm.rank
        nranks = self.comm.size
        owns = self._owning_rank
        abs2idx = self._var_allprocs_abs2idx
        abs2meta = self._var_abs2meta['output']
        sizes = self._var_sizes['output']
        global_offsets = self._get_var_offsets()['output']
        oflist = list(self._jac_of_iter())
        tsize = oflist[-1][2]
        toffset = myrank * tsize
        has_dist_data = False

        sinds = []
        tinds = []

        for name, tstart, tend, jinds, dist_sizes in oflist:
            vind = abs2idx[name]
            if dist_sizes is None:
                if name in abs2meta:
                    owner = myrank
                else:
                    owner = owns[name]
                    has_dist_data = nranks > 1

                voff = global_offsets[owner, vind]
                if jinds is _full_slice:
                    vsize = sizes[owner, vind]
                    sinds.append(range(voff, voff + vsize))
                else:
                    sinds.append(jinds + voff)
                tinds.append(range(tstart + toffset, tend + toffset))
                assert(len(sinds[-1]) == len(tinds[-1]))
            else:  # 'name' refers to a distributed variable
                has_dist_data = nranks > 1
                dtstart = dtend = tstart
                dsstart = dsend = 0
                for rnk, sz in enumerate(dist_sizes):
                    dsend += sz
                    if sz > 0:
                        voff = global_offsets[rnk, vind]
                        if jinds is _full_slice:
                            dtend += sz
                            sinds.append(range(voff, voff + sz))
                            tinds.append(range(toffset + dtstart, toffset + dtend))
                        elif jinds.size > 0:  # jinds is a flat array
                            subinds = jinds[jinds >= dsstart]
                            subinds = subinds[subinds < dsend]
                            if subinds.size > 0:
                                dtend += subinds.size
                                sinds.append(subinds + (voff - dsstart))
                                tinds.append(range(toffset + dtstart, toffset + dtend))
                        dtstart = dtend
                    dsstart = dsend
                assert((len(sinds) == 0 and len(tinds) == 0) or len(sinds[-1]) == len(tinds[-1]))

        sarr = np.array(list(chain(*sinds)), dtype=INT_DTYPE)
        tarr = np.array(list(chain(*tinds)), dtype=INT_DTYPE)

        if not has_dist_data:
            # convert global indices back to local so we can use them to transfer between two
            # local arrays
            sysoffset = np.sum(sizes[:myrank, :])
            sarr -= sysoffset
            tarr -= toffset

        return sarr, tarr, tsize, has_dist_data
