diff --git a/CHANGES.rst b/CHANGES.rst
index d7fe12596a00..40b457f2cbca 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -237,6 +237,10 @@ astropy.utils
   to return the smallest array that can be broadcasted back to the initial
   array. [#9209]

+- The specific IERS Earth rotation parameter table used for time and
+  coordinate transformations can now be set, either in a context or per
+  session, using ``astropy.utils.iers.earth_rotation_table``. [#9244]
+
 astropy.visualization
 ^^^^^^^^^^^^^^^^^^^^^

diff --git a/astropy/coordinates/builtin_frames/utils.py b/astropy/coordinates/builtin_frames/utils.py
index 1ab33cceab6c..153b42cf92c6 100644
--- a/astropy/coordinates/builtin_frames/utils.py
+++ b/astropy/coordinates/builtin_frames/utils.py
@@ -38,15 +38,16 @@ def get_polar_motion(time):
     gets the two polar motion components in radians for use with apio13
     """
     # Get the polar motion from the IERS table
-    xp, yp, status = iers.IERS_Auto.open().pm_xy(time, return_status=True)
+    iers_table = iers.earth_orientation_table.get()
+    xp, yp, status = iers_table.pm_xy(time, return_status=True)

     wmsg = None
     if np.any(status == iers.TIME_BEFORE_IERS_RANGE):
         wmsg = ('Tried to get polar motions for times before IERS data is '
                 'valid. Defaulting to polar motion from the 50-yr mean for those. '
                 'This may affect precision at the 10s of arcsec level')
-        xp.ravel()[status.ravel() == iers.TIME_BEFORE_IERS_RANGE] = _DEFAULT_PM[0]
-        yp.ravel()[status.ravel() == iers.TIME_BEFORE_IERS_RANGE] = _DEFAULT_PM[1]
+        xp[status == iers.TIME_BEFORE_IERS_RANGE] = _DEFAULT_PM[0]
+        yp[status == iers.TIME_BEFORE_IERS_RANGE] = _DEFAULT_PM[1]

         warnings.warn(wmsg, AstropyWarning)

@@ -55,8 +56,8 @@ def get_polar_motion(time):
                 'valid. Defaulting to polar motion from the 50-yr mean for those. '
                 'This may affect precision at the 10s of arcsec level')

-        xp.ravel()[status.ravel() == iers.TIME_BEYOND_IERS_RANGE] = _DEFAULT_PM[0]
-        yp.ravel()[status.ravel() == iers.TIME_BEYOND_IERS_RANGE] = _DEFAULT_PM[1]
+        xp[status == iers.TIME_BEYOND_IERS_RANGE] = _DEFAULT_PM[0]
+        yp[status == iers.TIME_BEYOND_IERS_RANGE] = _DEFAULT_PM[1]

         warnings.warn(wmsg, AstropyWarning)

diff --git a/astropy/time/core.py b/astropy/time/core.py
index 9cf38aa522ee..7e6a1e8e3f94 100644
--- a/astropy/time/core.py
+++ b/astropy/time/core.py
@@ -1654,8 +1654,9 @@ def get_delta_ut1_utc(self, iers_table=None, return_status=False):
         ----------
         iers_table : `~astropy.utils.iers.IERS` table, optional
             Table containing UT1-UTC differences from IERS Bulletins A
-            and/or B.  If `None`, use default combined version (see
-            `astropy.utils.iers.IERS_Auto`)
+            and/or B.  Default: `~astropy.utils.iers.earth_orientation_table`
+            (which in turn defaults to the combined version provided by
+            `~astropy.utils.iers.IERS_Auto`).
         return_status : bool
             Whether to return status values.  If `False` (default), iers
             raises `IndexError` if any time is out of the range
@@ -1689,8 +1690,8 @@ def get_delta_ut1_utc(self, iers_table=None, return_status=False):
             array([ True, False]...)
         """
         if iers_table is None:
-            from astropy.utils.iers import IERS_Auto
-            iers_table = IERS_Auto.open()
+            from astropy.utils.iers import earth_orientation_table
+            iers_table = earth_orientation_table.get()

         return iers_table.ut1_utc(self.utc, return_status=return_status)

@@ -1705,8 +1706,8 @@ def _get_delta_ut1_utc(self, jd1=None, jd2=None):
         # Sec. 4.3.1: the arg DUT is the quantity delta_UT1 = UT1 - UTC in
         # seconds. It is obtained from tables published by the IERS.
         if not hasattr(self, '_delta_ut1_utc'):
-            from astropy.utils.iers import IERS_Auto
-            iers_table = IERS_Auto.open()
+            from astropy.utils.iers import earth_orientation_table
+            iers_table = earth_orientation_table.get()
             # jd1, jd2 are normally set (see above), except if delta_ut1_utc
             # is access directly; ensure we behave as expected for that case
             if jd1 is None:
diff --git a/astropy/utils/iers/iers.py b/astropy/utils/iers/iers.py
index 4b76132e6106..c3f6dccd0afc 100644
--- a/astropy/utils/iers/iers.py
+++ b/astropy/utils/iers/iers.py
@@ -22,11 +22,12 @@
 from astropy import units as u
 from astropy.table import QTable, MaskedColumn
 from astropy.utils.data import get_pkg_data_filename, clear_download_cache
+from astropy.utils.state import ScienceState
 from astropy.utils.compat import NUMPY_LT_1_17
 from astropy import utils
 from astropy.utils.exceptions import AstropyWarning

-__all__ = ['Conf', 'conf',
+__all__ = ['Conf', 'conf', 'earth_orientation_table',
            'IERS', 'IERS_B', 'IERS_A', 'IERS_Auto',
            'FROM_IERS_B', 'FROM_IERS_A', 'FROM_IERS_A_PREDICTION',
            'TIME_BEFORE_IERS_RANGE', 'TIME_BEYOND_IERS_RANGE',
@@ -802,3 +803,47 @@ def _substitute_iers_b(cls, table):
             table['dY_2000A_B'][:n_iers_b] = iers_b['dY_2000A']

         return table
+
+
+class earth_orientation_table(ScienceState):
+    """Default IERS table for Earth rotation and reference systems service.
+
+    These tables are used to calculate the offsets between ``UT1`` and ``UTC``
+    and for conversion to Earth-based coordinate systems.
+
+    The state itself is an IERS table, as an instance of one of the
+    `~astropy.utils.iers.IERS` classes.  The default, the auto-updating
+    `~astropy.utils.iers.IERS_Auto` class, should suffice for most
+    purposes.
+
+    Examples
+    --------
+    To temporarily use the IERS-B file packaged with astropy::
+
+      >>> from astropy.utils import iers
+      >>> from astropy.time import Time
+      >>> iers_b = iers.IERS_B.open(iers.IERS_B_FILE)
+      >>> with iers.earth_orientation_table.set(iers_b):
+      ...     print(Time('2000-01-01').ut1.isot)
+      2000-01-01T00:00:00.355
+
+    To use the most recent IERS-A file for the whole session::
+
+      >>> iers_a = iers.IERS_A.open(iers.IERS_A_URL)  # doctest: +SKIP
+      >>> iers.earth_orientation_table.set(iers_a)  # doctest: +SKIP
+      <ScienceState earth_orientation_table: <IERS_A length=17463>...>
+
+    To go back to the default (of `~astropy.utils.iers.IERS_Auto`)::
+
+      >>> iers.earth_orientation_table.set(None)  # doctest: +SKIP
+      <ScienceState earth_orientation_table: <IERS_Auto length=17428>...>
+    """
+    _value = None
+
+    @classmethod
+    def validate(cls, value):
+        if value is None:
+            value = IERS_Auto.open()
+        if not isinstance(value, IERS):
+            raise ValueError("earth_orientation_table requires an IERS Table.")
+        return value
diff --git a/astropy/utils/state.py b/astropy/utils/state.py
index 262b41dd2773..050ad9a3b9e5 100644
--- a/astropy/utils/state.py
+++ b/astropy/utils/state.py
@@ -56,8 +56,13 @@ def __exit__(self, type, value, tb):
                 self._parent._value = self._value

             def __repr__(self):
-                return ('<ScienceState {}: {!r}>'
-                        .format(self._parent.__name__, self._parent._value))
+                # Ensure we have a single-line repr, just in case our
+                # value is not something simple like a string.
+                value_repr, lb, _ = repr(self._parent._value).partition('\n')
+                if lb:
+                    value_repr += '...'
+                return ('<ScienceState {}: {}>'
+                        .format(self._parent.__name__, value_repr))

         ctx = _Context(cls, cls._value)
         value = cls.validate(value)
diff --git a/docs/utils/iers.rst b/docs/utils/iers.rst
index 7fe000ef7d9d..726cee3ce0f5 100644
--- a/docs/utils/iers.rst
+++ b/docs/utils/iers.rst
@@ -29,7 +29,7 @@ and how this can be controlled.
 Basic usage
 -----------

-The IERS data are managed via a instances of the
+By default, the IERS data are managed via instances of the
 :class:`~astropy.utils.iers.IERS_Auto` class.  These instances are created
 internally within the relevant time and coordinate objects during
 transformations.  If the astropy data cache does not have the required IERS
@@ -47,7 +47,7 @@ machine.  Here is an example that shows the typical download progress bar::
 Note that you can forcibly clear the download cache as follows::

   >>> from astropy.utils.data import clear_download_cache
-  >>> clear_download_cache()
+  >>> clear_download_cache()  # doctest: +SKIP

 The default IERS data used automatically is updated by the service every 7 days
 and includes transforms dating back to 1973-01-01.
@@ -146,34 +146,44 @@ Direct table access
 -------------------

 In most cases the automatic interface will suffice, but you may need to
-directly load and manipulate IERS tables.  IERS-B values are provided as
-part of astropy and can be used for transformations.  For example::
+directly load and manipulate IERS tables.  IERS-B values are provided as part
+of astropy and can be used to calculate time offsets and polar motion
+directly, or set up for internal use in further time and coordinate
+transformations.  For example::

   >>> from astropy.utils import iers
   >>> t = Time('2010:001')
   >>> iers_b = iers.IERS_B.open()
   >>> iers_b.ut1_utc(t)  # doctest: +FLOAT_CMP
   <Quantity 0.1140827 s>
-  >>> t.delta_ut1_utc = iers_b.ut1_utc(t)
+  >>> iers.earth_orientation_table.set(iers_b)
+  <ScienceState earth_orientation_table: <IERS_B length=...>...>
   >>> t.ut1.iso
   '2010-01-01 00:00:00.114'

 Instead of local copies of IERS files, one can also download them, using
-``iers.IERS_A_URL`` (or ``iers.IERS_A_URL_MIRROR``) and ``iers.IERS_B_URL``::
+``iers.IERS_A_URL`` (or ``iers.IERS_A_URL_MIRROR``) and ``iers.IERS_B_URL``,
+and then use those for future time and coordinate transformations (in this
+example, just for a single calculation, by using
+`~astropy.utils.iers.earth_orientation_table` as a context manager)::

   >>> iers_a = iers.IERS_A.open(iers.IERS_A_URL)  # doctest: +SKIP
+  >>> with iers.earth_orientation_table.set(iers_a):  # doctest: +SKIP
+  ...     print(t.ut1.iso)
+  2010-01-01 00:00:00.114

-For coordinate transformations that require IERS polar motion values,
-setting the values manually can be done as follows (where one could also
-select IERS_B)::
+To reset to the default, pass in `None` (which is equivalent to passing in
+``iers.IERS_Auto.open()``)::

-  >>> iers.conf.auto_download = False
-  >>> iers.IERS.iers_table = iers.IERS_A.open(iers.IERS_A_URL)  # doctest: +SKIP
+  >>> iers.earth_orientation_table.set(None)  # doctest: +REMOTE_DATA
+  <ScienceState earth_orientation_table: <IERS_Auto length=...>...>

 To see the internal IERS data that gets used in astropy you can do the
 following::

-  >>> dat = iers.IERS_Auto.open()  # doctest: +SKIP
+  >>> dat = iers.earth_orientation_table.get()  # doctest: +REMOTE_DATA
+  >>> type(dat)  # doctest: +REMOTE_DATA
+  <class 'astropy.utils.iers.iers.IERS_Auto'>
   >>> dat  # doctest: +SKIP
   <IERS_Auto length=16196>
    year month  day    MJD   PolPMFlag_A ... UT1Flag    PM_x     PM_y   PolPMFlag

diff --git a/astropy/coordinates/tests/test_iau_fullstack.py b/astropy/coordinates/tests/test_iau_fullstack.py
index cd788834326b..996800b55b24 100644
--- a/astropy/coordinates/tests/test_iau_fullstack.py
+++ b/astropy/coordinates/tests/test_iau_fullstack.py
@@ -117,7 +117,8 @@ def test_iau_fullstack(fullstack_icrs, fullstack_fiducial_altaz,
     assert np.all(addecs < tol), 'largest Dec change is {} mas, > {}'.format(np.max(addecs.arcsec*1000), tol)

     # check that we're consistent with the ERFA alt/az result
-    xp, yp = u.Quantity(iers.IERS_Auto.open().pm_xy(fullstack_times)).to_value(u.radian)
+    iers_tab = iers.earth_orientation_table.get()
+    xp, yp = u.Quantity(iers_tab.pm_xy(fullstack_times)).to_value(u.radian)
     lon = fullstack_locations.geodetic[0].to_value(u.radian)
     lat = fullstack_locations.geodetic[1].to_value(u.radian)
     height = fullstack_locations.geodetic[2].to_value(u.m)
@@ -178,7 +179,7 @@ def test_future_altaz():
     # table does not raise an exception.  Only if using IERS_B (which happens without
     # --remote-data, i.e. for all CI testing) do we expect another warning.
     messages_to_find = ["Tried to get polar motions for times after IERS data is valid."]
-    if isinstance(iers.IERS_Auto.iers_table, iers.IERS_B):
+    if isinstance(iers.earth_orientation_table.get(), iers.IERS_B):
         messages_to_find.append("(some) times are outside of range covered by IERS table.")

     messages_found = [False for _ in messages_to_find]
diff --git a/astropy/coordinates/tests/test_intermediate_transformations.py b/astropy/coordinates/tests/test_intermediate_transformations.py
index d80a7c271a3f..35dea9cdd587 100644
--- a/astropy/coordinates/tests/test_intermediate_transformations.py
+++ b/astropy/coordinates/tests/test_intermediate_transformations.py
@@ -7,12 +7,14 @@
 import numpy as np

 from astropy import units as u
-from astropy.tests.helper import (assert_quantity_allclose as assert_allclose)
+from astropy.tests.helper import (assert_quantity_allclose as assert_allclose,
+                                  catch_warnings)
 from astropy.time import Time
 from astropy.coordinates import (EarthLocation, get_sun, ICRS, GCRS, CIRS, ITRS, AltAz,
                 PrecessedGeocentric, CartesianRepresentation, SkyCoord,
                 SphericalRepresentation, UnitSphericalRepresentation,
                 HCRS, HeliocentricMeanEcliptic)
+from astropy.utils import iers

 from astropy._erfa import epv00
@@ -490,6 +492,37 @@ def test_gcrs_self_transform_closeby():
     assert_allclose(delta, 0.0*u.m, atol=1*u.m)

+@pytest.mark.remote_data
+def test_earth_orientation_table():
+    """Check that we can set the IERS table used as Earth Reference.
+
+    Use the here and now to be sure we get a difference.
+    """
+    t = Time.now()
+    location = EarthLocation(lat=0*u.deg, lon=0*u.deg)
+    altaz = AltAz(location=location, obstime=t)
+    sc = SkyCoord(1*u.deg, 2*u.deg)
+    # Default: uses IERS_Auto, which will give a prediction.
+    with catch_warnings() as w:
+        altaz_auto = sc.transform_to(altaz)
+
+    assert len(w) == 0
+
+    with iers.earth_orientation_table.set(iers.IERS_B.open()):
+        with catch_warnings() as w:
+            altaz_b = sc.transform_to(altaz)
+        assert len(w) == 1
+        assert 'after IERS data' in str(w[0].message)
+
+    sep_b_auto = altaz_b.separation(altaz_auto)
+    assert_allclose(sep_b_auto, 0.0*u.deg, atol=1*u.arcsec)
+    assert sep_b_auto > 10*u.microarcsecond
+
+    # Check we returned to regular IERS system.
+    altaz_auto2 = sc.transform_to(altaz)
+    assert altaz_auto2.separation(altaz_auto) == 0.
+
+
 @pytest.mark.remote_data
 @pytest.mark.skipif('not HAS_JPLEPHEM')
 def test_ephemerides():
diff --git a/astropy/coordinates/tests/test_regression.py b/astropy/coordinates/tests/test_regression.py
index 4e6c02048118..3b124d5eccc7 100644
--- a/astropy/coordinates/tests/test_regression.py
+++ b/astropy/coordinates/tests/test_regression.py
@@ -225,7 +225,13 @@ def test_regression_futuretimes_4302():
         c = CIRS(1*u.deg, 2*u.deg, obstime=future_time)
         c.transform_to(ITRS(obstime=future_time))

-    if not isinstance(iers.IERS_Auto.iers_table, iers.IERS_Auto):
+    # check that out-of-range warning appears among any other warnings.  If
+    # tests are run with --remote-data then the IERS table will be an instance
+    # of IERS_Auto which is assured of being "fresh".  In this case getting
+    # times outside the range of the table does not raise an exception.  Only
+    # if using IERS_B (which happens without --remote-data, i.e. for all CI
+    # testing) do we expect another warning.
+    if isinstance(iers.earth_orientation_table.get(), iers.IERS_B):
         saw_iers_warnings = False
         for w in found_warnings:
             if issubclass(w.category, AstropyWarning):
diff --git a/astropy/time/tests/test_ut1.py b/astropy/time/tests/test_ut1.py
index 7fed75ea9192..21443387e28b 100644
--- a/astropy/time/tests/test_ut1.py
+++ b/astropy/time/tests/test_ut1.py
@@ -72,23 +72,42 @@ def test_delta_ut1_utc(self):
             assert allclose_sec(t._delta_ut1_utc, -0.58682110003124965)

-@pytest.mark.skipif('not HAS_IERS_A')
-class TestTimeUT1_IERSA:
-    def test_ut1_iers_A(self):
+class TestTimeUT1SpecificIERSTable:
+    def do_ut1_prediction_test(self, iers_type):
         tnow = Time.now()
-        iers_a = iers.IERS_A.open()
-        tnow.delta_ut1_utc, status = iers_a.ut1_utc(tnow, return_status=True)
+        iers_tab = iers_type.open()
+        tnow.delta_ut1_utc, status = iers_tab.ut1_utc(tnow, return_status=True)
         assert status == iers.FROM_IERS_A_PREDICTION
         tnow_ut1_jd = tnow.ut1.jd
         assert tnow_ut1_jd != tnow.jd

+        delta_ut1_utc = tnow.delta_ut1_utc
+        with iers.earth_orientation_table.set(iers_type.open()):
+            delta2, status2 = tnow.get_delta_ut1_utc(return_status=True)
+            assert status2 == status
+            assert delta2.to_value('s') == delta_ut1_utc
+
+            tnow_ut1 = tnow.ut1
+            assert tnow_ut1._delta_ut1_utc == delta_ut1_utc
+            assert tnow_ut1.jd != tnow.jd
+
+    @pytest.mark.skipif('not HAS_IERS_A')
+    def test_ut1_iers_A(self):
+        self.do_ut1_prediction_test(iers.IERS_A)

-@pytest.mark.remote_data
-class TestTimeUT1_IERS_Auto:
+    @pytest.mark.remote_data
     def test_ut1_iers_auto(self):
+        self.do_ut1_prediction_test(iers.IERS_Auto)
+
+    def test_ut1_iers_B(self):
         tnow = Time.now()
-        iers_a = iers.IERS_Auto.open()
-        tnow.delta_ut1_utc, status = iers_a.ut1_utc(tnow, return_status=True)
-        assert status == iers.FROM_IERS_A_PREDICTION
-        tnow_ut1_jd = tnow.ut1.jd
-        assert tnow_ut1_jd != tnow.jd
+        iers_b = iers.IERS_B.open()
+        delta1, status1 = tnow.get_delta_ut1_utc(iers_b, return_status=True)
+        assert status1 == iers.TIME_BEYOND_IERS_RANGE
+
+        with iers.earth_orientation_table.set(iers.IERS_B.open()):
+            delta2, status2 = tnow.get_delta_ut1_utc(return_status=True)
+            assert status2 == status1
+
+            with pytest.raises(iers.IERSRangeError):
+                tnow.ut1

diff --git a/docs/utils/iers.rst b/docs/utils/iers.rst
index 7fe000ef7d..726cee3ce0 100644
--- a/docs/utils/iers.rst
+++ b/docs/utils/iers.rst
@@ -29,7 +29,7 @@ and how this can be controlled.
 Basic usage
 -----------

-The IERS data are managed via a instances of the
+By default, the IERS data are managed via instances of the
 :class:`~astropy.utils.iers.IERS_Auto` class.  These instances are created
 internally within the relevant time and coordinate objects during
 transformations.  If the astropy data cache does not have the required IERS
@@ -47,7 +47,7 @@ machine.  Here is an example that shows the typical download progress bar::
 Note that you can forcibly clear the download cache as follows::

   >>> from astropy.utils.data import clear_download_cache
-  >>> clear_download_cache()
+  >>> clear_download_cache()  # doctest: +SKIP

 The default IERS data used automatically is updated by the service every 7 days
 and includes transforms dating back to 1973-01-01.
@@ -146,34 +146,44 @@ Direct table access
 -------------------

 In most cases the automatic interface will suffice, but you may need to
-directly load and manipulate IERS tables.  IERS-B values are provided as
-part of astropy and can be used for transformations.  For example::
+directly load and manipulate IERS tables.  IERS-B values are provided as part
+of astropy and can be used to calculate time offsets and polar motion
+directly, or set up for internal use in further time and coordinate
+transformations.  For example::

   >>> from astropy.utils import iers
   >>> t = Time('2010:001')
   >>> iers_b = iers.IERS_B.open()
   >>> iers_b.ut1_utc(t)  # doctest: +FLOAT_CMP
   <Quantity 0.1140827 s>
-  >>> t.delta_ut1_utc = iers_b.ut1_utc(t)
+  >>> iers.earth_orientation_table.set(iers_b)
+  <ScienceState earth_orientation_table: <IERS_B length=...>...>
   >>> t.ut1.iso
   '2010-01-01 00:00:00.114'

 Instead of local copies of IERS files, one can also download them, using
-``iers.IERS_A_URL`` (or ``iers.IERS_A_URL_MIRROR``) and ``iers.IERS_B_URL``::
+``iers.IERS_A_URL`` (or ``iers.IERS_A_URL_MIRROR``) and ``iers.IERS_B_URL``,
+and then use those for future time and coordinate transformations (in this
+example, just for a single calculation, by using
+`~astropy.utils.iers.earth_orientation_table` as a context manager)::

   >>> iers_a = iers.IERS_A.open(iers.IERS_A_URL)  # doctest: +SKIP
+  >>> with iers.earth_orientation_table.set(iers_a):  # doctest: +SKIP
+  ...     print(t.ut1.iso)
+  2010-01-01 00:00:00.114

-For coordinate transformations that require IERS polar motion values,
-setting the values manually can be done as follows (where one could also
-select IERS_B)::
+To reset to the default, pass in `None` (which is equivalent to passing in
+``iers.IERS_Auto.open()``)::

-  >>> iers.conf.auto_download = False
-  >>> iers.IERS.iers_table = iers.IERS_A.open(iers.IERS_A_URL)  # doctest: +SKIP
+  >>> iers.earth_orientation_table.set(None)  # doctest: +REMOTE_DATA
+  <ScienceState earth_orientation_table: <IERS_Auto length=...>...>

 To see the internal IERS data that gets used in astropy you can do the
 following::

-  >>> dat = iers.IERS_Auto.open()  # doctest: +SKIP
+  >>> dat = iers.earth_orientation_table.get()  # doctest: +REMOTE_DATA
+  >>> type(dat)  # doctest: +REMOTE_DATA
+  <class 'astropy.utils.iers.iers.IERS_Auto'>
   >>> dat  # doctest: +SKIP
   <IERS_Auto length=16196>
    year month  day    MJD   PolPMFlag_A ... UT1Flag    PM_x     PM_y   PolPMFlag


diff --git a/CHANGES.rst b/CHANGES.rst
index 7035d4a29860..b1c7305aee37 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -142,6 +142,11 @@ astropy.table

 - Added ``Table.iterrows()`` for making row-wise iteration faster. [#8969]

+- Allow table to be initialized with a list of dict where the dict keys
+  are not the same in every row. The table column names are the set of all keys
+  found in the input data, and any missing key/value pairs are turned into
+  missing data in the table. [#9425]
+
 astropy.tests
 ^^^^^^^^^^^^^

diff --git a/astropy/table/table.py b/astropy/table/table.py
index 81b9126c4cba..2058af76eb64 100644
--- a/astropy/table/table.py
+++ b/astropy/table/table.py
@@ -3,7 +3,7 @@
 from .index import TableIndices, TableLoc, TableILoc, TableLocIndices

 import sys
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
 from collections.abc import Mapping
 import warnings
 from copy import deepcopy
@@ -175,6 +175,23 @@ def has_info_class(obj, cls):
     return hasattr(obj, 'info') and isinstance(obj.info, cls)

+def _get_names_from_list_of_dict(rows):
+    """Return list of column names if ``rows`` is a list of dict that
+    defines table data.
+
+    If rows is not a list of dict then return None.
+    """
+    if rows is None:
+        return None
+
+    names = set()
+    for row in rows:
+        if not isinstance(row, dict):
+            return None
+        names.update(row)
+    return list(names)
+
+
 # Note to future maintainers: when transitioning this to dict
 # be sure to change the OrderedDict ref(s) in Row and in __len__().

@@ -450,18 +467,23 @@ def __init__(self, data=None, masked=False, names=None, dtype=None,
         if not copy and dtype is not None:
             raise ValueError('Cannot specify dtype when copy=False')

+        # Specifies list of names found for the case of initializing table with
+        # a list of dict. If data are not list of dict then this is None.
+        names_from_list_of_dict = None
+
         # Row-oriented input, e.g. list of lists or list of tuples, list of
         # dict, Row instance.  Set data to something that the subsequent code
         # will parse correctly.
-        is_list_of_dict = False
         if rows is not None:
             if data is not None:
                 raise ValueError('Cannot supply both `data` and `rows` values')
             if isinstance(rows, types.GeneratorType):
                 # Without this then the all(..) test below uses up the generator
                 rows = list(rows)
-            if all(isinstance(row, dict) for row in rows):
-                is_list_of_dict = True  # Avoid doing the all(...) test twice.
+
+            # Get column names if `rows` is a list of dict, otherwise this is None
+            names_from_list_of_dict = _get_names_from_list_of_dict(rows)
+            if names_from_list_of_dict:
                 data = rows
             elif isinstance(rows, self.Row):
                 data = rows
@@ -493,10 +515,15 @@ def __init__(self, data=None, masked=False, names=None, dtype=None,
             data = data._table[data._index:data._index + 1]

         if isinstance(data, (list, tuple)):
-            init_func = self._init_from_list
-            if data and (is_list_of_dict or all(isinstance(row, dict) for row in data)):
-                n_cols = len(data[0])
+            # Get column names from `data` if it is a list of dict, otherwise this is None.
+            # This might be previously defined if `rows` was supplied as an init arg.
+            names_from_list_of_dict = (names_from_list_of_dict or
+                                       _get_names_from_list_of_dict(data))
+            if names_from_list_of_dict:
+                init_func = self._init_from_list_of_dicts
+                n_cols = len(names_from_list_of_dict)
             else:
+                init_func = self._init_from_list
                 n_cols = len(data)

         elif isinstance(data, np.ndarray):
@@ -804,10 +831,20 @@ def _check_names_dtype(self, names, dtype, n_cols):
                 .format(inp_str))

     def _init_from_list_of_dicts(self, data, names, dtype, n_cols, copy):
+        """Initialize table from a list of dictionaries representing rows."""
+        # Define placeholder for missing values as a unique object that cannot
+        # every occur in user data.
+        MISSING = object()
+
+        # Gather column names that exist in the input `data`.
         names_from_data = set()
         for row in data:
             names_from_data.update(row)

+        # Put names into a preferred order, either using the first row of data
+        # if it is ordered, or alphabetically.  Starting with Python 3.7, dict
+        # is ordered so this test can be relaxed.  (In practice CPython 3.6 is
+        # this way, but not according to the formal spec).
         if (isinstance(data[0], OrderedDict) and
                 set(data[0].keys()) == names_from_data):
             names_from_data = list(data[0].keys())
@@ -817,29 +854,53 @@ def _init_from_list_of_dicts(self, data, names, dtype, n_cols, copy):
         # Note: if set(data[0].keys()) != names_from_data, this will give an
         # exception later, so NO need to catch here.

+        # Convert list of dict into dict of list (cols), keep track of missing
+        # indexes and put in MISSING placeholders in the `cols` lists.
         cols = {}
+        missing_indexes = defaultdict(list)
         for name in names_from_data:
             cols[name] = []
-            for i, row in enumerate(data):
+            for ii, row in enumerate(data):
                 try:
-                    cols[name].append(row[name])
+                    val = row[name]
                 except KeyError:
-                    raise ValueError(f'Row {i} has no value for column {name}')
-
+                    missing_indexes[name].append(ii)
+                    val = MISSING
+                cols[name].append(val)
+
+        # Fill the missing entries with first values
+        if missing_indexes:
+            for name, indexes in missing_indexes.items():
+                col = cols[name]
+                first_val = next(val for val in col if val is not MISSING)
+                for index in indexes:
+                    col[index] = first_val
+
+        # prepare initialization
         if all(name is None for name in names):
             names = names_from_data

         self._init_from_dict(cols, names, dtype, n_cols, copy)
+
+        # Mask the missing values if necessary, converting columns to MaskedColumn
+        # as needed.
+        if missing_indexes:
+            for name, indexes in missing_indexes.items():
+                col = self[name]
+                # Ensure that any Column subclasses with MISSING values can support
+                # setting masked values. As of astropy 4.0 the test condition below is
+                # always True since _init_from_dict cannot result in mixin columns.
+                if isinstance(col, Column) and not isinstance(col, MaskedColumn):
+                    self[name] = self.MaskedColumn(col, copy=False)
+
+                # Finally do the masking in a mixin-safe way.
+                self[name][indexes] = np.ma.masked
         return

     def _init_from_list(self, data, names, dtype, n_cols, copy):
         """Initialize table from a list of column data.  A column can be a
         Column object, np.ndarray, mixin, or any other iterable object.
         """
-        if data and all(isinstance(row, dict) for row in data):
-            self._init_from_list_of_dicts(data, names, dtype, n_cols, copy)
-            return
-
         cols = []
         default_names = _auto_names(n_cols)

diff --git a/docs/table/construct_table.rst b/docs/table/construct_table.rst
index 56b36e076610..c9a96c5613a5 100644
--- a/docs/table/construct_table.rst
+++ b/docs/table/construct_table.rst
@@ -225,20 +225,22 @@ list of dict objects.  The keys determine the column names::

   >>> data = [{'a': 5, 'b': 10},
   ...         {'a': 15, 'b': 20}]
-  >>> Table(rows=data)  # doctest: +SKIP
-  <Table length=2>
-    a     b
-  int64 int64
-  ----- -----
-      5    10
-     15    20
+  >>> t = Table(rows=data)
+  >>> print(t)
+   a   b
+  --- ---
+    5  10
+   15  20

-Every row must have the same set of keys or a ValueError will be thrown::
+If there are missing keys in one or more rows then the corresponding values will be
+marked as missing (masked)::

-  >>> t = Table(rows=[{'a': 5, 'b': 10}, {'a': 15, 'b': 30, 'c': 50}])
-  Traceback (most recent call last):
-    ...
-  ValueError: Row 0 has no value for column c
+  >>> t = Table(rows=[{'a': 5, 'b': 10}, {'a': 15, 'c': 50}])
+  >>> print(t)
+   a   b   c
+  --- --- ---
+    5  10  --
+   15  --  50

 You can also preserve the column order by using ``OrderedDict``. If the first item is an
 ``OrderedDict`` then the order is preserved:

diff --git a/astropy/table/tests/test_init_table.py b/astropy/table/tests/test_init_table.py
index 613c8c071b81..e1b1e008d612 100644
--- a/astropy/table/tests/test_init_table.py
+++ b/astropy/table/tests/test_init_table.py
@@ -6,7 +6,7 @@
 import pytest
 import numpy as np

-from astropy.table import Column, TableColumns, Table
+from astropy.table import Column, TableColumns, Table, MaskedColumn

 class TestTableColumnsInit():
@@ -168,7 +168,7 @@ def test_bad_data(self, table_type):
         self._setup(table_type)
         with pytest.raises(ValueError):
             table_type([[1, 2],
-                   [3, 4, 5]])
+                        [3, 4, 5]])

 @pytest.mark.usefixtures('table_type')
@@ -188,11 +188,21 @@ def test_names_ordered(self, table_type):
         t = table_type(self.data, names=('c', 'b', 'a'))
         assert t.colnames == ['c', 'b', 'a']

-    def test_bad_data(self, table_type):
-        self._setup(table_type)
-        with pytest.raises(ValueError):
-            table_type([{'a': 1, 'b': 2, 'c': 3},
-                   {'a': 2, 'b': 4}])
+    def test_missing_data_init_from_dict(self, table_type):
+        dat = [{'a': 1, 'b': 2},
+               {'a': 2, 'c': 4}]
+        for rows in [False, True]:
+            t = table_type(rows=dat) if rows else table_type(dat)
+
+            assert np.all(t['a'] == [1, 2])
+            assert np.all(t['b'].mask == [False, True])
+            assert np.all(t['b'].data == [2, 2])
+            assert np.all(t['c'].mask == [True, False])
+            assert np.all(t['c'].data == [4, 4])
+
+            assert type(t['a']) is (MaskedColumn if t.masked else Column)
+            assert type(t['b']) is MaskedColumn
+            assert type(t['c']) is MaskedColumn

 @pytest.mark.usefixtures('table_type')
@@ -517,9 +527,6 @@ def test_init_from_row_OrderedDict(table_type):
     assert t1.colnames == ['b', 'a']
     assert t2.colnames == ['a', 'b']

-    with pytest.raises(ValueError):
-        table_type(rows=[OrderedDict([('b', 1)]), {'a': 10, 'b': 20}])
-

 def test_init_from_rows_as_generator():
     rows = ((1 + ii, 2 + ii) for ii in range(2))

diff --git a/docs/table/construct_table.rst b/docs/table/construct_table.rst
index 56b36e0766..c9a96c5613 100644
--- a/docs/table/construct_table.rst
+++ b/docs/table/construct_table.rst
@@ -225,20 +225,22 @@ list of dict objects.  The keys determine the column names::

   >>> data = [{'a': 5, 'b': 10},
   ...         {'a': 15, 'b': 20}]
-  >>> Table(rows=data)  # doctest: +SKIP
-  <Table length=2>
-    a     b
-  int64 int64
-  ----- -----
-      5    10
-     15    20
+  >>> t = Table(rows=data)
+  >>> print(t)
+   a   b
+  --- ---
+    5  10
+   15  20

-Every row must have the same set of keys or a ValueError will be thrown::
+If there are missing keys in one or more rows then the corresponding values will be
+marked as missing (masked)::

-  >>> t = Table(rows=[{'a': 5, 'b': 10}, {'a': 15, 'b': 30, 'c': 50}])
-  Traceback (most recent call last):
-    ...
-  ValueError: Row 0 has no value for column c
+  >>> t = Table(rows=[{'a': 5, 'b': 10}, {'a': 15, 'c': 50}])
+  >>> print(t)
+   a   b   c
+  --- --- ---
+    5  10  --
+   15  --  50

 You can also preserve the column order by using ``OrderedDict``. If the first item is an
 ``OrderedDict`` then the order is preserved:

If completing this task requires creating new files, classes, fields, or error messages, you may consider using the following suggested entity names:


diff --git a/CHANGES.rst b/CHANGES.rst
index b585271083c5..f6b820b72052 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -161,6 +161,10 @@ astropy.table
 - Added depth-wise stacking ``cstack()`` in higher level table operation.
   It help will in stacking table column depth-wise. [#8939]

+- Added a new table equality method ``values_equal()`` which allows comparison
+  table values to another table, list, or value, and returns an element-by-element
+  equality table. [#9068]
+
 - Added new ``join_type='cartesian'`` option to the ``join`` operation.
   [#9288]

diff --git a/astropy/table/table.py b/astropy/table/table.py
index 2058af76eb64..6561100da223 100644
--- a/astropy/table/table.py
+++ b/astropy/table/table.py
@@ -2998,6 +2998,35 @@ def __ge__(self, other):
         return super().__ge__(other)

     def __eq__(self, other):
+        return self._rows_equal(other)
+
+    def __ne__(self, other):
+        return ~self.__eq__(other)
+
+    def _rows_equal(self, other):
+        """
+        Row-wise comparison of table with any other object.
+
+        This is actual implementation for __eq__.
+
+        Returns a 1-D boolean numpy array showing result of row-wise comparison.
+        This is the same as the ``==`` comparison for tables.
+
+        Parameters
+        ----------
+        other : Table or DataFrame or ndarray
+             An object to compare with table
+
+        Examples
+        --------
+        Comparing one Table with other::
+
+            >>> t1 = Table([[1,2],[4,5],[7,8]], names=('a','b','c'))
+            >>> t2 = Table([[1,2],[4,5],[7,8]], names=('a','b','c'))
+            >>> t1._rows_equal(t2)
+            array([ True,  True])
+
+        """

         if isinstance(other, Table):
             other = other.as_array()
@@ -3021,8 +3050,80 @@ def __eq__(self, other):

         return result

-    def __ne__(self, other):
-        return ~self.__eq__(other)
+    def values_equal(self, other):
+        """
+        Element-wise comparison of table with another table, list, or scalar.
+
+        Returns a ``Table`` with the same columns containing boolean values
+        showing result of comparison.
+
+        Parameters
+        ----------
+        other : Table-like object or list or scalar
+             Object to compare with table
+
+        Examples
+        --------
+        Compare one Table with other::
+
+          >>> t1 = Table([[1, 2], [4, 5], [-7, 8]], names=('a', 'b', 'c'))
+          >>> t2 = Table([[1, 2], [-4, 5], [7, 8]], names=('a', 'b', 'c'))
+          >>> t1.values_equal(t2)
+

+           a     b     c
+          bool  bool  bool
+          ---- ----- -----
+          True False False
+          True  True  True
+
+        """
+        if isinstance(other, Table):
+            names = other.colnames
+        else:
+            try:
+                other = Table(other, copy=False)
+                names = other.colnames
+            except Exception:
+                # Broadcast other into a dict, so e.g. other = 2 will turn into
+                # other = {'a': 2, 'b': 2} and then equality does a
+                # column-by-column broadcasting.
+                names = self.colnames
+                other = {name: other for name in names}
+
+        # Require column names match but do not require same column order
+        if set(self.colnames) != set(names):
+            raise ValueError('cannot compare tables with different column names')
+
+        eqs = []
+        for name in names:
+            try:
+                np.broadcast(self[name], other[name])  # Check if broadcast-able
+                # Catch the numpy FutureWarning related to equality checking,
+                # "elementwise comparison failed; returning scalar instead, but
+                #  in the future will perform elementwise comparison".  Turn this
+                # into an exception since the scalar answer is not what we want.
+                with warnings.catch_warnings(record=True) as warns:
+                    warnings.simplefilter('always')
+                    eq = self[name] == other[name]
+                    if (warns and issubclass(warns[-1].category, FutureWarning) and
+                            'elementwise comparison failed' in str(warns[-1].message)):
+                        raise FutureWarning(warns[-1].message)
+            except Exception as err:
+                raise ValueError(f'unable to compare column {name}') from err
+
+            # Be strict about the result from the comparison. E.g. SkyCoord __eq__ is just
+            # broken and completely ignores that it should return an array.
+            if not (isinstance(eq, np.ndarray) and
+                    eq.dtype is np.dtype('bool') and
+                    len(eq) == len(self)):
+                raise TypeError(f'comparison for column {name} returned {eq} '
+                                f'instead of the expected boolean ndarray')
+
+            eqs.append(eq)
+
+        out = Table(eqs, names=names)
+
+        return out

     @property
     def groups(self):
diff --git a/docs/table/access_table.rst b/docs/table/access_table.rst
index a27604fa928a..cf472aeb56f8 100644
--- a/docs/table/access_table.rst
+++ b/docs/table/access_table.rst
@@ -314,6 +314,56 @@ structured array by creating a copy or reference with ``np.array``::
   >>> data = np.array(t, copy=False)  # reference to data in t

+Table Equality
+--------------
+We can check table data equality using two different methods:
+
+- The ``==`` comparison operator.  This returns a ``True`` or ``False`` for
+  each row if the *entire row* matches.  This is the same as the behavior of
+  numpy structured arrays.
+- Table :meth:`~astropy.table.Table.values_equal` to compare table values
+  element-wise.  This returns a boolean ``True`` or ``False`` for each table
+  *element*, so one gets a `~astropy.table.Table` of values.
+
+Examples::
+
+  >>> t1 = Table(rows=[[1, 2, 3],
+  ...                  [4, 5, 6],
+  ...                  [7, 7, 9]], names=['a', 'b', 'c'])
+  >>> t2 = Table(rows=[[1, 2, -1],
+  ...                  [4, -1, 6],
+  ...                  [7, 7, 9]], names=['a', 'b', 'c'])
+
+  >>> t1 == t2
+  array([False, False,  True])
+
+  >>> t1.values_equal(t2)  # Compare to another table
+

+   a     b     c
+  bool  bool  bool
+  ---- ----- -----
+  True  True False
+  True False  True
+  True  True  True
+
+  >>> t1.values_equal([2, 4, 7])  # Compare to an array column-wise
+

+    a     b     c
+   bool  bool  bool
+  ----- ----- -----
+  False  True False
+   True False False
+   True  True False
+
+  >>> t1.values_equal(7)  # Compare to a scalar column-wise
+

+    a     b     c
+   bool  bool  bool
+  ----- ----- -----
+  False False False
+  False False False
+   True  True False
+
 Formatted printing
 ------------------

diff --git a/astropy/table/tests/test_table.py b/astropy/table/tests/test_table.py
index 188b908a8a8a..fa787ebd04d7 100644
--- a/astropy/table/tests/test_table.py
+++ b/astropy/table/tests/test_table.py
@@ -15,6 +15,7 @@
 from astropy.table import Table, QTable, MaskedColumn
 from astropy.tests.helper import (assert_follows_unicode_guidelines,
                                   ignore_warnings, catch_warnings)
+from astropy.coordinates import SkyCoord

 from astropy.utils.data import get_pkg_data_filename
 from astropy import table
@@ -1382,7 +1383,71 @@ def test_disallow_inequality_comparisons():
         t <= -1.1

-def test_equality():
+def test_values_equal_part1():
+
+    col1 = [1, 2]
+    col2 = [1.0, 2.0]
+    col3 = ['a', 'b']
+    t1 = table.Table([col1, col2, col3], names=['a', 'b', 'c'])
+    t2 = table.Table([col1, col2], names=['a', 'b'])
+    t3 = table.table_helpers.simple_table()
+    tm = t1.copy()
+    tm['time'] = Time([1, 2], format='cxcsec')
+    tm1 = tm.copy()
+    tm1['time'][0] = np.ma.masked
+
+    tq = table.table_helpers.simple_table()
+    tq['quantity'] = [1., 2., 3.]*u.m
+
+    tsk = table.table_helpers.simple_table()
+    tsk['sk'] = SkyCoord(1, 2,  unit='deg')
+
+    with pytest.raises(ValueError, match='cannot compare tables with different column names'):
+        t2.values_equal(t1)
+
+    with pytest.raises(ValueError, match='unable to compare column a'):
+        # Shape mismatch
+        t3.values_equal(t1)
+
+    with pytest.raises(ValueError, match='unable to compare column c'):
+        # Type mismatch in column c causes FutureWarning
+        t1.values_equal(2)
+
+    with pytest.raises(ValueError, match='unable to compare column c'):
+        t1.values_equal([1, 2])
+
+    with pytest.raises(TypeError, match='comparison for column sk'):
+        tsk.values_equal(tsk)
+
+    eq = t2.values_equal(t2)
+    for col in eq.colnames:
+        assert np.all(eq[col] == [True, True])
+
+    eq1 = tm1.values_equal(tm)
+    for col in eq1.colnames:
+        assert np.all(eq1[col] == [True, True])
+
+    eq2 = tq.values_equal(tq)
+    for col in eq2.colnames:
+        assert np.all(eq2[col] == [True, True, True])
+
+    eq3 = t2.values_equal(2)
+    for col in eq3.colnames:
+        assert np.all(eq3[col] == [False, True])
+
+    eq4 = t2.values_equal([1, 2])
+    for col in eq4.colnames:
+        assert np.all(eq4[col] == [True, True])
+
+    # Compare table to its first row
+    t = table.Table(rows=[(1, 'a'),
+                          (1, 'b')])
+    eq = t.values_equal(t[0])
+    assert np.all(eq['col0'] == [True, True])
+    assert np.all(eq['col1'] == [True, False])
+
+
+def test_rows_equal():

     t = table.Table.read([' a b  c  d',
                           ' 2 c 7.0 0',
@@ -1392,8 +1457,8 @@ def test_equality():
                           ' 0 a 0.0 4',
                           ' 1 b 3.0 5',
                           ' 1 a 2.0 6',
-                          ' 1 a 1.0 7',
-                         ], format='ascii')
+                          ' 1 a 1.0 7'],
+                         format='ascii')

     # All rows are equal
     assert np.all(t == t)

astropy/table/tests/test_table.py::TestMetaTable::test_non_mapping_init[ceci n'est pas un meta]

astropy/table/tests/test_table.py::TestMetaTable::test_non_mapping_set[ceci n'est pas un meta]

diff --git a/docs/table/access_table.rst b/docs/table/access_table.rst
index a27604fa92..cf472aeb56 100644
--- a/docs/table/access_table.rst
+++ b/docs/table/access_table.rst
@@ -314,6 +314,56 @@ structured array by creating a copy or reference with ``np.array``::
   >>> data = np.array(t, copy=False)  # reference to data in t

+Table Equality
+--------------
+We can check table data equality using two different methods:
+
+- The ``==`` comparison operator.  This returns a ``True`` or ``False`` for
+  each row if the *entire row* matches.  This is the same as the behavior of
+  numpy structured arrays.
+- Table :meth:`~astropy.table.Table.values_equal` to compare table values
+  element-wise.  This returns a boolean ``True`` or ``False`` for each table
+  *element*, so one gets a `~astropy.table.Table` of values.
+
+Examples::
+
+  >>> t1 = Table(rows=[[1, 2, 3],
+  ...                  [4, 5, 6],
+  ...                  [7, 7, 9]], names=['a', 'b', 'c'])
+  >>> t2 = Table(rows=[[1, 2, -1],
+  ...                  [4, -1, 6],
+  ...                  [7, 7, 9]], names=['a', 'b', 'c'])
+
+  >>> t1 == t2
+  array([False, False,  True])
+
+  >>> t1.values_equal(t2)  # Compare to another table
+

+   a     b     c
+  bool  bool  bool
+  ---- ----- -----
+  True  True False
+  True False  True
+  True  True  True
+
+  >>> t1.values_equal([2, 4, 7])  # Compare to an array column-wise
+

+    a     b     c
+   bool  bool  bool
+  ----- ----- -----
+  False  True False
+   True False False
+   True  True False
+
+  >>> t1.values_equal(7)  # Compare to a scalar column-wise
+

+    a     b     c
+   bool  bool  bool
+  ----- ----- -----
+  False False False
+  False False False
+   True  True False
+
 Formatted printing
 ------------------

If completing this task requires creating new files, classes, fields, or error messages, you may consider using the following suggested entity names:
[{'type': 'error_msg', 'name': 'cannot compare tables with different column names'}]


diff --git a/CHANGES.rst b/CHANGES.rst
index 3c8869f97b64..ded568008cfd 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -91,6 +91,9 @@ astropy.table
 - ``MaskedColumn.data`` will now return a plain ``MaskedArray`` rather than
   the previous (unintended) ``masked_BaseColumn``. [#8855]

+- Adding depth-wise stacking ``cstack()`` in higher level table operation.
+  It help will in stacking table column depth-wise. [#8939]
+
 astropy.tests
 ^^^^^^^^^^^^^

diff --git a/astropy/table/__init__.py b/astropy/table/__init__.py
index effa98c74657..66a1645c6c6b 100644
--- a/astropy/table/__init__.py
+++ b/astropy/table/__init__.py
@@ -42,7 +42,7 @@ class Conf(_config.ConfigNamespace):
 from .groups import TableGroups, ColumnGroups
 from .table import (Table, QTable, TableColumns, Row, TableFormatter,
                     NdarrayMixin, TableReplaceWarning)
-from .operations import join, setdiff, hstack, vstack, unique, TableMergeError
+from .operations import join, setdiff, hstack, cstack, vstack, unique, TableMergeError
 from .bst import BST, FastBST, FastRBT
 from .sorted_array import SortedArray
 from .soco import SCEngine
diff --git a/astropy/table/operations.py b/astropy/table/operations.py
index 213898c221c2..105b093f77e3 100644
--- a/astropy/table/operations.py
+++ b/astropy/table/operations.py
@@ -5,6 +5,7 @@
 - setdiff()
 - hstack()
 - vstack()
+- cstack()
 """
 # Licensed under a 3-clause BSD style license - see LICENSE.rst

@@ -231,6 +232,93 @@ def setdiff(table1, table2, keys=None):
     return t12_diff

+def cstack(tables, join_type='outer', metadata_conflicts='warn'):
+    """
+    Stack columns within tables depth-wise
+
+    A ``join_type`` of 'exact' means that the tables must all have exactly
+    the same column names (though the order can vary).  If ``join_type``
+    is 'inner' then the intersection of common columns will be the output.
+    A value of 'outer' (default) means the output will have the union of
+    all columns, with table values being masked where no common values are
+    available.
+
+    Parameters
+    ----------
+    tables : Table or list of Table objects
+        Table(s) to stack along depth-wise with the current table
+        Table columns should have same shape and name for depth-wise stacking
+    join_type : str
+        Join type ('inner' | 'exact' | 'outer'), default is 'outer'
+    metadata_conflicts : str
+        How to proceed with metadata conflicts. This should be one of:
+            * ``'silent'``: silently pick the last conflicting meta-data value
+            * ``'warn'``: pick the last conflicting meta-data value, but emit a warning (default)
+            * ``'error'``: raise an exception.
+
+    Returns
+    -------
+    stacked_table : `~astropy.table.Table` object
+        New table containing the stacked data from the input tables.
+
+    Examples
+    --------
+    To stack two tables along rows do::
+
+      >>> from astropy.table import vstack, Table
+      >>> t1 = Table({'a': [1, 2], 'b': [3, 4]}, names=('a', 'b'))
+      >>> t2 = Table({'a': [5, 6], 'b': [7, 8]}, names=('a', 'b'))
+      >>> print(t1)
+       a   b
+      --- ---
+        1   3
+        2   4
+      >>> print(t2)
+       a   b
+      --- ---
+        5   7
+        6   8
+      >>> print(cstack([t1, t2]))
+      a [2]  b [2]
+      ------ ------
+      1 .. 5 3 .. 7
+      2 .. 6 4 .. 8
+    """
+    tables = _get_list_of_tables(tables)
+    if len(tables) == 1:
+        return tables[0]  # no point in stacking a single table
+
+    n_rows = set(len(table) for table in tables)
+    if len(n_rows) != 1:
+        raise ValueError('Table lengths must all match for cstack')
+    n_row = n_rows.pop()
+
+    out = vstack(tables, join_type, metadata_conflicts)
+
+    for name, col in out.columns.items():
+        col = out[name]
+
+        # Reshape to so each original column is now in a row.
+        # If entries are not 0-dim then those additional shape dims
+        # are just carried along.
+        # [x x x y y y] => [[x x x],
+        #                   [y y y]]
+        col.shape = (len(tables), n_row) + col.shape[1:]
+
+        # Transpose the table and row axes to get to
+        # [[x, y],
+        #  [x, y]
+        #  [x, y]]
+        axes = np.arange(len(col.shape))
+        axes[:2] = [1, 0]
+
+        # This temporarily makes `out` be corrupted (columns of different
+        # length) but it all works out in the end.
+        out.columns.__setitem__(name, col.transpose(axes), validated=True)
+
+    return out
+
+
 def vstack(tables, join_type='outer', metadata_conflicts='warn'):
     """
     Stack tables vertically (along rows)
diff --git a/docs/table/operations.rst b/docs/table/operations.rst
index 3f44210d851f..504b0b8122b1 100644
--- a/docs/table/operations.rst
+++ b/docs/table/operations.rst
@@ -591,6 +591,56 @@ The metadata from the input tables is merged by the process described in the `Me
 metadata`_ section.  Note also that you can use a single table row instead of a
 full table as one of the inputs.

+.. _stack-depthwise:
+
+Stack depth-wise
+----------------
+
+The |Table| class supports stacking columns within tables depth-wise using
+the `~astropy.table.cstack` function. It corresponds roughly
+to running the `numpy.dstack` function on the individual columns matched
+by name.
+
+For example, suppose one has tables of data for sources giving information on the enclosed
+source counts for different PSF fractions::
+
+  >>> from astropy.table import Table, cstack
+  >>> src1 = Table.read("""psf_frac  counts
+  ...                      0.10        45
+  ...                      0.50        90
+  ...                      0.90       120
+  ...                      """, format='ascii')
+
+  >>> src2 = Table.read("""psf_frac  counts
+  ...                      0.10       200
+  ...                      0.50       300
+  ...                      0.90       350
+  ...                      """, format='ascii')
+
+Now we can stack these two tables depth-wise to get a single table with the
+characteristics of both sources::
+
+  >>> srcs = cstack([src1, src2])
+  >>> print(srcs)
+  psf_frac [2] counts [2]
+  ------------ ----------
+    0.1 .. 0.1  45 .. 200
+    0.5 .. 0.5  90 .. 300
+    0.9 .. 0.9 120 .. 350
+
+In this case the counts for the first source are accessible as ``srcs['counts'][:, 0]``,
+and likewise the second source counts are ``srcs['counts'][:, 1]``.
+
+For this function the length of all input tables must be the same.  This function can
+accept ``join_type`` and ``metadata_conflicts`` just like the `~astropy.table.vstack`
+function.  The ``join_type`` argument controls how to handle mismatches in the columns of
+the input table.
+
+See also the sections on `Merging metadata`_ and `Merging column
+attributes`_ for details on how these characteristics of the input tables are merged in
+the single output table.  Note also that you can use a single table row instead of a
+full table as one of the inputs.
+
 .. _table-join:

 Join

diff --git a/astropy/table/tests/test_operations.py b/astropy/table/tests/test_operations.py
index 3cb6652d90ed..3e4ba76c6732 100644
--- a/astropy/table/tests/test_operations.py
+++ b/astropy/table/tests/test_operations.py
@@ -1004,6 +1004,123 @@ def test_mixin_functionality(self, mixin_cols):
                     'vstack unavailable' in str(err.value))

+class TestCStack():
+
+    def _setup(self, t_cls=Table):
+        self.t1 = t_cls.read([' a   b',
+                              ' 0. foo',
+                              ' 1. bar'], format='ascii')
+
+        self.t2 = t_cls.read([' a    b   c',
+                              ' 2.  pez  4',
+                              ' 3.  sez  5'], format='ascii')
+        self.t2['d'] = Time([1, 2], format='cxcsec')
+
+        self.t3 = t_cls({'a': [[5., 6.], [4., 3.]],
+                         'b': [['foo', 'bar'], ['pez', 'sez']]},
+                        names=('a', 'b'))
+
+        self.t4 = t_cls(self.t1, copy=True, masked=t_cls is Table)
+
+        self.t5 = t_cls({'a': [[4., 2.], [1., 6.]],
+                         'b': [['foo', 'pez'], ['bar', 'sez']]},
+                        names=('a', 'b'))
+        self.t6 = t_cls.read([' a    b   c',
+                              ' 7.  pez  2',
+                              ' 4.  sez  6',
+                              ' 6.  foo  3'], format='ascii')
+
+    @staticmethod
+    def compare_cstack(tables, out):
+        for ii, tbl in enumerate(tables):
+            for name, out_col in out.columns.items():
+                if name in tbl.colnames:
+                    # Columns always compare equal
+                    assert np.all(tbl[name] == out[name][:, ii])
+
+                    # If input has a mask then output must have same mask
+                    if hasattr(tbl[name], 'mask'):
+                        assert np.all(tbl[name].mask == out[name].mask[:, ii])
+
+                    # If input has no mask then output might have a mask (if other table
+                    # is missing that column). If so then all mask values should be False.
+                    elif hasattr(out[name], 'mask'):
+                        assert not np.any(out[name].mask[:, ii])
+
+                else:
+                    # Column missing for this table, out must have a mask with all True.
+                    assert np.all(out[name].mask[:, ii])
+
+    def test_cstack_table_column(self, operation_table_type):
+        """Stack a table with 3 cols and one column (gets auto-converted to Table).
+        """
+        self._setup(operation_table_type)
+        t2 = self.t1.copy()
+        out = table.cstack([self.t1, t2['a']])
+        self.compare_cstack([self.t1, t2[('a',)]], out)
+
+    def test_cstack_basic_outer(self, operation_table_type):
+        if operation_table_type is QTable:
+            pytest.xfail('Quantity columns do not support masking.')
+        self._setup(operation_table_type)
+        t1 = self.t1
+        t2 = self.t2
+        t4 = self.t4
+        t4['a'].mask[0] = True
+        # Test for non-masked table
+        t12 = table.cstack([t1, t2], join_type='outer')
+        assert type(t12) is operation_table_type
+        assert type(t12['a']) is type(t1['a'])
+        assert type(t12['b']) is type(t1['b'])
+        self.compare_cstack([t1, t2], t12)
+
+        # Test for masked table
+        t124 = table.cstack([t1, t2, t4], join_type='outer')
+        assert type(t124) is operation_table_type
+        assert type(t124['a']) is type(t4['a'])
+        assert type(t124['b']) is type(t4['b'])
+        self.compare_cstack([t1, t2, t4], t124)
+
+    def test_cstack_basic_inner(self, operation_table_type):
+        self._setup(operation_table_type)
+        t1 = self.t1
+        t2 = self.t2
+        t4 = self.t4
+
+        # Test for masked table
+        t124 = table.cstack([t1, t2, t4], join_type='inner')
+        assert type(t124) is operation_table_type
+        assert type(t124['a']) is type(t4['a'])
+        assert type(t124['b']) is type(t4['b'])
+        self.compare_cstack([t1, t2, t4], t124)
+
+    def test_cstack_multi_dimension_column(self, operation_table_type):
+        self._setup(operation_table_type)
+        t3 = self.t3
+        t5 = self.t5
+        t2 = self.t2
+        t35 = table.cstack([t3, t5])
+        assert type(t35) is operation_table_type
+        assert type(t35['a']) is type(t3['a'])
+        assert type(t35['b']) is type(t3['b'])
+        self.compare_cstack([t3, t5], t35)
+
+        with pytest.raises(TableMergeError):
+            table.cstack([t2, t3])
+
+    def test_cstack_different_length_table(self, operation_table_type):
+        self._setup(operation_table_type)
+        t2 = self.t2
+        t6 = self.t6
+        with pytest.raises(ValueError):
+            table.cstack([t2, t6])
+
+    def test_cstack_single_table(self):
+        self._setup(Table)
+        out = table.cstack(self.t1)
+        assert np.all(out == self.t1)
+
+
 class TestHStack():

     def _setup(self, t_cls=Table):

diff --git a/docs/table/operations.rst b/docs/table/operations.rst
index 3f44210d85..504b0b8122 100644
--- a/docs/table/operations.rst
+++ b/docs/table/operations.rst
@@ -591,6 +591,56 @@ The metadata from the input tables is merged by the process described in the `Me
 metadata`_ section.  Note also that you can use a single table row instead of a
 full table as one of the inputs.

+.. _stack-depthwise:
+
+Stack depth-wise
+----------------
+
+The |Table| class supports stacking columns within tables depth-wise using
+the `~astropy.table.cstack` function. It corresponds roughly
+to running the `numpy.dstack` function on the individual columns matched
+by name.
+
+For example, suppose one has tables of data for sources giving information on the enclosed
+source counts for different PSF fractions::
+
+  >>> from astropy.table import Table, cstack
+  >>> src1 = Table.read("""psf_frac  counts
+  ...                      0.10        45
+  ...                      0.50        90
+  ...                      0.90       120
+  ...                      """, format='ascii')
+
+  >>> src2 = Table.read("""psf_frac  counts
+  ...                      0.10       200
+  ...                      0.50       300
+  ...                      0.90       350
+  ...                      """, format='ascii')
+
+Now we can stack these two tables depth-wise to get a single table with the
+characteristics of both sources::
+
+  >>> srcs = cstack([src1, src2])
+  >>> print(srcs)
+  psf_frac [2] counts [2]
+  ------------ ----------
+    0.1 .. 0.1  45 .. 200
+    0.5 .. 0.5  90 .. 300
+    0.9 .. 0.9 120 .. 350
+
+In this case the counts for the first source are accessible as ``srcs['counts'][:, 0]``,
+and likewise the second source counts are ``srcs['counts'][:, 1]``.
+
+For this function the length of all input tables must be the same.  This function can
+accept ``join_type`` and ``metadata_conflicts`` just like the `~astropy.table.vstack`
+function.  The ``join_type`` argument controls how to handle mismatches in the columns of
+the input table.
+
+See also the sections on `Merging metadata`_ and `Merging column
+attributes`_ for details on how these characteristics of the input tables are merged in
+the single output table.  Note also that you can use a single table row instead of a
+full table as one of the inputs.
+
 .. _table-join:

 Join


diff --git a/CHANGES.rst b/CHANGES.rst
index 80b4e8d3406e..95d78702124c 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -129,9 +129,12 @@ astropy.table
 - ``MaskedColumn.data`` will now return a plain ``MaskedArray`` rather than
   the previous (unintended) ``masked_BaseColumn``. [#8855]

-- Adding depth-wise stacking ``cstack()`` in higher level table operation.
+- Added depth-wise stacking ``cstack()`` in higher level table operation.
   It help will in stacking table column depth-wise. [#8939]

+- Added new ``join_type='cartesian'`` option to the ``join`` operation.
+  [#9288]
+
 - Allow adding a table column as a list of mixin-type objects, for instance
   ``t['q'] = [1 * u.m, 2 * u.m]``. [#9165]

diff --git a/astropy/table/operations.py b/astropy/table/operations.py
index f6810b2dc8c8..46a84b1a764a 100644
--- a/astropy/table/operations.py
+++ b/astropy/table/operations.py
@@ -104,7 +104,7 @@ def join(left, right, keys=None, join_type='inner',
         Name(s) of column(s) used to match rows of left and right tables.
         Default is to use all columns which are common to both tables.
     join_type : str
-        Join type ('inner' | 'outer' | 'left' | 'right'), default is 'inner'
+        Join type ('inner' | 'outer' | 'left' | 'right' | 'cartesian'), default is 'inner'
     uniq_col_name : str or None
         String generate a unique output column name in case of a conflict.
         The default is '{col_name}_{table_name}'.
@@ -777,7 +777,7 @@ def _join(left, right, keys=None, join_type='inner',
         Name(s) of column(s) used to match rows of left and right tables.
         Default is to use all columns which are common to both tables.
     join_type : str
-        Join type ('inner' | 'outer' | 'left' | 'right'), default is 'inner'
+        Join type ('inner' | 'outer' | 'left' | 'right' | 'cartesian'), default is 'inner'
     uniq_col_name : str or None
         String generate a unique output column name in case of a conflict.
         The default is '{col_name}_{table_name}'.
@@ -801,11 +801,27 @@ def _join(left, right, keys=None, join_type='inner',
     # Store user-provided col_name_map until the end
     _col_name_map = col_name_map

-    if join_type not in ('inner', 'outer', 'left', 'right'):
+    # Special column name for cartesian join, should never collide with real column
+    cartesian_index_name = '__table_cartesian_join_temp_index__'
+
+    if join_type not in ('inner', 'outer', 'left', 'right', 'cartesian'):
         raise ValueError("The 'join_type' argument should be in 'inner', "
-                         "'outer', 'left' or 'right' (got '{}' instead)".
+                         "'outer', 'left', 'right', or 'cartesian' "
+                         "(got '{}' instead)".
                          format(join_type))

+    if join_type == 'cartesian':
+        if keys:
+            raise ValueError('cannot supply keys for a cartesian join')
+
+        # Make light copies of left and right, then add temporary index columns
+        # with all the same value so later an outer join turns into a cartesian join.
+        left = left.copy(copy_data=False)
+        right = right.copy(copy_data=False)
+        left[cartesian_index_name] = np.uint8(0)
+        right[cartesian_index_name] = np.uint8(0)
+        keys = (cartesian_index_name, )
+
     # If we have a single key, put it in a tuple
     if keys is None:
         keys = tuple(name for name in left.colnames if name in right.colnames)
@@ -840,13 +856,16 @@ def _join(left, right, keys=None, join_type='inner',

     # Main inner loop in Cython to compute the cartesian product
     # indices for the given join type
-    int_join_type = {'inner': 0, 'outer': 1, 'left': 2, 'right': 3}[join_type]
+    int_join_type = {'inner': 0, 'outer': 1, 'left': 2, 'right': 3,
+                     'cartesian': 1}[join_type]
     masked, n_out, left_out, left_mask, right_out, right_mask = \
         _np_utils.join_inner(idxs, idx_sort, len_left, int_join_type)

     out = _get_out_class([left, right])()

     for out_name, dtype, shape in out_descrs:
+        if out_name == cartesian_index_name:
+            continue

         left_name, right_name = col_name_map[out_name]
         if left_name and right_name:  # this is a key which comes from left and right
diff --git a/docs/table/operations.rst b/docs/table/operations.rst
index fc40c8d63bb7..2c026fbd8a31 100644
--- a/docs/table/operations.rst
+++ b/docs/table/operations.rst
@@ -721,7 +721,7 @@ matching key includes both ``name`` and ``obs_date``.  Specifying the key as onl
 Likewise one can construct a new table with every row of the right table and matching left
 values (when available) using ``join_type='right'``.

-Finally, to make a table with the union of rows from both tables do an "outer" join::
+To make a table with the union of rows from both tables do an "outer" join::

   >>> print(join(optical, xray, join_type='outer'))
     name   obs_date  mag_b mag_v logLx
@@ -732,9 +732,26 @@ Finally, to make a table with the union of rows from both tables do an "outer" j
       M82 2012-10-29  16.2  15.2  45.0
   NGC3516 2011-11-11    --    --  42.1

-In all cases the output join table will be sorted by the key column(s) and in general
+In all the above cases the output join table will be sorted by the key column(s) and in general
 will not preserve the row order of the input tables.

+Finally, one can do a "cartesian" join, which is the cartesian product of all available
+rows.  In this case one there are no key columns (and supplying a ``keys`` argument is
+an error)::
+
+  >>> print(join(optical, xray, join_type='cartesian'))
+  name_1 obs_date_1 mag_b mag_v  name_2 obs_date_2 logLx
+  ------ ---------- ----- ----- ------- ---------- -----
+     M31 2012-01-02  17.0  16.0 NGC3516 2011-11-11  42.1
+     M31 2012-01-02  17.0  16.0     M31 1999-01-05  43.1
+     M31 2012-01-02  17.0  16.0     M82 2012-10-29  45.0
+     M82 2012-10-29  16.2  15.2 NGC3516 2011-11-11  42.1
+     M82 2012-10-29  16.2  15.2     M31 1999-01-05  43.1
+     M82 2012-10-29  16.2  15.2     M82 2012-10-29  45.0
+    M101 2012-10-31  15.1  15.5 NGC3516 2011-11-11  42.1
+    M101 2012-10-31  15.1  15.5     M31 1999-01-05  43.1
+    M101 2012-10-31  15.1  15.5     M82 2012-10-29  45.0
+
 Non-identical key column names
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

diff --git a/astropy/table/tests/test_operations.py b/astropy/table/tests/test_operations.py
index da5f9f1020be..6da2f2a8eff5 100644
--- a/astropy/table/tests/test_operations.py
+++ b/astropy/table/tests/test_operations.py
@@ -547,15 +547,36 @@ def test_mixin_functionality(self, mixin_cols):
                 assert ('join requires masking' in str(err.value) or
                         'join unavailable' in str(err.value))

+    def test_cartesian_join(self, operation_table_type):
+        t1 = Table(rows=[(1, 'a'),
+                         (2, 'b')], names=['a', 'b'])
+        t2 = Table(rows=[(3, 'c'),
+                         (4, 'd')], names=['a', 'c'])
+        t12 = table.join(t1, t2, join_type='cartesian')
+
+        assert t1.colnames == ['a', 'b']
+        assert t2.colnames == ['a', 'c']
+        assert len(t12) == len(t1) * len(t2)
+        assert str(t12).splitlines() == [
+            'a_1  b  a_2  c ',
+            '--- --- --- ---',
+            '  1   a   3   c',
+            '  1   a   4   d',
+            '  2   b   3   c',
+            '  2   b   4   d']
+
+        with pytest.raises(ValueError, match='cannot supply keys for a cartesian join'):
+            t12 = table.join(t1, t2, join_type='cartesian', keys='a')
+

 class TestSetdiff():

     def _setup(self, t_cls=Table):
         lines1 = [' a   b ',
-                 '  0 foo ',
-                 '  1 foo ',
-                 '  1 bar ',
-                 '  2 bar ']
+                  '  0 foo ',
+                  '  1 foo ',
+                  '  1 bar ',
+                  '  2 bar ']
         lines2 = [' a   b ',
                   '  0 foo ',
                   '  3 foo ',

diff --git a/docs/table/operations.rst b/docs/table/operations.rst
index fc40c8d63b..2c026fbd8a 100644
--- a/docs/table/operations.rst
+++ b/docs/table/operations.rst
@@ -721,7 +721,7 @@ matching key includes both ``name`` and ``obs_date``.  Specifying the key as onl
 Likewise one can construct a new table with every row of the right table and matching left
 values (when available) using ``join_type='right'``.

-Finally, to make a table with the union of rows from both tables do an "outer" join::
+To make a table with the union of rows from both tables do an "outer" join::

   >>> print(join(optical, xray, join_type='outer'))
     name   obs_date  mag_b mag_v logLx
@@ -732,9 +732,26 @@ Finally, to make a table with the union of rows from both tables do an "outer" j
       M82 2012-10-29  16.2  15.2  45.0
   NGC3516 2011-11-11    --    --  42.1

-In all cases the output join table will be sorted by the key column(s) and in general
+In all the above cases the output join table will be sorted by the key column(s) and in general
 will not preserve the row order of the input tables.

+Finally, one can do a "cartesian" join, which is the cartesian product of all available
+rows.  In this case one there are no key columns (and supplying a ``keys`` argument is
+an error)::
+
+  >>> print(join(optical, xray, join_type='cartesian'))
+  name_1 obs_date_1 mag_b mag_v  name_2 obs_date_2 logLx
+  ------ ---------- ----- ----- ------- ---------- -----
+     M31 2012-01-02  17.0  16.0 NGC3516 2011-11-11  42.1
+     M31 2012-01-02  17.0  16.0     M31 1999-01-05  43.1
+     M31 2012-01-02  17.0  16.0     M82 2012-10-29  45.0
+     M82 2012-10-29  16.2  15.2 NGC3516 2011-11-11  42.1
+     M82 2012-10-29  16.2  15.2     M31 1999-01-05  43.1
+     M82 2012-10-29  16.2  15.2     M82 2012-10-29  45.0
+    M101 2012-10-31  15.1  15.5 NGC3516 2011-11-11  42.1
+    M101 2012-10-31  15.1  15.5     M31 1999-01-05  43.1
+    M101 2012-10-31  15.1  15.5     M82 2012-10-29  45.0
+
 Non-identical key column names
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

If completing this task requires creating new files, classes, fields, or error messages, you may consider using the following suggested entity names:
[{'type': 'error_msg', 'name': 'cannot supply keys for a cartesian join'}]


diff --git a/CHANGES.rst b/CHANGES.rst
index 67d0d76684a9..085cc9382632 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -71,7 +71,12 @@ astropy.modeling
 - Add ``Tabular1D.inverse`` [#9083]
 - Significant reorganization of the documentation. [#9078, #9171]

-- ``Model.rename`` was changed to add the ability to rename ``Model.inputs`` and ``Model.outputs``. [#9220]
+- ``Model.rename`` was changed to add the ability to rename ``Model.inputs``
+  and ``Model.outputs``. [#9220]
+
+- New function ``fix_inputs`` to generate new models from others by fixing
+  specific inputs variable values to constants. [#9135]
+

 astropy.nddata
 ^^^^^^^^^^^^^^
diff --git a/astropy/io/misc/asdf/tags/transform/compound.py b/astropy/io/misc/asdf/tags/transform/compound.py
index b228c0f9de6d..aa41693138d9 100644
--- a/astropy/io/misc/asdf/tags/transform/compound.py
+++ b/astropy/io/misc/asdf/tags/transform/compound.py
@@ -22,7 +22,8 @@
     '/'  : 'divide',
     '**' : 'power',
     '|'  : 'compose',
-    '&'  : 'concatenate'
+    '&'  : 'concatenate',
+    'fix_inputs': 'fix_inputs'
 }

@@ -33,7 +34,8 @@
     'divide'      : '__truediv__',
     'power'       : '__pow__',
     'compose'     : '__or__',
-    'concatenate' : '__and__'
+    'concatenate' : '__and__',
+    'fix_inputs'  : 'fix_inputs'
 }

@@ -54,10 +56,15 @@ def from_tree_tagged(cls, node, ctx):
                 node['forward'][0]._tag))
         right = yamlutil.tagged_tree_to_custom_tree(
             node['forward'][1], ctx)
-        if not isinstance(right, Model):
+        if not isinstance(right, Model) and \
+            not (oper == 'fix_inputs' and isinstance(right, dict)):
             raise TypeError("Unknown model type '{0}'".format(
                 node['forward'][1]._tag))
-        model = getattr(left, oper)(right)
+        if oper == 'fix_inputs':
+            right = dict(zip(right['keys'], right['values']))
+            model = CompoundModel('fix_inputs', left, right)
+        else:
+            model = getattr(left, oper)(right)

         model = cls._from_tree_base_transform_members(model, node, ctx)
         model.map_parameters()
@@ -73,8 +80,13 @@ def _to_tree_from_model_tree(cls, tree, ctx):
             left = cls._to_tree_from_model_tree(tree.left, ctx)

         if not isinstance(tree.right, CompoundModel):
-            right = yamlutil.custom_tree_to_tagged_tree(
-                tree.right, ctx)
+            if isinstance(tree.right, dict):
+                right = {'keys': list(tree.right.keys()),
+                         'values': list(tree.right.values())
+                        }
+            else:
+                right = yamlutil.custom_tree_to_tagged_tree(
+                    tree.right, ctx)
         else:
             right = cls._to_tree_from_model_tree(tree.right, ctx)

diff --git a/astropy/modeling/core.py b/astropy/modeling/core.py
index debfa057e753..6af24eee49eb 100644
--- a/astropy/modeling/core.py
+++ b/astropy/modeling/core.py
@@ -47,7 +47,7 @@

 __all__ = ['Model', 'FittableModel', 'Fittable1DModel', 'Fittable2DModel',
-           'CompoundModel', 'custom_model', 'ModelDefinitionError']
+           'CompoundModel', 'fix_inputs', 'custom_model', 'ModelDefinitionError']

 def _model_oper(oper, **kwargs):
@@ -103,6 +103,7 @@ def __new__(mcls, name, bases, members):
             ('__pow__', _model_oper('**')),
             ('__or__', _model_oper('|')),
             ('__and__', _model_oper('&')),
+            ('_fix_inputs', _model_oper('fix_inputs'))
         ]

         for opermethod, opercall in opermethods:
@@ -455,6 +456,7 @@ def __init__(self, *params, **kwargs):
     __pow__ = _model_oper('**')
     __or__ = _model_oper('|')
     __and__ = _model_oper('&')
+    _fix_inputs = _model_oper('fix_inputs')

     # *** Other utilities ***

@@ -2315,14 +2317,13 @@ def __init__(self, op, left, right, name=None, inverse=None):
         self._has_inverse = False  # may be set to True in following code
         if inverse:
             self.inverse = inverse
-        # else:
-        #     self._user_inverse = None
-        if len(left) != len(right):
+
+        if op != 'fix_inputs' and len(left) != len(right):
             raise ValueError(
                 'Both operands must have equal values for n_models')
         else:
             self._n_models = len(left)
-        if ((left.model_set_axis != right.model_set_axis)
+        if op != 'fix_inputs' and ((left.model_set_axis != right.model_set_axis)
                 or left.model_set_axis):  # not False and not 0
             raise ValueError("model_set_axis must be False or 0 and consistent for operands")
         else:
@@ -2368,15 +2369,62 @@ def __init__(self, op, left, right, name=None, inverse=None):
                                               self.right.inverse,
                                               self.left.inverse,
                                               inverse=self)
+        elif op == 'fix_inputs':
+            if not isinstance(left, Model):
+                raise ValueError('First argument to "fix_inputs" must be an instance of an astropy Model.')
+            if not isinstance(right, dict):
+                raise ValueError('Expected a dictionary for second argument of "fix_inputs".')
+
+            # Dict keys must match either possible indices
+            # for model on left side, or names for inputs.
+            self.n_inputs = left.n_inputs - len(right)
+            self.outputs = left.outputs
+            self.n_outputs = left.n_outputs
+            newinputs = list(left.inputs)
+            keys = right.keys()
+            input_ind = []
+            for key in keys:
+                if isinstance(key, int):
+                    if key >= left.n_inputs or key < 0:
+                        raise ValueError(
+                            'Substitution key integer value '
+                            'not among possible input choices.')
+                    else:
+                        if key in input_ind:
+                            raise ValueError("Duplicate specification of "
+                                             "same input (index/name).")
+                        else:
+                            input_ind.append(key)
+                elif isinstance(key, str):
+                    if key not in left.inputs:
+                        raise ValueError(
+                            'Substitution key string not among possible '
+                            'input choices.')
+                    # Check to see it doesn't match positional
+                    # specification.
+                    ind = left.inputs.index(key)
+                    if ind in input_ind:
+                        raise ValueError("Duplicate specification of "
+                                         "same input (index/name).")
+                    else:
+                        input_ind.append(ind)
+            # Remove substituted inputs
+            input_ind.sort()
+            input_ind.reverse()
+            for ind in input_ind:
+                del newinputs[ind]
+            self.inputs = tuple(newinputs)
+            # Now check to see if the input model has bounding_box defined.
+            # If so, remove the appropriate dimensions and set it for this
+            # instance.
+            try:
+                bounding_box = self.left.bounding_box
+                self._fix_input_bounding_box(input_ind)
+            except NotImplementedError:
+                pass
+
         else:
-            #raise ModelDefinitionError('Illegal operator: ', self.op)
-            raise ModelDefinitionError(
-                "Unsupported operands for {0}: {1} (n_inputs={2}, "
-                "n_outputs={3}) and {4} (n_inputs={5}, n_outputs={6}); "
-                "models must have the same n_inputs and the same "
-                "n_outputs for this operator".format(
-                    operator, left.name, left.n_inputs, left.n_outputs,
-                    right.name, right.n_inputs, right.n_outputs))
+            raise ModelDefinitionError('Illegal operator: ', self.op)
         self.name = name
         self._fittable = None
         self.fit_deriv = None
@@ -2475,38 +2523,81 @@ def __call__(self, *args, **kw):

     def _evaluate(self, *args, **kw):
         op = self.op
-        if op != '&':
-            leftval = self.left(*args, **kw)
-            if op != '|':
-                rightval = self.right(*args, **kw)
-        else:
-            leftval = self.left(*(args[:self.left.n_inputs]), **kw)
-            rightval = self.right(*(args[self.left.n_inputs:]), **kw)
-        if op == '+':
-            return binary_operation(operator.add, leftval, rightval)
-        elif op == '-':
-            return binary_operation(operator.sub, leftval, rightval)
-        elif op == '*':
-            return binary_operation(operator.mul, leftval, rightval)
-        elif op == '/':
-            return binary_operation(operator.truediv, leftval, rightval)
-        elif op == '**':
-            return binary_operation(operator.pow, leftval, rightval)
-        elif op == '&':
-            if not isinstance(leftval, tuple):
-                leftval = (leftval,)
-            if not isinstance(rightval, tuple):
-                rightval = (rightval,)
-            return leftval + rightval
-        elif op == '|':
-            if isinstance(leftval, tuple):
-                return self.right(*leftval, **kw)
+        if op != 'fix_inputs':
+            if op != '&':
+                leftval = self.left(*args, **kw)
+                if op != '|':
+                    rightval = self.right(*args, **kw)
+
+            else:
+                leftval = self.left(*(args[:self.left.n_inputs]), **kw)
+                rightval = self.right(*(args[self.left.n_inputs:]), **kw)
+            if op == '+':
+                return binary_operation(operator.add, leftval, rightval)
+            elif op == '-':
+                return binary_operation(operator.sub, leftval, rightval)
+            elif op == '*':
+                return binary_operation(operator.mul, leftval, rightval)
+            elif op == '/':
+                return binary_operation(operator.truediv, leftval, rightval)
+            elif op == '**':
+                return binary_operation(operator.pow, leftval, rightval)
+            elif op == '&':
+                if not isinstance(leftval, tuple):
+                    leftval = (leftval,)
+                if not isinstance(rightval, tuple):
+                    rightval = (rightval,)
+                return leftval + rightval
+            elif op == '|':
+                if isinstance(leftval, tuple):
+                    return self.right(*leftval, **kw)
+                else:
+                    return self.right(leftval, **kw)
+            elif op in SPECIAL_OPERATORS:
+                return binary_operation(SPECIAL_OPERATORS[op], leftval, rightval)
             else:
-                return self.right(leftval, **kw)
-        elif op in SPECIAL_OPERATORS:
-            return binary_operation(SPECIAL_OPERATORS[op], leftval, rightval)
+
+                raise ModelDefinitionError('Unrecognized operator {op}')
         else:
-            raise ModelDefinitionError('Unrecognized operator {op}')
+            subs = self.right
+            newargs = list(args)
+            subinds = []
+            subvals = []
+            for key in subs.keys():
+                if isinstance(key, int):
+                    subinds.append(key)
+                elif isinstance(key, str):
+                    ind = self.left.inputs.index(key)
+                    subinds.append(ind)
+                subvals.append(subs[key])
+            # Turn inputs specified in kw into positional indices.
+            # Names for compound inputs do not propagate to sub models.
+            kwind = []
+            kwval = []
+            for kwkey in list(kw.keys()):
+                if kwkey in self.inputs:
+                    ind = self.inputs.index(kwkey)
+                    if ind < len(args):
+                        raise ValueError("Keyword argument duplicates "
+                                        "positional value supplied.")
+                    kwind.append(ind)
+                    kwval.append(kw[kwkey])
+                    del kw[kwkey]
+            # Build new argument list
+            # Append keyword specified args first
+            if kwind:
+                kwargs = list(zip(kwind, kwval))
+                kwargs.sort()
+                kwindsorted, kwvalsorted = list(zip(*kwargs))
+                newargs = newargs + list(kwvalsorted)
+            if subinds:
+                subargs = list(zip(subinds, subvals))
+                subargs.sort()
+                subindsorted, subvalsorted = list(zip(*subargs))
+            # The substitutions must be inserted in order
+            for ind, val in subargs:
+                newargs.insert(ind, val)
+            return self.left(*newargs, **kw)

     @property
     def param_names(self):
@@ -2735,14 +2826,6 @@ def isleaf(self):
     def has_inverse(self):
         return self._has_inverse

-    @property
-    def bounding_box(self):
-        return self._bounding_box
-
-    @bounding_box.setter
-    def bounding_box(self, bounding_box):
-        self._bounding_box = bounding_box
-
     @property
     def inverse(self):
         if self.has_inverse():
@@ -2864,13 +2947,14 @@ def map_parameters(self):
         param_map = {}
         self._param_names = []
         for lindex, leaf in enumerate(self._leaflist):
-            for param_name in leaf.param_names:
-                param = getattr(leaf, param_name)
-                new_param_name = "{}_{}".format(param_name, lindex)
-                self.__dict__[new_param_name] = param
-                self._parameters_[new_param_name] = param
-                self._param_names.append(new_param_name)
-                param_map[new_param_name] = (lindex, param_name)
+            if not isinstance(leaf, dict):
+                for param_name in leaf.param_names:
+                    param = getattr(leaf, param_name)
+                    new_param_name = "{}_{}".format(param_name, lindex)
+                    self.__dict__[new_param_name] = param
+                    self._parameters_[new_param_name] = param
+                    self._param_names.append(new_param_name)
+                    param_map[new_param_name] = (lindex, param_name)
         self._param_metrics = {}
         self._param_map =  param_map
         self._param_map_inverse = dict((v, k) for k, v in param_map.items())
@@ -3207,6 +3291,21 @@ def bounding_box(self, bounding_box):
     def bounding_box(self):
         self._user_bounding_box = None

+    def _fix_input_bounding_box(self, input_ind):
+        """
+        If the ``fix_inputs`` operator is used and the model it is applied to
+        has a bounding box definition, delete the corresponding inputs from
+        that bounding box. This method presumes the bounding_box is not None.
+        This also presumes that the list of input indices to remove (i.e.,
+        input_ind has already been put in reverse sorted order).
+        """
+        bounding_box = list(self.left.bounding_box)
+        for ind in input_ind:
+            del bounding_box[ind]
+        if self.n_inputs == 1:
+            bounding_box = bounding_box[0]
+        self.bounding_box = bounding_box
+
     @property
     def has_user_bounding_box(self):
         """
@@ -3403,7 +3502,7 @@ def make_subtree_dict(tree, nodepath, tdict, leaflist):
         tdict[nodepath] = (tree, leftmostind, rightmostind)

-_ORDER_OF_OPERATORS = [('|',), ('&',), ('+', '-'), ('*', '/'), ('**',)]
+_ORDER_OF_OPERATORS = [('fix_inputs',),('|',), ('&',), ('+', '-'), ('*', '/'), ('**',)]
 OPERATOR_PRECEDENCE = {}
 for idx, ops in enumerate(_ORDER_OF_OPERATORS):
     for op in ops:
@@ -3411,6 +3510,31 @@ def make_subtree_dict(tree, nodepath, tdict, leaflist):
 del idx, op, ops

+def fix_inputs(modelinstance, values):
+    """
+    This function creates a compound model with one or more of the input
+    values of the input model assigned fixed values (scalar or array).
+
+    Parameters
+    ----------
+    modelinstance : Model instance. This is the model that one or more of the
+        model input values will be fixed to some constant value.
+    values : A dictionary where the key identifies which input to fix
+        and its value is the value to fix it at. The key may either be the
+        name of the input or a number reflecting its order in the inputs.
+
+    Examples
+    --------
+
+    >>> from astropy.modeling.models import Gaussian2D
+    >>> g = Gaussian2D(1, 2, 3, 4, 5)
+    >>> gv = fix_inputs(g, {0: 2.5})
+
+    Results in a 1D function equivalent to Gaussian2D(1, 2, 3, 4, 5)(x=2.5, y)
+    """
+    return CompoundModel('fix_inputs', modelinstance, values)
+
+
 def custom_model(*args, fit_deriv=None, **kwargs):
     """
     Create a model from a user defined function. The inputs and parameters of
diff --git a/astropy/modeling/models.py b/astropy/modeling/models.py
index 6f6dbf348977..87f9ff07019e 100644
--- a/astropy/modeling/models.py
+++ b/astropy/modeling/models.py
@@ -5,7 +5,7 @@
 """

-from .core import custom_model, hide_inverse  # pylint: disable=W0611
+from .core import custom_model, hide_inverse, fix_inputs # pylint: disable=W0611
 from .mappings import *
 from .projections import *
 from .rotations import *
diff --git a/docs/modeling/compound-models.rst b/docs/modeling/compound-models.rst
index 54c35cce1532..a509bdf5d430 100644
--- a/docs/modeling/compound-models.rst
+++ b/docs/modeling/compound-models.rst
@@ -1369,3 +1369,34 @@ This opens up the possibility of essentially arbitrarily complex transformation
 graphs.  Currently the tools do not exist to make it easy to navigate and
 reason about highly complex compound models that use these mappings, but that
 is a possible enhancement for future versions.
+
+
+Model Reduction
+---------------
+
+In order to save much duplication in the construction of complex models, it is
+possible to define one complex model that covers all cases where the
+variables that distinguish the models are made part of the model's input
+variables. The ``fix_inputs`` function allows defining models derived from
+the more complex one by setting one or more of the inputs to a constant
+value. Examples of this sort of situation arise when working out
+the transformations from detector pixel to RA, Dec, and lambda for
+spectrographs when the slit locations may be moved (e.g., fiber fed or
+commandable slit masks), or different orders may be selected (e.g., Eschelle).
+In the case of order, one may have a function of pixel ``x``, ``y``, ``spectral_order``
+that map into ``RA``, ``Dec`` and ``lambda``. Without specifying ``spectral_order``, it is
+ambiguious what ``RA``, ``Dec`` and ``Lambda`` corresponds to a pixel location. It
+is usually possible to define a function of all three inputs. Presuming
+this model is ``general_transform`` then ``fix_inputs`` may be used to define
+the transform for a specific order as follows:
+
+::
+     >>> order1_transform = fix_inputs(general_transform, {'order': 1})  # doctest: +SKIP
+
+creates a new compound model that takes only pixel position and generates
+``RA``, ``Dec``, and ``lambda``. The ``fix_inputs`` function can be used to set input
+values by position (0 is the first) or by input variable name, and more
+than one can be set in the dictionary supplied.
+
+If the input model has a bounding_box, the generated model will have the
+bounding for the input coordinate removed.

diff --git a/astropy/io/misc/asdf/tags/transform/tests/test_transform.py b/astropy/io/misc/asdf/tags/transform/tests/test_transform.py
index e3ba12bfc0a2..173e642d30d1 100644
--- a/astropy/io/misc/asdf/tags/transform/tests/test_transform.py
+++ b/astropy/io/misc/asdf/tags/transform/tests/test_transform.py
@@ -11,6 +11,7 @@
 from asdf.tests import helpers

 import astropy.units as u
+from astropy.modeling.core import fix_inputs
 from astropy.modeling import models as astmodels

 test_models = [
@@ -165,3 +166,27 @@ def test_tabular_model_units(tmpdir):
                                  method='nearest')
     tree = {'model': model2}
     helpers.assert_roundtrip_tree(tree, tmpdir)
+
+
+def test_fix_inputs(tmpdir):
+    model = astmodels.Pix2Sky_TAN() | astmodels.Rotation2D()
+    tree = {
+        'compound': fix_inputs(model, {'x': 45}),
+        'compound1': fix_inputs(model, {0: 45})
+    }
+
+    helpers.assert_roundtrip_tree(tree, tmpdir)
+
+
+def test_fix_inputs_type():
+    with pytest.raises(TypeError):
+        tree = {
+        'compound': fix_inputs(3, {'x': 45})
+        }
+        helpers.assert_roundtrip_tree(tree, tmpdir)
+
+    with pytest.raises(AttributeError):
+        tree = {
+        'compound': astmodels.Pix2Sky_TAN() & {'x': 45}
+        }
+        helpers.assert_roundtrip_tree(tree, tmpdir)
diff --git a/astropy/modeling/tests/test_compound.py b/astropy/modeling/tests/test_compound.py
index 096b66c4b269..a7feb59db0a6 100644
--- a/astropy/modeling/tests/test_compound.py
+++ b/astropy/modeling/tests/test_compound.py
@@ -10,13 +10,13 @@
 from numpy.testing import assert_allclose, assert_array_equal

 from astropy.utils import minversion
-from astropy.modeling.core import Model, ModelDefinitionError
+from astropy.modeling.core import Model, ModelDefinitionError, CompoundModel
 from astropy.modeling.parameters import Parameter
 from astropy.modeling.models import (Const1D, Shift, Scale, Rotation2D, Gaussian1D,
                                      Gaussian2D, Polynomial1D, Polynomial2D,
                                      Chebyshev2D, Legendre2D, Chebyshev1D, Legendre1D,
                                      Identity, Mapping,
-                                     Tabular1D)
+                                     Tabular1D, fix_inputs)
 import astropy.units as u
 from ..core import CompoundModel

@@ -307,6 +307,63 @@ def test_compound_with_polynomials(poly):
     assert_allclose(result, result_compound)

+def test_fix_inputs():
+    g1 = Gaussian2D(1, 0, 0, 1, 2)
+    g2 = Gaussian2D(1.5, .5, -.2, .5, .3)
+    sg1_1 = fix_inputs(g1, {1: 0})
+    assert_allclose(sg1_1(0), g1(0, 0))
+    assert_allclose(sg1_1([0,1,3]), g1([0,1,3],[0,0,0]))
+    sg1_2 = fix_inputs(g1, {'x': 1})
+    assert_allclose(sg1_2(1.5), g1(1, 1.5))
+    gg1 = g1 & g2
+    sgg1_1 = fix_inputs(gg1, {1: 0.1, 3: 0.2})
+    assert_allclose(sgg1_1(0, 0), gg1(0, 0.1, 0, 0.2))
+    sgg1_2 = fix_inputs(gg1, {'x0': -.1, 2: .1})
+    assert_allclose(sgg1_2(1,1), gg1(-0.1, 1, 0.1, 1))
+    assert_allclose(sgg1_2(y0=1, y1=1), gg1(-0.1, 1, 0.1, 1))
+
+
+def test_fix_inputs_invalid():
+    g1 = Gaussian2D(1, 0, 0, 1, 2)
+    with pytest.raises(ValueError):
+        fix_inputs(g1, {'x0': 0, 0: 0})
+
+    with pytest.raises(ValueError):
+        fix_inputs(g1, (0, 1))
+
+    with pytest.raises(ValueError):
+        fix_inputs(g1, {3: 2})
+
+    with pytest.raises(ValueError):
+        fix_inputs(g1, {'w': 2})
+
+    with pytest.raises(ModelDefinitionError):
+        CompoundModel('#', g1, g1)
+
+    with pytest.raises(ValueError):
+        gg1 = fix_inputs(g1, {0: 1})
+        gg1(2, y=2)
+
+
+def test_fix_inputs_with_bounding_box():
+    g1 = Gaussian2D(1, 0, 0, 1, 1)
+    g2 = Gaussian2D(1, 0, 0, 1, 1)
+    assert g1.bounding_box == ((-5.5,5.5), (-5.5,5.5))
+
+    gg1 = g1 & g2
+    gg1.bounding_box = ((-5.5,5.5), (-5.4,5.4), (-5.3,5.3), (-5.2,5.2))
+    assert gg1.bounding_box == ((-5.5,5.5), (-5.4,5.4), (-5.3,5.3), (-5.2,5.2))
+
+    sg = fix_inputs(gg1, {0: 0, 2: 0})
+    assert sg.bounding_box == ((-5.4,5.4), (-5.2,5.2))
+
+    g1 = Gaussian1D(10, 3, 1)
+    g = g1 & g1
+    g.bounding_box = ((1, 4), (6, 8))
+    gf = fix_inputs(g, {0: 1})
+    assert gf.bounding_box == (6, 8)
+
+
 def test_indexing_on_instance():
     """Test indexing on compound model instances."""

diff --git a/astropy/modeling/tests/test_model_sets.py b/astropy/modeling/tests/test_model_sets.py
index a24c52c81b0b..808e5afbe1e2 100644
--- a/astropy/modeling/tests/test_model_sets.py
+++ b/astropy/modeling/tests/test_model_sets.py
@@ -244,3 +244,8 @@ def test_model_set_axis_outputs():
     assert_allclose(y0[:, 1], y1[1])
     with pytest.raises(ValueError):
         model_set(x)
+
+
+def test_compound_model_sets():
+    with pytest.raises(ValueError):
+        Polynomial1D(1, n_models=2, model_set_axis=1) | Polynomial1D(1, n_models=2, model_set_axis=0)

diff --git a/docs/modeling/compound-models.rst b/docs/modeling/compound-models.rst
index 54c35cce15..a509bdf5d4 100644
--- a/docs/modeling/compound-models.rst
+++ b/docs/modeling/compound-models.rst
@@ -1369,3 +1369,34 @@ This opens up the possibility of essentially arbitrarily complex transformation
 graphs.  Currently the tools do not exist to make it easy to navigate and
 reason about highly complex compound models that use these mappings, but that
 is a possible enhancement for future versions.
+
+
+Model Reduction
+---------------
+
+In order to save much duplication in the construction of complex models, it is
+possible to define one complex model that covers all cases where the
+variables that distinguish the models are made part of the model's input
+variables. The ``fix_inputs`` function allows defining models derived from
+the more complex one by setting one or more of the inputs to a constant
+value. Examples of this sort of situation arise when working out
+the transformations from detector pixel to RA, Dec, and lambda for
+spectrographs when the slit locations may be moved (e.g., fiber fed or
+commandable slit masks), or different orders may be selected (e.g., Eschelle).
+In the case of order, one may have a function of pixel ``x``, ``y``, ``spectral_order``
+that map into ``RA``, ``Dec`` and ``lambda``. Without specifying ``spectral_order``, it is
+ambiguious what ``RA``, ``Dec`` and ``Lambda`` corresponds to a pixel location. It
+is usually possible to define a function of all three inputs. Presuming
+this model is ``general_transform`` then ``fix_inputs`` may be used to define
+the transform for a specific order as follows:
+
+::
+     >>> order1_transform = fix_inputs(general_transform, {'order': 1})  # doctest: +SKIP
+
+creates a new compound model that takes only pixel position and generates
+``RA``, ``Dec``, and ``lambda``. The ``fix_inputs`` function can be used to set input
+values by position (0 is the first) or by input variable name, and more
+than one can be set in the dictionary supplied.
+
+If the input model has a bounding_box, the generated model will have the
+bounding for the input coordinate removed.