Repository: OpenMDAO/OpenMDAO

Issue:
Suggest pathnames when set_val or get_val are called without a valid promoted pathname.
### Desired capability or behavior.

OpenMDAO expects users to set or get variable values using the promoted pathname of those variables.  We should implement the same string matching/suggestion technique used in connect to help the user when they may have issued set_val or get_val with an invalid variable name.

For example

```
p.set_val('invalid.pathname', 5.0)
```
Should raise an exception along the lines of 
```python
ValueError('invalid.pathname is not a known variable in the model. Perhaps you meant `valid.pathname`?')
```

### Associated POEM

N/A

Discussion:

Solution patch:
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..ee3ffdda9 100644
--- a/openmdao/core/system.py
+++ b/openmdao/core/system.py
@@ -5544,8 +5544,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 +5661,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):


Test patch:
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):