Repository: USEPA/WNTR (language: python)

Issue:
Pump/Valve speed/setting controls should make the pump/valve open/active
When a control acts on a pump speed or a valve setting, the pump/valve should be made open/active. This pull request makes this work.


Discussion / hints:
@kaklise Did you have any concerns with this pull request?
This looks good.  I just updated documentation.  This PR closes #48.

Fix patch:
diff --git a/documentation/resilience.rst b/documentation/resilience.rst
index e9979bd8b..833201c1c 100644
--- a/documentation/resilience.rst
+++ b/documentation/resilience.rst
@@ -523,5 +523,5 @@ The following examples compute economic metrics, including:
       >>> pump_flowrate = results.link['flowrate'].loc[:,wn.pump_name_list]
       >>> head = results.node['head']
       >>> pump_energy = wntr.metrics.pump_energy(pump_flowrate, head, wn)
-      >>> pump_cost = wntr.metrics.pump_cost(pump_flowrate, head, wn)
+      >>> pump_cost = wntr.metrics.pump_cost(pump_energy, wn)
     
diff --git a/documentation/whatsnew/v0.3.1.rst b/documentation/whatsnew/v0.3.1.rst
new file mode 100644
index 000000000..10ccf73b0
--- /dev/null
+++ b/documentation/whatsnew/v0.3.1.rst
@@ -0,0 +1,28 @@
+.. _whatsnew_031:
+
+v0.3.1 (dev branch)
+---------------------------------------------------
+
+* The `run_sim` method for the EpanetSimulator now includes the option to define convergence error behavior.
+  
+  * If the simulation does not converge and `convergence_error` is True, an error is raised. 
+  * If the simulation does not converge and `convergence_error` is False, partial results are returned, a warning is issued, and results object `error_code` is set to 0.
+  
+  The default setting for `convergence_error` when using the EpanetSimulator is False.
+  
+* The default setting for `convergence_error` in the WNTRSimulator was changed to False.
+  
+* Updated the `add_outage` method on pumps so it can be used with the EpanetSimulator.  
+  Added a `remove_outage` method on pumps.
+ 
+* Added a `convert_controls_to_rules` method on the water network model.  In some instances, mixing controls and rules has unexpected consequences.  
+  By converting all controls to rules, the user can specify the priority for each rule. 
+  
+* Suppressed the OptimizeWarning from scipy.optimize that often occurs when fitting a pump curve.  
+  This is not an error.
+
+* Bug fix in the pump energy calculation and added additional tests.  The function, which was returning power in Watts, now returns energy in Joules. 
+
+* Pump cost function was updated to use pump energy, instead of recomputing those values.
+
+* Updated API documentation to include a table of class properties for each network class (Junction, Tank, Pipe, etc.).
diff --git a/wntr/__init__.py b/wntr/__init__.py
index 6cd04da94..2f59b9c94 100644
--- a/wntr/__init__.py
+++ b/wntr/__init__.py
@@ -7,7 +7,7 @@
 from wntr import graphics
 from wntr import utils
 
-__version__ = '0.3.0'
+__version__ = '0.3.1'
 
 __copyright__ = """Copyright 2019 National Technology & Engineering 
 Solutions of Sandia, LLC (NTESS). Under the terms of Contract DE-NA0003525 
diff --git a/wntr/epanet/io.py b/wntr/epanet/io.py
index 394546147..ad11ac60e 100644
--- a/wntr/epanet/io.py
+++ b/wntr/epanet/io.py
@@ -1364,6 +1364,8 @@ def _write_rules(self, f, wn):
         for text, all_control in wn.controls():
             entry = '{}\n'
             if all_control.epanet_control_type == _ControlType.rule:
+                if all_control.name == '':
+                    all_control._name = text
                 rule = _EpanetRule('blah', self.flow_units, self.mass_units)
                 rule.from_if_then_else(all_control)
                 f.write(entry.format(str(rule)).encode('ascii'))
@@ -2547,6 +2549,15 @@ def __init__(self, result_types=None, network=False, energy=False, statistics=Fa
         self.keep_energy = energy
         self.keep_statistics = statistics
 
+    def _get_time(self, t):
+        s = int(t)
+        h = int(s/3600)
+        s -= h*3600
+        m = int(s/60)
+        s -= m*60
+        s = int(s)
+        return '{:02}:{:02}:{:02}'.format(h, m, s)
+    
     def setup_ep_results(self, times, nodes, links, result_types=None):
         """Set up the results object (or file, etc.) for save_ep_line() calls to use.
 
@@ -2664,13 +2675,18 @@ def finalize_save(self, good_read, sim_warnings):
         pass
 
 #    @run_lineprofile()
-    def read(self, filename, custom_handlers=False):
+    def read(self, filename, convergence_error=False, custom_handlers=False):
         """Read a binary file and create a results object.
 
         Parameters
         ----------
         filename : str
             An EPANET BIN output file
+        convergence_error: bool (optional)
+            If convergence_error is True, an error will be raised if the
+            simulation does not converge. If convergence_error is False, partial results are returned, 
+            a warning will be issued, and results.error_code will be set to 0
+            if the simulation does not converge.  Default = False.
         custom_handlers : bool, optional
             If true, then the the custom, by-line handlers will be used. (:func:`~save_ep_line`, 
             :func:`~setup_ep_results`, :func:`~finalize_save`, etc.) Otherwise read will use
@@ -2873,13 +2889,29 @@ def read(self, filename, custom_handlers=False):
 #                tuples = zip(type_list, valuetype, name_list)
                 tuples = list(zip(valuetype, name_list))
 #                tuples = [(valuetype[i], v) for i, v in enumerate(name_list)]
-                index = pd.MultiIndex.from_tuples(tuples, names=['value','name'])          
+                index = pd.MultiIndex.from_tuples(tuples, names=['value','name'])      
+                
                 try:
                     data = np.fromfile(fin, dtype = np.dtype(ftype), count = (4*nnodes+8*nlinks)*nrptsteps)
-                    data = np.reshape(data, (nrptsteps, (4*nnodes+8*nlinks)))
                 except Exception as e:
                     logger.exception('Failed to process file: %s', e)
                     
+                N = int(np.floor(len(data)/(4*nnodes+8*nlinks)))
+                if N < nrptsteps:
+                    t = reporttimes[N]
+                    if convergence_error:
+                        logger.error('Simulation did not converge at time ' + self._get_time(t) + '.')
+                        raise RuntimeError('Simulation did not converge at time ' + self._get_time(t) + '.')
+                    else:
+                        data = data[0:N*(4*nnodes+8*nlinks)]
+                        data = np.reshape(data, (N, (4*nnodes+8*nlinks)))
+                        reporttimes = reporttimes[0:N]
+                        warnings.warn('Simulation did not converge at time ' + self._get_time(t) + '.')
+                        self.results.error_code = wntr.sim.results.ResultsStatus.error
+                else:
+                    data = np.reshape(data, (nrptsteps, (4*nnodes+8*nlinks)))
+                    self.results.error_code = None
+
                 df = pd.DataFrame(data.transpose(), index =index, columns = reporttimes)
                 df = df.transpose()
                 
diff --git a/wntr/metrics/economic.py b/wntr/metrics/economic.py
index d78548f25..d9e5f8cc7 100644
--- a/wntr/metrics/economic.py
+++ b/wntr/metrics/economic.py
@@ -256,21 +256,12 @@ def pump_energy(flowrate, head, wn):
     """
     Compute the pump energy over time.
     
-    The computation uses pump flow rate, pump head, pump efficiency, and the 
-    electricity price. Pump efficiency curves may be specified through the 
-    "efficiency" attribute on the pump object. Alternatively, a global 
-    efficiency may be set on the wn.energy object:
+    The computation uses pump flow rate, node head (used to compute headloss at
+    each pump), and pump efficiency. Pump efficiency is defined in
+    ``wn.options.energy.global_efficiency``. Pump efficiency curves are currently 
+    not supported.
 
-        wn.energy.global_efficiency = 75 # This means 75% or 0.75
-
-    The price can also be set on the pump or the energy object:
-
-        wn.energy.global_price = 3.61e-8  # $/J; equal to $0.13/kW-h
-
-    or
-
-        pump = wn.get_link('pump1')
-        pump.energy_price = 3.61e-8  # $/J
+        wn.options.energy.global_efficiency = 75 # This means 75% or 0.75
 
     Parameters
     ----------
@@ -279,23 +270,18 @@ def pump_energy(flowrate, head, wn):
         (index = times, columns = pump names).
     
     head : pandas DataFrame
-        A pandas Dataframe containing node head 
+        A pandas DataFrame containing node head 
         (index = times, columns = node names).
         
-     wn: wntr WaterNetworkModel
+    wn: wntr WaterNetworkModel
         Water network model.  The water network model is needed to 
         define energy efficiency.
 
     Returns
     -------
-    A tuple of pandas DataFrames: the first DataFrame contains pump energy in Watts,
-    the second contains pump cost is $/s (index = times, columns = pump names).
+    A DataFrame that contains pump energy in J (index = times, columns = pump names).
     """
     
-    # TODO: Need to get this unit tested and not just functionally tested
-    if wn.options.energy.demand_charge is not None and wn.options.energy.demand_charge != 0:
-        raise ValueError('WNTR does not support demand charge yet.')
-
     pumps = wn.pump_name_list
     time = flowrate.index
     
@@ -321,23 +307,25 @@ def pump_energy(flowrate, head, wn):
 
     efficiency = pd.DataFrame(data=efficiency_dict, index=time, columns=pumps)
 
-    energy = 1000.0 * 9.81 * headloss * flowrate / efficiency
+    power = 1000.0 * 9.81 * headloss * flowrate / efficiency # Watts = J/s
+    energy = power * wn.options.time.report_timestep # J = Ws
     
     return energy
 
-def pump_cost(flowrate, head, wn):
+def pump_cost(energy, wn):
     """
-    Compute the pump cost over time.
+    Compute the pump cost over time. 
     
+    Energy cost is defined in ``wn.options.energy.global_price``. Pump energy 
+    price and price patterns are currently not supported. 
+
+        wn.options.energy.global_price = 3.61e-8  # $/J; equal to $0.13/kW-h
+        
     Parameters
     ----------
-    flowrate : pandas DataFrame
-        A pandas Dataframe containing pump flowrates 
+    energy : pandas DataFrame
+        A pandas DataFrame containing pump energy (J), computed from ``wntr.metrics.pump_energy`` 
         (index = times, columns = pump names).
-    
-    head : pandas DataFrame
-        A pandas Dataframe containing node head 
-        (index = times, columns = node names).
         
     wn: wntr WaterNetworkModel
         Water network model.  The water network model is needed to 
@@ -345,13 +333,17 @@ def pump_cost(flowrate, head, wn):
         
     Returns
     -----------
-    Pump cost (float)
+    A DataFrame that contains pump cost in $ (index = times, columns = pump names).
     
     """
-    time = flowrate.index
+    time = energy.index
     pumps = wn.pump_name_list
-    energy = pump_energy(flowrate, head, wn)
     
+    # TODO: Need to get this unit tested and not just functionally tested
+    if wn.options.energy.demand_charge is not None and wn.options.energy.demand_charge != 0:
+        # Additional energy charge per maximum kilowatt usage
+        raise ValueError('WNTR does not support demand charge yet.')
+        
     price_dict = {}
     for pump_name, pump in wn.pumps():
         if pump.energy_price is None and pump.energy_pattern is None:
@@ -369,7 +361,7 @@ def pump_cost(flowrate, head, wn):
             
     price = pd.DataFrame(data=price_dict, index=time, columns=pumps)
     
-    pump_cost =energy * price
+    pump_cost = energy * price
     
     return pump_cost
     
diff --git a/wntr/network/base.py b/wntr/network/base.py
index e089eadd2..c9c06468a 100644
--- a/wntr/network/base.py
+++ b/wntr/network/base.py
@@ -6,15 +6,16 @@
 
 .. autosummary::
 
-    AbstractModel
-    Subject
-    Observer
     Node
     Link
     Registry
     NodeType
     LinkType
     LinkStatus
+    AbstractModel
+    Subject
+    Observer
+
 
 """
 import logging
@@ -76,13 +77,34 @@ class Node(six.with_metaclass(abc.ABCMeta, object)):
     :class:`~wntr.network.elements.Tank`, and
     :class:`~wntr.network.elements.Reservoir`
 
+
+    .. rubric:: Constructor
+    
+    This is an abstract class and should not be instantiated directly.
+
     Parameters
     -----------
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         WaterNetworkModel object
     name : string
         Name of the node (must be unique among nodes of all types)
+    
+
+    .. rubric:: Attributes
+
+    .. autosummary::
 
+        name
+        node_type
+        head
+        demand
+        coordinates
+        initial_quality
+        tag
+        leak_demand
+        leak_status
+        leak_area
+        leak_discharge_coeff
 
     """
     def __init__(self, wn, name):
@@ -107,6 +129,19 @@ def __init__(self, wn, name):
         self._is_isolated = False
 
     def _compare(self, other):
+        """
+        Comparison function
+
+        Parameters
+        ----------
+        other : Node
+            object to compare with
+
+        Returns
+        -------
+        bool
+            is these the same items
+        """        
         if not type(self) == type(other):
             return False
         if self.name == other.name and \
@@ -171,12 +206,12 @@ def leak_discharge_coeff(self, value):
 
     @property
     def node_type(self):
-        """str: The node type"""
+        """str: The node type (read only)"""
         return 'Node'
     
     @property
     def name(self):
-        """str: The name of the node"""
+        """str: The name of the node (read only)"""
         return self._name
     
     @property
@@ -215,9 +250,11 @@ def todict(self):
         d = {}
         for k in dir(self):
             if not k.startswith('_'):
-                val = getattr(self, k)
-                if not isinstance(val, types.MethodType):
-                    d[k] = val
+                try:
+                    val = getattr(self, k)
+                    if not isinstance(val, types.MethodType):
+                        d[k] = val
+                except DeprecationWarning: pass
         return d
 
 
@@ -229,6 +266,10 @@ class Link(six.with_metaclass(abc.ABCMeta, object)):
     :class:`~wntr.network.elements.Pump`, and
     :class:`~wntr.network.elements.Valve`
 
+    .. rubric:: Constructor
+    
+    This is an abstract class and should not be instantiated directly.
+
     Parameters
     ----------
     wn : :class:`~wntr.network.model.WaterNetworkModel`
@@ -240,6 +281,25 @@ class Link(six.with_metaclass(abc.ABCMeta, object)):
     end_node_name : string
         Name of the end node
     
+
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        status
+        setting
+        tag
+        vertices
+
+
     """
     def __init__(self, wn, link_name, start_node_name, end_node_name):
         # Set the registries
@@ -258,13 +318,13 @@ def __init__(self, wn, link_name, start_node_name, end_node_name):
         self._end_node = self._node_reg[end_node_name]
         self._node_reg.add_usage(end_node_name, (link_name, self.link_type))
         # Set up other metadata fields
-        self._initial_status = LinkStatus.opened
+        self._initial_status = LinkStatus.Opened
         self._initial_setting = None
         self._vertices = []
         self._tag = None
         # Model state variables
-        self._user_status = LinkStatus.opened
-        self._internal_status = LinkStatus.active
+        self._user_status = LinkStatus.Opened
+        self._internal_status = LinkStatus.Active
         self._prev_setting = None
         self._setting = None
         self._flow = None
@@ -304,7 +364,7 @@ def __repr__(self):
 
     @property
     def link_type(self):
-        """str: The link type"""
+        """str: the link type (read only)"""
         return 'Link'
 
     @property
@@ -369,7 +429,7 @@ def flow(self):
     @property
     @abc.abstractmethod
     def status(self):
-        """:class:`~wntr.network.base.LinkStatus`: Current status of the link"""
+        """:class:`~wntr.network.base.LinkStatus`: (**abstract**) current status of the link"""
         pass
     @status.setter
     @abc.abstractmethod
@@ -426,6 +486,7 @@ class Registry(MutableMapping):
     ----------
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         WaterNetworkModel object
+    
     """
     
     def __init__(self, wn):
@@ -494,6 +555,7 @@ def usage(self):
             The name of the object in the registry
         value : tuple of (str, str)
             Tuple of (name, typestr) of the external items using the object
+        
         """
         for k, v in self._usage.items():
             yield k, v
@@ -598,16 +660,17 @@ class NodeType(enum.IntEnum):
 
     .. rubric:: Enum Members
 
-    ==================  ==================================================================
-    :attr:`~Junction`   Node is a :class:`~wntr.network.model.Junction`
-    :attr:`~Reservoir`  Node is a :class:`~wntr.network.model.Reservoir`
-    :attr:`~Tank`       Node is a :class:`~wntr.network.model.Tank`
-    ==================  ==================================================================
+    .. autosummary::
+
+        Junction
+        Reservoir
+        Tank
+
 
     """
-    Junction = 0
-    Reservoir = 1
-    Tank = 2
+    Junction = 0  #: node is a junction
+    Reservoir = 1  #: node is a reservoir
+    Tank = 2  #: node is a tank
 
     def __init__(self, val):
         if self.name != self.name.upper():
@@ -629,30 +692,31 @@ class LinkType(enum.IntEnum):
 
     .. rubric:: Enum Members
 
-    ===============  ==================================================================
-    :attr:`~CV`      Pipe with check valve
-    :attr:`~Pipe`    Regular pipe
-    :attr:`~Pump`    Pump
-    :attr:`~Valve`   Any valve type (see following)
-    :attr:`~PRV`     Pressure reducing valve
-    :attr:`~PSV`     Pressure sustaining valve
-    :attr:`~PBV`     Pressure breaker valve
-    :attr:`~FCV`     Flow control valve
-    :attr:`~TCV`     Throttle control valve
-    :attr:`~GPV`     General purpose valve
-    ===============  ==================================================================
+    .. autosummary::
+
+        CV
+        Pipe
+        Pump
+        PRV
+        PSV
+        PBV
+        FCV
+        TCV
+        GPV
+        Valve
+
 
     """
-    CV = 0
-    Pipe = 1
-    Pump = 2
-    PRV = 3
-    PSV = 4
-    PBV = 5
-    FCV = 6
-    TCV = 7
-    GPV = 8
-    Valve = 9
+    CV = 0  #: pipe with a check valve
+    Pipe = 1  #: pipe with no check valve
+    Pump = 2  #: a pump of any type 
+    PRV = 3  #: a pressure reducing valve
+    PSV = 4  #: a pressure sustaining valve
+    PBV = 5  #: a pressure breaker valve
+    FCV = 6  #: a flow control valve
+    TCV = 7  #: a throttle control valve
+    GPV = 8  #: a general purpose valve
+    Valve = 9  #: a valve of any type
 
     def __init__(self, val):
         if self.name != self.name.upper():
@@ -673,24 +737,26 @@ class LinkStatus(enum.IntEnum):
     Enum class for link statuses.
 
     .. warning::
-        This is NOT the class for determining output status from an EPANET binary file.
+        This is NOT the class for determining output status from an EPANET **binary** file.
         The class for output status is wntr.epanet.util.LinkTankStatus.
 
     .. rubric:: Enum Members
 
-    =================  ==================================================================
-    :attr:`~Closed`    Pipe/valve/pump is closed.
-    :attr:`~Opened`    Pipe/valve/pump is open.
-    :attr:`~Open`      Alias to "Opened"
-    :attr:`~Active`    Valve is partially open.
-    =================  ==================================================================
+    .. autosummary::
+
+        Closed
+        Opened
+        Active
+        CV
+        Open
+
 
     """
-    Closed = 0
-    Open = 1
-    Opened = 1
-    Active = 2
-    CV = 3
+    Closed = 0  #: pipe/valve/pump is closed
+    Open = 1  #: alias for `Opened`
+    Opened = 1  #: pipe/valve/pump is open
+    Active = 2  #: valve is partially open or pump has a specific setting
+    CV = 3  #: pipe has a check valve
 
     def __init__(self, val):
         if self.name != self.name.upper():
diff --git a/wntr/network/elements.py b/wntr/network/elements.py
index a4bff6642..5f579a85d 100644
--- a/wntr/network/elements.py
+++ b/wntr/network/elements.py
@@ -34,7 +34,7 @@
 import math
 import six
 import copy
-from scipy.optimize import curve_fit
+from scipy.optimize import curve_fit, OptimizeWarning
 
 from collections.abc import MutableSequence
 
@@ -42,6 +42,9 @@
 from .options import TimeOptions
 from wntr.epanet.util import MixType
 
+import warnings
+warnings.simplefilter("ignore", OptimizeWarning) # ignore scipy.optimize.OptimizeWarning
+
 logger = logging.getLogger(__name__)
 
 
@@ -49,8 +52,14 @@ class Junction(Node):
     """
     Junction class, inherited from Node.
     
+    Junctions are the nodes that contain demand, emitters, and 
+    water quality sources.
+
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_junction()` method. 
+    Direct creation through the constructor is highly discouraged.
 
     Parameters
     ----------
@@ -58,34 +67,45 @@ class Junction(Node):
         Name of the junction.
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         WaterNetworkModel object the junction will belong to
-        
-    """
 
-    def __init__(self, name, wn):
-        super(Junction, self).__init__(wn, name)
-        self.demand_timeseries_list = Demands(self._pattern_reg)
-        self.elevation = 0.0
 
-        self.required_pressure = None
-        """float: The required pressure attribute is used for pressure-dependent demand
-        simulations. This is the lowest pressure at which the junction receives 
-        the full requested demand. If set to None, the global value in 
-        wn.options.hydraulic.required_pressure is used."""
+    .. rubric:: Attributes
 
-        self.minimum_pressure = None
-        """float: The minimum pressure attribute is used for pressure-dependent demand 
-        simulations. Below this pressure, the junction will not receive any water.
-        If set to None, the global value in wn.options.hydraulic.minimum_pressure is used."""
+    .. autosummary:: 
 
-        self.emitter_coefficient = None
-        
+        name
+        node_type
+        head
+        demand
+        demand_timeseries_list
+        elevation
+        required_pressure
+        minimum_pressure
+        emitter_coefficient
+        base_demand
+        coordinates
+        initial_quality
+        tag
+        leak_demand
+        leak_status
+        leak_area
+        leak_discharge_coeff
+
+
+    """
+    def __init__(self, name, wn):
+        super(Junction, self).__init__(wn, name)
+        self._demand_timeseries_list = Demands(self._pattern_reg)
+        self._elevation = 0.0
+        self._required_pressure = None
+        self._minimum_pressure = None
+        self._emitter_coefficient = None
         self._leak = False
         self.leak_status = False
         self.leak_area = 0.0
         self.leak_discharge_coeff = 0.0
         self._leak_start_control_name = 'junction'+self._name+'start_leak_control'
         self._leak_end_control_name = 'junction'+self._name+'end_leak_control'
-
         
     def __repr__(self):
         return "<Junction '{}', elevation={}, demand_timeseries_list={}>".format(self._name, self.elevation, repr(self.demand_timeseries_list))
@@ -100,6 +120,51 @@ def _compare(self, other):
             return True
         return False
     
+    @property
+    def elevation(self):
+        """float : elevation of the junction"""
+        return self._elevation
+    @elevation.setter
+    def elevation(self, value):
+        self._elevation = value
+
+    @property
+    def demand_timeseries_list(self):
+        """Demands : list of demand patterns and base multipliers"""
+        return self._demand_timeseries_list
+    @demand_timeseries_list.setter
+    def demand_timeseries_list(self, value):
+        self._demand_timeseries_list = value
+
+    @property
+    def required_pressure(self):
+        """float: The required pressure attribute is used for pressure-dependent demand
+        simulations. This is the lowest pressure at which the junction receives 
+        the full requested demand. If set to None, the global value in 
+        wn.options.hydraulic.required_pressure is used."""
+        return self._required_pressure
+    @required_pressure.setter
+    def required_pressure(self, value):
+        self._required_pressure = value
+
+    @property
+    def minimum_pressure(self):
+        """float: The minimum pressure attribute is used for pressure-dependent demand 
+        simulations. Below this pressure, the junction will not receive any water.
+        If set to None, the global value in wn.options.hydraulic.minimum_pressure is used."""
+        return self._minimum_pressure
+    @minimum_pressure.setter
+    def minimum_pressure(self, value):
+        self._minimum_pressure = value
+
+    @property
+    def emitter_coefficient(self):
+        """float : if not None, then activate an emitter with the specified coefficient"""
+        return self._emitter_coefficient
+    @emitter_coefficient.setter
+    def emitter_coefficient(self, value):
+        self._emitter_coefficient = value
+
     @property
     def nominal_pressure(self):
         """deprecated - use required pressure"""
@@ -112,7 +177,7 @@ def nominal_pressure(self, value):
 
     @property
     def node_type(self):
-        """returns ``"Junction"``"""
+        """str : ``"Junction"`` (read only)"""
         return 'Junction'
 
     def add_demand(self, base, pattern_name, category=None):
@@ -259,38 +324,70 @@ class Tank(Node):
     """
     Tank class, inherited from Node.
     
-    This class is intended to be instantiated through the 
-    :class:`~wntr.network.model.WaterNetworkModel.add_tank()` method. 
-    
     Tank volume can be defined using a constant diameter or a volume curve. 
     If the tank has a volume curve, the diameter has no effect on hydraulic 
     simulations. 
     
+    .. rubric:: Constructor
+
+    This class is intended to be instantiated through the 
+    :class:`~wntr.network.model.WaterNetworkModel.add_tank()` method. 
+    Direct creation through the constructor is highly discouraged.
+
     Parameters
     ----------
     name : string
         Name of the tank.
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         WaterNetworkModel object the tank will belong to
-        
+    
+
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        node_type
+        head
+        demand
+        elevation
+        init_level
+        min_level
+        max_level
+        level
+        diameter
+        min_vol
+        vol_curve_name
+        vol_curve
+        overflow
+        mixing_model
+        mixing_fraction
+        bulk_coeff
+        coordinates
+        initial_quality
+        tag
+        leak_demand
+        leak_status
+        leak_area
+        leak_discharge_coeff
+
     """
 
     def __init__(self, name, wn):
         super(Tank, self).__init__(wn, name)
-        self.elevation=0.0
+        self._elevation=0.0
         self._init_level=3.048
-        self.min_level=0.0
-        self.max_level=6.096
-        self.diameter=15.24,
+        self._min_level=0.0
+        self._max_level=6.096
+        self._diameter=15.24
         self.head = self.elevation + self._init_level
         self._prev_head = self.head
-        self.min_vol=0
+        self._min_vol=0 
         self._vol_curve_name = None
         self._mixing_model = None
-        self.mixing_fraction = None
-        self.bulk_coeff = None
+        self._mixing_fraction = None
+        self._bulk_coeff = None
         self._overflow = False
-        
         self._leak = False
         self.leak_status = False
         self.leak_area = 0.0
@@ -315,6 +412,46 @@ def _compare(self, other):
             return True
         return False
     
+    @property
+    def elevation(self):
+        """float : elevation to the bottom of the tank. head = level + elevation"""
+        return self._elevation
+    @elevation.setter
+    def elevation(self, value):
+        self._elevation = value
+
+    @property
+    def min_level(self):
+        """float : minimum level for the tank to be able to drain"""
+        return self._min_level
+    @min_level.setter
+    def min_level(self, value):
+        self._min_level = value
+
+    @property 
+    def max_level(self):
+        """float : maximum level before tank begins to overflow (if permitted)"""
+        return self._max_level
+    @max_level.setter
+    def max_level(self, value):
+        self._max_level = value
+
+    @property
+    def diameter(self):
+        """float : diameter of the tank as a cylinder""" 
+        return self._diameter
+    @diameter.setter
+    def diameter(self, value):
+        self._diameter = value
+
+    @property
+    def min_vol(self):
+        """float : minimum volume to be able to drain (when using a tank curve)"""
+        return self._min_vol
+    @min_vol.setter
+    def min_vol(self, value):
+        self._min_vol = value
+
     @property
     def mixing_model(self):
         """
@@ -340,6 +477,22 @@ def mixing_model(self, value):
         else:
             raise ValueError('Mixing model must be MIXED, 2COMP, FIFO or LIFO or a MixType object')
 
+    @property
+    def mixing_fraction(self):
+        """float : for water quality simulations only, the compartment size for 2-compartment mixing"""
+        return self._mixing_fraction
+    @mixing_fraction.setter
+    def mixing_fraction(self, value):
+        self._mixing_fraction = value    
+
+    @property
+    def bulk_coeff(self):
+        """float : bulk reaction coefficient for this tank only; leave None to use global value"""
+        return self._bulk_coeff
+    @bulk_coeff.setter
+    def bulk_coeff(self, value):
+        self._bulk_coeff = value
+
     @property
     def init_level(self):
         """The initial tank level at the start of simulation"""
@@ -356,7 +509,10 @@ def node_type(self):
 
     @property
     def vol_curve(self):
-        """The volume curve, if defined"""
+        """The volume curve, if defined (read only)
+        
+        Set this using the vol_curve_name.
+        """
         return self._curve_reg[self._vol_curve_name]
 
     @property
@@ -393,7 +549,7 @@ def overflow(self, value):
 
     @property
     def level(self):
-        """Returns tank level (head - elevation)"""
+        """Returns tank level = head - elevation (read only)"""
         return self.head - self.elevation
     
     
@@ -497,8 +653,11 @@ class Reservoir(Node):
     """
     Reservoir class, inherited from Node
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_reservoir()` method. 
+    Direct creation through the constructor is highly discouraged.
     
     Parameters
     ----------
@@ -510,14 +669,35 @@ class Reservoir(Node):
         Base head at the reservoir.
         Internal units must be meters (m).
     head_pattern : str, optional
-        Head pattern.
-        
+        Head pattern **name**
+    
+
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        node_type
+        base_head
+        head_pattern_name
+        head_timeseries
+        head
+        demand
+        leak_demand
+        leak_status
+        leak_area
+        leak_discharge_coeff
+        tag
+        initial_quality
+        coordinates
+
     """
     
     def __init__(self, name, wn, base_head=0.0, head_pattern=None):
         super(Reservoir, self).__init__(wn, name)
         self._head_timeseries = TimeSeries(wn._pattern_reg, base_head)
         self.head_pattern_name = head_pattern
+        """str : Name of the head pattern to use"""
 
     def __repr__(self):
         return "<Reservoir '{}', head={}>".format(self._name, self._head_timeseries)
@@ -531,12 +711,12 @@ def _compare(self, other):
 
     @property
     def node_type(self):
-        """returns ``"Reservoir"``"""
+        """``"Reservoir"`` (read only)"""
         return 'Reservoir'
 
     @property
     def head_timeseries(self):
-        """The head timeseries for the reservoir"""
+        """The head timeseries for the reservoir (read only)"""
         return self._head_timeseries
 
     @property
@@ -563,9 +743,12 @@ class Pipe(Link):
     """
     Pipe class, inherited from Link.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_pipe()` method. 
-    
+    Direct creation through the constructor is highly discouraged.
+
     Parameters
     ----------
     name : string
@@ -576,18 +759,44 @@ class Pipe(Link):
          Name of the end node
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this pipe will belong to.
-        
+    
+
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        start_node_name
+        end_node_name
+        link_type
+        length
+        diameter
+        roughness
+        minor_loss
+        cv
+        bulk_coeff
+        wall_coeff
+        initial_status
+        initial_setting
+        start_node
+        end_node
+        status
+        setting
+        tag
+        vertices
+
+
     """
 
     def __init__(self, name, start_node_name, end_node_name, wn):
         super(Pipe, self).__init__(wn, name, start_node_name, end_node_name)
-        self.length = 304.8
-        self.diameter = 0.3048
-        self.roughness = 100
-        self.minor_loss = 0.0
-        self.cv = False
-        self.bulk_coeff = None
-        self.wall_coeff = None
+        self._length = 304.8
+        self._diameter = 0.3048
+        self._roughness = 100
+        self._minor_loss = 0.0
+        self._cv = False
+        self._bulk_coeff = None
+        self._wall_coeff = None
         
     def __repr__(self):
         return "<Pipe '{}' from '{}' to '{}', length={}, diameter={}, roughness={}, minor_loss={}, check_valve={}, status={}>".format(self._link_name,
@@ -612,9 +821,65 @@ def link_type(self):
         """returns ``"Pipe"``"""
         return 'Pipe'
     
+    @property
+    def length(self):
+        """float : length of the pipe"""
+        return self._length
+    @length.setter
+    def length(self, value):
+        self._length = value
+
+    @property
+    def diameter(self):
+        """float : diameter of the pipe"""
+        return self._diameter
+    @diameter.setter
+    def diameter(self, value):
+        self._diameter = value
+
+    @property
+    def roughness(self):
+        """float : pipe roughness"""
+        return self._roughness 
+    @roughness.setter
+    def roughness(self, value):
+        self._roughness = value
+
+    @property
+    def minor_loss(self):
+        """float : minor loss coefficient"""
+        return self._minor_loss
+    @minor_loss.setter
+    def minor_loss(self, value):
+        self._minor_loss = value
+
+    @property
+    def cv(self):
+        """bool : does this pipe have a check valve"""
+        return self._cv
+    @cv.setter
+    def cv(self, value): 
+        self._cv = value
+
+    @property
+    def bulk_coeff(self):
+        """float or None : if not None, then a pipe specific bulk reaction coefficient"""
+        return self._bulk_coeff
+    @bulk_coeff.setter
+    def bulk_coeff(self, value):
+        self._bulk_coeff = value
+
+    @property
+    def wall_coeff(self):
+        """float or None : if not None, then a pipe specific wall reaction coefficient"""
+        return self._wall_coeff
+    @wall_coeff.setter
+    def wall_coeff(self, value):
+        self._wall_coeff = value
+
     @property
     def status(self):
-        """The current status of the pipe"""
+        """LinkStatus : the current status of the pipe"""
         if self._internal_status == LinkStatus.Closed:
             return LinkStatus.Closed
         else:
@@ -628,12 +893,15 @@ class Pump(Link):
     """
     Pump class, inherited from Link.
 
-    This class is intended to be instantiated through the 
-    :class:`~wntr.network.model.WaterNetworkModel.add_pump` method. 
-    
     For details about the different subclasses, please see one of the following:
     :class:`~wntr.network.elements.HeadPump` and :class:`~wntr.network.elements.PowerPump`
 
+    .. rubric:: Constructor
+    
+    This class is intended to be instantiated through the 
+    :class:`~wntr.network.model.WaterNetworkModel.add_pump` method. 
+    Direct creation through the constructor is highly discouraged.
+    
     Parameters
     ----------
     name : string
@@ -644,7 +912,30 @@ class Pump(Link):
          Name of the end node
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this pump will belong to.
-        
+    
+
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        speed_timeseries
+        efficiency
+        energy_price
+        energy_pattern
+        status
+        setting
+        tag
+        vertices
+
+
     """
 
     def __init__(self, name, start_node_name, end_node_name, wn):
@@ -652,19 +943,45 @@ def __init__(self, name, start_node_name, end_node_name, wn):
         self._speed_timeseries = TimeSeries(wn._pattern_reg, 1.0)
         self._base_power = None
         self._pump_curve_name = None
-        self.efficiency = None
-        self.energy_price = None
-        self.energy_pattern = None
+        self._efficiency = None
+        self._energy_price = None 
+        self._energy_pattern = None
         self._power_outage = LinkStatus.Open
+        self._outage_rule_name = name+'_outage'
+        self._after_outage_rule_name = name+'_after_outage'
 
     def _compare(self, other):
         if not super(Pump, self)._compare(other):
             return False
         return True
-   
+
+    @property
+    def efficiency(self): 
+        """float : pump efficiency"""
+        return self._efficiency
+    @efficiency.setter
+    def efficiency(self, value):
+        self._efficiency = value
+
+    @property
+    def energy_price(self):
+        """float : energy price surcharge (only used by EPANET)"""
+        return self._energy_price
+    @energy_price.setter
+    def energy_price(self, value):
+        self._energy_price = value
+
+    @property
+    def energy_pattern(self):
+        """str : energy pattern name"""
+        return self._energy_pattern
+    @energy_pattern.setter
+    def energy_pattern(self, value):
+        self._energy_pattern = value
+
     @property
     def status(self):
-        """The current status of the pump"""
+        """LinkStatus : the current status of the pump"""
         if self._internal_status == LinkStatus.Closed:
             return LinkStatus.Closed
         elif self._power_outage == LinkStatus.Closed:
@@ -677,15 +994,17 @@ def status(self, status):
 
     @property
     def link_type(self):
-        """returns ``"Pump"``"""
+        """str : ``"Pump"`` (read only)"""
         return 'Pump'
 
     @property
     def speed_timeseries(self):
+        """TimeSeries : timeseries of speed values (retrieve only)"""
         return self._speed_timeseries
 
     @property
     def base_speed(self):
+        """float : base multiplier for a speed timeseries"""
         return self._speed_timeseries.base_value
     @base_speed.setter
     def base_speed(self, value):
@@ -693,6 +1012,7 @@ def base_speed(self, value):
         
     @property
     def speed_pattern_name(self):
+        """str : pattern name for the speed"""
         return self._speed_timeseries.pattern_name
     @speed_pattern_name.setter
     def speed_pattern_name(self, name):
@@ -705,9 +1025,9 @@ def setting(self):
         """Alias to speed for consistency with other link types"""
         return self._speed_timeseries
     
-    def add_outage(self, wn, start_time, end_time):
+    def add_outage(self, wn, start_time, end_time=None, priority=6, add_after_outage_rule=False):
         """
-        Adds a pump outage control to the water network model
+        Add a pump outage rule to the water network model.
 
         Parameters
         ----------
@@ -717,26 +1037,63 @@ def add_outage(self, wn, start_time, end_time):
            The time at which the outage starts.
         end_time : int
            The time at which the outage stops.
-           
+        priority : int
+            The outage rule priority, default = 6 (very high)
+        add_after_outage_rule : bool
+            Flag indicating if a rule is added to open the pump after the outage. 
+            Pump status after the outage is generally defined by existing controls/rules in the water network model. 
+            For example, the pump opens based on the level of a specific tank.
         """
-        from wntr.network.controls import _InternalControlAction, Control
-
-        start_power_outage_action = _InternalControlAction(self, '_power_outage', LinkStatus.Closed, 'status')
-        end_power_outage_action = _InternalControlAction(self, '_power_outage', LinkStatus.Open, 'status')
-
-        start_control = Control._time_control(wn, start_time, 'SIM_TIME', False, start_power_outage_action)
-        end_control = Control._time_control(wn, end_time, 'SIM_TIME', False, end_power_outage_action)
-
-        wn.add_control(self.name+'_power_off_'+str(start_time), start_control)
-        wn.add_control(self.name+'_power_on_'+str(end_time), end_control)
+        from wntr.network.controls import ControlAction, SimTimeCondition, AndCondition, Rule
+        
+        self._power_outage = True
+        
+        # Outage
+        act = ControlAction(self, 'status', LinkStatus.Closed)
+        cond1 = SimTimeCondition(wn, 'Above' , start_time)
+        if end_time is not None:
+            cond2 = SimTimeCondition(wn, 'Below' , end_time)
+            cond = AndCondition(cond1, cond2)
+        else:
+            cond = cond1
+        rule = Rule(cond, act, priority=priority)
+        wn.add_control(self._outage_rule_name, rule)
+        
+        # After outage
+        if add_after_outage_rule and end_time is not None:
+            act = ControlAction(self, 'status', LinkStatus.Open)
+            cond = SimTimeCondition(wn, 'Above' , end_time)
+            rule = Rule(cond, act, priority=priority)
+            wn.add_control(self._after_outage_rule_name, rule)
+
+    def remove_outage(self,wn):
+        """
+        Remove an outage control from the water network model
 
+        Parameters
+        ----------
+        wn : :class:`~wntr.network.model.WaterNetworkModel`
+           Water network model
+        """
+        self._power_outage = False
+        
+        wn._discard_control(self._outage_rule_name)
+        wn._discard_control(self._after_outage_rule_name)
+        
 
-class HeadPump(Pump):
+class HeadPump(Pump):    
     """
     Head pump class, inherited from Pump.
     
+    This type of pump uses a pump curve (see curves). The curve is 
+    set using the ``pump_curve_name`` attribute. The curve itself 
+    can be accessed using the ``get_pump_curve()`` method.
+
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_pump` method. 
+    Direct creation through the constructor is highly discouraged.
     
     Parameters
     ----------
@@ -748,7 +1105,32 @@ class HeadPump(Pump):
          Name of the end node
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this pump will belong to.
-    
+
+
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        pump_type
+        pump_curve_name
+        speed_timeseries
+        efficiency
+        energy_price
+        energy_pattern
+        status
+        setting
+        tag
+        vertices
+
+
     """
 #    def __init__(self, name, start_node_name, end_node_name, wn):
 #        super(HeadPump,self).__init__(name, start_node_name, 
@@ -773,12 +1155,12 @@ def _compare(self, other):
     
     @property
     def pump_type(self): 
-        """returns ``"HEAD"``"""
+        """str : ``"HEAD"`` (read only)"""
         return 'HEAD'
     
     @property
     def pump_curve_name(self):
-        """Returns the pump curve name"""
+        """str : the pump curve name"""
         return self._pump_curve_name
     @pump_curve_name.setter
     def pump_curve_name(self, name):
@@ -791,6 +1173,14 @@ def pump_curve_name(self, name):
         self._curve_coeffs = None 
 
     def get_pump_curve(self):
+        """
+        Get the pump curve object
+
+        Returns
+        -------
+        Curve
+            the head curve for this pump
+        """        
         curve = self._curve_reg[self.pump_curve_name]
         return curve
         
@@ -904,9 +1294,15 @@ def get_design_flow(self):
 class PowerPump(Pump):
     """
     Power pump class, inherited from Pump.
-    
+
+    This is a constant power type of pump. The constant power is
+    set and modified through the ``power`` attribute.
+
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_pump` method. 
+    Direct creation through the constructor is highly discouraged.
     
     Parameters
     ----------
@@ -919,6 +1315,31 @@ class PowerPump(Pump):
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this pump will belong to.
         
+
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        pump_type
+        power
+        speed_timeseries
+        efficiency
+        energy_price
+        energy_pattern
+        status
+        setting
+        tag
+        vertices
+
+
     """
     
     def __repr__(self):
@@ -936,12 +1357,12 @@ def _compare(self, other):
     
     @property
     def pump_type(self): 
-        """returns ``"POWER"``"""
+        """str : ``"POWER"`` (read only)"""
         return 'POWER'
     
     @property
     def power(self):
-        """Returns the fixed_power value"""
+        """float : the fixed power value"""
         return self._base_power
     @power.setter
     def power(self, kW):
@@ -950,16 +1371,20 @@ def power(self, kW):
 
 
 class Valve(Link):
+    
     """
     Valve class, inherited from Link.
     
-    This class is intended to be instantiated through the 
-    :class:`~wntr.network.model.WaterNetworkModel.add_valve` method. 
-    
-    For details about the different subclasses, please see one of the following:
+    For details about the  subclasses, please see one of the following:
     :class:`~wntr.network.elements.PRValve`, :class:`~wntr.network.elements.PSValve`,
     :class:`~wntr.network.elements.PBValve`, :class:`~wntr.network.elements.FCValve`,
-    :class:`~wntr.network.elements.TCValve`, and :class:`~wntr.network.elements.GPValve`.
+    :class:`~wntr.network.elemedifferentnts.TCValve`, and :class:`~wntr.network.elements.GPValve`.
+
+    .. rubric:: Constructor
+
+    This class is intended to be instantiated through the 
+    :class:`~wntr.network.model.WaterNetworkModel.add_valve` method. 
+    Direct creation through the constructor is highly discouraged.
 
     Parameters
     ----------
@@ -972,6 +1397,25 @@ class Valve(Link):
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this valve will belong to.
         
+    
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        valve_type
+        status
+        setting
+        tag
+        vertices
+
     """
     
     def __init__(self, name, start_node_name, end_node_name, wn):
@@ -1015,14 +1459,22 @@ def status(self, status):
     def link_type(self):
         """returns ``"Valve"``"""
         return 'Valve'
-        
+
+    @property
+    def valve_type(self): 
+        """returns ``None`` because this is an abstact class"""
+        return None
+
 
 class PRValve(Valve):
     """
     Pressure reducing valve class, inherited from Valve.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_valve` method. 
+    Direct creation through the constructor is highly discouraged.
     
     Parameters
     ----------
@@ -1035,6 +1487,25 @@ class PRValve(Valve):
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this valve will belong to.
         
+    
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        valve_type
+        status
+        setting
+        tag
+        vertices
+
     """
     
     def __init__(self, name, start_node_name, end_node_name, wn):
@@ -1050,8 +1521,11 @@ class PSValve(Valve):
     """
     Pressure sustaining valve class, inherited from Valve.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_valve` method. 
+    Direct creation through the constructor is highly discouraged.
     
     Parameters
     ----------
@@ -1064,6 +1538,25 @@ class PSValve(Valve):
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this valve will belong to.
         
+    
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        valve_type
+        status
+        setting
+        tag
+        vertices
+
     """
     
     def __init__(self, name, start_node_name, end_node_name, wn):
@@ -1079,8 +1572,11 @@ class PBValve(Valve):
     """
     Pressure breaker valve class, inherited from Valve.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_valve` method. 
+    Direct creation through the constructor is highly discouraged.
     
     Parameters
     ----------
@@ -1093,6 +1589,25 @@ class PBValve(Valve):
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this valve will belong to.
         
+    
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        valve_type
+        status
+        setting
+        tag
+        vertices
+
     """
     
     def __init__(self, name, start_node_name, end_node_name, wn):
@@ -1108,9 +1623,12 @@ class FCValve(Valve):
     """
     Flow control valve class, inherited from Valve.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_valve` method. 
-    
+    Direct creation through the constructor is highly discouraged.
+   
     Parameters
     ----------
     name : string
@@ -1122,6 +1640,25 @@ class FCValve(Valve):
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this valve will belong to
         
+    
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        valve_type
+        status
+        setting
+        tag
+        vertices
+
     """
     
     def __init__(self, name, start_node_name, end_node_name, wn):
@@ -1137,8 +1674,11 @@ class TCValve(Valve):
     """
     Throttle control valve class, inherited from Valve.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_valve` method. 
+    Direct creation through the constructor is highly discouraged.
     
     Parameters
     ----------
@@ -1151,6 +1691,25 @@ class TCValve(Valve):
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this valve will belong to
         
+    
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        valve_type
+        status
+        setting
+        tag
+        vertices
+
     """
     
     def __init__(self, name, start_node_name, end_node_name, wn):
@@ -1166,8 +1725,11 @@ class GPValve(Valve):
     """
     General purpose valve class, inherited from Valve.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_valve` method. 
+    Direct creation through the constructor is highly discouraged.
     
     Parameters
     ----------
@@ -1180,6 +1742,27 @@ class GPValve(Valve):
     wn : :class:`~wntr.network.model.WaterNetworkModel`
         The water network model this valve will belong to
         
+    
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        link_type
+        start_node
+        start_node_name
+        end_node
+        end_node_name
+        initial_status
+        initial_setting
+        valve_type
+        headloss_curve
+        headloss_curve_name
+        status
+        setting
+        tag
+        vertices
+
     """
     
     def __init__(self, name, start_node_name, end_node_name, wn):
@@ -1193,6 +1776,7 @@ def valve_type(self):
 
     @property
     def headloss_curve(self):
+        """Curve : the headloss curve object (read only)"""
         return self._curve_reg[self._headloss_curve_name]
 
     @property
@@ -1211,6 +1795,8 @@ class Pattern(object):
     """
     Pattern class.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_pattern` method. 
     
@@ -1371,7 +1957,9 @@ class TimeSeries(object):
     The object can be used to store changes in junction demand, source injection, 
     pricing, pump speed, and reservoir head. The class provides methods
     to calculate values using the base value and a multiplier pattern.
-    
+
+    .. rubric:: Constructor
+
     Parameters
     ----------
     base : number
@@ -1618,6 +2206,8 @@ class Curve(object):
     """
     Curve base class.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_curve` method. 
     
@@ -1749,6 +2339,8 @@ class Source(object):
     """
     Water quality source class.
     
+    .. rubric:: Constructor
+
     This class is intended to be instantiated through the 
     :class:`~wntr.network.model.WaterNetworkModel.add_source` method. 
     
@@ -1766,7 +2358,17 @@ class Source(object):
     pattern: Pattern, optional
         If None, then the value will be constant. Otherwise, the Pattern will be used
         (default = None).
-        
+    
+
+    .. rubric:: Attributes
+
+    .. autosummary::
+
+        name
+        node_name
+        source_type
+        strength_timeseries
+
     """
 
 #    def __init__(self, name, node_registry, pattern_registry):
@@ -1776,9 +2378,9 @@ def __init__(self, model, name, node_name, source_type, strength, pattern=None):
         self._pattern_reg.add_usage(pattern, (name, 'Source'))
         self._node_reg = model._node_reg
         self._node_reg.add_usage(node_name, (name, 'Source'))
-        self.name = name
-        self.node_name = node_name
-        self.source_type = source_type
+        self._name = name
+        self._node_name = node_name
+        self._source_type = source_type
 
     def __eq__(self, other):
         if not type(self) == type(other):
@@ -1795,4 +2397,29 @@ def __repr__(self):
 
     @property
     def strength_timeseries(self): 
+        """TimeSeries : timeseries of the source values (read only)"""
         return self._strength_timeseries
+
+    @property
+    def name(self):
+        """str : the name for this source"""
+        return self._name 
+    @name.setter
+    def name(self, value):
+        self._name = value
+
+    @property
+    def node_name(self):
+        """str : the node where this source is located"""
+        return self._node_name 
+    @node_name.setter
+    def node_name(self, value):
+        self._node_name = value
+
+    @property
+    def source_type(self):
+        """str : the source type for this source"""
+        return self._source_type 
+    @source_type.setter
+    def source_type(self, value):
+        self._source_type = value
diff --git a/wntr/network/model.py b/wntr/network/model.py
index 6181d3241..e5d2d7d04 100644
--- a/wntr/network/model.py
+++ b/wntr/network/model.py
@@ -1693,6 +1693,29 @@ def query_link_attribute(self, attribute, operation=None, value=None, link_type=
                 pass
         return pd.Series(link_attribute_dict)
 
+    def convert_controls_to_rules(self, priority=3):
+        """
+        Convert all controls to rules.
+        
+        Note that for an exact match between controls and rules, the rule 
+        timestep must be very small.
+
+        Parameters
+        ----------
+        priority : int, optional
+           Rule priority, default is 3.
+
+        """
+        for name in self.control_name_list:
+            control = self.get_control(name)
+            if isinstance(control, Control):
+                act = control.actions()[0]
+                cond = control.condition
+                rule = Rule(cond, act, priority=priority)
+                self.add_control(name.replace(' ', '_')+'_Rule', rule)
+                self.remove_control(name)
+                
+        
     def reset_initial_values(self):
         """
         Resets all initial values in the network
diff --git a/wntr/sim/core.py b/wntr/sim/core.py
index fcb98bd21..6d928f2c4 100644
--- a/wntr/sim/core.py
+++ b/wntr/sim/core.py
@@ -505,7 +505,7 @@ def __init__(self, wn):
         self._backup_solver = None
         self._solver_options = dict()
         self._backup_solver_options = dict()
-        self._convergence_error = True
+        self._convergence_error = False
 
         # other attributes
         self._hydraulic_timestep = None
@@ -527,7 +527,7 @@ def _get_time(self):
         m = int(s/60)
         s -= m*60
         s = int(s)
-        return str(h)+':'+str(m)+':'+str(s)
+        return '{:02}:{:02}:{:02}'.format(h, m, s)
 
     def _setup_sim_options(self, solver, backup_solver, solver_options, backup_solver_options, convergence_error):
         self._report_timestep = self._wn.options.time.report_timestep
@@ -793,7 +793,7 @@ def _run_postsolve_controls(self):
                 logger.debug('\t{0}.{1} changed to {2}'.format(obj, attr, getattr(obj, attr)))
 
     def run_sim(self, solver=NewtonSolver, backup_solver=None, solver_options=None,
-                backup_solver_options=None, convergence_error=True, HW_approx='default',
+                backup_solver_options=None, convergence_error=False, HW_approx='default',
                 diagnostics=False):
         """
         Run an extended period simulation (hydraulics only).
@@ -807,19 +807,19 @@ def run_sim(self, solver=NewtonSolver, backup_solver=None, solver_options=None,
         solver_options: dict
             Solver options are specified using the following dictionary keys:
 
-            * MAXITER: the maximum number of iterations for each hydraulic solve (each timestep and trial) (default = 100)
+            * MAXITER: the maximum number of iterations for each hydraulic solve (each timestep and trial) (default = 3000)
             * TOL: tolerance for the hydraulic equations (default = 1e-6)
             * BT_RHO: the fraction by which the step length is reduced at each iteration of the line search (default = 0.5)
-            * BT_MAXITER: the maximum number of iterations for each line search (default = 20)
+            * BT_MAXITER: the maximum number of iterations for each line search (default = 100)
             * BACKTRACKING: whether or not to use a line search (default = True)
             * BT_START_ITER: the newton iteration at which a line search should start being used (default = 2)
             * THREADS: the number of threads to use in constraint and jacobian computations
         backup_solver_options: dict
         convergence_error: bool (optional)
             If convergence_error is True, an error will be raised if the
-            simulation does not converge. If convergence_error is False,
-            a warning will be issued and results.error_code will be set to 2
-            if the simulation does not converge.  Default = True.
+            simulation does not converge. If convergence_error is False, partial results are returned, 
+            a warning will be issued, and results.error_code will be set to 0
+            if the simulation does not converge.  Default = False.
         HW_approx: str
             Specifies which Hazen-Williams headloss approximation to use. Options are 'default' and 'piecewise'. Please
             see the WNTR documentation on hydraulics for details.
@@ -899,10 +899,10 @@ def run_sim(self, solver=NewtonSolver, backup_solver=None, solver_options=None,
                 solver_status, mesg, iter_count = _solver_helper(self._model, self._backup_solver, self._backup_solver_options)
             if solver_status == 0:
                 if self._convergence_error:
-                    logger.error('Simulation did not converge. ' + mesg)
-                    raise RuntimeError('Simulation did not converge. ' + mesg)
-                warnings.warn('Simulation did not converge. ' + mesg)
-                logger.warning('Simulation did not converge at time ' + str(self._get_time()) + '. ' + mesg)
+                    logger.error('Simulation did not converge at time ' + self._get_time() + '. ' + mesg) 
+                    raise RuntimeError('Simulation did not converge at time ' + self._get_time() + '. ' + mesg)
+                warnings.warn('Simulation did not converge at time ' + self._get_time() + '. ' + mesg)
+                logger.warning('Simulation did not converge at time ' + self._get_time() + '. ' + mesg)
                 results.error_code = wntr.sim.results.ResultsStatus.error
                 diagnostics.run(last_step='solve', next_step='break')
                 break
@@ -924,11 +924,11 @@ def run_sim(self, solver=NewtonSolver, backup_solver=None, solver_options=None,
                 trial += 1
                 if trial > max_trials:
                     if convergence_error:
-                        logger.error('Exceeded maximum number of trials.')
-                        raise RuntimeError('Exceeded maximum number of trials.')
+                        logger.error('Exceeded maximum number of trials at time ' + self._get_time() + '. ') 
+                        raise RuntimeError('Exceeded maximum number of trials at time ' + self._get_time() + '. ' ) 
                     results.error_code = wntr.sim.results.ResultsStatus.error
-                    warnings.warn('Exceeded maximum number of trials.')
-                    logger.warning('Exceeded maximum number of trials at time %s', self._get_time())
+                    warnings.warn('Exceeded maximum number of trials at time ' + self._get_time() + '. ') 
+                    logger.warning('Exceeded maximum number of trials at time ' + self._get_time() + '. ' ) 
                     break
                 continue
 
diff --git a/wntr/sim/epanet.py b/wntr/sim/epanet.py
index 63ef2d796..0f9111088 100644
--- a/wntr/sim/epanet.py
+++ b/wntr/sim/epanet.py
@@ -59,7 +59,7 @@ def __init__(self, wn, reader=None, result_types=None):
         if self.reader is None:
             self.reader = wntr.epanet.io.BinFile(result_types=result_types)
 
-    def run_sim(self, file_prefix='temp', save_hyd=False, use_hyd=False, hydfile=None, version=2.2):
+    def run_sim(self, file_prefix='temp', save_hyd=False, use_hyd=False, hydfile=None, version=2.2, convergence_error=False):
         """
         Run the EPANET simulator.
 
@@ -87,7 +87,11 @@ def run_sim(self, file_prefix='temp', save_hyd=False, use_hyd=False, hydfile=Non
         version : float, {2.0, **2.2**}
             Optionally change the version of the EPANET toolkit libraries. Valid choices are
             either 2.2 (the default if no argument provided) or 2.0.
-
+        convergence_error: bool (optional)
+            If convergence_error is True, an error will be raised if the
+            simulation does not converge. If convergence_error is False, partial results are returned, 
+            a warning will be issued, and results.error_code will be set to 0
+            if the simulation does not converge.  Default = False.
         """
         if isinstance(version, str):
             version = float(version)
@@ -115,5 +119,5 @@ def run_sim(self, file_prefix='temp', save_hyd=False, use_hyd=False, hydfile=Non
         enData.ENclose()
         logger.debug('Completed run')
         #os.sys.stderr.write('Finished Closing\n')
-        return self.reader.read(outfile)
+        return self.reader.read(outfile, convergence_error)
 


Test patch:
diff --git a/wntr/tests/test_metrics_pump_cost.py b/wntr/tests/test_metrics_pump_cost.py
index 1c0acf9b3..629698552 100644
--- a/wntr/tests/test_metrics_pump_cost.py
+++ b/wntr/tests/test_metrics_pump_cost.py
@@ -1,49 +1,76 @@
 import unittest
+from nose.tools import *
 from os.path import abspath, dirname, join
+import pandas as pd
 
 testdir = dirname(abspath(str(__file__)))
 test_datadir = join(testdir, 'networks_for_testing')
 ex_datadir = join(testdir,'..','..','examples','networks')
 
-
-class TestPumpCost(unittest.TestCase):
+class TestPumpNet3(unittest.TestCase):
 
     @classmethod
     def setUpClass(self):
         import wntr
         self.wntr = wntr
 
-        inp_file = join(ex_datadir, 'Net6.inp')
+        inp_file = join(ex_datadir, 'Net3.inp')
         self.wn = self.wntr.network.WaterNetworkModel(inp_file)
-        self.wn.options.time.report_timestep = 'all'
-        self.wn.options.time.duration = 12*3600
-        self.wn.options.energy.global_price = 3.61111111111e-8
-        self.wn.options.hydraulic.demand_model = 'DDA'
-        sim = self.wntr.sim.WNTRSimulator(self.wn)
-        self.results = sim.run_sim()
+        self.wn.options.energy.global_efficiency = 75 # 75%
+        self.wn.options.energy.global_price = 3.61e-8  # $/J; equal to $0.13/kW-h
 
     @classmethod
     def tearDownClass(self):
         pass
 
-    def test_pump_cost(self):
-        flowrate = self.results.link['flowrate'].loc[:,self.wn.pump_name_list]
-        head = self.results.node['head'].loc[:,self.wn.node_name_list]
+    def test_energy_report(self):
         
-        cost = self.wntr.metrics.pump_cost(flowrate, head, self.wn)
-
-        total_cost = 0
-        times = self.results.link['flowrate'].index
-        for i in range(len(times) - 1):
-            t = times[i]
-            delta_t = times[i + 1] - t
-            total_cost = total_cost + cost.loc[t, :] * delta_t
+        sim = self.wntr.sim.EpanetSimulator(self.wn)
+        results = sim.run_sim()
 
-        avg_cost = total_cost / (times[-1] - times[0])
+        flowrate = results.link['flowrate'].loc[:,self.wn.pump_name_list]
+        head = results.node['head'].loc[:,self.wn.node_name_list]
+        pump_energy = self.wntr.metrics.pump_energy(flowrate, head, self.wn)
+        
+        pump_status = results.link['status'].loc[:,self.wn.pump_name_list]
+        utilization = pump_status.mean()*100
+        
+        timestep = self.wn.options.time.report_timestep
+        total_pumped_volume_Mgal = (flowrate*timestep*264.172/1e6).sum()
+        pump_energy_kWhr = pump_energy/(1000*3600)
+        total_pump_energy_kWhr = pump_energy_kWhr.sum()
+        energy_per_vol = total_pump_energy_kWhr/total_pumped_volume_Mgal # kW-hr/Mgal
+        
+        pump_power_kW = pump_energy_kWhr/(timestep/3600)
+        average_power = {}
+        for pump in pump_power_kW.columns:
+            average_power[pump] = pump_power_kW.loc[pump_status[pump]==1,pump].mean()
+        average_power = pd.Series(average_power)
+        
+        peak_power = pump_power_kW.max()
+        
+        pump_cost = self.wntr.metrics.pump_cost(pump_energy, self.wn)
+        cost_per_day = pump_cost.sum()/7 # pump energy (J) * cost ($/J)
 
-        avg_cost_sum = avg_cost.sum()
+        # Compare results to an Energy Report from EPANET GUI (setting global price to $0.13)
+        results = pd.DataFrame(index=['10', '335'])
+        results['utilization'] = utilization
+        results['energy_per_vol'] = energy_per_vol
+        results['average_power'] = average_power
+        results['peak_power'] = peak_power
+        results['cost_per_day'] = cost_per_day
 
-        self.assertAlmostEqual(avg_cost_sum, 0.070484, 5)
+        solution = {'utilization': [58.33, 23.60],
+                    'energy_per_vol': [313.71, 394.29],
+                    'average_power': [62.05, 309.41],
+                    'peak_power': [62.76, 310.84],
+                    'cost_per_day': [112.93, 227.85]}
+        solution = pd.DataFrame(solution, index=['10', '335'])
+        
+        error = abs(results - solution)/solution
+        
+        assert_less(error.max().max(), 0.01) 
+ 
 
 if __name__ == '__main__':
     unittest.main()
\ No newline at end of file
diff --git a/wntr/tests/test_network.py b/wntr/tests/test_network.py
index 6c3d8e0ab..24a84feb4 100644
--- a/wntr/tests/test_network.py
+++ b/wntr/tests/test_network.py
@@ -4,6 +4,7 @@
 import numpy as np
 from nose.tools import *
 import wntr
+from wntr.network.controls import Control, Rule
 
 testdir = dirname(abspath(str(__file__)))
 test_network_dir = join(testdir,'networks_for_testing')
@@ -821,7 +822,50 @@ def test_assign_demand():
     assert_equal(len(wn.pattern_name_list), 2*wn.num_junctions+5)
     assert_less(abs((demands2/demands1).max().max()-2), 0.01)
     assert_less(abs((demands_sim2/demands_sim1).max().max()-2), 0.01)
-                
+
+def test_convert_controls_to_rules():
+    
+    inp_file = join(ex_datadir, 'Net3.inp')
+    wn = wntr.network.WaterNetworkModel(inp_file)
+    wn.options.time.rule_timestep = 1 # For an exact match, the rule timestep must be very small
+     
+    nControls = 0
+    nRules = 0
+    for name, control in wn.controls():
+        if isinstance(control, Control):
+            nControls = nControls + 1
+        elif isinstance(control, Rule):
+            nRules = nRules + 1
+    assert nControls == 18
+    assert nRules == 0
+    
+    sim = wntr.sim.EpanetSimulator(wn)
+    results1 = sim.run_sim()
+
+    wn.convert_controls_to_rules(priority=3)
+    
+    nControls = 0
+    nRules = 0
+    for name, control in wn.controls():
+        if isinstance(control, Control):
+            nControls = nControls + 1
+        elif isinstance(control, Rule):
+            nRules = nRules + 1
+    assert nControls == 0
+    assert nRules == 18
+    
+    sim = wntr.sim.EpanetSimulator(wn)
+    results2 = sim.run_sim()
+    
+    #(results1.node['pressure'] - results2.node['pressure']).plot()
+    
+    for node_name, node in wn.nodes():
+        for t in results1.node['pressure'].index:
+            assert_less(abs(results1.node['pressure'].at[t,node_name] - 
+                            results2.node['pressure'].at[t,node_name]), 0.001)
+    
+
+
 if __name__ == '__main__':
     #unittest.main()
-    test_assign_demand()
\ No newline at end of file
+    test_convert_controls_to_rules()
\ No newline at end of file
diff --git a/wntr/tests/test_controls.py b/wntr/tests/test_network_controls.py
similarity index 100%
rename from wntr/tests/test_controls.py
rename to wntr/tests/test_network_controls.py
diff --git a/wntr/tests/test_leaks.py b/wntr/tests/test_network_leaks.py
similarity index 100%
rename from wntr/tests/test_leaks.py
rename to wntr/tests/test_network_leaks.py
diff --git a/wntr/tests/test_minor_loss.py b/wntr/tests/test_network_minor_loss.py
similarity index 100%
rename from wntr/tests/test_minor_loss.py
rename to wntr/tests/test_network_minor_loss.py
diff --git a/wntr/tests/test_network_pump_outage.py b/wntr/tests/test_network_pump_outage.py
new file mode 100644
index 000000000..13bfe83dc
--- /dev/null
+++ b/wntr/tests/test_network_pump_outage.py
@@ -0,0 +1,67 @@
+import unittest
+import math
+from os.path import abspath, dirname, join
+from wntr.network.controls import Control, Rule
+
+testdir = dirname(abspath(str(__file__)))
+test_datadir = join(testdir,'networks_for_testing')
+ex_datadir = join(testdir,'..','..','examples','networks')
+
+
+class TestOutageResults(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(self):
+        import wntr
+        self.wntr = wntr
+
+    @classmethod
+    def tearDownClass(self):
+        pass
+    
+    def test_outage(self):
+        inp_file = join(ex_datadir, 'Net3.inp')
+        wn = self.wntr.network.WaterNetworkModel(inp_file)
+        wn.convert_controls_to_rules(priority=3)
+        
+        # Original simulation
+        sim = self.wntr.sim.EpanetSimulator(wn)
+        results1 = sim.run_sim()
+        
+        pump_flowrate = results1.link['flowrate'].loc[13*3600:36*3600,'10']
+        assert pump_flowrate.sum() > 0
+        
+        # Add power outage
+        pump = wn.get_link('10')
+        pump.add_outage(wn, 12*3600, 36*3600)
+        
+        sim = self.wntr.sim.EpanetSimulator(wn)
+        results2 = sim.run_sim()
+        
+        assert '10_outage' in wn.control_name_list
+        num_controls = wn.num_controls
+        
+        pump_flowrate = results2.link['flowrate'].loc[13*3600:36*3600,'10']
+        assert pump_flowrate.sum() == 0
+        
+        # Remove power outage
+        pump.remove_outage(wn)
+        
+        assert wn.num_controls == num_controls -1
+        
+        sim = self.wntr.sim.EpanetSimulator(wn)
+        results3 = sim.run_sim()
+        
+        pump_flowrate = results3.link['flowrate'].loc[13*3600:36*3600,'10']
+        assert pump_flowrate.sum() > 0
+        
+        #(results1.node['pressure'] - results2.node['pressure']).plot()
+        #(results1.node['pressure'] - results3.node['pressure']).plot()
+        
+        max_diff = abs(results1.node['pressure'] - results3.node['pressure']).max().max()
+        
+        assert max_diff < 0.0001
+        
+if __name__ == '__main__':
+    unittest.main()
+
diff --git a/wntr/tests/test_valves.py b/wntr/tests/test_network_valves.py
similarity index 100%
rename from wntr/tests/test_valves.py
rename to wntr/tests/test_network_valves.py