Repository: OpenMDAO/OpenMDAO

Issue:
Set a minimum version for the Bokeh library
### Task Description

The real-time optimization plot feature fails if using Bokeh < 3.4. Here is the error. 


```
ERROR:bokeh.util.tornado:Error thrown from periodic callback:
ERROR:bokeh.util.tornado:Traceback (most recent call last):
  File "/Users/hschilli/miniforge3/envs/openmdao/lib/python3.11/site-packages/tornado/gen.py", line 529, in callback
    result_list.append(f.result())
                       ^^^^^^^^^^
  File "/Users/hschilli/miniforge3/envs/openmdao/lib/python3.11/site-packages/bokeh/server/session.py", line 94, in _needs_document_lock_wrapper
    result = func(self, *args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/hschilli/miniforge3/envs/openmdao/lib/python3.11/site-packages/bokeh/server/session.py", line 226, in with_document_locked
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/hschilli/miniforge3/envs/openmdao/lib/python3.11/site-packages/bokeh/document/callbacks.py", line 485, in wrapper
    return invoke_with_curdoc(doc, invoke)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/hschilli/miniforge3/envs/openmdao/lib/python3.11/site-packages/bokeh/document/callbacks.py", line 443, in invoke_with_curdoc
    return f()
           ^^^
  File "/Users/hschilli/miniforge3/envs/openmdao/lib/python3.11/site-packages/bokeh/document/callbacks.py", line 484, in invoke
    return f(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^
  File "/Users/hschilli/Documents/OpenMDAO/dev/I3403-real-time-plotting/openmdao/visualization/realtime_opt_plot/realtime_opt_plot.py", line 678, in _update
    self._toggles[iline].label = f"{desvar_name} ({units}) {desvar_value.shape}"
    ~~~~~~~~~~~~~^^^^^^^
IndexError: list index out of range

```

Need to set a minimum version in pyproject.toml

Discussion:

Solution patch:
diff --git a/.github/workflows/openmdao_docs_workflow.yml b/.github/workflows/openmdao_docs_workflow.yml
new file mode 100644
index 000000000..4f49c1873
--- /dev/null
+++ b/.github/workflows/openmdao_docs_workflow.yml
@@ -0,0 +1,283 @@
+name: OpenMDAO Docs
+
+on:
+  # Trigger on push or pull request events for the master branch
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+  # Trigger on release, to publish release versioned docs to openmdao.org
+  release:
+    types: [published]
+
+  # Trigger via workflow_dispatch event
+  workflow_dispatch:
+
+
+run-name:  ${{ inputs.run_name }}
+
+permissions: {}
+
+jobs:
+
+  tests:
+
+    timeout-minutes: 120
+
+    strategy:
+      fail-fast: false
+      matrix:
+        include:
+
+          # build docs (baseline versions)
+          - NAME: Build Docs
+            OS: ubuntu-latest
+            PY: '3.11'
+            NUMPY: '1.26'
+            SCIPY: '1.13'
+            PETSc: '3.19'
+            PYOPTSPARSE: 'v2.13.0'
+            SNOPT: '7.7'
+            OPTIONAL: '[all]'
+            PUBLISH: true
+
+    runs-on: ${{ matrix.OS }}
+
+    name: ${{ matrix.NAME }}
+
+    defaults:
+      run:
+        shell: bash -l {0}
+
+    steps:
+      - name: Display run details
+        run: |
+          echo "============================================================="
+          echo "Run #${GITHUB_RUN_NUMBER}"
+          echo "Run ID: ${GITHUB_RUN_ID}"
+          echo "Testing: ${GITHUB_REPOSITORY}"
+          echo "Triggered by: ${GITHUB_EVENT_NAME}"
+          echo "Initiated by: ${GITHUB_ACTOR}"
+          echo "============================================================="
+
+      - name: Create SSH key
+        env:
+          SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}}
+          SSH_KNOWN_HOSTS: ${{secrets.SSH_KNOWN_HOSTS}}
+        run: |
+          mkdir -p ~/.ssh/
+          echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
+          sudo chmod 600 ~/.ssh/id_rsa
+          echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
+
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Setup conda
+        uses: conda-incubator/setup-miniconda@v3
+        with:
+          python-version: ${{ matrix.PY }}
+          conda-remove-defaults: true
+          channels: conda-forge
+
+      - name: Install OpenMDAO
+        run: |
+          conda install numpy=${{ matrix.NUMPY }} scipy=${{ matrix.SCIPY }} -q -y
+
+          python -m pip install --upgrade pip
+
+          echo "============================================================="
+          echo "Install OpenMDAO"
+          echo "============================================================="
+          python -m pip install .${{ matrix.OPTIONAL }}
+
+      - name: Install PETSc
+        if: matrix.PETSc
+        run: |
+          echo "============================================================="
+          echo "Install compilers for PETSc"
+          COMPILERS="cython compilers openmpi-mpicc"
+          conda install $COMPILERS -q -y
+
+          echo "============================================================="
+          echo "Install PETSc"
+          echo "============================================================="
+          conda install mpi4py petsc4py=${{ matrix.PETSc }} -q -y
+
+          echo "============================================================="
+          echo "Check MPI and PETSc installation"
+          echo "============================================================="
+          export PRTE_MCA_rmaps_default_mapping_policy=:oversubscribe
+          export OMPI_MCA_rmaps_base_oversubscribe=1
+          export OMPI_MCA_btl=^openib
+          echo "-----------------------"
+          echo "Quick test of mpi4py:"
+          mpirun -n 3 python -c "from mpi4py import MPI; print(f'Rank: {MPI.COMM_WORLD.rank}')"
+          echo "-----------------------"
+          echo "Quick test of petsc4py:"
+          mpirun -n 3 python -c "import numpy; from mpi4py import MPI; comm = MPI.COMM_WORLD; \
+                                 import petsc4py; petsc4py.init(); \
+                                 x = petsc4py.PETSc.Vec().createWithArray(numpy.ones(5)*comm.rank, comm=comm);  \
+                                 print(x.getArray())"
+          echo "-----------------------"
+
+          echo "export PRTE_MCA_rmaps_default_mapping_policy=:oversubscribe" >> $GITHUB_ENV
+          echo "OMPI_MCA_rmaps_base_oversubscribe=1" >> $GITHUB_ENV
+          echo "OMPI_MCA_btl=^openib" >> $GITHUB_ENV
+
+          echo "Workaround for intermittent failures with OMPI https://github.com/open-mpi/ompi/issues/7393"
+          echo "TMPDIR=/tmp" >> $GITHUB_ENV
+
+      - name: Install pyOptSparse
+        if: matrix.PYOPTSPARSE
+        run: |
+          echo "============================================================="
+          echo "Install pyoptsparse"
+          echo "============================================================="
+          python -m pip install git+https://github.com/OpenMDAO/build_pyoptsparse
+
+          BRANCH="-b ${{ matrix.PYOPTSPARSE }}"
+
+          if [[ "${{ matrix.SNOPT }}" == "7.7" && "${{ secrets.SNOPT_LOCATION_77 }}" ]]; then
+            echo "  > Secure copying SNOPT 7.7 over SSH"
+            mkdir SNOPT
+            scp -qr ${{ secrets.SNOPT_LOCATION_77 }} SNOPT
+            SNOPT="-s SNOPT/src"
+          elif [[ "${{ matrix.SNOPT }}" == "7.2" && "${{ secrets.SNOPT_LOCATION_72 }}" ]]; then
+            echo "  > Secure copying SNOPT 7.2 over SSH"
+            mkdir SNOPT
+            scp -qr ${{ secrets.SNOPT_LOCATION_72 }} SNOPT
+            SNOPT="-s SNOPT/source"
+          elif [[ "${{ matrix.SNOPT }}" ]]; then
+            echo "SNOPT version ${{ matrix.SNOPT }} was requested but source is not available"
+          fi
+
+          build_pyoptsparse -v $BRANCH $SNOPT
+
+      - name: Display environment info
+        id: env_info
+        continue-on-error: true
+        run: |
+          conda info
+          conda list
+
+          echo "============================================================="
+          echo "Check installed versions of Python, Numpy and Scipy"
+          echo "============================================================="
+          echo 'errors<<EOF' >> $GITHUB_OUTPUT
+
+          FINAL_VER=`python -c "import platform; print(platform.python_version())"`
+          if [[ ! "$FINAL_VER" == "${{ matrix.PY }}"* ]]; then
+            echo "Python version was changed from ${{ matrix.PY }} to $FINAL_VER" >> $GITHUB_OUTPUT
+          fi
+
+          FINAL_VER=`python -c "import numpy; print(numpy.__version__)"`
+          if [[ ! "$FINAL_VER" == "${{ matrix.NUMPY }}"* ]]; then
+            echo "NumPy version was changed from ${{ matrix.NUMPY }} to $FINAL_VER" >> $GITHUB_OUTPUT
+          fi
+
+          FINAL_VER=`python -c "import scipy; print(scipy.__version__)"`
+          if [[ ! "$FINAL_VER" == "${{ matrix.SCIPY }}"* ]]; then
+            echo "SciPy version was changed from ${{ matrix.SCIPY }} to $FINAL_VER" >> $GITHUB_OUTPUT
+          fi
+
+          echo 'EOF' >> $GITHUB_OUTPUT
+
+          grep changed $GITHUB_OUTPUT || echo ""
+
+      - name: Build docs
+        id: build_docs
+        run: |
+          export OPENMDAO_REPORTS=0
+          export PYDEVD_DISABLE_FILE_VALIDATION=1
+
+          cd openmdao/docs
+          if [[ "${{ secrets.SNOPT_LOCATION_72 }}" || "${{ secrets.SNOPT_LOCATION_77 }}" ]]; then
+            echo "============================================================="
+            echo "Building docs with SNOPT examples."
+            echo "============================================================="
+          else
+            echo "============================================================="
+            echo "Disabling SNOPT cells in notebooks."
+            echo "============================================================="
+            python openmdao_book/other/disable_snopt_cells.py
+          fi
+
+          # start ipcluster to run MPI under notebooks
+          ./ipcluster_start.sh
+          sleep 12
+
+          echo "============================================================="
+          echo "Build the docs"
+          echo "============================================================="
+          python build_source_docs.py
+          jupyter-book build -W --keep-going openmdao_book
+          python copy_build_artifacts.py
+
+      - name: Display doc build reports
+        if: steps.build_docs.outcome == 'failure'
+        run: |
+          for f in $(find /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/openmdao_book/_build/html/reports -name '*.log'); do
+            echo "============================================================="
+            echo $f
+            echo "============================================================="
+            cat $f
+          done
+
+      - name: Publish docs
+        if: github.event_name != 'pull_request' && matrix.PUBLISH
+        env:
+          DOCS_LOCATION: ${{ secrets.DOCS_LOCATION }}
+        run: |
+          if [[ "${#DOCS_LOCATION}" ]]; then
+            echo "============================================================="
+            echo "Install version of openssl compatible with hosting service"
+            echo "============================================================="
+            conda install -c conda-forge 'openssl=3.0'
+
+            echo "============================================================="
+            echo "Fetch tags to get docs version"
+            echo "============================================================="
+            git fetch --prune --unshallow --tags
+
+            echo "============================================================="
+            echo "Publish docs"
+            echo "============================================================="
+            cd openmdao/docs
+            python upload_doc_version.py openmdao_book/_build/html/ ${{ secrets.DOCS_LOCATION }}
+          else
+            echo "Docs destination not available."
+          fi
+
+      - name: Slack env change
+        if: steps.env_info.outputs.errors != ''
+        uses: act10ns/slack@v2.0.0
+        with:
+          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
+          status: 'warning'
+          message: |
+            Environment change detected on `${{ matrix.NAME }}` build.
+            Python, NumPy or SciPy was not the requested version:
+            ```${{steps.env_info.outputs.errors}}```
+            ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+
+      - name: Slack doc build failure
+        if: steps.build_docs.outcome == 'failure'
+        uses: act10ns/slack@v2.0.0
+        with:
+          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
+          status: ${{ steps.build_docs.outcome }}
+          message: |
+            Doc build failed on `${{ matrix.NAME }}` build.
+            ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+
+      - name: Fail the workflow if doc build failed
+        if: steps.build_docs.outcome == 'failure'
+        uses: actions/github-script@v3
+        with:
+          script: |
+              let docs_fail = ${{ steps.build_docs.outcome == 'failure' }};
+              if (docs_fail) {
+                  core.setFailed('Doc build failed.');
+              }
diff --git a/openmdao/core/component.py b/openmdao/core/component.py
index 49a1cce8c..a56162fb6 100644
--- a/openmdao/core/component.py
+++ b/openmdao/core/component.py
@@ -875,10 +875,11 @@ class Component(System):
             self._has_output_scaling |= np.any(ref0)
             self._has_output_adder |= np.any(ref0)
 
-        if isscalar(res_ref):
-            self._has_resid_scaling |= res_ref != 1.0
-        else:
-            self._has_resid_scaling |= np.any(res_ref != 1.0)
+        if res_ref is not None:
+            if isscalar(res_ref):
+                self._has_resid_scaling |= res_ref != 1.0
+            else:
+                self._has_resid_scaling |= np.any(res_ref != 1.0)
 
         # until we get rid of component level distributed option, handle the case where
         # component distributed has been set to True but variable distributed has been set
diff --git a/openmdao/core/explicitcomponent.py b/openmdao/core/explicitcomponent.py
index a519a3610..506b5209c 100644
--- a/openmdao/core/explicitcomponent.py
+++ b/openmdao/core/explicitcomponent.py
@@ -302,8 +302,8 @@ class ExplicitComponent(Component):
         Compute outputs. The model is assumed to be in a scaled state.
         """
         with Recording(self.pathname + '._solve_nonlinear', self.iter_count, self):
-            with self._unscaled_context(outputs=[self._outputs], residuals=[self._residuals]):
-                self._residuals.set_val(0.0)
+            self._residuals.set_val(0.0)
+            with self._unscaled_context(outputs=[self._outputs]):
                 self._compute_wrapper()
 
             # Iteration counter is incremented in the Recording context manager at exit.
diff --git a/openmdao/core/group.py b/openmdao/core/group.py
index ce31b824d..e4986ffe2 100644
--- a/openmdao/core/group.py
+++ b/openmdao/core/group.py
@@ -135,6 +135,15 @@ class _PromotesInfo(object):
         return mismatches
 
 
+def _chk_scale_factor(factor):
+    try:
+        if factor == (0.0, 1.0):
+            return None
+    except ValueError:
+        pass
+    return factor
+
+
 class Group(System):
     """
     Class used to group systems together; instantiate or inherit.
@@ -417,20 +426,29 @@ class Group(System):
         dict
             Mapping of each absolute 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, 1.0 if res_ref is None else res_ref),
-            }
+        scale_factors = {}
+
+        if self._has_output_scaling or self._has_resid_scaling:
+            for abs_name, meta in self._var_allprocs_abs2meta['output'].items():
+                ref0 = meta['ref0']
+                a0 = ref0
+                a1 = meta['ref'] - ref0
+                tup = _chk_scale_factor((a0, a1))
+                if tup is not None:
+                    scale_factors[abs_name] = {'output': tup}
+
+                res_ref = meta['res_ref']
+                try:
+                    if res_ref == 1.0:
+                        res_ref = None
+                except ValueError:
+                    pass
+
+                if res_ref is not None:
+                    if abs_name not in scale_factors:
+                        scale_factors[abs_name] = {'residual': (0.0, res_ref)}
+                    else:
+                        scale_factors[abs_name]['residual'] = (0.0, res_ref)
 
         # Input scaling for connected inputs is added here.
         # This is a combined scale factor that includes the scaling of the connected source
@@ -444,6 +462,9 @@ class Group(System):
                 ref = meta_out['ref']
                 ref0 = meta_out['ref0']
 
+                units_in = meta_in['units']
+                units_out = meta_out['units']
+
                 src_indices = meta_in['src_indices']
 
                 if src_indices is not None:
@@ -483,28 +504,21 @@ class Group(System):
                 #   b1 = d0 + d1 a1 - d0
                 #   b1 = g(a1) - g(0)
 
-                units_in = meta_in['units']
-                units_out = meta_out['units']
+                a0 = ref0
+                a1 = ref - ref0
 
                 if units_in is None or units_out is None or units_in == units_out:
-                    a0 = ref0
-                    a1 = ref - ref0
-
+                    tup = _chk_scale_factor((a0, a1))
+                    if tup is None:
+                        continue
                     # No unit conversion, only scaling. Just send the scale factors.
-                    scale_factors[abs_in] = {
-                        'input': (a0, a1),
-                    }
-
+                    scale_factors[abs_in] = {'input': tup}
                 else:
                     factor, offset = unit_conversion(units_out, units_in)
-                    a0 = ref0
-                    a1 = ref - ref0
 
                     # Send both unit scaling and solver scaling. Linear input vectors need to
                     # treat them differently in reverse mode.
-                    scale_factors[abs_in] = {
-                        'input': (a0, a1, factor, offset),
-                    }
+                    scale_factors[abs_in] = {'input': (a0, a1, factor, offset)}
 
                     # For adder allocation check.
                     a0 = (ref0 + offset) * factor
@@ -2949,7 +2963,7 @@ class Group(System):
 
                 # if units are defined and different, or if a connected output has any scaling,
                 # we need input scaling.
-                self._has_input_scaling = self._has_output_scaling or self._has_resid_scaling or \
+                self._has_input_scaling = self._has_output_scaling or \
                     (in_units and out_units and in_units != out_units)
 
         # check compatability for any discrete connections
@@ -3102,18 +3116,19 @@ class Group(System):
 
         if mode == 'fwd':
             if xfer is not None:
-                if self._has_input_scaling:
+                if xfer._has_input_scaling:
                     vec_inputs.scale_to_norm()
                     xfer._transfer(vec_inputs, self._vectors['output'][vec_name], mode)
                     vec_inputs.scale_to_phys()
                 else:
                     xfer._transfer(vec_inputs, self._vectors['output'][vec_name], mode)
+
             if self._conn_discrete_in2out and vec_name == 'nonlinear':
                 self._discrete_transfer(sub)
 
         else:  # rev
             if xfer is not None:
-                if self._has_input_scaling:
+                if xfer._has_input_scaling:
                     vec_inputs.scale_to_norm(mode='rev')
 
                 xfer._transfer(vec_inputs, self._vectors['output'][vec_name], mode)
@@ -3124,7 +3139,7 @@ class Group(System):
                         xfer = self._transfers['rev'][key]
                         xfer._transfer(vec_inputs, self._vectors['output'][vec_name], mode)
 
-                if self._has_input_scaling:
+                if xfer._has_input_scaling:
                     vec_inputs.scale_to_phys(mode='rev')
 
     def _discrete_transfer(self, sub):
diff --git a/openmdao/core/problem.py b/openmdao/core/problem.py
index c9fc2d268..6cceb746e 100644
--- a/openmdao/core/problem.py
+++ b/openmdao/core/problem.py
@@ -540,15 +540,12 @@ class Problem(object, metaclass=ProblemMetaclass):
         """
         if self._metadata['setup_status'] <= _SetupStatus.POST_SETUP2:
             abs_names = name2abs_names(self.model, name)
-            if abs_names:
-                val = self.model._get_cached_val(name, abs_names, get_remote=get_remote)
-                if not is_undefined(val):
-                    if indices is not None:
-                        val = val[indices]
-                    if units is not None:
-                        val = self.model.convert2units(name, val, simplify_unit(units))
-            else:
-                raise KeyError(f'{self.model.msginfo}: Variable "{name}" not found.')
+            val = self.model._get_cached_val(name, abs_names, get_remote=get_remote)
+            if not is_undefined(val):
+                if indices is not None:
+                    val = val[indices]
+                if units is not None:
+                    val = self.model.convert2units(name, val, simplify_unit(units))
         else:
             val = self.model.get_val(name, units=units, indices=indices, get_remote=get_remote,
                                      from_src=True)
diff --git a/openmdao/core/system.py b/openmdao/core/system.py
index 47dc0d5e7..75035a5e3 100644
--- a/openmdao/core/system.py
+++ b/openmdao/core/system.py
@@ -952,10 +952,11 @@ class System(object, metaclass=SystemMetaclass):
                     subsys._has_output_adder |= np.any(ref0)
 
                 res_ref = metadata['res_ref']
-                if np.isscalar(res_ref):
-                    subsys._has_resid_scaling |= res_ref != 1.0
-                else:
-                    subsys._has_resid_scaling |= np.any(res_ref != 1.0)
+                if res_ref is not None:
+                    if np.isscalar(res_ref):
+                        subsys._has_resid_scaling |= res_ref != 1.0
+                    else:
+                        subsys._has_resid_scaling |= np.any(res_ref != 1.0)
 
                 if metadata['lower'] is not None or metadata['upper'] is not None:
                     subsys._has_bounds = True
@@ -2385,7 +2386,7 @@ class System(object, metaclass=SystemMetaclass):
                     raise RuntimeError(msg.format(self.msginfo, name, var_units, units))
 
                 # Derivation of the total scaler and total adder for design variables:
-                # Given based design variable value y
+                # Given base design variable value y
                 # First we apply the desired unit conversion
                 # y_in_desired_units = unit_scaler * (y + unit_adder)
                 # Then we apply the user-declared scaling
@@ -5544,8 +5545,6 @@ class System(object, metaclass=SystemMetaclass):
             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:
@@ -5663,24 +5662,21 @@ class System(object, metaclass=SystemMetaclass):
         except AttributeError:
             ginputs = {}  # could happen if this system is not a Group
 
-        if abs_names:
-            n_proms = len(abs_names)  # for output this will never be > 1
-            if n_proms > 1 and name in ginputs:
-                abs_name = ginputs[name][0].get('use_tgt', abs_names[0])
-            else:
-                abs_name = abs_names[0]
-
-            if not has_vectors:
-                has_dyn_shape = []
-                for n in abs_names:
-                    if n in all_meta['input']:
-                        m = all_meta['input'][n]
-                        if 'shape_by_conn' in m and m['shape_by_conn']:
-                            has_dyn_shape.append(True)
-                    else:
-                        has_dyn_shape.append(False)
+        n_proms = len(abs_names)  # for output this will never be > 1
+        if n_proms > 1 and name in ginputs:
+            abs_name = ginputs[name][0].get('use_tgt', abs_names[0])
         else:
-            raise KeyError(f'{model.msginfo}: Variable "{name}" not found.')
+            abs_name = abs_names[0]
+
+        if not has_vectors:
+            has_dyn_shape = []
+            for n in abs_names:
+                if n in all_meta['input']:
+                    m = all_meta['input'][n]
+                    if 'shape_by_conn' in m and m['shape_by_conn']:
+                        has_dyn_shape.append(True)
+                else:
+                    has_dyn_shape.append(False)
 
         set_units = None
 
diff --git a/openmdao/utils/name_maps.py b/openmdao/utils/name_maps.py
index 3d9f7d9cd..8be649005 100644
--- a/openmdao/utils/name_maps.py
+++ b/openmdao/utils/name_maps.py
@@ -1,4 +1,5 @@
 """Maps between promoted/relative/absolute names and name pairs."""
+from difflib import get_close_matches
 
 
 def rel_name2abs_name(system, rel_name):
@@ -203,7 +204,13 @@ def name2abs_names(system, name):
         if absnames[0] in system._var_allprocs_abs2prom['input']:
             return absnames
 
-    return ()
+    guesses = get_close_matches(name, list(system._var_allprocs_prom2abs_list['output'].keys())
+                                + list(system._var_allprocs_prom2abs_list['input'].keys())
+                                + list(system._problem_meta['prom2abs']['output'].keys())
+                                + list(system._problem_meta['prom2abs']['input'].keys()),
+                                n=10, cutoff=0.15)
+    raise KeyError(f"{system.msginfo}: Could not find '{name}'. Perhaps you "
+                   f"meant one of the following variables: {guesses}")
 
 
 def prom_key2abs_key(system, prom_key):
diff --git a/openmdao/vectors/default_transfer.py b/openmdao/vectors/default_transfer.py
index db93b6374..b009fb70e 100644
--- a/openmdao/vectors/default_transfer.py
+++ b/openmdao/vectors/default_transfer.py
@@ -63,7 +63,7 @@ def _setup_index_views(tot_size, in_xfers, out_xfers):
     return full_in, full_out
 
 
-def _setup_index_arrays(tot_size, in_xfers, out_xfers, vectors):
+def _setup_index_arrays(tot_size, in_xfers, out_xfers, vectors, scaled_in_set):
     """
     Create index arrays for all subsystems.
 
@@ -77,6 +77,8 @@ def _setup_index_arrays(tot_size, in_xfers, out_xfers, vectors):
         Mapping of subsystem name to output index arrays.
     vectors : dict
         Dictionary of input and output vectors.
+    scaled_in_set : set
+        Set of subsystems that have input scaling or connect to an input scaled subsystem.
 
     Returns
     -------
@@ -88,7 +90,8 @@ def _setup_index_arrays(tot_size, in_xfers, out_xfers, vectors):
 
     if tot_size > 0:
         xfer_all = DefaultTransfer(vectors['input']['nonlinear'],
-                                   vectors['output']['nonlinear'], xfer_in, xfer_out)
+                                   vectors['output']['nonlinear'], xfer_in, xfer_out,
+                                   len(scaled_in_set) > 0)
     else:
         xfer_all = None
 
@@ -98,7 +101,8 @@ def _setup_index_arrays(tot_size, in_xfers, out_xfers, vectors):
         if inds.size > 0:
             xfer_dict[sname] = DefaultTransfer(vectors['input']['nonlinear'],
                                                vectors['output']['nonlinear'],
-                                               inds, out_xfers[sname])
+                                               inds, out_xfers[sname],
+                                               sname in scaled_in_set)
         else:
             xfer_dict[sname] = None
 
@@ -119,6 +123,8 @@ class DefaultTransfer(Transfer):
         Input indices for the transfer.
     out_inds : int ndarray
         Output indices for the transfer.
+    has_input_scaling : bool
+        Whether any of the inputs has scaling.
     """
 
     @staticmethod
@@ -160,6 +166,9 @@ class DefaultTransfer(Transfer):
 
         tot_size = 0
 
+        scaled_in_set = set()
+        scale_factors = group._problem_meta['model_ref']()._scale_factors
+
         # Loop through all connections owned by this group
         for abs_in, abs_out in group._conn_abs_in2out.items():
             # This weeds out discrete vars (all vars are local if using this Transfer)
@@ -188,18 +197,29 @@ class DefaultTransfer(Transfer):
                 input_inds = range(offsets_in[idx_in], offsets_in[idx_in] + sizes_in[idx_in])
                 tot_size += sizes_in[idx_in]
 
-                # Now the indices are ready - input_inds, output_inds
                 sub_in = abs_in[mypathlen:].split('.', 1)[0]
+
+                if scale_factors is not None and abs_in in scale_factors:
+                    factors = scale_factors[abs_in]
+                    if 'input' in factors:
+                        scaled_in_set.add(sub_in)
+
+                # Now the indices are ready - input_inds, output_inds
                 fwd_xfer_in[sub_in].append(input_inds)
                 fwd_xfer_out[sub_in].append(output_inds)
+
                 if rev and abs_out in abs2meta['output']:
                     sub_out = abs_out[mypathlen:].split('.', 1)[0]
+                    if sub_in in scaled_in_set:
+                        scaled_in_set.add(sub_out)
                     rev_xfer_in[sub_out].append(input_inds)
                     rev_xfer_out[sub_out].append(output_inds)
 
-        transfers['fwd'] = _setup_index_arrays(tot_size, fwd_xfer_in, fwd_xfer_out, vectors)
+        transfers['fwd'] = _setup_index_arrays(tot_size, fwd_xfer_in, fwd_xfer_out, vectors,
+                                               scaled_in_set)
         if rev:
-            transfers['rev'] = _setup_index_arrays(tot_size, rev_xfer_in, rev_xfer_out, vectors)
+            transfers['rev'] = _setup_index_arrays(tot_size, rev_xfer_in, rev_xfer_out, vectors,
+                                                   scaled_in_set)
 
     @staticmethod
     def _setup_discrete_transfers(group):
diff --git a/openmdao/vectors/default_vector.py b/openmdao/vectors/default_vector.py
index 5c8903b27..9f216f1ed 100644
--- a/openmdao/vectors/default_vector.py
+++ b/openmdao/vectors/default_vector.py
@@ -231,30 +231,33 @@ class DefaultVector(Vector):
             if rel_lookup:
                 views_rel[abs_name[relstart:]] = (v, shape == ())
 
-            if do_scaling:
-                factor_tuple = factors[abs_name][kind]
-
-                if len(factor_tuple) == 4:
-                    # Only input vectors can have 4 factors. Linear input vectors need to be able
-                    # to handle the unit and solver scaling in opposite directions in reverse mode.
-                    a0, a1, factor, offset = factor_tuple
-
-                    if islinear:
-                        scale0 = None
-                        scale1 = factor / a1
+            if do_scaling and abs_name in factors:
+                factor = factors[abs_name]
+                if kind in factor:
+                    factor_tuple = factor[kind]
+
+                    if len(factor_tuple) == 4:
+                        # Only input vectors can have 4 factors. Linear input vectors need to be
+                        # able to handle the unit and solver scaling in opposite directions in
+                        # reverse mode.
+                        a0, a1, factor, offset = factor_tuple
+
+                        if islinear:
+                            scale0 = None
+                            scale1 = factor / a1
+                        else:
+                            scale0 = (a0 + offset) * factor
+                            scale1 = a1 * factor
                     else:
-                        scale0 = (a0 + offset) * factor
-                        scale1 = a1 * factor
-                else:
-                    if self._name == 'linear' and self._typ == 'input':
-                        scale0 = None
-                        scale1 = 1.0 / factor_tuple[1]
-                    else:
-                        scale0, scale1 = factor_tuple
-
-                if scaling[0] is not None:
-                    scaling[0][start:end] = scale0
-                scaling[1][start:end] = scale1
+                        if self._name == 'linear' and self._typ == 'input':
+                            scale0 = None
+                            scale1 = 1.0 / factor_tuple[1]
+                        else:
+                            scale0, scale1 = factor_tuple
+
+                    if scaling[0] is not None:
+                        scaling[0][start:end] = scale0
+                    scaling[1][start:end] = scale1
 
             start = end
 
diff --git a/openmdao/vectors/petsc_transfer.py b/openmdao/vectors/petsc_transfer.py
index 6d1c305c4..048fb43d1 100644
--- a/openmdao/vectors/petsc_transfer.py
+++ b/openmdao/vectors/petsc_transfer.py
@@ -37,6 +37,8 @@ else:
             Input indices for the transfer.
         out_inds : int ndarray
             Output indices for the transfer.
+        has_input_scaling : bool
+            Whether any of the inputs has scaling.
         comm : MPI.Comm or <FakeComm>
             Communicator of the system that owns this transfer.
 
@@ -46,11 +48,11 @@ else:
             Method that performs a PETSc scatter.
         """
 
-        def __init__(self, in_vec, out_vec, in_inds, out_inds, comm):
+        def __init__(self, in_vec, out_vec, in_inds, out_inds, has_input_scaling, comm):
             """
             Initialize all attributes.
             """
-            super().__init__(in_vec, out_vec, in_inds, out_inds)
+            super().__init__(in_vec, out_vec, in_inds, out_inds, has_input_scaling)
             in_indexset = PETSc.IS().createGeneral(self._in_inds, comm=comm)
             out_indexset = PETSc.IS().createGeneral(self._out_inds, comm=comm)
 
@@ -97,6 +99,9 @@ else:
 
             total_len = 0
 
+            scaled_in_set = set()
+            scale_factors = group._problem_meta['model_ref']()._scale_factors
+
             # Loop through all connections owned by this system
             for abs_in, abs_out in group._conn_abs_in2out.items():
                 sub_in = abs_in[mypathlen:].partition('.')[0]
@@ -111,6 +116,11 @@ else:
 
                     total_len += len(input_inds)
 
+                    if scale_factors is not None and abs_in in scale_factors:
+                        factors = scale_factors[abs_in]
+                        if 'input' in factors:
+                            scaled_in_set.add(sub_in)
+
                     xfer_in[sub_in].append(input_inds)
                     xfer_out[sub_in].append(output_inds)
                 else:
@@ -124,13 +134,15 @@ else:
                 # full transfer (transfer to all subsystems at once)
                 transfers[None] = PETScTransfer(group._vectors['input']['nonlinear'],
                                                 group._vectors['output']['nonlinear'],
-                                                full_xfer_in, full_xfer_out, group._comm)
+                                                full_xfer_in, full_xfer_out, len(scaled_in_set) > 0,
+                                                group._comm)
 
                 # transfers to individual subsystems
                 for sname, inds in xfer_in.items():
                     transfers[sname] = PETScTransfer(group._vectors['input']['nonlinear'],
                                                      group._vectors['output']['nonlinear'],
-                                                     inds, xfer_out[sname], group._comm)
+                                                     inds, xfer_out[sname],
+                                                     sname in scaled_in_set, group._comm)
 
             return transfers
 
@@ -191,12 +203,20 @@ else:
 
             total_size = total_size_nocolor = 0
 
+            scaled_in_set = set()
+            scale_factors = group._problem_meta['model_ref']()._scale_factors
+
             # Loop through all connections owned by this system
             for abs_in, abs_out in group._conn_abs_in2out.items():
                 sub_out = abs_out[mypathlen:].partition('.')[0]
 
                 # Only continue if the input exists on this processor
                 if abs_in in abs2meta_in:
+                    if scale_factors is not None and abs_in in scale_factors:
+                        factors = scale_factors[abs_in]
+                        if 'input' in factors:
+                            scaled_in_set.add(sub_out)
+
                     meta_in = abs2meta_in[abs_in]
                     idx_in = allprocs_abs2idx[abs_in]
                     idx_out = allprocs_abs2idx[abs_out]
@@ -306,13 +326,15 @@ else:
             transfers = {
                 None: PETScTransfer(vectors['input']['nonlinear'],
                                     vectors['output']['nonlinear'],
-                                    full_xfer_in, full_xfer_out, group._comm)
+                                    full_xfer_in, full_xfer_out, len(scaled_in_set) > 0,
+                                    group._comm)
             }
 
             for sname, inds in xfer_out.items():
                 transfers[sname] = PETScTransfer(vectors['input']['nonlinear'],
                                                  vectors['output']['nonlinear'],
-                                                 xfer_in[sname], inds, group._comm)
+                                                 xfer_in[sname], inds, sname in scaled_in_set,
+                                                 group._comm)
 
             if has_par_coloring:
                 has_nocolor_xfers = group._comm.allreduce(has_nocolor_xfers)
@@ -324,13 +346,15 @@ else:
                     transfers[(None, '@nocolor')] = PETScTransfer(vectors['input']['nonlinear'],
                                                                   vectors['output']['nonlinear'],
                                                                   full_xfer_in, full_xfer_out,
+                                                                  len(scaled_in_set) > 0,
                                                                   group._comm)
 
                     for sname, inds in xfer_out_nocolor.items():
                         transfers[(sname, '@nocolor')] = \
                             PETScTransfer(vectors['input']['nonlinear'],
                                           vectors['output']['nonlinear'],
-                                          xfer_in_nocolor[sname], inds, group._comm)
+                                          xfer_in_nocolor[sname], inds,
+                                          sname in scaled_in_set, group._comm)
 
             return transfers
 
diff --git a/openmdao/vectors/transfer.py b/openmdao/vectors/transfer.py
index f44a26be8..7ffeb3b45 100644
--- a/openmdao/vectors/transfer.py
+++ b/openmdao/vectors/transfer.py
@@ -15,6 +15,8 @@ class Transfer(object):
         Input indices for the transfer.
     out_inds : int ndarray
         Output indices for the transfer.
+    has_input_scaling : bool
+        Whether any of the inputs has scaling.
 
     Attributes
     ----------
@@ -22,14 +24,17 @@ class Transfer(object):
         input indices for the transfer.
     _out_inds : int ndarray
         output indices for the transfer.
+    _has_input_scaling : bool
+        Whether any of the inputs has scaling.
     """
 
-    def __init__(self, in_vec, out_vec, in_inds, out_inds):
+    def __init__(self, in_vec, out_vec, in_inds, out_inds, has_input_scaling):
         """
         Initialize all attributes.
         """
         self._in_inds = in_inds
         self._out_inds = out_inds
+        self._has_input_scaling = has_input_scaling
 
     def __str__(self):
         """
diff --git a/pyproject.toml b/pyproject.toml
index e91931795..ebf5103ed 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -82,7 +82,7 @@ test = [
 ]
 visualization = [
     "panel",
-    "bokeh>=2.4.0",
+    "bokeh>=3.4.0",
     "colorama",
     "matplotlib",
     "rich"


Test patch:
diff --git a/.github/workflows/openmdao_test_workflow.yml b/.github/workflows/openmdao_test_workflow.yml
index 9f7511c40..21d7f9783 100644
--- a/.github/workflows/openmdao_test_workflow.yml
+++ b/.github/workflows/openmdao_test_workflow.yml
@@ -1,4 +1,3 @@
-# Run OpenMDAO Tests
 name: OpenMDAO Tests
 
 on:
@@ -56,12 +55,6 @@ on:
         required: false
         default: false
 
-      Build_Docs:
-        type: boolean
-        description: 'Build docs'
-        required: false
-        default: false
-
       debug_enabled:
         type: boolean
         description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
@@ -95,7 +88,6 @@ jobs:
             OPTIONAL: '[docs,doe,jax,notebooks,numba,test,visualization]'
             BANDIT: true
             PEP517: true
-            TESTS: true
             EXCLUDE: ${{ github.event_name == 'workflow_dispatch'  && ! inputs.Ubuntu_Baseline }}
 
           # test baseline versions on MacOS
@@ -109,7 +101,6 @@ jobs:
             # PAROPT: true
             SNOPT: '7.7'
             OPTIONAL: '[docs,doe,jax,notebooks,numba,test]'
-            TESTS: true
             EXCLUDE: ${{ github.event_name == 'workflow_dispatch'  && ! inputs.MacOS_Baseline }}
 
           # test baseline versions on MacOS/ARM
@@ -123,7 +114,6 @@ jobs:
             # PAROPT: true
             # SNOPT: '7.7'
             OPTIONAL: '[all]'
-            TESTS: true
             EXCLUDE: ${{ github.event_name == 'workflow_dispatch'  && ! inputs.MacOS_ARM }}
 
           # test minimal install
@@ -133,7 +123,6 @@ jobs:
             NUMPY: '1.26'
             SCIPY: '1.14'
             OPTIONAL: '[test]'
-            TESTS: true
             EXCLUDE: ${{ github.event_name == 'workflow_dispatch'  && ! inputs.Ubuntu_Minimal }}
 
           # test oldest supported versions
@@ -148,22 +137,8 @@ jobs:
             PYOPTSPARSE: 'v2.10.2'
             SNOPT: '7.2'
             OPTIONAL: '[all]'
-            TESTS: true
             EXCLUDE: ${{ github.event_name == 'workflow_dispatch'  && ! inputs.Ubuntu_Oldest }}
 
-          # build docs (baseline versions)
-          - NAME: Build Docs
-            OS: ubuntu-latest
-            PY: '3.11'
-            NUMPY: '1.26'
-            SCIPY: '1.13'
-            PETSc: '3.19'
-            PYOPTSPARSE: 'v2.13.0'
-            SNOPT: '7.7'
-            OPTIONAL: '[all]'
-            BUILD_DOCS: true
-            EXCLUDE: ${{ github.event_name == 'workflow_dispatch'  && ! inputs.Build_Docs }}
-
     runs-on: ${{ matrix.OS }}
 
     name: ${{ matrix.NAME }}
@@ -190,7 +165,7 @@ jobs:
           script: core.setFailed('The ${{ matrix.NAME }} job was excluded from the run, exiting...');
 
       - name: Create SSH key
-        if: (matrix.SNOPT || matrix.BUILD_DOCS)
+        if: matrix.SNOPT
         env:
           SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
           SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
@@ -395,7 +370,6 @@ jobs:
 
       - name: Run tests
         id: run_tests
-        if: matrix.TESTS
         env:
           OPENMDAO_CHECK_ALL_PARTIALS: true
         run: |
@@ -411,7 +385,6 @@ jobs:
 
       - name: Submit coverage
         id: coveralls
-        if: matrix.TESTS
         continue-on-error: true
         env:
           COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -436,71 +409,6 @@ jobs:
             Uploading of coverage data to coveralls.io failed.
             ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
 
-      - name: Build docs
-        if: matrix.BUILD_DOCS
-        id: build_docs
-        run: |
-          export OPENMDAO_REPORTS=0
-          export PYDEVD_DISABLE_FILE_VALIDATION=1
-
-          cd openmdao/docs
-          if [[ "${{ secrets.SNOPT_LOCATION_72 }}" || "${{ secrets.SNOPT_LOCATION_77 }}" ]]; then
-            echo "============================================================="
-            echo "Building docs with SNOPT examples."
-            echo "============================================================="
-          else
-            echo "============================================================="
-            echo "Disabling SNOPT cells in notebooks."
-            echo "============================================================="
-            python openmdao_book/other/disable_snopt_cells.py
-          fi
-
-          # start ipcluster to run MPI under notebooks
-          ./ipcluster_start.sh
-          sleep 12
-
-          echo "============================================================="
-          echo "Build the docs"
-          echo "============================================================="
-          python build_source_docs.py
-          jupyter-book build -W --keep-going openmdao_book
-          python copy_build_artifacts.py
-
-      - name: Display doc build reports
-        if: failure() && matrix.BUILD_DOCS && steps.build_docs.outcome == 'failure'
-        run: |
-          for f in $(find /home/runner/work/OpenMDAO/OpenMDAO/openmdao/docs/openmdao_book/_build/html/reports -name '*.log'); do
-            echo "============================================================="
-            echo $f
-            echo "============================================================="
-            cat $f
-          done
-
-      - name: Publish docs
-        if: ${{ github.event_name != 'pull_request' && matrix.BUILD_DOCS }}
-        env:
-          DOCS_LOCATION: ${{ secrets.DOCS_LOCATION }}
-        run: |
-          if [[ "${#DOCS_LOCATION}" ]]; then
-            echo "============================================================="
-            echo "Install version of openssl compatible with hosting service"
-            echo "============================================================="
-            conda install -c conda-forge 'openssl=3.0'
-
-            echo "============================================================="
-            echo "Fetch tags to get docs version"
-            echo "============================================================="
-            git fetch --prune --unshallow --tags
-
-            echo "============================================================="
-            echo "Publish docs"
-            echo "============================================================="
-            cd openmdao/docs
-            python upload_doc_version.py openmdao_book/_build/html/ ${{ secrets.DOCS_LOCATION }}
-          else
-            echo "Docs destination not available."
-          fi
-
       - name: Scan for security issues
         if: matrix.BANDIT
         id: bandit
@@ -553,16 +461,6 @@ jobs:
             Unit testing failed on `${{ matrix.NAME }}` build.
             ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
 
-      - name: Slack doc build failure
-        if: failure() && steps.build_docs.outcome == 'failure'
-        uses: act10ns/slack@v2.0.0
-        with:
-          webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
-          status: ${{ steps.build_docs.outcome }}
-          message: |
-            Doc build failed on `${{ matrix.NAME }}` build.
-            ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
-
       - name: Slack security issue
         if: steps.bandit.outcome == 'failure'
         uses: act10ns/slack@v2.0.0
@@ -579,16 +477,9 @@ jobs:
         with:
           script: |
               let test_fail = ${{ steps.run_tests.outcome == 'failure' }};
-              let docs_fail = ${{ steps.build_docs.outcome == 'failure' }};
-              if (test_fail && docs_fail) {
-                  core.setFailed('Tests and doc build failed.');
-              }
-              else if (test_fail) {
+              if (test_fail) {
                   core.setFailed('Tests failed.');
               }
-              else if (docs_fail) {
-                  core.setFailed('Doc build failed.');
-              }
 
   windows_tests:
     runs-on: windows-latest
diff --git a/openmdao/components/tests/test_submodel_comp.py b/openmdao/components/tests/test_submodel_comp.py
index 9d57b6106..cc6ea750a 100644
--- a/openmdao/components/tests/test_submodel_comp.py
+++ b/openmdao/components/tests/test_submodel_comp.py
@@ -94,7 +94,7 @@ class TestSubmodelComp(unittest.TestCase):
         with self.assertRaises(Exception) as ctx:
             p.set_val('r', 1)
 
-        msg = '\'<model> <class Group>: Variable "r" not found.\''
+        msg = "\"<model> <class Group>: Could not find 'r'. Perhaps you meant one of the following variables: []\""
         self.assertTrue(str(ctx.exception).startswith(msg))
 
     def test_variable_alias(self):
diff --git a/openmdao/core/tests/test_getset_vars.py b/openmdao/core/tests/test_getset_vars.py
index 1799a886b..535e1b1d0 100644
--- a/openmdao/core/tests/test_getset_vars.py
+++ b/openmdao/core/tests/test_getset_vars.py
@@ -119,25 +119,25 @@ class TestGetSetVariables(unittest.TestCase):
 
         # -------------------------------------------------------------------
 
-        msg = '\'<model> <class Group>: Variable "{}" not found.\''
+        msg = "\"<model> <class Group>: Could not find '{}'. Perhaps you meant one of the following variables: ['g.c.{}']\""
 
         # inputs
         with self.assertRaises(KeyError) as ctx:
             p['x'] = 5.0
-        self.assertEqual(str(ctx.exception), msg.format('x'))
+        self.assertEqual(str(ctx.exception), msg.format('x', 'x'))
 
         with self.assertRaises(KeyError) as ctx:
             p['x']
-        self.assertEqual(str(ctx.exception), msg.format('x'))
+        self.assertEqual(str(ctx.exception), msg.format('x', 'x'))
 
         # outputs
         with self.assertRaises(KeyError) as ctx:
             p['y'] = 5.0
-        self.assertEqual(str(ctx.exception), msg.format('y'))
+        self.assertEqual(str(ctx.exception), msg.format('y', 'y'))
 
         with self.assertRaises(KeyError) as ctx:
             p['y']
-        self.assertEqual(str(ctx.exception), msg.format('y'))
+        self.assertEqual(str(ctx.exception), msg.format('y', 'y'))
 
         p.final_setup()
 
diff --git a/openmdao/core/tests/test_group.py b/openmdao/core/tests/test_group.py
index 941d9dc9a..c5726cecc 100644
--- a/openmdao/core/tests/test_group.py
+++ b/openmdao/core/tests/test_group.py
@@ -37,7 +37,7 @@ class SimpleGroup(om.Group):
 
     def setup(self):
         self.add_subsystem('comp1', om.IndepVarComp('x', 5.0))
-        self.add_subsystem('comp2', om.ExecComp('b=2*a'))
+        self.add_subsystem('comp2', om.ExecComp(['b=2*a', 'c=5*arg']))
         self.connect('comp1.x', 'comp2.a')
 
 
@@ -1835,7 +1835,7 @@ class TestGroupPromotes(unittest.TestCase):
             top['a']
 
         self.assertEqual(str(cm.exception),
-                         "'<model> <class SimpleGroup>: Variable \"a\" not found.'")
+                         "\"<model> <class SimpleGroup>: Could not find 'a'. Perhaps you meant one of the following variables: ['comp2.a']\"")
 
     def test_promotes_inputs_in_config(self):
 
@@ -1855,7 +1855,7 @@ class TestGroupPromotes(unittest.TestCase):
             top['b']
 
         self.assertEqual(str(cm.exception),
-                         "'<model> <class SimpleGroup>: Variable \"b\" not found.'")
+                         "\"<model> <class SimpleGroup>: Could not find 'b'. Perhaps you meant one of the following variables: ['comp2.b']\"")
 
     def test_promotes_any_in_config(self):
 
@@ -1875,7 +1875,7 @@ class TestGroupPromotes(unittest.TestCase):
             top['a']
 
         self.assertEqual(str(cm.exception),
-                         "'<model> <class SimpleGroup>: Variable \"a\" not found.'")
+                         "\"<model> <class SimpleGroup>: Could not find 'a'. Perhaps you meant one of the following variables: ['comp2.a']\"")
 
     def test_promotes_alias(self):
         class SubGroup(om.Group):
@@ -2032,7 +2032,7 @@ class TestGroupPromotes(unittest.TestCase):
             top['Branch1.G1.comp1.a']
 
         self.assertEqual(str(cm.exception),
-                         "'<model> <class BranchGroup>: Variable \"Branch1.G1.comp1.a\" not found.'")
+                           "\"<model> <class BranchGroup>: Could not find 'Branch1.G1.comp1.a'. Perhaps you meant one of the following variables: ['Branch1.G1.comp1.b', 'Branch1.G1.a']\"")
 
     def test_multiple_promotes_collision(self):
 
diff --git a/openmdao/core/tests/test_scaling.py b/openmdao/core/tests/test_scaling.py
index c065a4125..9b3828961 100644
--- a/openmdao/core/tests/test_scaling.py
+++ b/openmdao/core/tests/test_scaling.py
@@ -15,6 +15,12 @@ from openmdao.utils.assert_utils import assert_near_equal, assert_check_partials
 
 from openmdao.test_suite.components.unit_conv import SrcComp, TgtCompF
 
+try:
+    from parameterized import parameterized
+except ImportError:
+    from openmdao.utils.assert_utils import SkipParameterized as parameterized
+from openmdao.utils.testing_utils import parameterized_name
+
 
 class PassThroughLength(om.ExplicitComponent):
     """Units/scaling test component taking length in cm and passing it through in km."""
@@ -604,7 +610,8 @@ class TestScaling(unittest.TestCase):
         prob.setup()
         prob.final_setup()
 
-    def test_scale_and_add_array_with_array(self):
+    @parameterized.expand(['fwd', 'rev'], name_func=parameterized_name)
+    def test_scale_and_add_array_with_array(self, mode):
 
         class ExpCompArrayScale(TestExplCompArrayDense):
 
@@ -635,7 +642,7 @@ class TestScaling(unittest.TestCase):
         model.add_subsystem('comp', ExpCompArrayScale())
         model.connect('p1.x', 'comp.lengths')
 
-        prob.setup()
+        prob.setup(mode=mode)
         prob['comp.widths'] = np.ones((2, 2))
         prob.run_model()