Repository: mcgibbon/sympl (language: python)

Issue:
Constants string does not get updated when adding new constants
The get_constants_string provides a printable string of all constants currently registered with sympl. set_constant adds a new constant, but there is currently no logic to add this constant to output of  get_constants_string().

The constant exists, but does not show up when printing, and this could confuse users.

Also, the docstring for reset_constants need to reflect the fact that constant aliases are also reset and the 
condensible is set back to `water'.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 60200d7..95a5c0f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,3 +63,6 @@ target/
 
 # pyenv python configuration file
 .python-version
+
+# Spyder
+.spyderproject
diff --git a/.travis.yml b/.travis.yml
index cfa6b60..df1a762 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,9 @@
 # This file was autogenerated and will overwrite each time you run travis_pypi_setup.py
 env:
-- TOXENV=cov
-- TOXENV=flake8
 - TOXENV=py35
 - TOXENV=py27
+- TOXENV=flake8
+- TOXENV=cov
 install:
     - pip install -U tox
 language: python
diff --git a/HISTORY.rst b/HISTORY.rst
index 216f084..ea6e6e3 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -5,6 +5,98 @@ What's New
 Latest
 ------
 
+* Stepper, DiagnosticComponent, ImplicitTendencyComponent, and TendencyComponent base classes were
+  modified to include functionality that was previously in ScalingWrapper,
+  UpdateFrequencyWrapper, and TendencyInDiagnosticsWrapper. The functionality of
+  TendencyInDiagnosticsWrapper is now to be used in Stepper and TendencyStepper objects.
+* Composites now have a component_list attribute which contains the components being
+  composited.
+* TimeSteppers now have a prognostic_list attribute which contains the
+  prognostics used to calculate tendencies.
+* TimeSteppers from sympl can now handle ImplicitTendencyComponent components.
+* Added a check for netcdftime having the required objects, to fall back on not
+  using netcdftime when those are missing. This is because most objects are missing in
+  older versions of netcdftime (that come packaged with netCDF4) (closes #23).
+* TimeSteppers should now be called with individual Prognostics as args, rather
+  than a list of components, and will emit a warning when lists are given.
+* TimeSteppers now have input, output, and diagnostic properties as attributes.
+  These are handled entirely by the base class.
+* TimeSteppers now allow you to put tendencies in their diagnostic output. This
+  is done using first-order time differencing.
+* Composites now have properties dictionaries.
+* Updated basic components to use new component API.
+* Components enforce consistency of output from array_call with properties
+  dictionaries, raising ComponentMissingOutputError or ComponentExtraOutputError
+  respectively if outputs do not match.
+* Added a priority order of property types for determining which aliases are
+  returned by get_component_aliases.
+* Fixed a bug where TendencyStepper objects would modify the arrays passed to them by
+  TendencyComponent objects, leading to unexpected value changes.
+* Fixed a bug where constants were missing from the string returned by
+  get_constants_string, particularly any new constants (issue #27)
+* Fixed a bug in NetCDFMonitor which led to some aliases being skipped.
+* Modified class checking on components so that components which satisfy the
+  component's API will be recognized as instances using isinstance(obj, Class).
+  Right now this only checks for the presence and lack of presence of
+  component attributes, and correct signature of __call__. Later it may also
+  check properties dictionaries for consistency, or perform other checks.
+* Fixed a bug where ABCMeta was not being used in Python 3.
+* Added initialize_numpy_arrays_with_properties which creates zero arrays for an output
+  properties dictionary.
+* Added reference_air_temperature constant.
+* Fixed bug where degrees Celcius or Fahrenheit could not be used as units on inputs
+  because it would lead to an error.
+* Added combine_component_properties as a public function.
+* Added some unit helper functions (units_are_same, units_are_compatible,
+  is_valid_unit) to public API.
+* Added tracer-handling funcitonality to component base classes.
+
+Breaking changes
+~~~~~~~~~~~~~~~~
+
+* Implicit, Timestepper, Prognostic, ImplicitPrognostic, and Diagnostic objects have been renamed to
+  TendencyStepper, Stepper, TendencyComponent, ImplicitTendencyComponent,
+  and DiagnosticComponent. These changes are also reflected in subclass names.
+* inputs, outputs, diagnostics, and tendencies are no longer attributes of components.
+  In order to get these, you should use e.g. input_properties.keys()
+* properties dictionaries are now abstract methods, so subclasses must define them.
+  Previously they defaulted to empty dictionaries.
+* Base classes now raise InvalidPropertyDictError when output property units conflict with input
+  property units (which probably indicates that they're wrong).
+* Components should now be written using a new array_call method rather than __call__.
+  __call__ will automatically unwrap DataArrays to numpy arrays to be passed into
+  array_call based on the component's properties dictionaries, and re-wrap to
+  DataArrays when done.
+* TimeSteppers should now be written using a _call method rather than __call__.
+  __call__ wraps _call to provide some base class functionality, like putting
+  tendencies in diagnostics.
+* ScalingWrapper, UpdateFrequencyWrapper, and TendencyInDiagnosticsWrapper
+  have been removed. The functionality of these wrappers has been moved to the
+  component base types as methods and initialization options.
+* 'time' now must be present in the model state dictionary. This is strictly required
+  for calls to DiagnosticComponent, TendencyComponent, ImplicitTendencyComponent, and Stepper components,
+  and may be strictly required in other ways in the future
+* Removed everything to do with directional wildcards. Currently '*' is the
+  only wildcard dimension. 'x', 'y', and 'z' refer to their own names only.
+* Removed the combine_dimensions function, which wasn't used anywhere and no
+  longer has much purpose without directional wildcards
+* RelaxationTendencyComponent no longer allows caching of equilibrium values or
+  timescale. They must be provided through the input state. This is to ensure
+  proper conversion of dimensions and units.
+* Removed ComponentTestBase from package. All of its tests except for output
+  caching are now performed on object initialization or call time.
+* "*" matches are now enforced to be the same across all quantities of a
+  component, such that the length of the "*" axis will be the same for all
+  quantities. Any missing dimensions that are present on other quantities
+  will be created and broadcast to achieve this.
+* dims_like is obsolete as a result, and is no longer used. `dims` should be
+  used instead. If present, `dims` from input properties will be used as
+  default.
+* Components will now raise an exception when __call__ of the component base
+  class (e.g. Stepper, TendencyComponent, etc.) if the __init__ method of the base
+  class has not been called, telling the user that the component __init__
+  method should make a call to the superclass init.
+
 v0.3.2
 ------
 
@@ -18,7 +110,6 @@ v0.3.2
 
 Breaking changes
 ~~~~~~~~~~~~~~~~
-
 * tendencies in diagnostics are now named as X_tendency_from_Y, instead of
   tendency_of_X_due_to_Y. The idea is that it's shorter, and can easily be
   shortened more by aliasing "tendency" to "tend"
@@ -44,15 +135,15 @@ v0.3.0
   restore_data_arrays_with_properties
 * corrected heat capacity of snow and ice to be floats instead of ints
 * Added get_constant function as the way to retrieve constants
-* Added ImplicitPrognostic as a new component type. It is like a Prognostic,
+* Added ImplicitTendencyComponent as a new component type. It is like a TendencyComponent,
   but its call signature also requires that a timestep be given.
-* Added TimeDifferencingWrapper, which turns an Implicit into an
-  ImplicitPrognostic by applying first-order time differencing.
+* Added TimeDifferencingWrapper, which turns an Stepper into an
+  ImplicitTendencyComponent by applying first-order time differencing.
 * Added set_condensible_name as a way of changing what condensible aliases
   (for example, density_of_solid_phase) refer to. Default is 'water'.
 * Moved wrappers to their own file (out from util.py).
-* Corrected str representation of Diagnostic to say Diagnostic instead of
-  Implicit.
+* Corrected str representation of DiagnosticComponent to say DiagnosticComponent instead of
+  Stepper.
 * Added a function reset_constants to reset the constants library to its
   initial state.
 * Added a function datetime which accepts calendar as a keyword argument, and
diff --git a/docs/composites.rst b/docs/composites.rst
index 2c24fd1..a157d67 100644
--- a/docs/composites.rst
+++ b/docs/composites.rst
@@ -4,7 +4,7 @@ Composites
 
 There are a set of objects in Sympl that wrap multiple components into a single
 object so they can be called as if they were one component. There is one each
-for :py:class:`~sympl.Prognostic`, :py:class:`~sympl.Diagnostic`, and
+for :py:class:`~sympl.TendencyComponent`, :py:class:`~sympl.DiagnosticComponent`, and
 :py:class:`~sympl.Monitor`. These can be used to simplify code, so that
 the way you call a list of components is the same as the way you would
 call a single component. For example, *instead* of writing:
@@ -35,7 +35,7 @@ You could write:
 
 .. code-block:: python
 
-    prognostic_composite = PrognosticComposite([
+    prognostic_composite = TendencyComponentComposite([
         MyPrognostic(),
         MyOtherPrognostic(),
         YetAnotherPrognostic(),
@@ -46,7 +46,7 @@ This second call is much cleaner. It will also automatically detect whether
 multiple components are trying to write out the same diagnostic, and raise
 an exception if that is the case (so no results are being silently
 overwritten). You can get similar simplifications for
-:py:class:`~sympl.Diagnostic` and :py:class:`~sympl.Monitor`.
+:py:class:`~sympl.DiagnosticComponent` and :py:class:`~sympl.Monitor`.
 
 .. note:: PrognosticComposites are mainly useful inside of TimeSteppers, so
           if you're only writing a model script it's unlikely you'll need them.
@@ -54,12 +54,12 @@ overwritten). You can get similar simplifications for
 API Reference
 -------------
 
-.. autoclass:: sympl.PrognosticComposite
+.. autoclass:: sympl.TendencyComponentComposite
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
 
-.. autoclass:: sympl.DiagnosticComposite
+.. autoclass:: sympl.DiagnosticComponentComposite
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
diff --git a/docs/computation.rst b/docs/computation.rst
index cfa91c3..6cad3a9 100644
--- a/docs/computation.rst
+++ b/docs/computation.rst
@@ -2,17 +2,17 @@
 Component Types
 ===============
 
-In Sympl, computation is mainly performed using :py:class:`~sympl.Prognostic`,
-:py:class:`~sympl.Diagnostic`, and :py:class:`~sympl.Implicit` objects.
+In Sympl, computation is mainly performed using :py:class:`~sympl.TendencyComponent`,
+:py:class:`~sympl.DiagnosticComponent`, and :py:class:`~sympl.Stepper` objects.
 Each of these types, once initialized, can be passed in a current model state.
-:py:class:`~sympl.Prognostic` objects use the state to return tendencies and
-diagnostics at the current time. :py:class:`~sympl.Diagnostic` objects
-return only diagnostics from the current time. :py:class:`~sympl.Implicit`
+:py:class:`~sympl.TendencyComponent` objects use the state to return tendencies and
+diagnostics at the current time. :py:class:`~sympl.DiagnosticComponent` objects
+return only diagnostics from the current time. :py:class:`~sympl.Stepper`
 objects will take in a timestep along with the state, and then return the
 next state as well as modifying the current state to include more diagnostics
-(it is similar to a :py:class:`~sympl.TimeStepper` in how it is called).
+(it is similar to a :py:class:`~sympl.TendencyStepper` in how it is called).
 
-In specific cases, it may be necessary to use a :py:class:`~sympl.ImplicitPrognostic`
+In specific cases, it may be necessary to use a :py:class:`~sympl.ImplicitTendencyComponent`
 object, which is discussed at the end of this section.
 
 These classes themselves (listed in the previous paragraph) are not ones you
@@ -24,15 +24,18 @@ In addition to the computational functionality below, all components have "prope
 for their inputs and outputs, which are described in the section
 :ref:`Input/Output Properties`.
 
-Prognostic
+Details on the internals of components and how to write them are in the section on
+:ref:`Writing Components`.
+
+TendencyComponent
 ----------
 
-As stated above, :py:class:`~sympl.Prognostic` objects use the state to return
+As stated above, :py:class:`~sympl.TendencyComponent` objects use the state to return
 tendencies and diagnostics at the current time. In a full model, the tendencies
-are used by a time stepping scheme (in Sympl, a :py:class:`~sympl.TimeStepper`)
+are used by a time stepping scheme (in Sympl, a :py:class:`~sympl.TendencyStepper`)
 to determine the values of quantities at the next time.
 
-You can call a :py:class:`~sympl.Prognostic` directly to get diagnostics and
+You can call a :py:class:`~sympl.TendencyComponent` directly to get diagnostics and
 tendencies like so:
 
 .. code-block:: python
@@ -41,35 +44,35 @@ tendencies like so:
     diagnostics, tendencies = radiation(state)
 
 ``diagnostics`` and ``tendencies`` in this case will both be dictionaries,
-similar to ``state``. Even if the :py:class:`~sympl.Prognostic` being called
+similar to ``state``. Even if the :py:class:`~sympl.TendencyComponent` being called
 does not compute any diagnostics, it will still return an empty
 diagnostics dictionary.
 
-Usually, you will call a Prognostic object through a
-:py:class:`~sympl.TimeStepper` that uses it to determine values at the next
+Usually, you will call a TendencyComponent object through a
+:py:class:`~sympl.TendencyStepper` that uses it to determine values at the next
 timestep.
 
-.. autoclass:: sympl.Prognostic
+.. autoclass:: sympl.TendencyComponent
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
 
-.. autoclass:: sympl.ConstantPrognostic
+.. autoclass:: sympl.ConstantTendencyComponent
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
 
-.. autoclass:: sympl.RelaxationPrognostic
+.. autoclass:: sympl.RelaxationTendencyComponent
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
 
-Diagnostic
+DiagnosticComponent
 ----------
 
-:py:class:`~sympl.Diagnostic` objects use the state to return quantities
+:py:class:`~sympl.DiagnosticComponent` objects use the state to return quantities
 ('diagnostics') from the same timestep as the input state. You can call a
-:py:class:`~sympl.Diagnostic` directly to get diagnostic quantities like so:
+:py:class:`~sympl.DiagnosticComponent` directly to get diagnostic quantities like so:
 
 .. code-block:: python
 
@@ -77,8 +80,8 @@ Diagnostic
     diagnostics = diagnostic_component(state)
 
 You should be careful to check in the documentation of the particular
-:py:class:`~sympl.Diagnostic` you are using to see whether it modifies the
-``state`` given to it as input. :py:class:`~sympl.Diagnostic` objects in charge
+:py:class:`~sympl.DiagnosticComponent` you are using to see whether it modifies the
+``state`` given to it as input. :py:class:`~sympl.DiagnosticComponent` objects in charge
 of updating ghost cells in particular may modify the arrays in the input
 dictionary, so that the arrays in the returned ``diagnostics`` dictionary are
 the same ones as were sent as input in the ``state``. To make it clear that
@@ -89,22 +92,22 @@ syntax like:
 
     state.update(diagnostic_component(state))
 
-.. autoclass:: sympl.Diagnostic
+.. autoclass:: sympl.DiagnosticComponent
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
 
-.. autoclass:: sympl.ConstantDiagnostic
+.. autoclass:: sympl.ConstantDiagnosticComponent
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
 
-Implicit
+Stepper
 --------
 
-:py:class:`~sympl.Implicit` objects use a state and a timestep to return the next
+:py:class:`~sympl.Stepper` objects use a state and a timestep to return the next
 state, and update the input state with any relevant diagnostic quantities. You
-can call an Implicit object like so:
+can call an Stepper object like so:
 
 .. code-block:: python
 
@@ -124,7 +127,7 @@ current state. Or if an object provides 'cloud_fraction' as a diagnostic, it
 may modify an existing 'cloud_fraction' array in the input state if one is
 present, instead of allocating a new array.
 
-.. autoclass:: sympl.Implicit
+.. autoclass:: sympl.Stepper
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
@@ -137,7 +140,7 @@ there are a number of attributes with names like ``input_properties`` for
 components. These attributes give a fairly complete description of the inputs
 and outputs of the component.
 
-You can access them like this (for an example :py:class:`~sympl.Prognostic`
+You can access them like this (for an example :py:class:`~sympl.TendencyComponent`
 class ``RRTMRadiation``):
 
 .. code-block:: python
@@ -279,47 +282,56 @@ equal to:
 that the object will output ``cloud_fraction`` in its diagnostics on the
 same grid as ``air_temperature``, in dimensionless units.
 
-ImplicitPrognostic
+ImplicitTendencyComponent
 ------------------
 
 .. warning:: This component type should be avoided unless you know you need it,
              for reasons discussed in this section.
 
 In addition to the component types described above, computation may be performed by a
-:py:class:`~sympl.ImplicitPrognostic`. This class should be avoided unless you
+:py:class:`~sympl.ImplicitTendencyComponent`. This class should be avoided unless you
 know what you are doing, but it may be necessary in certain cases. An
-:py:class:`~sympl.ImplicitPrognostic`, like a :py:class:`~sympl.Prognostic`,
+:py:class:`~sympl.ImplicitTendencyComponent`, like a :py:class:`~sympl.TendencyComponent`,
 calculates tendencies, but it does so using both the model state and a timestep.
 Certain components, like ones handling advection using a spectral method, may
-need to derive tendencies from an :py:class:`~sympl.Implicit` object by
-representing it using an :py:class:`~sympl.ImplicitPrognostic`.
+need to derive tendencies from an :py:class:`~sympl.Stepper` object by
+representing it using an :py:class:`~sympl.ImplicitTendencyComponent`.
 
-The reason to avoid using an :py:class:`~sympl.ImplicitPrognostic` is that if
+The reason to avoid using an :py:class:`~sympl.ImplicitTendencyComponent` is that if
 a component requires a timestep, it is making internal assumptions about how
 you are timestepping. For example, it may use the timestep to ensure that all
 supersaturated water is condensed by the end of the timestep using an assumption
-about the timestepping. However, if you use a :py:class:`~sympl.TimeStepper`
+about the timestepping. However, if you use a :py:class:`~sympl.TendencyStepper`
 which does not obey those assumptions, you may get unintended behavior, such as
 some supersaturated water remaining, or too much water being condensed.
 
-For this reason, the :py:class:`~sympl.TimeStepper` objects included in Sympl
-do not wrap :py:class:`~sympl.ImplicitPrognostic` components. If you would like
+For this reason, the :py:class:`~sympl.TendencyStepper` objects included in Sympl
+do not wrap :py:class:`~sympl.ImplicitTendencyComponent` components. If you would like
 to use this type of component, and know what you are doing, it is pretty easy
-to write your own :py:class:`~sympl.TimeStepper` to do so (you can base the code
+to write your own :py:class:`~sympl.TendencyStepper` to do so (you can base the code
 off of the code in Sympl), or the model you are using might already have
 components to do this for you.
 
 If you are wrapping a parameterization and notice that it needs a timestep to
 compute its tendencies, that is likely *not* a good reason to write an
-:py:class:`~sympl.ImplicitPrognostic`. If at all possible you should modify the
+:py:class:`~sympl.ImplicitTendencyComponent`. If at all possible you should modify the
 code to compute the value at the next timestep, and write an
-:py:class:`~sympl.Implicit` component. You are welcome to reach out to the
+:py:class:`~sympl.Stepper` component. You are welcome to reach out to the
 developers of Sympl if you would like advice on your specific situation! We're
 always excited about new wrapped components.
 
-.. autoclass:: sympl.ImplicitPrognostic
+.. autoclass:: sympl.ImplicitTendencyComponent
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
 
+Tracer Properties
+-----------------
+
+You may notice some components have properties that mention tracers.
+`uses_tracers` is a boolean that tells you whether the component makes use of
+tracers or not, while `tracer_dims` contains the dimensions of tracer arrays
+used internally by the object. For more on tracers as a user, see :ref:`Tracers`,
+and for more on tracers as a component author, see :ref:`Writing Components`.
+
 .. _Python documentation for dicts: https://docs.python.org/3/tutorial/datastructures.html#dictionaries
diff --git a/docs/constants.rst b/docs/constants.rst
index 41390d6..a3f183b 100644
--- a/docs/constants.rst
+++ b/docs/constants.rst
@@ -25,6 +25,18 @@ imported by calling :py:func:`~sympl.reset_constants`.
 
 .. autofunction:: sympl.reset_constants
 
+Debugging and Logging Constants
+-------------------------------
+
+You can get a string describing current constants by calling :py:func:`~sympl.get_constants_string`:
+
+.. code-block:: python
+
+    import sympl
+    print(sympl.get_constants_string())
+
+.. autofunction:: sympl.get_constants_string
+
 Condensible Quantities
 ----------------------
 
diff --git a/docs/index.rst b/docs/index.rst
index afcb1b3..291f215 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -28,6 +28,8 @@ Documentation
    computation
    monitors
    composites
+   tracers
+   units
    writing_components
    memory_management
    contributing
diff --git a/docs/memory_management.rst b/docs/memory_management.rst
index f7164d4..6598a80 100644
--- a/docs/memory_management.rst
+++ b/docs/memory_management.rst
@@ -10,15 +10,15 @@ Arrays
 
 If possible, you should try to be aware of when there are two code references
 to the same in-memory array. This can help avoid some common bugs. Let's start
-with an example. Say you create a ConstantPrognostic object like so::
+with an example. Say you create a ConstantTendencyComponent object like so::
 
     >>> import numpy as np
-    >>> from sympl import ConstantPrognostic, DataArray
+    >>> from sympl import ConstantTendencyComponent, DataArray
     >>> array = DataArray(
             np.ones((5, 5, 10)),
             dims=('lon', 'lat', 'lev'), attrs={'units': 'K/s'})
     >>> tendencies = {'air_temperature': array}
-    >>> prognostic = ConstantPrognostic(tendencies)
+    >>> prognostic = ConstantTendencyComponent(tendencies)
 
 This is all fine so far. But it's important to know that now ``array`` is the
 same array stored inside ``prognostic``::
diff --git a/docs/overview.rst b/docs/overview.rst
index af784a9..567c81f 100644
--- a/docs/overview.rst
+++ b/docs/overview.rst
@@ -45,6 +45,8 @@ and packages to be used alongside one another.
 Then where's the model?
 -----------------------
 
+Check out `CliMT <https://github.com/climt/climt>`_ as an example.
+
 Models created with Sympl can work differently from traditional Fortran models.
 A model developer makes the components of their model available. Using these
 components, you can write a script which acts as the model executable, but also
@@ -71,21 +73,21 @@ set to initial values. Code to do this may be present in other packages, or you
 can write this code yourself. The state and its initialization is discussed
 further in :ref:`Model State`.
 
-The state dictionary is evolved by :py:class:`~sympl.TimeStepper` and
-:py:class:`~sympl.Implicit` objects. These types of objects take in the state
+The state dictionary is evolved by :py:class:`~sympl.TendencyStepper` and
+:py:class:`~sympl.Stepper` objects. These types of objects take in the state
 and a timedelta object that indicates the time step, and return the next
-model state. :py:class:`~sympl.TimeStepper` objects do this by wrapping
-:py:class:`~sympl.Prognostic` objects, which calculate tendencies using the
-state dictionary. We should note that the meaning of "Implicit" in Sympl is
-slightly different than its traditional definition. Here an "Implicit" object is
+model state. :py:class:`~sympl.TendencyStepper` objects do this by wrapping
+:py:class:`~sympl.TendencyComponent` objects, which calculate tendencies using the
+state dictionary. We should note that the meaning of "Stepper" in Sympl is
+slightly different than its traditional definition. Here an "Stepper" object is
 one that calculates the new state directly from the current state, or any
 object that requires the timestep to calculate the new state, while
-"Prognostic" objects are ones that calculate tendencies without using the
-timestep. If a :py:class:`~sympl.TimeStepper` or :py:class:`~sympl.Implicit`
+"TendencyComponent" objects are ones that calculate tendencies without using the
+timestep. If a :py:class:`~sympl.TendencyStepper` or :py:class:`~sympl.Stepper`
 object needs to use multiple time steps in its calculation, it does so by
 storing states it was previously given until they are no longer needed.
 
-The state is also calculated using :py:class:`~sympl.Diagnostic` objects which
+The state is also calculated using :py:class:`~sympl.DiagnosticComponent` objects which
 determine diagnostic quantities at the current time from the current state,
 returning them in a new dictionary. This type of object is particularly useful
 if you want to write your own online diagnostics.
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 169b739..7e6bb48 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -109,17 +109,17 @@ for us:
     state = get_initial_state(nx=256, ny=128, nz=64)
     state['time'] = datetime(2000, 1, 1)
 
-An initialized `state` is a dictionary whose keys are strings (like
+An initialized ``state`` is a dictionary whose keys are strings (like
 'air_temperature') and values are :py:class:`~sympl.DataArray` objects, which
 store not only the data but also metadata like units. The one exception
-is the "time" quantity which is either a `datetime`-like or `timedelta`-like
+is the "time" quantity which is either a ``datetime``-like or ``timedelta``-like
 object. Here we are calling :py:func:`sympl.datetime` to initialize time,
 rather than directly creating a Python datetime. This is because
 :py:func:`sympl.datetime` can support a number of calendars using the
-`netcdftime` package, if installed, unlike the built-in `datetime` which only
+`netcdftime` package, if installed, unlike the built-in ``datetime`` which only
 supports the Proleptic Gregorian calendar.
 
-You can read more about the `state`, including :py:func:`sympl.datetime` in
+You can read more about the ``state``, including :py:func:`sympl.datetime` in
 :ref:`Model State`.
 
 Initialize Components
@@ -137,26 +137,26 @@ Those are the "components":
     ])
     implicit_dynamics = ImplicitDynamics()
 
-:py:class:`~sympl.AdamsBashforth` is a :py:class:`~sympl.TimeStepper`, which is
-created with a set of :py:class:`~sympl.Prognostic` components.
-The :py:class:`~sympl.Prognostic` components we have here are `Radiation`,
-`BoundaryLayer`, and `DeepConvection`. Each of these carries information about
+:py:class:`~sympl.AdamsBashforth` is a :py:class:`~sympl.TendencyStepper`, which is
+created with a set of :py:class:`~sympl.TendencyComponent` components.
+The :py:class:`~sympl.TendencyComponent` components we have here are ``Radiation``,
+``BoundaryLayer``, and ``DeepConvection``. Each of these carries information about
 what it takes as inputs and provides as outputs, and can be called with a model
 state to return tendencies for a set of quantities. The
-:py:class:`~sympl.TimeStepper` uses this information to step the model state
+:py:class:`~sympl.TendencyStepper` uses this information to step the model state
 forward in time.
 
-The :py:class:`~sympl.UpdateFrequencyWrapper` applied to the `Radiation` object
-is an object that acts like a :py:class:`~sympl.Prognostic` but only computes
+The :py:class:`~sympl.UpdateFrequencyWrapper` applied to the ``Radiation`` object
+is an object that acts like a :py:class:`~sympl.TendencyComponent` but only computes
 its output if at least a certain amount of model time has passed since the last
 time the output was computed. Otherwise, it returns the last computed output.
 This is commonly used in atmospheric models to avoid doing radiation
 calculations (which are very expensive) every timestep, but it can be applied
-to any Prognostic.
+to any TendencyComponent.
 
-The :py:class:`ImplicitDynamics` class is a :py:class:`~sympl.Implicit` object, which
-steps the model state forward in time in the same way that a :py:class:`~sympl.TimeStepper`
-would, but doesn't use :py:class:`~sympl.Prognostic` objects in doing so.
+The :py:class:`ImplicitDynamics` class is a :py:class:`~sympl.Stepper` object, which
+steps the model state forward in time in the same way that a :py:class:`~sympl.TendencyStepper`
+would, but doesn't use :py:class:`~sympl.TendencyComponent` objects in doing so.
 
 The Main Loop
 -------------
@@ -180,6 +180,6 @@ In the main loop, a series of component calls update the state, and the figure
 presented by ``plot_monitor`` is updated. The code is meant to be as
 self-explanatory as possible. It is necessary to manually set the time of the
 next state at the end of the loop. This is not done automatically by
-:py:class:`~sympl.TimeStepper` and :py:class:`~sympl.Implicit` objects, because
+:py:class:`~sympl.TendencyStepper` and :py:class:`~sympl.Stepper` objects, because
 in many models you may want to update the state with multiple such objects
 in a sequence over the course of a single time step.
diff --git a/docs/state.rst b/docs/state.rst
index 7f543e4..26e00ab 100644
--- a/docs/state.rst
+++ b/docs/state.rst
@@ -11,7 +11,7 @@ subtraction, and contains a helpful method for converting units. Any
 information about the grid the data is using that components need should be
 put as attributes in the ``attrs`` of the ``DataArray`` objects. Deciding on
 these attributes (if any) is mostly up to the component developers. However,
-in order to use the TimeStepper objects and several helper functions from Sympl,
+in order to use the TendencyStepper objects and several helper functions from Sympl,
 it is required that a "units" attribute is present.
 
 .. _xarray: http://xarray.pydata.org/en/stable/
diff --git a/docs/timestepping.rst b/docs/timestepping.rst
index 85ea6db..517f826 100644
--- a/docs/timestepping.rst
+++ b/docs/timestepping.rst
@@ -1,22 +1,22 @@
 Timestepping
 ============
 
-:py:class:`~sympl.TimeStepper` objects use time derivatives from
-:py:class:`~sympl.Prognostic` objects to step a model state forward in time.
-They are initialized using a list of :py:class:`~sympl.Prognostic` objects.
+:py:class:`~sympl.TendencyStepper` objects use time derivatives from
+:py:class:`~sympl.TendencyComponent` objects to step a model state forward in time.
+They are initialized using any number of :py:class:`~sympl.TendencyComponent` objects.
 
 .. code-block:: python
 
     from sympl import AdamsBashforth
-    time_stepper = AdamsBashforth([MyPrognostic(), MyOtherPrognostic()])
+    time_stepper = AdamsBashforth(MyPrognostic(), MyOtherPrognostic())
 
-Once initialized, a :py:class:`~sympl.TimeStepper` object has a very similar
-interface to the :py:class:`~sympl.Implicit` object.
+Once initialized, a :py:class:`~sympl.TendencyStepper` object has a very similar
+interface to the :py:class:`~sympl.Stepper` object.
 
 .. code-block:: python
 
     from datetime import timedelta
-    time_stepper = AdamsBashforth([MyPrognostic()])
+    time_stepper = AdamsBashforth(MyPrognostic())
     timestep = timedelta(minutes=10)
     diagnostics, next_state = time_stepper(state, timestep)
     state.update(diagnostics)
@@ -29,32 +29,32 @@ and that they have been modified. In other words, ``state`` may be modified by
 this call. For instance, the time filtering necessary when using Leapfrog
 time stepping means the current model state has to be modified by the filter.
 
-It is only after calling the :py:class:`~sympl.TimeStepper` and getting the
+It is only after calling the :py:class:`~sympl.TendencyStepper` and getting the
 diagnostics that you will have a complete state with all diagnostic quantities.
 This means you will sometimes want to pass ``state`` to your
 :py:class:`~sympl.Monitor` objects *after* calling
-the :py:class:`~sympl.TimeStepper` and getting ``next_state``.
+the :py:class:`~sympl.TendencyStepper` and getting ``next_state``.
 
-.. warning:: :py:class:`~sympl.TimeStepper` objects do not, and should not,
+.. warning:: :py:class:`~sympl.TendencyStepper` objects do not, and should not,
     update 'time' in the model state.
 
-Keep in mind that for split-time models, multiple :py:class:`~sympl.TimeStepper`
+Keep in mind that for split-time models, multiple :py:class:`~sympl.TendencyStepper`
 objects might be called in in a single pass of the main loop. If each one
 updated ``state['time']``, the time would be moved forward more than it should.
-For that reason, :py:class:`~sympl.TimeStepper` objects do not update
+For that reason, :py:class:`~sympl.TendencyStepper` objects do not update
 ``state['time']``.
 
 There are also
-:py:class:`~sympl.Implicit` objects which evolve the state forward in time
-without the use of Prognostic objects. These function exactly the same as a
-:py:class:`~sympl.TimeStepper` once they are created, but do not accept
-:py:class:`~sympl.Prognostic` objects when you create them. One example might
+:py:class:`~sympl.Stepper` objects which evolve the state forward in time
+without the use of TendencyComponent objects. These function exactly the same as a
+:py:class:`~sympl.TendencyStepper` once they are created, but do not accept
+:py:class:`~sympl.TendencyComponent` objects when you create them. One example might
 be a component that condenses all supersaturated moisture over some time period.
-:py:class:`~sympl.Implicit` objects are generally used for parameterizations
+:py:class:`~sympl.Stepper` objects are generally used for parameterizations
 that work by determining the target model state in some way, or involve
-limiters, and cannot be represented as a :py:class:`~sympl.Prognostic`.
+limiters, and cannot be represented as a :py:class:`~sympl.TendencyComponent`.
 
-.. autoclass:: sympl.TimeStepper
+.. autoclass:: sympl.TendencyStepper
     :members:
     :special-members:
     :exclude-members: __weakref__,__metaclass__
diff --git a/docs/tracers.rst b/docs/tracers.rst
new file mode 100644
index 0000000..e8cd697
--- /dev/null
+++ b/docs/tracers.rst
@@ -0,0 +1,24 @@
+=======
+Tracers
+=======
+
+In an Earth system model, "tracer" refers to quantities that are passively
+moved around in a model without actively interacting with a component. Generally
+these are moved around by a dynamical core or subgrid advection scheme. It is
+possible for components to do something else to tracers (let us know if you
+think of something!) but for now let's assume that's what's going on.
+
+If a component moves around tracers, it will have its ``uses_tracers`` property
+set to ``True``, and will also have a ``tracer_dims`` property set.
+
+You can tell Sympl that you want components to move a tracer around by
+registering it with :py:func:`~sympl.register_tracer`.
+
+.. autofunction:: sympl.register_tracer
+
+To see the current list of registered tracers, you can call
+:py:func:`~sympl.get_tracer_names` or
+:py:func:`~sympl.get_tracer_unit_dict`.
+
+.. autofunction:: sympl.get_tracer_names
+.. autofunction:: sympl.get_tracer_unit_dict
diff --git a/docs/units.rst b/docs/units.rst
new file mode 100644
index 0000000..5c18921
--- /dev/null
+++ b/docs/units.rst
@@ -0,0 +1,10 @@
+=====
+Units
+=====
+
+While nearly all unit functionality is handled internally within Sympl, it does
+expose a few helper functions which may be useful to you.
+
+.. autofunction:: sympl.is_valid_unit
+.. autofunction:: sympl.units_are_same
+.. autofunction:: sympl.units_are_compatible
diff --git a/docs/writing_components.rst b/docs/writing_components.rst
index 6056fbe..d93f4df 100644
--- a/docs/writing_components.rst
+++ b/docs/writing_components.rst
@@ -13,16 +13,17 @@ to talk about the parts of their code.
 Writing an Example
 ------------------
 
-Let's start with a Prognostic component which relaxes temperature towards some
-target temperature.
+Let's start with a TendencyComponent component which relaxes temperature towards some
+target temperature. We'll go over the sections of this example step-by-step
+below.
 
 .. code-block:: python
 
     from sympl import (
-        Prognostic, get_numpy_arrays_with_properties,
+        TendencyComponent, get_numpy_arrays_with_properties,
         restore_data_arrays_with_properties)
 
-    class TemperatureRelaxation(Prognostic):
+    class TemperatureRelaxation(TendencyComponent):
 
         input_properties = {
             'air_temperature': {
@@ -45,24 +46,15 @@ target temperature.
             }
         }
 
-        def __init__(self, tau=1., target_temperature=300.):
-            self._tau = tau
-            self._T0 = target_temperature
-
-        def __call__(self, state):
-            # we get numpy arrays with specifications from input_properties
-            raw_arrays = get_numpy_arrays_with_properties(
-                state, self.input_properties)
-            T = raw_arrays['air_temperature']
-            # here the actual computation happens
-            raw_tendencies = {
-                'air_temperature': (T - self._T0)/self._tau,
+        def __init__(self, damping_timescale_seconds=1., target_temperature_K=300.):
+            self._tau = damping_timescale_seconds
+            self._T0 = target_temperature_K
+
+        def array_call(self, state):
+            tendencies = {
+                'air_temperature': (state['air_temperature'] - self._T0)/self._tau,
             }
-            # now we re-format the data in a way the host model can use
             diagnostics = {}
-            tendencies = restore_data_arrays_with_properties(
-                raw_tendencies, self.tendency_properties,
-                state, self.input_properties)
             return tendencies, diagnostics
 
 Imports
@@ -76,7 +68,7 @@ so that it can be found right away by anyone reading your code.
 .. code-block:: python
 
     from sympl import (
-        Prognostic, get_numpy_arrays_with_properties,
+        TendencyComponent, get_numpy_arrays_with_properties,
         restore_data_arrays_with_properties)
 
 Define an Object
@@ -86,13 +78,13 @@ Once these are imported, there's this line:
 
 .. code-block:: python
 
-    class TemperatureRelaxation(Prognostic):
+    class TemperatureRelaxation(TendencyComponent):
 
 This is the syntax for defining an object in Python. ``TemperatureRelaxation``
-will be the name of the new object. The :py:class:`~sympl.Prognostic`
+will be the name of the new object. The :py:class:`~sympl.TendencyComponent`
 in parentheses is telling Python that ``TemperatureRelaxation`` is a *subclass* of
-:py:class:`~sympl.Prognostic`. This tells Sympl that it can expect your object
-to behave like a :py:class:`~sympl.Prognostic`.
+:py:class:`~sympl.TendencyComponent`. This tells Sympl that it can expect your object
+to behave like a :py:class:`~sympl.TendencyComponent`.
 
 Define Attributes
 *****************
@@ -142,13 +134,11 @@ or on an instance, as when you do:
 These properties are described in :ref:`Component Types`. They are very useful!
 They clearly document your code. Here we can see that air_temperature will
 be used as a 1-dimensional flattened array in units of degrees Kelvin. Sympl
-can also understand these properties, and use them to automatically
-acquire arrays in the dimensions and units that you need.
-It can also test thatsome of these properties are accurate.
-It's your responsibility, though, to make sure that the input units are the
-units you want to acquire in the numpy array data, and that the output units
-are the units of the values in the raw output arrays that you want to convert
-to :py:class:`~sympl.DataArray` objects.
+uses these properties to automatically acquire arrays in the dimensions and
+units that you need, and to automatically convert your output back into a
+form consistent with the dimensions of the model state. It will warn you if
+you create extra outputs which are not defined in the properties, or if there
+is an output defined in the properties that is missing.
 
 It is possible that some of these attributes won't be known until you
 create the object (they may depend on things passed in on initialization).
@@ -164,14 +154,9 @@ weird name:
 
 .. code-block:: python
 
-        def __init__(self, damping_timescale=1., target_temperature=300.):
-            """
-            damping_timescale is the damping timescale in seconds.
-            target_temperature is the temperature that will be relaxed to,
-            in degrees Kelvin.
-            """
-            self._tau = damping_timescale
-            self._T0 = target_temperature
+        def __init__(self, damping_timescale_seconds=1., target_temperature_K=300.):
+            self._tau = damping_timescale_seconds
+            self._T0 = target_temperature_K
 
 This is the function that is called when you create an instance of your object.
 All methods on objects take in a first argument called ``self``. You don't see
@@ -192,8 +177,8 @@ is also optimal. What these attributes mean is clearly defined in the two lines:
 
 .. code-block:: python
 
-            self._tau = damping_timescale
-            self._T0 = target_temperature
+            self._tau = damping_timescale_seconds
+            self._T0 = target_temperature_K
 
 Obviously ``self._tau`` is the damping timescale, and ``self._T0`` is the
 target temperature for the relaxation. Now you can use these shorter variables
@@ -203,57 +188,45 @@ variables are well-documented.
 The Computation
 ***************
 
-That brings us to the ``__call__`` method. This is what's called when you
-use the object as though it is a function. In Sympl components, this is the
-method which takes in a state dictionary and returns dictionaries with outputs.
+That brings us to the ``array_call`` method. In Sympl components, this is the
+method which takes in a state dictionary as numpy arrays (*not* ``DataArray``)
+and returns dictionaries with numpy array outputs.
 
 .. code-block:: python
 
-        def __call__(self, state):
-            # we get numpy arrays with specifications from input_properties
-            raw_arrays = get_numpy_arrays_with_properties(
-                state, self.input_properties)
-            T = raw_arrays['air_temperature']
-            # here the actual computation happens
-            raw_tendencies = {
-                'air_temperature': (T - self._T0)/self._tau,
+        def array_call(self, state):
+            tendencies = {
+                'air_temperature': (state['air_temperature'] - self._T0)/self._tau,
             }
-            # now we re-format the data in a way the host model can use
             diagnostics = {}
-            tendencies = restore_data_arrays_with_properties(
-                raw_tendencies, self.tendency_properties,
-                state, self.input_properties)
-            return diagnostics, tendencies
-
-There are two helper functions used in this code that we strongly recommend
-using. They take care of the work of making sure you get variables that are
-in the units your component needs, and have the dimensions your component needs.
-
-:py:func:`~sympl.get_numpy_arrays_with_properties` uses the input_properties
-dictionary you give it to extract numpy arrays with those properties from the
-input state. It will convert units to ensure the numbers are in the specified
+            return tendencies, diagnostics
+
+Sympl will automatically handle taking in the input state of ``DataArray``
+objects and converting it to the form defined by the ``input_properties`` of your
+component. It will convert units to ensure the numbers are in the specified
 units, and it will reshape the data to give it the shape specified in ``dims``.
-For example, if dims is ``['*', 'z']`` then it will give you a 2-dimensional array
-whose second axis is the vertical, and first axis is a flattening of any other
-dimensions. If you specify ``['*', 'mid_levels']`` then the result is similar, but
-only 'mid_levels' is an acceptable vertical dimension. The ``match_dims_like``
+For example, if dims is ``['*', 'mid_levels']`` then it will give you a
+2-dimensional array whose second axis is the vertical on mid levels, and first
+axis is a flattening of any other dimensions. The ``match_dims_like``
 property on ``air_pressure`` tells Sympl that any wildcard-matched dimensions
-(ones that match 'x', 'y', 'z', or '*') should be the same between the two
+('*') should be the same between the two
 quantities, meaning they're on the same grid for those wildcards. You can still,
 however, have one be on say 'mid_levels' and another on 'interface_levels' if
-those dimensions are explicitly listed (instead of listing 'z').
-
-:py:func:`~sympl.restore_data_arrays_with_properties` does something fairly
-magical. In this example, it takes the raw_tendencies dictionary and converts
-the value for 'air_temperature' from a numpy array to a DataArray that has
-the same dimensions as ``air_temperature`` had in the input state. That means
-that you could pass this object a state with whatever dimensions you want,
-whether it's (x, y, z), or (z, x, y), or (x, y), or (station_number, z), etc.
+those dimensions are explicitly listed.
+
+After you return dictionaries of numpy arrays, Sympl will convert these outputs
+back to ``DataArray`` objects. In this example, it takes the tendencies
+dictionary and converts the value for 'air_temperature' from a numpy array to a
+``DataArray`` that has the same dimensions as ``air_temperature`` had in the
+input state. That means that you could pass this object a state with whatever
+dimensions you want, whether it's ``('longitude', 'latitude', 'mid_levels')``, or
+``('interface_levels',)`` or ``('station_number', 'planet_number')``, etc.
 and this component will be able to take in that state, and return a
 tendency dictionary with the same dimensions (and order) that the model uses!
 And internally you can work with a simple 1-dimensional array. This is
 particularly useful for writing pointwise components using ``['*']`` or column
-components with ``['*', 'z']`` or ``['z', '*']``.
+components with, for example, ``['*', 'mid_levels']`` or
+``['interface_levels', '*']``.
 
 You can read more about properties in the section
 :ref:`Input/Output Properties`.
@@ -294,21 +267,12 @@ computational code, we can write:
 
 .. code-block:: python
 
-        def __call__(self, state):
-            # we get numpy arrays with specifications from input_properties
-            raw_arrays = get_numpy_arrays_with_properties(
-                state, self.input_properties)
-            T = raw_arrays['T']
-            # here the actual computation happens
-            raw_tendencies = {
-                'T': (T - self._T0)/self._tau,
+        def array_call(self, state):
+            tendencies = {
+                'T': (state['T'] - self._T0)/self._tau,
             }
-            # now we re-format the data in a way the host model can use
             diagnostics = {}
-            tendencies = restore_data_arrays_with_properties(
-                raw_tendencies, self.tendency_properties,
-                state, self.input_properties)
-            return diagnostics, tendencies
+            return tendencies, diagnostics
 
 Instead of using 'air_temperature' in the raw_arrays and raw_tendencies
 dictionaries, we can use 'T'. This doesn't matter much for a name as short as
@@ -319,3 +283,53 @@ Also notice that even though the alias is set in input_properties, it is also
 used when restoring DataArrays. If there is an output that is not
 also an input, the alias could instead be set in ``diagnostic_properties``,
 ``tendency_properties``, or ``output_properties``, wherever is relevant.
+
+
+Using Tracers
+-------------
+
+.. note:: This feature is mostly used in dynamical cores. If you don't think you need
+          this, you probably don't.
+
+Sympl's base components have some features to automatically create tracer arrays
+for use by dynamical components. If an :py:class:`~sympl.Stepper`,
+:py:class:`~sympl.TendencyComponent`, or :py:class:`~sympl.ImplicitTendencyComponent`
+component specifies ``uses_tracers = True`` and sets ``tracer_dims``, this
+feature is enabled.
+
+.. code-block:: python
+
+        class MyDynamicalCore(Stepper):
+
+            uses_tracers = True
+            tracer_dims = ['tracer', '*', 'mid_levels']
+
+            [...]
+
+``tracer_dims`` is a list or tuple in the form of a ``dims`` attribute on one of its
+inputs, and must have a "tracer" dimension. This dimension refers to which
+tracer (you could call it "tracer number").
+
+Once this feature is enabled, the ``state`` passed to ``array_call`` on the
+component will include a quantity called "tracers" with the dimensions
+specified by ``tracer_dims``. It will also be required that these tracers
+are used in the output. For a :py:class:`~sympl.Stepper` component, "tracers"
+must be present in the output state, and for a :py:class:`~sympl.TendencyComponent` or
+:py:class:`~sympl.ImplicitTendencyComponent` component "tracers" must be present in
+the tendencies, with the same dimensions as the input "tracers".
+
+On these latter two components, you should also specify a
+``tracer_tendency_time_unit`` property, which refers to the time part of the
+tendency unit. For example, if the input tracer is in units of ``g m^-3``, and
+``tracer_tendency_time_unit`` is "s", then the output tendency will be in
+units of ``g m^-3 s^-1``. This value is set as "s" (or seconds) by default.
+
+.. code-block:: python
+
+        class MyDynamicalCore(TendencyComponent):
+
+            uses_tracers = True
+            tracer_dims = ['tracer', '*', 'mid_levels']
+            tracer_tendency_time_unit = 's'
+
+            [...]
diff --git a/requirements_dev.txt b/requirements_dev.txt
index a831e4c..1e76fbe 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,19 +1,19 @@
 pip>=8.1.2
-bumpversion==0.5.3
-wheel==0.29.0
-watchdog==0.8.3
-flake8==2.6.0
-tox==2.3.1
-coverage==4.1
-Sphinx==1.4.8
-cryptography==1.4
-PyYAML==3.11
-pytest==2.9.2
-pytest-cov==2.4.0
-mock==2.0.0
-xarray==0.8.2
+bumpversion>=0.5.3
+wheel>=0.29.0
+watchdog>=0.8.3
+flake8>=2.6.0
+tox>=2.3.1
+coverage>=4.1
+Sphinx>=1.4.8
+cryptography>=1.4
+PyYAML>=3.11
+pytest>=2.9.2
+pytest-cov>=2.4.0
+mock>=2.0.0
+xarray>=0.9.3
 six
-pint==0.7.0
+pint>=0.7.0
 scipy
 numpy>=0.10
 coveralls
diff --git a/setup.py b/setup.py
index 66154a5..95b58fc 100644
--- a/setup.py
+++ b/setup.py
@@ -11,7 +11,7 @@
 requirements = [
     'numpy>=1.10',
     'pint>=0.7.0',
-    'xarray>=0.8.0',
+    'xarray>=0.9.3',
     'six',
 ]
 
diff --git a/sympl/__init__.py b/sympl/__init__.py
index 661e40d..25ca3d2 100644
--- a/sympl/__init__.py
+++ b/sympl/__init__.py
@@ -1,52 +1,60 @@
 # -*- coding: utf-8 -*-
 from ._core.base_components import (
-    Prognostic, Diagnostic, Implicit, Monitor, PrognosticComposite,
-    DiagnosticComposite, MonitorComposite, ImplicitPrognostic
+    TendencyComponent, DiagnosticComponent, Stepper, Monitor, ImplicitTendencyComponent
 )
-from ._core.timestepping import TimeStepper, Leapfrog, AdamsBashforth
+from ._core.composite import TendencyComponentComposite, DiagnosticComponentComposite, \
+    MonitorComposite, ImplicitTendencyComponentComposite
+from ._core.tendencystepper import TendencyStepper
+from ._components.timesteppers import AdamsBashforth, Leapfrog, SSPRungeKutta
 from ._core.exceptions import (
     InvalidStateError, SharedKeyError, DependencyError,
-    InvalidPropertyDictError)
-from ._core.array import DataArray
+    InvalidPropertyDictError, ComponentExtraOutputError,
+    ComponentMissingOutputError)
+from ._core.dataarray import DataArray
 from ._core.constants import (
     get_constant, set_constant, set_condensible_name, reset_constants,
     get_constants_string)
+from ._core.tracers import (
+    register_tracer, get_tracer_unit_dict, get_tracer_input_properties, get_tracer_names)
 from ._core.util import (
-    combine_dimensions,
     ensure_no_shared_keys,
     get_numpy_array, jit,
-    restore_dimensions, get_numpy_arrays_with_properties,
-    restore_data_arrays_with_properties,
-    set_direction_names, add_direction_names,
+    restore_dimensions,
     get_component_aliases)
-from ._core.wrappers import (
-    UpdateFrequencyWrapper, TendencyInDiagnosticsWrapper,
-    TimeDifferencingWrapper, ScalingWrapper)
-from ._core.testing import ComponentTestBase
+from sympl._core.combine_properties import combine_component_properties
+from ._core.units import units_are_same, units_are_compatible, is_valid_unit
+from sympl._core.get_np_arrays import get_numpy_arrays_with_properties
+from sympl._core.restore_dataarray import restore_data_arrays_with_properties
+from sympl._core.init_np_arrays import initialize_numpy_arrays_with_properties
 from ._components import (
     PlotFunctionMonitor, NetCDFMonitor, RestartMonitor,
-    ConstantPrognostic, ConstantDiagnostic, RelaxationPrognostic)
+    ConstantTendencyComponent, ConstantDiagnosticComponent, RelaxationTendencyComponent,
+    TimeDifferencingWrapper)
+from ._core.wrappers import UpdateFrequencyWrapper, ScalingWrapper
 from ._core.time import datetime, timedelta
 
 __version__ = '0.3.2'
 __all__ = (
-    Prognostic, Diagnostic, Implicit, Monitor, PrognosticComposite,
-    DiagnosticComposite, MonitorComposite, ImplicitPrognostic,
-    TimeStepper, Leapfrog, AdamsBashforth,
+    TendencyComponent, DiagnosticComponent, Stepper, Monitor, TendencyComponentComposite,
+    ImplicitTendencyComponentComposite,
+    DiagnosticComponentComposite, MonitorComposite, ImplicitTendencyComponent,
+    TendencyStepper, Leapfrog, AdamsBashforth, SSPRungeKutta,
     InvalidStateError, SharedKeyError, DependencyError,
-    InvalidPropertyDictError,
+    InvalidPropertyDictError, ComponentExtraOutputError,
+    ComponentMissingOutputError,
+    units_are_same, units_are_compatible, is_valid_unit,
     DataArray,
     get_constant, set_constant, set_condensible_name, reset_constants,
-    get_constants_string,
-    UpdateFrequencyWrapper, TimeDifferencingWrapper, combine_dimensions,
+    get_constants_string, TimeDifferencingWrapper,
     ensure_no_shared_keys,
-    get_numpy_array, jit, TendencyInDiagnosticsWrapper,
+    get_numpy_array, jit,
+    register_tracer, get_tracer_unit_dict, get_tracer_input_properties, get_tracer_names,
     restore_dimensions, get_numpy_arrays_with_properties,
     restore_data_arrays_with_properties,
-    set_direction_names, add_direction_names, get_component_aliases,
-    ScalingWrapper,
-    ComponentTestBase,
+    initialize_numpy_arrays_with_properties,
+    get_component_aliases, combine_component_properties,
     PlotFunctionMonitor, NetCDFMonitor, RestartMonitor,
-    ConstantPrognostic, ConstantDiagnostic, RelaxationPrognostic,
+    ConstantTendencyComponent, ConstantDiagnosticComponent, RelaxationTendencyComponent,
+    UpdateFrequencyWrapper, ScalingWrapper,
     datetime, timedelta
 )
diff --git a/sympl/_components/__init__.py b/sympl/_components/__init__.py
index a818515..ba29856 100644
--- a/sympl/_components/__init__.py
+++ b/sympl/_components/__init__.py
@@ -1,8 +1,11 @@
 from .netcdf import NetCDFMonitor, RestartMonitor
 from .plot import PlotFunctionMonitor
-from .basic import ConstantPrognostic, ConstantDiagnostic, RelaxationPrognostic
+from .basic import (
+    ConstantTendencyComponent, ConstantDiagnosticComponent, RelaxationTendencyComponent,
+    TimeDifferencingWrapper)
 
 __all__ = (
     PlotFunctionMonitor,
     NetCDFMonitor, RestartMonitor,
-    ConstantPrognostic, ConstantDiagnostic, RelaxationPrognostic)
+    ConstantTendencyComponent, ConstantDiagnosticComponent, RelaxationTendencyComponent,
+    TimeDifferencingWrapper)
diff --git a/sympl/_components/basic.py b/sympl/_components/basic.py
index b1be2d2..c8251c2 100644
--- a/sympl/_components/basic.py
+++ b/sympl/_components/basic.py
@@ -1,64 +1,162 @@
-from .._core.base_components import Prognostic, Diagnostic
-from .._core.array import DataArray
+from .._core.dataarray import DataArray
+from .._core.base_components import ImplicitTendencyComponent, TendencyComponent, DiagnosticComponent
 from .._core.units import unit_registry as ureg
 
 
-class ConstantPrognostic(Prognostic):
+class ConstantTendencyComponent(TendencyComponent):
     """
     Prescribes constant tendencies provided at initialization.
 
-    Note: Any arrays in the passed dictionaries are not copied, so that
-        if you were to modify them after passing them into this object,
-        it would also modify the values inside this object.
+    Attributes
+    ----------
+    input_properties : dict
+        A dictionary whose keys are quantities required in the state when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    tendency_properties : dict
+        A dictionary whose keys are quantities for which tendencies are returned when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    diagnostic_properties : dict
+        A dictionary whose keys are diagnostic quantities returned when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    input_scale_factors : dict
+        A (possibly empty) dictionary whose keys are quantity names and
+        values are floats by which input values are scaled before being used
+        by this object.
+    tendency_scale_factors : dict
+        A (possibly empty) dictionary whose keys are quantity names and
+        values are floats by which tendency values are scaled before being
+        returned by this object.
+    diagnostic_scale_factors : dict
+        A (possibly empty) dictionary whose keys are quantity names and
+        values are floats by which diagnostic values are scaled before being
+        returned by this object.
+    update_interval : timedelta
+        If not None, the component will only give new output if at least
+        a period of update_interval has passed since the last time new
+        output was given. Otherwise, it would return that cached output.
+    tendencies_in_diagnostics : boo
+        A boolean indicating whether this object will put tendencies of
+        quantities in its diagnostic output based on first order time
+        differencing of output values.
+    name : string
+        A label to be used for this object, for example as would be used for
+        Y in the name "X_tendency_from_Y".
+
+    Note
+    ----
+    Any arrays in the passed dictionaries are not copied, so that
+    if you were to modify them after passing them into this object,
+    it would also modify the values inside this object.
     """
 
-    def __init__(self, tendencies, diagnostics=None):
+    @property
+    def input_properties(self):
+        return {}
+
+    @property
+    def tendency_properties(self):
+        return_dict = {}
+        for name, data_array in self.__tendencies.items():
+            return_dict[name] = {
+                'dims': data_array.dims,
+                'units': data_array.attrs['units'],
+            }
+        return return_dict
+
+    @property
+    def diagnostic_properties(self):
+        return_dict = {}
+        for name, data_array in self.__diagnostics.items():
+            return_dict[name] = {
+                'dims': data_array.dims,
+                'units': data_array.attrs['units'],
+            }
+        return return_dict
+
+    def __init__(self, tendencies, diagnostics=None, **kwargs):
         """
         Args
         ----
         tendencies : dict
             A dictionary whose keys are strings indicating
             state quantities and values are the time derivative of those
-            quantities in units/second to be returned by this Prognostic.
-        diagnostics : dict
+            quantities in units/second to be returned by this TendencyComponent.
+        diagnostics : dict, optional
             A dictionary whose keys are strings indicating
             state quantities and values are the value of those quantities
-            to be returned by this Prognostic.
+            to be returned by this TendencyComponent. By default an empty dictionary
+            is used.
+        input_scale_factors : dict, optional
+            A (possibly empty) dictionary whose keys are quantity names and
+            values are floats by which input values are scaled before being used
+            by this object.
+        tendency_scale_factors : dict, optional
+            A (possibly empty) dictionary whose keys are quantity names and
+            values are floats by which tendency values are scaled before being
+            returned by this object.
+        diagnostic_scale_factors : dict, optional
+            A (possibly empty) dictionary whose keys are quantity names and
+            values are floats by which diagnostic values are scaled before being
+            returned by this object.
+        update_interval : timedelta, optional
+            If given, the component will only give new output if at least
+            a period of update_interval has passed since the last time new
+            output was given. Otherwise, it would return that cached output.
+        tendencies_in_diagnostics : bool, optional
+            A boolean indicating whether this object will put tendencies of
+            quantities in its diagnostic output.
+        name : string, optional
+            A label to be used for this object, for example as would be used for
+            Y in the name "X_tendency_from_Y". By default the class name in
+            lowercase is used.
         """
-        self._tendencies = tendencies.copy()
+        self.__tendencies = tendencies.copy()
         if diagnostics is not None:
-            self._diagnostics = diagnostics.copy()
+            self.__diagnostics = diagnostics.copy()
         else:
-            self._diagnostics = {}
+            self.__diagnostics = {}
+        super(ConstantTendencyComponent, self).__init__(**kwargs)
 
-    def __call__(self, state):
-        """
-        Gets tendencies and diagnostics from the passed model state. The
-        returned dictionaries will contain the same values as were passed at
-        initialization.
+    def array_call(self, state):
+        tendencies = {}
+        for name, data_array in self.__tendencies.items():
+            tendencies[name] = data_array.values
+        diagnostics = {}
+        for name, data_array in self.__diagnostics.items():
+            diagnostics[name] = data_array.values
+        return tendencies, diagnostics
 
-        Args
-        ----
-        state : dict
-            A model state dictionary.
 
-        Returns
-        -------
-        tendencies : dict
-            A dictionary whose keys are strings indicating
-            state quantities and values are the time derivative of those
-            quantities in units/second.
-        diagnostics : dict
-            A dictionary whose keys are strings indicating
-            state quantities and values are the value of those quantities.
-        """
-        return self._tendencies.copy(), self._diagnostics.copy()
-
-
-class ConstantDiagnostic(Diagnostic):
+class ConstantDiagnosticComponent(DiagnosticComponent):
     """
     Yields constant diagnostics provided at initialization.
 
+    Attributes
+    ----------
+    input_properties : dict
+        A dictionary whose keys are quantities required in the state when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    diagnostic_properties : dict
+        A dictionary whose keys are diagnostic quantities returned when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    input_scale_factors : dict
+        A (possibly empty) dictionary whose keys are quantity names and
+        values are floats by which input values are scaled before being used
+        by this object.
+    diagnostic_scale_factors : dict
+        A (possibly empty) dictionary whose keys are quantity names and
+        values are floats by which diagnostic values are scaled before being
+        returned by this object.
+    update_interval : timedelta
+        If not None, the component will only give new output if at least
+        a period of update_interval has passed since the last time new
+        output was given. Otherwise, it would return that cached output.
+
     Note
     ----
     Any arrays in the passed dictionaries are not copied, so that
@@ -66,7 +164,21 @@ class ConstantDiagnostic(Diagnostic):
     it would also modify the values inside this object.
     """
 
-    def __init__(self, diagnostics):
+    @property
+    def input_properties(self):
+        return {}
+
+    @property
+    def diagnostic_properties(self):
+        return_dict = {}
+        for name, data_array in self.__diagnostics.items():
+            return_dict[name] = {
+                'dims': data_array.dims,
+                'units': data_array.attrs['units'],
+            }
+        return return_dict
+
+    def __init__(self, diagnostics, **kwargs):
         """
         Args
         ----
@@ -74,32 +186,31 @@ def __init__(self, diagnostics):
             A dictionary whose keys are strings indicating
             state quantities and values are the value of those quantities.
             The values in the dictionary will be returned when this
-            Diagnostic is called.
+            DiagnosticComponent is called.
+        input_scale_factors : dict, optional
+            A (possibly empty) dictionary whose keys are quantity names and
+            values are floats by which input values are scaled before being used
+            by this object.
+        diagnostic_scale_factors : dict, optional
+            A (possibly empty) dictionary whose keys are quantity names and
+            values are floats by which diagnostic values are scaled before being
+            returned by this object.
+        update_interval : timedelta, optional
+            If given, the component will only give new output if at least
+            a period of update_interval has passed since the last time new
+            output was given. Otherwise, it would return that cached output.
         """
-        self._diagnostics = diagnostics.copy()
+        self.__diagnostics = diagnostics.copy()
+        super(ConstantDiagnosticComponent, self).__init__(**kwargs)
 
-    def __call__(self, state):
-        """
-        Returns diagnostic values.
+    def array_call(self, state):
+        return_state = {}
+        for name, data_array in self.__diagnostics.items():
+            return_state[name] = data_array.values
+        return return_state
 
-        Args
-        ----
-        state : dict
-            A model state dictionary. Is not used, and is only
-            taken in to keep an API consistent with a Diagnostic.
 
-        Returns
-        -------
-        diagnostics : dict
-            A dictionary whose keys are strings indicating
-            state quantities and values are the value of those quantities.
-            The values in the returned dictionary are the same as were
-            passed into this object at initialization.
-        """
-        return self._diagnostics.copy()
-
-
-class RelaxationPrognostic(Prognostic):
+class RelaxationTendencyComponent(TendencyComponent):
     r"""
     Applies Newtonian relaxation to a single quantity.
 
@@ -107,37 +218,117 @@ class RelaxationPrognostic(Prognostic):
     :math:`\frac{dx}{dt} = - \frac{x - x_{eq}}{\tau}`
     where :math:`x` is the quantity being relaxed, :math:`x_{eq}` is the
     equilibrium value, and :math:`\tau` is the timescale of the relaxation.
+
+    Attributes
+    ----------
+    input_properties : dict
+        A dictionary whose keys are quantities required in the state when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    tendency_properties : dict
+        A dictionary whose keys are quantities for which tendencies are returned when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    diagnostic_properties : dict
+        A dictionary whose keys are diagnostic quantities returned when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    input_scale_factors : dict
+        A (possibly empty) dictionary whose keys are quantity names and
+        values are floats by which input values are scaled before being used
+        by this object.
+    tendency_scale_factors : dict
+        A (possibly empty) dictionary whose keys are quantity names and
+        values are floats by which tendency values are scaled before being
+        returned by this object.
+    diagnostic_scale_factors : dict
+        A (possibly empty) dictionary whose keys are quantity names and
+        values are floats by which diagnostic values are scaled before being
+        returned by this object.
+    update_interval : timedelta
+        If not None, the component will only give new output if at least
+        a period of update_interval has passed since the last time new
+        output was given. Otherwise, it would return that cached output.
+    tendencies_in_diagnostics : boo
+        A boolean indicating whether this object will put tendencies of
+        quantities in its diagnostic output based on first order time
+        differencing of output values.
+    name : string
+        A label to be used for this object, for example as would be used for
+        Y in the name "X_tendency_from_Y".
     """
 
-    def __init__(self, quantity_name, equilibrium_value=None,
-                 relaxation_timescale=None):
+    @property
+    def input_properties(self):
+        return_dict = {
+            self._quantity_name: {
+                'dims': ['*'],
+                'units': self._units,
+            },
+            'equilibrium_{}'.format(self._quantity_name): {
+                'dims': ['*'],
+                'units': self._units,
+            },
+            '{}_relaxation_timescale'.format(self._quantity_name): {
+                'dims': ['*'],
+                'units': 's',
+            }
+        }
+        return return_dict
+
+    @property
+    def tendency_properties(self):
+        return {
+            self._quantity_name: {
+                'dims': ['*'],
+                'units': str(ureg(self._units) / ureg('s')),
+            }
+        }
+
+    @property
+    def diagnostic_properties(self):
+        return {}
+
+    def __init__(self, quantity_name, units, **kwargs):
         """
         Args
         ----
         quantity_name : str
-            The name of the quantity to which Newtonian
-            relaxation should be applied
-        equilibrium_value : DataArray, optional
-            The equilibrium value to which the quantity is relaxed. If
-            not given, it should be provided in the state when
-            the object is called.
-        relaxation_timescale : DataArray, optional
-            The timescale tau with which the Newtonian relaxation occurs.
-            If not given, it should be provided in the state when
-            the object is called.
+            The name of the quantity to which Newtonian relaxation should be
+            applied.
+        units : str
+            The units of the relaxed quantity as to be used internally when
+            computing tendency. Can be any units convertible from the actual
+            input you plan to use.
+        input_scale_factors : dict, optional
+            A (possibly empty) dictionary whose keys are quantity names and
+            values are floats by which input values are scaled before being used
+            by this object.
+        tendency_scale_factors : dict, optional
+            A (possibly empty) dictionary whose keys are quantity names and
+            values are floats by which tendency values are scaled before being
+            returned by this object.
+        diagnostic_scale_factors : dict, optional
+            A (possibly empty) dictionary whose keys are quantity names and
+            values are floats by which diagnostic values are scaled before being
+            returned by this object.
+        update_interval : timedelta, optional
+            If given, the component will only give new output if at least
+            a period of update_interval has passed since the last time new
+            output was given. Otherwise, it would return that cached output.
         """
         self._quantity_name = quantity_name
-        self._equilibrium_value = equilibrium_value
-        self._tau = relaxation_timescale
+        self._units = units
+        super(RelaxationTendencyComponent, self).__init__(**kwargs)
 
-    def __call__(self, state):
+    def array_call(self, state):
         """
         Gets tendencies and diagnostics from the passed model state.
 
         Args
         ----
         state : dict
-            A model state dictionary. Below, (quantity_name)
+            A model state dictionary as numpy arrays. Below, (quantity_name)
             refers to the quantity_name passed at initialization. The state
             must contain:
 
@@ -152,32 +343,97 @@ def __call__(self, state):
         tendencies : dict
             A dictionary whose keys are strings indicating
             state quantities and values are the time derivative of those
-            quantities in units/second at the time of the input state.
+            quantities in units/second at the time of the input state, as
+            numpy arrays.
         diagnostics : dict
             A dictionary whose keys are strings indicating
             state quantities and values are the value of those quantities
-            at the time of the input state.
+            at the time of the input state, as numpy arrays.
         """
         value = state[self._quantity_name]
-        if self._equilibrium_value is None:
-            equilibrium = state['equilibrium_' + self._quantity_name].to_units(
-                value.attrs['units'])
-        else:
-            equilibrium = self._equilibrium_value.to_units(
-                value.attrs['units'])
-        if self._tau is None:
-            tau = state[
-                self._quantity_name + '_relaxation_timescale'].to_units(
-                's')
-        else:
-            tau = self._tau.to_units('s')
-        tendency_unit_string = str(
-            ureg(state[self._quantity_name].attrs['units']) / ureg('s'))
+        equilibrium = state['equilibrium_' + self._quantity_name]
+        tau = state[self._quantity_name + '_relaxation_timescale']
         tendencies = {
-            self._quantity_name: DataArray(
-                (equilibrium.values - value.values)/tau.values,
-                dims=value.dims,
-                attrs={'units': tendency_unit_string}
-            )
+            self._quantity_name: (equilibrium - value)/tau
         }
         return tendencies, {}
+
+
+class TimeDifferencingWrapper(ImplicitTendencyComponent):
+    """
+    Wraps an Stepper object and turns it into an ImplicitTendencyComponent by applying
+    simple first-order time differencing to determine tendencies.
+
+    Example
+    -------
+    This how the wrapper should be used on an Stepper class
+    called GridScaleCondensation.
+
+    >>> component = TimeDifferencingWrapper(GridScaleCondensation())
+    """
+
+    @property
+    def input_properties(self):
+        return self._implicit.input_properties
+
+    @property
+    def tendency_properties(self):
+        return_dict = {}
+        for name, properties in self._implicit.output_properties.items():
+            return_dict[name] = properties.copy()
+            return_dict[name]['units'] += ' s^-1'
+        return return_dict
+
+    @property
+    def diagnostic_properties(self):
+        return self._implicit.diagnostic_properties
+
+    def __init__(self, implicit, **kwargs):
+        """
+        Initializes the TimeDifferencingWrapper. Some kwargs of Stepper
+        objects are not implemented, and should be applied instead on the
+        Stepper object which is wrapped by this one.
+
+        Parameters
+        ----------
+        implicit: Stepper
+            An Stepper component to wrap.
+        """
+        if len(kwargs) > 0:
+            raise TypeError('Received unexpected keyword argument {}'.format(
+                kwargs.popitem()[0]))
+        self._implicit = implicit
+
+    def __call__(self, state, timestep):
+        diagnostics, new_state = self._implicit(state, timestep)
+        tendencies = {}
+        timestep_seconds = timestep.total_seconds()
+        for varname, data_array in new_state.items():
+            if isinstance(data_array, DataArray):
+                if varname in self._implicit.output_properties.keys():
+                    if varname not in state.keys():
+                        raise RuntimeError(
+                            'Cannot calculate tendency for {} because it is not'
+                            ' present in the input state.'.format(varname))
+                    tendency = (data_array - state[varname].to_units(data_array.attrs['units'])) / timestep_seconds
+                    if data_array.attrs['units'] == '':
+                        tendency.attrs['units'] = 's^-1'
+                    else:
+                        tendency.attrs['units'] = data_array.attrs['units'] + ' s^-1'
+                    tendencies[varname] = tendency.to_units(
+                        self._implicit.output_properties[varname]['units'] + ' s^-1')
+            elif varname != 'time':
+                raise ValueError(
+                    'Wrapped implicit gave an output {} of type {}, but should'
+                    'only give sympl.DataArray objects.'.format(
+                        varname, type(data_array)))
+        return tendencies, diagnostics
+
+    def array_call(self, state, timestep):
+        raise NotImplementedError()
+
+    def __getattr__(self, item):
+        if item in ('outputs', 'output_properties'):
+            raise AttributeError()
+        else:
+            return getattr(self._implicit, item)
diff --git a/sympl/_components/netcdf.py b/sympl/_components/netcdf.py
index 0ae2829..cc87b6f 100644
--- a/sympl/_components/netcdf.py
+++ b/sympl/_components/netcdf.py
@@ -2,212 +2,204 @@
 from .._core.exceptions import (
     DependencyError, InvalidStateError)
 from .._core.units import from_unit_to_another
-from .._core.array import DataArray
+from .._core.dataarray import DataArray
 from .._core.util import same_list, datetime64_to_datetime
 import xarray as xr
 import os
 import numpy as np
 from datetime import timedelta
 from six import string_types
+
 try:
     import netCDF4 as nc4
 except ImportError:
     nc4 = None
 
-if nc4 is None:
-    # If dependency is not installed, use a dummy object that will alert the
-    # user they need to install the dependency if they try to use it
-    class NetCDFMonitor(Monitor):
 
-        def __init__(self, filename):
+class NetCDFMonitor(Monitor):
+    """A Monitor which caches stored states and then writes them to a
+    NetCDF file when requested."""
+
+    def __init__(
+            self, filename, time_units='seconds', store_names=None,
+            write_on_store=False, aliases=None):
+        """
+        Args
+        ----
+        filename : str
+            The file to which the NetCDF file will be written.
+        time_units : str, optional
+            The units in which time will be
+            stored in the NetCDF file. Time is stored as an integer
+            number of these units. Default is seconds.
+        store_names : iterable of str, optional
+            Names of quantities to store. If not given,
+            all quantities are stored.
+        write_on_store : bool, optional
+            If True, stored changes are immediately written to file.
+            This can result in many file open/close operations.
+            Default is to write only when the write() method is
+            called directly.
+        aliases : dict
+            A dictionary of string replacements to apply to state variable
+            names before saving them in netCDF files.
+        """
+        if nc4 is None:
             raise DependencyError(
                 'netCDF4-python must be installed to use NetCDFMonitor')
+        self._cached_state_dict = {}
+        self._filename = filename
+        self._time_units = time_units
+        self._write_on_store = write_on_store
+        if aliases is None:
+            self._aliases = {}
+        else:
+            self._aliases = aliases
+        for key, val in self._aliases.items():
+            if not isinstance(key, string_types):
+                raise TypeError("Bad alias key type: {}. Expected string.".format(type(key)))
+            elif not isinstance(val, string_types):
+                raise TypeError("Bad alias value type: {}. Expected string.".format(type(val)))
+        if store_names is None:
+            self._store_names = None
+        else:
+            self._store_names = ['time'] + list(store_names)
 
-        def store(self, state):
-            pass
-
-else:
-    class NetCDFMonitor(Monitor):
-        """A Monitor which caches stored states and then writes them to a
-        NetCDF file when requested."""
-
-        def __init__(
-                self, filename, time_units='seconds', store_names=None,
-                write_on_store=False, aliases=None):
-            """
-            Args
-            ----
-            filename : str
-                The file to which the NetCDF file will be written.
-            time_units : str, optional
-                The units in which time will be
-                stored in the NetCDF file. Time is stored as an integer
-                number of these units. Default is seconds.
-            store_names : iterable of str, optional
-                Names of quantities to store. If not given,
-                all quantities are stored.
-            write_on_store : bool, optional
-                If True, stored changes are immediately written to file.
-                This can result in many file open/close operations.
-                Default is to write only when the write() method is
-                called directly.
-            aliases : dict
-                A dictionary of string replacements to apply to state variable
-                names before saving them in netCDF files.
-            """
-            self._cached_state_dict = {}
-            self._filename = filename
-            self._time_units = time_units
-            self._write_on_store = write_on_store
-            if aliases is None:
-                self._aliases = {}
-            else:
-                self._aliases = aliases
-            for key, val in self._aliases.items():
-                if not isinstance(key, string_types):
-                    raise TypeError("Bad alias key type: {}. Expected string.".format(type(key)))
-                elif not isinstance(val, string_types):
-                    raise TypeError("Bad alias value type: {}. Expected string.".format(type(val)))
-            if store_names is None:
-                self._store_names = None
-            else:
-                self._store_names = ['time'] + list(store_names)
-
-        def store(self, state):
-            """
-            Caches the given state. If write_on_store=True was passed on
-            initialization, also writes to file. Normally a call to the
-            write() method is required to write to file.
-
-            Args
-            ----
-            state : dict
-                A model state dictionary.
-
-            Raises
-            ------
-            InvalidStateError
-                If state is not a valid input for the Diagnostic instance.
-            """
-            if self._store_names is not None:
-                name_list = set(state.keys()).intersection(self._store_names)
-                cache_state = {name: state[name] for name in name_list}
-            else:
-                cache_state = state.copy()
-
-            # raise an exception if the state has any empty string variables
-            for full_var_name in cache_state.keys():
-                if len(full_var_name) == 0:
-                    raise ValueError('The given state has an empty string as a variable name.')
-
-            # replace cached variable names with their aliases
-            for longname, shortname in self._aliases.items():
-                for full_var_name in cache_state.keys():
-                    # replace any string in the full variable name that matches longname
-                    # example: if longname is "temperature", shortname is "T", and
-                    #    full_var_name is "temperature_tendency_from_radiation", the
-                    #    alias_name for the variable would be: "T_tendency_from_radiation"
-                    if longname in full_var_name:
-                        alias_name = full_var_name.replace(longname, shortname)
-                        if len(alias_name) == 0:  # raise exception if the alias is an empty str
-                            errstr = 'Tried to alias variable "{}" to an empty string.\n' + \
-                                     'xarray will not allow empty strings as variable names.'
-                            raise ValueError(errstr.format(full_var_name))
-                        cache_state[alias_name] = cache_state.pop(full_var_name)
-
-            cache_state.pop('time')  # stored as key, not needed in state dict
-            if state['time'] in self._cached_state_dict.keys():
-                self._cached_state_dict[state['time']].update(cache_state)
-            else:
-                self._cached_state_dict[state['time']] = cache_state
-            if self._write_on_store:
-                self.write()
-
-        @property
-        def _write_mode(self):
-            if not os.path.isfile(self._filename):
-                return 'w'
-            else:
-                return 'a'
-
-        def _ensure_cached_state_keys_compatible_with_dataset(self, dataset):
-            file_keys = list(dataset.variables.keys())
-            if 'time' in file_keys:
-                file_keys.remove('time')
-            if len(file_keys) > 0:
-                self._ensure_cached_states_have_same_keys(file_keys)
-            else:
-                self._ensure_cached_states_have_same_keys()
-
-        def _ensure_cached_states_have_same_keys(self, desired_keys=None):
-            """
-            Ensures all states in self._cached_state_dict have the same keys.
-            If desired_keys is given, also ensure the keys are the same as
-            the ones in desired_keys.
-
-            Raises
-            ------
-            InvalidStateError
-                If the cached states do not meet the requirements.
-            """
-            if len(self._cached_state_dict) == 0:
-                return  # trivially true
-            if desired_keys is not None:
-                reference_keys = desired_keys
-            else:
-                reference_state = tuple(self._cached_state_dict.values())[0]
-                reference_keys = reference_state.keys()
-            for state in self._cached_state_dict.values():
-                if not same_list(list(state.keys()), list(reference_keys)):
-                    raise InvalidStateError(
-                        'NetCDFMonitor was passed a different set of '
-                        'quantities for different times: {} vs. {}'.format(
-                            list(reference_keys), list(state.keys())))
-
-        def _get_ordered_times_and_states(self):
-            """Returns the items in self._cached_state_dict, sorted by time."""
-            return zip(*sorted(self._cached_state_dict.items(), key=lambda x: x[0]))
-
-        def write(self):
-            """
-            Write all cached states to the NetCDF file, and clear the cache.
-            This will append to any existing NetCDF file.
-
-            Raises
-            ------
-            InvalidStateError
-                If cached states do not all have the same quantities
-                as every other cached and written state.
-            """
-            with nc4.Dataset(self._filename, self._write_mode) as dataset:
-                self._ensure_cached_state_keys_compatible_with_dataset(dataset)
-                time_list, state_list = self._get_ordered_times_and_states()
-                self._ensure_time_exists(dataset, time_list[0])
-                it_start = dataset.dimensions['time'].size
-                it_end = it_start + len(time_list)
-                append_times_to_dataset(time_list, dataset, self._time_units)
-                all_states = combine_states(state_list)
-                for name, value in all_states.items():
-                    ensure_variable_exists(dataset, name, value)
-                    dataset.variables[name][
-                        it_start:it_end, :] = value.values[:, :]
-            self._cached_state_dict = {}
-
-        def _ensure_time_exists(self, dataset, possible_reference_time):
-            """Ensure an unlimited time dimension relevant to this monitor
-            exists in the NetCDF4 dataset, and create it if it does not."""
-            ensure_dimension_exists(dataset, 'time', None)
-            if 'time' not in dataset.variables:
-                dataset.createVariable('time', np.int64, ('time',))
-                if isinstance(possible_reference_time, timedelta):
-                    dataset.variables['time'].setncattr(
-                        'units', self._time_units)
-                else:  # assume datetime
-                    dataset.variables['time'].setncattr(
-                        'units', '{} since {}'.format(
-                            self._time_units, possible_reference_time))
-                    dataset.variables['time'].setncattr(
-                        'calendar', 'proleptic_gregorian')
+    def store(self, state):
+        """
+        Caches the given state. If write_on_store=True was passed on
+        initialization, also writes to file. Normally a call to the
+        write() method is required to write to file.
+
+        Args
+        ----
+        state : dict
+            A model state dictionary.
+
+        Raises
+        ------
+        InvalidStateError
+            If state is not a valid input for the DiagnosticComponent instance.
+        """
+        if self._store_names is not None:
+            name_list = set(state.keys()).intersection(self._store_names)
+            cache_state = {name: state[name] for name in name_list}
+        else:
+            cache_state = state.copy()
+
+        # raise an exception if the state has any empty string variables
+        for full_var_name in cache_state.keys():
+            if len(full_var_name) == 0:
+                raise ValueError('The given state has an empty string as a variable name.')
+
+        # replace cached variable names with their aliases
+        for longname, shortname in self._aliases.items():
+            for full_var_name in tuple(cache_state.keys()):
+                # replace any string in the full variable name that matches longname
+                # example: if longname is "temperature", shortname is "T", and
+                #    full_var_name is "temperature_tendency_from_radiation", the
+                #    alias_name for the variable would be: "T_tendency_from_radiation"
+                if longname in full_var_name:
+                    alias_name = full_var_name.replace(longname, shortname)
+                    if len(alias_name) == 0:  # raise exception if the alias is an empty str
+                        errstr = 'Tried to alias variable "{}" to an empty string.\n' + \
+                                 'xarray will not allow empty strings as variable names.'
+                        raise ValueError(errstr.format(full_var_name))
+                    cache_state[alias_name] = cache_state.pop(full_var_name)
+
+        cache_state.pop('time')  # stored as key, not needed in state dict
+        if state['time'] in self._cached_state_dict.keys():
+            self._cached_state_dict[state['time']].update(cache_state)
+        else:
+            self._cached_state_dict[state['time']] = cache_state
+        if self._write_on_store:
+            self.write()
+
+    @property
+    def _write_mode(self):
+        if not os.path.isfile(self._filename):
+            return 'w'
+        else:
+            return 'a'
+
+    def _ensure_cached_state_keys_compatible_with_dataset(self, dataset):
+        file_keys = list(dataset.variables.keys())
+        if 'time' in file_keys:
+            file_keys.remove('time')
+        if len(file_keys) > 0:
+            self._ensure_cached_states_have_same_keys(file_keys)
+        else:
+            self._ensure_cached_states_have_same_keys()
+
+    def _ensure_cached_states_have_same_keys(self, desired_keys=None):
+        """
+        Ensures all states in self._cached_state_dict have the same keys.
+        If desired_keys is given, also ensure the keys are the same as
+        the ones in desired_keys.
+
+        Raises
+        ------
+        InvalidStateError
+            If the cached states do not meet the requirements.
+        """
+        if len(self._cached_state_dict) == 0:
+            return  # trivially true
+        if desired_keys is not None:
+            reference_keys = desired_keys
+        else:
+            reference_state = tuple(self._cached_state_dict.values())[0]
+            reference_keys = reference_state.keys()
+        for state in self._cached_state_dict.values():
+            if not same_list(list(state.keys()), list(reference_keys)):
+                raise InvalidStateError(
+                    'NetCDFMonitor was passed a different set of '
+                    'quantities for different times: {} vs. {}'.format(
+                        list(reference_keys), list(state.keys())))
+
+    def _get_ordered_times_and_states(self):
+        """Returns the items in self._cached_state_dict, sorted by time."""
+        return zip(*sorted(self._cached_state_dict.items(), key=lambda x: x[0]))
+
+    def write(self):
+        """
+        Write all cached states to the NetCDF file, and clear the cache.
+        This will append to any existing NetCDF file.
+
+        Raises
+        ------
+        InvalidStateError
+            If cached states do not all have the same quantities
+            as every other cached and written state.
+        """
+        with nc4.Dataset(self._filename, self._write_mode) as dataset:
+            self._ensure_cached_state_keys_compatible_with_dataset(dataset)
+            time_list, state_list = self._get_ordered_times_and_states()
+            self._ensure_time_exists(dataset, time_list[0])
+            it_start = dataset.dimensions['time'].size
+            it_end = it_start + len(time_list)
+            append_times_to_dataset(time_list, dataset, self._time_units)
+            all_states = combine_states(state_list)
+            for name, value in all_states.items():
+                ensure_variable_exists(dataset, name, value)
+                dataset.variables[name][
+                    it_start:it_end, :] = value.values[:, :]
+        self._cached_state_dict = {}
+
+    def _ensure_time_exists(self, dataset, possible_reference_time):
+        """Ensure an unlimited time dimension relevant to this monitor
+        exists in the NetCDF4 dataset, and create it if it does not."""
+        ensure_dimension_exists(dataset, 'time', None)
+        if 'time' not in dataset.variables:
+            dataset.createVariable('time', np.int64, ('time',))
+            if isinstance(possible_reference_time, timedelta):
+                dataset.variables['time'].setncattr(
+                    'units', self._time_units)
+            else:  # assume datetime
+                dataset.variables['time'].setncattr(
+                    'units', '{} since {}'.format(
+                        self._time_units, possible_reference_time))
+                dataset.variables['time'].setncattr(
+                    'calendar', 'proleptic_gregorian')
 
 
 class RestartMonitor(Monitor):
@@ -217,6 +209,9 @@ class RestartMonitor(Monitor):
     """
 
     def __init__(self, filename):
+        if nc4 is None:
+            raise DependencyError(
+                'netCDF4-python must be installed to use RestartMonitor')
         self._filename = filename
 
     def store(self, state):
diff --git a/sympl/_components/plot.py b/sympl/_components/plot.py
index b4969b2..21a6f24 100644
--- a/sympl/_components/plot.py
+++ b/sympl/_components/plot.py
@@ -1,6 +1,6 @@
 from .._core.base_components import Monitor
 from .._core.exceptions import DependencyError
-from .._core.array import DataArray
+from .._core.dataarray import DataArray
 
 
 def copy_state(state):
diff --git a/sympl/_core/timestepping.py b/sympl/_components/timesteppers.py
similarity index 75%
rename from sympl/_core/timestepping.py
rename to sympl/_components/timesteppers.py
index ca64403..e76f53e 100644
--- a/sympl/_core/timestepping.py
+++ b/sympl/_components/timesteppers.py
@@ -1,65 +1,44 @@
-from .base_components import PrognosticComposite
-import abc
-from .array import DataArray
-
-
-class TimeStepper(object):
-    """An object which integrates model state forward in time.
-
-    It uses Prognostic and Diagnostic objects to update the current model state
-    with diagnostics, and to return the model state at the next timestep.
-
-    Attributes
-    ----------
-    inputs : tuple of str
-        The quantities required in the state when the object is called.
-    diagnostics: tuple of str
-        The quantities for which values for the old state are returned
-        when the object is called.
-    outputs: tuple of str
-        The quantities for which values for the new state are returned
-        when the object is called.
+from .._core.tendencystepper import TendencyStepper
+from .._core.dataarray import DataArray
+from .._core.state import copy_untouched_quantities, add, multiply
+
+
+class SSPRungeKutta(TendencyStepper):
+    """
+    A TendencyStepper using the Strong Stability Preserving Runge-Kutta scheme,
+    as in Numerical Methods for Fluid Dynamics by Dale Durran (2nd ed) and
+    as proposed by Shu and Osher (1988).
     """
 
-    __metaclass__ = abc.ABCMeta
-
-    def __str__(self):
-        return (
-            'instance of {}(TimeStepper)\n'
-            '    inputs: {}\n'
-            '    outputs: {}\n'
-            '    diagnostics: {}\n'
-            '    Prognostic components: {}'.format(
-                self.__class__, self.inputs, self.outputs, self.diagnostics,
-                str(self._prognostic))
-        )
+    def __init__(self, *args, **kwargs):
+        """
+        Initialize a strong stability preserving Runge-Kutta time stepper.
 
-    def __repr__(self):
-        if hasattr(self, '_making_repr') and self._making_repr:
-            return '{}(recursive reference)'.format(self.__class__)
-        else:
-            self._making_repr = True
-            return_value = '{}({})'.format(
-                self.__class__,
-                '\n'.join('{}: {}'.format(repr(key), repr(value))
-                          for key, value in self.__dict__.items()
-                          if key != '_making_repr'))
-            self._making_repr = False
-            return return_value
-
-    def __init__(self, prognostic_list, **kwargs):
-        self._prognostic = PrognosticComposite(*prognostic_list)
-
-    @abc.abstractmethod
-    def __call__(self, state, timestep):
+        Args
+        ----
+        *args : TendencyComponent or ImplicitTendencyComponent
+            Objects to call for tendencies when doing time stepping.
+        stages: int, optional
+            Number of stages to use. Should be 2 or 3. Default is 3.
+        """
+        stages = kwargs.pop('stages', 3)
+        if stages not in (2, 3):
+            raise ValueError(
+                'stages must be one of 2 or 3, received {}'.format(stages))
+        self._stages = stages
+        self._euler_stepper = AdamsBashforth(*args, order=1)
+        super(SSPRungeKutta, self).__init__(*args, **kwargs)
+
+    def _call(self, state, timestep):
         """
-        Retrieves any diagnostics and returns a new state corresponding
+        Updates the input state dictionary and returns a new state corresponding
         to the next timestep.
 
         Args
         ----
         state : dict
-            The current model state.
+            The current model state. Will be updated in-place by
+            the call with any diagnostics from the current timestep.
         timestep : timedelta
             The amount of time to step forward.
 
@@ -70,40 +49,44 @@ def __call__(self, state, timestep):
         new_state : dict
             The model state at the next timestep.
         """
-
-    def _copy_untouched_quantities(self, old_state, new_state):
-        for key in old_state.keys():
-            if key not in new_state:
-                new_state[key] = old_state[key]
-
-    @property
-    def inputs(self):
-        return self._prognostic.inputs
-
-    @property
-    def outputs(self):
-        return self._prognostic.tendencies
-
-    @property
-    def diagnostics(self):
-        return self._prognostic.diagnostics
-
-
-class AdamsBashforth(TimeStepper):
-    """A TimeStepper using the Adams-Bashforth scheme."""
-
-    def __init__(self, prognostic_list, order=3):
+        if self._stages == 2:
+            return self._step_2_stages(state, timestep)
+        elif self._stages == 3:
+            return self._step_3_stages(state, timestep)
+
+    def _step_3_stages(self, state, timestep):
+        diagnostics, state_1 = self._euler_stepper(state, timestep)
+        _, state_1_5 = self._euler_stepper(state_1, timestep)
+        state_2 = add(multiply(0.75, state), multiply(0.25, state_1_5))
+        _, state_2_5 = self._euler_stepper(state_2, timestep)
+        out_state = add(multiply(1./3, state), multiply(2./3, state_2_5))
+        return diagnostics, out_state
+
+    def _step_2_stages(self, state, timestep):
+        assert state is not None
+        diagnostics, state_1 = self._euler_stepper(state, timestep)
+        assert state_1 is not None
+        _, state_2 = self._euler_stepper(state_1, timestep)
+        out_state = multiply(0.5, add(state, state_2))
+        return diagnostics, out_state
+
+
+class AdamsBashforth(TendencyStepper):
+    """A TendencyStepper using the Adams-Bashforth scheme."""
+
+    def __init__(self, *args, **kwargs):
         """
         Initialize an Adams-Bashforth time stepper.
 
         Args
         ----
-        prognostic_list : iterable of Prognostic
-            Objects used to get tendencies for time stepping.
+        *args : TendencyComponent or ImplicitTendencyComponent
+            Objects to call for tendencies when doing time stepping.
         order : int, optional
             The order of accuracy to use. Must be between
             1 and 4. 1 is the same as the Euler method. Default is 3.
         """
+        order = kwargs.pop('order', 3)
         if isinstance(order, float) and order.is_integer():
             order = int(order)
         if not isinstance(order, int):
@@ -113,9 +96,9 @@ def __init__(self, prognostic_list, order=3):
         self._order = order
         self._timestep = None
         self._tendencies_list = []
-        super(AdamsBashforth, self).__init__(prognostic_list)
+        super(AdamsBashforth, self).__init__(*args, **kwargs)
 
-    def __call__(self, state, timestep):
+    def _call(self, state, timestep):
         """
         Updates the input state dictionary and returns a new state corresponding
         to the next timestep.
@@ -143,11 +126,11 @@ def __call__(self, state, timestep):
         """
         self._ensure_constant_timestep(timestep)
         state = state.copy()
-        tendencies, diagnostics = self._prognostic(state)
+        tendencies, diagnostics = self.prognostic(state, timestep)
         convert_tendencies_units_for_state(tendencies, state)
         self._tendencies_list.append(tendencies)
         new_state = self._perform_step(state, timestep)
-        self._copy_untouched_quantities(state, new_state)
+        copy_untouched_quantities(state, new_state)
         if len(self._tendencies_list) == self._order:
             self._tendencies_list.pop(0)  # remove the oldest entry
         return diagnostics, new_state
@@ -190,8 +173,8 @@ def convert_tendencies_units_for_state(tendencies, state):
             tendencies[quantity_name] = tendencies[quantity_name].to_units(desired_units)
 
 
-class Leapfrog(TimeStepper):
-    """A TimeStepper using the Leapfrog scheme.
+class Leapfrog(TendencyStepper):
+    """A TendencyStepper using the Leapfrog scheme.
 
     This scheme calculates the
     values at time $t_{n+1}$ using the derivatives at $t_{n}$ and values at
@@ -201,16 +184,14 @@ class Leapfrog(TimeStepper):
     $t_{n+1}$, according to Williams (2009)) closer to the mean of the values
     at $t_{n-1}$ and $t_{n+1}$."""
 
-    def __init__(
-            self, prognostic_list, asselin_strength=0.05,
-            alpha=0.5):
+    def __init__(self, *args, **kwargs):
         """
         Initialize a Leapfrog time stepper.
 
         Args
         ----
-        prognostic_list : iterable of Prognostic
-            Objects used to get tendencies for time stepping.
+        *args : TendencyComponent or ImplicitTendencyComponent
+            Objects to call for tendencies when doing time stepping.
         asselin_strength : float, optional
             The filter parameter used to determine the strength
             of the Asselin filter. Default is 0.05.
@@ -229,12 +210,12 @@ def __init__(
         doi: 10.1175/2009MWR2724.1.
         """
         self._old_state = None
-        self._asselin_strength = asselin_strength
+        self._asselin_strength = kwargs.pop('asselin_strength', 0.05)
         self._timestep = None
-        self._alpha = alpha
-        super(Leapfrog, self).__init__(prognostic_list)
+        self._alpha = kwargs.pop('alpha', 0.5)
+        super(Leapfrog, self).__init__(*args, **kwargs)
 
-    def __call__(self, state, timestep):
+    def _call(self, state, timestep):
         """
         Updates the input state dictionary and returns a new state corresponding
         to the next timestep.
@@ -257,7 +238,7 @@ def __call__(self, state, timestep):
         Raises
         ------
         SharedKeyError
-            If a Diagnostic object has an output that is
+            If a DiagnosticComponent object has an output that is
             already in the state at the start of the timestep.
         ValueError
             If the timestep is not the same as the last time
@@ -266,7 +247,7 @@ def __call__(self, state, timestep):
         original_state = state
         state = state.copy()
         self._ensure_constant_timestep(timestep)
-        tendencies, diagnostics = self._prognostic(state)
+        tendencies, diagnostics = self.prognostic(state, timestep)
         convert_tendencies_units_for_state(tendencies, state)
         if self._old_state is None:
             new_state = step_forward_euler(state, tendencies, timestep)
@@ -274,7 +255,7 @@ def __call__(self, state, timestep):
             state, new_state = step_leapfrog(
                 self._old_state, state, tendencies, timestep,
                 asselin_strength=self._asselin_strength, alpha=self._alpha)
-        self._copy_untouched_quantities(state, new_state)
+        copy_untouched_quantities(state, new_state)
         self._old_state = state
         for key in original_state.keys():
             original_state[key] = state[key]  # allow filtering to be applied
diff --git a/sympl/_core/base_components.py b/sympl/_core/base_components.py
index 58ed688..3945bba 100644
--- a/sympl/_core/base_components.py
+++ b/sympl/_core/base_components.py
@@ -1,20 +1,415 @@
 import abc
-from .util import ensure_no_shared_keys, update_dict_by_adding_another
-from .exceptions import SharedKeyError
+from .get_np_arrays import get_numpy_arrays_with_properties
+from .restore_dataarray import restore_data_arrays_with_properties
+from .time import timedelta
+from .exceptions import (
+    InvalidPropertyDictError, ComponentExtraOutputError,
+    ComponentMissingOutputError, InvalidStateError)
+from six import add_metaclass
+from .units import units_are_compatible
+from .tracers import TracerPacker
+try:
+    from inspect import getfullargspec as getargspec
+except ImportError:
+    from inspect import getargspec
+
+
+def option_or_default(option, default):
+    if option is None:
+        return default
+    else:
+        return option
+
+
+def apply_scale_factors(array_state, scale_factors):
+    for key, factor in scale_factors.items():
+        array_state[key] *= factor
+
+
+def is_component_class(cls):
+    return any(issubclass(cls, cls2) for cls2 in (Stepper, TendencyComponent, ImplicitTendencyComponent, DiagnosticComponent))
+
+
+def is_component_base_class(cls):
+    return cls in (Stepper, TendencyComponent, ImplicitTendencyComponent, DiagnosticComponent)
+
+
+def get_kwarg_defaults(func):
+    return_dict = {}
+    argspec = getargspec(func)
+    if argspec.defaults is not None:
+        n = len(argspec.args) - 1
+        for i, default in enumerate(reversed(argspec.defaults)):
+            return_dict[argspec.args[n-i]] = default
+    return return_dict
+
+
+class ComponentMeta(abc.ABCMeta):
+
+    def __instancecheck__(cls, instance):
+        if is_component_class(instance.__class__) or not is_component_base_class(cls):
+            return issubclass(instance.__class__, cls)
+        else:  # checking if non-inheriting instance is a duck-type of a component base class
+            required_attributes, disallowed_attributes = cls.__get_attribute_requirements()
+            has_attributes = (
+                all(hasattr(instance, att) for att in required_attributes) and
+                not any(hasattr(instance, att) for att in disallowed_attributes)
+            )
+            if hasattr(cls, '__call__') and not hasattr(instance, '__call__'):
+                return False
+            elif hasattr(cls, '__call__'):
+                timestep_in_class_call = 'timestep' in getargspec(cls.__call__).args
+                instance_argspec = getargspec(instance.__call__)
+                timestep_in_instance_call = 'timestep' in instance_argspec.args
+                instance_defaults = get_kwarg_defaults(instance.__call__)
+                timestep_is_optional = (
+                    'timestep' in instance_defaults.keys() and instance_defaults['timestep'] is None)
+                has_correct_spec = (timestep_in_class_call == timestep_in_instance_call) or timestep_is_optional
+            else:
+                raise RuntimeError(
+                    'Cannot check instance type on component subclass that has '
+                    'no __call__ method')
+            return has_attributes and has_correct_spec
+
+    def __get_attribute_requirements(cls):
+        check_attributes = (
+            'input_properties',
+            'tendency_properties',
+            'diagnostic_properties',
+            'output_properties',
+            '__call__',
+            'array_call',
+            'tendencies_in_diagnostics',
+            'name',
+        )
+        required_attributes = list(
+            att for att in check_attributes if hasattr(cls, att)
+        )
+        disallowed_attributes = list(
+            att for att in check_attributes if att not in required_attributes
+        )
+        if 'name' in disallowed_attributes:  # name is always allowed
+            disallowed_attributes.remove('name')
+        return required_attributes, disallowed_attributes
+
+
+def check_overlapping_aliases(properties, properties_name):
+    defined_aliases = set()
+    for name, properties in properties.items():
+        if 'alias' in properties.keys():
+            if properties['alias'] not in defined_aliases:
+                defined_aliases.add(properties['alias'])
+            else:
+                raise InvalidPropertyDictError(
+                    'Multiple quantities map to alias {} in {} '
+                    'properties'.format(
+                        properties['alias'], properties_name)
+                )
+
+
+class InputChecker(object):
+
+    def __init__(self, component):
+        self.component = component
+        if not hasattr(component, 'input_properties'):
+            raise InvalidPropertyDictError(
+                'Component of type {} is missing input_properties'.format(
+                    component.__class__.__name__)
+            )
+        elif not isinstance(component.input_properties, dict):
+            raise InvalidPropertyDictError(
+                'input_properties on component of type {} is of type {}, but '
+                'should be an instance of dict'.format(
+                    component.__class__.__name__,
+                    component.input_properties.__class__)
+            )
+        for name, properties in self.component.input_properties.items():
+            if 'units' not in properties.keys():
+                raise InvalidPropertyDictError(
+                    'Input properties do not have units defined for {}'.format(name))
+            if 'dims' not in properties.keys():
+                raise InvalidPropertyDictError(
+                    'Input properties do not have dims defined for {}'.format(name)
+                )
+        check_overlapping_aliases(self.component.input_properties, 'input')
+        super(InputChecker, self).__init__()
+
+    def check_inputs(self, state):
+        for key in self.component.input_properties.keys():
+            if key not in state.keys():
+                raise InvalidStateError('Missing input quantity {}'.format(key))
+
+
+def get_name_with_incompatible_units(properties1, properties2):
+    """
+    If there are any keys shared by the two properties
+    dictionaries which indicate units that are incompatible with one another,
+    this returns such a key. Otherwise returns None.
+    """
+    for name in set(properties1.keys()).intersection(properties2.keys()):
+        if not units_are_compatible(
+                properties1[name]['units'], properties2[name]['units']):
+            return name
+    return None
+
+
+def get_tendency_name_with_incompatible_units(input_properties, tendency_properties):
+    """
+    Returns False if there are any keys shared by the two properties
+    dictionaries which indicate units that are incompatible with one another,
+    and True otherwise (if there are no conflicting unit specifications).
+    """
+    for name in set(input_properties.keys()).intersection(tendency_properties.keys()):
+        if input_properties[name]['units'] == '':
+            expected_tendency_units = 's^-1'
+        else:
+            expected_tendency_units = input_properties[name]['units'] + ' s^-1'
+        if not units_are_compatible(
+                expected_tendency_units, tendency_properties[name]['units']):
+            return name
+    return None
+
+
+class TendencyChecker(object):
+
+    def __init__(self, component):
+        self.component = component
+        if not hasattr(component, 'tendency_properties'):
+            raise InvalidPropertyDictError(
+                'Component of type {} is missing tendency_properties'.format(
+                    component.__class__.__name__)
+            )
+        elif not isinstance(component.tendency_properties, dict):
+            raise InvalidPropertyDictError(
+                'tendency_properties on component of type {} is of type {}, but '
+                'should be an instance of dict'.format(
+                    component.__class__.__name__,
+                    component.input_properties.__class__)
+            )
+        for name, properties in self.component.tendency_properties.items():
+            if 'units' not in properties.keys():
+                raise InvalidPropertyDictError(
+                    'Tendency properties do not have units defined for {}'.format(name))
+            if 'dims' not in properties.keys() and name not in self.component.input_properties.keys():
+                raise InvalidPropertyDictError(
+                    'Tendency properties do not have dims defined for {}'.format(name)
+                )
+        check_overlapping_aliases(self.component.tendency_properties, 'tendency')
+        incompatible_name = get_tendency_name_with_incompatible_units(
+            self.component.input_properties, self.component.tendency_properties)
+        if incompatible_name is not None:
+            raise InvalidPropertyDictError(
+                'Component of type {} has input {} with tendency units {} that '
+                'are incompatible with input units {}'.format(
+                    type(self.component), incompatible_name,
+                    self.component.tendency_properties[incompatible_name]['units'],
+                    self.component.input_properties[incompatible_name]['units']))
+        super(TendencyChecker, self).__init__()
+
+    @property
+    def _wanted_tendency_aliases(self):
+        wanted_tendency_aliases = {}
+        for name, properties in self.component.tendency_properties.items():
+            wanted_tendency_aliases[name] = []
+            if 'alias' in properties.keys():
+                wanted_tendency_aliases[name].append(properties['alias'])
+            if (name in self.component.input_properties.keys() and
+                    'alias' in self.component.input_properties[name].keys()):
+                wanted_tendency_aliases[name].append(self.component.input_properties[name]['alias'])
+        return wanted_tendency_aliases
+
+    def _check_missing_tendencies(self, tendency_dict):
+        missing_tendencies = set()
+        for name, aliases in self._wanted_tendency_aliases.items():
+            if (name not in tendency_dict.keys() and
+                    not any(alias in tendency_dict.keys() for alias in aliases)):
+                missing_tendencies.add(name)
+        if len(missing_tendencies) > 0:
+            raise ComponentMissingOutputError(
+                'Component {} did not compute tendencies for {}'.format(
+                    self.component.__class__.__name__, ', '.join(missing_tendencies)))
+
+    def _check_extra_tendencies(self, tendency_dict):
+        wanted_set = set()
+        wanted_set.update(self._wanted_tendency_aliases.keys())
+        for value_list in self._wanted_tendency_aliases.values():
+            wanted_set.update(value_list)
+        extra_tendencies = set(tendency_dict.keys()).difference(wanted_set)
+        if len(extra_tendencies) > 0:
+            raise ComponentExtraOutputError(
+                'Component {} computed tendencies for {} which are not in '
+                'tendency_properties'.format(
+                    self.component.__class__.__name__, ', '.join(extra_tendencies)))
+
+    def check_tendencies(self, tendency_dict):
+        self._check_missing_tendencies(tendency_dict)
+        self._check_extra_tendencies(tendency_dict)
+
+
+class DiagnosticChecker(object):
+
+    def __init__(self, component):
+        self.component = component
+        if not hasattr(component, 'diagnostic_properties'):
+            raise InvalidPropertyDictError(
+                'Component of type {} is missing diagnostic_properties'.format(
+                    component.__class__.__name__)
+            )
+        elif not isinstance(component.diagnostic_properties, dict):
+            raise InvalidPropertyDictError(
+                'diagnostic_properties on component of type {} is of type {}, but '
+                'should be an instance of dict'.format(
+                    component.__class__.__name__,
+                    component.input_properties.__class__)
+            )
+        self._ignored_diagnostics = []
+        for name, properties in component.diagnostic_properties.items():
+            if 'units' not in properties.keys():
+                raise InvalidPropertyDictError(
+                    'DiagnosticComponent properties do not have units defined for {}'.format(name))
+            if 'dims' not in properties.keys() and name not in component.input_properties.keys():
+                raise InvalidPropertyDictError(
+                    'DiagnosticComponent properties do not have dims defined for {}'.format(name)
+                )
+        incompatible_name = get_name_with_incompatible_units(
+            self.component.input_properties, self.component.diagnostic_properties)
+        if incompatible_name is not None:
+            raise InvalidPropertyDictError(
+                'Component of type {} has input {} with diagnostic units {} that '
+                'are incompatible with input units {}'.format(
+                    type(self.component), incompatible_name,
+                    self.component.diagnostic_properties[incompatible_name]['units'],
+                    self.component.input_properties[incompatible_name]['units']))
+        check_overlapping_aliases(component.diagnostic_properties, 'diagnostic')
 
+    @property
+    def _wanted_diagnostic_aliases(self):
+        wanted_diagnostic_aliases = {}
+        for name, properties in self.component.diagnostic_properties.items():
+            wanted_diagnostic_aliases[name] = []
+            if 'alias' in properties.keys():
+                wanted_diagnostic_aliases[name].append(properties['alias'])
+            if (name in self.component.input_properties.keys() and
+                    'alias' in self.component.input_properties[name].keys()):
+                wanted_diagnostic_aliases[name].append(
+                    self.component.input_properties[name]['alias'])
+        return wanted_diagnostic_aliases
+
+    def _check_missing_diagnostics(self, diagnostics_dict):
+        missing_diagnostics = set()
+        for name, aliases in self._wanted_diagnostic_aliases.items():
+            if (name not in diagnostics_dict.keys() and
+                    name not in self._ignored_diagnostics and
+                    not any(alias in diagnostics_dict.keys() for alias in aliases)):
+                missing_diagnostics.add(name)
+        if len(missing_diagnostics) > 0:
+            raise ComponentMissingOutputError(
+                'Component {} did not compute diagnostic(s) {}'.format(
+                    self.component.__class__.__name__, ', '.join(missing_diagnostics)))
+
+    def _check_extra_diagnostics(self, diagnostics_dict):
+        wanted_set = set()
+        wanted_set.update(self._wanted_diagnostic_aliases.keys())
+        for value_list in self._wanted_diagnostic_aliases.values():
+            wanted_set.update(value_list)
+        extra_diagnostics = set(diagnostics_dict.keys()).difference(wanted_set)
+        if len(extra_diagnostics) > 0:
+            raise ComponentExtraOutputError(
+                'Component {} computed diagnostic(s) {} which are not in '
+                'diagnostic_properties'.format(
+                    self.component.__class__.__name__, ', '.join(extra_diagnostics)))
+
+    def set_ignored_diagnostics(self, ignored_diagnostics):
+        self._ignored_diagnostics = ignored_diagnostics
+
+    def check_diagnostics(self, diagnostics_dict):
+        self._check_missing_diagnostics(diagnostics_dict)
+        self._check_extra_diagnostics(diagnostics_dict)
+
+
+class OutputChecker(object):
+
+    def __init__(self, component):
+        self.component = component
+        if not hasattr(component, 'output_properties'):
+            raise InvalidPropertyDictError(
+                'Component of type {} is missing output_properties'.format(
+                    component.__class__.__name__)
+            )
+        elif not isinstance(component.output_properties, dict):
+            raise InvalidPropertyDictError(
+                'output_properties on component of type {} is of type {}, but '
+                'should be an instance of dict'.format(
+                    component.__class__.__name__,
+                    component.input_properties.__class__)
+            )
+        for name, properties in self.component.output_properties.items():
+            if 'units' not in properties.keys():
+                raise InvalidPropertyDictError(
+                    'Output properties do not have units defined for {}'.format(name))
+            if 'dims' not in properties.keys() and name not in self.component.input_properties.keys():
+                raise InvalidPropertyDictError(
+                    'Output properties do not have dims defined for {}'.format(name)
+                )
+        check_overlapping_aliases(self.component.output_properties, 'output')
+        incompatible_name = get_name_with_incompatible_units(
+            self.component.input_properties, self.component.output_properties)
+        if incompatible_name is not None:
+            raise InvalidPropertyDictError(
+                'Component of type {} has input {} with output units {} that '
+                'are incompatible with input units {}'.format(
+                    type(self.component), incompatible_name,
+                    self.component.output_properties[incompatible_name]['units'],
+                    self.component.input_properties[incompatible_name]['units']))
+        super(OutputChecker, self).__init__()
 
-class Implicit(object):
+    @property
+    def _wanted_output_aliases(self):
+        wanted_output_aliases = {}
+        for name, properties in self.component.output_properties.items():
+            wanted_output_aliases[name] = []
+            if 'alias' in properties.keys():
+                wanted_output_aliases[name].append(properties['alias'])
+            if (name in self.component.input_properties.keys() and
+                    'alias' in self.component.input_properties[name].keys()):
+                wanted_output_aliases[name].append(
+                    self.component.input_properties[name]['alias'])
+        return wanted_output_aliases
+
+    def _check_missing_outputs(self, outputs_dict):
+        missing_outputs = set()
+        for name, aliases in self._wanted_output_aliases.items():
+            if (name not in outputs_dict.keys() and
+                    not any(alias in outputs_dict.keys() for alias in
+                            aliases)):
+                missing_outputs.add(name)
+        if len(missing_outputs) > 0:
+            raise ComponentMissingOutputError(
+                'Component {} did not compute output(s) {}'.format(
+                    self.component.__class__.__name__, ', '.join(missing_outputs)))
+
+    def _check_extra_outputs(self, outputs_dict):
+        wanted_set = set()
+        wanted_set.update(self._wanted_output_aliases.keys())
+        for value_list in self._wanted_output_aliases.values():
+            wanted_set.update(value_list)
+        extra_outputs = set(outputs_dict.keys()).difference(wanted_set)
+        if len(extra_outputs) > 0:
+            raise ComponentExtraOutputError(
+                'Component {} computed output(s) {} which are not in '
+                'output_properties'.format(
+                    self.component.__class__.__name__, ', '.join(extra_outputs)))
+
+    def check_outputs(self, output_dict):
+        self._check_missing_outputs(output_dict)
+        self._check_extra_outputs(output_dict)
+
+
+@add_metaclass(ComponentMeta)
+class Stepper(object):
     """
     Attributes
     ----------
-    inputs : tuple of str
-        The quantities required in the state when the object is called.
-    diagnostics: tuple of str
-        The quantities for which values for the old state are returned
-        when the object is called.
-    outputs: tuple of str
-        The quantities for which values for the new state are returned
-        when the object is called.
     input_properties : dict
         A dictionary whose keys are quantities required in the state when the
         object is called, and values are dictionaries which indicate 'dims' and
@@ -29,32 +424,47 @@ class Implicit(object):
         for the new state are returned when the
         object is called, and values are dictionaries which indicate 'dims' and
         'units'.
+    tendencies_in_diagnostics : bool
+        A boolean indicating whether this object will put tendencies of
+        quantities in its diagnostic output based on first order time
+        differencing of output values.
+    time_unit_name : str
+        The unit to use for time differencing when putting tendencies in
+        diagnostics.
+    time_unit_timedelta: timedelta
+        A timedelta corresponding to a single time unit as used for time
+        differencing when putting tendencies in diagnostics.
+    name : string
+        A label to be used for this object, for example as would be used for
+        Y in the name "X_tendency_from_Y".
     """
-    __metaclass__ = abc.ABCMeta
 
-    input_properties = {}
-    diagnostic_properties = {}
-    output_properties = {}
+    time_unit_name = 's'
+    time_unit_timedelta = timedelta(seconds=1)
+    uses_tracers = False
+    tracer_dims = None
 
-    @property
-    def inputs(self):
-        return list(self.input_properties.keys())
+    @abc.abstractproperty
+    def input_properties(self):
+        return {}
 
-    @property
-    def diagnostics(self):
-        return list(self.diagnostic_properties.keys())
+    @abc.abstractproperty
+    def diagnostic_properties(self):
+        return {}
 
-    @property
-    def outputs(self):
-        return list(self.output_properties.keys())
+    @abc.abstractproperty
+    def output_properties(self):
+        return {}
 
     def __str__(self):
         return (
-            'instance of {}(Implicit)\n'
+            'instance of {}(Stepper)\n'
             '    inputs: {}\n'
             '    outputs: {}\n'
             '    diagnostics: {}'.format(
-                self.__class__, self.inputs, self.outputs, self.diagnostics)
+                self.__class__, self.input_properties.keys(),
+                self.output_properties.keys(),
+                self.diagnostic_properties.keys())
         )
 
     def __repr__(self):
@@ -70,7 +480,104 @@ def __repr__(self):
             self._making_repr = False
             return return_value
 
-    @abc.abstractmethod
+    def __init__(self, tendencies_in_diagnostics=False, name=None):
+        """
+        Initializes the Stepper object.
+
+        Args
+        ----
+        tendencies_in_diagnostics : bool, optional
+            A boolean indicating whether this object will put tendencies of
+            quantities in its diagnostic output based on first order time
+            differencing of output values.
+        name : string, optional
+            A label to be used for this object, for example as would be used for
+            Y in the name "X_tendency_from_Y". By default the class name in
+            lowercase is used.
+        """
+        self._tendencies_in_diagnostics = tendencies_in_diagnostics
+        self.name = name or self.__class__.__name__
+        super(Stepper, self).__init__()
+        self._input_checker = InputChecker(self)
+        self._diagnostic_checker = DiagnosticChecker(self)
+        self._output_checker = OutputChecker(self)
+        if tendencies_in_diagnostics:
+            self._diagnostic_checker.set_ignored_diagnostics(
+                self._insert_tendency_properties())
+        self.__initialized = True
+        if self.uses_tracers:
+            if self.tracer_dims is None:
+                raise ValueError(
+                    'Component of type {} must specify tracer_dims property '
+                    'when uses_tracers=True'.format(self.__class__.__name__))
+            prepend_tracers = getattr(self, 'prepend_tracers', None)
+            self._tracer_packer = TracerPacker(
+                self, self.tracer_dims, prepend_tracers=prepend_tracers)
+        super(Stepper, self).__init__()
+
+    def _insert_tendency_properties(self):
+        added_names = []
+        for name, properties in self.output_properties.items():
+            tendency_name = self._get_tendency_name(name)
+            if properties['units'] is '':
+                units = 's^-1'
+            else:
+                units = '{} s^-1'.format(properties['units'])
+            if 'dims' in properties.keys():
+                dims = properties['dims']
+            else:
+                dims = self.input_properties[name]['dims']
+            self.diagnostic_properties[tendency_name] = {
+                'units': units,
+                'dims': dims,
+            }
+            if name not in self.input_properties.keys():
+                self.input_properties[name] = {
+                    'dims': dims,
+                    'units': properties['units'],
+                }
+            elif self.input_properties[name]['dims'] != dims:
+                raise InvalidPropertyDictError(
+                    'Can only calculate tendencies when input and output values'
+                    ' have the same dimensions, but dims for {} are '
+                    '{} (input) and {} (output)'.format(
+                        name, self.input_properties[name]['dims'],
+                        self.output_properties[name]['dims']
+                    )
+                )
+            elif self.input_properties[name]['units'] != self.output_properties[name]['units']:
+                raise InvalidPropertyDictError(
+                    'Can only calculate tendencies when input and output values'
+                    ' have the same units, but units for {} are '
+                    '{} (input) and {} (output)'.format(
+                        name, self.input_properties[name]['units'],
+                        self.output_properties[name]['units']
+                    )
+                )
+            added_names.append(tendency_name)
+        return added_names
+
+    def _get_tendency_name(self, name):
+        return '{}_tendency_from_{}'.format(name, self.name)
+
+    @property
+    def tendencies_in_diagnostics(self):
+        return self._tendencies_in_diagnostics  # value cannot be modified
+
+    def _check_self_is_initialized(self):
+        try:
+            initialized = self.__initialized
+        except AttributeError:
+            initialized = False
+        if not initialized:
+            raise RuntimeError(
+                'Component has not called __init__ of base class, likely '
+                'because its class {} is missing a call to '
+                'super({}, self).__init__(**kwargs) in its __init__ '
+                'method.'.format(
+                    self.__class__.__name__, self.__class__.__name__)
+            )
+
     def __call__(self, state, timestep):
         """
         Gets diagnostics from the current model state and steps the state
@@ -79,9 +586,8 @@ def __call__(self, state, timestep):
         Args
         ----
         state : dict
-            A model state dictionary. Will be updated with any
-            diagnostic quantities produced by this object for the time of
-            the input state.
+            A model state dictionary satisfying the input_properties of this
+            object.
         timestep : timedelta
             The amount of time to step forward.
 
@@ -99,22 +605,74 @@ def __call__(self, state, timestep):
         KeyError
             If a required quantity is missing from the state.
         InvalidStateError
-            If state is not a valid input for the Implicit instance
+            If state is not a valid input for the Stepper instance
             for other reasons.
         """
+        self._check_self_is_initialized()
+        self._input_checker.check_inputs(state)
+        raw_state = get_numpy_arrays_with_properties(state, self.input_properties)
+        if self.uses_tracers:
+            raw_state['tracers'] = self._tracer_packer.pack(state)
+        raw_state['time'] = state['time']
+        raw_diagnostics, raw_new_state = self.array_call(raw_state, timestep)
+        if self.uses_tracers:
+            new_state = self._tracer_packer.unpack(
+                raw_new_state.pop('tracers'), state)
+        else:
+            new_state = {}
+        self._diagnostic_checker.check_diagnostics(raw_diagnostics)
+        self._output_checker.check_outputs(raw_new_state)
+        if self.tendencies_in_diagnostics:
+            self._insert_tendencies_to_diagnostics(
+                raw_state, raw_new_state, timestep, raw_diagnostics)
+        diagnostics = restore_data_arrays_with_properties(
+            raw_diagnostics, self.diagnostic_properties,
+            state, self.input_properties)
+        new_state.update(restore_data_arrays_with_properties(
+            raw_new_state, self.output_properties,
+            state, self.input_properties))
+        return diagnostics, new_state
+
+    def _insert_tendencies_to_diagnostics(
+            self, raw_state, raw_new_state, timestep, raw_diagnostics):
+        for name in self.output_properties.keys():
+            tendency_name = self._get_tendency_name(name)
+            raw_diagnostics[tendency_name] = (
+                (raw_new_state[name] - raw_state[name]) /
+                timestep.total_seconds() * self.time_unit_timedelta.total_seconds())
+
+    @abc.abstractmethod
+    def array_call(self, state, timestep):
+        """
+        Gets diagnostics from the current model state and steps the state
+        forward in time according to the timestep.
+
+        Args
+        ----
+        state : dict
+            A numpy array state dictionary. Instead of data arrays, should
+            include numpy arrays that satisfy the input_properties of this
+            object.
+        timestep : timedelta
+            The amount of time to step forward.
+
+        Returns
+        -------
+        diagnostics : dict
+            Diagnostics from the timestep of the input state, as numpy arrays.
+        new_state : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the value of those quantities
+            at the timestep after input state, as numpy arrays.
+        """
+        pass
 
 
-class Prognostic(object):
+@add_metaclass(ComponentMeta)
+class TendencyComponent(object):
     """
     Attributes
     ----------
-    inputs : tuple of str
-        The quantities required in the state when the object is called.
-    tendencies : tuple of str
-        The quantities for which tendencies are returned when
-        the object is called.
-    diagnostics : tuple of str
-        The diagnostic quantities returned when the object is called.
     input_properties : dict
         A dictionary whose keys are quantities required in the state when the
         object is called, and values are dictionaries which indicate 'dims' and
@@ -127,32 +685,40 @@ class Prognostic(object):
         A dictionary whose keys are diagnostic quantities returned when the
         object is called, and values are dictionaries which indicate 'dims' and
         'units'.
+    tendencies_in_diagnostics : bool
+        A boolean indicating whether this object will put tendencies of
+        quantities in its diagnostic output based on first order time
+        differencing of output values.
+    name : string
+        A label to be used for this object, for example as would be used for
+        Y in the name "X_tendency_from_Y".
     """
-    __metaclass__ = abc.ABCMeta
 
-    input_properties = {}
-    tendency_properties = {}
-    diagnostic_properties = {}
+    @abc.abstractproperty
+    def input_properties(self):
+        return {}
 
-    @property
-    def inputs(self):
-        return list(self.input_properties.keys())
+    @abc.abstractproperty
+    def tendency_properties(self):
+        return {}
 
-    @property
-    def tendencies(self):
-        return list(self.tendency_properties.keys())
+    @abc.abstractproperty
+    def diagnostic_properties(self):
+        return {}
 
-    @property
-    def diagnostics(self):
-        return list(self.diagnostic_properties.keys())
+    name = None
+    uses_tracers = False
+    tracer_tendency_time_unit = 's^-1'
 
     def __str__(self):
         return (
-            'instance of {}(Prognostic)\n'
+            'instance of {}(TendencyComponent)\n'
             '    inputs: {}\n'
             '    tendencies: {}\n'
             '    diagnostics: {}'.format(
-                self.__class__, self.inputs, self.tendencies, self.diagnostics)
+                self.__class__, self.input_properties.keys(),
+                self.tendency_properties.keys(),
+                self.diagnostic_properties.keys())
         )
 
     def __repr__(self):
@@ -168,7 +734,77 @@ def __repr__(self):
             self._making_repr = False
             return return_value
 
-    @abc.abstractmethod
+    def __init__(self, tendencies_in_diagnostics=False, name=None):
+        """
+        Initializes the Stepper object.
+
+        Args
+        ----
+        tendencies_in_diagnostics : bool, optional
+            A boolean indicating whether this object will put tendencies of
+            quantities in its diagnostic output.
+        name : string, optional
+            A label to be used for this object, for example as would be used for
+            Y in the name "X_tendency_from_Y". By default the class name in
+            lowercase is used.
+        """
+        self._tendencies_in_diagnostics = tendencies_in_diagnostics
+        self.name = name or self.__class__.__name__
+        self._input_checker = InputChecker(self)
+        self._tendency_checker = TendencyChecker(self)
+        self._diagnostic_checker = DiagnosticChecker(self)
+        if self.tendencies_in_diagnostics:
+            self._added_diagnostic_names = self._insert_tendency_properties()
+            self._diagnostic_checker.set_ignored_diagnostics(self._added_diagnostic_names)
+        else:
+            self._added_diagnostic_names = []
+        if self.uses_tracers:
+            if self.tracer_dims is None:
+                raise ValueError(
+                    'Component of type {} must specify tracer_dims property '
+                    'when uses_tracers=True'.format(self.__class__.__name__))
+            prepend_tracers = getattr(self, 'prepend_tracers', None)
+            self._tracer_packer = TracerPacker(
+                self, self.tracer_dims, prepend_tracers=prepend_tracers)
+        self.__initialized = True
+        super(TendencyComponent, self).__init__()
+
+    @property
+    def tendencies_in_diagnostics(self):
+        return self._tendencies_in_diagnostics
+
+    def _insert_tendency_properties(self):
+        added_names = []
+        for name, properties in self.tendency_properties.items():
+            tendency_name = self._get_tendency_name(name)
+            if 'dims' in properties.keys():
+                dims = properties['dims']
+            else:
+                dims = self.input_properties[name]['dims']
+            self.diagnostic_properties[tendency_name] = {
+                'units': properties['units'],
+                'dims': dims,
+            }
+            added_names.append(tendency_name)
+        return added_names
+
+    def _get_tendency_name(self, name):
+        return '{}_tendency_from_{}'.format(name, self.name)
+
+    def _check_self_is_initialized(self):
+        try:
+            initialized = self.__initialized
+        except AttributeError:
+            initialized = False
+        if not initialized:
+            raise RuntimeError(
+                'Component has not called __init__ of base class, likely '
+                'because its class {} is missing a call to '
+                'super({}, self).__init__(**kwargs) in its __init__ '
+                'method.'.format(
+                    self.__class__.__name__, self.__class__.__name__)
+            )
+
     def __call__(self, state):
         """
         Gets tendencies and diagnostics from the passed model state.
@@ -176,7 +812,8 @@ def __call__(self, state):
         Args
         ----
         state : dict
-            A model state dictionary.
+            A model state dictionary satisfying the input_properties of this
+            object.
 
         Returns
         -------
@@ -195,59 +832,117 @@ def __call__(self, state):
         KeyError
             If a required quantity is missing from the state.
         InvalidStateError
-            If state is not a valid input for the Prognostic instance.
+            If state is not a valid input for the TendencyComponent instance.
+        """
+        self._check_self_is_initialized()
+        self._input_checker.check_inputs(state)
+        raw_state = get_numpy_arrays_with_properties(state, self.input_properties)
+        if self.uses_tracers:
+            raw_state['tracers'] = self._tracer_packer.pack(state)
+        raw_state['time'] = state['time']
+        raw_tendencies, raw_diagnostics = self.array_call(raw_state)
+        if self.uses_tracers:
+            out_tendencies = self._tracer_packer.unpack(
+                raw_tendencies.pop('tracers'), state,
+                multiply_unit=self.tracer_tendency_time_unit)
+        else:
+            out_tendencies = {}
+        self._tendency_checker.check_tendencies(raw_tendencies)
+        self._diagnostic_checker.check_diagnostics(raw_diagnostics)
+        out_tendencies.update(restore_data_arrays_with_properties(
+            raw_tendencies, self.tendency_properties,
+            state, self.input_properties))
+        diagnostics = restore_data_arrays_with_properties(
+            raw_diagnostics, self.diagnostic_properties,
+            state, self.input_properties,
+            ignore_names=self._added_diagnostic_names)
+        if self.tendencies_in_diagnostics:
+            self._insert_tendencies_to_diagnostics(out_tendencies, diagnostics)
+        return out_tendencies, diagnostics
+
+    def _insert_tendencies_to_diagnostics(self, tendencies, diagnostics):
+        for name, value in tendencies.items():
+            tendency_name = self._get_tendency_name(name)
+            diagnostics[tendency_name] = value
+
+    @abc.abstractmethod
+    def array_call(self, state):
         """
+        Gets tendencies and diagnostics from the passed model state.
+
+        Args
+        ----
+        state : dict
+            A model state dictionary. Instead of data arrays, should
+            include numpy arrays that satisfy the input_properties of this
+            object.
 
+        Returns
+        -------
+        tendencies : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the time derivative of those
+            quantities in units/second at the time of the input state, as
+            numpy arrays.
 
-class ImplicitPrognostic(object):
+        diagnostics : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the value of those quantities
+            at the time of the input state, as numpy arrays.
+        """
+        pass
+
+
+@add_metaclass(ComponentMeta)
+class ImplicitTendencyComponent(object):
     """
     Attributes
     ----------
-    inputs : tuple of str
-        The quantities required in the state when the object is called.
-    tendencies : tuple of str
-        The quantities for which tendencies are returned when
-        the object is called.
-    diagnostics : tuple of str
-        The diagnostic quantities returned when the object is called.
     input_properties : dict
         A dictionary whose keys are quantities required in the state when the
         object is called, and values are dictionaries which indicate 'dims' and
         'units'.
     tendency_properties : dict
-        A dictionary whose keys are quantities for which tendencies are returned when the
-        object is called, and values are dictionaries which indicate 'dims' and
-        'units'.
+        A dictionary whose keys are quantities for which tendencies are returned
+        when the object is called, and values are dictionaries which indicate
+        'dims' and 'units'.
     diagnostic_properties : dict
         A dictionary whose keys are diagnostic quantities returned when the
         object is called, and values are dictionaries which indicate 'dims' and
         'units'.
+    tendencies_in_diagnostics : bool
+        A boolean indicating whether this object will put tendencies of
+        quantities in its diagnostic output based on first order time
+        differencing of output values.
+    name : string
+        A label to be used for this object, for example as would be used for
+        Y in the name "X_tendency_from_Y".
     """
-    __metaclass__ = abc.ABCMeta
 
-    input_properties = {}
-    tendency_properties = {}
-    diagnostic_properties = {}
+    @abc.abstractproperty
+    def input_properties(self):
+        return {}
 
-    @property
-    def inputs(self):
-        return list(self.input_properties.keys())
+    @abc.abstractproperty
+    def tendency_properties(self):
+        return {}
 
-    @property
-    def tendencies(self):
-        return list(self.tendency_properties.keys())
+    @abc.abstractproperty
+    def diagnostic_properties(self):
+        return {}
 
-    @property
-    def diagnostics(self):
-        return list(self.diagnostic_properties.keys())
+    name = None
+    uses_tracers = False
+    tracer_tendency_time_unit = 's^-1'
 
     def __str__(self):
         return (
-            'instance of {}(Prognostic)\n'
+            'instance of {}(TendencyComponent)\n'
             '    inputs: {}\n'
             '    tendencies: {}\n'
             '    diagnostics: {}'.format(
-                self.__class__, self.inputs, self.tendencies, self.diagnostics)
+                self.__class__, self.input_properties.keys(),
+                self.tendency_properties.keys(), self.diagnostic_properties.keys())
         )
 
     def __repr__(self):
@@ -263,7 +958,76 @@ def __repr__(self):
             self._making_repr = False
             return return_value
 
-    @abc.abstractmethod
+    def __init__(self, tendencies_in_diagnostics=False, name=None):
+        """
+        Initializes the Stepper object.
+
+        Args
+        ----
+        tendencies_in_diagnostics : bool, optional
+            A boolean indicating whether this object will put tendencies of
+            quantities in its diagnostic output.
+        name : string, optional
+            A label to be used for this object, for example as would be used for
+            Y in the name "X_tendency_from_Y". By default the class name in
+            lowercase is used.
+        """
+        self._tendencies_in_diagnostics = tendencies_in_diagnostics
+        self.name = name or self.__class__.__name__
+        self._added_diagnostic_names = []
+        self._input_checker = InputChecker(self)
+        self._diagnostic_checker = DiagnosticChecker(self)
+        self._tendency_checker = TendencyChecker(self)
+        if self.tendencies_in_diagnostics:
+            self._added_diagnostic_names = self._insert_tendency_properties()
+            self._diagnostic_checker.set_ignored_diagnostics(self._added_diagnostic_names)
+        if self.uses_tracers:
+            if self.tracer_dims is None:
+                raise ValueError(
+                    'Component of type {} must specify tracer_dims property '
+                    'when uses_tracers=True'.format(self.__class__.__name__))
+            prepend_tracers = getattr(self, 'prepend_tracers', None)
+            self._tracer_packer = TracerPacker(
+                self, self.tracer_dims, prepend_tracers=prepend_tracers)
+        self.__initialized = True
+        super(ImplicitTendencyComponent, self).__init__()
+
+    @property
+    def tendencies_in_diagnostics(self):
+        return self._tendencies_in_diagnostics
+
+    def _insert_tendency_properties(self):
+        added_names = []
+        for name, properties in self.tendency_properties.items():
+            tendency_name = self._get_tendency_name(name)
+            if 'dims' in properties.keys():
+                dims = properties['dims']
+            else:
+                dims = self.input_properties[name]['dims']
+            self.diagnostic_properties[tendency_name] = {
+                'units': properties['units'],
+                'dims': dims,
+            }
+            added_names.append(tendency_name)
+        return added_names
+
+    def _get_tendency_name(self, name):
+        return '{}_tendency_from_{}'.format(name, self.name)
+
+    def _check_self_is_initialized(self):
+        try:
+            initialized = self.__initialized
+        except AttributeError:
+            initialized = False
+        if not initialized:
+            raise RuntimeError(
+                'Component has not called __init__ of base class, likely '
+                'because its class {} is missing a call to '
+                'super({}, self).__init__(**kwargs) in its __init__ '
+                'method.'.format(
+                    self.__class__.__name__, self.__class__.__name__)
+            )
+
     def __call__(self, state, timestep):
         """
         Gets tendencies and diagnostics from the passed model state.
@@ -271,7 +1035,8 @@ def __call__(self, state, timestep):
         Args
         ----
         state : dict
-            A model state dictionary.
+            A model state dictionary satisfying the input_properties of this
+            object.
         timestep : timedelta
             The time over which the model is being stepped.
 
@@ -292,18 +1057,74 @@ def __call__(self, state, timestep):
         KeyError
             If a required quantity is missing from the state.
         InvalidStateError
-            If state is not a valid input for the Prognostic instance.
+            If state is not a valid input for the TendencyComponent instance.
+        """
+        self._check_self_is_initialized()
+        self._input_checker.check_inputs(state)
+        raw_state = get_numpy_arrays_with_properties(state, self.input_properties)
+        if self.uses_tracers:
+            raw_state['tracers'] = self._tracer_packer.pack(state)
+        raw_state['time'] = state['time']
+        raw_tendencies, raw_diagnostics = self.array_call(raw_state, timestep)
+        if self.uses_tracers:
+            out_tendencies = self._tracer_packer.unpack(
+                raw_tendencies.pop('tracers'), state,
+                multiply_unit=self.tracer_tendency_time_unit)
+        else:
+            out_tendencies = {}
+        self._tendency_checker.check_tendencies(raw_tendencies)
+        self._diagnostic_checker.check_diagnostics(raw_diagnostics)
+        out_tendencies.update(restore_data_arrays_with_properties(
+            raw_tendencies, self.tendency_properties,
+            state, self.input_properties))
+        diagnostics = restore_data_arrays_with_properties(
+            raw_diagnostics, self.diagnostic_properties,
+            state, self.input_properties,
+            ignore_names=self._added_diagnostic_names)
+        if self.tendencies_in_diagnostics:
+            self._insert_tendencies_to_diagnostics(out_tendencies, diagnostics)
+        self._last_update_time = state['time']
+        return out_tendencies, diagnostics
+
+    def _insert_tendencies_to_diagnostics(self, tendencies, diagnostics):
+        for name, value in tendencies.items():
+            tendency_name = self._get_tendency_name(name)
+            diagnostics[tendency_name] = value
+
+    @abc.abstractmethod
+    def array_call(self, state, timestep):
         """
+        Gets tendencies and diagnostics from the passed model state.
 
+        Args
+        ----
+        state : dict
+            A model state dictionary. Instead of data arrays, should
+            include numpy arrays that satisfy the input_properties of this
+            object.
+        timestep : timedelta
+            The time over which the model is being stepped.
 
-class Diagnostic(object):
+        Returns
+        -------
+        tendencies : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the time derivative of those
+            quantities in units/second at the time of the input state, as
+            numpy arrays.
+
+        diagnostics : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the value of those quantities
+            at the time of the input state, as numpy arrays.
+        """
+
+
+@add_metaclass(ComponentMeta)
+class DiagnosticComponent(object):
     """
     Attributes
     ----------
-    inputs : tuple of str
-        The quantities required in the state when the object is called.
-    diagnostics : tuple of str
-        The diagnostic quantities returned when the object is called.
     input_properties : dict
         A dictionary whose keys are quantities required in the state when the
         object is called, and values are dictionaries which indicate 'dims' and
@@ -313,25 +1134,22 @@ class Diagnostic(object):
         object is called, and values are dictionaries which indicate 'dims' and
         'units'.
     """
-    __metaclass__ = abc.ABCMeta
-
-    input_properties = {}
-    diagnostic_properties = {}
 
-    @property
-    def inputs(self):
-        return list(self.input_properties.keys())
+    @abc.abstractproperty
+    def input_properties(self):
+        return {}
 
-    @property
-    def diagnostics(self):
-        return list(self.diagnostic_properties.keys())
+    @abc.abstractproperty
+    def diagnostic_properties(self):
+        return {}
 
     def __str__(self):
         return (
-            'instance of {}(Diagnostic)\n'
+            'instance of {}(DiagnosticComponent)\n'
             '    inputs: {}\n'
             '    diagnostics: {}'.format(
-                self.__class__, self.inputs, self.diagnostics)
+                self.__class__, self.input_properties.keys(),
+                self.diagnostic_properties.keys())
         )
 
     def __repr__(self):
@@ -347,7 +1165,29 @@ def __repr__(self):
             self._making_repr = False
             return return_value
 
-    @abc.abstractmethod
+    def __init__(self):
+        """
+        Initializes the Stepper object.
+        """
+        self._input_checker = InputChecker(self)
+        self._diagnostic_checker = DiagnosticChecker(self)
+        self.__initialized = True
+        super(DiagnosticComponent, self).__init__()
+
+    def _check_self_is_initialized(self):
+        try:
+            initialized = self.__initialized
+        except AttributeError:
+            initialized = False
+        if not initialized:
+            raise RuntimeError(
+                'Component has not called __init__ of base class, likely '
+                'because its class {} is missing a call to '
+                'super({}, self).__init__(**kwargs) in its __init__ '
+                'method.'.format(
+                    self.__class__.__name__, self.__class__.__name__)
+            )
+
     def __call__(self, state):
         """
         Gets diagnostics from the passed model state.
@@ -355,7 +1195,8 @@ def __call__(self, state):
         Args
         ----
         state : dict
-            A model state dictionary.
+            A model state dictionary satisfying the input_properties of this
+            object.
 
         Returns
         -------
@@ -369,12 +1210,42 @@ def __call__(self, state):
         KeyError
             If a required quantity is missing from the state.
         InvalidStateError
-            If state is not a valid input for the Prognostic instance.
+            If state is not a valid input for the TendencyComponent instance.
+        """
+        self._check_self_is_initialized()
+        self._input_checker.check_inputs(state)
+        raw_state = get_numpy_arrays_with_properties(state, self.input_properties)
+        raw_state['time'] = state['time']
+        raw_diagnostics = self.array_call(raw_state)
+        self._diagnostic_checker.check_diagnostics(raw_diagnostics)
+        diagnostics = restore_data_arrays_with_properties(
+            raw_diagnostics, self.diagnostic_properties,
+            state, self.input_properties)
+        return diagnostics
+
+    @abc.abstractmethod
+    def array_call(self, state):
         """
+        Gets diagnostics from the passed model state.
+
+        Args
+        ----
+        state : dict
+            A model state dictionary. Instead of data arrays, should
+            include numpy arrays that satisfy the input_properties of this
+            object.
 
+        Returns
+        -------
+        diagnostics : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the value of those quantities
+            at the time of the input state, as numpy arrays.
+        """
 
+
+@add_metaclass(abc.ABCMeta)
 class Monitor(object):
-    __metaclass__ = abc.ABCMeta
 
     def __str__(self):
         return 'instance of {}(Monitor)'.format(self.__class__)
@@ -406,208 +1277,5 @@ def store(self, state):
         Raises
         ------
         InvalidStateError
-            If state is not a valid input for the Diagnostic instance.
-        """
-
-
-class ComponentComposite(object):
-
-    component_class = None
-
-    def __str__(self):
-        return '{}(\n{}\n)'.format(
-            self.__class__,
-            ',\n'.join(str(component) for component in self._components))
-
-    def __repr__(self):
-        return '{}(\n{}\n)'.format(
-            self.__class__,
-            ',\n'.join(repr(component) for component in self._components))
-
-    def __init__(self, *args):
-        """
-        Args
-        ----
-        *args
-            The components that should be wrapped by this object.
-
-        Raises
-        ------
-        SharedKeyError
-            If two components compute the same diagnostic quantity.
-        """
-        if self.component_class is not None:
-            ensure_components_have_class(args, self.component_class)
-        self._components = args
-        if hasattr(self, 'diagnostics'):
-            if (len(self.diagnostics) !=
-                    sum([len(comp.diagnostics) for comp in self._components])):
-                raise SharedKeyError(
-                    'Two components in a composite should not compute '
-                    'the same diagnostic')
-
-    def _combine_attribute(self, attr):
-        return_attr = []
-        for component in self._components:
-            return_attr.extend(getattr(component, attr))
-        return tuple(set(return_attr))  # set to deduplicate
-
-
-def ensure_components_have_class(components, component_class):
-    for component in components:
-        for attr in ('input_properties', 'output_properties',
-                     'diagnostic_properties', 'tendency_properties'):
-            if hasattr(component_class, attr) and not hasattr(component, attr):
-                raise TypeError(
-                    'Component should have attribute {} but does not'.format(
-                        attr))
-            elif hasattr(component, attr) and not hasattr(component_class, attr):
-                raise TypeError(
-                    'Component should not have attribute {}, but does'.format(
-                        attr))
-
-
-class PrognosticComposite(ComponentComposite):
-    """
-    Attributes
-    ----------
-    inputs : tuple of str
-        The quantities required in the state when the object is called.
-    tendencies : tuple of str
-        The quantities for which tendencies are returned when
-        the object is called.
-    diagnostics : tuple of str
-        The diagnostic quantities returned when the object is called.
-    """
-
-    component_class = Prognostic
-
-    def __call__(self, state):
-        """
-        Gets tendencies and diagnostics from the passed model state.
-
-        Args
-        ----
-        state : dict
-            A model state dictionary.
-
-        Returns
-        -------
-        tendencies : dict
-            A dictionary whose keys are strings indicating
-            state quantities and values are the time derivative of those
-            quantities in units/second at the time of the input state.
-        diagnostics : dict
-            A dictionary whose keys are strings indicating
-            state quantities and values are the value of those quantities
-            at the time of the input state.
-
-        Raises
-        ------
-        SharedKeyError
-            If multiple Prognostic objects contained in the
-            collection return the same diagnostic quantity.
-        KeyError
-            If a required quantity is missing from the state.
-        InvalidStateError
-            If state is not a valid input for a Prognostic instance.
-        """
-        return_tendencies = {}
-        return_diagnostics = {}
-        for prognostic in self._components:
-            tendencies, diagnostics = prognostic(state)
-            update_dict_by_adding_another(return_tendencies, tendencies)
-            return_diagnostics.update(diagnostics)
-        return return_tendencies, return_diagnostics
-
-    @property
-    def inputs(self):
-        return self._combine_attribute('inputs')
-
-    @property
-    def diagnostics(self):
-        return self._combine_attribute('diagnostics')
-
-    @property
-    def tendencies(self):
-        return self._combine_attribute('tendencies')
-
-
-class DiagnosticComposite(ComponentComposite):
-    """
-    Attributes
-    ----------
-    inputs : tuple of str
-        The quantities required in the state when the object is called.
-    diagnostics : tuple of str
-        The diagnostic quantities returned when the object is called.
-    """
-
-    component_class = Diagnostic
-
-    def __call__(self, state):
-        """
-        Gets diagnostics from the passed model state.
-
-        Args
-        ----
-        state : dict
-            A model state dictionary.
-
-        Returns
-        -------
-        diagnostics: dict
-            A dictionary whose keys are strings indicating
-            state quantities and values are the value of those quantities
-            at the time of the input state.
-
-        Raises
-        ------
-        SharedKeyError
-            If multiple Diagnostic objects contained in the
-            collection return the same diagnostic quantity.
-        KeyError
-            If a required quantity is missing from the state.
-        InvalidStateError
-            If state is not a valid input for a Diagnostic instance.
-        """
-        return_diagnostics = {}
-        for diagnostic_component in self._components:
-            diagnostics = diagnostic_component(state)
-            # ensure two diagnostics don't compute the same quantity
-            ensure_no_shared_keys(return_diagnostics, diagnostics)
-            return_diagnostics.update(diagnostics)
-        return return_diagnostics
-
-    @property
-    def inputs(self):
-        return self._combine_attribute('inputs')
-
-    @property
-    def diagnostics(self):
-        return self._combine_attribute('diagnostics')
-
-
-class MonitorComposite(ComponentComposite):
-
-    component_class = Monitor
-
-    def store(self, state):
-        """
-        Stores the given state in the Monitor and performs class-specific
-        actions.
-
-        Args
-        ----
-        state : dict
-            A model state dictionary.
-
-        Raises
-        ------
-        KeyError
-            If a required quantity is missing from the state.
-        InvalidStateError
-            If state is not a valid input for a Monitor instance.
+            If state is not a valid input for the DiagnosticComponent instance.
         """
-        for monitor in self._components:
-            monitor.store(state)
diff --git a/sympl/_core/combine_properties.py b/sympl/_core/combine_properties.py
new file mode 100644
index 0000000..623a3cf
--- /dev/null
+++ b/sympl/_core/combine_properties.py
@@ -0,0 +1,108 @@
+from .exceptions import InvalidPropertyDictError
+from .tracers import get_tracer_input_properties
+from .units import units_are_compatible
+
+
+def combine_dims(dims1, dims2):
+    """
+    Takes in two dims specifications and returns a single specification that
+    satisfies both, if possible. Raises an InvalidPropertyDictError if not.
+
+    Parameters
+    ----------
+    dims1 : iterable of str
+    dims2 : iterable of str
+
+    Returns
+    -------
+    dims : iterable of str
+
+    Raises
+    ------
+    InvalidPropertyDictError
+        If the two dims specifications cannot be combined
+    """
+    if dims1 == dims2:
+        return dims1
+    dims_out = []
+    dims1 = set(dims1)
+    dims2 = set(dims2)
+    dims1_wildcard = '*' in dims1
+    dims1.discard('*')
+    dims2_wildcard = '*' in dims2
+    dims2.discard('*')
+    unmatched_dims = set(dims1).union(dims2).difference(dims_out)
+    shared_dims = set(dims2).intersection(dims2)
+    if dims1_wildcard and dims2_wildcard:
+        dims_out.insert(0, '*')  # either dim can match anything
+        dims_out.extend(unmatched_dims)
+    elif not dims1_wildcard and not dims2_wildcard:
+        if shared_dims != set(dims1) or shared_dims != set(dims2):
+            raise InvalidPropertyDictError(
+                'dims {} and {} are incompatible'.format(dims1, dims2))
+        dims_out.extend(unmatched_dims)
+    elif dims1_wildcard:
+        if shared_dims != set(dims2):
+            raise InvalidPropertyDictError(
+                'dims {} and {} are incompatible'.format(dims1, dims2))
+        dims_out.extend(unmatched_dims)
+    elif dims2_wildcard:
+        if shared_dims != set(dims1):
+            raise InvalidPropertyDictError(
+                'dims {} and {} are incompatible'.format(dims1, dims2))
+        dims_out.extend(unmatched_dims)
+    return dims_out
+
+
+def combine_component_properties(component_list, property_name, input_properties=None):
+    property_list = []
+    for component in component_list:
+        property_list.append(getattr(component, property_name))
+        if property_name == 'input_properties' and getattr(component, 'uses_tracers', False):
+            tracer_dims = list(component.tracer_dims)
+            if 'tracer' not in tracer_dims:
+                raise InvalidPropertyDictError(
+                    "tracer_dims must include a 'tracer' dimension indicating "
+                    "tracer number"
+                )
+            tracer_dims.remove('tracer')
+            property_list.append(get_tracer_input_properties(getattr(component, 'prepend_tracers', ()), tracer_dims))
+    return combine_properties(property_list, input_properties)
+
+
+def combine_properties(property_list, input_properties=None):
+    if input_properties is None:
+        input_properties = {}
+    return_dict = {}
+    for property_dict in property_list:
+        for name, properties in property_dict.items():
+            if name not in return_dict:
+                return_dict[name] = {}
+                return_dict[name].update(properties)
+                if 'dims' not in properties.keys():
+                    if name in input_properties.keys() and 'dims' in input_properties[name].keys():
+                        return_dict[name]['dims'] = input_properties[name]['dims']
+                    else:
+                        raise InvalidPropertyDictError()
+            elif not units_are_compatible(
+                    properties['units'], return_dict[name]['units']):
+                raise InvalidPropertyDictError(
+                    'Cannot combine components with incompatible units '
+                    '{} and {} for quantity {}'.format(
+                        return_dict[name]['units'],
+                        properties['units'], name))
+            else:
+                if 'dims' in properties.keys():
+                    new_dims = properties['dims']
+                elif name in input_properties.keys() and 'dims' in input_properties[name].keys():
+                    new_dims = input_properties[name]['dims']
+                else:
+                    raise InvalidPropertyDictError()
+                try:
+                    dims = combine_dims(return_dict[name]['dims'], new_dims)
+                    return_dict[name]['dims'] = dims
+                except InvalidPropertyDictError as err:
+                    raise InvalidPropertyDictError(
+                        'Incompatibility between dims of quantity {}: {}'.format(
+                            name, err.args[0]))
+    return return_dict
diff --git a/sympl/_core/composite.py b/sympl/_core/composite.py
new file mode 100644
index 0000000..383a8fb
--- /dev/null
+++ b/sympl/_core/composite.py
@@ -0,0 +1,310 @@
+from .base_components import TendencyComponent, DiagnosticComponent, Monitor, ImplicitTendencyComponent
+from .util import (
+    update_dict_by_adding_another, ensure_no_shared_keys)
+from .combine_properties import combine_component_properties
+from .exceptions import InvalidPropertyDictError
+
+
+class InputPropertiesCompositeMixin(object):
+
+    @property
+    def input_properties(self):
+        return combine_component_properties(self.component_list, 'input_properties')
+
+    def __init__(self, *args):
+        self.input_properties
+        super(InputPropertiesCompositeMixin, self).__init__()
+
+    def _combine_attribute(self, attr):
+        return_attr = []
+        for component in self.component_list:
+            return_attr.extend(getattr(component, attr))
+        return tuple(set(return_attr))  # set to deduplicate
+
+
+class DiagnosticPropertiesCompositeMixin(object):
+
+    @property
+    def diagnostic_properties(self):
+        return_dict = {}
+        for component in self.component_list:
+            ensure_no_shared_keys(component.diagnostic_properties, return_dict)
+            return_dict.update(component.diagnostic_properties)
+        for name, properties in return_dict.items():
+            if 'dims' not in properties.keys() and not (name in self.input_properties):
+                raise InvalidPropertyDictError(
+                    'Must define dims for diagnostic output {}'.format(name))
+        return return_dict
+
+    def __init__(self, *args):
+        self.diagnostic_properties
+        super(DiagnosticPropertiesCompositeMixin, self).__init__()
+
+    def _combine_attribute(self, attr):
+        return_attr = []
+        for component in self.component_list:
+            return_attr.extend(getattr(component, attr))
+        return tuple(set(return_attr))  # set to deduplicate
+
+
+class ComponentComposite(object):
+    """
+    A composite of components that allows them to be called as one object.
+
+    Attributes
+    ----------
+    component_list: list
+        The components being composited by this object.
+    """
+
+    component_class = None
+
+    def __str__(self):
+        return '{}(\n{}\n)'.format(
+            self.__class__,
+            ',\n'.join(str(component) for component in self.component_list))
+
+    def __repr__(self):
+        return '{}(\n{}\n)'.format(
+            self.__class__,
+            ',\n'.join(repr(component) for component in self.component_list))
+
+    def __init__(self, *args):
+        """
+        Args
+        ----
+        *args
+            The components that should be wrapped by this object.
+
+        Raises
+        ------
+        SharedKeyError
+            If two components compute the same diagnostic quantity.
+        InvalidPropertyDictError
+            If two components require the same input or compute the same
+            output quantity, and their dimensions or units are incompatible
+            with one another.
+        """
+        if self.component_class is not None:
+            ensure_components_have_class(args, self.component_class)
+        self.component_list = args
+        super(ComponentComposite, self).__init__()
+
+
+def ensure_components_have_class(components, component_class):
+    for component in components:
+        if not isinstance(component, component_class):
+            raise TypeError(
+                'Component should be of type {} but is type {}'.format(
+                    component_class, type(component)))
+
+
+class TendencyComponentComposite(
+        ComponentComposite, InputPropertiesCompositeMixin,
+        DiagnosticPropertiesCompositeMixin, TendencyComponent):
+
+    component_class = TendencyComponent
+
+    @property
+    def tendency_properties(self):
+        return combine_component_properties(
+            self.component_list, 'tendency_properties', self.input_properties)
+
+    def __init__(self, *args):
+        """
+        Args
+        ----
+        *args
+            The components that should be wrapped by this object.
+
+        Raises
+        ------
+        SharedKeyError
+            If two components compute the same diagnostic quantity.
+        InvalidPropertyDictError
+            If two components require the same input or compute the same
+            output quantity, and their dimensions or units are incompatible
+            with one another.
+        """
+        super(TendencyComponentComposite, self).__init__(*args)
+        self.input_properties
+        self.tendency_properties
+        self.diagnostic_properties
+
+    def __call__(self, state):
+        """
+        Gets tendencies and diagnostics from the passed model state.
+
+        Args
+        ----
+        state : dict
+            A model state dictionary.
+
+        Returns
+        -------
+        tendencies : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the time derivative of those
+            quantities in units/second at the time of the input state.
+        diagnostics : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the value of those quantities
+            at the time of the input state.
+
+        Raises
+        ------
+        KeyError
+            If a required quantity is missing from the state.
+        InvalidStateError
+            If state is not a valid input for a TendencyComponent instance.
+        """
+        return_tendencies = {}
+        return_diagnostics = {}
+        for prognostic in self.component_list:
+            tendencies, diagnostics = prognostic(state)
+            update_dict_by_adding_another(return_tendencies, tendencies)
+            return_diagnostics.update(diagnostics)
+        return return_tendencies, return_diagnostics
+
+    def array_call(self, state):
+        raise NotImplementedError()
+
+
+class ImplicitTendencyComponentComposite(
+        ComponentComposite, InputPropertiesCompositeMixin,
+        DiagnosticPropertiesCompositeMixin, ImplicitTendencyComponent):
+
+    component_class = (TendencyComponent, ImplicitTendencyComponent)
+
+    @property
+    def tendency_properties(self):
+        return combine_component_properties(
+            self.component_list, 'tendency_properties', self.input_properties)
+
+    def __init__(self, *args):
+        """
+        Args
+        ----
+        *args
+            The components that should be wrapped by this object.
+
+        Raises
+        ------
+        SharedKeyError
+            If two components compute the same diagnostic quantity.
+        InvalidPropertyDictError
+            If two components require the same input or compute the same
+            output quantity, and their dimensions or units are incompatible
+            with one another.
+        """
+        super(ImplicitTendencyComponentComposite, self).__init__(*args)
+        self.input_properties
+        self.tendency_properties
+        self.diagnostic_properties
+
+    def __call__(self, state, timestep):
+        """
+        Gets tendencies and diagnostics from the passed model state.
+
+        Args
+        ----
+        state : dict
+            A model state dictionary.
+
+        Returns
+        -------
+        tendencies : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the time derivative of those
+            quantities in units/second at the time of the input state.
+        diagnostics : dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the value of those quantities
+            at the time of the input state.
+
+        Raises
+        ------
+        KeyError
+            If a required quantity is missing from the state.
+        InvalidStateError
+            If state is not a valid input for a TendencyComponent instance.
+        """
+        return_tendencies = {}
+        return_diagnostics = {}
+        for prognostic in self.component_list:
+            if isinstance(prognostic, ImplicitTendencyComponent):
+                tendencies, diagnostics = prognostic(state, timestep)
+            elif isinstance(prognostic, TendencyComponent):
+                tendencies, diagnostics = prognostic(state)
+            update_dict_by_adding_another(return_tendencies, tendencies)
+            return_diagnostics.update(diagnostics)
+        return return_tendencies, return_diagnostics
+
+    def array_call(self, state):
+        raise NotImplementedError()
+
+
+class DiagnosticComponentComposite(
+        ComponentComposite, InputPropertiesCompositeMixin,
+        DiagnosticPropertiesCompositeMixin, DiagnosticComponent):
+
+    component_class = DiagnosticComponent
+
+    def __call__(self, state):
+        """
+        Gets diagnostics from the passed model state.
+
+        Args
+        ----
+        state : dict
+            A model state dictionary.
+
+        Returns
+        -------
+        diagnostics: dict
+            A dictionary whose keys are strings indicating
+            state quantities and values are the value of those quantities
+            at the time of the input state.
+
+        Raises
+        ------
+        KeyError
+            If a required quantity is missing from the state.
+        InvalidStateError
+            If state is not a valid input for a DiagnosticComponent instance.
+        """
+        return_diagnostics = {}
+        for diagnostic_component in self.component_list:
+            diagnostics = diagnostic_component(state)
+            # ensure two diagnostics don't compute the same quantity
+            ensure_no_shared_keys(return_diagnostics, diagnostics)
+            return_diagnostics.update(diagnostics)
+        return return_diagnostics
+
+    def array_call(self, state):
+        raise NotImplementedError()
+
+
+class MonitorComposite(ComponentComposite):
+
+    component_class = Monitor
+
+    def store(self, state):
+        """
+        Stores the given state in the Monitor and performs class-specific
+        actions.
+
+        Args
+        ----
+        state : dict
+            A model state dictionary.
+
+        Raises
+        ------
+        KeyError
+            If a required quantity is missing from the state.
+        InvalidStateError
+            If state is not a valid input for a Monitor instance.
+        """
+        for monitor in self.component_list:
+            monitor.store(state)
diff --git a/sympl/_core/constants.py b/sympl/_core/constants.py
index 3fc1dfb..c5c5ec0 100644
--- a/sympl/_core/constants.py
+++ b/sympl/_core/constants.py
@@ -1,4 +1,4 @@
-from .array import DataArray
+from .dataarray import DataArray
 from .units import is_valid_unit
 
 
@@ -9,10 +9,12 @@ def __repr__(self):
 
     def _repr(self, sphinx=False):
         return_string = ''
+        printed_names = set()
         for category, name_list in constant_names_by_category.items():
             if len(name_list) > 0:
                 return_string += category.title() + '\n'
             for name in name_list:
+                printed_names.add(name)
                 units = self[name].attrs['units']
                 units = units.replace('dimensionless', '')
                 return_string += '\t{}: {} {}\n'.format(
@@ -20,6 +22,16 @@ def _repr(self, sphinx=False):
                 if sphinx:
                     return_string += '\n'
             return_string += '\n'
+        if len(set(self.keys()).difference(printed_names)) > 0:
+            return_string += 'User Defined\n'
+            for name in self.keys():
+                if name not in printed_names:
+                    units = self[name].attrs['units']
+                    return_string += '\t{}: {} {}\n'.format(
+                        name, self[name].values.item(), units)
+                    if sphinx:
+                        return_string += '\n'
+            return_string += '\n'
         return return_string
 
     @property
@@ -95,6 +107,7 @@ def __getitem__(self, item):
     'heat_capacity_of_solid_water_as_snow': DataArray(2108., attrs={'units': 'J kg^-1 K^-1'}),
     'thermal_conductivity_of_solid_water_as_snow': DataArray(0.2, attrs={'units': 'W m^-1 K^-1'}),
     'reference_air_pressure': DataArray(1.0132e5, attrs={'units': 'Pa'}),
+    'reference_air_temperature': DataArray(300., attrs={'units': 'degK'}),
     'thermal_conductivity_of_dry_air': DataArray(0.026, attrs={'units': 'W m^-1 K^-1'}),
     'gas_constant_of_dry_air': DataArray(287., attrs={'units': 'J kg^-1 K^-1'}),
     'gas_constant_of_water_vapor': DataArray(461.5, attrs={'units': 'J kg^-1 K^-1'}),
@@ -120,7 +133,8 @@ def __getitem__(self, item):
         'gravitational_acceleration',
         'planetary_radius',
         'planetary_rotation_rate',
-        'seconds_per_day'],
+        'seconds_per_day'
+    ],
 
     'physical': [
         'stefan_boltzmann_constant',
@@ -129,7 +143,8 @@ def __getitem__(self, item):
         'boltzmann_constant',
         'loschmidt_constant',
         'universal_gas_constant',
-        'planck_constant'],
+        'planck_constant'
+    ],
 
     'condensible': [
         'density_of_liquid_phase',
@@ -146,18 +161,26 @@ def __getitem__(self, item):
         'thermal_conductivity_of_solid_phase_as_ice',
         'thermal_conductivity_of_solid_phase_as_snow',
         'thermal_conductivity_of_liquid_phase',
-        'freezing_temperature_of_liquid_phase'],
+        'freezing_temperature_of_liquid_phase',
+        'enthalpy_of_fusion',
+        'latent_heat_of_vaporization',
+    ],
 
     'atmospheric': [
         'heat_capacity_of_dry_air_at_constant_pressure',
         'gas_constant_of_dry_air',
         'thermal_conductivity_of_dry_air',
-        'reference_air_pressure'],
+        'reference_air_pressure',
+        'reference_air_temperature',
+    ],
 
     'stellar': [
-        'stellar_irradiance'],
+        'stellar_irradiance',
+        'solar_constant',
+    ],
 
-    'oceanographic': [],
+    'oceanographic': [
+    ],
 
     'chemical': [
         'heat_capacity_of_water_vapor_at_constant_pressure',
@@ -165,7 +188,23 @@ def __getitem__(self, item):
         'gas_constant_of_water_vapor',
         'latent_heat_of_vaporization_of_water',
         'heat_capacity_of_liquid_water',
-        'latent_heat_of_fusion_of_water'],
+        'latent_heat_of_fusion_of_water',
+        'heat_capacity_of_solid_water_as_ice',
+        'heat_capacity_of_solid_water_as_snow',
+        'thermal_conductivity_of_solid_water_as_ice',
+        'thermal_conductivity_of_solid_water_as_snow',
+        'thermal_conductivity_of_liquid_water',
+        'density_of_solid_water_as_ice',
+        'density_of_solid_water_as_snow',
+        'freezing_temperature_of_liquid_water',
+        'specific_enthalpy_of_water_vapor',
+        'density_of_snow',
+        'heat_capacity_of_snow',
+        'heat_capacity_of_ice',
+        'density_of_ice',
+        'thermal_conductivity_of_ice',
+        'thermal_conductivity_of_snow',
+    ],
 
 }
 
diff --git a/sympl/_core/array.py b/sympl/_core/dataarray.py
similarity index 100%
rename from sympl/_core/array.py
rename to sympl/_core/dataarray.py
index b063080..b6b3f84 100644
--- a/sympl/_core/array.py
+++ b/sympl/_core/dataarray.py
@@ -1,6 +1,6 @@
-from .units import data_array_to_units as to_units_function
 import xarray as xr
 from pint.errors import DimensionalityError
+from .units import data_array_to_units as to_units_function
 
 
 class DataArray(xr.DataArray):
diff --git a/sympl/_core/exceptions.py b/sympl/_core/exceptions.py
index 8c8b0e0..4c71df8 100644
--- a/sympl/_core/exceptions.py
+++ b/sympl/_core/exceptions.py
@@ -12,3 +12,11 @@ class SharedKeyError(Exception):
 
 class DependencyError(Exception):
     pass
+
+
+class ComponentMissingOutputError(Exception):
+    pass
+
+
+class ComponentExtraOutputError(Exception):
+    pass
diff --git a/sympl/_core/get_np_arrays.py b/sympl/_core/get_np_arrays.py
new file mode 100644
index 0000000..9a84b57
--- /dev/null
+++ b/sympl/_core/get_np_arrays.py
@@ -0,0 +1,68 @@
+import numpy as np
+from .exceptions import InvalidStateError
+from .wildcard import get_wildcard_matches_and_dim_lengths, flatten_wildcard_dims
+
+
+def get_numpy_arrays_with_properties(state, property_dictionary):
+    out_dict = {}
+    wildcard_names, dim_lengths = get_wildcard_matches_and_dim_lengths(
+        state, property_dictionary)
+    #  Now we actually retrieve output arrays since we know the precise out dims
+    for name, properties in property_dictionary.items():
+        ensure_quantity_has_units(state[name], name)
+        try:
+            quantity = state[name].to_units(properties['units'])
+        except ValueError:
+            raise InvalidStateError(
+                'Could not convert quantity {} from units {} to units {}'.format(
+                    name, state[name].attrs['units'], properties['units']
+                )
+            )
+        out_dims = []
+        out_dims.extend(properties['dims'])
+        has_wildcard = '*' in out_dims
+        if has_wildcard:
+            i_wildcard = out_dims.index('*')
+            out_dims[i_wildcard:i_wildcard+1] = wildcard_names
+        out_array = get_numpy_array(
+            quantity, out_dims=out_dims, dim_lengths=dim_lengths)
+        if has_wildcard:
+            out_array = flatten_wildcard_dims(
+                out_array, i_wildcard, i_wildcard + len(wildcard_names))
+        if 'alias' in properties.keys():
+            out_name = properties['alias']
+        else:
+            out_name = name
+        out_dict[out_name] = out_array
+    return out_dict
+
+
+def get_numpy_array(data_array, out_dims, dim_lengths):
+    """
+    Gets a numpy array from the data_array with the desired out_dims, and a
+    dict of dim_lengths that will give the length of any missing dims in the
+    data_array.
+    """
+    if len(data_array.values.shape) == 0 and len(out_dims) == 0:
+        return data_array.values  # special case, 0-dimensional scalar array
+    else:
+        missing_dims = [dim for dim in out_dims if dim not in data_array.dims]
+        for dim in missing_dims:
+            data_array = data_array.expand_dims(dim)
+        numpy_array = data_array.transpose(*out_dims).values
+        if len(missing_dims) == 0:
+            out_array = numpy_array
+        else:  # expand out missing dims which are currently length 1.
+            out_shape = [dim_lengths.get(name, 1) for name in out_dims]
+            if out_shape == list(numpy_array.shape):
+                out_array = numpy_array
+            else:
+                out_array = np.empty(out_shape, dtype=numpy_array.dtype)
+                out_array[:] = numpy_array
+        return out_array
+
+
+def ensure_quantity_has_units(quantity, quantity_name):
+    if 'units' not in quantity.attrs:
+        raise InvalidStateError(
+            'quantity {} is missing units attribute'.format(quantity_name))
diff --git a/sympl/_core/init_np_arrays.py b/sympl/_core/init_np_arrays.py
new file mode 100644
index 0000000..c6298a2
--- /dev/null
+++ b/sympl/_core/init_np_arrays.py
@@ -0,0 +1,78 @@
+import numpy as np
+from .tracers import get_tracer_names
+from .exceptions import InvalidStateError
+from .restore_dataarray import extract_output_dims_properties
+
+
+def initialize_numpy_arrays_with_properties(
+        output_properties, raw_input_state, input_properties, tracer_dims=None,
+        prepend_tracers=()):
+    """
+    Parameters
+    ----------
+    output_properties : dict
+        A dictionary whose keys are quantity names and values are dictionaries
+        with properties for those quantities. The property "dims" must be
+        present for each quantity not also present in input_properties.
+    raw_input_state : dict
+        A state dictionary of numpy arrays that was used as input to a component
+        for which return arrays are being generated.
+    input_properties : dict
+        A dictionary whose keys are quantity names and values are dictionaries
+        with input properties for those quantities. The property "dims" must be
+        present, indicating the dimensions that the quantity was transformed to
+        when taken as input to a component.
+
+    Returns
+    -------
+    out_dict : dict
+        A dictionary whose keys are quantities and values are numpy arrays
+        corresponding to those quantities, with shapes determined from the
+        inputs to this function.
+
+    Raises
+    ------
+    InvalidPropertyDictError
+        When an output property is specified to have dims_like an input
+        property, but the arrays for the two properties have incompatible
+        shapes.
+    """
+    dim_lengths = get_dim_lengths_from_raw_input(raw_input_state, input_properties)
+    dims_from_out_properties = extract_output_dims_properties(
+        output_properties, input_properties, [])
+    out_dict = {}
+    tracer_names = list(get_tracer_names())
+    tracer_names.extend(entry[0] for entry in prepend_tracers)
+    for name, out_dims in dims_from_out_properties.items():
+        if tracer_dims is None or name not in tracer_names:
+            out_shape = []
+            for dim in out_dims:
+                out_shape.append(dim_lengths[dim])
+            dtype = output_properties[name].get('dtype', np.float64)
+            out_dict[name] = np.zeros(out_shape, dtype=dtype)
+    if tracer_dims is not None:
+        out_shape = []
+        dim_lengths['tracer'] = len(tracer_names)
+        for dim in tracer_dims:
+            out_shape.append(dim_lengths[dim])
+        out_dict['tracers'] = np.zeros(out_shape, dtype=np.float64)
+    return out_dict
+
+
+def get_dim_lengths_from_raw_input(raw_input, input_properties):
+    dim_lengths = {}
+    for name, properties in input_properties.items():
+        if properties.get('tracer', False):
+            continue
+        if 'alias' in properties.keys():
+            name = properties['alias']
+        for i, dim_name in enumerate(properties['dims']):
+            if dim_name in dim_lengths:
+                if raw_input[name].shape[i] != dim_lengths[dim_name]:
+                    raise InvalidStateError(
+                        'Dimension name {} has differing lengths on different '
+                        'inputs'.format(dim_name)
+                    )
+            else:
+                dim_lengths[dim_name] = raw_input[name].shape[i]
+    return dim_lengths
diff --git a/sympl/_core/restore_dataarray.py b/sympl/_core/restore_dataarray.py
new file mode 100644
index 0000000..791fba1
--- /dev/null
+++ b/sympl/_core/restore_dataarray.py
@@ -0,0 +1,135 @@
+import numpy as np
+from .exceptions import InvalidPropertyDictError
+from .dataarray import DataArray
+from .wildcard import (
+    get_wildcard_matches_and_dim_lengths, fill_dims_wildcard,
+    expand_array_wildcard_dims
+)
+
+
+def ensure_values_are_arrays(array_dict):
+    for name, value in array_dict.items():
+        if not isinstance(value, np.ndarray):
+            array_dict[name] = np.asarray(value)
+
+
+def get_alias_or_name(name, output_properties, input_properties):
+    if 'alias' in output_properties[name].keys():
+        raw_name = output_properties[name]['alias']
+    elif name in input_properties.keys() and 'alias' in input_properties[name].keys():
+        raw_name = input_properties[name]['alias']
+    else:
+        raw_name = name
+    return raw_name
+
+
+def check_array_shape(out_dims, raw_array, name, dim_lengths):
+    if len(out_dims) != len(raw_array.shape):
+        raise InvalidPropertyDictError(
+            'Returned array for {} has shape {} '
+            'which is incompatible with dims {} in properties'.format(
+                name, raw_array.shape, out_dims))
+    for dim, length in zip(out_dims, raw_array.shape):
+        if dim in dim_lengths.keys() and dim_lengths[dim] != length:
+            raise InvalidPropertyDictError(
+                'Dimension {} of quantity {} has length {}, but '
+                'another quantity has length {}'.format(
+                    dim, name, length, dim_lengths[dim])
+            )
+
+
+def restore_data_arrays_with_properties(
+        raw_arrays, output_properties, input_state, input_properties,
+        ignore_names=None, ignore_missing=False):
+    """
+    Parameters
+    ----------
+    raw_arrays : dict
+        A dictionary whose keys are quantity names and values are numpy arrays
+        containing the data for those quantities.
+    output_properties : dict
+        A dictionary whose keys are quantity names and values are dictionaries
+        with properties for those quantities. The property "dims" must be
+        present for each quantity not also present in input_properties. All
+        other properties are included as attributes on the output DataArray
+        for that quantity, including "units" which is required.
+    input_state : dict
+        A state dictionary that was used as input to a component for which
+        DataArrays are being restored.
+    input_properties : dict
+        A dictionary whose keys are quantity names and values are dictionaries
+        with input properties for those quantities. The property "dims" must be
+        present, indicating the dimensions that the quantity was transformed to
+        when taken as input to a component.
+    ignore_names : iterable of str, optional
+        Names to ignore when encountered in output_properties, will not be
+        included in the returned dictionary.
+    ignore_missing : bool, optional
+        If True, ignore any values in output_properties not present in
+        raw_arrays rather than raising an exception. Default is False.
+
+    Returns
+    -------
+    out_dict : dict
+        A dictionary whose keys are quantities and values are DataArrays
+        corresponding to those quantities, with data, shapes and attributes
+        determined from the inputs to this function.
+
+    Raises
+    ------
+    InvalidPropertyDictError
+        When an output property is specified to have dims_like an input
+        property, but the arrays for the two properties have incompatible
+        shapes.
+    """
+    raw_arrays = raw_arrays.copy()
+    if ignore_names is None:
+        ignore_names = []
+    if ignore_missing:
+        ignore_names = set(output_properties.keys()).difference(raw_arrays.keys()).union(ignore_names)
+    wildcard_names, dim_lengths = get_wildcard_matches_and_dim_lengths(
+        input_state, input_properties)
+    ensure_values_are_arrays(raw_arrays)
+    dims_from_out_properties = extract_output_dims_properties(
+        output_properties, input_properties, ignore_names)
+    out_dict = {}
+    for name, out_dims in dims_from_out_properties.items():
+        if name in ignore_names:
+            continue
+        raw_name = get_alias_or_name(name, output_properties, input_properties)
+        if '*' in out_dims:
+            for dim_name, length in zip(out_dims, raw_arrays[raw_name].shape):
+                if dim_name not in dim_lengths and dim_name != '*':
+                    dim_lengths[dim_name] = length
+            out_dims_without_wildcard, target_shape = fill_dims_wildcard(
+                out_dims, dim_lengths, wildcard_names)
+            out_array = expand_array_wildcard_dims(
+                raw_arrays[raw_name], target_shape, name, out_dims)
+        else:
+            check_array_shape(out_dims, raw_arrays[raw_name], name, dim_lengths)
+            out_dims_without_wildcard = out_dims
+            out_array = raw_arrays[raw_name]
+        out_dict[name] = DataArray(
+            out_array,
+            dims=out_dims_without_wildcard,
+            attrs={'units': output_properties[name]['units']}
+        )
+    return out_dict
+
+
+def extract_output_dims_properties(output_properties, input_properties, ignore_names):
+    return_array = {}
+    for name, properties in output_properties.items():
+        if name in ignore_names:
+            continue
+        elif 'dims' in properties.keys():
+            return_array[name] = properties['dims']
+        elif name not in input_properties.keys():
+            raise InvalidPropertyDictError(
+                'Output dims must be specified for {} in properties'.format(name))
+        elif 'dims' not in input_properties[name].keys():
+            raise InvalidPropertyDictError(
+                'Input dims must be specified for {} in properties'.format(name))
+        else:
+            return_array[name] = input_properties[name]['dims']
+    return return_array
diff --git a/sympl/_core/state.py b/sympl/_core/state.py
new file mode 100644
index 0000000..04da25b
--- /dev/null
+++ b/sympl/_core/state.py
@@ -0,0 +1,28 @@
+def copy_untouched_quantities(old_state, new_state):
+    for key in old_state.keys():
+        if key not in new_state:
+            new_state[key] = old_state[key]
+
+
+def add(state_1, state_2):
+    out_state = {}
+    if 'time' in state_1.keys():
+        out_state['time'] = state_1['time']
+    for key in state_1.keys():
+        if key != 'time':
+            out_state[key] = state_1[key] + state_2[key]
+            if hasattr(out_state[key], 'attrs'):
+                out_state[key].attrs = state_1[key].attrs
+    return out_state
+
+
+def multiply(scalar, state):
+    out_state = {}
+    if 'time' in state.keys():
+        out_state['time'] = state['time']
+    for key in state.keys():
+        if key != 'time':
+            out_state[key] = scalar * state[key]
+            if hasattr(out_state[key], 'attrs'):
+                out_state[key].attrs = state[key].attrs
+    return out_state
diff --git a/sympl/_core/tendencystepper.py b/sympl/_core/tendencystepper.py
new file mode 100644
index 0000000..65950c7
--- /dev/null
+++ b/sympl/_core/tendencystepper.py
@@ -0,0 +1,249 @@
+import abc
+from .composite import ImplicitTendencyComponentComposite
+from .time import timedelta
+from .combine_properties import combine_properties, combine_component_properties
+from .units import clean_units
+from .state import copy_untouched_quantities
+from .base_components import ImplicitTendencyComponent, Stepper
+from .exceptions import InvalidPropertyDictError
+import warnings
+
+
+class TendencyStepper(Stepper):
+    """An object which integrates model state forward in time.
+
+    It uses TendencyComponent and DiagnosticComponent objects to update the current model state
+    with diagnostics, and to return the model state at the next timestep.
+
+    Attributes
+    ----------
+    diagnostic_properties : dict
+        A dictionary whose keys are quantities for which values
+        for the old state are returned when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    output_properties : dict
+        A dictionary whose keys are quantities for which values
+        for the new state are returned when the
+        object is called, and values are dictionaries which indicate 'dims' and
+        'units'.
+    prognostic : ImplicitTendencyComponentComposite
+        A composite of the TendencyComponent and ImplicitPrognostic objects used by
+        the TendencyStepper.
+    prognostic_list: list of TendencyComponent and ImplicitPrognosticComponent
+        A list of TendencyComponent objects called by the TendencyStepper. These should
+        be referenced when determining what inputs are necessary for the
+        TendencyStepper.
+    tendencies_in_diagnostics : bool
+        A boolean indicating whether this object will put tendencies of
+        quantities in its diagnostic output.
+    time_unit_name : str
+        The unit to use for time differencing when putting tendencies in
+        diagnostics.
+    time_unit_timedelta: timedelta
+        A timedelta corresponding to a single time unit as used for time
+        differencing when putting tendencies in diagnostics.
+    name : string
+        A label to be used for this object, for example as would be used for
+        Y in the name "X_tendency_from_Y".
+    """
+    __metaclass__ = abc.ABCMeta
+
+    time_unit_name = 's'
+    time_unit_timedelta = timedelta(seconds=1)
+
+    @property
+    def input_properties(self):
+        input_properties = combine_component_properties(
+            self.prognostic_list, 'input_properties')
+        return combine_properties([input_properties, self.output_properties])
+
+    @property
+    def _tendencycomponent_input_properties(self):
+        return combine_component_properties(
+            self.prognostic_list, 'input_properties')
+
+    @property
+    def diagnostic_properties(self):
+        return_value = {}
+        for prognostic in self.prognostic_list:
+            return_value.update(prognostic.diagnostic_properties)
+        if self.tendencies_in_diagnostics:
+            self._insert_tendencies_to_diagnostic_properties(
+                return_value, self._tendency_properties)
+        return return_value
+
+    def _insert_tendencies_to_diagnostic_properties(
+            self, diagnostic_properties, tendency_properties):
+        for quantity_name, properties in tendency_properties.items():
+            tendency_name = self._get_tendency_name(quantity_name)
+            diagnostic_properties[tendency_name] = {
+                'units': properties['units'],
+                'dims': properties['dims'],
+            }
+
+    @property
+    def output_properties(self):
+        output_properties = self._tendency_properties
+        for name, properties in output_properties.items():
+            properties['units'] += ' {}'.format(self.time_unit_name)
+            properties['units'] = clean_units(properties['units'])
+        return output_properties
+
+    @property
+    def _tendency_properties(self):
+        return_dict = {}
+        return_dict.update(combine_component_properties(
+            self.prognostic_list, 'tendency_properties',
+            input_properties=self._tendencycomponent_input_properties
+        ))
+        return return_dict
+
+    def __str__(self):
+        return (
+            'instance of {}(TendencyStepper)\n'
+            '    TendencyComponent components: {}'.format(self.prognostic_list)
+        )
+
+    def __repr__(self):
+        if hasattr(self, '_making_repr') and self._making_repr:
+            return '{}(recursive reference)'.format(self.__class__)
+        else:
+            self._making_repr = True
+            return_value = '{}({})'.format(
+                self.__class__,
+                '\n'.join('{}: {}'.format(repr(key), repr(value))
+                          for key, value in self.__dict__.items()
+                          if key != '_making_repr'))
+            self._making_repr = False
+            return return_value
+
+    def array_call(self, state, timestep):
+        raise NotImplementedError('TendencyStepper objects do not implement array_call')
+
+    def __init__(self, *args, **kwargs):
+        """
+        Initialize the TendencyStepper.
+
+        Parameters
+        ----------
+        *args : TendencyComponent or ImplicitTendencyComponent
+            Objects to call for tendencies when doing time stepping.
+        tendencies_in_diagnostics : bool, optional
+            A boolean indicating whether this object will put tendencies of
+            quantities in its diagnostic output. Default is False. If set to
+            True, you probably want to give a name also.
+        name : str
+            A label to be used for this object, for example as would be used for
+            Y in the name "X_tendency_from_Y". By default the class name is used.
+        """
+        if len(args) == 1 and isinstance(args[0], list):
+            warnings.warn(
+                'TimeSteppers should be given individual Prognostics rather '
+                'than a list, and will not accept lists in a later version.',
+                DeprecationWarning)
+            args = args[0]
+        if any(isinstance(a, ImplicitTendencyComponent) for a in args):
+            warnings.warn(
+                'Using an ImplicitTendencyComponent in sympl TendencyStepper objects may '
+                'lead to scientifically invalid results. Make sure the component '
+                'follows the same numerical assumptions as the TendencyStepper used.')
+        self.prognostic = ImplicitTendencyComponentComposite(*args)
+        super(TendencyStepper, self).__init__(**kwargs)
+        for name in self.prognostic.tendency_properties.keys():
+            if name not in self.output_properties.keys():
+                raise InvalidPropertyDictError(
+                    'Prognostic object has tendency output for {} but '
+                    'TendencyStepper containing it does not have it in '
+                    'output_properties.'.format(name))
+        self.__initialized = True
+
+    @property
+    def prognostic_list(self):
+        return self.prognostic.component_list
+
+    @property
+    def tendencies_in_diagnostics(self):  # value cannot be modified
+        return self._tendencies_in_diagnostics
+
+    def _get_tendency_name(self, quantity_name):
+        return '{}_tendency_from_{}'.format(quantity_name, self.name)
+
+    def __call__(self, state, timestep):
+        """
+        Retrieves any diagnostics and returns a new state corresponding
+        to the next timestep.
+
+        Args
+        ----
+        state : dict
+            The current model state.
+        timestep : timedelta
+            The amount of time to step forward.
+
+        Returns
+        -------
+        diagnostics : dict
+            Diagnostics from the timestep of the input state.
+        new_state : dict
+            The model state at the next timestep.
+        """
+        if not self.__initialized:
+            raise AssertionError(
+                'TendencyStepper component has not had its base class '
+                '__init__ called, likely due to a missing call to '
+                'super(ClassName, self).__init__(*args, **kwargs) in its '
+                '__init__ method.'
+            )
+        diagnostics, new_state = self._call(state, timestep)
+        copy_untouched_quantities(state, new_state)
+        if self.tendencies_in_diagnostics:
+            self._insert_tendencies_to_diagnostics(
+                state, new_state, timestep, diagnostics)
+        return diagnostics, new_state
+
+    def _insert_tendencies_to_diagnostics(
+            self, state, new_state, timestep, diagnostics):
+        output_properties = self.output_properties
+        input_properties = self.input_properties
+        for name in output_properties.keys():
+            tendency_name = self._get_tendency_name(name)
+            if tendency_name in diagnostics.keys():
+                raise RuntimeError(
+                    'A TendencyComponent has output tendencies as a diagnostic and has'
+                    ' caused a name clash when trying to do so from this '
+                    'TendencyStepper ({}). You must disable '
+                    'tendencies_in_diagnostics for this TendencyStepper.'.format(
+                        tendency_name))
+            base_units = input_properties[name]['units']
+            diagnostics[tendency_name] = (
+                (new_state[name].to_units(base_units) - state[name].to_units(base_units)) /
+                timestep.total_seconds() * self.time_unit_timedelta.total_seconds()
+            )
+            if base_units == '':
+                diagnostics[tendency_name].attrs['units'] = '{}^-1'.format(
+                    self.time_unit_name)
+            else:
+                diagnostics[tendency_name].attrs['units'] = '{} {}^-1'.format(
+                    base_units, self.time_unit_name)
+
+    def _call(self, state, timestep):
+        """
+        Retrieves any diagnostics and returns a new state corresponding
+        to the next timestep.
+
+        Args
+        ----
+        state : dict
+            The current model state.
+        timestep : timedelta
+            The amount of time to step forward.
+
+        Returns
+        -------
+        diagnostics : dict
+            Diagnostics from the timestep of the input state.
+        new_state : dict
+            The model state at the next timestep.
+        """
+        raise NotImplementedError()
diff --git a/sympl/_core/time.py b/sympl/_core/time.py
index 371939a..f0401d8 100644
--- a/sympl/_core/time.py
+++ b/sympl/_core/time.py
@@ -1,9 +1,13 @@
 from datetime import datetime as real_datetime, timedelta
 from .exceptions import DependencyError
 try:
-    import netcdftime as nt
+    import cftime as ct
+    if not all(hasattr(ct, attr) for attr in [
+            'DatetimeNoLeap', 'DatetimeProlepticGregorian', 'DatetimeAllLeap',
+            'Datetime360Day', 'DatetimeJulian', 'DatetimeGregorian']):
+        ct = None
 except ImportError:
-    nt = None
+    ct = None
 
 
 def datetime(
@@ -47,20 +51,20 @@ def datetime(
         return real_datetime(tzinfo=tzinfo, **kwargs)
     elif tzinfo is not None:
         raise ValueError('netcdftime does not support timezone-aware datetimes')
-    elif nt is None:
+    elif ct is None:
         raise DependencyError(
             "Calendars other than 'proleptic_gregorian' require the netcdftime "
             "package, which is not installed.")
     elif calendar.lower() in ('all_leap', '366_day'):
-        return nt.DatetimeAllLeap(**kwargs)
+        return ct.DatetimeAllLeap(**kwargs)
     elif calendar.lower() in ('no_leap', 'noleap', '365_day'):
-        return nt.DatetimeNoLeap(**kwargs)
+        return ct.DatetimeNoLeap(**kwargs)
     elif calendar.lower() == '360_day':
-        return nt.Datetime360Day(**kwargs)
+        return ct.Datetime360Day(**kwargs)
     elif calendar.lower() == 'julian':
-        return nt.DatetimeJulian(**kwargs)
+        return ct.DatetimeJulian(**kwargs)
     elif calendar.lower() == 'gregorian':
-        return nt.DatetimeGregorian(**kwargs)
+        return ct.DatetimeGregorian(**kwargs)
 
 
 __all__ = (datetime, timedelta)
diff --git a/sympl/_core/tracers.py b/sympl/_core/tracers.py
new file mode 100644
index 0000000..b84c853
--- /dev/null
+++ b/sympl/_core/tracers.py
@@ -0,0 +1,238 @@
+from .exceptions import InvalidPropertyDictError
+import numpy as np
+from .units import units_are_same
+from .get_np_arrays import get_numpy_arrays_with_properties
+from .restore_dataarray import restore_data_arrays_with_properties
+
+_tracer_unit_dict = {}
+_tracer_names = []
+_packers = set()
+
+
+def reset_tracers():
+    global _tracer_names
+    while len(_tracer_unit_dict) > 0:
+        _tracer_unit_dict.popitem()
+    while len(_tracer_names) > 0:
+        _tracer_names.pop()
+
+
+def reset_packers():
+    while len(_packers) > 0:
+        _packers.pop()
+
+
+def register_tracer(name, units):
+    """
+    Parameters
+    ----------
+    name : str
+        Quantity name to register as a tracer.
+    units : str
+        Unit string of that quantity.
+    """
+    if name in _tracer_unit_dict and not units_are_same(_tracer_unit_dict[name], units):
+        raise ValueError(
+            'Tracer {} is already registered with units {} which are different '
+            'from input units {}'.format(
+                name, _tracer_unit_dict[name], units
+            )
+        )
+    _tracer_unit_dict[name] = units
+    _tracer_names.append(name)
+    for packer in _packers:
+        packer.ensure_tracer_not_in_outputs(name, units)
+
+
+def get_tracer_unit_dict():
+    """
+    Returns
+    -------
+    unit_dict : dict
+        A dictionary whose keys are tracer quantity names as str and values are
+        units of those quantities as str.
+    """
+    return_dict = {}
+    return_dict.update(_tracer_unit_dict)
+    return return_dict
+
+
+def get_tracer_names():
+    """
+    Returns
+    -------
+    tracer_names : tuple of str
+        Tracer names in the order that they will appear in tracer arrays.\
+    """
+    return tuple(_tracer_names)
+
+
+def get_tracer_input_properties(prepend_tracers, tracer_dims):
+    """
+
+    Args:
+        prepend_tracers (list of tuple): Pairs of (name, units) describing
+            tracers that are to be included in addition to any registered
+            tracers.
+        tracer_dims (list): Dimensions to use for each tracer
+            (e.g. ['dim1', 'dim2']).
+
+    Returns:
+        input_properties (dict): A properties dictionary for registered and
+            additional tracers.
+    """
+    tracer_dims = list(tracer_dims)
+    if 'tracer' in tracer_dims:
+        tracer_dims.remove('tracer')
+    tracer_units = {}
+    tracer_units.update(get_tracer_unit_dict())
+    tracer_units.update(dict(prepend_tracers))
+    tracer_properties = {}
+    for name, units in tracer_units.items():
+        tracer_properties[name] = {'units': units, 'dims': tracer_dims}
+    return tracer_properties
+
+
+def get_quantity_dims(tracer_dims):
+    if 'tracer' not in tracer_dims:
+        raise ValueError("Tracer dims must include a dimension named 'tracer'")
+    quantity_dims = list(tracer_dims)
+    quantity_dims.remove('tracer')
+    return tuple(quantity_dims)
+
+
+class TracerPacker(object):
+
+    def __init__(self, component, tracer_dims, prepend_tracers=None):
+        self._prepend_tracers = prepend_tracers or ()
+        self._tracer_dims = tuple(tracer_dims)
+        self._tracer_quantity_dims = get_quantity_dims(tracer_dims)
+        if hasattr(component, 'tendency_properties') or hasattr(component, 'output_properties'):
+            self.component = component
+        else:
+            raise TypeError(
+                'Expected a component object subclassing type Stepper, '
+                'ImplicitTendencyComponent, or TendencyComponent but received component of '
+                'type {}'.format(component.__class__.__name__))
+        for name, units in self._prepend_tracers:
+            if name not in _tracer_unit_dict.keys():
+                self.ensure_tracer_not_in_outputs(name, units)
+        for name, units in _tracer_unit_dict.items():
+            self.ensure_tracer_not_in_outputs(name, units)
+        _packers.add(self)
+
+    def ensure_tracer_not_in_outputs(self, name, units):
+        if hasattr(self.component, 'tendency_properties'):
+            self._ensure_tracer_not_in_tendency_properties(name, units)
+        elif hasattr(self.component, 'output_properties'):
+            self.ensure_tracer_not_in_output_properties(name, units)
+
+    def ensure_tracer_not_in_output_properties(self, name, units):
+        if name in self.component.output_properties.keys():
+            raise InvalidPropertyDictError(
+                'Attempted to insert {} as tracer to component of type {} but '
+                'it already has that quantity defined as an output.'.format(
+                    name, self.component.__class__.__name__
+                )
+            )
+
+    def _ensure_tracer_not_in_tendency_properties(self, name, units):
+        if name in self.component.tendency_properties.keys():
+            raise InvalidPropertyDictError(
+                'Attempted to insert {} as tracer to component of type {} but '
+                'it already has that quantity defined as a tendency '
+                'output.'.format(
+                    name, self.component.__class__.__name__
+                )
+            )
+
+    def _ensure_tracer_quantity_dims(self, dims):
+        if tuple(self._tracer_quantity_dims) != tuple(dims):
+            raise InvalidPropertyDictError(
+                'Tracers have conflicting dims {} and {}'.format(
+                    self._tracer_quantity_dims, dims)
+            )
+
+    def register_tracers(self, unit_dict, input_properties):
+        for name, units in unit_dict.items():
+            if name not in input_properties.keys():
+                input_properties[name] = {
+                    'dims': self._tracer_quantity_dims,
+                    'units': units,
+                    'tracer': True,
+                }
+
+    @property
+    def tracer_names(self):
+        return_list = []
+        for name, units in self._prepend_tracers:
+            return_list.append(name)
+        for name in _tracer_names:
+            if name not in return_list:
+                return_list.append(name)
+        return tuple(return_list)
+
+    @property
+    def _tracer_index(self):
+        return self._tracer_dims.index('tracer')
+
+    def pack(self, state):
+        """
+
+        Args:
+            state (dict): A state dictionary.
+
+        Returns:
+            tracer_array (ndarray): An array containing the tracer data, with
+                dimensions as specified by tracer_dims on initializing this
+                object.
+        """
+        tracer_properties = get_tracer_input_properties(
+            self._prepend_tracers, self._tracer_quantity_dims)
+        raw_state = get_numpy_arrays_with_properties(state, tracer_properties)
+        if len(self.tracer_names) == 0:
+            shape = [0 for dim in self._tracer_dims]
+        else:
+            shape = list(raw_state[self.tracer_names[0]].shape)
+            shape.insert(self._tracer_index, len(self.tracer_names))
+        array = np.empty(shape, dtype=np.float64)
+        for i, name in enumerate(self.tracer_names):
+            tracer_slice = [slice(0, d) for d in shape]
+            tracer_slice[self._tracer_index] = i
+            array[tracer_slice] = raw_state[name]
+        return array
+
+    def unpack(self, tracer_array, input_state, multiply_unit=''):
+        """
+
+        Args:
+            tracer_array (ndarray): An array containing tracer values, with
+                dimensions as specified by tracer_dims on initializing this
+                object.
+            input_state (dict): A state dictionary from which the tracer array
+                was originally packed.
+            multiply_unit (str, optional): A unit string which should be multiplied to
+                the units of each output in the returned DataArrays, for example
+                to represent the units over which a time difference was taken.
+
+        Returns:
+            tracer_dict (dict): A dictionary whose keys are tracer names and
+                values are DataArrays containing the values of each
+                tracer.
+        """
+        tracer_properties = get_tracer_input_properties(
+            self._prepend_tracers, self._tracer_quantity_dims)
+        raw_state = {}
+        for i, name in enumerate(self.tracer_names):
+            tracer_slice = [slice(0, d) for d in tracer_array.shape]
+            tracer_slice[self._tracer_index] = i
+            raw_state[name] = tracer_array[tracer_slice]
+        out_properties = {}
+        for name, properties in tracer_properties.items():
+            out_properties[name] = properties.copy()
+            if multiply_unit is not '':
+                out_properties[name]['units'] = '{} {}'.format(
+                    out_properties[name]['units'], multiply_unit)
+        return_state = restore_data_arrays_with_properties(
+            raw_state, out_properties, input_state, tracer_properties)
+        return return_state
diff --git a/sympl/_core/units.py b/sympl/_core/units.py
index c0d46e7..d2977e8 100644
--- a/sympl/_core/units.py
+++ b/sympl/_core/units.py
@@ -19,6 +19,48 @@ def __call__(self, input_string, **kwargs):
 unit_registry.define('percent = 0.01*count = %')
 
 
+def units_are_compatible(unit1, unit2):
+    """
+    Determine whether a unit can be converted to another unit.
+
+    Parameters
+    ----------
+    unit1 : str
+    unit2 : str
+
+    Returns
+    -------
+    units_are_compatible : bool
+        True if the first unit can be converted to the second unit.
+    """
+    try:
+        unit_registry(unit1).to(unit2)
+        return True
+    except pint.errors.DimensionalityError:
+        return False
+
+
+def units_are_same(unit1, unit2):
+    """
+    Compare two unit strings for equality.
+
+    Parameters
+    ----------
+    unit1 : str
+    unit2 : str
+
+    Returns
+    -------
+    units_are_same : bool
+        True if the two input unit strings represent the same unit.
+    """
+    return unit_registry(unit1) == unit_registry(unit2)
+
+
+def clean_units(unit_string):
+    return str(unit_registry(unit_string).to_base_units().units)
+
+
 def is_valid_unit(unit_string):
     """Returns True if the unit string is recognized, and False otherwise."""
     unit_string = unit_string.replace(
@@ -38,7 +80,7 @@ def data_array_to_units(value, units):
             'Cannot retrieve units from type {}'.format(type(value)))
     elif unit_registry(value.attrs['units']) != unit_registry(units):
         attrs = value.attrs.copy()
-        value = (unit_registry(value.attrs['units'])*value).to(units).magnitude
+        value = unit_registry.Quantity(value, value.attrs['units']).to(units).magnitude
         attrs['units'] = units
         value.attrs = attrs
     return value
diff --git a/sympl/_core/util.py b/sympl/_core/util.py
index 973959d..3cc687b 100644
--- a/sympl/_core/util.py
+++ b/sympl/_core/util.py
@@ -1,11 +1,10 @@
 from datetime import datetime
 
 import numpy as np
-from six import string_types
 
-from .array import DataArray
+from .dataarray import DataArray
 from .exceptions import (
-    SharedKeyError, InvalidStateError, InvalidPropertyDictError)
+    SharedKeyError, InvalidStateError)
 
 try:
     from numba import jit
@@ -17,8 +16,6 @@ def jit(signature_or_function=None, **kwargs):
         else:
             return signature_or_function
 
-dim_names = {'x': ['x'], 'y': ['y'], 'z': ['z']}
-
 # internal exceptions used only within this module
 
 
@@ -34,161 +31,6 @@ class ShapeMismatchError(Exception):
     pass
 
 
-def ensure_consistent_dimension_lengths(state):
-    dimension_lengths = {}
-    for name, array in state.items():
-        if isinstance(array, DataArray):
-            for i, name in enumerate(array.dims):
-                if name in dimension_lengths and (array.shape[i] != dimension_lengths[name]):
-                    raise InvalidStateError(
-                        'dimension {} has multiple lengths (at least {} and {})'.format(
-                            name, array.shape[i], dimension_lengths[name]))
-                else:
-                    dimension_lengths[name] = array.shape[i]
-
-
-def ensure_properties_have_dims_and_units(properties, quantity_name):
-    if 'dims' not in properties:
-        raise InvalidPropertyDictError(
-            'dims not specified for quantity {}'.format(quantity_name))
-    if 'units' not in properties:
-        raise InvalidPropertyDictError(
-            'units not specified for quantity {}'.format(quantity_name))
-
-
-def ensure_quantity_has_units(quantity, quantity_name):
-    if 'units' not in quantity.attrs:
-        raise InvalidStateError(
-            'quantity {} is missing units attribute'.format(quantity_name))
-
-
-def independent_wildcards_first(item_list):
-    """
-    Sorts the items in a (quantity_name, properties) item list so that
-    quantities that match_dims_like other quantities are placed *after* those
-    quantities in the list. In the case that two quantities match_dims_like
-    one another, a InvalidPropertyDictError is raised.
-
-    This is necessary so that information about the wildcard matches from
-    previous calls can be given to later calls to ensure wildcard-matched
-    dimensions are ordered the same.
-    """
-    name_list = []
-    properties_list = []
-    matched_by_dict = {}
-    for quantity_name, properties in item_list:
-        if 'match_dims_like' in properties:
-            target_name = properties['match_dims_like']
-            if target_name in matched_by_dict:
-                matched_by_dict[target_name].append(quantity_name)
-            else:
-                matched_by_dict[target_name] = [quantity_name]
-    for quantity_name, properties in item_list:
-        if quantity_name not in matched_by_dict:
-            name_list.append(quantity_name)
-            properties_list.append(properties)
-        else:
-            matched_by_indices = []
-            for matched_by_name in matched_by_dict[quantity_name]:
-                if matched_by_name in name_list:
-                    matched_by_indices.append(name_list.index(matched_by_name))
-            if len(matched_by_indices) == 0:
-                name_list.append(quantity_name)
-                properties_list.append(properties)
-            else:
-                index = min(matched_by_indices)
-                name_list.insert(index, quantity_name)
-                properties_list.insert(index, properties)
-    return zip(name_list, properties_list)
-
-
-def get_numpy_arrays_with_properties(state, property_dictionary):
-    """
-    Parameters
-    ----------
-    state : dict
-        A state dictionary.
-    property_dictionary : dict
-        A dictionary whose keys are quantity names and values are dictionaries
-        with properties for those quantities. The property "dims" must be
-        present, indicating the dimensions that the quantity must have when it
-        is returned as a numpy array. The property "units" must be present,
-        and will be used to check the units on the input state and perform
-        a conversion if necessary. If the optional property "match_dims_like"
-        is present, its value should be a quantity also present in
-        property_dictionary, and it will be ensured that any shared wildcard
-        dimensions ('x', 'y', 'z', '*') for this quantity match the same
-        dimensions as the specified quantity.
-
-    Returns
-    -------
-    out_dict : dict
-        A dictionary whose keys are quantity names and values are numpy arrays
-        containing the data for those quantities, as specified by
-        property_dictionary.
-
-    Raises
-    ------
-    InvalidStateError
-        If a DataArray in the state is missing an explicitly-specified
-        dimension defined in its properties (dimension names other than
-        'x', 'y', 'z', or '*'), or if the state is missing a required quantity.
-    InvalidPropertyError
-        If a quantity in property_dictionary is missing values for "dims" or
-        "units".
-    """
-    ensure_consistent_dimension_lengths(state)
-    out_dict = {}
-    matches = {}
-    for quantity_name, properties in independent_wildcards_first(property_dictionary.items()):
-        ensure_properties_have_dims_and_units(properties, quantity_name)
-        if quantity_name not in state.keys():
-            raise InvalidStateError(
-                'state is missing quantity {}'.format(quantity_name))
-        ensure_quantity_has_units(state[quantity_name], quantity_name)
-        quantity_has_alias = 'alias' in properties.keys()
-        if quantity_has_alias:
-            out_name = properties['alias']
-        else:
-            out_name = quantity_name
-        if out_name in out_dict.keys():
-            raise InvalidPropertyDictError(
-                'Multiple arrays with output name {}'.format(out_name))
-        try:
-            quantity_array = state[quantity_name].to_units(properties['units'])
-        except ValueError:
-            raise ValueError(
-                'Invalid target units {} for quantity {} '
-                'with units {}'.format(
-                    properties['units'],
-                    quantity_name,
-                    state[quantity_name].attrs['units']))
-        try:
-            if ('match_dims_like' in properties.keys() and
-                    properties['match_dims_like'] in matches):
-                out_dict[out_name], matches[quantity_name] = get_numpy_array(
-                    quantity_array,
-                    out_dims=properties['dims'], return_wildcard_matches=True,
-                    require_wildcard_matches=matches[properties['match_dims_like']])
-            else:
-                out_dict[out_name], matches[quantity_name] = get_numpy_array(
-                    quantity_array,
-                    out_dims=properties['dims'], return_wildcard_matches=True)
-        except NoMatchForDirectionError as err:
-            raise InvalidStateError(
-                'dimension {} is missing from quantity {}'.format(
-                    err, quantity_name)
-            )
-        except DimensionNotInOutDimsError as err:
-            raise InvalidStateError(
-                'dims property {} on quantity {} does not allow for state'
-                'quantity to have dimension {} (but it does)'.format(
-                    properties['dims'], quantity_name, err)
-            )
-    ensure_dims_like_are_satisfied(matches, property_dictionary)
-    return out_dict
-
-
 def get_numpy_array(
         data_array, out_dims, return_wildcard_matches=False,
         require_wildcard_matches=None):
@@ -210,8 +52,8 @@ def get_numpy_array(
         which is the flattened collection of all dimensions not explicitly
         listed in out_dims, including any dimensions with unknown direction.
     return_wildcard_matches : bool, optional
-        If True, will additionally return a dictionary whose keys are direciton
-        wildcards ('x', 'y', 'z', or '*') and values are lists of matched
+        If True, will additionally return a dictionary whose keys are direction
+        wildcards (currently only '*') and values are lists of matched
         dimensions in the order they appear.
     require_wildcard_matches : dict, optional
         A dictionary mapping wildcards to matches. If the wildcard is used in
@@ -231,13 +73,15 @@ def get_numpy_array(
         in data_array, or data_array's dimensions are invalid in some way.
 
     """
+    # This function was written when we had directional wildcards, and could
+    # be re-written to be simpler now that we do not.
     if (len(data_array.values.shape) == 0) and (len(out_dims) == 0):
         direction_to_names = {}  # required in case we need wildcard_matches
         return_array = data_array.values  # special case, 0-dimensional scalar array
     else:
-        current_dim_names = dim_names.copy()
+        current_dim_names = {}
         for dim in out_dims:
-            if dim not in ('x', 'y', 'z', '*'):
+            if dim != '*':
                 current_dim_names[dim] = [dim]
         direction_to_names = get_input_array_dim_names(
             data_array, out_dims, current_dim_names)
@@ -266,112 +110,12 @@ def get_numpy_array(
     if return_wildcard_matches:
         wildcard_matches = {
             key: value for key, value in direction_to_names.items()
-            if key in ('x', 'y', 'z', '*')}
+            if key == '*'}
         return return_array, wildcard_matches
     else:
         return return_array
 
 
-def ensure_dims_like_are_satisfied(matches, property_dictionary):
-    for quantity_name, properties in property_dictionary.items():
-        if 'match_dims_like' in properties:
-            if properties['match_dims_like'] not in property_dictionary.keys():
-                raise InvalidPropertyDictError(
-                    'quantity {} is not specified in property dictionary, '
-                    'but is referred to by {} in match_dims_like'.format(
-                        properties['match_dims_like'], quantity_name
-                    ))
-            like_name = properties['match_dims_like']
-            for wildcard_dim in set(matches[quantity_name].keys()).intersection(
-                    matches[like_name].keys()):
-                # We must use == because we need the dim order to be the same
-                if not same_list(
-                        matches[quantity_name][wildcard_dim],
-                        matches[like_name][wildcard_dim]):
-                    raise InvalidStateError(
-                        'quantity {} matches dimensions {} for direction {}, but '
-                        'is referred to in match_dims_like by quantity {} with matches '
-                        '{}'.format(
-                            like_name, matches[like_name][wildcard_dim],
-                            wildcard_dim, quantity_name,
-                            matches[quantity_name][wildcard_dim]))
-
-
-def restore_data_arrays_with_properties(
-        raw_arrays, output_properties, input_state, input_properties):
-    """
-    Parameters
-    ----------
-    raw_arrays : dict
-        A dictionary whose keys are quantity names and values are numpy arrays
-        containing the data for those quantities.
-    output_properties : dict
-        A dictionary whose keys are quantity names and values are dictionaries
-        with properties for those quantities. The property "dims_like" must be
-        present, and specifies an input quantity that the dimensions of the
-        output quantity should be like. All other properties are included as
-        attributes on the output DataArray for that quantity, including "units"
-        which is required.
-    input_state : dict
-        A state dictionary that was used as input to a component for which
-        DataArrays are being restored.
-    input_properties : dict
-        A dictionary whose keys are quantity names and values are dictionaries
-        with input properties for those quantities. The property "dims" must be
-        present, indicating the dimensions that the quantity was transformed to
-        when taken as input to a component.
-
-    Returns
-    -------
-    out_dict : dict
-        A dictionary whose keys are quantities and values are DataArrays
-        corresponding to those quantities, with data, shapes and attributes
-        determined from the inputs to this function.
-
-    Raises
-    ------
-    InvalidPropertyDictError
-        When an output property is specified to have dims_like an input
-        property, but the arrays for the two properties have incompatible
-        shapes.
-    """
-    ensure_consistent_dimension_lengths(input_state)
-    out_dict = {}
-    for quantity_name, properties in output_properties.items():
-        attrs = properties.copy()
-        dims_like = attrs.pop('dims_like', quantity_name)
-        if (quantity_name not in raw_arrays.keys()) and ('alias' in properties):
-            from_name = attrs.pop('alias')
-        elif quantity_name in input_properties.keys() and 'alias' in input_properties[quantity_name].keys():
-            from_name = input_properties[quantity_name]['alias']
-        else:
-            from_name = quantity_name
-        if from_name not in raw_arrays.keys():
-            raise ValueError(
-                'requested output {} is not present in raw_arrays'.format(
-                    from_name))
-        array = raw_arrays[from_name]
-        from_dims = input_properties[dims_like]['dims']
-        result_like = input_state[dims_like]
-        try:
-            out_dict[quantity_name] = restore_dimensions(
-                array,
-                from_dims=from_dims,
-                result_like=result_like,
-                result_attrs=attrs)
-        except ShapeMismatchError:
-            raise InvalidPropertyDictError(
-                'output quantity {} has dims_like input {}, but the '
-                'provided output array for {} has '
-                'a shape {} incompatible with the input shape {} of {}. '
-                'Do they really have the same dimensions?'.format(
-                    quantity_name, dims_like, quantity_name, array.shape,
-                    result_like.shape, dims_like
-                )
-            )
-    return out_dict
-
-
 def restore_dimensions(array, from_dims, result_like, result_attrs=None):
     """
     Restores a numpy array to a DataArray with similar dimensions to a reference
@@ -409,9 +153,9 @@ def restore_dimensions(array, from_dims, result_like, result_attrs=None):
     :py:function:~sympl.get_numpy_array: : Retrieves a numpy array with desired
         dimensions from a given DataArray.
     """
-    current_dim_names = dim_names.copy()
+    current_dim_names = {}
     for dim in from_dims:
-        if dim not in ('x', 'y', 'z', '*'):
+        if dim != '*':
             current_dim_names[dim] = [dim]
     direction_to_names = get_input_array_dim_names(
         result_like, from_dims, current_dim_names)
@@ -448,84 +192,6 @@ def same_list(list1, list2):
         [item in list2 for item in list1] + [item in list1 for item in list2]))
 
 
-def set_direction_names(x=None, y=None, z=None):
-    """
-    Sets the directional wildcards 'x', 'y', and 'z' to match the provided
-    dimension names only.
-    """
-    for key, value in [('x', x), ('y', y), ('z', z)]:
-        if isinstance(value, string_types):
-            dim_names[key] = [key, value]
-        elif value is not None:
-            dim_names[key] = [key] + list(value)
-
-
-def add_direction_names(x=None, y=None, z=None):
-    """
-    Sets the directional wildcards 'x', 'y', and 'z' to match the provided
-    dimension names, in addition to any names they are already matching.
-    """
-    for key, value in [('x', x), ('y', y), ('z', z)]:
-        if isinstance(value, string_types):
-            dim_names[key].append(value)
-        elif value is not None:
-            dim_names[key].extend(value)
-
-
-def combine_dimensions(arrays, out_dims):
-    """
-    Returns a tuple of dimension names corresponding to
-    dimension names from the DataArray objects given by *args when present.
-    The names returned correspond to the directions in out_dims.
-
-    Args
-    ----
-    arrays : iterable of DataArray
-        Objects from which to deduce dimension names.
-    out_dims : {'x', 'y', 'z'}
-        The desired output directions. Should contain only 'x', 'y', or 'z'.
-        For example, ('y', 'x') is valid.
-
-    Raises
-    ------
-    ValueError
-        If there are multiple names for a single direction, or if
-        an array has a dimension along a direction not present in out_dims.
-
-    Returns
-    -------
-    dimensions : list of str
-        The deduced dimension names, in the order given by out_dims.
-    """
-    _ensure_no_invalid_directions(out_dims)
-    out_names = [None for _ in range(len(out_dims))]
-    all_names = set()
-    for value in arrays:
-        all_names.update(value.dims)
-    for direction, dir_names in dim_names.items():
-        if direction in out_dims:
-            names = all_names.intersection(dir_names)
-            if len(names) > 1:
-                raise ValueError(
-                    'Multiple dimensions along {} direction'.format(direction))
-            elif len(names) == 1:
-                out_names[out_dims.index(direction)] = names.pop()
-            else:
-                out_names[out_dims.index(direction)] = direction
-        elif len(all_names.intersection(dir_names)) > 0:
-            raise ValueError(
-                'Arrays have dimensions along {} direction, which is '
-                'not included in output'.format(direction))
-    return out_names
-
-
-def _ensure_no_invalid_directions(out_dims):
-    invalid_dims = set(out_dims).difference(['x', 'y', 'z'])
-    if len(invalid_dims) != 0:
-        raise ValueError(
-            'Invalid direction(s) in out_dims: {}'.format(invalid_dims))
-
-
 def update_dict_by_adding_another(dict1, dict2):
     """
     Takes two dictionaries. Add values in dict2 to the values in dict1, if
@@ -536,11 +202,19 @@ def update_dict_by_adding_another(dict1, dict2):
     """
     for key in dict2.keys():
         if key not in dict1:
-            dict1[key] = dict2[key]
+            if hasattr(dict2[key], 'copy'):
+                dict1[key] = dict2[key].copy()
+            else:
+                dict1[key] = dict2[key]
         else:
-            if (isinstance(dict1[key], DataArray) and isinstance(dict2[key], DataArray) and
-                    ('units' in dict1[key].attrs) and ('units' in dict2[key].attrs)):
-                dict1[key] += dict2[key].to_units(dict1[key].attrs['units'])
+            if (isinstance(dict1[key], DataArray) and isinstance(dict2[key], DataArray)):
+                if 'units' not in dict1[key].attrs or 'units' not in dict2[key].attrs:
+                    raise InvalidStateError(
+                        'DataArray objects must have units property defined')
+                try:
+                    dict1[key] += dict2[key].to_units(dict1[key].attrs['units'])
+                except ValueError:  # dict1[key] is missing a dimension present in dict2[key]
+                    dict1[key] = dict1[key] + dict2[key].to_units(dict1[key].attrs['units'])
             else:
                 dict1[key] += dict2[key]  # += is in-place addition operator
     return  # not returning anything emphasizes that this is in-place
@@ -656,13 +330,13 @@ def get_final_shape(data_array, out_dims, direction_to_names):
 
 def get_component_aliases(*args):
     """
-    Returns aliases for variables in the properties of Components (e.g., Prognostics).
+    Returns aliases for variables in the properties of Components (TendencyComponent,
+    DiagnosticComponent, Stepper, and ImplicitTendencyComponent objects).
 
-    Notes
-    -----
-     -  If a variable shows up in the input_properties or diagnostic_properties
-        of two or more different Components, make sure they have the same 'alias'
-        keyword in all Components.
+    If multiple aliases are present for the same variable, the following
+    properties have priority in descending order: input, output, diagnostic,
+    tendency. If multiple components give different aliases at the same priority
+    level, one is chosen arbitrarily.
 
     Args
     ----
@@ -673,22 +347,16 @@ def get_component_aliases(*args):
     Returns
     -------
     aliases : dict
-        A dictionary with keys containing old variable names and values containing
-        new variable names
+        A dictionary mapping quantity names to aliases
     """
-
-    aliases = {}
-
-    # Update the aliases dict with the properties in each provided Component
-    for component in args:
-        # combine the input, output, diagnostic, and tendency variables into one dict
-        for prop_type in ['input_properties', 'output_properties',
-                          'diagnostic_properties', 'tendency_properties']:
-            if hasattr(component, prop_type):
-                component_properties = getattr(component, prop_type)
-                # save the alias (if there is one) for each variable
-                for varname, properties in component_properties.items():
+    return_dict = {}
+    for property_type in (
+            'tendency_properties', 'diagnostic_properties', 'output_properties',
+            'input_properties'):
+        for component in args:
+            if hasattr(component, property_type):
+                component_properties = getattr(component, property_type)
+                for name, properties in component_properties.items():
                     if 'alias' in properties.keys():
-                        aliases.update({varname: properties['alias']})
-
-    return aliases
+                        return_dict[name] = properties['alias']
+    return return_dict
diff --git a/sympl/_core/wildcard.py b/sympl/_core/wildcard.py
new file mode 100644
index 0000000..9628a84
--- /dev/null
+++ b/sympl/_core/wildcard.py
@@ -0,0 +1,93 @@
+import numpy as np
+from .exceptions import InvalidStateError, InvalidPropertyDictError
+
+
+def get_wildcard_matches_and_dim_lengths(state, property_dictionary):
+    wildcard_names = []
+    dim_lengths = {}
+    # Loop to get the set of names matching "*" (wildcard names)
+    for quantity_name, properties in property_dictionary.items():
+        ensure_properties_have_dims_and_units(properties, quantity_name)
+        for dim_name, length in zip(state[quantity_name].dims, state[quantity_name].shape):
+            if dim_name not in dim_lengths.keys():
+                dim_lengths[dim_name] = length
+            elif length != dim_lengths[dim_name]:
+                raise InvalidStateError(
+                    'Dimension {} conflicting lengths {} and {} in different '
+                    'state quantities.'.format(dim_name, length, dim_lengths[dim_name]))
+        new_wildcard_names = [
+            dim for dim in state[quantity_name].dims if dim not in properties['dims']]
+        if len(new_wildcard_names) > 0 and '*' not in properties['dims']:
+            raise InvalidStateError(
+                'Quantity {} has unexpected dimensions {}.'.format(
+                    quantity_name, new_wildcard_names))
+        wildcard_names.extend(
+            [name for name in new_wildcard_names if name not in wildcard_names])
+    if not any('dims' in p.keys() and '*' in p['dims'] for p in property_dictionary.values()):
+        wildcard_names = None  # can't determine wildcard matches if there is no wildcard
+    else:
+        wildcard_names = tuple(wildcard_names)
+    return wildcard_names, dim_lengths
+
+
+def flatten_wildcard_dims(array, i_start, i_end):
+    if i_end > len(array.shape):
+        raise ValueError('i_end should be less than the number of axes in array')
+    elif i_start < 0:
+        raise ValueError('i_start should be greater than 0')
+    elif i_start > i_end:
+        raise ValueError('i_start should be less than or equal to i_end')
+    elif i_start == i_end:
+        # We need to insert a singleton dimension at i_start
+        target_shape = []
+        target_shape.extend(array.shape)
+        target_shape.insert(i_start, 1)
+    else:
+        target_shape = []
+        wildcard_length = 1
+        for i, length in enumerate(array.shape):
+            if i_start <= i < i_end:
+                wildcard_length *= length
+            else:
+                target_shape.append(length)
+            if i == i_end - 1:
+                target_shape.append(wildcard_length)
+    return array.reshape(target_shape)
+
+
+def fill_dims_wildcard(
+        out_dims, dim_lengths, wildcard_names, expand_wildcard=True):
+    i_wildcard = out_dims.index('*')
+    target_shape = []
+    out_dims_without_wildcard = []
+    for i, out_dim in enumerate(out_dims):
+        if i == i_wildcard and expand_wildcard:
+            target_shape.extend([dim_lengths[n] for n in wildcard_names])
+            out_dims_without_wildcard.extend(wildcard_names)
+        elif i == i_wildcard and not expand_wildcard:
+            target_shape.append(np.product([dim_lengths[n] for n in wildcard_names]))
+        else:
+            target_shape.append(dim_lengths[out_dim])
+            out_dims_without_wildcard.append(out_dim)
+    return out_dims_without_wildcard, target_shape
+
+
+def expand_array_wildcard_dims(raw_array, target_shape, name, out_dims):
+    try:
+        out_array = np.reshape(raw_array, target_shape)
+    except ValueError:
+        raise InvalidPropertyDictError(
+            'Failed to restore shape for output {} with raw shape {} '
+            'and target shape {}, are the output dims {} correct?'.format(
+                name, raw_array.shape, target_shape,
+                out_dims))
+    return out_array
+
+
+def ensure_properties_have_dims_and_units(properties, quantity_name):
+    if 'dims' not in properties:
+        raise InvalidPropertyDictError(
+            'dims not specified for quantity {}'.format(quantity_name))
+    if 'units' not in properties:
+        raise InvalidPropertyDictError(
+            'units not specified for quantity {}'.format(quantity_name))
diff --git a/sympl/_core/wrappers.py b/sympl/_core/wrappers.py
index 331dde1..63bb902 100644
--- a/sympl/_core/wrappers.py
+++ b/sympl/_core/wrappers.py
@@ -1,15 +1,17 @@
-from .base_components import ImplicitPrognostic
-from .array import DataArray
+from .._core.base_components import (
+    TendencyComponent, DiagnosticComponent, ImplicitTendencyComponent, Stepper
+)
 
 
 class ScalingWrapper(object):
     """
     Wraps any component and scales either inputs, outputs or tendencies
     by a floating point value.
+
     Example
     -------
-    This is how the ScaledInputOutputWrapper can be used to wrap a Prognostic.
-    >>> scaled_component = ScaledInputOutputWrapper(
+    This is how the ScalingWrapper can be used to wrap a TendencyComponent.
+    >>> scaled_component = ScalingWrapper(
     >>>     RRTMRadiation(),
     >>>     input_scale_factors = {
     >>>         'specific_humidity' = 0.2},
@@ -25,9 +27,10 @@ def __init__(self,
                  diagnostic_scale_factors=None):
         """
         Initializes the ScaledInputOutputWrapper object.
+
         Args
         ----
-        component : Prognostic, Implicit
+        component : TendencyComponent, Stepper, DiagnosticComponent, ImplicitTendencyComponent
             The component to be wrapped.
         input_scale_factors : dict
             a dictionary whose keys are the inputs that will be scaled
@@ -41,24 +44,34 @@ def __init__(self,
         diagnostic_scale_factors : dict
             a dictionary whose keys are the diagnostics that will be scaled
             and values are floating point scaling factors.
+
         Returns
         -------
         scaled_component : ScaledInputOutputWrapper
             the scaled version of the component
+
         Raises
         ------
         TypeError
-            The component is not of type Implicit or Prognostic.
+            The component is not of type Stepper or TendencyComponent.
         ValueError
             The keys in the scale factors do not correspond to valid
             input/output/tendency for this component.
         """
+        if not any(
+                isinstance(component, t) for t in [
+                    DiagnosticComponent, TendencyComponent, ImplicitTendencyComponent, Stepper]):
+            raise TypeError(
+                'component must be a component type (DiagnosticComponent, TendencyComponent, '
+                'ImplicitTendencyComponent, or Stepper)'
+            )
 
+        self._component = component
         self._input_scale_factors = dict()
         if input_scale_factors is not None:
 
             for input_field in input_scale_factors.keys():
-                if input_field not in component.inputs:
+                if input_field not in component.input_properties.keys():
                     raise ValueError(
                         "{} is not a valid input quantity.".format(input_field))
 
@@ -66,261 +79,178 @@ def __init__(self,
 
         self._diagnostic_scale_factors = dict()
         if diagnostic_scale_factors is not None:
-
-            for diagnostic_field in diagnostic_scale_factors.keys():
-                if diagnostic_field not in component.diagnostics:
-                    raise ValueError(
-                        "{} is not a valid diagnostic quantity.".format(diagnostic_field))
-
+            if not hasattr(component, 'diagnostic_properties'):
+                raise TypeError(
+                    'Cannot apply diagnostic scale factors to component without '
+                    'diagnostic output.')
+            self._ensure_fields_have_properties(
+                diagnostic_scale_factors, component.diagnostic_properties, 'diagnostic')
             self._diagnostic_scale_factors = diagnostic_scale_factors
 
-        if hasattr(component, 'input_properties') and hasattr(component, 'output_properties'):
-
-            self._output_scale_factors = dict()
-            if output_scale_factors is not None:
-
-                for output_field in output_scale_factors.keys():
-                    if output_field not in component.outputs:
-                        raise ValueError(
-                            "{} is not a valid output quantity.".format(output_field))
-
-                self._output_scale_factors = output_scale_factors
-            self._component_type = 'Implicit'
-
-        elif hasattr(component, 'input_properties') and hasattr(component, 'tendency_properties'):
-
-            self._tendency_scale_factors = dict()
-            if tendency_scale_factors is not None:
-
-                for tendency_field in tendency_scale_factors.keys():
-                    if tendency_field not in component.tendencies:
-                        raise ValueError(
-                            "{} is not a valid tendency quantity.".format(tendency_field))
-
-                self._tendency_scale_factors = tendency_scale_factors
-            self._component_type = 'Prognostic'
-
-        elif hasattr(component, 'input_properties') and hasattr(component, 'diagnostic_properties'):
-            self._component_type = 'Diagnostic'
-        else:
-            raise TypeError(
-                "Component must be either of type Implicit or Prognostic or Diagnostic")
-
-        self._component = component
+        self._output_scale_factors = dict()
+        if output_scale_factors is not None:
+            if not hasattr(component, 'output_properties'):
+                raise TypeError(
+                    'Cannot apply output scale factors to component without '
+                    'output_properties.')
+            self._ensure_fields_have_properties(
+                output_scale_factors, component.output_properties, 'output')
+            self._output_scale_factors = output_scale_factors
+
+        self._tendency_scale_factors = dict()
+        if tendency_scale_factors is not None:
+            if not hasattr(component, 'tendency_properties'):
+                raise TypeError(
+                    'Cannot apply tendency scale factors to component that does '
+                    'not output tendencies.')
+            self._ensure_fields_have_properties(
+                tendency_scale_factors, component.tendency_properties, 'tendency')
+            self._tendency_scale_factors = tendency_scale_factors
+
+    def _ensure_fields_have_properties(
+            self, scale_factors, properties, properties_name):
+        for field in scale_factors.keys():
+            if field not in properties.keys():
+                raise ValueError(
+                    "{} is not a {} quantity in the given component"
+                    ", but was given a scale factor.".format(field, properties_name))
 
     def __getattr__(self, item):
         return getattr(self._component, item)
 
     def __call__(self, state, timestep=None):
+        """
+        Call the underlying component, applying scaling.
+
+        Parameters
+        ----------
+        state : dict
+            A model state dictionary.
+        timestep : timedelta, optional
+            A time step. If the underlying component does not use a timestep,
+            this will be discarded. If it does, this argument is required.
 
+        Returns
+        -------
+        *args
+            The return values of the underlying component.
+        """
         scaled_state = {}
         if 'time' in state:
             scaled_state['time'] = state['time']
 
-        for input_field in self.inputs:
+        for input_field in self.input_properties.keys():
             if input_field in self._input_scale_factors:
                 scale_factor = self._input_scale_factors[input_field]
                 scaled_state[input_field] = state[input_field]*float(scale_factor)
+                scaled_state[input_field].attrs = state[input_field].attrs
             else:
                 scaled_state[input_field] = state[input_field]
 
-        if self._component_type == 'Implicit':
+        if isinstance(self._component, Stepper):
+            if timestep is None:
+                raise TypeError('Must give timestep to call Stepper.')
             diagnostics, new_state = self._component(scaled_state, timestep)
-
-            for output_field in self._output_scale_factors.keys():
-                scale_factor = self._output_scale_factors[output_field]
-                new_state[output_field] *= float(scale_factor)
-
-            for diagnostic_field in self._diagnostic_scale_factors.keys():
-                scale_factor = self._diagnostic_scale_factors[diagnostic_field]
-                diagnostics[diagnostic_field] *= float(scale_factor)
-
+            for name in self._output_scale_factors.keys():
+                scale_factor = self._output_scale_factors[name]
+                new_state[name] *= float(scale_factor)
+            for name in self._diagnostic_scale_factors.keys():
+                scale_factor = self._diagnostic_scale_factors[name]
+                diagnostics[name] *= float(scale_factor)
             return diagnostics, new_state
-        elif self._component_type == 'Prognostic':
+        elif isinstance(self._component, TendencyComponent):
             tendencies, diagnostics = self._component(scaled_state)
-
             for tend_field in self._tendency_scale_factors.keys():
                 scale_factor = self._tendency_scale_factors[tend_field]
                 tendencies[tend_field] *= float(scale_factor)
-
-            for diagnostic_field in self._diagnostic_scale_factors.keys():
-                scale_factor = self._diagnostic_scale_factors[diagnostic_field]
-                diagnostics[diagnostic_field] *= float(scale_factor)
-
+            for name in self._diagnostic_scale_factors.keys():
+                scale_factor = self._diagnostic_scale_factors[name]
+                diagnostics[name] *= float(scale_factor)
+            return tendencies, diagnostics
+        elif isinstance(self._component, ImplicitTendencyComponent):
+            if timestep is None:
+                raise TypeError('Must give timestep to call ImplicitTendencyComponent.')
+            tendencies, diagnostics = self._component(scaled_state, timestep)
+            for tend_field in self._tendency_scale_factors.keys():
+                scale_factor = self._tendency_scale_factors[tend_field]
+                tendencies[tend_field] *= float(scale_factor)
+            for name in self._diagnostic_scale_factors.keys():
+                scale_factor = self._diagnostic_scale_factors[name]
+                diagnostics[name] *= float(scale_factor)
             return tendencies, diagnostics
-        elif self._component_type == 'Diagnostic':
+        elif isinstance(self._component, DiagnosticComponent):
             diagnostics = self._component(scaled_state)
-
-            for diagnostic_field in self._diagnostic_scale_factors.keys():
-                scale_factor = self._diagnostic_scale_factors[diagnostic_field]
-                diagnostics[diagnostic_field] *= float(scale_factor)
-
+            for name in self._diagnostic_scale_factors.keys():
+                scale_factor = self._diagnostic_scale_factors[name]
+                diagnostics[name] *= float(scale_factor)
             return diagnostics
         else:  # Should never reach this
-            raise ValueError(
+            raise RuntimeError(
                 'Unknown component type, seems to be a bug in ScalingWrapper')
 
 
 class UpdateFrequencyWrapper(object):
     """
-    Wraps a prognostic object so that when it is called, it only computes new
+    Wraps a component object so that when it is called, it only computes new
     output if sufficient time has passed, and otherwise returns its last
-    computed output. The Delayed object requires that the 'time' attribute is
-    set in the state, in addition to any requirements of the Prognostic
+    computed output.
 
     Example
     -------
-    This how the wrapper should be used on a fictional Prognostic class
+    This how the wrapper should be used on a fictional TendencyComponent class
     called MyPrognostic.
-
     >>> from datetime import timedelta
     >>> prognostic = UpdateFrequencyWrapper(MyPrognostic(), timedelta(hours=1))
     """
 
-    def __init__(self, prognostic, update_timedelta):
+    def __init__(self, component, update_timedelta):
         """
         Initialize the UpdateFrequencyWrapper object.
 
         Args
         ----
-        prognostic : Prognostic
-            The object to be wrapped.
+        component : TendencyComponent, Stepper, DiagnosticComponent, ImplicitTendencyComponent
+            The component to be wrapped.
         update_timedelta : timedelta
             The amount that state['time'] must differ from when output
             was cached before new output is computed.
         """
-        self._prognostic = prognostic
+        self.component = component
         self._update_timedelta = update_timedelta
         self._cached_output = None
         self._last_update_time = None
 
-    def __call__(self, state, **kwargs):
+    def __call__(self, state, timestep=None, **kwargs):
+        """
+        Call the underlying component, or return cached values instead if
+        insufficient time has passed since the last time cached values were
+        stored.
+
+        Parameters
+        ----------
+        state : dict
+            A model state dictionary.
+        timestep : timedelta, optional
+            A time step. If the underlying component does not use a timestep,
+            this will be discarded. If it does, this argument is required.
+
+        Returns
+        -------
+        *args
+            The return values of the underlying component.
+        """
         if ((self._last_update_time is None) or
                 (state['time'] >= self._last_update_time +
                  self._update_timedelta)):
-            self._cached_output = self._prognostic(state, **kwargs)
+            if timestep is not None:
+                try:
+                    self._cached_output = self.component(state, timestep, **kwargs)
+                except TypeError:
+                    self._cached_output = self.component(state, **kwargs)
+            else:
+                self._cached_output = self.component(state, **kwargs)
             self._last_update_time = state['time']
         return self._cached_output
 
     def __getattr__(self, item):
-        return getattr(self._prognostic, item)
-
-
-class TendencyInDiagnosticsWrapper(object):
-    """
-    Wraps a prognostic object so that when it is called, it returns all
-    tendencies in its diagnostics.
-
-    Example
-    -------
-    This how the wrapper should be used on a fictional Prognostic class
-    called RRTMRadiation.
-
-    >>> prognostic = TendencyInDiagnosticsWrapper(RRTMRadiation(), 'radiation')
-    """
-
-    def __init__(self, prognostic, label):
-        """
-        Initialize the Delayed object.
-
-        Args
-        ----
-        prognostic : Prognostic
-            The object to be wrapped
-        label : str
-            Label describing what the tendencies are due to, to be
-            put in the diagnostic quantity names.
-        """
-        self._prognostic = prognostic
-        self._tendency_label = label
-        self._tendency_diagnostic_properties = {}
-        for quantity_name, properties in prognostic.tendency_properties.items():
-            diagnostic_name = '{}_tendency_from_{}'.format(quantity_name, label)
-            self._tendency_diagnostic_properties[diagnostic_name] = properties
-
-    @property
-    def inputs(self):
-        return list(self.diagnostic_properties.keys())
-
-    @property
-    def input_properties(self):
-        return_dict = self._prognostic.input_properties.copy()
-        return_dict.update(self._tendency_diagnostic_properties)
-        return return_dict
-
-    @property
-    def diagnostics(self):
-        return list(self.diagnostic_properties.keys())
-
-    @property
-    def diagnostic_properties(self):
-        return_dict = self._prognostic.diagnostic_properties.copy()
-        return_dict.update(self._tendency_diagnostic_properties)
-        return return_dict
-
-    def __call__(self, state, **kwargs):
-        tendencies, diagnostics = self._prognostic(state, **kwargs)
-        for quantity_name in tendencies.keys():
-            diagnostic_name = '{}_tendency_from_{}'.format(
-                quantity_name, self._tendency_label)
-            diagnostics[diagnostic_name] = tendencies[quantity_name]
-        return tendencies, diagnostics
-
-    def __getattr__(self, item):
-        return getattr(self._prognostic, item)
-
-
-class TimeDifferencingWrapper(ImplicitPrognostic):
-    """
-    Wraps an Implicit object and turns it into an ImplicitPrognostic by applying
-    simple first-order time differencing to determine tendencies.
-
-    Example
-    -------
-    This how the wrapper should be used on an Implicit class
-    called GridScaleCondensation.
-
-    >>> component = TimeDifferencingWrapper(GridScaleCondensation())
-    """
-
-    def __init__(self, implicit):
-        self._implicit = implicit
-
-    def __call__(self, state, timestep):
-        diagnostics, new_state = self._implicit(state, timestep)
-        tendencies = {}
-        timestep_seconds = timestep.total_seconds()
-        for varname, data_array in new_state.items():
-            if isinstance(data_array, DataArray):
-                if varname in self._implicit.output_properties.keys():
-                    if varname not in state.keys():
-                        raise RuntimeError(
-                            'Cannot calculate tendency for {} because it is not'
-                            ' present in the input state.'.format(varname))
-                    tendency = (data_array - state[varname].to_units(data_array.attrs['units'])) / timestep_seconds
-                    if data_array.attrs['units'] == '':
-                        tendency.attrs['units'] = 's^-1'
-                    else:
-                        tendency.attrs['units'] = data_array.attrs['units'] + ' s^-1'
-                    tendencies[varname] = tendency.to_units(
-                        self._implicit.output_properties[varname]['units'] + ' s^-1')
-            elif varname != 'time':
-                raise ValueError(
-                    'Wrapped implicit gave an output {} of type {}, but should'
-                    'only give sympl.DataArray objects.'.format(
-                        varname, type(data_array)))
-        return tendencies, diagnostics
-
-    @property
-    def tendencies(self):
-        return list(self.tendency_properties.keys())
-
-    @property
-    def tendency_properties(self):
-        return_dict = self._implicit.output_properties.copy()
-        return_dict.update(self._tendency_diagnostic_properties)
-        return return_dict
-
-    def __getattr__(self, item):
-        if item not in ('outputs', 'output_properties'):
-            return getattr(self._implicit, item)
+        return getattr(self.component, item)
diff --git a/tox.ini b/tox.ini
index 1d140b5..04f9fa6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -17,7 +17,7 @@ deps =
     -r{toxinidir}/requirements_dev.txt
 commands =
     pip install -U pip
-    pip install netcdftime
+    pip install cftime
     py.test --basetemp={envtmpdir} -v
 
 [testenv:cov]
@@ -26,7 +26,7 @@ commands =
 passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH
 usedevelop = true
 commands =
-    pip install codecov netcdftime
+    pip install codecov cftime
     py.test --cov=sympl
     codecov --token=970a49ea-e6db-4abb-a7e2-909b139e0ce1
 deps =


Test patch:
diff --git a/sympl/_core/testing.py b/sympl/_core/testing.py
deleted file mode 100644
index 6df1a0d..0000000
--- a/sympl/_core/testing.py
+++ /dev/null
@@ -1,310 +0,0 @@
-import abc
-import os
-from glob import glob
-import xarray as xr
-from .util import same_list
-from .base_components import Diagnostic, Prognostic, Implicit
-from .timestepping import TimeStepper
-import numpy as np
-from .units import is_valid_unit
-from datetime import timedelta
-
-
-def cache_dictionary(dictionary, filename):
-    dataset = xr.Dataset(dictionary)
-    dataset.to_netcdf(filename, engine='scipy')
-
-
-def load_dictionary(filename):
-    dataset = xr.open_dataset(filename, engine='scipy')
-    return dict(dataset.data_vars)
-
-
-def call_with_timestep_if_needed(
-        component, state, timestep=timedelta(seconds=10.)):
-    if isinstance(component, (Implicit, TimeStepper)):
-        return component(state, timestep=timestep)
-    else:
-        return component(state)
-
-
-def compare_outputs(current, cached):
-    if isinstance(current, tuple) and isinstance(cached, tuple):
-        for i in range(len(current)):
-            compare_one_state_pair(current[i], cached[i])
-    elif (not isinstance(current, tuple)) and (not isinstance(cached, tuple)):
-        compare_one_state_pair(current, cached)
-    else:
-        raise AssertionError('Different number of dicts returned than cached.')
-
-
-def compare_one_state_pair(current, cached):
-    for key in current.keys():
-        try:
-            assert np.all(np.isclose(current[key].values, cached[key].values))
-            for attr in current[key].attrs:
-                assert current[key].attrs[attr] == cached[key].attrs[attr]
-            for attr in cached[key].attrs:
-                assert attr in current[key].attrs
-            assert current[key].dims == cached[key].dims
-        except AssertionError as err:
-            raise AssertionError('Error for {}: {}'.format(key, err))
-    for key in cached.keys():
-        assert key in current.keys()
-
-
-def assert_dimension_lengths_are_consistent(state):
-    dimension_lengths = {}
-    for name, value in state.items():
-        if name != 'time':
-            for i, dim_name in enumerate(value.dims):
-                try:
-                    if dim_name in dimension_lengths:
-                        assert dimension_lengths[dim_name] == value.shape[i]
-                    else:
-                        dimension_lengths[dim_name] = value.shape[i]
-                except AssertionError as err:
-                    raise AssertionError(
-                        'Inconsistent length on dimension {} for value {}:'
-                        '{}'.format(dim_name, name, err))
-
-
-class ComponentTestBase(object):
-
-    cache_folder = None
-
-    @abc.abstractmethod
-    def get_input_state(self):
-        pass
-
-    @abc.abstractmethod
-    def get_component_instance(self):
-        pass
-
-    def get_cached_output(self,):
-        cache_filename_list = sorted(glob(
-            os.path.join(
-                self.cache_folder,
-                '{}-*.cache'.format(
-                    self.__class__.__name__))))
-        if len(cache_filename_list) > 0:
-            return_list = []
-            for filename in cache_filename_list:
-                return_list.append(load_dictionary(filename))
-            if len(return_list) > 1:
-                return tuple(return_list)
-            elif len(return_list) == 1:
-                return return_list[0]
-        else:
-            return None
-
-    def cache_output(self, output):
-        if not isinstance(output, tuple):
-            output = (output,)
-        for i in range(len(output)):
-            cache_filename = os.path.join(
-                self.cache_folder, '{}-{}.cache'.format(self.__class__.__name__, i))
-            cache_dictionary(output[i], cache_filename)
-
-    def test_output_matches_cached_output(self):
-        state = self.get_input_state()
-        component = self.get_component_instance()
-        output = call_with_timestep_if_needed(component, state)
-        cached_output = self.get_cached_output()
-        if cached_output is None:
-            self.cache_output(output)
-            raise AssertionError(
-                'Failed due to no cached output, cached current output')
-        else:
-            compare_outputs(output, cached_output)
-
-    def test_component_listed_inputs_are_accurate(self):
-        state = self.get_input_state()
-        component = self.get_component_instance()
-        input_state = {}
-        for key in component.inputs:
-            input_state[key] = state[key]
-        output = call_with_timestep_if_needed(component, state)
-        cached_output = self.get_cached_output()
-        if cached_output is not None:
-            compare_outputs(output, cached_output)
-
-    def test_inputs_and_outputs_have_consistent_dim_lengths(self):
-        """A given dimension name should always have the same length."""
-        input_state = self.get_input_state()
-        assert_dimension_lengths_are_consistent(input_state)
-        component = self.get_component_instance()
-        output = call_with_timestep_if_needed(component, input_state)
-        if isinstance(output, tuple):
-            # Check diagnostics/tendencies/outputs are consistent with one
-            # another
-            test_state = {}
-            for state in output:
-                test_state.update(state)
-            assert_dimension_lengths_are_consistent(test_state)
-        else:
-            test_state = output  # if not a tuple assume it's a dict
-            assert_dimension_lengths_are_consistent(test_state)
-
-    def test_listed_outputs_are_accurate(self):
-        state = self.get_input_state()
-        component = self.get_component_instance()
-        if isinstance(component, Diagnostic):
-            diagnostics = component(state)
-            assert same_list(component.diagnostics, diagnostics.keys())
-        elif isinstance(component, Prognostic):
-            tendencies, diagnostics = component(state)
-            assert same_list(component.tendencies, tendencies.keys())
-            assert same_list(component.diagnostics, diagnostics.keys())
-        elif isinstance(component, Implicit):
-            diagnostics, new_state = component(state)
-            assert same_list(component.diagnostics, diagnostics.keys())
-            assert same_list(component.outputs, new_state.keys())
-
-    def test_modifies_attribute_is_accurate(self):
-        state = self.get_input_state()
-        component = self.get_component_instance()
-        if not hasattr(component, 'modifies'):
-            raise AssertionError("component does not have a 'modifies' property")
-        original_state = {}
-        for key, value in state.items():
-            if key == 'time':
-                original_state[key] = state[key]
-            else:
-                original_state[key] = state[key].copy(deep=True)
-        component(state)
-        for key in state.keys():
-            if key not in state.modifies:
-                assert np.all(original_state[key] == state[key]), key
-
-    def test_has_input_properties(self):
-        component = self.get_component_instance()
-        assert hasattr(component, 'input_properties')
-        assert isinstance(component.input_properties, dict)
-
-    def test_has_output_properties(self):
-        component = self.get_component_instance()
-        if isinstance(component, [Implicit, TimeStepper]):
-            assert hasattr(component, 'output_properties')
-            assert isinstance(component.output_properties, dict)
-
-    def test_has_tendency_properties(self):
-        component = self.get_component_instance()
-        if isinstance(component, Prognostic):
-            assert hasattr(component, 'tendency_properties')
-            assert isinstance(component.tendency_properties, dict)
-
-    def test_has_diagnostic_properties(self):
-        component = self.get_component_instance()
-        if isinstance(component, [Diagnostic, Prognostic, Implicit, TimeStepper]):
-            assert hasattr(component, 'diagnostic_properties')
-            assert isinstance(component.diagnostic_properties, dict)
-
-    def test_input_unit_properties_are_valid(self):
-        component = self.get_component_instance()
-        for name, properties in component.input_properties.items():
-            if 'units' not in properties.keys():
-                raise AssertionError(
-                    "quantity {} has no 'units' property defined".format(name))
-            if not is_valid_unit(properties['units']):
-                raise AssertionError(
-                    "unit {} for quantity {} is not recognized".format(
-                        properties['units'], name
-                    )
-                )
-
-    def test_diagnostic_unit_properties_are_valid(self):
-        component = self.get_component_instance()
-        if hasattr(component, 'diagnostic_properties'):
-            for name, properties in component.diagnostic_properties.items():
-                if 'units' not in properties.keys():
-                    raise AssertionError(
-                        "quantity {} has no 'units' property defined in "
-                        "diagnostic_properties".format(name))
-                if not is_valid_unit(properties['units']):
-                    raise AssertionError(
-                        "unit {} for quantity {} in diagnostic_properties "
-                        "is not recognized".format(
-                            properties['units'], name
-                        )
-                    )
-
-    def test_tendency_unit_properties_are_valid(self):
-        component = self.get_component_instance()
-        if hasattr(component, 'tendency_properties'):
-            for name, properties in component.tendency_properties.items():
-                if 'units' not in properties.keys():
-                    raise AssertionError(
-                        "quantity {} has no 'units' property defined in "
-                        "tendency_properties".format(name))
-                if not is_valid_unit(properties['units']):
-                    raise AssertionError(
-                        "unit {} for quantity {} in tendency_properties "
-                        "is not recognized".format(
-                            properties['units'], name
-                        )
-                    )
-
-    def test_output_unit_properties_are_valid(self):
-        component = self.get_component_instance()
-        if hasattr(component, 'output_properties'):
-            for name, properties in component.output_properties.items():
-                if 'units' not in properties.keys():
-                    raise AssertionError(
-                        "quantity {} has no 'units' property defined in "
-                        "output_properties".format(name))
-                if not is_valid_unit(properties['units']):
-                    raise AssertionError(
-                        "unit {} for quantity {} in output_properties "
-                        "is not recognized".format(
-                            properties['units'], name
-                        )
-                    )
-
-    def test_tendency_dims_like_properties_are_inputs(self):
-        component = self.get_component_instance()
-        if hasattr(component, 'tendency_properties'):
-            for name, properties in component.tendency_properties.items():
-                if 'dims_like' not in properties.keys():
-                    raise AssertionError(
-                        "quantity {} has no 'dims_like' property defined in "
-                        "tendency_properties".format(name))
-                if properties['dims_like'] not in component.inputs:
-                    raise AssertionError(
-                        'quantity {} has dims_like {} in tendency_properties, '
-                        'but {} is not specified as an input'.format(
-                            name, properties['dims_like'],
-                            properties['dims_like'])
-                    )
-
-    def test_diagnostic_dims_like_properties_are_inputs(self):
-        component = self.get_component_instance()
-        if hasattr(component, 'diagnostic_properties'):
-            for name, properties in component.diagnostic_properties.items():
-                if 'dims_like' not in properties.keys():
-                    raise AssertionError(
-                        "quantity {} has no 'dims_like' property defined in "
-                        "diagnostic_properties".format(name))
-                if properties['dims_like'] not in component.inputs:
-                    raise AssertionError(
-                        'quantity {} has dims_like {} in diagnostic_properties, '
-                        'but {} is not specified as an input'.format(
-                            name, properties['dims_like'],
-                            properties['dims_like'])
-                    )
-
-    def test_output_dims_like_properties_are_inputs(self):
-        component = self.get_component_instance()
-        if hasattr(component, 'output_properties'):
-            for name, properties in component.output_properties.items():
-                if 'dims_like' not in properties.keys():
-                    raise AssertionError(
-                        "quantity {} has no 'dims_like' property defined in "
-                        "output_properties".format(name))
-                if properties['dims_like'] not in component.inputs:
-                    raise AssertionError(
-                        'quantity {} has dims_like {} in output_properties, '
-                        'but {} is not specified as an input'.format(
-                            name, properties['dims_like'],
-                            properties['dims_like'])
-                    )
diff --git a/tests/test_base_components.py b/tests/test_base_components.py
index 386e6d5..fa9c547 100644
--- a/tests/test_base_components.py
+++ b/tests/test_base_components.py
@@ -1,8 +1,12 @@
 import pytest
 import mock
+import numpy as np
+import unittest
 from sympl import (
-    Prognostic, Diagnostic, Monitor, PrognosticComposite, DiagnosticComposite,
-    MonitorComposite, SharedKeyError, DataArray
+    TendencyComponent, DiagnosticComponent, Monitor, Stepper, ImplicitTendencyComponent,
+    datetime, timedelta, DataArray, InvalidPropertyDictError,
+    ComponentMissingOutputError, ComponentExtraOutputError,
+    InvalidStateError
 )
 
 def same_list(list1, list2):
@@ -10,22 +14,102 @@ def same_list(list1, list2):
         [item in list2 for item in list1] + [item in list1 for item in list2]))
 
 
-class MockPrognostic(Prognostic):
+class MockTendencyComponent(TendencyComponent):
 
-    def __call__(self, state):
-        return {}, {}
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
 
+    def __init__(
+            self, input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.tendency_properties = tendency_properties
+        self.diagnostic_output = diagnostic_output
+        self.tendency_output = tendency_output
+        self.times_called = 0
+        self.state_given = None
+        super(MockTendencyComponent, self).__init__(**kwargs)
 
-class MockPrognostic2(Prognostic):
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return self.tendency_output, self.diagnostic_output
 
-    def __call__(self, state):
-        return {}, {}
 
+class MockImplicitTendencyComponent(ImplicitTendencyComponent):
 
-class MockDiagnostic(Diagnostic):
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
 
-    def __call__(self, state):
-        return {}
+    def __init__(
+            self, input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.tendency_properties = tendency_properties
+        self.diagnostic_output = diagnostic_output
+        self.tendency_output = tendency_output
+        self.times_called = 0
+        self.state_given = None
+        self.timestep_given = None
+        super(MockImplicitTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state, timestep):
+        self.times_called += 1
+        self.state_given = state
+        self.timestep_given = timestep
+        return self.tendency_output, self.diagnostic_output
+
+
+class MockDiagnosticComponent(DiagnosticComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, diagnostic_output,
+            **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.diagnostic_output = diagnostic_output
+        self.times_called = 0
+        self.state_given = None
+        super(MockDiagnosticComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return self.diagnostic_output
+
+
+class MockStepper(Stepper):
+
+    input_properties = None
+    diagnostic_properties = None
+    output_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, state_output,
+            **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.output_properties = output_properties
+        self.diagnostic_output = diagnostic_output
+        self.state_output = state_output
+        self.times_called = 0
+        self.state_given = None
+        self.timestep_given = None
+        super(MockStepper, self).__init__(**kwargs)
+
+    def array_call(self, state, timestep):
+        self.times_called += 1
+        self.state_given = state
+        self.timestep_given = timestep
+        return self.diagnostic_output, self.state_output
 
 
 class MockMonitor(Monitor):
@@ -33,241 +117,1953 @@ class MockMonitor(Monitor):
     def store(self, state):
         return
 
+class BadMockTendencyComponent(TendencyComponent):
+
+    input_properties = {}
+    tendency_properties = {}
+    diagnostic_properties = {}
 
-def test_empty_prognostic_composite():
-    prognostic_composite = PrognosticComposite()
-    state = {'air_temperature': 273.15}
-    tendencies, diagnostics = prognostic_composite(state)
-    assert len(tendencies) == 0
-    assert len(diagnostics) == 0
-    assert isinstance(tendencies, dict)
-    assert isinstance(diagnostics, dict)
-
-
-@mock.patch.object(MockPrognostic, '__call__')
-def test_prognostic_composite_calls_one_prognostic(mock_call):
-    mock_call.return_value = ({'air_temperature': 0.5}, {'foo': 50.})
-    prognostic_composite = PrognosticComposite(MockPrognostic())
-    state = {'air_temperature': 273.15}
-    tendencies, diagnostics = prognostic_composite(state)
-    assert mock_call.called
-    assert tendencies == {'air_temperature': 0.5}
-    assert diagnostics == {'foo': 50.}
-
-
-@mock.patch.object(MockPrognostic, '__call__')
-def test_prognostic_composite_calls_two_prognostics(mock_call):
-    mock_call.return_value = ({'air_temperature': 0.5}, {})
-    prognostic_composite = PrognosticComposite(
-        MockPrognostic(), MockPrognostic())
-    state = {'air_temperature': 273.15}
-    tendencies, diagnostics = prognostic_composite(state)
-    assert mock_call.called
-    assert mock_call.call_count == 2
-    assert tendencies == {'air_temperature': 1.}
-    assert diagnostics == {}
-
-
-def test_empty_diagnostic_composite():
-    diagnostic_composite = DiagnosticComposite()
-    state = {'air_temperature': 273.15}
-    diagnostics = diagnostic_composite(state)
-    assert len(diagnostics) == 0
-    assert isinstance(diagnostics, dict)
-
-
-@mock.patch.object(MockDiagnostic, '__call__')
-def test_diagnostic_composite_calls_one_diagnostic(mock_call):
-    mock_call.return_value = {'foo': 50.}
-    diagnostic_composite = DiagnosticComposite(MockDiagnostic())
-    state = {'air_temperature': 273.15}
-    diagnostics = diagnostic_composite(state)
-    assert mock_call.called
-    assert diagnostics == {'foo': 50.}
-
-
-def test_empty_monitor_collection():
-    # mainly we're testing that nothing errors
-    monitor_collection = MonitorComposite()
-    state = {'air_temperature': 273.15}
-    monitor_collection.store(state)
-
-
-@mock.patch.object(MockMonitor, 'store')
-def test_monitor_collection_calls_one_monitor(mock_store):
-    mock_store.return_value = None
-    monitor_collection = MonitorComposite(MockMonitor())
-    state = {'air_temperature': 273.15}
-    monitor_collection.store(state)
-    assert mock_store.called
-
-
-@mock.patch.object(MockMonitor, 'store')
-def test_monitor_collection_calls_two_monitors(mock_store):
-    mock_store.return_value = None
-    monitor_collection = MonitorComposite(MockMonitor(), MockMonitor())
-    state = {'air_temperature': 273.15}
-    monitor_collection.store(state)
-    assert mock_store.called
-    assert mock_store.call_count == 2
-
-
-def test_prognostic_composite_cannot_use_diagnostic():
-    try:
-        PrognosticComposite(MockDiagnostic())
-    except TypeError:
+    def __init__(self):
         pass
-    except Exception as err:
-        raise err
-    else:
-        raise AssertionError('TypeError should have been raised')
 
+    def array_call(self, state):
+        return {}, {}
+
+
+class BadMockImplicitTendencyComponent(ImplicitTendencyComponent):
 
-def test_diagnostic_composite_cannot_use_prognostic():
-    try:
-        DiagnosticComposite(MockPrognostic())
-    except TypeError:
+    input_properties = {}
+    tendency_properties = {}
+    diagnostic_properties = {}
+
+    def __init__(self):
         pass
-    except Exception as err:
-        raise err
-    else:
-        raise AssertionError('TypeError should have been raised')
-
-
-@mock.patch.object(MockDiagnostic, '__call__')
-def test_diagnostic_composite_call(mock_call):
-    mock_call.return_value = {'foo': 5.}
-    state = {'bar': 10.}
-    diagnostics = DiagnosticComposite(MockDiagnostic())
-    new_state = diagnostics(state)
-    assert list(state.keys()) == ['bar']
-    assert state['bar'] == 10.
-    assert list(new_state.keys()) == ['foo']
-    assert new_state['foo'] == 5.
-
-
-def test_prognostic_composite_includes_attributes():
-    prognostic = MockPrognostic()
-    prognostic.input_properties = {'input1': {'units': 'K', 'dims':['z', 'y', 'x']}}
-    prognostic.diagnostic_properties = {'diagnostic1': {'units': 'm/s'}}
-    prognostic.tendency_properties = {'tendency1': {}}
-    composite = PrognosticComposite(prognostic)
-    assert composite.inputs == ('input1',)
-    assert composite.diagnostics == ('diagnostic1',)
-    assert composite.tendencies == ('tendency1',)
-
-
-def test_prognostic_composite_includes_attributes_from_two():
-    prognostic1 = MockPrognostic()
-    prognostic1.input_properties = {'input1': {'units': 'K', 'dims':['z', 'y', 'x']}}
-    prognostic1.diagnostic_properties = {'diagnostic1': {'units': 'm/s'}}
-    prognostic1.tendency_properties = {'tendency1': {}}
-    prognostic2 = MockPrognostic()
-    prognostic2.input_properties = {'input2': {'units': 'K', 'dims':['z', 'y', 'x']}}
-    prognostic2.diagnostic_properties = {'diagnostic2': {'units': 'm/s'}}
-    prognostic2.tendency_properties = {'tendency2': {}}
-    composite = PrognosticComposite(prognostic1, prognostic2)
-    assert same_list(composite.inputs, ('input1', 'input2'))
-    assert same_list(composite.diagnostics, ('diagnostic1', 'diagnostic2'))
-    assert same_list(composite.tendencies, ('tendency1', 'tendency2'))
-
-
-def test_prognostic_merges_attributes():
-    prognostic1 = MockPrognostic()
-    prognostic1.input_properties = {'input1': {}}
-    prognostic1.diagnostic_properties = {'diagnostic1': {}}
-    prognostic1.tendency_properties = {'tendency1': {}, 'tendency2': {}}
-    prognostic2 = MockPrognostic()
-    prognostic2.input_properties = {'input1': {}, 'input2': {}}
-    prognostic2.diagnostic_properties = {'diagnostic2': {}}
-    prognostic2.tendency_properties = {'tendency2': {}}
-    composite = PrognosticComposite(prognostic1, prognostic2)
-    assert same_list(composite.inputs, ('input1', 'input2'))
-    assert same_list(composite.diagnostics, ('diagnostic1', 'diagnostic2'))
-    assert same_list(composite.tendencies, ('tendency1', 'tendency2'))
-
-
-def test_prognostic_composite_ensures_valid_state():
-    prognostic1 = MockPrognostic()
-    prognostic1.input_properties = {'input1': {}}
-    prognostic1.diagnostic_properties = {'diagnostic1': {}}
-    prognostic2 = MockPrognostic()
-    prognostic2.input_properties = {'input1': {}, 'input2': {}}
-    prognostic2.diagnostic_properties = {'diagnostic1': {}}
-    try:
-        PrognosticComposite(prognostic1, prognostic2)
-    except SharedKeyError:
+
+    def array_call(self, state, timestep):
+        return {}, {}
+
+
+class BadMockDiagnosticComponent(DiagnosticComponent):
+
+    input_properties = {}
+    diagnostic_properties = {}
+
+    def __init__(self):
         pass
-    except Exception as err:
-        raise err
-    else:
-        raise AssertionError(
-            'Should not be able to have overlapping diagnostics in composite')
-
-
-@mock.patch.object(MockPrognostic, '__call__')
-@mock.patch.object(MockPrognostic2, '__call__')
-def test_prognostic_component_handles_units_when_combining(mock_call, mock2_call):
-    mock_call.return_value = ({
-        'eastward_wind': DataArray(1., attrs={'units': 'm/s'})}, {})
-    mock2_call.return_value = ({
-        'eastward_wind': DataArray(50., attrs={'units': 'cm/s'})}, {})
-    prognostic1 = MockPrognostic()
-    prognostic2 = MockPrognostic2()
-    composite = PrognosticComposite(prognostic1, prognostic2)
-    tendencies, diagnostics = composite({})
-    assert tendencies['eastward_wind'].to_units('m/s').values.item() == 1.5
-
-
-def test_diagnostic_composite_includes_attributes():
-    diagnostic = MockDiagnostic()
-    diagnostic.input_properties = {'input1': {}}
-    diagnostic.diagnostic_properties = {'diagnostic1': {}}
-    composite = DiagnosticComposite(diagnostic)
-    assert composite.inputs == ('input1',)
-    assert composite.diagnostics == ('diagnostic1',)
-
-
-def test_diagnostic_composite_includes_attributes_from_two():
-    diagnostic1 = MockDiagnostic()
-    diagnostic1.input_properties = {'input1': {}}
-    diagnostic1.diagnostic_properties = {'diagnostic1': {}}
-    diagnostic2 = MockDiagnostic()
-    diagnostic2.input_properties = {'input2': {}}
-    diagnostic2.diagnostic_properties = {'diagnostic2': {}}
-    composite = DiagnosticComposite(diagnostic1, diagnostic2)
-    assert same_list(composite.inputs, ('input1', 'input2'))
-    assert same_list(composite.diagnostics, ('diagnostic1', 'diagnostic2'))
-
-
-def test_diagnostic_composite_merges_attributes():
-    diagnostic1 = MockDiagnostic()
-    diagnostic1.input_properties = {'input1': {}}
-    diagnostic1.diagnostic_properties = {'diagnostic1': {}}
-    diagnostic2 = MockDiagnostic()
-    diagnostic2.input_properties = {'input1': {}, 'input2': {}}
-    diagnostic2.diagnostic_properties = {'diagnostic2': {}}
-    composite = DiagnosticComposite(diagnostic1, diagnostic2)
-    assert same_list(composite.inputs, ('input1', 'input2'))
-    assert same_list(composite.diagnostics, ('diagnostic1', 'diagnostic2'))
-
-
-def test_diagnostic_composite_ensures_valid_state():
-    diagnostic1 = MockDiagnostic()
-    diagnostic1.input_properties = {'input1': {}}
-    diagnostic1.diagnostic_properties = {'diagnostic1': {}}
-    diagnostic2 = MockDiagnostic()
-    diagnostic2.input_properties = {'input1': {}, 'input2': {}}
-    diagnostic2.diagnostic_properties = {'diagnostic1': {}}
-    try:
-        DiagnosticComposite(diagnostic1, diagnostic2)
-    except SharedKeyError:
+
+    def array_call(self, state):
+        return {}
+
+
+class BadMockStepper(Stepper):
+
+    input_properties = {}
+    diagnostic_properties = {}
+    output_properties = {}
+
+    def __init__(self):
         pass
-    except Exception as err:
-        raise err
-    else:
-        raise AssertionError(
-            'Should not be able to have overlapping diagnostics in composite')
+
+    def array_call(self, state, timestep):
+        return {}, {}
+
+
+class InputTestBase():
+
+    def test_raises_on_input_properties_of_wrong_type(self):
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(input_properties=({},))
+
+    def test_cannot_overlap_input_aliases(self):
+        input_properties = {
+            'input1': {'dims': ['dim1'], 'units': 'm', 'alias': 'input'},
+            'input2': {'dims': ['dim1'], 'units': 'm', 'alias': 'input'}
+        }
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(input_properties=input_properties)
+
+    def test_raises_when_input_missing(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {'time': timedelta(0)}
+        with self.assertRaises(InvalidStateError):
+            self.call_component(component, state)
+
+    def test_raises_when_input_incorrect_units(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.zeros([10]),
+                dims=['dim1'],
+                attrs={'units': 's'},
+            ),
+        }
+        with self.assertRaises(InvalidStateError):
+            self.call_component(component, state)
+
+    def test_raises_when_input_incorrect_dims(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.zeros([10]),
+                dims=['dim2'],
+                attrs={'units': 'm'},
+            ),
+        }
+        with self.assertRaises(InvalidStateError):
+            self.call_component(component, state)
+
+    def test_raises_when_input_conflicting_dim_lengths(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+            'input1': {
+                'dims': ['dim2'],
+                'units': 'm',
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.zeros([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'},
+            ),
+            'input2': DataArray(
+                np.zeros([7]),
+                dims=['dim1'],
+                attrs={'units': 'm'},
+            ),
+        }
+        with self.assertRaises(InvalidStateError):
+            self.call_component(component, state)
+
+    def test_collects_independent_wildcard_dims(self):
+        input_properties = {
+            'input1': {
+                'dims': ['*'],
+                'units': 'm',
+            },
+            'input2': {
+                'dims': ['*'],
+                'units': 'm',
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.zeros([4]),
+                dims=['dim1'],
+                attrs={'units': 'm'},
+            ),
+            'input2': DataArray(
+                np.zeros([3]),
+                dims=['dim2'],
+                attrs={'units': 'm'},
+            ),
+        }
+        self.call_component(component, state)
+        given = component.state_given
+        assert len(given.keys()) == 3
+        assert 'time' in given.keys()
+        assert 'input1' in given.keys()
+        assert given['input1'].shape == (12,)
+        assert 'input2' in given.keys()
+        assert given['input2'].shape == (12,)
+
+    def test_accepts_when_input_swapped_dims(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1', 'dim2'],
+                'units': 'm',
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.zeros([3, 4]),
+                dims=['dim2', 'dim1'],
+                attrs={'units': 'm'},
+            ),
+        }
+        self.call_component(component, state)
+        assert component.state_given['input1'].shape == (4, 3)
+
+    def test_input_requires_dims(self):
+        input_properties = {'input1': {'units': 'm'}}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(input_properties=input_properties)
+
+    def test_input_requires_units(self):
+        input_properties = {'input1': {'dims': ['dim1']}}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(input_properties=input_properties)
+
+    def test_input_no_transformations(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        self.call_component(component, state)
+        assert len(component.state_given) == 2
+        assert 'time' in component.state_given.keys()
+        assert 'input1' in component.state_given.keys()
+        assert isinstance(component.state_given['input1'], np.ndarray)
+        assert np.all(component.state_given['input1'] == np.ones([10]))
+
+    def test_input_converts_units(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'km'}
+            )
+        }
+        self.call_component(component, state)
+        assert len(component.state_given) == 2
+        assert 'time' in component.state_given.keys()
+        assert 'input1' in component.state_given.keys()
+        assert isinstance(component.state_given['input1'], np.ndarray)
+        assert np.all(component.state_given['input1'] == np.ones([10])*1000.)
+
+    def test_input_converts_temperature_units(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'degK'
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'degC'}
+            )
+        }
+        self.call_component(component, state)
+        assert len(component.state_given) == 2
+        assert 'time' in component.state_given.keys()
+        assert 'input1' in component.state_given.keys()
+        assert isinstance(component.state_given['input1'], np.ndarray)
+        assert np.all(component.state_given['input1'] == np.ones([10])*274.15)
+
+    def test_input_collects_one_dimension(self):
+        input_properties = {
+            'input1': {
+                'dims': ['*'],
+                'units': 'm'
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        self.call_component(component, state)
+        assert len(component.state_given) == 2
+        assert 'time' in component.state_given.keys()
+        assert 'input1' in component.state_given.keys()
+        assert isinstance(component.state_given['input1'], np.ndarray)
+        assert np.all(component.state_given['input1'] == np.ones([10]))
+
+    def test_input_is_aliased(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+                'alias': 'in1',
+            }
+        }
+        component = self.get_component(input_properties=input_properties)
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        self.call_component(component, state)
+        assert len(component.state_given) == 2
+        assert 'time' in component.state_given.keys()
+        assert 'in1' in component.state_given.keys()
+        assert isinstance(component.state_given['in1'], np.ndarray)
+        assert np.all(component.state_given['in1'] == np.ones([10]))
+
+
+class DiagnosticTestBase():
+
+    def test_raises_on_diagnostic_properties_of_wrong_type(self):
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(diagnostic_properties=({},))
+
+    def test_diagnostic_requires_dims(self):
+        diagnostic_properties = {'diag1': {'units': 'm'}}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(diagnostic_properties=diagnostic_properties)
+
+    def test_diagnostic_requires_units(self):
+        diagnostic_properties = {'diag1': {'dims': ['dim1']}}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(diagnostic_properties=diagnostic_properties)
+
+    def test_diagnostic_raises_when_units_incompatible_with_input(self):
+        input_properties = {
+            'diag1': {'units': 'km', 'dims': ['dim1', 'dim2']}
+        }
+        diagnostic_properties = {
+            'diag1': {'units': 'seconds', 'dims': ['dim1', 'dim2']}
+        }
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(
+                input_properties=input_properties,
+                diagnostic_properties=diagnostic_properties
+            )
+
+    def test_diagnostic_requires_correct_number_of_dims(self):
+        input_properties = {
+            'input1': {'units': 'm', 'dims': ['dim1', 'dim2']}
+        }
+        diagnostic_properties = {
+            'diag1': {'units': 'm', 'dims': ['dim1', 'dim2']}
+        }
+        diagnostic_output = {'diag1': np.zeros([10]),}
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10, 2]),
+                dims=['dim1', 'dim2'],
+                attrs={'units': 'm'}
+            )
+        }
+        component = self.get_component(
+            input_properties = input_properties,
+            diagnostic_properties=diagnostic_properties,
+            diagnostic_output=diagnostic_output,
+        )
+        with self.assertRaises(InvalidPropertyDictError):
+            _, _ = self.call_component(component, state)
+
+    def test_diagnostic_requires_correct_dim_length(self):
+        input_properties = {
+            'input1': {'units': 'm', 'dims': ['dim1', 'dim2']}
+        }
+        diagnostic_properties = {
+            'diag1': {'units': 'm', 'dims': ['dim1', 'dim2']}
+        }
+        diagnostic_output = {'diag1': np.zeros([5, 2]),}
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10, 2]),
+                dims=['dim1', 'dim2'],
+                attrs={'units': 'm'}
+            )
+        }
+        component = self.get_component(
+            input_properties=input_properties,
+            diagnostic_properties=diagnostic_properties,
+            diagnostic_output=diagnostic_output
+        )
+        with self.assertRaises(InvalidPropertyDictError):
+            _, _ = self.call_component(component, state)
+
+    def test_diagnostic_uses_input_dims(self):
+        input_properties = {'diag1': {'dims': ['dim1'], 'units': 'm'}}
+        diagnostic_properties = {'diag1': {'units': 'm'}}
+        self.get_component(
+            input_properties=input_properties,
+            diagnostic_properties=diagnostic_properties
+        )
+
+    def test_diagnostic_doesnt_use_input_units(self):
+        input_properties = {'diag1': {'dims': ['dim1'], 'units': 'm'}}
+        diagnostic_properties = {'diag1': {'dims': ['dim1']}}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(
+                input_properties=input_properties,
+                diagnostic_properties=diagnostic_properties
+            )
+
+    def test_diagnostics_no_transformations(self):
+        diagnostic_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        diagnostic_output = {
+            'output1': np.ones([10]),
+        }
+        component = self.get_component(
+            diagnostic_properties=diagnostic_properties,
+            diagnostic_output=diagnostic_output,
+        )
+        state = {'time': timedelta(0)}
+        diagnostics = self.get_diagnostics(self.call_component(component, state))
+        assert len(diagnostics) == 1
+        assert 'output1' in diagnostics.keys()
+        assert isinstance(diagnostics['output1'], DataArray)
+        assert len(diagnostics['output1'].dims) == 1
+        assert 'dim1' in diagnostics['output1'].dims
+        assert 'units' in diagnostics['output1'].attrs
+        assert len(diagnostics['output1'].attrs) == 1
+        assert diagnostics['output1'].attrs['units'] == 'm'
+        assert np.all(diagnostics['output1'].values == np.ones([10]))
+
+    def test_diagnostics_restoring_dims(self):
+        input_properties = {
+            'input1': {
+                'dims': ['*', 'dim1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_properties = {
+            'output1': {
+                'dims': ['*', 'dim1'],
+                'units': 'm'
+            }
+        }
+        diagnostic_output = {
+            'output1': np.ones([1, 10]),
+        }
+        component = self.get_component(
+            input_properties=input_properties,
+            diagnostic_properties=diagnostic_properties,
+            diagnostic_output=diagnostic_output,
+        )
+        state = {
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}),
+            'time': timedelta(0)}
+        diagnostics = self.get_diagnostics(self.call_component(component, state))
+        assert len(diagnostics) == 1
+        assert 'output1' in diagnostics.keys()
+        assert isinstance(diagnostics['output1'], DataArray)
+        assert len(diagnostics['output1'].dims) == 1
+        assert 'dim1' in diagnostics['output1'].dims
+        assert 'units' in diagnostics['output1'].attrs
+        assert diagnostics['output1'].attrs['units'] == 'm'
+        assert np.all(diagnostics['output1'].values == np.ones([10]))
+
+    def test_diagnostics_with_alias(self):
+        diagnostic_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+                'alias': 'out1',
+            }
+        }
+        diagnostic_output = {
+            'out1': np.ones([10]),
+        }
+        component = self.get_component(
+            diagnostic_properties=diagnostic_properties,
+            diagnostic_output=diagnostic_output,
+        )
+        state = {'time': timedelta(0)}
+        diagnostics = self.get_diagnostics(self.call_component(component, state))
+        assert len(diagnostics) == 1
+        assert 'output1' in diagnostics.keys()
+        assert isinstance(diagnostics['output1'], DataArray)
+        assert len(diagnostics['output1'].dims) == 1
+        assert 'dim1' in diagnostics['output1'].dims
+        assert 'units' in diagnostics['output1'].attrs
+        assert diagnostics['output1'].attrs['units'] == 'm'
+        assert np.all(diagnostics['output1'].values == np.ones([10]))
+
+    def test_diagnostics_with_alias_from_input(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+                'alias': 'out1',
+            }
+        }
+        diagnostic_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_output = {
+            'out1': np.ones([10]),
+        }
+        component = self.get_component(
+            input_properties=input_properties,
+            diagnostic_properties=diagnostic_properties,
+            diagnostic_output=diagnostic_output,
+        )
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        diagnostics = self.get_diagnostics(self.call_component(component, state))
+        assert len(diagnostics) == 1
+        assert 'output1' in diagnostics.keys()
+        assert isinstance(diagnostics['output1'], DataArray)
+        assert len(diagnostics['output1'].dims) == 1
+        assert 'dim1' in diagnostics['output1'].dims
+        assert 'units' in diagnostics['output1'].attrs
+        assert diagnostics['output1'].attrs['units'] == 'm'
+        assert np.all(diagnostics['output1'].values == np.ones([10]))
+
+    def test_diagnostics_with_dims_from_input(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_properties = {
+            'output1': {
+                'units': 'm',
+            }
+        }
+        diagnostic_output = {
+            'output1': np.ones([10]),
+        }
+        component = self.get_component(
+            input_properties=input_properties,
+            diagnostic_properties=diagnostic_properties,
+            diagnostic_output=diagnostic_output,
+        )
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        diagnostics = self.get_diagnostics(self.call_component(component, state))
+        assert len(diagnostics) == 1
+        assert 'output1' in diagnostics.keys()
+        assert isinstance(diagnostics['output1'], DataArray)
+        assert len(diagnostics['output1'].dims) == 1
+        assert 'dim1' in diagnostics['output1'].dims
+        assert 'units' in diagnostics['output1'].attrs
+        assert diagnostics['output1'].attrs['units'] == 'm'
+        assert np.all(diagnostics['output1'].values == np.ones([10]))
+
+    def test_raises_when_diagnostic_not_given(self):
+        diagnostic_properties = {
+            'diag1': {
+                'dims': ['dims1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_output = {}
+        diagnostic = self.get_component(
+            diagnostic_properties=diagnostic_properties,
+            diagnostic_output=diagnostic_output
+        )
+        state = {'time': timedelta(0)}
+        with self.assertRaises(ComponentMissingOutputError):
+            self.call_component(diagnostic, state)
+
+    def test_raises_when_extraneous_diagnostic_given(self):
+        diagnostic_properties = {}
+        diagnostic_output = {
+            'diag1': np.zeros([10])
+        }
+        diagnostic = self.get_component(
+            diagnostic_properties=diagnostic_properties,
+            diagnostic_output=diagnostic_output
+        )
+        state = {'time': timedelta(0)}
+        with self.assertRaises(ComponentExtraOutputError):
+            self.call_component(diagnostic, state)
+
+
+class PrognosticTests(unittest.TestCase, InputTestBase):
+
+    component_class = MockTendencyComponent
+
+    def call_component(self, component, state):
+        return component(state)
+
+    def get_component(
+            self, input_properties=None, tendency_properties=None,
+            diagnostic_properties=None, tendency_output=None,
+            diagnostic_output=None):
+        return MockTendencyComponent(
+            input_properties=input_properties or {},
+            tendency_properties=tendency_properties or {},
+            diagnostic_properties=diagnostic_properties or {},
+            tendency_output=tendency_output or {},
+            diagnostic_output=diagnostic_output or {},
+        )
+
+    def get_diagnostics(self, result):
+        return result[1]
+
+    def test_raises_on_tendency_properties_of_wrong_type(self):
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(tendency_properties=({},))
+
+    def test_cannot_use_bad_component(self):
+        component = BadMockTendencyComponent()
+        with self.assertRaises(RuntimeError):
+            self.call_component(component, {'time': timedelta(0)})
+
+    def test_subclass_check(self):
+        class MyPrognostic(object):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def __call__(self):
+                pass
+            def array_call(self):
+                pass
+
+        instance = MyPrognostic()
+        assert isinstance(instance, TendencyComponent)
+
+    def test_tendency_raises_when_units_incompatible_with_input(self):
+        input_properties = {
+            'input1': {'units': 'km', 'dims': ['dim1', 'dim2']}
+        }
+        tendency_properties = {
+            'input1': {'units': 'degK/s', 'dims': ['dim1', 'dim2']}
+        }
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(
+                input_properties=input_properties,
+                tendency_properties=tendency_properties
+            )
+
+    def test_two_components_are_not_instances_of_each_other(self):
+        class MyTendencyComponent1(TendencyComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def array_call(self, state):
+                pass
+
+        class MyTendencyComponent2(TendencyComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def array_call(self, state):
+                pass
+
+        prog1 = MyTendencyComponent1()
+        prog2 = MyTendencyComponent2()
+        assert not isinstance(prog1, MyTendencyComponent2)
+        assert not isinstance(prog2, MyTendencyComponent1)
+
+    def test_ducktype_not_instance_of_subclass(self):
+        class MyPrognostic1(object):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def __init__(self):
+                pass
+            def array_call(self, state):
+                pass
+
+        class MyTendencyComponent2(TendencyComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def array_call(self, state):
+                pass
+
+        prog1 = MyPrognostic1()
+        assert not isinstance(prog1, MyTendencyComponent2)
+
+    def test_empty_prognostic(self):
+        prognostic = self.component_class({}, {}, {}, {}, {})
+        tendencies, diagnostics = self.call_component(
+            prognostic, {'time': timedelta(seconds=0)})
+        assert tendencies == {}
+        assert diagnostics == {}
+        assert len(prognostic.state_given) == 1
+        assert 'time' in prognostic.state_given.keys()
+        assert prognostic.state_given['time'] == timedelta(seconds=0)
+        assert prognostic.times_called == 1
+
+    def test_tendency_requires_dims(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {'tend1': {'units': 'm'}}
+        diagnostic_output = {}
+        tendency_output = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                tendency_properties,
+                diagnostic_output, tendency_output
+            )
+
+    def test_tendency_uses_base_dims(self):
+        input_properties = {'diag1': {'dims': ['dim1'], 'units': 'm'}}
+        diagnostic_properties = {}
+        tendency_properties = {'diag1': {'units': 'm/s'}}
+        diagnostic_output = {}
+        tendency_output = {}
+        self.component_class(
+            input_properties, diagnostic_properties,
+            tendency_properties,
+            diagnostic_output, tendency_output
+        )
+
+    def test_tendency_doesnt_use_base_units(self):
+        input_properties = {'diag1': {'dims': ['dim1'], 'units': 'm'}}
+        diagnostic_properties = {}
+        tendency_properties = {'diag1': {'dims': ['dim1']}}
+        diagnostic_output = {}
+        tendency_output = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                tendency_properties,
+                diagnostic_output, tendency_output
+            )
+
+    def test_tendency_requires_units(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {'tend1': {'dims': ['dim1']}}
+        diagnostic_output = {}
+        tendency_output = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                tendency_properties,
+                diagnostic_output, tendency_output
+            )
+
+    def test_raises_when_tendency_not_given(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'tend1': {
+                'dims': ['dims1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_output = {}
+        tendency_output = {}
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        state = {'time': timedelta(0)}
+        with self.assertRaises(ComponentMissingOutputError):
+            _, _ = self.call_component(prognostic, state)
+
+    def test_cannot_overlap_input_aliases(self):
+        input_properties = {
+            'input1': {'dims': ['dim1'], 'units': 'm', 'alias': 'input'},
+            'input2': {'dims': ['dim1'], 'units': 'm', 'alias': 'input'}
+        }
+        diagnostic_properties = {}
+        tendency_properties = {}
+        diagnostic_output = {}
+        tendency_output = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                tendency_properties,
+                diagnostic_output, tendency_output
+            )
+
+    def test_cannot_overlap_diagnostic_aliases(self):
+        input_properties = {
+        }
+        diagnostic_properties = {
+            'diag1': {'dims': ['dim1'], 'units': 'm', 'alias': 'diag'},
+            'diag2': {'dims': ['dim1'], 'units': 'm', 'alias': 'diag'}
+        }
+        tendency_properties = {}
+        diagnostic_output = {}
+        tendency_output = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                tendency_properties,
+                diagnostic_output, tendency_output
+            )
+
+    def test_cannot_overlap_tendency_aliases(self):
+        input_properties = {
+        }
+        diagnostic_properties = {
+        }
+        tendency_properties = {
+            'tend1': {'dims': ['dim1'], 'units': 'm', 'alias': 'tend'},
+            'tend2': {'dims': ['dim1'], 'units': 'm', 'alias': 'tend'}
+        }
+        diagnostic_output = {}
+        tendency_output = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                tendency_properties,
+                diagnostic_output, tendency_output
+            )
+
+    def test_raises_when_extraneous_tendency_given(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {}
+        diagnostic_output = {}
+        tendency_output = {
+            'tend1': np.zeros([10]),
+        }
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        state = {'time': timedelta(0)}
+        with self.assertRaises(ComponentExtraOutputError):
+            _, _ = self.call_component(prognostic, state)
+
+    def test_raises_when_diagnostic_not_given(self):
+        input_properties = {}
+        diagnostic_properties = {
+            'diag1': {
+                'dims': ['dims1'],
+                'units': 'm',
+            }
+        }
+        tendency_properties = {}
+        diagnostic_output = {}
+        tendency_output = {}
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        state = {'time': timedelta(0)}
+        with self.assertRaises(ComponentMissingOutputError):
+            _, _ = self.call_component(prognostic, state)
+
+    def test_raises_when_extraneous_diagnostic_given(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {}
+        diagnostic_output = {
+            'diag1': np.zeros([10])
+        }
+        tendency_output = {}
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        state = {'time': timedelta(0)}
+        with self.assertRaises(ComponentExtraOutputError):
+            _, _ = self.call_component(prognostic, state)
+
+    def test_tendencies_no_transformations(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            }}
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]),
+        }
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        state = {'time': timedelta(0)}
+        tendencies, _ = self.call_component(prognostic, state)
+        assert len(tendencies) == 1
+        assert 'output1' in tendencies.keys()
+        assert isinstance(tendencies['output1'], DataArray)
+        assert len(tendencies['output1'].dims) == 1
+        assert 'dim1' in tendencies['output1'].dims
+        assert 'units' in tendencies['output1'].attrs
+        assert len(tendencies['output1'].attrs) == 1
+        assert tendencies['output1'].attrs['units'] == 'm/s'
+        assert np.all(tendencies['output1'].values == np.ones([10]))
+
+    def test_tendencies_with_alias(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s',
+                'alias': 'out1',
+            }}
+        diagnostic_output = {}
+        tendency_output = {
+            'out1': np.ones([10]),
+        }
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        state = {'time': timedelta(0)}
+        tendencies, _ = self.call_component(prognostic, state)
+        assert len(tendencies) == 1
+        assert 'output1' in tendencies.keys()
+        assert isinstance(tendencies['output1'], DataArray)
+        assert len(tendencies['output1'].dims) == 1
+        assert 'dim1' in tendencies['output1'].dims
+        assert 'units' in tendencies['output1'].attrs
+        assert tendencies['output1'].attrs['units'] == 'm/s'
+        assert np.all(tendencies['output1'].values == np.ones([10]))
+
+    def test_tendencies_with_alias_from_input(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+                'alias': 'out1',
+            }
+        }
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s',
+            }
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'out1': np.ones([10]),
+        }
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        tendencies, _ = self.call_component(prognostic, state)
+        assert len(tendencies) == 1
+        assert 'output1' in tendencies.keys()
+        assert isinstance(tendencies['output1'], DataArray)
+        assert len(tendencies['output1'].dims) == 1
+        assert 'dim1' in tendencies['output1'].dims
+        assert 'units' in tendencies['output1'].attrs
+        assert tendencies['output1'].attrs['units'] == 'm/s'
+        assert np.all(tendencies['output1'].values == np.ones([10]))
+
+    def test_tendencies_with_dims_from_input(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'units': 'm/s',
+            }
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]),
+        }
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        tendencies, _ = self.call_component(prognostic, state)
+        assert len(tendencies) == 1
+        assert 'output1' in tendencies.keys()
+        assert isinstance(tendencies['output1'], DataArray)
+        assert len(tendencies['output1'].dims) == 1
+        assert 'dim1' in tendencies['output1'].dims
+        assert 'units' in tendencies['output1'].attrs
+        assert tendencies['output1'].attrs['units'] == 'm/s'
+        assert np.all(tendencies['output1'].values == np.ones([10]))
+
+    def test_tendencies_in_diagnostics_no_tendency(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {}
+        diagnostic_output = {}
+        tendency_output = {}
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, tendencies_in_diagnostics=True
+        )
+        assert prognostic.input_properties == {}
+        assert prognostic.diagnostic_properties == {}
+        assert prognostic.tendency_properties == {}
+        state = {'time': timedelta(0)}
+        _, diagnostics = self.call_component(prognostic, state)
+        assert diagnostics == {}
+
+    def test_tendencies_in_diagnostics_one_tendency(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            }
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 20.,
+        }
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, tendencies_in_diagnostics=True,
+        )
+        tendency_name = 'output1_tendency_from_{}'.format(prognostic.__class__.__name__)
+        assert len(prognostic.diagnostic_properties) == 1
+        assert tendency_name in prognostic.diagnostic_properties.keys()
+        properties = prognostic.diagnostic_properties[tendency_name]
+        assert properties['dims'] == ['dim1']
+        assert properties['units'] == 'm/s'
+        state = {
+            'time': timedelta(0),
+        }
+        _, diagnostics = self.call_component(prognostic, state)
+        assert tendency_name in diagnostics.keys()
+        assert len(
+            diagnostics[tendency_name].dims) == 1
+        assert 'dim1' in diagnostics[tendency_name].dims
+        assert diagnostics[tendency_name].attrs['units'] == 'm/s'
+        assert np.all(diagnostics[tendency_name].values == 20.)
+
+    def test_tendencies_in_diagnostics_one_tendency_dims_from_input(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'units': 'm/s'
+            }
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 20.,
+        }
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, tendencies_in_diagnostics=True,
+        )
+        tendency_name = 'output1_tendency_from_{}'.format(prognostic.__class__.__name__)
+        assert len(prognostic.diagnostic_properties) == 1
+        assert tendency_name in prognostic.diagnostic_properties.keys()
+        properties = prognostic.diagnostic_properties[tendency_name]
+        assert properties['dims'] == ['dim1']
+        assert properties['units'] == 'm/s'
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}),
+        }
+        _, diagnostics = self.call_component(prognostic, state)
+        assert tendency_name in diagnostics.keys()
+        assert len(
+            diagnostics[tendency_name].dims) == 1
+        assert 'dim1' in diagnostics[tendency_name].dims
+        assert diagnostics[tendency_name].attrs['units'] == 'm/s'
+        assert np.all(diagnostics[tendency_name].values == 20.)
+
+    def test_tendencies_in_diagnostics_one_tendency_with_component_name(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            }
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 20.,
+        }
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, tendencies_in_diagnostics=True,
+            name='component',
+        )
+        tendency_name = 'output1_tendency_from_component'
+        assert len(prognostic.diagnostic_properties) == 1
+        assert tendency_name in prognostic.diagnostic_properties.keys()
+        properties = prognostic.diagnostic_properties[tendency_name]
+        assert properties['dims'] == ['dim1']
+        assert properties['units'] == 'm/s'
+        state = {
+            'time': timedelta(0),
+        }
+        _, diagnostics = self.call_component(prognostic, state)
+        print(diagnostics.keys())
+        assert tendency_name in diagnostics.keys()
+        assert len(
+            diagnostics[tendency_name].dims) == 1
+        assert 'dim1' in diagnostics[tendency_name].dims
+        assert diagnostics[tendency_name].attrs['units'] == 'm/s'
+        assert np.all(diagnostics[tendency_name].values == 20.)
+
+
+class ImplicitPrognosticTests(PrognosticTests):
+
+    component_class = MockImplicitTendencyComponent
+
+    def call_component(self, component, state):
+        return component(state, timedelta(seconds=1))
+
+    def get_component(
+            self, input_properties=None, tendency_properties=None,
+            diagnostic_properties=None, tendency_output=None,
+            diagnostic_output=None):
+        return MockImplicitTendencyComponent(
+            input_properties=input_properties or {},
+            tendency_properties=tendency_properties or {},
+            diagnostic_properties=diagnostic_properties or {},
+            tendency_output=tendency_output or {},
+            diagnostic_output=diagnostic_output or {},
+        )
+
+    def test_cannot_use_bad_component(self):
+        component = BadMockImplicitTendencyComponent()
+        with self.assertRaises(RuntimeError):
+            self.call_component(component, {'time': timedelta(0)})
+
+    def test_subclass_check(self):
+        class MyImplicitPrognostic(object):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def __call__(self, state, timestep):
+                pass
+            def array_call(self, state, timestep):
+                pass
+
+        instance = MyImplicitPrognostic()
+        assert isinstance(instance, ImplicitTendencyComponent)
+
+    def test_two_components_are_not_instances_of_each_other(self):
+        class MyImplicitTendencyComponent1(ImplicitTendencyComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+
+            def array_call(self, state, timestep):
+                pass
+
+        class MyImplicitTendencyComponent2(ImplicitTendencyComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+
+            def array_call(self, state):
+                pass
+
+        prog1 = MyImplicitTendencyComponent1()
+        prog2 = MyImplicitTendencyComponent2()
+        assert not isinstance(prog1, MyImplicitTendencyComponent2)
+        assert not isinstance(prog2, MyImplicitTendencyComponent1)
+
+    def test_ducktype_not_instance_of_subclass(self):
+        class MyImplicitPrognostic1(object):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def __init__(self):
+                pass
+            def array_call(self, state, timestep):
+                pass
+
+        class MyImplicitTendencyComponent2(ImplicitTendencyComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+
+            def array_call(self, state):
+                pass
+
+        prog1 = MyImplicitPrognostic1()
+        assert not isinstance(prog1, MyImplicitTendencyComponent2)
+
+    def test_subclass_is_not_prognostic(self):
+        class MyImplicitTendencyComponent(ImplicitTendencyComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def array_call(self, state, timestep):
+                pass
+
+        instance = MyImplicitTendencyComponent()
+        assert not isinstance(instance, TendencyComponent)
+
+    def test_ducktype_is_not_prognostic(self):
+        class MyImplicitPrognostic(object):
+            input_properties = {}
+            diagnostic_properties = {}
+            tendency_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def __call__(self, state, timestep):
+                pass
+            def array_call(self, state, timestep):
+                pass
+
+        instance = MyImplicitPrognostic()
+        assert not isinstance(instance, TendencyComponent)
+
+    def test_timedelta_is_passed(self):
+        prognostic = MockImplicitTendencyComponent({}, {}, {}, {}, {})
+        tendencies, diagnostics = prognostic(
+            {'time': timedelta(seconds=0)}, timedelta(seconds=5))
+        assert tendencies == {}
+        assert diagnostics == {}
+        assert prognostic.timestep_given == timedelta(seconds=5)
+        assert prognostic.times_called == 1
+
+
+class DiagnosticTests(unittest.TestCase, InputTestBase, DiagnosticTestBase):
+
+    component_class = MockDiagnosticComponent
+
+    def call_component(self, component, state):
+        return component(state)
+
+    def get_component(
+            self, input_properties=None,
+            diagnostic_properties=None,
+            diagnostic_output=None):
+        return MockDiagnosticComponent(
+            input_properties=input_properties or {},
+            diagnostic_properties=diagnostic_properties or {},
+            diagnostic_output=diagnostic_output or {},
+        )
+
+    def get_diagnostics(self, result):
+        return result
+
+    def test_cannot_use_bad_component(self):
+        component = BadMockDiagnosticComponent()
+        with self.assertRaises(RuntimeError):
+            self.call_component(component, {'time': timedelta(0)})
+
+    def test_subclass_check(self):
+        class MyDiagnostic(object):
+            input_properties = {}
+            diagnostic_properties = {}
+            def __call__(self, state):
+                pass
+            def array_call(self, state):
+                pass
+
+        instance = MyDiagnostic()
+        assert isinstance(instance, DiagnosticComponent)
+
+    def test_two_components_are_not_instances_of_each_other(self):
+        class MyDiagnosticComponent1(DiagnosticComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+
+            def array_call(self, state):
+                pass
+
+        class MyDiagnosticComponent2(DiagnosticComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+
+            def array_call(self, state):
+                pass
+
+        diag1 = MyDiagnosticComponent1()
+        diag2 = MyDiagnosticComponent2()
+        assert not isinstance(diag1, MyDiagnosticComponent2)
+        assert not isinstance(diag2, MyDiagnosticComponent1)
+
+    def test_ducktype_not_instance_of_subclass(self):
+        class MyDiagnostic1(object):
+            input_properties = {}
+            diagnostic_properties = {}
+            def __init__(self):
+                pass
+            def array_call(self, state):
+                pass
+
+        class MyDiagnosticComponent2(DiagnosticComponent):
+            input_properties = {}
+            diagnostic_properties = {}
+
+            def array_call(self, state):
+                pass
+
+        diag1 = MyDiagnostic1()
+        assert not isinstance(diag1, MyDiagnosticComponent2)
+
+    def test_empty_diagnostic(self):
+        diagnostic = self.component_class({}, {}, {})
+        diagnostics = diagnostic({'time': timedelta(seconds=0)})
+        assert diagnostics == {}
+        assert len(diagnostic.state_given) == 1
+        assert 'time' in diagnostic.state_given.keys()
+        assert diagnostic.state_given['time'] == timedelta(seconds=0)
+        assert diagnostic.times_called == 1
+
+
+class ImplicitTests(unittest.TestCase, InputTestBase, DiagnosticTestBase):
+
+    component_class = MockStepper
+
+    def call_component(self, component, state):
+        return component(state, timedelta(seconds=1))
+
+    def get_component(
+            self, input_properties=None, output_properties=None,
+            diagnostic_properties=None, state_output=None,
+            diagnostic_output=None):
+        return MockStepper(
+            input_properties=input_properties or {},
+            output_properties=output_properties or {},
+            diagnostic_properties=diagnostic_properties or {},
+            state_output=state_output or {},
+            diagnostic_output=diagnostic_output or {},
+        )
+
+    def get_diagnostics(self, result):
+        return result[0]
+
+    def test_raises_on_output_properties_of_wrong_type(self):
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(output_properties=({},))
+
+    def test_cannot_use_bad_component(self):
+        component = BadMockStepper()
+        with self.assertRaises(RuntimeError):
+            self.call_component(component, {'time': timedelta(0)})
+
+    def test_subclass_check(self):
+        class MyImplicit(object):
+            input_properties = {}
+            diagnostic_properties = {}
+            output_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def __call__(self, state, timestep):
+                pass
+            def array_call(self, state, timestep):
+                pass
+
+        instance = MyImplicit()
+        assert isinstance(instance, Stepper)
+
+    def test_output_raises_when_units_incompatible_with_input(self):
+        input_properties = {
+            'input1': {'units': 'km', 'dims': ['dim1', 'dim2']}
+        }
+        output_properties = {
+            'input1': {'units': 'degK', 'dims': ['dim1', 'dim2']}
+        }
+        with self.assertRaises(InvalidPropertyDictError):
+            self.get_component(
+                input_properties=input_properties,
+                output_properties=output_properties,
+            )
+
+    def test_two_components_are_not_instances_of_each_other(self):
+        class MyStepper1(Stepper):
+            input_properties = {}
+            diagnostic_properties = {}
+            output_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+
+            def array_call(self, state):
+                pass
+
+        class MyStepper2(Stepper):
+            input_properties = {}
+            diagnostic_properties = {}
+            output_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+
+            def array_call(self, state):
+                pass
+
+        implicit1 = MyStepper1()
+        implicit2 = MyStepper2()
+        assert not isinstance(implicit1, MyStepper2)
+        assert not isinstance(implicit2, MyStepper1)
+
+    def test_ducktype_not_instance_of_subclass(self):
+        class MyImplicit1(object):
+            input_properties = {}
+            diagnostic_properties = {}
+            output_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+            def __init__(self):
+                pass
+            def array_call(self, state):
+                pass
+
+        class MyStepper2(Stepper):
+            input_properties = {}
+            diagnostic_properties = {}
+            output_properties = {}
+            tendencies_in_diagnostics = False
+            name = ''
+
+            def array_call(self, state):
+                pass
+
+        implicit1 = MyImplicit1()
+        assert not isinstance(implicit1, MyStepper2)
+
+    def test_empty_implicit(self):
+        implicit = self.component_class(
+            {}, {}, {}, {}, {})
+        tendencies, diagnostics = self.call_component(
+            implicit, {'time': timedelta(seconds=0)})
+        assert tendencies == {}
+        assert diagnostics == {}
+        assert len(implicit.state_given) == 1
+        assert 'time' in implicit.state_given.keys()
+        assert implicit.state_given['time'] == timedelta(seconds=0)
+        assert implicit.times_called == 1
+
+    def test_output_requires_dims(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        output_properties = {'diag1': {'units': 'm'}}
+        diagnostic_output = {}
+        state_output = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                output_properties,
+                diagnostic_output, state_output
+            )
+
+    def test_output_uses_base_dims(self):
+        input_properties = {'diag1': {'dims': ['dim1'], 'units': 'm'}}
+        diagnostic_properties = {}
+        output_properties = {'diag1': {'units': 'm'}}
+        diagnostic_output = {}
+        state_output = {}
+        self.component_class(
+            input_properties, diagnostic_properties,
+            output_properties,
+            diagnostic_output, state_output
+        )
+
+    def test_output_doesnt_use_base_units(self):
+        input_properties = {'diag1': {'dims': ['dim1'], 'units': 'm'}}
+        diagnostic_properties = {}
+        output_properties = {'diag1': {'dims': ['dim1']}}
+        diagnostic_output = {}
+        state_output = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                output_properties,
+                diagnostic_output, state_output
+            )
+
+    def test_output_requires_units(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        output_properties = {'output1': {'dims': ['dim1']}}
+        diagnostic_output = {}
+        state_output = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                output_properties,
+                diagnostic_output, state_output
+            )
+
+    def test_cannot_overlap_output_aliases(self):
+        input_properties = {
+        }
+        diagnostic_properties = {
+        }
+        output_properties = {
+            'out1': {'dims': ['dim1'], 'units': 'm', 'alias': 'out'},
+            'out2': {'dims': ['dim1'], 'units': 'm', 'alias': 'out'}
+        }
+        diagnostic_output = {}
+        output_state = {}
+        with self.assertRaises(InvalidPropertyDictError):
+            self.component_class(
+                input_properties, diagnostic_properties,
+                output_properties,
+                diagnostic_output, output_state
+            )
+
+    def test_timedelta_is_passed(self):
+        implicit = MockStepper({}, {}, {}, {}, {})
+        tendencies, diagnostics = implicit(
+            {'time': timedelta(seconds=0)}, timedelta(seconds=5))
+        assert tendencies == {}
+        assert diagnostics == {}
+        assert implicit.timestep_given == timedelta(seconds=5)
+        assert implicit.times_called == 1
+
+    def test_raises_when_output_not_given(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'dims': ['dims1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_output = {}
+        state_output = {}
+        implicit = self.component_class(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, state_output
+        )
+        state = {'time': timedelta(0)}
+        with self.assertRaises(ComponentMissingOutputError):
+            _, _ = self.call_component(implicit, state)
+
+    def test_raises_when_extraneous_output_given(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        output_properties = {}
+        diagnostic_output = {}
+        state_output = {
+            'tend1': np.zeros([10]),
+        }
+        implicit = self.component_class(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, state_output
+        )
+        state = {'time': timedelta(0)}
+        with self.assertRaises(ComponentExtraOutputError):
+            _, _ = self.call_component(implicit, state)
+
+    def test_output_no_transformations(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            }}
+        diagnostic_output = {}
+        output_state = {
+            'output1': np.ones([10]),
+        }
+        prognostic = self.component_class(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, output_state
+        )
+        state = {'time': timedelta(0)}
+        _, output = self.call_component(prognostic, state)
+        assert len(output) == 1
+        assert 'output1' in output.keys()
+        assert isinstance(output['output1'], DataArray)
+        assert len(output['output1'].dims) == 1
+        assert 'dim1' in output['output1'].dims
+        assert 'units' in output['output1'].attrs
+        assert output['output1'].attrs['units'] == 'm/s'
+        assert np.all(output['output1'].values == np.ones([10]))
+
+    def test_output_with_alias(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s',
+                'alias': 'out1',
+            }}
+        diagnostic_output = {}
+        output_state = {
+            'out1': np.ones([10]),
+        }
+        implicit = self.component_class(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, output_state
+        )
+        state = {'time': timedelta(0)}
+        _, output = self.call_component(implicit, state)
+        assert len(output) == 1
+        assert 'output1' in output.keys()
+        assert isinstance(output['output1'], DataArray)
+        assert len(output['output1'].dims) == 1
+        assert 'dim1' in output['output1'].dims
+        assert 'units' in output['output1'].attrs
+        assert output['output1'].attrs['units'] == 'm/s'
+        assert np.all(output['output1'].values == np.ones([10]))
+
+    def test_output_with_alias_from_input(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+                'alias': 'out1',
+            }
+        }
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_output = {}
+        output_state = {
+            'out1': np.ones([10]),
+        }
+        implicit = self.component_class(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, output_state
+        )
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        _, output = self.call_component(implicit, state)
+        assert len(output) == 1
+        assert 'output1' in output.keys()
+        assert isinstance(output['output1'], DataArray)
+        assert len(output['output1'].dims) == 1
+        assert 'dim1' in output['output1'].dims
+        assert 'units' in output['output1'].attrs
+        assert output['output1'].attrs['units'] == 'm'
+        assert np.all(output['output1'].values == np.ones([10]))
+
+    def test_output_with_dims_from_input(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'units': 'm',
+            }
+        }
+        diagnostic_output = {}
+        output_state = {
+            'output1': np.ones([10]),
+        }
+        implicit = self.component_class(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, output_state
+        )
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        _, output = self.call_component(implicit, state)
+        assert len(output) == 1
+        assert 'output1' in output.keys()
+        assert isinstance(output['output1'], DataArray)
+        assert len(output['output1'].dims) == 1
+        assert 'dim1' in output['output1'].dims
+        assert 'units' in output['output1'].attrs
+        assert output['output1'].attrs['units'] == 'm'
+        assert np.all(output['output1'].values == np.ones([10]))
+
+    def test_tendencies_in_diagnostics_no_tendency(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        output_properties = {}
+        diagnostic_output = {}
+        output_state = {}
+        implicit = MockStepper(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, output_state, tendencies_in_diagnostics=True
+        )
+        assert implicit.input_properties == {}
+        assert implicit.diagnostic_properties == {}
+        assert implicit.output_properties == {}
+        state = {'time': timedelta(0)}
+        diagnostics, _ = implicit(state, timedelta(seconds=5))
+        assert diagnostics == {}
+
+    def test_tendencies_in_diagnostics_one_tendency(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        diagnostic_output = {}
+        output_state = {
+            'output1': np.ones([10]) * 20.,
+        }
+        implicit = MockStepper(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, output_state, tendencies_in_diagnostics=True,
+        )
+        assert len(implicit.diagnostic_properties) == 1
+        assert 'output1_tendency_from_MockStepper' in implicit.diagnostic_properties.keys()
+        assert 'output1' in input_properties.keys(), 'Stepper needs original value to calculate tendency'
+        assert input_properties['output1']['dims'] == ['dim1']
+        assert input_properties['output1']['units'] == 'm'
+        properties = implicit.diagnostic_properties[
+            'output1_tendency_from_MockStepper']
+        assert properties['dims'] == ['dim1']
+        assert properties['units'] == 'm s^-1'
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10])*10.,
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+        }
+        diagnostics, _ = implicit(state, timedelta(seconds=5))
+        assert 'output1_tendency_from_MockStepper' in diagnostics.keys()
+        assert len(
+            diagnostics['output1_tendency_from_MockStepper'].dims) == 1
+        assert 'dim1' in diagnostics['output1_tendency_from_MockStepper'].dims
+        assert diagnostics['output1_tendency_from_MockStepper'].attrs['units'] == 'm s^-1'
+        assert np.all(
+            diagnostics['output1_tendency_from_MockStepper'].values == 2.)
+
+    def test_tendencies_in_diagnostics_one_tendency_dims_from_input(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'units': 'm'
+            }
+        }
+        diagnostic_output = {}
+        output_state = {
+            'output1': np.ones([10]) * 20.,
+        }
+        implicit = MockStepper(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, output_state, tendencies_in_diagnostics=True,
+        )
+        assert len(implicit.diagnostic_properties) == 1
+        assert 'output1_tendency_from_MockStepper' in implicit.diagnostic_properties.keys()
+        assert 'output1' in input_properties.keys(), 'Stepper needs original value to calculate tendency'
+        assert input_properties['output1']['dims'] == ['dim1']
+        assert input_properties['output1']['units'] == 'm'
+        properties = implicit.diagnostic_properties[
+            'output1_tendency_from_MockStepper']
+        assert properties['dims'] == ['dim1']
+        assert properties['units'] == 'm s^-1'
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10])*10.,
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+        }
+        diagnostics, _ = implicit(state, timedelta(seconds=5))
+        assert 'output1_tendency_from_MockStepper' in diagnostics.keys()
+        assert len(
+            diagnostics['output1_tendency_from_MockStepper'].dims) == 1
+        assert 'dim1' in diagnostics['output1_tendency_from_MockStepper'].dims
+        assert diagnostics['output1_tendency_from_MockStepper'].attrs['units'] == 'm s^-1'
+        assert np.all(
+            diagnostics['output1_tendency_from_MockStepper'].values == 2.)
+
+    def test_tendencies_in_diagnostics_one_tendency_mismatched_units(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'km'
+            }
+        }
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        diagnostic_output = {}
+        output_state = {
+            'output1': np.ones([10]) * 20.,
+        }
+        with self.assertRaises(InvalidPropertyDictError):
+            implicit = MockStepper(
+                input_properties, diagnostic_properties, output_properties,
+                diagnostic_output, output_state, tendencies_in_diagnostics=True,
+            )
+
+    def test_tendencies_in_diagnostics_one_tendency_mismatched_dims(self):
+        input_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'dims': ['dim2'],
+                'units': 'm'
+            }
+        }
+        diagnostic_output = {}
+        output_state = {
+            'output1': np.ones([10]) * 20.,
+        }
+        with self.assertRaises(InvalidPropertyDictError):
+            implicit = MockStepper(
+                input_properties, diagnostic_properties, output_properties,
+                diagnostic_output, output_state, tendencies_in_diagnostics=True,
+            )
+
+    def test_tendencies_in_diagnostics_one_tendency_with_component_name(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        output_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        diagnostic_output = {}
+        output_state = {
+            'output1': np.ones([10]) * 7.,
+        }
+        implicit = MockStepper(
+            input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, output_state, tendencies_in_diagnostics=True,
+            name='component'
+        )
+        assert len(implicit.diagnostic_properties) == 1
+        assert 'output1_tendency_from_component' in implicit.diagnostic_properties.keys()
+        properties = implicit.diagnostic_properties[
+            'output1_tendency_from_component']
+        assert properties['dims'] == ['dim1']
+        assert properties['units'] == 'm s^-1'
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10]) * 2.,
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+        }
+        diagnostics, _ = implicit(state, timedelta(seconds=5))
+        assert 'output1_tendency_from_component' in diagnostics.keys()
+        assert len(diagnostics['output1_tendency_from_component'].dims) == 1
+        assert 'dim1' in diagnostics['output1_tendency_from_component'].dims
+        assert diagnostics['output1_tendency_from_component'].attrs['units'] == 'm s^-1'
+        assert np.all(diagnostics['output1_tendency_from_component'].values == 1.)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_components.py b/tests/test_components.py
index b33855d..7267cf4 100644
--- a/tests/test_components.py
+++ b/tests/test_components.py
@@ -1,13 +1,14 @@
 import pytest
 from sympl import (
-    ConstantPrognostic, ConstantDiagnostic, RelaxationPrognostic, DataArray
+    ConstantTendencyComponent, ConstantDiagnosticComponent, RelaxationTendencyComponent, DataArray,
+    timedelta,
 )
 import numpy as np
 
 
 def test_constant_prognostic_empty_dicts():
-    prog = ConstantPrognostic({}, {})
-    tendencies, diagnostics = prog({})
+    prog = ConstantTendencyComponent({}, {})
+    tendencies, diagnostics = prog({'time': timedelta(0)})
     assert isinstance(tendencies, dict)
     assert isinstance(diagnostics, dict)
     assert len(tendencies) == 0
@@ -17,51 +18,136 @@ def test_constant_prognostic_empty_dicts():
 def test_constant_prognostic_cannot_modify_through_input_dict():
     in_tendencies = {}
     in_diagnostics = {}
-    prog = ConstantPrognostic(in_tendencies, in_diagnostics)
+    prog = ConstantTendencyComponent(in_tendencies, in_diagnostics)
     in_tendencies['a'] = 'b'
     in_diagnostics['c'] = 'd'
-    tendencies, diagnostics = prog({})
+    tendencies, diagnostics = prog({'time': timedelta(0)})
     assert len(tendencies) == 0
     assert len(diagnostics) == 0
 
 
 def test_constant_prognostic_cannot_modify_through_output_dict():
-    prog = ConstantPrognostic({}, {})
-    tendencies, diagnostics = prog({})
+    prog = ConstantTendencyComponent({}, {})
+    tendencies, diagnostics = prog({'time': timedelta(0)})
     tendencies['a'] = 'b'
     diagnostics['c'] = 'd'
-    tendencies, diagnostics = prog({})
+    tendencies, diagnostics = prog({'time': timedelta(0)})
     assert len(tendencies) == 0
     assert len(diagnostics) == 0
 
 
+def test_constant_prognostic_tendency_properties():
+    tendencies = {
+        'tend1': DataArray(
+            np.zeros([10]),
+            dims=['dim1'],
+            attrs={'units': 'm/s'},
+        ),
+        'tend2': DataArray(
+            np.zeros([2, 2]),
+            dims=['dim2', 'dim3'],
+            attrs={'units': 'degK/s'},
+        )
+    }
+    prog = ConstantTendencyComponent(tendencies)
+    assert prog.tendency_properties == {
+        'tend1': {
+            'dims': ('dim1',),
+            'units': 'm/s',
+        },
+        'tend2': {
+            'dims': ('dim2', 'dim3'),
+            'units': 'degK/s'
+        }
+    }
+    assert prog.diagnostic_properties == {}
+    assert prog.input_properties == {}
+
+
+def test_constant_prognostic_diagnostic_properties():
+    tendencies = {}
+    diagnostics = {
+        'diag1': DataArray(
+            np.zeros([10]),
+            dims=['dim1'],
+            attrs={'units': 'm'},
+        ),
+        'diag2': DataArray(
+            np.zeros([2, 2]),
+            dims=['dim2', 'dim3'],
+            attrs={'units': 'degK'},
+        )
+    }
+    prog = ConstantTendencyComponent(tendencies, diagnostics)
+    assert prog.diagnostic_properties == {
+        'diag1': {
+            'dims': ('dim1',),
+            'units': 'm',
+        },
+        'diag2': {
+            'dims': ('dim2', 'dim3'),
+            'units': 'degK',
+        }
+    }
+    assert prog.tendency_properties == {}
+    assert prog.input_properties == {}
+
+
 def test_constant_diagnostic_empty_dict():
-    diag = ConstantDiagnostic({})
-    diagnostics = diag({})
+    diag = ConstantDiagnosticComponent({})
+    diagnostics = diag({'time': timedelta(0)})
     assert isinstance(diagnostics, dict)
     assert len(diagnostics) == 0
 
 
 def test_constant_diagnostic_cannot_modify_through_input_dict():
     in_diagnostics = {}
-    diag = ConstantDiagnostic(in_diagnostics)
+    diag = ConstantDiagnosticComponent(in_diagnostics)
     in_diagnostics['a'] = 'b'
-    diagnostics = diag({})
+    diagnostics = diag({'time': timedelta(0)})
     assert isinstance(diagnostics, dict)
     assert len(diagnostics) == 0
 
 
 def test_constant_diagnostic_cannot_modify_through_output_dict():
-    diag = ConstantDiagnostic({})
-    diagnostics = diag({})
+    diag = ConstantDiagnosticComponent({})
+    diagnostics = diag({'time': timedelta(0)})
     diagnostics['c'] = 'd'
-    diagnostics = diag({})
+    diagnostics = diag({'time': timedelta(0)})
     assert len(diagnostics) == 0
 
 
+def test_constant_diagnostic_diagnostic_properties():
+    diagnostics = {
+        'diag1': DataArray(
+            np.zeros([10]),
+            dims=['dim1'],
+            attrs={'units': 'm'},
+        ),
+        'diag2': DataArray(
+            np.zeros([2, 2]),
+            dims=['dim2', 'dim3'],
+            attrs={'units': 'degK'},
+        )
+    }
+    diagnostic = ConstantDiagnosticComponent(diagnostics)
+    assert diagnostic.diagnostic_properties == {
+        'diag1': {
+            'dims': ('dim1',),
+            'units': 'm',
+        },
+        'diag2': {
+            'dims': ('dim2', 'dim3'),
+            'units': 'degK',
+        }
+    }
+    assert diagnostic.input_properties == {}
+
+
 def test_relaxation_prognostic_at_equilibrium():
-    prognostic = RelaxationPrognostic('quantity')
+    prognostic = RelaxationTendencyComponent('quantity', 'degK')
     state = {
+        'time': timedelta(0),
         'quantity': DataArray(np.array([0., 1., 2.]), attrs={'units': 'degK'}),
         'quantity_relaxation_timescale': DataArray(
             np.array([1., 1., 1.]), attrs={'units': 's'}),
@@ -73,8 +159,9 @@ def test_relaxation_prognostic_at_equilibrium():
 
 
 def test_relaxation_prognostic_with_change():
-    prognostic = RelaxationPrognostic('quantity')
+    prognostic = RelaxationTendencyComponent('quantity', 'degK')
     state = {
+        'time': timedelta(0),
         'quantity': DataArray(np.array([0., 1., 2.]), attrs={'units': 'degK'}),
         'quantity_relaxation_timescale': DataArray(
             np.array([1., 1., 1.]), attrs={'units': 's'}),
@@ -86,8 +173,9 @@ def test_relaxation_prognostic_with_change():
 
 
 def test_relaxation_prognostic_with_change_different_timescale_units():
-    prognostic = RelaxationPrognostic('quantity')
+    prognostic = RelaxationTendencyComponent('quantity', 'degK')
     state = {
+        'time': timedelta(0),
         'quantity': DataArray(np.array([0., 1., 2.]), attrs={'units': 'degK'}),
         'quantity_relaxation_timescale': DataArray(
             np.array([1/60., 2/60., 3/60.]), attrs={'units': 'minutes'}),
@@ -99,8 +187,9 @@ def test_relaxation_prognostic_with_change_different_timescale_units():
 
 
 def test_relaxation_prognostic_with_change_different_equilibrium_units():
-    prognostic = RelaxationPrognostic('quantity')
+    prognostic = RelaxationTendencyComponent('quantity', 'm')
     state = {
+        'time': timedelta(0),
         'quantity': DataArray(np.array([0., 1., 2.]), attrs={'units': 'm'}),
         'quantity_relaxation_timescale': DataArray(
             np.array([1., 2., 3.]), attrs={'units': 's'}),
@@ -111,37 +200,5 @@ def test_relaxation_prognostic_with_change_different_equilibrium_units():
     assert np.all(tendencies['quantity'].values == np.array([1., 1., 1.]))
 
 
-def test_relaxation_prognostic_caching_timescale_and_equilibrium():
-    prognostic = RelaxationPrognostic(
-        'quantity',
-        relaxation_timescale=DataArray(
-            np.array([1., 1., 1.]), attrs={'units': 's'}),
-        equilibrium_value=DataArray(
-            np.array([1., 3., 5.]), attrs={'units': 'degK'}))
-    state = {
-        'quantity': DataArray(np.array([0., 1., 2.]), attrs={'units': 'degK'}),
-    }
-    tendencies, diagnostics = prognostic(state)
-    assert np.all(tendencies['quantity'].values == np.array([1., 2., 3.]))
-
-
-def test_relaxation_prognostic_cannot_override_cached_values():
-    prognostic = RelaxationPrognostic(
-        'quantity',
-        relaxation_timescale=DataArray(
-            np.array([1., 1., 1.]), attrs={'units': 's'}),
-        equilibrium_value=DataArray(
-            np.array([1., 3., 5.]), attrs={'units': 'degK'}))
-    state = {
-        'quantity': DataArray(np.array([0., 1., 2.]), attrs={'units': 'degK'}),
-        # random values which should not be used by the prognostic
-        'quantity_relaxation_timescale': DataArray(
-            np.array([5., 1., 18.]), attrs={'units': 's'}),
-        'equilibrium_quantity': DataArray(
-            np.array([4., 7., 2.])*1e-3, attrs={'units': 'km'}),
-    }
-    tendencies, diagnostics = prognostic(state)
-    assert np.all(tendencies['quantity'].values == np.array([1., 2., 3.]))
-
 if __name__ == '__main__':
     pytest.main([__file__])
diff --git a/tests/test_composite.py b/tests/test_composite.py
new file mode 100644
index 0000000..04658f7
--- /dev/null
+++ b/tests/test_composite.py
@@ -0,0 +1,1329 @@
+import pytest
+import unittest
+import mock
+from sympl import (
+    TendencyComponent, DiagnosticComponent, Monitor, TendencyComponentComposite, DiagnosticComponentComposite,
+    MonitorComposite, SharedKeyError, DataArray, InvalidPropertyDictError
+)
+from sympl._core.units import units_are_compatible
+
+
+def same_list(list1, list2):
+    return (len(list1) == len(list2) and all(
+        [item in list2 for item in list1] + [item in list1 for item in list2]))
+
+
+class MockTendencyComponent(TendencyComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.tendency_properties = tendency_properties
+        self._diagnostic_output = diagnostic_output
+        self._tendency_output = tendency_output
+        self.times_called = 0
+        self.state_given = None
+        super(MockTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return self._tendency_output, self._diagnostic_output
+
+
+class MockDiagnosticComponent(DiagnosticComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, diagnostic_output,
+            **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self._diagnostic_output = diagnostic_output
+        self.times_called = 0
+        self.state_given = None
+        super(MockDiagnosticComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return self._diagnostic_output
+
+
+class MockEmptyTendencyComponent(TendencyComponent):
+
+    input_properties = {}
+    diagnostic_properties = {}
+    tendency_properties = {}
+
+    def __init__(self, **kwargs):
+        super(MockEmptyTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        return {}, {}
+
+
+class MockEmptyTendencyComponent2(TendencyComponent):
+
+    input_properties = {}
+    diagnostic_properties = {}
+    tendency_properties = {}
+
+    def __init__(self, **kwargs):
+        super(MockEmptyTendencyComponent2, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        return {}, {}
+
+
+class MockEmptyDiagnosticComponent(DiagnosticComponent):
+
+    input_properties = {}
+    diagnostic_properties = {}
+
+    def __init__(self, **kwargs):
+        super(MockEmptyDiagnosticComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        return {}
+
+
+class MockMonitor(Monitor):
+
+    def store(self, state):
+        return
+
+
+def test_empty_prognostic_composite():
+    prognostic_composite = TendencyComponentComposite()
+    state = {'air_temperature': 273.15}
+    tendencies, diagnostics = prognostic_composite(state)
+    assert len(tendencies) == 0
+    assert len(diagnostics) == 0
+    assert isinstance(tendencies, dict)
+    assert isinstance(diagnostics, dict)
+
+
+@mock.patch.object(MockEmptyTendencyComponent, '__call__')
+def test_prognostic_composite_calls_one_prognostic(mock_call):
+    mock_call.return_value = ({'air_temperature': 0.5}, {'foo': 50.})
+    prognostic_composite = TendencyComponentComposite(MockEmptyTendencyComponent())
+    state = {'air_temperature': 273.15}
+    tendencies, diagnostics = prognostic_composite(state)
+    assert mock_call.called
+    assert tendencies == {'air_temperature': 0.5}
+    assert diagnostics == {'foo': 50.}
+
+
+@mock.patch.object(MockEmptyTendencyComponent, '__call__')
+def test_prognostic_composite_calls_two_prognostics(mock_call):
+    mock_call.return_value = ({'air_temperature': 0.5}, {})
+    prognostic_composite = TendencyComponentComposite(
+        MockEmptyTendencyComponent(), MockEmptyTendencyComponent())
+    state = {'air_temperature': 273.15}
+    tendencies, diagnostics = prognostic_composite(state)
+    assert mock_call.called
+    assert mock_call.call_count == 2
+    assert tendencies == {'air_temperature': 1.}
+    assert diagnostics == {}
+
+
+def test_empty_diagnostic_composite():
+    diagnostic_composite = DiagnosticComponentComposite()
+    state = {'air_temperature': 273.15}
+    diagnostics = diagnostic_composite(state)
+    assert len(diagnostics) == 0
+    assert isinstance(diagnostics, dict)
+
+
+@mock.patch.object(MockEmptyDiagnosticComponent, '__call__')
+def test_diagnostic_composite_calls_one_diagnostic(mock_call):
+    mock_call.return_value = {'foo': 50.}
+    diagnostic_composite = DiagnosticComponentComposite(MockEmptyDiagnosticComponent())
+    state = {'air_temperature': 273.15}
+    diagnostics = diagnostic_composite(state)
+    assert mock_call.called
+    assert diagnostics == {'foo': 50.}
+
+
+def test_empty_monitor_collection():
+    # mainly we're testing that nothing errors
+    monitor_collection = MonitorComposite()
+    state = {'air_temperature': 273.15}
+    monitor_collection.store(state)
+
+
+@mock.patch.object(MockMonitor, 'store')
+def test_monitor_collection_calls_one_monitor(mock_store):
+    mock_store.return_value = None
+    monitor_collection = MonitorComposite(MockMonitor())
+    state = {'air_temperature': 273.15}
+    monitor_collection.store(state)
+    assert mock_store.called
+
+
+@mock.patch.object(MockMonitor, 'store')
+def test_monitor_collection_calls_two_monitors(mock_store):
+    mock_store.return_value = None
+    monitor_collection = MonitorComposite(MockMonitor(), MockMonitor())
+    state = {'air_temperature': 273.15}
+    monitor_collection.store(state)
+    assert mock_store.called
+    assert mock_store.call_count == 2
+
+
+def test_prognostic_composite_cannot_use_diagnostic():
+    try:
+        TendencyComponentComposite(MockEmptyDiagnosticComponent())
+    except TypeError:
+        pass
+    except Exception as err:
+        raise err
+    else:
+        raise AssertionError('TypeError should have been raised')
+
+
+def test_diagnostic_composite_cannot_use_prognostic():
+    try:
+        DiagnosticComponentComposite(MockEmptyTendencyComponent())
+    except TypeError:
+        pass
+    except Exception as err:
+        raise err
+    else:
+        raise AssertionError('TypeError should have been raised')
+
+
+@mock.patch.object(MockEmptyDiagnosticComponent, '__call__')
+def test_diagnostic_composite_call(mock_call):
+    mock_call.return_value = {'foo': 5.}
+    state = {'bar': 10.}
+    diagnostics = DiagnosticComponentComposite(MockEmptyDiagnosticComponent())
+    new_state = diagnostics(state)
+    assert list(state.keys()) == ['bar']
+    assert state['bar'] == 10.
+    assert list(new_state.keys()) == ['foo']
+    assert new_state['foo'] == 5.
+
+
+@mock.patch.object(MockEmptyTendencyComponent, '__call__')
+@mock.patch.object(MockEmptyTendencyComponent2, '__call__')
+def test_prognostic_component_handles_units_when_combining(mock_call, mock2_call):
+    mock_call.return_value = ({
+        'eastward_wind': DataArray(1., attrs={'units': 'm/s'})}, {})
+    mock2_call.return_value = ({
+        'eastward_wind': DataArray(50., attrs={'units': 'cm/s'})}, {})
+    prognostic1 = MockEmptyTendencyComponent()
+    prognostic2 = MockEmptyTendencyComponent2()
+    composite = TendencyComponentComposite(prognostic1, prognostic2)
+    tendencies, diagnostics = composite({})
+    assert tendencies['eastward_wind'].to_units('m/s').values.item() == 1.5
+
+
+def test_diagnostic_composite_single_component_input():
+    input_properties = {
+        'input1': {
+            'dims': ['dim1'],
+            'units': 'm',
+        },
+        'input2': {
+            'dims': ['dim2'],
+            'units': 'm/s'
+        },
+    }
+    diagnostic_properties = {}
+    diagnostic_output = {}
+    diagnostic = MockDiagnosticComponent(
+        input_properties, diagnostic_properties, diagnostic_output)
+    composite = DiagnosticComponentComposite(diagnostic)
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+
+
+def test_diagnostic_composite_single_component_diagnostic():
+    input_properties = {}
+    diagnostic_properties = {
+        'diag1': {
+            'dims': ['lon'],
+            'units': 'km',
+        },
+        'diag2': {
+            'dims': ['lon'],
+            'units': 'degK',
+        },
+    }
+    diagnostic_output = {}
+    diagnostic = MockDiagnosticComponent(
+        input_properties, diagnostic_properties, diagnostic_output)
+    composite = DiagnosticComponentComposite(diagnostic)
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+
+
+def test_diagnostic_composite_single_empty_component():
+    input_properties = {}
+    diagnostic_properties = {}
+    diagnostic_output = {}
+    diagnostic = MockDiagnosticComponent(
+        input_properties, diagnostic_properties, diagnostic_output)
+    composite = DiagnosticComponentComposite(diagnostic)
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+
+
+def test_diagnostic_composite_single_full_component():
+    input_properties = {
+        'input1': {
+            'dims': ['dim1'],
+            'units': 'm',
+        },
+        'input2': {
+            'dims': ['dim2'],
+            'units': 'm/s'
+        },
+    }
+    diagnostic_properties = {
+        'diag1': {
+            'dims': ['lon'],
+            'units': 'km',
+        },
+        'diag2': {
+            'dims': ['lon'],
+            'units': 'degK',
+        },
+    }
+    diagnostic_output = {}
+    diagnostic = MockDiagnosticComponent(
+        input_properties, diagnostic_properties, diagnostic_output)
+    composite = DiagnosticComponentComposite(diagnostic)
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+
+
+def test_diagnostic_composite_single_component_no_dims_on_diagnostic():
+    input_properties = {
+        'diag1': {
+            'dims': ['dim1'],
+            'units': 'm',
+        },
+    }
+    diagnostic_properties = {
+        'diag1': {
+            'units': 'km',
+        },
+    }
+    diagnostic_output = {}
+    diagnostic = MockDiagnosticComponent(
+        input_properties, diagnostic_properties, diagnostic_output)
+    composite = DiagnosticComponentComposite(diagnostic)
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+
+
+def test_diagnostic_composite_single_component_missing_dims_on_diagnostic():
+    input_properties = {}
+    diagnostic_properties = {
+        'diag1': {
+            'units': 'km',
+        },
+    }
+    diagnostic_output = {}
+    try:
+        diagnostic = MockDiagnosticComponent(
+            input_properties, diagnostic_properties, diagnostic_output)
+        DiagnosticComponentComposite(diagnostic)
+    except InvalidPropertyDictError:
+        pass
+    else:
+        raise AssertionError('Should have raised InvalidPropertyDictError')
+
+
+def test_diagnostic_composite_two_components_no_overlap():
+    diagnostic1 = MockDiagnosticComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+        },
+        diagnostic_properties={
+            'diag1': {
+                'dims': ['lon'],
+                'units': 'km',
+            },
+        },
+        diagnostic_output={}
+    )
+    diagnostic2 = MockDiagnosticComponent(
+        input_properties={
+            'input2': {
+                'dims': ['dim2'],
+                'units': 'm/s'
+            },
+        },
+        diagnostic_properties={
+            'diag2': {
+                'dims': ['lon'],
+                'units': 'degK',
+            },
+        },
+        diagnostic_output={}
+    )
+    composite = DiagnosticComponentComposite(diagnostic1, diagnostic2)
+    input_properties = {
+        'input1': {
+            'dims': ['dim1'],
+            'units': 'm',
+        },
+        'input2': {
+            'dims': ['dim2'],
+            'units': 'm/s'
+        },
+    }
+    diagnostic_properties = {
+        'diag1': {
+            'dims': ['lon'],
+            'units': 'km',
+        },
+        'diag2': {
+            'dims': ['lon'],
+            'units': 'degK',
+        },
+    }
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+
+
+def test_diagnostic_composite_two_components_overlap_input():
+    diagnostic1 = MockDiagnosticComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+            'input2': {
+                'dims': ['dim2'],
+                'units': 'm/s'
+            },
+        },
+        diagnostic_properties={
+            'diag1': {
+                'dims': ['lon'],
+                'units': 'km',
+            },
+        },
+        diagnostic_output={}
+    )
+    diagnostic2 = MockDiagnosticComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+            'input2': {
+                'dims': ['dim2'],
+                'units': 'm/s'
+            },
+        },
+        diagnostic_properties={
+            'diag2': {
+                'dims': ['lon'],
+                'units': 'degK',
+            },
+        },
+        diagnostic_output={}
+    )
+    composite = DiagnosticComponentComposite(diagnostic1, diagnostic2)
+    input_properties = {
+        'input1': {
+            'dims': ['dim1'],
+            'units': 'm',
+        },
+        'input2': {
+            'dims': ['dim2'],
+            'units': 'm/s'
+        },
+    }
+    diagnostic_properties = {
+        'diag1': {
+            'dims': ['lon'],
+            'units': 'km',
+        },
+        'diag2': {
+            'dims': ['lon'],
+            'units': 'degK',
+        },
+    }
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+
+
+def test_diagnostic_composite_two_components_overlap_diagnostic():
+    diagnostic1 = MockDiagnosticComponent(
+        input_properties={},
+        diagnostic_properties={
+            'diag1': {
+                'dims': ['lon'],
+                'units': 'km',
+            },
+        },
+        diagnostic_output={}
+    )
+    diagnostic2 = MockDiagnosticComponent(
+        input_properties={},
+        diagnostic_properties={
+            'diag1': {
+                'dims': ['lon'],
+                'units': 'km',
+            },
+        },
+        diagnostic_output={}
+    )
+    try:
+        DiagnosticComponentComposite(diagnostic1, diagnostic2)
+    except SharedKeyError:
+        pass
+    else:
+        raise AssertionError('Should have raised SharedKeyError')
+
+
+def test_diagnostic_composite_two_components_incompatible_input_dims():
+    diagnostic1 = MockDiagnosticComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        },
+        diagnostic_properties={},
+        diagnostic_output={}
+    )
+    diagnostic2 = MockDiagnosticComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dim2'],
+                'units': 'm',
+            }
+        },
+        diagnostic_properties={},
+        diagnostic_output={}
+    )
+    try:
+        composite = DiagnosticComponentComposite(diagnostic1, diagnostic2)
+    except InvalidPropertyDictError:
+        pass
+    else:
+        raise AssertionError('Should have raised InvalidPropertyDictError')
+
+
+def test_diagnostic_composite_two_components_incompatible_input_units():
+    diagnostic1 = MockDiagnosticComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        },
+        diagnostic_properties={},
+        diagnostic_output={}
+    )
+    diagnostic2 = MockDiagnosticComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dim1'],
+                'units': 's',
+            }
+        },
+        diagnostic_properties={},
+        diagnostic_output={}
+    )
+    try:
+        DiagnosticComponentComposite(diagnostic1, diagnostic2)
+    except InvalidPropertyDictError:
+        pass
+    else:
+        raise AssertionError('Should have raised InvalidPropertyDictError')
+
+
+def test_prognostic_composite_single_input():
+    prognostic = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1'],
+                'units': 'm',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == prognostic.tendency_properties
+
+
+def test_prognostic_composite_single_diagnostic():
+    prognostic = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={
+            'diag1': {
+                'dims': ['dims2'],
+                'units': '',
+            }
+        },
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == prognostic.tendency_properties
+
+
+def test_prognostic_composite_single_tendency():
+    prognostic = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == prognostic.tendency_properties
+
+
+def test_prognostic_composite_implicit_dims():
+    prognostic = MockTendencyComponent(
+        input_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == {
+        'tend1': {
+            'dims': ['dims1', 'dims2'],
+            'units': 'degK / day',
+        }
+    }
+
+
+def test_two_prognostic_composite_implicit_dims():
+    prognostic = MockTendencyComponent(
+        input_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic, prognostic2)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == {
+        'tend1': {
+            'dims': ['dims1', 'dims2'],
+            'units': 'degK / day',
+        }
+    }
+
+
+def test_prognostic_composite_explicit_dims():
+    prognostic = MockTendencyComponent(
+        input_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == prognostic.tendency_properties
+
+
+def test_two_prognostic_composite_explicit_dims():
+    prognostic = MockTendencyComponent(
+        input_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic, prognostic2)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == prognostic.tendency_properties
+
+
+def test_two_prognostic_composite_explicit_and_implicit_dims():
+    prognostic = MockTendencyComponent(
+        input_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic, prognostic2)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == prognostic.tendency_properties
+
+
+def test_prognostic_composite_explicit_dims_not_in_input():
+    prognostic = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == prognostic.tendency_properties
+
+
+def test_two_prognostic_composite_incompatible_dims():
+    prognostic = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            },
+            'input2': {
+                'dims': ['dims3', 'dims1'],
+                'units': 'degK'
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            },
+            'input2': {
+                'dims': ['dims3', 'dims1'],
+                'units': 'degK / day'
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims3', 'dims1'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    try:
+        TendencyComponentComposite(prognostic, prognostic2)
+    except InvalidPropertyDictError:
+        pass
+    else:
+        raise AssertionError('Should have raised InvalidPropertyDictError')
+
+
+def test_two_prognostic_composite_compatible_dims():
+    prognostic = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            },
+            'input2': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK'
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            },
+            'input2': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK'
+            }
+        },
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK / day',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic, prognostic2)
+    assert composite.input_properties == prognostic.input_properties
+    assert composite.diagnostic_properties == prognostic.diagnostic_properties
+    assert composite.tendency_properties == prognostic.tendency_properties
+
+
+def test_prognostic_composite_two_components_input():
+    prognostic1 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            },
+            'input2': {
+                'dims': ['dims1'],
+                'units': 'm',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            },
+            'input3': {
+                'dims': ['dims2'],
+                'units': '',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic1, prognostic2)
+    input_properties = {
+        'input1': {
+            'dims': ['dims1', 'dims2'],
+            'units': 'degK',
+        },
+        'input2': {
+            'dims': ['dims1'],
+            'units': 'm',
+        },
+        'input3': {
+            'dims': ['dims2'],
+            'units': '',
+        },
+    }
+    diagnostic_properties = {}
+    tendency_properties = {}
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+    assert composite.tendency_properties == tendency_properties
+
+
+def test_prognostic_composite_two_components_swapped_input_dims():
+    prognostic1 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims2', 'dims1'],
+                'units': 'degK',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic1, prognostic2)
+    diagnostic_properties = {}
+    tendency_properties = {}
+    assert (composite.input_properties == prognostic1.input_properties or
+            composite.input_properties == prognostic2.input_properties)
+    assert composite.diagnostic_properties == diagnostic_properties
+    assert composite.tendency_properties == tendency_properties
+
+
+def test_prognostic_composite_two_components_incompatible_input_dims():
+    prognostic1 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims3'],
+                'units': 'degK',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    try:
+        TendencyComponentComposite(prognostic1, prognostic2)
+    except InvalidPropertyDictError:
+        pass
+    else:
+        raise AssertionError('Should have raised InvalidPropertyDictError')
+
+
+def test_prognostic_composite_two_components_incompatible_input_units():
+    prognostic1 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'degK',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'm',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    try:
+        TendencyComponentComposite(prognostic1, prognostic2)
+    except InvalidPropertyDictError:
+        pass
+    else:
+        raise AssertionError('Should have raised InvalidPropertyDictError')
+
+
+def test_prognostic_composite_two_components_compatible_input_units():
+    prognostic1 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'km',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={
+            'input1': {
+                'dims': ['dims1', 'dims2'],
+                'units': 'cm',
+            },
+        },
+        diagnostic_properties={},
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic1, prognostic2)
+    assert 'input1' in composite.input_properties.keys()
+    assert composite.input_properties['input1']['dims'] == ['dims1', 'dims2']
+    assert units_are_compatible(composite.input_properties['input1']['units'], 'm')
+
+
+def test_prognostic_composite_two_components_tendency():
+    prognostic1 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dim1'],
+                'units': 'm/s',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            },
+            'tend2': {
+                'dims': ['dim1'],
+                'units': 'degK/s'
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic1, prognostic2)
+    input_properties = {}
+    diagnostic_properties = {}
+    tendency_properties = {
+        'tend1': {
+            'dims': ['dim1'],
+            'units': 'm/s'
+        },
+        'tend2': {
+            'dims': ['dim1'],
+            'units': 'degK/s'
+        }
+    }
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+    assert composite.tendency_properties == tendency_properties
+
+
+def test_prognostic_composite_two_components_tendency_incompatible_dims():
+    prognostic1 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dim1'],
+                'units': 'm/s',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dim2'],
+                'units': 'm/s'
+            },
+            'tend2': {
+                'dims': ['dim1'],
+                'units': 'degK/s'
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    try:
+        TendencyComponentComposite(prognostic1, prognostic2)
+    except InvalidPropertyDictError:
+        pass
+    else:
+        raise AssertionError('Should have raised InvalidPropertyDictError')
+
+
+def test_prognostic_composite_two_components_tendency_incompatible_units():
+    prognostic1 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dim1'],
+                'units': 'm/s',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dim1'],
+                'units': 'degK/s'
+            },
+            'tend2': {
+                'dims': ['dim1'],
+                'units': 'degK/s'
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    try:
+        TendencyComponentComposite(prognostic1, prognostic2)
+    except InvalidPropertyDictError:
+        pass
+    else:
+        raise AssertionError('Should have raised InvalidPropertyDictError')
+
+
+def test_prognostic_composite_two_components_tendency_compatible_units():
+    prognostic1 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dim1'],
+                'units': 'km/s',
+            }
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={},
+        tendency_properties={
+            'tend1': {
+                'dims': ['dim1'],
+                'units': 'm/day'
+            },
+        },
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic1, prognostic2)
+    assert 'tend1' in composite.tendency_properties.keys()
+    assert composite.tendency_properties['tend1']['dims'] == ['dim1']
+    assert units_are_compatible(composite.tendency_properties['tend1']['units'], 'm/s')
+
+
+def test_prognostic_composite_two_components_diagnostic():
+    prognostic1 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        },
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={
+            'diag2': {
+                'dims': ['dim2'],
+                'units': 'm',
+            }
+        },
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    composite = TendencyComponentComposite(prognostic1, prognostic2)
+    input_properties = {}
+    diagnostic_properties = {
+        'diag1': {
+            'dims': ['dim1'],
+            'units': 'm',
+        },
+        'diag2': {
+            'dims': ['dim2'],
+            'units': 'm',
+        },
+    }
+    tendency_properties = {}
+    assert composite.input_properties == input_properties
+    assert composite.diagnostic_properties == diagnostic_properties
+    assert composite.tendency_properties == tendency_properties
+
+
+def test_prognostic_composite_two_components_overlapping_diagnostic():
+    prognostic1 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        },
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    prognostic2 = MockTendencyComponent(
+        input_properties={},
+        diagnostic_properties={
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        },
+        tendency_properties={},
+        diagnostic_output={},
+        tendency_output={},
+    )
+    try:
+        TendencyComponentComposite(prognostic1, prognostic2)
+    except SharedKeyError:
+        pass
+    else:
+        raise AssertionError('Should have raised SharedKeyError')
+
+
+if __name__ == '__main__':
+    pytest.main([__file__])
diff --git a/tests/test_get_restore_numpy_array.py b/tests/test_get_restore_numpy_array.py
index 88cc04f..c148b64 100644
--- a/tests/test_get_restore_numpy_array.py
+++ b/tests/test_get_restore_numpy_array.py
@@ -1,59 +1,12 @@
 import pytest
 from sympl import (
-    DataArray, set_direction_names, get_numpy_array,
+    DataArray, get_numpy_array,
     restore_dimensions, get_numpy_arrays_with_properties,
     restore_data_arrays_with_properties, InvalidStateError,
     InvalidPropertyDictError)
 import numpy as np
 import unittest
 
-"""
-get_numpy_arrays_with_properties:
-    - returns numpy arrays in the dict
-    - those numpy arrays should have same dtype as original data
-        * even when unit conversion happens
-    - properly collects dimensions along a direction
-    - they should actually be the same numpy arrays (with memory) as original data if no conversion happens
-        * even when units are specified (so long as they match)
-    - should be the same quantities as requested by the properties
-        * contain all
-        * not contain extra
-        * raise exception if some are missing
-    - units
-        * converts if requested and present
-        * does nothing if not requested whether or not present
-        * raises exception if not requested or not present
-        * unit conversion should not modify the input array
-    - requires "dims" to be specified, raises exception if they aren't
-    - match_dims_like
-        * should work if matched dimensions are identical
-        * should raise exception if matched dimensions are not identical
-        * should require value to be a quantity in property_dictionary
-        * should require all A matches to look like B and all B matches to look like A
-    - should raise ValueError when explicitly specified dimension is not present
-    - should return a scalar array when called on a scalar DataArray
-
-Test case for when wildcard dimension doesn't match anything - the error message needs to be much more descriptive
-    e.g. dims=['x', 'y', 'z'] and state=['foo', 'y', 'z']
-
-restore_data_arrays_with_properties:
-    - should return a dictionary of DataArrays
-    - DataArray values should be the same arrays as original data if no conversion happens
-    - properly restores collected dimensions
-    - if conversion does happen, dtype should be the same as the input
-    - should return same quantities as requested by the properties
-        * contain all
-        * not contain extra
-        * raise exception if some are missing
-    - units
-        * should be the same value as specified in output_properties dict
-    - requires dims_like to be specified, raises exception if it's not
-        * returned DataArray should have same dimensions as dims_like object
-        * exception should be raised if dims_like is wrong (shape is incompatible)
-        * should return coords like the dims_like quantity
-
-Should add any created exceptions to the docstrings for these functions
-"""
 
 def test_get_numpy_array_3d_no_change():
     array = DataArray(
@@ -206,26 +159,6 @@ def test_get_numpy_array_no_dimensions_listed_raises_value_error():
         raise AssertionError('Expected ValueError but no error was raised')
 
 
-def test_get_numpy_array_multiple_dims_on_same_direction():
-    try:
-        set_direction_names(x=['lon'])
-        array = DataArray(
-            np.random.randn(2, 3),
-            dims=['x', 'lon'],
-            attrs={'units': ''},
-        )
-        try:
-            numpy_array = get_numpy_array(array, ['x', 'y'])
-        except ValueError:
-            pass
-        except Exception as err:
-            raise err
-        else:
-            raise AssertionError('Expected ValueError but no error was raised')
-    finally:
-        set_direction_names(x=[], y=[], z=[])
-
-
 def test_get_numpy_array_not_enough_out_dims():
     array = DataArray(
         np.random.randn(2, 3),
@@ -481,12 +414,6 @@ def test_restore_dimensions_removes_dummy_axes():
 
 class GetNumpyArraysWithPropertiesTests(unittest.TestCase):
 
-    def setUp(self):
-        pass
-
-    def tearDown(self):
-        set_direction_names(x=(), y=(), z=())
-
     def test_returns_numpy_array(self):
         T_array = np.zeros([2, 3, 4], dtype=np.float64) + 280.
         property_dictionary = {
@@ -551,12 +478,12 @@ def test_returns_numpy_array_alias_doesnt_apply_to_state(self):
             ),
         }
         try:
-            return_value = get_numpy_arrays_with_properties(
+            get_numpy_arrays_with_properties(
                 state, property_dictionary)
-        except InvalidStateError:
+        except KeyError:
             pass
         else:
-            raise AssertionError('should have raised InvalidStateError')
+            raise AssertionError('should have raised KeyError')
 
     def test_returns_scalar_array(self):
         T_array = np.array(0.)
@@ -604,59 +531,11 @@ def test_scalar_becomes_multidimensional_array(self):
             T_array)
         assert return_value['air_temperature'].base is T_array
 
-    def test_collects_wildcard_dimension(self):
-        set_direction_names(z=['mid_levels'])
-        T_array = np.zeros([2, 3, 4], dtype=np.float64) + 280.
-        property_dictionary = {
-            'air_temperature': {
-                'units': 'degK',
-                'dims': ['x', 'y', 'z'],
-            },
-        }
-        state = {
-            'air_temperature': DataArray(
-                T_array,
-                dims=['x', 'y', 'mid_levels'],
-                attrs={'units': 'degK'},
-            ),
-        }
-        return_value = get_numpy_arrays_with_properties(state, property_dictionary)
-        assert isinstance(return_value, dict)
-        assert len(return_value.keys()) == 1
-        assert isinstance(return_value['air_temperature'], np.ndarray)
-        assert np.byte_bounds(return_value['air_temperature']) == np.byte_bounds(
-            T_array)
-        assert return_value['air_temperature'].base is T_array
-        assert return_value['air_temperature'].shape == (2, 3, 4)
-
-    def test_raises_on_missing_explicit_dimension(self):
-        set_direction_names(z=['mid_levels'])
-        T_array = np.zeros([2, 3, 4], dtype=np.float64) + 280.
-        property_dictionary = {
-            'air_temperature': {
-                'units': 'degK',
-                'dims': ['x', 'y', 'mid_levels'],
-            },
-        }
-        state = {
-            'air_temperature': DataArray(
-                T_array,
-                dims=['x', 'y', 'z'],
-                attrs={'units': 'degK'},
-            ),
-        }
-        try:
-            return_value = get_numpy_arrays_with_properties(state, property_dictionary)
-        except InvalidStateError:
-            pass
-        else:
-            raise AssertionError('should have raised InvalidStateError')
-
     def test_creates_length_1_dimensions(self):
         T_array = np.zeros([4], dtype=np.float64) + 280.
         property_dictionary = {
             'air_temperature': {
-                'dims': ['x', 'y', 'z'],
+                'dims': ['*', 'z'],
                 'units': 'degK',
             },
         }
@@ -675,7 +554,7 @@ def test_creates_length_1_dimensions(self):
             return_value['air_temperature']) == np.byte_bounds(
             T_array)
         assert return_value['air_temperature'].base is T_array
-        assert return_value['air_temperature'].shape == (1, 1, 4)
+        assert return_value['air_temperature'].shape == (1, 4)
 
     def test_only_requested_properties_are_returned(self):
         property_dictionary = {
@@ -736,35 +615,6 @@ def test_all_requested_properties_are_returned(self):
         assert np.all(return_value['air_temperature'] == 0.)
         assert np.all(return_value['air_pressure'] == 0.)
 
-    def test_raises_exception_on_missing_quantity(self):
-        property_dictionary = {
-            'air_temperature': {
-                'dims': ['x', 'y', 'z'],
-                'units': 'degK',
-            },
-            'air_pressure': {
-                'dims': ['x', 'y', 'z'],
-                'units': 'Pa',
-            },
-        }
-        state = {
-            'air_temperature': DataArray(
-                np.zeros([4], dtype=np.float64),
-                dims=['z'],
-                attrs={'units': 'degK'},
-            ),
-            'eastward_wind': DataArray(
-                np.zeros([2,2,4], dtype=np.float64),
-                attrs={'units': 'm/s'}
-            ),
-        }
-        try:
-            get_numpy_arrays_with_properties(state, property_dictionary)
-        except InvalidStateError:
-            pass
-        else:
-            raise AssertionError('should have raised InvalidStateError')
-
     def test_converts_units(self):
         property_dictionary = {
             'air_temperature': {
@@ -904,100 +754,6 @@ def test_raises_if_dims_property_not_specified(self):
         else:
             raise AssertionError('should have raised ValueError')
 
-    def test_dims_like_accepts_valid_case(self):
-        set_direction_names(x=['x_cell_center', 'x_cell_interface'],
-                            z=['mid_levels', 'interface_levels'])
-        property_dictionary = {
-            'air_temperature': {
-                'dims': ['x', 'y', 'mid_levels'],
-                'units': 'degK',
-            },
-            'air_pressure': {
-                'dims': ['x', 'y', 'interface_levels'],
-                'units': 'Pa',
-                'match_dims_like': 'air_temperature'
-            },
-        }
-        state = {
-            'air_temperature': DataArray(
-                np.zeros([2, 2, 4], dtype=np.float64),
-                dims=['x_cell_center', 'y', 'mid_levels'],
-                attrs={'units': 'degK'},
-            ),
-            'air_pressure': DataArray(
-                np.zeros([2, 2, 4], dtype=np.float64),
-                dims=['x_cell_center', 'y', 'interface_levels'],
-                attrs={'units': 'Pa'}
-            ),
-        }
-        return_value = get_numpy_arrays_with_properties(state, property_dictionary)
-        assert isinstance(return_value, dict)
-        assert len(return_value.keys()) == 2
-        assert 'air_temperature' in return_value.keys()
-        assert 'air_pressure' in return_value.keys()
-
-    def test_dims_like_rejects_mismatched_dimensions(self):
-        set_direction_names(x=['x_cell_center', 'x_cell_interface'],
-                            z=['mid_levels', 'interface_levels'])
-        property_dictionary = {
-            'air_temperature': {
-                'dims': ['x', 'y', 'mid_levels'],
-                'units': 'degK',
-            },
-            'air_pressure': {
-                'dims': ['x', 'y', 'interface_levels'],
-                'units': 'Pa',
-                'match_dims_like': 'air_temperature'
-            },
-        }
-        state = {
-            'air_temperature': DataArray(
-                np.zeros([2, 2, 4], dtype=np.float64),
-                dims=['x_cell_center', 'y', 'mid_levels'],
-                attrs={'units': 'degK'},
-            ),
-            'air_pressure': DataArray(
-                np.zeros([2, 2, 4], dtype=np.float64),
-                dims=['x_cell_interface', 'y', 'interface_levels'],
-                attrs={'units': 'Pa'}
-            ),
-        }
-        try:
-            get_numpy_arrays_with_properties(state, property_dictionary)
-        except InvalidStateError:
-            pass
-        else:
-            raise AssertionError('should have raised InvalidStateError')
-
-    def test_dims_like_raises_if_quantity_not_in_property_dict(self):
-        set_direction_names(x=['x_cell_center', 'x_cell_interface'],
-                            z=['mid_levels', 'interface_levels'])
-        property_dictionary = {
-            'air_pressure': {
-                'dims': ['x', 'y', 'interface_levels'],
-                'units': 'Pa',
-                'match_dims_like': 'air_temperature'
-            },
-        }
-        state = {
-            'air_temperature': DataArray(
-                np.zeros([2, 2, 4], dtype=np.float64),
-                dims=['x_cell_center', 'y', 'mid_levels'],
-                attrs={'units': 'degK'},
-            ),
-            'air_pressure': DataArray(
-                np.zeros([2, 2, 4], dtype=np.float64),
-                dims=['x_cell_interface', 'y', 'interface_levels'],
-                attrs={'units': 'Pa'}
-            ),
-        }
-        try:
-            get_numpy_arrays_with_properties(state, property_dictionary)
-        except InvalidPropertyDictError:
-            pass
-        else:
-            raise AssertionError('should have raised InvalidPropertyDictError')
-
     def test_collects_horizontal_dimensions(self):
         random = np.random.RandomState(0)
         T_array = random.randn(3, 2, 4)
@@ -1015,10 +771,6 @@ def test_collects_horizontal_dimensions(self):
             }
         }
         return_value = get_numpy_arrays_with_properties(input_state, input_properties)
-        assert np.byte_bounds(
-            return_value['air_temperature']) == np.byte_bounds(
-            T_array)
-        assert return_value['air_temperature'].base is T_array
         assert return_value['air_temperature'].shape == (4, 6)
         for i in range(3):
             for j in range(2):
@@ -1067,14 +819,86 @@ def test_raises_when_quantity_has_extra_dim_and_unmatched_wildcard(self):
         else:
             raise AssertionError('should have raised InvalidStateError')
 
+    def test_expands_named_dimension(self):
+        random = np.random.RandomState(0)
+        T_array = random.randn(3)
+        input_state = {
+            'air_pressure': DataArray(
+                np.zeros([3, 4]),
+                dims=['dim1', 'dim2'],
+                attrs={'units': 'Pa'},
+            ),
+            'air_temperature': DataArray(
+                T_array,
+                dims=['dim1'],
+                attrs={'units': 'degK'},
+            )
+        }
+        input_properties = {
+            'air_pressure': {
+                'dims': ['dim1', 'dim2'],
+                'units': 'Pa',
+            },
+            'air_temperature': {
+                'dims': ['dim1', 'dim2'],
+                'units': 'degK',
+            },
+        }
+        return_value = get_numpy_arrays_with_properties(input_state, input_properties)
+        assert return_value['air_temperature'].shape == (3, 4)
+        assert np.all(return_value['air_temperature'] == T_array[:, None])
+
+    def test_expands_named_dimension_with_wildcard_present(self):
+        random = np.random.RandomState(0)
+        T_array = random.randn(3)
+        input_state = {
+            'air_pressure': DataArray(
+                np.zeros([3, 4]),
+                dims=['dim1', 'dim2'],
+                attrs={'units': 'Pa'},
+            ),
+            'air_temperature': DataArray(
+                T_array,
+                dims=['dim1'],
+                attrs={'units': 'degK'},
+            )
+        }
+        input_properties = {
+            'air_pressure': {
+                'dims': ['*', 'dim2'],
+                'units': 'Pa',
+            },
+            'air_temperature': {
+                'dims': ['*', 'dim2'],
+                'units': 'degK',
+            },
+        }
+        return_value = get_numpy_arrays_with_properties(input_state, input_properties)
+        assert return_value['air_temperature'].shape == (3, 4)
+        assert np.all(return_value['air_temperature'] == T_array[:, None])
 
-class RestoreDataArraysWithPropertiesTests(unittest.TestCase):
 
-    def setUp(self):
-        pass
+class RestoreDataArraysWithPropertiesTests(unittest.TestCase):
 
-    def tearDown(self):
-        set_direction_names(x=(), y=(), z=())
+    def test_restores_with_dims(self):
+        raw_arrays = {
+            'output1': np.ones([10]),
+        }
+        output_properties =  {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        output = restore_data_arrays_with_properties(
+            raw_arrays, output_properties, {}, {})
+        assert len(output) == 1
+        assert 'output1' in output.keys()
+        assert isinstance(output['output1'], DataArray)
+        assert len(output['output1'].dims) == 1
+        assert 'dim1' in output['output1'].dims
+        assert 'units' in output['output1'].attrs.keys()
+        assert output['output1'].attrs['units'] == 'm'
 
     def test_returns_simple_value(self):
         input_state = {
@@ -1094,7 +918,7 @@ def test_returns_simple_value(self):
         raw_arrays = {key + '_tendency': value for key, value in raw_arrays.items()}
         output_properties = {
             'air_temperature_tendency': {
-                'dims_like': 'air_temperature',
+                'dims': ['x', 'y', 'z'],
                 'units': 'degK/s',
             }
         }
@@ -1167,7 +991,7 @@ def test_restores_collected_horizontal_dimensions(self):
         raw_arrays = {key + '_tendency': value for key, value in raw_arrays.items()}
         output_properties = {
             'air_temperature_tendency': {
-                'dims_like': 'air_temperature',
+                'dims': ['z', '*'],
                 'units': 'degK/s',
             }
         }
@@ -1183,96 +1007,10 @@ def test_restores_collected_horizontal_dimensions(self):
             input_state['air_temperature'].values)
         assert (return_value['air_temperature_tendency'].values.base is
                 input_state['air_temperature'].values)
-        assert return_value['air_temperature_tendency'].shape == (3, 2, 4)
-        assert np.all(return_value['air_temperature_tendency'] == T_array)
-        assert return_value['air_temperature_tendency'].dims == input_state['air_temperature'].dims
-
-    def test_restores_coords(self):
-        x = np.array([0., 10.])
-        y = np.array([0., 10.])
-        z = np.array([0., 5., 10., 15.])
-        input_state = {
-            'air_temperature': DataArray(
-                np.zeros([2, 2, 4]),
-                dims=['x', 'y', 'z'],
-                attrs={'units': 'degK'},
-                coords=[
-                    ('x', x, {'units': 'm'}),
-                    ('y', y, {'units': 'km'}),
-                    ('z', z, {'units': 'cm'})]
-            )
-        }
-        input_properties = {
-            'air_temperature': {
-                'dims': ['x', 'y', 'z'],
-                'units': 'degK',
-            }
-        }
-        raw_arrays = get_numpy_arrays_with_properties(input_state, input_properties)
-        raw_arrays = {key + '_tendency': value for key, value in raw_arrays.items()}
-        output_properties = {
-            'air_temperature_tendency': {
-                'dims_like': 'air_temperature',
-                'units': 'degK/s',
-            }
-        }
-        return_value = restore_data_arrays_with_properties(
-            raw_arrays, output_properties, input_state, input_properties
-        )
-        assert np.all(return_value['air_temperature_tendency'].coords['x'] ==
-                      input_state['air_temperature'].coords['x'])
-        assert return_value['air_temperature_tendency'].coords['x'].attrs['units'] == 'm'
-        assert np.all(return_value['air_temperature_tendency'].coords['y'] ==
-                      input_state['air_temperature'].coords['y'])
-        assert return_value['air_temperature_tendency'].coords['y'].attrs['units'] == 'km'
-        assert np.all(return_value['air_temperature_tendency'].coords['z'] ==
-                      input_state['air_temperature'].coords['z'])
-        assert return_value['air_temperature_tendency'].coords['z'].attrs['units'] == 'cm'
-        assert return_value['air_temperature_tendency'].dims == input_state['air_temperature'].dims
-
-    def test_restores_matched_coords(self):
-        set_direction_names(x=['lon'], y=['lat'], z=['height'])
-        x = np.array([0., 10.])
-        y = np.array([0., 10.])
-        z = np.array([0., 5., 10., 15.])
-        input_state = {
-            'air_temperature': DataArray(
-                np.zeros([2, 2, 4]),
-                dims=['lon', 'lat', 'height'],
-                attrs={'units': 'degK'},
-                coords=[
-                    ('lon', x, {'units': 'degrees_E'}),
-                    ('lat', y, {'units': 'degrees_N'}),
-                    ('height', z, {'units': 'km'})]
-            )
-        }
-        input_properties = {
-            'air_temperature': {
-                'dims': ['x', 'y', 'z'],
-                'units': 'degK',
-            }
-        }
-        raw_arrays = get_numpy_arrays_with_properties(input_state, input_properties)
-        raw_arrays = {key + '_tendency': value for key, value in raw_arrays.items()}
-        output_properties = {
-            'air_temperature_tendency': {
-                'dims_like': 'air_temperature',
-                'units': 'degK/s',
-            }
-        }
-        return_value = restore_data_arrays_with_properties(
-            raw_arrays, output_properties, input_state, input_properties
-        )
-        assert np.all(return_value['air_temperature_tendency'].coords['lon'] ==
-                      input_state['air_temperature'].coords['lon'])
-        assert return_value['air_temperature_tendency'].coords['lon'].attrs['units'] == 'degrees_E'
-        assert np.all(return_value['air_temperature_tendency'].coords['lat'] ==
-                      input_state['air_temperature'].coords['lat'])
-        assert return_value['air_temperature_tendency'].coords['lat'].attrs['units'] == 'degrees_N'
-        assert np.all(return_value['air_temperature_tendency'].coords['height'] ==
-                      input_state['air_temperature'].coords['height'])
-        assert return_value['air_temperature_tendency'].coords['height'].attrs['units'] == 'km'
-        assert return_value['air_temperature_tendency'].dims == input_state['air_temperature'].dims
+        assert return_value['air_temperature_tendency'].dims == ('z', 'x', 'y')
+        assert return_value['air_temperature_tendency'].shape == (4, 3, 2)
+        for i in range(4):
+            assert np.all(return_value['air_temperature_tendency'][i, :, :] == T_array[:, :, i])
 
     def test_restores_scalar_array(self):
         T_array = np.array(0.)
@@ -1322,7 +1060,7 @@ def test_raises_on_invalid_shape(self):
         }
         output_properties = {
             'foo': {
-                'dims_like': 'air_temperature',
+                'dims': ['x', 'y', 'z'],
                 'units': 'm',
             }
         }
@@ -1354,22 +1092,22 @@ def test_raises_on_raw_array_missing(self):
         }
         output_properties = {
             'foo': {
-                'dims_like': 'air_temperature',
+                'dims': ['x', 'y', 'z'],
                 'units': 'm',
             },
             'bar': {
-                'dims_like': 'air_temperature',
+                'dims': ['x', 'y', 'z'],
                 'units': 'm',
-            }
+            },
         }
         try:
             restore_data_arrays_with_properties(
                 raw_arrays, output_properties, input_state, input_properties
             )
-        except ValueError:
+        except KeyError:
             pass
         else:
-            raise AssertionError('should have raised ValueError')
+            raise AssertionError('should have raised KeyError')
 
     def test_restores_aliased_name(self):
         input_state = {
@@ -1390,7 +1128,7 @@ def test_restores_aliased_name(self):
         }
         output_properties = {
             'air_pressure': {
-                'dims_like': 'air_temperature',
+                'dims': ['x', 'y', 'z'],
                 'units': 'm',
                 'alias': 'p',
             },
@@ -1403,28 +1141,59 @@ def test_restores_aliased_name(self):
         assert np.all(data_arrays['air_pressure'].values == raw_arrays['p'])
         assert np.byte_bounds(data_arrays['air_pressure'].values) == np.byte_bounds(raw_arrays['p'])
 
-    def test_restores_when_name_has_alias(self):
+    def test_restores_using_alias_from_input(self):
         input_state = {
             'air_temperature': DataArray(
                 np.zeros([2, 2, 4]),
                 dims=['x', 'y', 'z'],
                 attrs={'units': 'degK'},
-            )
+            ),
+            'air_pressure': DataArray(
+                np.zeros([2, 2, 4]),
+                dims=['x', 'y', 'z'],
+                attrs={'units': 'degK'},
+            ),
         }
         input_properties = {
             'air_temperature': {
                 'dims': ['x', 'y', 'z'],
                 'units': 'degK',
-            }
+            },
+            'air_pressure': {
+                'dims': ['x', 'y', 'z'],
+                'units': 'degK',
+                'alias': 'p'
+            },
         }
+        raw_arrays = {
+            'p': np.zeros([2, 2, 4])
+        }
+        output_properties = {
+            'air_pressure': {
+                'dims': ['x', 'y', 'z'],
+                'units': 'm',
+            },
+        }
+        data_arrays = restore_data_arrays_with_properties(
+            raw_arrays, output_properties, input_state, input_properties
+        )
+        assert len(data_arrays.keys()) == 1
+        assert 'air_pressure' in data_arrays.keys()
+        assert np.all(data_arrays['air_pressure'].values == raw_arrays['p'])
+        assert np.byte_bounds(
+            data_arrays['air_pressure'].values) == np.byte_bounds(
+            raw_arrays['p'])
+
+    def test_restores_new_dims(self):
+        input_state = {}
+        input_properties = {}
         raw_arrays = {
             'air_pressure': np.zeros([2, 2, 4])
         }
         output_properties = {
             'air_pressure': {
-                'dims_like': 'air_temperature',
+                'dims': ['x', 'y', 'z'],
                 'units': 'm',
-                'alias': 'p',
             },
         }
         data_arrays = restore_data_arrays_with_properties(
@@ -1437,13 +1206,8 @@ def test_restores_when_name_has_alias(self):
             data_arrays['air_pressure'].values) == np.byte_bounds(
             raw_arrays['air_pressure'])
 
-    def test_restores_using_alias_from_input(self):
+    def test_restores_new_dims_with_wildcard(self):
         input_state = {
-            'air_temperature': DataArray(
-                np.zeros([2, 2, 4]),
-                dims=['x', 'y', 'z'],
-                attrs={'units': 'degK'},
-            ),
             'air_pressure': DataArray(
                 np.zeros([2, 2, 4]),
                 dims=['x', 'y', 'z'],
@@ -1451,22 +1215,18 @@ def test_restores_using_alias_from_input(self):
             ),
         }
         input_properties = {
-            'air_temperature': {
-                'dims': ['x', 'y', 'z'],
-                'units': 'degK',
-            },
             'air_pressure': {
-                'dims': ['x', 'y', 'z'],
+                'dims': ['*'],
                 'units': 'degK',
                 'alias': 'p'
             },
         }
         raw_arrays = {
-            'p': np.zeros([2, 2, 4])
+            'q': np.zeros([16, 2])
         }
         output_properties = {
-            'air_pressure': {
-                'dims_like': 'air_temperature',
+            'q': {
+                'dims': ['*', 'new_dim'],
                 'units': 'm',
             },
         }
@@ -1474,11 +1234,13 @@ def test_restores_using_alias_from_input(self):
             raw_arrays, output_properties, input_state, input_properties
         )
         assert len(data_arrays.keys()) == 1
-        assert 'air_pressure' in data_arrays.keys()
-        assert np.all(data_arrays['air_pressure'].values == raw_arrays['p'])
+        assert 'q' in data_arrays.keys()
+        assert np.all(data_arrays['q'].values.flatten() == raw_arrays['q'].flatten())
         assert np.byte_bounds(
-            data_arrays['air_pressure'].values) == np.byte_bounds(
-            raw_arrays['p'])
+            data_arrays['q'].values) == np.byte_bounds(
+            raw_arrays['q'])
+        assert data_arrays['q'].dims == ('x', 'y', 'z', 'new_dim')
+        assert data_arrays['q'].shape == (2, 2, 4, 2)
 
 
 if __name__ == '__main__':
diff --git a/tests/test_match_dims_like.py b/tests/test_match_dims_like.py
index 9be29b3..3fdd6e1 100644
--- a/tests/test_match_dims_like.py
+++ b/tests/test_match_dims_like.py
@@ -1,11 +1,7 @@
 import pytest
 from sympl import (
-    DataArray, set_direction_names, get_numpy_array,
-    restore_dimensions, get_numpy_arrays_with_properties,
-    restore_data_arrays_with_properties, InvalidStateError,
-    InvalidPropertyDictError)
+    DataArray, get_numpy_arrays_with_properties, InvalidStateError)
 import numpy as np
-import unittest
 
 
 def test_match_dims_like_hardcoded_dimensions_matching_lengths():
@@ -185,73 +181,5 @@ def test_match_dims_like_wildcard_dimensions_use_same_ordering():
     assert np.all(raw_arrays['air_temperature'] == raw_arrays['air_pressure'])
 
 
-class MultipleWildcardDimensionsTests(unittest.TestCase):
-
-    def setUp(self):
-        pass
-
-    def tearDown(self):
-        set_direction_names(x=(), y=(), z=())
-
-    def test_match_dims_like_x_y_z_matching_lengths(self):
-        set_direction_names(x=['lat'], y=['lon'], z=['mid_levels', 'interface_levels'])
-        input_state = {
-            'air_temperature': DataArray(
-                np.zeros([2, 3, 4]),
-                dims=['lat', 'lon', 'mid_levels'],
-                attrs={'units': 'degK'},
-            ),
-            'air_pressure': DataArray(
-                np.zeros([2, 3, 4]),
-                dims=['lat', 'lon', 'mid_levels'],
-                attrs={'units': 'Pa'},
-            ),
-        }
-        input_properties = {
-            'air_temperature': {
-                'dims': ['x', 'y', 'z'],
-                'units': 'degK',
-                'match_dims_like': 'air_pressure',
-            },
-            'air_pressure': {
-                'dims': ['x', 'y', 'z'],
-                'units': 'Pa',
-            },
-        }
-        raw_arrays = get_numpy_arrays_with_properties(
-            input_state, input_properties)
-        assert np.byte_bounds(raw_arrays['air_temperature']) == np.byte_bounds(input_state['air_temperature'].values)
-        assert np.byte_bounds(raw_arrays['air_pressure']) == np.byte_bounds(input_state['air_pressure'].values)
-
-    def test_match_dims_like_star_z_matching_lengths(self):
-        set_direction_names(x=['lat'], y=['lon'], z=['mid_levels', 'interface_levels'])
-        input_state = {
-            'air_temperature': DataArray(
-                np.zeros([2, 3, 4]),
-                dims=['lat', 'lon', 'interface_levels'],
-                attrs={'units': 'degK'},
-            ),
-            'air_pressure': DataArray(
-                np.zeros([2, 3, 4]),
-                dims=['lat', 'lon', 'interface_levels'],
-                attrs={'units': 'Pa'},
-            ),
-        }
-        input_properties = {
-            'air_temperature': {
-                'dims': ['*', 'z'],
-                'units': 'degK',
-                'match_dims_like': 'air_pressure',
-            },
-            'air_pressure': {
-                'dims': ['*', 'z'],
-                'units': 'Pa',
-            },
-        }
-        raw_arrays = get_numpy_arrays_with_properties(
-            input_state, input_properties)
-        assert np.byte_bounds(raw_arrays['air_temperature']) == np.byte_bounds(input_state['air_temperature'].values)
-        assert np.byte_bounds(raw_arrays['air_pressure']) == np.byte_bounds(input_state['air_pressure'].values)
-
 if __name__ == '__main__':
     pytest.main([__file__])
diff --git a/tests/test_state.py b/tests/test_state.py
new file mode 100644
index 0000000..62bd162
--- /dev/null
+++ b/tests/test_state.py
@@ -0,0 +1,226 @@
+import unittest
+from sympl import initialize_numpy_arrays_with_properties, DataArray
+import numpy as np
+
+
+class InitializeNumpyArraysWithPropertiesTests(unittest.TestCase):
+
+    def test_empty(self):
+        output_properties = {
+        }
+        input_properties = {
+        }
+        input_state = {
+        }
+        result = initialize_numpy_arrays_with_properties(
+            output_properties, input_state, input_properties)
+        assert result == {}
+
+    def test_single_output_single_dim(self):
+        output_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 's^-1',
+            }
+        }
+        input_state = {
+            'input1': np.zeros([10])
+        }
+
+        result = initialize_numpy_arrays_with_properties(
+            output_properties, input_state, input_properties)
+        assert len(result.keys()) == 1
+        assert 'output1' in result.keys()
+        assert result['output1'].shape == (10,)
+        assert np.all(result['output1'] == np.zeros([10]))
+        assert result['output1'].dtype == np.float64
+
+    def test_single_output_single_dim_aliased(self):
+        output_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 's^-1',
+                'alias': 'in1'
+            }
+        }
+        input_state = {
+            'in1': np.zeros([10])
+        }
+
+        result = initialize_numpy_arrays_with_properties(
+            output_properties, input_state, input_properties)
+        assert len(result.keys()) == 1
+        assert 'output1' in result.keys()
+        assert result['output1'].shape == (10,)
+        assert np.all(result['output1'] == np.zeros([10]))
+        assert result['output1'].dtype == np.float64
+
+    def test_single_output_single_dim_custom_dtype(self):
+        output_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm',
+                'dtype': np.int32,
+            }
+        }
+        input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 's^-1',
+            }
+        }
+        input_state = {
+            'input1': np.zeros([10])
+        }
+
+        result = initialize_numpy_arrays_with_properties(
+            output_properties, input_state, input_properties)
+        assert len(result.keys()) == 1
+        assert 'output1' in result.keys()
+        assert result['output1'].shape == (10,)
+        assert np.all(result['output1'] == np.zeros([10]))
+        assert result['output1'].dtype == np.int32
+
+    def test_single_output_two_dims(self):
+        output_properties = {
+            'output1': {
+                'dims': ['dim1', 'dim2'],
+                'units': 'm',
+            }
+        }
+        input_properties = {
+            'input1': {
+                'dims': ['dim1', 'dim2'],
+                'units': 's^-1',
+            }
+        }
+        input_state = {
+            'input1': np.zeros([3, 7])
+        }
+
+        result = initialize_numpy_arrays_with_properties(
+            output_properties, input_state, input_properties)
+        assert len(result.keys()) == 1
+        assert 'output1' in result.keys()
+        assert result['output1'].shape == (3, 7)
+        assert np.all(result['output1'] == np.zeros([3, 7]))
+
+    def test_single_output_two_dims_opposite_order(self):
+        output_properties = {
+            'output1': {
+                'dims': ['dim2', 'dim1'],
+                'units': 'm',
+            }
+        }
+        input_properties = {
+            'input1': {
+                'dims': ['dim1', 'dim2'],
+                'units': 's^-1',
+            }
+        }
+        input_state = {
+            'input1': np.zeros([3, 7])
+        }
+
+        result = initialize_numpy_arrays_with_properties(
+            output_properties, input_state, input_properties)
+        assert len(result.keys()) == 1
+        assert 'output1' in result.keys()
+        assert result['output1'].shape == (7, 3)
+        assert np.all(result['output1'] == np.zeros([7, 3]))
+
+    def test_two_outputs(self):
+        output_properties = {
+            'output1': {
+                'dims': ['dim2', 'dim1'],
+                'units': 'm',
+            },
+            'output2': {
+                'dims': ['dim1', 'dim2'],
+                'units': 'm',
+            }
+        }
+        input_properties = {
+            'input1': {
+                'dims': ['dim1', 'dim2'],
+                'units': 's^-1',
+            }
+        }
+        input_state = {
+            'input1': np.zeros([3, 7])
+        }
+
+        result = initialize_numpy_arrays_with_properties(
+            output_properties, input_state, input_properties)
+        assert len(result.keys()) == 2
+        assert 'output1' in result.keys()
+        assert result['output1'].shape == (7, 3)
+        assert np.all(result['output1'] == np.zeros([7, 3]))
+        assert 'output2' in result.keys()
+        assert result['output2'].shape == (3, 7)
+        assert np.all(result['output2'] == np.zeros([3, 7]))
+
+    def test_two_inputs(self):
+        output_properties = {
+            'output1': {
+                'dims': ['dim2', 'dim1'],
+                'units': 'm',
+            },
+        }
+        input_properties = {
+            'input1': {
+                'dims': ['dim1', 'dim2'],
+                'units': 's^-1',
+            },
+            'input2': {
+                'dims': ['dim2', 'dim1'],
+                'units': 's^-1',
+            }
+        }
+        input_state = {
+            'input1': np.zeros([3, 7]),
+            'input2': np.zeros([7, 3]),
+        }
+
+        result = initialize_numpy_arrays_with_properties(
+            output_properties, input_state, input_properties)
+        assert len(result.keys()) == 1
+        assert 'output1' in result.keys()
+        assert result['output1'].shape == (7, 3)
+        assert np.all(result['output1'] == np.zeros([7, 3]))
+
+    def test_single_dim_wildcard(self):
+        output_properties = {
+            'output1': {
+                'dims': ['*'],
+                'units': 'm',
+            }
+        }
+        input_properties = {
+            'input1': {
+                'dims': ['*'],
+                'units': 's^-1',
+            }
+        }
+        input_state = {
+            'input1': np.zeros([10])
+        }
+
+        result = initialize_numpy_arrays_with_properties(
+            output_properties, input_state, input_properties)
+        assert len(result.keys()) == 1
+        assert 'output1' in result.keys()
+        assert result['output1'].shape == (10,)
+        assert np.all(result['output1'] == np.zeros([10]))
diff --git a/tests/test_time.py b/tests/test_time.py
index 8a38709..e14462c 100644
--- a/tests/test_time.py
+++ b/tests/test_time.py
@@ -3,11 +3,11 @@
 import unittest
 import pytz
 try:
-    import netcdftime as nt
+    import cftime as ct
 except ImportError:
-    nt = None
+    ct = None
 
-netcdftime_not_installed = 'netcdftime module is not installed'
+netcdftime_not_installed = 'cftime module is not installed'
 
 
 class DatetimeBase(object):
@@ -48,7 +48,6 @@ def testMaxArgDatetimeHasCorrectValues(self):
         assert max_args_dt.microsecond == 10
 
 
-
 class ProlepticGregorianTests(unittest.TestCase, DatetimeBase):
 
     dt_class = real_datetime
@@ -71,24 +70,24 @@ def testTimezoneAwareDatetimeHasCorrectValues(self):
         assert tz_dt.microsecond == 0
 
 
-@unittest.skipIf(nt is None, netcdftime_not_installed)
+@unittest.skipIf(ct is None, netcdftime_not_installed)
 class NoLeapTests(unittest.TestCase, DatetimeBase):
 
     calendar = 'no_leap'
 
     @property
     def dt_class(self):
-        return nt.DatetimeNoLeap
+        return ct.DatetimeNoLeap
 
 
-@unittest.skipIf(nt is None, netcdftime_not_installed)
+@unittest.skipIf(ct is None, netcdftime_not_installed)
 class Datetime365DayTests(unittest.TestCase, DatetimeBase):
 
     calendar = '365_day'
 
     @property
     def dt_class(self):
-        return nt.DatetimeNoLeap
+        return ct.DatetimeNoLeap
 
     def test_incrementing_years_using_days(self):
         dt = datetime(1900, 1, 1, calendar=self.calendar)
@@ -107,22 +106,22 @@ def test_decrementing_years_using_days(self):
         assert dt_1850.hour == 0
 
 
-@unittest.skipIf(nt is None, netcdftime_not_installed)
+@unittest.skipIf(ct is None, netcdftime_not_installed)
 class AllLeapTests(unittest.TestCase, DatetimeBase):
     calendar = 'all_leap'
 
     @property
     def dt_class(self):
-        return nt.DatetimeAllLeap
+        return ct.DatetimeAllLeap
 
 
-@unittest.skipIf(nt is None, netcdftime_not_installed)
+@unittest.skipIf(ct is None, netcdftime_not_installed)
 class Datetime366DayTests(unittest.TestCase, DatetimeBase):
     calendar = '366_day'
 
     @property
     def dt_class(self):
-        return nt.DatetimeAllLeap
+        return ct.DatetimeAllLeap
 
     def test_incrementing_years_using_days(self):
         dt = datetime(1900, 1, 1, calendar=self.calendar)
@@ -141,13 +140,13 @@ def test_decrementing_years_using_days(self):
         assert dt_1850.hour == 0
 
 
-@unittest.skipIf(nt is None, netcdftime_not_installed)
+@unittest.skipIf(ct is None, netcdftime_not_installed)
 class Datetime360DayTests(unittest.TestCase, DatetimeBase):
     calendar = '360_day'
 
     @property
     def dt_class(self):
-        return nt.Datetime360Day
+        return ct.Datetime360Day
 
     def test_incrementing_years_using_days(self):
         dt = datetime(1900, 1, 1, calendar=self.calendar)
@@ -166,19 +165,19 @@ def test_decrementing_years_using_days(self):
         assert dt_1850.hour == 0
 
 
-@unittest.skipIf(nt is None, netcdftime_not_installed)
+@unittest.skipIf(ct is None, netcdftime_not_installed)
 class JulianTests(unittest.TestCase, DatetimeBase):
     calendar = 'julian'
 
     @property
     def dt_class(self):
-        return nt.DatetimeJulian
+        return ct.DatetimeJulian
 
 
-unittest.skipIf(nt is None, netcdftime_not_installed)
+@unittest.skipIf(ct is None, netcdftime_not_installed)
 class GregorianTests(unittest.TestCase, DatetimeBase):
     calendar = 'gregorian'
 
     @property
     def dt_class(self):
-        return nt.DatetimeGregorian
+        return ct.DatetimeGregorian
diff --git a/tests/test_time_differencing_wrapper.py b/tests/test_time_differencing_wrapper.py
new file mode 100644
index 0000000..3cd4499
--- /dev/null
+++ b/tests/test_time_differencing_wrapper.py
@@ -0,0 +1,150 @@
+from datetime import timedelta, datetime
+import unittest
+from sympl import (
+    TendencyComponent, Stepper, DiagnosticComponent, TimeDifferencingWrapper, DataArray
+)
+import pytest
+from numpy.testing import assert_allclose
+from copy import deepcopy
+
+
+class MockTendencyComponent(TendencyComponent):
+
+    def __init__(self):
+        self._num_updates = 0
+
+    def __call__(self, state):
+        self._num_updates += 1
+        return {}, {'num_updates': self._num_updates}
+
+
+class MockStepper(Stepper):
+
+    input_properties = {}
+
+    output_properties = {
+        'value': {
+            'dims': [],
+            'units': 'm'
+        }
+    }
+
+    diagnostic_properties = {
+        'num_updates': {
+            'dims': [],
+            'units': ''
+        }
+    }
+
+    def __init__(self):
+        self._num_updates = 0
+        super(MockStepper, self).__init__()
+
+    def array_call(self, state, timestep):
+        self._num_updates += 1
+
+        return (
+            {'num_updates': self._num_updates},
+            {'value': 1}
+        )
+
+
+class MockStepperThatExpects(Stepper):
+
+    input_properties = {'expected_field': {}}
+    output_properties = {'expected_field': {}}
+    diagnostic_properties = {'expected_field': {}}
+
+    def __init__(self, expected_value):
+        self._expected_value = expected_value
+
+    def __call__(self, state, timestep):
+
+        input_value = state['expected_field']
+        if input_value != self._expected_value:
+            raise ValueError(
+                'Expected {}, but got {}'.format(self._expected_value, input_value))
+
+        return deepcopy(state), state
+
+
+class MockTendencyComponentThatExpects(TendencyComponent):
+
+    input_properties = {'expected_field': {}}
+    tendency_properties = {'expected_field': {}}
+    diagnostic_properties = {'expected_field': {}}
+
+    def __init__(self, expected_value):
+        self._expected_value = expected_value
+
+    def __call__(self, state):
+
+        input_value = state['expected_field']
+        if input_value != self._expected_value:
+            raise ValueError(
+                'Expected {}, but got {}'.format(self._expected_value, input_value))
+
+        return deepcopy(state), state
+
+
+class MockDiagnosticComponentThatExpects(DiagnosticComponent):
+
+    input_properties = {'expected_field': {}}
+    diagnostic_properties = {'expected_field': {}}
+
+    def __init__(self, expected_value):
+        self._expected_value = expected_value
+
+    def __call__(self, state):
+
+        input_value = state['expected_field']
+        if input_value != self._expected_value:
+            raise ValueError(
+                'Expected {}, but got {}'.format(self._expected_value, input_value))
+
+        return state
+
+
+class TimeDifferencingTests(unittest.TestCase):
+
+    def setUp(self):
+        self.implicit = MockStepper()
+        self.wrapped = TimeDifferencingWrapper(self.implicit)
+        self.state = {
+            'time': timedelta(0),
+            'value': DataArray([0], attrs={'units': 'm'})
+        }
+
+    def tearDown(self):
+        self.component = None
+
+    def testWrapperCallsImplicit(self):
+        tendencies, diagnostics = self.wrapped(self.state, timedelta(seconds=1))
+        assert diagnostics['num_updates'].values == 1
+        tendencies, diagnostics = self.wrapped(self.state, timedelta(seconds=1))
+        assert diagnostics['num_updates'].values == 2
+        assert len(diagnostics.keys()) == 1
+
+    def testWrapperComputesTendency(self):
+        tendencies, diagnostics = self.wrapped(self.state, timedelta(seconds=1))
+        assert len(tendencies.keys()) == 1
+        assert 'value' in tendencies.keys()
+        assert isinstance(tendencies['value'], DataArray)
+        assert_allclose(tendencies['value'].to_units('m s^-1').values[0], 1.)
+        assert_allclose(tendencies['value'].values[0], 1.)
+
+    def testWrapperComputesTendencyWithUnitConversion(self):
+        state = {
+            'time': timedelta(0),
+            'value': DataArray([0.011], attrs={'units': 'km'})
+        }
+        tendencies, diagnostics = self.wrapped(state, timedelta(seconds=5))
+        assert 'value' in tendencies.keys()
+        assert isinstance(tendencies['value'], DataArray)
+        assert_allclose(tendencies['value'].to_units('m s^-1').values[0], -2)
+        assert_allclose(tendencies['value'].values[0], -2.)
+        assert_allclose(tendencies['value'].to_units('km s^-1').values[0], -0.002)
+
+
+if __name__ == '__main__':
+    pytest.main([__file__])
diff --git a/tests/test_timestepping.py b/tests/test_timestepping.py
index f4416e3..fb202dc 100644
--- a/tests/test_timestepping.py
+++ b/tests/test_timestepping.py
@@ -1,9 +1,11 @@
 import pytest
 import mock
 from sympl import (
-    Prognostic, Leapfrog, AdamsBashforth, DataArray)
-from datetime import timedelta
+    TendencyComponent, Leapfrog, AdamsBashforth, DataArray, SSPRungeKutta, timedelta,
+    InvalidPropertyDictError, ImplicitTendencyComponent)
+from sympl._core.units import units_are_compatible
 import numpy as np
+import warnings
 
 
 def same_list(list1, list2):
@@ -11,410 +13,840 @@ def same_list(list1, list2):
         [item in list2 for item in list1] + [item in list1 for item in list2]))
 
 
-class MockPrognostic(Prognostic):
+class MockEmptyTendencyComponent(TendencyComponent):
 
-    def __call__(self, state):
+    input_properties = {}
+    tendency_properties = {}
+    diagnostic_properties = {}
+
+    def array_call(self, state):
         return {}, {}
 
 
+class MockTendencyComponent(TendencyComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.tendency_properties = tendency_properties
+        self._diagnostic_output = diagnostic_output
+        self._tendency_output = tendency_output
+        self.times_called = 0
+        self.state_given = None
+        super(MockTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return self._tendency_output, self._diagnostic_output
+
+
+class MockImplicitTendencyComponent(ImplicitTendencyComponent):
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.tendency_properties = tendency_properties
+        self._diagnostic_output = diagnostic_output
+        self._tendency_output = tendency_output
+        self.times_called = 0
+        self.state_given = None
+        super(MockImplicitTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state, timestep):
+        self.times_called += 1
+        self.state_given = state
+        return self._tendency_output, self._diagnostic_output
+
+
+class PrognosticBase(object):
+
+    prognostic_class = MockTendencyComponent
+
+    def test_given_tendency_not_modified_with_two_components(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'tend1': {
+                'dims': ['dim1'],
+                'units': 'm/s',
+            }
+        }
+        diagnostic_output = {}
+        tendency_output_1 = {
+            'tend1': np.ones([10]) * 5.
+        }
+        prognostic1 = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output_1
+        )
+        tendency_output_2 = {
+            'tend1': np.ones([10]) * 5.
+        }
+        prognostic2 = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output_2
+        )
+        stepper = self.timestepper_class(
+            prognostic1, prognostic2, tendencies_in_diagnostics=True)
+        state = {
+            'time': timedelta(0),
+            'tend1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'},
+            )
+        }
+        _, _ = stepper(state, timedelta(seconds=5))
+        assert np.all(tendency_output_1['tend1'] == 5.)
+        assert np.all(tendency_output_2['tend1'] == 5.)
+
+    def test_input_state_not_modified_with_two_components(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'tend1': {
+                'dims': ['dim1'],
+                'units': 'm/s',
+            }
+        }
+        diagnostic_output = {}
+        tendency_output_1 = {
+            'tend1': np.ones([10]) * 5.
+        }
+        prognostic1 = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output_1
+        )
+        tendency_output_2 = {
+            'tend1': np.ones([10]) * 5.
+        }
+        prognostic2 = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output_2
+        )
+        stepper = self.timestepper_class(
+            prognostic1, prognostic2, tendencies_in_diagnostics=True)
+        state = {
+            'time': timedelta(0),
+            'tend1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'},
+            )
+        }
+        _, _ = stepper(state, timedelta(seconds=5))
+        assert state['tend1'].attrs['units'] == 'm'
+        assert np.all(state['tend1'].values == 1.)
+
+    def test_tendencies_in_diagnostics_no_tendency(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {}
+        diagnostic_output = {}
+        tendency_output = {}
+        prognostic = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        stepper = self.timestepper_class(
+            prognostic, tendencies_in_diagnostics=True)
+        state = {'time': timedelta(0)}
+        diagnostics, _ = stepper(state, timedelta(seconds=5))
+        assert diagnostics == {}
+
+    def test_tendencies_in_diagnostics_one_tendency(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            }
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 2.,
+        }
+        prognostic = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        stepper = self.timestepper_class(
+            prognostic, tendencies_in_diagnostics=True)
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10])*10.,
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+        }
+        diagnostics, _ = stepper(state, timedelta(seconds=5))
+        tendency_name = 'output1_tendency_from_{}'.format(stepper.__class__.__name__)
+        assert tendency_name in diagnostics.keys()
+        assert len(
+            diagnostics[tendency_name].dims) == 1
+        assert 'dim1' in diagnostics[tendency_name].dims
+        assert units_are_compatible(diagnostics[tendency_name].attrs['units'], 'm s^-1')
+        assert np.allclose(diagnostics[tendency_name].values, 2.)
+
+    def test_tendencies_in_diagnostics_one_tendency_with_component_name(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            }
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 2.,
+        }
+        prognostic = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        stepper = self.timestepper_class(
+            prognostic, tendencies_in_diagnostics=True, name='component')
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10])*10.,
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+        }
+        diagnostics, _ = stepper(state, timedelta(seconds=5))
+        assert 'output1_tendency_from_component' in diagnostics.keys()
+        assert len(
+            diagnostics['output1_tendency_from_component'].dims) == 1
+        assert 'dim1' in diagnostics['output1_tendency_from_component'].dims
+        assert units_are_compatible(diagnostics['output1_tendency_from_component'].attrs['units'], 'm s^-1')
+        assert np.allclose(diagnostics['output1_tendency_from_component'].values, 2.)
+
+    def test_copies_untouched_quantities(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            },
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 2.,
+        }
+        prognostic = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        stepper = self.timestepper_class(
+            prognostic, tendencies_in_diagnostics=True, name='component')
+        untouched_quantity = DataArray(
+            np.ones([10])*10.,
+            dims=['dim1'],
+            attrs={'units': 'J'}
+        )
+        state = {
+            'time': timedelta(0),
+            'output1': DataArray(
+                np.ones([10])*10.,
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+            'input1': untouched_quantity,
+        }
+        _, new_state = stepper(state, timedelta(seconds=5))
+        assert 'input1' in new_state.keys()
+        assert new_state['input1'].dims == untouched_quantity.dims
+        assert np.allclose(new_state['input1'].values, 10.)
+        assert new_state['input1'].attrs['units'] == 'J'
+
+    def test_stepper_requires_input_for_stepped_quantity(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            },
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 2.,
+        }
+        prognostic = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        stepper = self.timestepper_class(prognostic)
+        assert 'output1' in stepper.input_properties.keys()
+        assert stepper.input_properties['output1']['dims'] == ['dim1']
+        assert units_are_compatible(stepper.input_properties['output1']['units'], 'm')
+
+    def test_stepper_outputs_stepped_quantity(self):
+        input_properties = {}
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            },
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 2.,
+        }
+        prognostic = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        stepper = self.timestepper_class(prognostic)
+        assert 'output1' in stepper.output_properties.keys()
+        assert stepper.output_properties['output1']['dims'] == ['dim1']
+        assert units_are_compatible(stepper.output_properties['output1']['units'], 'm')
+
+    def test_stepper_requires_input_for_input_quantity(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1', 'dim2'],
+                'units': 's',
+            }
+        }
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            },
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 2.,
+        }
+        prognostic = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        stepper = self.timestepper_class(prognostic)
+        assert 'input1' in stepper.input_properties.keys()
+        assert stepper.input_properties['input1']['dims'] == ['dim1', 'dim2']
+        assert units_are_compatible(stepper.input_properties['input1']['units'], 's')
+        assert len(stepper.diagnostic_properties) == 0
+
+    def test_stepper_gives_diagnostic_quantity(self):
+        input_properties = {}
+        diagnostic_properties = {
+            'diag1': {
+                'dims': ['dim2'],
+                'units': '',
+            }
+        }
+        tendency_properties = {
+        }
+        diagnostic_output = {}
+        tendency_output = {
+        }
+        prognostic = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        stepper = self.timestepper_class(
+            prognostic, tendencies_in_diagnostics=True, name='component')
+        assert 'diag1' in stepper.diagnostic_properties.keys()
+        assert stepper.diagnostic_properties['diag1']['dims'] == ['dim2']
+        assert units_are_compatible(
+            stepper.diagnostic_properties['diag1']['units'], '')
+        assert len(stepper.input_properties) == 0
+        assert len(stepper.output_properties) == 0
+
+    def test_stepper_gives_diagnostic_tendency_quantity(self):
+        input_properties = {
+            'input1': {
+                'dims': ['dim1', 'dim2'],
+                'units': 's',
+            }
+        }
+        diagnostic_properties = {}
+        tendency_properties = {
+            'output1': {
+                'dims': ['dim1'],
+                'units': 'm/s'
+            },
+        }
+        diagnostic_output = {}
+        tendency_output = {
+            'output1': np.ones([10]) * 2.,
+        }
+        prognostic = self.prognostic_class(
+            input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output
+        )
+        stepper = self.timestepper_class(
+            prognostic, tendencies_in_diagnostics=True)
+        tendency_name = 'output1_tendency_from_{}'.format(stepper.__class__.__name__)
+        assert tendency_name in stepper.diagnostic_properties.keys()
+        assert len(stepper.diagnostic_properties) == 1
+        assert stepper.diagnostic_properties[tendency_name]['dims'] == ['dim1']
+        assert units_are_compatible(stepper.input_properties['output1']['units'], 'm')
+        assert units_are_compatible(stepper.diagnostic_properties[tendency_name]['units'], 'm/s')
+
+
+class ImplicitPrognosticBase(PrognosticBase):
+
+    prognostic_class = MockImplicitTendencyComponent
+
+
 class TimesteppingBase(object):
 
     timestepper_class = None
 
     def test_unused_quantities_carried_over(self):
-        state = {'air_temperature': 273.}
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        state = {'time': timedelta(0), 'air_temperature': 273.}
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         timestep = timedelta(seconds=1.)
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'air_temperature': 273.}
-        assert new_state == {'air_temperature': 273.}
-
-    def test_timestepper_reveals_inputs(self):
-        prog1 = MockPrognostic()
-        prog1.input_properties = {'input1': {}}
-        time_stepper = self.timestepper_class([prog1])
-        assert same_list(time_stepper.inputs, ('input1',))
-
-    def test_timestepper_combines_inputs(self):
-        prog1 = MockPrognostic()
-        prog1.input_properties = {'input1': {}}
-        prog2 = MockPrognostic()
-        prog2.input_properties = {'input2': {}}
-        time_stepper = self.timestepper_class([prog1, prog2])
-        assert same_list(time_stepper.inputs, ('input1', 'input2'))
-
-    def test_timestepper_doesnt_duplicate_inputs(self):
-        prog1 = MockPrognostic()
-        prog1.input_properties = {'input1': {}}
-        prog2 = MockPrognostic()
-        prog2.input_properties = {'input1': {}}
-        time_stepper = self.timestepper_class([prog1, prog2])
-        assert same_list(time_stepper.inputs, ('input1',))
-
-    def test_timestepper_reveals_outputs(self):
-        prog1 = MockPrognostic()
-        prog1.tendency_properties = {'output1': {}}
-        time_stepper = self.timestepper_class([prog1])
-        assert same_list(time_stepper.outputs, ('output1',))
-
-    def test_timestepper_combines_outputs(self):
-        prog1 = MockPrognostic()
-        prog1.tendency_properties = {'output1': {}}
-        prog2 = MockPrognostic()
-        prog2.tendency_properties = {'output2': {}}
-        time_stepper = self.timestepper_class([prog1, prog2])
-        assert same_list(time_stepper.outputs, ('output1', 'output2'))
-
-    def test_timestepper_doesnt_duplicate_outputs(self):
-        prog1 = MockPrognostic()
-        prog1.tendency_properties = {'output1': {}}
-        prog2 = MockPrognostic()
-        prog2.tendency_properties = {'output1': {}}
-        time_stepper = self.timestepper_class([prog1, prog2])
-        assert same_list(time_stepper.outputs, ('output1',))
-
-    @mock.patch.object(MockPrognostic, '__call__')
+        assert state == {'time': timedelta(0), 'air_temperature': 273.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 273.}
+
+    def test_timestepper_reveals_prognostics(self):
+        prog1 = MockEmptyTendencyComponent()
+        prog1.input_properties = {'input1': {'dims': ['dim1'], 'units': 'm'}}
+        time_stepper = self.timestepper_class(prog1)
+        assert same_list(time_stepper.prognostic_list, (prog1,))
+
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_float_no_change_one_step(self, mock_prognostic_call):
         mock_prognostic_call.return_value = ({'air_temperature': 0.}, {})
-        state = {'air_temperature': 273.}
+        state = {'time': timedelta(0), 'air_temperature': 273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'air_temperature': 273.}
-        assert new_state == {'air_temperature': 273.}
+        assert state == {'time': timedelta(0), 'air_temperature': 273.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 273.}
         assert diagnostics == {}
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_float_no_change_one_step_diagnostic(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
             {'air_temperature': 0.}, {'foo': 'bar'})
-        state = {'air_temperature': 273.}
+        state = {'time': timedelta(0), 'air_temperature': 273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'air_temperature': 273.}
-        assert new_state == {'air_temperature': 273.}
+        assert state == {'time': timedelta(0), 'air_temperature': 273.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 273.}
         assert diagnostics == {'foo': 'bar'}
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_float_no_change_three_steps(self, mock_prognostic_call):
         mock_prognostic_call.return_value = ({'air_temperature': 0.}, {})
-        state = {'air_temperature': 273.}
+        state = {'time': timedelta(0), 'air_temperature': 273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'air_temperature': 273.}
-        assert new_state == {'air_temperature': 273.}
+        assert state == {'time': timedelta(0), 'air_temperature': 273.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 273.}
         state = new_state
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'air_temperature': 273.}
-        assert new_state == {'air_temperature': 273.}
+        assert state == {'time': timedelta(0), 'air_temperature': 273.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 273.}
         state = new_state
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'air_temperature': 273.}
-        assert new_state == {'air_temperature': 273.}
+        assert state == {'time': timedelta(0), 'air_temperature': 273.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 273.}
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_float_one_step(self, mock_prognostic_call):
         mock_prognostic_call.return_value = ({'air_temperature': 1.}, {})
-        state = {'air_temperature': 273.}
+        state = {'time': timedelta(0), 'air_temperature': 273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'air_temperature': 273.}
-        assert new_state == {'air_temperature': 274.}
+        assert state == {'time': timedelta(0), 'air_temperature': 273.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 274.}
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_float_one_step_with_units(self, mock_prognostic_call):
         mock_prognostic_call.return_value = ({'eastward_wind': DataArray(0.02, attrs={'units': 'km/s^2'})}, {})
-        state = {'eastward_wind': DataArray(1., attrs={'units': 'm/s'})}
+        state = {'time': timedelta(0), 'eastward_wind': DataArray(1., attrs={'units': 'm/s'})}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'eastward_wind': DataArray(1., attrs={'units': 'm/s'})}
-        assert new_state == {'eastward_wind': DataArray(21., attrs={'units': 'm/s'})}
+        assert state == {'time': timedelta(0), 'eastward_wind': DataArray(1., attrs={'units': 'm/s'})}
+        assert same_list(new_state.keys(), ['time', 'eastward_wind'])
+        assert np.allclose(new_state['eastward_wind'].values, 21.)
+        assert new_state['eastward_wind'].attrs['units'] == 'm/s'
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_float_three_steps(self, mock_prognostic_call):
         mock_prognostic_call.return_value = ({'air_temperature': 1.}, {})
-        state = {'air_temperature': 273.}
+        state = {'time': timedelta(0), 'air_temperature': 273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'air_temperature': 273.}
-        assert new_state == {'air_temperature': 274.}
+        assert state == {'time': timedelta(0), 'air_temperature': 273.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 274.}
         state = new_state
         diagnostics, new_state = time_stepper.__call__(new_state, timestep)
-        assert state == {'air_temperature': 274.}
-        assert new_state == {'air_temperature': 275.}
+        assert state == {'time': timedelta(0), 'air_temperature': 274.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 275.}
         state = new_state
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert state == {'air_temperature': 275.}
-        assert new_state == {'air_temperature': 276.}
+        assert state == {'time': timedelta(0), 'air_temperature': 275.}
+        assert new_state == {'time': timedelta(0), 'air_temperature': 276.}
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_array_no_change_one_step(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
             {'air_temperature': np.zeros((3, 3))}, {})
-        state = {'air_temperature': np.ones((3, 3))*273.}
+        state = {'time': timedelta(0), 'air_temperature': np.ones((3, 3))*273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*273.).all()
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_array_no_change_three_steps(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
             {'air_temperature': np.ones((3, 3))*0.}, {})
-        state = {'air_temperature': np.ones((3, 3))*273.}
+        state = {'time': timedelta(0), 'air_temperature': np.ones((3, 3))*273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*273.).all()
         state = new_state
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*273.).all()
         state = new_state
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*273.).all()
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_array_one_step(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
             {'air_temperature': np.ones((3, 3))*1.}, {})
-        state = {'air_temperature': np.ones((3, 3))*273.}
+        state = {'time': timedelta(0), 'air_temperature': np.ones((3, 3))*273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*274.).all()
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_array_three_steps(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
             {'air_temperature': np.ones((3, 3))*1.}, {})
-        state = {'air_temperature': np.ones((3, 3))*273.}
+        state = {'time': timedelta(0), 'air_temperature': np.ones((3, 3))*273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*274.).all()
         state = new_state
         diagnostics, new_state = time_stepper.__call__(new_state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*274.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*275.).all()
         state = new_state
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*275.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*276.).all()
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_dataarray_no_change_one_step(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
             {'air_temperature':
                  DataArray(np.zeros((3, 3)), attrs={'units': 'K/s'})},
             {})
-        state = {'air_temperature': DataArray(np.ones((3, 3))*273.,
+        state = {'time': timedelta(0), 'air_temperature': DataArray(np.ones((3, 3))*273.,
                                               attrs={'units': 'K'})}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'].values == np.ones((3, 3))*273.).all()
         assert len(state['air_temperature'].attrs) == 1
         assert state['air_temperature'].attrs['units'] == 'K'
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'].values == np.ones((3, 3))*273.).all()
         assert len(new_state['air_temperature'].attrs) == 1
         assert new_state['air_temperature'].attrs['units'] == 'K'
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_dataarray_no_change_three_steps(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
             {'air_temperature':
                 DataArray(np.zeros((3, 3)), attrs={'units': 'K/s'})},
             {})
-        state = {'air_temperature': DataArray(np.ones((3, 3))*273.,
+        state = {'time': timedelta(0), 'air_temperature': DataArray(np.ones((3, 3))*273.,
                                               attrs={'units': 'K'})}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
         assert len(state['air_temperature'].attrs) == 1
         assert state['air_temperature'].attrs['units'] == 'K'
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*273.).all()
         state = new_state
         assert len(state['air_temperature'].attrs) == 1
         assert state['air_temperature'].attrs['units'] == 'K'
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*273.).all()
         state = new_state
         assert len(state['air_temperature'].attrs) == 1
         assert state['air_temperature'].attrs['units'] == 'K'
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*273.).all()
         assert len(new_state['air_temperature'].attrs) == 1
         assert new_state['air_temperature'].attrs['units'] == 'K'
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_dataarray_one_step(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
             {'air_temperature':
                 DataArray(np.ones((3, 3)), attrs={'units': 'K/s'})},
             {})
-        state = {'air_temperature': DataArray(np.ones((3, 3))*273.,
+        state = {'time': timedelta(0), 'air_temperature': DataArray(np.ones((3, 3))*273.,
                                               attrs={'units': 'K'})}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
         assert len(state['air_temperature'].attrs) == 1
         assert state['air_temperature'].attrs['units'] == 'K'
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*274.).all()
         assert len(new_state['air_temperature'].attrs) == 1
         assert new_state['air_temperature'].attrs['units'] == 'K'
 
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_dataarray_three_steps(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
             {'air_temperature':
                 DataArray(np.ones((3, 3)), attrs={'units': 'K/s'})},
             {})
-        state = {'air_temperature': DataArray(np.ones((3, 3))*273.,
+        state = {'time': timedelta(0), 'air_temperature': DataArray(np.ones((3, 3))*273.,
                                               attrs={'units': 'K'})}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
         assert len(state['air_temperature'].attrs) == 1
         assert state['air_temperature'].attrs['units'] == 'K'
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*274.).all()
         state = new_state
         assert len(state['air_temperature'].attrs) == 1
         assert state['air_temperature'].attrs['units'] == 'K'
         diagnostics, new_state = time_stepper.__call__(new_state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*274.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*275.).all()
         state = new_state
         assert len(state['air_temperature'].attrs) == 1
         assert state['air_temperature'].attrs['units'] == 'K'
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
+        assert same_list(state.keys(), ['time', 'air_temperature'])
         assert (state['air_temperature'] == np.ones((3, 3))*275.).all()
-        assert list(new_state.keys()) == ['air_temperature']
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
         assert (new_state['air_temperature'] == np.ones((3, 3))*276.).all()
         assert len(new_state['air_temperature'].attrs) == 1
         assert new_state['air_temperature'].attrs['units'] == 'K'
 
-
-class TestLeapfrog(TimesteppingBase):
-    timestepper_class = Leapfrog
-
-
-class TestAdamsBashforthSecondOrder(TimesteppingBase):
-    def timestepper_class(self, *args):
-        return AdamsBashforth(*args, order=2)
-
-
-class TestAdamsBashforthThirdOrder(TimesteppingBase):
-    def timestepper_class(self, *args):
-        return AdamsBashforth(*args, order=3)
-
-
-class TestAdamsBashforthFourthOrder(TimesteppingBase):
-    def timestepper_class(self, *args):
-        return AdamsBashforth(*args, order=4)
-
-    @mock.patch.object(MockPrognostic, '__call__')
+    @mock.patch.object(MockEmptyTendencyComponent, '__call__')
     def test_array_four_steps(self, mock_prognostic_call):
         mock_prognostic_call.return_value = (
-            {'air_temperature': np.ones((3, 3))*1.}, {})
-        state = {'air_temperature': np.ones((3, 3))*273.}
+            {'air_temperature': np.ones((3, 3)) * 1.}, {})
+        state = {'time': timedelta(0), 'air_temperature': np.ones((3, 3)) * 273.}
         timestep = timedelta(seconds=1.)
-        time_stepper = self.timestepper_class([MockPrognostic()])
+        time_stepper = self.timestepper_class(MockEmptyTendencyComponent())
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
-        assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-        assert list(new_state.keys()) == ['air_temperature']
-        assert (new_state['air_temperature'] == np.ones((3, 3))*274.).all()
+        assert same_list(state.keys(), ['time', 'air_temperature'])
+        assert (state['air_temperature'] == np.ones((3, 3)) * 273.).all()
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
+        assert (new_state['air_temperature'] == np.ones((3, 3)) * 274.).all()
         state = new_state
         diagnostics, new_state = time_stepper.__call__(new_state, timestep)
-        assert list(state.keys()) == ['air_temperature']
-        assert (state['air_temperature'] == np.ones((3, 3))*274.).all()
-        assert list(new_state.keys()) == ['air_temperature']
-        assert (new_state['air_temperature'] == np.ones((3, 3))*275.).all()
+        assert same_list(state.keys(), ['time', 'air_temperature'])
+        assert (state['air_temperature'] == np.ones((3, 3)) * 274.).all()
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
+        assert (new_state['air_temperature'] == np.ones((3, 3)) * 275.).all()
         state = new_state
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
-        assert (state['air_temperature'] == np.ones((3, 3))*275.).all()
-        assert list(new_state.keys()) == ['air_temperature']
-        assert (new_state['air_temperature'] == np.ones((3, 3))*276.).all()
+        assert same_list(state.keys(), ['time', 'air_temperature'])
+        assert (state['air_temperature'] == np.ones((3, 3)) * 275.).all()
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
+        assert (new_state['air_temperature'] == np.ones((3, 3)) * 276.).all()
         state = new_state
         diagnostics, new_state = time_stepper.__call__(state, timestep)
-        assert list(state.keys()) == ['air_temperature']
-        assert (state['air_temperature'] == np.ones((3, 3))*276.).all()
-        assert list(new_state.keys()) == ['air_temperature']
-        assert (new_state['air_temperature'] == np.ones((3, 3))*277.).all()
+        assert same_list(state.keys(), ['time', 'air_temperature'])
+        assert (state['air_temperature'] == np.ones((3, 3)) * 276.).all()
+        assert same_list(new_state.keys(), ['time', 'air_temperature'])
+        assert (new_state['air_temperature'] == np.ones((3, 3)) * 277.).all()
+
+
+class TestAdamsBashforthFirstOrder(TimesteppingBase, PrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['order'] = 1
+        return AdamsBashforth(*args, **kwargs)
+
+
+class TestAdamsBashforthFirstOrderImplicitPrognostic(TimesteppingBase, ImplicitPrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['order'] = 1
+        return AdamsBashforth(*args, **kwargs)
+
+
+class TestSSPRungeKuttaTwoStep(TimesteppingBase, PrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['stages'] = 2
+        return SSPRungeKutta(*args, **kwargs)
+
+
+class TestSSPRungeKuttaTwoStepImplicitPrognostic(TimesteppingBase, ImplicitPrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['stages'] = 2
+        return SSPRungeKutta(*args, **kwargs)
+
+
+class TestSSPRungeKuttaThreeStep(TimesteppingBase, PrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['stages'] = 3
+        return SSPRungeKutta(*args, **kwargs)
+
+class TestSSPRungeKuttaThreeStepImplicitPrognostic(TimesteppingBase, ImplicitPrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['stages'] = 3
+        return SSPRungeKutta(*args, **kwargs)
+
+
+class TestLeapfrog(TimesteppingBase, PrognosticBase):
+
+    timestepper_class = Leapfrog
+
+
+class TestLeapfrogImplicitPrognostic(TimesteppingBase, ImplicitPrognosticBase):
+
+    timestepper_class = Leapfrog
+
+
+class TestAdamsBashforthSecondOrder(TimesteppingBase, PrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['order'] = 2
+        return AdamsBashforth(*args, **kwargs)
+
+
+class TestAdamsBashforthSecondOrderImplicitPrognostic(TimesteppingBase, ImplicitPrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['order'] = 2
+        return AdamsBashforth(*args, **kwargs)
+
+
+class TestAdamsBashforthThirdOrder(TimesteppingBase, PrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['order'] = 3
+        return AdamsBashforth(*args, **kwargs)
+
+
+class TestAdamsBashforthThirdOrderImplicitPrognostic(TimesteppingBase, ImplicitPrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['order'] = 3
+        return AdamsBashforth(*args, **kwargs)
+
+
+class TestAdamsBashforthFourthOrder(TimesteppingBase, PrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['order'] = 4
+        return AdamsBashforth(*args, **kwargs)
+
+
+class TestAdamsBashforthFourthOrderImplicitPrognostic(TimesteppingBase, ImplicitPrognosticBase):
+
+    def timestepper_class(self, *args, **kwargs):
+        kwargs['order'] = 4
+        return AdamsBashforth(*args, **kwargs)
 
 
-@mock.patch.object(MockPrognostic, '__call__')
+@mock.patch.object(MockEmptyTendencyComponent, '__call__')
 def test_leapfrog_float_two_steps_filtered(mock_prognostic_call):
     """Test that the Asselin filter is being correctly applied"""
     mock_prognostic_call.return_value = ({'air_temperature': 0.}, {})
-    state = {'air_temperature': 273.}
+    state = {'time': timedelta(0), 'air_temperature': 273.}
     timestep = timedelta(seconds=1.)
-    time_stepper = Leapfrog([MockPrognostic()], asselin_strength=0.5, alpha=1.)
+    time_stepper = Leapfrog(MockEmptyTendencyComponent(), asselin_strength=0.5, alpha=1.)
     diagnostics, new_state = time_stepper.__call__(state, timestep)
-    assert state == {'air_temperature': 273.}
-    assert new_state == {'air_temperature': 273.}
+    assert state == {'time': timedelta(0), 'air_temperature': 273.}
+    assert new_state == {'time': timedelta(0), 'air_temperature': 273.}
     state = new_state
     mock_prognostic_call.return_value = ({'air_temperature': 2.}, {})
     diagnostics, new_state = time_stepper.__call__(state, timestep)
     # Asselin filter modifies the current state
-    assert state == {'air_temperature': 274.}
-    assert new_state == {'air_temperature': 277.}
+    assert state == {'time': timedelta(0), 'air_temperature': 274.}
+    assert new_state == {'time': timedelta(0), 'air_temperature': 277.}
 
 
-@mock.patch.object(MockPrognostic, '__call__')
+@mock.patch.object(MockEmptyTendencyComponent, '__call__')
 def test_leapfrog_requires_same_timestep(mock_prognostic_call):
     """Test that the Asselin filter is being correctly applied"""
     mock_prognostic_call.return_value = ({'air_temperature': 0.}, {})
-    state = {'air_temperature': 273.}
-    time_stepper = Leapfrog([MockPrognostic()], asselin_strength=0.5)
+    state = {'time': timedelta(0), 'air_temperature': 273.}
+    time_stepper = Leapfrog([MockEmptyTendencyComponent()], asselin_strength=0.5)
     diagnostics, state = time_stepper.__call__(state, timedelta(seconds=1.))
     try:
         time_stepper.__call__(state, timedelta(seconds=2.))
@@ -426,12 +858,12 @@ def test_leapfrog_requires_same_timestep(mock_prognostic_call):
         raise AssertionError('Leapfrog must require timestep to be constant')
 
 
-@mock.patch.object(MockPrognostic, '__call__')
+@mock.patch.object(MockEmptyTendencyComponent, '__call__')
 def test_adams_bashforth_requires_same_timestep(mock_prognostic_call):
     """Test that the Asselin filter is being correctly applied"""
     mock_prognostic_call.return_value = ({'air_temperature': 0.}, {})
-    state = {'air_temperature': 273.}
-    time_stepper = AdamsBashforth([MockPrognostic()])
+    state = {'time': timedelta(0), 'air_temperature': 273.}
+    time_stepper = AdamsBashforth(MockEmptyTendencyComponent())
     state = time_stepper.__call__(state, timedelta(seconds=1.))
     try:
         time_stepper.__call__(state, timedelta(seconds=2.))
@@ -444,52 +876,52 @@ def test_adams_bashforth_requires_same_timestep(mock_prognostic_call):
             'AdamsBashforth must require timestep to be constant')
 
 
-@mock.patch.object(MockPrognostic, '__call__')
+@mock.patch.object(MockEmptyTendencyComponent, '__call__')
 def test_leapfrog_array_two_steps_filtered(mock_prognostic_call):
     """Test that the Asselin filter is being correctly applied"""
     mock_prognostic_call.return_value = (
         {'air_temperature': np.ones((3, 3))*0.}, {})
-    state = {'air_temperature': np.ones((3, 3))*273.}
+    state = {'time': timedelta(0), 'air_temperature': np.ones((3, 3))*273.}
     timestep = timedelta(seconds=1.)
-    time_stepper = Leapfrog([MockPrognostic()], asselin_strength=0.5, alpha=1.)
+    time_stepper = Leapfrog(MockEmptyTendencyComponent(), asselin_strength=0.5, alpha=1.)
     diagnostics, new_state = time_stepper.__call__(state, timestep)
-    assert list(state.keys()) == ['air_temperature']
+    assert same_list(state.keys(), ['time', 'air_temperature'])
     assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-    assert list(new_state.keys()) == ['air_temperature']
+    assert same_list(new_state.keys(), ['time', 'air_temperature'])
     assert (new_state['air_temperature'] == np.ones((3, 3))*273.).all()
     state = new_state
     mock_prognostic_call.return_value = (
         {'air_temperature': np.ones((3, 3))*2.}, {})
     diagnostics, new_state = time_stepper.__call__(state, timestep)
     # Asselin filter modifies the current state
-    assert list(state.keys()) == ['air_temperature']
+    assert same_list(state.keys(), ['time', 'air_temperature'])
     assert (state['air_temperature'] == np.ones((3, 3))*274.).all()
-    assert list(new_state.keys()) == ['air_temperature']
+    assert same_list(new_state.keys(), ['time', 'air_temperature'])
     assert (new_state['air_temperature'] == np.ones((3, 3))*277.).all()
 
 
-@mock.patch.object(MockPrognostic, '__call__')
+@mock.patch.object(MockEmptyTendencyComponent, '__call__')
 def test_leapfrog_array_two_steps_filtered_williams(mock_prognostic_call):
     """Test that the Asselin filter is being correctly applied with a
     Williams factor of alpha=0.5"""
     mock_prognostic_call.return_value = (
         {'air_temperature': np.ones((3, 3))*0.}, {})
-    state = {'air_temperature': np.ones((3, 3))*273.}
+    state = {'time': timedelta(0), 'air_temperature': np.ones((3, 3))*273.}
     timestep = timedelta(seconds=1.)
-    time_stepper = Leapfrog([MockPrognostic()], asselin_strength=0.5, alpha=0.5)
+    time_stepper = Leapfrog(MockEmptyTendencyComponent(), asselin_strength=0.5, alpha=0.5)
     diagnostics, new_state = time_stepper.__call__(state, timestep)
-    assert list(state.keys()) == ['air_temperature']
+    assert same_list(state.keys(), ['time', 'air_temperature'])
     assert (state['air_temperature'] == np.ones((3, 3))*273.).all()
-    assert list(new_state.keys()) == ['air_temperature']
+    assert same_list(new_state.keys(), ['time', 'air_temperature'])
     assert (new_state['air_temperature'] == np.ones((3, 3))*273.).all()
     state = new_state
     mock_prognostic_call.return_value = (
         {'air_temperature': np.ones((3, 3))*2.}, {})
     diagnostics, new_state = time_stepper.__call__(state, timestep)
     # Asselin filter modifies the current state
-    assert list(state.keys()) == ['air_temperature']
+    assert same_list(state.keys(), ['time', 'air_temperature'])
     assert (state['air_temperature'] == np.ones((3, 3))*273.5).all()
-    assert list(new_state.keys()) == ['air_temperature']
+    assert same_list(new_state.keys(), ['time', 'air_temperature'])
     assert (new_state['air_temperature'] == np.ones((3, 3))*276.5).all()
 
 
diff --git a/tests/test_tracers.py b/tests/test_tracers.py
new file mode 100644
index 0000000..93f3a3f
--- /dev/null
+++ b/tests/test_tracers.py
@@ -0,0 +1,806 @@
+from sympl._core.tracers import TracerPacker, reset_tracers, reset_packers
+from sympl import (
+    TendencyComponent, Stepper, DiagnosticComponent, ImplicitTendencyComponent, register_tracer,
+    get_tracer_unit_dict, units_are_compatible, DataArray, InvalidPropertyDictError
+)
+import unittest
+import numpy as np
+import pytest
+from datetime import timedelta
+
+
+class MockTendencyComponent(TendencyComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
+
+    def __init__(self, **kwargs):
+        self.input_properties = kwargs.pop('input_properties', {})
+        self.diagnostic_properties = kwargs.pop('diagnostic_properties', {})
+        self.tendency_properties = kwargs.pop('tendency_properties', {})
+        self.diagnostic_output = {}
+        self.tendency_output = {}
+        self.times_called = 0
+        self.state_given = None
+        super(MockTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return self.tendency_output, self.diagnostic_output
+
+
+class MockTracerTendencyComponent(TendencyComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
+
+    uses_tracers = True
+    tracer_dims = ('tracer', '*')
+
+    def __init__(self, **kwargs):
+        prepend_tracers = kwargs.pop('prepend_tracers', None)
+        if prepend_tracers is not None:
+            self.prepend_tracers = prepend_tracers
+        self.input_properties = kwargs.pop('input_properties', {})
+        self.diagnostic_properties = kwargs.pop('diagnostic_properties', {})
+        self.tendency_properties = kwargs.pop('tendency_properties', {})
+        self.diagnostic_output = {}
+        self.times_called = 0
+        self.state_given = None
+        super(MockTracerTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return_state = {}
+        return_state.update(state)
+        return_state.pop('time')
+        return return_state, self.diagnostic_output
+
+
+class MockImplicitTendencyComponent(ImplicitTendencyComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
+
+    def __init__( self, **kwargs):
+        self.input_properties = kwargs.pop('input_properties', {})
+        self.diagnostic_properties = kwargs.pop('diagnostic_properties', {})
+        self.tendency_properties = kwargs.pop('tendency_properties', {})
+        self.diagnostic_output = {}
+        self.tendency_output = {}
+        self.times_called = 0
+        self.state_given = None
+        self.timestep_given = None
+        super(MockImplicitTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state, timestep):
+        self.times_called += 1
+        self.state_given = state
+        self.timestep_given = timestep
+        return self.tendency_output, self.diagnostic_output
+
+
+class MockTracerImplicitTendencyComponent(ImplicitTendencyComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
+
+    uses_tracers = True
+    tracer_dims = ('tracer', '*')
+
+    def __init__(self, **kwargs):
+        prepend_tracers = kwargs.pop('prepend_tracers', None)
+        if prepend_tracers is not None:
+            self.prepend_tracers = prepend_tracers
+        self.input_properties = kwargs.pop('input_properties', {})
+        self.diagnostic_properties = kwargs.pop('diagnostic_properties', {})
+        self.tendency_properties = kwargs.pop('tendency_properties', {})
+        self.diagnostic_output = {}
+        self.times_called = 0
+        self.state_given = None
+        self.timestep_given = None
+        super(MockTracerImplicitTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state, timestep):
+        self.times_called += 1
+        self.state_given = state
+        self.timestep_given = timestep
+        return_state = {}
+        return_state.update(state)
+        return_state.pop('time')
+        return return_state, self.diagnostic_output
+
+
+class MockDiagnosticComponent(DiagnosticComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+
+    def __init__(self, **kwargs):
+        self.input_properties = kwargs.pop('input_properties', {})
+        self.diagnostic_properties = kwargs.pop('diagnostic_properties', {})
+        self.diagnostic_output = {}
+        self.times_called = 0
+        self.state_given = None
+        super(MockDiagnosticComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return self.diagnostic_output
+
+
+class MockStepper(Stepper):
+
+    input_properties = None
+    diagnostic_properties = None
+    output_properties = None
+
+    def __init__(self, **kwargs):
+        self.input_properties = kwargs.pop('input_properties', {})
+        self.diagnostic_properties = kwargs.pop('diagnostic_properties', {})
+        self.output_properties = kwargs.pop('output_properties', {})
+        self.diagnostic_output = {}
+        self.state_output = {}
+        self.times_called = 0
+        self.state_given = None
+        self.timestep_given = None
+        super(MockStepper, self).__init__(**kwargs)
+
+    def array_call(self, state, timestep):
+        self.times_called += 1
+        self.state_given = state
+        self.timestep_given = timestep
+        return self.diagnostic_output, self.state_output
+
+
+class MockTracerStepper(Stepper):
+
+    input_properties = None
+    diagnostic_properties = None
+    output_properties = None
+
+    uses_tracers = True
+    tracer_dims = ('tracer', '*')
+
+    def __init__(self, **kwargs):
+        prepend_tracers = kwargs.pop('prepend_tracers', None)
+        if prepend_tracers is not None:
+            self.prepend_tracers = prepend_tracers
+        self.input_properties = kwargs.pop('input_properties', {})
+        self.diagnostic_properties = kwargs.pop('diagnostic_properties', {})
+        self.output_properties = kwargs.pop('output_properties', {})
+        self.diagnostic_output = {}
+        self.state_output = {}
+        self.times_called = 0
+        self.state_given = None
+        self.timestep_given = None
+        super(MockTracerStepper, self).__init__(**kwargs)
+
+    def array_call(self, state, timestep):
+        self.times_called += 1
+        self.state_given = state
+        self.timestep_given = timestep
+        return_state = {}
+        return_state.update(state)
+        return_state.pop('time')
+        return self.diagnostic_output, return_state
+
+
+class RegisterTracerTests(unittest.TestCase):
+
+    def setUp(self):
+        reset_tracers()
+
+    def tearDown(self):
+        reset_tracers()
+
+    def test_initially_empty(self):
+        assert len(get_tracer_unit_dict()) == 0
+
+    def test_register_one_tracer(self):
+        register_tracer('tracer1', 'm')
+        d = get_tracer_unit_dict()
+        assert len(d) == 1
+        assert 'tracer1' in d
+        assert d['tracer1'] == 'm'
+
+    def test_register_two_tracers(self):
+        register_tracer('tracer1', 'm')
+        register_tracer('tracer2', 'degK')
+        d = get_tracer_unit_dict()
+        assert len(d) == 2
+        assert 'tracer1' in d
+        assert 'tracer2' in d
+        assert d['tracer1'] == 'm'
+        assert d['tracer2'] == 'degK'
+
+    def test_reregister_tracer(self):
+        register_tracer('tracer1', 'm')
+        register_tracer('tracer1', 'm')
+        d = get_tracer_unit_dict()
+        assert len(d) == 1
+        assert 'tracer1' in d
+        assert d['tracer1'] == 'm'
+
+    def test_reregister_tracer_different_units(self):
+        register_tracer('tracer1', 'm')
+        with self.assertRaises(ValueError):
+            register_tracer('tracer1', 'degK')
+
+
+class TracerPackerBase(object):
+
+    def setUp(self):
+        reset_tracers()
+        reset_packers()
+
+    def tearDown(self):
+        reset_tracers()
+        reset_packers()
+
+    def test_packs_no_tracers(self):
+        dims = ['tracer', '*']
+        packer = TracerPacker(self.component, dims)
+        packed = packer.pack({})
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (0, 0)
+
+    def test_unpacks_no_tracers(self):
+        dims = ['tracer', '*']
+        packer = TracerPacker(self.component, dims)
+        unpacked = packer.unpack({}, {})
+        assert isinstance(unpacked, dict)
+        assert len(unpacked) == 0
+
+    def test_packs_one_tracer(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        register_tracer('tracer1', 'g/m^3')
+        state = {'tracer1': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'g/m^3'})}
+        packer = TracerPacker(self.component, dims)
+        packed = packer.pack(state)
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (1, 5)
+        assert np.all(packed[0, :] == state['tracer1'].values)
+
+    def test_packs_one_tracer_converts_units(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        register_tracer('tracer1', 'kg/m^3')
+        state = {'tracer1': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'g/m^3'})}
+        packer = TracerPacker(self.component, dims)
+        packed = packer.pack(state)
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (1, 5)
+        assert np.all(packed[0, :] == state['tracer1'].values * 1e-3)
+
+    def test_packs_one_3d_tracer(self):
+        np.random.seed(0)
+        dims = ['tracer', 'latitude', 'longitude', 'mid_levels']
+        register_tracer('tracer1', 'g/m^3')
+        state = {
+            'tracer1': DataArray(
+                np.random.randn(2, 3, 4),
+                dims=['latitude', 'longitude', 'mid_levels'],
+                attrs={'units': 'g/m^3'}
+            )
+        }
+        packer = TracerPacker(self.component, dims)
+        packed = packer.pack(state)
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (1, 2, 3, 4)
+        assert np.all(packed[0, :, :, :] == state['tracer1'].values)
+
+    def test_packer_does_not_change_input_properties(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        register_tracer('tracer1', 'g/m^3')
+        packer = TracerPacker(self.component, dims)
+        assert len(self.component.input_properties) == 0
+
+    def test_packer_does_not_change_input_properties_after_init(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        packer = TracerPacker(self.component, dims)
+        assert len(self.component.input_properties) == 0
+        register_tracer('tracer1', 'g/m^3')
+        assert len(self.component.input_properties) == 0
+
+    def test_packs_one_tracer_registered_after_init(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        state = {'tracer1': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'g/m^3'})}
+        packer = TracerPacker(self.component, dims)
+        register_tracer('tracer1', 'g/m^3')
+        packed = packer.pack(state)
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (1, 5)
+        assert np.all(packed[0, :] == state['tracer1'].values)
+
+    def test_packs_two_tracers(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        register_tracer('tracer1', 'g/m^3')
+        register_tracer('tracer2', 'kg')
+        state = {
+            'tracer1': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'g/m^3'}),
+            'tracer2': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'kg'})
+        }
+        packer = TracerPacker(self.component, dims)
+        packed = packer.pack(state)
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (2, 5)
+
+    def test_packs_three_tracers_in_order_registered(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        register_tracer('tracer1', 'g/m^3')
+        register_tracer('tracer2', 'kg'),
+        register_tracer('tracer3', 'kg/m^3')
+        state = {
+            'tracer1': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'g/m^3'}),
+            'tracer2': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'kg'}),
+            'tracer3': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'kg/m^3'}),
+        }
+        packer = TracerPacker(self.component, dims)
+        packed = packer.pack(state)
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (3, 5)
+        assert np.all(packed[0, :] == state['tracer1'].values)
+        assert np.all(packed[1, :] == state['tracer2'].values)
+        assert np.all(packed[2, :] == state['tracer3'].values)
+
+    def test_unpacks_three_tracers_in_order_registered(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        register_tracer('tracer1', 'g/m^3')
+        register_tracer('tracer2', 'kg'),
+        register_tracer('tracer3', 'kg/m^3')
+        state = {
+            'tracer1': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'g/m^3'}),
+            'tracer2': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'kg'}),
+            'tracer3': DataArray(np.random.randn(5), dims=['dim1'], attrs={'units': 'kg/m^3'}),
+        }
+        packer = TracerPacker(self.component, dims)
+        packed = packer.pack(state)
+        unpacked = packer.unpack(packed, state)
+        assert isinstance(unpacked, dict)
+        assert len(unpacked) == 3
+        assert np.all(unpacked['tracer1'] == state['tracer1'])
+        assert np.all(unpacked['tracer2'] == state['tracer2'])
+        assert np.all(unpacked['tracer3'] == state['tracer3'])
+
+    def test_packer_allows_overlap_input_registered_after_init(self):
+        self.component = self.component.__class__(
+            input_properties={
+                'name': {
+                    'units': 'm',
+                    'dims': ['*'],
+                }
+            }
+        )
+        packer = TracerPacker(self.component, ['tracer', '*'])
+        register_tracer('name', 'm')
+
+    def test_packer_allows_overlap_input_registered_before_init(self):
+        self.component = self.component.__class__(
+            input_properties={
+                'name': {
+                    'units': 'm',
+                    'dims': ['*'],
+                }
+            }
+        )
+        register_tracer('name', 'm')
+        packer = TracerPacker(self.component, ['tracer', '*'])
+
+
+class PrognosticTracerPackerTests(TracerPackerBase, unittest.TestCase):
+
+    def setUp(self):
+        self.component = MockTendencyComponent()
+        super(PrognosticTracerPackerTests, self).setUp()
+
+    def tearDown(self):
+        self.component = None
+        super(PrognosticTracerPackerTests, self).tearDown()
+
+    def test_packer_does_not_change_tendency_properties(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        register_tracer('tracer1', 'g/m^3')
+        packer = TracerPacker(self.component, dims)
+        assert 'tracer1' not in self.component.tendency_properties
+        assert len(self.component.tendency_properties) == 0
+
+    def test_packer_does_not_change_tendency_properties_after_init(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        packer = TracerPacker(self.component, dims)
+        assert len(self.component.tendency_properties) == 0
+        register_tracer('tracer1', 'g/m^3')
+        assert len(self.component.tendency_properties) == 0
+
+    def test_packer_wont_overwrite_tendency_registered_after_init(self):
+        self.component = MockTendencyComponent(
+            tendency_properties={
+                'name': {
+                    'units': 'm',
+                    'dims': ['*'],
+                }
+            }
+        )
+        packer = TracerPacker(self.component, ['tracer', '*'])
+        with self.assertRaises(InvalidPropertyDictError):
+            register_tracer('name', 'm')
+
+    def test_packer_wont_overwrite_tendency_registered_before_init(self):
+        self.component = MockTendencyComponent(
+            tendency_properties={
+                'name': {
+                    'units': 'm',
+                    'dims': ['*'],
+                }
+            }
+        )
+        register_tracer('name', 'm')
+        with self.assertRaises(InvalidPropertyDictError):
+            packer = TracerPacker(self.component, ['tracer', '*'])
+
+
+class ImplicitPrognosticTracerPackerTests(PrognosticTracerPackerTests):
+
+    def setUp(self):
+        self.component = MockImplicitTendencyComponent()
+        super(ImplicitPrognosticTracerPackerTests, self).setUp()
+
+    def tearDown(self):
+        self.component = None
+        super(ImplicitPrognosticTracerPackerTests, self).tearDown()
+
+
+class ImplicitTracerPackerTests(TracerPackerBase, unittest.TestCase):
+
+    def setUp(self):
+        self.component = MockStepper()
+        super(ImplicitTracerPackerTests, self).setUp()
+
+    def tearDown(self):
+        self.component = None
+        super(ImplicitTracerPackerTests, self).tearDown()
+
+    def test_packer_does_not_change_output_properties(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        register_tracer('tracer1', 'g/m^3')
+        packer = TracerPacker(self.component, dims)
+        assert len(self.component.output_properties) == 0
+
+    def test_packer_does_not_change_output_properties_after_init(self):
+        np.random.seed(0)
+        dims = ['tracer', '*']
+        packer = TracerPacker(self.component, dims)
+        assert len(self.component.output_properties) == 0
+        register_tracer('tracer1', 'g/m^3')
+        assert len(self.component.output_properties) == 0
+
+    def test_packer_wont_overwrite_output_registered_after_init(self):
+        self.component = MockStepper(
+            output_properties={
+                'name': {
+                    'units': 'm',
+                    'dims': ['*'],
+                }
+            }
+        )
+        packer = TracerPacker(self.component, ['tracer', '*'])
+        with self.assertRaises(InvalidPropertyDictError):
+            register_tracer('name', 'm')
+
+    def test_packer_wont_overwrite_output_registered_before_init(self):
+        self.component = MockStepper(
+            output_properties={
+                'name': {
+                    'units': 'm',
+                    'dims': ['*'],
+                }
+            }
+        )
+        register_tracer('name', 'm')
+        with self.assertRaises(InvalidPropertyDictError):
+            packer = TracerPacker(self.component, ['tracer', '*'])
+
+
+class DiagnosticTracerPackerTests(unittest.TestCase):
+
+    def test_raises_on_diagnostic_init(self):
+        diagnostic = MockDiagnosticComponent()
+        with self.assertRaises(TypeError):
+            TracerPacker(diagnostic, ['tracer', '*'])
+
+
+class TracerComponentBase(object):
+
+    def setUp(self):
+        reset_tracers()
+        reset_packers()
+
+    def tearDown(self):
+        reset_tracers()
+        reset_packers()
+
+    def call_component(self, input_state):
+        pass
+
+    def test_packs_no_tracers(self):
+        input_state = {
+            'time': timedelta(0),
+        }
+        self.call_component(input_state)
+        packed = self.component.state_given['tracers']
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (0, 0)
+
+    def test_unpacks_no_tracers(self):
+        input_state = {
+            'time': timedelta(0),
+        }
+        unpacked = self.call_component(input_state)
+        assert isinstance(unpacked, dict)
+        assert len(unpacked) == 0
+
+    def test_packs_one_tracer(self):
+        np.random.seed(0)
+        register_tracer('tracer1', 'g/m^3')
+        input_state = {
+            'time': timedelta(0),
+            'tracer1': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'g/m^3'},
+            )
+        }
+        self.call_component(input_state)
+        packed = self.component.state_given['tracers']
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (1, 5)
+        assert np.all(packed[0, :] == input_state['tracer1'].values)
+
+    def test_packs_one_prepended_tracer(self):
+        np.random.seed(0)
+        self.component = self.component.__class__(prepend_tracers=[('tracer1', 'g/m^3')])
+        input_state = {
+            'time': timedelta(0),
+            'tracer1': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'g/m^3'},
+            )
+        }
+        self.call_component(input_state)
+        packed = self.component.state_given['tracers']
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (1, 5)
+        assert np.all(packed[0, :] == input_state['tracer1'].values)
+
+    def test_packs_two_prepended_tracers(self):
+        np.random.seed(0)
+        self.component = self.component.__class__(
+            prepend_tracers=[('tracer1', 'g/m^3'), ('tracer2', 'J/m^3')])
+        input_state = {
+            'time': timedelta(0),
+            'tracer1': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'g/m^3'},
+            ),
+            'tracer2': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'J/m^3'},
+            ),
+        }
+        unpacked = self.call_component(input_state)
+        packed = self.component.state_given['tracers']
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (2, 5)
+        assert np.all(packed[0, :] == input_state['tracer1'].values)
+        assert np.all(packed[1, :] == input_state['tracer2'].values)
+        assert len(unpacked) == 2
+        assert np.all(unpacked['tracer1'].values == input_state['tracer1'].values)
+        assert np.all(unpacked['tracer2'].values == input_state['tracer2'].values)
+
+    def test_packs_prepended_and_normal_tracers_register_first(self):
+        register_tracer('tracer2', 'J/m^3')
+        self.component = self.component.__class__(
+            prepend_tracers=[('tracer1', 'g/m^3')])
+        input_state = {
+            'time': timedelta(0),
+            'tracer1': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'g/m^3'},
+            ),
+            'tracer2': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'J/m^3'},
+            ),
+        }
+        unpacked = self.call_component(input_state)
+        packed = self.component.state_given['tracers']
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (2, 5)
+        assert np.all(packed[0, :] == input_state['tracer1'].values)
+        assert np.all(packed[1, :] == input_state['tracer2'].values)
+        assert len(unpacked) == 2
+        assert np.all(
+            unpacked['tracer1'].values == input_state['tracer1'].values)
+        assert np.all(
+            unpacked['tracer2'].values == input_state['tracer2'].values)
+
+    def test_packs_prepended_and_normal_tracers_register_after_init(self):
+        self.component = self.component.__class__(
+            prepend_tracers=[('tracer1', 'g/m^3')])
+        register_tracer('tracer2', 'J/m^3')
+        input_state = {
+            'time': timedelta(0),
+            'tracer1': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'g/m^3'},
+            ),
+            'tracer2': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'J/m^3'},
+            ),
+        }
+        unpacked = self.call_component(input_state)
+        packed = self.component.state_given['tracers']
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (2, 5)
+        assert np.all(packed[0, :] == input_state['tracer1'].values)
+        assert np.all(packed[1, :] == input_state['tracer2'].values)
+        assert len(unpacked) == 2
+        assert np.all(
+            unpacked['tracer1'].values == input_state['tracer1'].values)
+        assert np.all(
+            unpacked['tracer2'].values == input_state['tracer2'].values)
+
+    def test_packs_one_3d_tracer(self):
+        np.random.seed(0)
+        register_tracer('tracer1', 'g/m^3')
+        input_state = {
+            'time': timedelta(0),
+            'tracer1': DataArray(
+                np.random.randn(1, 2, 3),
+                dims=['dim1', 'dim2', 'dim3'],
+                attrs={'units': 'g/m^3'},
+            )
+        }
+        self.call_component(input_state)
+        packed = self.component.state_given['tracers']
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (1, 6)
+        assert np.all(packed[0, :] == input_state['tracer1'].values.flatten())
+
+    def test_restores_one_3d_tracer(self):
+        np.random.seed(0)
+        register_tracer('tracer1', 'g/m^3')
+        input_state = {
+            'time': timedelta(0),
+            'tracer1': DataArray(
+                np.random.randn(1, 2, 3),
+                dims=['dim1', 'dim2', 'dim3'],
+                attrs={'units': 'g/m^3'},
+            )
+        }
+        unpacked = self.call_component(input_state)
+        assert len(unpacked) == 1
+        assert unpacked['tracer1'].shape == input_state['tracer1'].shape
+        assert np.all(unpacked['tracer1'].values == input_state['tracer1'].values)
+
+    def test_does_not_change_input_properties(self):
+        np.random.seed(0)
+        register_tracer('tracer1', 'g/m^3')
+        assert 'tracer1' not in self.component.input_properties
+
+    def test_packs_two_tracers(self):
+        np.random.seed(0)
+        register_tracer('tracer1', 'g/m^3')
+        register_tracer('tracer2', 'kg')
+        input_state = {
+            'time': timedelta(0),
+            'tracer1': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'g/m^3'},
+            ),
+            'tracer2': DataArray(
+                np.random.randn(5),
+                dims=['dim1'],
+                attrs={'units': 'kg'}
+            ),
+        }
+        unpacked = self.call_component(input_state)
+        packed = self.component.state_given['tracers']
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (2, 5)
+        assert np.all(packed[0, :] == input_state['tracer1'].values)
+        assert np.all(packed[1, :] == input_state['tracer2'].values)
+        assert len(unpacked) == 2
+        assert np.all(unpacked['tracer1'].values == input_state['tracer1'].values)
+        assert np.all(unpacked['tracer2'].values == input_state['tracer2'].values)
+
+    def test_packing_differing_dims(self):
+        np.random.seed(0)
+        register_tracer('tracer1', 'g/m^3')
+        register_tracer('tracer2', 'kg')
+        input_state = {
+            'time': timedelta(0),
+            'tracer1': DataArray(
+                np.random.randn(2),
+                dims=['dim1'],
+                attrs={'units': 'g/m^3'},
+            ),
+            'tracer2': DataArray(
+                np.random.randn(2),
+                dims=['dim2'],
+                attrs={'units': 'kg'}
+            ),
+        }
+        unpacked = self.call_component(input_state)
+        packed = self.component.state_given['tracers']
+        assert isinstance(packed, np.ndarray)
+        assert packed.shape == (2, 4)
+        assert len(unpacked) == 2
+
+
+class PrognosticTracerComponentTests(TracerComponentBase, unittest.TestCase):
+
+    def setUp(self):
+        super(PrognosticTracerComponentTests, self).setUp()
+        self.component = MockTracerTendencyComponent()
+
+    def tearDown(self):
+        super(PrognosticTracerComponentTests, self).tearDown()
+        self.component = None
+
+    def call_component(self, input_state):
+        return self.component(input_state)[0]
+
+
+class ImplicitPrognosticTracerComponentTests(TracerComponentBase, unittest.TestCase):
+
+    def setUp(self):
+        super(ImplicitPrognosticTracerComponentTests, self).setUp()
+        self.component = MockTracerImplicitTendencyComponent()
+
+    def tearDown(self):
+        super(ImplicitPrognosticTracerComponentTests, self).tearDown()
+        self.component = None
+
+    def call_component(self, input_state):
+        return self.component(input_state, timedelta(hours=1))[0]
+
+
+class ImplicitTracerComponentTests(TracerComponentBase, unittest.TestCase):
+
+    def setUp(self):
+        super(ImplicitTracerComponentTests, self).setUp()
+        self.component = MockTracerStepper()
+
+    def tearDown(self):
+        super(ImplicitTracerComponentTests, self).tearDown()
+        self.component = None
+
+    def call_component(self, input_state):
+        return self.component(input_state, timedelta(hours=1))[1]
diff --git a/tests/test_units.py b/tests/test_units.py
new file mode 100644
index 0000000..6ae08c0
--- /dev/null
+++ b/tests/test_units.py
@@ -0,0 +1,24 @@
+from sympl import units_are_same, units_are_compatible, is_valid_unit
+
+
+def test_is_valid_unit_meters():
+    assert is_valid_unit('m')
+    assert is_valid_unit('meter')
+    assert is_valid_unit('meters')
+
+
+def test_units_are_compatible_meters():
+    assert units_are_compatible('m', 'km')
+    assert units_are_compatible('kilometers', 'cm')
+    assert not units_are_compatible('m', 'm/s')
+
+
+def test_units_are_same_meters():
+    assert units_are_same('m', 'meter')
+    assert units_are_same('meters', 'm')
+    assert units_are_same('kilometers', 'km')
+
+
+def test_is_valid_unit_invalid_values():
+    assert not is_valid_unit('george')
+    assert not is_valid_unit('boop')
diff --git a/tests/test_util.py b/tests/test_util.py
index 3984943..663b4c8 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -1,13 +1,13 @@
 import unittest
-
 import numpy as np
 import pytest
 from sympl import (
-    Prognostic, ensure_no_shared_keys, SharedKeyError, DataArray,
-    combine_dimensions, set_direction_names, Implicit, Diagnostic,
-    TendencyInDiagnosticsWrapper)
-from sympl._core.util import (
-    update_dict_by_adding_another, get_component_aliases)
+    TendencyComponent, ensure_no_shared_keys, SharedKeyError, DataArray,
+    Stepper, DiagnosticComponent,
+    InvalidPropertyDictError)
+from sympl._core.util import update_dict_by_adding_another, \
+    get_component_aliases
+from sympl._core.combine_properties import combine_dims
 
 
 def same_list(list1, list2):
@@ -15,32 +15,87 @@ def same_list(list1, list2):
         [item in list2 for item in list1] + [item in list1 for item in list2]))
 
 
-class MockPrognostic(Prognostic):
+class PrognosticPropertiesContainer(object):
 
-    def __init__(self):
-        self._num_updates = 0
+    def __init__(self, input_properties, tendency_properties, diagnostic_properties):
+        self.input_properties = input_properties
+        self.tendency_properties = tendency_properties
+        self.diagnostic_properties = diagnostic_properties
 
-    def __call__(self, state):
-        self._num_updates += 1
-        return {}, {'num_updates': self._num_updates}
 
+class ImplicitPropertiesContainer(object):
 
-class MockImplicit(Implicit):
+    def __init__(self, input_properties, diagnostic_properties, output_properties):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.output_properties = output_properties
 
-    def __init__(self):
-        self._a = 1
 
-    def __call__(self, state):
-        return self._a
+class DiagnosticPropertiesContainer(object):
 
+    def __init__(self, input_properties, diagnostic_properties):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
 
-class MockDiagnostic(Diagnostic):
 
-    def __init__(self):
-        self._a = 1
-
-    def __call__(self, state):
-        return self._a
+def test_update_dict_by_adding_another_works_on_different_dim_orders():
+    dict1 = {
+        'quantity': DataArray(
+            np.ones([2, 3, 4]),
+            dims=['dim1', 'dim2', 'dim3'],
+            attrs={'units': 'm'},
+        )
+    }
+    dict2 = {
+        'quantity': DataArray(
+            np.ones([3, 2, 4]),
+            dims=['dim2', 'dim1', 'dim3'],
+            attrs={'units': 'm'},
+        )
+    }
+    update_dict_by_adding_another(dict1, dict2)
+    assert dict1['quantity'].shape == (2, 3, 4)
+    assert np.all(dict1['quantity'].values == 2.)
+
+
+def test_update_dict_by_adding_another_broadcasts_added_dim():
+    dict1 = {
+        'quantity': DataArray(
+            np.ones([2, 3, 4]),
+            dims=['dim1', 'dim2', 'dim3'],
+            attrs={'units': 'm'},
+        )
+    }
+    dict2 = {
+        'quantity': DataArray(
+            np.ones([3, 2]),
+            dims=['dim2', 'dim1'],
+            attrs={'units': 'm'},
+        )
+    }
+    update_dict_by_adding_another(dict1, dict2)
+    assert dict1['quantity'].shape == (2, 3, 4)
+    assert np.all(dict1['quantity'].values == 2.)
+
+
+def test_update_dict_by_adding_another_broadcasts_initial_dim():
+    dict1 = {
+        'quantity': DataArray(
+            np.ones([2, 3]),
+            dims=['dim1', 'dim2'],
+            attrs={'units': 'm'},
+        )
+    }
+    dict2 = {
+        'quantity': DataArray(
+            np.ones([3, 2, 4]),
+            dims=['dim2', 'dim1', 'dim3'],
+            attrs={'units': 'm'},
+        )
+    }
+    update_dict_by_adding_another(dict1, dict2)
+    assert dict1['quantity'].shape == (2, 3, 4)
+    assert np.all(dict1['quantity'].values == 2.)
 
 
 def test_update_dict_by_adding_another_adds_shared_arrays():
@@ -71,7 +126,7 @@ def test_update_dict_by_adding_another_adds_shared_arrays_reversed():
     assert len(dict2.keys()) == 2
 
 
-class DummyPrognostic(Prognostic):
+class DummyTendencyComponent(TendencyComponent):
     input_properties = {'temperature': {'alias': 'T'}}
     diagnostic_properties = {'pressure': {'alias': 'P'}}
     tendency_properties = {'temperature': {}}
@@ -89,76 +144,342 @@ def test_get_component_aliases_with_no_args():
     assert len(aliases.keys()) == 0
 
 
-def test_get_component_aliases_with_single_component_arg():
-    components = [MockPrognostic(), MockImplicit(), MockDiagnostic(),
-                  TendencyInDiagnosticsWrapper(DummyPrognostic(), 'dummy')]
-    for c, comp in enumerate(components):
-        aliases = get_component_aliases(comp)
-        assert type(aliases) == dict
-        if c == 3:
-            assert len(aliases.keys()) == 2
-            for k in ['T', 'P']:
-                assert k in list(aliases.values())
-        else:
-            assert len(aliases.keys()) == 0
-
-
-def test_get_component_aliases_with_two_component_args():
-    components = [MockDiagnostic(), MockImplicit(), MockDiagnostic(),
-                  TendencyInDiagnosticsWrapper(DummyPrognostic(), 'dummy')]
-    for comp in components[:3]:
-        aliases = get_component_aliases(comp, components[-1])
-        assert type(aliases) == dict
-        assert len(aliases.keys()) == 2
-        for k in ['T', 'P']:
-            assert k in list(aliases.values())
-
-
-class DummyProg1(Prognostic):
-    input_properties = {'temperature': {'alias': 'T'}}
-    tendency_properties = {'temperature': {'alias': 'TEMP'}}
-
-    def __init__(self):
-        self._a = 1
-
-    def __call__(self, state):
-        return self._a
-
-
-class DummyProg2(Prognostic):
-    input_properties = {'temperature': {'alias': 't'}}
-
-    def __init__(self):
-        self._a = 1
-
-    def __call__(self, state):
-        return self._a
-
-
-class DummyProg3(Prognostic):
-    input_properties = {'temperature': {}}
-    diagnostic_properties = {'pressure': {}}
-    tendency_properties = {'temperature': {}}
-
-    def __init__(self):
-        self._a = 1
-
-    def __call__(self, state):
-        return self._a
-
-
-def test_get_component_aliases_with_different_values():
-    # two different aliases in the same Component:
-    aliases = get_component_aliases(DummyProg1())
-    assert len(aliases.keys()) == 1
-    assert aliases['temperature'] == 'TEMP'
-    # two different aliases in different Components:
-    aliases = get_component_aliases(DummyProg1(), DummyProg2())
-    assert len(aliases.keys()) == 1
-    assert aliases['temperature'] == 't'
-    # NO aliases in component
-    aliases = get_component_aliases(DummyProg3)
-    assert len(aliases.keys()) == 0
+def test_get_component_aliases_prognostic():
+    aliases = get_component_aliases(
+        PrognosticPropertiesContainer(
+            input_properties={
+                'temperature': {
+                    'alias': 'T',
+                }
+            },
+            tendency_properties={
+                'wind': {
+                    'alias': 'u'
+                }
+            },
+            diagnostic_properties={
+                'specific_humidity': {
+                    'alias': 'q'
+                },
+            }
+        )
+    )
+    assert aliases == {'temperature': 'T', 'wind': 'u', 'specific_humidity': 'q'}
+
+
+def test_get_component_aliases_no_alias_prognostic():
+    aliases = get_component_aliases(
+        PrognosticPropertiesContainer(
+            input_properties={
+                'temperature': {
+                }
+            },
+            tendency_properties={
+                'wind': {
+                }
+            },
+            diagnostic_properties={
+                'specific_humidity': {
+                },
+            }
+        )
+    )
+    assert aliases == {}
+
+
+def test_get_component_aliases_empty_prognostic():
+    aliases = get_component_aliases(
+        PrognosticPropertiesContainer(
+            input_properties={},
+            tendency_properties={},
+            diagnostic_properties={}
+        )
+    )
+    assert aliases == {}
+
+
+def test_get_component_aliases_diagnostic():
+    aliases = get_component_aliases(
+        DiagnosticPropertiesContainer(
+            input_properties={
+                'temperature': {
+                    'alias': 'T',
+                }
+            },
+            diagnostic_properties={
+                'specific_humidity': {
+                    'alias': 'q'
+                },
+            }
+        )
+    )
+    assert aliases == {'temperature': 'T', 'specific_humidity': 'q'}
+
+
+def test_get_component_aliases_no_alias_diagnostic():
+    aliases = get_component_aliases(
+        DiagnosticPropertiesContainer(
+            input_properties={
+                'temperature': {
+                }
+            },
+            diagnostic_properties={
+                'specific_humidity': {
+                },
+            }
+        )
+    )
+    assert aliases == {}
+
+
+def test_get_component_aliases_empty_diagnostic():
+    aliases = get_component_aliases(
+        DiagnosticPropertiesContainer(
+            input_properties={},
+            diagnostic_properties={}
+        )
+    )
+    assert aliases == {}
+
+
+def test_get_component_aliases_implicit():
+    aliases = get_component_aliases(
+        ImplicitPropertiesContainer(
+            input_properties={
+                'temperature': {
+                    'alias': 'T',
+                },
+                'input2': {
+                    'alias': 'in2',
+                }
+            },
+            output_properties={
+                'wind': {
+                    'alias': 'u'
+                }
+            },
+            diagnostic_properties={
+                'specific_humidity': {
+                    'alias': 'q'
+                },
+            }
+        )
+    )
+    assert aliases == {
+        'temperature': 'T', 'input2': 'in2', 'wind': 'u', 'specific_humidity': 'q'}
+
+
+def test_get_component_aliases_no_alias_implicit():
+    aliases = get_component_aliases(
+        ImplicitPropertiesContainer(
+            input_properties={
+                'temperature': {},
+            },
+            output_properties={
+                'wind': {}
+            },
+            diagnostic_properties={
+                'specific_humidity': {},
+            }
+        )
+    )
+    assert aliases == {}
+
+
+def test_get_component_aliases_input_over_diagnostic():
+    aliases = get_component_aliases(
+        ImplicitPropertiesContainer(
+            input_properties={
+                'temperature': {'alias': 'correct'},
+            },
+            output_properties={},
+            diagnostic_properties={
+                'temperature': {'alias': 'incorrect'},
+            }
+        )
+    )
+    assert aliases == {'temperature': 'correct'}
+
+
+def test_get_component_aliases_input_over_output():
+    aliases = get_component_aliases(
+        ImplicitPropertiesContainer(
+            input_properties={
+                'temperature': {'alias': 'correct'},
+            },
+            output_properties={
+                'temperature': {'alias': 'incorrect'},
+            },
+            diagnostic_properties={}
+        )
+    )
+    assert aliases == {'temperature': 'correct'}
+
+
+def test_get_component_aliases_output_over_diagnostic():
+    aliases = get_component_aliases(
+        ImplicitPropertiesContainer(
+            input_properties={
+            },
+            output_properties={
+                'temperature': {'alias': 'correct'},
+            },
+            diagnostic_properties={
+                'temperature': {'alias': 'not'},
+            }
+        )
+    )
+    assert aliases == {'temperature': 'correct'}
+
+
+def test_get_component_aliases_diagnostic_over_tendency():
+    aliases = get_component_aliases(
+        PrognosticPropertiesContainer(
+            input_properties={
+            },
+            tendency_properties={
+                'temperature': {'alias': 'not'},
+            },
+            diagnostic_properties={
+                'temperature': {'alias': 'correct'},
+            }
+        )
+    )
+    assert aliases == {'temperature': 'correct'}
+
+
+def test_get_component_aliases_input_over_tendency():
+    aliases = get_component_aliases(
+        PrognosticPropertiesContainer(
+            input_properties={
+                'temperature': {'alias': 'T'},
+            },
+            tendency_properties={
+                'temperature': {'alias': 'not'},
+            },
+            diagnostic_properties={
+            }
+        )
+    )
+    assert aliases == {'temperature': 'T'}
+
+
+def test_get_component_aliases_empty_implicit():
+    aliases = get_component_aliases(
+        ImplicitPropertiesContainer(
+            input_properties={},
+            output_properties={},
+            diagnostic_properties={}
+        )
+    )
+    assert aliases == {}
+
+
+def test_get_component_aliases_all_types_no_overlap():
+    aliases = get_component_aliases(
+        ImplicitPropertiesContainer(
+            input_properties={
+                'input1': {
+                    'alias': 'in1',
+                }
+            },
+            output_properties={
+                'output1': {
+                    'alias': 'out1'
+                }
+            },
+            diagnostic_properties={
+                'diagnostic1': {
+                    'alias': 'diag1'
+                },
+            }
+        ),
+        DiagnosticPropertiesContainer(
+            input_properties={
+                'input2': {
+                    'alias': 'in2',
+                }
+            },
+            diagnostic_properties={
+                'diagnostic2': {
+                    'alias': 'diag2'
+                },
+            }
+        ),
+        PrognosticPropertiesContainer(
+            input_properties={
+                'input3': {
+                    'alias': 'in3',
+                }
+            },
+            tendency_properties={
+                'tendency3': {
+                    'alias': 'tend3'
+                }
+            },
+            diagnostic_properties={
+                'diagnostic3': {
+                    'alias': 'diag3'
+                },
+            }
+        )
+    )
+    assert aliases == {
+        'input1': 'in1', 'output1': 'out1', 'diagnostic1': 'diag1',
+        'input2': 'in2', 'diagnostic2': 'diag2', 'input3': 'in3',
+        'tendency3': 'tend3', 'diagnostic3': 'diag3'}
+
+def test_get_component_aliases_all_types_with_overlap():
+    aliases = get_component_aliases(
+        ImplicitPropertiesContainer(
+            input_properties={
+                'input1': {
+                    'alias': 'in1',
+                }
+            },
+            output_properties={
+                'output1': {
+                    'alias': 'out1'
+                }
+            },
+            diagnostic_properties={
+                'diagnostic1': {
+                    'alias': 'diag1'
+                },
+            }
+        ),
+        DiagnosticPropertiesContainer(
+            input_properties={
+                'input1': {
+                    'alias': 'in1',
+                }
+            },
+            diagnostic_properties={
+                'diagnostic1': {
+                    'alias': 'diag1'
+                },
+            }
+        ),
+        PrognosticPropertiesContainer(
+            input_properties={
+                'input1': {
+                    'alias': 'in1',
+                }
+            },
+            tendency_properties={
+                'tendency1': {
+                    'alias': 'tend1',
+                }
+            },
+            diagnostic_properties={
+                'diagnostic1': {
+                    'alias': 'diag1'
+                },
+            }
+        )
+    )
+    assert aliases == {
+        'input1': 'in1', 'output1': 'out1', 'diagnostic1': 'diag1', 'tendency1': 'tend1'}
 
 
 def test_ensure_no_shared_keys_empty_dicts():
@@ -187,61 +508,34 @@ def test_ensure_no_shared_keys_with_shared_keys():
             'No exception raised but expected SharedKeyError.')
 
 
-class CombineDimensionsTests(unittest.TestCase):
-
-    def setUp(self):
-        self.array_1d = DataArray(np.zeros((2,)), dims=['lon'])
-        self.array_2d = DataArray(np.zeros((2, 2)), dims=['lat', 'lon'])
-        self.array_3d = DataArray(np.zeros((2, 2, 2)),
-                                  dims=['lon', 'lat', 'interface_levels'])
-        set_direction_names(
-            x=['lon'], y=['lat'], z=['mid_levels', 'interface_levels'])
-
-    def tearDown(self):
-        set_direction_names(x=[], y=[], z=[])
-
-    def test_combine_dimensions_2d_and_3d(self):
-        dims = combine_dimensions(
-            [self.array_2d, self.array_3d], out_dims=('x', 'y', 'z'))
-        assert same_list(dims, ['lon', 'lat', 'interface_levels'])
-
-    def test_combine_dimensions_2d_and_3d_z_y_x(self):
-        dims = combine_dimensions(
-            [self.array_2d, self.array_3d], out_dims=('z', 'y', 'x'))
-        assert same_list(dims, ['interface_levels', 'lat', 'lon'])
-
-    def combine_dimensions_1d_shared(self):
-        dims = combine_dimensions(
-            [self.array_1d, self.array_1d], out_dims=['x'])
-        assert same_list(dims, ['lon'])
-
-    def combine_dimensions_1d_not_shared(self):
-        array_1d_x = DataArray(np.zeros((2,)), dims=['lon'])
-        array_1d_y = DataArray(np.zeros((2,)), dims=['lat'])
-        dims = combine_dimensions([array_1d_x, array_1d_y], out_dims=['x', 'y'])
-        assert same_list(dims, ['lon', 'lat'])
-
-    def combine_dimensions_1d_wrong_direction(self):
-        try:
-            combine_dimensions(
-                [self.array_1d, self.array_1d], out_dims=['z'])
-        except ValueError:
-            pass
-        except Exception as err:
-            raise err
-        else:
-            raise AssertionError('No exception raised but expected ValueError.')
-
-    def combine_dimensions_1d_and_2d_extra_direction(self):
-        try:
-            combine_dimensions(
-                [self.array_1d, self.array_2d], out_dims=['y'])
-        except ValueError:
-            pass
-        except Exception as err:
-            raise err
-        else:
-            raise AssertionError('No exception raised but expected ValueError.')
+class CombineDimsTests(unittest.TestCase):
+
+    def test_same_dims(self):
+        assert combine_dims(['dim1'], ['dim1']) == ['dim1']
+
+    def test_one_wildcard(self):
+        assert combine_dims(['*'], ['dim1']) == ['dim1']
+
+    def test_both_wildcard(self):
+        assert combine_dims(['*'], ['*']) == ['*']
+
+    def test_both_wildcard_2d(self):
+        assert set(combine_dims(['*', 'dim1'], ['*', 'dim1'])) == {'*', 'dim1'}
+
+    def test_one_wildcard_2d(self):
+        assert set(combine_dims(['*', 'dim2'], ['dim1', 'dim2'])) == {'dim1', 'dim2'}
+
+    def test_different_dims(self):
+        with self.assertRaises(InvalidPropertyDictError):
+            combine_dims(['dim1'], ['dim2'])
+
+    def test_swapped_dims(self):
+        result = set(combine_dims(['dim1', 'dim2'], ['dim2', 'dim1']))
+        assert result == {'dim1', 'dim2'}
+
+    def test_swapped_dims_with_wildcard(self):
+        result = combine_dims(['*', 'dim1', 'dim2'], ['*', 'dim2', 'dim1'])
+        assert set(result) == {'*', 'dim1', 'dim2'}
 
 
 if __name__ == '__main__':
diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py
index a6b5665..96fc67c 100644
--- a/tests/test_wrapper.py
+++ b/tests/test_wrapper.py
@@ -1,384 +1,774 @@
 from datetime import timedelta, datetime
 import unittest
 from sympl import (
-    Prognostic, Implicit, Diagnostic, UpdateFrequencyWrapper, ScalingWrapper,
-    TendencyInDiagnosticsWrapper, TimeDifferencingWrapper, DataArray
+    TendencyComponent, Stepper, DiagnosticComponent, UpdateFrequencyWrapper, ScalingWrapper,
+    TimeDifferencingWrapper, DataArray, ImplicitTendencyComponent
 )
 import pytest
-from numpy.testing import assert_allclose
-from copy import deepcopy
+import numpy as np
+
+
+class MockTendencyComponent(TendencyComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.tendency_properties = tendency_properties
+        self.diagnostic_output = diagnostic_output
+        self.tendency_output = tendency_output
+        self.times_called = 0
+        self.state_given = None
+        super(MockTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return self.tendency_output, self.diagnostic_output
+
+
+class MockImplicitTendencyComponent(ImplicitTendencyComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+    tendency_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, tendency_properties,
+            diagnostic_output, tendency_output, **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.tendency_properties = tendency_properties
+        self.diagnostic_output = diagnostic_output
+        self.tendency_output = tendency_output
+        self.times_called = 0
+        self.state_given = None
+        self.timestep_given = None
+        super(MockImplicitTendencyComponent, self).__init__(**kwargs)
+
+    def array_call(self, state, timestep):
+        self.times_called += 1
+        self.state_given = state
+        self.timestep_given = timestep
+        return self.tendency_output, self.diagnostic_output
+
+
+class MockDiagnosticComponent(DiagnosticComponent):
+
+    input_properties = None
+    diagnostic_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, diagnostic_output,
+            **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.diagnostic_output = diagnostic_output
+        self.times_called = 0
+        self.state_given = None
+        super(MockDiagnosticComponent, self).__init__(**kwargs)
+
+    def array_call(self, state):
+        self.times_called += 1
+        self.state_given = state
+        return self.diagnostic_output
+
+
+class MockStepper(Stepper):
+
+    input_properties = None
+    diagnostic_properties = None
+    output_properties = None
+
+    def __init__(
+            self, input_properties, diagnostic_properties, output_properties,
+            diagnostic_output, state_output,
+            **kwargs):
+        self.input_properties = input_properties
+        self.diagnostic_properties = diagnostic_properties
+        self.output_properties = output_properties
+        self.diagnostic_output = diagnostic_output
+        self.state_output = state_output
+        self.times_called = 0
+        self.state_given = None
+        self.timestep_given = None
+        super(MockStepper, self).__init__(**kwargs)
+
+    def array_call(self, state, timestep):
+        self.times_called += 1
+        self.state_given = state
+        self.timestep_given = timestep
+        return self.diagnostic_output, self.state_output
+
+
+class MockEmptyPrognostic(MockTendencyComponent):
+
+    def __init__(self, **kwargs):
+        super(MockEmptyPrognostic, self).__init__(
+            input_properties={},
+            diagnostic_properties={},
+            tendency_properties={},
+            diagnostic_output={},
+            tendency_output={},
+            **kwargs
+        )
+
+
+class MockEmptyImplicitPrognostic(MockImplicitTendencyComponent):
+    def __init__(self, **kwargs):
+        super(MockEmptyImplicitPrognostic, self).__init__(
+            input_properties={},
+            diagnostic_properties={},
+            tendency_properties={},
+            diagnostic_output={},
+            tendency_output={},
+            **kwargs
+        )
+
+
+class MockEmptyDiagnostic(MockDiagnosticComponent):
+
+    def __init__(self, **kwargs):
+        super(MockEmptyDiagnostic, self).__init__(
+            input_properties={},
+            diagnostic_properties={},
+            diagnostic_output={},
+            **kwargs
+        )
+
+
+class MockEmptyImplicit(MockStepper):
 
+    def __init__(self, **kwargs):
+        super(MockEmptyImplicit, self).__init__(
+            input_properties={},
+            diagnostic_properties={},
+            output_properties={},
+            diagnostic_output={},
+            state_output={},
+            **kwargs
+        )
 
-class MockPrognostic(Prognostic):
 
-    def __init__(self):
-        self._num_updates = 0
+class UpdateFrequencyBase(object):
 
-    def __call__(self, state):
-        self._num_updates += 1
-        return {}, {'num_updates': self._num_updates}
+    def get_component(self):
+        raise NotImplementedError()
 
+    def call_component(self, component, state):
+        raise NotImplementedError()
 
-class MockImplicit(Implicit):
+    def test_set_update_frequency_calls_initially(self):
+        component = UpdateFrequencyWrapper(self.get_component(), timedelta(hours=1))
+        assert isinstance(component, self.component_type)
+        state = {'time': timedelta(hours=0)}
+        result = self.call_component(component, state)
+        assert component.times_called == 1
 
-    output_properties = {
-        'value': {
-            'dims': [],
-            'units': 'm'
-        }
-    }
-
-    diagnostic_properties = {
-        'num_updates': {
-            'dims': [],
-            'units': ''
-        }
-    }
-
-    def __init__(self):
-        self._num_updates = 0
-
-    def __call__(self, state, timestep):
-        self._num_updates += 1
+    def test_set_update_frequency_does_not_repeat_call_at_same_timedelta(self):
+        component = UpdateFrequencyWrapper(self.get_component(), timedelta(hours=1))
+        assert isinstance(component, self.component_type)
+        state = {'time': timedelta(hours=0)}
+        result = self.call_component(component, state)
+        result = self.call_component(component, state)
+        assert component.times_called == 1
 
-        return (
-            {'num_updates': DataArray([self._num_updates], attrs={'units': ''})},
-            {'value': DataArray([1], attrs={'units': 'm'})})
+    def test_set_update_frequency_does_not_repeat_call_at_same_datetime(self):
+        component = UpdateFrequencyWrapper(self.get_component(), timedelta(hours=1))
+        assert isinstance(component, self.component_type)
+        state = {'time': datetime(2010, 1, 1)}
+        result = self.call_component(component, state)
+        result = self.call_component(component, state)
+        assert component.times_called == 1
 
+    def test_set_update_frequency_updates_result_when_equal(self):
+        component = UpdateFrequencyWrapper(self.get_component(), timedelta(hours=1))
+        assert isinstance(component, self.component_type)
+        result = self.call_component(component, {'time': timedelta(hours=0)})
+        result = self.call_component(component, {'time': timedelta(hours=1)})
+        assert component.times_called == 2
 
-class MockImplicitThatExpects(Implicit):
+    def test_set_update_frequency_updates_result_when_greater(self):
+        component = UpdateFrequencyWrapper(self.get_component(), timedelta(hours=1))
+        assert isinstance(component, self.component_type)
+        result = self.call_component(component, {'time': timedelta(hours=0)})
+        result = self.call_component(component, {'time': timedelta(hours=2)})
+        assert component.times_called == 2
 
-    input_properties = {'expected_field': {}}
-    output_properties = {'expected_field': {}}
-    diagnostic_properties = {'expected_field': {}}
+    def test_set_update_frequency_does_not_update_when_less(self):
+        component = UpdateFrequencyWrapper(self.get_component(), timedelta(hours=1))
+        assert isinstance(component, self.component_type)
+        result = self.call_component(component, {'time': timedelta(hours=0)})
+        result = self.call_component(component, {'time': timedelta(minutes=59)})
+        assert component.times_called == 1
 
-    def __init__(self, expected_value):
-        self._expected_value = expected_value
 
-    def __call__(self, state, timestep):
+class PrognosticUpdateFrequencyTests(unittest.TestCase, UpdateFrequencyBase):
 
-        input_value = state['expected_field']
-        if input_value != self._expected_value:
-            raise ValueError(
-                'Expected {}, but got {}'.format(self._expected_value, input_value))
+    component_type = TendencyComponent
 
-        return deepcopy(state), state
+    def get_component(self):
+        return MockEmptyPrognostic()
 
+    def call_component(self, component, state):
+        return component(state)
 
-class MockPrognosticThatExpects(Prognostic):
 
-    input_properties = {'expected_field': {}}
-    tendency_properties = {'expected_field': {}}
-    diagnostic_properties = {'expected_field': {}}
+class ImplicitPrognosticUpdateFrequencyTests(unittest.TestCase, UpdateFrequencyBase):
 
-    def __init__(self, expected_value):
-        self._expected_value = expected_value
+    component_type = ImplicitTendencyComponent
 
-    def __call__(self, state):
+    def get_component(self):
+        return MockEmptyImplicitPrognostic()
 
-        input_value = state['expected_field']
-        if input_value != self._expected_value:
-            raise ValueError(
-                'Expected {}, but got {}'.format(self._expected_value, input_value))
+    def call_component(self, component, state):
+        return component(state, timestep=timedelta(hours=1))
 
-        return deepcopy(state), state
 
+class ImplicitUpdateFrequencyTests(unittest.TestCase, UpdateFrequencyBase):
 
-class MockDiagnosticThatExpects(Diagnostic):
+    component_type = Stepper
 
-    input_properties = {'expected_field': {}}
-    diagnostic_properties = {'expected_field': {}}
+    def get_component(self):
+        return MockEmptyImplicit()
 
-    def __init__(self, expected_value):
-        self._expected_value = expected_value
+    def call_component(self, component, state):
+        return component(state, timedelta(minutes=1))
 
-    def __call__(self, state):
 
-        input_value = state['expected_field']
-        if input_value != self._expected_value:
-            raise ValueError(
-                'Expected {}, but got {}'.format(self._expected_value, input_value))
+class DiagnosticUpdateFrequencyTests(unittest.TestCase, UpdateFrequencyBase):
 
-        return state
+    component_type = DiagnosticComponent
 
+    def get_component(self):
+        return MockEmptyDiagnostic()
 
-class TimeDifferencingTests(unittest.TestCase):
-
-    def setUp(self):
-        self.implicit = MockImplicit()
-        self.wrapped = TimeDifferencingWrapper(self.implicit)
-        self.state = {
-            'value': DataArray([0], attrs={'units': 'm'})
-        }
-
-    def tearDown(self):
-        self.component = None
-
-    def testWrapperCallsImplicit(self):
-        tendencies, diagnostics = self.wrapped(self.state, timedelta(seconds=1))
-        assert diagnostics['num_updates'].values[0] == 1
-        tendencies, diagnostics = self.wrapped(self.state, timedelta(seconds=1))
-        assert diagnostics['num_updates'].values[0] == 2
-        assert len(diagnostics.keys()) == 1
-
-    def testWrapperComputesTendency(self):
-        tendencies, diagnostics = self.wrapped(self.state, timedelta(seconds=1))
-        assert len(tendencies.keys()) == 1
-        assert 'value' in tendencies.keys()
-        assert isinstance(tendencies['value'], DataArray)
-        assert_allclose(tendencies['value'].to_units('m s^-1').values[0], 1.)
-        assert_allclose(tendencies['value'].values[0], 1.)
-
-    def testWrapperComputesTendencyWithUnitConversion(self):
-        state = {
-            'value': DataArray([0.011], attrs={'units': 'km'})
-        }
-        tendencies, diagnostics = self.wrapped(state, timedelta(seconds=5))
-        assert 'value' in tendencies.keys()
-        assert isinstance(tendencies['value'], DataArray)
-        assert_allclose(tendencies['value'].to_units('m s^-1').values[0], -2)
-        assert_allclose(tendencies['value'].values[0], -2.)
-        assert_allclose(tendencies['value'].to_units('km s^-1').values[0], -0.002)
-
-
-def test_set_prognostic_update_frequency_calls_initially():
-    prognostic = UpdateFrequencyWrapper(MockPrognostic(), timedelta(hours=1))
-    state = {'time': timedelta(hours=0)}
-    tendencies, diagnostics = prognostic(state)
-    assert len(diagnostics) == 1
-    assert diagnostics['num_updates'] == 1
-
-
-def test_set_prognostic_update_frequency_caches_result():
-    prognostic = UpdateFrequencyWrapper(MockPrognostic(), timedelta(hours=1))
-    state = {'time': timedelta(hours=0)}
-    tendencies, diagnostics = prognostic(state)
-    tendencies, diagnostics = prognostic(state)
-    assert len(diagnostics) == 1
-    assert diagnostics['num_updates'] == 1
-
-
-def test_set_prognostic_update_frequency_caches_result_with_datetime():
-    prognostic = UpdateFrequencyWrapper(MockPrognostic(), timedelta(hours=1))
-    state = {'time': datetime(2000, 1, 1)}
-    tendencies, diagnostics = prognostic(state)
-    tendencies, diagnostics = prognostic(state)
-    assert len(diagnostics) == 1
-    assert diagnostics['num_updates'] == 1
-
-
-def test_set_prognostic_update_frequency_updates_result_when_equal():
-    prognostic = UpdateFrequencyWrapper(MockPrognostic(), timedelta(hours=1))
-    state = {'time': timedelta(hours=0)}
-    tendencies, diagnostics = prognostic({'time': timedelta(hours=0)})
-    tendencies, diagnostics = prognostic({'time': timedelta(hours=1)})
-    assert len(diagnostics) == 1
-    assert diagnostics['num_updates'] == 2
-
-
-def test_set_prognostic_update_frequency_updates_result_when_greater():
-    prognostic = UpdateFrequencyWrapper(MockPrognostic(), timedelta(hours=1))
-    state = {'time': timedelta(hours=0)}
-    tendencies, diagnostics = prognostic({'time': timedelta(hours=0)})
-    tendencies, diagnostics = prognostic({'time': timedelta(hours=2)})
-    assert len(diagnostics) == 1
-    assert diagnostics['num_updates'] == 2
-
-
-def test_put_prognostic_tendency_in_diagnostics_no_tendencies():
-    class MockPrognostic(Prognostic):
-        def __call__(self, state):
-            return {}, {}
-
-    prognostic = TendencyInDiagnosticsWrapper(MockPrognostic(), 'scheme')
-    tendencies, diagnostics = prognostic({})
-    assert len(tendencies) == 0
-    assert len(diagnostics) == 0
-
-
-def test_put_prognostic_tendency_in_diagnostics_one_tendency():
-    class MockPrognostic(Prognostic):
-        tendency_properties = {'quantity': {}}
-        def __call__(self, state):
-            return {'quantity': 1.}, {}
-
-    prognostic = TendencyInDiagnosticsWrapper(MockPrognostic(), 'scheme')
-    tendencies, diagnostics = prognostic({})
-    assert 'quantity_tendency_from_scheme' in prognostic.diagnostics
-    tendencies, diagnostics = prognostic({})
-    assert 'quantity_tendency_from_scheme' in diagnostics.keys()
-    assert len(diagnostics) == 1
-    assert tendencies['quantity'] == 1.
-    assert diagnostics['quantity_tendency_from_scheme'] == 1.
+    def call_component(self, component, state):
+        return component(state)
 
 
 def test_scaled_component_wrong_type():
-    class WrongType(object):
+    class WrongObject(object):
         def __init__(self):
             self.a = 1
 
-    wrong_component = WrongType()
-
-    with pytest.raises(TypeError) as excinfo:
-        component = ScalingWrapper(wrong_component)
-
-    assert 'either of type Implicit' in str(excinfo.value)
-
-
-def test_scaled_implicit_inputs():
-    implicit = ScalingWrapper(
-        MockImplicitThatExpects(2.0),
-        input_scale_factors={'expected_field': 0.5})
-
-    state = {'expected_field': 4.0}
-
-    diagnostics, new_state = implicit(state)
-
-    assert new_state['expected_field'] == 2.0
-    assert diagnostics['expected_field'] == 2.0
-
-
-def test_scaled_implicit_outputs():
-    implicit = ScalingWrapper(
-        MockImplicitThatExpects(4.0),
-        output_scale_factors={'expected_field': 0.5})
-
-    state = {'expected_field': 4.0}
-
-    diagnostics, new_state = implicit(state)
-
-    assert new_state['expected_field'] == 2.0
-    assert diagnostics['expected_field'] == 4.0
+    wrong_component = WrongObject()
 
+    with pytest.raises(TypeError):
+        ScalingWrapper(wrong_component)
 
-def test_scaled_implicit_diagnostics():
-    implicit = ScalingWrapper(
-        MockImplicitThatExpects(4.0),
-        diagnostic_scale_factors={'expected_field': 0.5})
 
-    state = {'expected_field': 4.0}
+class ScalingInputMixin(object):
 
-    diagnostics, new_state = implicit(state)
-
-    assert diagnostics['expected_field'] == 2.0
-    assert new_state['expected_field'] == 4.0
-
-
-def test_scaled_implicit_created_with_wrong_input_field():
-    with pytest.raises(ValueError) as excinfo:
-        implicit = ScalingWrapper(
-            MockImplicitThatExpects(2.0),
-            input_scale_factors={'abcd': 0.5})
-
-    assert 'not a valid input' in str(excinfo.value)
-
-
-def test_scaled_implicit_created_with_wrong_output_field():
-    with pytest.raises(ValueError) as excinfo:
-        implicit = ScalingWrapper(
-            MockImplicitThatExpects(2.0),
-            output_scale_factors={'abcd': 0.5})
-
-    assert 'not a valid output' in str(excinfo.value)
-
-
-def test_scaled_implicit_created_with_wrong_diagnostic_field():
-    with pytest.raises(ValueError) as excinfo:
-        implicit = ScalingWrapper(
-            MockImplicitThatExpects(2.0),
-            diagnostic_scale_factors={'abcd': 0.5})
-
-    assert 'not a valid diagnostic' in str(excinfo.value)
-
-
-def test_scaled_prognostic_inputs():
-    prognostic = ScalingWrapper(
-        MockPrognosticThatExpects(2.0),
-        input_scale_factors={'expected_field': 0.5})
-
-    state = {'expected_field': 4.0}
-
-    tendencies, diagnostics = prognostic(state)
+    def test_inputs_no_scaling(self):
+        self.input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+        }
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(base_component, input_scale_factors={})
+        assert isinstance(component, self.component_type)
+        self.call_component(component, state)
+        assert base_component.state_given.keys() == state.keys()
+        assert np.all(base_component.state_given['input1'] == state['input1'].values)
+
+    def test_inputs_one_scaling(self):
+        self.input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+            'input2': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+        }
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+            'input2': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            input_scale_factors={
+                'input1': 10.
+            })
+        assert isinstance(component, self.component_type)
+        self.call_component(component, state)
+        assert base_component.state_given.keys() == state.keys()
+        assert np.all(base_component.state_given['input1'] == state['input1'].values * 10.)
+        assert np.all(base_component.state_given['input2'] == state['input2'].values)
+
+    def test_inputs_two_scalings(self):
+        self.input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+            'input2': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+        }
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+            'input2': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            input_scale_factors={
+                'input1': 10.,
+                'input2': 5.,
+            })
+        assert isinstance(component, self.component_type)
+        self.call_component(component, state)
+        assert base_component.state_given.keys() == state.keys()
+        assert np.all(base_component.state_given['input1'] == 10.)
+        assert np.all(base_component.state_given['input2'] == 5.)
+
+    def test_inputs_one_scaling_with_unit_conversion(self):
+        self.input_properties = {
+            'input1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+            'input2': {
+                'dims': ['dim1'],
+                'units': 'm',
+            },
+        }
+        state = {
+            'time': timedelta(0),
+            'input1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'km'}
+            ),
+            'input2': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            ),
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            input_scale_factors={
+                'input1': 0.5
+            })
+        assert isinstance(component, self.component_type)
+        self.call_component(component, state)
+        assert base_component.state_given.keys() == state.keys()
+        assert np.all(base_component.state_given['input1'] == 500.)
+        assert np.all(base_component.state_given['input2'] == 1.)
+
+
+class ScalingOutputMixin(object):
+
+    def test_output_no_scaling(self):
+        self.output_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        self.output_state = {
+            'diag1': np.ones([10])
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            output_scale_factors={},
+        )
+        assert isinstance(component, self.component_type)
+        state = {'time': timedelta(0)}
+        outputs = self.get_outputs(self.call_component(component, state))
+        assert outputs.keys() == self.output_state.keys()
+        assert np.all(outputs['diag1'] == 1.)
+
+    def test_output_one_scaling(self):
+        self.output_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        self.output_state = {
+            'diag1': np.ones([10])
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            output_scale_factors={
+                'diag1': 10.,
+            },
+        )
+        assert isinstance(component, self.component_type)
+        state = {'time': timedelta(0)}
+        outputs = self.get_outputs(
+            self.call_component(component, state))
+        assert outputs.keys() == self.output_state.keys()
+        assert np.all(outputs['diag1'] == 10.)
+
+    def test_output_no_scaling_when_input_scaled(self):
+        self.input_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        self.output_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        self.output_state = {
+            'diag1': np.ones([10])
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            input_scale_factors={
+                'diag1': 10.,
+            },
+        )
+        assert isinstance(component, self.component_type)
+        state = {
+            'time': timedelta(0),
+            'diag1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        outputs = self.get_outputs(
+            self.call_component(component, state))
+        assert outputs.keys() == self.output_state.keys()
+        assert np.all(outputs['diag1'] == 1.)
 
-    assert tendencies['expected_field'] == 2.0
-    assert diagnostics['expected_field'] == 2.0
 
+class ScalingDiagnosticMixin(object):
 
-def test_scaled_prognostic_tendencies():
-    prognostic = ScalingWrapper(
-        MockPrognosticThatExpects(4.0),
-        tendency_scale_factors={'expected_field': 0.5})
+    def test_diagnostic_no_scaling(self):
+        self.diagnostic_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        self.diagnostic_output = {
+            'diag1': np.ones([10])
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            diagnostic_scale_factors={},
+        )
+        assert isinstance(component, self.component_type)
+        state = {'time': timedelta(0)}
+        diagnostics = self.get_diagnostics(self.call_component(component, state))
+        assert diagnostics.keys() == self.diagnostic_output.keys()
+        assert np.all(diagnostics['diag1'] == 1.)
+
+    def test_diagnostic_one_scaling(self):
+        self.diagnostic_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        self.diagnostic_output = {
+            'diag1': np.ones([10])
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            diagnostic_scale_factors={
+                'diag1': 10.,
+            },
+        )
+        assert isinstance(component, self.component_type)
+        state = {'time': timedelta(0)}
+        diagnostics = self.get_diagnostics(
+            self.call_component(component, state))
+        assert diagnostics.keys() == self.diagnostic_output.keys()
+        assert np.all(diagnostics['diag1'] == 10.)
+
+    def test_diagnostic_no_scaling_when_input_scaled(self):
+        self.input_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        self.diagnostic_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        self.diagnostic_output = {
+            'diag1': np.ones([10])
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            input_scale_factors={
+                'diag1': 10.,
+            },
+        )
+        assert isinstance(component, self.component_type)
+        state = {
+            'time': timedelta(0),
+            'diag1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        diagnostics = self.get_diagnostics(
+            self.call_component(component, state))
+        assert diagnostics.keys() == self.diagnostic_output.keys()
+        assert np.all(diagnostics['diag1'] == 1.)
 
-    state = {'expected_field': 4.0}
 
-    tendencies, diagnostics = prognostic(state)
+class ScalingTendencyMixin(object):
 
-    assert tendencies['expected_field'] == 2.0
-    assert diagnostics['expected_field'] == 4.0
+    def test_tendency_no_scaling(self):
+        self.tendency_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        self.tendency_output = {
+            'diag1': np.ones([10])
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            tendency_scale_factors={},
+        )
+        assert isinstance(component, self.component_type)
+        state = {'time': timedelta(0)}
+        tendencies = self.get_tendencies(self.call_component(component, state))
+        assert tendencies.keys() == self.tendency_output.keys()
+        assert np.all(tendencies['diag1'] == 1.)
+
+    def test_tendency_one_scaling(self):
+        self.tendency_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm',
+            }
+        }
+        self.tendency_output = {
+            'diag1': np.ones([10])
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            tendency_scale_factors={
+                'diag1': 10.,
+            },
+        )
+        assert isinstance(component, self.component_type)
+        state = {'time': timedelta(0)}
+        tendencies = self.get_tendencies(
+            self.call_component(component, state))
+        assert tendencies.keys() == self.tendency_output.keys()
+        assert np.all(tendencies['diag1'] == 10.)
+
+    def test_tendency_no_scaling_when_input_scaled(self):
+        self.input_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm'
+            }
+        }
+        self.tendency_properties = {
+            'diag1': {
+                'dims': ['dim1'],
+                'units': 'm/s',
+            }
+        }
+        self.tendency_output = {
+            'diag1': np.ones([10])
+        }
+        base_component = self.get_component()
+        component = ScalingWrapper(
+            base_component,
+            input_scale_factors={
+                'diag1': 10.,
+            },
+        )
+        assert isinstance(component, self.component_type)
+        state = {
+            'time': timedelta(0),
+            'diag1': DataArray(
+                np.ones([10]),
+                dims=['dim1'],
+                attrs={'units': 'm'}
+            )
+        }
+        tendencies = self.get_tendencies(
+            self.call_component(component, state))
+        assert tendencies.keys() == self.tendency_output.keys()
+        assert np.all(tendencies['diag1'] == 1.)
 
 
-def test_scaled_prognostic_diagnostics():
-    prognostic = ScalingWrapper(
-        MockPrognosticThatExpects(4.0),
-        diagnostic_scale_factors={'expected_field': 0.5})
+class DiagnosticScalingTests(
+    unittest.TestCase, ScalingInputMixin, ScalingDiagnosticMixin):
 
-    state = {'expected_field': 4.0}
+    component_type = DiagnosticComponent
 
-    tendencies, diagnostics = prognostic(state)
+    def setUp(self):
+        self.input_properties = {}
+        self.diagnostic_properties = {}
+        self.diagnostic_output = {}
 
-    assert tendencies['expected_field'] == 4.0
-    assert diagnostics['expected_field'] == 2.0
+    def get_component(self):
+        return MockDiagnosticComponent(
+            self.input_properties,
+            self.diagnostic_properties,
+            self.diagnostic_output
+        )
 
+    def get_diagnostics(self, output):
+        return output
 
-def test_scaled_prognostic_with_wrong_tendency_field():
-    with pytest.raises(ValueError) as excinfo:
-        prognostic = ScalingWrapper(
-            MockPrognosticThatExpects(4.0),
-            tendency_scale_factors={'abcd': 0.5})
+    def call_component(self, component, state):
+        return component(state)
 
-    assert 'not a valid tendency' in str(excinfo.value)
 
+class PrognosticScalingTests(
+    unittest.TestCase, ScalingInputMixin, ScalingDiagnosticMixin, ScalingTendencyMixin):
 
-def test_scaled_diagnostic_inputs():
-    diagnostic = ScalingWrapper(
-        MockDiagnosticThatExpects(2.0),
-        input_scale_factors={'expected_field': 0.5})
+    component_type = TendencyComponent
 
-    state = {'expected_field': 4.0}
+    def setUp(self):
+        self.input_properties = {}
+        self.diagnostic_properties = {}
+        self.tendency_properties = {}
+        self.diagnostic_output = {}
+        self.tendency_output = {}
 
-    diagnostics = diagnostic(state)
+    def get_component(self):
+        return MockTendencyComponent(
+            self.input_properties,
+            self.diagnostic_properties,
+            self.tendency_properties,
+            self.diagnostic_output,
+            self.tendency_output,
+        )
 
-    assert diagnostics['expected_field'] == 2.0
+    def get_diagnostics(self, output):
+        return output[1]
 
+    def get_tendencies(self, output):
+        return output[0]
 
+    def call_component(self, component, state):
+        return component(state)
 
-def test_scaled_diagnostic_diagnostics():
 
-    diagnostic = ScalingWrapper(
-        MockDiagnosticThatExpects(4.0),
-        diagnostic_scale_factors = {'expected_field': 0.5})
+class ImplicitPrognosticScalingTests(
+    unittest.TestCase, ScalingInputMixin, ScalingDiagnosticMixin,
+    ScalingTendencyMixin):
 
-    state = {'expected_field': 4.0}
+    component_type = ImplicitTendencyComponent
 
-    diagnostics = diagnostic(state)
+    def setUp(self):
+        self.input_properties = {}
+        self.diagnostic_properties = {}
+        self.tendency_properties = {}
+        self.diagnostic_output = {}
+        self.tendency_output = {}
 
-    assert diagnostics['expected_field'] == 2.0
+    def get_component(self):
+        return MockImplicitTendencyComponent(
+            self.input_properties,
+            self.diagnostic_properties,
+            self.tendency_properties,
+            self.diagnostic_output,
+            self.tendency_output,
+        )
 
+    def get_diagnostics(self, output):
+        return output[1]
 
-def test_scaled_component_type_wrongly_modified():
+    def get_tendencies(self, output):
+        return output[0]
 
-    diagnostic = ScalingWrapper(
-        MockDiagnosticThatExpects(4.0),
-        diagnostic_scale_factors = {'expected_field': 0.5})
+    def call_component(self, component, state):
+        return component(state, timedelta(hours=1))
 
-    state = {'expected_field': 4.0}
 
-    diagnostic._component_type = 'abcd'
+class ImplicitScalingTests(
+    unittest.TestCase, ScalingInputMixin, ScalingDiagnosticMixin,
+    ScalingOutputMixin):
 
-    with pytest.raises(ValueError) as excinfo:
-        diagnostics = diagnostic(state)
+    component_type = Stepper
 
-    assert 'bug in ScalingWrapper' in str(excinfo.value)
+    def setUp(self):
+        self.input_properties = {}
+        self.diagnostic_properties = {}
+        self.output_properties = {}
+        self.diagnostic_output = {}
+        self.output_state = {}
+
+    def get_component(self):
+        return MockStepper(
+            self.input_properties,
+            self.diagnostic_properties,
+            self.output_properties,
+            self.diagnostic_output,
+            self.output_state,
+        )
+
+    def get_diagnostics(self, output):
+        return output[0]
+
+    def get_outputs(self, output):
+        return output[1]
+
+    def call_component(self, component, state):
+        return component(state, timedelta(hours=1))
 
 if __name__ == '__main__':
     pytest.main([__file__])