Repository: USEPA/WNTR (language: python)

Issue:
Writing the BACKDROP section of inp files correctly.
This pull request fixes a bug in the inp file writer related to the BACKDROP section. I am also sneaking in some improved printing of controls...


Discussion / hints:

Fix patch:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index af2240aba..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,80 +0,0 @@
-# Based on
-# http://conda.pydata.org/docs/travis.html
-# https://github.com/xray/xray/blob/master/.travis.yml
-# https://github.com/scipy/scipy/blob/master/.travis.yml
-
-language: python
-sudo: false # if false, use TravisCI's container based build
-
-matrix:
-    include:
-    - python: 3.6
-      dist: xenial
-      env: CONDA_ENV=py36
-      services:
-      - xvfb
-      sudo: true
-    - python: 3.7
-      dist: xenial
-      services:
-      - xvfb
-      sudo: true
-      env: CONDA_ENV=py37
-    - python: 3.8
-      dist: xenial
-      env: CONDA_ENV=py38
-      services:
-      - xvfb
-      sudo: true
-    - python: 3.9
-      dist: xenial
-      env: CONDA_ENV=py39
-      services:
-      - xvfb
-      sudo: true
-addons:
-    apt:
-        packages:
-            - ccache
-
-cache:
-    directories:
-        - $HOME/.ccache
-
-before_script: 
-    - "export DISPLAY=:99.0"
-    - if [[ "$CONDA_ENV" != "py37" ]]; then 
-        source /etc/init.d/xvfb start; 
-        sleep 3;
-      fi
-
-# setup miniconda for numpy, scipy, pandas
-before_install:
-    - echo "before install"
-    - export PATH=/usr/lib/ccache:$PATH
-    - wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh
-    - bash miniconda.sh -b -p $HOME/miniconda
-    - export PATH="$HOME/miniconda/bin:$PATH"
-    - hash -r
-    - conda config --set always_yes yes --set changeps1 no
-    - conda update -q conda
-    - conda info -a
-    
-install:
-    - echo "install"
-    - conda env create --file ci/requirements-$CONDA_ENV.yml
-    - source activate test_env # all envs are named test_env in the yml files
-    - conda list
-    - echo $PATH
-    - ls -l /home/travis/miniconda/envs/test_env/lib
-    - pip install --upgrade pip
-    - pip install chama coveralls coverage
-    - conda install -y --channel conda-forge glpk
-    - python setup.py develop
-
-script:
-    - coverage run --source=wntr --omit="*/tests/*","*/aml/evaluator.py","*/network_isolation/network_isolation.py" -m nose -v --nologcapture --with-doctest --doctest-extension=.rst --traverse-namespace documentation/*.rst wntr
-    # - nosetests -v --nologcapture --with-coverage --with-doctest --doctest-extension=.rst --cover-package=wntr documentation/*.rst wntr
-      
-after_success:
-    coveralls
diff --git a/ci/install_python.ps1 b/ci/install_python.ps1
deleted file mode 100644
index b41eea5e5..000000000
--- a/ci/install_python.ps1
+++ /dev/null
@@ -1,93 +0,0 @@
-# Sample script to install Python and pip under Windows
-# Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner
-# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
-
-$MINICONDA_URL = "http://repo.continuum.io/miniconda/"
-$BASE_URL = "https://www.python.org/ftp/python/"
-
-
-function DownloadMiniconda ($python_version, $platform_suffix) {
-    $webclient = New-Object System.Net.WebClient
-    if ($python_version -eq "3.4") {
-        $filename = "Miniconda3-3.7.3-Windows-" + $platform_suffix + ".exe"
-    } else {
-        $filename = "Miniconda-3.7.3-Windows-" + $platform_suffix + ".exe"
-    }
-    $url = $MINICONDA_URL + $filename
-
-    $basedir = $pwd.Path + "\"
-    $filepath = $basedir + $filename
-    if (Test-Path $filename) {
-        Write-Host "Reusing" $filepath
-        return $filepath
-    }
-
-    # Download and retry up to 3 times in case of network transient errors.
-    Write-Host "Downloading" $filename "from" $url
-    $retry_attempts = 2
-    for($i=0; $i -lt $retry_attempts; $i++){
-        try {
-            $webclient.DownloadFile($url, $filepath)
-            break
-        }
-        Catch [Exception]{
-            Start-Sleep 1
-        }
-   }
-   if (Test-Path $filepath) {
-       Write-Host "File saved at" $filepath
-   } else {
-       # Retry once to get the error message if any at the last try
-       $webclient.DownloadFile($url, $filepath)
-   }
-   return $filepath
-}
-
-
-function InstallMiniconda ($python_version, $architecture, $python_home) {
-    Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
-    if (Test-Path $python_home) {
-        Write-Host $python_home "already exists, skipping."
-        return $false
-    }
-    if ($architecture -eq "32") {
-        $platform_suffix = "x86"
-    } else {
-        $platform_suffix = "x86_64"
-    }
-    $filepath = DownloadMiniconda $python_version $platform_suffix
-    Write-Host "Installing" $filepath "to" $python_home
-    $install_log = $python_home + ".log"
-    $args = "/S /D=$python_home"
-    Write-Host $filepath $args
-    Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru
-    if (Test-Path $python_home) {
-        Write-Host "Python $python_version ($architecture) installation complete"
-    } else {
-        Write-Host "Failed to install Python in $python_home"
-        Get-Content -Path $install_log
-        Exit 1
-    }
-}
-
-
-function InstallMinicondaPip ($python_home) {
-    $pip_path = $python_home + "\Scripts\pip.exe"
-    $conda_path = $python_home + "\Scripts\conda.exe"
-    if (-not(Test-Path $pip_path)) {
-        Write-Host "Installing pip..."
-        $args = "install --yes pip"
-        Write-Host $conda_path $args
-        Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru
-    } else {
-        Write-Host "pip already installed."
-    }
-}
-
-
-function main () {
-    InstallMiniconda $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON
-    InstallMinicondaPip $env:PYTHON
-}
-
-main
\ No newline at end of file
diff --git a/ci/requirements-py36.yml b/ci/requirements-py36.yml
deleted file mode 100644
index ca77b4fb9..000000000
--- a/ci/requirements-py36.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-name: test_env
-channels:
-    - defaults
-    - conda-forge
-dependencies:
-    - python=3.6
-    - numpy
-    - scipy
-    - networkx
-    - pandas
-    - matplotlib
-    - plotly
-    - folium
-    - utm
-    - nose
-    - openpyxl
-    - pip
diff --git a/ci/requirements-py37.yml b/ci/requirements-py37.yml
deleted file mode 100644
index 1dd330b64..000000000
--- a/ci/requirements-py37.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: test_env
-channels:
-    - defaults
-    - conda-forge
-dependencies:
-    - python=3.7
-    - numpy
-    - scipy
-    - networkx
-    - pandas
-    - matplotlib
-    - plotly
-    - folium
-    - utm
-    - nose
-    - openpyxl
-    - pip
-
diff --git a/ci/requirements-py38.yml b/ci/requirements-py38.yml
deleted file mode 100644
index bed2cb58a..000000000
--- a/ci/requirements-py38.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: test_env
-channels:
-    - defaults
-    - conda-forge
-dependencies:
-    - python=3.8
-    - numpy
-    - scipy
-    - networkx
-    - pandas
-    - matplotlib
-    - plotly
-    - folium
-    - utm
-    - nose
-    - openpyxl
-    - pip
-
diff --git a/ci/requirements-py39.yml b/ci/requirements-py39.yml
deleted file mode 100644
index 237517d2d..000000000
--- a/ci/requirements-py39.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: test_env
-channels:
-    - defaults
-    - conda-forge
-dependencies:
-    - python=3.9
-    - numpy
-    - scipy
-    - networkx
-    - pandas
-    - matplotlib
-    - plotly
-    - folium
-    - utm
-    - nose
-    - openpyxl
-    - pip
-
diff --git a/documentation/conf.py b/documentation/conf.py
index 11f3db5b3..75ea1fee8 100644
--- a/documentation/conf.py
+++ b/documentation/conf.py
@@ -135,7 +135,7 @@
 #html_theme = 'sphinx_rtd_theme'
 
 def setup(app):
-  app.add_css_file( "wntr.css" )
+  app.add_css_file( "wntr.css")
   
 on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
 if not on_rtd:  # only import and set the theme if we're building docs locally
@@ -147,8 +147,8 @@ def setup(app):
     html_theme = 'default'
 #    html_context = {
 #        'css_files': ['_static/wntr.css'],
-#    }
-
+#    }  
+    
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
 # documentation.
diff --git a/documentation/framework.rst b/documentation/framework.rst
index 759d028a5..2edd5483a 100644
--- a/documentation/framework.rst
+++ b/documentation/framework.rst
@@ -142,7 +142,7 @@ Of the EPANET model options that directly apply to hydraulic simulations, **the
 * D-W and C-M headloss options in the [OPTIONS] section (H-W option is used)
 * Accuracy, unbalanced, and emitter exponent from the [OPTIONS] section
 * Pump speed in the [PUMPS] section
-* Pattern start, report start, start clocktime, and statistics in the [TIMES] section
+* Pattern start, report start, and statistics in the [TIMES] section
 * PBV and GPV values in the [VALVES] section
 
 **Future development of WNTR will address these limitations.**
diff --git a/documentation/resilience.rst b/documentation/resilience.rst
index f8f6199f7..727dbae2f 100644
--- a/documentation/resilience.rst
+++ b/documentation/resilience.rst
@@ -212,8 +212,8 @@ use NetworkX directly, while others use metrics included in WNTR.
       >>> sim = wntr.sim.EpanetSimulator(wn)
       >>> results = sim.run_sim()
       
-      >>> flowrate = results.link['flowrate']
-      >>> G = wn.get_graph(link_weight=flowrate)
+      >>> flowrate = results.link['flowrate'].iloc[-1,:] # flowrate from the last timestep
+      >>> G = wn.get_graph(link_weight=flowrate, modify_direction=True)
       >>> all_paths = nx.all_simple_paths(G, '119', '193')
 
 * Valve segmentation, where each valve is defined by a node and link pair (see :ref:`valvelayer`)
diff --git a/documentation/resultsobject.rst b/documentation/resultsobject.rst
index 7665d535b..8ecaa4b28 100644
--- a/documentation/resultsobject.rst
+++ b/documentation/resultsobject.rst
@@ -82,9 +82,9 @@ Link results include DataFrames for each of the following attributes:
 
 * Velocity
 * Flowrate
+* Setting
 * Status (0 indicates closed, 1 indicates open)
 * Headloss (only when the EpanetSimulator is used)
-* Setting (only when the EpanetSimulator is used)
 * Friction factor (only when the EpanetSimulator is used)
 * Reaction rate (only when the EpanetSimulator is used)
 * Link quality (only when the EpanetSimulator is used)
diff --git a/documentation/waternetworkmodel.rst b/documentation/waternetworkmodel.rst
index 0c8e2dcaf..57e993f85 100644
--- a/documentation/waternetworkmodel.rst
+++ b/documentation/waternetworkmodel.rst
@@ -289,10 +289,10 @@ water network model.
     >>> wn.add_junction('node2', base_demand=0.02, demand_pattern='pat2', elevation=50, 
     ...     coordinates=(1,3))
     >>> wn.add_pipe('pipe1', 'node1', 'node2', length=304.8, diameter=0.3048, 
-    ...    roughness=100, minor_loss=0.0, status='OPEN')
+    ...    roughness=100, minor_loss=0.0, initial_status='OPEN')
     >>> wn.add_reservoir('res', base_head=125, head_pattern='pat1', coordinates=(0,2))
     >>> wn.add_pipe('pipe2', 'node1', 'res', length=100, diameter=0.3048, roughness=100, 
-    ...     minor_loss=0.0, status='OPEN')
+    ...     minor_loss=0.0, initial_status='OPEN')
     >>> ax = wntr.graphics.plot_network(wn)
 
 .. doctest::
diff --git a/documentation/whatsnew.rst b/documentation/whatsnew.rst
index dd1b8acd3..011fdb7dd 100644
--- a/documentation/whatsnew.rst
+++ b/documentation/whatsnew.rst
@@ -1,6 +1,8 @@
 Release notes
 ================
 
+.. include:: whatsnew/v0.3.2.rst
+
 .. include:: whatsnew/v0.3.1.rst
 
 .. include:: whatsnew/v0.3.0.rst
diff --git a/documentation/whatsnew/v0.3.2.rst b/documentation/whatsnew/v0.3.2.rst
new file mode 100644
index 000000000..c80119649
--- /dev/null
+++ b/documentation/whatsnew/v0.3.2.rst
@@ -0,0 +1,28 @@
+.. _whatsnew_032:
+
+v0.3.2 (main branch)
+---------------------------------------------------
+
+* Changed the following network component attributes:
+  
+  * Pipe ``check_valve_flag`` and ``cv`` attributes were merged and renamed ``check_valve``
+  * Pipe, Valve, and Pump ``status`` now refers to the current simulation status and is read-only, the user can set the initial status using ``initial_status``
+  * Valve ``setting`` now refers to the current simulation setting and is read-only, the user can set the initial setting using ``initial_setting``
+
+* Added slots to water network model options, ``wn.options``, to define available option names.
+  This update helps eliminate the use of incorrect or deprecated options.
+  The user can still create custom options under ``wn.options.user``. 
+
+* Removed custom handlers from the EPANET bin filer reader.  
+
+* Added a metric to compute pump energy
+
+* Added type checking for input into `wn.add_*` methods.
+
+* Updated the WNTRSimulator link results to include ``setting``.
+
+* Updated the WNTRSimulator to use `wn.options.time.start_clocktime`.
+
+* Updated documentation example that creates a weighted graph.
+
+* Updated all tests to use unittest. Removed Travis CI testing framework, all tests are run through GitHub Actions.
diff --git a/wntr/__init__.py b/wntr/__init__.py
index 2f59b9c94..acc9f8a25 100644
--- a/wntr/__init__.py
+++ b/wntr/__init__.py
@@ -7,7 +7,7 @@
 from wntr import graphics
 from wntr import utils
 
-__version__ = '0.3.1'
+__version__ = '0.3.2'
 
 __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 2a30a68e6..6417a0e86 100644
--- a/wntr/epanet/io.py
+++ b/wntr/epanet/io.py
@@ -15,32 +15,37 @@
 from __future__ import absolute_import
 
 import datetime
-import re
+import difflib
 import io
-import os, sys
 import logging
-import six
+import os
+import re
+import sys
 import warnings
-import numpy as np
-import pandas as pd
-import difflib
 from collections import OrderedDict
 
-#from .time_utils import run_lineprofile
-
+import numpy as np
+import pandas as pd
+import six
 import wntr
 import wntr.network
 from wntr.network.base import Link
-from wntr.network.model import WaterNetworkModel
-from wntr.network.elements import Junction, Reservoir, Tank, Pipe, Pump, Valve
+from wntr.network.controls import (AndCondition, Comparison, Control,
+                                   ControlAction, OrCondition, Rule,
+                                   SimTimeCondition, TimeOfDayCondition,
+                                   ValueCondition, _ControlType)
+from wntr.network.elements import Junction, Pipe, Pump, Reservoir, Tank, Valve
+from wntr.network.model import (Curve, Demands, LinkStatus, Pattern, Source,
+                                WaterNetworkModel)
 from wntr.network.options import Options
-from wntr.network.model import Pattern, LinkStatus, Curve, Demands, Source
-from wntr.network.controls import TimeOfDayCondition, SimTimeCondition, ValueCondition, Comparison
-from wntr.network.controls import OrCondition, AndCondition, Control, ControlAction, _ControlType, Rule
 
-from .util import FlowUnits, MassUnits, HydParam, QualParam, MixType, ResultType, EN
-from .util import to_si, from_si
-from .util import StatisticsType, QualType, PressureUnits
+from .util import (EN, FlowUnits, HydParam, MassUnits, MixType, PressureUnits,
+                   QualParam, QualType, ResultType, StatisticsType, from_si,
+                   to_si)
+
+#from .time_utils import run_lineprofile
+
+
 
 logger = logging.getLogger(__name__)
 
@@ -98,6 +103,10 @@ def _is_number(s):
     ----------
     s : anything
 
+    Returns
+    -------
+    bool
+        Input is a number
     """
 
     try:
@@ -161,7 +170,8 @@ def _clock_time_to_sec(s, am_pm):
 
     Returns
     -------
-    Integer value of time in seconds
+    int
+        Integer value of time in seconds
 
     """
     if am_pm.upper() == 'AM':
@@ -240,13 +250,15 @@ def __init__(self):
 
     def read(self, inp_files, wn=None):
         """
-        Method to read an EPANET INP file and load data into a water network model object.
+        Read an EPANET INP file and load data into a water network model object.
         Both EPANET 2.0 and EPANET 2.2 INP file options are recognized and handled.
 
         Parameters
         ----------
         inp_files : str or list
             An EPANET INP input file or list of INP files to be combined
+        wn : WaterNetworkModel, optional
+            An optional network model to append onto; by default a new model is created.
 
         Returns
         -------
@@ -678,18 +690,18 @@ def _read_pipes(self):
                 minor_loss = float(current[6])
                 if current[7].upper() == 'CV':
                     link_status = LinkStatus.Open
-                    check_valve_flag = True
+                    check_valve = True
                 else:
                     link_status = LinkStatus[current[7].upper()]
-                    check_valve_flag = False
+                    check_valve = False
             elif len(current) == 7:
                 minor_loss = float(current[6])
                 link_status = LinkStatus.Open
-                check_valve_flag = False
+                check_valve = False
             elif len(current) == 6:
                 minor_loss = 0.
                 link_status = LinkStatus.Open
-                check_valve_flag = False
+                check_valve = False
 
             self.wn.add_pipe(current[0],
                         current[1],
@@ -699,7 +711,7 @@ def _read_pipes(self):
                         float(current[5]),
                         minor_loss,
                         link_status,
-                        check_valve_flag)
+                        check_valve)
 
     def _write_pipes(self, f, wn):
         f.write('[PIPES]\n'.encode('ascii'))
@@ -718,7 +730,7 @@ def _write_pipes(self, f, wn):
                  'mloss': pipe.minor_loss,
                  'status': str(pipe.initial_status),
                  'com': ';'}
-            if pipe.cv:
+            if pipe.check_valve:
                 E['status'] = 'CV'
             f.write(_PIPE_ENTRY.format(**E).encode('ascii'))
         f.write('\n'.encode('ascii'))
@@ -751,7 +763,7 @@ def create_curve(curve_name):
                 if current[i].upper() == 'HEAD':
 #                    assert pump_type is None, 'In [PUMPS] entry, specify either HEAD or POWER once.'
                     pump_type = 'HEAD'
-                    value = create_curve(current[i+1])
+                    value = create_curve(current[i+1]).name
                 elif current[i].upper() == 'POWER':
 #                    assert pump_type is None, 'In [PUMPS] entry, specify either HEAD or POWER once.'
                     pump_type = 'POWER'
@@ -761,7 +773,7 @@ def create_curve(curve_name):
                     speed = float(current[i+1])
                 elif current[i].upper() == 'PATTERN':
 #                    assert pattern is None, 'In [PUMPS] entry, PATTERN may only be specified once.'
-                    pattern = self.wn.get_pattern(current[i+1])
+                    pattern = self.wn.get_pattern(current[i+1]).name
                 else:
                     raise RuntimeError('Pump keyword in inp file not recognized.')
 
@@ -856,17 +868,17 @@ def _write_valves(self, f, wn):
                  'node2': valve.end_node_name,
                  'diam': from_si(self.flow_units, valve.diameter, HydParam.PipeDiameter),
                  'vtype': valve.valve_type,
-                 'set': valve._setting,
+                 'set': valve.initial_setting,
                  'mloss': valve.minor_loss,
                  'com': ';'}
             valve_type = valve.valve_type
             formatter = _VALVE_ENTRY
             if valve_type in ['PRV', 'PSV', 'PBV']:
-                valve_set = from_si(self.flow_units, valve._setting, HydParam.Pressure)
+                valve_set = from_si(self.flow_units, valve.initial_setting, HydParam.Pressure)
             elif valve_type == 'FCV':
-                valve_set = from_si(self.flow_units, valve._setting, HydParam.Flow)
+                valve_set = from_si(self.flow_units, valve.initial_setting, HydParam.Flow)
             elif valve_type == 'TCV':
-                valve_set = valve._setting
+                valve_set = valve.initial_setting
             elif valve_type == 'GPV':
                 valve_set = valve.headloss_curve_name
                 formatter = _GPV_ENTRY
@@ -1106,136 +1118,50 @@ def _read_status(self):
     def _write_status(self, f, wn):
         f.write('[STATUS]\n'.encode('ascii'))
         f.write( '{:10s} {:10s}\n'.format(';ID', 'Setting').encode('ascii'))
-        for link_name, link in wn.links():
-            if isinstance(link, Pipe):
-                continue
-            if isinstance(link, Pump):
-                setting = link.initial_setting
+
+        pnames = list(wn.pump_name_list)
+        for pump_name in pnames:
+            pump = wn.links[pump_name]
+            if pump.initial_status in (LinkStatus.Closed,):
+                f.write('{:10s} {:10s}\n'.format(pump_name, LinkStatus(pump.initial_status).name).encode('ascii'))
+            else:
+                setting = pump.initial_setting
                 if type(setting) is float and setting != 1.0:
-                    f.write('{:10s} {:10.10g}\n'.format(link_name,
-                            setting).encode('ascii'))
-            if link.initial_status in (LinkStatus.Closed,):
-                f.write('{:10s} {:10s}\n'.format(link_name,
-                        LinkStatus(link.initial_status).name).encode('ascii'))
-            if isinstance(link, wntr.network.Valve) and link.initial_status in (LinkStatus.Open, LinkStatus.Opened):
-#           if link.initial_status in (LinkStatus.Closed,):
-                f.write('{:10s} {:10s}\n'.format(link_name,
-                        LinkStatus(link.initial_status).name).encode('ascii'))
-#                if link.initial_status is LinkStatus.Active:
-#                    valve_type = link.valve_type
-#                    if valve_type in ['PRV', 'PSV', 'PBV']:
-#                        setting = from_si(self.flow_units, link.initial_setting, HydParam.Pressure)
-#                    elif valve_type == 'FCV':
-#                        setting = from_si(self.flow_units, link.initial_setting, HydParam.Flow)
-#                    elif valve_type == 'TCV':
-#                        setting = link.initial_setting
-#                    else:
-#                        continue
-#                    continue
-#                elif isinstance(link, wntr.network.Pump):
-#                    setting = link.initial_setting
-#                else: continue
-#                f.write('{:10s} {:10.10g}\n'.format(link_name,
-#                        setting).encode('ascii'))
-#        f.write('\n'.encode('ascii'))
+                    f.write('{:10s} {:10.7g}\n'.format(pump_name, setting).encode('ascii'))
+        
+        vnames = list(wn.valve_name_list)
+        # lnames.sort()
+        for valve_name in vnames:
+            valve = wn.links[valve_name]
+            #valve_type = valve.valve_type
+
+            if valve.initial_status not in (LinkStatus.Active,): #LinkStatus.Opened, LinkStatus.Open,
+                f.write('{:10s} {:10s}\n'.format(valve_name, LinkStatus(valve.initial_status).name).encode('ascii'))
+        #     if valve_type in ['PRV', 'PSV', 'PBV']:
+        #         valve_set = from_si(self.flow_units, valve.initial_setting, HydParam.Pressure)
+        #     elif valve_type == 'FCV':
+        #         valve_set = from_si(self.flow_units, valve.initial_setting, HydParam.Flow)
+        #     elif valve_type == 'TCV':
+        #         valve_set = valve.initial_setting
+        #     elif valve_type == 'GPV':
+        #         valve_set = None
+        #     if valve_set is not None:
+        #         f.write('{:10s} {:10.7g}\n'.format(valve_name, float(valve_set)).encode('ascii'))
+        
+        f.write('\n'.encode('ascii'))
 
     def _read_controls(self):
         control_count = 0
         for lnum, line in self.sections['[CONTROLS]']:
-            line = line.split(';')[0]
-            current = line.split()
-            if current == []:
-                continue
-            link_name = current[1]
-            link = self.wn.get_link(link_name)
-            if current[5].upper() != 'TIME' and current[5].upper() != 'CLOCKTIME':
-                node_name = current[5]
-            current = [i.upper() for i in current]
-            current[1] = link_name # don't capitalize the link name
-
-            # Create the control action object
-
-            status = current[2].upper()
-            if status == 'OPEN' or status == 'OPENED' or status == 'CLOSED' or status == 'ACTIVE':
-                setting = LinkStatus[status].value
-                action_obj = wntr.network.ControlAction(link, 'status', setting)
-            else:
-                if isinstance(link, wntr.network.Pump):
-                    action_obj = wntr.network.ControlAction(link, 'base_speed', float(current[2]))
-                elif isinstance(link, wntr.network.Valve):
-                    if link.valve_type == 'PRV' or link.valve_type == 'PSV' or link.valve_type == 'PBV':
-                        setting = to_si(self.flow_units, float(current[2]), HydParam.Pressure)
-                    elif link.valve_type == 'FCV':
-                        setting = to_si(self.flow_units, float(current[2]), HydParam.Flow)
-                    elif link.valve_type == 'TCV':
-                        setting = float(current[2])
-                    elif link.valve_type == 'GPV':
-                        setting = current[2]
-                    else:
-                        raise ValueError('Unrecognized valve type {0} while parsing control {1}'.format(link.valve_type, line))
-                    action_obj = wntr.network.ControlAction(link, 'setting', setting)
-                else:
-                    raise RuntimeError(('Links of type {0} can only have controls that change\n'.format(type(link))+
-                                        'the link status. Control: {0}'.format(line)))
 
-            # Create the control object
             control_count += 1
             control_name = 'control '+str(control_count)
-            if 'TIME' not in current and 'CLOCKTIME' not in current:
-                threshold = None
-                if 'IF' in current:
-                    node = self.wn.get_node(node_name)
-                    if current[6] == 'ABOVE':
-                        oper = np.greater
-                    elif current[6] == 'BELOW':
-                        oper = np.less
-                    else:
-                        raise RuntimeError("The following control is not recognized: " + line)
-                    # OKAY - we are adding in the elevation. This is A PROBLEM
-                    # IN THE INP WRITER. Now that we know, we can fix it, but
-                    # if this changes, it will affect multiple pieces, just an
-                    # FYI.
-                    if node.node_type == 'Junction':
-                        threshold = to_si(self.flow_units,
-                                          float(current[7]), HydParam.Pressure)# + node.elevation
-                        control_obj = Control._conditional_control(node, 'pressure', oper, threshold, action_obj, control_name)
-                    elif node.node_type == 'Tank':
-                        threshold = to_si(self.flow_units, 
-                                          float(current[7]), HydParam.HydraulicHead)# + node.elevation
-                        control_obj = Control._conditional_control(node, 'level', oper, threshold, action_obj, control_name)
-                else:
-                    raise RuntimeError("The following control is not recognized: " + line)
-#                control_name = ''
-#                for i in range(len(current)-1):
-#                    control_name = control_name + '/' + current[i]
-#                control_name = control_name + '/' + str(round(threshold, 2))
-            else:
-                if 'CLOCKTIME' not in current:  # at time
-                    if 'TIME' not in current:
-                        raise ValueError('Unrecognized line in inp file: {0}'.format(line))
-
-                    if ':' in current[5]:
-                        run_at_time = int(_str_time_to_sec(current[5]))
-                    else:
-                        run_at_time = int(float(current[5])*3600)
-                    control_obj = Control._time_control(self.wn, run_at_time, 'SIM_TIME', False, action_obj, control_name)
-#                    control_name = ''
-#                    for i in range(len(current)-1):
-#                        control_name = control_name + '/' + current[i]
-#                    control_name = control_name + '/' + str(run_at_time)
-                else:  # at clocktime
-                    if len(current) < 7:
-                        if ':' in current[5]:
-                            run_at_time = int(_str_time_to_sec(current[5]))
-                        else:
-                            run_at_time = int(float(current[5])*3600)
-                    else:
-                        run_at_time = int(_clock_time_to_sec(current[5], current[6]))
-                    control_obj = Control._time_control(self.wn, run_at_time, 'CLOCK_TIME', True, action_obj, control_name)
-#                    control_name = ''
-#                    for i in range(len(current)-1):
-#                        control_name = control_name + '/' + current[i]
-#                    control_name = control_name + '/' + str(run_at_time)
+            
+            control_obj = _read_control_line(line, self.wn, self.flow_units, control_name)
+            if control_obj is None:
+                control_count -= 1 # control was not found 
+                continue
+            
             if control_name in self.wn.control_name_list:
                 warnings.warn('One or more [CONTROLS] were duplicated in "{}"; duplicates are ignored.'.format(self.wn.name), stacklevel=0)
                 logger.warning('Control already exists: "{}"'.format(control_name))
@@ -1716,7 +1642,7 @@ def _read_options(self):
                 elif key == 'VISCOSITY':
                     opts.hydraulic.viscosity = float(words[1])
                 elif key == 'DIFFUSIVITY':
-                    opts.hydraulic.diffusivity = float(words[1])
+                    opts.quality.diffusivity = float(words[1])
                 elif key == 'SPECIFIC':
                     opts.hydraulic.specific_gravity = float(words[2])
                 elif key == 'TRIALS':
@@ -1932,7 +1858,6 @@ def _write_times(self, f, wn):
 
         hrs, mm, sec = time.seconds_to_tuple(time.rule_timestep)
 
-        ### TODO: RULE TIMESTEP is not written?!
         f.write(time_entry.format('RULE TIMESTEP', hrs, mm, int(sec)).encode('ascii'))
         f.write(entry.format('STATISTIC', wn.options.time.statistic).encode('ascii'))
         f.write('\n'.encode('ascii'))
@@ -2498,7 +2423,7 @@ def generate_control(self, model):
 
 class BinFile(object):
     """
-    EPANET binary output file reader class.
+    EPANET binary output file reader.
     
     This class provides read functionality for EPANET binary output files.
     
@@ -2506,7 +2431,7 @@ class BinFile(object):
     ----------
     results_type : list of :class:`~wntr.epanet.util.ResultType`, default=None
         This parameter is *only* active when using a subclass of the BinFile that implements
-	a custom reader or writer.
+	    a custom reader or writer.
         If ``None``, then all results will be saved (node quality, demand, link flow, etc.).
         Otherwise, a list of result types can be passed to limit the memory used.
     network : bool, default=False
@@ -2574,71 +2499,6 @@ def _get_time(self, t):
         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.
-
-        The basic implementation sets up a dictionary of pandas DataFrames with the keys
-        being member names of the ResultsType class. If the items parameter is left blank,
-        the function will use the items that were specified during object creation.
-        If this too was blank, then all results parameters will be saved.
-
-        """
-        if result_types is None:
-            result_types = self.items
-        for member in result_types:
-            if member.is_node:
-                self.results.node[member.name] = pd.DataFrame(index=times, columns=nodes)
-            elif member.is_link:
-                self.results.link[member.name] = pd.DataFrame(index=times, columns=links)
-            else:
-                pass
-        self.results.network_name = self.inp_file
-
-    def save_ep_line(self, period, result_type, values):
-        """
-        Save an extended period set of values.
-
-        Each report period contains all the hydraulics and quality values for
-        the nodes and links. Nodes and link values are provided in the same
-        order as the names are specified in the prolog.
-
-        The result types for node data are: :attr:`ResultType.demand`, :attr:`ResultType.head`,
-        :attr:`ResultType.pressure` and :attr:`ResultType.quality`.
-
-        The result types for link data are: :attr:`ResultType.linkquality`,
-        :attr:`ResultType.flowrate`, and :attr:`ResultType.velocity`.
-
-        Parameters
-        ----------
-        period : int
-            The report period
-        result_type : str
-            One of the type strings listed above
-        values : numpy.array
-            The values to save, in the node or link order specified earlier in the file
-
-        """
-        if result_type in [ResultType.quality, ResultType.linkquality]:
-            if self.quality_type is QualType.Chem:
-                values = QualParam.Concentration._to_si(self.flow_units, values, mass_units=self.mass_units)
-            elif self.quality_type is QualType.Age:
-                values = QualParam.WaterAge._to_si(self.flow_units, values)
-        elif result_type == ResultType.demand:
-            values = HydParam.Demand._to_si(self.flow_units, values)
-        elif result_type == ResultType.flowrate:
-            values = HydParam.Flow._to_si(self.flow_units, values)
-        elif result_type == ResultType.head:
-            values = HydParam.HydraulicHead._to_si(self.flow_units, values)
-        elif result_type == ResultType.pressure:
-            values = HydParam.Pressure._to_si(self.flow_units, values)
-        elif result_type == ResultType.velocity:
-            values = HydParam.Velocity._to_si(self.flow_units, values)
-        if result_type in self.items:
-            if result_type.is_node:
-                self.results.node[result_type.name].iloc[period] = values
-            else:
-                self.results.link[result_type.name].iloc[period] = values
 
     def save_network_desc_line(self, element, values):
         """Save network description meta-data and element characteristics.
@@ -2655,7 +2515,7 @@ def save_network_desc_line(self, element, values):
             The values that go with the information
 
         """
-        self.results.meta[element] = values
+        pass
 
     def save_energy_line(self, pump_idx, pump_name, values):
         """Save pump energy from the output file.
@@ -2692,7 +2552,7 @@ def finalize_save(self, good_read, sim_warnings):
         pass
 
 #    @run_lineprofile()
-    def read(self, filename, convergence_error=False, custom_handlers=False):
+    def read(self, filename, convergence_error=False, darcy_weisbach=False, convert=True):
         """Read a binary file and create a results object.
 
         Parameters
@@ -2704,22 +2564,11 @@ def read(self, filename, convergence_error=False, custom_handlers=False):
             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
-            a faster, all-at-once reader that reads all results.
 
         Returns
         -------
         object
-            returns a WaterNetworkResults object
-
-        .. note:: Overloading
-            This function should **not** be overloaded. Instead, overload the other functions
-            to change how it saves the results. Specifically, overload :func:`~setup_ep_results`,
-            :func:`~save_ep_line` and :func:`~finalize_save` to change how extended period
-            simulation results in a different format (such as directly to a file or database).
-            
+            returns a WaterNetworkResults object    
         """
         self.results = wntr.sim.SimulationResults()
         
@@ -2865,82 +2714,50 @@ def read(self, filename, convergence_error=False, custom_handlers=False):
             self.save_network_desc_line('link_start', pd.Series(data=names[linkstart-1], index=linknames, copy=True))
             self.save_network_desc_line('link_end', pd.Series(data=names[linkend-1], index=linknames, copy=True))
             """
-            if custom_handlers is True:  
-                logger.debug('... set up results object ...')
-                self.setup_ep_results(reporttimes, nodenames, linknames)
-    
-                for ts in range(nrptsteps):
-                    try:
-                        demand = np.fromfile(fin, dtype=np.dtype(ftype), count=nnodes)
-                        head = np.fromfile(fin, dtype=np.dtype(ftype), count=nnodes)
-                        pressure = np.fromfile(fin, dtype=np.dtype(ftype), count=nnodes)
-                        quality = np.fromfile(fin, dtype=np.dtype(ftype), count=nnodes)
-                        flow = np.fromfile(fin, dtype=np.dtype(ftype), count=nlinks)
-                        velocity = np.fromfile(fin, dtype=np.dtype(ftype), count=nlinks)
-                        headloss = np.fromfile(fin, dtype=np.dtype(ftype), count=nlinks)
-                        linkquality = np.fromfile(fin, dtype=np.dtype(ftype), count=nlinks)
-                        linkstatus = np.fromfile(fin, dtype=np.dtype(ftype), count=nlinks)
-                        linksetting = np.fromfile(fin, dtype=np.dtype(ftype), count=nlinks)
-                        reactionrate = np.fromfile(fin, dtype=np.dtype(ftype), count=nlinks)
-                        frictionfactor = np.fromfile(fin, dtype=np.dtype(ftype), count=nlinks)
-                        self.save_ep_line(ts, ResultType.demand, demand)
-                        self.save_ep_line(ts, ResultType.head, head)
-                        self.save_ep_line(ts, ResultType.pressure, pressure)
-                        self.save_ep_line(ts, ResultType.quality, quality)
-                        self.save_ep_line(ts, ResultType.flowrate, flow)
-                        self.save_ep_line(ts, ResultType.velocity, velocity)
-                        self.save_ep_line(ts, ResultType.headloss, headloss)
-                        self.save_ep_line(ts, ResultType.linkquality, linkquality)
-                        self.save_ep_line(ts, ResultType.status, linkstatus)
-                        self.save_ep_line(ts, ResultType.setting, linksetting)
-                        self.save_ep_line(ts, ResultType.rxnrate, reactionrate)
-                        self.save_ep_line(ts, ResultType.frictionfact, frictionfactor)
-                    except Exception as e:
-                        logger.exception('Error reading or writing EP line: %s', e)
-                        logger.warning('Missing results from report period %d',ts)
-            else:
-#                type_list = 4*nnodes*['node'] + 8*nlinks*['link']
-                name_list = nodenames*4 + linknames*8
-                valuetype = nnodes*['demand']+nnodes*['head']+nnodes*['pressure']+nnodes*['quality'] + nlinks*['flow']+nlinks*['velocity']+nlinks*['headloss']+nlinks*['linkquality']+nlinks*['linkstatus']+nlinks*['linksetting']+nlinks*['reactionrate']+nlinks*['frictionfactor']
-                
-#                tuples = zip(type_list, valuetype, name_list)
-                tuples = list(zip(valuetype, name_list))
+            
+#           type_list = 4*nnodes*['node'] + 8*nlinks*['link']
+            name_list = nodenames*4 + linknames*8
+            valuetype = nnodes*['demand']+nnodes*['head']+nnodes*['pressure']+nnodes*['quality'] + nlinks*['flow']+nlinks*['velocity']+nlinks*['headloss']+nlinks*['linkquality']+nlinks*['linkstatus']+nlinks*['linksetting']+nlinks*['reactionrate']+nlinks*['frictionfactor']
+            
+#           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)
+            except Exception as e:
+                logger.exception('Failed to process file: %s', e)
                 
-                try:
-                    data = np.fromfile(fin, dtype = np.dtype(ftype), count = (4*nnodes+8*nlinks)*nrptsteps)
-                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
+            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 = np.reshape(data, (nrptsteps, (4*nnodes+8*nlinks)))
-                    self.results.error_code = None
+                    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()
-                
-                self.results.node = {}
-                self.results.link = {}
-                self.results.network_name = self.inp_file
-                
+            df = pd.DataFrame(data.transpose(), index =index, columns = reporttimes)
+            df = df.transpose()
+            
+            self.results.node = {}
+            self.results.link = {}
+            self.results.network_name = self.inp_file
+            
+            if convert:
                 # Node Results
                 self.results.node['demand'] = HydParam.Demand._to_si(self.flow_units, df['demand'])
                 self.results.node['head'] = HydParam.HydraulicHead._to_si(self.flow_units, df['head'])
                 self.results.node['pressure'] = HydParam.Pressure._to_si(self.flow_units, df['pressure'])
-
+        
                 # Water Quality Results (node and link)
                 if self.quality_type is QualType.Chem:
                     self.results.node['quality'] = QualParam.Concentration._to_si(self.flow_units, df['quality'], mass_units=self.mass_units)
@@ -2951,13 +2768,16 @@ def read(self, filename, convergence_error=False, custom_handlers=False):
                 else:
                     self.results.node['quality'] = df['quality']
                     self.results.link['quality'] = df['linkquality']
-
+        
                 # Link Results
                 self.results.link['flowrate'] = HydParam.Flow._to_si(self.flow_units, df['flow'])
-                self.results.link['headloss'] = df['headloss']  # Unit is per 1000
                 self.results.link['velocity'] = HydParam.Velocity._to_si(self.flow_units, df['velocity'])
                 
-#                self.results.link['status'] = df['linkstatus']
+                headloss = np.array(df['headloss'])
+                headloss[:, linktype < 2] = to_si(self.flow_units, headloss[:, linktype < 2], HydParam.HeadLoss) # Pipe or CV
+                headloss[:, linktype >= 2] = to_si(self.flow_units, headloss[:, linktype >= 2], HydParam.Length) # Pump or Valve
+                self.results.link["headloss"] = pd.DataFrame(data=headloss, columns=linknames, index=reporttimes)
+        
                 status = np.array(df['linkstatus'])
                 if self.convert_status:
                     status[status <= 2] = 0
@@ -2966,15 +2786,33 @@ def read(self, filename, convergence_error=False, custom_handlers=False):
                     status[status == 4] = 2
                 self.results.link['status'] = pd.DataFrame(data=status, columns=linknames, index=reporttimes)
                 
-                settings = np.array(df['linksetting'])
-                settings[:, linktype == EN.PRV] = to_si(self.flow_units, settings[:, linktype == EN.PRV], HydParam.Pressure)
-                settings[:, linktype == EN.PSV] = to_si(self.flow_units, settings[:, linktype == EN.PSV], HydParam.Pressure)
-                settings[:, linktype == EN.PBV] = to_si(self.flow_units, settings[:, linktype == EN.PBV], HydParam.Pressure)
-                settings[:, linktype == EN.FCV] = to_si(self.flow_units, settings[:, linktype == EN.FCV], HydParam.Flow)
-                self.results.link['setting'] = pd.DataFrame(data=settings, columns=linknames, index=reporttimes)
+                setting = np.array(df['linksetting'])
+                # pump setting is relative speed (unitless)
+                setting[:, linktype == EN.PIPE] = to_si(self.flow_units, setting[:, linktype == EN.PIPE], HydParam.RoughnessCoeff, 
+                                                darcy_weisbach=darcy_weisbach)
+                setting[:, linktype == EN.PRV] = to_si(self.flow_units, setting[:, linktype == EN.PRV], HydParam.Pressure)
+                setting[:, linktype == EN.PSV] = to_si(self.flow_units, setting[:, linktype == EN.PSV], HydParam.Pressure)
+                setting[:, linktype == EN.PBV] = to_si(self.flow_units, setting[:, linktype == EN.PBV], HydParam.Pressure)
+                setting[:, linktype == EN.FCV] = to_si(self.flow_units, setting[:, linktype == EN.FCV], HydParam.Flow)
+                self.results.link['setting'] = pd.DataFrame(data=setting, columns=linknames, index=reporttimes)
+                
                 self.results.link['friction_factor'] = df['frictionfactor']
-                self.results.link['reaction_rate'] = df['reactionrate']
+                self.results.link['reaction_rate'] = QualParam.ReactionRate._to_si(self.flow_units, df['reactionrate'],self.mass_units) 
+            else:
+                self.results.node['demand'] = df['demand']
+                self.results.node['head'] = df['head']
+                self.results.node['pressure'] = df['pressure']
+                self.results.node['quality'] = df['quality']
                 
+                self.results.link['flowrate'] = df['flow']
+                self.results.link['headloss'] = df['headloss']
+                self.results.link['velocity'] = df['velocity']
+                self.results.link['quality'] = df['linkquality']
+                self.results.link['status'] = df['linkstatus']
+                self.results.link['setting'] = df['linksetting']
+                self.results.link['friction_factor'] = df['frictionfactor']
+                self.results.link['reaction_rate'] = df['reactionrate']
+            
             logger.debug('... read epilog ...')
             # Read the averages and then the number of periods for checks
             averages = np.fromfile(fin, dtype=np.dtype(ftype), count=4)
@@ -3162,6 +3000,118 @@ def _clean_line(wn, sec, line):  # pragma: no cover
 
     return line
 
+
+def _read_control_line(line, wn, flow_units, control_name):
+    """
+    Parameters
+    ----------
+    line: str
+    wn: wntr.network.WaterNetworkModel
+    flow_units: str
+    control_name: str
+
+    Returns
+    -------
+    control_obj: Control
+	
+    """
+    line = line.split(';')[0]
+    current = line.split()
+    if current == []:
+        return
+    link_name = current[1]
+    link = wn.get_link(link_name)
+    if current[5].upper() != 'TIME' and current[5].upper() != 'CLOCKTIME':
+        node_name = current[5]
+    current = [i.upper() for i in current]
+    current[1] = link_name # don't capitalize the link name
+
+    # Create the control action object
+
+    status = current[2].upper()
+    if status == 'OPEN' or status == 'OPENED' or status == 'CLOSED' or status == 'ACTIVE':
+        setting = LinkStatus[status].value
+        action_obj = wntr.network.ControlAction(link, 'status', setting)
+    else:
+        if isinstance(link, wntr.network.Pump):
+            action_obj = wntr.network.ControlAction(link, 'base_speed', float(current[2]))
+        elif isinstance(link, wntr.network.Valve):
+            if link.valve_type == 'PRV' or link.valve_type == 'PSV' or link.valve_type == 'PBV':
+                setting = to_si(flow_units, float(current[2]), HydParam.Pressure)
+            elif link.valve_type == 'FCV':
+                setting = to_si(flow_units, float(current[2]), HydParam.Flow)
+            elif link.valve_type == 'TCV':
+                setting = float(current[2])
+            elif link.valve_type == 'GPV':
+                setting = current[2]
+            else:
+                raise ValueError('Unrecognized valve type {0} while parsing control {1}'.format(link.valve_type, line))
+            action_obj = wntr.network.ControlAction(link, 'setting', setting)
+        else:
+            raise RuntimeError(('Links of type {0} can only have controls that change\n'.format(type(link))+
+                                'the link status. Control: {0}'.format(line)))
+
+    # Create the control object
+    #control_count += 1
+    #control_name = 'control '+str(control_count)
+    if 'TIME' not in current and 'CLOCKTIME' not in current:
+        threshold = None
+        if 'IF' in current:
+            node = wn.get_node(node_name)
+            if current[6] == 'ABOVE':
+                oper = np.greater
+            elif current[6] == 'BELOW':
+                oper = np.less
+            else:
+                raise RuntimeError("The following control is not recognized: " + line)
+            # OKAY - we are adding in the elevation. This is A PROBLEM
+            # IN THE INP WRITER. Now that we know, we can fix it, but
+            # if this changes, it will affect multiple pieces, just an
+            # FYI.
+            if node.node_type == 'Junction':
+                threshold = to_si(flow_units,
+                                  float(current[7]), HydParam.Pressure)# + node.elevation
+                control_obj = Control._conditional_control(node, 'pressure', oper, threshold, action_obj, control_name)
+            elif node.node_type == 'Tank':
+                threshold = to_si(flow_units, 
+                                  float(current[7]), HydParam.HydraulicHead)# + node.elevation
+                control_obj = Control._conditional_control(node, 'level', oper, threshold, action_obj, control_name)
+        else:
+            raise RuntimeError("The following control is not recognized: " + line)
+#                control_name = ''
+#                for i in range(len(current)-1):
+#                    control_name = control_name + '/' + current[i]
+#                control_name = control_name + '/' + str(round(threshold, 2))
+    else:
+        if 'CLOCKTIME' not in current:  # at time
+            if 'TIME' not in current:
+                raise ValueError('Unrecognized line in inp file: {0}'.format(line))
+
+            if ':' in current[5]:
+                run_at_time = int(_str_time_to_sec(current[5]))
+            else:
+                run_at_time = int(float(current[5])*3600)
+            control_obj = Control._time_control(wn, run_at_time, 'SIM_TIME', False, action_obj, control_name)
+#                    control_name = ''
+#                    for i in range(len(current)-1):
+#                        control_name = control_name + '/' + current[i]
+#                    control_name = control_name + '/' + str(run_at_time)
+        else:  # at clocktime
+            if len(current) < 7:
+                if ':' in current[5]:
+                    run_at_time = int(_str_time_to_sec(current[5]))
+                else:
+                    run_at_time = int(float(current[5])*3600)
+            else:
+                run_at_time = int(_clock_time_to_sec(current[5], current[6]))
+            control_obj = Control._time_control(wn, run_at_time, 'CLOCK_TIME', True, action_obj, control_name)
+#                    control_name = ''
+#                    for i in range(len(current)-1):
+#                        control_name = control_name + '/' + current[i]
+#                    control_name = control_name + '/' + str(run_at_time)
+    return control_obj
+
+
 def _diff_inp_files(file1, file2=None, float_tol=1e-8, max_diff_lines_per_section=5, 
                     htmldiff_file='diff.html'):   # pragma: no cover
     """
@@ -3285,3 +3235,4 @@ def _diff_inp_files(file1, file2=None, float_tol=1e-8, max_diff_lines_per_sectio
     g.close()
     
     return n
+
diff --git a/wntr/epanet/toolkit.py b/wntr/epanet/toolkit.py
index 3bd5e0cd9..44cfe5d5b 100644
--- a/wntr/epanet/toolkit.py
+++ b/wntr/epanet/toolkit.py
@@ -12,54 +12,79 @@
     ENgetwarning
 
 """
-import ctypes, os, sys
-from ctypes import byref
+import ctypes
+import os
 import os.path
-from pkg_resources import resource_filename
 import platform
-epanet_toolkit = 'wntr.epanet.toolkit'
+import sys
+from ctypes import byref
+
+from pkg_resources import resource_filename
 
-if os.name in ['nt','dos']:
-    libepanet = resource_filename(__name__,'Windows/epanet2.dll')
-elif sys.platform in ['darwin']:
-    libepanet = resource_filename(__name__,'Darwin/libepanet.dylib')
+epanet_toolkit = "wntr.epanet.toolkit"
+
+if os.name in ["nt", "dos"]:
+    libepanet = resource_filename(__name__, "Windows/epanet2.dll")
+elif sys.platform in ["darwin"]:
+    libepanet = resource_filename(__name__, "Darwin/libepanet.dylib")
 else:
-    libepanet = resource_filename(__name__,'Linux/libepanet2.so')
+    libepanet = resource_filename(__name__, "Linux/libepanet2.so")
 
 import logging
+
 logger = logging.getLogger(__name__)
 
 # import warnings
 
+
 class EpanetException(Exception):
     pass
 
 
 def ENgetwarning(code, sec=-1):
     if sec >= 0:
-        hours = int(sec/3600.)
-        sec -= hours*3600
-        mm = int(sec/60.)
-        sec -= mm*60
-        header = 'At %3d:%.2d:%.2d, '%(hours,mm,sec)
+        hours = int(sec / 3600.0)
+        sec -= hours * 3600
+        mm = int(sec / 60.0)
+        sec -= mm * 60
+        header = "At %3d:%.2d:%.2d, " % (hours, mm, sec)
     else:
-        header = ''
+        header = ""
     if code == 1:
-        return header+'System hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials'
+        return (
+            header
+            + "System hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials"
+        )
     elif code == 2:
-        return header+'System may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed'
+        return (
+            header
+            + "System may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed"
+        )
     elif code == 3:
-        return header+'System disconnected - one or more nodes with positive demands were disconnected for all supply sources'
+        return (
+            header
+            + "System disconnected - one or more nodes with positive demands were disconnected for all supply sources"
+        )
     elif code == 4:
-        return header+'Pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow'
+        return (
+            header
+            + "Pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow"
+        )
     elif code == 5:
-        return header+'Vavles cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open'
+        return (
+            header
+            + "Vavles cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open"
+        )
     elif code == 6:
-        return header+'System has negative pressures - negative pressures occurred at one or more junctions with positive demand'
+        return (
+            header
+            + "System has negative pressures - negative pressures occurred at one or more junctions with positive demand"
+        )
     else:
-        return header+'Unknown warning: %d'%code
+        return header + "Unknown warning: %d" % code
+
 
-def runepanet(inpfile):
+def runepanet(inpfile, rptfile=None, binfile=None):
     """Run an EPANET command-line simulation
     
     Parameters
@@ -69,10 +94,13 @@ def runepanet(inpfile):
 
     """
     file_prefix, file_ext = os.path.splitext(inpfile)
+    if rptfile is None:
+        rptfile = file_prefix + ".rpt"
+    if binfile is None:
+        binfile = file_prefix + ".bin"
+    
     enData = ENepanet()
-    rptfile = file_prefix + '.rpt'
-    outfile = file_prefix + '.bin'
-    enData.ENopen(inpfile, rptfile, outfile)
+    enData.ENopen(inpfile, rptfile, binfile)
     enData.ENsolveH()
     enData.ENsolveQ()
     try:
@@ -82,7 +110,7 @@ def runepanet(inpfile):
     enData.ENclose()
 
 
-class ENepanet():
+class ENepanet:
     """Wrapper class to load the EPANET DLL object, then perform operations on
     the EPANET object that is created when a file is loaded.
 
@@ -98,7 +126,7 @@ class ENepanet():
         EPANET version to use (either 2.0 or 2.2)
     
     """
-    
+
     ENlib = None
     """The variable that holds the ctypes Library object"""
 
@@ -114,44 +142,49 @@ class ENepanet():
     Errflag = False
     """A fatal error occurred at some point during EPANET execution"""
 
-    inpfile = 'temp.inp'
+    inpfile = "temp.inp"
     """The name of the EPANET input file"""
 
-    rptfile = 'temp.rpt'
+    rptfile = "temp.rpt"
     """The report file to generate"""
 
-    binfile = 'temp.bin'
+    binfile = "temp.bin"
     """The optional binary output file"""
 
     fileLoaded = False
 
-    def __init__(self, inpfile='', rptfile='', binfile='', version=2.2):
+    def __init__(self, inpfile="", rptfile="", binfile="", version=2.2):
 
         self.inpfile = inpfile
         self.rptfile = rptfile
         self.binfile = binfile
 
-
         if float(version) == 2.0:
-            libnames = ['epanet2_x86','epanet2','epanet']
-            if '64' in platform.machine():
-                libnames.insert(0, 'epanet2_amd64')
+            libnames = ["epanet2_x86", "epanet2", "epanet"]
+            if "64" in platform.machine():
+                libnames.insert(0, "epanet2_amd64")
         elif float(version) == 2.2:
-            libnames = ['epanet22', 'epanet22_win32']
-            if '64' in platform.machine():
-                libnames.insert(0, 'epanet22_amd64')
+            libnames = ["epanet22", "epanet22_win32"]
+            if "64" in platform.machine():
+                libnames.insert(0, "epanet22_amd64")
         for lib in libnames:
             try:
-                if os.name in ['nt','dos']:
-                    libepanet = resource_filename(epanet_toolkit,'Windows/%s.dll' % lib)
+                if os.name in ["nt", "dos"]:
+                    libepanet = resource_filename(
+                        epanet_toolkit, "Windows/%s.dll" % lib
+                    )
                     self.ENlib = ctypes.windll.LoadLibrary(libepanet)
-                elif sys.platform in ['darwin']:
-                    libepanet = resource_filename(epanet_toolkit,'Darwin/lib%s.dylib' % lib)
+                elif sys.platform in ["darwin"]:
+                    libepanet = resource_filename(
+                        epanet_toolkit, "Darwin/lib%s.dylib" % lib
+                    )
                     self.ENlib = ctypes.cdll.LoadLibrary(libepanet)
                 else:
-                    libepanet = resource_filename(epanet_toolkit,'Linux/lib%s.so' % lib)
+                    libepanet = resource_filename(
+                        epanet_toolkit, "Linux/lib%s.so" % lib
+                    )
                     self.ENlib = ctypes.cdll.LoadLibrary(libepanet)
-                return # OK!
+                return  # OK!
             except Exception as E1:
                 if lib == libnames[-1]:
                     raise E1
@@ -164,17 +197,18 @@ def isOpen(self):
 
     def _error(self):
         """Print the error text the corresponds to the error code returned"""
-        if not self.errcode: return
-        #errtxt = self.ENlib.ENgeterror(self.errcode)
-        logger.error("EPANET error: %d",self.errcode)
+        if not self.errcode:
+            return
+        # errtxt = self.ENlib.ENgeterror(self.errcode)
+        logger.error("EPANET error: %d", self.errcode)
         if self.errcode >= 100:
             self.Errflag = True
             self.errcodelist.append(self.errcode)
-            raise EpanetException('EPANET Error {}'.format(self.errcode))
+            raise EpanetException("EPANET Error {}".format(self.errcode))
         else:
             self.Warnflag = True
             # warnings.warn(ENgetwarning(self.errcode))
-            self.errcodelist.append(ENgetwarning(self.errcode,self.cur_time))
+            self.errcodelist.append(ENgetwarning(self.errcode, self.cur_time))
         return
 
     def ENopen(self, inpfile=None, rptfile=None, binfile=None):
@@ -191,15 +225,19 @@ def ENopen(self, inpfile=None, rptfile=None, binfile=None):
             Binary output file to create (default to constructor value)
             
         """
-        if self.fileLoaded: self.ENclose()
+        if self.fileLoaded:
+            self.ENclose()
         if self.fileLoaded:
             raise RuntimeError("File is loaded and cannot be closed")
-        if inpfile is None: inpfile = self.inpfile
-        if rptfile is None: rptfile = self.rptfile
-        if binfile is None: binfile = self.binfile
-        inpfile = inpfile.encode('ascii')
-        rptfile = rptfile.encode('ascii')
-        binfile = binfile.encode('ascii')
+        if inpfile is None:
+            inpfile = self.inpfile
+        if rptfile is None:
+            rptfile = self.rptfile
+        if binfile is None:
+            binfile = self.binfile
+        inpfile = inpfile.encode("ascii")
+        rptfile = rptfile.encode("ascii")
+        binfile = binfile.encode("ascii")
         self.errcode = self.ENlib.ENopen(inpfile, rptfile, binfile)
         self._error()
         if self.errcode < 100:
@@ -262,7 +300,8 @@ def ENrunH(self):
         
         Returns
         --------
-        Current simulation time (seconds)
+        int
+            Current simulation time (seconds)
         
         """
         lT = ctypes.c_long()
@@ -280,7 +319,8 @@ def ENnextH(self):
         
         Returns
         ---------
-         Time (seconds) until next hydraulic event (0 marks end of simulation period)
+        int
+            Time (seconds) until next hydraulic event (0 marks end of simulation period)
          
         """
         lTstep = ctypes.c_long()
@@ -300,10 +340,10 @@ def ENsavehydfile(self, filename):
         Parameters
         -------------
         filename : str
-            Name of file
+            Name of hydraulics file to output
             
         """
-        self.errcode = self.ENlib.ENsavehydfile(filename.encode('ascii'))
+        self.errcode = self.ENlib.ENsavehydfile(filename.encode("ascii"))
         self._error()
         return
 
@@ -313,10 +353,10 @@ def ENusehydfile(self, filename):
         Parameters
         -------------
         filename : str
-            Name of file
+            Name of hydraulics file to use
             
         """
-        self.errcode = self.ENlib.ENusehydfile(filename.encode('ascii'))
+        self.errcode = self.ENlib.ENusehydfile(filename.encode("ascii"))
         self._error()
         return
 
@@ -337,7 +377,7 @@ def ENinitQ(self, iSaveflag):
 
         Parameters
         -------------
-         iSaveflag : int
+        iSaveflag : int
              EN_SAVE (1) if results saved to file, EN_NOSAVE (0) if not
              
         """
@@ -354,7 +394,8 @@ def ENrunQ(self):
         
         Returns
         -------
-        Current simulation time (seconds)
+        int
+            Current simulation time (seconds)
          
         """
         lT = ctypes.c_long()
@@ -371,7 +412,8 @@ def ENnextQ(self):
         
         Returns
         --------
-        Time (seconds) until next hydraulic event (0 marks end of simulation period)
+        int
+            Time (seconds) until next hydraulic event (0 marks end of simulation period)
          
         """
         lTstep = ctypes.c_long()
@@ -401,7 +443,8 @@ def ENgetcount(self, iCode):
 
         Returns
         ---------
-        Number of components in network
+        int
+            Number of components in network
         
         """
         iCount = ctypes.c_int()
@@ -436,12 +479,13 @@ def ENgetnodeindex(self, sId):
         
         """
         iIndex = ctypes.c_int()
-        self.errcode = self.ENlib.ENgetnodeindex(sId.encode('ascii'), byref(iIndex))
+        self.errcode = self.ENlib.ENgetnodeindex(sId.encode("ascii"), byref(iIndex))
         self._error()
         return iIndex.value
 
     def ENgetnodevalue(self, iIndex, iCode):
-        """Retrieves parameter value for a node
+        """
+        Retrieves parameter value for a node
 
         Parameters
         -------------
@@ -474,7 +518,7 @@ def ENgetlinkindex(self, sId):
 
         """
         iIndex = ctypes.c_int()
-        self.errcode = self.ENlib.ENgetlinkindex(sId.encode('ascii'), byref(iIndex))
+        self.errcode = self.ENlib.ENgetlinkindex(sId.encode("ascii"), byref(iIndex))
         self._error()
         return iIndex.value
 
@@ -498,7 +542,6 @@ def ENgetlinkvalue(self, iIndex, iCode):
         self._error()
         return fValue.value
 
-
     def ENsaveinpfile(self, inpfile):
         """Saves EPANET input file
 
@@ -509,11 +552,9 @@ def ENsaveinpfile(self, inpfile):
 
         """
 
-        inpfile = inpfile.encode('ascii')
+        inpfile = inpfile.encode("ascii")
         self.errcode = self.ENlib.ENsaveinpfile(inpfile)
         self._error()
 
         return
 
-
-    
diff --git a/wntr/epanet/util.py b/wntr/epanet/util.py
index 4affb578a..55c2b4888 100644
--- a/wntr/epanet/util.py
+++ b/wntr/epanet/util.py
@@ -23,21 +23,36 @@
 	EN
 
 """
-import numpy as np
-import pandas as pd
 import enum
 import logging
 
+import numpy as np
+import pandas as pd
+
 logger = logging.getLogger(__name__)
 
-__all__ = ["FlowUnits", "MassUnits", "QualParam", "HydParam", "to_si", "from_si",
-           "StatisticsType", "QualType", "SourceType", "PressureUnits", "FormulaType",
-           "ControlType", "LinkTankStatus",
-           "MixType", "ResultType", "EN"]
+__all__ = [
+    "FlowUnits",
+    "MassUnits",
+    "QualParam",
+    "HydParam",
+    "to_si",
+    "from_si",
+    "StatisticsType",
+    "QualType",
+    "SourceType",
+    "PressureUnits",
+    "FormulaType",
+    "ControlType",
+    "LinkTankStatus",
+    "MixType",
+    "ResultType",
+    "EN",
+]
 
 
 class FlowUnits(enum.Enum):
-    u"""Epanet Units Enum class.
+    r"""Epanet Units Enum class
 
     EPANET has defined unit codes that are used in its INP input files.
     This enumerated type class provides the appropriate values, rather than
@@ -116,24 +131,27 @@ class FlowUnits(enum.Enum):
 
     """
     CFS = (0, 0.0283168466)
-    GPM = (1, (0.003785411784/60.0))
-    MGD = (2, (1e6*0.003785411784/86400.0))
-    IMGD = (3, (1e6*0.00454609/86400.0))
-    AFD = (4, (1233.48184/86400.0))
+    GPM = (1, (0.003785411784 / 60.0))
+    MGD = (2, (1e6 * 0.003785411784 / 86400.0))
+    IMGD = (3, (1e6 * 0.00454609 / 86400.0))
+    AFD = (4, (1233.48184 / 86400.0))
     LPS = (5, 0.001)
-    LPM = (6, (0.001/60.0))
-    MLD = (7, (1e6*0.001/86400.0))
-    CMH = (8, (1.0/3600.0))
-    CMD = (9, (1.0/86400.0))
+    LPM = (6, (0.001 / 60.0))
+    MLD = (7, (1e6 * 0.001 / 86400.0))
+    CMH = (8, (1.0 / 3600.0))
+    CMD = (9, (1.0 / 86400.0))
     SI = (11, 1.0)
 
-    def __init__(self, EN_id, flow_factor):
-        self._value2member_map_[EN_id] = self
-        self._member_map_[self.name.lower()] = self
+    def __init__(self, EN_id, flow_factor=1.0):
+        mmap = getattr(self, "_member_map_")
+        v2mmap = getattr(self, "_value2member_map_")
+        v2mmap[EN_id] = self
+        mmap[str(self.name).lower()] = self
 
     def __int__(self):
         """Convert to an EPANET Toolkit enum number."""
-        return int(self.value[0])
+        value = super().value
+        return int(value[0])
 
     def __str__(self):
         """Convert to a string for INP files."""
@@ -141,7 +159,7 @@ def __str__(self):
 
     @property
     def factor(self):
-        """float: The conversion factor to convert units into SI units of :math:`m^3\,s^{-1}`.
+        r"""float: The conversion factor to convert units into SI units of :math:`m^3\,s^{-1}`.
 
         Letting values in the original units be :math:`v`, and the resulting values in SI units
         be :math:`s`, the conversion factor, :math:`f`, such that
@@ -150,7 +168,8 @@ def factor(self):
             v f = s
 
         """
-        return self.value[1]
+        value = super().value
+        return value[1]
 
     @property
     def is_traditional(self):
@@ -168,7 +187,13 @@ def is_traditional(self):
         False
 
         """
-        return self in [FlowUnits.CFS, FlowUnits.GPM, FlowUnits.MGD, FlowUnits.IMGD, FlowUnits.AFD]
+        return self in [
+            FlowUnits.CFS,
+            FlowUnits.GPM,
+            FlowUnits.MGD,
+            FlowUnits.IMGD,
+            FlowUnits.AFD,
+        ]
 
     @property
     def is_metric(self):
@@ -187,7 +212,13 @@ def is_metric(self):
         False
 
         """
-        return self in [FlowUnits.LPS, FlowUnits.LPM, FlowUnits.MLD, FlowUnits.CMH, FlowUnits.CMD]
+        return self in [
+            FlowUnits.LPS,
+            FlowUnits.LPM,
+            FlowUnits.MLD,
+            FlowUnits.CMH,
+            FlowUnits.CMD,
+        ]
 
 
 class MassUnits(enum.Enum):
@@ -220,7 +251,8 @@ class MassUnits(enum.Enum):
     @property
     def factor(self):
         """float : The scaling factor to convert to kg."""
-        return self.value[1]
+        value = super().value
+        return value[1]
 
 
 class QualParam(enum.Enum):
@@ -257,13 +289,13 @@ class QualParam(enum.Enum):
     WaterAge = 39
 
     def __init__(self, value):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, "_member_map_")
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
-    def _to_si(self, flow_units, data, mass_units=MassUnits.mg,
-              reaction_order=0):
+    def _to_si(self, flow_units, data, mass_units=MassUnits.mg, reaction_order=0):
         """Convert a water quality parameter to SI units from EPANET units.
 
         By default, the mass units are the EPANET default of mg, and the reaction order is 0.
@@ -293,32 +325,35 @@ def _to_si(self, flow_units, data, mass_units=MassUnits.mg,
             data = np.array(data)
 
         if mass_units is None:
-            mass_units=MassUnits.mg
+            mass_units = MassUnits.mg
 
         # Do conversions
-        if self in [QualParam.Concentration, QualParam.Quality, QualParam.LinkQuality]:
-            data = data * (mass_units.factor/0.001)  # MASS /L to kg/m3
-
+        if self in [QualParam.Concentration, QualParam.Quality, 
+                    QualParam.LinkQuality, QualParam.ReactionRate]:
+            data = data * (mass_units.factor / 0.001)  # MASS /L to kg/m3
+            if self in [QualParam.ReactionRate]:
+                data = data / (24*3600)  # 1/day to 1/s
+            
         elif self in [QualParam.SourceMassInject]:
-            data = data * (mass_units.factor/60.0)  # MASS /min to kg/s
+            data = data * (mass_units.factor / 60.0)  # MASS /min to kg/s
 
         elif self in [QualParam.BulkReactionCoeff] and reaction_order == 1:
-                data = data * (1/86400.0)  # per day to per second
+            data = data * (1 / 86400.0)  # per day to per second
 
         elif self in [QualParam.WallReactionCoeff] and reaction_order == 0:
             if flow_units.is_traditional:
-                data = data * (mass_units.factor*0.092903/86400.0)  # M/ft2/d to SI
+                data = data * (mass_units.factor * 0.092903 / 86400.0)  # M/ft2/d to SI
             else:
-                data = data * (mass_units.factor/86400.0)  # M/m2/day to M/m2/s
+                data = data * (mass_units.factor / 86400.0)  # M/m2/day to M/m2/s
 
         elif self in [QualParam.WallReactionCoeff] and reaction_order == 1:
             if flow_units.is_traditional:
-                data = data * (0.3048/86400.0)  # ft/d to m/s
+                data = data * (0.3048 / 86400.0)  # ft/d to m/s
             else:
-                data = data * (1.0/86400.0)  # m/day to m/s
+                data = data * (1.0 / 86400.0)  # m/day to m/s
 
         elif self in [QualParam.SourceMassInject]:
-            data = data * (mass_units.factor/60.0)  # per min to per second
+            data = data * (mass_units.factor / 60.0)  # per min to per second
 
         elif self in [QualParam.WaterAge]:
             data = data * 3600.0  # hr to s
@@ -330,8 +365,7 @@ def _to_si(self, flow_units, data, mass_units=MassUnits.mg,
             data = list(data)
         return data
 
-    def _from_si(self, flow_units, data, mass_units=MassUnits.mg,
-                reaction_order=0):
+    def _from_si(self, flow_units, data, mass_units=MassUnits.mg, reaction_order=0):
         """Convert a water quality parameter back to EPANET units from SI units.
 
         Mass units defaults to :class:`MassUnits.mg`, as this is the EPANET default.
@@ -361,29 +395,32 @@ def _from_si(self, flow_units, data, mass_units=MassUnits.mg,
             data = np.array(data)
 
         # Do conversions
-        if self in [QualParam.Concentration, QualParam.Quality, QualParam.LinkQuality]:
-            data = data / (mass_units.factor/0.001)  # MASS /L fr kg/m3
-
+        if self in [QualParam.Concentration, QualParam.Quality, 
+                    QualParam.LinkQuality, QualParam.ReactionRate]:
+            data = data / (mass_units.factor / 0.001)  # MASS /L fr kg/m3
+            if self in [QualParam.ReactionRate]:
+                data = data * (24*3600)  # 1/day fr 1/s
+                
         elif self in [QualParam.SourceMassInject]:
-            data = data / (mass_units.factor/60.0)  # MASS /min fr kg/s
+            data = data / (mass_units.factor / 60.0)  # MASS /min fr kg/s
 
         elif self in [QualParam.BulkReactionCoeff] and reaction_order == 1:
-                data = data / (1/86400.0)  # per day fr per second
+            data = data / (1 / 86400.0)  # per day fr per second
 
         elif self in [QualParam.WallReactionCoeff] and reaction_order == 0:
             if flow_units.is_traditional:
-                data = data / (mass_units.factor*0.092903/86400.0)  # M/ft2/d fr SI
+                data = data / (mass_units.factor * 0.092903 / 86400.0)  # M/ft2/d fr SI
             else:
-                data = data / (mass_units.factor/86400.0)  # M/m2/day fr M/m2/s
+                data = data / (mass_units.factor / 86400.0)  # M/m2/day fr M/m2/s
 
         elif self in [QualParam.WallReactionCoeff] and reaction_order == 1:
             if flow_units.is_traditional:
-                data = data / (0.3048/86400.0)  # ft/d fr m/s
+                data = data / (0.3048 / 86400.0)  # ft/d fr m/s
             else:
-                data = data / (1.0/86400.0)  # m/day fr m/s
+                data = data / (1.0 / 86400.0)  # m/day fr m/s
 
         elif self in [QualParam.SourceMassInject]:
-            data = data / (mass_units.factor/60.0)  # per min fr per second
+            data = data / (mass_units.factor / 60.0)  # per min fr per second
 
         elif self in [QualParam.WaterAge]:
             data = data / 3600.0  # hr fr s
@@ -461,10 +498,11 @@ class HydParam(enum.Enum):
     Energy = 34
 
     def __init__(self, value):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, "_member_map_")
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def _to_si(self, flow_units, data, darcy_weisbach=False):
         """Convert from EPANET units groups to SI units.
@@ -516,15 +554,21 @@ def _to_si(self, flow_units, data, darcy_weisbach=False):
 
         elif self in [HydParam.RoughnessCoeff] and darcy_weisbach:
             if flow_units.is_traditional:
-                data = data * (1000.0*0.3048)  # 1e-3 ft to m
+                data = data * (1000.0 * 0.3048)  # 1e-3 ft to m
             elif flow_units.is_metric:
                 data = data * 0.001  # mm to m
 
-        elif self in [HydParam.TankDiameter, HydParam.Elevation, HydParam.HydraulicHead,
-                      HydParam.Length]:#, HydParam.HeadLoss]:
+        elif self in [
+            HydParam.TankDiameter,
+            HydParam.Elevation,
+            HydParam.HydraulicHead,
+            HydParam.Length]:
             if flow_units.is_traditional:
                 data = data * 0.3048  # ft to m
-
+        
+        elif self in [HydParam.HeadLoss]:
+            data = data / 1000  # m/1000m or ft/1000ft to unitless
+            
         elif self in [HydParam.Velocity]:
             if flow_units.is_traditional:
                 data = data * 0.3048  # ft/s to m/s
@@ -540,8 +584,8 @@ def _to_si(self, flow_units, data, darcy_weisbach=False):
 
         elif self in [HydParam.Pressure]:
             if flow_units.is_traditional:
-#                data = data * 0.703249614902
-                data = data * (0.3048/0.4333)  # psi * (m/ft / psi/ft)
+                #                data = data * 0.703249614902
+                data = data * (0.3048 / 0.4333)  # psi * (m/ft / psi/ft)
 
         elif self in [HydParam.Volume]:
             if flow_units.is_traditional:
@@ -602,15 +646,21 @@ def _from_si(self, flow_units, data, darcy_weisbach=False):
 
         elif self in [HydParam.RoughnessCoeff] and darcy_weisbach:
             if flow_units.is_traditional:
-                data = data / (1000.0*0.3048)  # 1e-3 ft from m
+                data = data / (1000.0 * 0.3048)  # 1e-3 ft from m
             elif flow_units.is_metric:
                 data = data / 0.001  # mm from m
 
-        elif self in [HydParam.TankDiameter, HydParam.Elevation, HydParam.HydraulicHead,
-                      HydParam.Length]:#, HydParam.HeadLoss]:
+        elif self in [
+            HydParam.TankDiameter,
+            HydParam.Elevation,
+            HydParam.HydraulicHead,
+            HydParam.Length]:
             if flow_units.is_traditional:
                 data = data / 0.3048  # ft from m
-
+                    
+        elif self in [HydParam.HeadLoss]:
+            data = data * 1000  # m/1000m or ft/1000ft from unitless
+            
         elif self in [HydParam.Velocity]:
             if flow_units.is_traditional:
                 data = data / 0.3048  # ft/s from m/s
@@ -626,8 +676,10 @@ def _from_si(self, flow_units, data, darcy_weisbach=False):
 
         elif self in [HydParam.Pressure]:
             if flow_units.is_traditional:
-#                data = data / 0.703249614902
-                data = data / (0.3048/0.4333)  # psi from mH2O, i.e. psi / (m/ft / psi/ft) )
+                #                data = data / 0.703249614902
+                data = data / (
+                    0.3048 / 0.4333
+                )  # psi from mH2O, i.e. psi / (m/ft / psi/ft) )
 
         elif self in [HydParam.Volume]:
             if flow_units.is_traditional:
@@ -641,169 +693,6 @@ def _from_si(self, flow_units, data, darcy_weisbach=False):
         return data
 
 
-def to_si(from_units, data, param,
-          mass_units=MassUnits.mg, pressure_units=None,
-          darcy_weisbach=False, reaction_order=0):
-    """Convert an EPANET parameter from internal to SI standard units.
-
-    .. note:: 
-
-        See the `Units <../units.html>`__ page for details on the units for each :class:`~HydParam` or :class:`~QualParam`.
-        Other than for flows, most parameters only have one US and one metric unit that is used by EPANET.
-        For example, even though flow might be specified in gallons, volumes would be specified in cubic feet
-        for any US/English flow rates, and in cubic meters for all metric flow units; e.g., never liters 
-        for volumes even when flow is declared as LPS.
-
-        Rememeber that internally, WNTR is **always** expecting the values for a parameter to be in true SI
-        units -- meters, kilograms, and seconds -- unless explicitly stated otherwise (e.g., hours for control times).
-
-
-    Parameters
-    ----------
-    from_units : :class:`~FlowUnits`
-        The EPANET flow units (and therefore units system) to use for conversion
-    data : float, array-like, dict
-        The data to be converted
-    param : :class:`~HydParam` or :class:`~QualParam`
-        The parameter type for the data
-    mass_units : :class:`~MassUnits`, optional
-        The EPANET mass units (mg or ug internal to EPANET)
-    pressure_units : :class:`~PressureUnits`, optional
-        The EPANET pressure units being used (based on `flow_units`, normally)
-    darcy_weisbach : bool, optional
-        For roughness coefficients, is this used in a Darcy-Weisbach formula?
-    reaction_order : int, optional
-        For reaction coefficients, what is the reaction order?
-
-    Returns
-    -------
-    float, array-like, or dict
-        The data values convert into SI standard units
-
-
-    Examples
-    --------
-
-    First, we convert an array of flows from GPM to cubic meters per second (the SI units).
-
-    >>> from wntr.epanet.util import *
-    >>> flow_si = to_si(FlowUnits.GPM, [0.1, 1.0, 4.3], HydParam.Flow)
-    >>> print(flow_si)
-    [6.309019640000001e-06, 6.30901964e-05, 0.00027128784452]
-
-    Next, we show how to convert the quality parameter from the EPANET units of mg/L to kg/m3.
-    If that is not the mass units you prefer, it is possible to change them to ug/L, g/L, or kg/L,
-    as shown in the second example.
-
-    >>> to_si(FlowUnits.GPM, 4.6, QualParam.Quality)
-    0.0046
-    >>> to_si(FlowUnits.GPM, 4.6, QualParam.Quality, mass_units=MassUnits.ug)
-    4.599999999999999e-06
-
-    It is also possible to convert a dictionary of values.
-
-    >>> to_si(FlowUnits.GPM, {'node1': 5.6, 'node2': 1.2}, HydParam.Pressure)
-    {'node1': 3.9392568659127623, 'node2': 0.8441264712670206}
-
-    For certain coefficients, there are flags that will change how the conversion occurs. For example,
-    reaction coefficients depend on the reaction order.
-
-    >>> to_si(FlowUnits.GPM, 0.45, QualParam.BulkReactionCoeff, reaction_order=0)
-    0.45
-    >>> to_si(FlowUnits.GPM, 0.45, QualParam.BulkReactionCoeff, reaction_order=1)
-    5.208333333333333e-06
-
-
-    """
-    if isinstance(param, HydParam):
-        return param._to_si(from_units, data, darcy_weisbach)
-    elif isinstance(param, QualParam):
-        return param._to_si(from_units, data, mass_units, reaction_order)
-    else:
-        raise RuntimeError('Invalid parameter: %s' % param)
-
-
-def from_si(to_units, data, param,
-          mass_units=MassUnits.mg, pressure_units=None,
-          darcy_weisbach=False, reaction_order=0):
-    """Convert an EPANET parameter from SI standard units back to internal units.
-
-    .. note:: 
-
-        See the `Units <../units.html>`__ page for details on the units for each :class:`~HydParam` or :class:`~QualParam`.
-        Other than for flows, most parameters only have one US and one metric unit that is used by EPANET.
-        For example, even though flow might be specified in gallons, volumes would be specified in cubic feet
-        for any US/English flow rates, and in cubic meters for all metric flow units; e.g., never liters 
-        for volumes even when flow is declared as LPS.
-
-        Rememeber that internally, WNTR is **always** expecting the values for a parameter to be in true SI
-        units -- meters, kilograms, and seconds -- unless explicitly stated otherwise (e.g., hours for control times).
-
-
-    Parameters
-    ----------
-    to_units : :class:`~FlowUnits`
-        The EPANET flow units (and therefore units system) to use for conversion
-    data : float, array-like, dict
-        The data to be converted
-    param : :class:`~HydParam` or :class:`~QualParam`
-        The parameter type for the data
-    mass_units : :class:`~MassUnits`, optional
-        The EPANET mass units (mg or ug internal to EPANET)
-    pressure_units : :class:`~PressureUnits`, optional
-        The EPANET pressure units being used (based on `flow_units`, normally)
-    darcy_weisbach : bool, optional
-        For roughness coefficients, is this used in a Darcy-Weisbach formula?
-    reaction_order : int, optional
-        For reaction coefficients, what is the reaction order?
-
-    Returns
-    -------
-    float, array-like, or dict
-        The data values converted into EPANET internal units
-
-
-    Examples
-    --------
-
-    First, we convert an array of flows from SI (cubic meters per second) to GPM.
-
-    >>> from wntr.epanet.util import *
-    >>> flow_us = from_si(FlowUnits.GPM, [6.309019640000001e-06, 6.30901964e-05, 0.00027128784452], HydParam.Flow)
-    >>> print(flow_us)
-    [0.1, 1.0, 4.3]
-
-    Next, we show how to convert the quality parameter from kg/m3 to mg/L and then to ug/L.
-    
-    >>> from_si(FlowUnits.GPM, 0.0046, QualParam.Quality)
-    4.6
-    >>> from_si(FlowUnits.GPM, 0.0046, QualParam.Quality, mass_units=MassUnits.ug)
-    4600.0
-
-    It is also possible to convert a dictionary of values.
-
-    >>> from_si(FlowUnits.GPM, {'node1': 3.9392568659127623, 'node2': 0.8441264712670206}, HydParam.Pressure)
-    {'node1': 5.6, 'node2': 1.2}
-    
-    Finally, an example showing the conversion of 1000 cubic meters per second into the different flow units.
-
-    >>> from_si(FlowUnits.GPM, 1000.0, HydParam.Flow)  # to gallons per minute
-    15850323.141488904
-    >>> from_si(FlowUnits.LPS, 1000.0, HydParam.Flow)  # to liters per second
-    1000000.0
-    >>> from_si(FlowUnits.MGD, 1000.0, HydParam.Flow)  # to million gallons per day
-    22824.465323744018
-
-
-    """
-    if isinstance(param, HydParam):
-        return param._from_si(to_units, data, darcy_weisbach)
-    elif isinstance(param, QualParam):
-        return param._from_si(to_units, data, mass_units, reaction_order)
-    else:
-        raise RuntimeError('Invalid parameter: %s' % param)
-
-
 class StatisticsType(enum.Enum):
     """EPANET time series statistics processing.
 
@@ -818,6 +707,7 @@ class StatisticsType(enum.Enum):
     ================  =========================================================================
 
     """
+
     none = 0
     Average = 1
     Minimum = 2
@@ -825,10 +715,11 @@ class StatisticsType(enum.Enum):
     Range = 4
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, "_member_map_")
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
@@ -847,16 +738,18 @@ class QualType(enum.Enum):
     ================  =========================================================================
 
     """
+
     none = 0
     Chem = 1
     Age = 2
     Trace = 3
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, "_member_map_")
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
@@ -875,16 +768,18 @@ class SourceType(enum.Enum):
     ==================  =========================================================================
 
     """
+
     Concen = 0
     Mass = 1
     Setpoint = 2
     FlowPaced = 3
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, "_member_map_")
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
@@ -902,15 +797,17 @@ class PressureUnits(enum.Enum):
     ===============  ====================================================
 
     """
+
     psi = 0
     kPa = 1
     Meters = 2
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, "_member_map_")
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
@@ -928,23 +825,37 @@ class FormulaType(enum.Enum):
     ===============  ==================================================================
 
     """
-    HW = (0, "H-W",)
-    DW = (1, "D-W",)
-    CM = (2, "C-M",)
+
+    HW = (
+        0,
+        "H-W",
+    )
+    DW = (
+        1,
+        "D-W",
+    )
+    CM = (
+        2,
+        "C-M",
+    )
 
     def __init__(self, eid, inpcode):
-        self._value2member_map_[eid] = self
-        self._member_map_[inpcode] = self
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        v2mm = getattr(self, "_value2member_map_")
+        mmap = getattr(self, "_member_map_")
+        v2mm[eid] = self
+        mmap[inpcode] = self
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __int__(self):
-        return self.value[0]
+        value = super().value
+        return value[0]
 
     def __str__(self):
-        return self.value[1]
+        value = super().value
+        return value[1]
 
 
 class ControlType(enum.Enum):
@@ -967,10 +878,11 @@ class ControlType(enum.Enum):
     TimeOfDay = 3
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, "_member_map_")
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
@@ -995,23 +907,24 @@ class LinkTankStatus(enum.Enum):
     ====================  ==================================================================
 
     """
-    
-    XHead = 0  
-    TempClosed = 1  
-    Closed = 2 
-    Open = 3 
-    Active = 4  
-    XFlow = 5  
-    XFCV = 6  
-    XPressure = 7  
-    Filling = 8  
-    Emptying = 9 
+
+    XHead = 0
+    TempClosed = 1
+    Closed = 2
+    Open = 3
+    Active = 4
+    XFlow = 5
+    XFCV = 6
+    XPressure = 7
+    Filling = 8
+    Emptying = 9
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, "_member_map_")
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
@@ -1030,6 +943,7 @@ class MixType(enum.Enum):
     ===============  ==================================================================
 
     """
+
     Mix1 = 0
     Mix2 = 1
     FIFO = 2
@@ -1038,10 +952,11 @@ class MixType(enum.Enum):
     TwoComp = 1
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, "_member_map_")
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
@@ -1049,6 +964,7 @@ def __str__(self):
 
 class ResultType(enum.Enum):
     """Extended period simulation results type"""
+
     demand = 1
     head = 2
     pressure = 3
@@ -1086,7 +1002,7 @@ def is_qual(self):
     @property
     def is_hyd(self):
         """Is related to hydraulics"""
-        if self.value in [1,2,3,5,6,7,12]:
+        if self.value in [1, 2, 3, 5, 6, 7, 12]:
             return True
         return False
 
@@ -1119,151 +1035,327 @@ class EN(enum.IntEnum):
 
 
     """
+
     # Node parameters
-    ELEVATION    = 0
-    BASEDEMAND   = 1
-    PATTERN      = 2
-    EMITTER      = 3
-    INITQUAL     = 4
-    SOURCEQUAL   = 5
-    SOURCEPAT    = 6
-    SOURCETYPE   = 7
-    TANKLEVEL    = 8
-    DEMAND       = 9
-    HEAD         = 10
-    PRESSURE     = 11
-    QUALITY      = 12
-    SOURCEMASS   = 13
-    INITVOLUME   = 14
-    MIXMODEL     = 15
-    MIXZONEVOL   = 16
-    TANKDIAM     = 17
-    MINVOLUME    = 18
-    VOLCURVE     = 19
-    MINLEVEL     = 20
-    MAXLEVEL     = 21
-    MIXFRACTION  = 22
-    TANK_KBULK   = 23
-    TANKVOLUME   = 24
-    MAXVOLUME    = 25
+    ELEVATION = 0
+    BASEDEMAND = 1
+    PATTERN = 2
+    EMITTER = 3
+    INITQUAL = 4
+    SOURCEQUAL = 5
+    SOURCEPAT = 6
+    SOURCETYPE = 7
+    TANKLEVEL = 8
+    DEMAND = 9
+    HEAD = 10
+    PRESSURE = 11
+    QUALITY = 12
+    SOURCEMASS = 13
+    INITVOLUME = 14
+    MIXMODEL = 15
+    MIXZONEVOL = 16
+    TANKDIAM = 17
+    MINVOLUME = 18
+    VOLCURVE = 19
+    MINLEVEL = 20
+    MAXLEVEL = 21
+    MIXFRACTION = 22
+    TANK_KBULK = 23
+    TANKVOLUME = 24
+    MAXVOLUME = 25
 
     # Link parameters
-    DIAMETER     = 0
-    LENGTH       = 1
-    ROUGHNESS    = 2
-    MINORLOSS    = 3
-    INITSTATUS   = 4
-    INITSETTING  = 5
-    KBULK        = 6
-    KWALL        = 7
-    FLOW         = 8
-    VELOCITY     = 9
-    HEADLOSS     = 10
-    STATUS       = 11
-    SETTING      = 12
-    ENERGY       = 13
-    LINKQUAL     = 14
-    LINKPATTERN  = 15
+    DIAMETER = 0
+    LENGTH = 1
+    ROUGHNESS = 2
+    MINORLOSS = 3
+    INITSTATUS = 4
+    INITSETTING = 5
+    KBULK = 6
+    KWALL = 7
+    FLOW = 8
+    VELOCITY = 9
+    HEADLOSS = 10
+    STATUS = 11
+    SETTING = 12
+    ENERGY = 13
+    LINKQUAL = 14
+    LINKPATTERN = 15
 
     # Time parameters
-    DURATION     = 0
-    HYDSTEP      = 1
-    QUALSTEP     = 2
-    PATTERNSTEP  = 3
+    DURATION = 0
+    HYDSTEP = 1
+    QUALSTEP = 2
+    PATTERNSTEP = 3
     PATTERNSTART = 4
-    REPORTSTEP   = 5
-    REPORTSTART  = 6
-    RULESTEP     = 7
-    STATISTIC    = 8
-    PERIODS      = 9
-    STARTTIME    = 10
-    HTIME        = 11
-    HALTFLAG     = 12
-    NEXTEVENT    = 13
+    REPORTSTEP = 5
+    REPORTSTART = 6
+    RULESTEP = 7
+    STATISTIC = 8
+    PERIODS = 9
+    STARTTIME = 10
+    HTIME = 11
+    HALTFLAG = 12
+    NEXTEVENT = 13
 
     # Solver parameters
-    ITERATIONS   = 0
-    RELATIVEERROR= 1
+    ITERATIONS = 0
+    RELATIVEERROR = 1
 
     # Count parameters
-    NODECOUNT    = 0
-    TANKCOUNT    = 1
-    LINKCOUNT    = 2
-    PATCOUNT     = 3
-    CURVECOUNT   = 4
+    NODECOUNT = 0
+    TANKCOUNT = 1
+    LINKCOUNT = 2
+    PATCOUNT = 3
+    CURVECOUNT = 4
     CONTROLCOUNT = 5
 
     # Node Types
-    JUNCTION     = 0
-    RESERVOIR    = 1
-    TANK         = 2
+    JUNCTION = 0
+    RESERVOIR = 1
+    TANK = 2
 
     # Link Types
-    CVPIPE       = 0
-    PIPE         = 1
-    PUMP         = 2
-    PRV          = 3
-    PSV          = 4
-    PBV          = 5
-    FCV          = 6
-    TCV          = 7
-    GPV          = 8
+    CVPIPE = 0
+    PIPE = 1
+    PUMP = 2
+    PRV = 3
+    PSV = 4
+    PBV = 5
+    FCV = 6
+    TCV = 7
+    GPV = 8
 
     # Quality Types
-    NONE         = 0
-    CHEM         = 1
-    AGE          = 2
-    TRACE        = 3
+    NONE = 0
+    CHEM = 1
+    AGE = 2
+    TRACE = 3
 
     # Source quality types
-    CONCEN       = 0
-    MASS         = 1
-    SETPOINT     = 2
-    FLOWPACED    = 3
+    CONCEN = 0
+    MASS = 1
+    SETPOINT = 2
+    FLOWPACED = 3
 
     # Flow units parameter
-    CFS          = 0
-    GPM          = 1
-    MGD          = 2
-    IMGD         = 3
-    AFD          = 4
-    LPS          = 5
-    LPM          = 6
-    MLD          = 7
-    CMH          = 8
-    CMD          = 9
+    CFS = 0
+    GPM = 1
+    MGD = 2
+    IMGD = 3
+    AFD = 4
+    LPS = 5
+    LPM = 6
+    MLD = 7
+    CMH = 8
+    CMD = 9
 
     # Miscelaneous parameters
-    TRIALS       = 0
-    ACCURACY     = 1
-    TOLERANCE    = 2
-    EMITEXPON    = 3
-    DEMANDMULT   = 4
+    TRIALS = 0
+    ACCURACY = 1
+    TOLERANCE = 2
+    EMITEXPON = 3
+    DEMANDMULT = 4
 
     # Control types
-    LOWLEVEL     = 0
-    HILEVEL      = 1
-    TIMER        = 2
-    TIMEOFDAY    = 3
+    LOWLEVEL = 0
+    HILEVEL = 1
+    TIMER = 2
+    TIMEOFDAY = 3
 
     # Statistic Types
-    AVERAGE      = 1
-    MINIMUM      = 2
-    MAXIMUM      = 3
-    RANGE        = 4
+    AVERAGE = 1
+    MINIMUM = 2
+    MAXIMUM = 3
+    RANGE = 4
 
     # Tank mixing parameters
-    MIX1         = 0
-    MIX2         = 1
-    FIFO         = 2
-    LIFO         = 3
+    MIX1 = 0
+    MIX2 = 1
+    FIFO = 2
+    LIFO = 3
 
     # Hydraulic solver / file parameters
-    NOSAVE       = 0
-    SAVE         = 1
-    INITFLOW     = 10
+    NOSAVE = 0
+    SAVE = 1
+    INITFLOW = 10
 
     # Pump behavior Types
-    CONST_HP     = 0
-    POWER_FUNC   = 1
-    CUSTOM       = 2
+    CONST_HP = 0
+    POWER_FUNC = 1
+    CUSTOM = 2
+
+
+def to_si(
+    from_units: FlowUnits,
+    data,
+    param,
+    mass_units: MassUnits = MassUnits.mg,
+    pressure_units: PressureUnits = None,
+    darcy_weisbach: bool = False,
+    reaction_order: int = 0,
+):
+    """Convert an EPANET parameter from internal to SI standard units.
+
+    .. note:: 
+
+        See the `Units <../units.html>`__ page for details on the units for each :class:`~HydParam` or :class:`~QualParam`.
+        Other than for flows, most parameters only have one US and one metric unit that is used by EPANET.
+        For example, even though flow might be specified in gallons, volumes would be specified in cubic feet
+        for any US/English flow rates, and in cubic meters for all metric flow units; e.g., never liters 
+        for volumes even when flow is declared as LPS.
+
+        Rememeber that internally, WNTR is **always** expecting the values for a parameter to be in true SI
+        units -- meters, kilograms, and seconds -- unless explicitly stated otherwise (e.g., hours for control times).
+
+
+    Parameters
+    ----------
+    from_units : :class:`~FlowUnits`
+        The EPANET flow units (and therefore units system) to use for conversion
+    data : float, array-like, dict
+        The data to be converted
+    param : :class:`~HydParam` or :class:`~QualParam`
+        The parameter type for the data
+    mass_units : :class:`~MassUnits`, optional
+        The EPANET mass units (mg or ug internal to EPANET)
+    pressure_units : :class:`~PressureUnits`, optional
+        The EPANET pressure units being used (based on `flow_units`, normally)
+    darcy_weisbach : bool, optional
+        For roughness coefficients, is this used in a Darcy-Weisbach formula?
+    reaction_order : int, optional
+        For reaction coefficients, what is the reaction order?
+
+    Returns
+    -------
+    float, array-like, or dict
+        The data values convert into SI standard units
+
+
+    Examples
+    --------
+
+    First, we convert an array of flows from GPM to cubic meters per second (the SI units).
+
+    >>> from wntr.epanet.util import *
+    >>> flow_si = to_si(FlowUnits.GPM, [0.1, 1.0, 4.3], HydParam.Flow)
+    >>> print(flow_si)
+    [6.309019640000001e-06, 6.30901964e-05, 0.00027128784452]
+
+    Next, we show how to convert the quality parameter from the EPANET units of mg/L to kg/m3.
+    If that is not the mass units you prefer, it is possible to change them to ug/L, g/L, or kg/L,
+    as shown in the second example.
+
+    >>> to_si(FlowUnits.GPM, 4.6, QualParam.Quality)
+    0.0046
+    >>> to_si(FlowUnits.GPM, 4.6, QualParam.Quality, mass_units=MassUnits.ug)
+    4.599999999999999e-06
+
+    It is also possible to convert a dictionary of values.
+
+    >>> to_si(FlowUnits.GPM, {'node1': 5.6, 'node2': 1.2}, HydParam.Pressure)
+    {'node1': 3.9392568659127623, 'node2': 0.8441264712670206}
+
+    For certain coefficients, there are flags that will change how the conversion occurs. For example,
+    reaction coefficients depend on the reaction order.
+
+    >>> to_si(FlowUnits.GPM, 0.45, QualParam.BulkReactionCoeff, reaction_order=0)
+    0.45
+    >>> to_si(FlowUnits.GPM, 0.45, QualParam.BulkReactionCoeff, reaction_order=1)
+    5.208333333333333e-06
+
+
+    """
+    if isinstance(param, HydParam):
+        return param._to_si(from_units, data, darcy_weisbach)
+    elif isinstance(param, QualParam):
+        return param._to_si(from_units, data, mass_units, reaction_order)
+    else:
+        raise RuntimeError("Invalid parameter: %s" % param)
+
+
+def from_si(
+    to_units: FlowUnits,
+    data,
+    param,
+    mass_units: MassUnits = MassUnits.mg,
+    pressure_units: PressureUnits = None,
+    darcy_weisbach: bool = False,
+    reaction_order: int = 0,
+):
+    """Convert an EPANET parameter from SI standard units back to internal units.
+
+    .. note:: 
+
+        See the `Units <../units.html>`__ page for details on the units for each :class:`~HydParam` or :class:`~QualParam`.
+        Other than for flows, most parameters only have one US and one metric unit that is used by EPANET.
+        For example, even though flow might be specified in gallons, volumes would be specified in cubic feet
+        for any US/English flow rates, and in cubic meters for all metric flow units; e.g., never liters 
+        for volumes even when flow is declared as LPS.
+
+        Rememeber that internally, WNTR is **always** expecting the values for a parameter to be in true SI
+        units -- meters, kilograms, and seconds -- unless explicitly stated otherwise (e.g., hours for control times).
+
+
+    Parameters
+    ----------
+    to_units : :class:`~FlowUnits`
+        The EPANET flow units (and therefore units system) to use for conversion
+    data : float, array-like, dict
+        The data to be converted
+    param : :class:`~HydParam` or :class:`~QualParam`
+        The parameter type for the data
+    mass_units : :class:`~MassUnits`, optional
+        The EPANET mass units (mg or ug internal to EPANET)
+    pressure_units : :class:`~PressureUnits`, optional
+        The EPANET pressure units being used (based on `flow_units`, normally)
+    darcy_weisbach : bool, optional
+        For roughness coefficients, is this used in a Darcy-Weisbach formula?
+    reaction_order : int, optional
+        For reaction coefficients, what is the reaction order?
+
+    Returns
+    -------
+    float, array-like, or dict
+        The data values converted into EPANET internal units
+
+
+    Examples
+    --------
+
+    First, we convert an array of flows from SI (cubic meters per second) to GPM.
+
+    >>> from wntr.epanet.util import *
+    >>> flow_us = from_si(FlowUnits.GPM, [6.309019640000001e-06, 6.30901964e-05, 0.00027128784452], HydParam.Flow)
+    >>> print(flow_us)
+    [0.1, 1.0, 4.3]
+
+    Next, we show how to convert the quality parameter from kg/m3 to mg/L and then to ug/L.
+    
+    >>> from_si(FlowUnits.GPM, 0.0046, QualParam.Quality)
+    4.6
+    >>> from_si(FlowUnits.GPM, 0.0046, QualParam.Quality, mass_units=MassUnits.ug)
+    4600.0
+
+    It is also possible to convert a dictionary of values.
+
+    >>> from_si(FlowUnits.GPM, {'node1': 3.9392568659127623, 'node2': 0.8441264712670206}, HydParam.Pressure)
+    {'node1': 5.6, 'node2': 1.2}
+    
+    Finally, an example showing the conversion of 1000 cubic meters per second into the different flow units.
+
+    >>> from_si(FlowUnits.GPM, 1000.0, HydParam.Flow)  # to gallons per minute
+    15850323.141488904
+    >>> from_si(FlowUnits.LPS, 1000.0, HydParam.Flow)  # to liters per second
+    1000000.0
+    >>> from_si(FlowUnits.MGD, 1000.0, HydParam.Flow)  # to million gallons per day
+    22824.465323744018
+
+
+    """
+    if isinstance(param, HydParam):
+        return param._from_si(to_units, data, darcy_weisbach)
+    elif isinstance(param, QualParam):
+        return param._from_si(to_units, data, mass_units, reaction_order)
+    else:
+        raise RuntimeError("Invalid parameter: %s" % param)
diff --git a/wntr/graphics/network.py b/wntr/graphics/network.py
index ebc1c1e8a..085f1d152 100644
--- a/wntr/graphics/network.py
+++ b/wntr/graphics/network.py
@@ -159,7 +159,7 @@ def plot_network(wn, node_attribute=None, link_attribute=None, title=None,
             add_node_colorbar = False
         
         if node_cmap is None:
-            node_cmap = plt.cm.Spectral_r
+            node_cmap = plt.get_cmap('Spectral_r')
         elif isinstance(node_cmap, list):
             if len(node_cmap) == 1:
                 node_cmap = node_cmap*2
@@ -181,7 +181,7 @@ def plot_network(wn, node_attribute=None, link_attribute=None, title=None,
             add_link_colorbar = False
 
         if link_cmap is None:
-            link_cmap = plt.cm.Spectral_r
+            link_cmap = plt.get_cmap('Spectral_r')
         elif isinstance(link_cmap, list):
             if len(link_cmap) == 1:
                 link_cmap = link_cmap*2
diff --git a/wntr/metrics/__init__.py b/wntr/metrics/__init__.py
index f41d1315c..93021532e 100644
--- a/wntr/metrics/__init__.py
+++ b/wntr/metrics/__init__.py
@@ -11,5 +11,5 @@
 from wntr.metrics.water_security import mass_contaminant_consumed, \
     volume_contaminant_consumed, extent_contaminant
 from wntr.metrics.economic import annual_network_cost, annual_ghg_emissions, \
-    pump_energy, pump_cost
+    pump_power, pump_energy, pump_cost
 from wntr.metrics.misc import query, population, population_impacted
diff --git a/wntr/metrics/economic.py b/wntr/metrics/economic.py
index d9e5f8cc7..7c9f9b802 100644
--- a/wntr/metrics/economic.py
+++ b/wntr/metrics/economic.py
@@ -251,10 +251,9 @@ def annual_ghg_emissions(wn, pipe_ghg=None):
        
     return network_ghg    
 
-
-def pump_energy(flowrate, head, wn):
+def pump_power(flowrate, head, wn):
     """
-    Compute the pump energy over time.
+    Compute pump power.
     
     The computation uses pump flow rate, node head (used to compute headloss at
     each pump), and pump efficiency. Pump efficiency is defined in
@@ -279,7 +278,7 @@ def pump_energy(flowrate, head, wn):
 
     Returns
     -------
-    A DataFrame that contains pump energy in J (index = times, columns = pump names).
+    A DataFrame that contains pump power in W (index = times, columns = pump names).
     """
     
     pumps = wn.pump_name_list
@@ -299,15 +298,50 @@ def pump_energy(flowrate, head, wn):
             efficiency_dict[pump_name] = [wn.options.energy.global_efficiency/100.0 for i in time]
         else:
             raise NotImplementedError('WNTR does not support pump efficiency curves yet.')
-            curve = wn.get_curve(pump.efficiency)
-            x = [point[0] for point in curve.points]
-            y = [point[1]/100.0 for point in curve.points]
-            interp = scipy.interpolate.interp1d(x, y, kind='linear')
-            efficiency_dict[pump_name] = interp(np.array(flowrate.loc[:, pump_name]))
+            # TODO: WNTR does not support pump efficiency curves yet
+            # curve = wn.get_curve(pump.efficiency)
+            # x = [point[0] for point in curve.points]
+            # y = [point[1]/100.0 for point in curve.points]
+            # interp = scipy.interpolate.interp1d(x, y, kind='linear')
+            # efficiency_dict[pump_name] = interp(np.array(flowrate.loc[:, pump_name]))
 
     efficiency = pd.DataFrame(data=efficiency_dict, index=time, columns=pumps)
 
     power = 1000.0 * 9.81 * headloss * flowrate / efficiency # Watts = J/s
+
+    return power
+
+def pump_energy(flowrate, head, wn):
+    """
+    Compute the pump energy over time.
+    
+    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.options.energy.global_efficiency = 75 # This means 75% or 0.75
+
+    Parameters
+    ----------
+    flowrate : pandas DataFrame
+        A pandas Dataframe containing pump flowrates 
+        (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 
+        define energy efficiency.
+
+    Returns
+    -------
+    A DataFrame that contains pump energy in J (index = times, columns = pump names).
+    """
+    
+    power = pump_power(flowrate, head, wn) # Watts = J/s
     energy = power * wn.options.time.report_timestep # J = Ws
     
     return energy
diff --git a/wntr/metrics/hydraulic.py b/wntr/metrics/hydraulic.py
index 00cc220c3..990596f6f 100644
--- a/wntr/metrics/hydraulic.py
+++ b/wntr/metrics/hydraulic.py
@@ -119,7 +119,7 @@ def _lcml(*list):
   return reduce(_lcm, *list)
 
 def water_service_availability(expected_demand, demand):
-    """
+    r"""
     Compute water service availability (WSA) at junctions, defined as follows:
         
     .. math:: WSA = \dfrac{demand}{expected\_demand}
diff --git a/wntr/metrics/misc.py b/wntr/metrics/misc.py
index 8fe363359..7a5d837b8 100644
--- a/wntr/metrics/misc.py
+++ b/wntr/metrics/misc.py
@@ -46,7 +46,7 @@ def query(arg1, operation, arg2):
     return mask
 
 def population(wn, R=0.00000876157):
-    """
+    r"""
     Compute population per node, rounded to the nearest integer [USEPA15]_.
 
     .. math:: pop=\dfrac{Average\ expected\ demand}{R}
diff --git a/wntr/morph/link.py b/wntr/morph/link.py
index fa5cd7e4e..5a6b3d472 100644
--- a/wntr/morph/link.py
+++ b/wntr/morph/link.py
@@ -198,17 +198,17 @@ def _split_or_break_pipe(wn, pipe_name_to_split, new_pipe_name,
         # add new pipe and change original length
         wn2.add_pipe(new_pipe_name, j1, end_node.name,
                      original_length*(1-split_at_point), pipe.diameter, 
-                     pipe.roughness, pipe.minor_loss, pipe.status, pipe.cv)
+                     pipe.roughness, pipe.minor_loss, pipe.status, pipe.check_valve)
         pipe.length = original_length*split_at_point
     else: # add pipe at start
         pipe.start_node = wn2.get_node(j0) 
         # add new pipe and change original length
         wn2.add_pipe(new_pipe_name, start_node.name, j1, 
                      original_length*split_at_point, pipe.diameter, 
-                     pipe.roughness, pipe.minor_loss, pipe.status, pipe.cv)
+                     pipe.roughness, pipe.minor_loss, pipe.status, pipe.check_valve)
         pipe.length = original_length*(1-split_at_point)
         
-    if pipe.cv:
+    if pipe.check_valve:
         logger.warn('You are splitting a pipe with a check valve. The new \
                     pipe will not have a check valve.')
     
diff --git a/wntr/morph/skel.py b/wntr/morph/skel.py
index 8f1996d5e..a310ed3f7 100644
--- a/wntr/morph/skel.py
+++ b/wntr/morph/skel.py
@@ -286,7 +286,7 @@ def series_pipe_merge(self, pipe_threshold):
                              diameter=props['diameter'], 
                              roughness=props['roughness'], 
                              minor_loss=props['minorloss'],
-                             status=props['status']) 
+                             initial_status=props['status']) 
             self.G.add_edge(neigh_junc_name0, 
                             neigh_junc_name1, 
                             dominant_pipe.name)
@@ -346,7 +346,7 @@ def parallel_pipe_merge(self, pipe_threshold):
                                      diameter=props['diameter'], 
                                      roughness=props['roughness'], 
                                      minor_loss=props['minorloss'],
-                                     status=props['status']) 
+                                     initial_status=props['status']) 
                     self.G.add_edge(dominant_pipe.start_node_name, 
                                     dominant_pipe.end_node_name, 
                                     dominant_pipe.name)
diff --git a/wntr/network/base.py b/wntr/network/base.py
index ec33ab34a..81f35de25 100644
--- a/wntr/network/base.py
+++ b/wntr/network/base.py
@@ -408,12 +408,9 @@ def initial_status(self):
     @initial_status.setter
     def initial_status(self, status):
         if not isinstance(status, LinkStatus):
-            if isinstance (status, (int, float)):
-                status = LinkStatus(int(status))
-            elif isinstance(status, str):
-                status = LinkStatus[status]
-            else: 
-                raise ValueError('initial_status must be a str, integer, or LinkStatus')
+            if isinstance(status, int): status = LinkStatus(status)
+            elif isinstance(status, str): status = LinkStatus[status]
+            else: status = LinkStatus(int(status))
         self._initial_status = status
         
     @property
@@ -733,10 +730,11 @@ class NodeType(enum.IntEnum):
     Tank = 2  #: node is a tank
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, '_member_map_')
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
@@ -779,10 +777,11 @@ class LinkType(enum.IntEnum):
     Valve = 9  #: a valve of any type
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, '_member_map_')
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
@@ -819,10 +818,11 @@ class LinkStatus(enum.IntEnum):
     CV = 3  #: pipe has a check valve
 
     def __init__(self, val):
-        if self.name != self.name.upper():
-            self._member_map_[self.name.upper()] = self
-        if self.name != self.name.lower():
-            self._member_map_[self.name.lower()] = self
+        mmap = getattr(self, '_member_map_')
+        if self.name != str(self.name).upper():
+            mmap[str(self.name).upper()] = self
+        if self.name != str(self.name).lower():
+            mmap[str(self.name).lower()] = self
 
     def __str__(self):
         return self.name
diff --git a/wntr/network/controls.py b/wntr/network/controls.py
index 61c5f1158..384c2a348 100644
--- a/wntr/network/controls.py
+++ b/wntr/network/controls.py
@@ -34,7 +34,8 @@
 from .elements import LinkStatus
 import abc
 from wntr.utils.ordered_set import OrderedSet
-from collections import OrderedDict, Iterable
+from collections import OrderedDict
+from collections.abc import Iterable
 from .elements import Tank, Junction, Valve, Pump, Reservoir, Pipe
 from wntr.utils.doc_inheritor import DocInheritor
 import warnings
@@ -152,7 +153,8 @@ def __str__(self):
     @property
     def func(self):
         """The function call to use for this comparison"""
-        return self.value[1]
+        value = getattr(self, '_value_')
+        return value[1]
     __call__ = func
 
     @property
@@ -414,6 +416,8 @@ class TimeOfDayCondition(ControlCondition):
         the time specified.
     first_day : float, default=0
         Start rule on day `first_day`, with the first day of simulation as day 0
+
+    TODO:  WE ARE NOT TESTING THIS!!!!
     """
     def __init__(self, model, relation, threshold, repeat=True, first_day=0):
         self._model = model
@@ -428,7 +432,7 @@ def __init__(self, model, relation, threshold, repeat=True, first_day=0):
         self._first_day = first_day
         self._repeat = repeat
         self._backtrack = 0
-        if model is not None and not self._repeat and self._threshold < model._start_clocktime and first_day < 1:
+        if model is not None and not self._repeat and self._threshold < model.options.time.start_clocktime and first_day < 1:
             self._first_day = 1
 
     def _compare(self, other):
@@ -1619,6 +1623,7 @@ class BaseControlAction(six.with_metaclass(abc.ABCMeta, Subject)):
 
     def __init__(self):
         super(BaseControlAction, self).__init__()
+        self._value = None
 
     @abc.abstractmethod
     def run_control_action(self):
@@ -1817,6 +1822,14 @@ class ControlBase(six.with_metaclass(abc.ABCMeta, object)):
     reaches 6 AM, the ControlAction would be "turn the pump on", and the ControlCondition would be "when the simulation
     reaches 6 AM".
     """
+
+    def __init__(self):
+        super().__init__()
+        self._control_type = None
+        self._condition = None
+        self._priority = None
+
+
     @abc.abstractmethod
     def is_control_action_required(self):
         """
@@ -1968,7 +1981,7 @@ def epanet_control_type(self):
         control_type: _ControlType
         """
         return self._control_type
-
+    
     def requires(self):
         req = self._condition.requires()
         for action in self._then_actions:
@@ -2072,7 +2085,7 @@ def __init__(self, condition, then_action, priority=ControlPriority.medium, name
             self._control_type = _ControlType.presolve
         else:
             self._control_type = _ControlType.postsolve
-
+        
     @classmethod
     def _time_control(cls, wnm, run_at_time, time_flag, daily_flag, control_action, name=None):
         """
diff --git a/wntr/network/elements.py b/wntr/network/elements.py
index db3a14a37..1754e7a74 100644
--- a/wntr/network/elements.py
+++ b/wntr/network/elements.py
@@ -854,7 +854,7 @@ def __init__(self, name, start_node_name, end_node_name, wn):
     def __repr__(self):
         return "<Pipe '{}' from '{}' to '{}', length={}, diameter={}, roughness={}, minor_loss={}, check_valve={}, status={}>".format(self._link_name,
                        self.start_node, self.end_node, self.length, self.diameter, 
-                       self.roughness, self.minor_loss, self.cv, str(self.status))
+                       self.roughness, self.minor_loss, self.check_valve, str(self.status))
     
     def _compare(self, other):
         if not super(Pipe, self)._compare(other):
@@ -863,7 +863,7 @@ def _compare(self, other):
            abs(self.diameter      - other.diameter)<1e-9   and \
            abs(self.roughness     - other.roughness)<1e-9  and \
            abs(self.minor_loss    - other.minor_loss)<1e-9 and \
-           self.cv               == other.cv                and \
+           self.check_valve  == other.check_valve                and \
            self.bulk_coeff   == other.bulk_coeff    and \
            self.wall_coeff   == other.wall_coeff:
             return True
@@ -1449,9 +1449,9 @@ def power(self):
         """float : the fixed power value"""
         return self._base_power
     @power.setter
-    def power(self, kW):
+    def power(self, value):
         self._curve_reg.remove_usage(self._pump_curve_name, (self._link_name, 'Pump'))
-        self._base_power = kW
+        self._base_power = value
 
 
 class Valve(Link):
diff --git a/wntr/network/model.py b/wntr/network/model.py
index c7a9dac54..d212ce161 100644
--- a/wntr/network/model.py
+++ b/wntr/network/model.py
@@ -15,31 +15,35 @@
 
 """
 import logging
-import six
-
 import sys
+from collections import OrderedDict
 from collections.abc import MutableSequence
 
-import numpy as np
 import networkx as nx
+import numpy as np
 import pandas as pd
-
-from .options import Options
-from .base import Link, Registry, LinkStatus, AbstractModel
-from .elements import Junction, Reservoir, Tank
-from .elements import Pipe, Pump, HeadPump, PowerPump
-from .elements import Valve, PRValve, PSValve, PBValve, TCValve, FCValve, GPValve
-from .elements import Pattern, TimeSeries, Demands, Curve, Source
-from .controls import ControlPriority, _ControlType, TimeOfDayCondition, SimTimeCondition, ValueCondition, \
-    TankLevelCondition, RelativeCondition, OrCondition, AndCondition, _CloseCVCondition, _OpenCVCondition, \
-    _ClosePowerPumpCondition, _OpenPowerPumpCondition, _CloseHeadPumpCondition, _OpenHeadPumpCondition, \
-    _ClosePRVCondition, _OpenPRVCondition, _ActivePRVCondition, _ClosePSVCondition, _OpenPSVCondition, \
-    _ActivePSVCondition, _OpenFCVCondition, _ActiveFCVCondition, ControlAction, _InternalControlAction, Control, \
-    ControlManager, Comparison, Rule
-from collections import OrderedDict
+import six
+import wntr.epanet
 from wntr.utils.ordered_set import OrderedSet
 
-import wntr.epanet
+from .base import AbstractModel, Link, LinkStatus, Registry
+from .controls import (AndCondition, Comparison, Control, ControlAction,
+                       ControlManager, ControlPriority, OrCondition,
+                       RelativeCondition, Rule, SimTimeCondition,
+                       TankLevelCondition, TimeOfDayCondition, ValueCondition,
+                       _ActiveFCVCondition, _ActivePRVCondition,
+                       _ActivePSVCondition, _CloseCVCondition,
+                       _CloseHeadPumpCondition, _ClosePowerPumpCondition,
+                       _ClosePRVCondition, _ClosePSVCondition, _ControlType,
+                       _InternalControlAction, _OpenCVCondition,
+                       _OpenFCVCondition, _OpenHeadPumpCondition,
+                       _OpenPowerPumpCondition, _OpenPRVCondition,
+                       _OpenPSVCondition)
+from .elements import (Curve, Demands, FCValve, GPValve, HeadPump, Junction,
+                       Pattern, PBValve, Pipe, PowerPump, PRValve, PSValve,
+                       Pump, Reservoir, Source, Tank, TCValve, TimeSeries,
+                       Valve)
+from .options import Options
 
 logger = logging.getLogger(__name__)
 
@@ -347,7 +351,7 @@ def add_junction(self, name, base_demand=0.0, demand_pattern=None,
         base_demand : float
             Base demand at the junction.
         demand_pattern : string or Pattern
-            Name of the demand pattern or the actual Pattern object
+            Name of the demand pattern or the Pattern object
         elevation : float
             Elevation of the junction.
         coordinates : tuple of floats
@@ -369,7 +373,7 @@ def add_tank(self, name, elevation=0.0, init_level=3.048,
         name : string
             Name of the tank.
         elevation : float
-            Elevation at the Tank.
+            Elevation at the tank.
         init_level : float
             Initial tank level.
         min_level : float
@@ -380,18 +384,13 @@ def add_tank(self, name, elevation=0.0, init_level=3.048,
             Tank diameter.
         min_vol : float
             Minimum tank volume.
-        vol_curve : str
-            Name of a volume curve, optional
+        vol_curve : str, optional
+            Name of a volume curve
         overflow : bool
-            Does this tank overflow (EpanetSimulator only)
+           Overflow indicator (Always False for the WNTRSimulator)
         coordinates : tuple of floats, optional
             X-Y coordinates of the node location.
             
-        Raises
-        ------
-        ValueError
-            If `init_level` greater than `max_level` or less than `min_level`
-            
         """
         self._node_reg.add_tank(name, elevation, init_level, min_level, 
                                 max_level, diameter, min_vol, vol_curve, 
@@ -407,8 +406,8 @@ def add_reservoir(self, name, base_head=0.0, head_pattern=None, coordinates=None
             Name of the reservoir.
         base_head : float, optional
             Base head at the reservoir.
-        head_pattern : string
-            Name of the head pattern (optional)
+        head_pattern : string, optional
+            Name of the head pattern.
         coordinates : tuple of floats, optional
             X-Y coordinates of the node location.
         
@@ -416,8 +415,8 @@ def add_reservoir(self, name, base_head=0.0, head_pattern=None, coordinates=None
         self._node_reg.add_reservoir(name, base_head, head_pattern, coordinates)
 
     def add_pipe(self, name, start_node_name, end_node_name, length=304.8,
-                 diameter=0.3048, roughness=100, minor_loss=0.0, status='OPEN', 
-                 check_valve_flag=False):
+                 diameter=0.3048, roughness=100, minor_loss=0.0, initial_status='OPEN', 
+                 check_valve=False):
         """
         Adds a pipe to the water network model
 
@@ -437,22 +436,22 @@ def add_pipe(self, name, start_node_name, end_node_name, length=304.8,
             Pipe roughness coefficient.
         minor_loss : float, optional
             Pipe minor loss coefficient.
-        status : string, optional
-            Pipe status. Options are 'Open' or 'Closed'.
-        check_valve_flag : bool, optional
+        initial_status : string or LinkStatus, optional
+            Pipe initial status. Options are 'OPEN' or 'CLOSED'.
+        check_valve : bool, optional
             True if the pipe has a check valve.
             False if the pipe does not have a check valve.
         
         """
         self._link_reg.add_pipe(name, start_node_name, end_node_name, length, 
-                                diameter, roughness, minor_loss, status, 
-                                check_valve_flag)
-        if check_valve_flag:
+                                diameter, roughness, minor_loss, initial_status, 
+                                check_valve)
+        if check_valve:
             self._check_valves.append(name)
 
 
     def add_pump(self, name, start_node_name, end_node_name, pump_type='POWER',
-                 pump_parameter=50.0, speed=1.0, pattern=None):
+                 pump_parameter=50.0, speed=1.0, pattern=None, initial_status='OPEN'):
         """
         Adds a pump to the water network model
 
@@ -466,19 +465,23 @@ def add_pump(self, name, start_node_name, end_node_name, pump_type='POWER',
              Name of the end node.
         pump_type : string, optional
             Type of information provided for a pump. Options are 'POWER' or 'HEAD'.
-        pump_parameter : float or str object
-            Float value of power in KW. Head curve name.
+        pump_parameter : float or string
+            For a POWER pump, the pump power.
+            For a HEAD pump, the head curve name.
         speed: float
             Relative speed setting (1.0 is normal speed)
-        pattern: str
-            ID of pattern for speed setting
+        pattern: string
+            Name of the speed pattern
+        initial_status : string or LinkStatus
+            Pump initial status. Options are 'OPEN' or 'CLOSED'.
         
         """
         self._link_reg.add_pump(name, start_node_name, end_node_name, pump_type, 
-                                pump_parameter, speed, pattern)
+                                pump_parameter, speed, pattern, initial_status)
     
     def add_valve(self, name, start_node_name, end_node_name,
-                 diameter=0.3048, valve_type='PRV', minor_loss=0.0, setting=0.0):
+                 diameter=0.3048, valve_type='PRV', minor_loss=0.0, 
+                 initial_setting=0.0, initial_status='ACTIVE'):
         """
         Adds a valve to the water network model
 
@@ -493,18 +496,20 @@ def add_valve(self, name, start_node_name, end_node_name,
         diameter : float, optional
             Diameter of the valve.
         valve_type : string, optional
-            Type of valve. Options are 'PRV', etc.
+            Type of valve. Options are 'PRV', 'PSV', 'PBV', 'FCV', 'TCV', and 'GPV'
         minor_loss : float, optional
             Pipe minor loss coefficient.
-        setting : float or string, optional
-            pressure setting for PRV, PSV, or PBV,
-            flow setting for FCV,
-            loss coefficient for TCV,
-            name of headloss curve for GPV.
-        
+        initial_setting : float or string, optional
+            Valve initial setting.
+            Pressure setting for PRV, PSV, or PBV. 
+            Flow setting for FCV. 
+            Loss coefficient for TCV.
+            Name of headloss curve for GPV.
+        initial_status: string or LinkStatus
+            Valve initial status. Options are 'OPEN',  'CLOSED', or 'ACTIVE'.
         """
         self._link_reg.add_valve(name, start_node_name, end_node_name, diameter, 
-                                 valve_type, minor_loss, setting)
+                                 valve_type, minor_loss, initial_setting, initial_status)
 
     def add_pattern(self, name, pattern=None):
         """
@@ -796,7 +801,7 @@ def _get_all_tank_controls(self):
                 link = self.get_link(link_name)
                 link_has_cv = False # flow leaving the tank (start node = tank)
                 if isinstance(link, Pipe):
-                    if link.cv:
+                    if link.check_valve:
                         if link.end_node_name == tank_name:
                             continue
                         else:
@@ -843,7 +848,7 @@ def _get_all_tank_controls(self):
                 link = self.get_link(link_name)
                 link_has_cv = False # flow entering the tank (end node = tank)
                 if isinstance(link, Pipe):
-                    if link.cv:
+                    if link.check_valve:
                         if link.start_node_name == tank_name:
                             continue
                         else:
@@ -1714,18 +1719,19 @@ def convert_controls_to_rules(self, priority=3):
                 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
         """
+        #### TODO: move reset conditions to /sim
         self.sim_time = 0.0
         self._prev_sim_time = None
 
         for name, node in self.nodes(Junction):
             node._head = None
             node._demand = None
+            node._pressure = None
             node._leak_demand = None
             node._leak_status = False
             node._is_isolated = False
@@ -1754,6 +1760,7 @@ def reset_initial_values(self):
 
         for name, link in self.links(Pump):
             link._user_status = link.initial_status
+            link._setting = link.initial_setting
             link._internal_status = LinkStatus.Active
             link._is_isolated = False
             link._flow = None
@@ -1772,7 +1779,7 @@ def reset_initial_values(self):
 
         for name, control in self.controls():
             control._reset()
-
+    
     def read_inpfile(self, filename):
         """
         Defines water network model components from an EPANET INP file
@@ -1829,7 +1836,7 @@ def write_inpfile(self, filename, units=None, version=2.2, force_coordinates=Fal
 class PatternRegistry(Registry):
     """A registry for patterns."""
     def _finalize_(self, model):
-        super(self.__class__, self)._finalize_(model)
+        super()._finalize_(model)
         self._pattern_reg = None
 
     class DefaultPattern(object):
@@ -1889,6 +1896,9 @@ def add_pattern(self, name, pattern=None):
         ValueError
             If adding a pattern with `name` that already exists.
         """
+        assert isinstance(name, str) and len(name) < 32 and name.find(' ') == -1, "name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(pattern, (list, np.ndarray, Pattern)), "pattern must be a list or Pattern"
+                          
         if not isinstance(pattern, Pattern):
             pattern = Pattern(name, multipliers=pattern, time_options=self._options.time)            
         else: #elif pattern.time_options is None:
@@ -1924,7 +1934,7 @@ def __init__(self, model):
         self._volume_curves = OrderedSet()
 
     def _finalize_(self, model):
-        super(self.__class__, self)._finalize_(model)
+        super()._finalize_(model)
         self._curve_reg = None
 
     def __setitem__(self, key, value):
@@ -1935,8 +1945,11 @@ def __setitem__(self, key, value):
             self.set_curve_type(key, value.curve_type)
     
     def set_curve_type(self, key, curve_type):
-        """WARNING -- does not check to make sure key is typed before assigning it - you could end up
-        with a curve that is used for more than one type, which would be really weird"""
+        """
+        Sets curve type.
+        
+        WARNING -- this does not check to make sure key is typed before assigning it - 
+        you could end up with a curve that is used for more than one type"""
         if curve_type is None:
             return
         curve_type = curve_type.upper()
@@ -1964,6 +1977,10 @@ def add_curve(self, name, curve_type, xy_tuples_list):
         xy_tuples_list : list of (x, y) tuples
             List of X-Y coordinate tuples on the curve.
         """
+        assert isinstance(name, str) and len(name) < 32 and name.find(' ') == -1, "name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(curve_type, (type(None), str)), "curve_type must be a string"
+        assert isinstance(xy_tuples_list, (list, np.ndarray)), "xy_tuples_list must be a list of (x,y) tuples"
+        
         curve = Curve(name, curve_type, xy_tuples_list)
         self[name] = curve
         
@@ -2088,7 +2105,7 @@ def volume_curve_names(self):
 class SourceRegistry(Registry):
     """A registry for sources."""
     def _finalize_(self, model):
-        super(self.__class__, self)._finalize_(model)
+        super()._finalize_(model)
         self._sources = None
 
     def __delitem__(self, key):
@@ -2118,7 +2135,7 @@ def __init__(self, model):
         self._tanks = OrderedSet()
     
     def _finalize_(self, model):
-        super(self.__class__, self)._finalize_(model)
+        super()._finalize_(model)
         self._node_reg = None
     
     def __setitem__(self, key, value):
@@ -2204,23 +2221,32 @@ def add_junction(self, name, base_demand=0.0, demand_pattern=None,
         base_demand : float
             Base demand at the junction.
         demand_pattern : string or Pattern
-            Name of the demand pattern or the actual Pattern object
+            Name of the demand pattern or the Pattern object
         elevation : float
             Elevation of the junction.
         coordinates : tuple of floats, optional
             X-Y coordinates of the node location.
         demand_category : str, optional
-            Add a category to the **base** demand
-        emitter_ceoff : float, optional
-            Add an emitter at this node with the specified coefficient
+            Category to the **base** demand
+        emitter_coeff : float, optional
+            Emitter coefficient
         initial_quality : float, optional
             Initial quality at this junction
         """
+        assert isinstance(name, str) and len(name) < 32 and name.find(' ') == -1, "name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(base_demand, (int, float)), "base_demand must be a float"
+        assert isinstance(demand_pattern, (type(None), str, PatternRegistry.DefaultPattern, Pattern)), "demand_pattern must be a string or Pattern"
+        assert isinstance(elevation, (int, float)), "elevation must be a float"
+        assert isinstance(coordinates, (type(None), tuple)), "coordinates must be a tuple"
+        assert isinstance(demand_category, (type(None), str)), "demand_category must be a string"
+        assert isinstance(emitter_coeff, (type(None), int, float)), "emitter_coeff must be a float"
+        assert isinstance(initial_quality, (type(None), int, float)), "initial_quality must be a float"
+        
         base_demand = float(base_demand)
         elevation = float(elevation)
+        
         junction = Junction(name, self)
         junction.elevation = elevation
-#        if base_demand:
         junction.add_demand(base_demand, demand_pattern, demand_category)
         self[name] = junction
         if coordinates is not None:
@@ -2242,7 +2268,7 @@ def add_tank(self, name, elevation=0.0, init_level=3.048,
         name : string
             Name of the tank.
         elevation : float
-            Elevation at the Tank.
+            Elevation at the tank.
         init_level : float
             Initial tank level.
         min_level : float
@@ -2254,20 +2280,26 @@ def add_tank(self, name, elevation=0.0, init_level=3.048,
             curve is None)
         min_vol : float
             Minimum tank volume (only used when the volume curve is None)
-        vol_curve : str, optional
+        vol_curve : string, optional
             Name of a volume curve. The volume curve overrides the tank diameter
             and minimum volume.
         overflow : bool, optional
-            Overflow indicator (allows "yes"/"no", True/False, 1/0; default False)
+            Overflow indicator (Always False for the WNTRSimulator)
         coordinates : tuple of floats, optional
             X-Y coordinates of the node location.
             
-        Raises
-        ------
-        ValueError
-            If `init_level` greater than `max_level` or less than `min_level`
-            
         """
+        assert isinstance(name, str) and len(name) < 32 and name.find(' ') == -1, "name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(elevation, (int, float)), "elevation must be a float"
+        assert isinstance(init_level, (int, float)), "init_level must be a float"
+        assert isinstance(min_level, (int, float)), "min_level must be a float"
+        assert isinstance(max_level, (int, float)), "max_level must be a float"
+        assert isinstance(diameter, (int, float)), "diameter must be a float"
+        assert isinstance(min_vol, (int, float)), "min_vol must be a float"
+        assert isinstance(vol_curve, (type(None), str)), "vol_curve must be a string"
+        assert isinstance(overflow, (type(None), bool)), "overflow must be a Boolean"
+        assert isinstance(coordinates, (type(None), tuple)), "coordinates must be a tuple"
+        
         elevation = float(elevation)
         init_level = float(init_level)
         min_level = float(min_level)
@@ -2287,13 +2319,13 @@ def add_tank(self, name, elevation=0.0, init_level=3.048,
                                  str(self._curve_reg.volume_curve_names))
             vcurve = np.array(self._curve_reg[vol_curve].points)
             if min_level < vcurve[0,0]:
-                raise ValueError('The volume curve ' + vol_curve + ' has a minimum value ({0:5.2f}) \n' +
+                raise ValueError(('The volume curve ' + vol_curve + ' has a minimum value ({0:5.2f}) \n' +
                                  'greater than the minimum level for tank "' + name + '" ({1:5.2f})\n' +
-                                 'please correct the user input.'.format(vcurve[0,0],min_level))
+                                 'please correct the user input.').format(vcurve[0,0],min_level))
             elif max_level > vcurve[-1,0]:
-                raise ValueError('The volume curve ' + vol_curve + ' has a maximum value ({0:5.2f}) \n' +
+                raise ValueError(('The volume curve ' + vol_curve + ' has a maximum value ({0:5.2f}) \n' +
                                  'less than the maximum level for tank "' + name + '" ({1:5.2f})\n' +
-                                 'please correct the user input.'.format(vcurve[-1,0],max_level))
+                                 'please correct the user input.').format(vcurve[-1,0],max_level))
 
         tank = Tank(name, self)
         tank.elevation = elevation
@@ -2318,15 +2350,19 @@ def add_reservoir(self, name, base_head=0.0, head_pattern=None, coordinates=None
             Name of the reservoir.
         base_head : float, optional
             Base head at the reservoir.
-        head_pattern : string
-            Name of the head pattern (optional)
+        head_pattern : string, optional
+            Name of the head pattern.
         coordinates : tuple of floats, optional
             X-Y coordinates of the node location.
         
         """
+        assert isinstance(name, str) and len(name) < 32 and name.find(' ') == -1, "name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(base_head, (int, float)), "base_head must be float"
+        assert isinstance(head_pattern, (type(None), str)), "head_pattern must be a string"
+        assert isinstance(coordinates, (type(None), tuple)), "coordinates must be a tuple"
+        
         base_head = float(base_head)
-        if head_pattern and not isinstance(head_pattern, six.string_types):
-            raise ValueError('Head pattern must be a string')
+
         reservoir = Reservoir(name, self)
         reservoir.base_head = base_head
         reservoir.head_pattern_name = head_pattern
@@ -2424,7 +2460,7 @@ def __init__(self, model):
         self._valves = OrderedSet()
     
     def _finalize_(self, model):
-        super(self.__class__, self)._finalize_(model)
+        super()._finalize_(model)
         self._link_reg = None
 
     def __setitem__(self, key, value):
@@ -2514,7 +2550,7 @@ def __call__(self, link_type=None):
             raise RuntimeError('link_type, '+str(link_type)+', not recognized.')
 
     def add_pipe(self, name, start_node_name, end_node_name, length=304.8,
-                 diameter=0.3048, roughness=100, minor_loss=0.0, status='OPEN', check_valve_flag=False):
+                 diameter=0.3048, roughness=100, minor_loss=0.0, initial_status='OPEN', check_valve=False):
         """
         Adds a pipe to the water network model.
 
@@ -2534,31 +2570,42 @@ def add_pipe(self, name, start_node_name, end_node_name, length=304.8,
             Pipe roughness coefficient.
         minor_loss : float, optional
             Pipe minor loss coefficient.
-        status : string, optional
-            Pipe status. Options are 'Open' or 'Closed'.
-        check_valve_flag : bool, optional
+        initial_status : string, optional
+            Pipe initial status. Options are 'OPEN' or 'CLOSED'.
+        check_valve : bool, optional
             True if the pipe has a check valve.
             False if the pipe does not have a check valve.
         
         """
+        assert isinstance(name, str) and len(name) < 32 and name.find(' ') == -1, "name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(start_node_name, str) and len(start_node_name) < 32 and start_node_name.find(' ') == -1, "start_node_name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(end_node_name, str) and len(end_node_name) < 32 and end_node_name.find(' ') == -1, "end_node_name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(length, (int, float)), "length must be a float"
+        assert isinstance(diameter, (int, float)), "diameter must be a float"
+        assert isinstance(roughness, (int, float)), "roughness must be a float"
+        assert isinstance(minor_loss, (int, float)), "minor_loss must be a float"
+        assert isinstance(initial_status, (str, LinkStatus)), "initial_status must be a string or LinkStatus"
+        assert isinstance(check_valve, bool), "check_valve must be a Boolean"
+        
         length = float(length)
         diameter = float(diameter)
         roughness = float(roughness)
         minor_loss = float(minor_loss)
-        if isinstance(status, str):
-            status = LinkStatus[status]
+        if isinstance(initial_status, str):
+            initial_status = LinkStatus[initial_status]
+            
         pipe = Pipe(name, start_node_name, end_node_name, self)
         pipe.length = length
         pipe.diameter = diameter
         pipe.roughness = roughness
         pipe.minor_loss = minor_loss
-        pipe.initial_status = status
-        pipe._user_status = status
-        pipe.cv = check_valve_flag
+        pipe.initial_status = initial_status
+        pipe._user_status = initial_status
+        pipe.check_valve = check_valve
         self[name] = pipe
 
     def add_pump(self, name, start_node_name, end_node_name, pump_type='POWER',
-                 pump_parameter=50.0, speed=1.0, pattern=None):
+                 pump_parameter=50.0, speed=1.0, pattern=None, initial_status='OPEN'):
         """
         Adds a pump to the water network model.
 
@@ -2572,34 +2619,44 @@ def add_pump(self, name, start_node_name, end_node_name, pump_type='POWER',
              Name of the end node.
         pump_type : string, optional
             Type of information provided for a pump. Options are 'POWER' or 'HEAD'.
-        pump_parameter : float or str object
-            Float value of power in KW. Head curve name.
+        pump_parameter : float or string
+            For a POWER pump, the pump power (float).
+            For a HEAD pump, the head curve name (string).
         speed: float
             Relative speed setting (1.0 is normal speed)
-        pattern: str
-            ID of pattern for speed setting
-        
-        """
+        pattern: string
+            Name of the speed pattern
+        initial_status: str or LinkStatus
+            Pump initial status. Options are 'OPEN' or 'CLOSED'.
+        
+        """
+        assert isinstance(name, str) and len(name) < 32 and name.find(' ') == -1, "name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(start_node_name, str) and len(start_node_name) < 32 and start_node_name.find(' ') == -1, "start_node_name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(end_node_name, str) and len(end_node_name) < 32 and end_node_name.find(' ') == -1, "end_node_name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(pump_type, str), "pump_type must be a string"
+        assert isinstance(pump_parameter, (int, float, str)), "pump_parameter must be a float or string"
+        assert isinstance(speed, (int, float)), "speed must be a float"
+        assert isinstance(pattern, (type(None), str)), "pattern must be a string"
+        assert isinstance(initial_status, (str, LinkStatus)), "initial_status must be a string or LinkStatus"
+        
+        if isinstance(initial_status, str):
+            initial_status = LinkStatus[initial_status]
         if pump_type.upper() == 'POWER':
             pump = PowerPump(name, start_node_name, end_node_name, self)
             pump.power = pump_parameter
         elif pump_type.upper() == 'HEAD':
             pump = HeadPump(name, start_node_name, end_node_name, self)
-            if not isinstance(pump_parameter, six.string_types):
-                pump.pump_curve_name = pump_parameter.name
-            else:
-                pump.pump_curve_name = pump_parameter
+            pump.pump_curve_name = pump_parameter
         else:
             raise ValueError('pump_type must be "POWER" or "HEAD"')
         pump.base_speed = speed
-        if isinstance(pattern, Pattern):
-            pump.speed_pattern_name = pattern.name
-        else:
-            pump.speed_pattern_name = pattern
+        pump.initial_status = initial_status
+        pump.speed_pattern_name = pattern
         self[name] = pump
     
     def add_valve(self, name, start_node_name, end_node_name,
-                 diameter=0.3048, valve_type='PRV', minor_loss=0.0, setting=0.0):
+                 diameter=0.3048, valve_type='PRV', minor_loss=0.0, 
+                 initial_setting=0.0, initial_status='ACTIVE'):
         """
         Adds a valve to the water network model.
 
@@ -2614,44 +2671,76 @@ def add_valve(self, name, start_node_name, end_node_name,
         diameter : float, optional
             Diameter of the valve.
         valve_type : string, optional
-            Type of valve. Options are 'PRV', etc.
+            Type of valve. Options are 'PRV', 'PSV', 'PBV', 'FCV', 'TCV', and 'GPV'
         minor_loss : float, optional
             Pipe minor loss coefficient.
-        setting : float or string, optional
-            pressure setting for PRV, PSV, or PBV,
-            flow setting for FCV,
-            loss coefficient for TCV,
-            name of headloss curve for GPV.
-        
+        initial_setting : float or string, optional
+            Valve initial setting.
+            Pressure setting for PRV, PSV, or PBV. 
+            Flow setting for FCV. 
+            Loss coefficient for TCV.
+            Name of headloss curve for GPV.
+        initial_status: string or LinkStatus
+            Valve initial status. Options are 'OPEN',  'CLOSED', or 'ACTIVE'
+            
         """
+        assert isinstance(name, str) and len(name) < 32 and name.find(' ') == -1, "name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(start_node_name, str) and len(start_node_name) < 32 and start_node_name.find(' ') == -1, "start_node_name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(end_node_name, str) and len(end_node_name) < 32 and end_node_name.find(' ') == -1, "end_node_name must be a string with less than 32 characters and contain no spaces"
+        assert isinstance(diameter, (int, float)), "diameter must be a float"
+        assert isinstance(valve_type, str), "valve_type must be a string"
+        assert isinstance(minor_loss, (int, float)), "minor_loss must be a float"
+        assert isinstance(initial_setting, (int, float, str)), "initial_setting must be a float or string"
+        assert isinstance(initial_status, (str, LinkStatus)), "initial_status must be a string or LinkStatus"
+        
+        if isinstance(initial_status, str):
+            initial_status = LinkStatus[initial_status]
         start_node = self._node_reg[start_node_name]
         end_node = self._node_reg[end_node_name]
-        if type(start_node)==Tank or type(end_node)==Tank:
-            logger.warn('Valves should not be connected to tanks! Please add a pipe between the tank and valve. Note that this will be an error in the next release.')
+        
         valve_type = valve_type.upper()
+        
+        # A PRV, PSV or FCV cannot be directly connected to a reservoir or tank (use a length of pipe to separate the two)
+        if valve_type in ['PRV', 'PSV', 'FCV']:
+            if type(start_node)==Tank or type(end_node)==Tank or type(start_node)==Reservoir or type(end_node)==Reservoir:
+                msg = '%ss cannot be directly connected to a tank.  Add a pipe to separate the valve from the tank.' % valve_type
+                logger.error(msg)   
+                raise RuntimeError(msg)
+            if type(start_node)==Reservoir or type(end_node)==Reservoir:
+                msg = '%ss cannot be directly connected to a reservoir.  Add a pipe to separate the valve from the reservoir.' % valve_type
+                logger.error(msg)   
+                raise RuntimeError(msg)
+        
+        # TODO check the following: PRVs cannot share the same downstream node or be linked in series
+            
+        # TODO check the following: Two PSVs cannot share the same upstream node or be linked in series
+        
+        # TODO check the following: A PSV cannot be connected to the downstream node of a PRV
+        
         if valve_type == 'PRV':
             valve = PRValve(name, start_node_name, end_node_name, self)
-            valve.initial_setting = setting
-            valve._setting = setting
+            valve.initial_setting = initial_setting
+            valve._setting = initial_setting
         elif valve_type == 'PSV':
             valve = PSValve(name, start_node_name, end_node_name, self)
-            valve.initial_setting = setting
-            valve._setting = setting
+            valve.initial_setting = initial_setting
+            valve._setting = initial_setting
         elif valve_type == 'PBV':
             valve = PBValve(name, start_node_name, end_node_name, self)
-            valve.initial_setting = setting
-            valve._setting = setting
+            valve.initial_setting = initial_setting
+            valve._setting = initial_setting
         elif valve_type == 'FCV':
             valve = FCValve(name, start_node_name, end_node_name, self)
-            valve.initial_setting = setting
-            valve._setting = setting
+            valve.initial_setting = initial_setting
+            valve._setting = initial_setting
         elif valve_type == 'TCV':
             valve = TCValve(name, start_node_name, end_node_name, self)
-            valve.initial_setting = setting
-            valve._setting = setting
+            valve.initial_setting = initial_setting
+            valve._setting = initial_setting
         elif valve_type == 'GPV':
             valve = GPValve(name, start_node_name, end_node_name, self)
-            valve.headloss_curve_name = setting
+            valve.headloss_curve_name = initial_setting
+        valve.initial_status = initial_status
         valve.diameter = diameter
         valve.minor_loss = minor_loss
         self[name] = valve
@@ -2668,7 +2757,7 @@ def check_valves(self):
             
         """
         for name in self._pipes:
-            if self._data[name].cv:
+            if self._data[name].check_valve:
                 yield name
 
     @property
diff --git a/wntr/network/options.py b/wntr/network/options.py
index 09529dd4b..5bd042807 100644
--- a/wntr/network/options.py
+++ b/wntr/network/options.py
@@ -8,9 +8,10 @@
     with EPANET nomenclature. This change is not backwards compatible, particularly 
     when trying to use pickle files with older options.
 
-.. rubric:: Contents
+.. rubric:: Classes
 
 .. autosummary::
+    :nosignatures:
 
     Options
     TimeOptions
@@ -113,54 +114,53 @@ class TimeOptions(_OptionsBase):
     Parameters
     ----------
     duration : int
-        Simulation duration (seconds). Defaults to 0.
+        Simulation duration (seconds), by default 0.
 
     hydraulic_timestep : int
-        Hydraulic timestep (seconds). Defaults to 3600 (one hour).
+        Hydraulic timestep (seconds), by default 3600 (one hour).
 
     quality_timestep : int
-        Water quality timestep (seconds). Defaults to 360 (five minutes).
+        Water quality timestep (seconds), by default 360 (five minutes).
 
     rule_timestep : int
-        Rule timestep (seconds). Defaults to 360 (five minutes).
+        Rule timestep (seconds), by default 360 (five minutes).
 
     pattern_timestep : int
-        Pattern timestep (seconds). Defaults to 3600 (one hour).
+        Pattern timestep (seconds), by default 3600 (one hour).
 
     pattern_start : int
         Time offset (in seconds) to find the starting pattern step; changes 
         where in pattern the pattern starts out, *not* what time the pattern 
-        starts. Defaults to 0.
+        starts, by default 0.
 
     report_timestep : int
-        Reporting timestep (seconds). Defaults to 3600 (one hour).
+        Reporting timestep (seconds), by default 3600 (one hour).
 
     report_start : int
-        Start time of the report (in seconds) from the start of the simulation. Default 0.
+        Start time of the report (in seconds) from the start of the simulation, by default 0.
 
     start_clocktime : int
-        Time of day (in seconds from midnight) at which the simulation begins. Default 0 (midnight).
+        Time of day (in seconds from midnight) at which the simulation begins, by default 0 (midnight).
 
     statistic: str
         Provide statistics rather than time series report in the report file.
-        Options are AVERAGED, MINIMUM, MAXIUM, RANGE, and NONE (as defined in the 
-        EPANET User Manual). Defaults to NONE.
+        Options are "AVERAGED", "MINIMUM", "MAXIUM", "RANGE", and "NONE" (as defined in the 
+        EPANET User Manual). Defaults to "NONE".
     
-
     """
     _pattern1 = re.compile(r'^(\d+):(\d+):(\d+)$')
     _pattern2 = re.compile(r'^(\d+):(\d+)$')
     _pattern3 = re.compile(r'^(\d+)$')
     def __init__(self,
-                duration: float = 0.0,
-                hydraulic_timestep: float=3600.0,
-                quality_timestep: float=360.0,
-                rule_timestep: float=360.0,
-                pattern_timestep: float=3600.0,
-                pattern_start: float=0.0,
-                report_timestep: float=3600.0,
-                report_start: float=0.0,
-                start_clocktime: float=0.0,
+                duration: int = 0,
+                hydraulic_timestep: int=3600,
+                quality_timestep: int=360,
+                rule_timestep: int=360,
+                pattern_timestep: int=3600,
+                pattern_start: int=0,
+                report_timestep: int=3600,
+                report_start: int=0,
+                start_clocktime: int=0,
                 statistic: str='NONE'):
         self.duration = duration
         self.hydraulic_timestep = hydraulic_timestep
@@ -182,7 +182,11 @@ def __setattr__(self, name, value):
             try:
                 value = float(value)
             except ValueError:
-                raise ValueError('%s must be a number', name)
+                raise ValueError('%s must be a number'%name)
+        elif name not in ['duration', 'hydraulic_timestep', 'quality_timestep', 'rule_timestep',
+                            'pattern_timestep', 'pattern_start', 'report_timestep', 'report_start',
+                            'start_clocktime', 'statistic']:
+            raise AttributeError('%s is not a valid attribute in TimeOptions'%name)
         self.__dict__[name] = value
 
     @classmethod
@@ -307,14 +311,13 @@ class GraphicsOptions(_OptionsBase):
         EPANET will make the image match the full extent of node coordinates (set to `None`).
 
     units : str
-        Units for backdrop image dimensions. Must be one of FEET, METERS, DEGREES or NONE. 
-        Default is NONE.
+        Units for backdrop image dimensions. Must be one of FEET, METERS, DEGREES or NONE, by default "NONE".
 
     offset : 2-tuple or list
-        Offset for the network in order (X, Y). Default is None (no offset).
+        Offset for the network in order (X, Y), by default ``None`` (no offset).
 
     image_filename : string
-        Filename where image is located. Default is None.
+        Filename where image is located, by default ``None``.
 
     map_filename : string
         Filename used to store node coordinates in (node, x, y) format. This option is
@@ -349,6 +352,8 @@ def __setattr__(self, name, value):
             value = str(value).upper()
             if value not in ['FEET','METERS','DEGREES','NONE']:
                 raise ValueError('Backdrop units must be one of FEET, METERS, DEGREES, or NONE')
+        elif name not in ['dimensions', 'units', 'offset', 'image_filename', 'map_filename']:
+            raise AttributeError('%s is not a valid attribute of GraphicsOptions'%name)
         self.__dict__[name] = value
 
 
@@ -361,21 +366,21 @@ class HydraulicOptions(_OptionsBase):
     Parameters
     ----------
     headloss : str
-        Formula to use for computing head loss through a pipe. Options are H-W, 
-        D-W, and C-M. Default is `H-W`.
+        Formula to use for computing head loss through a pipe. Options are "H-W", 
+        "D-W", and "C-M", by default "H-W".
 
     hydraulics : str
         Indicates if a hydraulics file should be read in or saved; options are 
-        None, USE and SAVE. Defaults to ``None``.
+        ``None``, "USE" and "SAVE", by default ``None``.
 
     hydraulics_filename : str
-        Filename to use if ``hydraulics = 'SAVE'``. Defaults to ``None``.
+        Filename to use if ``hydraulics is not None``, by default ``None``.
 
     viscosity : float
-        Kinematic viscosity of the fluid. Defaults to 1.0.
+        Kinematic viscosity of the fluid, by default 1.0.
 
     specific_gravity : float
-        Specific gravity of the fluid. Defaults to 1.0.
+        Specific gravity of the fluid, by default 1.0.
 
     pattern : str
         Name of the default pattern for junction demands. By default,
@@ -385,58 +390,58 @@ class HydraulicOptions(_OptionsBase):
 
     demand_multiplier : float
         The demand multiplier adjusts the values of baseline demands for all 
-        junctions. Defaults to 1.0.
+        junctions, by default 1.0.
 
     emitter_exponent : float
-        The exponent used when computing flow from an emitter. Defaults to 0.5.
+        The exponent used when computing flow from an emitter, by default 0.5.
 
     minimum_pressure : float
-        (EPANET 2.2 only) The global minimum nodal pressure. Defaults to 0.0.
+        (EPANET 2.2 only) The global minimum nodal pressure, by default 0.0.
 
     required_pressure: float
-        (EPANET 2.2 only) The required nodal pressure. Defaults to 0.07 (m H2O)
+        (EPANET 2.2 only) The required nodal pressure, by default 0.07 (m H2O)
 
     pressure_exponent: float
-        (EPANET 2.2 only) The pressure exponent. Defaults to 0.5.
+        (EPANET 2.2 only) The pressure exponent, by default 0.5.
 
     trials : int
-        Maximum number of trials used to solve network hydraulics. Defaults to 200.
+        Maximum number of trials used to solve network hydraulics, by default 200.
 
     accuracy : float
-        Convergence criteria for hydraulic solutions. Defaults to 0.001.
+        Convergence criteria for hydraulic solutions, by default 0.001.
 
     headerror : float
         (EPANET 2.2 only) Augments the `accuracy` option by adjusting the head 
-        error convergence limit. Defaults to 0 (off).
+        error convergence limit, by default 0 (off).
 
     flowchange : float
         (EPANET 2.2 only) Augments the `accuracy` option by adjusting the flow 
-        change convergence limit. Defaults to 0 (off).
+        change convergence limit, by default 0 (off).
 
     unbalanced : str
         Indicate what happens if a hydraulic solution cannot be reached.  
-        Options are STOP and CONTINUE. Defaults to STOP.
+        Options are "STOP" and "CONTINUE", by default "STOP".
 
     unbalanced_value : int
-        Number of additional trials if unbalanced = CONTINUE. Default is None.
+        Number of additional trials if ``unbalanced == "CONTINUE"``, by default ``None``.
 
     checkfreq : int
-        Number of solution trials that pass between status checks. Default is 2.
+        Number of solution trials that pass between status checks, by default 2.
 
     maxcheck : int
-        Number of solution trials that pass between status check. Default is 10.
+        Number of solution trials that pass between status check, by default 10.
 
     damplimit : float
-        Accuracy value at which solution damping begins. Default is 0 (no damping).
+        Accuracy value at which solution damping begins, by default 0 (no damping).
 
     demand_model : str
-        Demand model for EPANET 2.2; acceptable values are DD and PDD. Default is DD.
+        Demand model for EPANET 2.2; acceptable values are "DD" and "PDD", by default "DD".
         EPANET 2.0 only contains demand driven analysis, and will issue a warning 
         if this option is not set to DD.
 
     inpfile_units : str
-        Units for the INP-file; options are CFS, GPM, MGD, IMGD, AFD, LPS, 
-        LPM, MLD, CMH, and CMD. This **only** changes the units used in generating
+        Units for the INP file; options are "CFS", "GPM", "MGD", "IMGD", "AFD", "LPS", 
+        "LPM", "MLD", "CMH", and "CMD". This **only** changes the units used in generating
         the INP file -- it has **no impact** on the units used in WNTR, which are 
         **always** SI units (m, kg, s).
     
@@ -528,6 +533,12 @@ def __setattr__(self, name, value):
                 value = float(value)
             except ValueError:
                 raise ValueError('%s must be a number', name)
+        if name not in ['headloss', 'hydraulics', 'hydraulics_filename', 'viscosity', 'specific_gravity',
+                        'pattern', 'demand_multiplier', 'demand_model', 'minimum_pressure', 'required_pressure',
+                        'pressure_exponent', 'emitter_exponent', 'trials', 'accuracy', 'unbalanced', 
+                        'unbalanced_value', 'checkfreq', 'maxcheck', 'damplimit', 'headerror',
+                        'flowchange', 'inpfile_units']:
+            raise AttributeError('%s is not a valid attribute of HydraulicOptions'%name)
         self.__dict__[name] = value
 
 
@@ -539,29 +550,29 @@ class ReactionOptions(_OptionsBase):
     Parameters
     ----------
     bulk_order : float
-        Order of reaction occurring in the bulk fluid. Defaults to 1.0.
+        Order of reaction occurring in the bulk fluid, by default 1.0.
 
     wall_order : float
-        Order of reaction occurring at the pipe wall; must be either 0 or 1. Defaults to 1.0.
+        Order of reaction occurring at the pipe wall; must be either 0 or 1, by default 1.0.
 
     tank_order : float
-        Order of reaction occurring in the tanks. Defaults to 1.0.
+        Order of reaction occurring in the tanks, by default 1.0.
 
     bulk_coeff : float
-        Global reaction coefficient for bulk fluid and tanks. Defaults to 0.0.
+        Global reaction coefficient for bulk fluid and tanks, by default 0.0.
 
     wall_coeff : float
-        Global reaction coefficient for pipe walls. Defaults 0.0.
+        Global reaction coefficient for pipe walls, by default 0.0.
 
     limiting_potential : float
         Specifies that reaction rates are proportional to the difference 
         between the current concentration and some limiting potential value, 
-        Defaults to None (off).
+        by default ``None`` (off).
 
     roughness_correl : float
         Makes all default pipe wall reaction coefficients related to pipe 
-        roughness, according to functions as defined in EPANET. Defaults
-        to None (off).
+        roughness, according to functions as defined in EPANET, by default 
+        ``None`` (off).
         
 
     .. note::
@@ -598,6 +609,9 @@ def __setattr__(self, name, value):
                 value = _float_or_None(value)
             except ValueError:
                 raise ValueError('%s must be a number or None', name)
+        if name not in ['bulk_order', 'wall_order', 'tank_order', 'bulk_coeff',
+                        'wall_coeff', 'limiting_potential', 'roughness_correl']:
+            raise AttributeError('%s is not a valid attribute of ReactionOptions'%name)
         self.__dict__[name] = value
 
 
@@ -609,20 +623,20 @@ class QualityOptions(_OptionsBase):
     Parameters
     ----------
     parameter : str
-        Type of water quality analysis.  Options are NONE, CHEMICAL, AGE, and 
-        TRACE. Defaults to None.
+        Type of water quality analysis.  Options are "NONE", "CHEMICAL", "AGE", and 
+        "TRACE", by default ``None``.
 
     trace_node : str
-        Trace node name if quality = TRACE. Defaults to None.
+        Trace node name if ``quality == "TRACE"``, by default ``None``.
 
     chemical : str
-        Chemical name for 'chemical' analysis. Defaults to "CHEMICAL" if appropriate.
+        Chemical name for "CHEMICAL" analysis, by default "CHEMICAL" if appropriate.
 
     diffusivity : float
-        Molecular diffusivity of the chemical. Defaults to 1.0.
+        Molecular diffusivity of the chemical, by default 1.0.
 
     tolerance : float
-        Water quality solver tolerance. Defaults to 0.01.
+        Water quality solver tolerance, by default 0.01.
 
     inpfile_units : str
         Units for quality analysis if the parameter is set to CHEMICAL. 
@@ -652,6 +666,9 @@ def __setattr__(self, name, value):
                 value = float(value)
             except ValueError:
                 raise ValueError('%s must be a number or None', name)
+        if name not in ['parameter', 'trace_node', 'chemical_name', 'diffusivity',
+                        'tolerance', 'inpfile_units']:
+            raise AttributeError('%s is not a valid attribute of QualityOptions'%name)
         self.__dict__[name] = value
 
 
@@ -663,19 +680,19 @@ class EnergyOptions(_OptionsBase):
     Parameters
     ----------
     global_price : float
-        Global average cost per Joule. Defaults to 0.
+        Global average cost per Joule, by default 0.
 
     global_pattern : str
-        ID label of time pattern describing how energy price varies with time.
-        Defaults to None.
+        ID label of time pattern describing how energy price varies with
+        time, by default ``None``.
 
     global_efficiency : float
-        Global pump efficiency as percent; i.e., 75.0 means 75%.
-        Defaults to None.
+        Global pump efficiency as percent; i.e., 75.0 means 75%,
+        by default ``None``.
 
     demand_charge : float
-        Added cost per maximum kW usage during the simulation period.
-        Defaults to None.
+        Added cost per maximum kW usage during the simulation period,
+        by default ``None``.
 
     
     """
@@ -689,6 +706,11 @@ def __init__(self,
         self.global_efficiency = global_efficiency
         self.demand_charge = demand_charge
 
+    def __setattr__(self, name, value):
+        if name not in ['global_price', 'global_pattern', 'global_efficiency', 'demand_charge']:
+            raise AttributeError('%s is not a valid attribute of EnergyOptions'%name)
+        self.__dict__[name] = value
+
 
 class ReportOptions(_OptionsBase):
     """
@@ -700,14 +722,14 @@ class ReportOptions(_OptionsBase):
     Parameters
     ----------
     report_filename : str
-        Provides the filename to use for outputting an EPANET report file.
-        By default, this will be the prefix plus ".rpt".
+        Provides the filename to use for outputting an EPANET report file,
+        by default this will be the prefix plus ".rpt".
 
     status : str
-        Output solver status ('YES', 'NO', 'FULL'). 'FULL' is only useful for debugging
+        Output solver status ("YES", "NO", "FULL"). "FULL" is only useful for debugging
 
     summary : str
-        Output summary information ('YES' or 'NO')
+        Output summary information ("YES" or "NO")
 
     energy : str
         Output energy information
@@ -744,6 +766,12 @@ def __init__(self,
         self.links = links
         self.report_params = report_params if report_params is not None else _new_report_params()
         self.param_opts = param_opts if param_opts is not None else _new_param_opts()
+    
+    def __setattr__(self, name, value):
+        if name not in ['pagesize', 'report_filename', 'status', 'summary', 'energy', 'nodes',
+                        'links', 'report_params', 'param_opts']:
+            raise AttributeError('%s is not a valid attribute of ReportOptions'%name)
+        self.__dict__[name] = value
 
 
 class UserOptions(_OptionsBase):
diff --git a/wntr/sim/core.py b/wntr/sim/core.py
index 6d928f2c4..f09776a97 100644
--- a/wntr/sim/core.py
+++ b/wntr/sim/core.py
@@ -279,8 +279,8 @@ def compare_link_sol(self):
         os.system('open link_comparison_' + link_name + '_' + str(t) + '.html')
 
     def store_var_values_in_network(self):
-        self.mode = self._wn.options.hydraulic.demand_model
-        wntr.sim.hydraulics.store_results_in_network(self.wn, self.model, self.mode)
+        # self.mode = self._wn.options.hydraulic.demand_model
+        wntr.sim.hydraulics.store_results_in_network(self.wn, self.model) #, self.mode)
 
     @classmethod
     def _plot_interactive_network(cls, wn, title=None, node_size=8, link_width=2,
@@ -795,6 +795,7 @@ def _run_postsolve_controls(self):
     def run_sim(self, solver=NewtonSolver, backup_solver=None, solver_options=None,
                 backup_solver_options=None, convergence_error=False, HW_approx='default',
                 diagnostics=False):
+
         """
         Run an extended period simulation (hydraulics only).
 
@@ -962,6 +963,7 @@ def run_sim(self, solver=NewtonSolver, backup_solver=None, solver_options=None,
                 break
 
         wntr.sim.hydraulics.get_results(self._wn, results, node_res, link_res)
+        
         return results
 
     def _initialize_name_id_maps(self):
@@ -995,7 +997,7 @@ def _initialize_internal_graph(self):
             cols.append(to_node_id)
             rows.append(to_node_id)
             cols.append(from_node_id)
-            if link.status == wntr.network.LinkStatus.closed:
+            if link.status == wntr.network.LinkStatus.Closed:
                 vals.append(0)
                 vals.append(0)
             else:
@@ -1037,7 +1039,7 @@ def _initialize_internal_graph(self):
                     link = self._wn.get_link(link_name)
                     if link.start_node_name == to_node_name or link.end_node_name == to_node_name:
                         tmp_list.append(link)
-                        if link.status != wntr.network.LinkStatus.closed:
+                        if link.status != wntr.network.LinkStatus.Closed:
                             ndx1, ndx2 = ndx_map[link]
                             self._internal_graph.data[ndx1] = 1
                             self._internal_graph.data[ndx2] = 1
@@ -1057,7 +1059,7 @@ def _update_internal_graph(self):
         for mgr in [self._presolve_controls, self._rules, self._postsolve_controls]:
             for obj, attr in mgr.get_changes():
                 if 'status' == attr:
-                    if obj.status == wntr.network.LinkStatus.closed:
+                    if obj.status == wntr.network.LinkStatus.Closed:
                         ndx1, ndx2 = ndx_map[obj]
                         data[ndx1] = 0
                         data[ndx2] = 0
@@ -1072,7 +1074,7 @@ def _update_internal_graph(self):
             data[ndx1] = 0
             data[ndx2] = 0
             for link in link_list:
-                if link.status != wntr.network.LinkStatus.closed:
+                if link.status != wntr.network.LinkStatus.Closed:
                     ndx1, ndx2 = ndx_map[link]
                     data[ndx1] = 1
                     data[ndx2] = 1
diff --git a/wntr/sim/epanet.py b/wntr/sim/epanet.py
index 0f9111088..d5a395595 100644
--- a/wntr/sim/epanet.py
+++ b/wntr/sim/epanet.py
@@ -1,5 +1,6 @@
 from wntr.sim.core import WaterNetworkSimulator
 import wntr.epanet.io
+import warnings
 import logging
 
 logger = logging.getLogger(__name__)
@@ -59,7 +60,9 @@ 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, convergence_error=False):
+    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.
 
@@ -100,6 +103,7 @@ def run_sim(self, file_prefix='temp', save_hyd=False, use_hyd=False, hydfile=Non
         enData = wntr.epanet.toolkit.ENepanet(version=version)
         rptfile = file_prefix + '.rpt'
         outfile = file_prefix + '.bin'
+        
         if hydfile is None:
             hydfile = file_prefix + '.hyd'
         enData.ENopen(inpfile, rptfile, outfile)
@@ -119,5 +123,8 @@ 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, convergence_error)
+        
+        results = self.reader.read(outfile, convergence_error, self._wn.options.hydraulic.headloss=='D-W')
+
+        return results
 
diff --git a/wntr/sim/hydraulics.py b/wntr/sim/hydraulics.py
index aab2f8675..51b7fc9bf 100644
--- a/wntr/sim/hydraulics.py
+++ b/wntr/sim/hydraulics.py
@@ -6,7 +6,7 @@
 import logging
 from wntr.network.model import WaterNetworkModel
 from wntr.network.base import NodeType, LinkType, LinkStatus
-from wntr.network.elements import Junction, Tank, Reservoir, Pipe, HeadPump, PowerPump, PRValve, PSValve, FCValve, \
+from wntr.network.elements import Junction, Tank, Reservoir, Pipe, Pump, HeadPump, PowerPump, PRValve, PSValve, FCValve, \
     TCValve, GPValve, PBValve
 from collections import OrderedDict
 from wntr.utils.ordered_set import OrderedSet
@@ -215,6 +215,7 @@ def initialize_results_dict(wn):
     link_res['flowrate'] = OrderedDict((name, list()) for name, obj in wn.links())
     link_res['velocity'] = OrderedDict((name, list()) for name, obj in wn.links())
     link_res['status'] = OrderedDict((name, list()) for name, obj in wn.links())
+    link_res['setting'] = OrderedDict((name, list()) for name, obj in wn.links())
 
     return node_res, link_res
 
@@ -252,12 +253,14 @@ def save_results(wn, node_res, link_res):
         link_res['flowrate'][name].append(link.flow)
         link_res['velocity'][name].append(abs(link.flow)*4.0 / (math.pi*link.diameter**2))
         link_res['status'][name].append(link.status)
+        link_res['setting'][name].append(link.roughness)
 
     for name, link in wn.head_pumps():
         link_res['flowrate'][name].append(link.flow)
         link_res['velocity'][name].append(0)
         link_res['status'][name].append(link.status)
-
+        link_res['setting'][name].append(1)
+        
         A, B, C = link.get_head_curve_coefficients()
         if link.flow > (A/B)**(1.0/C):
             start_node_name = link.start_node_name
@@ -275,11 +278,13 @@ def save_results(wn, node_res, link_res):
         link_res['flowrate'][name].append(link.flow)
         link_res['velocity'][name].append(0)
         link_res['status'][name].append(link.status)
+        link_res['setting'][name].append(1) # power pumps have no speed
 
     for name, link in wn.valves():
         link_res['flowrate'][name].append(link.flow)
         link_res['velocity'][name].append(abs(link.flow)*4.0 / (math.pi*link.diameter**2))
         link_res['status'][name].append(link.status)
+        link_res['setting'][name].append(link.setting)
 
 
 def get_results(wn, results, node_res, link_res):
@@ -306,7 +311,30 @@ def get_results(wn, results, node_res, link_res):
         link_res[key] = pd.DataFrame(data=np.array([link_res[key][name] for name in link_names]).transpose(), index=results.time,
                                             columns=link_names)
     results.link = link_res
-
+    
+    # Add headloss to results.link -- removed for now, this is slow
+    #headloss = pd.DataFrame(data=None, index=results.time, columns=link_names)
+    # for name, link in wn.links():
+    #     start_node = link.start_node_name
+    #     end_node = link.end_node_name
+    #     start_head = results.node['head'].loc[:,start_node]
+    #     end_head = results.node['head'].loc[:,end_node]
+    #     if isinstance(link, Pipe):
+    #         # Unit headloss for pipes
+    #         headloss.loc[:,name] = abs(end_head - start_head)/link.length
+    #     elif isinstance(link, Pump):
+    #         # Negative of head gain for pumps 
+    #         head_gain = -(end_head - start_head)
+    #         head_gain[head_gain > 0] = 0
+    #         headloss.loc[:,name] = head_gain
+    #     else:
+    #         # Total head loss for valves
+    #         headloss.loc[:,name] = (end_head - start_head)
+            
+    #     # Headloss is 0 if the link is closed
+    #     headloss.loc[results.link['status'].loc[:,name] == 0,name] = 0
+        
+    #results.link['headloss'] = headloss
 
 def store_results_in_network(wn, m):
     """
@@ -321,10 +349,12 @@ def store_results_in_network(wn, m):
     for name, link in wn.links():
         if link._is_isolated:
             link._flow = 0
-            link
         else:
             link._flow = m.flow[name].value
 
+    for name, link in wn.valves():
+        link._setting = m.valve_setting[name].value
+
     for name, node in wn.junctions():
         if node._is_isolated:
             node._head = 0


Test patch:
diff --git a/wntr/tests/__init__.py b/wntr/tests/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/wntr/tests/test_aml.py b/wntr/tests/test_aml.py
index 31c600819..8d7e89e08 100644
--- a/wntr/tests/test_aml.py
+++ b/wntr/tests/test_aml.py
@@ -1,8 +1,9 @@
-import wntr.sim.aml as aml
 import unittest
-import numpy as np
 from collections import OrderedDict
 
+import numpy as np
+import wntr.sim.aml as aml
+
 
 def compare_evaluation(self, m, true_r, true_j):
     m.set_structure()
@@ -71,8 +72,8 @@ def test_divide(self):
         expr = m.x / m.y
         self.assertAlmostEqual(expr.evaluate(), x / y, 10)
         ders = expr.reverse_ad()
-        self.assertAlmostEqual(ders[m.x], 1/y, 10)
-        self.assertAlmostEqual(ders[m.y], -x/y**2, 10)
+        self.assertAlmostEqual(ders[m.x], 1 / y, 10)
+        self.assertAlmostEqual(ders[m.y], -x / y ** 2, 10)
 
     def test_power(self):
         m = aml.Model()
@@ -81,11 +82,11 @@ def test_power(self):
         m.x = aml.Var(x)
         m.y = aml.Var(y)
 
-        expr = m.x**(m.y)
+        expr = m.x ** (m.y)
         self.assertAlmostEqual(expr.evaluate(), x ** y, 10)
         ders = expr.reverse_ad()
-        self.assertAlmostEqual(ders[m.x], y*x**(y-1), 10)
-        self.assertAlmostEqual(ders[m.y], x**y * np.log(x), 10)
+        self.assertAlmostEqual(ders[m.x], y * x ** (y - 1), 10)
+        self.assertAlmostEqual(ders[m.y], x ** y * np.log(x), 10)
 
     def test_exp(self):
         m = aml.Model()
@@ -103,17 +104,20 @@ def test_log(self):
 
         expr = aml.log(m.x)
         self.assertAlmostEqual(aml.value(expr), np.log(x), 10)
-        self.assertAlmostEqual(expr.reverse_ad()[m.x], 1/x, 10)
+        self.assertAlmostEqual(expr.reverse_ad()[m.x], 1 / x, 10)
 
     def test_chain_rule(self):
         m = aml.Model()
         x = 1.1
         m.x = aml.Var(x)
 
-        expr = aml.exp((m.x + m.x**0.5)**2) - m.x
+        expr = aml.exp((m.x + m.x ** 0.5) ** 2) - m.x
 
-        actual_deriv = np.exp((x + x**0.5)**2)*2*(x + x**0.5)*(1 + 0.5*x**(-0.5)) - 1
-        self.assertAlmostEqual(aml.value(expr), np.exp((x + x**0.5)**2) - x, 10)
+        actual_deriv = (
+            np.exp((x + x ** 0.5) ** 2) * 2 * (x + x ** 0.5) * (1 + 0.5 * x ** (-0.5))
+            - 1
+        )
+        self.assertAlmostEqual(aml.value(expr), np.exp((x + x ** 0.5) ** 2) - x, 10)
         self.assertAlmostEqual(expr.reverse_ad()[m.x], actual_deriv, 10)
 
 
@@ -163,21 +167,25 @@ def test_if_then_constraints(self):
         m.y = aml.Var(y)
 
         e = aml.ConditionalExpression()
-        e.add_condition(aml.inequality(body=m.x, ub=-1), -(-m.x)**1.852 - (-m.x)**2 - m.y)
+        e.add_condition(
+            aml.inequality(body=m.x, ub=-1), -((-m.x) ** 1.852) - (-m.x) ** 2 - m.y
+        )
         e.add_condition(aml.inequality(body=m.x, ub=1), m.x)
-        e.add_final_expr(m.x**1.852 + m.x**2 - m.y)
+        e.add_final_expr(m.x ** 1.852 + m.x ** 2 - m.y)
         m.con1 = aml.Constraint(e)
 
         e = aml.ConditionalExpression()
-        e.add_condition(aml.inequality(body=m.y, ub=-1), -(-m.y)**(1.852) - (-m.y)**(2) - m.x)
+        e.add_condition(
+            aml.inequality(body=m.y, ub=-1), -((-m.y) ** (1.852)) - (-m.y) ** (2) - m.x
+        )
         e.add_condition(aml.inequality(body=m.y, ub=1), m.y)
-        e.add_final_expr(m.y**(1.852) + m.y**(2) - m.x)
+        e.add_final_expr(m.y ** (1.852) + m.y ** (2) - m.x)
         m.con2 = aml.Constraint(e)
 
         true_con_values = OrderedDict()
         true_jac = OrderedDict()
-        true_con_values[m.con1] = -(abs(x)**1.852 + abs(x)**2) - y
-        true_con_values[m.con2] = -(abs(y)**1.852 + abs(y)**2) - x
+        true_con_values[m.con1] = -(abs(x) ** 1.852 + abs(x) ** 2) - y
+        true_con_values[m.con2] = -(abs(y) ** 1.852 + abs(y) ** 2) - x
         true_jac[m.con1] = OrderedDict()
         true_jac[m.con2] = OrderedDict()
         true_jac[m.con1][m.x] = 1.852 * abs(x) ** 0.852 + 2 * abs(x)
@@ -202,8 +210,8 @@ def test_if_then_constraints(self):
         y = 3.7
         m.x.value = x
         m.y.value = y
-        true_con_values[m.con1] = x**1.852 + x**2 - y
-        true_con_values[m.con2] = y**1.852 + y**2 - x
+        true_con_values[m.con1] = x ** 1.852 + x ** 2 - y
+        true_con_values[m.con2] = y ** 1.852 + y ** 2 - x
         true_jac[m.con1][m.x] = 1.852 * x ** 0.852 + 2 * x
         true_jac[m.con1][m.y] = -1
         true_jac[m.con2][m.x] = -1
@@ -214,9 +222,11 @@ def test_if_then_constraints(self):
         del true_jac[m.con2]
         del m.con2
         e = aml.ConditionalExpression()
-        e.add_condition(aml.inequality(body=m.y, ub=-1), -(-m.y)**2.852 - (-m.y)**3 - m.x)
-        e.add_condition(aml.inequality(body=m.y, ub=1), m.y**2)
-        e.add_final_expr(m.y**2.852 + m.y**3 - m.x)
+        e.add_condition(
+            aml.inequality(body=m.y, ub=-1), -((-m.y) ** 2.852) - (-m.y) ** 3 - m.x
+        )
+        e.add_condition(aml.inequality(body=m.y, ub=1), m.y ** 2)
+        e.add_final_expr(m.y ** 2.852 + m.y ** 3 - m.x)
         m.con2 = aml.Constraint(e)
 
         true_jac[m.con2] = OrderedDict()
@@ -224,8 +234,8 @@ def test_if_then_constraints(self):
         y = -3.7
         m.x.value = x
         m.y.value = y
-        true_con_values[m.con1] = -(abs(x)**1.852 + abs(x)**2) - y
-        true_con_values[m.con2] = -(abs(y)**2.852 + abs(y)**3) - x
+        true_con_values[m.con1] = -(abs(x) ** 1.852 + abs(x) ** 2) - y
+        true_con_values[m.con2] = -(abs(y) ** 2.852 + abs(y) ** 3) - x
         true_jac[m.con1][m.x] = 1.852 * abs(x) ** 0.852 + 2 * abs(x)
         true_jac[m.con1][m.y] = -1
         true_jac[m.con2][m.x] = -1
@@ -237,19 +247,19 @@ def test_if_then_constraints(self):
         m.x.value = x
         m.y.value = y
         true_con_values[m.con1] = x
-        true_con_values[m.con2] = y**2
+        true_con_values[m.con2] = y ** 2
         true_jac[m.con1][m.x] = 1
         true_jac[m.con1][m.y] = 0
         true_jac[m.con2][m.x] = 0
-        true_jac[m.con2][m.y] = 2*y
+        true_jac[m.con2][m.y] = 2 * y
         compare_evaluation(self, m, true_con_values, true_jac)
 
         x = 4.5
         y = 3.7
         m.x.value = x
         m.y.value = y
-        true_con_values[m.con1] = x**1.852 + x**2 - y
-        true_con_values[m.con2] = y**2.852 + y**3 - x
+        true_con_values[m.con1] = x ** 1.852 + x ** 2 - y
+        true_con_values[m.con2] = y ** 2.852 + y ** 3 - x
         true_jac[m.con1][m.x] = 1.852 * x ** 0.852 + 2 * x
         true_jac[m.con1][m.y] = -1
         true_jac[m.con2][m.x] = -1
@@ -266,7 +276,7 @@ def test_register_and_remove_constraint(self):
         m.v = aml.Var(10.0)
         m.c1 = aml.Constraint(m.x + m.y)
         m.c2 = aml.Constraint(m.x * m.y * m.v)
-        m.c3 = aml.Constraint(m.z**3.0)
+        m.c3 = aml.Constraint(m.z ** 3.0)
         m.c4 = aml.Constraint(m.x + 1.0 / m.v)
         m.set_structure()
         con_values = m.evaluate_residuals()
@@ -344,5 +354,5 @@ def test_structure_exception(self):
         self.assertEqual(len(x), 2)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/wntr/tests/test_epanet_io.py b/wntr/tests/test_epanet_io.py
index e7002e299..ef9a96ca5 100644
--- a/wntr/tests/test_epanet_io.py
+++ b/wntr/tests/test_epanet_io.py
@@ -1,23 +1,23 @@
+import sys
 import unittest
-import nose
 from os.path import abspath, dirname, join
-import sys
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir, 'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
-class TestWriter(unittest.TestCase):
 
+class TestWriter(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(test_datadir, 'io.inp')
+        inp_file = join(test_datadir, "io.inp")
         self.wn = self.wntr.network.WaterNetworkModel(inp_file)
-        self.wn.write_inpfile('temp.inp', 'GPM')
-        self.wn2 = self.wntr.network.WaterNetworkModel('temp.inp')
+        self.wn.write_inpfile("temp.inp", "GPM")
+        self.wn2 = self.wntr.network.WaterNetworkModel("temp.inp")
 
     @classmethod
     def tearDownClass(self):
@@ -27,54 +27,56 @@ def test_all(self):
         self.assertTrue(self.wn._compare(self.wn2))
 
     def test_pipe_minor_loss(self):
-        p1 = self.wn2.get_link('p1')
+        p1 = self.wn2.get_link("p1")
         self.assertAlmostEqual(p1.minor_loss, 0.73, 6)
 
     def test_tcv_valve(self):
-        v1 = self.wn2.get_link('v1')
-        self.assertEqual(v1.start_node_name, 'j1')
-        self.assertEqual(v1.end_node_name, 'j2')
+        v1 = self.wn2.get_link("v1")
+        self.assertEqual(v1.start_node_name, "j1")
+        self.assertEqual(v1.end_node_name, "j2")
         self.assertAlmostEqual(v1.diameter, 0.3048, 6)
-        self.assertEqual(v1.valve_type, 'TCV')
+        self.assertEqual(v1.valve_type, "TCV")
         self.assertAlmostEqual(v1.setting, 3.52, 6)
         self.assertAlmostEqual(v1.minor_loss, 0.54, 6)
 
     def test_pump(self):
-        p1 = self.wn2.get_link('pump1')
-        p11 = self.wn.get_link('pump1')
-        self.assertEqual(p1.start_node_name, 'j2')
-        self.assertEqual(p1.end_node_name, 'j3')
+        p1 = self.wn2.get_link("pump1")
+        p11 = self.wn.get_link("pump1")
+        self.assertEqual(p1.start_node_name, "j2")
+        self.assertEqual(p1.end_node_name, "j3")
         self.assertEqual(type(p1), self.wntr.network.elements.HeadPump)
-        self.assertEqual(p1.pump_curve_name, 'curve1')
+        self.assertEqual(p1.pump_curve_name, "curve1")
         self.assertAlmostEqual(p1.speed_timeseries.base_value, 1.2, 6)
         self.assertEqual(p1.speed_timeseries, p11.speed_timeseries)
-        self.assertEqual(p1.speed_timeseries.pattern_name, 'pattern1')
+        self.assertEqual(p1.speed_timeseries.pattern_name, "pattern1")
 
-        p2 = self.wn2.get_link('pump2')
+        p2 = self.wn2.get_link("pump2")
         self.assertEqual(type(p2), self.wntr.network.elements.PowerPump)
         self.assertAlmostEqual(p2._base_power, 16629.107, 2)
 
     def test_valve_setting_control(self):
-        control = self.wn2.get_control('control 1')
+        control = self.wn2.get_control("control 1")
         run_time = control._condition._threshold
-        self.assertAlmostEqual(run_time, 3600.0*3.4, 6)
+        self.assertAlmostEqual(run_time, 3600.0 * 3.4, 6)
         value = control.actions()[0]._value
         self.assertAlmostEqual(value, 0.82, 6)
 
-        control = self.wn2.get_control('control 2')
+        control = self.wn2.get_control("control 2")
         value = control.actions()[0]._value
         self.assertAlmostEqual(value, 1.8358, 3)
 
     def test_controls(self):
         for name, control in self.wn.controls():
             self.assertTrue(control._compare(self.wn2.get_control(name)))
-        
+
     def test_demands(self):
         # In EPANET, the [DEMANDS] section overrides demands specified in [JUNCTIONS]
-        expected_length = {'j1': 2, # DEMANDS duplicates demand in JUNCTIONS
-                           'j2': 2, # DEMANDS does not duplicate demand in JUNCTIONS
-                           'j3': 1, # Only in JUNCTIONS
-                           'j4': 1} # Only in DEMANDS
+        expected_length = {
+            "j1": 2,  # DEMANDS duplicates demand in JUNCTIONS
+            "j2": 2,  # DEMANDS does not duplicate demand in JUNCTIONS
+            "j3": 1,  # Only in JUNCTIONS
+            "j4": 1,
+        }  # Only in DEMANDS
         for j_name, j in self.wn.junctions():
             j2 = self.wn2.get_node(j_name)
             assert len(j.demand_timeseries_list) == len(j2.demand_timeseries_list)
@@ -82,21 +84,21 @@ def test_demands(self):
             for d, d2 in zip(j.demand_timeseries_list, j2.demand_timeseries_list):
                 self.assertEqual(d, d2)
                 # DEMANDS use pattern2, JUNCTIONS demands use pattern1
-                if j_name in ['j1', 'j2', 'j4']:    
-                    self.assertEqual(d2.pattern_name, 'pattern2') 
+                if j_name in ["j1", "j2", "j4"]:
+                    self.assertEqual(d2.pattern_name, "pattern2")
                 else:
-                    self.assertEqual(d2.pattern_name, 'pattern1') 
-                
+                    self.assertEqual(d2.pattern_name, "pattern1")
 
-class TestInpFileWriter(unittest.TestCase):
 
+class TestInpFileWriter(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
-        inp_file = join(test_datadir, 'Net6_plus.inp') # UNITS = GPM
+        inp_file = join(test_datadir, "Net6_plus.inp")  # UNITS = GPM
         self.wn = wntr.network.WaterNetworkModel(inp_file)
-        self.wn.write_inpfile('temp.inp', units='LPM')
+        self.wn.write_inpfile("temp.inp", units="LPM")
         self.wn2 = self.wntr.network.WaterNetworkModel(inp_file)
 
     @classmethod
@@ -110,13 +112,15 @@ def test_junctions(self):
         for name, node in self.wn.nodes(self.wntr.network.Junction):
             node2 = self.wn2.get_node(name)
             self.assertTrue(node._compare(node2))
-            #self.assertAlmostEqual(node.base_demand, node2.base_demand, 5)
+            # self.assertAlmostEqual(node.base_demand, node2.base_demand, 5)
 
     def test_reservoirs(self):
         for name, node in self.wn.nodes(self.wntr.network.Reservoir):
             node2 = self.wn2.get_node(name)
             self.assertTrue(node._compare(node2))
-            self.assertAlmostEqual(node.head_timeseries.base_value, node2.head_timeseries.base_value, 5)
+            self.assertAlmostEqual(
+                node.head_timeseries.base_value, node2.head_timeseries.base_value, 5
+            )
 
     def test_tanks(self):
         for name, node in self.wn.nodes(self.wntr.network.Tank):
@@ -157,10 +161,10 @@ def test_demands(self):
                 self.assertEqual(d, d2)
 
     ### TODO
-#    def test_controls(self):
-#        for name1, control1 in self.wn.controls.items():
-#            control2 = self.wn2._controls[name1]
-#            self.assertEqual(control1 == control2, True)
+    #    def test_controls(self):
+    #        for name1, control1 in self.wn.controls.items():
+    #            control2 = self.wn2._controls[name1]
+    #            self.assertEqual(control1 == control2, True)
 
     def test_options(self):
         options1 = self.wn.options
@@ -173,53 +177,52 @@ def test_controls(self):
 
 
 class TestInp22FileWriter(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
-        inp_file = join(test_datadir, 'Net6_plus.inp') # UNITS = GPM
+        inp_file = join(test_datadir, "Net6_plus.inp")  # UNITS = GPM
         self.wn = wntr.network.WaterNetworkModel(inp_file)
-        self.wn.options.hydraulic.demand_model = 'PDA'
+        self.wn.options.hydraulic.demand_model = "PDA"
         self.wn.options.hydraulic.required_pressure = 1.0
-        self.wn.write_inpfile('temp2.inp', units='LPM')
-        self.wn2 = self.wntr.network.WaterNetworkModel('temp2.inp')
+        self.wn.write_inpfile("temp2.inp", units="LPM")
+        self.wn2 = self.wntr.network.WaterNetworkModel("temp2.inp")
 
     @classmethod
     def tearDownClass(self):
         pass
 
     def test_pda(self):
-        self.assertTrue(self.wn2.options.hydraulic.demand_model == 'PDA')
+        self.assertTrue(self.wn2.options.hydraulic.demand_model == "PDA")
         self.assertTrue(self.wn.options.hydraulic.required_pressure == 1.0)
 
 
 class TestNet3InpWriterResults(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(ex_datadir, 'Net3.inp')
+        inp_file = join(ex_datadir, "Net3.inp")
         self.wn = self.wntr.network.WaterNetworkModel(inp_file)
 
         sim = self.wntr.sim.EpanetSimulator(self.wn)
         self.results = sim.run_sim()
 
-        self.wn.write_inpfile('temp.inp')
-        self.wn2 = self.wntr.network.WaterNetworkModel('temp.inp')
+        self.wn.write_inpfile("temp.inp")
+        self.wn2 = self.wntr.network.WaterNetworkModel("temp.inp")
 
         sim = self.wntr.sim.EpanetSimulator(self.wn2)
         self.results2 = sim.run_sim()
-        
-        self.wn.write_inpfile('temp.inp')
-        self.wn22 = self.wntr.network.WaterNetworkModel('temp.inp')
-        self.wn22.options.hydraulic.demand_model = 'PDA'
+
+        self.wn.write_inpfile("temp.inp")
+        self.wn22 = self.wntr.network.WaterNetworkModel("temp.inp")
+        self.wn22.options.hydraulic.demand_model = "PDA"
 
         sim = self.wntr.sim.EpanetSimulator(self.wn22)
         self.results22 = sim.run_sim(version=2.2)
-        
 
     @classmethod
     def tearDownClass(self):
@@ -227,44 +230,88 @@ def tearDownClass(self):
 
     def test_link_flowrate(self):
         for link_name, link in self.wn.links():
-            for t in self.results2.link['flowrate'].index:
-                self.assertLessEqual(abs(self.results2.link['flowrate'].loc[t,link_name] - self.results.link['flowrate'].loc[t,link_name]), 0.00001)
-                self.assertLessEqual(abs(self.results22.link['flowrate'].loc[t,link_name] - self.results.link['flowrate'].loc[t,link_name]), 0.00001)
+            for t in self.results2.link["flowrate"].index:
+                self.assertLessEqual(
+                    abs(
+                        self.results2.link["flowrate"].loc[t, link_name]
+                        - self.results.link["flowrate"].loc[t, link_name]
+                    ),
+                    0.00001,
+                )
+                self.assertLessEqual(
+                    abs(
+                        self.results22.link["flowrate"].loc[t, link_name]
+                        - self.results.link["flowrate"].loc[t, link_name]
+                    ),
+                    0.00001,
+                )
 
     def test_node_demand(self):
         for node_name, node in self.wn.nodes():
-            for t in self.results2.node['demand'].index:
-                self.assertAlmostEqual(self.results2.node['demand'].loc[t,node_name], self.results.node['demand'].loc[t,node_name], 4)
-                self.assertAlmostEqual(self.results22.node['demand'].loc[t,node_name], self.results.node['demand'].loc[t,node_name], 4)
+            for t in self.results2.node["demand"].index:
+                self.assertAlmostEqual(
+                    self.results2.node["demand"].loc[t, node_name],
+                    self.results.node["demand"].loc[t, node_name],
+                    4,
+                )
+                self.assertAlmostEqual(
+                    self.results22.node["demand"].loc[t, node_name],
+                    self.results.node["demand"].loc[t, node_name],
+                    4,
+                )
 
     def test_node_head(self):
         for node_name, node in self.wn.nodes():
-            for t in self.results2.node['head'].index:
-                self.assertLessEqual(abs(self.results2.node['head'].loc[t,node_name] - self.results.node['head'].loc[t,node_name]), 0.01)
-                self.assertLessEqual(abs(self.results22.node['head'].loc[t,node_name] - self.results.node['head'].loc[t,node_name]), 0.01)
+            for t in self.results2.node["head"].index:
+                self.assertLessEqual(
+                    abs(
+                        self.results2.node["head"].loc[t, node_name]
+                        - self.results.node["head"].loc[t, node_name]
+                    ),
+                    0.01,
+                )
+                self.assertLessEqual(
+                    abs(
+                        self.results22.node["head"].loc[t, node_name]
+                        - self.results.node["head"].loc[t, node_name]
+                    ),
+                    0.01,
+                )
 
     def test_node_pressure(self):
         for node_name, node in self.wn.nodes():
-            for t in self.results2.node['pressure'].index:
-                self.assertLessEqual(abs(self.results2.node['pressure'].loc[t,node_name] - self.results.node['pressure'].loc[t,node_name]), 0.05)
-                self.assertLessEqual(abs(self.results22.node['pressure'].loc[t,node_name] - self.results.node['pressure'].loc[t,node_name]), 0.05)
+            for t in self.results2.node["pressure"].index:
+                self.assertLessEqual(
+                    abs(
+                        self.results2.node["pressure"].loc[t, node_name]
+                        - self.results.node["pressure"].loc[t, node_name]
+                    ),
+                    0.05,
+                )
+                self.assertLessEqual(
+                    abs(
+                        self.results22.node["pressure"].loc[t, node_name]
+                        - self.results.node["pressure"].loc[t, node_name]
+                    ),
+                    0.05,
+                )
 
 
 class TestNet3InpUnitsResults(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(ex_datadir, 'Net3.inp')
+        inp_file = join(ex_datadir, "Net3.inp")
         self.wn = self.wntr.network.WaterNetworkModel(inp_file)
 
         sim = self.wntr.sim.EpanetSimulator(self.wn)
         self.results = sim.run_sim()
 
-        self.wn.write_inpfile('temp.inp', units='CMH')
-        self.wn2 = self.wntr.network.WaterNetworkModel('temp.inp')
+        self.wn.write_inpfile("temp.inp", units="CMH")
+        self.wn2 = self.wntr.network.WaterNetworkModel("temp.inp")
 
         sim = self.wntr.sim.EpanetSimulator(self.wn2)
         self.results2 = sim.run_sim()
@@ -275,8 +322,15 @@ def tearDownClass(self):
 
     def test_link_flowrate_units_convert(self):
         for link_name, link in self.wn.links():
-            for t in self.results2.link['flowrate'].index:
-                self.assertLessEqual(abs(self.results2.link['flowrate'].loc[t,link_name] - self.results.link['flowrate'].loc[t,link_name]), 0.00001)
-
-if __name__ == '__main__':
-    unittest.main()
\ No newline at end of file
+            for t in self.results2.link["flowrate"].index:
+                self.assertLessEqual(
+                    abs(
+                        self.results2.link["flowrate"].loc[t, link_name]
+                        - self.results.link["flowrate"].loc[t, link_name]
+                    ),
+                    0.00001,
+                )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_epanet_toolkit.py b/wntr/tests/test_epanet_toolkit.py
index b21009424..333304982 100644
--- a/wntr/tests/test_epanet_toolkit.py
+++ b/wntr/tests/test_epanet_toolkit.py
@@ -1,23 +1,91 @@
-from nose.tools import *
+import unittest
+from os.path import abspath, dirname, join, exists
+
 import wntr.epanet.toolkit
-from os.path import abspath, dirname, join
 
 testdir = dirname(abspath(__file__))
-datadir = join(testdir,'..','..','examples','networks')
+datadir = join(testdir, "..", "..", "examples", "networks")
+
+
+class TestEpanetToolkit(unittest.TestCase):
+    
+    def test_isOpen(self):
+        enData = wntr.epanet.toolkit.ENepanet()
+        enData.inpfile = join(datadir, "Net1.inp")
+        self.assertEqual(0, enData.isOpen())
+        enData.ENopen(enData.inpfile, "temp.rpt")
+        self.assertEqual(1, enData.isOpen())
 
-def test_isOpen():
-    enData = wntr.epanet.toolkit.ENepanet()
-    enData.inpfile = join(datadir,'Net1.inp')
-    assert_equal(0, enData.isOpen())
-    enData.ENopen(enData.inpfile,'temp.rpt')
-    assert_equal(1, enData.isOpen())
+    def test_ENgetcount(self):
+        enData = wntr.epanet.toolkit.ENepanet()
+        enData.inpfile = join(datadir, "Net1.inp")
+        enData.ENopen(enData.inpfile, "temp.rpt")
+        
+        nNodes = enData.ENgetcount(wntr.epanet.util.EN.NODECOUNT)
+        self.assertEqual(11, nNodes)
+        nLinks = enData.ENgetcount(wntr.epanet.util.EN.LINKCOUNT)
+        self.assertEqual(13, nLinks)
+    
+    def test_ENgetflowunits(self):
+        enData = wntr.epanet.toolkit.ENepanet()
+        enData.inpfile = join(datadir, "Net1.inp")
+        enData.ENopen(enData.inpfile, "temp.rpt")
+        
+        flowunit = enData.ENgetflowunits()
+        assert(flowunit==1) # GPM
+        
+    def test_ENgetindex_ENgetvalue(self):
+        enData = wntr.epanet.toolkit.ENepanet()
+        enData.inpfile = join(datadir, "Net1.inp")
+        enData.ENopen(enData.inpfile, "temp.rpt")
+        
+        node_index = enData.ENgetnodeindex('10') 
+        assert(node_index == 1) 
+        node_val = enData.ENgetnodevalue(node_index, 0) # ELEVATION = 0
+        assert(node_val == 710) 
+        
+        link_index = enData.ENgetlinkindex('11') 
+        assert(link_index == 2) 
+        link_val = enData.ENgetlinkvalue(link_index, 0) # DIAMETER = 0
+        assert(link_val == 14) 
+    
+    def test_ENsaveinpfile(self):
+        enData = wntr.epanet.toolkit.ENepanet()
+        enData.inpfile = join(datadir, "Net1.inp")
+        enData.ENopen(enData.inpfile, "temp.rpt")
+        
+        enData.ENsaveinpfile("Net1_toolkit.inp") 
+        file_exists = exists("Net1_toolkit.inp")
+        assert file_exists
+        
+    def test_runepanet(self):
+        inpfile = join(datadir, "Net1.inp")
+        wntr.epanet.toolkit.runepanet(inpfile, "test_runepanet.rpt", "test_runepanet.bin")
+                 
+        reader = wntr.epanet.io.BinFile()
+        results = reader.read("test_runepanet.bin")
+        
+        assert(isinstance(results, wntr.sim.results.SimulationResults))
+        assert(results.node['pressure'].shape == (25,11))
+        
+    def test_runepanet_step(self):
+        enData = wntr.epanet.toolkit.ENepanet()
+        enData.inpfile = join(datadir, "Net1.inp")
+        enData.ENopen(enData.inpfile, "test_runepanet_step.rpt", "test_runepanet_step.bin")
+        
+        enData.ENopenH()
+        enData.ENinitH(0)
+        t = 0
+        while True:
+            enData.ENrunH()
+            tstep = enData.ENnextH()
+            t = t + tstep
+            if (tstep <= 0):
+                break
+        
+        enData.ENcloseH()
 
-def test_ENgetcount():
-    enData = wntr.epanet.toolkit.ENepanet()
-    enData.inpfile = join(datadir,'Net1.inp')
-    enData.ENopen(enData.inpfile,'temp.rpt')
-    nNodes = enData.ENgetcount(wntr.epanet.util.EN.NODECOUNT)
-    assert_equal(11, nNodes)
-    nLinks = enData.ENgetcount(wntr.epanet.util.EN.LINKCOUNT)
-    assert_equal(13, nLinks)
+        assert (t == 86400)
 
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_epanet_units.py b/wntr/tests/test_epanet_units.py
index e0a2f68b9..d58df1ef4 100644
--- a/wntr/tests/test_epanet_units.py
+++ b/wntr/tests/test_epanet_units.py
@@ -1,163 +1,177 @@
-from nose.tools import *
+import unittest
 from os.path import abspath, dirname, join
+
 import numpy as np
 import wntr
-import unittest
 
 testdir = dirname(abspath(str(__file__)))
 
-def test_Concentration():
-    typestring = 'Concentration'
-    data_expected = 1 # kg/m3
-    data = 1000 # mg/L
-    for flowunit in range(10):
-        execute_test_qual(typestring, flowunit, data, data_expected)
 
-def test_Demand():
-    data_expected = 1.0 # m/s
-    for typestring in ['Demand', 'Flow']:
+class TestEpanetUnits(unittest.TestCase):
+    def test_Concentration(self):
+        typestring = "Concentration"
+        data_expected = 1  # kg/m3
+        data = 1000  # mg/L
+        for flowunit in range(10):
+            self.execute_check_qual(typestring, flowunit, data, data_expected)
+
+    def test_Demand(self):
+        data_expected = 1.0  # m/s
+        for typestring in ["Demand", "Flow"]:
+            for flowunit in range(10):
+                if flowunit == 0:
+                    data = 35.3146667  # ft3/s
+                elif flowunit == 1:
+                    data = 15850.3231  # gall/min
+                elif flowunit == 2:
+                    data = 22.8244653  # million gall/d
+                elif flowunit == 3:
+                    data = 19.0  # million imperial gall/d
+                elif flowunit == 4:
+                    data = 70.0456199  # acre-feet/day
+                elif flowunit == 5:
+                    data = 1000.0  # L/s
+                elif flowunit == 6:
+                    data = 60000.0  # L/min
+                elif flowunit == 7:
+                    data = 86.4  # million L/d
+                elif flowunit == 8:
+                    data = 3600.0  # m3/h
+                elif flowunit == 9:
+                    data = 86400.0  # m3/d
+                self.execute_check(typestring, flowunit, data, data_expected)
+
+    def test_emitter_coefficient(self):
+        pass
+
+    def test_Pipe_Diameter(self):
+        typestring = "PipeDiameter"
+        data_expected = 1.0  # m
+        for flowunit in range(10):
+            if flowunit in [0, 1, 2, 3, 4]:
+                data = 39.3701  # in
+            elif flowunit in [5, 6, 7, 8, 9]:
+                data = 1000  # mm
+            self.execute_check(typestring, flowunit, data, data_expected)
+
+    def test_Length(self):
+        data_expected = 1.0  # m
+        for typestring in ["TankDiameter", "Elevation", "HydraulicHead", "Length"]:
+            for flowunit in range(10):
+                if flowunit in [0, 1, 2, 3, 4]:
+                    data = 3.28084  # ft
+                elif flowunit in [5, 6, 7, 8, 9]:
+                    data = 1  # m
+                self.execute_check(typestring, flowunit, data, data_expected)
+
+    def test_Velocity(self):
+        typestring = "Velocity"
+        data_expected = 1.0  # m/s
+        for flowunit in range(10):
+            if flowunit in [0, 1, 2, 3, 4]:
+                data = 3.28084  # ft/s
+            elif flowunit in [5, 6, 7, 8, 9]:
+                data = 1  # m/s
+            self.execute_check(typestring, flowunit, data, data_expected)
+
+        # test list
+        data = np.array([10, 20, 30])  # ft/s
+        data_expected = [3.048, 6.096, 9.144]  # m/s
+        self.execute_check_list(typestring, 1, data, data_expected)
+
+    def test_Energy(self):
+        typestring = "Energy"
+        data_expected = 1000000  # J
+        data = 0.277777777778  # kW*hr
+        for flowunit in range(10):
+            self.execute_check(typestring, flowunit, data, data_expected)
+
+    def test_Power(self):
+        typestring = "Power"
+        data_expected = 1000  # W
+        for flowunit in range(10):
+            if flowunit in [0, 1, 2, 3, 4]:
+                data = 1.34102209  # hp
+            elif flowunit in [5, 6, 7, 8, 9]:
+                data = 1  # kW
+            self.execute_check(typestring, flowunit, data, data_expected)
+
+    def test_Pressure(self):
+        typestring = "Pressure"
+        data_expected = 1  # m
+        for flowunit in range(10):
+            if flowunit in [0, 1, 2, 3, 4]:
+                data = 1.421970206324753  # psi
+            elif flowunit in [5, 6, 7, 8, 9]:
+                data = 1  # m
+            self.execute_check(typestring, flowunit, data, data_expected)
+
+    def test_Source_Mass_Injection(self):
+        typestring = "SourceMassInject"
+        data_expected = 1  # kg/s
+        data = 6.0e7  # mg/min
+        for flowunit in range(10):
+            self.execute_check_qual(typestring, flowunit, data, data_expected)
+
+    def test_Volume(self):
+        typestring = "Volume"
+        data_expected = 1  # m3
         for flowunit in range(10):
-            if flowunit == 0:
-                data = 35.3146667 # ft3/s
-            elif flowunit == 1:
-                data = 15850.3231 # gall/min
-            elif flowunit == 2:
-                data = 22.8244653 # million gall/d
-            elif flowunit == 3:
-                data = 19. # million imperial gall/d
-            elif flowunit == 4:
-                data = 70.0456199 # acre-feet/day
-            elif flowunit == 5:
-                data = 1000. # L/s
-            elif flowunit == 6:
-                data = 60000. # L/min
-            elif flowunit == 7:
-                data = 86.4 # million L/d
-            elif flowunit == 8:
-                data = 3600. # m3/h
-            elif flowunit == 9:
-                data = 86400. # m3/d
-            execute_test(typestring, flowunit, data, data_expected)
-
-def testemitter_coefficient():
-    pass
-
-def test_Pipe_Diameter():
-    typestring = 'PipeDiameter'
-    data_expected = 1.0 # m
-    for flowunit in range(10):
-        if flowunit in [0,1,2,3,4]:
-            data = 39.3701 # in
-        elif flowunit in [5,6,7,8,9]:
-            data = 1000 # mm
-        execute_test(typestring, flowunit, data, data_expected)
-
-def test_Length():
-    data_expected = 1.0 # m
-    for typestring in ['TankDiameter', 'Elevation', 'HydraulicHead', 'Length']:
+            if flowunit in [0, 1, 2, 3, 4]:
+                data = 35.3147  # ft3
+            elif flowunit in [5, 6, 7, 8, 9]:
+                data = 1  # m3
+            self.execute_check(typestring, flowunit, data, data_expected)
+
+    def test_Water_Age(self):
+        typestring = "WaterAge"
+        data_expected = 1  # s
+        data = 0.000277778  # hr
         for flowunit in range(10):
-            if flowunit in [0,1,2,3,4]:
-                data = 3.28084 # ft
-            elif flowunit in [5,6,7,8,9]:
-                data = 1 # m
-            execute_test(typestring, flowunit, data, data_expected)
-
-def test_Velocity():
-    typestring = 'Velocity'
-    data_expected = 1.0 # m/s
-    for flowunit in range(10):
-        if flowunit in [0,1,2,3,4]:
-            data = 3.28084 # ft/s
-        elif flowunit in [5,6,7,8,9]:
-            data = 1 # m/s
-        execute_test(typestring, flowunit, data, data_expected)
-
-    # test list
-    data = np.array([10,20,30]) # ft/s
-    data_expected = [3.048,  6.096,  9.144] # m/s
-    execute_test_list(typestring, 1, data, data_expected)
-
-def test_Energy():
-    typestring = 'Energy'
-    data_expected = 1000000 # J
-    data = 0.277777777778 # kW*hr
-    for flowunit in range(10):
-        execute_test(typestring, flowunit, data, data_expected)
-
-def test_Power():
-    typestring = 'Power'
-    data_expected = 1000 # W
-    for flowunit in range(10):
-        if flowunit in [0,1,2,3,4]:
-            data = 1.34102209 # hp
-        elif flowunit in [5,6,7,8,9]:
-            data = 1 # kW
-        execute_test(typestring, flowunit, data, data_expected)
-
-def test_Pressure():
-    typestring = 'Pressure'
-    data_expected = 1 # m
-    for flowunit in range(10):
-        if flowunit in [0,1,2,3,4]:
-            data = 1.421970206324753 # psi
-        elif flowunit in [5,6,7,8,9]:
-            data = 1 # m
-        execute_test(typestring, flowunit, data, data_expected)
-
-def test_Source_Mass_Injection():
-    typestring = 'SourceMassInject'
-    data_expected = 1 # kg/s
-    data = 6.0e7 # mg/min
-    for flowunit in range(10):
-        execute_test_qual(typestring, flowunit, data, data_expected)
-
-def test_Volume():
-    typestring = 'Volume'
-    data_expected = 1 # m3
-    for flowunit in range(10):
-        if flowunit in [0,1,2,3,4]:
-            data = 35.3147 # ft3
-        elif flowunit in [5,6,7,8,9]:
-            data = 1 # m3
-        execute_test(typestring, flowunit, data, data_expected)
-
-def test_Water_Age():
-    typestring = 'WaterAge'
-    data_expected = 1 # s
-    data = 0.000277778 # hr
-    for flowunit in range(10):
-        execute_test_qual(typestring, flowunit, data, data_expected)
-
-@nottest
-def execute_test(typestring, flowunit, data, data_expected):
-#    data_convert = wntr.utils.convert(typestring, flowunit, data)
-    data_convert = wntr.epanet.util.HydParam[typestring]._to_si(wntr.epanet.util.FlowUnits(flowunit), data)
-    assert_less(abs((data_convert - data_expected)/float(data_expected)), 0.001)
-    data_convert = wntr.epanet.util.HydParam[typestring]._from_si(wntr.epanet.util.FlowUnits(flowunit), data_expected)
-    assert_less(abs((data_convert - data)/data), 0.001)
-
-@nottest
-def execute_test_list(typestring, flowunit, data, data_expected):
-#    data_convert = wntr.utils.convert(typestring, flowunit, data)
-    data_convert = wntr.epanet.util.HydParam[typestring]._to_si(wntr.epanet.util.FlowUnits(flowunit), data)
-    data_convert = [round(k,3) for k in data_convert]
-    assert_list_equal(data_convert, data_expected)
-
-@nottest
-def execute_test_qual(typestring, flowunit, data, data_expected):
-#    data_convert = wntr.utils.convert(typestring, flowunit, data)
-    data_convert = wntr.epanet.util.QualParam[typestring]._to_si(wntr.epanet.util.FlowUnits(flowunit), data)
-    assert_less(abs((data_convert - data_expected)/float(data_expected)), 0.001)
-    data_convert = wntr.epanet.util.QualParam[typestring]._from_si(wntr.epanet.util.FlowUnits(flowunit), data_expected)
-    assert_less(abs((data_convert - data)/data), 0.001)
-
-@nottest
-def execute_test_qual_list(typestring, flowunit, data, data_expected):
-#    data_convert = wntr.utils.convert(typestring, flowunit, data)
-    data_convert = wntr.epanet.util.QualParam[typestring]._to_si(wntr.epanet.util.FlowUnits(flowunit), data)
-    data_convert = [round(k,3) for k in data_convert]
-    assert_list_equal(data_convert, data_expected)
-
-if __name__ == '__main__':
-    test_Demand()
-    test_EN_Enum()
\ No newline at end of file
+            self.execute_check_qual(typestring, flowunit, data, data_expected)
+
+    def execute_check(self, typestring, flowunit, data, data_expected):
+        #    data_convert = wntr.utils.convert(typestring, flowunit, data)
+        data_convert = wntr.epanet.util.HydParam[typestring]._to_si(
+            wntr.epanet.util.FlowUnits(flowunit), data
+        )
+        self.assertLess(
+            abs((data_convert - data_expected) / float(data_expected)), 0.001
+        )
+        data_convert = wntr.epanet.util.HydParam[typestring]._from_si(
+            wntr.epanet.util.FlowUnits(flowunit), data_expected
+        )
+        self.assertLess(abs((data_convert - data) / data), 0.001)
+
+    def execute_check_list(self, typestring, flowunit, data, data_expected):
+        #    data_convert = wntr.utils.convert(typestring, flowunit, data)
+        data_convert = wntr.epanet.util.HydParam[typestring]._to_si(
+            wntr.epanet.util.FlowUnits(flowunit), data
+        )
+        data_convert = [round(k, 3) for k in data_convert]
+        self.assertListEqual(data_convert, data_expected)
+
+    def execute_check_qual(self, typestring, flowunit, data, data_expected):
+        #    data_convert = wntr.utils.convert(typestring, flowunit, data)
+        data_convert = wntr.epanet.util.QualParam[typestring]._to_si(
+            wntr.epanet.util.FlowUnits(flowunit), data
+        )
+        self.assertLess(
+            abs((data_convert - data_expected) / float(data_expected)), 0.001
+        )
+        data_convert = wntr.epanet.util.QualParam[typestring]._from_si(
+            wntr.epanet.util.FlowUnits(flowunit), data_expected
+        )
+        self.assertLess(abs((data_convert - data) / data), 0.001)
+
+    def execute_check_qual_list(self, typestring, flowunit, data, data_expected):
+        #    data_convert = wntr.utils.convert(typestring, flowunit, data)
+        data_convert = wntr.epanet.util.QualParam[typestring]._to_si(
+            wntr.epanet.util.FlowUnits(flowunit), data
+        )
+        data_convert = [round(k, 3) for k in data_convert]
+        self.assertListEqual(data_convert, data_expected)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_examples.py b/wntr/tests/test_examples.py
index 600b76589..8ece0b444 100644
--- a/wntr/tests/test_examples.py
+++ b/wntr/tests/test_examples.py
@@ -1,19 +1,20 @@
 # This is a test to ensure all of the examples run.
-import unittest
-import sys
 import os
+import sys
+import unittest
 from os import listdir
-from os.path import isfile, abspath, dirname, join
+from os.path import abspath, dirname, isfile, join
 from subprocess import call
 
 testdir = dirname(abspath(str(__file__)))
-examplesdir = join(testdir,'..','..','examples')
+examplesdir = join(testdir, "..", "..", "examples")
 
-class TestExamples(unittest.TestCase):
 
+class TestExamples(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
     @classmethod
@@ -23,24 +24,31 @@ def tearDownClass(self):
     def test_that_examples_run(self):
         cwd = os.getcwd()
         os.chdir(examplesdir)
-        example_files = [f for f in listdir(examplesdir) if isfile(join(examplesdir,f)) and f.endswith('.py') and not f.startswith('test')]
+        example_files = [
+            f
+            for f in listdir(examplesdir)
+            if isfile(join(examplesdir, f))
+            and f.endswith(".py")
+            and not f.startswith("test")
+        ]
         flag = 0
         failed_examples = []
         for f in example_files:
-            if 'sensor_placement' in f:
+            if "sensor_placement" in f:
                 try:
                     import chama
                 except ImportError:
                     continue
-            tmp_flag = call([sys.executable, join(examplesdir,f)])
+            tmp_flag = call([sys.executable, join(examplesdir, f)])
             print(f, tmp_flag)
             if tmp_flag == 1:
                 failed_examples.append(f)
                 flag = 1
         os.chdir(cwd)
         if len(failed_examples) > 0:
-            print('failed examples: {0}'.format(failed_examples))
-        self.assertEqual(flag,0)
+            print("failed examples: {0}".format(failed_examples))
+        self.assertEqual(flag, 0)
+
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/wntr/tests/test_graphics.py b/wntr/tests/test_graphics.py
index da4a2537c..4390ee5a7 100644
--- a/wntr/tests/test_graphics.py
+++ b/wntr/tests/test_graphics.py
@@ -1,221 +1,228 @@
-from nose.tools import *
-from os.path import abspath, dirname, join, isfile
-import os, sys
-import wntr
+
+import os
+import sys
+import unittest
+import warnings
+from os.path import abspath, dirname, isfile, join
+
 import matplotlib.pylab as plt
+import wntr
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir,'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
-
-def test_plot_network1():
-    filename = abspath(join(testdir, 'plot_network1.png'))
-    if isfile(filename):
-        os.remove(filename)
-    
-    inp_file = join(ex_datadir,'Net6.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-		
-    plt.figure()
-    wntr.graphics.plot_network(wn)
-    plt.savefig(filename, format='png')
-    plt.close()
-    
-    assert_true(isfile(filename))
-
-def test_plot_network2():
-    filename = abspath(join(testdir, 'plot_network2.png'))
-    if isfile(filename):
-        os.remove(filename)
-    
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-		
-    plt.figure()
-    wntr.graphics.plot_network(wn, node_attribute='elevation', link_attribute='length')
-    plt.savefig(filename, format='png')
-    plt.close()
-    
-    assert_true(isfile(filename))
-
-def test_plot_network3():
-    filename = abspath(join(testdir, 'plot_network3.png'))
-    if isfile(filename):
-        os.remove(filename)
-    
-    inp_file = join(ex_datadir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-		
-    plt.figure()
-    wntr.graphics.plot_network(wn, node_attribute=['11', '21'], link_attribute=['112', '113'], link_labels=True)
-    plt.savefig(filename, format='png')
-    plt.close()
-    
-    assert_true(isfile(filename))
-    
-def test_plot_network4():
-    filename = abspath(join(testdir, 'plot_network4.png'))
-    if isfile(filename):
-        os.remove(filename)
-    
-    inp_file = join(ex_datadir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-		
-    plt.figure()
-    wntr.graphics.plot_network(wn, node_attribute={'11': 5, '21': 10}, link_attribute={'112': 3, '113': 9}, node_labels=True)
-    plt.savefig(filename, format='png')
-    plt.close()
-    
-    assert_true(isfile(filename))
-
-def test_plot_network5():
-    filename = abspath(join(testdir, 'plot_network5.png'))
-    if isfile(filename):
-        os.remove(filename)
-
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    pop = wntr.metrics.population(wn)
-    
-    plt.figure()
-    wntr.graphics.plot_network(wn, node_attribute=pop, node_range=[0,500], title='Population')
-    plt.savefig(filename, format='png')
-    plt.close()
-    
-    assert_true(isfile(filename))
-
-def test_plot_network6():
-    filename = abspath(join(testdir, 'plot_network6.png'))
-    if isfile(filename):
-        os.remove(filename)
-
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim()
-    flowrate_at_5hr = results.link['flowrate'].loc[5*3600, :]*100
-
-    plt.figure(figsize=(15,10))
-    ax = plt.gca()
-    wntr.graphics.plot_network(wn, node_attribute='elevation', link_attribute=flowrate_at_5hr, 
-                               node_size=7, directed=True, node_colorbar_label='Elevation', 
-                               link_colorbar_label='Flowrate', ax=ax)
-    plt.savefig(filename, format='png')
-    plt.close()
-    
-    assert_true(isfile(filename))
-    
-def test_plot_interactive_network1():
-    
-    filename = abspath(join(testdir, 'plot_interactive_network1.html'))
-    if isfile(filename):
-        os.remove(filename)
-        
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-		
-    plt.figure()
-    wntr.graphics.plot_interactive_network(wn, node_attribute=['107', '123'],  
-                                       filename=filename, auto_open=False)
-    
-    assert_true(isfile(filename))
-
-def test_plot_leaflet_network1():
-
-    filename = abspath(join(testdir, 'plot_leaflet_network1.html'))
-    if isfile(filename):
-        os.remove(filename)
-        
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    longlat_map = {'Lake':(-106.6587, 35.0623), 
-                   '219': (-106.5248, 35.1918)}
-    wn2 = wntr.morph.convert_node_coordinates_to_longlat(wn, longlat_map)
-    
-    plt.figure()
-    wntr.graphics.plot_leaflet_network(wn2, node_attribute='elevation', 
-                                       link_attribute='length', add_legend=True, filename=filename)
-    
-    assert_true(isfile(filename))
-    
-def test_network_animation1():
-    filename = abspath(join(testdir, 'plot_leaflet_network1.html'))
-    if isfile(filename):
-        os.remove(filename)
-        
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim()
-    
-    pressure = results.node['pressure']
-    flowrate = results.link['flowrate']
-    anim = wntr.graphics.network_animation(wn, node_attribute=pressure, 
-                                           link_attribute=flowrate, repeat=True)
-    
-    from matplotlib.animation import FuncAnimation
-    assert_true(isinstance(anim, FuncAnimation))
-    
-def test_plot_fragility_curve1():
-    from scipy.stats import lognorm
-    filename = abspath(join(testdir, 'plot_fragility_curve1.png'))
-    if isfile(filename):
-        os.remove(filename)
-        
-    FC = wntr.scenario.FragilityCurve()
-    FC.add_state('Minor', 1, {'Default': lognorm(0.5,scale=0.3)})
-    FC.add_state('Major', 2, {'Default': lognorm(0.5,scale=0.7)}) 
-    
-    plt.figure()
-    wntr.graphics.plot_fragility_curve(FC)
-    plt.savefig(filename, format='png')
-    plt.close()
-    
-    assert_true(isfile(filename))
-    
-def test_plot_pump_curve1():
-    filename = abspath(join(testdir, 'plot_pump_curve1.png'))
-    if isfile(filename):
-        os.remove(filename)
-        
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    pump = wn.get_link('10')
-    
-    plt.figure()
-    wntr.graphics.plot_pump_curve(pump)
-    plt.savefig(filename, format='png')
-    plt.close()
-    
-    assert_true(isfile(filename))
-    
-def test_plot_tank_curve():
-    filename = abspath(join(testdir, 'plot_tank_curve.png'))
-    if isfile(filename):
-        os.remove(filename)
-        
-    inp_file = join(test_datadir,'Anytown_multipointcurves.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    tank_w_curve = wn.get_node('41')
-    tank_no_curve = wn.get_node('42')
-    
-    plt.figure()
-    shouldBeAxis = wntr.graphics.plot_tank_volume_curve(tank_w_curve)
-    plt.savefig(filename, format='png')
-    plt.close()
-    
-    assert_true(isfile(filename))
-    
-    shouldBeNone = wntr.graphics.plot_tank_volume_curve(tank_no_curve)
-    assert_true(shouldBeNone is None)
-
-def test_custom_colormap():
-    cmp = wntr.graphics.custom_colormap(3, colors=['blue','white','red'], name='custom')
-    assert_equal(cmp.N,3)
-    assert_equal(cmp.name,'custom')
-    
-if __name__ == '__main__':
-    test_network_animation1()
-    test_plot_tank_curve()
-    test_plot_network6()
-    
\ No newline at end of file
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
+
+
+class TestGraphics(unittest.TestCase):
+    def test_plot_network1(self):
+        filename = abspath(join(testdir, "plot_network1.png"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(ex_datadir, "Net6.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        plt.figure()
+        wntr.graphics.plot_network(wn)
+        plt.savefig(filename, format="png")
+        plt.close()
+
+        self.assertTrue(isfile(filename))
+
+    def test_plot_network2(self):
+        filename = abspath(join(testdir, "plot_network2.png"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        plt.figure()
+        wntr.graphics.plot_network(
+            wn, node_attribute="elevation", link_attribute="length"
+        )
+        plt.savefig(filename, format="png")
+        plt.close()
+
+        self.assertTrue(isfile(filename))
+
+    def test_plot_network3(self):
+        filename = abspath(join(testdir, "plot_network3.png"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(ex_datadir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        plt.figure()
+        wntr.graphics.plot_network(
+            wn,
+            node_attribute=["11", "21"],
+            link_attribute=["112", "113"],
+            link_labels=True,
+        )
+        plt.savefig(filename, format="png")
+        plt.close()
+
+        self.assertTrue(isfile(filename))
+
+    def test_plot_network4(self):
+        filename = abspath(join(testdir, "plot_network4.png"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(ex_datadir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        plt.figure()
+        wntr.graphics.plot_network(
+            wn,
+            node_attribute={"11": 5, "21": 10},
+            link_attribute={"112": 3, "113": 9},
+            node_labels=True,
+        )
+        plt.savefig(filename, format="png")
+        plt.close()
+
+        self.assertTrue(isfile(filename))
+
+    def test_plot_network5(self):
+        filename = abspath(join(testdir, "plot_network5.png"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        pop = wntr.metrics.population(wn)
+
+        plt.figure()
+        wntr.graphics.plot_network(
+            wn, node_attribute=pop, node_range=[0, 500], title="Population"
+        )
+        plt.savefig(filename, format="png")
+        plt.close()
+
+        self.assertTrue(isfile(filename))
+
+    def test_plot_interactive_network1(self):
+
+        filename = abspath(join(testdir, "plot_interactive_network1.html"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        plt.figure()
+        wntr.graphics.plot_interactive_network(
+            wn, node_attribute=["107", "123"], filename=filename, auto_open=False
+        )
+
+        self.assertTrue(isfile(filename))
+
+    def test_plot_leaflet_network1(self):
+
+        filename = abspath(join(testdir, "plot_leaflet_network1.html"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        longlat_map = {"Lake": (-106.6587, 35.0623), "219": (-106.5248, 35.1918)}
+        wn2 = wntr.morph.convert_node_coordinates_to_longlat(wn, longlat_map)
+
+        plt.figure()
+        wntr.graphics.plot_leaflet_network(
+            wn2,
+            node_attribute="elevation",
+            link_attribute="length",
+            add_legend=True,
+            filename=filename,
+        )
+
+        self.assertTrue(isfile(filename))
+
+    def test_network_animation1(self):
+        filename = abspath(join(testdir, "plot_leaflet_network1.html"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim()
+
+        pressure = results.node["pressure"]
+        flowrate = results.link["flowrate"]
+        anim = wntr.graphics.network_animation(
+            wn, node_attribute=pressure, link_attribute=flowrate, repeat=True
+        )
+
+        from matplotlib.animation import FuncAnimation
+
+        self.assertTrue(isinstance(anim, FuncAnimation))
+
+    def test_plot_fragility_curve1(self):
+        from scipy.stats import lognorm
+
+        filename = abspath(join(testdir, "plot_fragility_curve1.png"))
+        if isfile(filename):
+            os.remove(filename)
+
+        FC = wntr.scenario.FragilityCurve()
+        FC.add_state("Minor", 1, {"Default": lognorm(0.5, scale=0.3)})
+        FC.add_state("Major", 2, {"Default": lognorm(0.5, scale=0.7)})
+
+        plt.figure()
+        wntr.graphics.plot_fragility_curve(FC)
+        plt.savefig(filename, format="png")
+        plt.close()
+
+        self.assertTrue(isfile(filename))
+
+    def test_plot_pump_curve1(self):
+        filename = abspath(join(testdir, "plot_pump_curve1.png"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        pump = wn.get_link("10")
+
+        plt.figure()
+        wntr.graphics.plot_pump_curve(pump)
+        plt.savefig(filename, format="png")
+        plt.close()
+
+        self.assertTrue(isfile(filename))
+
+    def test_plot_tank_curve(self):
+        filename = abspath(join(testdir, "plot_tank_curve.png"))
+        if isfile(filename):
+            os.remove(filename)
+
+        inp_file = join(test_datadir, "Anytown_multipointcurves.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        tank_w_curve = wn.get_node("41")
+        tank_no_curve = wn.get_node("42")
+
+        plt.figure()
+        shouldBeAxis = wntr.graphics.plot_tank_volume_curve(tank_w_curve)
+        plt.savefig(filename, format="png")
+        plt.close()
+
+        self.assertTrue(isfile(filename))
+
+        shouldBeNone = wntr.graphics.plot_tank_volume_curve(tank_no_curve)
+        self.assertTrue(shouldBeNone is None)
+
+    def test_custom_colormap(self):
+        cmp = wntr.graphics.custom_colormap(
+            3, colors=["blue", "white", "red"], name="custom"
+        )
+        self.assertEqual(cmp.N, 3)
+        self.assertEqual(cmp.name, "custom")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_metrics_demand.py b/wntr/tests/test_metrics_demand.py
index 77ae44831..32d47b32a 100644
--- a/wntr/tests/test_metrics_demand.py
+++ b/wntr/tests/test_metrics_demand.py
@@ -1,66 +1,92 @@
 from __future__ import division
-from nose.tools import *
+
+import unittest
 from os.path import abspath, dirname, join
+
 import pandas as pd
-from pandas.util.testing import assert_frame_equal, assert_series_equal
 import wntr
+from pandas.testing import assert_frame_equal, assert_series_equal
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'networks_for_testing')
-net3dir = join(testdir,'..','..','examples','networks')
-
-
-def test_expected_demand_net3_node101():
-    inp_file = join(net3dir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    expected_demand = wntr.metrics.hydraulic.expected_demand(wn)
-    ave_expected_demand = wntr.metrics.hydraulic.average_expected_demand(wn)
-    
-    ex_demand_101 = expected_demand['101']
-    ave_ex_demand_101 = ave_expected_demand['101']
-    
-    expected = 0.012813608
-    error = abs((ex_demand_101.mean() - expected)/expected)
-    assert_less(error, 0.01) # 1% error
-    
-    error = abs((ave_ex_demand_101 - expected)/expected)
-    assert_less(error, 0.01) # 1% error
-
-def test_expected_demand_category():
-    inp_file = join(net3dir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    node = wn.get_node('123')
-    node.demand_timeseries_list[0].category='A'
-    
-    ave_expected_demand = wntr.metrics.hydraulic.average_expected_demand(wn)
-    ave_expected_demand_A = wntr.metrics.hydraulic.average_expected_demand(wn, category='A')
-    
-    error = abs(ave_expected_demand['123'] - ave_expected_demand_A['123'])
-    assert_less(error, 1e-7) 
-    
-    error = abs(ave_expected_demand_A.sum() - ave_expected_demand_A['123']) # all other entries are 0
-    assert_less(error, 1e-7) 
-    
-def test_wsa():
-    
-    expected_demand = pd.DataFrame(data=[[12,2],[3,4],[5,10]], columns=['A', 'B'], index=[0,1,2])
-    demand = pd.DataFrame(data=[[5,2],[3,2],[3,4]], columns=['A', 'B'], index=[0,1,2])
-    
-    # WSA at each junction and time
-    wsa = wntr.metrics.hydraulic.water_service_availability(expected_demand, demand)
-    expected = pd.DataFrame(data=[[5/12,2/2],[3/3,2/4],[3/5,4/10]], columns=['A', 'B'], index=[0,1,2])
-    assert_frame_equal(wsa, expected, check_dtype=False)
-
-    # WSA at each junction
-    wsa = wntr.metrics.hydraulic.water_service_availability(expected_demand.sum(axis=0), demand.sum(axis=0))
-    expected = pd.Series(data=[(5+3+3)/(12+3+5), (2+2+4)/(2+4+10)], index=['A', 'B'])
-    assert_series_equal(wsa, expected, check_dtype=False)
-
-    # WSA at each time
-    wsa = wntr.metrics.hydraulic.water_service_availability(expected_demand.sum(axis=1), demand.sum(axis=1))
-    expected = pd.Series(data=[(5+2)/(12+2), (3+2)/(3+4),(3+4)/(5+10)], index=[0,1,2])
-    assert_series_equal(wsa, expected, check_dtype=False)
-
-if __name__ == '__main__':
-    test_wsa()
+datadir = join(testdir, "networks_for_testing")
+net3dir = join(testdir, "..", "..", "examples", "networks")
+
+
+class TestMetricsDemand(unittest.TestCase):
+    def test_expected_demand_net3_node101(self):
+        inp_file = join(net3dir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        expected_demand = wntr.metrics.hydraulic.expected_demand(wn)
+        ave_expected_demand = wntr.metrics.hydraulic.average_expected_demand(wn)
+
+        ex_demand_101 = expected_demand["101"]
+        ave_ex_demand_101 = ave_expected_demand["101"]
+
+        expected = 0.012813608
+        error = abs((ex_demand_101.mean() - expected) / expected)
+        self.assertLess(error, 0.01)  # 1% error
+
+        error = abs((ave_ex_demand_101 - expected) / expected)
+        self.assertLess(error, 0.01)  # 1% error
+
+    def test_expected_demand_category(self):
+        inp_file = join(net3dir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        node = wn.get_node("123")
+        node.demand_timeseries_list[0].category = "A"
+
+        ave_expected_demand = wntr.metrics.hydraulic.average_expected_demand(wn)
+        ave_expected_demand_A = wntr.metrics.hydraulic.average_expected_demand(
+            wn, category="A"
+        )
+
+        error = abs(ave_expected_demand["123"] - ave_expected_demand_A["123"])
+        self.assertLess(error, 1e-7)
+
+        error = abs(
+            ave_expected_demand_A.sum() - ave_expected_demand_A["123"]
+        )  # all other entries are 0
+        self.assertLess(error, 1e-7)
+
+    def test_wsa(self):
+
+        expected_demand = pd.DataFrame(
+            data=[[12, 2], [3, 4], [5, 10]], columns=["A", "B"], index=[0, 1, 2]
+        )
+        demand = pd.DataFrame(
+            data=[[5, 2], [3, 2], [3, 4]], columns=["A", "B"], index=[0, 1, 2]
+        )
+
+        # WSA at each junction and time
+        wsa = wntr.metrics.hydraulic.water_service_availability(expected_demand, demand)
+        expected = pd.DataFrame(
+            data=[[5 / 12, 2 / 2], [3 / 3, 2 / 4], [3 / 5, 4 / 10]],
+            columns=["A", "B"],
+            index=[0, 1, 2],
+        )
+        assert_frame_equal(wsa, expected, check_dtype=False)
+
+        # WSA at each junction
+        wsa = wntr.metrics.hydraulic.water_service_availability(
+            expected_demand.sum(axis=0), demand.sum(axis=0)
+        )
+        expected = pd.Series(
+            data=[(5 + 3 + 3) / (12 + 3 + 5), (2 + 2 + 4) / (2 + 4 + 10)],
+            index=["A", "B"],
+        )
+        assert_series_equal(wsa, expected, check_dtype=False)
+
+        # WSA at each time
+        wsa = wntr.metrics.hydraulic.water_service_availability(
+            expected_demand.sum(axis=1), demand.sum(axis=1)
+        )
+        expected = pd.Series(
+            data=[(5 + 2) / (12 + 2), (3 + 2) / (3 + 4), (3 + 4) / (5 + 10)],
+            index=[0, 1, 2],
+        )
+        assert_series_equal(wsa, expected, check_dtype=False)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_metrics_economic.py b/wntr/tests/test_metrics_economic.py
index 8944e99ad..bf417b021 100644
--- a/wntr/tests/test_metrics_economic.py
+++ b/wntr/tests/test_metrics_economic.py
@@ -1,37 +1,38 @@
-from nose.tools import *
+import unittest
 from os.path import abspath, dirname, join
+
 import pandas as pd
-from pandas.util.testing import assert_frame_equal, assert_series_equal
 import wntr
+from pandas.testing import assert_frame_equal, assert_series_equal
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'networks_for_testing')
-netdir = join(testdir,'..','..','examples','networks')
-
-
-def test_annual_network_cost1():
-    inp_file = join(netdir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    cost = wntr.metrics.annual_network_cost(wn)
-    assert_almost_equal(cost, 460147, 0) 
-
-def test_annual_network_cost2():
-    # Network cost using a tank volume curve
-    inp_file = join(datadir,'Anytown_multipointcurves.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    cost = wntr.metrics.annual_network_cost(wn)    
-    assert_almost_equal(cost, 1201467.78, 0)
-    
-def test_annual_ghg_emissions():
-    inp_file = join(netdir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    cost = wntr.metrics.annual_ghg_emissions(wn)
-    assert_almost_equal(cost, 410278, 0) 
-    
-if __name__ == '__main__':
-    test_annual_network_cost1()
-    test_annual_network_cost2()
-    test_annual_ghg_emissions()
\ No newline at end of file
+datadir = join(testdir, "networks_for_testing")
+netdir = join(testdir, "..", "..", "examples", "networks")
+
+
+class TestEconomicMetrics(unittest.TestCase):
+    def test_annual_network_cost1(self):
+        inp_file = join(netdir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        cost = wntr.metrics.annual_network_cost(wn)
+        self.assertAlmostEqual(cost, 460147, 0)
+
+    def test_annual_network_cost2(self):
+        # Network cost using a tank volume curve
+        inp_file = join(datadir, "Anytown_multipointcurves.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        cost = wntr.metrics.annual_network_cost(wn)
+        self.assertAlmostEqual(cost, 1201467.78, 0)
+
+    def test_annual_ghg_emissions(self):
+        inp_file = join(netdir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        cost = wntr.metrics.annual_ghg_emissions(wn)
+        self.assertAlmostEqual(cost, 410278, 0)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_metrics_entropy.py b/wntr/tests/test_metrics_entropy.py
index 8e2eaf951..051811c54 100644
--- a/wntr/tests/test_metrics_entropy.py
+++ b/wntr/tests/test_metrics_entropy.py
@@ -1,84 +1,91 @@
-from nose.tools import *
+import unittest
 from os.path import abspath, dirname, join
+
 import numpy as np
 import wntr
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir, 'networks_for_testing')
-
-def test_layout1():
-    inp_file = join(datadir,'Awumah_layout1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    attr = {'1': 940.7,
-            '2': 550.0,
-            '3': 659.3,
-            '4': 290.7,
-            '5': 400.0,
-            '6': 59.3,
-            '7': 450.0,
-            '8': 200.0,
-            '9': 300.0,
-            '10': 250.0,
-            '11': 100.0,
-            '12': 150.0}
-
-    G_flowrate = wn.get_graph(link_weight=attr, modify_direction=True)
-
-    [S, S_ave] = wntr.metrics.entropy(G_flowrate)
-    
-    # The values in the paper are different, perhaps due to significant figure
-    # rounding during the calculation
-    expected_Saverage = 0.0805 # 0.088
-    error = abs((S.mean() - expected_Saverage)/expected_Saverage)
-    assert_less(error, 0.05) # 5% error
-
-    expected_Smax = 0.5108 # 0.5130
-    error = abs((S.max() - expected_Smax)/expected_Smax)
-
-    expected_S_ave = 2.289 # 2.280
-    error = abs((S_ave - expected_S_ave)/expected_S_ave)
-    assert_less(error, 0.05) # 5% error
-
-
-def test_layout8():
-    inp_file = join(datadir,'Awumah_layout8.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    attr = {'1': 678.8,
-            '2': 403.3,
-            '3': 921.2,
-            '4': 175.5,
-            '5': 253.3,
-            '6': 227.1,
-            '7': 126.3,
-            '8': 544.1,
-            '9': 126.3,
-            '10': 279.6,
-            '11': 200.0,
-            '12': 144.1,
-            '13': 126.3,
-            '14': 79.6,
-            '15': 44.1,
-            '16': 20.4}
-
-    G_flowrate = wn.get_graph(link_weight=attr, modify_direction=True)
-
-    [S, S_ave] = wntr.metrics.entropy(G_flowrate)
-    
-    # The values in the paper are different, perhaps due to significant figure
-    # rounding during the calculation
-    expected_Saverage = 0.4391 # 0.3860
-    error = abs((S.mean() - expected_Saverage)/expected_Saverage)
-    assert_less(error, 0.05) # 5% error
-
-    expected_Smax = 1.1346 # 0.5130
-    error = abs((S.max() - expected_Smax)/expected_Smax)
-    assert_less(error, 0.05) # 5% error
-
-    expected_S_ave = 2.544 # 2.670
-    error = abs((S_ave - expected_S_ave)/expected_S_ave)
-    assert_less(error, 0.05) # 5% error
-
-if __name__ == '__main__':
-    test_layout8()
+datadir = join(testdir, "networks_for_testing")
+
+
+class TestEntropyMetric(unittest.TestCase):
+    def test_layout1(self):
+        inp_file = join(datadir, "Awumah_layout1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        attr = {
+            "1": 940.7,
+            "2": 550.0,
+            "3": 659.3,
+            "4": 290.7,
+            "5": 400.0,
+            "6": 59.3,
+            "7": 450.0,
+            "8": 200.0,
+            "9": 300.0,
+            "10": 250.0,
+            "11": 100.0,
+            "12": 150.0,
+        }
+
+        G_flowrate = wn.get_graph(link_weight=attr, modify_direction=True)
+
+        [S, S_ave] = wntr.metrics.entropy(G_flowrate)
+
+        # The values in the paper are different, perhaps due to significant figure
+        # rounding during the calculation
+        expected_Saverage = 0.0805  # 0.088
+        error = abs((S.mean() - expected_Saverage) / expected_Saverage)
+        self.assertLess(error, 0.05)  # 5% error
+
+        expected_Smax = 0.5108  # 0.5130
+        error = abs((S.max() - expected_Smax) / expected_Smax)
+
+        expected_S_ave = 2.289  # 2.280
+        error = abs((S_ave - expected_S_ave) / expected_S_ave)
+        self.assertLess(error, 0.05)  # 5% error
+
+    def test_layout8(self):
+        inp_file = join(datadir, "Awumah_layout8.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        attr = {
+            "1": 678.8,
+            "2": 403.3,
+            "3": 921.2,
+            "4": 175.5,
+            "5": 253.3,
+            "6": 227.1,
+            "7": 126.3,
+            "8": 544.1,
+            "9": 126.3,
+            "10": 279.6,
+            "11": 200.0,
+            "12": 144.1,
+            "13": 126.3,
+            "14": 79.6,
+            "15": 44.1,
+            "16": 20.4,
+        }
+
+        G_flowrate = wn.get_graph(link_weight=attr, modify_direction=True)
+
+        [S, S_ave] = wntr.metrics.entropy(G_flowrate)
+
+        # The values in the paper are different, perhaps due to significant figure
+        # rounding during the calculation
+        expected_Saverage = 0.4391  # 0.3860
+        error = abs((S.mean() - expected_Saverage) / expected_Saverage)
+        self.assertLess(error, 0.05)  # 5% error
+
+        expected_Smax = 1.1346  # 0.5130
+        error = abs((S.max() - expected_Smax) / expected_Smax)
+        self.assertLess(error, 0.05)  # 5% error
+
+        expected_S_ave = 2.544  # 2.670
+        error = abs((S_ave - expected_S_ave) / expected_S_ave)
+        self.assertLess(error, 0.05)  # 5% error
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_metrics_healthimpacts.py b/wntr/tests/test_metrics_healthimpacts.py
index adf2e62fa..3a0d1e3c8 100644
--- a/wntr/tests/test_metrics_healthimpacts.py
+++ b/wntr/tests/test_metrics_healthimpacts.py
@@ -1,101 +1,123 @@
-from nose.tools import *
+import unittest
 from os.path import abspath, dirname, join
+
 import wntr
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'networks_for_testing')
-netdir = join(testdir,'..','..','examples','networks')
-
-
-"""
-Compare the following results to WST impact files using TSG file
-121          SETPOINT      100000          0                86400
-"""
-def test_mass_consumed():
-    inp_file = join(netdir,'Net3.inp')
-
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    wn.options.quality.parameter = 'CHEMICAL'
-    newpat = wntr.network.elements.Pattern.binary_pattern('NewPattern', 0, 24*3600, wn.options.time.pattern_timestep, wn.options.time.duration)
-    wn.add_pattern(newpat.name, newpat)
-    wn.add_source('Source1', '121', 'SETPOINT', 100, 'NewPattern')
-
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim(version=2.0)
-
-    demand = results.node['demand'].loc[:,wn.junction_name_list]
-    quality = results.node['quality'].loc[:,wn.junction_name_list]
-    
-    MC = wntr.metrics.mass_contaminant_consumed(demand, quality)
-    MC_timeseries = MC.sum(axis=1)
-    MC_cumsum = MC_timeseries.cumsum()
-    #MC_timeseries.to_csv('MC.txt')
-
-    expected = float(39069900000/1000000) # hour 2
-    error = abs((MC_cumsum[2*3600] - expected)/expected)
-    assert_less(error, 0.01) # 1% error
-
-    expected = float(1509440000000/1000000) # hour 12
-    error = abs((MC_cumsum[12*3600] - expected)/expected)
-    assert_less(error, 0.01) # 1% error
-
-def test_volume_consumed():
-
-    inp_file = join(netdir,'Net3.inp')
-
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    wn.options.quality.parameter = 'CHEMICAL'
-    newpat = wntr.network.elements.Pattern.binary_pattern('NewPattern', 0, 24*3600, wn.options.time.pattern_timestep, wn.options.time.duration)
-    wn.add_pattern(newpat.name, newpat)
-    wn.add_source('Source1', '121', 'SETPOINT', 100, 'NewPattern')
-
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim(version=2.0)
-
-    demand = results.node['demand'].loc[:,wn.junction_name_list]
-    quality = results.node['quality'].loc[:,wn.junction_name_list]
-    
-    VC = wntr.metrics.volume_contaminant_consumed(demand, quality, 0)
-    VC_timeseries = VC.sum(axis=1)
-    VC_cumsum = VC_timeseries.cumsum()
-    #VC_timeseries.to_csv('VC.txt')
-
-    expected = float(156760/264.172) # hour 2, convert gallons to m3
-    error = abs((VC_cumsum[2*3600] - expected)/expected)
-    assert_less(error, 0.02) # 2% error
-
-    expected = float(4867920/264.172) # hour 12, convert gallons to m3
-    error = abs((VC_cumsum[12*3600] - expected)/expected)
-    assert_less(error, 0.01) # 1% error
-
-def test_extent_contaminated():
-
-    inp_file = join(netdir,'Net3.inp')
-
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    wn.options.quality.parameter = 'CHEMICAL'
-    newpat = wntr.network.elements.Pattern.binary_pattern('NewPattern', 0, 24*3600, wn.options.time.pattern_timestep, wn.options.time.duration)
-    wn.add_pattern(newpat.name, newpat)
-    wn.add_source('Source1', '121', 'SETPOINT', 100, 'NewPattern')
-
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim(version=2.0)
-    
-    quality = results.node['quality']
-    flowrate = results.link['flowrate'].loc[:,wn.pipe_name_list] 
-    
-    EC = wntr.metrics.extent_contaminant(quality, flowrate, wn, 0)
-
-    expected = float(80749.9*0.3048) # hour 2
-    error = abs((EC[2*3600] - expected)/expected)
-    assert_less(error, 0.01) # 1% error
-
-    expected = float(135554*0.3048) # hour 12
-    error = abs((EC[12*3600] - expected)/expected)
-    assert_less(error, 0.01) # 1% error
-
-if __name__ == '__main__':
-    test_extent_contaminated()
+datadir = join(testdir, "networks_for_testing")
+netdir = join(testdir, "..", "..", "examples", "networks")
+
+
+class TestHealthImpactsMetric(unittest.TestCase):
+    """
+    Compare the following results to WST impact files using TSG file
+    121          SETPOINT      100000          0                86400
+    """
+
+    def test_mass_consumed(self):
+        inp_file = join(netdir, "Net3.inp")
+
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        wn.options.quality.parameter = "CHEMICAL"
+        newpat = wntr.network.elements.Pattern.binary_pattern(
+            "NewPattern",
+            0,
+            24 * 3600,
+            wn.options.time.pattern_timestep,
+            wn.options.time.duration,
+        )
+        wn.add_pattern(newpat.name, newpat)
+        wn.add_source("Source1", "121", "SETPOINT", 100, "NewPattern")
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim(version=2.0)
+
+        demand = results.node["demand"].loc[:, wn.junction_name_list]
+        quality = results.node["quality"].loc[:, wn.junction_name_list]
+
+        MC = wntr.metrics.mass_contaminant_consumed(demand, quality)
+        MC_timeseries = MC.sum(axis=1)
+        MC_cumsum = MC_timeseries.cumsum()
+        # MC_timeseries.to_csv('MC.txt')
+
+        expected = float(39069900000 / 1000000)  # hour 2
+        error = abs((MC_cumsum[2 * 3600] - expected) / expected)
+        self.assertLess(error, 0.01)  # 1% error
+
+        expected = float(1509440000000 / 1000000)  # hour 12
+        error = abs((MC_cumsum[12 * 3600] - expected) / expected)
+        self.assertLess(error, 0.01)  # 1% error
+
+    def test_volume_consumed(self):
+
+        inp_file = join(netdir, "Net3.inp")
+
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        wn.options.quality.parameter = "CHEMICAL"
+        newpat = wntr.network.elements.Pattern.binary_pattern(
+            "NewPattern",
+            0,
+            24 * 3600,
+            wn.options.time.pattern_timestep,
+            wn.options.time.duration,
+        )
+        wn.add_pattern(newpat.name, newpat)
+        wn.add_source("Source1", "121", "SETPOINT", 100, "NewPattern")
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim(version=2.0)
+
+        demand = results.node["demand"].loc[:, wn.junction_name_list]
+        quality = results.node["quality"].loc[:, wn.junction_name_list]
+
+        VC = wntr.metrics.volume_contaminant_consumed(demand, quality, 0)
+        VC_timeseries = VC.sum(axis=1)
+        VC_cumsum = VC_timeseries.cumsum()
+        # VC_timeseries.to_csv('VC.txt')
+
+        expected = float(156760 / 264.172)  # hour 2, convert gallons to m3
+        error = abs((VC_cumsum[2 * 3600] - expected) / expected)
+        self.assertLess(error, 0.02)  # 2% error
+
+        expected = float(4867920 / 264.172)  # hour 12, convert gallons to m3
+        error = abs((VC_cumsum[12 * 3600] - expected) / expected)
+        self.assertLess(error, 0.01)  # 1% error
+
+    def test_extent_contaminated(self):
+
+        inp_file = join(netdir, "Net3.inp")
+
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        wn.options.quality.parameter = "CHEMICAL"
+        newpat = wntr.network.elements.Pattern.binary_pattern(
+            "NewPattern",
+            0,
+            24 * 3600,
+            wn.options.time.pattern_timestep,
+            wn.options.time.duration,
+        )
+        wn.add_pattern(newpat.name, newpat)
+        wn.add_source("Source1", "121", "SETPOINT", 100, "NewPattern")
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim(version=2.0)
+
+        quality = results.node["quality"]
+        flowrate = results.link["flowrate"].loc[:, wn.pipe_name_list]
+
+        EC = wntr.metrics.extent_contaminant(quality, flowrate, wn, 0)
+
+        expected = float(80749.9 * 0.3048)  # hour 2
+        error = abs((EC[2 * 3600] - expected) / expected)
+        self.assertLess(error, 0.01)  # 1% error
+
+        expected = float(135554 * 0.3048)  # hour 12
+        error = abs((EC[12 * 3600] - expected) / expected)
+        self.assertLess(error, 0.01)  # 1% error
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_metrics_population.py b/wntr/tests/test_metrics_population.py
index 7c7b3a172..c1e64a2a8 100644
--- a/wntr/tests/test_metrics_population.py
+++ b/wntr/tests/test_metrics_population.py
@@ -1,58 +1,66 @@
-from nose.tools import *
+import unittest
 from os.path import abspath, dirname, join
-import pandas as pd
-from pandas.util.testing import assert_frame_equal, assert_series_equal
 import numpy as np
+import pandas as pd
 import wntr
+from pandas.testing import assert_frame_equal, assert_series_equal
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'networks_for_testing')
-net3dir = join(testdir,'..','..','examples','networks')
-              
-    
-def test_population_net3():
-    inp_file = join(net3dir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    pop = wntr.metrics.population(wn)
-    expected = 79000
-    error = abs((pop.sum() - expected)/expected)
-    assert_less(error, 0.01) # 1% error
-    
-def test_population_net6():
-    inp_file = join(net3dir,'Net6.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    pop = wntr.metrics.population(wn)
-    expected = 152000
-    error = abs((pop.sum() - expected)/expected)
-    assert_less(error, 0.01) # 1% error
-
-def test_population_impacted(): # tests query and population impacted
-    
-    pop = pd.Series([100,200,300,400,500], index=['J1', 'J2', 'J3', 'J4', 'J5'])
-    
-    # arg1 as a Series, arg2 as a scalar
-    wsa = pd.Series([0.6,0.7,0.8,0.9,1], index=['J1', 'J2', 'J3', 'J4', 'J5'])
-    pop_impacted = wntr.metrics.population_impacted(pop, wsa, np.less, 0.8)
-    expected = pd.Series([100,200,0,0,0], index=['J1', 'J2', 'J3', 'J4', 'J5'])
-    assert_series_equal(pop_impacted, expected, check_dtype=False)
-    
-    # arg1 as a Series, arg2 as a Series
-    wsa = pd.Series([0.6,0.7,0.8,0.9,1], index=['J1', 'J2', 'J3', 'J4', 'J5'])
-    wsa_threshold = pd.Series([1,0,1,0,1], index=['J1', 'J2', 'J3', 'J4', 'J5'])
-    pop_impacted = wntr.metrics.population_impacted(pop, wsa, np.less_equal, wsa_threshold)
-    expected = pd.Series([100,0,300,0,500], index=['J1', 'J2', 'J3', 'J4', 'J5'])
-    assert_series_equal(pop_impacted, expected, check_dtype=False)
-    
-    # arg1 as a DataFrame, arg2 as a scalar
-    wsa = pd.Series([0.6,0.7,0.8,0.9,1], index=['J1', 'J2', 'J3', 'J4', 'J5'])
-    wsa = wsa.to_frame().transpose()
-    wsa.loc[1,:] = [0,1,0,1,0]
-    wsa.loc[2,:] = [1,0,1,0,1]
-    pop_impacted = wntr.metrics.population_impacted(pop, wsa, np.less, 0.8)
-    expected = pd.DataFrame([[100,200,0,0,0],[100,0,300,0,500],[0,200,0,400,0]], 
-                            columns=['J1', 'J2', 'J3', 'J4', 'J5'], index=[0,1,2])
-    assert_frame_equal(pop_impacted, expected, check_dtype=False)
-    
-
-if __name__ == '__main__':
-    test_population_impacted()
+datadir = join(testdir, "networks_for_testing")
+net3dir = join(testdir, "..", "..", "examples", "networks")
+
+
+class TestPopulationMetrics(unittest.TestCase):
+    def test_population_net3(self):
+        inp_file = join(net3dir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        pop = wntr.metrics.population(wn)
+        expected = 79000
+        error = abs((pop.sum() - expected) / expected)
+        self.assertLess(error, 0.01)  # 1% error
+
+    def test_population_net6(self):
+        inp_file = join(net3dir, "Net6.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        pop = wntr.metrics.population(wn)
+        expected = 152000
+        error = abs((pop.sum() - expected) / expected)
+        self.assertLess(error, 0.01)  # 1% error
+
+    def test_population_impacted(self):  # tests query and population impacted
+
+        pop = pd.Series([100, 200, 300, 400, 500], index=["J1", "J2", "J3", "J4", "J5"])
+
+        # arg1 as a Series, arg2 as a scalar
+        wsa = pd.Series([0.6, 0.7, 0.8, 0.9, 1], index=["J1", "J2", "J3", "J4", "J5"])
+        pop_impacted = wntr.metrics.population_impacted(pop, wsa, np.less, 0.8)
+        expected = pd.Series([100, 200, 0, 0, 0], index=["J1", "J2", "J3", "J4", "J5"])
+        assert_series_equal(pop_impacted, expected, check_dtype=False)
+
+        # arg1 as a Series, arg2 as a Series
+        wsa = pd.Series([0.6, 0.7, 0.8, 0.9, 1], index=["J1", "J2", "J3", "J4", "J5"])
+        wsa_threshold = pd.Series([1, 0, 1, 0, 1], index=["J1", "J2", "J3", "J4", "J5"])
+        pop_impacted = wntr.metrics.population_impacted(
+            pop, wsa, np.less_equal, wsa_threshold
+        )
+        expected = pd.Series(
+            [100, 0, 300, 0, 500], index=["J1", "J2", "J3", "J4", "J5"]
+        )
+        assert_series_equal(pop_impacted, expected, check_dtype=False)
+
+        # arg1 as a DataFrame, arg2 as a scalar
+        wsa = pd.Series([0.6, 0.7, 0.8, 0.9, 1], index=["J1", "J2", "J3", "J4", "J5"])
+        wsa = wsa.to_frame().transpose()
+        wsa.loc[1, :] = [0, 1, 0, 1, 0]
+        wsa.loc[2, :] = [1, 0, 1, 0, 1]
+        pop_impacted = wntr.metrics.population_impacted(pop, wsa, np.less, 0.8)
+        expected = pd.DataFrame(
+            [[100, 200, 0, 0, 0], [100, 0, 300, 0, 500], [0, 200, 0, 400, 0]],
+            columns=["J1", "J2", "J3", "J4", "J5"],
+            index=[0, 1, 2],
+        )
+        assert_frame_equal(pop_impacted, expected, check_dtype=False)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_metrics_pump_cost.py b/wntr/tests/test_metrics_pump_cost.py
index 629698552..b73a5e778 100644
--- a/wntr/tests/test_metrics_pump_cost.py
+++ b/wntr/tests/test_metrics_pump_cost.py
@@ -1,22 +1,23 @@
 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')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
-class TestPumpNet3(unittest.TestCase):
 
+class TestPumpNet3(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(ex_datadir, 'Net3.inp')
+        inp_file = join(ex_datadir, "Net3.inp")
         self.wn = self.wntr.network.WaterNetworkModel(inp_file)
-        self.wn.options.energy.global_efficiency = 75 # 75%
+        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
@@ -24,53 +25,55 @@ def tearDownClass(self):
         pass
 
     def test_energy_report(self):
-        
+
         sim = self.wntr.sim.EpanetSimulator(self.wn)
         results = sim.run_sim()
 
-        flowrate = results.link['flowrate'].loc[:,self.wn.pump_name_list]
-        head = results.node['head'].loc[:,self.wn.node_name_list]
+        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
-        
+
+        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_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)
+        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[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)
+        cost_per_day = pump_cost.sum() / 7  # pump energy (J) * cost ($/J)
 
         # 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
-
-        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
+        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
+
+        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
+
+        self.assertLess(error.max().max(), 0.01)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_metrics_segments.py b/wntr/tests/test_metrics_segments.py
index 9d232a699..aee0a2dde 100644
--- a/wntr/tests/test_metrics_segments.py
+++ b/wntr/tests/test_metrics_segments.py
@@ -1,111 +1,134 @@
 import unittest
+from os.path import abspath, dirname, join
+
 import numpy as np
 import pandas as pd
-from os.path import abspath, dirname, join
 import wntr
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir, 'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
 
 class TestSegmentation(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
-        
-        inp_file1 = join(test_datadir, 'CCWI17-HermanMahmoud.inp')
+
+        inp_file1 = join(test_datadir, "CCWI17-HermanMahmoud.inp")
         self.wn1 = wntr.network.WaterNetworkModel(inp_file1)
 
-        inp_file2 = join(ex_datadir, 'Net3.inp')
+        inp_file2 = join(ex_datadir, "Net3.inp")
         self.wn2 = wntr.network.WaterNetworkModel(inp_file2)
 
     @classmethod
     def tearDownClass(self):
         pass
 
-        
     def test_segmentation_small(self):
         # test a small network
         G = self.wn1.get_graph()
-        
-        valves = [['p1', 'n1'] # valve 0 is on link p1 and protects node n1
-                  ,['p3', 'n2']
-                  ,['p7', 'n5']
-                  ,['p7', 'n6']
-                  ,['p8', 'n6']
-                  ]
-        valves = pd.DataFrame(valves, columns=['link', 'node']) 
-        
-        node_segments, link_segments, seg_size = wntr.metrics.topographic.valve_segments(G, valves)
-        
+
+        valves = [
+            ["p1", "n1"],  # valve 0 is on link p1 and protects node n1
+            ["p3", "n2"],
+            ["p7", "n5"],
+            ["p7", "n6"],
+            ["p8", "n6"],
+        ]
+        valves = pd.DataFrame(valves, columns=["link", "node"])
+
+        (
+            node_segments,
+            link_segments,
+            seg_size,
+        ) = wntr.metrics.topographic.valve_segments(G, valves)
+
         max_seg_size = seg_size.sum(axis=1).max()
         self.assertEqual(max_seg_size, 11)
         self.assertEqual(seg_size.shape[0], 4)
-        
+
     def test_segmentation_random(self):
         # test Net3
         G = self.wn2.get_graph()
-        valves = pd.read_csv(join(test_datadir, 'valve_layer_random.csv'), index_col=0, dtype='object')
-            
-        node_segments, link_segments, seg_size = wntr.metrics.topographic.valve_segments(G, valves)
+        valves = pd.read_csv(
+            join(test_datadir, "valve_layer_random.csv"), index_col=0, dtype="object"
+        )
+
+        (
+            node_segments,
+            link_segments,
+            seg_size,
+        ) = wntr.metrics.topographic.valve_segments(G, valves)
         max_seg_size = seg_size.sum(axis=1).max()
         num_segments = seg_size.shape[0]
-        
-#        node_segments.to_csv('node_segments_random.csv')
-#        link_segments.to_csv('link_segments_random.csv')
-        
-        expected_node_segments = pd.read_csv(join(test_datadir, 'node_segments_random.csv'), 
-                                             index_col=0, squeeze=True)
-        expected_link_segments = pd.read_csv(join(test_datadir, 'link_segments_random.csv'), 
-                                             index_col=0, squeeze=True)
-        expected_node_segments.astype('int32')
-        expected_link_segments.astype('int32')
-        
-#        import matplotlib
-#        cmap = matplotlib.colors.ListedColormap(np.random.rand(num_segments,3))
-#        wntr.graphics.plot_network(self.wn2, node_segments, link_segments, valve_layer=valves,
-#                                   node_cmap=cmap, link_cmap=cmap,
-#                                   node_range=[0.5,num_segments+0.5], 
-#                                   link_range=[0.5,num_segments+0.5])
-        
+
+        #        node_segments.to_csv('node_segments_random.csv')
+        #        link_segments.to_csv('link_segments_random.csv')
+
+        expected_node_segments = pd.read_csv(
+            join(test_datadir, "node_segments_random.csv"), index_col=0, squeeze=True
+        )
+        expected_link_segments = pd.read_csv(
+            join(test_datadir, "link_segments_random.csv"), index_col=0, squeeze=True
+        )
+        expected_node_segments.astype("int32")
+        expected_link_segments.astype("int32")
+
+        #        import matplotlib
+        #        cmap = matplotlib.colors.ListedColormap(np.random.rand(num_segments,3))
+        #        wntr.graphics.plot_network(self.wn2, node_segments, link_segments, valve_layer=valves,
+        #                                   node_cmap=cmap, link_cmap=cmap,
+        #                                   node_range=[0.5,num_segments+0.5],
+        #                                   link_range=[0.5,num_segments+0.5])
+
         self.assertListEqual(list(node_segments), list(expected_node_segments))
         self.assertListEqual(list(link_segments), list(expected_link_segments))
-        
+
         self.assertEqual(max_seg_size, 112)
         self.assertEqual(num_segments, 15)
-        
+
     def test_segmentation_strategic(self):
         # test Net3
         G = self.wn2.get_graph()
-        valves = pd.read_csv(join(test_datadir, 'valve_layer_stategic_1.csv'), index_col=0, dtype='object')
-        
-        node_segments, link_segments, seg_size = wntr.metrics.topographic.valve_segments(G, valves)
+        valves = pd.read_csv(
+            join(test_datadir, "valve_layer_stategic_1.csv"),
+            index_col=0,
+            dtype="object",
+        )
+
+        (
+            node_segments,
+            link_segments,
+            seg_size,
+        ) = wntr.metrics.topographic.valve_segments(G, valves)
         max_seg_size = seg_size.sum(axis=1).max()
         num_segments = seg_size.shape[0]
-        
-#        node_segments.to_csv('node_segments_strategic.csv')
-#        link_segments.to_csv('link_segments_strategic.csv')
-        
-        expected_node_segments = pd.read_csv(join(test_datadir, 'node_segments_strategic.csv'), 
-                                             index_col=0, squeeze=True)
-        expected_link_segments = pd.read_csv(join(test_datadir, 'link_segments_strategic.csv'), 
-                                             index_col=0, squeeze=True)
-        expected_node_segments.astype('int32')
-        expected_link_segments.astype('int32')
-        
-#        import matplotlib
-#        cmap = matplotlib.colors.ListedColormap(np.random.rand(num_segments,3))
-#        wntr.graphics.plot_network(self.wn2, node_segments, link_segments, valve_layer=valves,
-#                                   node_cmap=cmap, link_cmap=cmap,
-#                                   node_range=[0.5,num_segments+0.5], 
-#                                   link_range=[0.5,num_segments+0.5])
+
+        #        node_segments.to_csv('node_segments_strategic.csv')
+        #        link_segments.to_csv('link_segments_strategic.csv')
+
+        expected_node_segments = pd.read_csv(
+            join(test_datadir, "node_segments_strategic.csv"), index_col=0, squeeze=True
+        )
+        expected_link_segments = pd.read_csv(
+            join(test_datadir, "link_segments_strategic.csv"), index_col=0, squeeze=True
+        )
+        expected_node_segments.astype("int32")
+        expected_link_segments.astype("int32")
+
+        #        import matplotlib
+        #        cmap = matplotlib.colors.ListedColormap(np.random.rand(num_segments,3))
+        #        wntr.graphics.plot_network(self.wn2, node_segments, link_segments, valve_layer=valves,
+        #                                   node_cmap=cmap, link_cmap=cmap,
+        #                                   node_range=[0.5,num_segments+0.5],
+        #                                   link_range=[0.5,num_segments+0.5])
 
         self.assertListEqual(list(node_segments), list(expected_node_segments))
         self.assertListEqual(list(link_segments), list(expected_link_segments))
-        
+
         self.assertEqual(max_seg_size, 3)
         self.assertEqual(num_segments, 119)
-        
-if __name__ == '__main__':
-    unittest.main()
\ No newline at end of file
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_metrics_todini.py b/wntr/tests/test_metrics_todini.py
index 203a80a12..3fc357689 100644
--- a/wntr/tests/test_metrics_todini.py
+++ b/wntr/tests/test_metrics_todini.py
@@ -1,82 +1,93 @@
-from nose.tools import *
+import unittest
 from os.path import abspath, dirname, join
+
 import wntr
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'networks_for_testing')
-
-
-def test_Todini_Fig2_optCost_GPM():
-    inp_file = join(datadir,'Todini_Fig2_optCost_GPM.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    sim = wntr.sim.WNTRSimulator(wn)
-    results = sim.run_sim()
-    
-    # Compute todini index
-    head = results.node['head']
-    pressure = results.node['pressure']
-    demand = results.node['demand']
-    flowrate = results.link['flowrate']
-    todini = wntr.metrics.todini_index(head, pressure, demand, flowrate, wn, 30) # h* = 30 m
-
-    expected = 0.22
-    error = abs(todini[0] - expected)
-    #print(todini[0], expected, error)
-    assert_less(error, 0.01) 
-
-def test_Todini_Fig2_optCost_CMH():
-    inp_file = join(datadir,'Todini_Fig2_optCost_CMH.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    sim = wntr.sim.WNTRSimulator(wn)
-    results = sim.run_sim()
-
-    # Compute todini index
-    head = results.node['head']
-    pressure = results.node['pressure']
-    demand = results.node['demand']
-    flowrate = results.link['flowrate']
-    todini = wntr.metrics.todini_index(head, pressure, demand, flowrate, wn, 30) # h* = 30 m
-
-    expected = 0.22
-    error = abs(todini[0] - expected)
-    #print(todini[0], expected, error)
-    assert_less(error, 0.01) 
-
-def test_Todini_Fig2_solA_GPM():
-    inp_file = join(datadir,'Todini_Fig2_solA_GPM.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    sim = wntr.sim.WNTRSimulator(wn)
-    results = sim.run_sim()
-
-    # Compute todini index
-    head = results.node['head']
-    pressure = results.node['pressure']
-    demand = results.node['demand']
-    flowrate = results.link['flowrate']
-    todini = wntr.metrics.todini_index(head, pressure, demand, flowrate, wn, 30) # h* = 30 m
-    
-    expected = 0.41
-    error = abs(todini[0] - expected)
-    #print(todini[0], expected, error)
-    assert_less(error, 0.03) 
-
-def test_Todini_Fig2_solA_CMH():
-    inp_file = join(datadir,'Todini_Fig2_solA_CMH.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    sim = wntr.sim.WNTRSimulator(wn)
-    results = sim.run_sim()
-
-    # Compute todini index
-    head = results.node['head']
-    pressure = results.node['pressure']
-    demand = results.node['demand']
-    flowrate = results.link['flowrate']
-    todini = wntr.metrics.todini_index(head, pressure, demand, flowrate, wn, 30) # h* = 30 m
-    
-    expected = 0.41
-    error = abs(todini[0] - expected)
-    #print(todini[0], expected, error)
-    assert_less(error, 0.03) 
-
-if __name__ == '__main__':
-    test_Todini_Fig2_solA_GPM()
+datadir = join(testdir, "networks_for_testing")
+
+
+class TestTodiniMetrics(unittest.TestCase):
+    def test_Todini_Fig2_optCost_GPM(self):
+        inp_file = join(datadir, "Todini_Fig2_optCost_GPM.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        sim = wntr.sim.WNTRSimulator(wn)
+        results = sim.run_sim()
+
+        # Compute todini index
+        head = results.node["head"]
+        pressure = results.node["pressure"]
+        demand = results.node["demand"]
+        flowrate = results.link["flowrate"]
+        todini = wntr.metrics.todini_index(
+            head, pressure, demand, flowrate, wn, 30
+        )  # h* = 30 m
+
+        expected = 0.22
+        error = abs(todini[0] - expected)
+        # print(todini[0], expected, error)
+        self.assertLess(error, 0.01)
+
+    def test_Todini_Fig2_optCost_CMH(self):
+        inp_file = join(datadir, "Todini_Fig2_optCost_CMH.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        sim = wntr.sim.WNTRSimulator(wn)
+        results = sim.run_sim()
+
+        # Compute todini index
+        head = results.node["head"]
+        pressure = results.node["pressure"]
+        demand = results.node["demand"]
+        flowrate = results.link["flowrate"]
+        todini = wntr.metrics.todini_index(
+            head, pressure, demand, flowrate, wn, 30
+        )  # h* = 30 m
+
+        expected = 0.22
+        error = abs(todini[0] - expected)
+        # print(todini[0], expected, error)
+        self.assertLess(error, 0.01)
+
+    def test_Todini_Fig2_solA_GPM(self):
+        inp_file = join(datadir, "Todini_Fig2_solA_GPM.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        sim = wntr.sim.WNTRSimulator(wn)
+        results = sim.run_sim()
+
+        # Compute todini index
+        head = results.node["head"]
+        pressure = results.node["pressure"]
+        demand = results.node["demand"]
+        flowrate = results.link["flowrate"]
+        todini = wntr.metrics.todini_index(
+            head, pressure, demand, flowrate, wn, 30
+        )  # h* = 30 m
+
+        expected = 0.41
+        error = abs(todini[0] - expected)
+        # print(todini[0], expected, error)
+        self.assertLess(error, 0.03)
+
+    def test_Todini_Fig2_solA_CMH(self):
+        inp_file = join(datadir, "Todini_Fig2_solA_CMH.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        sim = wntr.sim.WNTRSimulator(wn)
+        results = sim.run_sim()
+
+        # Compute todini index
+        head = results.node["head"]
+        pressure = results.node["pressure"]
+        demand = results.node["demand"]
+        flowrate = results.link["flowrate"]
+        todini = wntr.metrics.todini_index(
+            head, pressure, demand, flowrate, wn, 30
+        )  # h* = 30 m
+
+        expected = 0.41
+        error = abs(todini[0] - expected)
+        # print(todini[0], expected, error)
+        self.assertLess(error, 0.03)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_metrics_valve_criticality.py b/wntr/tests/test_metrics_valve_criticality.py
index c5b6f6538..c211b4ff9 100644
--- a/wntr/tests/test_metrics_valve_criticality.py
+++ b/wntr/tests/test_metrics_valve_criticality.py
@@ -1,95 +1,113 @@
 import unittest
-import pandas as pd
 from os.path import abspath, dirname, join
-from pandas.util.testing import assert_frame_equal, assert_series_equal
+
+import pandas as pd
 import wntr
+from pandas.testing import assert_frame_equal, assert_series_equal
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir, 'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
 
 class TestSegmentation(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
-        inp_file = join(ex_datadir, 'Net3.inp')
+        inp_file = join(ex_datadir, "Net3.inp")
         self.wn = wntr.network.WaterNetworkModel(inp_file)
         G = self.wn.get_graph()
-        self.valves = pd.read_csv(join(test_datadir, 'valve_layer_stategic_1.csv'), index_col=0, dtype='object')
-        self.node_segments, self.link_segments, self.seg_size = wntr.metrics.topographic.valve_segments(G, self.valves)
-        
+        self.valves = pd.read_csv(
+            join(test_datadir, "valve_layer_stategic_1.csv"),
+            index_col=0,
+            dtype="object",
+        )
+        (
+            self.node_segments,
+            self.link_segments,
+            self.seg_size,
+        ) = wntr.metrics.topographic.valve_segments(G, self.valves)
+
         # import matplotlib
         # cmap = matplotlib.colors.ListedColormap(np.random.rand(num_segments,3))
         # wntr.graphics.plot_network(self.wn2, node_segments, link_segments, valve_layer=valves,
         #                            node_cmap=cmap, link_cmap=cmap,
-        #                            node_range=[0.5,num_segments+0.5], 
+        #                            node_range=[0.5,num_segments+0.5],
         #                            link_range=[0.5,num_segments+0.5])
 
     @classmethod
     def tearDownClass(self):
         pass
 
-        
     def test_valve_criticality_length(self):
 
         # Gather the link lengths for the length-based criticality calculation
-        link_lengths = self.wn.query_link_attribute('length')
-    
+        link_lengths = self.wn.query_link_attribute("length")
+
         # Calculate the length-based valve criticality for each valve
-        valve_crit = wntr.metrics.topographic._valve_criticality_length(link_lengths, 
-                                self.valves, self.node_segments, self.link_segments)
-        
-        #valve_crit.to_csv('valve_crit_length.csv') 
+        valve_crit = wntr.metrics.topographic._valve_criticality_length(
+            link_lengths, self.valves, self.node_segments, self.link_segments
+        )
+
+        # valve_crit.to_csv('valve_crit_length.csv')
         # filename = 'valve_crit_length.jpg'
-        # wntr.graphics.plot_network(self.wn, valve_layer=self.valves, 
-        #                            valve_criticality=valve_crit, 
+        # wntr.graphics.plot_network(self.wn, valve_layer=self.valves,
+        #                            valve_criticality=valve_crit,
         #                            node_size=10, filename=filename)
-        
-        
-        expected_valve_crit = pd.read_csv(join(test_datadir, 'valve_crit_length.csv'), 
-                                          index_col=[0], squeeze=True)
-        
-        assert_series_equal(valve_crit, expected_valve_crit, check_dtype=False, check_names=False)
- 
+
+        expected_valve_crit = pd.read_csv(
+            join(test_datadir, "valve_crit_length.csv"), index_col=[0], squeeze=True
+        )
+
+        assert_series_equal(
+            valve_crit, expected_valve_crit, check_dtype=False, check_names=False
+        )
+
     def test_valve_criticality_demand(self):
 
         # Gather the node demands for the demand-based criticality calculation
         node_demands = wntr.metrics.average_expected_demand(self.wn)
-    
+
         # Calculate the demand-based valve criticality for each valve
-        valve_crit = wntr.metrics.topographic._valve_criticality_demand(node_demands, 
-                                self.valves, self.node_segments, self.link_segments)
-      
-        # valve_crit.to_csv('valve_crit_demand.csv') 
+        valve_crit = wntr.metrics.topographic._valve_criticality_demand(
+            node_demands, self.valves, self.node_segments, self.link_segments
+        )
+
+        # valve_crit.to_csv('valve_crit_demand.csv')
         # filename = 'valve_crit_demand.jpg'
-        # wntr.graphics.plot_network(self.wn, valve_layer=self.valves, 
-        #                            valve_criticality=valve_crit, 
+        # wntr.graphics.plot_network(self.wn, valve_layer=self.valves,
+        #                            valve_criticality=valve_crit,
         #                            node_size=10, filename=filename)
-           
-        expected_valve_crit = pd.read_csv(join(test_datadir, 'valve_crit_demand.csv'), 
-                                          index_col=[0], squeeze=True)
-        
-        assert_series_equal(valve_crit, expected_valve_crit, check_dtype=False, check_names=False)
-        
+
+        expected_valve_crit = pd.read_csv(
+            join(test_datadir, "valve_crit_demand.csv"), index_col=[0], squeeze=True
+        )
+
+        assert_series_equal(
+            valve_crit, expected_valve_crit, check_dtype=False, check_names=False
+        )
+
     def test_valve_criticality(self):
-        
+
         # Calculate the valve-based valve criticality for each valve
-        valve_crit = wntr.metrics.topographic._valve_criticality(self.valves, 
-                                self.node_segments, self.link_segments)
+        valve_crit = wntr.metrics.topographic._valve_criticality(
+            self.valves, self.node_segments, self.link_segments
+        )
 
-        #valve_crit.to_csv('valve_crit_valve.csv') 
+        # valve_crit.to_csv('valve_crit_valve.csv')
         # filename = 'valve_crit_valve.jpg'
-        # wntr.graphics.plot_network(self.wn, valve_layer=self.valves, 
-        #                            valve_criticality=valve_crit, 
+        # wntr.graphics.plot_network(self.wn, valve_layer=self.valves,
+        #                            valve_criticality=valve_crit,
         #                            node_size=10, filename=filename)
-                      
-        expected_valve_crit = pd.read_csv(join(test_datadir, 'valve_crit_valve.csv'), 
-                                          index_col=[0], squeeze=True)
 
-        assert_series_equal(valve_crit, expected_valve_crit, 
-                            check_dtype=False, check_names=False)
+        expected_valve_crit = pd.read_csv(
+            join(test_datadir, "valve_crit_valve.csv"), index_col=[0], squeeze=True
+        )
+
+        assert_series_equal(
+            valve_crit, expected_valve_crit, check_dtype=False, check_names=False
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
 
-        
-if __name__ == '__main__':
-    unittest.main() 
\ No newline at end of file
diff --git a/wntr/tests/test_models.py b/wntr/tests/test_models.py
index 5cfc201b1..84b7d83d1 100644
--- a/wntr/tests/test_models.py
+++ b/wntr/tests/test_models.py
@@ -1,81 +1,101 @@
-import unittest
 import math
+import unittest
+from os.path import abspath, dirname, join
+
 import numpy as np
 import pandas as pd
-from os.path import abspath, dirname, join
 import wntr
 from wntr.sim.models.utils import ModelUpdater
 
 testdir = dirname(abspath(str(__file__)))
-test_data_dir = join(testdir,'data_for_testing')
+test_data_dir = join(testdir, "data_for_testing")
+
 
 def compare_floats(a, b, tol=1e-5, rel_tol=1e-3):
     if abs(a) >= 1e-8:
-        if abs(a-b)/abs(a)*100 > rel_tol:
+        if abs(a - b) / abs(a) * 100 > rel_tol:
             return False
         return True
-    if abs(a-b) > tol:
+    if abs(a - b) > tol:
         return False
     return True
 
 
 def approximate_derivative(con, var, h):
     orig_value = var.value
-    var_vals = [orig_value - 2*h, orig_value - h, orig_value + h, orig_value + 2*h]
+    var_vals = [orig_value - 2 * h, orig_value - h, orig_value + h, orig_value + 2 * h]
     f_vals = []
     for i in var_vals:
         var.value = i
         f_vals.append(con.evaluate())
     coeffs = [1, -8, 8, -1]
     var.value = orig_value
-    return sum(coeffs[i]*f_vals[i] for i in range(len(f_vals))) / (12*h)
+    return sum(coeffs[i] * f_vals[i] for i in range(len(f_vals))) / (12 * h)
 
 
 def abs_hw(pipe, f, m):
     f = abs(f)
     if f >= m.hw_q2:
-        r = m.hw_k*pipe.roughness**(-1.852)*pipe.diameter**(-4.871)*pipe.length*(f**1.852)
+        r = (
+            m.hw_k
+            * pipe.roughness ** (-1.852)
+            * pipe.diameter ** (-4.871)
+            * pipe.length
+            * (f ** 1.852)
+        )
     elif f >= m.hw_q1:
-        r = m.hw_k*pipe.roughness**(-1.852)*pipe.diameter**(-4.871)*pipe.length*(m.hw_a*f**3 + m.hw_b*f**2 + m.hw_c*f + m.hw_d)
+        r = (
+            m.hw_k
+            * pipe.roughness ** (-1.852)
+            * pipe.diameter ** (-4.871)
+            * pipe.length
+            * (m.hw_a * f ** 3 + m.hw_b * f ** 2 + m.hw_c * f + m.hw_d)
+        )
     else:
-        r = m.hw_k*pipe.roughness**(-1.852)*pipe.diameter**(-4.871)*pipe.length*m.hw_m*f
+        r = (
+            m.hw_k
+            * pipe.roughness ** (-1.852)
+            * pipe.diameter ** (-4.871)
+            * pipe.length
+            * m.hw_m
+            * f
+        )
     return r
 
 
 def abs_minor_loss(pipe, f):
     f = abs(f)
-    return pipe.minor_loss/2.0/9.81*(f/(math.pi/4*pipe.diameter**2))**2
+    return pipe.minor_loss / 2.0 / 9.81 * (f / (math.pi / 4 * pipe.diameter ** 2)) ** 2
 
 
 class TestHeadloss(unittest.TestCase):
-
     @classmethod
     def setUpClass(cls):
         cls.wn = wn = wntr.network.WaterNetworkModel()
-        wn.add_tank('t1', 0, 10, 0, 20, 15)
-        wn.add_junction('j1', 0.01)
-        wn.add_pipe('p1', 't1', 'j1', minor_loss=10.0)
-        wn.add_curve('curve1', 'HEAD', [(0.0, 10.0), (0.05, 5.0), (0.1, 0.0)])
-        wn.add_curve('curve2', 'HEAD', [(0.0, 10.0), (0.03, 5.0), (0.1, 0.0)])
-        wn.add_curve('curve3', 'HEAD', [(0.0, 10.0), (0.07, 5.0), (0.1, 0.0)])
-        
+        wn.add_tank("t1", 0, 10, 0, 20, 15)
+        wn.add_junction("j1", 0.01)
+        wn.add_pipe("p1", "t1", "j1", minor_loss=10.0)
+        wn.add_curve("curve1", "HEAD", [(0.0, 10.0), (0.05, 5.0), (0.1, 0.0)])
+        wn.add_curve("curve2", "HEAD", [(0.0, 10.0), (0.03, 5.0), (0.1, 0.0)])
+        wn.add_curve("curve3", "HEAD", [(0.0, 10.0), (0.07, 5.0), (0.1, 0.0)])
+
         # add a single point, 2-point, and a set of multi-point curves to test
-        wn.add_curve('curve4', 'HEAD', [(0.05, 5.0)])
-        wn.add_curve('curve5', 'HEAD', [(0.0, 10.0),(0.1, 0.0)])
-        
-        #multi_point_pump_curves = pump_curves_for_testing() # change to read in a csv file
-        
-        df = pd.read_csv(join(test_data_dir,'pump_practice_curves.csv'),skiprows=5)
+        wn.add_curve("curve4", "HEAD", [(0.05, 5.0)])
+        wn.add_curve("curve5", "HEAD", [(0.0, 10.0), (0.1, 0.0)])
+
+        # multi_point_pump_curves = pump_curves_for_testing() # change to read in a csv file
+
+        df = pd.read_csv(join(test_data_dir, "pump_practice_curves.csv"), skiprows=5)
         multi_point_pump_curves = []
         for i in range(11):
-            multi_point_pump_curves.append(df[df['curve number']==i].iloc[:,1:3])
-        
+            multi_point_pump_curves.append(df[df["curve number"] == i].iloc[:, 1:3])
+
         for i, curve in enumerate(multi_point_pump_curves):
-            curve_name = 'curve{0:d}'.format(i+6)
-            wn.add_curve(curve_name,'HEAD',curve.values)
-            
-        wn.add_pump('pump1', 't1', 'j1', 'HEAD', 'curve1')
-        wn.add_pump('pump2', 't1', 'j1', 'POWER', 50.0)
+            curve_name = "curve{0:d}".format(i + 6)
+            wn.add_curve(curve_name, "HEAD", curve.values)
+
+        wn.add_pump("pump1", "t1", "j1", "HEAD", "curve1")
+        wn.add_pump("pump2", "t1", "j1", "POWER", 50.0)
         cls.m = m = wntr.sim.aml.Model()
         cls.updater = updater = ModelUpdater()
         wntr.sim.models.constants.hazen_williams_constants(m)
@@ -84,7 +104,9 @@ def setUpClass(cls):
         wntr.sim.models.param.source_head_param(m, wn)
         wntr.sim.models.var.flow_var(m, wn)
         wntr.sim.models.var.head_var(m, wn)
-        wntr.sim.models.constraint.piecewise_hazen_williams_headloss_constraint.build(m, wn, updater)
+        wntr.sim.models.constraint.piecewise_hazen_williams_headloss_constraint.build(
+            m, wn, updater
+        )
         wntr.sim.models.constants.head_pump_constants(m)
         wntr.sim.models.constraint.head_pump_headloss_constraint.build(m, wn, updater)
         wntr.sim.models.param.pump_power_param.build(m, wn, updater)
@@ -93,9 +115,16 @@ def setUpClass(cls):
     def test_HW_headloss(self):
         wn = self.wn
         m = self.m
-        pipe = wn.get_link('p1')
-
-        flows_to_test = [0, m.hw_q1/2.0, m.hw_q1, (m.hw_q1+m.hw_q2)/2.0, m.hw_q2, m.hw_q2+0.1]
+        pipe = wn.get_link("p1")
+
+        flows_to_test = [
+            0,
+            m.hw_q1 / 2.0,
+            m.hw_q1,
+            (m.hw_q1 + m.hw_q2) / 2.0,
+            m.hw_q2,
+            m.hw_q2 + 0.1,
+        ]
         _flows_to_test = [-i for i in flows_to_test]
         flows_to_test.extend(_flows_to_test)
         status_to_test = [1, 0]
@@ -103,29 +132,45 @@ def test_HW_headloss(self):
         t1_head = 10
         j1_head = 0
 
-        m.source_head['t1'].value = t1_head
-        m.head['j1'].value = j1_head
+        m.source_head["t1"].value = t1_head
+        m.head["j1"].value = j1_head
 
         for status in status_to_test:
             pipe._user_status = status
-            self.updater.update(m, wn, pipe, 'status')
+            self.updater.update(m, wn, pipe, "status")
             for f in flows_to_test:
-                m.flow['p1'].value = f
-                r1 = m.piecewise_hazen_williams_headloss['p1'].evaluate()
+                m.flow["p1"].value = f
+                r1 = m.piecewise_hazen_williams_headloss["p1"].evaluate()
                 if status == 1:
                     if f <= 0:
-                        r2 = abs_hw(pipe, f, m) + abs_minor_loss(pipe, f) + t1_head - j1_head
+                        r2 = (
+                            abs_hw(pipe, f, m)
+                            + abs_minor_loss(pipe, f)
+                            + t1_head
+                            - j1_head
+                        )
                     else:
-                        r2 = -abs_hw(pipe, f, m) - abs_minor_loss(pipe, f) + t1_head - j1_head
+                        r2 = (
+                            -abs_hw(pipe, f, m)
+                            - abs_minor_loss(pipe, f)
+                            + t1_head
+                            - j1_head
+                        )
                 else:
                     r2 = f
                 self.assertAlmostEqual(r1, r2, 12)
-                d1 = m.piecewise_hazen_williams_headloss['p1'].reverse_ad()[m.flow['p1']]
-                d2 = m.piecewise_hazen_williams_headloss['p1'].reverse_ad()[m.flow['p1']]
-                d3 = approximate_derivative(m.piecewise_hazen_williams_headloss['p1'], m.flow['p1'], 1e-6)
-                rel_diff = abs(d1-d2)/abs(d1) * 100
+                d1 = m.piecewise_hazen_williams_headloss["p1"].reverse_ad()[
+                    m.flow["p1"]
+                ]
+                d2 = m.piecewise_hazen_williams_headloss["p1"].reverse_ad()[
+                    m.flow["p1"]
+                ]
+                d3 = approximate_derivative(
+                    m.piecewise_hazen_williams_headloss["p1"], m.flow["p1"], 1e-6
+                )
+                rel_diff = abs(d1 - d2) / abs(d1) * 100
                 self.assertLess(rel_diff, 0.1)
-                rel_diff = abs(d1-d3)/abs(d3) * 100
+                rel_diff = abs(d1 - d3) / abs(d3) * 100
                 self.assertLess(rel_diff, 0.1)
                 self.assertAlmostEqual(d1, d2, 3)
                 self.assertAlmostEqual(d1, d3, 3)
@@ -133,61 +178,92 @@ def test_HW_headloss(self):
     def test_head_pump_headloss(self):
         wn = self.wn
         m = self.m
-        pump = wn.get_link('pump1')
+        pump = wn.get_link("pump1")
         curves_to_test = []
         for curve_name, curve in wn.curves.items():
             if curve.curve_type == "HEAD":
                 curves_to_test.append(curve)
 
-
-        flows_to_test = [m.pump_q1 - 0.1, m.pump_q1, (m.pump_q1+m.pump_q2)/2, m.pump_q2, m.pump_q2+0.1]
+        flows_to_test = [
+            m.pump_q1 - 0.1,
+            m.pump_q1,
+            (m.pump_q1 + m.pump_q2) / 2,
+            m.pump_q2,
+            m.pump_q2 + 0.1,
+        ]
         status_to_test = [1, 0]
 
         t1_head = 5
         j1_head = 10
 
-        m.source_head['t1'].value = t1_head
-        m.head['j1'].value = j1_head
+        m.source_head["t1"].value = t1_head
+        m.head["j1"].value = j1_head
 
         for curve in curves_to_test:
             pump.pump_curve_name = curve.name
-            self.updater.update(m, wn, pump, 'pump_curve_name')
+            self.updater.update(m, wn, pump, "pump_curve_name")
             A, B, C = pump.get_head_curve_coefficients()
             for status in status_to_test:
                 pump._user_status = status
-                self.updater.update(m, wn, pump, 'status')
+                self.updater.update(m, wn, pump, "status")
                 for f in flows_to_test:
-                    m.flow['pump1'].value = f
-                    r1 = m.head_pump_headloss['pump1'].evaluate()
+                    m.flow["pump1"].value = f
+                    r1 = m.head_pump_headloss["pump1"].evaluate()
                     if status == 1:
                         if C <= 1:
-                            a, b, c, d = wntr.sim.models.constraint.get_pump_poly_coefficients(A, B, C, m)
+                            (
+                                a,
+                                b,
+                                c,
+                                d,
+                            ) = wntr.sim.models.constraint.get_pump_poly_coefficients(
+                                A, B, C, m
+                            )
                             if f <= m.pump_q1:
                                 r2 = m.pump_slope * f + A - j1_head + t1_head
                             elif f <= m.pump_q2:
-                                r2 = a*f**3 + b*f**2 + c*f + d - j1_head + t1_head
+                                r2 = (
+                                    a * f ** 3
+                                    + b * f ** 2
+                                    + c * f
+                                    + d
+                                    - j1_head
+                                    + t1_head
+                                )
                             else:
-                                r2 = A - B*f**C - j1_head + t1_head
+                                r2 = A - B * f ** C - j1_head + t1_head
                         else:
-                            q_bar, h_bar = wntr.sim.models.constraint.get_pump_line_params(A, B, C, m)
+                            (
+                                q_bar,
+                                h_bar,
+                            ) = wntr.sim.models.constraint.get_pump_line_params(
+                                A, B, C, m
+                            )
                             if f <= q_bar:
-                                r2 = m.pump_slope * (f - q_bar) + h_bar - j1_head + t1_head
+                                r2 = (
+                                    m.pump_slope * (f - q_bar)
+                                    + h_bar
+                                    - j1_head
+                                    + t1_head
+                                )
                             else:
-                                r2 = A - B*f**C - j1_head + t1_head
+                                r2 = A - B * f ** C - j1_head + t1_head
                     else:
                         r2 = f
                     self.assertTrue(compare_floats(r1, r2, 1e-12, 1e-12))
                     if f < m.pump_q1 or f > m.pump_q2:
-                        d1 = m.head_pump_headloss['pump1'].reverse_ad()[m.flow['pump1']]
-                        d2 = m.head_pump_headloss['pump1'].reverse_ad()[m.flow['pump1']]
-                        d3 = approximate_derivative(m.head_pump_headloss['pump1'], m.flow['pump1'], 1e-6)
+                        d1 = m.head_pump_headloss["pump1"].reverse_ad()[m.flow["pump1"]]
+                        d2 = m.head_pump_headloss["pump1"].reverse_ad()[m.flow["pump1"]]
+                        d3 = approximate_derivative(
+                            m.head_pump_headloss["pump1"], m.flow["pump1"], 1e-6
+                        )
                         self.assertTrue(compare_floats(d1, d2, 1e-12, 1e-12))
                         self.assertTrue(compare_floats(d1, d3, 1e-5, 1e-3))
 
     def test_power_pump_headloss(self):
         wn = self.wn
         m = self.m
-        pump = wn.get_link('pump2')
+        pump = wn.get_link("pump2")
 
         flows_to_test = np.linspace(-1.0, 1.0, 10, True)
         status_to_test = [1, 0]
@@ -195,41 +271,42 @@ def test_power_pump_headloss(self):
         t1_head = 5
         j1_head = 10
 
-        m.source_head['t1'].value = t1_head
-        m.head['j1'].value = j1_head
+        m.source_head["t1"].value = t1_head
+        m.head["j1"].value = j1_head
 
         for status in status_to_test:
             pump._user_status = status
-            self.updater.update(m, wn, pump, 'status')
+            self.updater.update(m, wn, pump, "status")
             for f in flows_to_test:
-                ff = float(f) # expr.m does not bind to
-                              # numpy.float64 correctly
-                m.flow['pump2'].value = ff   
-                                                  
-                r1 = m.power_pump_headloss['pump2'].evaluate()
+                ff = float(f)  # expr.m does not bind to
+                # numpy.float64 correctly
+                m.flow["pump2"].value = ff
+
+                r1 = m.power_pump_headloss["pump2"].evaluate()
                 if status == 1:
                     r2 = pump.power + (t1_head - j1_head) * ff * 9.81 * 1000.0
                 else:
                     r2 = ff
                 self.assertTrue(compare_floats(r1, r2, 1e-12, 1e-12))
-                d1 = m.power_pump_headloss['pump2'].reverse_ad()[m.flow['pump2']]
-                d2 = m.power_pump_headloss['pump2'].reverse_ad()[m.flow['pump2']]
-                d3 = approximate_derivative(m.power_pump_headloss['pump2'], m.flow['pump2'], 1e-6)
+                d1 = m.power_pump_headloss["pump2"].reverse_ad()[m.flow["pump2"]]
+                d2 = m.power_pump_headloss["pump2"].reverse_ad()[m.flow["pump2"]]
+                d3 = approximate_derivative(
+                    m.power_pump_headloss["pump2"], m.flow["pump2"], 1e-6
+                )
                 self.assertTrue(compare_floats(d1, d2, 1e-12, 1e-12))
                 self.assertTrue(compare_floats(d1, d3, 1e-8, 1e-6))
 
 
 class TestPDD(unittest.TestCase):
-
     @classmethod
     def setUpClass(cls):
         cls.wn = wn = wntr.network.WaterNetworkModel()
-        wn.add_junction('j1', 0.01)
+        wn.add_junction("j1", 0.01)
 
     def test_pdd(self):
         wn = self.wn
         # Changed to handle the specific heads_to_test range, which is bad
-        node = wn.get_node('j1')
+        node = wn.get_node("j1")
         node.required_pressure = 20.0
         node.minimum_pressure = 0.0
 
@@ -245,51 +322,64 @@ def test_pdd(self):
         wntr.sim.models.var.demand_var(m, wn)
         wntr.sim.models.constraint.pdd_constraint.build(m, wn, updater)
 
-        node = wn.get_node('j1')
+        node = wn.get_node("j1")
 
         pmin = node.minimum_pressure
         pnom = node.required_pressure
         h0 = node.elevation + pmin
         h1 = node.elevation + pnom
         delta = m.pdd_smoothing_delta
-        heads_to_test = [h0 - 1, h0, h0 + delta/2, h0 + delta, h0 + (pmin + pnom)/2, h1 - delta, h1 - delta/2, h1, h1 + 1]
-        d_expected = node.demand_timeseries_list.at(wn.sim_time, multiplier=wn.options.hydraulic.demand_multiplier)
+        heads_to_test = [
+            h0 - 1,
+            h0,
+            h0 + delta / 2,
+            h0 + delta,
+            h0 + (pmin + pnom) / 2,
+            h1 - delta,
+            h1 - delta / 2,
+            h1,
+            h1 + 1,
+        ]
+        d_expected = node.demand_timeseries_list.at(
+            wn.sim_time, multiplier=wn.options.hydraulic.demand_multiplier
+        )
 
         d = 0.01
 
-        m.demand['j1'].value = d
-        a1 = m.pdd_poly1_coeffs_a['j1'].value
-        b1 = m.pdd_poly1_coeffs_b['j1'].value
-        c1 = m.pdd_poly1_coeffs_c['j1'].value
-        d1 = m.pdd_poly1_coeffs_d['j1'].value
-        a2 = m.pdd_poly2_coeffs_a['j1'].value
-        b2 = m.pdd_poly2_coeffs_b['j1'].value
-        c2 = m.pdd_poly2_coeffs_c['j1'].value
-        d2 = m.pdd_poly2_coeffs_d['j1'].value
+        m.demand["j1"].value = d
+        a1 = m.pdd_poly1_coeffs_a["j1"].value
+        b1 = m.pdd_poly1_coeffs_b["j1"].value
+        c1 = m.pdd_poly1_coeffs_c["j1"].value
+        d1 = m.pdd_poly1_coeffs_d["j1"].value
+        a2 = m.pdd_poly2_coeffs_a["j1"].value
+        b2 = m.pdd_poly2_coeffs_b["j1"].value
+        c2 = m.pdd_poly2_coeffs_c["j1"].value
+        d2 = m.pdd_poly2_coeffs_d["j1"].value
         delta = m.pdd_smoothing_delta
         slope = m.pdd_slope
 
         for h in heads_to_test:
-            m.head['j1'].value = h
-            r1 = m.pdd['j1'].evaluate()
+            m.head["j1"].value = h
+            r1 = m.pdd["j1"].evaluate()
             p = h - node.elevation
             if p <= pmin:
                 r2 = d - d_expected * slope * (p - pmin)
             elif p <= pmin + delta:
-                r2 = d - d_expected * (a1*p**3 + b1*p**2 + c1*p + d1)
+                r2 = d - d_expected * (a1 * p ** 3 + b1 * p ** 2 + c1 * p + d1)
             elif p <= pnom - delta:
-                r2 = d - d_expected * ((p - pmin)/(pnom - pmin))**0.5
+                r2 = d - d_expected * ((p - pmin) / (pnom - pmin)) ** 0.5
             elif p <= pnom:
-                r2 = d - d_expected * (a2*p**3 + b2*p**2 + c2*p + d2)
+                r2 = d - d_expected * (a2 * p ** 3 + b2 * p ** 2 + c2 * p + d2)
             else:
-                r2 = d - d_expected * (slope*(p - pnom) + 1.0)
+                r2 = d - d_expected * (slope * (p - pnom) + 1.0)
             # print(p, r1, r2, abs(r1 - r2))
             self.assertAlmostEqual(r1, r2, 12)
-            der1 = m.pdd['j1'].reverse_ad()[m.head['j1']]
-            der2 = m.pdd['j1'].reverse_ad()[m.head['j1']]
-            der3 = approximate_derivative(m.pdd['j1'], m.head['j1'], 1e-6)
+            der1 = m.pdd["j1"].reverse_ad()[m.head["j1"]]
+            der2 = m.pdd["j1"].reverse_ad()[m.head["j1"]]
+            der3 = approximate_derivative(m.pdd["j1"], m.head["j1"], 1e-6)
             self.assertAlmostEqual(der1, der2, 7)
             self.assertAlmostEqual(der1, der3, 6)
 
+
 if __name__ == "__main__":
-    unittest.main()
\ No newline at end of file
+    unittest.main()
diff --git a/wntr/tests/test_morph.py b/wntr/tests/test_morph.py
index 1523aeceb..cb969579f 100644
--- a/wntr/tests/test_morph.py
+++ b/wntr/tests/test_morph.py
@@ -1,341 +1,415 @@
-from nose.tools import *
 import sys
+import unittest
+import warnings
 from os.path import abspath, dirname, join
-import pandas as pd
+
 import numpy as np
+import pandas as pd
 import wntr
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'networks_for_testing')
-netdir = join(testdir,'..','..','examples','networks')
-
-def test_scale_node_coordinates():
-    
-    inp_file = join(netdir, 'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    node = wn.get_node('123')
-    coord = node.coordinates
-    
-    wn2 = wntr.morph.scale_node_coordinates(wn, 100)
-    node2 = wn2.get_node('123')
-    coord2 = node2.coordinates
-    
-    assert_equal(coord[0]*100, coord2[0])
-    assert_equal(coord[1]*100, coord2[1])
-    
-    
-def test_translate_node_coordinates():
-    
-    inp_file = join(netdir, 'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    node = wn.get_node('123')
-    coord = node.coordinates
-    
-    wn2 = wntr.morph.translate_node_coordinates(wn, 5,10)
-    node2 = wn2.get_node('123')
-    coord2 = node2.coordinates
-    
-    assert_equal(coord[0]+5, coord2[0])
-    assert_equal(coord[1]+10, coord2[1])
-    
-
-def test_rotate_node_coordinates():
-    
-    wn = wntr.network.WaterNetworkModel()
-    wn.add_junction('J1', base_demand=5, elevation=100.0, coordinates=(2,0))
-    
-    wn2 = wntr.morph.rotate_node_coordinates(wn, 45)
-    node2 = wn2.get_node('J1')
-    coord2 = node2.coordinates
-    
-    assert_almost_equal(np.sqrt(2), coord2[0], 6)
-    assert_almost_equal(np.sqrt(2), coord2[1], 6)
-
-
-def test_UTM_to_longlat_to_UTM():
-        
-    wn = wntr.network.WaterNetworkModel()
-    wn.add_junction('J1', base_demand=5, elevation=100.0, 
-                    coordinates=(351521.07, 3886097.33)) # easting, northing
-    
-    wn2 = wntr.morph.convert_node_coordinates_UTM_to_longlat(wn, 13, 'S')
-    node2 = wn2.get_node('J1')
-    coord2 = node2.coordinates
-    
-    assert_almost_equal(-106.629181, coord2[0], 6) # longitude
-    assert_almost_equal(35.106766, coord2[1], 6) # latitude
-    
-    wn3 = wntr.morph.convert_node_coordinates_longlat_to_UTM(wn2)
-    node3 = wn3.get_node('J1')
-    coord3 = node3.coordinates
-    
-    assert_almost_equal(351521.07, coord3[0], 1) # easting
-    assert_almost_equal(3886097.33, coord3[1], 1) # northing
-    
-
-def test_convert_node_coordinates_to_longlat():
-        
-    inp_file = join(netdir, 'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    longlat_map = {'Lake':(-106.6587, 35.0623), 
-                   '219': (-106.5248, 35.191)}
-    wn2 = wntr.morph.convert_node_coordinates_to_longlat(wn, longlat_map)
-    for node_name in longlat_map.keys():
-        node = wn2.get_node(node_name)
+datadir = join(testdir, "networks_for_testing")
+netdir = join(testdir, "..", "..", "examples", "networks")
+
+
+
+class TestMorph(unittest.TestCase):
+    def test_scale_node_coordinates(self):
+
+        inp_file = join(netdir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        node = wn.get_node("123")
         coord = node.coordinates
-        assert_almost_equal(longlat_map[node_name][0], coord[0], 4)
-        assert_almost_equal(longlat_map[node_name][1], coord[1], 4)
-        
-    #opposite rotation
-    longlat_map = {'Lake':(-106.6851, 35.1344),
-                   '219': (-106.5073, 35.0713)}
-    wn2 = wntr.morph.convert_node_coordinates_to_longlat(wn, longlat_map)
-    for node_name in longlat_map.keys():
-        node = wn2.get_node(node_name)
+
+        wn2 = wntr.morph.scale_node_coordinates(wn, 100)
+        node2 = wn2.get_node("123")
+        coord2 = node2.coordinates
+
+        self.assertEqual(coord[0] * 100, coord2[0])
+        self.assertEqual(coord[1] * 100, coord2[1])
+
+    def test_translate_node_coordinates(self):
+
+        inp_file = join(netdir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        node = wn.get_node("123")
         coord = node.coordinates
-        assert_almost_equal(longlat_map[node_name][0], coord[0], 4)
-        assert_almost_equal(longlat_map[node_name][1], coord[1], 4)
-    
-    
-def test_split_pipe():
-        
-    inp_file = join(datadir, 'leaks.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    wn = wntr.morph.split_pipe(wn,'pipe1','pipe1__B','leak1')
-    
-    pipe = wn.get_link('pipe1')
-    pipeB = wn.get_link('pipe1__B')
-    
-    assert_equal(True, 'leak1' in [name for name,n in wn.nodes()])
-    assert_equal(True, 'leak1' in [name for name,n in wn.nodes(wntr.network.Junction)])
-    assert_equal(True, 'pipe1' in [name for name,l in wn.links()])
-    assert_equal(True, 'pipe1' in [name for name,l in wn.links(wntr.network.Pipe)])
-    assert_equal(True, 'pipe1__B' in [name for name,l in wn.links()])
-    assert_equal(True, 'pipe1__B' in [name for name,l in wn.links(wntr.network.Pipe)])
-    assert_equal(pipe.end_node_name, 'leak1')
-    assert_equal(pipeB.start_node_name, 'leak1')
-    assert_equal(pipe.diameter, pipeB.diameter)
-    assert_equal(pipe.roughness, pipeB.roughness)
-    assert_equal(pipe.minor_loss, pipeB.minor_loss)
-    assert_equal(pipe.initial_status, pipeB.initial_status)
-
-
-def test_break_pipe():
-        
-    inp_file = join(datadir, 'leaks.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    wn = wntr.morph.break_pipe(wn,'pipe1','pipe1__B','leak1','leak2')
-    
-    pipe = wn.get_link('pipe1')
-    pipeB = wn.get_link('pipe1__B')
-    
-    assert_equal(True, 'leak1' in [name for name,n in wn.nodes()])
-    assert_equal(True, 'leak1' in [name for name,n in wn.nodes(wntr.network.Junction)])
-    assert_equal(True, 'leak2' in [name for name,n in wn.nodes()])
-    assert_equal(True, 'leak2' in [name for name,n in wn.nodes(wntr.network.Junction)])
-    assert_equal(True, 'pipe1' in [name for name,l in wn.links()])
-    assert_equal(True, 'pipe1' in [name for name,l in wn.links(wntr.network.Pipe)])
-    assert_equal(True, 'pipe1__B' in [name for name,l in wn.links()])
-    assert_equal(True, 'pipe1__B' in [name for name,l in wn.links(wntr.network.Pipe)])
-    assert_equal(pipe.end_node_name, 'leak1')
-    assert_equal(pipeB.start_node_name, 'leak2')
-    assert_equal(pipe.diameter, pipeB.diameter)
-    assert_equal(pipe.roughness, pipeB.roughness)
-    assert_equal(pipe.minor_loss, pipeB.minor_loss)
-    assert_equal(pipe.initial_status, pipeB.initial_status)
-    
-    
-def test_skeletonize():
-    
-    inp_file = join(datadir, 'skeletonize.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    expected_total_demand = 12.1/264.172/60  # 12.1 GPM converted to m3/s
-
-    expected_nums = pd.DataFrame(index=[0,4,8,12,24,36], columns=['num_nodes', 'num_links'])
-    expected_nums.loc[0,:] = [wn.num_nodes, wn.num_links]
-    expected_nums.loc[4,:] = [wn.num_nodes-5, wn.num_links-5]
-    expected_nums.loc[8,:] = [wn.num_nodes-15, wn.num_links-18]
-    expected_nums.loc[12,:] = [wn.num_nodes-21, wn.num_links-26]
-    expected_nums.loc[24,:] = [wn.num_nodes-25, wn.num_links-30]
-    expected_nums.loc[36,:] = [wn.num_nodes-29, wn.num_links-34]
-    
-    for i in [0,4,8,12,24,36]:
-        skel_wn, skel_map = wntr.morph.skeletonize(wn, float(i)*0.0254, return_map=True, use_epanet=False)
-        
-        demand =  wntr.metrics.expected_demand(skel_wn)
-        total_demand = demand.loc[0,:].sum()
-        
-        # Write skel_wn to an inp file, read it back in, then extract the demands
-        skel_inp_file = 'temp.inp'
-        skel_wn.write_inpfile(skel_inp_file, 'GPM')
-        skel_wn_io = wntr.network.WaterNetworkModel(skel_inp_file)
-        demand_io =  wntr.metrics.expected_demand(skel_wn_io)
-        total_demand_io = demand_io.loc[0,:].sum()
-
-        #pipes = wn.query_link_attribute('diameter', np.less_equal, i*0.0254)
-        #wntr.graphics.plot_network(wn, link_attribute = list(pipes.keys()), title=str(i))
-        #wntr.graphics.plot_network(skel_wn, link_attribute='diameter', link_width=2, node_size=15, title=str(i))
-        
-        assert_almost_equal(total_demand.sum(), expected_total_demand,6)
-        assert_almost_equal(total_demand_io.sum(), expected_total_demand,6)
-        
-        assert_equal(skel_wn.num_nodes, expected_nums.loc[i,'num_nodes'])
-        assert_equal(skel_wn.num_links, expected_nums.loc[i,'num_links'])
-        
-        if i == 0:
-            expected_map = {} # 1:1 map
-            for name in wn.node_name_list:
-                expected_map[name] = [name]
-            assert_dict_contains_subset(expected_map, skel_map)
-        
-        if i == 4:
-            expected_map_subset = {}
-            expected_map_subset['15'] = ['15', '14', '16']
-            expected_map_subset['30'] = ['30', '32']
-            expected_map_subset['56'] = ['56', '57']
-            expected_map_subset['59'] = ['59', '64']
-            expected_map_subset['14'] = []
-            expected_map_subset['16'] = []
-            expected_map_subset['32'] = []
-            expected_map_subset['57'] = []
-            expected_map_subset['64'] = []
-            assert_dict_contains_subset(expected_map_subset, skel_map)
-
-
-def test_skeletonize_with_controls():
-    
-    inp_file = join(datadir, 'skeletonize.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    # add control to a link
-    action = wntr.network.ControlAction(wn.get_link('60'), 'status', wntr.network.LinkStatus.Closed)
-    condition = wntr.network.SimTimeCondition(wn, '==', 0)
-    control = wntr.network.Control(condition=condition, then_action=action)
-    wn.add_control('close_valve', control)
-    
-    # add control to a node
-    action = wntr.network.ControlAction(wn.get_node('13'), 'elevation', 1)
-    condition = wntr.network.SimTimeCondition(wn, '==', 0)
-    control = wntr.network.Control(condition=condition, then_action=action)
-    wn.add_control('raise_node', control)
-    
-    skel_wn = wntr.morph.skeletonize(wn, 12.0*0.0254, use_epanet=False)
-    
-    assert_equal(skel_wn.num_nodes, wn.num_nodes-17)
-    assert_equal(skel_wn.num_links, wn.num_links-22)
-    
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    # Change link 60 and 11 diameter to > 12, should get some results as above
-    link = wn.get_link('60')
-    link.diameter = 16*0.0254
-    link = wn.get_link('11')
-    link.diameter = 16*0.0254
-    
-    skel_wn = wntr.morph.skeletonize(wn, 12.0*0.0254, use_epanet=False)
-    
-    assert_equal(skel_wn.num_nodes, wn.num_nodes-17)
-    assert_equal(skel_wn.num_links, wn.num_links-22)
-    
-    
-def test_series_merge_properties():
-    
-    wn = wntr.network.WaterNetworkModel()
-    
-    wn.add_junction('J1', base_demand=5, elevation=100.0, coordinates=(0,0))
-    wn.add_junction('J2', base_demand=8, elevation=50.0, coordinates=(1,0))
-    wn.add_junction('J3', base_demand=5, elevation=25.0, coordinates=(2,0))
-    wn.add_pipe('P12', 'J1', 'J2', length=350, diameter=8, 
-                roughness=120, minor_loss=0.1, status='OPEN')
-    wn.add_pipe('P23', 'J2', 'J3', length=250, diameter=6, 
-                roughness=80, minor_loss=0.0, status='OPEN')
-    
-    # Add a source
-    wn.add_reservoir('R', base_head=125, coordinates=(0,2))
-    wn.add_pipe('PR', 'R', 'J1', length=100, diameter=12, roughness=100,
-                minor_loss=0.0, status='OPEN')
-    
-    wn.options.time.duration = 0
-    
-    skel_wn = wntr.morph.skeletonize(wn, 8, branch_trim=False, 
-            series_pipe_merge=True, parallel_pipe_merge=False, max_cycles=1, use_epanet=False)
-    
-    link = skel_wn.get_link('P12') # pipe P12 is the dominant pipe
-    
-    assert_equal(link.length, 600)
-    assert_equal(link.diameter, 8)
-    assert_almost_equal(link.roughness, 55, 0)
-    assert_equal(link.minor_loss, 0.1)
-    assert_equal(link.status, 1) # open
-
-
-def test_parallel_merge_properties():
-    
-    wn = wntr.network.WaterNetworkModel()
-    
-    wn.add_junction('J1', base_demand=5, elevation=100.0, coordinates=(0,0))
-    wn.add_junction('J2', base_demand=8, elevation=50.0, coordinates=(1,0))
-    wn.add_pipe('P12a', 'J1', 'J2', length=280, diameter=250, 
-                roughness=120, minor_loss=0.1, status='OPEN')
-    wn.add_pipe('P12b', 'J1', 'J2', length=220, diameter=300, 
-                roughness=100, minor_loss=0, status='OPEN')
-    # Add a source
-    wn.add_reservoir('R', base_head=125, coordinates=(0,2))
-    wn.add_pipe('PR', 'R', 'J1', length=100, diameter=450, roughness=100,
-                minor_loss=0.0, status='OPEN')
-    
-    wn.options.time.duration = 0
-    
-    skel_wn = wntr.morph.skeletonize(wn, 300, branch_trim=False, 
-            series_pipe_merge=False, parallel_pipe_merge=True, max_cycles=1, use_epanet=False)
-
-    link = skel_wn.get_link('P12b') # pipe P12b is the dominant pipe
-    
-    assert_equal(link.length, 220)
-    assert_equal(link.diameter, 300)
-    assert_almost_equal(link.roughness, 165, 0)
-    assert_equal(link.minor_loss, 0)
-    assert_equal(link.status, 1) # open
-    
-    
-def test_skeletonize_Net3():
-    
-    inp_file = join(netdir, 'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    skel_wn = wntr.morph.skeletonize(wn, 36.0*0.0254, use_epanet=False)
-    
-    assert_equal(wn.num_junctions, 92)
-    assert_equal(skel_wn.num_junctions, 45)
-    
-    sim = wntr.sim.WNTRSimulator(wn)
-    results_original = sim.run_sim()
-    
-    sim = wntr.sim.WNTRSimulator(skel_wn)
-    results_skel = sim.run_sim()
-    
-    skel_junctions = skel_wn.junction_name_list
-    
-    pressure_orig = results_original.node['pressure'].loc[:,skel_junctions]
-    pressure_skel = results_skel.node['pressure'].loc[:,skel_junctions]
-    pressure_diff = abs(pressure_orig - pressure_skel)
-    pressure_diff.index = pressure_diff.index/3600
-    
-    m50 = pressure_diff.quantile(0.50, axis=1)
-    
-    """
-    import matplotlib.pylab as plt
-    wntr.graphics.plot_network(wn, title='Original')
-    wntr.graphics.plot_network(skel_wn, title='Skeletonized')
-    plt.figure()
-    m50.plot()
-    print(m50.mean())
-    """
-    assert_less(m50.max(), 1.5)
-    assert_less(m50.mean(), 0.15)
-
-    
-if __name__ == '__main__':
-    test_UTM_to_longlat_to_UTM()
 
+        wn2 = wntr.morph.translate_node_coordinates(wn, 5, 10)
+        node2 = wn2.get_node("123")
+        coord2 = node2.coordinates
+
+        self.assertEqual(coord[0] + 5, coord2[0])
+        self.assertEqual(coord[1] + 10, coord2[1])
+
+    def test_rotate_node_coordinates(self):
+
+        wn = wntr.network.WaterNetworkModel()
+        wn.add_junction("J1", base_demand=5, elevation=100.0, coordinates=(2, 0))
+
+        wn2 = wntr.morph.rotate_node_coordinates(wn, 45)
+        node2 = wn2.get_node("J1")
+        coord2 = node2.coordinates
+
+        self.assertAlmostEqual(np.sqrt(2), coord2[0], 6)
+        self.assertAlmostEqual(np.sqrt(2), coord2[1], 6)
+
+    def test_UTM_to_longlat_to_UTM(self):
+
+        wn = wntr.network.WaterNetworkModel()
+        wn.add_junction(
+            "J1", base_demand=5, elevation=100.0, coordinates=(351521.07, 3886097.33)
+        )  # easting, northing
+
+        wn2 = wntr.morph.convert_node_coordinates_UTM_to_longlat(wn, 13, "S")
+        node2 = wn2.get_node("J1")
+        coord2 = node2.coordinates
+
+        self.assertAlmostEqual(-106.629181, coord2[0], 6)  # longitude
+        self.assertAlmostEqual(35.106766, coord2[1], 6)  # latitude
+
+        wn3 = wntr.morph.convert_node_coordinates_longlat_to_UTM(wn2)
+        node3 = wn3.get_node("J1")
+        coord3 = node3.coordinates
+
+        self.assertAlmostEqual(351521.07, coord3[0], 1)  # easting
+        self.assertAlmostEqual(3886097.33, coord3[1], 1)  # northing
+
+    def test_convert_node_coordinates_to_longlat(self):
+
+        inp_file = join(netdir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        longlat_map = {"Lake": (-106.6587, 35.0623), "219": (-106.5248, 35.191)}
+        wn2 = wntr.morph.convert_node_coordinates_to_longlat(wn, longlat_map)
+        for node_name in longlat_map.keys():
+            node = wn2.get_node(node_name)
+            coord = node.coordinates
+            self.assertAlmostEqual(longlat_map[node_name][0], coord[0], 4)
+            self.assertAlmostEqual(longlat_map[node_name][1], coord[1], 4)
+
+        # opposite rotation
+        longlat_map = {"Lake": (-106.6851, 35.1344), "219": (-106.5073, 35.0713)}
+        wn2 = wntr.morph.convert_node_coordinates_to_longlat(wn, longlat_map)
+        for node_name in longlat_map.keys():
+            node = wn2.get_node(node_name)
+            coord = node.coordinates
+            self.assertAlmostEqual(longlat_map[node_name][0], coord[0], 4)
+            self.assertAlmostEqual(longlat_map[node_name][1], coord[1], 4)
+
+    def test_split_pipe(self):
+
+        inp_file = join(datadir, "leaks.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        wn = wntr.morph.split_pipe(wn, "pipe1", "pipe1__B", "leak1")
+
+        pipe = wn.get_link("pipe1")
+        pipeB = wn.get_link("pipe1__B")
+
+        self.assertEqual(True, "leak1" in [name for name, n in wn.nodes()])
+        self.assertEqual(
+            True, "leak1" in [name for name, n in wn.nodes(wntr.network.Junction)]
+        )
+        self.assertEqual(True, "pipe1" in [name for name, l in wn.links()])
+        self.assertEqual(
+            True, "pipe1" in [name for name, l in wn.links(wntr.network.Pipe)]
+        )
+        self.assertEqual(True, "pipe1__B" in [name for name, l in wn.links()])
+        self.assertEqual(
+            True, "pipe1__B" in [name for name, l in wn.links(wntr.network.Pipe)]
+        )
+        self.assertEqual(pipe.end_node_name, "leak1")
+        self.assertEqual(pipeB.start_node_name, "leak1")
+        self.assertEqual(pipe.diameter, pipeB.diameter)
+        self.assertEqual(pipe.roughness, pipeB.roughness)
+        self.assertEqual(pipe.minor_loss, pipeB.minor_loss)
+        self.assertEqual(pipe.initial_status, pipeB.initial_status)
+
+    def test_break_pipe(self):
+
+        inp_file = join(datadir, "leaks.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        wn = wntr.morph.break_pipe(wn, "pipe1", "pipe1__B", "leak1", "leak2")
+
+        pipe = wn.get_link("pipe1")
+        pipeB = wn.get_link("pipe1__B")
+
+        self.assertEqual(True, "leak1" in [name for name, n in wn.nodes()])
+        self.assertEqual(
+            True, "leak1" in [name for name, n in wn.nodes(wntr.network.Junction)]
+        )
+        self.assertEqual(True, "leak2" in [name for name, n in wn.nodes()])
+        self.assertEqual(
+            True, "leak2" in [name for name, n in wn.nodes(wntr.network.Junction)]
+        )
+        self.assertEqual(True, "pipe1" in [name for name, l in wn.links()])
+        self.assertEqual(
+            True, "pipe1" in [name for name, l in wn.links(wntr.network.Pipe)]
+        )
+        self.assertEqual(True, "pipe1__B" in [name for name, l in wn.links()])
+        self.assertEqual(
+            True, "pipe1__B" in [name for name, l in wn.links(wntr.network.Pipe)]
+        )
+        self.assertEqual(pipe.end_node_name, "leak1")
+        self.assertEqual(pipeB.start_node_name, "leak2")
+        self.assertEqual(pipe.diameter, pipeB.diameter)
+        self.assertEqual(pipe.roughness, pipeB.roughness)
+        self.assertEqual(pipe.minor_loss, pipeB.minor_loss)
+        self.assertEqual(pipe.initial_status, pipeB.initial_status)
+
+    def test_skeletonize(self):
+
+        inp_file = join(datadir, "skeletonize.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        expected_total_demand = 12.1 / 264.172 / 60  # 12.1 GPM converted to m3/s
+
+        expected_nums = pd.DataFrame(
+            index=[0, 4, 8, 12, 24, 36], columns=["num_nodes", "num_links"]
+        )
+        expected_nums.loc[0, :] = [wn.num_nodes, wn.num_links]
+        expected_nums.loc[4, :] = [wn.num_nodes - 5, wn.num_links - 5]
+        expected_nums.loc[8, :] = [wn.num_nodes - 15, wn.num_links - 18]
+        expected_nums.loc[12, :] = [wn.num_nodes - 21, wn.num_links - 26]
+        expected_nums.loc[24, :] = [wn.num_nodes - 25, wn.num_links - 30]
+        expected_nums.loc[36, :] = [wn.num_nodes - 29, wn.num_links - 34]
+
+        for i in [0, 4, 8, 12, 24, 36]:
+            skel_wn, skel_map = wntr.morph.skeletonize(
+                wn, float(i) * 0.0254, return_map=True, use_epanet=False
+            )
+
+            demand = wntr.metrics.expected_demand(skel_wn)
+            total_demand = demand.loc[0, :].sum()
+
+            # Write skel_wn to an inp file, read it back in, then extract the demands
+            skel_inp_file = "temp.inp"
+            skel_wn.write_inpfile(skel_inp_file, "GPM")
+            skel_wn_io = wntr.network.WaterNetworkModel(skel_inp_file)
+            demand_io = wntr.metrics.expected_demand(skel_wn_io)
+            total_demand_io = demand_io.loc[0, :].sum()
+
+            # pipes = wn.query_link_attribute('diameter', np.less_equal, i*0.0254)
+            # wntr.graphics.plot_network(wn, link_attribute = list(pipes.keys()), title=str(i))
+            # wntr.graphics.plot_network(skel_wn, link_attribute='diameter', link_width=2, node_size=15, title=str(i))
+
+            self.assertAlmostEqual(total_demand.sum(), expected_total_demand, 6)
+            self.assertAlmostEqual(total_demand_io.sum(), expected_total_demand, 6)
+
+            self.assertEqual(skel_wn.num_nodes, expected_nums.loc[i, "num_nodes"])
+            self.assertEqual(skel_wn.num_links, expected_nums.loc[i, "num_links"])
+
+            if i == 0:
+                expected_map = {}  # 1:1 map
+                for name in wn.node_name_list:
+                    expected_map[name] = [name]
+                self.assertEqual(dict(expected_map, **skel_map), skel_map)
+
+            if i == 4:
+                expected_map_subset = {}
+                expected_map_subset["15"] = ["15", "14", "16"]
+                expected_map_subset["30"] = ["30", "32"]
+                expected_map_subset["56"] = ["56", "57"]
+                expected_map_subset["59"] = ["59", "64"]
+                expected_map_subset["14"] = []
+                expected_map_subset["16"] = []
+                expected_map_subset["32"] = []
+                expected_map_subset["57"] = []
+                expected_map_subset["64"] = []
+                self.assertEqual(dict(expected_map_subset, **skel_map), skel_map)
+
+    def test_skeletonize_with_controls(self):
+
+        inp_file = join(datadir, "skeletonize.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        # add control to a link
+        action = wntr.network.ControlAction(
+            wn.get_link("60"), "status", wntr.network.LinkStatus.Closed
+        )
+        condition = wntr.network.SimTimeCondition(wn, "==", 0)
+        control = wntr.network.Control(condition=condition, then_action=action)
+        wn.add_control("close_valve", control)
+
+        # add control to a node
+        action = wntr.network.ControlAction(wn.get_node("13"), "elevation", 1)
+        condition = wntr.network.SimTimeCondition(wn, "==", 0)
+        control = wntr.network.Control(condition=condition, then_action=action)
+        wn.add_control("raise_node", control)
+
+        skel_wn = wntr.morph.skeletonize(wn, 12.0 * 0.0254, use_epanet=False)
+
+        self.assertEqual(skel_wn.num_nodes, wn.num_nodes - 17)
+        self.assertEqual(skel_wn.num_links, wn.num_links - 22)
+
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        # Change link 60 and 11 diameter to > 12, should get some results as above
+        link = wn.get_link("60")
+        link.diameter = 16 * 0.0254
+        link = wn.get_link("11")
+        link.diameter = 16 * 0.0254
+
+        skel_wn = wntr.morph.skeletonize(wn, 12.0 * 0.0254, use_epanet=False)
+
+        self.assertEqual(skel_wn.num_nodes, wn.num_nodes - 17)
+        self.assertEqual(skel_wn.num_links, wn.num_links - 22)
+
+    def test_series_merge_properties(self):
+
+        wn = wntr.network.WaterNetworkModel()
+
+        wn.add_junction("J1", base_demand=5, elevation=100.0, coordinates=(0, 0))
+        wn.add_junction("J2", base_demand=8, elevation=50.0, coordinates=(1, 0))
+        wn.add_junction("J3", base_demand=5, elevation=25.0, coordinates=(2, 0))
+        wn.add_pipe(
+            "P12",
+            "J1",
+            "J2",
+            length=350,
+            diameter=8,
+            roughness=120,
+            minor_loss=0.1,
+            initial_status="OPEN",
+        )
+        wn.add_pipe(
+            "P23",
+            "J2",
+            "J3",
+            length=250,
+            diameter=6,
+            roughness=80,
+            minor_loss=0.0,
+            initial_status="OPEN",
+        )
+
+        # Add a source
+        wn.add_reservoir("R", base_head=125, coordinates=(0, 2))
+        wn.add_pipe(
+            "PR",
+            "R",
+            "J1",
+            length=100,
+            diameter=12,
+            roughness=100,
+            minor_loss=0.0,
+            initial_status="OPEN",
+        )
+
+        wn.options.time.duration = 0
+
+        skel_wn = wntr.morph.skeletonize(
+            wn,
+            8,
+            branch_trim=False,
+            series_pipe_merge=True,
+            parallel_pipe_merge=False,
+            max_cycles=1,
+            use_epanet=False,
+        )
+
+        link = skel_wn.get_link("P12")  # pipe P12 is the dominant pipe
+
+        self.assertEqual(link.length, 600)
+        self.assertEqual(link.diameter, 8)
+        self.assertAlmostEqual(link.roughness, 55, 0)
+        self.assertEqual(link.minor_loss, 0.1)
+        self.assertEqual(link.status, 1)  # open
+
+    def test_parallel_merge_properties(self):
+
+        wn = wntr.network.WaterNetworkModel()
+
+        wn.add_junction("J1", base_demand=5, elevation=100.0, coordinates=(0, 0))
+        wn.add_junction("J2", base_demand=8, elevation=50.0, coordinates=(1, 0))
+        wn.add_pipe(
+            "P12a",
+            "J1",
+            "J2",
+            length=280,
+            diameter=250,
+            roughness=120,
+            minor_loss=0.1,
+            initial_status="OPEN",
+        )
+        wn.add_pipe(
+            "P12b",
+            "J1",
+            "J2",
+            length=220,
+            diameter=300,
+            roughness=100,
+            minor_loss=0,
+            initial_status="OPEN",
+        )
+        # Add a source
+        wn.add_reservoir("R", base_head=125, coordinates=(0, 2))
+        wn.add_pipe(
+            "PR",
+            "R",
+            "J1",
+            length=100,
+            diameter=450,
+            roughness=100,
+            minor_loss=0.0,
+            initial_status="OPEN",
+        )
+
+        wn.options.time.duration = 0
+
+        skel_wn = wntr.morph.skeletonize(
+            wn,
+            300,
+            branch_trim=False,
+            series_pipe_merge=False,
+            parallel_pipe_merge=True,
+            max_cycles=1,
+            use_epanet=False,
+        )
+
+        link = skel_wn.get_link("P12b")  # pipe P12b is the dominant pipe
+
+        self.assertEqual(link.length, 220)
+        self.assertEqual(link.diameter, 300)
+        self.assertAlmostEqual(link.roughness, 165, 0)
+        self.assertEqual(link.minor_loss, 0)
+        self.assertEqual(link.status, 1)  # open
+
+    def test_skeletonize_Net3(self):
+
+        inp_file = join(netdir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        skel_wn = wntr.morph.skeletonize(wn, 36.0 * 0.0254, use_epanet=False)
+
+        self.assertEqual(wn.num_junctions, 92)
+        self.assertEqual(skel_wn.num_junctions, 45)
+
+        sim = wntr.sim.WNTRSimulator(wn)
+        results_original = sim.run_sim()
+
+        sim = wntr.sim.WNTRSimulator(skel_wn)
+        results_skel = sim.run_sim()
+
+        skel_junctions = skel_wn.junction_name_list
+
+        pressure_orig = results_original.node["pressure"].loc[:, skel_junctions]
+        pressure_skel = results_skel.node["pressure"].loc[:, skel_junctions]
+        pressure_diff = abs(pressure_orig - pressure_skel)
+        pressure_diff.index = pressure_diff.index / 3600
+
+        m50 = pressure_diff.quantile(0.50, axis=1)
+
+        """
+        import matplotlib.pylab as plt
+        wntr.graphics.plot_network(wn, title='Original')
+        wntr.graphics.plot_network(skel_wn, title='Skeletonized')
+        plt.figure()
+        m50.plot()
+        print(m50.mean())
+        """
+        self.assertLess(m50.max(), 1.5)
+        self.assertLess(m50.mean(), 0.15)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_multiple_simulations.py b/wntr/tests/test_multiple_simulations.py
index 3d35f28ad..2bb344eaf 100644
--- a/wntr/tests/test_multiple_simulations.py
+++ b/wntr/tests/test_multiple_simulations.py
@@ -1,30 +1,32 @@
 # These tests run a demand-driven simulation with both WNTR and Epanet and compare the results for the example networks
+import pickle
 import unittest
 from os.path import abspath, dirname, join
+
 import pandas as pd
-import pickle
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir,'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
-class TestResetInitialValues(unittest.TestCase):
 
+class TestResetInitialValues(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(ex_datadir, 'Net3.inp')
+        inp_file = join(ex_datadir, "Net3.inp")
         self.wn = self.wntr.network.WaterNetworkModel(inp_file)
         self.wn.options.time.hydraulic_timestep = 3600
-        self.wn.options.time.duration = 24*3600
+        self.wn.options.time.duration = 24 * 3600
 
         sim = self.wntr.sim.WNTRSimulator(self.wn)
-        self.res1 = sim.run_sim(solver_options={'TOL':1e-8})
+        self.res1 = sim.run_sim(solver_options={"TOL": 1e-8})
 
         self.wn.reset_initial_values()
-        self.res2 = sim.run_sim(solver_options={'TOL':1e-8})
+        self.res2 = sim.run_sim(solver_options={"TOL": 1e-8})
 
     @classmethod
     def tearDownClass(self):
@@ -33,48 +35,69 @@ def tearDownClass(self):
     def test_link_flowrate(self):
         for link_name, link in self.wn.links():
             for t in self.res1.time:
-                self.assertAlmostEqual(self.res1.link['flowrate'].at[t,link_name], self.res2.link['flowrate'].at[t,link_name], 7)
+                self.assertAlmostEqual(
+                    self.res1.link["flowrate"].at[t, link_name],
+                    self.res2.link["flowrate"].at[t, link_name],
+                    7,
+                )
 
     def test_link_velocity(self):
         for link_name, link in self.wn.links():
-            for t in self.res1.link['velocity'].index:
-                self.assertAlmostEqual(self.res1.link['velocity'].at[t,link_name], self.res2.link['velocity'].at[t,link_name], 7)
+            for t in self.res1.link["velocity"].index:
+                self.assertAlmostEqual(
+                    self.res1.link["velocity"].at[t, link_name],
+                    self.res2.link["velocity"].at[t, link_name],
+                    7,
+                )
 
     def test_node_demand(self):
         for node_name, node in self.wn.nodes():
-            for t in self.res1.node['demand'].index:
-                self.assertAlmostEqual(self.res1.node['demand'].at[t,node_name], self.res2.node['demand'].at[t,node_name], 7)
+            for t in self.res1.node["demand"].index:
+                self.assertAlmostEqual(
+                    self.res1.node["demand"].at[t, node_name],
+                    self.res2.node["demand"].at[t, node_name],
+                    7,
+                )
 
     def test_node_head(self):
         for node_name, node in self.wn.nodes():
-            for t in self.res1.node['head'].index:
-                self.assertAlmostEqual(self.res1.node['head'].at[t,node_name], self.res2.node['head'].at[t,node_name], 7)
+            for t in self.res1.node["head"].index:
+                self.assertAlmostEqual(
+                    self.res1.node["head"].at[t, node_name],
+                    self.res2.node["head"].at[t, node_name],
+                    7,
+                )
 
     def test_node_pressure(self):
         for node_name, node in self.wn.nodes():
-            for t in self.res1.node['pressure'].index:
-                self.assertAlmostEqual(self.res1.node['pressure'].at[t,node_name], self.res2.node['pressure'].at[t,node_name], 7)
+            for t in self.res1.node["pressure"].index:
+                self.assertAlmostEqual(
+                    self.res1.node["pressure"].at[t, node_name],
+                    self.res2.node["pressure"].at[t, node_name],
+                    7,
+                )
 
-class TestStopStartSim(unittest.TestCase):
 
+class TestStopStartSim(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(ex_datadir, 'Net3.inp')
+        inp_file = join(ex_datadir, "Net3.inp")
 
         parser = self.wntr.epanet.InpFile()
         self.wn = parser.read(inp_file)
         self.wn.options.time.hydraulic_timestep = 3600
-        self.wn.options.time.duration = 24*3600
+        self.wn.options.time.duration = 24 * 3600
         sim = self.wntr.sim.WNTRSimulator(self.wn)
-        self.res1 = sim.run_sim(solver_options={'TOL':1e-8})
+        self.res1 = sim.run_sim(solver_options={"TOL": 1e-8})
 
         parser = self.wntr.epanet.InpFile()
         self.wn = parser.read(inp_file)
         self.wn.options.time.hydraulic_timestep = 3600
-        self.wn.options.time.duration = 10*3600
+        self.wn.options.time.duration = 10 * 3600
         sim = self.wntr.sim.WNTRSimulator(self.wn)
         self.res2 = sim.run_sim(solver_options={'TOL':1e-8})
         self.wn.options.time.duration = 24*3600
@@ -82,71 +105,92 @@ def setUpClass(self):
 
         node_res = {}
         link_res = {}
-        for key in self.res2.node.keys():
+        for key in self.res2.node.keys(): 
             node_res[key] = pd.concat([self.res2.node[key],self.res3.node[key]],axis=0)
         for key in self.res2.link.keys():
             link_res[key] = pd.concat([self.res2.link[key],self.res3.link[key]],axis=0)
-    
         self.res2.node = node_res
         self.res2.link = link_res
 
     @classmethod
     def tearDownClass(self):
         pass
-    
+
     def test_link_flowrate(self):
         for link_name, link in self.wn.links():
-            for t in self.res1.link['flowrate'].index:
-                self.assertAlmostEqual(self.res1.link['flowrate'].at[t,link_name], self.res2.link['flowrate'].at[t,link_name], 7)
+            for t in self.res1.link["flowrate"].index:
+                self.assertAlmostEqual(
+                    self.res1.link["flowrate"].at[t, link_name],
+                    self.res2.link["flowrate"].at[t, link_name],
+                    7,
+                )
 
     def test_link_velocity(self):
         for link_name, link in self.wn.links():
-            for t in self.res1.link['velocity'].index:
-                self.assertAlmostEqual(self.res1.link['velocity'].at[t,link_name], self.res2.link['velocity'].at[t,link_name], 7)
+            for t in self.res1.link["velocity"].index:
+                self.assertAlmostEqual(
+                    self.res1.link["velocity"].at[t, link_name],
+                    self.res2.link["velocity"].at[t, link_name],
+                    7,
+                )
 
     def test_node_demand(self):
         for node_name, node in self.wn.nodes():
-            for t in self.res1.node['demand'].index:
-                self.assertAlmostEqual(self.res1.node['demand'].at[t,node_name], self.res2.node['demand'].at[t,node_name], 7)
+            for t in self.res1.node["demand"].index:
+                self.assertAlmostEqual(
+                    self.res1.node["demand"].at[t, node_name],
+                    self.res2.node["demand"].at[t, node_name],
+                    7,
+                )
 
     def test_node_head(self):
         for node_name, node in self.wn.nodes():
-            for t in self.res1.node['head'].index:
-                self.assertAlmostEqual(self.res1.node['head'].at[t,node_name], self.res2.node['head'].at[t,node_name], 7)
+            for t in self.res1.node["head"].index:
+                self.assertAlmostEqual(
+                    self.res1.node["head"].at[t, node_name],
+                    self.res2.node["head"].at[t, node_name],
+                    7,
+                )
 
     def test_node_pressure(self):
         for node_name, node in self.wn.nodes():
-            for t in self.res1.node['pressure'].index:
-                self.assertAlmostEqual(self.res1.node['pressure'].at[t,node_name], self.res2.node['pressure'].at[t,node_name], 7)
+            for t in self.res1.node["pressure"].index:
+                self.assertAlmostEqual(
+                    self.res1.node["pressure"].at[t, node_name],
+                    self.res2.node["pressure"].at[t, node_name],
+                    7,
+                )
 
-class TestPickle(unittest.TestCase):
 
+class TestPickle(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(ex_datadir, 'Net3.inp')
+        inp_file = join(ex_datadir, "Net3.inp")
 
         parser = self.wntr.epanet.InpFile()
         self.wn = parser.read(inp_file)
         self.wn.options.time.hydraulic_timestep = 3600
-        self.wn.options.time.duration = 24*3600
+        self.wn.options.time.duration = 24 * 3600
         sim = self.wntr.sim.WNTRSimulator(self.wn)
-        self.res1 = sim.run_sim(solver_options={'TOL':1e-8})
+        self.res1 = sim.run_sim(solver_options={"TOL": 1e-8})
 
         parser = self.wntr.epanet.InpFile()
         self.wn = parser.read(inp_file)
         self.wn.options.time.hydraulic_timestep = 3600
-        self.wn.options.time.duration = 10*3600
+        self.wn.options.time.duration = 10 * 3600
         sim = self.wntr.sim.WNTRSimulator(self.wn)
-        self.res2 = sim.run_sim(solver_options={'TOL':1e-8})
-        f=open('temp.pickle','wb')
-        pickle.dump(self.wn,f)
+        self.res2 = sim.run_sim(solver_options={"TOL": 1e-8})
+        f = open("temp.pickle", "wb")
+        pickle.dump(self.wn, f)
         f.close()
-        f=open('temp.pickle','rb')
+        f = open("temp.pickle", "rb")
         wn2 = pickle.load(f)
         f.close()
+        #wn2.set_initial_conditions(self.res2)
         wn2.options.time.duration = 24*3600
         sim = self.wntr.sim.WNTRSimulator(wn2)
         self.res3 = sim.run_sim(solver_options={'TOL':1e-8})
@@ -165,31 +209,51 @@ def setUpClass(self):
     def tearDownClass(self):
         pass
 
-        
     def test_link_flowrate(self):
         for link_name, link in self.wn.links():
-            for t in self.res1.link['flowrate'].index:
-                self.assertAlmostEqual(self.res1.link['flowrate'].at[t,link_name], self.res2.link['flowrate'].at[t,link_name], 7)
+            for t in self.res1.link["flowrate"].index:
+                self.assertAlmostEqual(
+                    self.res1.link["flowrate"].at[t, link_name],
+                    self.res2.link["flowrate"].at[t, link_name],
+                    7,
+                )
 
     def test_link_velocity(self):
         for link_name, link in self.wn.links():
-            for t in self.res1.link['velocity'].index:
-                self.assertAlmostEqual(self.res1.link['velocity'].at[t,link_name], self.res2.link['velocity'].at[t,link_name], 7)
+            for t in self.res1.link["velocity"].index:
+                self.assertAlmostEqual(
+                    self.res1.link["velocity"].at[t, link_name],
+                    self.res2.link["velocity"].at[t, link_name],
+                    7,
+                )
 
     def test_node_demand(self):
         for node_name, node in self.wn.nodes():
-            for t in self.res1.node['demand'].index:
-                self.assertAlmostEqual(self.res1.node['demand'].at[t,node_name], self.res2.node['demand'].at[t,node_name], 7)
+            for t in self.res1.node["demand"].index:
+                self.assertAlmostEqual(
+                    self.res1.node["demand"].at[t, node_name],
+                    self.res2.node["demand"].at[t, node_name],
+                    7,
+                )
 
     def test_node_head(self):
         for node_name, node in self.wn.nodes():
-            for t in self.res1.node['head'].index:
-                self.assertAlmostEqual(self.res1.node['head'].at[t,node_name], self.res2.node['head'].at[t,node_name], 7)
+            for t in self.res1.node["head"].index:
+                self.assertAlmostEqual(
+                    self.res1.node["head"].at[t, node_name],
+                    self.res2.node["head"].at[t, node_name],
+                    7,
+                )
 
     def test_node_pressure(self):
         for node_name, node in self.wn.nodes():
-            for t in self.res1.node['pressure'].index:
-                self.assertAlmostEqual(self.res1.node['pressure'].at[t,node_name], self.res2.node['pressure'].at[t,node_name], 7)
+            for t in self.res1.node["pressure"].index:
+                self.assertAlmostEqual(
+                    self.res1.node["pressure"].at[t, node_name],
+                    self.res2.node["pressure"].at[t, node_name],
+                    7,
+                )
+
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/wntr/tests/test_network.py b/wntr/tests/test_network.py
index 24a84feb4..9bbe6750e 100644
--- a/wntr/tests/test_network.py
+++ b/wntr/tests/test_network.py
@@ -1,24 +1,26 @@
 import unittest
+import warnings
 from os.path import abspath, dirname, join
-import pandas as pd
+
 import numpy as np
-from nose.tools import *
+import pandas as pd
 import wntr
 from wntr.network.controls import Control, Rule
 
 testdir = dirname(abspath(str(__file__)))
-test_network_dir = join(testdir,'networks_for_testing')
-test_data_dir = join(testdir,'data_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_network_dir = join(testdir, "networks_for_testing")
+test_data_dir = join(testdir, "data_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
-class TestNetworkCreation(unittest.TestCase):
 
+class TestNetworkCreation(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(ex_datadir,'Net6.inp')
+        inp_file = join(ex_datadir, "Net6.inp")
         self.wn = self.wntr.network.WaterNetworkModel(inp_file)
 
     @classmethod
@@ -44,29 +46,30 @@ def test_num_valves(self):
         self.assertEqual(self.wn.num_valves, 2)
 
     def test_num_nodes(self):
-        self.assertEqual(self.wn.num_nodes, 3323+1+32)
+        self.assertEqual(self.wn.num_nodes, 3323 + 1 + 32)
 
     def test_num_links(self):
-        self.assertEqual(self.wn.num_links, 3829+61+2)
+        self.assertEqual(self.wn.num_links, 3829 + 61 + 2)
 
     def test_junction_attr(self):
-        j = self.wn.get_node('JUNCTION-18')
-        #self.assertAlmostEqual(j.base_demand, 48.56/60.0*0.003785411784)
-        self.assertEqual(j.demand_timeseries_list[0].pattern_name, 'PATTERN-2')
-        self.assertAlmostEqual(j.elevation, 80.0*0.3048)
+        j = self.wn.get_node("JUNCTION-18")
+        # self.assertAlmostEqual(j.base_demand, 48.56/60.0*0.003785411784)
+        self.assertEqual(j.demand_timeseries_list[0].pattern_name, "PATTERN-2")
+        self.assertAlmostEqual(j.elevation, 80.0 * 0.3048)
 
     def test_reservoir_attr(self):
-        j = self.wn.get_node('RESERVOIR-3323')
-        self.assertAlmostEqual(j.head_timeseries.base_value, 27.45*0.3048)
+        j = self.wn.get_node("RESERVOIR-3323")
+        self.assertAlmostEqual(j.head_timeseries.base_value, 27.45 * 0.3048)
         self.assertEqual(j.head_timeseries.pattern_name, None)
 
-class TestNetworkMethods(unittest.TestCase):
 
+class TestNetworkMethods(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
-        from wntr.network.model import Junction, Tank, Reservoir, Pipe, Pump, Valve
-        from wntr.network.controls import ControlAction, Control
+        from wntr.network.controls import Control, ControlAction
+        from wntr.network.model import Junction, Pipe, Pump, Reservoir, Tank, Valve
+
         self.Junction = Junction
         self.Tank = Tank
         self.Reservoir = Reservoir
@@ -83,29 +86,29 @@ def tearDownClass(self):
 
     def test_add_junction(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_pattern('pattern1', [1])
-        wn.add_junction('j1', 150, 'pattern1', 15)
-        j = wn.get_node('j1')
-        self.assertEqual(j._name, 'j1')
-        #self.assertEqual(j.base_demand, 150.0)
-        self.assertEqual(j.demand_timeseries_list[0].pattern_name, 'pattern1')
+        wn.add_pattern("pattern1", [1])
+        wn.add_junction("j1", 150, "pattern1", 15)
+        j = wn.get_node("j1")
+        self.assertEqual(j._name, "j1")
+        # self.assertEqual(j.base_demand, 150.0)
+        self.assertEqual(j.demand_timeseries_list[0].pattern_name, "pattern1")
         self.assertEqual(j.elevation, 15.0)
-        self.assertEqual(list(wn.node_name_list),['j1'])
-        #self.assertEqual(type(j.base_demand), float)
+        self.assertEqual(list(wn.node_name_list), ["j1"])
+        # self.assertEqual(type(j.base_demand), float)
         self.assertEqual(type(j.elevation), float)
 
     def test_add_tank(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_tank('t1', 15, 75, 0, 100, 10, 0)
-        n = wn.get_node('t1')
-        self.assertEqual(n._name, 't1')
+        wn.add_tank("t1", 15, 75, 0, 100, 10, 0)
+        n = wn.get_node("t1")
+        self.assertEqual(n._name, "t1")
         self.assertEqual(n.elevation, 15.0)
         self.assertEqual(n.init_level, 75.0)
         self.assertEqual(n.min_level, 0.0)
         self.assertEqual(n.max_level, 100.0)
         self.assertEqual(n.diameter, 10.0)
         self.assertEqual(n.min_vol, 0.0)
-        self.assertEqual(list(wn.node_name_list),['t1'])
+        self.assertEqual(list(wn.node_name_list), ["t1"])
         self.assertEqual(type(n.elevation), float)
         self.assertEqual(type(n.init_level), float)
         self.assertEqual(type(n.min_level), float)
@@ -115,23 +118,23 @@ def test_add_tank(self):
 
     def test_add_reservoir(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_pattern('pattern1', [1])
-        wn.add_reservoir('r1', 30, 'pattern1')
-        n = wn.get_node('r1')
-        self.assertEqual(n._name, 'r1')
+        wn.add_pattern("pattern1", [1])
+        wn.add_reservoir("r1", 30, "pattern1")
+        n = wn.get_node("r1")
+        self.assertEqual(n._name, "r1")
         self.assertEqual(n.head_timeseries.base_value, 30.0)
-        self.assertEqual(n.head_timeseries.pattern_name, 'pattern1')
-        self.assertEqual(list(wn.node_name_list),['r1'])
+        self.assertEqual(n.head_timeseries.pattern_name, "pattern1")
+        self.assertEqual(list(wn.node_name_list), ["r1"])
 
     def test_add_pipe(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_pipe('p1', 'j1', 'j2', 1000, 1, 100, 0, 'Open')
-        l = wn.get_link('p1')
-        self.assertEqual(l.name, 'p1')
-        self.assertEqual(l.start_node_name, 'j1')
-        self.assertEqual(l.end_node_name, 'j2')
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_pipe("p1", "j1", "j2", 1000, 1, 100, 0, "Open")
+        l = wn.get_link("p1")
+        self.assertEqual(l.name, "p1")
+        self.assertEqual(l.start_node_name, "j1")
+        self.assertEqual(l.end_node_name, "j2")
         self.assertEqual(l.initial_status, self.wntr.network.LinkStatus.opened)
         self.assertEqual(l.length, 1000.0)
         self.assertEqual(l.diameter, 1.0)
@@ -144,117 +147,132 @@ def test_add_pipe(self):
 
     def test_add_pattern(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
+        wn.add_junction("j1")
         wn.options.time.duration = 10
         wn.options.time.pattern_timestep = 1
-#        wn.add_pattern('pat1', start_time=2, end_time=4)
-        pat1 = self.wntr.network.elements.Pattern.binary_pattern('pat1', start_time=2, end_time=4, duration=10, step_size=1)
-        wn.add_pattern('pat1', pat1)
-        wn.add_pattern('pat2', [1,2,3,4])
-
-        pat1 = wn.get_pattern('pat1')
-        pat2 = wn.get_pattern('pat2')
-
-        self.assertEqual(pat1.multipliers.tolist(), [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0])
-        self.assertEqual(pat2.multipliers.tolist(), [1,2,3,4])
+        #        wn.add_pattern('pat1', start_time=2, end_time=4)
+        pat1 = self.wntr.network.elements.Pattern.binary_pattern(
+            "pat1", start_time=2, end_time=4, duration=10, step_size=1
+        )
+        wn.add_pattern("pat1", pat1)
+        wn.add_pattern("pat2", [1, 2, 3, 4])
+
+        pat1 = wn.get_pattern("pat1")
+        pat2 = wn.get_pattern("pat2")
+
+        self.assertEqual(
+            pat1.multipliers.tolist(),
+            [0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+        )
+        self.assertEqual(pat2.multipliers.tolist(), [1, 2, 3, 4])
 
     def test_add_source(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
+        wn.add_junction("j1")
         wn.options.time.duration = 10
         wn.options.time.pattern_timestep = 1
-        pat1 = self.wntr.network.elements.Pattern.binary_pattern('pat1', start_time=2, end_time=4, duration=10, step_size=wn.options.time.pattern_timestep)
-        wn.add_pattern('pat1', pat1)
-        wn.add_source('s1', 'j1', 'SETPOINT', 100, 'pat1')
-        s = wn.get_source('s1')
-        self.assertEqual(s.name, 's1')
-        self.assertEqual(s.node_name, 'j1')
-        self.assertEqual(s.source_type, 'SETPOINT')
+        pat1 = self.wntr.network.elements.Pattern.binary_pattern(
+            "pat1",
+            start_time=2,
+            end_time=4,
+            duration=10,
+            step_size=wn.options.time.pattern_timestep,
+        )
+        wn.add_pattern("pat1", pat1)
+        wn.add_source("s1", "j1", "SETPOINT", 100, "pat1")
+        s = wn.get_source("s1")
+        self.assertEqual(s.name, "s1")
+        self.assertEqual(s.node_name, "j1")
+        self.assertEqual(s.source_type, "SETPOINT")
         self.assertEqual(s.strength_timeseries.base_value, 100)
-        self.assertEqual(s.strength_timeseries.pattern_name, 'pat1')
+        self.assertEqual(s.strength_timeseries.pattern_name, "pat1")
 
     def test_add_pipe_with_cv(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_pipe('p1', 'j1', 'j2', 1000, 1, 100, 0, "OPEN", True)
-        self.assertEqual(wn._check_valves, ['p1'])
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_pipe("p1", "j1", "j2", 1000, 1, 100, 0, "OPEN", True)
+        self.assertEqual(wn._check_valves, ["p1"])
 
     def test_remove_pipe(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_junction('j3')
-        wn.add_pipe('p2','j1','j3')
-        wn.add_pipe('p1','j1','j2', status=self.wntr.network.LinkStatus.cv)
-        wn.remove_link('p1')
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_junction("j3")
+        wn.add_pipe("p2", "j1", "j3")
+        wn.add_pipe("p1", "j1", "j2", initial_status=self.wntr.network.LinkStatus.CV)
+        wn.remove_link("p1")
         link_list = [link_name for link_name, link in wn.links()]
-        self.assertEqual(link_list, ['p2'])
-        self.assertEqual(wn._check_valves,[])
+        self.assertEqual(link_list, ["p2"])
+        self.assertEqual(wn._check_valves, [])
         self.assertEqual(wn.num_pipes, 1)
 
     def test_remove_node(self):
-        inp_file = join(ex_datadir,'Net6.inp')
+        inp_file = join(ex_datadir, "Net6.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
 
-        links_to_remove = wn.get_links_for_node('TANK-3326')
+        links_to_remove = wn.get_links_for_node("TANK-3326")
         for l in links_to_remove:
             wn.remove_link(l, with_control=True)
-        wn.remove_node('TANK-3326', with_control=True)
+        wn.remove_node("TANK-3326", with_control=True)
 
-        self.assertNotIn('TANK-3326',{name for name, node in wn.nodes()})
-        self.assertNotIn('TANK-3326',wn.node_name_list)
+        self.assertNotIn("TANK-3326", {name for name, node in wn.nodes()})
+        self.assertNotIn("TANK-3326", wn.node_name_list)
 
-        inp_file = join(test_network_dir,'conditional_controls_1.inp')
+        inp_file = join(test_network_dir, "conditional_controls_1.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
 
-        tank1 = wn.get_node('tank1')
-        action = self.ControlAction(tank1, 'elevation', 55)
-        control = self.TimeControl(wn, 3652, 'SIM_TIME', False, action)
-        wn.add_control('tank_control', control)
+        tank1 = wn.get_node("tank1")
+        action = self.ControlAction(tank1, "elevation", 55)
+        control = self.TimeControl(wn, 3652, "SIM_TIME", False, action)
+        wn.add_control("tank_control", control)
 
         controls_1 = {c_name for c_name, c in wn.controls()}
 
-        links_to_remove = wn.get_links_for_node('tank1')
+        links_to_remove = wn.get_links_for_node("tank1")
         for l in links_to_remove:
             wn.remove_link(l, with_control=True)
-        wn.remove_node('tank1', with_control=True)
+        wn.remove_node("tank1", with_control=True)
 
         controls_2 = {c_name for c_name, c in wn.controls()}
 
-        self.assertTrue('tank_control' in controls_1)
-        self.assertFalse('tank_control' in controls_2)
+        self.assertTrue("tank_control" in controls_1)
+        self.assertFalse("tank_control" in controls_2)
 
-        self.assertNotIn('tank1', {name for name, node in wn.nodes()})
-        self.assertNotIn('tank1', wn.node_name_list)
-        expected_nodes = {'junction1', 'res1'}
+        self.assertNotIn("tank1", {name for name, node in wn.nodes()})
+        self.assertNotIn("tank1", wn.node_name_list)
+        expected_nodes = {"junction1", "res1"}
         self.assertSetEqual({name for name, node in wn.nodes()}, expected_nodes)
 
     def test_remove_controls_for_removing_link(self):
-        inp_file = join(ex_datadir, 'Net1.inp')
+        inp_file = join(ex_datadir, "Net1.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
 
-        control_action = self.wntr.network.ControlAction(wn.get_link('21'), 'status', self.wntr.network.LinkStatus.opened)
-        control = self.wntr.network.controls.Control._conditional_control(wn.get_node('2'), 'head', np.greater_equal, 10.0, control_action)
-        wn.add_control('control_1',control)
+        control_action = self.wntr.network.ControlAction(
+            wn.get_link("21"), "status", self.wntr.network.LinkStatus.opened
+        )
+        control = self.wntr.network.controls.Control._conditional_control(
+            wn.get_node("2"), "head", np.greater_equal, 10.0, control_action
+        )
+        wn.add_control("control_1", control)
 
         controls_1 = {c_name for c_name, c in wn.controls()}
 
-        wn.remove_link('21', with_control=True)
+        wn.remove_link("21", with_control=True)
 
         controls_2 = {c_name for c_name, c in wn.controls()}
-        self.assertTrue('control_1' in controls_1)
-        self.assertFalse('control_1' in controls_2)
+        self.assertTrue("control_1" in controls_1)
+        self.assertFalse("control_1" in controls_2)
 
     def test_nodes(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_tank('t1')
-        wn.add_reservoir('r1')
-        wn.add_reservoir('r2')
-        wn.add_junction('j3')
-        node_list = [name for name,node in wn.nodes()]
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_tank("t1")
+        wn.add_reservoir("r1")
+        wn.add_reservoir("r2")
+        wn.add_junction("j3")
+        node_list = [name for name, node in wn.nodes()]
         node_list.sort()
         junction_list = [name for name, junction in wn.nodes(self.Junction)]
         junction_list.sort()
@@ -262,92 +280,93 @@ def test_nodes(self):
         tank_list.sort()
         reservoir_list = [name for name, reservoir in wn.nodes(self.Reservoir)]
         reservoir_list.sort()
-        self.assertEqual(node_list,['j1','j2','j3','r1','r2','t1'])
-        self.assertEqual(junction_list,['j1','j2','j3'])
-        self.assertEqual(tank_list,['t1'])
-        self.assertEqual(reservoir_list,['r1','r2'])
-        for name,node in wn.nodes():
+        self.assertEqual(node_list, ["j1", "j2", "j3", "r1", "r2", "t1"])
+        self.assertEqual(junction_list, ["j1", "j2", "j3"])
+        self.assertEqual(tank_list, ["t1"])
+        self.assertEqual(reservoir_list, ["r1", "r2"])
+        for name, node in wn.nodes():
             self.assertEqual(name, node._name)
 
     def test_links(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_tank('t1')
-        wn.add_tank('t2')
-        wn.add_reservoir('r1')
-        wn.add_reservoir('r2')
-        wn.add_junction('j3')
-        wn.add_pipe('p1','j1','j2')
-        wn.add_pipe('p2','j1','t1')
-        wn.add_pipe('p3','r1','j1')
-        wn.add_pump('pump1','r2','t2')
-        wn.add_valve('v1','t1','j2')
-        link_list = [name for name,link in wn.links()]
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_tank("t1")
+        wn.add_tank("t2")
+        wn.add_reservoir("r1")
+        wn.add_reservoir("r2")
+        wn.add_junction("j3")
+        wn.add_pipe("p1", "j1", "j2")
+        wn.add_pipe("p2", "j1", "t1")
+        wn.add_pipe("p3", "r1", "j1")
+        wn.add_pump("pump1", "r2", "t2")
+        wn.add_valve("v1", "j3", "j2")
+        link_list = [name for name, link in wn.links()]
         link_list.sort()
-        self.assertEqual(link_list,['p1','p2','p3','pump1','v1'])
-        pipe_list = [name for name,pipe in wn.links(self.Pipe)]
+        self.assertEqual(link_list, ["p1", "p2", "p3", "pump1", "v1"])
+        pipe_list = [name for name, pipe in wn.links(self.Pipe)]
         pipe_list.sort()
-        self.assertEqual(pipe_list,['p1','p2','p3'])
-        pump_list = [name for name,pump in wn.links(self.Pump)]
+        self.assertEqual(pipe_list, ["p1", "p2", "p3"])
+        pump_list = [name for name, pump in wn.links(self.Pump)]
         pump_list.sort()
-        self.assertEqual(pump_list,['pump1'])
-        valve_list = [name for name,valve in wn.links(self.Valve)]
+        self.assertEqual(pump_list, ["pump1"])
+        valve_list = [name for name, valve in wn.links(self.Valve)]
         valve_list.sort()
-        self.assertEqual(valve_list,['v1'])
-        for name,link in wn.links():
+        self.assertEqual(valve_list, ["v1"])
+        for name, link in wn.links():
             self.assertEqual(name, link._link_name)
 
     def test_1_pt_head_curve(self):
         q2 = 10.0
         h2 = 20.0
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_curve('curve1','HEAD',[(q2, h2)])
-        curve = wn.get_curve('curve1')
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_pump('p1', 'j1', 'j2', 'HEAD', curve)
-        link = wn.get_link('p1')
-        a,b,c = link.get_head_curve_coefficients()
+        wn.add_curve("curve1", "HEAD", [(q2, h2)])
+        curve = wn.get_curve("curve1")
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_pump("p1", "j1", "j2", "HEAD", curve.name)
+        link = wn.get_link("p1")
+        a, b, c = link.get_head_curve_coefficients()
         q1 = 0.0
-        h1 = 4.0/3.0*h2
-        q3 = 2.0*q2
+        h1 = 4.0 / 3.0 * h2
+        q3 = 2.0 * q2
         h3 = 0.0
 
         def curve_fun(x):
-            f=[1.0,1.0,1.0]
-            f[0] = h1 - x[0] + x[1]*q1**x[2]
-            f[1] = h2 - x[0] + x[1]*q2**x[2]
-            f[2] = h3 - x[0] + x[1]*q3**x[2]
+            f = [1.0, 1.0, 1.0]
+            f[0] = h1 - x[0] + x[1] * q1 ** x[2]
+            f[1] = h2 - x[0] + x[1] * q2 ** x[2]
+            f[2] = h3 - x[0] + x[1] * q3 ** x[2]
             return f
 
-        X = [a,b,c]
+        X = [a, b, c]
         Y = curve_fun(X)
 
-        self.assertAlmostEqual(Y[0],0.0)
-        self.assertAlmostEqual(Y[1],0.0)
-        self.assertAlmostEqual(Y[2],0.0)
-        
+        self.assertAlmostEqual(Y[0], 0.0)
+        self.assertAlmostEqual(Y[1], 0.0)
+        self.assertAlmostEqual(Y[2], 0.0)
+
     def test_2_pt_head_curve(self):
         q1 = 0.0
         h1 = 1.0
         q2 = 1.0
         h2 = 0.0
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_curve('curve1','HEAD',[(q1, h1),(q2, h2)])
-        curve = wn.get_curve('curve1')
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_pump('p1', 'j1', 'j2', 'HEAD', curve)
-        link = wn.get_link('p1')
-        a,b,c = link.get_head_curve_coefficients()
-
-        Y = [a-b,0.5 - (a - b * (0.5) ** c),1.0 - a]
-        
-        self.assertAlmostEqual(Y[0],0.0)
-        self.assertAlmostEqual(Y[1],0.0)
-        self.assertAlmostEqual(Y[2],0.0)
+        wn.add_curve("curve1", "HEAD", [(q1, h1), (q2, h2)])
+        curve = wn.get_curve("curve1")
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_pump("p1", "j1", "j2", "HEAD", curve.name)
+        link = wn.get_link("p1")
+        a, b, c = link.get_head_curve_coefficients()
 
+        Y = [a - b, 0.5 - (a - b * (0.5) ** c), 1.0 - a]
+
+        self.assertAlmostEqual(Y[0], 0.0)
+        self.assertAlmostEqual(Y[1], 0.0)
+        self.assertAlmostEqual(Y[2], 0.0)
+
+    
     def test_3_pt_head_curve(self):
         q1 = 0.0
         h1 = 35.0
@@ -356,51 +375,52 @@ def test_3_pt_head_curve(self):
         q3 = 18.0
         h3 = 2.0
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_curve('curve1','HEAD',[(q1, h1),(q2,h2),(q3,h3)])
-        curve = wn.get_curve('curve1')
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_pump('p1', 'j1', 'j2', 'HEAD', curve)
-        link = wn.get_link('p1')
-        a,b,c = link.get_head_curve_coefficients()
+        wn.add_curve("curve1", "HEAD", [(q1, h1), (q2, h2), (q3, h3)])
+        curve = wn.get_curve("curve1")
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_pump("p1", "j1", "j2", "HEAD", curve.name)
+        link = wn.get_link("p1")
+        a, b, c = link.get_head_curve_coefficients()
 
         def curve_fun(x):
-            f=[1.0,1.0,1.0]
-            f[0] = h1 - x[0] + x[1]*q1**x[2]
-            f[1] = h2 - x[0] + x[1]*q2**x[2]
-            f[2] = h3 - x[0] + x[1]*q3**x[2]
+            f = [1.0, 1.0, 1.0]
+            f[0] = h1 - x[0] + x[1] * q1 ** x[2]
+            f[1] = h2 - x[0] + x[1] * q2 ** x[2]
+            f[2] = h3 - x[0] + x[1] * q3 ** x[2]
             return f
 
-        X = [a,b,c]
+        X = [a, b, c]
         Y = curve_fun(X)
 
-        self.assertAlmostEqual(Y[0],0.0)
-        self.assertAlmostEqual(Y[1],0.0)
-        self.assertAlmostEqual(Y[2],0.0)
-        
+        self.assertAlmostEqual(Y[0], 0.0)
+        self.assertAlmostEqual(Y[1], 0.0)
+        self.assertAlmostEqual(Y[2], 0.0)
+
     def test_multi_pt_head_curve(self):
-        #pump_curves = pump_curves_for_testing() # change this to read in a csv file
-        
-        df = pd.read_csv(join(test_data_dir,'pump_practice_curves.csv'),skiprows=5)
+        # pump_curves = pump_curves_for_testing() # change this to read in a csv file
+
+        df = pd.read_csv(join(test_data_dir, "pump_practice_curves.csv"), skiprows=5)
         pump_curves = []
         for i in range(11):
-            pump_curves.append(df[df['curve number']==i].iloc[:,1:3])
+            pump_curves.append(df[df["curve number"] == i].iloc[:, 1:3])
 
-        # these are the least squares optimal curve coefficients for 
-        # pump_curves!            
+        # these are the least squares optimal curve coefficients for
+        # pump_curves!
         expected_coef = [
-                (95.2793750017631, 92.93210451708887, 1.7962733026912), 
-                (65.73126364420834, 754.0506268456613, 3.4783989343179305), 
-                (21.278376275311476, 4077.4535673165924, 2.358360722834581), 
-                (80.75954376866605, 7027.4864048631935, 4.082009299805731), 
-                (136.17943689581463, 829.0733504294045, 4.489091828720322), 
-                (127.24246902634025, 180.0189467345297, 3.932735276445609), 
-                (26.851737190843544, 546.1599637089113, 2.200609181978668), 
-                (49.44241876166496, 933337.4378918532, 2.7686910719067157), 
-                (34.29268375712387, 110108.09777459255, 3.522011383917406), 
-                (34.11693364265361, 170.07427285446153, 1.6314432418473557), 
-                (14.886248327061447, 3667.672645345962, 2.1488882176053345)]
-        
+            (95.2793750017631, 92.93210451708887, 1.7962733026912),
+            (65.73126364420834, 754.0506268456613, 3.4783989343179305),
+            (21.278376275311476, 4077.4535673165924, 2.358360722834581),
+            (80.75954376866605, 7027.4864048631935, 4.082009299805731),
+            (136.17943689581463, 829.0733504294045, 4.489091828720322),
+            (127.24246902634025, 180.0189467345297, 3.932735276445609),
+            (26.851737190843544, 546.1599637089113, 2.200609181978668),
+            (49.44241876166496, 933337.4378918532, 2.7686910719067157),
+            (34.29268375712387, 110108.09777459255, 3.522011383917406),
+            (34.11693364265361, 170.07427285446153, 1.6314432418473557),
+            (14.886248327061447, 3667.672645345962, 2.1488882176053345),
+        ]
+
         # start an empty WNTR model and add dummy junctions so that
         # head pumps can be added
         wn = self.wntr.network.WaterNetworkModel()
@@ -409,20 +429,23 @@ def test_multi_pt_head_curve(self):
 
         # check all of the pump_curves
         for idx, curve in enumerate(pump_curves):
-            pump_name = "p" + str(idx+1)
-            curve_name = "c" + str(idx+1)
-            
-            wn.add_curve(curve_name,"HEAD",curve.values)
-            wn.add_pump(pump_name,"j1","j2",pump_type="HEAD", pump_parameter=curve_name)
+            pump_name = "p" + str(idx + 1)
+            curve_name = "c" + str(idx + 1)
+
+            wn.add_curve(curve_name, "HEAD", curve.values)
+            wn.add_pump(
+                pump_name, "j1", "j2", pump_type="HEAD", pump_parameter=curve_name
+            )
             pump = wn.get_link(pump_name)
             coef = pump.get_head_curve_coefficients()
-    
+
             for i in range(2):
-                error = abs(coef[i] - expected_coef[idx][i])/expected_coef[idx][i]
+                error = abs(coef[i] - expected_coef[idx][i]) / expected_coef[idx][i]
                 self.assertLess(error, 1e-6)
-            #self.wntr.graphics.curve.plot_pump_curve(pump)
-            #savefig(pump_name + "_" + cname + ".png")
-            
+            # self.wntr.graphics.curve.plot_pump_curve(pump)
+            # savefig(pump_name + "_" + cname + ".png")
+
+    
     def test_multi_pt_head_curve_expected_error(self):
         # now test two error conditions using bad datasets.
         wn = self.wntr.network.WaterNetworkModel()
@@ -430,442 +453,488 @@ def test_multi_pt_head_curve_expected_error(self):
         wn.add_junction("j2")
 
         # Negative coefficients
-        data_points1 = [(x,x**2 + 2*x + 3) for x in range(10)]
-        wn.add_curve("curve1","HEAD", data_points1)
-        wn.add_pump("pump1","j1","j2",pump_type="HEAD", pump_parameter="curve1")
+        data_points1 = [(x, x ** 2 + 2 * x + 3) for x in range(10)]
+        wn.add_curve("curve1", "HEAD", data_points1)
+        wn.add_pump("pump1", "j1", "j2", pump_type="HEAD", pump_parameter="curve1")
         pump = wn.get_link("pump1")
         with self.assertRaises(RuntimeError):
             pump.get_head_curve_coefficients()
 
         # Bad fit
-        data_points2 = [(x,(-1)**x * np.exp(-0.001*x) * 100) for x in range(10)]
-        wn.add_curve("curve2","HEAD", data_points2)
-        wn.add_pump("pump2","j1","j2",pump_type="HEAD", pump_parameter="curve2")
+        data_points2 = [(x, (-1) ** x * np.exp(-0.001 * x) * 100) for x in range(10)]
+        wn.add_curve("curve2", "HEAD", data_points2)
+        wn.add_pump("pump2", "j1", "j2", pump_type="HEAD", pump_parameter="curve2")
         pump = wn.get_link("pump2")
         with self.assertRaises(RuntimeError):
             pump.get_head_curve_coefficients()
 
     def test_get_links_for_node_all(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_junction('j3')
-        wn.add_junction('j4')
-        wn.add_junction('j5')
-        wn.add_pipe('p1','j1','j2')
-        wn.add_pipe('p2','j1','j4')
-        wn.add_pipe('p3','j1','j5')
-        wn.add_pipe('p4','j3','j2')
-        wn.add_pipe('p5','j4','j3')
-        l1 = wn.get_links_for_node('j1')
-        l2 = wn.get_links_for_node('j2','all')
-        l3 = wn.get_links_for_node('j3','all')
-        l4 = wn.get_links_for_node('j4')
-        l5 = wn.get_links_for_node('j5')
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_junction("j3")
+        wn.add_junction("j4")
+        wn.add_junction("j5")
+        wn.add_pipe("p1", "j1", "j2")
+        wn.add_pipe("p2", "j1", "j4")
+        wn.add_pipe("p3", "j1", "j5")
+        wn.add_pipe("p4", "j3", "j2")
+        wn.add_pipe("p5", "j4", "j3")
+        l1 = wn.get_links_for_node("j1")
+        l2 = wn.get_links_for_node("j2", "all")
+        l3 = wn.get_links_for_node("j3", "all")
+        l4 = wn.get_links_for_node("j4")
+        l5 = wn.get_links_for_node("j5")
         l1.sort()
         l2.sort()
         l3.sort()
         l4.sort()
         l5.sort()
-        self.assertEqual(l1,['p1','p2','p3'])
-        self.assertEqual(l2,['p1','p4'])
-        self.assertEqual(l3,['p4','p5'])
-        self.assertEqual(l4,['p2','p5'])
-        self.assertEqual(l5,['p3'])
+        self.assertEqual(l1, ["p1", "p2", "p3"])
+        self.assertEqual(l2, ["p1", "p4"])
+        self.assertEqual(l3, ["p4", "p5"])
+        self.assertEqual(l4, ["p2", "p5"])
+        self.assertEqual(l5, ["p3"])
 
     def test_get_links_for_node_in(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_junction('j3')
-        wn.add_junction('j4')
-        wn.add_junction('j5')
-        wn.add_pipe('p1','j1','j2')
-        wn.add_pipe('p2','j1','j4')
-        wn.add_pipe('p3','j1','j5')
-        wn.add_pipe('p4','j3','j2')
-        wn.add_pipe('p5','j4','j3')
-        l1 = wn.get_links_for_node('j1','inlet')
-        l2 = wn.get_links_for_node('j2','inlet')
-        l3 = wn.get_links_for_node('j3','inlet')
-        l4 = wn.get_links_for_node('j4','inlet')
-        l5 = wn.get_links_for_node('j5','inlet')
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_junction("j3")
+        wn.add_junction("j4")
+        wn.add_junction("j5")
+        wn.add_pipe("p1", "j1", "j2")
+        wn.add_pipe("p2", "j1", "j4")
+        wn.add_pipe("p3", "j1", "j5")
+        wn.add_pipe("p4", "j3", "j2")
+        wn.add_pipe("p5", "j4", "j3")
+        l1 = wn.get_links_for_node("j1", "inlet")
+        l2 = wn.get_links_for_node("j2", "inlet")
+        l3 = wn.get_links_for_node("j3", "inlet")
+        l4 = wn.get_links_for_node("j4", "inlet")
+        l5 = wn.get_links_for_node("j5", "inlet")
         l1.sort()
         l2.sort()
         l3.sort()
         l4.sort()
         l5.sort()
-        self.assertEqual(l1,[])
-        self.assertEqual(l2,['p1','p4'])
-        self.assertEqual(l3,['p5'])
-        self.assertEqual(l4,['p2'])
-        self.assertEqual(l5,['p3'])
+        self.assertEqual(l1, [])
+        self.assertEqual(l2, ["p1", "p4"])
+        self.assertEqual(l3, ["p5"])
+        self.assertEqual(l4, ["p2"])
+        self.assertEqual(l5, ["p3"])
 
     def test_get_links_for_node_out(self):
         wn = self.wntr.network.WaterNetworkModel()
-        wn.add_junction('j1')
-        wn.add_junction('j2')
-        wn.add_junction('j3')
-        wn.add_junction('j4')
-        wn.add_junction('j5')
-        wn.add_pipe('p1','j1','j2')
-        wn.add_pipe('p2','j1','j4')
-        wn.add_pipe('p3','j1','j5')
-        wn.add_pipe('p4','j3','j2')
-        wn.add_pipe('p5','j4','j3')
-        l1 = wn.get_links_for_node('j1','outlet')
-        l2 = wn.get_links_for_node('j2','outlet')
-        l3 = wn.get_links_for_node('j3','outlet')
-        l4 = wn.get_links_for_node('j4','outlet')
-        l5 = wn.get_links_for_node('j5','outlet')
+        wn.add_junction("j1")
+        wn.add_junction("j2")
+        wn.add_junction("j3")
+        wn.add_junction("j4")
+        wn.add_junction("j5")
+        wn.add_pipe("p1", "j1", "j2")
+        wn.add_pipe("p2", "j1", "j4")
+        wn.add_pipe("p3", "j1", "j5")
+        wn.add_pipe("p4", "j3", "j2")
+        wn.add_pipe("p5", "j4", "j3")
+        l1 = wn.get_links_for_node("j1", "outlet")
+        l2 = wn.get_links_for_node("j2", "outlet")
+        l3 = wn.get_links_for_node("j3", "outlet")
+        l4 = wn.get_links_for_node("j4", "outlet")
+        l5 = wn.get_links_for_node("j5", "outlet")
         l1.sort()
         l2.sort()
         l3.sort()
         l4.sort()
         l5.sort()
-        self.assertEqual(l1,['p1','p2','p3'])
-        self.assertEqual(l2,[])
-        self.assertEqual(l3,['p4'])
-        self.assertEqual(l4,['p5'])
-        self.assertEqual(l5,[])
-
-    
-epanet_unit_id = {'CFS': 0, 'GPM': 1, 'MGD': 2, 'IMGD': 3, 'AFD': 4,
-                  'LPS': 5, 'LPM': 6, 'MLD': 7, 'CMH':  8, 'CMD': 9}
-
-def test_Net1():
-    inp_file = join(ex_datadir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    G = wn.get_graph()
-
-    node = G.nodes
-    elevation = wn.query_node_attribute('elevation')
-    #base_demand = wn.query_node_attribute('base_demand')
-    edge = G.adj
-    diameter = wn.query_link_attribute('diameter')
-    length = wn.query_link_attribute('length')
-
-    # Data from the INP file, converted using flowunits
-    expected_node = {'11': {'type': 'Junction', 'pos': (30.0, 70.0)},
-                     '10': {'type': 'Junction', 'pos': (20.0, 70.0)},
-                     '13': {'type': 'Junction', 'pos': (70.0, 70.0)},
-                     '12': {'type': 'Junction', 'pos': (50.0, 70.0)},
-                     '21': {'type': 'Junction', 'pos': (30.0, 40.0)},
-                     '22': {'type': 'Junction', 'pos': (50.0, 40.0)},
-                     '23': {'type': 'Junction', 'pos': (70.0, 40.0)},
-                     '32': {'type': 'Junction', 'pos': (50.0, 10.0)},
-                     '31': {'type': 'Junction', 'pos': (30.0, 10.0)},
-                     '2':  {'type': 'Tank', 'pos': (50.0, 90.0)},
-                     '9':  {'type': 'Reservoir', 'pos': (10.0, 70.0)}}
-
-    expected_elevation = {'11': 710.0,
-                          '10': 710.0,
-                          '13': 695.0,
-                          '12': 700.0,
-                          '21': 700.0,
-                          '22': 695.0,
-                          '23': 690.0,
-                          '32': 710.0,
-                          '31': 700.0,
-                          '2':  850.0}
-    expected_elevation = wntr.epanet.util.HydParam.Elevation._to_si(wn._inpfile.flow_units, expected_elevation)
-
-    expected_base_demand = {'11': 150,
-                            '10':   0,
-                            '13': 100,
-                            '12': 150,
-                            '21': 150,
-                            '22': 200,
-                            '23': 150,
-                            '32': 100,
-                            '31': 100}
-    expected_base_demand = wntr.epanet.util.HydParam.Demand._to_si(wn._inpfile.flow_units, expected_base_demand)
-
-    expected_edge = {'11': {'12': {'11':  {'type': 'Pipe'}},
-                            '21': {'111': {'type': 'Pipe'}}},
-                     '10': {'11': {'10':  {'type': 'Pipe'}}},
-                     '13': {'23': {'113': {'type': 'Pipe'}}},
-                     '12': {'13': {'12':  {'type': 'Pipe'}},
-                            '22': {'112': {'type': 'Pipe'}}},
-                     '21': {'31': {'121': {'type': 'Pipe'}},
-                            '22': {'21':  {'type': 'Pipe'}}},
-                     '22': {'32': {'122': {'type': 'Pipe'}},
-                            '23': {'22':  {'type': 'Pipe'}}},
-                     '23': {},
-                     '32': {},
-                     '31': {'32': {'31':  {'type': 'Pipe'}}},
-                     '2':  {'12': {'110': {'type': 'Pipe'}}},
-                     '9':  {'10': {'9':   {'type': 'Pump'}}}}
-
-    expected_diameter = {'11':  14.0,
-                         '111': 10.0,
-                         '10':  18.0,
-                         '113':  8.0,
-                         '12':  10.0,
-                         '112': 12.0,
-                         '121':  8.0,
-                         '21':  10.0,
-                         '122':  6.0,
-                         '22':  12.0,
-                         '31':   6.0,
-                         '110': 18.0}
-    expected_diameter = wntr.epanet.util.HydParam.PipeDiameter._to_si(wn._inpfile.flow_units, expected_diameter)
-
-    expected_length = {'11':  5280.0,
-                       '111': 5280.0,
-                       '10': 10530.0,
-                       '113': 5280.0,
-                       '12':  5280.0,
-                       '112': 5280.0,
-                       '121': 5280.0,
-                       '21':  5280.0,
-                       '122': 5280.0,
-                       '22':  5280.0,
-                       '31':  5280.0,
-                       '110':  200.0}
-    expected_length = wntr.epanet.util.HydParam.Length._to_si(wn._inpfile.flow_units, expected_length)
-
-    assert_dict_equal(dict(node), expected_node)
-    assert_dict_equal(dict(elevation), expected_elevation)
-    #assert_dict_equal(base_demand, expected_base_demand)
-
-    assert_dict_equal(dict(edge), expected_edge)
-    assert_dict_equal(dict(diameter), expected_diameter)
-    assert_dict_equal(dict(length), expected_length)
-
-def test_query_node_attribute():
-    inp_file = join(ex_datadir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    elevation = 213.36 #700*float(units.ft/units.m) # ft to m
-    nodes = wn.query_node_attribute('elevation', np.less, elevation)
-
-    expected_nodes = set(['13', '22', '23'])
-
-    assert_set_equal(set(nodes.keys()), expected_nodes)
-
-def test_query_pipe_attribute():
-    inp_file = join(ex_datadir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    length = 1609.344 #5280*float(units.ft/units.m) # ft to m
-    pipes = wn.query_link_attribute('length', np.greater, length)
-
-    expected_pipes = set(['10'])
-
-    assert_set_equal(set(pipes.keys()), expected_pipes)
-
-def test_nzd_nodes():
-    inp_file = join(ex_datadir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    nzd_nodes = []
-    for name, node in wn.junctions():
-        demand = 0
-        for ts in np.arange(0, wn.options.time.duration, wn.options.time.report_timestep):
-            demand += node.demand_timeseries_list.at(ts)
-        if demand > 0:
-            nzd_nodes.append(name)
-        
-    expected_nodes = set(['11', '13', '12', '21', '22', '23', '32', '31'])
-
-    assert_set_equal(set(nzd_nodes), expected_nodes)
-
-def test_name_list():
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    assert_in('10', wn.junction_name_list)
-    assert_in('1', wn.tank_name_list)
-    assert_in('River', wn.reservoir_name_list)
-    assert_in('20', wn.pipe_name_list)
-    assert_in('10', wn.pump_name_list)
-    assert_equal(0, len(wn.valve_name_list))
-    assert_in('1', wn.pattern_name_list)
-    assert_in('1', wn.curve_name_list)
-    assert_equal(0, len(wn.source_name_list))
-#    assert_equal(0, len(wn._demand_name_list))
-    assert_in('control 1', wn.control_name_list)
-
-def test_add_get_remove_num():
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    wn.add_junction('new_junc')
-    wn.get_node('new_junc')
-    
-    wn.add_tank('new_tank')
-    wn.get_node('new_tank')
-    
-    wn.add_reservoir('new_reservoir')
-    wn.get_node('new_reservoir')
-    
-    wn.add_pipe('new_pipe', '139', '131')
-    wn.get_link('new_pipe')
-    
-    wn.add_pump('new_pump', '139', '131')
-    wn.get_link('new_pump')
-    
-    wn.add_valve('new_valve', '139', '131')
-    wn.get_link('new_valve')
-    
-    wn.add_pattern('new_pattern', [])
-    wn.get_pattern('new_pattern')
-    
-    wn.add_curve('new_curve', 'HEAD', [])
-    wn.get_curve('new_curve')
-    
-    wn.add_source('new_source', 'new_junc', 'CONCEN', 1, 'new_pattern')
-    wn.get_source('new_source')
-    
-    nums = [wn.num_junctions,
-           wn.num_tanks,
-           wn.num_reservoirs,
-           wn.num_pipes,
-           wn.num_pumps,
-           wn.num_valves,
-           wn.num_patterns,
-           wn.num_curves,
-           wn.num_sources]
-    expected = [93,4,3,118,3,1,6,3,1]
-    assert_list_equal(nums, expected)
-
-    wn.remove_source('new_source')
-    wn.remove_curve('new_curve')
-    wn.remove_pattern('new_pattern')
-    wn.remove_link('new_pipe')
-    wn.remove_link('new_pump')
-    wn.remove_link('new_valve')
-    wn.remove_node('new_junc')
-    wn.remove_node('new_tank')
-    wn.remove_node('new_reservoir')
-    
-    nums = [wn.num_junctions,
-           wn.num_tanks,
-           wn.num_reservoirs,
-           wn.num_pipes,
-           wn.num_pumps,
-           wn.num_valves,
-           wn.num_patterns,
-           wn.num_curves,
-           wn.num_sources]
-    expected = [92,3,2,117,2,0,5,2,0]
-    assert_list_equal(nums, expected)
-
-def test_describe():
-    inp_file = join(ex_datadir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    d0 = wn.describe(0)
-    assert_dict_equal(d0, {'Nodes': 97, 'Links': 119, 'Patterns': 5, 
-                           'Curves': 2, 'Sources': 0, 'Controls': 18})
-    
-    d1 = wn.describe(1)
-    assert_dict_equal(d1, {'Nodes': {'Junctions': 92,
-                                     'Tanks': 3,
-                                     'Reservoirs': 2}, 
-                           'Links': {'Pipes': 117,
-                                     'Pumps': 2,
-                                     'Valves': 0}, 
-                           'Patterns': 5, 
-                           'Curves': {'Pump': 2, 
-                                      'Efficiency': 0,  
-                                      'Headloss': 0, 
-                                      'Volume': 0}, 
-                           'Sources': 0, 
-                           'Controls': 18})  
-
-def test_assign_demand():
-    
-    inp_file = join(ex_datadir, 'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    demands0 = wntr.metrics.expected_demand(wn)
-    pattern_name0 = wn.get_node('10').demand_timeseries_list[0].pattern_name
-    
-    wn.options.hydraulic.demand_multiplier = 1.5
-    demands1 = wntr.metrics.expected_demand(wn)
-    
-    assert_equal(pattern_name0, '1')
-    assert_equal(len(wn.pattern_name_list), 5) # number of original patterns
-    assert_less(abs((demands1/demands0).max().max()-1.5), 0.000001)
-    
-    sim1 = wntr.sim.EpanetSimulator(wn)
-    results1 = sim1.run_sim()
-    demands_sim1 = results1.node['demand'].loc[:,wn.junction_name_list]
-    
-    ### re-assign demands to be 2 times the original demands
-    wn.assign_demand(demands1*2, pattern_prefix='ResetDemand1_')
-
-    demands2 = wntr.metrics.expected_demand(wn)
-    pattern_name = wn.get_node('10').demand_timeseries_list[0].pattern_name
-    
-    sim = wntr.sim.EpanetSimulator(wn)
-    results2 = sim.run_sim()
-    demands_sim2 = results2.node['demand'].loc[:,wn.junction_name_list]
-    
-    assert_equal(pattern_name, 'ResetDemand1_10')
-    assert_equal(len(wn.pattern_name_list), wn.num_junctions+5)
-    assert_less(abs((demands2/demands1).max().max()-2), 0.000001)
-    assert_less(abs((demands_sim2/demands_sim1).max().max()-2), 0.01)
-    
-    ### re-assign demands using results from the simulation
-    wn.assign_demand(demands_sim2, pattern_prefix='ResetDemand2_')
-
-    demands2 = wntr.metrics.expected_demand(wn)
-    pattern_name = wn.get_node('10').demand_timeseries_list[0].pattern_name
-    
-    sim = wntr.sim.EpanetSimulator(wn)
-    results2 = sim.run_sim()
-    demands_sim2 = results2.node['demand'].loc[:,wn.junction_name_list]
-    
-    assert_equal(pattern_name, 'ResetDemand2_10')
-    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)
-    
-
+        self.assertEqual(l1, ["p1", "p2", "p3"])
+        self.assertEqual(l2, [])
+        self.assertEqual(l3, ["p4"])
+        self.assertEqual(l4, ["p5"])
+        self.assertEqual(l5, [])
+
+
+epanet_unit_id = {
+    "CFS": 0,
+    "GPM": 1,
+    "MGD": 2,
+    "IMGD": 3,
+    "AFD": 4,
+    "LPS": 5,
+    "LPM": 6,
+    "MLD": 7,
+    "CMH": 8,
+    "CMD": 9,
+}
+
+
+class TestCase(unittest.TestCase):
+    def test_Net1(self):
+        inp_file = join(ex_datadir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        G = wn.get_graph()
+
+        node = G.nodes
+        elevation = wn.query_node_attribute("elevation")
+        # base_demand = wn.query_node_attribute('base_demand')
+        edge = G.adj
+        diameter = wn.query_link_attribute("diameter")
+        length = wn.query_link_attribute("length")
+
+        # Data from the INP file, converted using flowunits
+        expected_node = {
+            "11": {"type": "Junction", "pos": (30.0, 70.0)},
+            "10": {"type": "Junction", "pos": (20.0, 70.0)},
+            "13": {"type": "Junction", "pos": (70.0, 70.0)},
+            "12": {"type": "Junction", "pos": (50.0, 70.0)},
+            "21": {"type": "Junction", "pos": (30.0, 40.0)},
+            "22": {"type": "Junction", "pos": (50.0, 40.0)},
+            "23": {"type": "Junction", "pos": (70.0, 40.0)},
+            "32": {"type": "Junction", "pos": (50.0, 10.0)},
+            "31": {"type": "Junction", "pos": (30.0, 10.0)},
+            "2": {"type": "Tank", "pos": (50.0, 90.0)},
+            "9": {"type": "Reservoir", "pos": (10.0, 70.0)},
+        }
+
+        expected_elevation = {
+            "11": 710.0,
+            "10": 710.0,
+            "13": 695.0,
+            "12": 700.0,
+            "21": 700.0,
+            "22": 695.0,
+            "23": 690.0,
+            "32": 710.0,
+            "31": 700.0,
+            "2": 850.0,
+        }
+        expected_elevation = wntr.epanet.util.HydParam.Elevation._to_si(
+            wn._inpfile.flow_units, expected_elevation
+        )
+
+        expected_base_demand = {
+            "11": 150,
+            "10": 0,
+            "13": 100,
+            "12": 150,
+            "21": 150,
+            "22": 200,
+            "23": 150,
+            "32": 100,
+            "31": 100,
+        }
+        expected_base_demand = wntr.epanet.util.HydParam.Demand._to_si(
+            wn._inpfile.flow_units, expected_base_demand
+        )
+
+        expected_edge = {
+            "11": {"12": {"11": {"type": "Pipe"}}, "21": {"111": {"type": "Pipe"}}},
+            "10": {"11": {"10": {"type": "Pipe"}}},
+            "13": {"23": {"113": {"type": "Pipe"}}},
+            "12": {"13": {"12": {"type": "Pipe"}}, "22": {"112": {"type": "Pipe"}}},
+            "21": {"31": {"121": {"type": "Pipe"}}, "22": {"21": {"type": "Pipe"}}},
+            "22": {"32": {"122": {"type": "Pipe"}}, "23": {"22": {"type": "Pipe"}}},
+            "23": {},
+            "32": {},
+            "31": {"32": {"31": {"type": "Pipe"}}},
+            "2": {"12": {"110": {"type": "Pipe"}}},
+            "9": {"10": {"9": {"type": "Pump"}}},
+        }
+
+        expected_diameter = {
+            "11": 14.0,
+            "111": 10.0,
+            "10": 18.0,
+            "113": 8.0,
+            "12": 10.0,
+            "112": 12.0,
+            "121": 8.0,
+            "21": 10.0,
+            "122": 6.0,
+            "22": 12.0,
+            "31": 6.0,
+            "110": 18.0,
+        }
+        expected_diameter = wntr.epanet.util.HydParam.PipeDiameter._to_si(
+            wn._inpfile.flow_units, expected_diameter
+        )
+
+        expected_length = {
+            "11": 5280.0,
+            "111": 5280.0,
+            "10": 10530.0,
+            "113": 5280.0,
+            "12": 5280.0,
+            "112": 5280.0,
+            "121": 5280.0,
+            "21": 5280.0,
+            "122": 5280.0,
+            "22": 5280.0,
+            "31": 5280.0,
+            "110": 200.0,
+        }
+        expected_length = wntr.epanet.util.HydParam.Length._to_si(
+            wn._inpfile.flow_units, expected_length
+        )
+
+        self.assertDictEqual(dict(node), expected_node)
+        self.assertDictEqual(dict(elevation), expected_elevation)
+        # self.assertDictEqual(base_demand, expected_base_demand)
+
+        self.assertDictEqual(dict(edge), expected_edge)
+        self.assertDictEqual(dict(diameter), expected_diameter)
+        self.assertDictEqual(dict(length), expected_length)
+
+    def test_query_node_attribute(self):
+        inp_file = join(ex_datadir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        elevation = 213.36  # 700*float(units.ft/units.m) # ft to m
+        nodes = wn.query_node_attribute("elevation", np.less, elevation)
+
+        expected_nodes = set(["13", "22", "23"])
+
+        self.assertSetEqual(set(nodes.keys()), expected_nodes)
+
+    def test_query_pipe_attribute(self):
+        inp_file = join(ex_datadir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        length = 1609.344  # 5280*float(units.ft/units.m) # ft to m
+        pipes = wn.query_link_attribute("length", np.greater, length)
+
+        expected_pipes = set(["10"])
+
+        self.assertSetEqual(set(pipes.keys()), expected_pipes)
+
+    def test_nzd_nodes(self):
+        inp_file = join(ex_datadir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        nzd_nodes = []
+        for name, node in wn.junctions():
+            demand = 0
+            for ts in np.arange(
+                0, wn.options.time.duration, wn.options.time.report_timestep
+            ):
+                demand += node.demand_timeseries_list.at(ts)
+            if demand > 0:
+                nzd_nodes.append(name)
+
+        expected_nodes = set(["11", "13", "12", "21", "22", "23", "32", "31"])
+
+        self.assertSetEqual(set(nzd_nodes), expected_nodes)
+
+    def test_name_list(self):
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        self.assertIn("10", wn.junction_name_list)
+        self.assertIn("1", wn.tank_name_list)
+        self.assertIn("River", wn.reservoir_name_list)
+        self.assertIn("20", wn.pipe_name_list)
+        self.assertIn("10", wn.pump_name_list)
+        self.assertEqual(0, len(wn.valve_name_list))
+        self.assertIn("1", wn.pattern_name_list)
+        self.assertIn("1", wn.curve_name_list)
+        self.assertEqual(0, len(wn.source_name_list))
+        #    self.assertEqual(0, len(wn._demand_name_list))
+        self.assertIn("control 1", wn.control_name_list)
+
+    def test_add_get_remove_num(self):
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        wn.add_junction("new_junc")
+        wn.get_node("new_junc")
+
+        wn.add_tank("new_tank")
+        wn.get_node("new_tank")
+
+        wn.add_reservoir("new_reservoir")
+        wn.get_node("new_reservoir")
+
+        wn.add_pipe("new_pipe", "139", "131")
+        wn.get_link("new_pipe")
+
+        wn.add_pump("new_pump", "139", "131")
+        wn.get_link("new_pump")
+
+        wn.add_valve("new_valve", "139", "131")
+        wn.get_link("new_valve")
+
+        wn.add_pattern("new_pattern", [])
+        wn.get_pattern("new_pattern")
+
+        wn.add_curve("new_curve", "HEAD", [])
+        wn.get_curve("new_curve")
+
+        wn.add_source("new_source", "new_junc", "CONCEN", 1, "new_pattern")
+        wn.get_source("new_source")
+
+        nums = [
+            wn.num_junctions,
+            wn.num_tanks,
+            wn.num_reservoirs,
+            wn.num_pipes,
+            wn.num_pumps,
+            wn.num_valves,
+            wn.num_patterns,
+            wn.num_curves,
+            wn.num_sources,
+        ]
+        expected = [93, 4, 3, 118, 3, 1, 6, 3, 1]
+        self.assertListEqual(nums, expected)
+
+        wn.remove_source("new_source")
+        wn.remove_curve("new_curve")
+        wn.remove_pattern("new_pattern")
+        wn.remove_link("new_pipe")
+        wn.remove_link("new_pump")
+        wn.remove_link("new_valve")
+        wn.remove_node("new_junc")
+        wn.remove_node("new_tank")
+        wn.remove_node("new_reservoir")
+
+        nums = [
+            wn.num_junctions,
+            wn.num_tanks,
+            wn.num_reservoirs,
+            wn.num_pipes,
+            wn.num_pumps,
+            wn.num_valves,
+            wn.num_patterns,
+            wn.num_curves,
+            wn.num_sources,
+        ]
+        expected = [92, 3, 2, 117, 2, 0, 5, 2, 0]
+        self.assertListEqual(nums, expected)
+
+    def test_describe(self):
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        d0 = wn.describe(0)
+        self.assertDictEqual(
+            d0,
+            {
+                "Nodes": 97,
+                "Links": 119,
+                "Patterns": 5,
+                "Curves": 2,
+                "Sources": 0,
+                "Controls": 18,
+            },
+        )
+
+        d1 = wn.describe(1)
+        self.assertDictEqual(
+            d1,
+            {
+                "Nodes": {"Junctions": 92, "Tanks": 3, "Reservoirs": 2},
+                "Links": {"Pipes": 117, "Pumps": 2, "Valves": 0},
+                "Patterns": 5,
+                "Curves": {"Pump": 2, "Efficiency": 0, "Headloss": 0, "Volume": 0},
+                "Sources": 0,
+                "Controls": 18,
+            },
+        )
+
+    def test_assign_demand(self):
+
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        demands0 = wntr.metrics.expected_demand(wn)
+        pattern_name0 = wn.get_node("10").demand_timeseries_list[0].pattern_name
+
+        wn.options.hydraulic.demand_multiplier = 1.5
+        demands1 = wntr.metrics.expected_demand(wn)
+
+        self.assertEqual(pattern_name0, "1")
+        self.assertEqual(len(wn.pattern_name_list), 5)  # number of original patterns
+        self.assertLess(abs((demands1 / demands0).max().max() - 1.5), 0.000001)
+
+        sim1 = wntr.sim.EpanetSimulator(wn)
+        results1 = sim1.run_sim()
+        demands_sim1 = results1.node["demand"].loc[:, wn.junction_name_list]
+
+        ### re-assign demands to be 2 times the original demands
+        wn.assign_demand(demands1 * 2, pattern_prefix="ResetDemand1_")
+
+        demands2 = wntr.metrics.expected_demand(wn)
+        pattern_name = wn.get_node("10").demand_timeseries_list[0].pattern_name
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results2 = sim.run_sim()
+        demands_sim2 = results2.node["demand"].loc[:, wn.junction_name_list]
+
+        self.assertEqual(pattern_name, "ResetDemand1_10")
+        self.assertEqual(len(wn.pattern_name_list), wn.num_junctions + 5)
+        self.assertLess(abs((demands2 / demands1).max().max() - 2), 0.000001)
+        self.assertLess(abs((demands_sim2 / demands_sim1).max().max() - 2), 0.01)
+
+        ### re-assign demands using results from the simulation
+        wn.assign_demand(demands_sim2, pattern_prefix="ResetDemand2_")
+
+        demands2 = wntr.metrics.expected_demand(wn)
+        pattern_name = wn.get_node("10").demand_timeseries_list[0].pattern_name
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results2 = sim.run_sim()
+        demands_sim2 = results2.node["demand"].loc[:, wn.junction_name_list]
+
+        self.assertEqual(pattern_name, "ResetDemand2_10")
+        self.assertEqual(len(wn.pattern_name_list), 2 * wn.num_junctions + 5)
+        self.assertLess(abs((demands2 / demands1).max().max() - 2), 0.01)
+        self.assertLess(abs((demands_sim2 / demands_sim1).max().max() - 2), 0.01)
+
+    def test_convert_controls_to_rules(self):
+
+        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:
+                self.assertLess(
+                    abs(
+                        results1.node["pressure"].at[t, node_name]
+                        - results2.node["pressure"].at[t, node_name]
+                    ),
+                    0.001,
+                )
+
 
-if __name__ == '__main__':
-    #unittest.main()
-    test_convert_controls_to_rules()
\ No newline at end of file
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_network_controls.py b/wntr/tests/test_network_controls.py
index c22f50bd5..7ab3bc840 100644
--- a/wntr/tests/test_network_controls.py
+++ b/wntr/tests/test_network_controls.py
@@ -1,82 +1,91 @@
 # These tests test controls
 import unittest
-from nose import SkipTest
+import warnings
 from os.path import abspath, dirname, join
+
 import wntr
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir,'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
 
 class TestValveSettingControls(unittest.TestCase):
     def test_status_open_when_setting_changes(self):
         wn = wntr.network.WaterNetworkModel()
-        wn.add_reservoir('r1', base_head=10)
-        wn.add_junction('j1', base_demand=0)
-        wn.add_junction('j2', base_demand=0.05)
-        wn.add_pipe('p1', 'r1', 'j1')
-        wn.add_valve('v1', 'j1', 'j2', valve_type='PRV', setting=2)
-        wn.options.time.duration = 3600*5
-
-        action = wntr.network.ControlAction(wn.get_link('v1'), 'status', wntr.network.LinkStatus.Closed)
-        condition = wntr.network.SimTimeCondition(wn, '==', 0)
+        wn.add_reservoir("r1", base_head=10)
+        wn.add_junction("j1", base_demand=0)
+        wn.add_junction("j2", base_demand=0.05)
+        wn.add_pipe("p1", "r1", "j1")
+        wn.add_valve("v1", "j1", "j2", valve_type="PRV", initial_setting=2)
+        wn.options.time.duration = 3600 * 5
+
+        action = wntr.network.ControlAction(
+            wn.get_link("v1"), "status", wntr.network.LinkStatus.Closed
+        )
+        condition = wntr.network.SimTimeCondition(wn, "==", 0)
         control = wntr.network.Control(condition=condition, then_action=action)
-        wn.add_control('close_valve', control)
+        wn.add_control("close_valve", control)
 
-        action = wntr.network.ControlAction(wn.get_link('v1'), 'setting', 2)
-        condition = wntr.network.SimTimeCondition(wn, '==', 7200)
+        action = wntr.network.ControlAction(wn.get_link("v1"), "setting", 2)
+        condition = wntr.network.SimTimeCondition(wn, "==", 7200)
         control = wntr.network.Control(condition=condition, then_action=action)
-        wn.add_control('valve_setting', control)
+        wn.add_control("valve_setting", control)
 
         sim = wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
-        self.assertEqual(results.link['status'].at[7200, 'v1'], wntr.network.LinkStatus.Active)
+        self.assertEqual(
+            results.link["status"].at[7200, "v1"], wntr.network.LinkStatus.Active
+        )
 
 
 class TestPumpSettingControls(unittest.TestCase):
     def test_status_open_when_setting_changes(self):
         wn = wntr.network.WaterNetworkModel()
-        wn.add_reservoir('r1', base_head=10)
-        wn.add_junction('j1', base_demand=0)
-        wn.add_junction('j2', base_demand=0.1)
-        wn.add_junction('j3', base_demand=0.05)
-        wn.add_pipe('p1', 'r1', 'j1')
-        wn.add_pipe('p2', 'j1', 'j3')
-        wn.add_curve('pump_curve', 'HEAD', [(0.1, 3)])
-        wn.add_pump('pump1', 'j1', 'j2', pump_type='HEAD', pump_parameter='pump_curve')
+        wn.add_reservoir("r1", base_head=10)
+        wn.add_junction("j1", base_demand=0)
+        wn.add_junction("j2", base_demand=0.1)
+        wn.add_junction("j3", base_demand=0.05)
+        wn.add_pipe("p1", "r1", "j1")
+        wn.add_pipe("p2", "j1", "j3")
+        wn.add_curve("pump_curve", "HEAD", [(0.1, 3)])
+        wn.add_pump("pump1", "j1", "j2", pump_type="HEAD", pump_parameter="pump_curve")
         wn.options.time.duration = 3600 * 5
 
-        action = wntr.network.ControlAction(wn.get_link('pump1'), 'status', wntr.network.LinkStatus.Closed)
-        condition = wntr.network.SimTimeCondition(wn, '==', 0)
+        action = wntr.network.ControlAction(
+            wn.get_link("pump1"), "status", wntr.network.LinkStatus.Closed
+        )
+        condition = wntr.network.SimTimeCondition(wn, "==", 0)
         control = wntr.network.Control(condition=condition, then_action=action)
-        wn.add_control('close_pump', control)
+        wn.add_control("close_pump", control)
 
-        action = wntr.network.ControlAction(wn.get_link('pump1'), 'base_speed', 1)
-        condition = wntr.network.SimTimeCondition(wn, '==', 3600 * 2)
+        action = wntr.network.ControlAction(wn.get_link("pump1"), "base_speed", 1)
+        condition = wntr.network.SimTimeCondition(wn, "==", 3600 * 2)
         control = wntr.network.Control(condition=condition, then_action=action)
-        wn.add_control('pump_base_speed', control)
+        wn.add_control("pump_base_speed", control)
 
         sim = wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
-        self.assertEqual(results.link['status'].at[7200, 'pump1'], wntr.network.LinkStatus.Open)
+        self.assertEqual(
+            results.link["status"].at[7200, "pump1"], wntr.network.LinkStatus.Open
+        )
 
 
 class TestTimeControls(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(test_datadir, 'time_controls.inp')
+        inp_file = join(test_datadir, "time_controls.inp")
         self.wn = self.wntr.network.WaterNetworkModel(inp_file)
-        self.wn.options.time.report_timestep = 'all'
+        self.wn.options.time.report_timestep = "all"
         for jname, j in self.wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
 
-        self.wn.options.hydraulic.demand_model = 'PDA'
+        self.wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(self.wn)
         self.results = sim.run_sim()
 
@@ -88,34 +97,42 @@ def test_time_control_open_vs_closed(self):
         res = self.results
         link_res = res.link
         for t in res.time:
-            if t < 5*3600 or t >= 10*3600:
-                self.assertAlmostEqual(link_res['flowrate'].at[t,'pipe2'], 150/3600.0)
-                self.assertEqual(link_res['status'].at[t,'pipe2'], self.wntr.network.LinkStatus.open)
+            if t < 5 * 3600 or t >= 10 * 3600:
+                self.assertAlmostEqual(
+                    link_res["flowrate"].at[t, "pipe2"], 150 / 3600.0
+                )
+                self.assertEqual(
+                    link_res["status"].at[t, "pipe2"], self.wntr.network.LinkStatus.open
+                )
             else:
-                self.assertAlmostEqual(link_res['flowrate'].at[t,'pipe2'], 0.0)
-                self.assertEqual(link_res['status'].at[t,'pipe2'], self.wntr.network.LinkStatus.closed)
+                self.assertAlmostEqual(link_res["flowrate"].at[t, "pipe2"], 0.0)
+                self.assertEqual(
+                    link_res["status"].at[t, "pipe2"],
+                    self.wntr.network.LinkStatus.closed,
+                )
 
 
 class TestConditionalControls(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
     @classmethod
     def tearDownClass(self):
         pass
 
+    
     def test_close_link_by_tank_level(self):
-        inp_file = join(test_datadir, 'conditional_controls_1.inp')
+        inp_file = join(test_datadir, "conditional_controls_1.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        wn.options.time.report_timestep = 'all'
+        wn.options.time.report_timestep = "all"
         for jname, j in wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
 
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
@@ -124,52 +141,70 @@ def test_close_link_by_tank_level(self):
         node_res = results.node
         link_res = results.link
         for t in results.time:
-            if node_res['pressure'].at[t,'tank1'] >= 50.0 and not activated_flag:
+            if node_res["pressure"].at[t, "tank1"] >= 50.0 and not activated_flag:
                 activated_flag = True
             if activated_flag:
-                self.assertAlmostEqual(link_res['flowrate'].at[t,'pump1'], 0.0)
-                self.assertEqual(link_res['status'].at[t,'pump1'], self.wntr.network.LinkStatus.closed)
+                self.assertAlmostEqual(link_res["flowrate"].at[t, "pump1"], 0.0)
+                self.assertEqual(
+                    link_res["status"].at[t, "pump1"],
+                    self.wntr.network.LinkStatus.closed,
+                )
                 count += 1
             else:
-                self.assertGreaterEqual(link_res['flowrate'].at[t,'pump1'], 0.0001)
-                self.assertEqual(link_res['status'].loc[t,'pump1'], self.wntr.network.LinkStatus.open)
+                self.assertGreaterEqual(link_res["flowrate"].at[t, "pump1"], 0.0001)
+                self.assertEqual(
+                    link_res["status"].loc[t, "pump1"],
+                    self.wntr.network.LinkStatus.open,
+                )
         self.assertEqual(activated_flag, True)
         self.assertGreaterEqual(count, 2)
 
     def test_open_link_by_tank_level(self):
-        inp_file = join(test_datadir, 'conditional_controls_2.inp')
+        inp_file = join(test_datadir, "conditional_controls_2.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        wn.options.time.report_timestep = 'all'
+        wn.options.time.report_timestep = "all"
         for jname, j in wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
         activated_flag = False
-        count = 0 # Used to make sure the link is opened for at least 2 timesteps to make sure the link stays open
-        for t in results.node['pressure'].index:
-            if results.node['pressure'].at[t,'tank1'] >= 300.0 and not activated_flag:
+        count = 0  # Used to make sure the link is opened for at least 2 timesteps to make sure the link stays open
+        for t in results.node["pressure"].index:
+            if results.node["pressure"].at[t, "tank1"] >= 300.0 and not activated_flag:
                 activated_flag = True
             if activated_flag:
-                self.assertGreaterEqual(results.link['flowrate'].at[t,'pipe1'], 0.002)
-                self.assertEqual(results.link['status'].at[t,'pipe1'], self.wntr.network.LinkStatus.open)
-                count +=1
+                self.assertGreaterEqual(results.link["flowrate"].at[t, "pipe1"], 0.002)
+                self.assertEqual(
+                    results.link["status"].at[t, "pipe1"],
+                    self.wntr.network.LinkStatus.open,
+                )
+                count += 1
             else:
-                self.assertAlmostEqual(results.link['flowrate'].at[t,'pipe1'], 0.0)
-                self.assertEqual(results.link['status'].at[t,'pipe1'], self.wntr.network.LinkStatus.closed)
+                self.assertAlmostEqual(results.link["flowrate"].at[t, "pipe1"], 0.0)
+                self.assertEqual(
+                    results.link["status"].at[t, "pipe1"],
+                    self.wntr.network.LinkStatus.closed,
+                )
         self.assertEqual(activated_flag, True)
         self.assertGreaterEqual(count, 2)
-        self.assertEqual(results.link['status'].at[results.link['status'].index[0],'pipe1'], self.wntr.network.LinkStatus.closed) # make sure the pipe starts closed
-        self.assertLessEqual(results.node['pressure'].at[results.node['pressure'].index[0],'tank1'],300.0) # make sure the pipe starts closed
+        self.assertEqual(
+            results.link["status"].at[results.link["status"].index[0], "pipe1"],
+            self.wntr.network.LinkStatus.closed,
+        )  # make sure the pipe starts closed
+        self.assertLessEqual(
+            results.node["pressure"].at[results.node["pressure"].index[0], "tank1"],
+            300.0,
+        )  # make sure the pipe starts closed
 
 
 class TestTankControls(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
     @classmethod
@@ -177,20 +212,23 @@ def tearDownClass(self):
         pass
 
     def test_pipe_closed_for_low_level(self):
-        inp_file = join(test_datadir, 'tank_controls_1.inp')
+        inp_file = join(test_datadir, "tank_controls_1.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
         for jname, j in wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
         tank_level_dropped_flag = False
-        for t in results.node['pressure'].index:
-            if results.node['pressure'].at[t,'tank1'] <= 10.0:
-                self.assertLessEqual(results.link['flowrate'].at[t,'pipe1'],0.0)
-                self.assertEqual(results.link['status'].at[t,'pipe1'],self.wntr.network.LinkStatus.closed)
+        for t in results.node["pressure"].index:
+            if results.node["pressure"].at[t, "tank1"] <= 10.0:
+                self.assertLessEqual(results.link["flowrate"].at[t, "pipe1"], 0.0)
+                self.assertEqual(
+                    results.link["status"].at[t, "pipe1"],
+                    self.wntr.network.LinkStatus.closed,
+                )
                 tank_level_dropped_flag = True
         self.assertEqual(tank_level_dropped_flag, True)
 
@@ -208,17 +246,17 @@ def test_reopen_pipe_after_tank_fills_back_up(self):
         tank_level_dropped_flag = False
         tank_refilled_flag = False
         for t in results.time:
-            if results.node.at['pressure',t,'tank1'] <= 10.0:
-                self.assertLessEqual(results.link.at['flowrate',t,'pipe1'],0.0)
+            if results.node.loc['pressure',t,'tank1'] <= 10.0:
+                self.assertLessEqual(results.link.loc['flowrate',t,'pipe1'],0.0)
                 tank_level_dropped_flag = True
-            elif results.node.at['pressure',t,'tank1'] > 10.0:
-                self.assertGreaterEqual(results.link.at['flowrate',t,'pipe1'],0.001)
+            elif results.node.loc['pressure',t,'tank1'] > 10.0:
+                self.assertGreaterEqual(results.link.loc['flowrate',t,'pipe1'],0.001)
                 if tank_level_dropped_flag:
                     tank_refilled_flag = True
         self.assertEqual(tank_level_dropped_flag, True)
         self.assertEqual(tank_refilled_flag, True)
         """
-        raise SkipTest
+        raise unittest.SkipTest
         self.assertEqual(True, False)
 
 
@@ -226,6 +264,7 @@ class TestValveControls(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
     @classmethod
@@ -233,24 +272,24 @@ def tearDownClass(self):
         pass
 
     def test_check_valve_closed(self):
-        inp_file = join(test_datadir, 'cv_controls.inp')
+        inp_file = join(test_datadir, "cv_controls.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
         for jname, j in wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
-        
-        flowrate = results.link['flowrate']
+
+        flowrate = results.link["flowrate"]
         for t in flowrate.index:
-            self.assertAlmostEqual(flowrate.at[t,'pipe1'], 0.0)
+            self.assertAlmostEqual(flowrate.at[t, "pipe1"], 0.0)
 
     def test_check_valve_opened(self):
-        inp_file = join(test_datadir, 'cv_controls.inp')
+        inp_file = join(test_datadir, "cv_controls.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        tank1 = wn.get_node('tank1')
-        tank2 = wn.get_node('tank2')
+        tank1 = wn.get_node("tank1")
+        tank2 = wn.get_node("tank2")
         tank1_init_level = tank1.init_level
         tank1.init_level = tank2.init_level
         tank2.init_level = tank1_init_level
@@ -259,18 +298,21 @@ def test_check_valve_opened(self):
         for jname, j in wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
         flag1 = False
         flag2 = False
-        for t in results.node['head'].index:
-            if results.node['head'].at[t,'tank1'] >= results.node['head'].at[t,'tank2']:
-                self.assertGreaterEqual(results.link['flowrate'].at[t,'pipe1'], 0.001)
+        for t in results.node["head"].index:
+            if (
+                results.node["head"].at[t, "tank1"]
+                >= results.node["head"].at[t, "tank2"]
+            ):
+                self.assertGreaterEqual(results.link["flowrate"].at[t, "pipe1"], 0.001)
                 flag1 = True
             else:
-                self.assertAlmostEqual(results.link['flowrate'].at[t,'pipe1'], 0.0)
+                self.assertAlmostEqual(results.link["flowrate"].at[t, "pipe1"], 0.0)
                 flag2 = True
 
         self.assertEqual(flag1, True)
@@ -281,6 +323,7 @@ class TestControlCombinations(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
     @classmethod
@@ -288,99 +331,147 @@ def tearDownClass(self):
         pass
 
     def test_open_by_time_close_by_condition(self):
-        inp_file = join(test_datadir, 'control_comb.inp')
+        inp_file = join(test_datadir, "control_comb.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        control_action = self.wntr.network.ControlAction(wn.get_link('pipe1'), 'status', self.wntr.network.LinkStatus.opened)
-        control = self.wntr.network.controls.Control._time_control(wn, 6*3600, 'SIM_TIME', False, control_action)
-        wn.add_control('open_time_6',control)
+        control_action = self.wntr.network.ControlAction(
+            wn.get_link("pipe1"), "status", self.wntr.network.LinkStatus.opened
+        )
+        control = self.wntr.network.controls.Control._time_control(
+            wn, 6 * 3600, "SIM_TIME", False, control_action
+        )
+        wn.add_control("open_time_6", control)
         for jname, j in wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
         flag1 = False
         flag2 = False
-        for t in results.node['head'].index:
-            if t == 6*3600:
+        for t in results.node["head"].index:
+            if t == 6 * 3600:
                 flag1 = True
-            if t > 0 and (results.node['head'].at[t-3600,'tank1'] + (results.node['demand'].at[t-3600,'tank1']*3600 * 4 / (3.14159 * wn.get_node('tank1').diameter**2))) <= 30:
+            if (
+                t > 0
+                and (
+                    results.node["head"].at[t - 3600, "tank1"]
+                    + (
+                        results.node["demand"].at[t - 3600, "tank1"]
+                        * 3600
+                        * 4
+                        / (3.14159 * wn.get_node("tank1").diameter ** 2)
+                    )
+                )
+                <= 30
+            ):
                 flag1 = False
                 flag2 = True
             if flag1 == False:
-                self.assertAlmostEqual(results.link['flowrate'].at[ t, 'pipe1'], 0.0)
+                self.assertAlmostEqual(results.link["flowrate"].at[t, "pipe1"], 0.0)
             elif flag1 == True:
-                self.assertGreaterEqual(results.link['flowrate'].at[t, 'pipe1'], 0.001)
+                self.assertGreaterEqual(results.link["flowrate"].at[t, "pipe1"], 0.001)
 
         self.assertEqual(flag1, False)
         self.assertEqual(flag2, True)
 
     def test_close_by_condition_open_by_time_stay(self):
-        inp_file = join(test_datadir, 'control_comb.inp')
+        inp_file = join(test_datadir, "control_comb.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        tank1 = wn.get_node('tank1')
+        tank1 = wn.get_node("tank1")
         tank1.init_level = 40.0
         tank1._head = tank1.elevation + 40.0
-        pipe1 = wn.get_link('pipe1')
+        pipe1 = wn.get_link("pipe1")
         pipe1._user_status = self.wntr.network.LinkStatus.opened
-        control_action = self.wntr.network.ControlAction(wn.get_link('pipe1'), 'status', self.wntr.network.LinkStatus.opened)
-        control = self.wntr.network.controls.Control._time_control(wn, 19*3600, 'SIM_TIME', False, control_action)
-        wn.add_control('open_time_19',control)
+        control_action = self.wntr.network.ControlAction(
+            wn.get_link("pipe1"), "status", self.wntr.network.LinkStatus.opened
+        )
+        control = self.wntr.network.controls.Control._time_control(
+            wn, 19 * 3600, "SIM_TIME", False, control_action
+        )
+        wn.add_control("open_time_19", control)
         for jname, j in wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
         flag1 = False
         flag2 = False
-        for t in results.node['head'].index:
-            if t == 19*3600:
+        for t in results.node["head"].index:
+            if t == 19 * 3600:
                 flag1 = False
-            if t > 0 and (results.node['head'].at[t-3600,'tank1'] + (results.node['demand'].at[t-3600,'tank1']*3600 * 4 / (3.14159 * wn.get_node('tank1').diameter**2))) <= 30:
+            if (
+                t > 0
+                and (
+                    results.node["head"].at[t - 3600, "tank1"]
+                    + (
+                        results.node["demand"].at[t - 3600, "tank1"]
+                        * 3600
+                        * 4
+                        / (3.14159 * wn.get_node("tank1").diameter ** 2)
+                    )
+                )
+                <= 30
+            ):
                 flag1 = True
                 flag2 = True
             if flag1 == False:
-                self.assertGreaterEqual(results.link['flowrate'].at[t,'pipe1'], 0.001)
+                self.assertGreaterEqual(results.link["flowrate"].at[t, "pipe1"], 0.001)
             elif flag1 == True:
-                self.assertAlmostEqual(results.link['flowrate'].at[t,'pipe1'], 0.0)
+                self.assertAlmostEqual(results.link["flowrate"].at[t, "pipe1"], 0.0)
 
         self.assertEqual(flag1, False)
         self.assertEqual(flag2, True)
 
     def test_close_by_condition_open_by_time_reclose(self):
-        inp_file = join(test_datadir, 'control_comb.inp')
+        inp_file = join(test_datadir, "control_comb.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        tank1 = wn.get_node('tank1')
+        tank1 = wn.get_node("tank1")
         tank1.init_level = 40.0
         tank1._head = tank1.elevation + 40.0
-        pipe1 = wn.get_link('pipe1')
+        pipe1 = wn.get_link("pipe1")
         pipe1._user_status = self.wntr.network.LinkStatus.opened
-        control_action = self.wntr.network.ControlAction(wn.get_link('pipe1'), 'status', self.wntr.network.LinkStatus.opened)
-        control = self.wntr.network.controls.Control._time_control(wn, 5*3600, 'SIM_TIME', False, control_action)
-        wn.add_control('open_time_5',control)
+        control_action = self.wntr.network.ControlAction(
+            wn.get_link("pipe1"), "status", self.wntr.network.LinkStatus.opened
+        )
+        control = self.wntr.network.controls.Control._time_control(
+            wn, 5 * 3600, "SIM_TIME", False, control_action
+        )
+        wn.add_control("open_time_5", control)
         for jname, j in wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
         flag1 = False
-        for t in results.node['head'].index:
-            if t > 0 and (results.node['head'].at[t-3600,'tank1'] + (results.node['demand'].at[t-3600,'tank1']*3600 * 4 / (3.14159 * wn.get_node('tank1').diameter**2))) <= 30.0:
+        for t in results.node["head"].index:
+            if (
+                t > 0
+                and (
+                    results.node["head"].at[t - 3600, "tank1"]
+                    + (
+                        results.node["demand"].at[t - 3600, "tank1"]
+                        * 3600
+                        * 4
+                        / (3.14159 * wn.get_node("tank1").diameter ** 2)
+                    )
+                )
+                <= 30.0
+            ):
                 flag1 = True
-            if t==5*3600:
-                flag1=False
+            if t == 5 * 3600:
+                flag1 = False
             if flag1 == False:
-                self.assertGreaterEqual(results.link['flowrate'].at[t,'pipe1'], 0.001)
+                self.assertGreaterEqual(results.link["flowrate"].at[t, "pipe1"], 0.001)
             elif flag1 == True:
-                self.assertAlmostEqual(results.link['flowrate'].at[t,'pipe1'], 0.0)
+                self.assertAlmostEqual(results.link["flowrate"].at[t, "pipe1"], 0.0)
 
         self.assertEqual(flag1, True)
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/wntr/tests/test_network_elements.py b/wntr/tests/test_network_elements.py
index eceb37993..a9d9892f0 100644
--- a/wntr/tests/test_network_elements.py
+++ b/wntr/tests/test_network_elements.py
@@ -1,237 +1,272 @@
 """
 Test the wntr.network.elements classes
 """
-import nose.tools
-from nose import SkipTest
-from nose.tools import *
+import unittest
 from os.path import abspath, dirname, join
+from unittest import SkipTest
+
 import numpy as np
 import wntr
 import wntr.network.elements as elements
 from wntr.network.options import TimeOptions
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'networks_for_testing')
-net1dir = join(testdir,'..','..','examples','networks')
-
-def test_Curve():
-    pts1 = [[3,5]]
-    pts2 = [[3, 6], [7, 2], [10, 0]]
-    pts3 = [[3, 6], [7, 2], [10, 1]]
-    expected_str = "<Curve: 'curve2', curve_type='HEAD', points=[[3, 6], [7, 2], [10, 0]]>"
-    # Create the curves
-    curve1 = elements.Curve('curve1', 'PUMP', pts1)
-    curve2a = elements.Curve('curve2', 'HEAD', pts2)
-    curve2b = elements.Curve('curve2', 'HEAD', pts3)
-    curve2c = elements.Curve('curve2', 'HEAD', pts3)
-    # Test that the assignments are working
-    nose.tools.assert_list_equal(curve2b.points, pts3)
-    nose.tools.assert_equal(curve1.num_points, 1)
-    nose.tools.assert_equal(len(curve2c), 3)
-    # Testing __eq__
-    nose.tools.assert_not_equal(curve1, curve2a)
-    nose.tools.assert_not_equal(curve2a, curve2b)
-    nose.tools.assert_equal(curve2b, curve2c)
-    # testing __getitem__ and __getslice__
-    nose.tools.assert_list_equal(curve2a[0], [3,6])
-    nose.tools.assert_list_equal(curve2a[:2], [[3, 6], [7, 2]])
-    # verify that the points are being deep copied
-    nose.tools.assert_not_equal(id(curve2b.points), id(curve2c.points))
-
-def test_Pattern():
-    pattern_points1 = [1, 2, 3, 4, 3, 2, 1]
-    pattern_points2 = [1.0, 1.2, 1.0 ]
-    pattern_points3 = 3.2
-    
-    # test constant pattern creation
-    timing1 = TimeOptions()
-    timing1.pattern_start = 0
-    timing1.pattern_timestep = 1
-    
-    timing2 = TimeOptions()
-    timing2.pattern_start = 0
-    timing2.pattern_timestep = 5
-    pattern1a = elements.Pattern('constant', multipliers=pattern_points3, time_options=timing1)
-    pattern1b = elements.Pattern('constant', multipliers=[pattern_points3], time_options=(0,1))
-    nose.tools.assert_list_equal(pattern1a.multipliers.tolist(), [pattern_points3])
-    nose.tools.assert_true(np.all(pattern1a.multipliers == pattern1b.multipliers))
-    nose.tools.assert_false(id(pattern1a.multipliers) == id(pattern1b.multipliers))
-    nose.tools.assert_equal(pattern1a.time_options, pattern1b.time_options)
-    
-    # def multipliers setter
-    pattern2a = elements.Pattern('oops', multipliers=pattern_points3, time_options=(0,5))
-    pattern2b = elements.Pattern('oops', multipliers=pattern_points1, time_options=timing2)
-    pattern2a.multipliers = pattern_points1
-    nose.tools.assert_equal(pattern2a.time_options, pattern2b.time_options)
-    
-    # test pattern evaluations
-    expected_value = pattern_points1[2]
-    nose.tools.assert_equal(pattern2a[2], expected_value)
-    nose.tools.assert_equal(pattern2b.at(10), expected_value)
-    nose.tools.assert_equal(pattern2b.at(12.5), expected_value)
-    nose.tools.assert_equal(pattern2b.at(14), expected_value)
-    nose.tools.assert_equal(pattern2b.at(9*5), expected_value)
-    nose.tools.assert_not_equal(pattern2b.at(15), expected_value)
-    
-    pattern3 = elements.Pattern('nowrap', multipliers=pattern_points2, time_options=(0, 100), wrap=False)
-    nose.tools.assert_equal(pattern3[5], 0.0)
-    nose.tools.assert_equal(pattern3[-39], 0.0)
-    nose.tools.assert_equal(pattern3.at(-39), 0.0)
-    nose.tools.assert_equal(pattern3.at(50), 1.0)
-    
-    pattern4 = elements.Pattern('constant')
-    nose.tools.assert_equal(len(pattern4), 0)
-    nose.tools.assert_equal(pattern4.at(492), 1.0)
-    
-    pattern5a = elements.Pattern('binary', [0.,0.,1.,1.,1.,1.,0.,0.,0.], time_options=(0, 1), wrap=False)
-    pattern5b = elements.Pattern.binary_pattern('binary', step_size=1, start_time=2, end_time=6, duration=9)
-    nose.tools.assert_false(pattern5a.__eq__(pattern5b))
-    nose.tools.assert_true(np.all(np.abs(pattern5a.multipliers - pattern5b.multipliers)<1.0e-10))
-    
-
-def test_TimeSeries():
-    wn = wntr.network.WaterNetworkModel()
-    
-    pattern_points2 = [1.0, 1.2, 1.0 ]
-    wn.add_pattern('oops', pattern_points2)
-    pattern2 = wn.get_pattern('oops')
-    pattern5 = elements.Pattern.binary_pattern('binary', step_size=1, start_time=2, end_time=6, duration=9)
-    wn.add_pattern('binary', pattern5)
-    base1 = 2.0
-    
-    # test constructor and setters, getters
-    tvv1 = elements.TimeSeries(wn.patterns, base1, None, None)
-    tvv2 = elements.TimeSeries(wn.patterns, base1, 'oops', 'tvv2')
-    nose.tools.assert_raises(ValueError, elements.TimeSeries, *('A', None, None))
-    nose.tools.assert_raises(ValueError, elements.TimeSeries, *(1.0, 'A', None))
-    nose.tools.assert_equals(tvv1._base, base1)
-    nose.tools.assert_equal(tvv1.base_value, tvv1._base)
-    nose.tools.assert_equals(tvv1.pattern_name, None)
-    nose.tools.assert_equals(tvv1.pattern, None)
-    nose.tools.assert_equals(tvv1.category, None)
-    tvv1.base_value = 3.0
-    nose.tools.assert_equals(tvv1.base_value, 3.0)
-    tvv1.pattern_name = 'binary'
-    nose.tools.assert_equals(tvv1.pattern_name, 'binary')
-    tvv1.category ='binary'
-    nose.tools.assert_equals(tvv1.category, 'binary')
-    
-    # Test getitem
-    #print(tvv1)
-    #print(tvv2, pattern2)
-    nose.tools.assert_equals(tvv1.at(1), 0.0)
-    nose.tools.assert_equals(tvv1.at(7202), 3.0)
-    nose.tools.assert_equals(tvv2.at(1), 2.0)
-    #print(tvv2, pattern2.time_options)
-    nose.tools.assert_equals(tvv2.at(3602), 2.4)
-    
-    price1 = elements.TimeSeries(wn.patterns, 35.0, None)
-    price2 = elements.TimeSeries(wn.patterns, 35.0, None)
-    nose.tools.assert_equal(price1, price2)
-    nose.tools.assert_equal(price1.base_value, 35.0)
-    
-    speed1 = elements.TimeSeries(wn.patterns, 35.0, pattern5)
-    speed2 = elements.TimeSeries(wn.patterns, 35.0, pattern5)
-    nose.tools.assert_equal(speed1, speed2)
-    nose.tools.assert_equal(speed1.base_value, 35.0)
-    
-    head1 = elements.TimeSeries(wn.patterns, 35.0, pattern2)
-    head2 = elements.TimeSeries(wn.patterns, 35.0, pattern2)
-    nose.tools.assert_equal(head1, head2)
-    nose.tools.assert_equal(head1.base_value, 35.0)
-
-    demand1 = elements.TimeSeries(wn.patterns, 1.35, pattern2)
-    demand2 = elements.TimeSeries(wn.patterns, 1.35, pattern2)
-    nose.tools.assert_equal(demand1, demand2)
-    nose.tools.assert_equal(demand1.base_value, 1.35)
-#    expected_values1 = np.array([1.35, 1.62, 1.35, 1.35, 1.62])
-#    demand_values1 = demand2.get_values(0, 40, 10)
-#    nose.tools.assert_true(np.all(np.abs(expected_values1-demand_values1)<1.0e-10))
-#    expected_values1 = np.array([1.35, 1.35, 1.62, 1.62, 1.35, 1.35, 1.35, 1.35, 1.62])
-#    demand_values1 = demand2.get_values(0, 40, 5)
-#    nose.tools.assert_true(np.all(np.abs(expected_values1-demand_values1)<1.0e-10))
-#    
-#    source1 = elements.Source('source1', 'NODE-1131', 'CONCEN', 1000.0, pattern5)
-#    source2 = elements.Source('source1', 'NODE-1131', 'CONCEN', 1000.0, pattern5)
-#    nose.tools.assert_equal(source1, source2)
-#    nose.tools.assert_equal(source1.strength_timeseries.base_value, 1000.0)
-    
-
-def test_Demands():
-    wn = wntr.network.WaterNetworkModel()
-    
-    pattern_points1 = [0.5, 1.0, 0.4, 0.2 ]
-    pattern1 = elements.Pattern('1', multipliers=pattern_points1, time_options=(0,10))
-    pattern_points2 = [1.0, 1.2, 1.0 ]
-    pattern2 = elements.Pattern('2', multipliers=pattern_points2, time_options=(0,10))
-    demand1 = elements.TimeSeries(wn.patterns, 2.5, pattern1, '_base_demand')
-    demand2 = elements.TimeSeries(wn.patterns, 1.0, pattern2, 'residential')
-    demand3 = elements.TimeSeries(wn.patterns, 0.8, pattern2, 'residential')
-    
-    expected1 = 2.5 * np.array(pattern_points1*3)
-    expected2 = 1.0 * np.array(pattern_points2*4)
-    expected3 = 0.8 * np.array(pattern_points2*4)
-    expectedtotal = expected1 + expected2 + expected3
-    expectedresidential = expected2 + expected3
-    demandlist1 = elements.Demands(wn.patterns, demand1, demand2, demand3 )
-    demandlist2 = elements.Demands(wn.patterns)
-    demandlist2.append(demand1)
-    demandlist2.append(demand1)
-    demandlist2[1] = demand2
-    demandlist2.append((0.8, pattern2, 'residential'))
-    nose.tools.assert_list_equal(list(demandlist1), list(demandlist2))
-    
-    demandlist2.extend(demandlist1)
-    nose.tools.assert_equal(len(demandlist1), 3)
-    nose.tools.assert_equal(len(demandlist2), 6)
-    del demandlist2[3]
-    del demandlist2[3]
-    del demandlist2[3]
-    del demandlist2[0]
-    demandlist2.insert(0, demand1)
-    nose.tools.assert_list_equal(list(demandlist1), list(demandlist2))
-    demandlist2.clear()
-    nose.tools.assert_equal(len(demandlist2), 0)
-    nose.tools.assert_false(demandlist2)
-    
-    raise SkipTest
-    
-    nose.tools.assert_equal(demandlist1.at(5), expectedtotal[0])
-    nose.tools.assert_equal(demandlist1.at(13), expectedtotal[1])
-    nose.tools.assert_equal(demandlist1.at(13, 'residential'), expectedresidential[1])
-    nose.tools.assert_true(np.all(np.abs(demandlist1.get_values(0,110,10)-expectedtotal)<1.0e-10))
-    nose.tools.assert_list_equal(demandlist1.base_demand_list(), [2.5, 1.0, 0.8])
-    nose.tools.assert_list_equal(demandlist1.base_demand_list('_base_demand'), [2.5])
-    nose.tools.assert_list_equal(demandlist1.pattern_list(), [pattern1, pattern2, pattern2])
-    nose.tools.assert_list_equal(demandlist1.pattern_list(category='residential'), [pattern2, pattern2])
-    nose.tools.assert_list_equal(demandlist1.category_list(), ['_base_demand','residential','residential'])    
-
-def test_fire_fighting_demand():
-    # Setup network
-    wn = wntr.network.WaterNetworkModel()
-    wn.add_junction('new_junction', base_demand=1, elevation=10,
-                    coordinates=(6, 25))
-    duration = 1*5*60*60
-    wn.options.time.duration = duration
-    
-    # Add fire demand
-    node = wn.get_node('new_junction')
-    fire_flow_demand = 0.252
-    fire_start = 2*60*60
-    fire_end = 4*60*60
-    node.add_fire_fighting_demand(wn, fire_flow_demand, fire_start, fire_end)
-    ff_demand = list(wntr.metrics.hydraulic.expected_demand(wn)['new_junction'].values)
-    expected_ff_demand = [1, 1, 1.252, 1.252, 1, 1]
-    nose.tools.assert_list_equal(ff_demand, expected_ff_demand)
-    
-    # Remove fire demand
-    node.remove_fire_fighting_demand(wn)
-    nose.tools.assert_true(not('Fire_Flow' in node.demand_timeseries_list.category_list()))
-    
-
-def test_Enums():
-    pass
-
-#if __name__ == '__main__':
-    # test_Demands()
-    # test_fire_fighting_demand()
+datadir = join(testdir, "networks_for_testing")
+net1dir = join(testdir, "..", "..", "examples", "networks")
+
+
+class TestElements(unittest.TestCase):
+    def test_Curve(self):
+        pts1 = [[3, 5]]
+        pts2 = [[3, 6], [7, 2], [10, 0]]
+        pts3 = [[3, 6], [7, 2], [10, 1]]
+        expected_str = (
+            "<Curve: 'curve2', curve_type='HEAD', points=[[3, 6], [7, 2], [10, 0]]>"
+        )
+        # Create the curves
+        curve1 = elements.Curve("curve1", "PUMP", pts1)
+        curve2a = elements.Curve("curve2", "HEAD", pts2)
+        curve2b = elements.Curve("curve2", "HEAD", pts3)
+        curve2c = elements.Curve("curve2", "HEAD", pts3)
+        # Test that the assignments are working
+        self.assertListEqual(curve2b.points, pts3)
+        self.assertEqual(curve1.num_points, 1)
+        self.assertEqual(len(curve2c), 3)
+        # Testing __eq__
+        self.assertNotEqual(curve1, curve2a)
+        self.assertNotEqual(curve2a, curve2b)
+        self.assertEqual(curve2b, curve2c)
+        # testing __getitem__ and __getslice__
+        self.assertListEqual(curve2a[0], [3, 6])
+        self.assertListEqual(curve2a[:2], [[3, 6], [7, 2]])
+        # verify that the points are being deep copied
+        self.assertNotEqual(id(curve2b.points), id(curve2c.points))
+
+    def test_Pattern(self):
+        pattern_points1 = [1, 2, 3, 4, 3, 2, 1]
+        pattern_points2 = [1.0, 1.2, 1.0]
+        pattern_points3 = 3.2
+
+        # test constant pattern creation
+        timing1 = TimeOptions()
+        timing1.pattern_start = 0
+        timing1.pattern_timestep = 1
+
+        timing2 = TimeOptions()
+        timing2.pattern_start = 0
+        timing2.pattern_timestep = 5
+        pattern1a = elements.Pattern(
+            "constant", multipliers=pattern_points3, time_options=timing1
+        )
+        pattern1b = elements.Pattern(
+            "constant", multipliers=[pattern_points3], time_options=(0, 1)
+        )
+        self.assertListEqual(pattern1a.multipliers.tolist(), [pattern_points3])
+        self.assertTrue(np.all(pattern1a.multipliers == pattern1b.multipliers))
+        self.assertFalse(id(pattern1a.multipliers) == id(pattern1b.multipliers))
+        self.assertEqual(pattern1a.time_options, pattern1b.time_options)
+
+        # def multipliers setter
+        pattern2a = elements.Pattern(
+            "oops", multipliers=pattern_points3, time_options=(0, 5)
+        )
+        pattern2b = elements.Pattern(
+            "oops", multipliers=pattern_points1, time_options=timing2
+        )
+        pattern2a.multipliers = pattern_points1
+        self.assertEqual(pattern2a.time_options, pattern2b.time_options)
+
+        # test pattern evaluations
+        expected_value = pattern_points1[2]
+        self.assertEqual(pattern2a[2], expected_value)
+        self.assertEqual(pattern2b.at(10), expected_value)
+        self.assertEqual(pattern2b.at(12.5), expected_value)
+        self.assertEqual(pattern2b.at(14), expected_value)
+        self.assertEqual(pattern2b.at(9 * 5), expected_value)
+        self.assertNotEqual(pattern2b.at(15), expected_value)
+
+        pattern3 = elements.Pattern(
+            "nowrap", multipliers=pattern_points2, time_options=(0, 100), wrap=False
+        )
+        self.assertEqual(pattern3[5], 0.0)
+        self.assertEqual(pattern3[-39], 0.0)
+        self.assertEqual(pattern3.at(-39), 0.0)
+        self.assertEqual(pattern3.at(50), 1.0)
+
+        pattern4 = elements.Pattern("constant")
+        self.assertEqual(len(pattern4), 0)
+        self.assertEqual(pattern4.at(492), 1.0)
+
+        pattern5a = elements.Pattern(
+            "binary",
+            [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0],
+            time_options=(0, 1),
+            wrap=False,
+        )
+        pattern5b = elements.Pattern.binary_pattern(
+            "binary", step_size=1, start_time=2, end_time=6, duration=9
+        )
+        self.assertFalse(pattern5a.__eq__(pattern5b))
+        self.assertTrue(
+            np.all(np.abs(pattern5a.multipliers - pattern5b.multipliers) < 1.0e-10)
+        )
+
+    def test_TimeSeries(self):
+        wn = wntr.network.WaterNetworkModel()
+
+        pattern_points2 = [1.0, 1.2, 1.0]
+        wn.add_pattern("oops", pattern_points2)
+        pattern2 = wn.get_pattern("oops")
+        pattern5 = elements.Pattern.binary_pattern(
+            "binary", step_size=1, start_time=2, end_time=6, duration=9
+        )
+        wn.add_pattern("binary", pattern5)
+        base1 = 2.0
+
+        # test constructor and setters, getters
+        tvv1 = elements.TimeSeries(wn.patterns, base1, None, None)
+        tvv2 = elements.TimeSeries(wn.patterns, base1, "oops", "tvv2")
+        self.assertRaises(ValueError, elements.TimeSeries, *("A", None, None))
+        self.assertRaises(ValueError, elements.TimeSeries, *(1.0, "A", None))
+        self.assertEqual(tvv1._base, base1)
+        self.assertEqual(tvv1.base_value, tvv1._base)
+        self.assertEqual(tvv1.pattern_name, None)
+        self.assertEqual(tvv1.pattern, None)
+        self.assertEqual(tvv1.category, None)
+        tvv1.base_value = 3.0
+        self.assertEqual(tvv1.base_value, 3.0)
+        tvv1.pattern_name = "binary"
+        self.assertEqual(tvv1.pattern_name, "binary")
+        tvv1.category = "binary"
+        self.assertEqual(tvv1.category, "binary")
+
+        # Test getitem
+        # print(tvv1)
+        # print(tvv2, pattern2)
+        self.assertEqual(tvv1.at(1), 0.0)
+        self.assertEqual(tvv1.at(7202), 3.0)
+        self.assertEqual(tvv2.at(1), 2.0)
+        # print(tvv2, pattern2.time_options)
+        self.assertEqual(tvv2.at(3602), 2.4)
+
+        price1 = elements.TimeSeries(wn.patterns, 35.0, None)
+        price2 = elements.TimeSeries(wn.patterns, 35.0, None)
+        self.assertEqual(price1, price2)
+        self.assertEqual(price1.base_value, 35.0)
+
+        speed1 = elements.TimeSeries(wn.patterns, 35.0, pattern5)
+        speed2 = elements.TimeSeries(wn.patterns, 35.0, pattern5)
+        self.assertEqual(speed1, speed2)
+        self.assertEqual(speed1.base_value, 35.0)
+
+        head1 = elements.TimeSeries(wn.patterns, 35.0, pattern2)
+        head2 = elements.TimeSeries(wn.patterns, 35.0, pattern2)
+        self.assertEqual(head1, head2)
+        self.assertEqual(head1.base_value, 35.0)
+
+        demand1 = elements.TimeSeries(wn.patterns, 1.35, pattern2)
+        demand2 = elements.TimeSeries(wn.patterns, 1.35, pattern2)
+        self.assertEqual(demand1, demand2)
+        self.assertEqual(demand1.base_value, 1.35)
+
+    #    expected_values1 = np.array([1.35, 1.62, 1.35, 1.35, 1.62])
+    #    demand_values1 = demand2.get_values(0, 40, 10)
+    #    self.assertTrue(np.all(np.abs(expected_values1-demand_values1)<1.0e-10))
+    #    expected_values1 = np.array([1.35, 1.35, 1.62, 1.62, 1.35, 1.35, 1.35, 1.35, 1.62])
+    #    demand_values1 = demand2.get_values(0, 40, 5)
+    #    self.assertTrue(np.all(np.abs(expected_values1-demand_values1)<1.0e-10))
+    #
+    #    source1 = elements.Source('source1', 'NODE-1131', 'CONCEN', 1000.0, pattern5)
+    #    source2 = elements.Source('source1', 'NODE-1131', 'CONCEN', 1000.0, pattern5)
+    #    self.assertEqual(source1, source2)
+    #    self.assertEqual(source1.strength_timeseries.base_value, 1000.0)
+
+    def test_Demands(self):
+        wn = wntr.network.WaterNetworkModel()
+
+        pattern_points1 = [0.5, 1.0, 0.4, 0.2]
+        pattern1 = elements.Pattern(
+            "1", multipliers=pattern_points1, time_options=(0, 10)
+        )
+        pattern_points2 = [1.0, 1.2, 1.0]
+        pattern2 = elements.Pattern(
+            "2", multipliers=pattern_points2, time_options=(0, 10)
+        )
+        demand1 = elements.TimeSeries(wn.patterns, 2.5, pattern1, "_base_demand")
+        demand2 = elements.TimeSeries(wn.patterns, 1.0, pattern2, "residential")
+        demand3 = elements.TimeSeries(wn.patterns, 0.8, pattern2, "residential")
+
+        expected1 = 2.5 * np.array(pattern_points1 * 3)
+        expected2 = 1.0 * np.array(pattern_points2 * 4)
+        expected3 = 0.8 * np.array(pattern_points2 * 4)
+        expectedtotal = expected1 + expected2 + expected3
+        expectedresidential = expected2 + expected3
+        demandlist1 = elements.Demands(wn.patterns, demand1, demand2, demand3)
+        demandlist2 = elements.Demands(wn.patterns)
+        demandlist2.append(demand1)
+        demandlist2.append(demand1)
+        demandlist2[1] = demand2
+        demandlist2.append((0.8, pattern2, "residential"))
+        self.assertListEqual(list(demandlist1), list(demandlist2))
+
+        demandlist2.extend(demandlist1)
+        self.assertEqual(len(demandlist1), 3)
+        self.assertEqual(len(demandlist2), 6)
+        del demandlist2[3]
+        del demandlist2[3]
+        del demandlist2[3]
+        del demandlist2[0]
+        demandlist2.insert(0, demand1)
+        self.assertListEqual(list(demandlist1), list(demandlist2))
+        demandlist2.clear()
+        self.assertEqual(len(demandlist2), 0)
+        self.assertFalse(demandlist2)
+
+        raise SkipTest
+
+        self.assertEqual(demandlist1.at(5), expectedtotal[0])
+        self.assertEqual(demandlist1.at(13), expectedtotal[1])
+        self.assertEqual(demandlist1.at(13, "residential"), expectedresidential[1])
+        self.assertTrue(
+            np.all(np.abs(demandlist1.get_values(0, 110, 10) - expectedtotal) < 1.0e-10)
+        )
+        self.assertListEqual(demandlist1.base_demand_list(), [2.5, 1.0, 0.8])
+        self.assertListEqual(demandlist1.base_demand_list("_base_demand"), [2.5])
+        self.assertListEqual(demandlist1.pattern_list(), [pattern1, pattern2, pattern2])
+        self.assertListEqual(
+            demandlist1.pattern_list(category="residential"), [pattern2, pattern2]
+        )
+        self.assertListEqual(
+            demandlist1.category_list(), ["_base_demand", "residential", "residential"]
+        )
+
+    def test_fire_fighting_demand(self):
+        # Setup network
+        wn = wntr.network.WaterNetworkModel()
+        wn.add_junction(
+            "new_junction", base_demand=1, elevation=10, coordinates=(6, 25)
+        )
+        duration = 1 * 5 * 60 * 60
+        wn.options.time.duration = duration
+
+        # Add fire demand
+        node = wn.get_node("new_junction")
+        fire_flow_demand = 0.252
+        fire_start = 2 * 60 * 60
+        fire_end = 4 * 60 * 60
+        node.add_fire_fighting_demand(wn, fire_flow_demand, fire_start, fire_end)
+        ff_demand = list(
+            wntr.metrics.hydraulic.expected_demand(wn)["new_junction"].values
+        )
+        expected_ff_demand = [1, 1, 1.252, 1.252, 1, 1]
+        self.assertListEqual(ff_demand, expected_ff_demand)
+
+        # Remove fire demand
+        node.remove_fire_fighting_demand(wn)
+        self.assertTrue(
+            not ("Fire_Flow" in node.demand_timeseries_list.category_list())
+        )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_network_graph.py b/wntr/tests/test_network_graph.py
index 2dc5bb97a..9c4b360b6 100644
--- a/wntr/tests/test_network_graph.py
+++ b/wntr/tests/test_network_graph.py
@@ -1,140 +1,168 @@
-from nose.tools import *
-from nose import SkipTest
+import unittest
 from os.path import abspath, dirname, join
-import numpy as np
+from unittest import SkipTest
+
 import networkx as nx
+import numpy as np
 import wntr
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'networks_for_testing')
-netdir = join(testdir,'..','..','examples','networks')
-
-def test_weight_graph():
-    inp_file = join(netdir,'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    
-    node_weight = wn.query_node_attribute('elevation')
-    link_weight = wn.query_link_attribute('length')
-    
-    G = wn.get_graph(node_weight, link_weight)
-    
-    assert_equal(G.nodes['111']['weight'], 10*0.3048)
-    assert_equal(G['159']['161']['177']['weight'], 2000*0.3048)
-
-def test_terminal_nodes():
-    inp_file = join(netdir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    G = wn.get_graph()
-    
-    terminal_nodes = wntr.metrics.terminal_nodes(G)
-    expected = set(['2', '9'])
-    assert_set_equal(set(terminal_nodes), expected)
-
-def test_bridges():
-    inp_file = join(netdir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    G = wn.get_graph()
-    
-    bridges = wntr.metrics.bridges(G)
-    expected = set(['9','10','110'])
-    assert_set_equal(set(bridges), expected)
-
-def test_diameter():
-    inp_file = join(datadir,'Anytown.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    for pump in wn.pump_name_list[:-1]: # remove 2 of the 3 pumps
-        wn.remove_link(pump)
-    G = wn.get_graph()
-    udG = G.to_undirected()
-    val = nx.diameter(udG)
-    excepted = 7 # Davide Soldi et al. (2015) Procedia Engineering
-    assert_equals(val, excepted)
-
-def test_central_point_dominance():
-    inp_file = join(datadir,'Anytown.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    for pump in wn.pump_name_list[:-1]: # remove 2 of the 3 pumps
-        wn.remove_link(pump)
-    G = wn.get_graph()
-
-    val = wntr.metrics.central_point_dominance(G)
-    expected = 0.23 # Davide Soldi et al. (2015) Procedia Engineering
-    error = abs(expected-val)
-    assert_less(error, 0.01)
-
-def test_spectral_gap():
-    inp_file = join(datadir,'Anytown.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    for pump in wn.pump_name_list[:-1]: # remove 2 of the 3 pumps
-        wn.remove_link(pump)
-    G = wn.get_graph()
-
-    val = wntr.metrics.spectral_gap(G)
-    expected = 1.5149 # Davide Soldi et al. (2015) Procedia Engineering
-    error = abs(expected-val)
-    assert_less(error,0.01)
-
-def test_algebraic_connectivity():
-    inp_file = join(datadir,'Anytown.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    for pump in wn.pump_name_list[:-1]: # remove 2 of the 3 pumps
-        wn.remove_link(pump)
-    G = wn.get_graph()
-    
-    val = wntr.metrics.algebraic_connectivity(G)
-    expected = 0.1708 # Davide Soldi et al. (2015) Procedia Engineering
-    error = abs(expected-val)
-    raise SkipTest
-    assert_less(error,0.01)
-
-def test_crit_ratio_defrag():
-    inp_file = join(datadir,'Anytown.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    for pump in wn.pump_name_list[:-1]: # remove 2 of the 3 pumps
-        wn.remove_link(pump)
-    G = wn.get_graph()
-    
-    val = wntr.metrics.critical_ratio_defrag(G)
-    expected = 0.63 # Pandit et al. (2012) Critical Infrastucture Symposium
-    error = abs(expected-val)
-    raise SkipTest
-    assert_less(error,0.01)
-    
-def test_Net1_MultiDiGraph():
-    inp_file = join(netdir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    G = wn.get_graph()
-
-    node = {'11': {'pos': (30.0, 70.0),'type': 'Junction'},
-            '10': {'pos': (20.0, 70.0),'type': 'Junction'},
-            '13': {'pos': (70.0, 70.0),'type': 'Junction'},
-            '12': {'pos': (50.0, 70.0),'type': 'Junction'},
-            '21': {'pos': (30.0, 40.0),'type': 'Junction'},
-            '22': {'pos': (50.0, 40.0),'type': 'Junction'},
-            '23': {'pos': (70.0, 40.0),'type': 'Junction'},
-            '32': {'pos': (50.0, 10.0),'type': 'Junction'},
-            '31': {'pos': (30.0, 10.0),'type': 'Junction'},
-            '2':  {'pos': (50.0, 90.0),'type': 'Tank'},
-            '9':  {'pos': (10.0, 70.0),'type': 'Reservoir'}}
-
-    edge = {'11': {'12': {'11':  {'type': 'Pipe'}},
-                   '21': {'111': {'type': 'Pipe'}}},
-            '10': {'11': {'10':  {'type': 'Pipe'}}},
-            '13': {'23': {'113': {'type': 'Pipe'}}},
-            '12': {'13': {'12':  {'type': 'Pipe'}},
-                   '22': {'112': {'type': 'Pipe'}}},
-            '21': {'31': {'121': {'type': 'Pipe'}},
-                   '22': {'21':  {'type': 'Pipe'}}},
-            '22': {'32': {'122': {'type': 'Pipe'}},
-                   '23': {'22':  {'type': 'Pipe'}}},
-            '23': {},
-            '32': {},
-            '31': {'32': {'31':  {'type': 'Pipe'}}},
-            '2':  {'12': {'110': {'type': 'Pipe'}}},
-            '9':  {'10': {'9':   {'type': 'Pump'}}}}
-
-    assert_dict_contains_subset(node, G.nodes)
-    assert_dict_contains_subset(edge, G.adj)
-
-if __name__ == '__main__':
-    test_weight_graph()
+datadir = join(testdir, "networks_for_testing")
+netdir = join(testdir, "..", "..", "examples", "networks")
+
+
+class TestNetworkGraphs(unittest.TestCase):
+    def test_weight_graph(self):
+        inp_file = join(netdir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        node_weight = wn.query_node_attribute("elevation")
+        link_weight = wn.query_link_attribute("length")
+
+        G = wn.get_graph(node_weight, link_weight)
+
+        self.assertEqual(G.nodes["111"]["weight"], 10 * 0.3048)
+        self.assertEqual(G["159"]["161"]["177"]["weight"], 2000 * 0.3048)
+
+    def test_weighted_graph_modify_direction(self):
+        inp_file = join(netdir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file) 
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim()
+        flowrate = results.link['flowrate'].iloc[-1,:]
+        G = wn.get_graph(link_weight=flowrate, modify_direction=True)
+        
+        # Positive flow, flowrate == graph weight
+        name = '173'
+        pipe = wn.get_link(name)
+
+        self.assertEqual(flowrate[name],
+                         G.edges[pipe.start_node_name, pipe.end_node_name, name]['weight'])
+        
+        # Negative flow, -flowrate == graph weight
+        name = '109'
+        pipe = wn.get_link(name)
+        
+        self.assertEqual(-flowrate[name],
+                         G.edges[pipe.end_node_name, pipe.start_node_name, name]['weight'])
+
+    def test_terminal_nodes(self):
+        inp_file = join(netdir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        G = wn.get_graph()
+
+        terminal_nodes = wntr.metrics.terminal_nodes(G)
+        expected = set(["2", "9"])
+        self.assertSetEqual(set(terminal_nodes), expected)
+
+    def test_bridges(self):
+        inp_file = join(netdir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        G = wn.get_graph()
+
+        bridges = wntr.metrics.bridges(G)
+        expected = set(["9", "10", "110"])
+        self.assertSetEqual(set(bridges), expected)
+
+    def test_diameter(self):
+        inp_file = join(datadir, "Anytown.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        for pump in wn.pump_name_list[:-1]:  # remove 2 of the 3 pumps
+            wn.remove_link(pump)
+        G = wn.get_graph()
+        udG = G.to_undirected()
+        val = nx.diameter(udG)
+        excepted = 7  # Davide Soldi et al. (2015) Procedia Engineering
+        self.assertEqual(val, excepted)
+
+    def test_central_point_dominance(self):
+        inp_file = join(datadir, "Anytown.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        for pump in wn.pump_name_list[:-1]:  # remove 2 of the 3 pumps
+            wn.remove_link(pump)
+        G = wn.get_graph()
+
+        val = wntr.metrics.central_point_dominance(G)
+        expected = 0.23  # Davide Soldi et al. (2015) Procedia Engineering
+        error = abs(expected - val)
+        self.assertLess(error, 0.01)
+
+    def test_spectral_gap(self):
+        inp_file = join(datadir, "Anytown.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        for pump in wn.pump_name_list[:-1]:  # remove 2 of the 3 pumps
+            wn.remove_link(pump)
+        G = wn.get_graph()
+
+        val = wntr.metrics.spectral_gap(G)
+        expected = 1.5149  # Davide Soldi et al. (2015) Procedia Engineering
+        error = abs(expected - val)
+        self.assertLess(error, 0.01)
+
+    def test_algebraic_connectivity(self):
+        inp_file = join(datadir, "Anytown.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        for pump in wn.pump_name_list[:-1]:  # remove 2 of the 3 pumps
+            wn.remove_link(pump)
+        G = wn.get_graph()
+
+        val = wntr.metrics.algebraic_connectivity(G)
+        expected = 0.1708  # Davide Soldi et al. (2015) Procedia Engineering
+        error = abs(expected - val)
+        raise SkipTest
+        self.assertLess(error, 0.01)
+
+    def test_crit_ratio_defrag(self):
+        inp_file = join(datadir, "Anytown.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        for pump in wn.pump_name_list[:-1]:  # remove 2 of the 3 pumps
+            wn.remove_link(pump)
+        G = wn.get_graph()
+
+        val = wntr.metrics.critical_ratio_defrag(G)
+        expected = 0.63  # Pandit et al. (2012) Critical Infrastucture Symposium
+        error = abs(expected - val)
+        raise SkipTest
+        self.assertLess(error, 0.01)
+
+    def test_Net1_MultiDiGraph(self):
+        inp_file = join(netdir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        G = wn.get_graph()
+
+        node = {
+            "11": {"pos": (30.0, 70.0), "type": "Junction"},
+            "10": {"pos": (20.0, 70.0), "type": "Junction"},
+            "13": {"pos": (70.0, 70.0), "type": "Junction"},
+            "12": {"pos": (50.0, 70.0), "type": "Junction"},
+            "21": {"pos": (30.0, 40.0), "type": "Junction"},
+            "22": {"pos": (50.0, 40.0), "type": "Junction"},
+            "23": {"pos": (70.0, 40.0), "type": "Junction"},
+            "32": {"pos": (50.0, 10.0), "type": "Junction"},
+            "31": {"pos": (30.0, 10.0), "type": "Junction"},
+            "2": {"pos": (50.0, 90.0), "type": "Tank"},
+            "9": {"pos": (10.0, 70.0), "type": "Reservoir"},
+        }
+
+        edge = {
+            "11": {"12": {"11": {"type": "Pipe"}}, "21": {"111": {"type": "Pipe"}}},
+            "10": {"11": {"10": {"type": "Pipe"}}},
+            "13": {"23": {"113": {"type": "Pipe"}}},
+            "12": {"13": {"12": {"type": "Pipe"}}, "22": {"112": {"type": "Pipe"}}},
+            "21": {"31": {"121": {"type": "Pipe"}}, "22": {"21": {"type": "Pipe"}}},
+            "22": {"32": {"122": {"type": "Pipe"}}, "23": {"22": {"type": "Pipe"}}},
+            "23": {},
+            "32": {},
+            "31": {"32": {"31": {"type": "Pipe"}}},
+            "2": {"12": {"110": {"type": "Pipe"}}},
+            "9": {"10": {"9": {"type": "Pump"}}},
+        }
+
+        self.assertEqual(dict(node, **G.nodes), node)
+        # assert_dict_contains_subset(node, G.nodes)
+        self.assertEqual(dict(edge, **G.adj), edge)
+        # assert_dict_contains_subset(edge, G.adj)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_network_layers.py b/wntr/tests/test_network_layers.py
index dd73317ae..963b068fc 100644
--- a/wntr/tests/test_network_layers.py
+++ b/wntr/tests/test_network_layers.py
@@ -1,20 +1,20 @@
 import unittest
-import pandas as pd
 from os.path import abspath, dirname, join
-from pandas.util.testing import assert_frame_equal
+
+import pandas as pd
 import wntr
+from pandas.testing import assert_frame_equal
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir, 'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
 
 class TestValveLayer(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
-        
-        inp_file = join(ex_datadir, 'Net3.inp')
+
+        inp_file = join(ex_datadir, "Net3.inp")
         self.wn = wntr.network.WaterNetworkModel(inp_file)
 
     @classmethod
@@ -23,39 +23,46 @@ def tearDownClass(self):
 
     def test_valve_layer_random(self):
 
-        valves = wntr.network.generate_valve_layer(self.wn, 'random', 40, 123)
-        
-        #valves.to_csv('valve_layer_random.csv')
-        #ax = wntr.graphics.plot_network(self.wn, node_size=7, valve_layer=valves,
+        valves = wntr.network.generate_valve_layer(self.wn, "random", 40, 123)
+
+        # valves.to_csv('valve_layer_random.csv')
+        # ax = wntr.graphics.plot_network(self.wn, node_size=7, valve_layer=valves,
         #                                          title='Random, 40')
-        
-        expected = pd.read_csv(join(test_datadir, 'valve_layer_random.csv'), index_col=0, dtype='object')
-           
+
+        expected = pd.read_csv(
+            join(test_datadir, "valve_layer_random.csv"), index_col=0, dtype="object"
+        )
+
         assert_frame_equal(valves, expected)
-        
+
     def test_valve_layer_strategic(self):
-        
+
         # Compute the expected number of valves for N-0, N-1, N-2, N-3, N-4
         G = self.wn.get_graph()
         node_degree = pd.Series(dict(G.degree()))
-        expected_n_valves = pd.Series(index=[4,3,2,1,0])
-        for N in [4,3,2,1,0]:
+        expected_n_valves = pd.Series(index=[4, 3, 2, 1, 0])
+        for N in [4, 3, 2, 1, 0]:
             expected_n_valves[N] = node_degree[node_degree > N].shape[0]
-        
+
         expected_n_valves = expected_n_valves.cumsum()
-        
-        for N in [0,1,2,3,4]:
-            
-            valves = wntr.network.generate_valve_layer(self.wn, 'strategic', N, 123)
-            
-            #valves.to_csv('valve_layer_stategic_'+str(N)+'.csv')
-            #ax = wntr.graphics.plot_network(self.wn, node_size=7, valve_layer=valves,
+
+        for N in [0, 1, 2, 3, 4]:
+
+            valves = wntr.network.generate_valve_layer(self.wn, "strategic", N, 123)
+
+            # valves.to_csv('valve_layer_stategic_'+str(N)+'.csv')
+            # ax = wntr.graphics.plot_network(self.wn, node_size=7, valve_layer=valves,
             #                                          title='Strategic, '+str(N))
-            
-            expected_valves = pd.read_csv(join(test_datadir, 'valve_layer_stategic_'+str(N)+'.csv'), index_col=0, dtype='object')
-            
+
+            expected_valves = pd.read_csv(
+                join(test_datadir, "valve_layer_stategic_" + str(N) + ".csv"),
+                index_col=0,
+                dtype="object",
+            )
+
             self.assertEqual(valves.shape[0], expected_n_valves[N])
             assert_frame_equal(valves, expected_valves)
-        
-if __name__ == '__main__':
-    unittest.main()
\ No newline at end of file
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_network_leaks.py b/wntr/tests/test_network_leaks.py
index 1c1897ce8..85531c21c 100644
--- a/wntr/tests/test_network_leaks.py
+++ b/wntr/tests/test_network_leaks.py
@@ -1,17 +1,17 @@
-import unittest
 import math
+import unittest
 from os.path import abspath, dirname, join
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir,'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
 
 class TestLeakResults(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
     @classmethod
@@ -19,62 +19,113 @@ def tearDownClass(self):
         pass
 
     def test_leak_demand(self):
-        inp_file = join(test_datadir, 'leaks.inp')
+        inp_file = join(test_datadir, "leaks.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        wn = self.wntr.morph.split_pipe(wn,'pipe2','pipe2__B','leak1')
-        leak1 = wn.get_node('leak1')
-        leak1.add_leak(wn, area=math.pi/4.0*0.01**2, discharge_coeff=0.75)
-        active_control_action = self.wntr.network.ControlAction(leak1, 'leak_status', True)
-        inactive_control_action = self.wntr.network.ControlAction(leak1, 'leak_status', False)
-        control = self.wntr.network.controls.Control._time_control(wn, 4*3600, 'SIM_TIME', False, active_control_action)
-        wn.add_control('control1', control)
-        control = self.wntr.network.controls.Control._time_control(wn, 8*3600, 'SIM_TIME', False, inactive_control_action)
-        wn.add_control('control2', control)
+        wn = self.wntr.morph.split_pipe(wn, "pipe2", "pipe2__B", "leak1")
+        leak1 = wn.get_node("leak1")
+        leak1.add_leak(wn, area=math.pi / 4.0 * 0.01 ** 2, discharge_coeff=0.75)
+        active_control_action = self.wntr.network.ControlAction(
+            leak1, "leak_status", True
+        )
+        inactive_control_action = self.wntr.network.ControlAction(
+            leak1, "leak_status", False
+        )
+        control = self.wntr.network.controls.Control._time_control(
+            wn, 4 * 3600, "SIM_TIME", False, active_control_action
+        )
+        wn.add_control("control1", control)
+        control = self.wntr.network.controls.Control._time_control(
+            wn, 8 * 3600, "SIM_TIME", False, inactive_control_action
+        )
+        wn.add_control("control2", control)
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
-        for t in results.node['leak_demand'].index:
-            if t < 4*3600 or t >= 8*3600:
-                self.assertAlmostEqual(results.node['leak_demand'].at[t,'leak1'], 0.0)
+        for t in results.node["leak_demand"].index:
+            if t < 4 * 3600 or t >= 8 * 3600:
+                self.assertAlmostEqual(results.node["leak_demand"].at[t, "leak1"], 0.0)
             else:
-                self.assertAlmostEqual(results.node['leak_demand'].at[t,'leak1'], 0.75*math.pi/4.0*0.01**2.0*math.sqrt(2*9.81*results.node['pressure'].loc[t,'leak1']))
+                self.assertAlmostEqual(
+                    results.node["leak_demand"].at[t, "leak1"],
+                    0.75
+                    * math.pi
+                    / 4.0
+                    * 0.01 ** 2.0
+                    * math.sqrt(2 * 9.81 * results.node["pressure"].loc[t, "leak1"]),
+                )
 
     def test_leak_against_epanet(self):
-        inp_file = join(test_datadir, 'leaks.inp')
+        inp_file = join(test_datadir, "leaks.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        wn = self.wntr.morph.split_pipe(wn,'pipe2','pipe2__B','leak1')
-        leak1 = wn.get_node('leak1')
-        leak1.add_leak(wn, area=math.pi/4.0*0.08**2, discharge_coeff=0.75, start_time=0, end_time=None)
+        wn = self.wntr.morph.split_pipe(wn, "pipe2", "pipe2__B", "leak1")
+        leak1 = wn.get_node("leak1")
+        leak1.add_leak(
+            wn,
+            area=math.pi / 4.0 * 0.08 ** 2,
+            discharge_coeff=0.75,
+            start_time=0,
+            end_time=None,
+        )
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
-        inp_file = join(test_datadir, 'epanet_leaks.inp')
+        inp_file = join(test_datadir, "epanet_leaks.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
         wn.options.hydraulic.inpfile_units = self.wntr.epanet.util.FlowUnits.CMH
         sim = self.wntr.sim.EpanetSimulator(wn)
         epanet_results = sim.run_sim()
 
         for link_name, link in wn.links():
-            for t in results.link['flowrate'].index:
-                self.assertLessEqual(abs(results.link['flowrate'].at[t,link_name] - epanet_results.link['flowrate'].at[t,link_name]), 0.00001)
+            for t in results.link["flowrate"].index:
+                self.assertLessEqual(
+                    abs(
+                        results.link["flowrate"].at[t, link_name]
+                        - epanet_results.link["flowrate"].at[t, link_name]
+                    ),
+                    0.00001,
+                )
 
         for node_name, node in wn.nodes():
-            if node_name != 'leak1':
-                for t in results.node['demand'].index:
-                    self.assertLessEqual(abs(results.node['demand'].at[t,node_name] - epanet_results.node['demand'].at[t,node_name]), 0.00001)
+            if node_name != "leak1":
+                for t in results.node["demand"].index:
+                    self.assertLessEqual(
+                        abs(
+                            results.node["demand"].at[t, node_name]
+                            - epanet_results.node["demand"].at[t, node_name]
+                        ),
+                        0.00001,
+                    )
             else:
-                for t in results.node['leak_demand'].index:
-                    self.assertLessEqual(abs(results.node['leak_demand'].at[t,node_name] - epanet_results.node['demand'].at[t,node_name]), 0.00001)
+                for t in results.node["leak_demand"].index:
+                    self.assertLessEqual(
+                        abs(
+                            results.node["leak_demand"].at[t, node_name]
+                            - epanet_results.node["demand"].at[t, node_name]
+                        ),
+                        0.00001,
+                    )
 
         for node_name, node in wn.nodes():
-            for t in results.node['head'].index:
-                self.assertLessEqual(abs(results.node['head'].at[t,node_name] - epanet_results.node['head'].at[t,node_name]), 0.001)
+            for t in results.node["head"].index:
+                self.assertLessEqual(
+                    abs(
+                        results.node["head"].at[t, node_name]
+                        - epanet_results.node["head"].at[t, node_name]
+                    ),
+                    0.001,
+                )
 
         for node_name, node in wn.nodes():
-            for t in results.node['pressure'].index:
-                self.assertLessEqual(abs(results.node['pressure'].at[t,node_name] - epanet_results.node['pressure'].at[t,node_name]), 0.001)
-
-    #def test_remove_leak_results(self):
+            for t in results.node["pressure"].index:
+                self.assertLessEqual(
+                    abs(
+                        results.node["pressure"].at[t, node_name]
+                        - epanet_results.node["pressure"].at[t, node_name]
+                    ),
+                    0.001,
+                )
+
+    # def test_remove_leak_results(self):
     #    inp_file = join(test_datadir. 'net_test_13.inp')
     #    wn = self.wntr.network.WaterNetworkModel(inp_file)
     #    sim = self.wntr.sim.PyomoSimulator(wn)
@@ -94,6 +145,7 @@ def test_leak_against_epanet(self):
     #    self.assertEqual(True, (results1.link == results2.link)['velocity'].all().all())
     #    self.assertEqual(True, (results1.link == results2.link)['type'].all().all())
 
-if __name__ == '__main__':
+
+if __name__ == "__main__":
     unittest.main()
 
diff --git a/wntr/tests/test_network_minor_loss.py b/wntr/tests/test_network_minor_loss.py
index e7d19c5dc..2e0e83ee1 100644
--- a/wntr/tests/test_network_minor_loss.py
+++ b/wntr/tests/test_network_minor_loss.py
@@ -1,10 +1,8 @@
 import unittest
-from nose import SkipTest
 import wntr
 
 
 class TestMinorLosses(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
         pass
@@ -16,22 +14,22 @@ def tearDownClass(self):
     def test_pipe_minor_loss(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 2
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_junction(name='j1', base_demand=0.1)
-        wn.add_pipe(name='p1', start_node_name='r1', end_node_name='j1', minor_loss=100.0)
-        wn.options.hydraulic.demand_model = 'DDA'
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_junction(name="j1", base_demand=0.1)
+        wn.add_pipe(
+            name="p1", start_node_name="r1", end_node_name="j1", minor_loss=100.0
+        )
+        wn.options.hydraulic.demand_model = "DDA"
         sim = wntr.sim.WNTRSimulator(wn)
 
         results1 = sim.run_sim()
-        wn.write_inpfile('temp.inp', 'CMH')
-        
-        #raise SkipTest # EPANET seg faults (on Travis)
-        
-        wn2 = wntr.network.WaterNetworkModel('temp.inp')
+        wn.write_inpfile("temp.inp", "CMH")
+
+        wn2 = wntr.network.WaterNetworkModel("temp.inp")
         sim = wntr.sim.EpanetSimulator(wn2)
         results2 = sim.run_sim()
 
-        head1 = results1.node['head'].loc[0, 'j1']
-        head2 = results2.node['head'].loc[0, 'j1']
-        head_diff = abs(head1-head2)
+        head1 = results1.node["head"].j1.iloc[0]
+        head2 = results2.node["head"].j1.iloc[0]
+        head_diff = abs(head1 - head2)
         self.assertLess(head_diff, 0.01)
diff --git a/wntr/tests/test_network_pump_outage.py b/wntr/tests/test_network_pump_outage.py
index 13bfe83dc..2307304c4 100644
--- a/wntr/tests/test_network_pump_outage.py
+++ b/wntr/tests/test_network_pump_outage.py
@@ -1,67 +1,71 @@
-import unittest
 import math
+import unittest
 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')
+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')
+        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']
+
+        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)
-        
+        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
+
+        assert "10_outage" in wn.control_name_list
         num_controls = wn.num_controls
-        
-        pump_flowrate = results2.link['flowrate'].loc[13*3600:36*3600,'10']
+
+        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
-        
+
+        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']
+
+        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()
-        
+
+        # (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__':
+
+
+if __name__ == "__main__":
     unittest.main()
 
diff --git a/wntr/tests/test_network_valves.py b/wntr/tests/test_network_valves.py
index f4bc3062b..7c7f5e7e2 100644
--- a/wntr/tests/test_network_valves.py
+++ b/wntr/tests/test_network_valves.py
@@ -1,10 +1,8 @@
 import unittest
-from nose import SkipTest
 import wntr
 
 
 class TestTCVs(unittest.TestCase):
-
     @classmethod
     def setUpClass(cls):
         pass
@@ -14,34 +12,50 @@ def tearDownClass(cls):
         pass
 
     def test_pipe_minor_loss(self):
-        
+
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 4
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.1)
-        wn.add_pipe(name='p1', start_node_name='r1', end_node_name='j1', length=10.0, diameter=0.3048,
-                    roughness=100, minor_loss=0.0)
-        wn.add_valve(name='v1', start_node_name='j1', end_node_name='j2', diameter=0.3048, valve_type='TCV',
-                     minor_loss=30.0, setting=60.0)
-
-        valve = wn.get_link('v1')
-        open_action = wntr.network.ControlAction(valve, 'status', wntr.network.LinkStatus.Opened)
-        control = wntr.network.controls.Control._time_control(wn, 7200, time_flag='SIM_TIME', daily_flag=False, control_action=open_action)
-        wn.add_control('c1', control)
-
-        wn.options.hydraulic.demand_model = 'DDA'
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.1)
+        wn.add_pipe(
+            name="p1",
+            start_node_name="r1",
+            end_node_name="j1",
+            length=10.0,
+            diameter=0.3048,
+            roughness=100,
+            minor_loss=0.0,
+        )
+        wn.add_valve(
+            name="v1",
+            start_node_name="j1",
+            end_node_name="j2",
+            diameter=0.3048,
+            valve_type="TCV",
+            minor_loss=30.0,
+            initial_setting=60.0,
+        )
+
+        valve = wn.get_link("v1")
+        open_action = wntr.network.ControlAction(
+            valve, "status", wntr.network.LinkStatus.Opened
+        )
+        control = wntr.network.controls.Control._time_control(
+            wn, 7200, time_flag="SIM_TIME", daily_flag=False, control_action=open_action
+        )
+        wn.add_control("c1", control)
+
+        wn.options.hydraulic.demand_model = "DDA"
         sim = wntr.sim.WNTRSimulator(wn)
         results1 = sim.run_sim()
-        
-        #raise SkipTest # EPANET seg faults
-        
+
         sim = wntr.sim.EpanetSimulator(wn)
         results2 = sim.run_sim()
 
-        for t in results2.node['head'].index:
-            head1 = results1.node['head'].loc[t, 'j2']
-            head2 = results2.node['head'].loc[t, 'j2']
+        for t in results2.node["head"].index:
+            head1 = results1.node["head"].loc[t, "j2"]
+            head2 = results2.node["head"].loc[t, "j2"]
             head_diff = abs(head1 - head2)
             self.assertLess(head_diff, 0.01)
 
@@ -50,134 +64,175 @@ class TestPSVs(unittest.TestCase):
     def test_active(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 4
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.0)
-        wn.add_tank(name='t1', init_level=10.0, max_level=25, min_vol=0.0)
-        wn.add_pipe(name='p1', start_node_name='r1', end_node_name='j1', diameter=0.2)
-        wn.add_valve(name='v1', start_node_name='j1', end_node_name='j2', diameter=0.3, valve_type='PSV',
-                     minor_loss=100.0, setting=18)
-        wn.add_pipe(name='p3', start_node_name='t1', end_node_name='j2')
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.0)
+        wn.add_tank(name="t1", init_level=10.0, max_level=25, min_vol=0.0)
+        wn.add_pipe(name="p1", start_node_name="r1", end_node_name="j1", diameter=0.2)
+        wn.add_valve(
+            name="v1",
+            start_node_name="j1",
+            end_node_name="j2",
+            diameter=0.3,
+            valve_type="PSV",
+            minor_loss=100.0,
+            initial_setting=18,
+        )
+        wn.add_pipe(name="p3", start_node_name="t1", end_node_name="j2")
 
         sim = wntr.sim.WNTRSimulator(wn)
         res = sim.run_sim()
 
         for t in res.time:
-            self.assertEqual(res.link['status'].loc[t, 'v1'], 2)
-            self.assertAlmostEqual(res.node['head'].loc[t, 'j1'], 18)
+            self.assertEqual(res.link["status"].loc[t, "v1"], 2)
+            self.assertAlmostEqual(res.node["head"].loc[t, "j1"], 18)
 
     def test_open(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 4
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.0)
-        wn.add_tank(name='t1', init_level=10.0, max_level=25, min_vol=0.0)
-        wn.add_pipe(name='p1', start_node_name='r1', end_node_name='j1', diameter=0.2)
-        wn.add_valve(name='v1', start_node_name='j1', end_node_name='j2', diameter=0.3, valve_type='PSV',
-                     minor_loss=100.0, setting=10)
-        wn.add_pipe(name='p3', start_node_name='t1', end_node_name='j2')
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.0)
+        wn.add_tank(name="t1", init_level=10.0, max_level=25, min_vol=0.0)
+        wn.add_pipe(name="p1", start_node_name="r1", end_node_name="j1", diameter=0.2)
+        wn.add_valve(
+            name="v1",
+            start_node_name="j1",
+            end_node_name="j2",
+            diameter=0.3,
+            valve_type="PSV",
+            minor_loss=100.0,
+            initial_setting=10,
+        )
+        wn.add_pipe(name="p3", start_node_name="t1", end_node_name="j2")
 
         sim = wntr.sim.WNTRSimulator(wn)
         res = sim.run_sim()
 
         for t in res.time:
-            self.assertEqual(res.link['status'].loc[t, 'v1'], 1)
+            self.assertEqual(res.link["status"].loc[t, "v1"], 1)
 
     def test_active_to_open_to_close(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 4
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.0)
-        wn.add_tank(name='t1', init_level=10.0, max_level=25, min_vol=0.0)
-        wn.add_reservoir(name='r2', base_head=30)
-        wn.add_pipe(name='p1', start_node_name='r1', end_node_name='j1', diameter=0.2)
-        wn.add_valve(name='v1', start_node_name='j1', end_node_name='j2', diameter=0.3, valve_type='PSV',
-                     minor_loss=100.0, setting=15)
-        wn.add_pipe(name='p3', start_node_name='t1', end_node_name='j2')
-        wn.add_pipe(name='p4', start_node_name='r2', end_node_name='t1')
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.0)
+        wn.add_tank(name="t1", init_level=10.0, max_level=25, min_vol=0.0)
+        wn.add_reservoir(name="r2", base_head=30)
+        wn.add_pipe(name="p1", start_node_name="r1", end_node_name="j1", diameter=0.2)
+        wn.add_valve(
+            name="v1",
+            start_node_name="j1",
+            end_node_name="j2",
+            diameter=0.3,
+            valve_type="PSV",
+            minor_loss=100.0,
+            initial_setting=15,
+        )
+        wn.add_pipe(name="p3", start_node_name="t1", end_node_name="j2")
+        wn.add_pipe(name="p4", start_node_name="r2", end_node_name="t1")
 
         sim = wntr.sim.WNTRSimulator(wn)
         res = sim.run_sim()
 
-        self.assertEqual(res.link['status'].loc[0, 'v1'], 2)
-        self.assertEqual(res.link['status'].loc[3600, 'v1'], 1)
-        self.assertEqual(res.link['status'].loc[7200, 'v1'], 0)
-        self.assertEqual(res.link['status'].loc[10800, 'v1'], 0)
-        self.assertEqual(res.link['status'].loc[14400, 'v1'], 0)
+        self.assertEqual(res.link["status"].loc[0, "v1"], 2)
+        self.assertEqual(res.link["status"].loc[3600, "v1"], 1)
+        self.assertEqual(res.link["status"].loc[7200, "v1"], 0)
+        self.assertEqual(res.link["status"].loc[10800, "v1"], 0)
+        self.assertEqual(res.link["status"].loc[14400, "v1"], 0)
 
     def test_active_to_open(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 4
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.0)
-        wn.add_tank(name='t1', init_level=10.0, max_level=25, min_vol=0.0)
-        wn.add_pipe(name='p1', start_node_name='r1', end_node_name='j1', diameter=0.2)
-        wn.add_valve(name='v1', start_node_name='j1', end_node_name='j2', diameter=0.3, valve_type='PSV',
-                     minor_loss=100.0, setting=15)
-        wn.add_pipe(name='p3', start_node_name='t1', end_node_name='j2')
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.0)
+        wn.add_tank(name="t1", init_level=10.0, max_level=25, min_vol=0.0)
+        wn.add_pipe(name="p1", start_node_name="r1", end_node_name="j1", diameter=0.2)
+        wn.add_valve(
+            name="v1",
+            start_node_name="j1",
+            end_node_name="j2",
+            diameter=0.3,
+            valve_type="PSV",
+            minor_loss=100.0,
+            initial_setting=15,
+        )
+        wn.add_pipe(name="p3", start_node_name="t1", end_node_name="j2")
 
         sim = wntr.sim.WNTRSimulator(wn)
         res = sim.run_sim()
 
-        self.assertEqual(res.link['status'].loc[0, 'v1'], 2)
-        self.assertEqual(res.link['status'].loc[3600, 'v1'], 2)
-        self.assertEqual(res.link['status'].loc[7200, 'v1'], 2)
-        self.assertEqual(res.link['status'].loc[10800, 'v1'], 1)
-        self.assertEqual(res.link['status'].loc[14400, 'v1'], 1)
+        self.assertEqual(res.link["status"].loc[0, "v1"], 2)
+        self.assertEqual(res.link["status"].loc[3600, "v1"], 2)
+        self.assertEqual(res.link["status"].loc[7200, "v1"], 2)
+        self.assertEqual(res.link["status"].loc[10800, "v1"], 1)
+        self.assertEqual(res.link["status"].loc[14400, "v1"], 1)
 
     def test_close(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 4
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.0)
-        wn.add_tank(name='t1', init_level=10.0, max_level=25, min_vol=0.0)
-        wn.add_pipe(name='p1', start_node_name='r1', end_node_name='j1', diameter=0.2)
-        wn.add_valve(name='v1', start_node_name='j2', end_node_name='j1', diameter=0.3, valve_type='PSV',
-                     minor_loss=100.0, setting=15)
-        wn.add_pipe(name='p3', start_node_name='t1', end_node_name='j2')
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.0)
+        wn.add_tank(name="t1", init_level=10.0, max_level=25, min_vol=0.0)
+        wn.add_pipe(name="p1", start_node_name="r1", end_node_name="j1", diameter=0.2)
+        wn.add_valve(
+            name="v1",
+            start_node_name="j2",
+            end_node_name="j1",
+            diameter=0.3,
+            valve_type="PSV",
+            minor_loss=100.0,
+            initial_setting=15,
+        )
+        wn.add_pipe(name="p3", start_node_name="t1", end_node_name="j2")
 
         sim = wntr.sim.WNTRSimulator(wn)
         res = sim.run_sim()
 
-        self.assertEqual(res.link['status'].loc[0, 'v1'], 0)
-        self.assertEqual(res.link['status'].loc[3600, 'v1'], 0)
-        self.assertEqual(res.link['status'].loc[7200, 'v1'], 0)
-        self.assertEqual(res.link['status'].loc[10800, 'v1'], 0)
-        self.assertEqual(res.link['status'].loc[14400, 'v1'], 0)
+        self.assertEqual(res.link["status"].loc[0, "v1"], 0)
+        self.assertEqual(res.link["status"].loc[3600, "v1"], 0)
+        self.assertEqual(res.link["status"].loc[7200, "v1"], 0)
+        self.assertEqual(res.link["status"].loc[10800, "v1"], 0)
+        self.assertEqual(res.link["status"].loc[14400, "v1"], 0)
 
     def test_close_to_open_to_active(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 12
-        wn.add_reservoir(name='t0', base_head=30)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.0)
-        wn.add_tank(name='t1', init_level=40.0, max_level=100, min_vol=0.0)
-        wn.add_tank(name='t2', init_level=0, max_level=100)
-        wn.add_pipe(name='p1', start_node_name='t0', end_node_name='j1', diameter=0.2)
-        wn.add_valve(name='v1', start_node_name='j1', end_node_name='j2', diameter=0.3, valve_type='PSV',
-                     minor_loss=100.0, setting=25)
-        wn.add_pipe(name='p3', start_node_name='j2', end_node_name='t1')
-        wn.add_pipe(name='p4', start_node_name='t1', end_node_name='t2')
+        wn.add_reservoir(name="t0", base_head=30)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.0)
+        wn.add_tank(name="t1", init_level=40.0, max_level=100, min_vol=0.0)
+        wn.add_tank(name="t2", init_level=0, max_level=100)
+        wn.add_pipe(name="p1", start_node_name="t0", end_node_name="j1", diameter=0.2)
+        wn.add_valve(
+            name="v1",
+            start_node_name="j1",
+            end_node_name="j2",
+            diameter=0.3,
+            valve_type="PSV",
+            minor_loss=100.0,
+            initial_setting=25,
+        )
+        wn.add_pipe(name="p3", start_node_name="j2", end_node_name="t1")
+        wn.add_pipe(name="p4", start_node_name="t1", end_node_name="t2")
 
         sim = wntr.sim.WNTRSimulator(wn)
         res = sim.run_sim()
-        #print(res.node['head'])
-        #print(res.link['flowrate'])
-        #print(res.link['status'])
+        # print(res.node['head'])
+        # print(res.link['flowrate'])
+        # print(res.link['status'])
 
-        self.assertEqual(res.link['status'].loc[0, 'v1'], 0)
-        self.assertEqual(res.link['status'].loc[3600, 'v1'], 0)
-        self.assertEqual(res.link['status'].loc[7200, 'v1'], 1)
-        self.assertEqual(res.link['status'].loc[10800, 'v1'], 2)
-        self.assertEqual(res.link['status'].loc[14400, 'v1'], 2)
+        self.assertEqual(res.link["status"].loc[0, "v1"], 0)
+        self.assertEqual(res.link["status"].loc[3600, "v1"], 0)
+        self.assertEqual(res.link["status"].loc[7200, "v1"], 1)
+        self.assertEqual(res.link["status"].loc[10800, "v1"], 2)
+        self.assertEqual(res.link["status"].loc[14400, "v1"], 2)
 
 
 class TestFCVs(unittest.TestCase):
-
     @classmethod
     def setUpClass(cls):
         pass
@@ -189,85 +244,168 @@ def tearDownClass(cls):
     def test_active_FCV(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 4
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.0)
-        wn.add_tank(name='t1', init_level=10.0, max_level=25.0, min_vol=0.0)
-        wn.add_pipe(name='p1', start_node_name='r1', end_node_name='j1', length=1000.0, diameter=0.3048,
-                    roughness=100, minor_loss=0.0)
-        wn.add_pipe(name='p2', start_node_name='j2', end_node_name='t1', length=1000.0, diameter=0.3048,
-                    roughness=100, minor_loss=0.0)
-        wn.add_valve(name='v1', start_node_name='j1', end_node_name='j2', diameter=0.3048, valve_type='FCV',
-                     minor_loss=100.0, setting=0.01)
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.0)
+        wn.add_tank(name="t1", init_level=10.0, max_level=25.0, min_vol=0.0)
+        wn.add_pipe(
+            name="p1",
+            start_node_name="r1",
+            end_node_name="j1",
+            length=1000.0,
+            diameter=0.3048,
+            roughness=100,
+            minor_loss=0.0,
+        )
+        wn.add_pipe(
+            name="p2",
+            start_node_name="j2",
+            end_node_name="t1",
+            length=1000.0,
+            diameter=0.3048,
+            roughness=100,
+            minor_loss=0.0,
+        )
+        wn.add_valve(
+            name="v1",
+            start_node_name="j1",
+            end_node_name="j2",
+            diameter=0.3048,
+            valve_type="FCV",
+            minor_loss=100.0,
+            initial_setting=0.01,
+        )
 
         sim = wntr.sim.WNTRSimulator(wn)
         results1 = sim.run_sim()
-        
-        #raise SkipTest # EPANET seg faults
 
         sim = wntr.sim.EpanetSimulator(wn)
         results2 = sim.run_sim()
 
-        for t in results2.link['flowrate'].index:
-            self.assertAlmostEqual(results1.link['flowrate'].loc[t, 'v1'], 0.01, 7)
-            self.assertAlmostEqual(results2.link['flowrate'].loc[t, 'v1'], 0.01, 7)
+        for t in results2.link["flowrate"].index:
+            self.assertAlmostEqual(results1.link["flowrate"].loc[t, "v1"], 0.01, 7)
+            self.assertAlmostEqual(results2.link["flowrate"].loc[t, "v1"], 0.01, 7)
 
     def test_open_FCV(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 4
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.0)
-        wn.add_tank(name='t1', init_level=10.0, max_level=25.0, min_vol=0.0)
-        wn.add_pipe(name='p1', start_node_name='r1', end_node_name='j1', length=1000.0, diameter=0.3048,
-                    roughness=100, minor_loss=0.0)
-        wn.add_pipe(name='p2', start_node_name='j2', end_node_name='t1', length=1000.0, diameter=0.3048,
-                    roughness=100, minor_loss=0.0)
-        wn.add_valve(name='v1', start_node_name='j1', end_node_name='j2', diameter=0.3048, valve_type='FCV',
-                     minor_loss=100.0, setting=0.1)
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.0)
+        wn.add_tank(name="t1", init_level=10.0, max_level=25.0, min_vol=0.0)
+        wn.add_pipe(
+            name="p1",
+            start_node_name="r1",
+            end_node_name="j1",
+            length=1000.0,
+            diameter=0.3048,
+            roughness=100,
+            minor_loss=0.0,
+        )
+        wn.add_pipe(
+            name="p2",
+            start_node_name="j2",
+            end_node_name="t1",
+            length=1000.0,
+            diameter=0.3048,
+            roughness=100,
+            minor_loss=0.0,
+        )
+        wn.add_valve(
+            name="v1",
+            start_node_name="j1",
+            end_node_name="j2",
+            diameter=0.3048,
+            valve_type="FCV",
+            minor_loss=100.0,
+            initial_setting=0.1,
+        )
 
         sim = wntr.sim.WNTRSimulator(wn)
         results1 = sim.run_sim()
-        
-        #raise SkipTest # EPANET seg faults
-        
+
         sim = wntr.sim.EpanetSimulator(wn)
         results2 = sim.run_sim()
 
-        for t in results2.link['flowrate'].index:
-            self.assertLess(abs(results1.link['flowrate'].loc[t, 'v1'] - results2.link['flowrate'].loc[t, 'v1']), 1e-5)
-            self.assertLess(results1.link['flowrate'].loc[t, 'v1'], 0.09)
+        for t in results2.link["flowrate"].index:
+            self.assertLess(
+                abs(
+                    results1.link["flowrate"].loc[t, "v1"]
+                    - results2.link["flowrate"].loc[t, "v1"]
+                ),
+                1e-5,
+            )
+            self.assertLess(results1.link["flowrate"].loc[t, "v1"], 0.09)
 
     def test_open_to_active_FCV(self):
         wn = wntr.network.WaterNetworkModel()
         wn.options.time.duration = 3600 * 10
-        wn.add_reservoir(name='r1', base_head=20.0)
-        wn.add_tank(name='t1', init_level=10.0, max_level=25.0, min_vol=0.0)
-        wn.add_junction(name='j1', base_demand=0.0)
-        wn.add_junction(name='j2', base_demand=0.0)
-        wn.add_tank(name='t2', init_level=10.0, max_level=25.0, min_vol=0.0)
-        wn.add_pipe(name='p0', start_node_name='r1', end_node_name='t1', length=1000.0, diameter=0.3048,
-                    roughness=100, minor_loss=0.0)
-        wn.add_pipe(name='p1', start_node_name='t1', end_node_name='j1', length=1000.0, diameter=0.3048,
-                    roughness=100, minor_loss=0.0)
-        wn.add_pipe(name='p2', start_node_name='j2', end_node_name='t2', length=1000.0, diameter=0.3048,
-                    roughness=100, minor_loss=0.0)
-        wn.add_valve(name='v1', start_node_name='j1', end_node_name='j2', diameter=0.3048, valve_type='FCV',
-                     minor_loss=100.0, setting=0.03)
+        wn.add_reservoir(name="r1", base_head=20.0)
+        wn.add_tank(name="t1", init_level=10.0, max_level=25.0, min_vol=0.0)
+        wn.add_junction(name="j1", base_demand=0.0)
+        wn.add_junction(name="j2", base_demand=0.0)
+        wn.add_tank(name="t2", init_level=10.0, max_level=25.0, min_vol=0.0)
+        wn.add_pipe(
+            name="p0",
+            start_node_name="r1",
+            end_node_name="t1",
+            length=1000.0,
+            diameter=0.3048,
+            roughness=100,
+            minor_loss=0.0,
+        )
+        wn.add_pipe(
+            name="p1",
+            start_node_name="t1",
+            end_node_name="j1",
+            length=1000.0,
+            diameter=0.3048,
+            roughness=100,
+            minor_loss=0.0,
+        )
+        wn.add_pipe(
+            name="p2",
+            start_node_name="j2",
+            end_node_name="t2",
+            length=1000.0,
+            diameter=0.3048,
+            roughness=100,
+            minor_loss=0.0,
+        )
+        wn.add_valve(
+            name="v1",
+            start_node_name="j1",
+            end_node_name="j2",
+            diameter=0.3048,
+            valve_type="FCV",
+            minor_loss=100.0,
+            initial_setting=0.03,
+        )
 
         sim = wntr.sim.WNTRSimulator(wn)
         results1 = sim.run_sim()
 
-        #raise SkipTest # EPANET seg faults
-        
         sim = wntr.sim.EpanetSimulator(wn)
         results2 = sim.run_sim()
 
-        for t in results2.link['flowrate'].index:
-            self.assertLess(abs(results1.link['flowrate'].loc[t, 'v1'] - results2.link['flowrate'].loc[t, 'v1']), 1e-4)
+        for t in results2.link["flowrate"].index:
+            self.assertLess(
+                abs(
+                    results1.link["flowrate"].loc[t, "v1"]
+                    - results2.link["flowrate"].loc[t, "v1"]
+                ),
+                1e-4,
+            )
             if t > 7200:
-                self.assertLess(abs(results1.link['flowrate'].loc[t, 'v1'] - 0.03), 1e-8)
-                
-        self.assertLess(abs(results1.link['flowrate'].loc[0, 'v1'] - 0.0), 1e-4)
-        self.assertLess(abs(results1.link['flowrate'].loc[3600, 'v1'] - 0.0245344210416), 1e-5)
-        self.assertLess(abs(results1.link['flowrate'].loc[7200, 'v1'] - 0.0293480847031), 1e-5)
+                self.assertLess(
+                    abs(results1.link["flowrate"].loc[t, "v1"] - 0.03), 1e-8
+                )
+
+        self.assertLess(abs(results1.link["flowrate"].loc[0, "v1"] - 0.0), 1e-4)
+        self.assertLess(
+            abs(results1.link["flowrate"].loc[3600, "v1"] - 0.0245344210416), 1e-5
+        )
+        self.assertLess(
+            abs(results1.link["flowrate"].loc[7200, "v1"] - 0.0293480847031), 1e-5
+        )
+
diff --git a/wntr/tests/test_sceanrio_earthquake.py b/wntr/tests/test_sceanrio_earthquake.py
index fd56335b9..c4188f444 100644
--- a/wntr/tests/test_sceanrio_earthquake.py
+++ b/wntr/tests/test_sceanrio_earthquake.py
@@ -1,108 +1,120 @@
-from nose.tools import *
+import unittest
+import warnings
 from os.path import abspath, dirname, join
+
 import numpy as np
 import pandas as pd
-from pandas.util.testing import assert_frame_equal, assert_series_equal
 import wntr
+from pandas.testing import assert_frame_equal, assert_series_equal
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'..','..','tests','networks_for_testing')
-netdir = join(testdir,'..','..','examples','networks')
+datadir = join(testdir, "..", "..", "tests", "networks_for_testing")
+netdir = join(testdir, "..", "..", "examples", "networks")
 
-def test_distance_to_epicenter():
-    inp_file = join(netdir,'Net1.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    earthquake = wntr.scenario.Earthquake((40,55), 5, 10000.0)
-    
-    """
-    Junction 12 is at (50,70)
-    """
-    R = earthquake.distance_to_epicenter(wn,wntr.network.Junction)
-    expected = np.sqrt(np.power(50-40,2)+np.power(70-55,2))
-    assert_less(np.abs(R['12']-expected), 1E-5) 
-
-    wn = wntr.morph.scale_node_coordinates(wn,100)
-    R = earthquake.distance_to_epicenter(wn,wntr.network.Junction)
-    expected = np.sqrt(np.power(5000-40,2)+np.power(7000-55,2))
-    assert_less(np.abs(R['12']-expected), 1E-5) 
-
-def test_correction_factor():
-    earthquake = wntr.scenario.Earthquake((40,55), 5, 10000.0)
-    
-    data = pd.DataFrame(columns=['Diameter', 'Material', 'Topography', 'Liquifaction'], index=['P1', 'P2', 'P3'])
-    data.loc['P1',:] = ['Small', 'ACP', 'Narrow valley', 'Total']
-    data.loc['P2',:] = ['Medium', 'PV', 'Terrace', 'Partial']
-    data.loc['P3',:] = ['Large', 'SP', 'Stiff alluvial', 'None']
-    C = earthquake.correction_factor(data)
-    expected = pd.Series([1*1.2*3.2*2.4,0.8*1*1.5*2,0.5*0.3*0.4*1],index=['P1','P2','P3'])
-    assert_series_equal(C,expected)
-    
-def test_pga_attenuation():
-    R = pd.Series({'1': 1000.0}) 
-    
-    earthquake = wntr.scenario.Earthquake((0, 0), 5, 10000.0)
-    
-    pga = earthquake.pga_attenuation_model(R, method=1)
-    #print(pga['1'])
-    assert_less(np.abs(pga['1']-1.3275E-1), 1E-5) 
-    
-    pga = earthquake.pga_attenuation_model(R, method=2)
-    #print(pga['1'])
-    assert_less(np.abs(pga['1']-9.6638E-2), 1E-6) 
-    
-    pga = earthquake.pga_attenuation_model(R, method=3)
-    #print(pga['1'])
-    assert_less(np.abs(pga['1']-1.2789E-3), 1E-7) 
-    
-    pga = earthquake.pga_attenuation_model(R)
-    #print(pga['1'])
-    assert_less(np.abs(pga['1']-7.6887E-2), 1E-6) 
-    
-def test_pgv_attenuation():
-    R = pd.Series({'1':1000}) 
-    
-    # Yu and Jin, 2008
-    earthquake = wntr.scenario.Earthquake((0, 0), 5, 0)
-    pgv = earthquake.pgv_attenuation_model(R, method=1)
-    assert_less(np.abs(pgv['1']-0.0531), 0.0001) 
-    
-    earthquake = wntr.scenario.Earthquake((0, 0), 7, 0)
-    pgv = earthquake.pgv_attenuation_model(R, method=1)
-    assert_less(np.abs(pgv['1']-1.8829), 0.0001) 
-    
-    earthquake = wntr.scenario.Earthquake((0, 0), 5, 0)
-    pgv = earthquake.pgv_attenuation_model(R, method=2)
-    assert_less(np.abs(pgv['1']-0.0884), 0.0001) 
-    
-    earthquake = wntr.scenario.Earthquake((0, 0), 7, 0)
-    pgv = earthquake.pgv_attenuation_model(R, method=2)
-    assert_less(np.abs(pgv['1']-2.3361), 0.0001) 
-    
-    earthquake = wntr.scenario.Earthquake((0, 0), 5, 0)
-    pgv = earthquake.pgv_attenuation_model(R)
-    assert_less(np.abs(pgv['1']-0.0707), 0.0001) 
-    
-    earthquake = wntr.scenario.Earthquake((0, 0), 7, 0)
-    pgv = earthquake.pgv_attenuation_model(R)
-    assert_less(np.abs(pgv['1']-2.1095), 0.0001) 
 
-def test_repair_rate():
-    
-    PGV = pd.Series({'1':0.0531}) 
-    C = pd.Series({'1':0.5}) 
-    earthquake = wntr.scenario.Earthquake((0, 0), 0, 0)
-    
-    RR = earthquake.repair_rate_model(PGV, C=1, method=1)
-    assert_less(np.abs(RR['1']-1.2823E-5), 1E-9) 
+class TestScenario(unittest.TestCase):
     
-    RR = earthquake.repair_rate_model(PGV, C, method=1)
-    assert_less(np.abs(RR['1']-6.4113E-6), 1E-10) 
-    
-    RR = earthquake.repair_rate_model(PGV, C=1, method=2)
-    assert_less(np.abs(RR['1']-8.4132E-6), 1E-10) 
-    
-    RR = earthquake.repair_rate_model(PGV, C, method=2)
-    assert_less(np.abs(RR['1']-4.2066E-6), 1E-10) 
-    
-if __name__ == '__main__':
-    test_correction_factor()
+    def test_distance_to_epicenter(self):
+        inp_file = join(netdir, "Net1.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        earthquake = wntr.scenario.Earthquake((40, 55), 5, 10000.0)
+
+        """
+        Junction 12 is at (50,70)
+        """
+        R = earthquake.distance_to_epicenter(wn, wntr.network.Junction)
+        expected = np.sqrt(np.power(50 - 40, 2) + np.power(70 - 55, 2))
+        self.assertLess(np.abs(R["12"] - expected), 1e-5)
+
+        wn = wntr.morph.scale_node_coordinates(wn, 100)
+        R = earthquake.distance_to_epicenter(wn, wntr.network.Junction)
+        expected = np.sqrt(np.power(5000 - 40, 2) + np.power(7000 - 55, 2))
+        self.assertLess(np.abs(R["12"] - expected), 1e-5)
+
+    def test_correction_factor(self):
+        earthquake = wntr.scenario.Earthquake((40, 55), 5, 10000.0)
+
+        data = pd.DataFrame(
+            columns=["Diameter", "Material", "Topography", "Liquifaction"],
+            index=["P1", "P2", "P3"],
+        )
+        data.loc["P1", :] = ["Small", "ACP", "Narrow valley", "Total"]
+        data.loc["P2", :] = ["Medium", "PV", "Terrace", "Partial"]
+        data.loc["P3", :] = ["Large", "SP", "Stiff alluvial", "None"]
+        C = earthquake.correction_factor(data)
+        expected = pd.Series(
+            [1 * 1.2 * 3.2 * 2.4, 0.8 * 1 * 1.5 * 2, 0.5 * 0.3 * 0.4 * 1],
+            index=["P1", "P2", "P3"],
+        )
+        assert_series_equal(C, expected)
+
+    def test_pga_attenuation(self):
+        R = pd.Series({"1": 1000.0})
+
+        earthquake = wntr.scenario.Earthquake((0, 0), 5, 10000.0)
+
+        pga = earthquake.pga_attenuation_model(R, method=1)
+        # print(pga['1'])
+        self.assertLess(np.abs(pga["1"] - 1.3275e-1), 1e-5)
+
+        pga = earthquake.pga_attenuation_model(R, method=2)
+        # print(pga['1'])
+        self.assertLess(np.abs(pga["1"] - 9.6638e-2), 1e-6)
+
+        pga = earthquake.pga_attenuation_model(R, method=3)
+        # print(pga['1'])
+        self.assertLess(np.abs(pga["1"] - 1.2789e-3), 1e-7)
+
+        pga = earthquake.pga_attenuation_model(R)
+        # print(pga['1'])
+        self.assertLess(np.abs(pga["1"] - 7.6887e-2), 1e-6)
+
+    def test_pgv_attenuation(self):
+        R = pd.Series({"1": 1000})
+
+        # Yu and Jin, 2008
+        earthquake = wntr.scenario.Earthquake((0, 0), 5, 0)
+        pgv = earthquake.pgv_attenuation_model(R, method=1)
+        self.assertLess(np.abs(pgv["1"] - 0.0531), 0.0001)
+
+        earthquake = wntr.scenario.Earthquake((0, 0), 7, 0)
+        pgv = earthquake.pgv_attenuation_model(R, method=1)
+        self.assertLess(np.abs(pgv["1"] - 1.8829), 0.0001)
+
+        earthquake = wntr.scenario.Earthquake((0, 0), 5, 0)
+        pgv = earthquake.pgv_attenuation_model(R, method=2)
+        self.assertLess(np.abs(pgv["1"] - 0.0884), 0.0001)
+
+        earthquake = wntr.scenario.Earthquake((0, 0), 7, 0)
+        pgv = earthquake.pgv_attenuation_model(R, method=2)
+        self.assertLess(np.abs(pgv["1"] - 2.3361), 0.0001)
+
+        earthquake = wntr.scenario.Earthquake((0, 0), 5, 0)
+        pgv = earthquake.pgv_attenuation_model(R)
+        self.assertLess(np.abs(pgv["1"] - 0.0707), 0.0001)
+
+        earthquake = wntr.scenario.Earthquake((0, 0), 7, 0)
+        pgv = earthquake.pgv_attenuation_model(R)
+        self.assertLess(np.abs(pgv["1"] - 2.1095), 0.0001)
+
+    def test_repair_rate(self):
+
+        PGV = pd.Series({"1": 0.0531})
+        C = pd.Series({"1": 0.5})
+        earthquake = wntr.scenario.Earthquake((0, 0), 0, 0)
+
+        RR = earthquake.repair_rate_model(PGV, C=1, method=1)
+        self.assertLess(np.abs(RR["1"] - 1.2823e-5), 1e-9)
+
+        RR = earthquake.repair_rate_model(PGV, C, method=1)
+        self.assertLess(np.abs(RR["1"] - 6.4113e-6), 1e-10)
+
+        RR = earthquake.repair_rate_model(PGV, C=1, method=2)
+        self.assertLess(np.abs(RR["1"] - 8.4132e-6), 1e-10)
+
+        RR = earthquake.repair_rate_model(PGV, C, method=2)
+        self.assertLess(np.abs(RR["1"] - 4.2066e-6), 1e-10)
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)
diff --git a/wntr/tests/test_scenario_fragility_curve.py b/wntr/tests/test_scenario_fragility_curve.py
index 0015cf673..c36b1ba0d 100644
--- a/wntr/tests/test_scenario_fragility_curve.py
+++ b/wntr/tests/test_scenario_fragility_curve.py
@@ -1,53 +1,60 @@
-from nose.tools import *
+import unittest
 from os.path import abspath, dirname, join
+
 import numpy as np
 import pandas as pd
-from scipy.stats import norm, lognorm
 import wntr
+from scipy.stats import lognorm, norm
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'..','..','tests','networks_for_testing')
+datadir = join(testdir, "..", "..", "tests", "networks_for_testing")
 
 FC1 = wntr.scenario.FragilityCurve()
-FC1.add_state('Major', 2, {'Default': norm(loc=1, scale=2)})
-FC1.add_state('Minor', 1, {'Default': norm(loc=0, scale=1)})     
+FC1.add_state("Major", 2, {"Default": norm(loc=1, scale=2)})
+FC1.add_state("Minor", 1, {"Default": norm(loc=0, scale=1)})
 
 FC2 = wntr.scenario.FragilityCurve()
-FC2.add_state('Minor', 1, {'Default': lognorm(0.25, loc=0, scale=1),
-                           '3': lognorm(0.2, loc=0, scale=1)})  
-FC2.add_state('Major', 2, {'Default': lognorm(0.25, loc=1, scale=2)}) 
+FC2.add_state(
+    "Minor",
+    1,
+    {"Default": lognorm(0.25, loc=0, scale=1), "3": lognorm(0.2, loc=0, scale=1)},
+)
+FC2.add_state("Major", 2, {"Default": lognorm(0.25, loc=1, scale=2)})
 
-#x = np.linspace(-5,5,100)
-#for name, state in FC2.states():
+# x = np.linspace(-5,5,100)
+# for name, state in FC2.states():
 #    dist=state.distribution['Default']
 #    plt.plot(x,dist.cdf(x), label=name)
-#plt.ylim((0,1))
-#plt.legend()
-    
-def test_get_priority_map():
-    priority_map = FC1.get_priority_map()
-    assert_dict_equal(priority_map, {None: 0, 'Minor': 1, 'Major': 2})
-
-def test_cdf_probability():
-    x = pd.Series({'1': 0, '2': 1, '3': 2})
-    Pr = FC1.cdf_probability(x)
-    assert_equal(Pr.loc['1','Minor'], 0.5)
-    assert_less(Pr.loc['2','Minor']-0.841, 0.001)
-    assert_less(Pr.loc['3','Minor']-0.977, 0.001)
-    assert_equal(Pr.loc['2','Major'], 0.5)
-
-def test_sample_damage_state():
-    x = pd.Series({'1': 0, '2': 1, '3': 2})
-    Pr = FC1.cdf_probability(x)
-    np.random.seed(45)
-    states = FC1.sample_damage_state(Pr)
-    #p with random seed of 45
-    #1    0.989012
-    #2    0.549545
-    #3    0.281447
-    assert_equal(states.loc['1'], None)
-    assert_equal(states.loc['2'], 'Minor')
-    assert_equal(states.loc['3'], 'Major')
-
-if __name__ == '__main__':
-    test_sample_damage_state()
+# plt.ylim((0,1))
+# plt.legend()
+
+
+class TestFragilityCurveScenario(unittest.TestCase):
+    def test_get_priority_map(self):
+        priority_map = FC1.get_priority_map()
+        self.assertDictEqual(priority_map, {None: 0, "Minor": 1, "Major": 2})
+
+    def test_cdf_probability(self):
+        x = pd.Series({"1": 0, "2": 1, "3": 2})
+        Pr = FC1.cdf_probability(x)
+        self.assertEqual(Pr.loc["1", "Minor"], 0.5)
+        self.assertLess(Pr.loc["2", "Minor"] - 0.841, 0.001)
+        self.assertLess(Pr.loc["3", "Minor"] - 0.977, 0.001)
+        self.assertEqual(Pr.loc["2", "Major"], 0.5)
+
+    def test_sample_damage_state(self):
+        x = pd.Series({"1": 0, "2": 1, "3": 2})
+        Pr = FC1.cdf_probability(x)
+        np.random.seed(45)
+        states = FC1.sample_damage_state(Pr)
+        # p with random seed of 45
+        # 1    0.989012
+        # 2    0.549545
+        # 3    0.281447
+        self.assertEqual(states.loc["1"], None)
+        self.assertEqual(states.loc["2"], "Minor")
+        self.assertEqual(states.loc["3"], "Major")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_sim_PDD.py b/wntr/tests/test_sim_PDD.py
index ee4a46960..7aa99d1e6 100644
--- a/wntr/tests/test_sim_PDD.py
+++ b/wntr/tests/test_sim_PDD.py
@@ -1,16 +1,17 @@
-import unittest
 import math
+import unittest
 from os.path import abspath, dirname, join
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir,'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
-class TestPDD(unittest.TestCase):
 
+class TestPDD(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
     @classmethod
@@ -18,25 +19,29 @@ def tearDownClass(self):
         pass
 
     def test_pdd_with_wntr(self):
-        inp_file = join(test_datadir, 'simulator.inp')
+        inp_file = join(test_datadir, "simulator.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        res1 = wn.get_node('reservoir1')
+        res1 = wn.get_node("reservoir1")
         res1.head_timeseries.base_value = 10.0
-        p1 = wn.get_link('pipe1')
+        p1 = wn.get_link("pipe1")
         p1.length = 0.0
-        p2 = wn.get_link('pipe2')
+        p2 = wn.get_link("pipe2")
         p2.length = 0.0
 
         for jname, j in wn.nodes(self.wntr.network.Junction):
             j.minimum_pressure = 0.0
             j.required_pressure = 15.0
 
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
         for t in results.time:
-            self.assertEqual(results.node['demand'].at[t,'junction2'], 150.0/3600.0*math.sqrt((10.0-0.0)/(15.0-0.0)))
+            self.assertEqual(
+                results.node["demand"].at[t, "junction2"],
+                150.0 / 3600.0 * math.sqrt((10.0 - 0.0) / (15.0 - 0.0)),
+            )
+
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/wntr/tests/test_sim_benchmark.py b/wntr/tests/test_sim_benchmark.py
index 7b82305c4..42e80cb98 100644
--- a/wntr/tests/test_sim_benchmark.py
+++ b/wntr/tests/test_sim_benchmark.py
@@ -5,239 +5,320 @@
 @author: dlvilla
 """
 import unittest
+import warnings
 from os.path import abspath, dirname, join
-from nose.tools import *
-from scipy.optimize import fsolve
-from scipy.integrate import solve_ivp
 
-from numpy import pi, arange,squeeze,array, zeros,sqrt, interp, mean, exp, log, concatenate
 from matplotlib import pyplot as plt
+from numpy import (
+    arange,
+    array,
+    concatenate,
+    exp,
+    interp,
+    log,
+    mean,
+    pi,
+    sqrt,
+    squeeze,
+    zeros,
+)
+from scipy.integrate import solve_ivp
+from scipy.optimize import fsolve
 
 testdir = dirname(abspath(str(__file__)))
 
 
 class Test_Benchmarks(unittest.TestCase):
-    
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
-        
+
         # build up the model to be used in several tests
         h20 = 150.0
-                # first test a constant diameter tank!
-        inp = {'h20': h20,  # meters
-                        'D': 1.0,     # meters
-                        'd': 0.2,     # meters
-                        'L': sqrt(2) * h20,
-                        'C': 130,      # pipe roughness factor
-                        'tf': 400.0,   # total simulation time
-                        'pA': 200.0,   # pump A coefficient
-                        'pB': 4.0,     # pump B coefficient
-                        'pC': 1.0,     # pump C coefficient
-                        'dt_max':1.0,   # maximum time step for the ode solution
-                        'dt_max_wntr':[1,10,50,100]}
+        # first test a constant diameter tank!
+        inp = {
+            "h20": h20,  # meters
+            "D": 1.0,  # meters
+            "d": 0.2,  # meters
+            "L": sqrt(2) * h20,
+            "C": 130,  # pipe roughness factor
+            "tf": 400.0,  # total simulation time
+            "pA": 200.0,  # pump A coefficient
+            "pB": 4.0,  # pump B coefficient
+            "pC": 1.0,  # pump C coefficient
+            "dt_max": 1.0,  # maximum time step for the ode solution
+            "dt_max_wntr": [1, 10, 50, 100],
+        }
         self.ode_inp = inp
 
         wn = self.wntr.network.WaterNetworkModel()
-        
+
         # build the equivalent WNTR model
-        # add nodes 
-        
-        wn.add_reservoir("r1",base_head=0.0,coordinates=(0,0))
-        wn.add_junction("j1",base_demand=0.0,elevation=0.0,coordinates=(0,0))
-        wn.add_tank("t1",elevation=h20,init_level=0.0,min_level=0.0,max_level=1.0e10,
-                    diameter=inp['D'], min_vol=0.0,coordinates=(h20,0.0))
+        # add nodes
+
+        wn.add_reservoir("r1", base_head=0.0, coordinates=(0, 0))
+        wn.add_junction("j1", base_demand=0.0, elevation=0.0, coordinates=(0, 0))
+        wn.add_tank(
+            "t1",
+            elevation=h20,
+            init_level=0.0,
+            min_level=0.0,
+            max_level=1.0e10,
+            diameter=inp["D"],
+            min_vol=0.0,
+            coordinates=(h20, 0.0),
+        )
         # add links
-        wn.add_pipe("p1",start_node_name="j1",end_node_name="t1",length=inp['L'],diameter=inp['d'],
-                    roughness=inp['C'],minor_loss=0.0)
-        wn.add_curve("pump1_curve","HEAD",[(0.0,inp['pA']),(inp['pA']/inp['pB'],0.0)])
-        wn.add_pump("pump1",start_node_name="r1",end_node_name="j1",
-                    pump_type="HEAD",pump_parameter="pump1_curve",speed=1.0)
-        
-        wn.options.time.duration=inp['tf']
-        wn.options.hydraulic.headloss="H-W"
-        wn.options.hydraulic.viscosity=0.0
-        wn.options.hydraulic.specific_gravity=1.0
-        
+        wn.add_pipe(
+            "p1",
+            start_node_name="j1",
+            end_node_name="t1",
+            length=inp["L"],
+            diameter=inp["d"],
+            roughness=inp["C"],
+            minor_loss=0.0,
+        )
+        wn.add_curve(
+            "pump1_curve", "HEAD", [(0.0, inp["pA"]), (inp["pA"] / inp["pB"], 0.0)]
+        )
+        wn.add_pump(
+            "pump1",
+            start_node_name="r1",
+            end_node_name="j1",
+            pump_type="HEAD",
+            pump_parameter="pump1_curve",
+            speed=1.0,
+        )
+
+        wn.options.time.duration = inp["tf"]
+        wn.options.hydraulic.headloss = "H-W"
+        wn.options.hydraulic.viscosity = 0.0
+        wn.options.hydraulic.specific_gravity = 1.0
+
         self.wn = wn
         sim = self.wntr.sim.WNTRSimulator(wn)
-        
+
         self.sim = sim
+
     @classmethod
     def tearDownClass(self):
         pass
 
+    
     def test_wntr_vs_ode_const_diam(self):
-        
+
         # run the case as set up in the Setup method.
-        
+
         wn = self.wn
         sim = self.sim
         inp = self.ode_inp
-        
+
         # run the wntr model
-        results = self._run_study(wn,sim)
-        
+        results = self._run_study(wn, sim)
+
         # run the ode solution
-        Q,t,h2 = single_reservoir_pump_pipe_tank_ode_solution(inp['h20'],
-                                                              inp['D'],
-                                                              inp['d'],
-                                                              inp['L'],
-                                                              inp['C'],
-                                                              inp['tf'],
-                                                              inp['pA'],
-                                                              inp['pB'],
-                                                              inp['pC'],
-                                                              inp['dt_max'])
-    
-        hR2 = self._calc_wntr_ode_R2(results[0],t, h2, 'head', 't1', 'node')
-        self.assertLessEqual(0.5,hR2,msg="The WNTR numerical solution for tank head has R2" +
-            " value less than 0.5 w/r to solution of an exact differential equation.")
+        Q, t, h2 = single_reservoir_pump_pipe_tank_ode_solution(
+            inp["h20"],
+            inp["D"],
+            inp["d"],
+            inp["L"],
+            inp["C"],
+            inp["tf"],
+            inp["pA"],
+            inp["pB"],
+            inp["pC"],
+            inp["dt_max"],
+        )
+
+        hR2 = self._calc_wntr_ode_R2(results[0], t, h2, "head", "t1", "node")
+        self.assertLessEqual(
+            0.5,
+            hR2,
+            msg="The WNTR numerical solution for tank head has R2"
+            + " value less than 0.5 w/r to solution of an exact differential equation.",
+        )
 
-        QR2 = self._calc_wntr_ode_R2(results[0],t,Q,'flowrate','pump1','link')
+        QR2 = self._calc_wntr_ode_R2(results[0], t, Q, "flowrate", "pump1", "link")
         # The R2 standard here used to be 0.95 but for some reason the travis-CI testing would not
         # pass and produced an R2 of 0.741. I could not replicate the difference
         # from unix or windows and cannot troubleshoot so the standard was reduced
         # since this comparison is not a requirement at such a high resolution time
         # step.
-        self.assertLessEqual(0.5,QR2,msg="The WNTR numerical solution for flow rate has R2" +
-            " value less than 0.5 w/r to solution of an exact differential equation.")
-        
-        
+        self.assertLessEqual(
+            0.5,
+            QR2,
+            msg="The WNTR numerical solution for flow rate has R2"
+            + " value less than 0.5 w/r to solution of an exact differential equation.",
+        )
+
         create_graph = False
         if create_graph:
-            self._create_graph(t,Q,h2,inp['dt_max_wntr'],results,"constant_diameter")
-        
+            self._create_graph(
+                t, Q, h2, inp["dt_max_wntr"], results, "constant_diameter"
+            )
+
+    
     def test_wntr_vs_ode_vcurve(self):
-        
+
         self._prepare_pump_and_vol_curve()
-        
+
         self._delete_controls()
-        
+
         wn = self.wn
         sim = self.sim
         inp = self.ode_inp
-        
 
-        
         # increase the duration of the simulation to capture saturation
-        inp['tf'] = 800.0
-        wn.options.time.duration=inp['tf']
-        
-        results = self._run_study(wn,sim)
+        inp["tf"] = 800.0
+        wn.options.time.duration = inp["tf"]
+
+        results = self._run_study(wn, sim)
 
         # run the ode solution
-        Q,t,h2 = single_reservoir_pump_pipe_tank_ode_solution(inp['h20'],
-                                                              inp['D'],
-                                                              inp['d'],
-                                                              inp['L'],
-                                                              inp['C'],
-                                                              inp['tf'],
-                                                              inp['pA'],
-                                                              inp['pB'],
-                                                              inp['pC'],
-                                                              inp['dt_max'],
-                                                              inp['vcurve'])
-        
+        Q, t, h2 = single_reservoir_pump_pipe_tank_ode_solution(
+            inp["h20"],
+            inp["D"],
+            inp["d"],
+            inp["L"],
+            inp["C"],
+            inp["tf"],
+            inp["pA"],
+            inp["pB"],
+            inp["pC"],
+            inp["dt_max"],
+            inp["vcurve"],
+        )
+
         create_graph = False
         if create_graph:
-            self._create_graph(t,Q,h2,inp['dt_max_wntr'],results,"parabolic_vcurve")
-
-        hR2 = self._calc_wntr_ode_R2(results[0],t, h2, 'head', 't1', 'node')
-        # r2 standard used to be 0.8 CI-travis testing would not pass though. 
-        self.assertLessEqual(0.3,hR2,msg="The WNTR numerical solution for tank head has R2" +
-            " value less than 0.3 w/r to solution of an exact differential equation.")
-
-        QR2 = self._calc_wntr_ode_R2(results[0],t,Q,'flowrate','pump1','link')
-        self.assertLessEqual(0.3,QR2,msg="The WNTR numerical solution for flow rate has R2" +
-            " value less than 0.3 w/r to solution of an exact differential equation.")
-        
-        
+            self._create_graph(
+                t, Q, h2, inp["dt_max_wntr"], results, "parabolic_vcurve"
+            )
+
+        hR2 = self._calc_wntr_ode_R2(results[0], t, h2, "head", "t1", "node")
+        # r2 standard used to be 0.8 CI-travis testing would not pass though.
+        self.assertLessEqual(
+            0.3,
+            hR2,
+            msg="The WNTR numerical solution for tank head has R2"
+            + " value less than 0.3 w/r to solution of an exact differential equation.",
+        )
+
+        QR2 = self._calc_wntr_ode_R2(results[0], t, Q, "flowrate", "pump1", "link")
+        self.assertLessEqual(
+            0.3,
+            QR2,
+            msg="The WNTR numerical solution for flow rate has R2"
+            + " value less than 0.3 w/r to solution of an exact differential equation.",
+        )
+
+    
     def test_wntr_vs_ode_tank_control(self):
-        
+
         self._prepare_pump_and_vol_curve()
-        
+
         wn = self.wn
         wntr = self.wntr
         sim = self.sim
         cntrl = wntr.network.controls
         inp = self.ode_inp
-        
-        wn.add_pipe("bypass_pump1","r1","j1",length=5.0,diameter=inp['d'])
+
+        wn.add_pipe("bypass_pump1", "r1", "j1", length=5.0, diameter=inp["d"])
         bypass_pump1 = wn.get_link("bypass_pump1")
         # this pipe is not open unless the pump closes.
         bypass_pump1._user_status = 0
-        
+
         pump1 = wn.get_link("pump1")
         t1 = wn.get_node("t1")
-        cond1 = cntrl.ValueCondition(t1,'level','>', 45)
-        cond2 = cntrl.ValueCondition(t1,'level','<', 40)
-        
-        act1 = cntrl.ControlAction(pump1,'status',0)
-        act2 = cntrl.ControlAction(pump1,'status',1)
-        act3 = cntrl.ControlAction(bypass_pump1,'status',1)
-        act4 = cntrl.ControlAction(bypass_pump1,'status',0)
-        rule1 = cntrl.Rule(cond1, [act1,act3], name="turn_off_pump")
-        rule2 = cntrl.Rule(cond2, [act2,act4], name="start_pump")
-        
-        wn.add_control("pump_off_bypass_open",rule1)
-        wn.add_control("pump_on_bypass_closed",rule2)
-        
-        inp['tf'] = 1200
-        wn.options.time.duration=inp['tf']
-        
-        results = self._run_study(wn,sim)
-        
+        cond1 = cntrl.ValueCondition(t1, "level", ">", 45)
+        cond2 = cntrl.ValueCondition(t1, "level", "<", 40)
+
+        act1 = cntrl.ControlAction(pump1, "status", 0)
+        act2 = cntrl.ControlAction(pump1, "status", 1)
+        act3 = cntrl.ControlAction(bypass_pump1, "status", 1)
+        act4 = cntrl.ControlAction(bypass_pump1, "status", 0)
+        rule1 = cntrl.Rule(cond1, [act1, act3], name="turn_off_pump")
+        rule2 = cntrl.Rule(cond2, [act2, act4], name="start_pump")
+
+        wn.add_control("pump_off_bypass_open", rule1)
+        wn.add_control("pump_on_bypass_closed", rule2)
+
+        inp["tf"] = 1200
+        wn.options.time.duration = inp["tf"]
+
+        results = self._run_study(wn, sim)
+
         # run the ode solution
-        Q,t,h2 = single_reservoir_pump_pipe_tank_ode_solution(inp['h20'],
-                                                              inp['D'],
-                                                              inp['d'],
-                                                              inp['L'],
-                                                              inp['C'],
-                                                              inp['tf'],
-                                                              inp['pA'],
-                                                              inp['pB'],
-                                                              inp['pC'],
-                                                              inp['dt_max'],
-                                                              inp['vcurve'],
-                                                              True,
-                                                              195.0,
-                                                              190.0)
+        Q, t, h2 = single_reservoir_pump_pipe_tank_ode_solution(
+            inp["h20"],
+            inp["D"],
+            inp["d"],
+            inp["L"],
+            inp["C"],
+            inp["tf"],
+            inp["pA"],
+            inp["pB"],
+            inp["pC"],
+            inp["dt_max"],
+            inp["vcurve"],
+            True,
+            195.0,
+            190.0,
+        )
 
         create_graph = False
         if create_graph:
-            self._create_graph(t,Q,h2,inp['dt_max_wntr'],results,"vcurve_and_controls")
-            
-        hR2 = self._calc_wntr_ode_R2(results[0],t, h2, 'head', 't1', 'node')
+            self._create_graph(
+                t, Q, h2, inp["dt_max_wntr"], results, "vcurve_and_controls"
+            )
 
-        self.assertLessEqual(0.9,hR2,msg="The WNTR numerical solution for tank head has R2" +
-            " value less than 0.9 w/r to solution of an exact differential equation.")
+        hR2 = self._calc_wntr_ode_R2(results[0], t, h2, "head", "t1", "node")
+
+        self.assertLessEqual(
+            0.9,
+            hR2,
+            msg="The WNTR numerical solution for tank head has R2"
+            + " value less than 0.9 w/r to solution of an exact differential equation.",
+        )
+
+        QR2 = self._calc_wntr_ode_R2(results[0], t, Q, "flowrate", "pump1", "link")
+        self.assertLessEqual(
+            0.05,
+            QR2,
+            msg="The WNTR numerical solution for flow rate has R2"
+            + " value less than 0.05 w/r to solution of an exact differential equation.",
+        )
 
-        QR2 = self._calc_wntr_ode_R2(results[0],t,Q,'flowrate','pump1','link')
-        self.assertLessEqual(0.05,QR2,msg="The WNTR numerical solution for flow rate has R2" +
-            " value less than 0.05 w/r to solution of an exact differential equation.")
-              
-    
     def _prepare_pump_and_vol_curve(self):
         """ change up the volume curve and make the pump curve nonlinear"""
-        if not 'vcurve' in self.ode_inp.keys():
-            self.ode_inp['vcurve'] = array(self._vol_curve1())
-            self.ode_inp['pC'] = 1.2
-            epoint = (self.ode_inp['pA']/self.ode_inp['pB'])**(1/self.ode_inp['pC'])
-            pcurve = [(0.0,self.ode_inp['pA']),
-                      (0.5*epoint,self.ode_inp['pA']-self.ode_inp['pB']*
-                       (0.5*epoint)**self.ode_inp['pC']),
-                      (epoint,0.0)]
+        if not "vcurve" in self.ode_inp.keys():
+            self.ode_inp["vcurve"] = array(self._vol_curve1())
+            self.ode_inp["pC"] = 1.2
+            epoint = (self.ode_inp["pA"] / self.ode_inp["pB"]) ** (
+                1 / self.ode_inp["pC"]
+            )
+            pcurve = [
+                (0.0, self.ode_inp["pA"]),
+                (
+                    0.5 * epoint,
+                    self.ode_inp["pA"]
+                    - self.ode_inp["pB"] * (0.5 * epoint) ** self.ode_inp["pC"],
+                ),
+                (epoint, 0.0),
+            ]
             # add the new curves to the model
-            self.wn.add_curve('pcurve','HEAD',pcurve)
-            self.wn.add_curve('vcurve','VOLUME',self.ode_inp['vcurve'])#[(x,x*pi*inp['D']**2/4.0) for x in range(54)])#
+            self.wn.add_curve("pcurve", "HEAD", pcurve)
+            self.wn.add_curve(
+                "vcurve", "VOLUME", self.ode_inp["vcurve"]
+            )  # [(x,x*pi*inp['D']**2/4.0) for x in range(54)])#
             # now change the model
-            t1 = self.wn.get_node('t1')
-            t1.vol_curve_name = 'vcurve'
-            pump1 = self.wn.get_link('pump1')
-            pump1.pump_curve_name = 'pcurve'
-    
+            t1 = self.wn.get_node("t1")
+            t1.vol_curve_name = "vcurve"
+            pump1 = self.wn.get_link("pump1")
+            pump1.pump_curve_name = "pcurve"
+
     def _delete_controls(self):
         try:
             self.wn.remove_control("pump_off_bypass_open")
@@ -245,63 +326,88 @@ def _delete_controls(self):
             self.wn.remove_link("bypass_pump1")
         except:
             pass
-        
-    def _run_study(self,wn,sim):
+
+    def _run_study(self, wn, sim):
         results = []
-        dt_max_wntr = self.ode_inp['dt_max_wntr']
+        dt_max_wntr = self.ode_inp["dt_max_wntr"]
         for dtmax in dt_max_wntr:
-            wn.options.time.hydraulic_timestep=dtmax
-            wn.options.time.report_timestep=dtmax
-            wn.options.time.rule_timestep=dtmax
+            wn.options.time.hydraulic_timestep = dtmax
+            wn.options.time.report_timestep = dtmax
+            wn.options.time.rule_timestep = dtmax
             result = sim.run_sim()
             results.append(result)
             wn.reset_initial_values()
         return results
-    
+
     def _vol_curve1(self):
         """volume curve that has the same volume as the 1meter diameter 
            cylindrical tank at d=50"""
-        return [(d,(pi/200.0)*d**2) 
-                                for d in arange(0,100)]
-    
-    def _calc_wntr_ode_R2(self,wntr_res,t,ode_res,var_str,name,attrname):
-        wntr_res_attr = getattr(wntr_res,attrname)
-        wntr_interp = interp(t,wntr_res_attr[var_str][name].index,
-                                   wntr_res_attr[var_str][name].values)
-        return self._R2(ode_res,wntr_interp)
-    
-    def _create_graph(self,t,Q,h2,dt_max_wntr,results,name_tag):
-        fig,axl = plt.subplots(2,1)
+        return [(d, (pi / 200.0) * d ** 2) for d in arange(0, 100)]
+
+    def _calc_wntr_ode_R2(self, wntr_res, t, ode_res, var_str, name, attrname):
+        wntr_res_attr = getattr(wntr_res, attrname)
+        wntr_interp = interp(
+            t, wntr_res_attr[var_str][name].index, wntr_res_attr[var_str][name].values
+        )
+        return self._R2(ode_res, wntr_interp)
+
+    def _create_graph(self, t, Q, h2, dt_max_wntr, results, name_tag):
+        fig, axl = plt.subplots(2, 1)
         fig.set_figheight(6)
         fig.set_figwidth(10)
-            
-        axl[0].set_title("Single pump with 200m maximum head capacity pumping to a tank with initial elevation head of 150m")
-        axl[0].plot(t,Q,label="ode",linewidth=3,linestyle="--")
-        for dt,result in zip(dt_max_wntr,results):
-            axl[0].plot(result.link['flowrate']['p1'],label="wntr dt={0:4.0f}".format(dt),drawstyle='steps-post')
+
+        axl[0].set_title(
+            "Single pump with 200m maximum head capacity pumping to a tank with initial elevation head of 150m"
+        )
+        axl[0].plot(t, Q, label="ode", linewidth=3, linestyle="--")
+        for dt, result in zip(dt_max_wntr, results):
+            axl[0].plot(
+                result.link["flowrate"]["p1"],
+                label="wntr dt={0:4.0f}".format(dt),
+                drawstyle="steps-post",
+            )
         axl[0].legend()
         axl[0].grid("on")
-        axl[0].set_ylabel('flow rate (m3/s)')
+        axl[0].set_ylabel("flow rate (m3/s)")
         # head plots
-        axl[1].plot(t,h2,label='ode',linewidth=3,linestyle="--")
-        for dt,result in zip(dt_max_wntr,results):
-            axl[1].plot(result.node['head']['t1'],label="wntr dt={0:4.0f}".format(dt),drawstyle='steps-post')
-        axl[1].set_xlabel('time (s)')
-        axl[1].set_ylabel('tank head (m)')
+        axl[1].plot(t, h2, label="ode", linewidth=3, linestyle="--")
+        for dt, result in zip(dt_max_wntr, results):
+            axl[1].plot(
+                result.node["head"]["t1"],
+                label="wntr dt={0:4.0f}".format(dt),
+                drawstyle="steps-post",
+            )
+        axl[1].set_xlabel("time (s)")
+        axl[1].set_ylabel("tank head (m)")
         axl[1].grid("on")
-        plt.savefig(join(testdir,"SinglePumpTankExampleWNTRvsODE_"+name_tag+".png"),dpi=300)
-                
-    def _R2(self,y,f):
+        plt.savefig(
+            join(testdir, "SinglePumpTankExampleWNTRvsODE_" + name_tag + ".png"),
+            dpi=300,
+        )
+
+    def _R2(self, y, f):
         y_bar = mean(y)
-        ss_tot = sum((y - y_bar)**2)
-        ss_res = sum((y - f)**2)
+        ss_tot = sum((y - y_bar) ** 2)
+        ss_res = sum((y - f) ** 2)
         return 1 - ss_res / ss_tot
 
-def single_reservoir_pump_pipe_tank_ode_solution(h20,D,d,L,C,tf,pA,pB,pC,
-                                                 dt_max,vcurve=None,
-                                                 include_pump_control=False,
-                                                 pump_off_level=0,
-                                                 pump_back_on_level=0):
+
+def single_reservoir_pump_pipe_tank_ode_solution(
+    h20,
+    D,
+    d,
+    L,
+    C,
+    tf,
+    pA,
+    pB,
+    pC,
+    dt_max,
+    vcurve=None,
+    include_pump_control=False,
+    pump_off_level=0,
+    pump_back_on_level=0,
+):
     """This produces a numerical solution to a water network that involves
        1 reservoir, 1 pump extracting water from the reservoir, a single pipe
        through which the water runs uphill with frictional losses, and a 
@@ -329,141 +435,193 @@ def single_reservoir_pump_pipe_tank_ode_solution(h20,D,d,L,C,tf,pA,pB,pC,
            h2  - Water tank surface elevation height 
        
        """
-    
-    def S_hazen_will_si(Q,C,d):
+
+    def S_hazen_will_si(Q, C, d):
         """ returns meters (water) per meter length of pipe"""
-        return 10.67 * (Q/C) ** 1.852 / d ** 4.8702
-    
-    def pump_func(Q,pA,pB,pC):
-        
+        return 10.67 * (Q / C) ** 1.852 / d ** 4.8702
+
+    def pump_func(Q, pA, pB, pC):
+
         return pA - pB * Q ** pC
-    
-    def energy_balance_time0(Q,h2,d,D,L,C,pA,pB,pC,vcurve,dh,h20):
+
+    def energy_balance_time0(Q, h2, d, D, L, C, pA, pB, pC, vcurve, dh, h20):
         """ Should balance to zero """
-        H2 = h2 # velocity head is not included here.?? Because everything is initially at rest + v_head(Q,D,h2,vcurve,dh,h20)
-        H1 = pump_func(Q,pA,pB,pC)
-        return H1 - H2 - L * S_hazen_will_si(Q,C,d)
-    
-    def energy_balance_no_pump(Q,h2,d,D,L,C,vcurve,dh,h20):
-        return (4.0*Q/(pi * d**2.0))**2/(2*9.81) - v_head(Q,d,h2,vcurve,dh,h20) - h2 + L * S_hazen_will_si(Q,C,d)
-    
-    def v_head(Q,diam,h2,vcurve,dh,h20):
-        dVdh2 = dVdh2_func(D,h2,vcurve,dh,h20)
-        return (Q/dVdh2)**2.0 / (2 * 9.81)
-    
-    def dVdh2_func(D,h2,vcurve,dh,h20):
+        H2 = h2  # velocity head is not included here.?? Because everything is initially at rest + v_head(Q,D,h2,vcurve,dh,h20)
+        H1 = pump_func(Q, pA, pB, pC)
+        return H1 - H2 - L * S_hazen_will_si(Q, C, d)
+
+    def energy_balance_no_pump(Q, h2, d, D, L, C, vcurve, dh, h20):
+        return (
+            (4.0 * Q / (pi * d ** 2.0)) ** 2 / (2 * 9.81)
+            - v_head(Q, d, h2, vcurve, dh, h20)
+            - h2
+            + L * S_hazen_will_si(Q, C, d)
+        )
+
+    def v_head(Q, diam, h2, vcurve, dh, h20):
+        dVdh2 = dVdh2_func(D, h2, vcurve, dh, h20)
+        return (Q / dVdh2) ** 2.0 / (2 * 9.81)
+
+    def dVdh2_func(D, h2, vcurve, dh, h20):
         if vcurve is None:
             dVdh2 = pi * D ** 2.0 / 4.0
         else:
-            V0 = interp(h2-h20,vcurve[:,0],vcurve[:,1])
-            V1 = interp(h2-h20 + dh,vcurve[:,0],vcurve[:,1])
-            dVdh2 = (V1 - V0)/dh
+            V0 = interp(h2 - h20, vcurve[:, 0], vcurve[:, 1])
+            V1 = interp(h2 - h20 + dh, vcurve[:, 0], vcurve[:, 1])
+            dVdh2 = (V1 - V0) / dh
         if dVdh2 == 0.0:
             raise ValueError("The depth-volume curve must not have a zero slope!")
         return dVdh2
-    
-    def diff_eq(t,x,d,D,C,L,pA,pB,pC,vcurve,dh,h20,pump_on):
+
+    def diff_eq(t, x, d, D, C, L, pA, pB, pC, vcurve, dh, h20, pump_on):
         # this can be derived from differentiating the energy balance and considering
         # the head gain integral of the tank becomes dh/dt = Q/A
         # the return is for dQ/dt and dh/dt
         Q = x[0]
         h2 = x[1]
-        
-            
 
-            
         if pump_on:
-            dVdh2 = dVdh2_func(D,h2,vcurve,dh,h20)
-            HZ_WIL = L * 1.852 * 10.67 * Q**0.852 / (C**1.852 * d**4.8702)
-            uu = ((Q/dVdh2) / (pB*pC * Q ** (pC-1.0) - (Q / (dVdh2)**2)/9.81 - 
-                   HZ_WIL), Q / dVdh2 )
+            dVdh2 = dVdh2_func(D, h2, vcurve, dh, h20)
+            HZ_WIL = L * 1.852 * 10.67 * Q ** 0.852 / (C ** 1.852 * d ** 4.8702)
+            uu = (
+                (Q / dVdh2)
+                / (pB * pC * Q ** (pC - 1.0) - (Q / (dVdh2) ** 2) / 9.81 - HZ_WIL),
+                Q / dVdh2,
+            )
         else:
-            dVdh2 = dVdh2_func(D,h2,vcurve,-dh,h20)
-            HZ_WIL = L * 1.852 * 10.67 * (-Q)**0.852 / (C**1.852 * d**4.8702)
-            uu = (9.81*dVdh2 / (1.0 - (9.81*dVdh2**2*(HZ_WIL)/Q)), Q / dVdh2)
-            
-        return uu[0], uu[1] 
-    
-    def choose_event(t,x,next_is_pump_on,event_1_threshold,event_2_threshold,include_pump_controls):
+            dVdh2 = dVdh2_func(D, h2, vcurve, -dh, h20)
+            HZ_WIL = L * 1.852 * 10.67 * (-Q) ** 0.852 / (C ** 1.852 * d ** 4.8702)
+            uu = (9.81 * dVdh2 / (1.0 - (9.81 * dVdh2 ** 2 * (HZ_WIL) / Q)), Q / dVdh2)
+
+        return uu[0], uu[1]
+
+    def choose_event(
+        t,
+        x,
+        next_is_pump_on,
+        event_1_threshold,
+        event_2_threshold,
+        include_pump_controls,
+    ):
         if include_pump_controls:
             if not next_is_pump_on:
-                return pump_off_if_above_thresh(t,x,event_1_threshold)
+                return pump_off_if_above_thresh(t, x, event_1_threshold)
             else:
-                return pump_on_if_below_thresh(t,x,event_2_threshold)
+                return pump_on_if_below_thresh(t, x, event_2_threshold)
         else:
             return 1.0
 
-    def pump_off_if_above_thresh(t, x,thresh):
+    def pump_off_if_above_thresh(t, x, thresh):
         h2 = x[1]
         if h2 < thresh:
             pump_on = 1.0
         else:
             pump_on = -1.0
         return pump_on
-    
-    def pump_on_if_below_thresh(t,x,thresh):
+
+    def pump_on_if_below_thresh(t, x, thresh):
         h2 = x[1]
         if h2 > thresh:
             pump_off = 1.0
         else:
             pump_off = -1.0
         return pump_off
-    
+
     if h20 > pA:
-        raise ValueError("The selected pump is incapable of pumping water into the tank!")
-    
+        raise ValueError(
+            "The selected pump is incapable of pumping water into the tank!"
+        )
+
     dh = 0.0000001
 
-    # first solve the initial state 
-    Q0_list = [exp(log((pA-h20)/pB)/pC)]  # this is the solution with zero friction losses
-    Q0 = fsolve(energy_balance_time0, Q0_list, args=(h20,d,D,L,C,pA,pB,pC,vcurve,dh,h20))
-    #Q0 = exp(log((pA-h20)/pB)/pC)
+    # first solve the initial state
+    Q0_list = [
+        exp(log((pA - h20) / pB) / pC)
+    ]  # this is the solution with zero friction losses
+    Q0 = fsolve(
+        energy_balance_time0,
+        Q0_list,
+        args=(h20, d, D, L, C, pA, pB, pC, vcurve, dh, h20),
+    )
+    # Q0 = exp(log((pA-h20)/pB)/pC)
     # make sure the solution is valid!
-    assert abs(energy_balance_time0(Q0,h20,d,D,L,C,pA,pB,pC,vcurve,dh,h20)) <= 0.00001
-    
+    assert (
+        abs(energy_balance_time0(Q0, h20, d, D, L, C, pA, pB, pC, vcurve, dh, h20))
+        <= 0.00001
+    )
+
     # now solve the differential equation
     pump_on = True
     sol = []
     tmax = 0.0
     next_is_1 = True
     h2_start = h20
-    while (tmax < tf):
-        event_func = lambda t,x:choose_event(t,x,not pump_on,pump_off_level,pump_back_on_level,include_pump_control)
+    while tmax < tf:
+        event_func = lambda t, x: choose_event(
+            t, x, not pump_on, pump_off_level, pump_back_on_level, include_pump_control
+        )
         event_func.terminal = True
-        temp_sol = solve_ivp(lambda t,x:diff_eq(t,x,d,D,C,L,pA,pB,pC,vcurve,dh,
-                                                 h20,pump_on),
-                    (tmax,tf),array([Q0,h2_start]),max_step=dt_max,
-                    
-                    events=event_func)
+        temp_sol = solve_ivp(
+            lambda t, x: diff_eq(
+                t, x, d, D, C, L, pA, pB, pC, vcurve, dh, h20, pump_on
+            ),
+            (tmax, tf),
+            array([Q0, h2_start]),
+            max_step=dt_max,
+            events=event_func,
+        )
         tmax = max(temp_sol.t)
         if tmax < tf:
             next_is_1 = not next_is_1
             pump_on = not pump_on
-            h2_start = temp_sol.y[1,-1]
+            h2_start = temp_sol.y[1, -1]
             Q0_list = [0.1]
             if pump_on:
-                Q0 = fsolve(energy_balance_time0, Q0_list, args=(h2_start,d,D,L,C,pA,pB,pC,vcurve,dh,h20))
-                assert abs(energy_balance_time0(Q0,h2_start,d,D,L,C,pA,pB,pC,vcurve,dh,h20)) <= 0.00001
+                Q0 = fsolve(
+                    energy_balance_time0,
+                    Q0_list,
+                    args=(h2_start, d, D, L, C, pA, pB, pC, vcurve, dh, h20),
+                )
+                assert (
+                    abs(
+                        energy_balance_time0(
+                            Q0, h2_start, d, D, L, C, pA, pB, pC, vcurve, dh, h20
+                        )
+                    )
+                    <= 0.00001
+                )
             else:
                 try:
-                    Q0 = fsolve(energy_balance_no_pump, Q0_list,args=(h2_start,d,D,L,C,vcurve,dh,h20) )
+                    Q0 = fsolve(
+                        energy_balance_no_pump,
+                        Q0_list,
+                        args=(h2_start, d, D, L, C, vcurve, dh, h20),
+                    )
                 except:
-                    raise RuntimeError("The nonlinear solver for no-pump conditions failed!")
-                assert abs(energy_balance_no_pump(Q0,h2_start,d,D,L,C,vcurve,dh,h20)) <= 0.00001
+                    raise RuntimeError(
+                        "The nonlinear solver for no-pump conditions failed!"
+                    )
+                assert (
+                    abs(
+                        energy_balance_no_pump(
+                            Q0, h2_start, d, D, L, C, vcurve, dh, h20
+                        )
+                    )
+                    <= 0.00001
+                )
                 Q0 = -Q0
-            
-            
+
         sol.append(temp_sol)
     t = array([])
     Q = array([])
     h2 = array([])
     for sl in sol:
-        t = concatenate((t,sl.t),axis=0)
-        Q = concatenate((Q,sl.y[0,:]))
-        h2 = concatenate((h2,sl.y[1,:]))
-    
-    return Q,t,h2
+        t = concatenate((t, sl.t), axis=0)
+        Q = concatenate((Q, sl.y[0, :]))
+        h2 = concatenate((h2, sl.y[1, :]))
+
+    return Q, t, h2
 
 
 if __name__ == "__main__":
-    unittest.main()
\ No newline at end of file
+    unittest.main(verbosity=2)
diff --git a/wntr/tests/test_sim_demand_multiplier.py b/wntr/tests/test_sim_demand_multiplier.py
index 8ff868968..f6a2cd1d1 100644
--- a/wntr/tests/test_sim_demand_multiplier.py
+++ b/wntr/tests/test_sim_demand_multiplier.py
@@ -1,72 +1,80 @@
-from nose.tools import *
+import unittest
+import warnings
 from os.path import abspath, dirname, join
+
 import wntr
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir,'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
-
-def test_demand_multiplier():
-    
-    node_name = '147'
-    time = 3600
-    
-    inp_file = join(ex_datadir, 'Net3.inp')
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    node = wn.get_node(node_name)
-    
-    # Demand multiplier = 1
-    wn.options.hydraulic.demand_multiplier = 1
-    sim = wntr.sim.EpanetSimulator(wn)
-    epanet_results1 = sim.run_sim()
-    sim = wntr.sim.WNTRSimulator(wn)
-    wntr_results1 = sim.run_sim()
-    
-    expected_demand1 = node.demand_timeseries_list.at(time)
-    epanet_actual_demand1 = epanet_results1.node['demand'].loc[time,node_name]
-    wntr_actual_demand1 = wntr_results1.node['demand'].loc[time,node_name]
-    
-    assert_greater(expected_demand1,0) # make sure expected demand is greater than 0
-                        
-    assert_almost_equal(expected_demand1, epanet_actual_demand1, 8)
-    assert_almost_equal(expected_demand1, wntr_actual_demand1, 8)
-    assert_almost_equal(epanet_actual_demand1, wntr_actual_demand1, 8)
-
-    # Increase demand multiplier to 1.5
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    wn.options.hydraulic.demand_multiplier = 1.5
-    sim = wntr.sim.EpanetSimulator(wn)
-    epanet_results2 = sim.run_sim()
-    sim = wntr.sim.WNTRSimulator(wn)
-    wntr_results2 = sim.run_sim()
-    
-    expected_demand2 = node.demand_timeseries_list.at(time, multiplier=1.5)
-    epanet_actual_demand2 = epanet_results2.node['demand'].loc[time,node_name]
-    wntr_actual_demand2 = wntr_results2.node['demand'].loc[time,node_name]
-    
-    assert_almost_equal(expected_demand1*1.5, epanet_actual_demand2, 8)
-    
-    assert_almost_equal(expected_demand2, epanet_actual_demand2, 8)
-    assert_almost_equal(expected_demand2, wntr_actual_demand2, 8)
-    assert_almost_equal(epanet_actual_demand2, wntr_actual_demand2, 8)
-    
-    # Decrease demand multiplier to 0.5
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    wn.options.hydraulic.demand_multiplier = 0.5
-    sim = wntr.sim.EpanetSimulator(wn)
-    epanet_results2 = sim.run_sim()
-    sim = wntr.sim.WNTRSimulator(wn)
-    wntr_results2 = sim.run_sim()
-    
-    expected_demand2 = node.demand_timeseries_list.at(time, multiplier=0.5)
-    epanet_actual_demand2 = epanet_results2.node['demand'].loc[time,node_name]
-    wntr_actual_demand2 = wntr_results2.node['demand'].loc[time,node_name]
-    
-    assert_almost_equal(expected_demand1*0.5, epanet_actual_demand2, 8)
-    
-    assert_almost_equal(expected_demand2, epanet_actual_demand2, 8)
-    assert_almost_equal(expected_demand2, wntr_actual_demand2, 8)
-    assert_almost_equal(epanet_actual_demand2, wntr_actual_demand2, 8)
-    
-if __name__ == '__main__':
-    test_demand_multiplier()
\ No newline at end of file
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
+
+
+
+class TestDemandMultiplier(unittest.TestCase):
+    def test_demand_multiplier(self):
+
+        node_name = "147"
+        time = 3600
+
+        inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        node = wn.get_node(node_name)
+
+        # Demand multiplier = 1
+        wn.options.hydraulic.demand_multiplier = 1
+        sim = wntr.sim.EpanetSimulator(wn)
+        epanet_results1 = sim.run_sim()
+        sim = wntr.sim.WNTRSimulator(wn)
+        wntr_results1 = sim.run_sim()
+
+        expected_demand1 = node.demand_timeseries_list.at(time)
+        epanet_actual_demand1 = epanet_results1.node["demand"].loc[time, node_name]
+        wntr_actual_demand1 = wntr_results1.node["demand"].loc[time, node_name]
+
+        self.assertGreater(
+            expected_demand1, 0
+        )  # make sure expected demand is greater than 0
+
+        self.assertAlmostEqual(expected_demand1, epanet_actual_demand1, 8)
+        self.assertAlmostEqual(expected_demand1, wntr_actual_demand1, 8)
+        self.assertAlmostEqual(epanet_actual_demand1, wntr_actual_demand1, 8)
+
+        # Increase demand multiplier to 1.5
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        wn.options.hydraulic.demand_multiplier = 1.5
+        sim = wntr.sim.EpanetSimulator(wn)
+        epanet_results2 = sim.run_sim()
+        sim = wntr.sim.WNTRSimulator(wn)
+        wntr_results2 = sim.run_sim()
+
+        expected_demand2 = node.demand_timeseries_list.at(time, multiplier=1.5)
+        epanet_actual_demand2 = epanet_results2.node["demand"].loc[time, node_name]
+        wntr_actual_demand2 = wntr_results2.node["demand"].loc[time, node_name]
+
+        self.assertAlmostEqual(expected_demand1 * 1.5, epanet_actual_demand2, 8)
+
+        self.assertAlmostEqual(expected_demand2, epanet_actual_demand2, 8)
+        self.assertAlmostEqual(expected_demand2, wntr_actual_demand2, 8)
+        self.assertAlmostEqual(epanet_actual_demand2, wntr_actual_demand2, 8)
+
+        # Decrease demand multiplier to 0.5
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        wn.options.hydraulic.demand_multiplier = 0.5
+        sim = wntr.sim.EpanetSimulator(wn)
+        epanet_results2 = sim.run_sim()
+        sim = wntr.sim.WNTRSimulator(wn)
+        wntr_results2 = sim.run_sim()
+
+        expected_demand2 = node.demand_timeseries_list.at(time, multiplier=0.5)
+        epanet_actual_demand2 = epanet_results2.node["demand"].loc[time, node_name]
+        wntr_actual_demand2 = wntr_results2.node["demand"].loc[time, node_name]
+
+        self.assertAlmostEqual(expected_demand1 * 0.5, epanet_actual_demand2, 8)
+
+        self.assertAlmostEqual(expected_demand2, epanet_actual_demand2, 8)
+        self.assertAlmostEqual(expected_demand2, wntr_actual_demand2, 8)
+        self.assertAlmostEqual(epanet_actual_demand2, wntr_actual_demand2, 8)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_sim_performance.py b/wntr/tests/test_sim_performance.py
index cc7f475b3..444e5a2c5 100644
--- a/wntr/tests/test_sim_performance.py
+++ b/wntr/tests/test_sim_performance.py
@@ -1,14 +1,15 @@
-import unittest
 import sys
+import unittest
 from os.path import abspath, dirname, join
+
 import pandas
 
-pandas.set_option('display.max_rows', 10000)
+pandas.set_option("display.max_rows", 10000)
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir,'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
-results_dir = join(testdir,'performance_results')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
+results_dir = join(testdir, "performance_results")
 
 
 def compare_results(wntr_res, epa_res, abs_threshold, rel_threshold):
@@ -24,47 +25,71 @@ def compare_results(wntr_res, epa_res, abs_threshold, rel_threshold):
 
 
 class TestPerformance(unittest.TestCase):
-
     @classmethod
     def setUpClass(self):
         sys.path.append(results_dir)
         import wntr
+
         self.wntr = wntr
 
     @classmethod
     def tearDownClass(self):
         pass
-    
+
     def test_Anytown_multipointcurves_performance(self):
         head_diff_abs_threshold = 1e-3
         demand_diff_abs_threshold = 1e-5
         flow_diff_abs_threshold = 1e-5
         rel_threshold = 1e-3
-        
-        inp_file = join(test_datadir, 'Anytown_multipointcurves.inp')
+
+        inp_file = join(test_datadir, "Anytown_multipointcurves.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
+        wn2 = self.wntr.network.WaterNetworkModel(inp_file)
 
-        # Apply a curve that is very fine along H = a - b * Q ** c and 
+        # Apply a curve that is very fine along H = a - b * Q ** c and
         # verify the answers are nearly identical under these conditions.
         A = 1.313e3
         B = 8.705e-6
         C = 1.796e0
-        new_curve_name = 'perfect_curve'
-        cdata = [(10*q,A - B*(10*q)**C) for q in range(1200)]
-        wn.add_curve(new_curve_name,'HEAD',cdata)
+        new_curve_name = "perfect_curve"
+        cdata = [(10 * q, A - B * (10 * q) ** C) for q in range(1200)]
+        wn.add_curve(new_curve_name, "HEAD", cdata)
+        wn2.add_curve(new_curve_name, "HEAD", cdata)
         for pump_name, pump in wn.pumps():
             pump.pump_curve_name = new_curve_name
-            
+        for pump_name, pump in wn2.pumps():
+            pump.pump_curve_name = new_curve_name
+
         epa_sim = self.wntr.sim.EpanetSimulator(wn)
         epa_res = epa_sim.run_sim()
 
-        sim = self.wntr.sim.WNTRSimulator(wn)
+        sim = self.wntr.sim.WNTRSimulator(wn2)
         results = sim.run_sim()
 
-        self.assertTrue(compare_results(results.node['head'], epa_res.node['head'], head_diff_abs_threshold, rel_threshold))
-        self.assertTrue(compare_results(results.node['demand'].loc[:, wn.tank_name_list], epa_res.node['demand'].loc[:, wn.tank_name_list], demand_diff_abs_threshold, rel_threshold))
-        self.assertTrue(compare_results(results.link['flowrate'], epa_res.link['flowrate'], flow_diff_abs_threshold, rel_threshold))
-        
+        self.assertTrue(
+            compare_results(
+                results.node["head"],
+                epa_res.node["head"],
+                head_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
+        self.assertTrue(
+            compare_results(
+                results.node["demand"].loc[:, wn.tank_name_list],
+                epa_res.node["demand"].loc[:, wn.tank_name_list],
+                demand_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
+        self.assertTrue(
+            compare_results(
+                results.link["flowrate"],
+                epa_res.link["flowrate"],
+                flow_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
 
     def test_Net1_performance(self):
         head_diff_abs_threshold = 1e-3
@@ -72,7 +97,7 @@ def test_Net1_performance(self):
         flow_diff_abs_threshold = 1e-5
         rel_threshold = 1e-3
 
-        inp_file = join(ex_datadir, 'Net1.inp')
+        inp_file = join(ex_datadir, "Net1.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
 
         epa_sim = self.wntr.sim.EpanetSimulator(wn)
@@ -81,9 +106,30 @@ def test_Net1_performance(self):
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
-        self.assertTrue(compare_results(results.node['head'], epa_res.node['head'], head_diff_abs_threshold, rel_threshold))
-        self.assertTrue(compare_results(results.node['demand'].loc[:, wn.tank_name_list], epa_res.node['demand'].loc[:, wn.tank_name_list], demand_diff_abs_threshold, rel_threshold))
-        self.assertTrue(compare_results(results.link['flowrate'], epa_res.link['flowrate'], flow_diff_abs_threshold, rel_threshold))
+        self.assertTrue(
+            compare_results(
+                results.node["head"],
+                epa_res.node["head"],
+                head_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
+        self.assertTrue(
+            compare_results(
+                results.node["demand"].loc[:, wn.tank_name_list],
+                epa_res.node["demand"].loc[:, wn.tank_name_list],
+                demand_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
+        self.assertTrue(
+            compare_results(
+                results.link["flowrate"],
+                epa_res.link["flowrate"],
+                flow_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
 
     def test_Net3_performance(self):
         head_diff_abs_threshold = 1e-3
@@ -91,7 +137,7 @@ def test_Net3_performance(self):
         flow_diff_abs_threshold = 1e-5
         rel_threshold = 1e-3
 
-        inp_file = join(ex_datadir, 'Net3.inp')
+        inp_file = join(ex_datadir, "Net3.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
 
         epa_sim = self.wntr.sim.EpanetSimulator(wn)
@@ -100,9 +146,30 @@ def test_Net3_performance(self):
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
-        self.assertTrue(compare_results(results.node['head'], epa_res.node['head'], head_diff_abs_threshold, rel_threshold))
-        self.assertTrue(compare_results(results.node['demand'].loc[:, wn.tank_name_list], epa_res.node['demand'].loc[:, wn.tank_name_list], demand_diff_abs_threshold, rel_threshold))
-        self.assertTrue(compare_results(results.link['flowrate'], epa_res.link['flowrate'], flow_diff_abs_threshold, rel_threshold))
+        self.assertTrue(
+            compare_results(
+                results.node["head"],
+                epa_res.node["head"],
+                head_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
+        self.assertTrue(
+            compare_results(
+                results.node["demand"].loc[:, wn.tank_name_list],
+                epa_res.node["demand"].loc[:, wn.tank_name_list],
+                demand_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
+        self.assertTrue(
+            compare_results(
+                results.link["flowrate"],
+                epa_res.link["flowrate"],
+                flow_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
 
     def test_Net3_performance_PDA(self):
         head_diff_abs_threshold = 1e-3
@@ -110,20 +177,41 @@ def test_Net3_performance_PDA(self):
         flow_diff_abs_threshold = 1e-5
         rel_threshold = 1e-3
 
-        inp_file = join(ex_datadir, 'Net3.inp')
+        inp_file = join(ex_datadir, "Net3.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
 
         epa_sim = self.wntr.sim.EpanetSimulator(wn)
         epa_res = epa_sim.run_sim(version=2.2)
 
-        wn.options.hydraulic.demand_model = 'PDA'
+        wn.options.hydraulic.demand_model = "PDA"
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
-        self.assertTrue(compare_results(results.node['head'], epa_res.node['head'], head_diff_abs_threshold, rel_threshold))
-        self.assertTrue(compare_results(results.node['demand'].loc[:, wn.tank_name_list], epa_res.node['demand'].loc[:, wn.tank_name_list], demand_diff_abs_threshold, rel_threshold))
-        self.assertTrue(compare_results(results.link['flowrate'], epa_res.link['flowrate'], flow_diff_abs_threshold, rel_threshold))
+        self.assertTrue(
+            compare_results(
+                results.node["head"],
+                epa_res.node["head"],
+                head_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
+        self.assertTrue(
+            compare_results(
+                results.node["demand"].loc[:, wn.tank_name_list],
+                epa_res.node["demand"].loc[:, wn.tank_name_list],
+                demand_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
+        self.assertTrue(
+            compare_results(
+                results.link["flowrate"],
+                epa_res.link["flowrate"],
+                flow_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
 
     def test_Net6_mod_performance(self):
         head_diff_abs_threshold = 1e-3
@@ -131,12 +219,14 @@ def test_Net6_mod_performance(self):
         flow_diff_abs_threshold = 1e-5
         rel_threshold = 1e-3
 
-        inp_file = join(ex_datadir,'Net6.inp')
+        inp_file = join(ex_datadir, "Net6.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
-        wn.options.time.duration = 24*3600
+        wn.options.time.duration = 24 * 3600
         wn.options.time.hydraulic_timestep = 3600
         wn.options.time.report_timestep = 3600
-        wn.remove_control('control 72')  # this control never gets activated in epanet because it uses a threshold equal to the tank max level
+        wn.remove_control(
+            "control 72"
+        )  # this control never gets activated in epanet because it uses a threshold equal to the tank max level
 
         epa_sim = self.wntr.sim.EpanetSimulator(wn)
         epa_res = epa_sim.run_sim()
@@ -144,10 +234,17 @@ def test_Net6_mod_performance(self):
         sim = self.wntr.sim.WNTRSimulator(wn)
         results = sim.run_sim()
 
-        self.assertTrue(compare_results(results.node['head'], epa_res.node['head'], head_diff_abs_threshold, rel_threshold))
+        self.assertTrue(
+            compare_results(
+                results.node["head"],
+                epa_res.node["head"],
+                head_diff_abs_threshold,
+                rel_threshold,
+            )
+        )
         # self.assertTrue(compare_results(results.node['demand'].loc[:, wn.tank_name_list], epa_res.node['demand'].loc[:, wn.tank_name_list], demand_diff_abs_threshold, rel_threshold))
         # self.assertTrue(compare_results(results.link['flowrate'], epa_res.link['flowrate'], flow_diff_abs_threshold, rel_threshold))
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
diff --git a/wntr/tests/test_sim_reset_conditions.py b/wntr/tests/test_sim_reset_conditions.py
new file mode 100644
index 000000000..bbe9fbb58
--- /dev/null
+++ b/wntr/tests/test_sim_reset_conditions.py
@@ -0,0 +1,145 @@
+import unittest
+import warnings
+from os.path import abspath, dirname, join
+
+import matplotlib.pylab as plt
+import wntr
+
+testdir = dirname(abspath(str(__file__)))
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
+
+### These tests need to be updated to be real tests, not graphics and use additional networks
+
+
+class Test_Reset_Conditions(unittest.TestCase):
+    @classmethod
+    def setUpClass(self):
+
+        self.inp_file = join(ex_datadir, "Net3.inp")
+        wn = wntr.network.WaterNetworkModel(self.inp_file)
+
+        wn.options.time.duration = 100 * 3600
+        sim = wntr.sim.EpanetSimulator(wn)
+        self.benchmarck_results = sim.run_sim()
+
+        plt.figure()
+        ax = plt.gca()
+        self.benchmarck_results.node["pressure"].plot(ax=ax, title="Benchmark")
+
+    @classmethod
+    def tearDownClass(self):
+        pass
+
+    def test_epanetsim_continue(self):
+
+        plt.figure()
+        ax = plt.gca()
+
+        wn = wntr.network.WaterNetworkModel(self.inp_file)
+
+        sim = wntr.sim.EpanetSimulator(wn)
+
+        wn.options.time.duration = 12 * 3600
+        results1 = sim.run_sim()
+        results1.node["pressure"].plot(ax=ax)
+
+        wn.options.time.duration = 56 * 3600
+        results2 = sim.run_sim()
+        results2.node["pressure"].plot(ax=ax)
+
+        wn.options.time.duration = 100 * 3600
+        results3 = sim.run_sim()
+        results3.node["pressure"].plot(ax=ax, title="EPANETsim_continue")
+
+    def test_wntrsim_continue(self):
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+
+            plt.figure()
+            ax = plt.gca()
+
+            wn = wntr.network.WaterNetworkModel(self.inp_file)
+
+            sim = wntr.sim.WNTRSimulator(wn)
+
+            wn.options.time.duration = 12 * 3600
+            results1 = sim.run_sim()
+            results1.node["pressure"].plot(ax=ax)
+
+            wn.options.time.duration = 56 * 3600
+            results2 = sim.run_sim()
+            results2.node["pressure"].plot(ax=ax)
+
+            wn.options.time.duration = 100 * 3600
+            wn.write_inpfile("Net3_epanet.inp")
+
+            results3 = sim.run_sim()
+            results3.node["pressure"].plot(ax=ax, title="WNTRSim_continue")
+
+    def test_epanetsim_reset(self):
+        plt.figure()
+        ax = plt.gca()
+
+        wn = wntr.network.WaterNetworkModel(self.inp_file)
+
+        sim = wntr.sim.EpanetSimulator(wn)
+
+        wn.options.time.duration = 100 * 3600
+        results1 = sim.run_sim()
+        results1.node["pressure"].plot(ax=ax, title="EPANETsim_reset1")
+
+        plt.figure()
+        ax = plt.gca()
+
+        wn.options.time.duration = 100 * 3600
+        results2 = sim.run_sim()
+        results2.node["pressure"].plot(ax=ax, title="EPANETsim_reset2")
+
+    def test_wntrsim_reset(self):
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+
+            plt.figure()
+            ax = plt.gca()
+
+            wn = wntr.network.WaterNetworkModel(self.inp_file)
+
+            sim = wntr.sim.WNTRSimulator(wn)
+
+            wn.options.time.duration = 100 * 3600
+            results1 = sim.run_sim()
+            results1.node["pressure"].plot(ax=ax, title="WNTRsim_reset1")
+
+            plt.figure()
+            ax = plt.gca()
+
+            wn.options.time.duration = 100 * 3600
+            results2 = sim.run_sim()
+            results2.node["pressure"].plot(ax=ax, title="WNTRsim_reset2")
+
+    # def test_both_continue(self):
+
+    #     plt.figure()
+    #     ax = plt.gca()
+
+    #     wn = wntr.network.WaterNetworkModel(self.inp_file)
+
+    #     sim = wntr.sim.WNTRSimulator(wn)
+    #     wn.options.time.duration = 12*3600
+    #     results1 = sim.run_sim()
+    #     results1.node['pressure'].plot(ax=ax)
+
+    #     sim = wntr.sim.EpanetSimulator(wn)
+    #     wn.options.time.duration = 56*3600
+    #     results2 = sim.run_sim()
+    #     results2.node['pressure'].plot(ax=ax)
+
+    #     sim = wntr.sim.WNTRSimulator(wn)
+    #     wn.options.time.duration = 100*3600
+    #     results3 = sim.run_sim()
+    #     results3.node['pressure'].plot(ax=ax, title='Switch_combo')
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/wntr/tests/test_sim_results.py b/wntr/tests/test_sim_results.py
new file mode 100644
index 000000000..115e9db76
--- /dev/null
+++ b/wntr/tests/test_sim_results.py
@@ -0,0 +1,52 @@
+import unittest
+from os.path import abspath, dirname, join
+#import matplotlib.pylab as plt
+import wntr
+
+testdir = dirname(abspath(str(__file__)))
+datadir = join(testdir, "..", "..", "examples", "networks")
+
+class TestSimulationResults(unittest.TestCase):
+    
+    @classmethod
+    def setUpClass(self):
+        inp_file = join(datadir, "Net2.inp")
+        self.wn = wntr.network.WaterNetworkModel(inp_file)
+        sim = wntr.sim.EpanetSimulator(self.wn)
+        sim.run_sim()
+        
+        binfile = wntr.epanet.io.BinFile()
+        results_GPM = binfile.read('temp.bin', False, False, False)
+        # time index needs to be converted to hours
+        for key in results_GPM.node.keys():
+            results_GPM.node[key].index = results_GPM.node[key].index/3600
+        for key in results_GPM.link.keys():
+            results_GPM.link[key].index = results_GPM.link[key].index/3600
+
+        self.results_GPM = results_GPM
+        
+        self.tol = {}
+        self.tol['node'] = {}
+        self.tol['node']['demand'] = 1e-3
+        self.tol['node']['head'] = 1e-3
+        self.tol['node']['pressure'] = 1e-3
+        self.tol['node']['quality'] = 1e-3
+        self.tol['link'] = {}
+        self.tol['link']['flowrate'] = 1e-2
+        self.tol['link']['velocity'] = 1e-3
+        self.tol['link']['headloss'] = 1e-3
+        self.tol['link']['status'] = None # status is not checked.
+        self.tol['link']['setting'] = 1e-5
+        self.tol['link']['friction_factor'] = 1e-5
+        self.tol['link']['quality'] = 1e-5
+        self.tol['link']['reaction_rate'] = 1e-3
+
+    @classmethod
+    def tearDownClass(self):
+        pass
+ 
+
+
+if __name__ == "__main__":
+    unittest.main()
+
diff --git a/wntr/tests/test_sim_waterquality.py b/wntr/tests/test_sim_waterquality.py
index 3b566708c..befa3588b 100644
--- a/wntr/tests/test_sim_waterquality.py
+++ b/wntr/tests/test_sim_waterquality.py
@@ -1,9 +1,10 @@
-from nose.tools import *
+import unittest
 from os.path import abspath, dirname, join
+
 import wntr
 
 testdir = dirname(abspath(str(__file__)))
-datadir = join(testdir,'..','..','examples','networks')
+datadir = join(testdir, "..", "..", "examples", "networks")
 
 """
 Compare results to EPANET GUI using Net3 and Source Quality at
@@ -13,113 +14,138 @@
 River        CONCEN        100000   mg/L
 """
 
-def test_setpoint_waterquality_simulation():
-    inp_file = join(datadir,'Net3.inp')
-
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    wn.options.quality.parameter = 'CHEMICAL'
-    wn.add_pattern('NewPattern', [1]) #start_time=0, end_time=wn.options.time.duration)
-    wn.add_source('Source1', '121', 'SETPOINT', 100, 'NewPattern')
-    #WQ = wntr.scenario.Waterquality('CHEM', ['121'], 'SETPOINT', 100, 0, -1)
-
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim(version=2.0)
-
-    expected = 91661.72*(1e-6/0.001) # Node '159' at hour 6
-    #print(results.node)
-    error = abs((results.node['quality'].loc[6*3600, '159'] - expected)/expected)
-    assert_less(error, 0.0001) # 0.01% error
-
-def test_flowpaced_waterquality_simulation():
-    inp_file = join(datadir,'Net3.inp')
-
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    wn.options.quality.parameter = 'CHEMICAL'
-    wn.add_pattern('NewPattern', [1]) #start_time=0, end_time=wn.options.time.duration)
-    wn.add_source('Source1', '121', 'FLOWPACED', 100, 'NewPattern')
-    #WQ = wntr.scenario.Waterquality('CHEM', ['121'], 'FLOWPACED', 100, 0, -1)
-
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim(version=2.0)
-
-    expected = 92246.55*(1e-6/0.001) # Node '159' at hour 6
-    error = abs((results.node['quality'].loc[6*3600, '159'] - expected)/expected)
-    assert_less(error, 0.0001) # 0.01% error
-
-def test_mass_waterquality_simulation():
-    inp_file = join(datadir,'Net3.inp')
-
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    wn.options.quality.parameter = 'CHEMICAL'
-    wn.add_pattern('NewPattern', [1.0] )# start_time=0, end_time=wn.options.time.duration)
-    wn.add_source('Source1', '121', 'MASS', 100, 'NewPattern')
-    #WQ = wntr.scenario.Waterquality('CHEM', ['121'], 'MASS', 100, 0, -1)
-
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim(version=2.0)
-
-    expected = 217903.60*(1e-6/0.001) # Node '159' at hour 6
-    error = abs((results.node['quality'].loc[6*3600, '159'] - expected)/expected)
-    assert_less(error, 0.0001) # 0.01% error
-
-def test_conc_waterquality_simulation():
-    inp_file = join(datadir,'Net3.inp')
-
-    wn = wntr.network.WaterNetworkModel(inp_file)
-
-    wn.options.quality.parameter = 'CHEMICAL'
-    wn.add_pattern('NewPattern', [1.0])# start_time=0, end_time=wn.options.time.duration)
-    wn.add_source('Source1', 'River', 'CONCEN', 100, 'NewPattern')
-    #WQ = wntr.scenario.Waterquality('CHEM', ['River'], 'CONCEN', 100, 0, -1)
-
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim(version=2.0)
-
-    expected = 91661.72*(1e-6/0.001) # Node '159' at hour 6
-    error = abs((results.node['quality'].loc[6*3600, '159'] - expected)/expected)
-    assert_less(error, 0.0001) # 0.01% error
-
-def test_age_waterquality_simulation():
-
-    inp_file = join(datadir,'Net3.inp')
-
-    wn = wntr.network.WaterNetworkModel(inp_file)
 
-    wn.options.quality.parameter = 'AGE'
-    #WQ = wntr.scenario.Waterquality('AGE')
+class TestWaterQualitySimulations(unittest.TestCase):
+    def test_setpoint_waterquality_simulation(self):
+        inp_file = join(datadir, "Net3.inp")
 
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim(version=2.0)
+        wn = wntr.network.WaterNetworkModel(inp_file)
 
-    # WARNING: This does NOT match the EPANET Windows results - it does match
-    # the epanet linux binary
-    expected = 3.652*3600 # Node '159' at hour 6
-    error = abs((results.node['quality'].loc[6*3600, '159'] - expected)/expected)
-    #print([expected, results.node['quality'].loc[6*3600, '159']])
-    assert_less(error, 0.001) # 0.01% error
+        wn.options.quality.parameter = "CHEMICAL"
+        wn.add_pattern(
+            "NewPattern", [1]
+        )  # start_time=0, end_time=wn.options.time.duration)
+        wn.add_source("Source1", "121", "SETPOINT", 100, "NewPattern")
+        # WQ = wntr.scenario.Waterquality('CHEM', ['121'], 'SETPOINT', 100, 0, -1)
 
-def test_trace_waterquality_simulation():
-    inp_file = join(datadir,'Net3.inp')
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim(version=2.0)
 
-    wn = wntr.network.WaterNetworkModel(inp_file)
-    wn.options.quality.parameter = 'TRACE'
-    wn.options.quality.trace_node = '121'
-    #WQ = wntr.scenario.Waterquality('TRACE', ['121'])
+        expected = 91661.72 * (1e-6 / 0.001)  # Node '159' at hour 6
+        # print(results.node)
+        error = abs(
+            (results.node["quality"].loc[6 * 3600, "159"] - expected) / expected
+        )
+        self.assertLess(error, 0.0001)  # 0.01% error
+
+    def test_flowpaced_waterquality_simulation(self):
+        inp_file = join(datadir, "Net3.inp")
 
-    sim = wntr.sim.EpanetSimulator(wn)
-    results = sim.run_sim(version=2.0)
-    #print(results.node.keys())
-    expected = 91.66 # Node '159' at hour 6
-    error = abs(float(results.node['quality'].loc[6*3600, '159'] - expected)/float(expected))
-    assert_less(error, 0.0001) # 0.01% error
+        wn = wntr.network.WaterNetworkModel(inp_file)
 
-if __name__ == '__main__':
-    test_setpoint_waterquality_simulation()
-    #test_flowpaced_waterquality_simulation()
-    #test_mass_waterquality_simulation()
-    #test_conc_waterquality_simulation()
-    #test_age_waterquality_simulation()
-    #test_trace_waterquality_simulation()
+        wn.options.quality.parameter = "CHEMICAL"
+        wn.add_pattern(
+            "NewPattern", [1]
+        )  # start_time=0, end_time=wn.options.time.duration)
+        wn.add_source("Source1", "121", "FLOWPACED", 100, "NewPattern")
+        # WQ = wntr.scenario.Waterquality('CHEM', ['121'], 'FLOWPACED', 100, 0, -1)
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim(version=2.0)
+
+        expected = 92246.55 * (1e-6 / 0.001)  # Node '159' at hour 6
+        error = abs(
+            (results.node["quality"].loc[6 * 3600, "159"] - expected) / expected
+        )
+        self.assertLess(error, 0.0001)  # 0.01% error
+
+    def test_mass_waterquality_simulation(self):
+        inp_file = join(datadir, "Net3.inp")
+
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        wn.options.quality.parameter = "CHEMICAL"
+        wn.add_pattern(
+            "NewPattern", [1.0]
+        )  # start_time=0, end_time=wn.options.time.duration)
+        wn.add_source("Source1", "121", "MASS", 100, "NewPattern")
+        # WQ = wntr.scenario.Waterquality('CHEM', ['121'], 'MASS', 100, 0, -1)
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim(version=2.0)
+
+        expected = 217903.60 * (1e-6 / 0.001)  # Node '159' at hour 6
+        error = abs(
+            (results.node["quality"].loc[6 * 3600, "159"] - expected) / expected
+        )
+        self.assertLess(error, 0.0001)  # 0.01% error
+
+    def test_conc_waterquality_simulation(self):
+        inp_file = join(datadir, "Net3.inp")
+
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        wn.options.quality.parameter = "CHEMICAL"
+        wn.add_pattern(
+            "NewPattern", [1.0]
+        )  # start_time=0, end_time=wn.options.time.duration)
+        wn.add_source("Source1", "River", "CONCEN", 100, "NewPattern")
+        # WQ = wntr.scenario.Waterquality('CHEM', ['River'], 'CONCEN', 100, 0, -1)
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim(version=2.0)
+
+        expected = 91661.72 * (1e-6 / 0.001)  # Node '159' at hour 6
+        error = abs(
+            (results.node["quality"].loc[6 * 3600, "159"] - expected) / expected
+        )
+        self.assertLess(error, 0.0001)  # 0.01% error
+
+    def test_age_waterquality_simulation(self):
+
+        inp_file = join(datadir, "Net3.inp")
+
+        wn = wntr.network.WaterNetworkModel(inp_file)
+
+        wn.options.quality.parameter = "AGE"
+        # WQ = wntr.scenario.Waterquality('AGE')
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim(version=2.0)
+
+        # WARNING: This does NOT match the EPANET Windows results - it does match
+        # the epanet linux binary
+        expected = 3.652 * 3600  # Node '159' at hour 6
+        error = abs(
+            (results.node["quality"].loc[6 * 3600, "159"] - expected) / expected
+        )
+        # print([expected, results.node['quality'].loc[6*3600, '159']])
+        self.assertLess(error, 0.001)  # 0.01% error
+
+    def test_trace_waterquality_simulation(self):
+        inp_file = join(datadir, "Net3.inp")
+
+        wn = wntr.network.WaterNetworkModel(inp_file)
+        wn.options.quality.parameter = "TRACE"
+        wn.options.quality.trace_node = "121"
+        # WQ = wntr.scenario.Waterquality('TRACE', ['121'])
+
+        sim = wntr.sim.EpanetSimulator(wn)
+        results = sim.run_sim(version=2.0)
+        # print(results.node.keys())
+        expected = 91.66  # Node '159' at hour 6
+        error = abs(
+            float(results.node["quality"].loc[6 * 3600, "159"] - expected)
+            / float(expected)
+        )
+        self.assertLess(error, 0.0001)  # 0.01% error
+
+
+if __name__ == "__main__":
+    unittest.main()
+    # test_setpoint_waterquality_simulation()
+    # test_flowpaced_waterquality_simulation()
+    # test_mass_waterquality_simulation()
+    # test_conc_waterquality_simulation()
+    # test_age_waterquality_simulation()
+    # test_trace_waterquality_simulation()
diff --git a/wntr/tests/test_times.py b/wntr/tests/test_times.py
index 42de2c67e..331ddc898 100644
--- a/wntr/tests/test_times.py
+++ b/wntr/tests/test_times.py
@@ -2,10 +2,10 @@
 from os.path import abspath, dirname, join
 
 testdir = dirname(abspath(str(__file__)))
-test_datadir = join(testdir,'networks_for_testing')
-ex_datadir = join(testdir,'..','..','examples','networks')
+test_datadir = join(testdir, "networks_for_testing")
+ex_datadir = join(testdir, "..", "..", "examples", "networks")
 
-#class TestNetworkTimeWarnings(unittest.TestCase):
+# class TestNetworkTimeWarnings(unittest.TestCase):
 #
 #    @classmethod
 #    def setUpClass(self):
@@ -20,7 +20,7 @@
 #        inp_file = join(test_datadir, 'net_test_8.inp')
 #        wn = self.wntr.network.WaterNetworkModel()
 #        parser = self.wntr.network.ParseWaterNetwork()
-#        
+#
 #        flag = False
 #        with warnings.catch_warnings(record=True) as w:
 #            warnings.simplefilter("always")
@@ -48,7 +48,7 @@
 #        inp_file = join(test_datadir, 'net_test_10.inp')
 #        wn = self.wntr.network.WaterNetworkModel()
 #        parser = self.wntr.network.ParseWaterNetwork()
-#        
+#
 #        flag = False
 #        with warnings.catch_warnings(record=True) as w:
 #            warnings.simplefilter("always")
@@ -62,7 +62,7 @@
 #        inp_file = join(test_datadir, 'net_test_11.inp')
 #        wn = self.wntr.network.WaterNetworkModel()
 #        parser = self.wntr.network.ParseWaterNetwork()
-#        
+#
 #        flag = False
 #        with warnings.catch_warnings(record=True) as w:
 #            warnings.simplefilter("always")
@@ -72,14 +72,15 @@
 #                flag = True
 #        self.assertEqual(flag, True)
 
-class TestNetworkTimeBehavior(unittest.TestCase):
 
+class TestNetworkTimeBehavior(unittest.TestCase):
     @classmethod
     def setUpClass(self):
         import wntr
+
         self.wntr = wntr
 
-        inp_file = join(test_datadir, 'times.inp')
+        inp_file = join(test_datadir, "times.inp")
         wn = self.wntr.network.WaterNetworkModel(inp_file)
         sim = self.wntr.sim.WNTRSimulator(wn)
         self.results = sim.run_sim()
@@ -90,26 +91,38 @@ def tearDownClass(self):
 
     def test_duration(self):
         results = self.results
-        self.assertEqual(len(results.node['head'].index), 26)
-        self.assertEqual(results.node['head'].index[25], 24*3600+3*3600+5*60)
+        self.assertEqual(len(results.node["head"].index), 26)
+        self.assertEqual(results.node["head"].index[25], 24 * 3600 + 3 * 3600 + 5 * 60)
 
     def test_report_timestep(self):
         results = self.results
-        self.assertEqual((results.node['head'].index[1] - results.node['head'].index[0]), 1*3600+5*60)
-        
+        self.assertEqual(
+            (results.node["head"].index[1] - results.node["head"].index[0]),
+            1 * 3600 + 5 * 60,
+        )
+
     def test_pattern_timestep(self):
         results = self.results
-        for t in results.node['demand'].index:
-            self.assertEqual(results.node['demand'].at[t, 'junction1'], 1.0)
+        for t in results.node["demand"].index:
+            self.assertEqual(results.node["demand"].at[t, "junction1"], 1.0)
             total_seconds = t
-            if (total_seconds/3900.0)%8 == 0.0 or ((total_seconds/3900.0)-1)%8 == 0.0:
-                self.assertEqual(results.node['demand'].at[t, 'junction2'], 0.5)
-            elif (total_seconds/3900.0)%8 == 2.0 or ((total_seconds/3900.0)-1)%8 == 2.0:
-                self.assertEqual(results.node['demand'].at[t, 'junction2'], 1.0)
-            elif (total_seconds/3900.0)%8 == 4.0 or ((total_seconds/3900.0)-1)%8 == 4.0:
-                self.assertEqual(results.node['demand'].at[t, 'junction2'], 1.5)
-            elif (total_seconds/3900.0)%8 == 6.0 or ((total_seconds/3900.0)-1)%8 == 6.0:
-                self.assertEqual(results.node['demand'].at[t, 'junction2'], 1.0)
+            if (total_seconds / 3900.0) % 8 == 0.0 or (
+                (total_seconds / 3900.0) - 1
+            ) % 8 == 0.0:
+                self.assertEqual(results.node["demand"].at[t, "junction2"], 0.5)
+            elif (total_seconds / 3900.0) % 8 == 2.0 or (
+                (total_seconds / 3900.0) - 1
+            ) % 8 == 2.0:
+                self.assertEqual(results.node["demand"].at[t, "junction2"], 1.0)
+            elif (total_seconds / 3900.0) % 8 == 4.0 or (
+                (total_seconds / 3900.0) - 1
+            ) % 8 == 4.0:
+                self.assertEqual(results.node["demand"].at[t, "junction2"], 1.5)
+            elif (total_seconds / 3900.0) % 8 == 6.0 or (
+                (total_seconds / 3900.0) - 1
+            ) % 8 == 6.0:
+                self.assertEqual(results.node["demand"].at[t, "junction2"], 1.0)
+
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()