Repository: pnnl/tesp (language: python)

Issue:
FY20 Release



Discussion / hints:

Fix patch:
diff --git a/src/comms_metrics/README.md b/src/comms_metrics/README.md
new file mode 100755
index 00000000..261735f2
--- /dev/null
+++ b/src/comms_metrics/README.md
@@ -0,0 +1,1222 @@
+
+# Introduction
+
+Welcome!
+
+As the use of wirelessly connected devices continues to rise, the need to evaluate the communication quality of these devices rises also. In particular, power distribution systems are converting to a wirelessly connected system to become more efficient. 
+
+We have limited access to the raw power distribution systems data. So, we use models to represent these systems. However, we needed a way to evaluate how well our models match the real-world data we *do* have. Thus, we have created `validation`. `validation` is a Python package that evaluates how well meters within power distribution system models can communicate with each other.
+
+Wirelessly connected power distribution meters generally have a maximum range of communication they can cover. So, the metrics we've developed calculate things like, how many meters are within a certain radius of each other, how many meters are isolated, among other things.
+
+## Installation
+This package is not currently deployed for public use. For now, clone this repository and explore the modules using your desired Python IDE. Once this package is deployed for public use, type `pip install validation` into your command prompt and hit `enter` (or `return`). From there, you will be able to import the package like normal.
+
+## Details
+
+As mentioned above, `validation` is a Python package. Not only can it perform metric calculations, it also has the ability to plot the models as a network and parse different file formats as pre-processing for the data analysis. Some required tools to be able to do these things are as follows:
+* [pandas](https://pandas.pydata.org/docs/)
+* numpy
+* xlrd
+* openpyxl
+* xarray\*
+* datashader\*
+* [holoviews](https://holoviews.org/)
+* [hvplot](https://hvplot.holoviz.org/)
+* [networkx](https://networkx.org/documentation/stable/index.html)
+* pyproj\**
+
+\* *Note: These might automatically be installed when installing holoviews and hvplot. If not, please install them before making and/or saving plots.*
+\** *Note: This might automatically be installed when installing python. If not, please install it before performing the metrics calculations.*
+
+All the methods and functions throughout `validation` are well documented within the documentation strings. Please reference those for more detailed explanations.
+
+## Examples
+
+Let's look at some examples of what `validation` can do.
+### `utils`
+`utils` is a module within `validation` that provides some file parsing to reformat data for the data analysis efforts. The current file formats that are used are JSON, [DSS](https://fileinfo.com/extension/dss#:~:text=Audio%20file%20saved%20by%20a,environments%2C%20such%20as%20law%20firms.), and .txt files.
+
+#### `parse_json_file(json_file, out_path, out_file_name)`
+This function turns a JSON file representation of the power distribution system model into a [pandas](https://pandas.pydata.org/docs/) data frame of the billing meter names and positions.
+``` 
+Args:
+	json_file (JSON file) - JSON file of the power distribution system model.
+    
+    	out_path (path) - Path to send the data frame. Saves the data frame as a csv.
+    
+    	out_file_name (str) - Name of the output file.
+
+Returns:
+	(null)
+```
+Here's an example of using this function:
+```
+import os
+from utils import parse_json_file
+
+if __name__ == '__main__':
+	# Reading in the file:
+	data_file = os.path.join(os.getcwd(), 'my_json.json')
+
+	# Creating an output path to send the csv:
+	my_out_path = os.path.join(os.getcwd())
+
+	# Creating a name for the file:
+	my_out_name = 'my_reformatted_file'
+
+	# Creating and saving the csv to my path:
+	parse_json_file(data_file, my_out_path, my_out_name)
+```
+Now, let's look at `parse_dss_file`.  This function takes two files: one that has meter names, and another that has the coordinates for those meters. The file that has meter names is a [DSS](https://fileinfo.com/extension/dss#:~:text=Audio%20file%20saved%20by%20a,environments%2C%20such%20as%20law%20firms.) file. The file that has the coordinates for those meters is a .txt file. These two file formats match the file format of real-world data sets.
+
+#### `parse_dss_file(load_file, coord_file, out_path, out_file_name)`
+This function parses the lines of the load\* and coordinate file, and it saves them as a single csv file.
+
+\* *Note: The load file should be the DSS file.*
+```
+Args:
+	load_file (str) - Name of the load file. This dss file has bus and load data, among other data. We use this to pair its data with the correct coordinates.
+
+        coord_file (str) - Name of the coordinate file. This contains the coordinates for the bus(es).
+
+        out_path (path) - Path to send the reformatted data file(s).
+
+        out_file_name (str) - Name to give the data file(s).
+
+Returns:
+        (null)
+```
+Here's an example:
+```
+import os
+from utils import parse_json_file
+
+if __name__ == '__main__':
+	# Reading in the files:
+	meter_file = os.path.join(os.getcwd(), 'my_meters.dss')
+	coord_file = os.path.join(os.getcwd(), 'my_coords.txt')
+
+	# Creating an output path to send the csv:
+	my_out_path = os.path.join(os.getcwd())
+
+	# Creating a name for the file:
+	my_out_name = 'my_reformatted_file'
+
+	# Creating and saving the csv to my path:
+	parse_dss_file(meter_file, coord_file, my_out_path, my_out_name)
+```
+It's that simple!
+
+*Note: It's important that the JSON file and/or DSS match the formatting we are using. Our next steps with these functions are to accommodate ANY format these files are in; that way people can use these instead of their own data re-formatting.* 
+
+### `system`
+This module provides the capability for turning the power distribution system model into a Python object. This allows for the ease in calculation and computation.
+
+In addition, this module provides the capability for creating and saving the model as a network plot, using [NetworkX](https://networkx.org/documentation/stable/index.html), [HoloViews](https://holoviews.org/), and [hvplot](https://hvplot.holoviz.org/).
+
+This module can read in data from JSON files, Excel spreadsheets, and CSV files. For our analysis, it grabs the meter names and positional information; all our metrics need to know the distance between these meters. In addition, it grabs the name of the model and the names of the feeders.
+
+To make things easy for testing potentially large sets of models, we created a class called `MeterNetwork` to house all the functions and methods we need to perform analysis.
+
+Here's a basic example:
+```
+from system import MeterNetwork
+
+
+if __name__ == '__main__':
+	# Initializing the MeterNetwork object:
+	MN = MeterNetwork()
+
+	# Adding meters to the object:
+	MN.meter_nodes = ['meter1', 'meter2', 'meter3', 'meter4']
+
+	# Adding edges (aka abstract connections between meters)
+	MN.meter_edges = [
+		('meter1', 'meter2'), ('meter1', 'meter3'),
+		('meter1', 'meter4'), ('meter2', 'meter3'),
+		('meter2', 'meter4'), ('meter3', 'meter4')]
+
+        # Adding meter positional information:
+	MN.meter_positions = {
+		'meter1': (1, 2), 'meter2': (2, 2),
+		'meter3': (3, 4), 'meter4': (5, 4)}
+
+	# Adding model name and feeder information
+	MN.model_name = 'my_model'
+	MN.feeder = 'my_feeder
+```
+The above example shows how to make a meter network from scratch.
+
+However, this is not the primary purpose of this class. This class has different methods for reading in models to do the above example for you. These methods are `from_json`, `from_csv`, and `from_excel`, respectively.
+
+Let's look at each of these individually to see how they can be used. In general, these methods turn the data into a [NetworkX](https://networkx.org/documentation/stable/index.html) representation of the model. This makes things easier for plotting and calculating our metrics.
+
+#### `from_excel(excel_file, sheet, columns)`
+This method turns an Excel file into a NetworkX representation of the meter system.
+```
+Args:
+	excel_file (file) - Excel file of the system that contains meter locations, connections between meters, among other information.
+
+    	sheet (str) - The sheet of the Excel file with meters' (aka nodes') information in it.
+
+    	columns (list) - List of node columns to call for reading in the data, e.g. 'name' or 'id', 'latitude', 'longitude', etc. The format assumes the name of the node is first and the location data is second.
+
+Returns:
+    	G (NetworkX graph) - NetworkX graph representation of the meter system.
+```
+Here's an example:
+```
+import os
+from system import MeterNetwork
+
+
+if __name__ == '__main__':
+	# Initializing the MeterNetwork class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.xlsx')
+
+	# Creating our graph representation of our model:
+	my_graph = MN.from_excel(my_file, 'meters', ['name', 'x', 'y'])
+```
+By doing this, the attributes that we care about (e.g. `meter_nodes`, `meter_edges`, etc.) get filled out for you.
+
+The following methods do the same operation, but for different file types.
+
+#### `from_csv(csv_file, columns)`
+This method turns a CSV file into a NetworkX representation of the meter system.
+```
+Args:
+	csv_file (file) - CSV file of the system that contains meter location, meter names, and other information.
+
+    	columns (list) - List of node columns to call for reading in the data, e.g. 'name' or 'id', 'latitude', 'longitude', etc. The format assumes the name of the node is first and the location data is second.
+
+Returns:
+	G (NetworkX graph) - NetworkX graph representation of the meter system.
+```
+
+```
+import os
+from system import MeterNetwork
+
+
+if __name__ == '__main__':
+	# Initializing the MeterNetwork class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph represenation of our model:
+	my_graph = MN.from_csv(my_file, ['name', 'x', 'y'])
+```
+#### `from_json(json_file, key_list, position_labels)`
+This method turns a JSON file into a NetworkX representation of the meter system.
+```
+Args:
+	json_file (file) - JSON file of the system that contains meter locations, connections between meters, and other information.
+
+    	key_list (list) - List of the keys from the JSON file. They should be something like 'name', 'id', etc. This method assumes the first key listed is the key that has the names/ids of the meters.
+
+    	position_labels (list) - List of the meter position keys in the JSON, e.g. 'lat' and 'lon', 'latitude' and 'longitude', 'x' and 'y', etc. This method assumes the first element is the 'longitude' element.
+
+Returns:
+     	G (NetworkX graph) - NetworkX graph reprsentation of the meter system.
+```
+```
+import os
+from system import MeterNetwork
+
+
+if __name__ == '__main__':
+	# Initializing the MeterNetwork class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.json')
+
+	# Creating our graph represenation of our model:
+	my_graph = MN.from_csv(my_file, ['name'], ['x', 'y'])
+```
+For all of these methods, if you want to make sure they grabbed the right information from your data, print out `meter_nodes`, `meter_edges`, and/or `meter_positions`.
+```
+print('Here are the meters we grabbed from your data:')
+print(MN.meter_nodes)
+```
+```
+Here are the meters we grabbed from your data:
+['meter1', 'meter2', 'meter3', 'meter4', 'meter5',
+ 'meter6', 'meter7', 'meter8', 'meter9', 'meter10',
+ 'meter11', 'meter12', 'meter13', 'meter14', 'meter15',
+ 'meter16', 'meter17, 'meter18', 'meter19', 'meter20',
+ 'meter21']
+```
+
+As mentioned previously, this module turns our data into a format can be used for calculations and computations more easily. To do this, we turn our models into [pandas](https://pandas.pydata.org/docs/) data frames. `make_dataframe` does that for us, instead of knowing how to make a data frame from scratch.
+#### `make_dataframe()`
+This method turns the meter graph into a pandas data frame. This allows for ease and the ability to perform metrics calculations and validation of the system as a whole.
+```
+Args:
+	(null)
+
+Returns:
+	dataframe (pandas data frame) - Pandas data frame representation of the meter names, locations, and other information.
+```
+Here's an example:
+```
+import os
+from system import MeterNetwork
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	my_graph = MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe()
+```
+Or, you could do this for any of the above methods:
+```
+import os
+from system import MeterNetwork
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Another way of creating our graph representation
+	# of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Another way of creating a dataframe of our graph:
+	df = MN.make_dataframe()
+```
+Now, our data is in a format that we can start performing metrics calculations.
+
+But before we get to that, let's look at how to make a plot of this meter network.
+
+#### `plot_graph(nodes, node_args, edges, edge_args, plot_positions, opts_dict, output_dir, save_plot=True)`
+This method creates a visual representation of the meter system using [HoloViews](https://holoviews.org/) and NetworkX drawing capabilities.
+```
+Args:
+	nodes (list) - List of the nodes in the NetworkX representation of the meter system.
+
+    	node_args (dict) - Dictionary of data of how to draw the graph. Things to consider include node color, node size, node labels, etc. See the above link for more examples.
+
+    	edges (list) - List of the edges that connect the meters.
+
+    	edge_args (dict) - Dictionary of the meter connections within the graph. Things to consider include edge color, edge size, etc. See the above link for more information.
+
+    	plot_positions (dict) - Dictionary of the meter positions within the graph. If they are not provided or updated, the plot_positions will be the default positions after instantiation of the MeterNetwork object.
+
+    	opts_dict (dict) - Dictionary of how the final plot should look. Things to consider include title, font size, the height and width of the plot, etc.
+
+    	output_dir (directory) - Directory to send the final plot of the graph.
+
+    	save_plot (bool) - Boolean of whether or not to save the plot generated. Default is 'True'.
+
+Returns:
+    	(null)
+```
+Let's plot our meter network.
+
+```
+import os
+from system import MeterNetwork
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating the argument dictionaries:
+	n_args = {
+		'node_color': 'tan', 'node_size': 3500,
+		'alpha': 0.5}
+	e_args = {
+		'edge_color': 'grey', 'edge_size': 1,
+		'style': 'dotted', 'alpha': 0.65}
+	o_args = {
+		'width': 900, 'height': 500,
+		'title': 'My Meter Network:', 'fontsize': {'title': 30}}
+
+	# Creating the output path:
+	out_dir = os.path.join(os.getcwd())
+
+	# Creating a plot of the meter network:
+	MN.plot_graph(
+		MN.meter_nodes, n_args, MN.meter_edges, e_args,
+		MN.meter_positions, o_args, out_dir, True)
+```
+Pretty simple!
+
+We have seen how to do some data pre-processing. In addition, we have seen how we can turn our data into a meter network. Now, we'll look at how we can evaluate our meter network, and even multiple networks.
+### `metrics`
+This module provides different metrics to evaluate the overall communication for a given power distribution system. As mentioned previously, we want to know how well meters can communicate with each other.
+
+For our analysis, we care about how far away meters are from each other. We have developed different metrics that capture things like, how many meters are near a central meter, how many meters are isolated, what is the largest radius to envelope all meters in one circle, etc.
+
+This module allows for performing this analysis on a single model or multiple models. It has the ability to save these results for further analysis.
+
+This module has three classes: `EvaluateSystem`, `Results`, and `Compare`. The `EvaluateSystem` class contains all the possible metric calculations we want to perform for a meter network or networks. The `Results` class simply saves the results from those calculations as an HDF5 file, to compensate for large file sizes. The `Compare` class contains the calculations to compare the models provided to the real power distribution systems' results.
+
+The methods that perform the metric calculations can  be summarized into two approaches: ad-hoc and iterative. The ad-hoc methods are to be used for one-off testing, especially for small power distribution system models. The iterative methods are to be used for testing against a range of values for multiple models at once.
+
+Let's take a look! First, we'll have to initialize the `EvaluateSystem` class. A more detailed description of what this class does and how to initialize it is contained in the documentation string.
+
+Here's an example:
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe()
+
+	# Creating an EvaluateSystem object from our network:
+	#     NOTE: To initialize the EvaluateSystem object, we
+	# 	  need the data frame that represents the model, the
+	# 	  meters (as nodes), their positional data, and what
+	# 	  unit of measurement the positional data is in. 
+	ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo') 
+```
+So, we have successfully created an `EvaluateSystem` object. Now, let's do some calculations on this power distribution model.
+
+Before we can do those calculations, we have to calculate the distances between all the meters in the model. Here's how we do that:
+#### `get_distances`
+This method calculates the distances between all the meters in the system and saves the result as a data frame to be used for further analysis.
+```
+Args:
+	(null)
+
+Returns:
+     distance_dataframe (pandas data frame) - This is a dataframe that has the calculated distance between all meters in a given meter network (aka model).
+```
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe()
+
+	# Creating an EvaluateSystem object from our network:
+	ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+	# Calculating the distances between all the meters in
+	# our model:
+	dd = ES.get_distances()
+```
+Okay. Now that we have calculated the distances between all the meters in the model, we can start calculating the metrics.
+#### `meter_density(meter, radius)`
+This method returns the number of meters within a distance of *x* from the meter in question. For example, suppose we have a model of 10 meters ('meter1', 'meter2', … , 'meter10'). Choose 'meter1' as the center. Suppose further our radius to test is 100 feet. This method counts how many meters are within 100 feet of 'meter1'.
+```
+Args:
+	meter (str) - The meter in question (aka the starting node in a graph).
+
+    	radius (float) - The distance to envelope the meters near the starting meter.
+
+Returns:
+    	total_meters (int) - The total number of meters within the radius.
+```
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe()
+
+	# Creating an EvaluateSystem object from our network:
+	ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+	# Calculating the distances between all the meters in
+	# our model:
+	dd = ES.get_distances()
+
+	# Calculating the total number of meters within
+	# 100 feet of 'meter1':
+	density = ES.meter_density('meter1', 100.0)
+
+	# This should print out the total number of
+	# meters within 100 feet of 'meter1'.
+	print('The number of meters within 100 feet of meter1:')
+	print(density)
+```
+```
+The number of meters within 100 feet of meter1:
+14 
+```
+Suppose we want to know the largest radius we need to capture all the meters in the model
+in one circle around a certain meter as the center. We would use the `meter_range` method.
+#### `meter_range(meter)`
+This method returns the radius, *x*, of the circle centered on a given meter encompasses all meters.
+
+For example, suppose we have a model of 10 meters ('meter1', 'meter2', … , 'meter10'). Choose 'meter1' as the center. This method finds the largest radius between 'meter1' and all the remaining meters.
+```
+Args:
+	meter (str) - The meter in question (aka the starting node in a graph).
+
+Returns:
+    	radius (float) - The maximum distance that envelopes all meters with a given meter as the center.
+```
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe()
+
+	# Creating an EvaluateSystem object from our network:
+	ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+	# Calculating the distances between all the meters in
+	# our model:
+	dd = ES.get_distances()
+
+	# Calculating the largest radius to capture all the
+	# meters in one circle around 'meter1':
+	meter_range = ES.meter_range('meter1')
+
+	# This will print out the largest radius to capture
+	# all the meters in one circle around 'meter1'.
+	print('The largest radius to get all meters around meter1:')
+	print(meter_range)
+```
+```
+The largest radius to get all meters around meter1:
+176.21
+```
+Suppose we want to know the total number of isolated meters within a certain radius. We would use the `isolated_meter_count` method.
+#### `isolated_meter_count(radius)`
+This method returns how many meters have no other meters within *x* distance of them.
+
+For example, suppose we have a model of 10 meters ('meter1', 'meter2', … , 'meter10'). Suppose further 'meter2' and 'meter3' are 300+ feet from each other and the remaining meters, and the remaining meters are 150 feet or less from each other. Let the radius to test against be 200 feet. Then, this method would say there are two meters that are isolated from the other meters because their distances are greater than 200 feet.
+```
+Args:
+	radius (float) - A given distance to test whether or not meters are near each other.
+
+Returns:
+    	isolated_count (int) - The total number of meters that are isolated from others, given a specific distance.
+```
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe()
+
+	# Creating an EvaluateSystem object from our network:
+	ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+	# Calculating the distances between all the meters in
+	# our model:
+	dd = ES.get_distances()
+
+	# Calculating the number of isolated meters that
+	# are greater than 100 feet from each other:
+	isos = ES.isolated_meter_count(30.0)
+
+	# This will print the total number of isolated
+	# meters that are greater than 30 feet from each
+	# other:
+	print('The total number of isolated meters:')
+	print(isos)
+```
+```
+The total number of isolated meters:
+6
+```
+Suppose we want to know if there exists (an abstract) direct or indirect path between any two meters in the model. We would use the `meter_continuity` method.
+#### `meter_continuity(radius)`
+This method aims to see if any two meters can communicate with each other, either directly or indirectly, within a certain distance.
+
+For example, suppose we have a model of 10 meters ('meter1', 'meter2', … , 'meter10'). Suppose further 'meter2' and 'meter3' are 300+ feet from each other, but they are 100 feet from the remaining meters. Let the radius to test against be 200 feet. Since 'meter2' and 'meter3' are closer to the remaining meters (rather than each other), this method will say that there is continuity because there is an indirect path from 'meter2' to 'meter3' through any of the remaining meters.
+```
+Args:
+	radius (float) - A given distance to test whether or not a path exists between any two meters.
+
+Returns:
+    	continuity (bool) - Whether or not the meters are connected to each other.
+```
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe(MN.graph)
+
+	# Creating an EvaluateSystem object from our network:
+	ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+	# Calculating the distances between all the meters in
+	# our model:
+	dd = ES.get_distances()
+
+	# Checking to see if there are direct or indirect
+	# paths from any two meters where each distance is
+	# less than or equal to 100 feet:
+	continuity = ES.meter_continuity(100.0)
+
+	# This will print whether or not direct or indirect
+	# paths exist between any two meters in the model:
+	print(continuity)
+```
+```
+True
+```
+Now, let's complicate things a little bit. Suppose we want to find the total number of meters that have a certain number of meters within a specified radius. We would use the `single_hop_count` method. This is a specialized version of the `meter_density` method.
+#### `single_hop_count(radius, y)`
+This method returns the number of meters with *y* meters within *x* distance of them.
+
+For example, suppose we have a model of 10 meters ('meter1', 'meter2', … , 'meter10'). Let *y* = 2 and let radius = 200 feet. This method counts how many meters have less than or equal to 2 meters within 200 feet of them. Suppose 'meter1', 'meter2', and 'meter3' are all 150 feet from each other, while the remaining meters are 300+ feet from them and each other. This method will return 3 because 'meter1', 'meter2', and 'meter3' all have 2 meters or less within 200 feet.
+```
+Args:
+	radius (float) - A given distance to test whether or not a number of meters, y, are within a certain distance of a specified meter as the center.
+
+    y (int) - Specific number of meters near the starting meters.
+
+Returns:
+    single_hop_count (int) - The total number of meters with a certain number of meters near them within a given distance.
+```
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe()
+
+	# Creating an EvaluateSystem object from our network:
+	ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+	# Calculating the distances between all the meters in
+	# our model:
+	dd = ES.get_distances()
+
+	# Counting how many meters have 3 or less meters
+	# within 100 feet:
+	shc = ES.single_hop_count(100.0, 3)
+
+	# This will print how many meters that have
+	# 3 or less meters within 100 feet:
+	print('This is the total number of meters with 3 meters nearby:')
+	print(shc)
+```
+```
+This is the total number of meters with 3 meters nearby:
+22
+```
+Another important metric to calculate is the number of islands in the system. An island is a group of one or more meters with distances from each other less than a specified radius. This tells us that there is still some communication between meters when some connections are removed. For this analysis, a lower number of islands indicates better communication between the meters.
+
+#### `island_count(radius)`
+```
+Args:
+	radius (float) - A given distance to count how many islands there are in the model.
+
+Returns:
+	island_count (int) - The number of islands in the model.
+```
+
+Let's look at an example of how to use this method.
+
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe()
+
+	# Creating an EvaluateSystem object from our network:
+	ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+	# Calculating the distances between all the meters in
+	# our model:
+	dd = ES.get_distances()
+
+	# Counting how many islands there are with
+	# meters that are less than 100 feet away:
+	isl = ES.island_count(100.0)
+
+	# This will print how many islands there are
+	# with meters that are less than 100 feet away:
+	print('This is the total number of islands:')
+	print(isl)
+```
+```
+This is the total number of islands:
+607
+```
+
+So far, we have looked at small tests for a single model. What if we want to test against multiple radii or all the meters in a model? What if we want to do these calculations for many models?
+
+Below, we provide an example for performing these calculations for a range of radii for more than one model\*. 
+
+\* *Note: For the sake of simplicity, we leave the descriptions for each of the methods to the reader. The documentation strings for the methods provide more details. In general, these methods are the same as what we've seen thus far, but are done iteratively for many meters, radii, and models.*
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Grabbing all the models:
+	model_path = os.path.join(os.getcwd(), 'models')
+	for root, dirs, files in os.walk(model_path):
+		for file in files:
+			# Initializing the class:
+			MN = MeterNetwork()
+
+			# Creating our graph representation of our model: 
+			MN.from_csv(
+				os.path.join(model_path, file), ['name', 'x', 'y'])
+
+			# Creating a dataframe of our graph:
+			df = MN.make_dataframe()
+
+			# Creating an EvaluateSystem object from our network:
+			ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+			# Calculating the distances between all the meters in
+			# our model:
+			dd = ES.get_distances()
+
+			# Calculating all the densities for all the meters
+			# for radii of 100, 200, and 300 feet:
+			densities = ES.all_densities([100.0, 200.0, 300.0])
+
+			# Calculating all the radii for all meters
+			# as the center:
+            		ranges = ES.all_ranges()
+ 
+            		# Calculating all the isolated meters for radii
+            		# of 100, 200, and 300 feet:
+            		isolates = ES.all_isolates([100.0, 200.0, 300.0])
+ 
+ 		        # Checking if paths exits between any two meters
+            		# for radii of 100, 200, and 300 feet:
+            		continuous = ES.all_continuous([100.0, 200.0, 300.0])
+ 
+            		# Calculating all the single_hop_counts for
+            		# radii of 100, 200, 300 feet:
+            		shc = ES.all_single_hops([100.0, 200.0, 300.0])
+
+			# Calculating all the islandcounts for
+			# radii of 100, 200, 300 feet:
+			isl = ES.all_islands([100.0, 200.0, 300.0])
+```
+This is good, but how can we use these values to figure out how well the meters in our models can communicate with each other?
+
+We can use the `evaluate_system`\* method. This method turns the results of the above values into an overall, weighted score. In general, the higher the score, the better the meters in the model(s) can communicate with each other.
+
+\* *Note: A more detailed description of how we use these values and generate the score is contained in the documentation string of this method.*
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem
+
+
+if __name__ == '__main__':
+	# Initializing the class:
+	MN = MeterNetwork()
+
+	# Grabbing our data:
+	my_file = os.path.join(os.getcwd(), 'my_file.csv')
+
+	# Creating our graph representation of our model:
+	MN.from_csv(my_file, ['name', 'x', 'y'])
+
+	# Creating a dataframe of our graph:
+	df = MN.make_dataframe()
+
+	# Creating an EvaluateSystem object from our network:
+	ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+	# Calculating the distances between all the meters in
+	# our model:
+	dd = ES.get_distances()
+
+	# Calculating all the densities for all the meters
+	# for radii of 100, 200, and 300 feet:
+	densities = ES.all_densities([100.0, 200.0, 300.0])
+
+	# Calculating all the radii for all meters
+	# as the center:
+    	ranges = ES.all_ranges()
+
+    	# Calculating all the isolated meters for radii
+    	# of 100, 200, and 300 feet:
+    	isolates = ES.all_isolates([100.0, 200.0, 300.0])
+
+    	# Checking if paths exits between any two meters
+    	# for radii of 100, 200, and 300 feet:
+    	continuous = ES.all_continuous([100.0, 200.0, 300.0])
+
+    	# Calculating all the single_hop_counts for
+    	# radii of 100, 200, 300 feet:
+    	shc = ES.all_single_hops([100.0, 200.0, 300.0])
+
+	# Calculating all the island counts for
+	# radii of 100, 200, 300 feet:
+	isl = ES.all_islands([100.0, 200.0, 300.0])
+
+	# Calculating the overall score for this
+	# model:
+	score = ES.evaluate_system([100.0, 200.0, 300.0])
+
+	# This will print a value that represents how well
+	# the meters can communicate with each other:
+	print(score)
+```
+```
+80.9
+```
+A similar approach can be applied for more than one model.
+
+We have shown different things to calculate and how to get them. Now, we need a way to save the results for further analysis. So, we use the `Results` class.
+
+This class collects and saves the results from more than one power distribution system model evaluation.
+
+For our analysis, we not only care about the communication between meters for a single model, but many models. This class is for collecting **all** the results from **all** models tested for analysis.
+
+The `Results` class has two methods: `add` and `save`. The `add` method simply appends each `EvaluateSystem` object to a list. The `save` method saves all the results from all the models into one HDF5 file.
+
+Let's put it all together!
+```
+import os
+from system import MeterNetwork
+from metrics import EvaluateSystem, Results
+
+
+if __name__ == '__main__':
+	# Grabbing all the models:
+	model_path = os.path.join(os.getcwd(), 'models')
+
+	# Creating a Results object:
+	RS = Results()
+	for root, dirs, files in os.walk(model_path):
+		for file in files:
+			# Initializing the class:
+			MN = MeterNetwork()
+
+			# Creating our graph representation of our model: 
+			MN.from_csv(
+				os.path.join(model_path, file), ['name', 'x', 'y'])
+
+			# Creating a dataframe of our graph:
+			df = MN.make_dataframe()
+
+			# Creating an EvaluateSystem object from our network:
+			ES = EvaluateSystem(df, MN.meter_nodes, 'x', 'y', 'geo')
+
+			# Calculating the distances between all the meters in
+			# our model:
+			dd = ES.get_distances()
+
+			# Calculating all the densities for all the meters
+			# for radii of 100, 200, and 300 feet:
+			densities = ES.all_densities([100.0, 200. 0, 300. 0])
+
+			# Calculating all the radii for all meters
+			# as the center:
+            		ranges = ES.all_ranges()
+ 
+            		# Calculating all the isolated meters for radii
+            		# of 100, 200, and 300 feet:
+            		isolates = ES.all_isolates([100.0, 200.0, 300.0])
+ 
+            		# Checking if paths exits between any two meters
+            		# for radii of 100, 200, and 300 feet:
+            		continuous = ES.all_continuous([100.0, 200.0, 300.0])
+ 
+            		# Calculating all the single_hop_counts for
+            		# radii of 100, 200, 300 feet:
+            		shc = ES.all_single_hops([100.0, 200.0, 300.0])
+
+			# Calculating all the island counts for
+			# radii of 100, 200, 300 feet:
+			isl = ES.all_islands([100.0, 200.0, 300.0])
+
+			# Calculating the overall score for this
+			# model:
+			score = ES.evaluate_system([100.0, 200.0, 300.0])
+
+			# Adding the model to the Results object:
+			RS.add(ES)
+	
+	# Saving the results for all the models:
+	out_dir = os.path.join(os.getcwd())
+	out_name = 'all_models_results.h5'
+	RS.save(out_dir, out_name)
+```
+The main goal of this project is to compare modelled power distribution systems to real power distribution systems. To do that, we developed the `Compare` class. This class takes the metrics results of the real and modelled systems and calculates the mean squared errors of those results; the lower the mean squared error, the better the models match the real data.
+
+Each of the above metrics have their own corresponding comparison method to be used. Each of the comparison methods have the capability to compare the models to the real power distribution systems as a whole, or individual feeders of the real systems.
+
+Let's look at what `Compare` has to offer. Again, more descriptions of what the methods in this class can be found in the documentation strings.
+
+The first comparison method compares the meter density results between the real power distribution systems and the models.
+#### `compare_meter_density(models, how, radii)`
+```
+Args:
+	models (list) - List of all the models (as pandas dataframes) for comparing to real data.
+
+	how (str) - How we should compare the models to the real data. The options are as follows:
+		'agg' - Choosing how='agg' will compare the model(s) to the real data as a whole, meaning we will compare the model(s) to ALL of the feeders at once in a particular region and demographic.
+		'single' - Choosing how='single' will compare the model(s) to the real data individually, meaning we will compare the model(s) to EACH of the feeders, one at a time.
+	radii (list) - List of radii (in feet) to count the number of meters near a given meter.
+
+Returns:
+	stats_df (pandas dataframe) - A pandas dataframe that contains the results of calculating the MSE. If how equals 'agg', then the dataframe will have the scores based off of comparing the model(s) to the WHOLE real dataset. If how equals 'single', the dataframe will have the scores based off of comparing the model(s) to each of the real feeders in the real data set individually.
+```
+Let's look at an example of how to use this method.
+```
+import pandas as pd
+from metrics import Compare
+
+if __name__ == '__main__`:
+	# Create a Compare object:
+	C = Compare()
+	
+	# Load the real metrics results:
+	C.load_data()
+
+	# Load the modelled metrics results:
+	model = pd.read_hdf('results.h5', key='density')
+
+	# Comparing the modelled results to
+	# the real results:
+	stats_df = C.compare_meter_density([model], 'agg', [100.0, 200.0, 300.0])
+
+	# Looking at the results:
+	print(stats_df)
+```
+
+| feeder | radius | mse |
+|:--------:|:-------------:|:---------:| 
+| feeder1 | 100.0 |0.017425|
+| feeder1 | 200.0 |0.231688|
+| feeder1 | 300.0 |3.021774|
+
+Again, each of the comparison methods calculate the mean squared error between the models and the real power distribution systems; we omit a description for each of the remaining methods because they're doing the same calculation, just on different metrics' results. More information is provided in the documentation string of each comparison method.
+
+#### `compare_meter_range(models, how)`
+```
+Args:
+	models (list) - List of all the models (as pandas dataframes) for comparing to real data.
+	how (str) - How we should compare the models to the real data. The options are as follows:
+		'agg' - Choosing how='agg' will compare the model(s) to the real data as a whole, meaning we will compare the model(s) to ALL of the feeders at once.
+		'single' - Choosing how='single' will compare the model(s) to the real data individually, meaning we will compare the model(s) to EACH of the feeders, one at a time, in a particular region and demographic
+
+Returns:
+	stats_df (pandas dataframe) - A pandas dataframe that contains the results of calculating the MSE. If how equals 'agg', then the dataframe will have the scores based off of comparing the model(s) to the WHOLE real dataset. If how equals 'single', the dataframe will have the scores based off of comparing the model(s) to each of the real feeders in the real data set individually.
+```
+Example:
+```
+import pandas as pd
+from metrics import Compare
+
+if __name__ == '__main__`:
+	# Create a Compare object:
+	C = Compare()
+	
+	# Load the real metrics results:
+	C.load_data()
+
+	# Load the modelled metrics results:
+	model = pd.read_hdf('results.h5', key='range')
+
+	# Comparing the modelled results to
+	# the real results:
+	stats_df = C.compare_meter_range([model], 'agg')
+
+	# Looking at the results:
+	print(stats_df)
+```
+
+| feeder | mse |
+|:--------:|:---------:| 
+| feeder1 | 2.396914 |
+
+#### `compare_isolated_meters(models, how, radii)`
+```
+Args:
+	models (list) - List of all the models (as pandas dataframes) for comparing to real data.
+	how (str) - How we should compare the models to the real data. The options are as follows:
+		'agg' - Choosing how='agg' will compare the model(s) to the real data as a whole, meaning we will compare the model(s) to ALL of the feeders at once.
+		'single' - Choosing how='single' will compare the model(s) to the real data individually, meaning we will compare the model(s) to EACH of the feeders, one at a time.
+	radii (list) - List of radii (in feet) to count the number of meters near a given meter.
+
+Returns:
+    stats_df (pandas dataframe) - A pandas dataframe that contains the results of calculating the MSE. If how equals 'agg', then the dataframe will have the scores based off of comparing the model(s) to the WHOLE real dataset. If how equals 'single', the dataframe will have the scores based off of comparing the model(s) to each of the real feeders in the real data set individually.
+```
+Example:
+```
+import pandas as pd
+from metrics import Compare
+
+if __name__ == '__main__`:
+	# Create a Compare object:
+	C = Compare()
+	
+	# Load the real metrics results:
+	C.load_data()
+
+	# Load the modelled metrics results:
+	model = pd.read_hdf('results.h5', key='isolated')
+
+	# Comparing the modelled results to
+	# the real results:
+	stats_df = C.compare_isolated_meters([model], 'agg', [100.0, 200.0, 300.0])
+
+	# Looking at the results:
+	print(stats_df)
+```
+
+| feeder | radius | mse |
+|:--------:|:-------------:|:---------:| 
+| feeder1 | 100.0 |2.032204|
+| feeder1 | 200.0 |1.906663|
+| feeder1 | 300.0 |1.938369|
+
+#### `compare_meter_continuity(models, how, radii)`
+```
+Args:
+	models (list) - List of all the models (as pandas dataframes) for comparing to real data.
+	how (str) - How we should compare the models to the real data. The options are as follows:
+		'agg' - Choosing how='agg' will compare the model(s) to the real data as a whole, meaning we will compare the model(s) to ALL of the feeders at once.
+		'single' - Choosing how='single' will compare the model(s) to the real data individually, meaning we will compare the model(s) to EACH of the feeders, one at a time.
+	radii (list) - List of radii (in feet) to count the number of meters near a given meter.
+
+Returns:
+	stats_df (pandas dataframe) - A pandas dataframe that contains the results of calculating the MSE. If how equals 'agg', then the dataframe will have the scores based off of comparing the model(s) to the WHOLE real dataset. If how equals 'single', the dataframe will have the scores based off of comparing the model(s) to each of the real feeders in the real data set individually.
+```
+Example
+```
+import pandas as pd
+from metrics import Compare
+
+if __name__ == '__main__`:
+	# Create a Compare object:
+	C = Compare()
+	
+	# Load the real metrics results:
+	C.load_data()
+
+	# Load the modelled metrics results:
+	model = pd.read_hdf('results.h5', key='continuous')
+
+	# Comparing the modelled results to
+	# the real results:
+	stats_df = C.compare_meter_continuity([model], 'agg', [100.0, 200.0, 300.0])
+
+	# Looking at the results:
+	print(stats_df)
+```
+
+| feeder | mse |
+|:--------:|:---------:| 
+| feeder1 |0.000798|
+
+#### `compare_single_hop_count(models, how, radii)`
+```
+Args:
+	models (list) - List of all the models (as pandas dataframes) for comparing to real data.
+	how (str) - How we should compare the models to the real data. The options are as follows:
+		'agg' - Choosing how='agg' will compare the model(s) to the real data as a whole, meaning we will compare the model(s) to ALL of the feeders at once.
+		'single' - Choosing how='single' will compare the model(s) to the real data individually, meaning we will compare the model(s) to EACH of the feeders, one at a time.
+	radii (list) - List of radii (in feet) to count the number of meters near a given meter.
+
+Returns:
+	stats_df (pandas dataframe) - A pandas dataframe that contains the results of calucalting the MSE. If how equals 'agg', then the dataframe will have the scores based off of comparing the model(s) to the WHOLE real dataset. If how equals 'single', the dataframe will have the scores based off of comparing the model(s) to each of the real feeders in the real data set individually.
+```
+Example
+```
+import pandas as pd
+from metrics import Compare
+
+if __name__ == '__main__`:
+	# Create a Compare object:
+	C = Compare()
+	
+	# Load the real metrics results:
+	C.load_data()
+
+	# Load the modelled metrics results:
+	model = pd.read_hdf('results.h5', key='single_hop')
+
+	# Comparing the modelled results to
+	# the real results:
+	stats_df = C.compare_single_hop_count([model], 'agg', [100.0, 200.0, 300.0])
+
+	# Looking at the results:
+	print(stats_df)
+```
+
+| feeder | radius | mse |
+|:--------:|:-------------:|:---------:| 
+| feeder1 | 100.0 |2.007354|
+| feeder1 | 200.0 |2.009119|
+| feeder1 | 300.0 |1.947478|
+
+#### `compare_island_count(models, how, radii)`
+```
+Args:
+	models (list) - List of all the models (as pandas dataframes) for comparing to real data.
+	how (str) - How we should compare the models to the real data. The options are as follows:
+		'agg' - Choosing how='agg' will compare the model(s) to the real data as a whole, meaning we will compare the model(s) to ALL of the feeders at once.
+		'single' - Choosing how='single' will compare the model(s) to the real data individually, meaning we will compare the model(s) to EACH of the feeders, one at a time.
+	radii (list) - List of radii (in feet) to count the number of meters near a given meter.
+
+Returns:
+	stats_df (pandas dataframe) - A pandas dataframe that contains the results of calculating the MSE. If how equals 'agg', then the dataframe will have the scores based off of comparing the model(s) to the WHOLE real dataset. If how equals 'single', the dataframe will have the scores based off of comparing the model(s) to each of the real feeders in the real data set individually.
+```
+Example
+```
+import pandas as pd
+from metrics import Compare
+
+if __name__ == '__main__`:
+	# Create a Compare object:
+	C = Compare()
+	
+	# Load the real metrics results:
+	C.load_data()
+
+	# Load the modelled metrics results:
+	model = pd.read_hdf('results.h5', key='island')
+
+	# Comparing the modelled results to
+	# the real results:
+	stats_df = C.compare_island_count([model], 'agg', [100.0, 200.0, 300.0])
+
+	# Looking at the results:
+	print(stats_df)
+```
+
+| feeder | radius | mse |
+|:--------:|:-------------:|:---------:| 
+| feeder1 | 100.0 |1.840693|
+| feeder1 | 200.0 |2.007030|
+| feeder1 | 300.0 |2.004431|
+
+That's `validation` in a nutshell!
+
+## Future Work
+Right now, the only calculation to evaluate how well the models match the real power distribution systems is the mean squared error. This does not tell us what value of mean squared error indicates that a model does not match the real data. We would like to expand upon this calculation and investigate similar calculations that will better determine whether or not the models match the real data.
+
+We would also like to investigate and expand upon the current metrics we have developed. We have a small set of metrics that we generated to evaluate our models. There are certainly more things we could create and use for our analysis. In addition, we desire to appropriately weight the metrics we have in the `evaluate_system` method.
+
+Some of our data pre-processing and other functions are developed to match a specific data format. We would like to generalize these functions to handle potentially any data or file format that is provided. For example, this would require creating more functions that can parse different file formats.
+
+Right now, we have one method that can create and save a plot. This plot is just the power distribution system model represented as a network. We would like to develop more methods and/or a module for plotting the results of the metric calculations. Since we use [HoloViews](https://holoviews.org/) and [NetworkX](https://networkx.org/documentation/stable/index.html) throughout our modules, we can use some of their methods for generating analytical plots.
\ No newline at end of file
diff --git a/src/comms_metrics/__init__.py b/src/comms_metrics/__init__.py
new file mode 100755
index 00000000..a5dd3b9e
--- /dev/null
+++ b/src/comms_metrics/__init__.py
@@ -0,0 +1,1 @@
+# This file helps make this a package
diff --git a/src/comms_metrics/metrics.py b/src/comms_metrics/metrics.py
new file mode 100755
index 00000000..7171de47
--- /dev/null
+++ b/src/comms_metrics/metrics.py
@@ -0,0 +1,2285 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Feb  5 08:21:30 2021
+
+This module provides different metrics to
+evaluate the overall communication for a given
+power distribution system. We want to know how
+well meters can communicate with each other.
+
+For our analysis, we care about how far away meters
+are from each other. We have developed different metrics
+that capture things like how many meters are near a
+central meter, how many meters are isolated, what is the
+largest radius to envelope all meters in one circle, etc.
+
+This module allows for performing this analysis on a
+single model or multiple models. It has the ability to
+save these results for further analysis.
+
+This module can be expanded to include more metrics
+and/or plotting results.
+
+@author: barn553
+"""
+import pandas as pd
+import numpy as np
+from sklearn.metrics import mean_squared_error
+import itertools
+import networkx as nx
+import pyproj
+import os
+from joblib import Parallel, delayed
+import logging
+import logging.config
+import json
+pd.options.mode.chained_assignment = None  # default='warn'
+
+# Creating a custom logger:
+head, tail = os.path.split(os.getcwd())
+config_file = os.path.join(
+    head, 'results', 'santa_fe', 'config.json')
+with open(config_file, 'r') as file:
+    config = json.load(file)
+    logging.config.dictConfig(config)
+
+logger = logging.getLogger(__name__)
+
+
+class EvaluateSystem:
+    """Evaluates the power distribution system model.
+
+    This class provides all the metrics for evaluating the
+    overall communication abilities of a power distribution
+    system. More information for each metric is provided
+    in the documentation string of each function.
+
+    Args:
+        dataframe (pandas dataframe) - This is a pandas
+        representation of the power distribution system
+        model. It contains information like the name of the
+        feeder, the names of the meters, and the meters'
+        locations.
+
+        meters (list) - List of the meters in a given
+        power distribution system.
+
+        pos_x (str) - The 'x' position of the meters'
+        locations. For example, this might be the longitude
+        value of a coordinate. This should be a column in
+        the dataframe.
+
+        pos_y (str) - The 'y' position of the meters'
+        locations. For example, this might be the latitude
+        value of a coordinate. This should be a column in
+        the dataframe.
+
+        unit (str) - The unit of measurement the positional
+        information is currently in. For example, the GLD
+        positional data is in 'feet', while the Santa Fe data
+        is latitude-longitude pairs. This should be 'feet' or
+        'geo', otherwise there will be an error.
+
+    Returns:
+        EvaluateSystem (obj) - An object that represents the
+        MeterNetwork object of the power distribution model.
+        This is the object that can be used for metric
+        function calls.
+    """
+
+    def __init__(self, dataframe, meters, pos_x, pos_y, unit):
+        """This initializes the class."""
+        self.dataframe = dataframe
+        # Checking to make sure the input data has
+        # all the important data for the metric
+        # calculations:
+        assert 'model_name' in self.dataframe.columns,\
+            'Make sure the name of the model is included.'
+        self.model_name = self.dataframe['model_name'].unique()[0]
+        assert 'feeder' in self.dataframe.columns,\
+            'Make sure the feeder name is included in the dataframe.'
+        self.feeder = self.dataframe['feeder'].unique()[0]
+        logger.info(
+            '----- An EvaluateSystem object has -----')
+        logger.info(
+            '----- been created for {} --'.format(self.feeder))
+        self.meters = meters
+        assert pos_x in self.dataframe.columns,\
+            'Oops! {} is not in the dataframe'.format(pos_x)
+        self.pos_x = pos_x
+        assert pos_y in self.dataframe.columns,\
+            'Oops! {} is not in the dataframe'.format(pos_y)
+        self.pos_y = pos_y
+        assert unit == 'feet' or unit == 'geo',\
+            'Oops! "{}" is not valid; it must be "feet" or "geo"'.format(
+                unit)
+        self.unit = unit
+        # NOTE: More descriptions of these attributes
+        # are provided in the functions below.
+        self.distance_dataframe = None
+        self.perm_df = None
+        self.dens_df = None
+        self.range_df = None
+        self.iso_df = None
+        self.cont_df = None
+        self.shc_df = None
+        self.island_df = None
+        self.score_df = None
+        self.composite_score = None
+
+    def _modify_dataframe(self):
+        """This funciton modifies the dataframe to speed up the
+        get_distances function.
+
+        It goes through all the permutations of abstact connections between
+        any two meters in the system. Then, it grabs each "start" and
+        "end" meters' positions. Then, it saves all this information
+        as a dataframe, which can be accessed as an EvaluateSystem object's
+        attribute.
+
+        This allows for the ability to calculate the
+        distance between these meter-pairs using pandas' array
+        functionality (which is based on numpy's array functionality).
+        This speeds up the calculation significantly.
+        """
+        # These are the names of the positional data (i.e., 'lon', 'lat'),
+        # the original dataframe representation of the model,
+        # and the unit of measurement the positional data is
+        # currently in (e.g. feet or geospatial).
+        df = self.dataframe
+        pos_x = self.pos_x
+        pos_y = self.pos_y
+        unit = self.unit
+        # For our analysis, we care about finding the distances
+        # between ANY two meters in the system. That means
+        # calculating the distance between ALL permutations of
+        # pairs of meters. To speed up that calculation, we create
+        # a tuple of (meter name, x-position, y-position). Then,
+        # we find all the permutations of pairs of tuples. Next,
+        # we grab the individual parts of the tuple. Lastly,
+        # we create a dataframe of the "start" and "end" meters
+        # and their positional information and save it as an
+        # attribute to be called in the get_distances() function.
+        #   NOTE: For the geospatial data, we use a separate
+        #   function that can take the starting and ending
+        #   positional data as arrays and returns an array of
+        #   calculated distance between the meters. So, we make
+        #   a dataframe with names and positional information only.
+        #   The "feet" data calculates the change in x and change
+        #   in y beforehand.
+        if unit == 'geo':
+            # Creating the (meter name, x-position, y-position) tuple:
+            df['location_tuple'] = [
+                (n, df[df.name == n][pos_x].values[0],
+                 df[df.name == n][pos_y].values[0])
+                for n in df.name.unique()]
+            # Getting all the permutations of pairs of tuples:
+            perms = list(itertools.permutations(df.location_tuple, 2))
+            # Grabbing the individual pieces of the tuples:
+            start = [p[0][0] for p in perms]
+            start_x = [p[0][1] for p in perms]
+            start_y = [p[0][2] for p in perms]
+            end = [p[1][0] for p in perms]
+            end_x = [p[1][1] for p in perms]
+            end_y = [p[1][2] for p in perms]
+            # Saving the results as a dataframe to be used elsewhere:
+            perm_df = pd.DataFrame(
+                {'start': start, 'start_x': start_x, 'start_y': start_y,
+                 'end': end, 'end_x': end_x, 'end_y': end_y},
+                index=range(len(perms)))
+            self.perm_df = perm_df
+        elif unit == 'feet':
+            # Creating the (meter name, x-position, y-position) tuple:
+            df['location_tuple'] = [
+                (n, df[df.name == n][pos_x].values[0],
+                 df[df.name == n][pos_y].values[0])
+                for n in df.name.unique()]
+            # Getting all the permutations of pairs of tuples:
+            perms = list(itertools.permutations(df.location_tuple, 2))
+            # Grabbing the individual pieces of the tuples:
+            start = [p[0][0] for p in perms]
+            start_x = [p[0][1] for p in perms]
+            start_y = [p[0][2] for p in perms]
+            end = [p[1][0] for p in perms]
+            end_x = [p[1][1] for p in perms]
+            end_y = [p[1][2] for p in perms]
+            # Calculating the change in x and y ahead of time:
+            delta_x = [np.abs(ex - sx) for sx, ex in zip(end_x, start_x)]
+            delta_y = [np.abs(ey - sy) for sy, ey in zip(end_y, start_y)]
+            # Saving the results as a dataframe to be used elsewhere:
+            perm_df = pd.DataFrame(
+                {'start': start, 'start_x': start_x, 'start_y': start_y,
+                 'end': end, 'end_x': end_x, 'end_y': end_y,
+                 'delta_x': delta_x, 'delta_y': delta_y},
+                index=range(len(perms)))
+            self.perm_df = perm_df
+        else:
+            logger.error(
+                '"{}" is invalid. It must be "feet" or "geo".'.format(
+                    unit))
+
+    def get_distances(self):
+        """This function calculates the distances between all
+        the meters in the system and saves the result as a
+        dataframe to be used for further analysis.
+
+        Args:
+            (null)
+
+        Returns:
+            distance_dataframe (pandas dataframe) - This is a dataframe
+            that has the calculated distance between all meters
+            in a given meter network (aka model).
+        """
+        logger.info(
+            '........................................')
+        logger.info('calculating the distances between all the meters')
+        if self.unit == 'geo':
+            self._modify_dataframe()
+            p_df = self.perm_df
+            geodesic = pyproj.Geod(ellps='WGS84')
+            # Calculating the distance using this function:
+            #   NOTE: The default result of the calculated
+            #   distance is in meters, so we multiply by 3.28084
+            #   to convert it to feet. This can be expanded to
+            #   provide the option of feet or meters.
+
+            #   IMPORTANT: The longitude MUST be the first argument.
+            #   So, it is important that the data read in has the
+            #   the longitude values as the "x" values and latitude
+            #   values as "y" values.
+            p_df['distance'] = geodesic.inv(
+                np.array(p_df.start_x), np.array(p_df.start_y),
+                np.array(p_df.end_x), np.array(p_df.end_y))[-1] * 3.28084
+            # Saving the result to be used for the different
+            # metrics:
+            distance_dataframe = p_df[['start', 'end', 'distance']]
+            logger.info(
+                '\tsaving the results to be used elsewhere')
+            self.distance_dataframe = distance_dataframe
+        elif self.unit == 'feet':
+            self._modify_dataframe()
+            p_df = self.perm_df
+            # Calculating distance using the distance formula:
+            p_df['distance'] = np.sqrt(
+                np.square(np.array(p_df['delta_x']))
+                + np.square(np.array(p_df['delta_y'])))
+            # Saving the result to be used for the different
+            # metrics:
+            distance_dataframe = p_df[['start', 'end', 'distance']]
+            logger.info(
+                '\tsaving the results to be used elsewhere')
+            self.distance_dataframe = distance_dataframe
+        else:
+            logger.error(
+                '{} is an invalid option. It must be "feet" or "geo".'.format(
+                    self.unit))
+        logger.info('finished calculating the distances.')
+        return distance_dataframe
+
+    def meter_density(self, meter, radius):
+        """This function returns the number of meters within
+        a distance of x from the meter in question.
+
+        For example, suppose we have a model of 10 meters
+        ('meter1', 'meter2', ..., 'meter10'). Choose 'meter1'
+        as the center. Suppose further our radius to test is
+        100 feet. This function counts how many meters are within
+        100 feet of 'meter1'.
+
+        Args:
+            meter (str) - The meter in question (aka the starting
+            node in a graph).
+
+            radius (float) - The distance to envelope the
+            meters near the starting meter.
+
+        Returns:
+            total_meters (int) - The total number of meters within
+            the radius.
+        """
+        logger.info(
+            '........................................')
+        logger.info(
+            'Finding all the meters within {} (ft) of {}'.format(
+                radius, meter))
+        assert isinstance(radius, float),\
+            'Oops, {} is not a float.'.format(radius)
+        # Grabbing the dataframe that has all the calculated
+        # distances between all the meters:
+        dd = self.distance_dataframe
+        reduced_dd = dd[(dd.start == meter)
+                        & (dd.distance <= radius)]
+        # Counting the total number of meters within a certain
+        # distance:
+        total_meters = len(reduced_dd)
+        logger.info(
+            'The number of meters within {} (ft) of {} is {}.'.format(
+                radius, meter, total_meters))
+        return total_meters
+
+    def meter_range(self, meter):
+        """This function returns the radius, x, of the circle
+        centered on a given meter encompasses all meters.
+
+        For example, suppose we have a model of 10 meters
+        (meter1, meter2, ... meter10). Choose meter1 as the
+        center. This function finds the largest radius between
+        meter1 and all the remaining meters.
+
+        Args:
+            meter (str) - The meter in question (aka the starting
+            node in a graph).
+
+        Returns:
+            radius (float) - The maximum distance that envelopes all
+            meters with a given meter as the center.
+        """
+        logger.info(
+            '........................................')
+        logger.info(
+            'Finding the largest radius where {} is the center'.format(
+                meter))
+        # Grabbing the dataframe that has all the calculated
+        # distances between all the meters:
+        dd = self.distance_dataframe
+        # Setting the given meter as the center:
+        reduced_dd = dd[dd.start == meter]
+        # Finding the maximum distance:
+        radius = np.max(reduced_dd['distance'])
+        logger.info(
+            '{} (ft) captures all meters with {} as the center'.format(
+                radius, meter))
+        return radius
+
+    def isolated_meter_count(self, radius):
+        """This function returns how many meters have no other
+        meters within x distance of them.
+
+        For example, suppose we have a model of 10 meters
+        (meter1, meter2, ... meter10). Suppose further meter2
+        and meter3 are 300+ feet from each other and the
+        remaining meters, and the remaining meters are 150
+        feet or less from each other. Let the radius to test
+        against be 200 feet. Then, this function would say there
+        are two meters that are isolated from the other meters
+        because their distances are greater than 200 feet.
+
+        Args:
+            radius (float) - A given distance to test whether
+            or not meters are near each other.
+
+        Returns:
+            isolated_count (int) - The total number of meters that are
+            isolated from others, given a specific distance.
+        """
+        assert isinstance(radius, float),\
+            'Oops, {} is not a float.'.format(radius)
+        logger.info(
+            '........................................')
+        logger.info(
+            'Counting all the isolated meters within {} (ft)'.format(
+                radius))
+        # Grabbing the dataframe that has all the calculated
+        # distances between all the meters:
+        dd = self.distance_dataframe.copy()
+        # Chekcing whether the calculated distances are
+        # bigger than the given radius:
+        bools = ['T' if d > radius else 'F' for d in dd.distance]
+        dd['is_isolated'] = bools
+        gpd = dd.groupby('start')
+        # Counting all the isolated meters:
+        gpd_df = gpd['is_isolated'].value_counts().to_frame(
+            name='total_count').reset_index()
+        isolated_count = len(gpd_df[
+            (gpd_df.is_isolated == 'T') &
+            (gpd_df.total_count == len(self.meters) - 1)]['start'].values[:])
+        logger.info(
+            '{} meters are isolated within {} (ft)'.format(
+                isolated_count, radius))
+        return isolated_count
+
+    def meter_continuity(self, radius):
+        """This function aims to see if any two meters can communicate
+        with each other, either directly or indirectly, within
+        a certain distance.
+
+        For example, suppose we have a model of 10 meters
+        (meter1, meter2, ... meter10). Suppose further meter2
+        and meter3 are 300+ feet from each other, but they
+        are 100 feet from the remaining meters. Let the radius
+        to test against be 200 feet. Since meter2 and meter3
+        are closer to the remaining meters (rather than each other),
+        this function will say that there is continuity because
+        there is an indirect path from meter2 to meter3 through
+        any of the remaining meters.
+
+        Args:
+            radius (float) - A given distance to test whether
+            or not a path exists between any two meters.
+
+        Returns:
+            continuity (bool) - Whether or not the meters are
+            connected to each other.
+        """
+        logger.info(
+            '........................................')
+        logger.info(
+            'Checking to see if paths exist between any {} {} {}'.format(
+                'two meters within', radius, 'feet.'))
+        assert isinstance(radius, float),\
+            'Oops, {} is not a float.'.format(radius)
+        # Grabbing the dataframe that has all the calculated
+        # distances between all meters:
+        dd = self.distance_dataframe.copy()
+        # Reducing the dataframe to meters that have distances
+        # less than or equal to the given radius:
+        reduced_dd = dd[dd.distance <= radius]
+        # Creating sets of the "start" and "end" meters:
+        dd_set1 = set(dd.start.unique())
+        dd_set2 = set(dd.end.unique())
+        r_set1 = set(reduced_dd.start.unique())
+        r_set2 = set(reduced_dd.end.unique())
+        # If the reduced dataframe still has all the
+        # "start" and "end" meters from the original
+        # dataframe, then a path exists between any two
+        # meters in the model because all their distances
+        # are less than or equal to the given radius.
+        # Otherwise, a path doesn't exist and the model
+        # is not continuous.
+        if dd_set1 == r_set1 and dd_set2 == r_set2:
+            logger.info(
+                '\tPaths exists between any two nodes.')
+            logger.info(
+                '\tThus, there is continuity across the network of meters.')
+            continuity = True
+        else:
+            logger.info(
+                '\tThere must not be a path between ANY two nodes.')
+            continuity = False
+        return continuity
+
+    def single_hop_count(self, radius, y):
+        """This function returns the number of meters with y
+        meters within x distance of them.
+
+        For example, suppose we have a model of 10 meters
+        (meter1, meter2, ... meter10). Let y = 2 and let
+        radius = 200 feet. This functions counts how many
+        of meter1 - meter10 have less than or equal to 2
+        meters within 200 feet of them. Suppose meter1, meter2,
+        and meter3 are all 150 feet from each other, while the
+        remaining meters are 300+ feet from them and each other.
+        This function will return 3 because meter1, meter2,
+        and meter3 all have 2 meters or less within 200 feet.
+
+        Args:
+            radius (float) - A given distance to test whether
+            or not a number of meters, y, are within a certain
+            distance of a specified meter as the center.
+
+            y (int) - Specific number of meters near a starting
+            meter.
+
+        Returns:
+            single_hop_count (int) - The total number of meters
+            with a certain number of meters near them within a
+            given distance.
+        """
+        logger.info(
+            '........................................')
+        logger.info(
+            'Counting how many meters have {} meters within {}'.format(
+                y, radius))
+        assert isinstance(radius, float),\
+            'Oops! {} is not a float.'.format(radius)
+        assert isinstance(y, int),\
+            'Oops! {} is not an int.'.format(y)
+        # Grabbing the dataframe that has all the calculated
+        # distances between all meters:
+        dd = self.distance_dataframe.copy()
+        # Checking if the distance is less than or equal to
+        # the given radius:
+        bools = ['T' if d <= radius else 'F' for d in dd.distance]
+        dd['within_radius'] = bools
+        gpd = dd.groupby('start')
+        # Counting how may meters have "y" meters
+        # within a certain radius:
+        gpd_df = gpd['within_radius'].value_counts().to_frame(
+            name='total_count').reset_index()
+        single_hop_count = len(gpd_df[
+            (gpd_df.within_radius == 'T') &
+            (gpd_df.total_count >= y)]['start'].values[:])
+        logger.info(
+            'There are {} meters that have {} meters within {} feet.'.format(
+                single_hop_count, y, radius))
+        return single_hop_count
+
+    def island_count(self, radius):
+        """This function returns the number of islands
+        in the model.
+
+        An island is a group of meters that is not connected
+        to the rest of the meters in the model. Here, we count
+        the number of islands for a given radius. We reduce the
+        model to contain meters whose distance from each other is
+        less than or equal to the given radius.
+
+        For this analysis, it is important to note that a single
+        node is considered as an island.
+
+        Args:
+            radius (float) - A given distance to count how
+            many islands there are in the model.
+
+        Returns:
+            island_count (int) - The number of islands in
+            the model.
+        """
+        assert isinstance(radius, float),\
+            'Oops! {} is not a float.'.format(radius)
+        # Grabbing the dataframe that has all the calculated
+        # distances between all meters:
+        dd = self.distance_dataframe.copy()
+        # Creating a NetworkX object:
+        G = nx.Graph()
+        # Adding data from the dataframe to
+        # the graph:
+        G.add_edges_from(
+            [(u, v, {'distance': d}) for u, v, d in zip(
+                dd['start'], dd['end'], dd['distance'])])
+        for e in G.edges:
+            d = G.get_edge_data(e[0], e[1])['distance']
+            # If the distance between two meters is
+            # greater than the given radius, we remove
+            # the edge from the graph.
+            # Checking if the distance between two
+            # meters is greater than the specified radius:
+            if d > radius:
+                G.remove_edge(e[0], e[1])
+        # Counting the number of connected components,
+        # which we treat as islands:
+        island_count = nx.number_connected_components(G)
+        return island_count
+
+    def all_densities(self, radii):
+        """Calculates all densities for all meters for
+        all radii.
+
+        This function performs the same counting as
+        the meter_density function. However, this is
+        an iterative function, where it treats each of
+        meters in the model as a center and counts how
+        many meters are nearby within a given radius.
+        Also, it does this counting over a range of
+        radii, not just a single radius.
+
+        Args:
+            radii (list) - List of radii (in feet) to count the
+            number of meters near a given meter.
+
+        Returns:
+            dens (list) - List of number of meters near a given
+            meter within each of the radii.
+        """
+        logger.info(
+            '........................................')
+        logger.info(
+            'Counting the number of meters nearby within each radius.')
+        # Grabbing the dataframe that has all the calculated
+        # distances between all meters:
+        dd = self.distance_dataframe.copy()
+        df_list = []
+        for rad in radii:
+            assert isinstance(rad, float),\
+                'Oops! {} is not a float.'.format(rad)
+            # Reducing the dataframe to meters with distances
+            # less than or equal to the given radius:
+            reduced_dd = dd[dd.distance <= rad]
+            # Counting how many meters are within that radius
+            # of each of the meters as the center:
+            gpd_df = reduced_dd.groupby('start')['start'].count()\
+                .to_frame(name='count').reset_index()
+            gpd_df['radius'] = [rad] * len(gpd_df)
+            gpd_df['meter'] = gpd_df['start']
+            df_list.append(gpd_df)
+        # Creating a dataframe of the results to be used
+        # elsewhere:
+        dens_df = pd.concat(df_list, axis=0, ignore_index=True)
+        density_dict = {
+            'model_name': [self.model_name] * len(dens_df['radius']),
+            'feeder': [self.feeder] * len(dens_df['radius']),
+            'meter': dens_df['meter'],
+            'radius': dens_df['radius'],
+            'count': dens_df['count']}
+        dens = dens_df['count']
+        # Saving the results as an attribute:
+        self.dens_df = pd.DataFrame(
+            density_dict, index=range(len(dens)))
+        logger.info(
+            'Finished counting the number of meters nearby.')
+        return dens
+
+    def all_ranges(self):
+        """Calculates all the meter ranges for all meters
+        as the center of each circle.
+
+        This function performs the same calculation as
+        the meter_range function. However, it performs
+        this for all meters in the model where each meter
+        is treated as the center.
+
+        Args:
+            (null)
+
+        Returns:
+            ranges (list) - List of the maximum radius to
+            capture all the meters around a given meter
+            as the center.
+        """
+        logger.info(
+            '........................................')
+        logger.info(
+            'Getting all the maximum radii for each meter as the center.')
+        # Grabbing the dataframe that has all the calculated
+        # distances between all meters:
+        dd = self.distance_dataframe.copy()
+        # Finding the maximum distance to capture all
+        # the meters in the circle around each meter
+        # as the center:
+        gpd_df = dd.groupby('start')['distance'].max().to_frame(
+            name='radius').reset_index()
+        gpd_df['model_name'] = [self.model_name] * len(gpd_df)
+        gpd_df['feeder'] = [self.feeder] * len(gpd_df)
+        ranges = gpd_df['radius']
+        # Saving the results as an attribute to be used
+        # elsewhere:
+        self.range_df = gpd_df
+        logger.info(
+            'Finished getting all the maximum radii for each meter.')
+        return ranges
+
+    def all_isolates(self, radii):
+        """Calculates isolated meter count for all meters for
+        all radii.
+
+        This function does the same counting as the
+        isolated_meter_count function. However, it does
+        this counting for a range of radii, not just
+        a single radius.
+
+        Args:
+            radii (list) - List of radii (in feet) for
+            counting all the isolated meters.
+
+        Returns:
+            iso (list) - List of all the isolated meter counts
+            for each of the radii.
+        """
+        logger.info(
+            '........................................')
+        logger.info(
+            'Getting the isolated meter count for all radii.')
+        # Getting the isolated meter count for each radius:
+        iso_tuples = [
+            (rad, self.isolated_meter_count(rad)) for rad in radii]
+        # Creating a dataframe to be used elsewhere:
+        isolated_dict = {
+            'model_name': [self.model_name] * len(iso_tuples),
+            'feeder': [self.feeder] * len(iso_tuples),
+            'radius': [tup[0] for tup in iso_tuples],
+            'count': [tup[1] for tup in iso_tuples]}
+        iso = [tup[1] for tup in iso_tuples]
+        # Saving results as an attribute:
+        self.iso_df = pd.DataFrame(
+            isolated_dict, index=range(len(iso)))
+        logger.info(
+            'Finished getting the isolated meter count for all radii.')
+        return iso
+
+    def all_continuous(self, radii):
+        """Calculates continuous count for all meters for
+        all radii.
+
+        This function does the same as the meter_continuity
+        function. However, this performs the same analysis
+        for a range of radii, not just a single radius.
+
+        Args:
+            radii (list) - List of radii (in feet) for finding
+            if paths exist between any two meters for each of the
+            radii.
+
+        Returns:
+            conts (list) - List of the booleans of whether or not
+            paths exist between any two nodes within given radii.
+        """
+        logger.info(
+            '........................................')
+        logger.info(
+            'Checking to see if paths exist between any two {}'.format(
+                'nodes within each of the given radii.'))
+        # Checking if the model is continuous across
+        # all radii:
+        conts_tuples = [
+            (rad, self.meter_continuity(rad)) for rad in radii]
+        # Creating a dataframe of the results to be used
+        # elsewhere:
+        continuity_dict = {
+            'model_name': [self.model_name] * len(conts_tuples),
+            'feeder': [self.feeder] * len(conts_tuples),
+            'radius': [tup[0] for tup in conts_tuples],
+            'continuous': [tup[1] for tup in conts_tuples]}
+        conts = [tup[1] for tup in conts_tuples]
+        # Saving the results as an attribute:
+        self.cont_df = pd.DataFrame(
+            continuity_dict, index=range(len(conts)))
+        logger.info(
+            'Finished seeing if paths exist within each of the radii.')
+        return conts
+
+    def all_single_hops(self, radii):
+        """Calculates single hop count for all meters for all
+        radii.
+
+        This function does the same counting as the
+        single_hop_count function. However, it performs
+        this counting for a range of radii, not just a
+        single radius.
+
+        Args:
+            radii (list) - List of radii (in feet) for counting
+            how many neters have a certain number of meters
+            near them.
+
+        Returns:
+            shc (list) - List of all the single hop counts
+            for all the radii.
+        """
+        # Reduced the number of meters nearby to speed up
+        # the calculations:
+        #   NOTE: In reality, it's not likely that
+        #   ALL meters will be near each other. So,
+        #   reducing the amount of values to test is
+        #   for better analysis and realistic results.
+        y_list = [1, 2, 3, 4, 5, 10]
+        logger.info(
+            '........................................')
+        logger.info(
+            'Counting the number of meters that have {}'.format(
+                'a given number of meters nearby within each radius.'))
+        # Grabbing the dataframe that has all the calculated
+        # distances between all meters:
+        dd = self.distance_dataframe.copy()
+        df_list = []
+        for rad in radii:
+            assert isinstance(rad, float),\
+                'Oops! {} is not a float.'.format(rad)
+            # Reducing the dataframe to meters with distances
+            # less than or equal to the given radius:
+            reduced_dd = dd[dd.distance <= rad]
+            # Counting how many meters are withing the given
+            # radius near each of the meters as the center:
+            gpd_df = reduced_dd.groupby('start')['start'].count()\
+                .to_frame(name='total_count').reset_index()
+            gpd_df['radius'] = [rad] * len(gpd_df)
+            df_list.append(gpd_df)
+        dens_df = pd.concat(df_list, axis=0, ignore_index=True)
+        df_list2 = []
+        for y in y_list:
+            assert isinstance(y, int),\
+                'Oops! {} is not an int.'.format(y)
+            # Counting how many meters have at least y meters
+            # nearby:
+            r_df = dens_df[dens_df['total_count'] >= y]
+            gpd_r_df = r_df.groupby('radius')['total_count'].count()\
+                .to_frame(name='count').reset_index()
+            gpd_r_df['y'] = [y] * len(gpd_r_df)
+            df_list2.append(gpd_r_df)
+        # Saving results as a dataframe to be used
+        # elsewhere:
+        shc_df = pd.concat(df_list2, axis=0, ignore_index=True)
+        shc_df['model_name'] = [self.model_name] * len(shc_df)
+        shc_df['feeder'] = [self.feeder] * len(shc_df)
+        shc_df = shc_df.set_index(['model_name', 'feeder']).reset_index()
+        # Saving results as an attribute:
+        self.shc_df = shc_df
+        shc = self.shc_df['count']
+        logger.info('Finished getting all the single hop counts.')
+        return shc
+
+    def all_islands(self, radii):
+        """This function calculates the number of islands in
+        the model with connections between meters being less
+        than or equal to the given radii.
+
+        An island is a group of meters that is not connected
+        to the rest of the meters in the model. Here, we count
+        the number of islands for a given radius. We reduce the
+        model to contain meters who distance from each other is
+        less than or equal to the given radius.
+
+        Args:
+            radii (list) - List of distances to count how
+            many islands there are in the model.
+
+        Returns:
+            islands (list) - The number of islands in
+            the model.
+        """
+        for rad in radii:
+            assert isinstance(rad, float),\
+                'Oops! {} is not a float.'.format(rad)
+        # Getting the island count for each radius:
+        islands_tup = [
+            (rad, self.island_count(rad)) for rad in radii]
+        # Creating a dataframe of the results to be used
+        # elsewhere:
+        island_df = pd.DataFrame({
+            'model_name': [self.model_name] * len(islands_tup),
+            'feeder': [self.feeder] * len(islands_tup),
+            'radius': [tup[0] for tup in islands_tup],
+            'count': [tup[1] for tup in islands_tup]})
+        # Saving the results as an attribute:
+        self.island_df = island_df
+        islands = self.island_df['count']
+        return islands
+
+    def evaluate_system(self, radii):
+        """This function performs all metrics for a given system
+        and returns a weighted, overall score.
+
+        The purpose of this function is to create an overall
+        score to show how well meters can communicate with
+        each other. It takes each of the results of the
+        different metrics and creates a weighted value, similar
+        to a weighted grade for a course in school.
+
+        Weighted Values:
+            Isolated Meter Counts:
+                For our analysis, we want as few isolated meters
+                as possible. The lower the isolated meter count,
+                the better it is for the model. This will show
+                that there are meters close enough to each other
+                to communicate.
+
+                To get the weighted isolated meter count, we find
+                the average of all the isolated meter counts. Then,
+                we divide that value by the total number of meters
+                in the model. The worst case would be that the average
+                is equal to the total number of meters in the system;
+                in other words, all meters are isolated. The best case
+                would be that there are no isolated meters. So, to
+                turn this weighted score into a positive value to show
+                that there are few isolated meters, we take 1 minus
+                the weighted value. Thus, our weighted isolated meter
+                count value is as follows:
+                    iso = 1 - ((sum(counts) / len(counts)) / total_meters)
+
+            Densities:
+                For our analysis, we want as many meters near
+                each other as possible. This will show that
+                the meters can communicate well with each other.
+
+                To get the weighted density count, we find the
+                average of all the density counts. Then, we divide
+                that value by the total number of meters minus 1.
+                The best case is that the average of the density
+                counts is equal to the total number of meters
+                minus 1. That would mean our model is highly
+                dense. The worst case would be the average is
+                0, which would say that our model is not dense
+                at all. So, our weighted density count values
+                is as follows:
+                    dens = (sum(counts) / len(counts)) / total_meters - 1
+
+                2021-03-23: Dividing by maximum density value instead;
+                will come back to this when we get more mony.
+
+            Ranges:
+                For our analysis, we need to know what the ranges
+                for different meters as the center are. This will
+                inform us how spread out the meters are from
+                each other. For now, an extremely large range value
+                is considered as based. However, it is important
+                to note that it is quite possible that meters for
+                a single feeder are spread out from each other in
+                the real world.
+
+                To get a weighted range value, we find the
+                average of all the ranges. Then, we divide that
+                value by the maximum range. The worse case is that
+                the average is equal to the maximum. This tells
+                us that the meters are very spread out from each
+                other. The best case is that the average is very
+                low compared to the maximum. To turn this value
+                into a positive value to show that the meters
+                aren't spread out from each other, we take 1 minus
+                that weighted value. Thus, our weighted range value
+                is as follows:
+                    range = 1 - ((sum(ranges) / len(ranges)) / max(ranges))
+
+            Continuous:
+                For our analysis, we want to know if any two
+                meters can communicate to each other, whether
+                that is diretly or indirectly.
+
+                To get a weighted continuity score, we find the
+                average of the continuity scores. The best case
+                is that the average continuity score would be
+                equal to 1. That would tell us that all meters
+                can communicate to each other, either directly
+                or indirectly. The worst case would be that the
+                average is equal to 0. That would tell us that
+                all the meters cannot communicate to each other,
+                either directly or indirectly. So, the weighted
+                continuity score is as follows:
+                    cont = sum(counts) / len(counts)
+
+            Single Hop Counts:
+                For our analysis, we need to know how many
+                meters have a certain number of meters nearby.
+                This is a form of density calculations. This
+                will tell us, in a way, how many options are
+                available for meters to communicate with other
+                meters.
+
+                To get a weighted single hop count score, we
+                find the average of all single hop counts. Then,
+                we divide by the total number of meters. The
+                best case is when the average is equal to the
+                total number of meters. In a similar vain, this
+                tells us that our model is highly dense. The worst
+                case is when the average is very low. So, our
+                weighted single hop count value is as follows:
+                    shc = (sum(counts) / len(counts)) / total_meters
+
+            Island Counts:
+                For our analysis, we need to know how many
+                islands of meters exist if communication beyond
+                a certain radius is removed.
+
+                To get a weighted island count score, we find
+                the average of all the island counts. Then, we
+                divide by the total number of meters. The best
+                case is when the average is equal to 1; this
+                tells us that all the meters can still communicate
+                with each other. The worse case is when the
+                average is equal to the total number of meters.
+                That tells us that the meters cannot communicate
+                with each other beyond a certain radius, which
+                is similar to the isolated meter count. So, our
+                weighted island count value is as follows:
+                    island = 1 - (sum(islands) / len(islands)) / total_meters
+
+            Final Score:
+                This is the overall score of a power distribution
+                model. This score tells us how well the meters
+                in the model can communicate with each other. The
+                higher the score, the better the meters can
+                communicate with each other.
+
+                The score value is as follows:
+                    score = (0.2 * (dens + iso + cont + shc))\
+                        + (0.1 * (range + island))
+
+        Args:
+            radii (list or array) - List or array of radii for
+            performing the metrics.
+
+        Returns:
+            score (float) - The weighted score of all the metrics
+            for all meters for all radii. It is represented as a
+            percent.
+        """
+        logger.info(
+            '........................................')
+        logger.info(
+            'Caclulating the composite score for the meter network.')
+        # Checking whether or the metric dataframes are empty or not:
+        #   NOTE: If they are empty, then we call the functions to
+        #   fill them out and grab the results. If they're not empty,
+        #   then we just grab the results and perform the calculations.
+        if (self.iso_df.empty or self.dens_df.empty
+                or self.shc_df.empty or self.range_df.empty
+                or self.cont_df.empty):
+            fns = [self.all_isolates, self.all_single_hops,
+                   self.all_continuous, self.all_densities,
+                   self.all_islands]
+            # This speeds up going through the functions by
+            # parallelizing:
+            delayed_fns = [delayed(fn)(radii) for fn in fns]
+            results = Parallel(n_jobs=4, prefer='threads')(delayed_fns)
+            isolates = results[0]
+            single_hops = results[1]
+            continuous = results[2]
+            densities = results[3]
+            islands = results[4]
+            ranges = self.all_ranges()
+        else:
+            isolates = self.iso_df['count'].values
+            single_hops = self.shc_df['count'].values
+            continuous = list(self.cont_df['continuous'].values)
+            densities = self.dens_df['count'].values
+            islands = self.island_df['count'].values
+            range_df = self.range_df
+            ranges = range_df['radius']
+        # Calculating the fractions:
+        meters = self.meters
+        iso_frac = 1 - ((np.sum(isolates) / len(isolates)) / len(meters))
+        shc_frac = (np.sum(single_hops) / len(single_hops)) / len(meters)
+        cont_frac = continuous.count(True) / len(continuous)
+        # 2021-03-23: Trying to divide density by max density for now:
+        dens_frac = (np.sum(densities) / len(densities))\
+            / np.max(densities)
+        # dens_frac = (np.sum(densities) / len(densities)) / (len(meters) - 1)
+        range_frac = 1 - ((np.sum(ranges) / len(ranges)) / np.max(ranges))
+        island_frac = 1 - (np.mean(islands) / len(meters))
+        logger.info(
+            '\tdensity score = {}, isolated score = {}'.format(
+                dens_frac, iso_frac))
+        logger.info(
+            '\tcontinuous score = {}, single hop score = {}'.format(
+                cont_frac, shc_frac))
+        logger.info('\trange score = {}'.format(range_frac))
+        # For now, evenly weighting each of the fractions:
+        score = (0.2 * (dens_frac + iso_frac + shc_frac + cont_frac))\
+            + (0.1 * (range_frac + island_frac))
+        logger.info(
+            'The overall score for this network = {}{}'.format(
+                score, '%'))
+        # Saving the scores:
+        self.composite_score = score
+        self.score_df = pd.DataFrame(
+            {'model_name': self.model_name,
+             'feeder': self.feeder,
+             'composite_score': self.composite_score,
+             'density': dens_frac,
+             'isolated': iso_frac,
+             'single_hop': shc_frac,
+             'continuous': cont_frac,
+             'range': range_frac,
+             'island': island_frac,
+             'number_of_meters': len(self.meters)},
+            index=range(0, 1))
+        return score * 100.0
+
+
+class Results:
+    """This class collects and saves the results from more
+    than one power distribution system evaluation.
+
+    For our analysis, we not only care about the communication
+    between meters for a single model, but many models. This class
+    is for collecting ALL the results from ALL models tested
+    for analysis.
+    """
+
+    def __init__(self):
+        """This initializes the class."""
+        logger.info(
+            '########### Created a Results object for saving data ###########')
+        self.systems = []
+
+    def add(self, system):
+        """This function adds the systems to the Results class.
+
+        Since each system that gets added is an object, all
+        of its attributes are saved (and added) as well.
+
+        Args:
+            system (object) - An EvaluateSystem object that
+            has meter network information and calculated
+            results from metrics performed.
+
+        Returns:
+            (null)
+        """
+        logger.info(
+            'Added {} to the Results object'.format(system.feeder))
+        self.systems.append(system)
+
+    def save(self, output_path, output_name):
+        """This function saves all the results as an hdf5 file.
+
+        Args:
+            output_path (path) - The path to send the results file.
+
+            output_name (str) - The name of the results file.
+
+        Returns:
+            (null)
+        """
+        path = os.path.join(output_path, output_name)
+        # Grabbing all the dataframes from all the systems:
+        densities = [s.dens_df for s in self.systems]
+        ranges = [s.range_df for s in self.systems]
+        isos = [s.iso_df for s in self.systems]
+        continuous = [s.cont_df for s in self.systems]
+        shcs = [s.shc_df for s in self.systems]
+        islands = [s.island_df for s in self.systems]
+        scores = [s.score_df for s in self.systems]
+        logger.info('\t Creating an HDF5 file')
+        # Creating an HDF% store:
+        #   NOTE: This will allow us to save the
+        #   results from all the dataframes into
+        #   one file without running into file size
+        #   issues.
+        store = pd.HDFStore(path)
+        # Combining all the results into single dataframes:
+        dens_df = pd.concat(densities, axis=0, ignore_index=True)
+        range_df = pd.concat(ranges, axis=0, ignore_index=True)
+        iso_df = pd.concat(isos, axis=0, ignore_index=True)
+        cont_df = pd.concat(continuous, axis=0, ignore_index=True)
+        shc_df = pd.concat(shcs, axis=0, ignore_index=True)
+        island_df = pd.concat(islands, axis=0, ignore_index=True)
+        scores_df = pd.concat(scores, axis=0, ignore_index=True)
+        logger.info('Saving all the results to the file')
+        # Adding the dataframes to the HDF5 file:
+        store['density'] = dens_df
+        store['range'] = range_df
+        store['isolated'] = iso_df
+        store['continuous'] = cont_df
+        store['single_hop'] = shc_df
+        store['island'] = island_df
+        store['scores'] = scores_df
+        store.close()
+        logger.info('Finished saving the results to file')
+        logger.info(
+            '########### Finished system evaluation calculations. ###########')
+
+
+class Compare:
+    """This class performs the statistical comparison between
+    the given model(s) and the real data.
+
+    The overall goal of this package is to evaluate how well the
+    model(s) match real-world power distribution systems. One way
+    to do that is using statistics.
+
+    Since our general question asks how well do things match, we
+    chose to use Pearson's correlation and the Kolmogorov-Smirnov
+    test. Both of these tests evaluate how well two sets of
+    data match each other. More information about how these
+    tests are used can be found in the documentation strings
+    of the methods of this class.
+    """
+
+    def __init__(self):
+        """This initializes the class"""
+        self.real_density = None
+        self.real_range = None
+        self.real_isolated = None
+        self.real_continuous = None
+        self.real_single_hop = None
+        self.real_island = None
+        self.real_scores = None
+
+    def load_data(self):
+        """This function loads the real data from file.
+        It will be used for comparing the model(s)
+        """
+        head, tail = os.path.split(os.getcwd())
+        # self.real_density = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'santa_fe', 'santa_fe_results.h5'),
+        #     key='density')
+        # self.real_range = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'santa_fe', 'santa_fe_results.h5'),
+        #     key='range')
+        # self.real_isolated = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'santa_fe', 'santa_fe_results.h5'),
+        #     key='isolated')
+        # self.real_continuous = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'santa_fe', 'santa_fe_results.h5'),
+        #     key='continuous')
+        # self.real_single_hop = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'santa_fe', 'santa_fe_results.h5'),
+        #     key='single_hop')
+        # self.real_island = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'santa_fe', 'santa_fe_results.h5'),
+        #     key='island')
+        # self.real_scores = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'santa_fe', 'santa_fe_results.h5'),
+        #     key='scores')
+        # self.real_density = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'grb', 'grb_results.h5'),
+        #     key='density')
+        # self.real_range = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'grb', 'grb_results.h5'),
+        #     key='range')
+        # self.real_isolated = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'grb', 'grb_results.h5'),
+        #     key='isolated')
+        # self.real_continuous = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'grb', 'grb_results.h5'),
+        #     key='continuous')
+        # self.real_single_hop = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'grb', 'grb_results.h5'),
+        #     key='single_hop')
+        # self.real_island = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'grb', 'grb_results.h5'),
+        #     key='island')
+        # self.real_scores = pd.read_hdf(
+        #     os.path.join(head, 'results',
+        #                  'grb', 'grb_results.h5'),
+        #     key='scores')
+        # Reading in the metric calculation results:
+        grb_density = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'grb', 'grb_results.h5'),
+            key='density')
+        grb_range = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'grb', 'grb_results.h5'),
+            key='range')
+        grb_isolated = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'grb', 'grb_results.h5'),
+            key='isolated')
+        grb_continuous = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'grb', 'grb_results.h5'),
+            key='continuous')
+        grb_single_hop = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'grb', 'grb_results.h5'),
+            key='single_hop')
+        grb_island = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'grb', 'grb_results.h5'),
+            key='island')
+        grb_scores = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'grb', 'grb_results.h5'),
+            key='scores')
+        santa_fe_density = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'santa_fe', 'santa_fe_results.h5'),
+            key='density')
+        santa_fe_range = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'santa_fe', 'santa_fe_results.h5'),
+            key='range')
+        santa_fe_isolated = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'santa_fe', 'santa_fe_results.h5'),
+            key='isolated')
+        santa_fe_continuous = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'santa_fe', 'santa_fe_results.h5'),
+            key='continuous')
+        santa_fe_single_hop = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'santa_fe', 'santa_fe_results.h5'),
+            key='single_hop')
+        santa_fe_island = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'santa_fe', 'santa_fe_results.h5'),
+            key='island')
+        santa_fe_scores = pd.read_hdf(
+            os.path.join(head, 'results',
+                         'santa_fe', 'santa_fe_results.h5'),
+            key='scores')
+        self.real_density = pd.concat(
+            [grb_density, santa_fe_density], axis=0, ignore_index=True)
+        self.real_range = pd.concat(
+            [grb_range, santa_fe_range], axis=0, ignore_index=True)
+        self.real_isolated = pd.concat(
+            [grb_isolated, santa_fe_isolated], axis=0, ignore_index=True)
+        self.real_continuous = pd.concat(
+            [grb_continuous, santa_fe_continuous], axis=0, ignore_index=True)
+        self.real_single_hop = pd.concat(
+            [grb_single_hop, santa_fe_single_hop], axis=0, ignore_index=True)
+        self.real_island = pd.concat(
+            [grb_island, santa_fe_island], axis=0, ignore_index=True)
+        self.real_scores = pd.concat(
+            [grb_scores, santa_fe_scores], axis=0, ignore_index=True)
+
+    def compare_meter_density(self, models, how, radii):
+        """This function compares the meter density distributions
+        between the real data and the model(s).
+
+        To compare the modelled data to the real data, we
+        calculate the Mean Squared Error (MSE) between the
+        two datasets. First, we capture the bar heights of
+        the histogram representatiom of the meter densities
+        per radius. Then, we normalize the bar heights so
+        that the modelled and real data are on the same scale.
+        Lastly, we use scikit-learn's mean_squared_error
+        function to get the MSE.
+
+        Args:
+            models (list) - List of all the models (as pandas
+            dataframes) for comparing to real data.
+
+            region (str) - A region of the United States that the
+            model is representing/coming from
+
+            demographic (str) - Whether the model(s) represent(s)
+            an urban, suburban, and/or a rural feeder.
+
+            how (str) - How we should compare the models to the real
+            data. The options are as follows:
+                'agg' - Choosing how='agg' will compare the model(s)
+                to the real data as a whole, meaning we will compare
+                the model(s) to ALL of the feeders at once in a
+                particular region and demographic.
+
+                'single' - Choosing how='single' will compare the
+                model(s) to the real data individually, meaning we will
+                compare the model(s) to EACH of the feeders, one at a
+                time, in a particular region and demographic.
+
+            radii (list) - List of radii (in feet) to count the
+            number of meters near a given meter.
+
+        Returns:
+            stats_df (pandas dataframe) - A pandas dataframe that
+            contains the results of calculating the MSE. If how
+            equals 'agg', then the dataframe will have the scores
+            based off of comparing the model(s) to the WHOLE real
+            dataset. If how equals 'single', the dataframe will have
+            the scores based off of comparing the model(s) to each of
+            the real feeders in the real data set individually.
+        """
+        model_names = []
+        feeders = []
+        mses = []
+        rads = []
+        # Making sure the models are dataframes and
+        # that they have the correct columns:
+        for m in models:
+            assert isinstance(m, pd.DataFrame),\
+                'Oops! Your model needs to be a pandas DataFrame.'
+            assert 'count' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_densities on your model and {}'.format(
+                    'extract the EvaluateSystem.dens_df dataframe {}'.format(
+                        'for this method to work.'))
+            assert 'radius' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_densities on your model and {}'.format(
+                    'extract the EvaluateSystem.dens_df dataframe {}'.format(
+                        'for this method to work.'))
+        # Checking which way the comparisons
+        # should be done:
+        if how == 'agg':
+            my_list = [models, radii]
+            # Making tuples of the combinations
+            # of models and radii to speed up the
+            # for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real density results:
+            real_dens = self.real_density.copy()
+            for c in combos:
+                m, rad = c[0], c[1]
+                # TODO: modify df1 to accommodate for region and
+                # demographic
+                # Getting the "histogram" bar heights
+                # from the real density results:
+                df1 = real_dens[real_dens['radius'] == rad]
+                bins1 = pd.cut(df1['count'], list(range(0, 21)))
+                gpd1 = df1.groupby(bins1)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                df2 = m[m['radius'] == rad]
+                bins2 = pd.cut(df2['count'], list(range(0, 21)))
+                gpd2 = df2.groupby(bins2)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                model_names.append(m.feeder.unique()[0])
+                mses.append(MSE)
+                rads.append(rad)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'model_feeder': model_names,
+                 'radius': rads,
+                 'mse': mses},
+                index=range(len(model_names)))
+        elif how == 'single':
+            my_list = [models, radii, list(self.real_density.feeder.unique())]
+            # Making tuples of the combinations
+            # of models, real feeders, and radii
+            # to speed up the for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real density results:
+            real_dens = self.real_density.copy()
+            for c in combos:
+                m, rad, f = c[0], c[1], c[2]
+                # Getting the "histogram" bar heights
+                # from the real density results:
+                df1 = real_dens[(real_dens['feeder'] == f)
+                                & (real_dens['radius'] == rad)]
+                bins1 = pd.cut(df1['count'], list(range(0, 21)))
+                gpd1 = df1.groupby(bins1)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                df2 = m[m['radius'] == rad]
+                bins2 = pd.cut(df2['count'], list(range(0, 21)))
+                gpd2 = df2.groupby(bins2)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                model_names.append(m.feeder.unique()[0])
+                feeders.append(f)
+                mses.append(MSE)
+                rads.append(rad)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'real_feeder': feeders,
+                 'model_feeder': model_names,
+                 'radius': rads,
+                 'mse': mses},
+                index=range(len(feeders)))
+        else:
+            logging.error(
+                '{} is an invalid option for the "how" argument; {}'.format(
+                    how, 'it must be "agg" or "single"'))
+        return stats_df
+
+    def compare_meter_range(self, models, how):
+        """This function compares the meter range distributions
+        between the real data and the model(s).
+
+        To compare the modelled data to the real data, we
+        calculate the Mean Squared Error (MSE) between the
+        two datasets. First, we capture the bar heights of
+        the histogram representatiom of the meter ranges.
+        Then, we normalize the bar heights so that the modelled
+        and real data are on the same scale. Lastly, we use
+        scikit-learn's mean_squared_error function to get
+        the MSE.
+
+        Args:
+            models (list) - List of all the models (as pandas
+            dataframes) for comparing to real data.
+
+            region (str) - A region of the United States that the
+            model is representing/coming from
+
+            demographic (str) - Whether the model(s) represent(s)
+            an urban, suburban, and/or a rural feeder.
+
+            how (str) - How we should compare the models to the real
+            data. The options are as follows:
+                'agg' - Choosing how='agg' will compare the model(s)
+                to the real data as a whole, meaning we will compare
+                the model(s) to ALL of the feeders at once in a
+                particular region and demographic.
+
+                'single' - Choosing how='single' will compare the
+                model(s) to the real data individually, meaning we will
+                compare the model(s) to EACH of the feeders, one at a
+                time, in a particular region and demographic
+
+        Returns:
+            stats_df (pandas dataframe) - A pandas dataframe that
+            contains the results of calculating the MSE. If how
+            equals 'agg', then the dataframe will have the scores
+            based off of comparing the model(s) to the WHOLE real
+            dataset. If how equals 'single', the dataframe will have
+            the scores based off of comparing the model(s) to each of
+            the real feeders in the real data set individually.
+        """
+        model_names = []
+        feeders = []
+        mses = []
+        # Making sure the models are dataframes and
+        # that they have the correct columns:
+        for m in models:
+            assert isinstance(m, pd.DataFrame),\
+                'Oops! Your model needs to be a pandas DataFrame.'
+            assert 'radius' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_ranges on your model and {}'.format(
+                    'extract the EvaluateSystem.range_df dataframe {}'.format(
+                        'for this method to work.'))
+        # Checking which way the comparisons
+        # should be done:
+        if how == 'agg':
+            # Grabbing the real range results:
+            real_range = self.real_range.copy()
+            for m in models:
+                df1 = real_range
+                df2 = m
+                # Getting the "histogram" bar heights
+                # from the real range results:
+                bins1 = pd.cut(df1['radius'], bins=list(range(2500, 20001)))
+                gpd1 = df1.groupby(bins1)['radius'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                bins2 = pd.cut(df2['radius'], bins=list(range(2500, 20001)))
+                gpd2 = df2.groupby(bins2)['radius'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                model_names.append(m.feeder.unique()[0])
+                mses.append(MSE)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'model_feeder': model_names,
+                 'mse': mses},
+                index=range(len(model_names)))
+        elif how == 'single':
+            my_list = [models, list(self.real_range.feeder.unique())]
+            # Making tuples of the combinations
+            # of models and real feeders to
+            # speed up the for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real range results:
+            real_range = self.real_range.copy()
+            for c in combos:
+                m, f = c[0], c[1]
+                df1 = real_range
+                df1 = df1[df1.feeder == f]
+                df2 = m
+                # Getting the "histogram" bar heights
+                # from the real range results:
+                bins1 = pd.cut(df1['radius'], bins=list(range(2500, 20001)))
+                gpd1 = df1.groupby(bins1)['radius'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                bins2 = pd.cut(df2['radius'], bins=list(range(2500, 20001)))
+                gpd2 = df2.groupby(bins2)['radius'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                feeders.append(f)
+                model_names.append(m.feeder.unique()[0])
+                mses.append(MSE)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'real_feeder': feeders,
+                 'model_feeder': model_names,
+                 'mse': mses},
+                index=range(len(feeders)))
+        else:
+            logging.error(
+                '{} is an invalid option for the "how" argument; {}'.format(
+                    how, 'it must be "agg" or "single"'))
+        return stats_df
+
+    def compare_isolated_meters(self, models, how, radii):
+        """This function compares the isolated mater counts
+        between the real data and the model(s).
+
+        To compare the modelled data to the real data, we
+        calculate the Mean Squared Error (MSE) between the
+        two datasets. First, we capture the bar heights of
+        the histogram representatiom of the isolated meters
+        per radius. Then, we normalize the bar heights so
+        that the modelled and real data are on the same scale.
+        Lastly, we use scikit-learn's mean_squared_error
+        function to get the MSE.
+
+        Args:
+            models (list) - List of all the models (as pandas
+            dataframes) for comparing to real data.
+
+            region (str) - A region of the United States that the
+            model is representing/coming from
+
+            demographic (str) - Whether the model(s) represent(s)
+            an urban, suburban, and/or a rural feeder.
+
+            how (str) - How we should compare the models to the real
+            data. The options are as follows:
+                'agg' - Choosing how='agg' will compare the model(s)
+                to the real data as a whole, meaning we will compare
+                the model(s) to ALL of the feeders at once in a
+                particular region and demographic.
+
+                'single' - Choosing how='single' will compare the
+                model(s) to the real data individually, meaning we will
+                compare the model(s) to EACH of the feeders, one at a
+                time, in a particular region and demographic
+
+            radii (list) - List of radii (in feet) to count the
+            number of meters near a given meter.
+
+        Returns:
+            stats_df (pandas dataframe) - A pandas dataframe that
+            contains the results of calculating the MSE. If how
+            equals 'agg', then the dataframe will have the scores
+            based off of comparing the model(s) to the WHOLE real
+            dataset. If how equals 'single', the dataframe will have
+            the scores based off of comparing the model(s) to each of
+            the real feeders in the real data set individually.
+        """
+        model_names = []
+        feeders = []
+        mses = []
+        rads = []
+        # Making sure the models are dataframes and
+        # that they have the correct columns:
+        for m in models:
+            assert isinstance(m, pd.DataFrame),\
+                'Oops! Your model needs to be a pandas DataFrame.'
+            assert 'count' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_isolates on your model and {}'.format(
+                    'extract the EvaluateSystem.iso_df dataframe {}'.format(
+                        'for this method to work.'))
+            assert 'radius' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_isolates on your model and {}'.format(
+                    'extract the EvaluateSystem.iso_df dataframe {}'.format(
+                        'for this method to work.'))
+        # Checking which way the comparisons
+        # should be done:
+        if how == 'agg':
+            my_list = [models, radii]
+            # Making tuples of the combinations
+            # of models and radii to speed up the
+            # for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real isolated results:
+            real_isos = self.real_isolated.copy()
+            for c in combos:
+                m, rad = c[0], c[1]
+                # TODO: modify df1 to accommodate for region and
+                # demographic
+                # Getting the "histogram" bar heights
+                # from the real isolated results:
+                df1 = real_isos[real_isos['radius'] == rad]
+                bins1 = pd.cut(df1['count'], list(range(0, 601)))
+                gpd1 = df1.groupby(bins1)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                df2 = m[m['radius'] == rad]
+                bins2 = pd.cut(df2['count'], list(range(0, 601)))
+                gpd2 = df2.groupby(bins2)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                model_names.append(m.feeder.unique()[0])
+                mses.append(MSE)
+                rads.append(rad)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'model_feeder': model_names,
+                 'radius': rads,
+                 'mse': mses},
+                index=range(len(model_names)))
+        elif how == 'single':
+            my_list = [models, radii,
+                       list(self.real_isolated.feeder.unique())]
+            # Making tuples of the combinations
+            # of models, real feeders, and radii
+            # to speed up the for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real isolated results:
+            real_isos = self.real_isolated.copy()
+            for c in combos:
+                m, rad, f = c[0], c[1], c[2]
+                # Getting the "histogram" bar heights
+                # from the real isolated results:
+                df1 = real_isos[(real_isos['feeder'] == f)
+                                & (real_isos['radius'] == rad)]
+                bins1 = pd.cut(df1['count'], list(range(0, 601)))
+                gpd1 = df1.groupby(bins1)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                df2 = m[m['radius'] == rad]
+                bins2 = pd.cut(df2['count'], list(range(0, 601)))
+                gpd2 = df2.groupby(bins2)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                model_names.append(m.feeder.unique()[0])
+                feeders.append(f)
+                mses.append(MSE)
+                rads.append(rad)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'real_feeder': feeders,
+                 'model_feeder': model_names,
+                 'radius': rads,
+                 'mse': mses},
+                index=range(len(feeders)))
+        else:
+            logging.error(
+                '{} is an invalid option for the "how" argument; {}'.format(
+                    how, 'it must be "agg" or "single"'))
+        return stats_df
+
+    def compare_meter_continuity(self, models, how, radii):
+        """This function compares the continuity of meters
+        between the real data and the model(s).
+
+        To compare the modelled data to the real data, we
+        calculate the Mean Squared Error (MSE) between the
+        two datasets. First, we calculate the rate of change
+        of the continuity values as the radius increases.
+        Then, we use scikit-learn's mean_squared_error
+        function to get the MSE between the continuity values.
+
+        Args:
+            models (list) - List of all the models (as pandas
+            dataframes) for comparing to real data.
+
+            region (str) - A region of the United States that the
+            model is representing/coming from
+
+            demographic (str) - Whether the model(s) represent(s)
+            an urban, suburban, and/or a rural feeder.
+
+            how (str) - How we should compare the models to the real
+            data. The options are as follows:
+                'agg' - Choosing how='agg' will compare the model(s)
+                to the real data as a whole, meaning we will compare
+                the model(s) to ALL of the feeders at once in a
+                particular region and demographic.
+
+                'single' - Choosing how='single' will compare the
+                model(s) to the real data individually, meaning we will
+                compare the model(s) to EACH of the feeders, one at a
+                time, in a particular region and demographic
+
+            radii (list) - List of radii (in feet) to count the
+            number of meters near a given meter.
+
+        Returns:
+            stats_df (pandas dataframe) - A pandas dataframe that
+            contains the results of calculating the MSE. If how
+            equals 'agg', then the dataframe will have the scores
+            based off of comparing the model(s) to the WHOLE real
+            dataset. If how equals 'single', the dataframe will have
+            the scores based off of comparing the model(s) to each of
+            the real feeders in the real data set individually.
+        """
+        model_names = []
+        feeders = []
+        mses = []
+        # Making sure the models are dataframes and
+        # that they have the correct columns:
+        for m in models:
+            assert isinstance(m, pd.DataFrame),\
+                'Oops! Your model needs to be a pandas DataFrame.'
+            assert 'continuous' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_continuous on your model {}'.format(
+                    'and extract the EvaluateSystem.cont_df {}'.format(
+                        'dataframe for this method to work.'))
+            assert 'radius' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_continuous on your model {}'.format(
+                    'and extract the EvaluateSystem.cont_df {}'.format(
+                        'dataframe for this method to work.'))
+        # Checking which way the comparisons
+        # should be done:
+        if how == 'agg':
+            # Grabbing the real continuity results:
+            real_continuous = self.real_continuous.copy()
+            for m in models:
+                # TODO: modify df1 to accommodate for region and
+                # demographic
+                df1 = real_continuous
+                df2 = m
+                # Getting the average continuity per radius
+                # for the real data and the model:
+                gpd1 = df1.groupby('radius')['continuous'].mean().reset_index()
+                gpd2 = df2.groupby('radius')['continuous'].mean().reset_index()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(
+                    gpd1['continuous'], gpd2['continuous'])
+                # Appending the results to the lists:
+                model_names.append(df2.feeder.unique()[0])
+                mses.append(MSE)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'model_feeder': model_names,
+                 'mse': mses})
+        elif how == 'single':
+            my_list = [models, list(self.real_continuous.feeder.unique())]
+            # Making tuples of the combinations
+            # of models and real feeders to
+            # speed up the for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real continuity results:
+            real_continuous = self.real_continuous.copy()
+            for c in combos:
+                m, f = c[0], c[1]
+                # TODO: modify df1 to accommodate for region and
+                # demographic
+                df1 = real_continuous
+                df1 = df1[(df1['feeder'] == f)]
+                df2 = m
+                # Getting the average continuity per radius
+                # for the real data and the model:
+                gpd1 = df1.groupby('radius')['continuous'].mean().reset_index()
+                gpd2 = df2.groupby('radius')['continuous'].mean().reset_index()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(
+                    gpd1['continuous'], gpd2['continuous'])
+                # Appending the results to the lists:
+                model_names.append(df2.feeder.unique()[0])
+                feeders.append(f)
+                mses.append(MSE)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'real_feeder': feeders,
+                 'model_feeder': model_names,
+                 'mse': mses})
+        else:
+            logging.error(
+                '{} is an invalid option for the "how" argument; {}'.format(
+                    how, 'it must be "agg" or "single"'))
+        return stats_df
+
+    def compare_single_hop_count(self, models, how, radii):
+        """This function compares the single hop count of
+        meters between real and modelled data.
+
+        To compare the modelled data to the real data, we
+        calculate the Mean Squared Error (MSE) between the
+        two datasets. First, we capture the bar heights of
+        the histogram representatiom of the single hops
+        per radius. Then, we normalize the bar heights so
+        that the modelled and real data are on the same scale.
+        Lastly, we use scikit-learn's mean_squared_error
+        function to get the MSE.
+
+        Args:
+            models (list) - List of all the models (as pandas
+            dataframes) for comparing to real data.
+
+            region (str) - A region of the United States that the
+            model is representing/coming from
+
+            demographic (str) - Whether the model(s) represent(s)
+            an urban, suburban, and/or a rural feeder.
+
+            how (str) - How we should compare the models to the real
+            data. The options are as follows:
+                'agg' - Choosing how='agg' will compare the model(s)
+                to the real data as a whole, meaning we will compare
+                the model(s) to ALL of the feeders at once in a
+                particular region and demographic.
+
+                'single' - Choosing how='single' will compare the
+                model(s) to the real data individually, meaning we will
+                compare the model(s) to EACH of the feeders, one at a
+                time, in a particular region and demographic
+
+            radii (list) - List of radii (in feet) to count the
+            number of meters near a given meter.
+
+        Returns:
+            stats_df (pandas dataframe) - A pandas dataframe that
+            contains the results of calucalting the MSE. If how
+            equals 'agg', then the dataframe will have the scores
+            based off of comparing the model(s) to the WHOLE real
+            dataset. If how equals 'single', the dataframe will have
+            the scores based off of comparing the model(s) to each of
+            the real feeders in the real data set individually.
+        """
+        model_names = []
+        feeders = []
+        mses = []
+        rads = []
+        ys = []
+        # Making sure the models are dataframes and
+        # that they have the correct columns:
+        for m in models:
+            assert isinstance(m, pd.DataFrame),\
+                'Oops! Your model needs to be a pandas DataFrame.'
+            assert 'count' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_single_hops on your model {}'.format(
+                    'extract the EvaluateSystem.shc_df dataframe{}'.format(
+                        'for this method to work.'))
+            assert 'radius' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_single_hops on your model {}'.format(
+                    'extract the EvaluateSystem.shc_df dataframe{}'.format(
+                        'for this method to work.'))
+            assert 'y' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_single_hops on your model {}'.format(
+                    'extract the EvaluateSystem.shc_df dataframe{}'.format(
+                        'for this method to work.'))
+        # Checking which way the comparisons
+        # should be done:
+        if how == 'agg':
+            y = [1.0, 2.0, 3.0, 4.0, 5.0, 10.0]
+            my_list = [models, radii, y]
+            # Making tuples of the combinations
+            # of models, radii, and y to speed
+            # up the for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real single hop results:
+            real_shc = self.real_single_hop
+            for c in combos:
+                m, rad, yi = c[0], c[1], c[2]
+                df1 = real_shc[(real_shc['radius'] == rad)
+                               & (real_shc['y'] == yi)]
+                df2 = m[(m['radius'] == rad) & (m['y'] == yi)]
+                # Getting the "histogram" bar heights
+                # from the real single hop results:
+                bins1 = pd.cut(df1['count'], bins=list(range(0, 2001)))
+                gpd1 = df1.groupby(bins1)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                bins2 = pd.cut(df2['count'], list(range(0, 2001)))
+                gpd2 = df2.groupby(bins2)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                model_names.append(m.feeder.unique()[0])
+                mses.append(MSE)
+                rads.append(rad)
+                ys.append(yi)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'model_feeder': model_names,
+                 'radius': rads,
+                 'y': ys,
+                 'mse': mses},
+                index=range(len(model_names)))
+        elif how == 'single':
+            y = [1.0, 2.0, 3.0, 4.0, 5.0, 10.0]
+            my_list = [models, radii,
+                       y, list(self.real_single_hop.feeder.unique())]
+            # Making tuples of the combinations
+            # of models, radii, real feeders and
+            # y to speed up the for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real single hop results:
+            real_shc = self.real_single_hop
+            for c in combos:
+                m, rad, yi, f = c[0], c[1], c[2], c[3]
+                df1 = real_shc[(real_shc['feeder'] == f)
+                               & (real_shc['radius'] == rad)
+                               & (real_shc['y'] == yi)]
+                df2 = m[(m['radius'] == rad) & (m['y'] == yi)]
+                # Getting the "histogram" bar heights
+                # from the real single hop results:
+                bins1 = pd.cut(df1['count'], bins=list(range(0, 2001)))
+                gpd1 = df1.groupby(bins1)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                bins2 = pd.cut(df2['count'], list(range(0, 2001)))
+                gpd2 = df2.groupby(bins2)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                model_names.append(m.feeder.unique()[0])
+                feeders.append(f)
+                mses.append(MSE)
+                rads.append(rad)
+                ys.append(yi)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'real_feeder': feeders,
+                 'model_feeder': model_names,
+                 'radius': rads,
+                 'y': ys,
+                 'mse': mses},
+                index=range(len(model_names)))
+        return stats_df
+
+    def compare_island_count(self, models, how, radii):
+        """This function compares the island count of
+        meters between real and modelled data.
+
+        To compare the modelled data to the real data, we
+        calculate the Mean Squared Error (MSE) between the
+        two datasets. First, we capture the bar heights of
+        the histogram representatiom of the island counts
+        per radius. Then, we normalize the bar heights so
+        that the modelled and real data are on the same scale.
+        Lastly, we use scikit-learn's mean_squared_error
+        function to get the MSE.
+
+        Args:
+            models (list) - List of all the models (as pandas
+            dataframes) for comparing to real data.
+
+            region (str) - A region of the United States that the
+            model is representing/coming from
+
+            demographic (str) - Whether the model(s) represent(s)
+            an urban, suburban, and/or a rural feeder.
+
+            how (str) - How we should compare the models to the real
+            data. The options are as follows:
+                'agg' - Choosing how='agg' will compare the model(s)
+                to the real data as a whole, meaning we will compare
+                the model(s) to ALL of the feeders at once in a
+                particular region and demographic.
+
+                'single' - Choosing how='single' will compare the
+                model(s) to the real data individually, meaning we will
+                compare the model(s) to EACH of the feeders, one at a
+                time, in a particular region and demographic
+
+            radii (list) - List of radii (in feet) to count the
+            number of meters near a given meter.
+
+        Returns:
+            stats_df (pandas dataframe) - A pandas dataframe that
+            contains the results of calculating the MSE. If how
+            equals 'agg', then the dataframe will have the scores
+            based off of comparing the model(s) to the WHOLE real
+            dataset. If how equals 'single', the dataframe will have
+            the scores based off of comparing the model(s) to each of
+            the real feeders in the real data set individually.
+        """
+        model_names = []
+        feeders = []
+        mses = []
+        rads = []
+        # Making sure the models are dataframes and
+        # that they have the correct columns:
+        for m in models:
+            assert isinstance(m, pd.DataFrame),\
+                'Oops! Your model needs to be a pandas DataFrame.'
+            assert 'count' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_isolates on your model and {}'.format(
+                    'extract the EvaluateSystem.island_df dataframe {}'.format(
+                        'for this method to work.'))
+            assert 'radius' in list(m.columns.unique()),\
+                'Run EvaluateSystem.all_islands on your model and {}'.format(
+                    'extract the EvaluateSystem.island_df dataframe {}'.format(
+                        'for this method to work.'))
+        # Checking which way the comparisons
+        # should be done:
+        if how == 'agg':
+            my_list = [models, radii]
+            # Making tuples of the combinations
+            # of models and radii to speed up the
+            # for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real island results:
+            real_isl = self.real_island.copy()
+            for c in combos:
+                m, rad = c[0], c[1]
+                # TODO: modify df1 to accommodate for region and
+                # demographic
+                # Getting the "histogram" bar heights
+                # from the real island results:
+                df1 = real_isl[real_isl['radius'] == rad]
+                bins1 = pd.cut(df1['count'], list(range(0, 2001)))
+                gpd1 = df1.groupby(bins1)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                df2 = m[m['radius'] == rad]
+                bins2 = pd.cut(df2['count'], list(range(0, 2001)))
+                gpd2 = df2.groupby(bins2)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                model_names.append(m.feeder.unique()[0])
+                mses.append(MSE)
+                rads.append(rad)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'model_feeder': model_names,
+                 'radius': rads,
+                 'mse': mses},
+                index=range(len(model_names)))
+        elif how == 'single':
+            my_list = [models, radii, list(self.real_island.feeder.unique())]
+            # Making tuples of the combinations
+            # of models, real feeders, and radii
+            # to speed up the for loop:
+            combos = list(itertools.product(*my_list))
+            # Grabbing the real island results:
+            real_isl = self.real_island.copy()
+            for c in combos:
+                m, rad, f = c[0], c[1], c[2]
+                # Getting the "histogram" bar heights
+                # from the real island results:
+                df1 = real_isl[(real_isl['feeder'] == f)
+                               & (real_isl['radius'] == rad)]
+                bins1 = pd.cut(df1['count'], list(range(0, 2001)))
+                gpd1 = df1.groupby(bins1)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd1['hist_height'].sum() == 0:
+                    gpd1['normed'] = [0] * len(gpd1)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd1['normed'] = (
+                        gpd1['hist_height'] - gpd1['hist_height'].mean())\
+                        / gpd1['hist_height'].std()
+                # Getting the "histogram" bar heights
+                # from the model results:
+                df2 = m[m['radius'] == rad]
+                bins2 = pd.cut(df2['count'], list(range(0, 2001)))
+                gpd2 = df2.groupby(bins2)['count'].count().to_frame(
+                    name='hist_height').reset_index()
+                if gpd2['hist_height'].sum() == 0:
+                    gpd2['normed'] = [0] * len(gpd2)
+                else:
+                    # Normalizing the histogram bar heights:
+                    gpd2['normed'] = (
+                        gpd2['hist_height'] - gpd2['hist_height'].mean())\
+                        / gpd2['hist_height'].std()
+                # Calculating the mean squared error:
+                MSE = mean_squared_error(gpd1['normed'], gpd2['normed'])
+                # Appending the results to the lists:
+                model_names.append(m.feeder.unique()[0])
+                feeders.append(f)
+                mses.append(MSE)
+                rads.append(rad)
+            # Creating a dataframe of the results:
+            stats_df = pd.DataFrame(
+                {'real_feeder': feeders,
+                 'model_feeder': model_names,
+                 'radius': rads,
+                 'mse': mses},
+                index=range(len(feeders)))
+        else:
+            logging.error(
+                '{} is an invalid option for the "how" argument; {}'.format(
+                    how, 'it must be "agg" or "single"'))
+        return stats_df
diff --git a/src/comms_metrics/requirements.txt b/src/comms_metrics/requirements.txt
new file mode 100755
index 00000000..43f46a35
--- /dev/null
+++ b/src/comms_metrics/requirements.txt
@@ -0,0 +1,12 @@
+networkx
+holoviews
+hvplot
+pandas
+numpy
+xlrd
+datashader
+xarray
+openpyxl
+pyproj
+joblib
+tables
\ No newline at end of file
diff --git a/src/comms_metrics/setup.py b/src/comms_metrics/setup.py
new file mode 100644
index 00000000..f092ba62
--- /dev/null
+++ b/src/comms_metrics/setup.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 30 07:28:23 2021
+
+@author: barn553
+"""
+
+import setuptools
+
+with open("README.md", "r") as fh:
+    long_description = fh.read()
+
+with open("requirements.txt") as fh:
+    requirements = fh.read()
+
+setuptools.setup(
+    name='validation',
+    version='0.0.1',
+    # scripts=['dokr'] ,
+    author="Corinne Roth",
+    author_email="corinne.roth@pnnl.gov",
+    description="Power Distribution System Model Analysis",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    # TODO: get the right url
+    # url="?",
+    packages=setuptools.find_packages(),
+    install_requires=requirements,
+    classifiers=[
+        "Programming Language :: Python :: 3",
+        "License :: OSI Approved :: MIT License",  # ??
+        "Operating System :: OS Independent",
+    ],
+)
diff --git a/src/comms_metrics/system.py b/src/comms_metrics/system.py
new file mode 100755
index 00000000..1739da6b
--- /dev/null
+++ b/src/comms_metrics/system.py
@@ -0,0 +1,373 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jan 15 08:40:05 2021
+
+This module turns a model of a power distribtuion
+system into a graph that can be evaluated.
+
+It has the ability to read in different file formats,
+such as Excel spreadsheets, .csvs, and JSON files. The
+types of files to read in can be expanded to handle
+any type of file format.
+
+For creating a graph, we use NetworkX, which is a
+Python library for graph theory analysis.
+
+In addition, we turn the graph of the model into
+a pandas dataframe for analysis. This allows for
+easy computations.
+
+This module has the capability of creating a plot
+of the model and saving it as a .png. It is possible
+to expand upon different plots to make, but we are
+leaving it as is for now.
+
+@author: barn553
+"""
+
+import pandas as pd
+import os
+import json
+import networkx as nx
+import holoviews as hv
+import hvplot.networkx as hvnx
+from itertools import combinations
+import logging
+import logging.config
+
+# Creating a custom logger
+head, tail = os.path.split(os.getcwd())
+config_file = os.path.join(
+    head, 'results', 'santa_fe', 'config.json')
+with open(config_file, 'r') as file:
+    config = json.load(file)
+    logging.config.dictConfig(config)
+logger = logging.getLogger(__name__)
+
+
+class MeterNetwork:
+    """This class creates a NetworkX representation of the
+    system of power distribution system meters.
+
+    This class has the ability to read in information about
+    the system, create a NetworkX graph, and generate plots
+    of the system.
+
+    This class grabs all the meta data about the model
+    that we care about for analysis purposes. This data
+    includes meter names and their locations. We also
+    care about the name of the model for comparing
+    results across multiple models.
+    """
+
+    def __init__(self):
+        """This initializes the class."""
+        logger.info(
+            '++++++++++ A MeterNetwork object has been created ++++++++++')
+
+    @classmethod
+    def from_excel(self, excel_file, sheet, columns):
+        """This function turns an Excel file into a NetworkX
+        representation of the meter system.
+
+        Args:
+            excel_file (file) - Excel file of the system that
+            contains meter locations, connections between
+            meters, among other information.
+
+            sheet (str) - The sheet of the Excel file with
+            meters' (aka nodes') information in it.
+
+            columns (list) - List of node columns to call for
+            reading in the data, e.g. 'name' or 'id', 'latitude',
+            'longitude', etc. The format assumes the name of the
+            node is first and the location data is second. We only
+            care about the names of the meters and their locations
+            for our analysis; all other information is unnecessary.
+
+        Returns:
+            G (NetworkX graph) - NetworkX graph representation
+            of the meter system.
+        """
+        # Checking to make sure this is a valid file:
+        assert os.path.isfile(excel_file),\
+            'Oops! %r is not a valid Excel file' % excel_file
+        logger.info('Reading in the data from the excel spredsheet')
+        # Reading in the spreadsheet:
+        nodes = pd.read_excel(excel_file, sheet_name=sheet)
+        # Checking if model name and feeder information is
+        # available in the data:
+        if ('model_name' in nodes.columns.unique()
+                and 'feeder' in nodes.columns.unique()):
+            model_name = nodes['model_name'].unique()[0]
+            feeder = nodes['feeder'].unique()[0]
+        else:
+            head1, tail1 = os.path.split(excel_file)
+            feeder = tail1
+            head2, tail2 = os.path.split(head1)
+            model_name = tail2
+        # Grabbing the metadata we care about:
+        #   NOTE: This function assumes that the order of the
+        #   columns is as follows:
+        #       ['name', 'x-value', 'y-value']
+        #   It is VERY important that the x-value comes before
+        #   the y-value, especially with geospatial data. There
+        #   will be errors or incorrect results, otherwise.
+        nodes = nodes[columns]
+        logger.info('Creating a NetworkX representation of the data')
+        # Creating a NetworkX representation of the data:
+        G = nx.Graph()
+        G.add_nodes_from(
+            [(n, {'pos': (a, b)}) for n, a, b, in zip(
+                nodes[columns[0]], nodes[columns[1]], nodes[columns[2]])]
+            )
+        edges = list(combinations(nodes[columns[0]], 2))
+        G.add_edges_from(edges)
+        logger.info('Reformatting the meter position info for plotting')
+        # Grabbing the positional data for plotting:
+        positions = {
+            n: (a, b) for n, a, b in zip(
+                nodes[columns[0]], nodes[columns[1]], nodes[columns[2]])}
+        logger.info('Saving the metadata as attributes of the object')
+        self.meter_nodes = list(G.nodes())
+        self.meter_edges = list(G.edges())
+        self.meter_positions = positions
+        self.model_name = model_name
+        self.feeder = feeder
+        self.graph = G
+        return G
+
+    @classmethod
+    def from_csv(self, csv_file, columns):
+        """This function turns a CSV file into a NetworkX
+        representation of the meter system.
+
+        Args:
+            csv_file (file) - CSV file of the system that
+            contains meter location, meter names, and other
+            information.
+
+            columns (list) - List of node columns to call for
+            reading in the data, e.g. 'name' or 'id', 'latitude',
+            'longitude', etc. The format assumes the name of the
+            node is first and the location data is second.
+
+        Returns:
+            G (NetworkX graph) - NetworkX graph representation
+            of the meter system.
+        """
+        assert os.path.isfile(csv_file),\
+            'Oops! %r is not a valid CSV file.' % csv_file
+        logger.info('Reading in the data from the csv file')
+        # Reading in the .csv file:
+        nodes = pd.read_csv(csv_file)
+        # Checking if model name and feeder information is
+        # available in the data:
+        if ('model_name' in nodes.columns.unique()
+                and 'feeder' in nodes.columns.unique()):
+            model_name = nodes['model_name'].unique()[0]
+            feeder = nodes['feeder'].unique()[0]
+        else:
+            head1, tail1 = os.path.split(csv_file)
+            feeder = tail1
+            head2, tail2 = os.path.split(head1)
+            model_name = tail2
+        # Grabbing the metadata we care about:
+        #   NOTE: This function assumes that the order of the
+        #   columns is as follows:
+        #       ['name', 'x-value', 'y-value']
+        #   It is VERY important that the x-value comes before
+        #   the y-value, especially with geospatial data. There
+        #   will be errors or incorrect results, otherwise.
+        nodes = nodes[columns]
+        logger.info('Creating a NetworkX representation of! the data')
+        # Creating a NetorkX representation of the data:
+        G = nx.Graph()
+        edges = list(combinations(nodes[columns[0]], 2))
+        G.add_edges_from(edges)
+        G.add_nodes_from(
+            [(n, {'pos': (a, b)}) for n, a, b, in zip(
+                nodes[columns[0]], nodes[columns[1]], nodes[columns[2]])]
+            )
+        logger.info('Reformatting the meter position info for plotting')
+        # Grabbing the positional data for plotting:
+        positions = {
+            n: (a, b) for n, a, b in zip(
+                nodes[columns[0]], nodes[columns[1]], nodes[columns[2]])}
+        logger.info('Saving the metadata as attributes of the object')
+        self.meter_nodes = list(G.nodes())
+        self.meter_edges = list(G.edges())
+        self.meter_positions = positions
+        self.model_name = model_name
+        self.feeder = feeder
+        self.graph = G
+        return G
+
+    @classmethod
+    def from_json(self, json_file, key_list, position_labels):
+        """This function turns a JSON file into a NetworkX
+        representation of the meter system.
+
+        Args:
+            json_file (file) - JSON file of the system that
+            contains meter locations, connections between
+            meters, and other information.
+
+            key_list (list) - List of the keys from the JSON file.
+            They should be something like 'name', 'id', etc.
+            This function assumes the first key listed is the key
+            that has the names/ids of the meters.
+
+            position_labels (list) - List of the meter position keys
+            in the JSON, e.g. 'lat' and 'lon', 'latitude' and 'longitude',
+            'x' and 'y', etc. This function assumes the first element
+            is the 'longitude' element.
+
+        Returns:
+            G (NetworkX graph) - NetworkX graph reprsentation
+            of the meter system.
+        """
+        assert os.path.isfile(json_file),\
+            'Oops! %r is not a valid JSON' % json_file
+        logger.info('Reading in the data from the JSON file')
+        # Reading in the JSON file:
+        with open(json_file, 'r') as file:
+            data = json.load(file)
+        # Checking to see if model name and feeder are in
+        # the data:
+        if ('model_name' in data.keys() and 'feeder' in data.keys()):
+            model_name = data['model_name']
+            feeder = data['feeder']
+        else:
+            head1, tail1 = os.path.split(json_file)
+            feeder = tail1[0:-5]
+            head2, tail2 = os.path.split(head1)
+            model_name = tail2
+        # Grabbing the data we care about:
+        #   NOTE: This function assumes the first
+        #   key is the key that contains all the meters.
+        nodes = data[key_list[0]]
+        logger.info('Creating a NetworkX representation of the data')
+        # Creating a NetworkX representation of the data:
+        G = nx.Graph()
+        edges = list(combinations(list(nodes.keys()), 2))
+        G.add_edges_from(edges)
+        G.add_nodes_from(
+            [(n, {'pos': (
+                nodes[n][position_labels[0]], nodes[n][position_labels[1]])})
+                for n in list(nodes.keys())])
+        logger.info('Reformatting the meter position info for plotting')
+        # Grabbing the positional data for plotting:
+        positions = {
+            n: (nodes[n][position_labels[0]],
+                nodes[n][position_labels[1]]) for n in list(nodes.keys())}
+        # logger.info('Saving the metadata as attributes of the object')
+        self.meter_nodes = list(nodes.keys())
+        self.meter_edges = list(G.edges())
+        self.meter_positions = positions
+        self.model_name = model_name
+        self.feeder = feeder
+        self.graph = G
+        return G
+
+    @classmethod
+    def make_dataframe(self):
+        """This function turns the meter graph into a pandas
+        dataframe. This allows for ease and the ability to
+        perform metrics calculations and validation of the
+        system as a whole.
+
+        Args:
+            (null)
+
+        Returns:
+            dataframe (pandas dataframe) - Pandas datafrane
+            representation of the meter names, locations, and
+            other information.
+        """
+        logger.info(
+            'Grabbing the meter names and positions from the network')
+        # Grabbing the meter names and positional information:
+        name = [n for n in self.graph.nodes()]
+        pos = nx.get_node_attributes(self.graph, 'pos')
+        pos_x = [pos[n][0] for n in self.graph.nodes()]
+        pos_y = [pos[n][1] for n in self.graph.nodes()]
+        logger.info(
+            'Creating a dataframe of the meter network')
+        # Creating a dataframe:
+        dataframe = pd.DataFrame(
+            {'model_name': [self.model_name] * len(name),
+             'feeder': [self.feeder] * len(name),
+             'name': name,
+             'pos_x': pos_x,
+             'pos_y': pos_y},
+            index=range(0, len(self.graph.nodes())))
+        logger.info(
+            'Finished creating a dataframe for {};'.format(
+                self.feeder))
+        logger.info(
+            'it has feeder info, meters, and meter positions.')
+        logger.info(
+            'Now, we evaluate {}'.format(self.feeder))
+        return dataframe
+
+    @classmethod
+    def plot_graph(self, nodes, node_args, edges, edge_args, plot_positions,
+                   opts_dict, output_dir, save_plot=True):
+        """This function creates a visual representation
+        of the meter system using Holoviews NetworkX
+        drawing capabilities.
+
+        Args:
+            nodes (list) - List of the nodes in the NetworkX representation
+            of the meter system.
+
+            node_args (dict) - Dictionary of data of how to draw the
+            graph. Things to consider include node color, node size, node
+            labels, etc. Visit
+            http://holoviews.org/user_guide/Network_Graphs.html for more
+            examples and suggestions.
+
+            edges (list) - List of the edges that connect the meters.
+
+            edge_args (dict) - Dictionary of the meter connections
+            within the graph. Things to consider include edge color,
+            edge size, etc. See the above link for more information.
+
+            plot_positions (dict) - Dictionary of the meter
+            positions within the graph. If they are not provided or
+            updated, the plot_positions will be the default positions
+            after instantiation of the MeterNetwork object.
+
+            opts_dict (dict) - Dictionary of how the final plot
+            should look. Things to consider include title, font size,
+            the height and width of the plot, etc.
+
+            output_dir (directory) - Directory to send the final
+            plot of the graph.
+
+            save_plot (bool) - Boolean of whether or not to save the
+            plot generated. Default is 'True'.
+
+        Returns:
+            (null)
+        """
+        logger.info(
+            'Creating a NetworkX representation of the meter network.')
+        # Creating a NetworkX representation of the meter network:
+        G = nx.Graph()
+        G.add_nodes_from(nodes)
+        G.add_edges_from(edges)
+        logger.info('Adding node (aka meter) positions.')
+        # "Drawing" it:
+        nodes = hvnx.draw_networkx_nodes(
+            G, pos=plot_positions, **node_args)
+        logger.info(
+            'Adding edges (abstract connections between nodes')
+        edges = hvnx.draw_networkx_edges(
+            G, pos=plot_positions, **edge_args)
+        plot = (nodes * edges).opts(**opts_dict)
+        # Saving it:
+        if save_plot is True:
+            hv.save(plot, '{}.png'.format(output_dir), fmt='png')
+            logger.info('Saving the graph as a picture.')
diff --git a/src/comms_metrics/utils.py b/src/comms_metrics/utils.py
new file mode 100755
index 00000000..1afcce64
--- /dev/null
+++ b/src/comms_metrics/utils.py
@@ -0,0 +1,259 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Thu Feb  4 07:01:52 2021
+
+The purpose of this module is to provide some
+miscellaneous funtionality for data pre-processing.
+This will help the analysis efforts of power
+distribution system models.
+
+This module provides the ability to parse JSON, .dss,
+.txt files for reformatting the power distribution
+models data. These files should have feeder and meter
+information, along with the meters' locations.
+
+The functions below turn the data into a pandas dataframe.
+A pandas dataframe is similar in structure and format
+to an Excel spreadsheet. This allows for easy data
+and statistical analysis for evaluating and comparing
+models of power distribution systems.
+
+This module can be expanded to include other
+data processing functions should the need arise.
+
+
+@author: barn553
+"""
+
+import os
+import pandas as pd
+import json
+
+
+def parse_json_file(json_file, out_path, out_file_name):
+    """This function turns a JSON file representation of
+    GLD power distribution system model into a dataframe
+    of the billing meter names and positions.
+
+    Args:
+        json_file (JSON file) - JSON file of the GLD power
+        distribution system model.
+
+        out_path (path) - Path to send the dataframe. Saves
+        the dataframe as a csv.
+
+        out_file_name (str) - Name of the output file.
+
+    Returns:
+        (null)
+    """
+    # Reading in the JSON file:
+    with open(json_file, 'r') as file:
+        # We use json.load NOT .loads because we're looking at
+        # a JSON file, NOT a JSON object.
+        data = json.load(file)
+    # There are other types of meters. For our analysis, we
+    # only care about the billing meters. So, we go through all
+    # the (key, value) pairs in the JSON and grab the billing meters
+    # only.
+    names = [n['id'] for n in data['nodes'] if n['nclass'] == 'billing_meter']
+    # For our analysis, we care about how far away meters are from
+    # each other. So, we go through the (key, value) pairs of
+    # the JSON and grab the positional information for each of
+    # the billing meters.
+    pos_x = [n['ndata']['x'] for n in data['nodes']
+             if n['nclass'] == 'billing_meter']
+    pos_y = [n['ndata']['y'] for n in data['nodes']
+             if n['nclass'] == 'billing_meter']
+    # The path/folder names provide model and feeder information.
+    # We need this information for making comparisons across
+    # feeders and different power distribution models. So, we grab
+    # parts of the path to save that information in our dataframe.
+    head1, tail1 = os.path.split(json_file)
+    feeder = tail1[:-5]
+    head2, tail2 = os.path.split(head1)
+    model_name = tail2
+    # Creating the dataframe after collecting all the necessary
+    # data:
+    #   NOTE: For this to work, every list/array has to be the same
+    #   length; otherwise there will be an error.
+    df = pd.DataFrame(
+        {'model_name': [model_name] * len(names),
+         'feeder': [feeder] * len(names),
+         'name': names,
+         'pos_x': pos_x,
+         'pos_y': pos_y},
+        index=range(len(names)))
+    # Saving the reformatted data as a .csv file.
+    # This function can be expanded to have the option
+    # of saving in different file types.
+    df.to_csv(r'{}/{}.csv'.format(out_path, out_file_name))
+
+
+def parse_dss_file(load_file, coord_file, out_path, out_file_name):
+    """This function parses the lines of a file
+    and saves them as a csv file.
+
+    Args:
+        load_file (str) - Name of the load file. This dss file has
+        bus and load data, among other data. We use this to pair
+        its data with the correct coordinates.
+
+        coord_file (str) - Name of the coordinate file. This
+        contains the coordinates for the bus(es).
+
+        out_path (path) - Path to send the reformatted data file(s)
+
+        out_file_name (str) - Name to give the data file(s).
+
+    Returns:
+        (null)
+    """
+    # Reading in the Load file:
+    #   NOTE: This can be parsed/treated like a .txt file.
+    with open(load_file, 'r') as load:
+        # In the .dss files, there is one line of data per
+        # bus. So, we read in line of the file as one
+        # unique string object.
+        load_data = load.readlines()
+    load_name = []
+    # Grabbing th bus information:
+    for i, line in enumerate(load_data):
+        line_list = line.split(' ')
+        # There are multiple pieces of information in each
+        # line of the .dss file. We only care about the bus
+        # information. So, we read through each line as a
+        # single string object and grab the bus information.
+        for ll in line_list:
+            if 'bus' in ll:
+                # The name of the bus, which is what we care about,
+                # precedes the '.2.3' or '.2'. So, we grab the part
+                # of the string before those numbers and dots.
+                if '.2.3' in ll:
+                    load_name.append(ll[5:-6])
+                else:
+                    load_name.append(ll[5:-4])
+    # Creating a dataframe of the load names:
+    #   NOTE: The names from the load file are what we care
+    #   about. The coordinate file has more names than what
+    #   we care about. So, we create a dataframe of the names
+    #   to merge the load file with the coordinate file.
+    load_df = pd.DataFrame(
+        load_name, columns=['name'], index=range(len(load_name)))
+    # Reading in the Coordinate file:
+    with open(coord_file, 'r') as coord:
+        # Each line represents a unique load, so we read
+        # them in as an individual string.
+        coord_data = coord.readlines()
+    names = []
+    lons = []
+    lats = []
+    # Grabbing the name, longitude, and latitude data:
+    for j, lne in enumerate(coord_data):
+        # Each line in the file has three pieces of information
+        # separated by a space. The order of these pieces is as
+        # follows: name of the load, longitude, and latitude. We
+        # grab each of these and add them to our lists.
+        lne_list = lne.split(' ')
+        names.append(lne_list[0])
+        lons.append(lne_list[1])
+        lats.append(lne_list[2][0:-1])
+    # The path/folder names provide model and feeder information.
+    # We need this information for making comparisons across
+    # feeders and different power distribution models. So, we grab
+    # parts of the path to save that information in our dataframe.
+    head, tail = os.path.split(out_path)
+    model_name = tail
+    feeder = out_file_name
+    # Creating a dataframe of the location data:
+    #   NOTE: The names from the load file are contained in the
+    #   coordinate file, but there are more names than what we care
+    #   about. So, we create a dataframe of the coordinate data.
+    #   Then, we get the intersection of the load data and coordinate
+    #   data to get the final set of data we need for analysis in
+    #   one dataframe.
+    loc_df = pd.DataFrame(
+        {'model_name': [model_name] * len(names),
+         'feeder': [feeder] * len(names),
+         'name': names,
+         'lat': lats,
+         'lon': lons}, index=range(len(names)))
+    # Merging the two dataframes into one to get the final set
+    # of data:
+    final_df = pd.merge(load_df, loc_df, on='name', how='inner')
+    final_df = final_df.drop_duplicates().set_index(
+        ['model_name', 'feeder']).reset_index()
+    # Saving the reformatted data as a .csv file.
+    # This function can be expanded to have the option
+    # of saving in different file types.
+    final_df.to_csv(r'{}/{}.csv'.format(
+        os.path.join(out_path, 'csvs'), out_file_name))
+
+
+if __name__ == '__main__':
+    # The purpose of this '__main__' stuff is to test
+    # the above functions.
+    head, tail = os.path.split(os.getcwd())
+    data_path = os.path.join(head, 'results')
+    # Uncomment the following lines if we need to run this
+    # script again for each of the models.
+    for root, dirs, files in os.walk(os.path.join(data_path, 'grb')):
+        print('root:', root)
+        root_head, root_tail = os.path.split(root)
+        print(root_head)
+        print(root_tail)
+        for file in files:
+            print('\tfile:', file)
+            if 'Loads' in file or 'Long' in file:
+                tamu_data_path = os.path.join(root_head, root_tail)
+                load_file = os.path.join(
+                    tamu_data_path, 'Loads.dss')
+                coord_file = os.path.join(
+                    tamu_data_path, 'Long_lat_buscoords.txt')
+                parse_dss_file(
+                    load_file, coord_file, root_head, root_tail)
+        print('')
+    # for root, dirs, files in os.walk(os.path.join(data_path, 'tamu')):
+    #     print('root:', root)
+    #     root_head, root_tail = os.path.split(root)
+    #     print(root_head)
+    #     print(root_tail)
+    #     for file in files:
+    #         print('\tfile:', file)
+    #         if 'Loads' in file or 'Long' in file:
+    #             tamu_data_path = os.path.join(root_head, root_tail)
+    #             load_file = os.path.join(
+    #                 tamu_data_path, 'Loads.dss')
+    #             coord_file = os.path.join(
+    #                 tamu_data_path, 'Long_lat_buscoords.txt')
+    #             parse_dss_file(
+    #                 load_file, coord_file, root_head, root_tail)
+    #     print('')
+    # for root, dirs, files in os.walk(os.path.join(data_path, 'santa_fe')):
+    #     print('root:', root)
+    #     root_head, root_tail = os.path.split(root)
+    #     print(root_head)
+    #     print(root_tail)
+    #     for file in files:
+    #         print('\tfile:', file)
+    #         if 'Loads' in file or 'Long' in file:
+    #             santa_fe = os.path.join(root_head, root_tail)
+    #             load_file = os.path.join(
+    #                 santa_fe, 'Loads.dss')
+    #             coord_file = os.path.join(
+    #                 santa_fe, 'Long_lat_buscoords.txt')
+    #             parse_dss_file(
+    #                 load_file, coord_file, root_head, root_tail)
+    #     print('')
+    # for root, dirs, files in os.walk(os.path.join(head, 'results', 'gld')):
+    #     # print('root:', root)
+    #     for file in files:
+    #         h, t = os.path.split(file)
+    #         if '.json' in t:
+    #             json_file = os.path.join(root, file)
+    #             # print(json_file)
+    #             parse_json_file(
+    #                 json_file, os.path.join(root), t[0:-5])
+    #         else:
+    #             pass
+    #     print('')
diff --git a/src/jupyter/.ipynb_checkpoints/TESP_demo-checkpoint.ipynb b/src/jupyter/.ipynb_checkpoints/TESP_demo-checkpoint.ipynb
new file mode 100644
index 00000000..be24d1d8
--- /dev/null
+++ b/src/jupyter/.ipynb_checkpoints/TESP_demo-checkpoint.ipynb
@@ -0,0 +1,1074 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n",
+      "Generator Dictionary:\n",
+      "Unit Bus Type Pnom Pmax Costs[Start Stop C2 C1 C0]\n",
+      "1 1 swing 0.0 247.0 [ 500.0 0.0 0.03 1.3 180.0 ]\n",
+      "2 2 pv 163.0 192.0 [ 2000.0 0.0 0.085 3.0 100.0 ]\n",
+      "3 3 pv 85.0 128.0 [ 3000.0 0.0 0.122 5.0 135.0 ]\n",
+      "4 9 pq 0.0 250.0 [ 3000.0 0.0 3.0 50.0 1500.0 ]\n",
+      "There are 577 sample times at 300 second intervals\n",
+      "{'coal': [2074.2012999999997, 1.0090000000000001, 0.6053999999999999], 'gas_combinedcycle': [898.0036, 0.00767, 0.057525], 'gas_singlecycle': [1331.1996, 0.01137, 0.08527499999999999]}\n",
+      "genfuel type hydro  has zero emission.\n",
+      "bus metrics:\n",
+      " <xarray.Dataset>\n",
+      "Dimensions:  (busNum: 1, time: 577)\n",
+      "Coordinates:\n",
+      "  * busNum   (busNum) int32 7\n",
+      "  * time     (time) float64 0.0 0.08333 0.1667 0.25 0.3333 0.4167 0.5 0.5833 ...\n",
+      "Data variables:\n",
+      "    LMP_P    (busNum, time) float64 0.01449 0.01665 0.01626 0.01613 0.0161 ...\n",
+      "    PD       (busNum, time) float64 100.0 150.4 140.8 139.1 138.8 139.0 ...\n",
+      "    VMAG     (busNum, time) float64 1.05 1.05 1.05 1.05 1.05 1.05 1.05 1.05 ...\n",
+      "Attributes:\n",
+      "    bus_info_dict:  {'7': {'GLDsubstations': ['SUBSTATION7'], 'ampFactor': 40...\n"
+     ]
+    }
+   ],
+   "source": [
+    "import os\n",
+    "import sys\n",
+    "module_path = os.path.abspath(os.path.join('..'))\n",
+    "if module_path not in sys.path:\n",
+    "    sys.path.append(module_path)\n",
+    "\n",
+    "from TransmissionMetricsProcessor_demo import TransmissionMetricsProcessor\n",
+    "\n",
+    "import matplotlib.pyplot as plt\n",
+    "\n",
+    "\n",
+    "casepath=r'''C:\\Qiuhua\\FY2016_Project_Transactive_system\\Simulation_Year1\\TE_Challenge'''\n",
+    "\n",
+    "colors = {0:'k', 1: 'b', 2:'r', 3: 'g',4: 'm',5:'y'}\n",
+    "time_interval_hours = 5/60;\n",
+    "\n",
+    "\n",
+    "casename = \"TE_Challenge\" # 'SGIP1a'\n",
+    "tmp = TransmissionMetricsProcessor()\n",
+    "\n",
+    "tmp.loadAllMetricsFromJSONFiles(casename, casepath+\"\\\\\")\n",
+    "\n",
+    "time_interval_hours = tmp.get_time_interval_hours()\n",
+    "\n",
+    "all_bus_metrics = tmp.get_bus_metrics()\n",
+    "all_gen_metrics = tmp.get_gen_metrics()\n",
+    "\n",
+    "print('bus metrics:\\n',all_bus_metrics)\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "<xarray.Dataset>\n",
+       "Dimensions:  (busNum: 1, time: 456)\n",
+       "Coordinates:\n",
+       "  * busNum   (busNum) int32 7\n",
+       "  * time     (time) float64 10.08 10.17 10.25 10.33 10.42 10.5 10.58 10.67 ...\n",
+       "Data variables:\n",
+       "    LMP_P    (busNum, time) float64 0.02474 0.02608 0.02548 0.027 0.02573 ...\n",
+       "    PD       (busNum, time) float64 212.1 222.1 220.0 228.6 221.7 227.3 ...\n",
+       "    VMAG     (busNum, time) float64 1.05 1.05 1.05 1.05 1.05 1.05 1.05 1.05 ...\n",
+       "Attributes:\n",
+       "    bus_info_dict:  {'7': {'GLDsubstations': ['SUBSTATION7'], 'ampFactor': 40..."
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "## show how to slice the dataset \n",
+    "\n",
+    "all_bus_metrics.where(all_bus_metrics.time > 10,drop=True)\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th>LMP_P</th>\n",
+       "      <th>PD</th>\n",
+       "      <th>VMAG</th>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>busNum</th>\n",
+       "      <th>time</th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th rowspan=\"61\" valign=\"top\">7</th>\n",
+       "      <th>0.000000</th>\n",
+       "      <td>0.014493</td>\n",
+       "      <td>100.00000</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.083333</th>\n",
+       "      <td>0.016655</td>\n",
+       "      <td>150.43260</td>\n",
+       "      <td>1.049995</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.166667</th>\n",
+       "      <td>0.016256</td>\n",
+       "      <td>140.79400</td>\n",
+       "      <td>1.049997</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.250000</th>\n",
+       "      <td>0.016130</td>\n",
+       "      <td>139.11644</td>\n",
+       "      <td>1.049997</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.333333</th>\n",
+       "      <td>0.016102</td>\n",
+       "      <td>138.77844</td>\n",
+       "      <td>1.049997</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.416667</th>\n",
+       "      <td>0.016154</td>\n",
+       "      <td>138.96244</td>\n",
+       "      <td>1.049997</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.500000</th>\n",
+       "      <td>0.016335</td>\n",
+       "      <td>144.99000</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.583333</th>\n",
+       "      <td>0.016305</td>\n",
+       "      <td>143.96600</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.666667</th>\n",
+       "      <td>0.016340</td>\n",
+       "      <td>144.07980</td>\n",
+       "      <td>1.049997</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.750000</th>\n",
+       "      <td>0.016264</td>\n",
+       "      <td>144.12980</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.833333</th>\n",
+       "      <td>0.016250</td>\n",
+       "      <td>143.07460</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.916667</th>\n",
+       "      <td>0.016600</td>\n",
+       "      <td>151.45700</td>\n",
+       "      <td>1.049996</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.000000</th>\n",
+       "      <td>0.016282</td>\n",
+       "      <td>145.05840</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.083333</th>\n",
+       "      <td>0.016294</td>\n",
+       "      <td>144.94940</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.166667</th>\n",
+       "      <td>0.016328</td>\n",
+       "      <td>145.00760</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.250000</th>\n",
+       "      <td>0.016260</td>\n",
+       "      <td>144.78460</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.333333</th>\n",
+       "      <td>0.016249</td>\n",
+       "      <td>144.41080</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.416667</th>\n",
+       "      <td>0.015864</td>\n",
+       "      <td>135.81204</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.500000</th>\n",
+       "      <td>0.015834</td>\n",
+       "      <td>135.88004</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.583333</th>\n",
+       "      <td>0.015860</td>\n",
+       "      <td>135.15204</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.666667</th>\n",
+       "      <td>0.015825</td>\n",
+       "      <td>135.07504</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.750000</th>\n",
+       "      <td>0.015878</td>\n",
+       "      <td>135.81404</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.833333</th>\n",
+       "      <td>0.015865</td>\n",
+       "      <td>135.60504</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.916667</th>\n",
+       "      <td>0.015760</td>\n",
+       "      <td>134.47424</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.000000</th>\n",
+       "      <td>0.015791</td>\n",
+       "      <td>134.40736</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.083333</th>\n",
+       "      <td>0.015780</td>\n",
+       "      <td>134.51936</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.166667</th>\n",
+       "      <td>0.015828</td>\n",
+       "      <td>136.60292</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.250000</th>\n",
+       "      <td>0.015834</td>\n",
+       "      <td>136.77704</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.333333</th>\n",
+       "      <td>0.015746</td>\n",
+       "      <td>134.83852</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.416667</th>\n",
+       "      <td>0.015836</td>\n",
+       "      <td>135.84280</td>\n",
+       "      <td>1.049998</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>45.583333</th>\n",
+       "      <td>0.021482</td>\n",
+       "      <td>178.67200</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>45.666667</th>\n",
+       "      <td>0.020794</td>\n",
+       "      <td>172.31280</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>45.750000</th>\n",
+       "      <td>0.022309</td>\n",
+       "      <td>187.73000</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>45.833333</th>\n",
+       "      <td>0.020949</td>\n",
+       "      <td>177.91720</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>45.916667</th>\n",
+       "      <td>0.022342</td>\n",
+       "      <td>191.94360</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.000000</th>\n",
+       "      <td>0.022244</td>\n",
+       "      <td>189.37640</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.083333</th>\n",
+       "      <td>0.021925</td>\n",
+       "      <td>189.43760</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.166667</th>\n",
+       "      <td>0.021553</td>\n",
+       "      <td>186.48880</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.250000</th>\n",
+       "      <td>0.021446</td>\n",
+       "      <td>185.52400</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.333333</th>\n",
+       "      <td>0.020711</td>\n",
+       "      <td>179.98320</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.416667</th>\n",
+       "      <td>0.020490</td>\n",
+       "      <td>178.65800</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.500000</th>\n",
+       "      <td>0.019945</td>\n",
+       "      <td>174.35240</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.583333</th>\n",
+       "      <td>0.019529</td>\n",
+       "      <td>170.80280</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.666667</th>\n",
+       "      <td>0.019630</td>\n",
+       "      <td>171.90000</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.750000</th>\n",
+       "      <td>0.020558</td>\n",
+       "      <td>182.12120</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.833333</th>\n",
+       "      <td>0.019379</td>\n",
+       "      <td>171.02800</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>46.916667</th>\n",
+       "      <td>0.019708</td>\n",
+       "      <td>176.50440</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.000000</th>\n",
+       "      <td>0.019286</td>\n",
+       "      <td>171.01040</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.083333</th>\n",
+       "      <td>0.019924</td>\n",
+       "      <td>179.08640</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.166667</th>\n",
+       "      <td>0.019345</td>\n",
+       "      <td>174.75120</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.250000</th>\n",
+       "      <td>0.018938</td>\n",
+       "      <td>169.99240</td>\n",
+       "      <td>1.050000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.333333</th>\n",
+       "      <td>0.018538</td>\n",
+       "      <td>167.97840</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.416667</th>\n",
+       "      <td>0.017962</td>\n",
+       "      <td>162.27320</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.500000</th>\n",
+       "      <td>0.018385</td>\n",
+       "      <td>168.09600</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.583333</th>\n",
+       "      <td>0.016690</td>\n",
+       "      <td>149.41680</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.666667</th>\n",
+       "      <td>0.016827</td>\n",
+       "      <td>152.27600</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.750000</th>\n",
+       "      <td>0.017827</td>\n",
+       "      <td>162.34080</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.833333</th>\n",
+       "      <td>0.016463</td>\n",
+       "      <td>143.73240</td>\n",
+       "      <td>1.049996</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>47.916667</th>\n",
+       "      <td>0.017406</td>\n",
+       "      <td>159.01400</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>48.000000</th>\n",
+       "      <td>0.018013</td>\n",
+       "      <td>166.33400</td>\n",
+       "      <td>1.049999</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>577 rows × 3 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "                     LMP_P         PD      VMAG\n",
+       "busNum time                                    \n",
+       "7      0.000000   0.014493  100.00000  1.049998\n",
+       "       0.083333   0.016655  150.43260  1.049995\n",
+       "       0.166667   0.016256  140.79400  1.049997\n",
+       "       0.250000   0.016130  139.11644  1.049997\n",
+       "       0.333333   0.016102  138.77844  1.049997\n",
+       "       0.416667   0.016154  138.96244  1.049997\n",
+       "       0.500000   0.016335  144.99000  1.050000\n",
+       "       0.583333   0.016305  143.96600  1.050000\n",
+       "       0.666667   0.016340  144.07980  1.049997\n",
+       "       0.750000   0.016264  144.12980  1.050000\n",
+       "       0.833333   0.016250  143.07460  1.050000\n",
+       "       0.916667   0.016600  151.45700  1.049996\n",
+       "       1.000000   0.016282  145.05840  1.050000\n",
+       "       1.083333   0.016294  144.94940  1.050000\n",
+       "       1.166667   0.016328  145.00760  1.050000\n",
+       "       1.250000   0.016260  144.78460  1.050000\n",
+       "       1.333333   0.016249  144.41080  1.050000\n",
+       "       1.416667   0.015864  135.81204  1.049998\n",
+       "       1.500000   0.015834  135.88004  1.049998\n",
+       "       1.583333   0.015860  135.15204  1.049998\n",
+       "       1.666667   0.015825  135.07504  1.049998\n",
+       "       1.750000   0.015878  135.81404  1.049998\n",
+       "       1.833333   0.015865  135.60504  1.049998\n",
+       "       1.916667   0.015760  134.47424  1.049998\n",
+       "       2.000000   0.015791  134.40736  1.049998\n",
+       "       2.083333   0.015780  134.51936  1.049998\n",
+       "       2.166667   0.015828  136.60292  1.049998\n",
+       "       2.250000   0.015834  136.77704  1.049998\n",
+       "       2.333333   0.015746  134.83852  1.049998\n",
+       "       2.416667   0.015836  135.84280  1.049998\n",
+       "...                    ...        ...       ...\n",
+       "       45.583333  0.021482  178.67200  1.050000\n",
+       "       45.666667  0.020794  172.31280  1.050000\n",
+       "       45.750000  0.022309  187.73000  1.050000\n",
+       "       45.833333  0.020949  177.91720  1.050000\n",
+       "       45.916667  0.022342  191.94360  1.050000\n",
+       "       46.000000  0.022244  189.37640  1.050000\n",
+       "       46.083333  0.021925  189.43760  1.050000\n",
+       "       46.166667  0.021553  186.48880  1.050000\n",
+       "       46.250000  0.021446  185.52400  1.050000\n",
+       "       46.333333  0.020711  179.98320  1.050000\n",
+       "       46.416667  0.020490  178.65800  1.050000\n",
+       "       46.500000  0.019945  174.35240  1.049999\n",
+       "       46.583333  0.019529  170.80280  1.050000\n",
+       "       46.666667  0.019630  171.90000  1.050000\n",
+       "       46.750000  0.020558  182.12120  1.050000\n",
+       "       46.833333  0.019379  171.02800  1.050000\n",
+       "       46.916667  0.019708  176.50440  1.049999\n",
+       "       47.000000  0.019286  171.01040  1.050000\n",
+       "       47.083333  0.019924  179.08640  1.049999\n",
+       "       47.166667  0.019345  174.75120  1.050000\n",
+       "       47.250000  0.018938  169.99240  1.050000\n",
+       "       47.333333  0.018538  167.97840  1.049999\n",
+       "       47.416667  0.017962  162.27320  1.049999\n",
+       "       47.500000  0.018385  168.09600  1.049999\n",
+       "       47.583333  0.016690  149.41680  1.049999\n",
+       "       47.666667  0.016827  152.27600  1.049999\n",
+       "       47.750000  0.017827  162.34080  1.049999\n",
+       "       47.833333  0.016463  143.73240  1.049996\n",
+       "       47.916667  0.017406  159.01400  1.049999\n",
+       "       48.000000  0.018013  166.33400  1.049999\n",
+       "\n",
+       "[577 rows x 3 columns]"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "# convert to Pandas dataframe to interact with the rest of the PyData ecosystem\n",
+    "df = all_bus_metrics.to_dataframe()\n",
+    "df"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "time range: 0.0 23.916666666666668\n"
+     ]
+    }
+   ],
+   "source": [
+    "## some common slicing methods have been provided\n",
+    "\n",
+    "all_bus_metrics_nonEventDay = tmp.get_bus_metrics_for_period(0,24)\n",
+    "\n",
+    "print(\"time range:\",all_bus_metrics_nonEventDay.time.min().values, all_bus_metrics_nonEventDay.time.max().values)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 36,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<matplotlib.figure.Figure at 0x25ce31e4160>"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "## plot some results\n",
+    "\n",
+    "fig1=plt.figure()\n",
+    "ax1 = fig1.add_subplot(111)\n",
+    "plt.plot(all_bus_metrics_nonEventDay.sel(busNum = 7).time.values, all_bus_metrics_nonEventDay.sel(busNum = 7).PD.values,label=casename)\n",
+    "\n",
+    "ax1.legend(loc='best')\n",
+    "ax1.grid(True)\n",
+    "ax1.set(title ='Bus 7 total load', xlabel='Time (hour)',ylabel='Load (MW)')\n",
+    "\n",
+    "\n",
+    "plt.show()\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>busNum</th>\n",
+       "      <th>PD</th>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>time</th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0.000000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>100.00000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.083333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>150.43260</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.166667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>140.79400</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.250000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>139.11644</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.333333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>138.77844</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.416667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>138.96244</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.500000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>144.99000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.583333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>143.96600</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.666667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>144.07980</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.750000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>144.12980</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.833333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>143.07460</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>0.916667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>151.45700</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.000000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>145.05840</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.083333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>144.94940</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.166667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>145.00760</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.250000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>144.78460</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.333333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>144.41080</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.416667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>135.81204</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.500000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>135.88004</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.583333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>135.15204</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.666667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>135.07504</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.750000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>135.81404</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.833333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>135.60504</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1.916667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>134.47424</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.000000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>134.40736</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.083333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>134.51936</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.166667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>136.60292</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.250000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>136.77704</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.333333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>134.83852</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2.416667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>135.84280</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>21.500000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>189.19400</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>21.583333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>196.56400</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>21.666667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>190.09440</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>21.750000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>197.68480</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>21.833333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>196.43440</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>21.916667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>187.91600</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.000000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>197.63600</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.083333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>195.51600</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.166667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>176.18800</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.250000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>180.43440</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.333333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>173.52920</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.416667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>183.94760</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.500000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>178.56400</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.583333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>186.11960</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.666667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>173.29680</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.750000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>183.21280</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.833333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>180.37480</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>22.916667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>177.39000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.000000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>172.85120</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.083333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>166.47280</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.166667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>165.59440</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.250000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>173.73440</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.333333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>157.51000</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.416667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>167.42680</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.500000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>167.71720</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.583333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>171.16080</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.666667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>165.68640</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.750000</th>\n",
+       "      <td>7</td>\n",
+       "      <td>171.82120</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.833333</th>\n",
+       "      <td>7</td>\n",
+       "      <td>155.49240</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>23.916667</th>\n",
+       "      <td>7</td>\n",
+       "      <td>152.50120</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>288 rows × 2 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "           busNum         PD\n",
+       "time                        \n",
+       "0.000000        7  100.00000\n",
+       "0.083333        7  150.43260\n",
+       "0.166667        7  140.79400\n",
+       "0.250000        7  139.11644\n",
+       "0.333333        7  138.77844\n",
+       "0.416667        7  138.96244\n",
+       "0.500000        7  144.99000\n",
+       "0.583333        7  143.96600\n",
+       "0.666667        7  144.07980\n",
+       "0.750000        7  144.12980\n",
+       "0.833333        7  143.07460\n",
+       "0.916667        7  151.45700\n",
+       "1.000000        7  145.05840\n",
+       "1.083333        7  144.94940\n",
+       "1.166667        7  145.00760\n",
+       "1.250000        7  144.78460\n",
+       "1.333333        7  144.41080\n",
+       "1.416667        7  135.81204\n",
+       "1.500000        7  135.88004\n",
+       "1.583333        7  135.15204\n",
+       "1.666667        7  135.07504\n",
+       "1.750000        7  135.81404\n",
+       "1.833333        7  135.60504\n",
+       "1.916667        7  134.47424\n",
+       "2.000000        7  134.40736\n",
+       "2.083333        7  134.51936\n",
+       "2.166667        7  136.60292\n",
+       "2.250000        7  136.77704\n",
+       "2.333333        7  134.83852\n",
+       "2.416667        7  135.84280\n",
+       "...           ...        ...\n",
+       "21.500000       7  189.19400\n",
+       "21.583333       7  196.56400\n",
+       "21.666667       7  190.09440\n",
+       "21.750000       7  197.68480\n",
+       "21.833333       7  196.43440\n",
+       "21.916667       7  187.91600\n",
+       "22.000000       7  197.63600\n",
+       "22.083333       7  195.51600\n",
+       "22.166667       7  176.18800\n",
+       "22.250000       7  180.43440\n",
+       "22.333333       7  173.52920\n",
+       "22.416667       7  183.94760\n",
+       "22.500000       7  178.56400\n",
+       "22.583333       7  186.11960\n",
+       "22.666667       7  173.29680\n",
+       "22.750000       7  183.21280\n",
+       "22.833333       7  180.37480\n",
+       "22.916667       7  177.39000\n",
+       "23.000000       7  172.85120\n",
+       "23.083333       7  166.47280\n",
+       "23.166667       7  165.59440\n",
+       "23.250000       7  173.73440\n",
+       "23.333333       7  157.51000\n",
+       "23.416667       7  167.42680\n",
+       "23.500000       7  167.71720\n",
+       "23.583333       7  171.16080\n",
+       "23.666667       7  165.68640\n",
+       "23.750000       7  171.82120\n",
+       "23.833333       7  155.49240\n",
+       "23.916667       7  152.50120\n",
+       "\n",
+       "[288 rows x 2 columns]"
+      ]
+     },
+     "execution_count": 10,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "total_load_df = all_bus_metrics_nonEventDay.sel(busNum = 7).PD.to_dataframe()\n",
+    "total_load_df\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.5"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}


Test patch:
diff --git a/src/comms_metrics/test.py b/src/comms_metrics/test.py
new file mode 100755
index 00000000..1f1b1643
--- /dev/null
+++ b/src/comms_metrics/test.py
@@ -0,0 +1,374 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jan 29 07:34:28 2021
+
+@author: barn553
+"""
+import os
+import random
+from system import MeterNetwork
+from metrics import EvaluateSystem, Results, Compare
+import pandas as pd
+import logging
+import logging.config
+# import numpy as np
+import json
+import itertools
+
+
+if __name__ == '__main__':
+    pd.options.mode.chained_assignment = None  # default='warn'
+    head, tail = os.path.split(os.getcwd())
+    # data_path = os.path.join(head, 'results', 'tamu', 'tamu_test1_csvs')
+    data_path = os.path.join(
+        head, 'results', 'gld')
+    # MN = MeterNetwork()
+    # MN.from_csv(
+    #     os.path.join(data_path, 'R1-12.47-1_ns3.csv'),
+    #     ['name', 'pos_x', 'pos_y'])
+    # df = MN.make_dataframe()
+    # # print(df.head())
+    # ES = EvaluateSystem(df, MN.meter_nodes, 'pos_x', 'pos_y', 'feet')
+    # # print(len(MN.meter_nodes))
+    # dd = ES.get_distances()
+    # isl = ES.island_count(100.0)
+    # print(isl)
+    # print(len(dd))
+    # print(dd['distance'].max())
+    # # print(dd.head())
+    # # print(MN.meter_nodes)
+    # combos = list(itertools.product(
+    #     *[MN.meter_nodes, [100.0, 200.0, 300.0, 400.0,
+    #                        500.0, 1000.0, 1500.0]]))
+    # densities = [ES.meter_density(c[0], c[1]) for c in combos]
+    # meters = [c[0] for c in combos]
+    # radii = [c[1] for c in combos]
+    # # for c in combos:
+    # #     dens = ES.meter_density(c[0], c[1])
+    # #     densities.append(dens)
+    # #     meters.append(c[0])
+    # #     radii.append(c[1])
+    # test_dens_df = pd.DataFrame({'meter': meters,
+    #                              'count': densities,
+    #                              'radius': radii})
+    # print(test_dens_df.sort_values(['meter', 'radius']))
+    # dens = ES.meter_density(MN.meter_nodes[0], 200.0)
+    # print(dens)
+    # full_dens = ES.all_densities(
+    #     [100.0, 200.0, 300.0, 400.0, 500.0, 1000.0, 1500.0])
+    # print(ES.dens_df.sort_values(['meter', 'radius']))
+    # densities = ES.all_densities([100.0, 200.0, 300.0, 400.0,
+    #                               500.0, 1000.0, 1500.0])
+    # ranges = ES.all_ranges()
+    # isos = ES.all_isolates([100.0, 200.0, 300.0, 400.0,
+    #                         500.0, 1000.0, 1500.0])
+    # conts = ES.all_continuous([100.0, 200.0, 300.0, 400.0,
+    #                            500.0, 1000.0, 1500.0])
+    # shc = ES.all_single_hops([100.0, 200.0, 300.0, 400.0,
+    #                           500.0, 1000.0, 1500.0])
+    # islands = ES.all_islands([100.0, 200.0, 300.0, 400.0,
+    #                           500.0, 1000.0, 1500.0])
+    # score = ES.evaluate_system([100.0, 200.0, 300.0, 400.0,
+    #                             500.0, 1000.0, 1500.0])
+    # print(score)
+    # # reduced_dd = dd[dd.distance > 100.0]
+    # print(np.round(score, 1))
+    # n_labels = {
+    #     m: 'meter{}'.format(i) for i, m in enumerate(df.name.unique())}
+    # n_args = {'node_color': 'tan',
+    #           'node_size': 3500,
+    #           'labels': n_labels,
+    #           'alpha': 0.5}
+    # e_args = {'edge_color': 'grey',
+    #           'edge_size': 1,
+    #           'style': 'dotted',
+    #           'alpha': 0.65}
+    # opts = {'width': 1600,
+    #           'height': 1000,
+    #           'title': 'My Meter Network:',
+    #           'fontsize': {'title': 30}}
+    # MN.plot_graph(
+    #     MN.meter_nodes, n_args, MN.meter_edges, e_args,
+    #     MN.meter_positions, opts, os.path.join(data_path, 'my_model'), True)
+    # Creating a custom logger
+    # config_file = os.path.join(
+    #     head, 'results', 'tamu', 'config.json')
+    # with open(config_file, 'r') as file:
+    #     config = json.load(file)
+    #     logging.config.dictConfig(config)
+    # logger = logging.getLogger(__name__)
+    # from timeit import default_timer as timer
+    # logger.info('Starting the calculations for the TAMU models...')
+    # print('Starting the calculations for the TAMU models...')
+    # # # counter = 0
+    # # # while counter < 10:
+    # start = timer()
+    # RS = Results()
+    # for root, dirs, files in os.walk(data_path):
+    #     # print('root:', root)
+    #     # rand_files = random.sample(os.listdir(data_path), 20)
+    #     for file in files:  # rand_files:
+    #         # print('file:', file)
+    #         h, t = os.path.split(file)
+    #         if '.csv' in t:
+    #             MN = MeterNetwork()
+    #             graph = MN.from_csv(
+    #                 os.path.join(data_path, file),
+    #                 ['name', 'lon', 'lat'])
+    #             df = MN.make_dataframe()
+    #             ES = EvaluateSystem(
+    #                 df, MN.meter_nodes, 'pos_x', 'pos_y', 'geo')
+    #             dd = ES.get_distances()
+    #             shc = ES.all_single_hops(
+    #                 [30.0, 60.0, 100.0, 200.0, 300.0,
+    #                  400.0, 500.0, 1000.0, 1500.0])
+    #             ranges = ES.all_ranges()
+    #             densities = ES.all_densities(
+    #                 [30.0, 60.0, 100.0, 200.0, 300.0,
+    #                  400.0, 500.0, 1000.0, 1500.0])
+    #             isolates = ES.all_isolates(
+    #                 [30.0, 60.0, 100.0, 200.0, 300.0,
+    #                  400.0, 500.0, 1000.0, 1500.0])
+    #             continuous = ES.all_continuous(
+    #                 [30.0, 60.0, 100.0, 200.0, 300.0,
+    #                  400.0, 500.0, 1000.0, 1500.0])
+    #             island = ES.all_islands(
+    #                 [30.0, 60.0, 100.0, 200.0, 300.0,
+    #                  400.0, 500.0, 1000.0, 1500.0])
+    #             score = ES.evaluate_system(
+    #                 [30.0, 60.0, 100.0, 200.0, 300.0,
+    #                  400.0, 500.0, 1000.0, 1500.0])
+    #             RS.add(ES)
+    # RS.save(
+    #     os.path.join(head, 'results', 'tamu'),
+    #     'tamu_test1_results.h5'.format())
+    # #     counter += 1
+    # print('Finished the calculations for the TAMU models.')
+    # logger.info('Finished the calculations for the TAMU models.')
+    # end = timer()
+    # print('total time:', end - start)
+    # -----Testing the Compare class -----
+    # gld use case: R5-12.47-4_ns3 (929 total meters)
+    # tamu use case: p1uhs1_1247--p1udt138 (944 total meters)
+    # start = timer()
+    # full_tamu_dens = pd.read_hdf(
+    #     os.path.join(head, 'results', 'tamu', 'tamu_results1.h5'),
+    #     key='density')
+    # full_tamu_range = pd.read_hdf(
+    #     os.path.join(head, 'results', 'tamu', 'tamu_results1.h5'),
+    #     key='range')
+    # full_tamu_isos = pd.read_hdf(
+    #     os.path.join(head, 'results', 'tamu', 'tamu_results1.h5'),
+    #     key='isolated')
+    # full_tamu_isl = pd.read_hdf(
+    #     os.path.join(head, 'results', 'tamu', 'tamu_results1.h5'),
+    #     key='island')
+    # full_tamu_cont = pd.read_hdf(
+    #     os.path.join(head, 'results', 'tamu', 'tamu_results1.h5'),
+    #     key='continuous')
+    # full_tamu_shc = pd.read_hdf(
+    #     os.path.join(head, 'results', 'tamu', 'tamu_results1.h5'),
+    #     key='single_hop')
+    # feeder_list = list(full_tamu_shc.feeder.unique())
+    # # print(feeder_list)
+    # test_df = full_tamu_shc[
+    #     full_tamu_shc['feeder'] == feeder_list[0]]
+    # # print(test_df.head())
+    C = Compare()
+    C.load_data()
+    dens = pd.read_hdf(
+        os.path.join(head, 'results', 'gld', 'gld_results2.h5'),
+        key='density')
+    rnge = pd.read_hdf(
+        os.path.join(head, 'results', 'gld', 'gld_results2.h5'),
+        key='range')
+    isos = pd.read_hdf(
+        os.path.join(head, 'results', 'gld', 'gld_results2.h5'),
+        key='isolated')
+    cont = pd.read_hdf(
+        os.path.join(head, 'results', 'gld', 'gld_results2.h5'),
+        key='continuous')
+    shc = pd.read_hdf(
+        os.path.join(head, 'results', 'gld', 'gld_results2.h5'),
+        key='single_hop')
+    isl = pd.read_hdf(
+        os.path.join(head, 'results', 'gld', 'gld_results2.h5'),
+        key='island')
+    dens_reduced = dens[dens['feeder'] == 'R1-12.47-1_ns3']
+    rnge_reduced = rnge[rnge['feeder'] == 'R1-12.47-1_ns3']
+    isos_reduced = isos[isos['feeder'] == 'R1-12.47-1_ns3']
+    cont_reduced = cont[cont['feeder'] == 'R1-12.47-1_ns3']
+    shc_reduced = shc[shc['feeder'] == 'R1-12.47-1_ns3']
+    isl_reduced = isl[isl['feeder'] == 'R1-12.47-1_ns3']
+    stats_df = C.compare_island_count(
+        [isl_reduced], 'agg',
+        [100.0, 200.0, 300.0])
+    print(stats_df)
+    # stats_df = C.compare_single_hop_count(
+    #     [test_df], 'agg',
+    #     [100.0, 200.0, 300.0, 400.0, 500.0, 1000.0, 1500.0])
+    # print(stats_df)
+    # print(stats_df['mse'].mean())
+    # gld_df = pd.read_hdf(
+    #     os.path.join(head, 'results', 'gld', 'gld_results.h5'),
+    #     key='density')
+    # models = list(gld_df['feeder'].unique())[0:20]
+    # print(len(models))
+    # keys = ['density', 'range', 'isolated',
+    #         'continuous', 'island', 'single_hop']
+    # print('Starting the comparison...')
+    # # Try this for 30 and 60 feet
+    # for key in keys:
+    #     if key == 'density':
+    #         tamu_df = pd.read_hdf(
+    #             os.path.join(head, 'results',
+    #                          'tamu', 'tamu_results1.h5'),
+    #             key='density')
+    #         models = list(tamu_df.feeder.unique())
+    #         model_dfs = [tamu_df[tamu_df['feeder'] == m] for m in models]
+    #         gld_df = pd.read_hdf(
+    #             os.path.join(head, 'results', 'gld', 'gld_results.h5'),
+    #             key='density')
+    #         glds = list(gld_df.feeder.unique())[0:20]
+    #         gld_dfs = [gld_df[gld_df['feeder'] == g] for g in glds]
+    #         C = Compare()
+    #         C.load_data()
+    #         # df = C.real_density
+    #         tamu_dens = C.compare_meter_density(
+    #             model_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #         gld_dens = C.compare_meter_density(
+    #             gld_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #     elif key == 'range':
+    #         tamu_df = pd.read_hdf(
+    #             os.path.join(head, 'results',
+    #                          'tamu', 'tamu_results1.h5'),
+    #             key='range')
+    #         models = list(tamu_df.feeder.unique())
+    #         model_dfs = [tamu_df[tamu_df['feeder'] == m] for m in models]
+    #         gld_df = pd.read_hdf(
+    #             os.path.join(head, 'results', 'gld', 'gld_results.h5'),
+    #             key='range')
+    #         glds = list(gld_df.feeder.unique())[0:20]
+    #         gld_dfs = [gld_df[gld_df['feeder'] == g] for g in glds]
+    #         C = Compare()
+    #         C.load_data()
+    #         # df = C.real_range
+    #         tamu_range = C.compare_meter_range(
+    #             model_dfs, 'single')
+    #         gld_range = C.compare_meter_range(
+    #             gld_dfs, 'single')
+    #     elif key == 'isolated':
+    #         tamu_df = pd.read_hdf(
+    #             os.path.join(head, 'results',
+    #                          'tamu', 'tamu_results1.h5'),
+    #             key='isolated')
+    #         models = list(tamu_df.feeder.unique())
+    #         model_dfs = [tamu_df[tamu_df['feeder'] == m] for m in models]
+    #         gld_df = pd.read_hdf(
+    #             os.path.join(head, 'results', 'gld', 'gld_results.h5'),
+    #             key='isolated')
+    #         glds = list(gld_df.feeder.unique())[0:20]
+    #         gld_dfs = [gld_df[gld_df['feeder'] == g] for g in glds]
+    #         C = Compare()
+    #         C.load_data()
+    #         # df = C.real_isolated
+    #         tamu_isos = C.compare_isolated_meters(
+    #             model_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #         gld_isos = C.compare_isolated_meters(
+    #             gld_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #     elif key == 'continuous':
+    #         tamu_df = pd.read_hdf(
+    #             os.path.join(head, 'results',
+    #                          'tamu', 'tamu_results1.h5'),
+    #             key='continuous')
+    #         models = list(tamu_df.feeder.unique())
+    #         model_dfs = [tamu_df[tamu_df['feeder'] == m] for m in models]
+    #         gld_df = pd.read_hdf(
+    #             os.path.join(head, 'results', 'gld', 'gld_results.h5'),
+    #             key='continuous')
+    #         glds = list(gld_df.feeder.unique())[0:20]
+    #         gld_dfs = [gld_df[gld_df['feeder'] == g] for g in glds]
+    #         C = Compare()
+    #         C.load_data()
+    #         # df = C.real_continuous
+    #         tamu_cont = C.compare_meter_continuity(
+    #             model_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #         gld_cont = C.compare_meter_continuity(
+    #             gld_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #     elif key == 'island':
+    #         tamu_df = pd.read_hdf(
+    #             os.path.join(head, 'results',
+    #                          'tamu', 'tamu_results1.h5'),
+    #             key='island')
+    #         models = list(tamu_df.feeder.unique())
+    #         model_dfs = [tamu_df[tamu_df['feeder'] == m] for m in models]
+    #         gld_df = pd.read_hdf(
+    #             os.path.join(head, 'results', 'gld', 'gld_results.h5'),
+    #             key='island')
+    #         glds = list(gld_df.feeder.unique())[0:20]
+    #         gld_dfs = [gld_df[gld_df['feeder'] == g] for g in glds]
+    #         C = Compare()
+    #         C.load_data()
+    #         # df = C.real_island
+    #         tamu_isl = C.compare_island_count(
+    #             model_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #         gld_isl = C.compare_island_count(
+    #             gld_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #     elif key == 'single_hop':
+    #         tamu_df = pd.read_hdf(
+    #             os.path.join(head, 'results',
+    #                          'tamu', 'tamu_results1.h5'),
+    #             key='single_hop')
+    #         models = list(tamu_df.feeder.unique())
+    #         model_dfs = [tamu_df[tamu_df['feeder'] == m] for m in models]
+    #         gld_df = pd.read_hdf(
+    #             os.path.join(head, 'results', 'gld', 'gld_results.h5'),
+    #             key='single_hop')
+    #         glds = list(gld_df.feeder.unique())[0:20]
+    #         gld_dfs = [gld_df[gld_df['feeder'] == g] for g in glds]
+    #         C = Compare()
+    #         C.load_data()
+    #         # df = C.real_single_hop
+    #         tamu_shc = C.compare_single_hop_count(
+    #             model_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #         gld_shc = C.compare_single_hop_count(
+    #             gld_dfs, 'agg',
+    #             [100.0, 200.0, 300.0,
+    #              400.0, 500.0, 1000.0, 1500.0])
+    #     else:
+    #         pass
+    # with pd.ExcelWriter('tamu_stats.xlsx') as writer:
+    #     tamu_dens.to_excel(writer, sheet_name='density')
+    #     tamu_range.to_excel(writer, sheet_name='range')
+    #     tamu_isos.to_excel(writer, sheet_name='isolated')
+    #     tamu_cont.to_excel(writer, sheet_name='continuous')
+    #     tamu_isl.to_excel(writer, sheet_name='island')
+    #     tamu_shc.to_excel(writer, sheet_name='single_hop')
+    # with pd.ExcelWriter('gld_stats.xlsx') as writer:
+    #     gld_dens.to_excel(writer, sheet_name='density')
+    #     gld_range.to_excel(writer, sheet_name='range')
+    #     gld_isos.to_excel(writer, sheet_name='isolated')
+    #     gld_cont.to_excel(writer, sheet_name='continuous')
+    #     gld_isl.to_excel(writer, sheet_name='island')
+    #     gld_shc.to_excel(writer, sheet_name='single_hop')
+    # end = timer()
+    # print('Finished the comparison')
+    # print('total time:', end - start)