diff --git a/lib/linux/swmm5 b/lib/linux/swmm5 deleted file mode 100755 index 83dc885..0000000 Binary files a/lib/linux/swmm5 and /dev/null differ diff --git a/lib/windows/swmm5_22.exe b/lib/windows/swmm5_22.exe deleted file mode 100644 index dc0792b..0000000 Binary files a/lib/windows/swmm5_22.exe and /dev/null differ diff --git a/requirements.txt b/requirements.txt index 018243a..bc9550e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,6 @@ m2r mistune==0.8.4 # Run dependencies pyproj>=3.0.0 +geopandas +matplotlib +pyswmm>=1.2 \ No newline at end of file diff --git a/swmmio/__init__.py b/swmmio/__init__.py index 71f7662..3cc1e90 100644 --- a/swmmio/__init__.py +++ b/swmmio/__init__.py @@ -2,7 +2,10 @@ from swmmio.elements import * from swmmio.version_control import * from swmmio.utils.dataframes import dataframe_from_bi, dataframe_from_rpt, dataframe_from_inp +from swmmio.utils.functions import find_network_trace from swmmio.graphics.swmm_graphics import create_map +from swmmio.graphics.profiler import (build_profile_plot, add_hgl_plot, + add_node_labels_plot, add_link_labels_plot) # import swmmio.core as swmmio '''Python SWMM Input/Output Tools''' diff --git a/swmmio/__main__.py b/swmmio/__main__.py index 4a27570..f38a5a5 100644 --- a/swmmio/__main__.py +++ b/swmmio/__main__.py @@ -1,58 +1,61 @@ +import argparse +import os +from itertools import chain + from swmmio.run_models.run import run_simple, run_hot_start_sequence from swmmio.run_models import start_pool -from swmmio import Model -from itertools import chain -import os -import argparse -from multiprocessing import Pool, cpu_count -from datetime import datetime -#parse the arguments -parser = argparse.ArgumentParser(description='Process some stuff') -parser.add_argument('-r', '--run', dest='model_to_run', nargs="+") -parser.add_argument('-rhs', '--run_hotstart', dest='hotstart_model_to_run', nargs="+") -parser.add_argument('-sp', '--start_pool', dest='start_pool', nargs="+") -parser.add_argument('-cores_left', '--cores_left', dest='cores_left', default=4, type=int) -parser.add_argument('-pp', '--post_process', dest='post_process', nargs="+") +def main(): + # parse the arguments + parser = argparse.ArgumentParser(description='Process some stuff') + parser.add_argument('-r', '--run', dest='model_to_run', nargs="+") + parser.add_argument('-rhs', '--run_hotstart', dest='hotstart_model_to_run', nargs="+") + parser.add_argument('-sp', '--start_pool', dest='start_pool', nargs="+") + parser.add_argument('-cores_left', '--cores_left', dest='cores_left', default=4, type=int) + parser.add_argument('-pp', '--post_process', dest='post_process', nargs="+") + + args = parser.parse_args() + wd = os.getcwd() # current directory script is being called from -args = parser.parse_args() -wd = os.getcwd() #current directory script is being called from + if args.model_to_run is not None: -if args.model_to_run is not None: + models_paths = [os.path.join(wd, f) for f in args.model_to_run] + print('Adding models to queue:\n\t{}'.format('\n\t'.join(models_paths))) - models_paths = [os.path.join(wd, f) for f in args.model_to_run] - print('Adding models to queue:\n\t{}'.format('\n\t'.join(models_paths))) + # run the models in series (one after the other) + list(map(run_simple, models_paths)) + # run_simple(args.model_to_run) - #run the models in series (one after the other) - list(map(run_simple, models_paths)) - # run_simple(args.model_to_run) + elif args.hotstart_model_to_run is not None: + models_paths = [os.path.join(wd, f) for f in args.hotstart_model_to_run] + print('hotstart_model_to_run the model: {}'.format(args.hotstart_model_to_run)) + # m = Model(args.hotstart_model_to_run) + # run_hot_start_sequence(m)#args.hotstart_model_to_run) + list(map(run_hot_start_sequence, models_paths)) -elif args.hotstart_model_to_run is not None: - models_paths = [os.path.join(wd, f) for f in args.hotstart_model_to_run] - print('hotstart_model_to_run the model: {}'.format(args.hotstart_model_to_run)) - # m = Model(args.hotstart_model_to_run) - # run_hot_start_sequence(m)#args.hotstart_model_to_run) - list(map(run_hot_start_sequence, models_paths)) + elif args.start_pool is not None: -elif args.start_pool is not None: + models_dirs = [os.path.join(wd, f) for f in args.start_pool] + print('Searching for models in:\n\t{}'.format('\n\t'.join(models_dirs))) + # combine the segments and options (combinations) into one iterable + inp_paths = [] + for root, dirs, files in chain.from_iterable(os.walk(path) for path in models_dirs): + for f in files: + if f.endswith('.inp') and 'bk' not in root: + # we've found a directory containing an inp + inp_paths.append(os.path.join(root, f)) - models_dirs = [os.path.join(wd, f) for f in args.start_pool] - print('Searching for models in:\n\t{}'.format('\n\t'.join(models_dirs))) - #combine the segments and options (combinations) into one iterable - inp_paths = [] - for root, dirs, files in chain.from_iterable(os.walk(path) for path in models_dirs): - for f in files: - if f.endswith('.inp') and 'bk' not in root: - #we've found a directory containing an inp - inp_paths.append(os.path.join(root, f)) + # call the main() function in start_pool.py + start_pool.main(inp_paths, args.cores_left) + print("swmmio has completed running {} models".format(len(inp_paths))) - #call the main() function in start_pool.py - start_pool.main(inp_paths, args.cores_left) + else: + print('you need to pass in some args') - print("swmmio has completed running {} models".format(len(inp_paths))) + return 0 -else: - print('you need to pass in some args') +if __name__ == '__main__': + main() diff --git a/swmmio/core.py b/swmmio/core.py index 84d215b..b85adaf 100644 --- a/swmmio/core.py +++ b/swmmio/core.py @@ -504,6 +504,7 @@ class inp(SWMMIOFile): def __init__(self, file_path): self._options_df = None self._files_df = None + self._report_df = None self._conduits_df = None self._xsections_df = None self._pumps_df = None @@ -529,6 +530,7 @@ def __init__(self, file_path): self._sections = [ '[OPTIONS]', '[FILES]', + '[REPORT]', '[CONDUITS]', '[XSECTIONS]', '[PUMPS]', @@ -562,9 +564,12 @@ def save(self, target_path=None): """ from swmmio.utils.modify_model import replace_inp_section import shutil - target_path = target_path if target_path is not None else self.path - shutil.copyfile(self.path, target_path) + if target_path is not None: + shutil.copyfile(self.path, target_path) + else: + target_path = self.path + for section in self._sections: # reformate the [SECTION] to section (and _section_df) sect_id = section.translate({ord(i): None for i in '[]'}).lower() @@ -651,8 +656,6 @@ def files(self): :return: files section of the INP file :rtype: pandas.DataFrame - - Examples: """ if self._files_df is None: self._files_df = dataframe_from_inp(self.path, "[FILES]") @@ -664,6 +667,33 @@ def files(self, df): first_col = df.columns[0] self._files_df = df.set_index(first_col) + @property + def report(self): + """ + Get/set report section of the INP file. + + :return: report section of the INP file + :rtype: pandas.DataFrame + + >>> from swmmio.examples import jersey + >>> jersey.inp.report #doctest: +NORMALIZE_WHITESPACE + Status + Param + INPUT YES + CONTROLS YES + SUBCATCHMENTS NONE + NODES ALL + LINKS NONE + """ + if self._report_df is None: + self._report_df = dataframe_from_inp(self.path, "report") + return self._report_df + + @report.setter + def report(self, df): + """Set inp.report DataFrame.""" + self._report_df = df + @property def conduits(self): """ diff --git a/swmmio/defs/config.py b/swmmio/defs/config.py index 637a8d8..2cbcccd 100644 --- a/swmmio/defs/config.py +++ b/swmmio/defs/config.py @@ -3,11 +3,8 @@ # This is the swmmio project root ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -# path to the SWMM5 executable used within the run_models module -if os.name == 'posix': - SWMM_ENGINE_PATH = os.path.join(ROOT_DIR, 'lib', 'linux', 'swmm5') -else: - SWMM_ENGINE_PATH = os.path.join(ROOT_DIR, 'lib', 'windows', 'swmm5_22.exe') +# path to the Python executable used to run your version of Python +PYTHON_EXE_PATH = "python"#os.path.join(os.__file__.split("lib/")[0],"bin","python") # feature class name of parcels in geodatabase PARCEL_FEATURES = r'PWD_PARCELS_SHEDS_PPORT' @@ -25,3 +22,5 @@ BASEMAP_PATH = os.path.join(ROOT_DIR, 'swmmio', 'reporting', 'basemaps', 'index.html') BETTER_BASEMAP_PATH = os.path.join(ROOT_DIR, 'swmmio', 'reporting', 'basemaps', 'mapbox_base.html') +# PySWMM Wrapper Path +PYSWMM_WRAPPER_PATH = os.path.join(ROOT_DIR, 'swmmio', 'wrapper', 'pyswmm_wrapper.py') diff --git a/swmmio/graphics/profiler.py b/swmmio/graphics/profiler.py new file mode 100644 index 0000000..63ccdd0 --- /dev/null +++ b/swmmio/graphics/profiler.py @@ -0,0 +1,330 @@ +import numpy as np +import pandas as pd +from swmmio.utils import error + +MH_WIDTH=10 +DEFAULT_ORIFICE_LENGTH = 50 +DEFAULT_PUMP_LENGTH = 50 +DEFAULT_WEIR_LENGTH = 50 +DEFAULT_OUTLET_LENGTH = 50 + +def build_profile_plot(ax, model, path_selection): + """ + This function builds the static network information for the profile. The + function accepts the swmmio model object and the network trace information + returned from calling `swmmio.find_network_trace()` function. The function + returns the relevant information adding data for HGL and labels. + + :param model: swmmio.Model() object + :param path_selection: list of tuples [(, , ),...] + :return: profile configuration information used for additional functions. + """ + nodes = model.nodes.dataframe + links = model.links.dataframe + + profile_config = {"nodes":[],"links":[], + "path_selection":path_selection} + ground_levels = {'x':[],'level':[]} + rolling_x_pos = 0.0 + + for ind, link_set in enumerate(path_selection): + us_node, ds_node, link_id = link_set + # Plot first Node + if ind == 0: + invert_el = float(nodes.loc[[us_node]].InvertElev) + profile_config['nodes'].append({"id_name":us_node,\ + "rolling_x_pos":rolling_x_pos,\ + "invert_el":invert_el}) + ret=_add_node_plot(ax, rolling_x_pos, model, us_node, link_set) + ground_levels['x'].extend(ret['x']) + ground_levels['level'].extend(ret['level']) + + # Add next link length to offset + old_rolling_x_pos = rolling_x_pos + # check link type + if links.loc[[link_id]].Type[0] == "CONDUIT": + rolling_x_pos += float(links.loc[[link_id]].Length) + elif links.loc[[link_id]].Type[0] == "WEIR": + rolling_x_pos += DEFAULT_WEIR_LENGTH + elif links.loc[[link_id]].Type[0] == "ORIFICE": + rolling_x_pos += DEFAULT_ORIFICE_LENGTH + elif links.loc[[link_id]].Type[0] == "PUMP": + rolling_x_pos += DEFAULT_PUMP_LENGTH + elif links.loc[[link_id]].Type[0] == "OUTLET": + rolling_x_pos += DEFAULT_OUTLET_LENGTH + # Plot DS node + invert_el = float(nodes.loc[[ds_node]].InvertElev) + profile_config['nodes'].append({"id_name":ds_node,\ + "rolling_x_pos":rolling_x_pos,\ + "invert_el":invert_el}) + # Check which plot to build + ret=_add_node_plot(ax, rolling_x_pos, model, ds_node, link_set) + ground_levels['x'].extend(ret['x']) + ground_levels['level'].extend(ret['level']) + + ret=_add_link_plot(ax, old_rolling_x_pos, rolling_x_pos, model, link_set) + link_mid_x, link_mid_y = sum(ret['x'])/2.0, sum(ret['bottom'])/2.0 + profile_config["links"].append({"id_name":link_id, + "rolling_x_pos":link_mid_x, + "midpoint_bottom": link_mid_y}) + + _add_ground_plot(ax, ground_levels) + + return profile_config + + +def _add_node_plot(ax, x, model, node_name, link_set, surcharge_depth=0, width=MH_WIDTH, + gradient=True): + """ + Adds a single manhole to the plot. Called by `build_profile_plot()`. + """ + us_node, ds_node, link_id = link_set + + nodes = model.nodes.dataframe + links = model.links.dataframe + + invert_el = float(nodes.loc[[node_name]].InvertElev) + # Node Type checker + if hasattr(model.inp, "junctions"): + if node_name in model.inp.junctions.index: + depth = float(nodes.loc[[node_name]].MaxDepth) + if hasattr(model.inp, "outfalls"): + if node_name in model.inp.outfalls.index: + depth = float(links.loc[[link_id]].Geom1) + if hasattr(model.inp, "storage"): + if node_name in model.inp.storage.index: + depth = float(nodes.loc[[node_name]].MaxD) + + # Plotting Configuration + ll_x, ll_y = x-width, invert_el + lr_x, lr_y = x+width, invert_el + ur_x, ur_y = x+width, invert_el + depth + ul_x, ul_y = x-width, invert_el + depth + + ax.plot([ll_x, lr_x, ur_x, ul_x, ll_x], + [ll_y, lr_y, ur_y, ul_y, ll_y], "-", color='black',lw=0.75) + + if gradient: + ax.fill_between([ul_x, ul_x+0.1], [lr_y, lr_y], [ur_y, ur_y], + zorder=1, color="gray") + ax.fill_between([ul_x+0.1, ul_x+0.2], [lr_y, lr_y], [ur_y, ur_y], + zorder=0, color="silver") + ax.fill_between([ul_x+0.2, ul_x+0.3], [lr_y, lr_y], [ur_y, ur_y], + zorder=0, color="whitesmoke") + ax.fill_between([ur_x-0.1, ur_x], [lr_y, lr_y], [ur_y, ur_y], + zorder=1, color="gray") + ax.fill_between([ur_x-0.2, ur_x-0.1], [lr_y, lr_y], [ur_y, ur_y], + zorder=0, color="silver") + ax.fill_between([ur_x-0.3, ur_x-0.2], [lr_y, lr_y], [ur_y, ur_y], + zorder=0, color="whitesmoke") + return {'x': [ul_x, ur_x], 'level': [ul_y, ur_y]} + + +def _add_link_plot(ax, us_x_position, ds_x_position, model, link_set, width=0, gradient=True): + """ + Adds a single conduit to the plot. Called by `build_profile_plot()`. + """ + us_node, ds_node, link_id = link_set + + nodes = model.nodes.dataframe + links = model.links.dataframe + + us_node_el = float(nodes.loc[[us_node]].InvertElev) + ds_node_el = float(nodes.loc[[ds_node]].InvertElev) + + link_type = links.loc[[link_id]].Type[0] + mid_x = [] + mid_y = [] + # check link type + if link_type == "CONDUIT": + depth = float(links.loc[[link_id]].Geom1) + us_link_offset = float(links.loc[[link_id]].InOffset) + ds_link_offset = float(links.loc[[link_id]].OutOffset) + # + us_bot_x, us_bot_y = us_x_position+width, us_node_el + us_link_offset + ds_bot_x, ds_bot_y = ds_x_position-width, ds_node_el + ds_link_offset + ds_top_x, ds_top_y = ds_x_position-width, ds_node_el + ds_link_offset + depth + us_top_x, us_top_y = us_x_position+width, us_node_el + us_link_offset + depth + + ax.plot([us_bot_x, ds_bot_x, ds_top_x, us_top_x, us_bot_x], + [us_bot_y, ds_bot_y, ds_top_y, us_top_y, us_bot_y], "-k", + lw=0.75, zorder=0) + + elif link_type == "ORIFICE": + depth = float(links.loc[[link_id]].Geom1) + us_link_offset = float(links.loc[[link_id]].CrestHeight) + ds_node_el = float(nodes.loc[[us_node]].InvertElev) # Plot it flat + ds_link_offset = float(links.loc[[link_id]].CrestHeight) + + us_bot_x, us_bot_y = us_x_position+width, us_node_el + us_link_offset + ds_bot_x, ds_bot_y = ds_x_position-width, ds_node_el + ds_link_offset + ds_top_x, ds_top_y = ds_x_position-width, ds_node_el + ds_link_offset + depth + us_top_x, us_top_y = us_x_position+width, us_node_el + us_link_offset + depth + + ax.plot([us_bot_x, ds_bot_x, ds_top_x, us_top_x, us_bot_x], + [us_bot_y, ds_bot_y, ds_top_y, us_top_y, us_bot_y], "-k", + lw=0.75, zorder=0) + elif link_type in ["PUMP", "OUTLET"]: + depth = 1.0 + us_link_offset = 0.0 + ds_link_offset = 0.0 + + us_bot_x, us_bot_y = us_x_position+width, us_node_el + us_link_offset + ds_bot_x, ds_bot_y = ds_x_position-width, ds_node_el + ds_link_offset + ds_top_x, ds_top_y = ds_x_position-width, ds_node_el + ds_link_offset + depth + us_top_x, us_top_y = us_x_position+width, us_node_el + us_link_offset + depth + + ax.plot([us_bot_x, ds_bot_x, ds_top_x, us_top_x, us_bot_x], + [us_bot_y, ds_bot_y, ds_top_y, us_top_y, us_bot_y], "-k", + lw=0.75, zorder=0) + + elif link_type == "WEIR": + depth = float(links.loc[[link_id]].Geom1) + us_link_offset = float(links.loc[[link_id]].CrestHeight) + ds_link_offset = 0.0 + + us_bot_x, us_bot_y = us_x_position+width, us_node_el + us_link_offset + ds_bot_x, ds_bot_y = ds_x_position-width, ds_node_el + ds_link_offset + ds_top_x, ds_top_y = ds_x_position-width, ds_node_el + ds_link_offset + depth + us_top_x, us_top_y = us_x_position+width, us_node_el + us_link_offset + depth + md_bot_x1, md_bot_y1 = (us_bot_x + ds_bot_x)/2.0, us_bot_y + md_bot_x2, md_bot_y2 = md_bot_x1, ds_bot_y + + ax.plot([us_bot_x, md_bot_x1, md_bot_x2, ds_bot_x], + [us_bot_y, md_bot_y1, md_bot_y2, ds_bot_y], "-k", + lw=0.75, zorder=0) + + mid_x = [md_bot_x1, md_bot_x2] + mid_y = [md_bot_y1, md_bot_y2] + + + + return {'x':[us_bot_x, ds_bot_x], 'bottom':[us_bot_y, ds_bot_y], + "link_type":link_type, 'mid_x':mid_x, 'mid_y':mid_y} + +def _add_ground_plot(ax, ground_levels): + """ + Adds the grond level to the profile plot. Called by `build_profile_plot()`. + """ + ax.plot(ground_levels['x'], ground_levels['level'], '--', color='brown', lw=.5) + +def add_hgl_plot(ax, profile_config, hgl=None, depth=None, color = 'b', label="HGL"): + if isinstance(hgl, pd.core.series.Series) == True \ + or isinstance(hgl, dict) == True: + hgl_calc = [hgl[val["id_name"]] for val in profile_config['nodes']] + elif isinstance(depth, pd.core.series.Series) == True \ + or isinstance(depth, dict) == True: + hgl_calc = [val["invert_el"]+float(depth[val["id_name"]]) for val in profile_config['nodes']] + else: + raise(error.InvalidDataTypes) + + x = [float(val["rolling_x_pos"]) for val in profile_config['nodes']] + + ax.plot(x, hgl_calc, '-', color=color, label=label) + + +def add_node_labels_plot(ax, model, profile_config, font_size=8, + label_offset=5, stagger=True): + """ + This function adds the node labels to the plot axis handle. + + :param ax: matplotlib.plot.axis() Axis handle + :param model: swmmio.Model object + :param profile_config: dict dictionary returned from the `build_profile_plot()`. + :param font_size: font size + :param label_offset: int just provides some clean separation from the + assets and the labels + :param stagger: True/False staggers the labels to prevent them from stacking + onto one another. + """ + nodes = model.nodes.dataframe + + label_y_max = 0 + for val in profile_config['nodes']: + name = val['id_name'] + invert_el = float(nodes.loc[[name]].InvertElev) + # Node Type checker + if hasattr(model.inp, "junctions"): + if name in model.inp.junctions.index: + depth = float(nodes.loc[[name]].MaxDepth) + if hasattr(model.inp, "outfalls"): + if name in model.inp.outfalls.index: + depth = 0 + if hasattr(model.inp, "storage"): + if name in model.inp.storage.index: + depth = float(nodes.loc[[name]].MaxD) + + calc = invert_el+depth + if calc > label_y_max: + label_y_max = calc + + mx_y_annotation = 0 + for ind, val in enumerate(profile_config['nodes']): + stagger_value = 0 + if stagger: + if ind % 2 ==0: + stagger_value = 4 + name = val['id_name'] + x_offset = val['rolling_x_pos'] + invert_el = float(nodes.loc[[name]].InvertElev) + # Node Type checker + if hasattr(model.inp, "junctions"): + if name in model.inp.junctions.index: + depth = float(nodes.loc[[name]].MaxDepth) + if hasattr(model.inp, "outfalls"): + if name in model.inp.outfalls.index: + depth = 0 + if hasattr(model.inp, "storage"): + if name in model.inp.storage.index: + depth = float(nodes.loc[[name]].MaxD) + pos_y = invert_el+depth + label=ax.annotate(name, xy=(x_offset, pos_y), + xytext=(x_offset, label_y_max+label_offset+stagger_value), + arrowprops=dict(arrowstyle="->", alpha=0.5), va='top', ha='center', + alpha=0.75, fontsize=font_size) + mx_y = label.xyann[1] + if mx_y > mx_y_annotation: + mx_y_annotation = mx_y + + ax.set_ylim([None, mx_y_annotation]) + + +def add_link_labels_plot(ax, model, profile_config, font_size=8, + label_offset=9, stagger=True): + """ + This function adds the link labels to the plot axis handle. + + :param ax: matplotlib.plot.axis() Axis handle + :param model: swmmio.Model object + :param profile_config: dict dictionary returned from the `build_profile_plot()`. + :param font_size: font size + :param label_offset: int just provides some clean separation from the + assets and the labels + :param stagger: True/False staggers the labels to prevent them from stacking + onto one another. + """ + label_y_min = 1e9 + for val in profile_config['links']: + y_midpoint_bottom = val["midpoint_bottom"] + if y_midpoint_bottom < label_y_min: + label_y_min = y_midpoint_bottom + + min_y_annotation = 1e9 + for ind, val in enumerate(profile_config['links']): + stagger_value = 0 + if stagger: + if ind % 2 != 0: + stagger_value = 4 + name = val['id_name'] + x_offset = val['rolling_x_pos'] + y_midpoint_bottom = val["midpoint_bottom"] + + label=ax.annotate(name, xy=(x_offset, y_midpoint_bottom), + xytext=(x_offset, label_y_min-label_offset+stagger_value), + arrowprops=dict(arrowstyle="->", alpha=0.5), va='bottom', + ha='center', alpha=0.75, fontsize=font_size) + mx_y = label.xyann[1] + if mx_y < min_y_annotation: + min_y_annotation = mx_y + + ax.set_ylim([min_y_annotation, None]) diff --git a/swmmio/run_models/defs.py b/swmmio/run_models/defs.py deleted file mode 100644 index 094a0eb..0000000 --- a/swmmio/run_models/defs.py +++ /dev/null @@ -1,38 +0,0 @@ -import pandas as pd -import os -from os.path import join as j -from swmmio.defs.sectionheaders import inp_header_dict - -current_dir = os.path.dirname(__file__) -sets = j(current_dir, 'inp_settings') - -# TODO: do something about his silly file -OPTIONS_no_rain = pd.read_table( - j(sets, 'OPTIONS_no_rain.txt'), - delim_whitespace=True, - names=inp_header_dict['[OPTIONS]'].split(), - skiprows=1, - index_col=0 - ) - -OPTIONS_normal = pd.read_table( - j(sets, 'OPTIONS_normal.txt'), - delim_whitespace=True, - names=inp_header_dict['[OPTIONS]'].split(), - skiprows=1, - index_col=0 - ) -REPORT_nodes_links = pd.read_table( - j(sets, 'REPORT_nodes_links.txt'), - delim_whitespace=True, - names=inp_header_dict['[REPORT]'].split(), - skiprows=1, - index_col=0 - ) -REPORT_none = pd.read_table( - j(sets, 'REPORT_none.txt'), - delim_whitespace=True, - names=inp_header_dict['[REPORT]'].split(), - skiprows=1, - index_col=0 - ) diff --git a/swmmio/run_models/inp_settings/FILES.txt b/swmmio/run_models/inp_settings/FILES.txt deleted file mode 100644 index e69de29..0000000 diff --git a/swmmio/run_models/inp_settings/OPTIONS_no_rain.txt b/swmmio/run_models/inp_settings/OPTIONS_no_rain.txt deleted file mode 100644 index ab68172..0000000 --- a/swmmio/run_models/inp_settings/OPTIONS_no_rain.txt +++ /dev/null @@ -1,28 +0,0 @@ -[OPTIONS] -FLOW_UNITS CFS -INFILTRATION GREEN_AMPT -FLOW_ROUTING DYNWAVE -START_DATE 01/01/1990 -START_TIME 00:00:00 -REPORT_START_DATE 01/01/1990 -REPORT_START_TIME 00:00:00 -END_DATE 01/02/1990 -END_TIME 00:00:00 -SWEEP_START 01/01 -SWEEP_END 01/02 -DRY_DAYS 0 -REPORT_STEP 00:01:00 -WET_STEP 00:01:00 -DRY_STEP 00:01:00 -ROUTING_STEP 0:00:05 -ALLOW_PONDING YES -INERTIAL_DAMPING NONE -VARIABLE_STEP 0.50 -LENGTHENING_STEP 20.0 -MIN_SURFAREA 12.566 -NORMAL_FLOW_LIMITED BOTH -SKIP_STEADY_STATE NO -FORCE_MAIN_EQUATION H-W -LINK_OFFSETS DEPTH -MIN_SLOPE 0 -IGNORE_RAINFALL YES diff --git a/swmmio/run_models/inp_settings/OPTIONS_normal.txt b/swmmio/run_models/inp_settings/OPTIONS_normal.txt deleted file mode 100644 index c6821c5..0000000 --- a/swmmio/run_models/inp_settings/OPTIONS_normal.txt +++ /dev/null @@ -1,27 +0,0 @@ -[OPTIONS] -FLOW_UNITS CFS -INFILTRATION GREEN_AMPT -FLOW_ROUTING DYNWAVE -START_DATE 01/01/1990 -START_TIME 00:00:00 -REPORT_START_DATE 01/01/1990 -REPORT_START_TIME 00:00:00 -END_DATE 01/02/1990 -END_TIME 00:00:00 -SWEEP_START 01/01 -SWEEP_END 01/02 -DRY_DAYS 0 -REPORT_STEP 00:05:00 -WET_STEP 00:01:00 -DRY_STEP 00:01:00 -ROUTING_STEP 0:00:05 -ALLOW_PONDING YES -INERTIAL_DAMPING NONE -VARIABLE_STEP 0.50 -LENGTHENING_STEP 20.0 -MIN_SURFAREA 12.566 -NORMAL_FLOW_LIMITED BOTH -SKIP_STEADY_STATE NO -FORCE_MAIN_EQUATION H-W -LINK_OFFSETS DEPTH -MIN_SLOPE 0 diff --git a/swmmio/run_models/inp_settings/REPORT_nodes_links.txt b/swmmio/run_models/inp_settings/REPORT_nodes_links.txt deleted file mode 100644 index 1072238..0000000 --- a/swmmio/run_models/inp_settings/REPORT_nodes_links.txt +++ /dev/null @@ -1,6 +0,0 @@ -[REPORT] -INPUT NO -CONTROLS NO -SUBCATCHMENTS NONE -NODES ALL -LINKS ALL diff --git a/swmmio/run_models/inp_settings/REPORT_none.txt b/swmmio/run_models/inp_settings/REPORT_none.txt deleted file mode 100644 index f53151b..0000000 --- a/swmmio/run_models/inp_settings/REPORT_none.txt +++ /dev/null @@ -1,6 +0,0 @@ -[REPORT] -INPUT NO -CONTROLS NO -SUBCATCHMENTS NONE -NODES NONE -LINKS NONE diff --git a/swmmio/run_models/run.py b/swmmio/run_models/run.py index b634f03..2c563a3 100644 --- a/swmmio/run_models/run.py +++ b/swmmio/run_models/run.py @@ -1,56 +1,59 @@ import subprocess import os + import pandas as pd -from swmmio.utils.modify_model import replace_inp_section -from swmmio.run_models import defs + from swmmio import Model -from swmmio.defs.config import SWMM_ENGINE_PATH +from swmmio.defs.config import PYTHON_EXE_PATH, PYSWMM_WRAPPER_PATH -def run_simple(inp_path, swmm_eng=SWMM_ENGINE_PATH): +def run_simple(inp_path, py_path=PYTHON_EXE_PATH, pyswmm_wrapper=PYSWMM_WRAPPER_PATH): """ run a model once as is. """ - print('running {} with {}'.format(inp_path, swmm_eng)) - #inp_path = model.inp.path + print('running {}'.format(inp_path)) + # inp_path = model.inp.path rpt_path = os.path.splitext(inp_path)[0] + '.rpt' + out_path = os.path.splitext(inp_path)[0] + '.out' - subprocess.call([swmm_eng, inp_path, rpt_path]) + # Pass Environment Info to Run + env_definition = os.environ.copy() + env_definition["PATH"] = "/usr/sbin:/sbin:" + env_definition["PATH"] -def run_hot_start_sequence(inp_path, swmm_eng=SWMM_ENGINE_PATH): + subprocess.call([py_path, pyswmm_wrapper, inp_path, rpt_path, out_path], + env=env_definition) + return 0 + + +def run_hot_start_sequence(inp_path, py_path=PYTHON_EXE_PATH, pyswmm_wrapper=PYSWMM_WRAPPER_PATH): - # inp_path = model.inp.path model = Model(inp_path) - rpt_path = os.path.splitext(inp_path)[0] + '.rpt' hotstart1 = os.path.join(model.inp.dir, model.inp.name + '_hot1.hsf') hotstart2 = os.path.join(model.inp.dir, model.inp.name + '_hot2.hsf') - # if not os.path.exists(hotstart1) and not os.path.exists(hotstart2): - #create new model inp with params to save hotstart1 + # create new model inp with params to save hotstart1 print('create new model inp with params to save hotstart1') - s = pd.Series(['SAVE HOTSTART "{}"'.format(hotstart1)]) - hot1_df = pd.DataFrame(s, columns=['[FILES]']) - model = replace_inp_section(model.inp.path, '[FILES]', hot1_df) - model = replace_inp_section(model.inp.path, '[REPORT]', defs.REPORT_none) - model = replace_inp_section(model.inp.path, '[OPTIONS]', defs.OPTIONS_no_rain) - subprocess.call([swmm_eng, model.inp.path, rpt_path]) - - # if os.path.exists(hotstart1) and not os.path.exists(hotstart2): - #create new model inp with params to use hotstart1 and save hotstart2 + + model.inp.report.loc[:, 'Status'] = 'NONE' + model.inp.report.loc[['INPUT', 'CONTROLS'], 'Status'] = 'NO' + model.inp.files = pd.DataFrame([f'SAVE HOTSTART "{hotstart1}"'], columns=['[FILES]']) + model.inp.options.loc['IGNORE_RAINFALL', 'Value'] = 'YES' + model.inp.save() + + run_simple(model.inp.path, py_path=py_path, pyswmm_wrapper=pyswmm_wrapper) + + # create new model inp with params to use hotstart1 and save hotstart2 print('with params to use hotstart1 and save hotstart2') - s = pd.Series(['USE HOTSTART "{}"'.format(hotstart1), 'SAVE HOTSTART "{}"'.format(hotstart2)]) - hot2_df = pd.DataFrame(s, columns=['[FILES]']) - model = replace_inp_section(model.inp.path, '[FILES]', hot2_df) - subprocess.call([swmm_eng, model.inp.path, rpt_path]) + model.inp.files = pd.DataFrame([f'USE HOTSTART "{hotstart1}"', f'SAVE HOTSTART "{hotstart2}"'], columns=['[FILES]']) + model.inp.save() + + run_simple(model.inp.path, py_path=py_path, pyswmm_wrapper=pyswmm_wrapper) - # if os.path.exists(hotstart2): - #create new model inp with params to use hotstart2 and not save anything + # create new model inp with params to use hotstart2 and not save anything print('params to use hotstart2 and not save anything') - s = pd.Series(['USE HOTSTART "{}"'.format(hotstart2)]) - hot3_df = pd.DataFrame(s, columns=['[FILES]']) - model = replace_inp_section(model.inp.path, '[FILES]', hot3_df) - model = replace_inp_section(model.inp.path, '[REPORT]', defs.REPORT_none)# defs.REPORT_nodes_links) - model = replace_inp_section(model.inp.path, '[OPTIONS]', defs.OPTIONS_normal) + model.inp.files = pd.DataFrame([f'USE HOTSTART "{hotstart2}"'], columns=['[FILES]']) + model.inp.options.loc['IGNORE_RAINFALL', 'Value'] = 'NO' + model.inp.save() - subprocess.call([swmm_eng, model.inp.path, rpt_path]) + return run_simple(model.inp.path, py_path=py_path, pyswmm_wrapper=pyswmm_wrapper) diff --git a/swmmio/tests/data/Example1_parallel_loop.inp b/swmmio/tests/data/Example1_parallel_loop.inp new file mode 100644 index 0000000..5655872 --- /dev/null +++ b/swmmio/tests/data/Example1_parallel_loop.inp @@ -0,0 +1,340 @@ +[TITLE] +Example 1 + + +[OPTIONS] +;;Options Value +;;------------------ ------------ +FLOW_UNITS CFS +INFILTRATION HORTON +FLOW_ROUTING DYNWAVE +LINK_OFFSETS DEPTH +MIN_SLOPE 0 +ALLOW_PONDING NO +SKIP_STEADY_STATE NO +START_DATE 01/01/1998 +START_TIME 00:00:00 +REPORT_START_DATE 01/01/1998 +REPORT_START_TIME 00:00:00 +END_DATE 01/02/1998 +END_TIME 12:00:00 +SWEEP_START 1/1 +SWEEP_END 12/31 +DRY_DAYS 5 +REPORT_STEP 01:00:00 +WET_STEP 00:15:00 +DRY_STEP 01:00:00 +ROUTING_STEP 60 +INERTIAL_DAMPING PARTIAL +NORMAL_FLOW_LIMITED BOTH +FORCE_MAIN_EQUATION H-W +VARIABLE_STEP 0.75 +LENGTHENING_STEP 0 +MIN_SURFAREA 0 +MAX_TRIALS 0 +HEAD_TOLERANCE 0 +SYS_FLOW_TOL 5 +LAT_FLOW_TOL 5 +MINIMUM_STEP 0.5 +THREADS 1 + +[EVAPORATION] +;;Type Parameters +;;------------- ---------- +CONSTANT 0.0 +DRY_ONLY NO + +[RAINGAGES] +;; Rain Time Snow Data +;;Name Type Intrvl Catch Source +;;-------------- --------- ------ ------ ---------- +RG1 INTENSITY 1:00 1.0 TIMESERIES TS1 + +[SUBCATCHMENTS] +;; Total Pcnt. Pcnt. Curb Snow +;;Name Raingage Outlet Area Imperv Width Slope Length Pack +;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- -------- +1 RG1 9 10 50 500 0.01 0 +2 RG1 10 10 50 500 0.01 0 +3 RG1 13 5 50 500 0.01 0 +4 RG1 22 5 50 500 0.01 0 +5 RG1 15 15 50 500 0.01 0 +6 RG1 23 12 10 500 0.01 0 +7 RG1 19 4 10 500 0.01 0 +8 RG1 18 10 10 500 0.01 0 + +[SUBAREAS] +;;Subcatchment N-Imperv N-Perv S-Imperv S-Perv PctZero RouteTo PctRouted +;;-------------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- +1 0.001 0.10 0.05 0.05 25 OUTLET +2 0.001 0.10 0.05 0.05 25 OUTLET +3 0.001 0.10 0.05 0.05 25 OUTLET +4 0.001 0.10 0.05 0.05 25 OUTLET +5 0.001 0.10 0.05 0.05 25 OUTLET +6 0.001 0.10 0.05 0.05 25 OUTLET +7 0.001 0.10 0.05 0.05 25 OUTLET +8 0.001 0.10 0.05 0.05 25 OUTLET + +[INFILTRATION] +;;Subcatchment MaxRate MinRate Decay DryTime MaxInfil +;;-------------- ---------- ---------- ---------- ---------- ---------- +1 0.35 0.25 4.14 0.50 0 +2 0.7 0.3 4.14 0.50 0 +3 0.7 0.3 4.14 0.50 0 +4 0.7 0.3 4.14 0.50 0 +5 0.7 0.3 4.14 0.50 0 +6 0.7 0.3 4.14 0.50 0 +7 0.7 0.3 4.14 0.50 0 +8 0.7 0.3 4.14 0.50 0 + +[JUNCTIONS] +;; Invert Max. Init. Surcharge Ponded +;;Name Elev. Depth Depth Depth Area +;;-------------- ---------- ---------- ---------- ---------- ---------- +10 995 3 0 0 0 +13 995 3 0 0 0 +14 990 3 0 0 0 +15 987 3 0 0 0 +16 985 3 0 0 0 +17 980 3 0 0 0 +19 1010 3 0 0 0 +20 1005 3 0 0 0 +21 990 3 0 0 0 +22 987 3 0 0 0 +23 990 3 0 0 0 +24 984 3 0 0 0 +9 1000 3 0 0 0 + +[OUTFALLS] +;; Invert Outfall Stage/Table Tide +;;Name Elev. Type Time Series Gate Route To +;;-------------- ---------- ------------ ---------------- ---- ---------------- +18 975 FREE NO + +[CONDUITS] +;; Inlet Outlet Manning Inlet Outlet Init. Max. +;;Name Node Node Length N Offset Offset Flow Flow +;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- +1 9 10 400 0.01 0 0 0 0 +10 17 18 400 0.01 0 0 0 0 +11 13 14 400 0.01 0 0 0 0 +12 14 15 400 0.01 0 0 0 0 +13 15 16 400 0.01 0 0 0 0 +14 23 24 400 0.01 0 0 0 0 +15 16 24 100 0.01 0 0 0 0 +16 24 17 400 0.01 0 0 0 0 +4 19 20 200 0.01 0 0 0 0 +5 20 21 200 0.01 0 0 0 0 +6 10 21 400 0.01 0 1 0 0 +7 21 22 300 0.01 1 1 0 0 +8 22 16 300 0.01 0 0 0 0 +LOOP 21 24 500 0.01 0 0 0 0 + +[XSECTIONS] +;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels +;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- +1 CIRCULAR 1.5 0 0 0 1 +10 CIRCULAR 2 0 0 0 1 +11 CIRCULAR 1.5 0 0 0 1 +12 CIRCULAR 1.5 0 0 0 1 +13 CIRCULAR 1.5 0 0 0 1 +14 CIRCULAR 1 0 0 0 1 +15 CIRCULAR 2 0 0 0 1 +16 CIRCULAR 2 0 0 0 1 +4 CIRCULAR 1 0 0 0 1 +5 CIRCULAR 1 0 0 0 1 +6 CIRCULAR 1 0 0 0 1 +7 CIRCULAR 2 0 0 0 1 +8 CIRCULAR 2 0 0 0 1 +LOOP CIRCULAR 1 0 0 0 1 + +[LOSSES] +;;Link Inlet Outlet Average Flap Gate SeepageRate +;;-------------- ---------- ---------- ---------- ---------- ---------- + +[POLLUTANTS] +;; Mass Rain GW I&I Decay Snow Co-Pollut. Co-Pollut. DWF Init. +;;Name Units Concen. Concen. Concen. Coeff. Only Name Fraction Concen. Concen. +;;-------------- ------ ---------- ---------- ---------- ---------- ----- ---------------- ---------- ---------- ---------- +Lead UG/L 0.0 0.0 0 0.0 NO TSS 0.2 0 0 +TSS MG/L 0.0 0.0 0 0.0 NO * 0.0 0 0 + +[LANDUSES] +;; Cleaning Fraction Last +;;Name Interval Available Cleaned +;;-------------- ---------- ---------- ---------- +Residential 0 0 0 +Undeveloped 0 0 0 + +[COVERAGES] +;;Subcatchment Land Use Percent +;;-------------- ---------------- ---------- +1 Residential 100.00 +2 Residential 50.00 +2 Undeveloped 50.00 +3 Residential 100.00 +4 Residential 50.00 +4 Undeveloped 50.00 +5 Residential 100.00 +6 Undeveloped 100.00 +7 Undeveloped 100.00 +8 Undeveloped 100.00 + +[LOADINGS] +;;Subcatchment Pollutant Loading +;;-------------- ---------------- ---------- + +[BUILDUP] +;;LandUse Pollutant Function Coeff1 Coeff2 Coeff3 Normalizer +;;-------------- ---------------- ---------- ---------- ---------- ---------- ---------- +Residential Lead NONE 0 0 0 AREA +Residential TSS SAT 50 0 2 AREA +Undeveloped Lead NONE 0 0 0 AREA +Undeveloped TSS SAT 100 0 3 AREA + +[WASHOFF] +;; Cleaning BMP +;;Land Use Pollutant Function Coeff1 Coeff2 Effic. Effic. +;;-------------- ---------------- ---------- ---------- ---------- ---------- ---------- +Residential Lead EMC 0 0 0 0 +Residential TSS EXP 0.1 1 0 0 +Undeveloped Lead EMC 0 0 0 0 +Undeveloped TSS EXP 0.1 0.7 0 0 + +[TIMESERIES] +;;Name Date Time Value +;;-------------- ---------- ---------- ---------- +;RAINFALL +TS1 0:00 0.0 +TS1 1:00 0.25 +TS1 2:00 0.5 +TS1 3:00 0.8 +TS1 4:00 0.4 +TS1 5:00 0.1 +TS1 6:00 0.0 +TS1 27:00 0.0 +TS1 28:00 0.4 +TS1 29:00 0.2 +TS1 30:00 0.0 + +[REPORT] +INPUT NO +CONTROLS NO +SUBCATCHMENTS ALL +NODES ALL +LINKS ALL + +[TAGS] + +[MAP] +DIMENSIONS -188.4225 -154.7365 9514.7325 10196.8465 +UNITS None + +[COORDINATES] +;;Node X-Coord Y-Coord +;;-------------- ---------------- ---------------- +10 4105.26 6947.37 +13 2336.84 4357.89 +14 3157.89 4294.74 +15 3221.05 3242.11 +16 4821.05 3326.32 +17 6252.63 2147.37 +19 7768.42 6736.84 +20 5957.89 6589.47 +21 4926.32 6105.26 +22 4421.05 4715.79 +23 6484.21 3978.95 +24 5389.47 3031.58 +9 4042.11 9600 +18 6631.58 505.26 + +[VERTICES] +;;Link X-Coord Y-Coord +;;-------------- ---------------- ---------------- +10 6673.68 1368.42 + +[POLYGONS] +;;Subcatchment X-Coord Y-Coord +;;-------------- ---------------- ---------------- +1 3936.84 6905.26 +1 3494.74 6252.63 +1 273.68 6336.84 +1 252.63 8526.32 +1 463.16 9200 +1 1157.89 9726.32 +1 4000 9705.26 +2 7600 9663.16 +2 7705.26 6736.84 +2 5915.79 6694.74 +2 4926.32 6294.74 +2 4189.47 7200 +2 4126.32 9621.05 +3 2357.89 6021.05 +3 2400 4336.84 +3 3031.58 4252.63 +3 2989.47 3389.47 +3 315.79 3410.53 +3 294.74 6000 +4 3473.68 6105.26 +4 3915.79 6421.05 +4 4168.42 6694.74 +4 4463.16 6463.16 +4 4821.05 6063.16 +4 4400 5263.16 +4 4357.89 4442.11 +4 4547.37 3705.26 +4 4000 3431.58 +4 3326.32 3368.42 +4 3242.11 3536.84 +4 3136.84 5157.89 +4 2589.47 5178.95 +4 2589.47 6063.16 +4 3284.21 6063.16 +4 3705.26 6231.58 +4 4126.32 6715.79 +5 2568.42 3200 +5 4905.26 3136.84 +5 5221.05 2842.11 +5 5747.37 2421.05 +5 6463.16 1578.95 +5 6610.53 968.42 +5 6589.47 505.26 +5 1305.26 484.21 +5 968.42 336.84 +5 315.79 778.95 +5 315.79 3115.79 +6 9052.63 4147.37 +6 7894.74 4189.47 +6 6442.11 4105.26 +6 5915.79 3642.11 +6 5326.32 3221.05 +6 4631.58 4231.58 +6 4568.42 5010.53 +6 4884.21 5768.42 +6 5368.42 6294.74 +6 6042.11 6568.42 +6 8968.42 6526.32 +7 8736.84 9642.11 +7 9010.53 9389.47 +7 9010.53 8631.58 +7 9052.63 6778.95 +7 7789.47 6800 +7 7726.32 9642.11 +8 9073.68 2063.16 +8 9052.63 778.95 +8 8505.26 336.84 +8 7431.58 315.79 +8 7410.53 484.21 +8 6842.11 505.26 +8 6842.11 589.47 +8 6821.05 1178.95 +8 6547.37 1831.58 +8 6147.37 2378.95 +8 5600 3073.68 +8 6589.47 3894.74 +8 8863.16 3978.95 + +[SYMBOLS] +;;Gage X-Coord Y-Coord +;;-------------- ---------------- ---------------- +RG1 10084.21 8210.53 diff --git a/swmmio/tests/data/Example6.inp b/swmmio/tests/data/Example6.inp new file mode 100644 index 0000000..ac1763a --- /dev/null +++ b/swmmio/tests/data/Example6.inp @@ -0,0 +1,154 @@ +[TITLE] +;;Project Title/Notes +Example 6 +Circular Culvert with Roadway Overtopping +and Upstream Storage + +[OPTIONS] +;;Option Value +FLOW_UNITS CFS +INFILTRATION HORTON +FLOW_ROUTING DYNWAVE +LINK_OFFSETS DEPTH +MIN_SLOPE 0 +ALLOW_PONDING NO +SKIP_STEADY_STATE NO + +START_DATE 06/08/2015 +START_TIME 00:00:00 +REPORT_START_DATE 06/08/2015 +REPORT_START_TIME 00:00:00 +END_DATE 06/08/2015 +END_TIME 05:00:00 +SWEEP_START 01/01 +SWEEP_END 12/31 +DRY_DAYS 0 +REPORT_STEP 00:07:30 +WET_STEP 00:05:00 +DRY_STEP 01:00:00 +ROUTING_STEP 0:00:05 + +INERTIAL_DAMPING PARTIAL +NORMAL_FLOW_LIMITED BOTH +FORCE_MAIN_EQUATION H-W +VARIABLE_STEP 0.75 +LENGTHENING_STEP 0 +MIN_SURFAREA 12.557 +MAX_TRIALS 8 +HEAD_TOLERANCE 0.005 +SYS_FLOW_TOL 5 +LAT_FLOW_TOL 5 +;MINIMUM_STEP 0.5 +THREADS 1 + +[EVAPORATION] +;;Data Source Parameters +;;-------------- ---------------- +CONSTANT 0.0 +DRY_ONLY NO + +[JUNCTIONS] +;;Name Elevation MaxDepth InitDepth SurDepth Aponded +;;-------------- ---------- ---------- ---------- ---------- ---------- +Outlet 868 15 0 0 0 + +[OUTFALLS] +;;Name Elevation Type Stage Data Gated Route To +;;-------------- ---------- ---------- ---------------- -------- ---------------- +TailWater 858 FIXED 859.5 NO + +[STORAGE] +;;Name Elev. MaxDepth InitDepth Shape Curve Name/Params N/A Fevap Psi Ksat IMD +;;-------------- -------- ---------- ----------- ---------- ---------------------------- -------- -------- -------- -------- +Inlet 878 9 0 TABULAR StorageCurve 0 0 + +[CONDUITS] +;;Name From Node To Node Length Roughness InOffset OutOffset InitFlow MaxFlow +;;-------------- ---------------- ---------------- ---------- ---------- ---------- ---------- ---------- ---------- +Culvert Inlet Outlet 200 0.014 0 0 0 0 +Channel Outlet TailWater 200 0.03 0 0 0 0 + +[WEIRS] +;;Name From Node To Node Type CrestHt Qcoeff Gated EndCon EndCoeff Surcharge RoadWidth RoadSurf +;;-------------- ---------------- ---------------- ------------ ---------- ---------- -------- -------- ---------- ---------- ---------- ---------- +Roadway Inlet Outlet ROADWAY 5 3.33 NO 0 0 NO 40 GRAVEL + +[XSECTIONS] +;;Link Shape Geom1 Geom2 Geom3 Geom4 Barrels Culvert +;;-------------- ------------ ---------------- ---------- ---------- ---------- ---------- ---------- +Culvert CIRCULAR 3 0 0 0 2 4 +Channel TRAPEZOIDAL 9 10 2 2 1 +Roadway RECT_OPEN 50 200 0 0 + +[INFLOWS] +;;Node Constituent Time Series Type Mfactor Sfactor Baseline Pattern +;;-------------- ---------------- ---------------- -------- -------- -------- -------- -------- +Inlet FLOW Inflow FLOW 1.0 1.0 + +[CURVES] +;;Name Type X-Value Y-Value +;;-------------- ---------- ---------- ---------- +StorageCurve Storage 0 0 +StorageCurve 2 9583 +StorageCurve 4 33977 +StorageCurve 6 72310 +StorageCurve 8 136778 + +[TIMESERIES] +;;Name Date Time Value +;;-------------- ---------- ---------- ---------- +Inflow 0 0 +Inflow .125 9 +Inflow .25 10 +Inflow .375 11 +Inflow .5 13 +Inflow .625 17 +Inflow .75 28 +Inflow .875 40 +Inflow 1 80 +Inflow 1.125 136 +Inflow 1.25 190 +Inflow 1.375 220 +Inflow 1.5 220 +Inflow 1.625 201 +Inflow 1.75 170 +Inflow 1.875 140 +Inflow 2 120 +Inflow 2.125 98 +Inflow 2.25 82 +Inflow 2.375 70 +Inflow 2.5 60 +Inflow 2.625 53 +Inflow 2.75 47 +Inflow 2.875 41 + +[REPORT] +;;Reporting Options +INPUT NO +CONTROLS NO +SUBCATCHMENTS ALL +NODES ALL +LINKS ALL + +[TAGS] + +[MAP] +DIMENSIONS -59.264 5535.422 7089.237 6044.959 +Units None + +[COORDINATES] +;;Node X-Coord Y-Coord +;;-------------- ------------------ ------------------ +Outlet 4206.542 6357.994 +TailWater 6019.146 6168.726 +Inlet 1628.472 6476.537 + +[VERTICES] +;;Link X-Coord Y-Coord +;;-------------- ------------------ ------------------ +Roadway 2311.068 7166.633 +Roadway 3762.491 7151.463 + +[LABELS] +;;X-Coord Y-Coord Label +1651.426 7755.664 "Circular Culvert with Roadway Overtopping and Upstream Storage" "" "Arial" 12 1 1 diff --git a/swmmio/tests/data/__init__.py b/swmmio/tests/data/__init__.py index bcc1723..e892a3b 100644 --- a/swmmio/tests/data/__init__.py +++ b/swmmio/tests/data/__init__.py @@ -27,6 +27,8 @@ MODEL_MOD_HORTON = os.path.join(DATA_PATH, 'model_mod_horton.inp') MODEL_EX_1 = os.path.join(DATA_PATH, 'Example1.inp') MODEL_EX_1B = os.path.join(DATA_PATH, 'Example1b.inp') +MODEL_EXAMPLE6 = os.path.join(DATA_PATH, 'Example6.inp') +MODEL_EX_1_PARALLEL_LOOP = os.path.join(DATA_PATH, 'Example1_parallel_loop.inp') MODEL_INFILTRAION_PARSE_FAILURE = os.path.join(DATA_PATH, 'model-with-infiltration-parse-failure.inp') # test rpt paths diff --git a/swmmio/tests/test_functions.py b/swmmio/tests/test_functions.py index 996de79..81db0b1 100644 --- a/swmmio/tests/test_functions.py +++ b/swmmio/tests/test_functions.py @@ -1,6 +1,10 @@ +import pytest import swmmio -from swmmio.tests.data import MODEL_FULL_FEATURES__NET_PATH, OWA_RPT_EXAMPLE, RPT_FULL_FEATURES -from swmmio.utils.functions import format_inp_section_header +from swmmio.tests.data import (MODEL_FULL_FEATURES__NET_PATH, + OWA_RPT_EXAMPLE, RPT_FULL_FEATURES, + MODEL_EX_1_PARALLEL_LOOP, MODEL_EX_1) +from swmmio.utils.functions import format_inp_section_header, find_network_trace +from swmmio.utils import error from swmmio.utils.text import get_rpt_metadata @@ -41,5 +45,55 @@ def test_model_to_networkx(): assert (G['J1']['J2']['C1:C2']['Length'] == 244.63) assert (round(G.nodes['J2']['InvertElev'], 3) == 13.0) - links = m.links() - assert(len(links) == len(G.edges())) \ No newline at end of file + links = m.links.dataframe + assert(len(links) == len(G.edges())) + + +def test_network_trace_loop(): + m = swmmio.Model(MODEL_EX_1_PARALLEL_LOOP) + start_node = "9" + end_node = "18" + path_selection = find_network_trace(m, start_node, end_node, + include_nodes=[], + include_links=["LOOP"]) + correct_path = [('9', '10', '1'), + ('10', '21', '6'), + ('21', '24', 'LOOP'), + ('24', '17', '16'), + ('17', '18', '10')] + assert (path_selection == correct_path) + + +def test_network_trace_bad_link(): + m = swmmio.Model(MODEL_EX_1) + start_node = "9" + end_node = "18" + with pytest.raises(error.LinkNotInInputFile) as execinfo: + path_selection = find_network_trace(m, start_node, end_node, + include_links=["LOOP"]) + + +def test_network_trace_bad_start_node(): + m = swmmio.Model(MODEL_EX_1) + start_node = "9000" + end_node = "18" + with pytest.raises(error.NodeNotInInputFile): + path_selection = find_network_trace(m, start_node, end_node) + + +def test_network_trace_bad_end_node(): + m = swmmio.Model(MODEL_EX_1) + start_node = "9" + end_node = "18000" + with pytest.raises(error.NodeNotInInputFile): + path_selection = find_network_trace(m, start_node, end_node) + + +def test_network_trace_bad_include_node(): + m = swmmio.Model(MODEL_EX_1) + start_node = "9" + end_node = "18" + with pytest.raises(error.NodeNotInInputFile): + path_selection = find_network_trace(m, start_node, + end_node, + include_nodes=["1000"]) diff --git a/swmmio/tests/test_graphics.py b/swmmio/tests/test_graphics.py index 8210240..3f5063b 100644 --- a/swmmio/tests/test_graphics.py +++ b/swmmio/tests/test_graphics.py @@ -2,13 +2,19 @@ import tempfile from io import StringIO +import matplotlib.pyplot as plt import pandas as pd import pytest -from swmmio.tests.data import (DATA_PATH, MODEL_FULL_FEATURES_XY, MODEL_FULL_FEATURES__NET_PATH, MODEL_A_PATH) +from swmmio.tests.data import (DATA_PATH, MODEL_FULL_FEATURES_XY, + MODEL_FULL_FEATURES__NET_PATH, MODEL_A_PATH, + MODEL_EX_1_PARALLEL_LOOP) import swmmio from swmmio.graphics import swmm_graphics as sg from swmmio.utils.spatial import centroid_and_bbox_from_coords, change_crs +from swmmio import (find_network_trace, build_profile_plot, + add_hgl_plot, add_node_labels_plot, add_link_labels_plot) +import pyswmm def test_draw_model(): @@ -23,7 +29,7 @@ def test_draw_model(): def test_draw_red_and_grey_nodes(): m = swmmio.Model(MODEL_FULL_FEATURES__NET_PATH) target_img_pth = os.path.join(DATA_PATH, 'test-draw-model.png') - nodes = m.nodes() + nodes = m.nodes.dataframe nodes['draw_color'] = '#787882' nodes.loc[['J1', 'J2', 'J3'], 'draw_color'] = '#ff0000' nodes['draw_size'] = nodes['InvertElev'] * 3 @@ -34,8 +40,7 @@ def test_draw_red_and_grey_nodes(): def test_web_map_01(): - - m = swmmio.Model(MODEL_A_PATH, crs="EPSG:2817") + m = swmmio.Model(MODEL_A_PATH, crs="EPSG:2817") with tempfile.TemporaryDirectory() as tempdir: fname = os.path.join(tempdir, 'test-map.html') sg.create_map(m, filename=fname) @@ -44,7 +49,6 @@ def test_web_map_01(): def test_centroid_and_bbox_from_coords(): - m = swmmio.Model(MODEL_A_PATH, crs="EPSG:2817") m.to_crs("EPSG:4326") @@ -58,13 +62,12 @@ def test_centroid_and_bbox_from_coords(): def test_change_crs(): - m = swmmio.Model(MODEL_A_PATH, crs="EPSG:2817") v1 = m.inp.vertices v2 = change_crs(m.inp.vertices, m.crs, "WGS84") assert v1.shape == v2.shape s = """ - Name X Y + Name X Y J4-001.1 -70.959386 43.732851 J4-001.1 -70.958415 43.732578 J4-001.1 -70.959423 43.730452 @@ -73,3 +76,70 @@ def test_change_crs(): v2_test = pd.read_csv(StringIO(s), index_col=0, delim_whitespace=True, skiprows=[0]) assert v2['X'].values == pytest.approx(v2_test['X'].values, rel=1e-3) assert v2['Y'].values == pytest.approx(v2_test['Y'].values, rel=1e-3) + + +profile_selection_assert = \ + { + 'nodes': + [{'id_name': '9', 'rolling_x_pos': 0.0, 'invert_el': 1000.0}, + {'id_name': '10', 'rolling_x_pos': 400.0, 'invert_el': 995.0}, + {'id_name': '21', 'rolling_x_pos': 800.0, 'invert_el': 990.0}, + {'id_name': '24', 'rolling_x_pos': 1300.0, 'invert_el': 984.0}, + {'id_name': '17', 'rolling_x_pos': 1700.0, 'invert_el': 980.0}, + {'id_name': '18', 'rolling_x_pos': 2100.0, 'invert_el': 975.0}], + 'links': + [{'id_name': '1', 'rolling_x_pos': 200.0, 'midpoint_bottom': 997.5}, + {'id_name': '6', 'rolling_x_pos': 600.0, 'midpoint_bottom': 993.0}, + {'id_name': 'LOOP', 'rolling_x_pos': 1050.0, 'midpoint_bottom': 987.0}, + {'id_name': '16', 'rolling_x_pos': 1500.0, 'midpoint_bottom': 982.0}, + {'id_name': '10', 'rolling_x_pos': 1900.0, 'midpoint_bottom': 977.5}], + 'path_selection': + [('9', '10', '1'), + ('10', '21', '6'), + ('21', '24', 'LOOP'), + ('24', '17', '16'), + ('17', '18', '10')] + } + + +def test_profile(): + with tempfile.TemporaryDirectory() as tempdir: + # instantiate a swmmio model object, save copy in temp dir + temp_model_path = os.path.join(tempdir, 'model.inp') + temp_pdf_path = os.path.join(tempdir, 'test.pdf') + mymodel = swmmio.Model(MODEL_EX_1_PARALLEL_LOOP) + mymodel.inp.save(temp_model_path) + + with pyswmm.Simulation(temp_model_path) as sim: + for step in sim: + pass + # instantiate a swmmio model object + mymodel = swmmio.Model(temp_model_path) + + depths = mymodel.rpt.node_depth_summary.MaxNodeDepthReported + + fig = plt.figure(figsize=(11, 8)) + fig.suptitle("TEST") + ax = fig.add_subplot(2, 1, 1) + path_selection = find_network_trace(mymodel, '9', '18', include_links=["LOOP"]) + profile_config = build_profile_plot(ax, mymodel, path_selection) + assert (profile_config == profile_selection_assert) + add_hgl_plot(ax, profile_config, depth=depths, label="Max HGL") + add_node_labels_plot(ax, mymodel, profile_config) + add_link_labels_plot(ax, mymodel, profile_config) + leg = ax.legend() + ax.grid('xy') + + ax2 = fig.add_subplot(2, 1, 2) + path_selection = find_network_trace(mymodel, '19', '18', include_nodes=['22']) + profile_config = build_profile_plot(ax2, mymodel, path_selection) + add_hgl_plot(ax2, profile_config, depth=depths, label="Max HGL") + add_node_labels_plot(ax2, mymodel, profile_config) + add_link_labels_plot(ax2, mymodel, profile_config) + leg = ax2.legend() + ax2.grid('xy') + + fig.tight_layout() + plt.savefig(temp_pdf_path) + plt.close() + assert (os.path.exists(temp_pdf_path)) diff --git a/swmmio/tests/test_run_models.py b/swmmio/tests/test_run_models.py new file mode 100644 index 0000000..b409ff1 --- /dev/null +++ b/swmmio/tests/test_run_models.py @@ -0,0 +1,52 @@ +import argparse +import os +import tempfile +import unittest +from unittest import mock + +import swmmio +from swmmio.examples import philly, jerzey +from swmmio.run_models.run import run_simple, run_hot_start_sequence + + +class TestRunModels(unittest.TestCase): + def setUp(self) -> None: + pass + + def test_run_simple(self): + with tempfile.TemporaryDirectory() as tmp_dir: + + # create copy of model in temp dir + inp_path = os.path.join(tmp_dir, 'philly-example.inp') + philly.inp.save(inp_path) + + # run the model + return_code = run_simple(inp_path) + self.assertEqual(return_code, 0) + + m = swmmio.Model(inp_path) + self.assertTrue(m.rpt_is_valid()) + + def test_run_hot_start_sequence(self): + with tempfile.TemporaryDirectory() as tmp_dir: + + # create copy of model in temp dir + inp_path = os.path.join(tmp_dir, 'philly-example.inp') + philly.inp.save(inp_path) + + # run the model + return_code = run_hot_start_sequence(inp_path) + self.assertEqual(return_code, 0) + + m = swmmio.Model(inp_path) + self.assertTrue(m.rpt_is_valid()) + + @mock.patch('argparse.ArgumentParser.parse_args', + return_value=argparse.Namespace( + model_to_run=[jerzey.inp.path] + )) + def test_swmmio_run(self, mock_args): + from swmmio import __main__ + self.assertEqual(__main__.main(), 0) + + diff --git a/swmmio/utils/error.py b/swmmio/utils/error.py new file mode 100644 index 0000000..67709a6 --- /dev/null +++ b/swmmio/utils/error.py @@ -0,0 +1,42 @@ +""" +Error.py contains the long noted error strings to assist the users in quickly +identifying what is wrong with their code. +""" + +NO_TRACE_FOUND = \ + """ + + No path has been discovered between your start and end nodes. + If you are sure there IS a path, look over your links to see + if any need to be reversed in your INP file. The network + traverse features are one-directional. Alternately, you might have + parallel loop items present in the include nodes/links. + + """ + +class NoTraceFound(Exception): + """ + Exception raised for impossible network trace. + """ + def __init__(self): + self.message = NO_TRACE_FOUND + super().__init__() + + +class NodeNotInInputFile(Exception): + """ + Exception raised for incomplete simulation. + """ + def __init__(self, node): + self.message = "Node {} is not present in INP file.".format(node) + print(self.message) + super().__init__(self.message) + +class LinkNotInInputFile(Exception): + """ + Exception raised for incomplete simulation. + """ + def __init__(self, link): + self.message = "Link {} is not present in INP file.".format(link) + print(self.message) + super().__init__(self.message) diff --git a/swmmio/utils/functions.py b/swmmio/utils/functions.py index 3004a34..c73c23d 100644 --- a/swmmio/utils/functions.py +++ b/swmmio/utils/functions.py @@ -1,7 +1,8 @@ +import warnings import pandas as pd import networkx as nx -import warnings -# from swmmio import get_inp_options_df, INFILTRATION_COLS + +from swmmio.utils import error def random_alphanumeric(n=6): @@ -11,7 +12,6 @@ def random_alphanumeric(n=6): def model_to_networkx(model, drop_cycles=True): - from swmmio.utils.dataframes import dataframe_from_rpt ''' Networkx MultiDiGraph representation of the model ''' @@ -217,3 +217,59 @@ def trace(node_id): return {'nodes': traced_nodes, 'conduits': traced_conduits} +def find_network_trace(model, start_node, end_node, + include_nodes=None, include_links=None): + """ + This function searches for a path between two nodes. In addition, since + SWMM allows multiple links (edges) between nodes, the user can specify + a list of both nodes, and links to include in the path. It will return a + single path selection. + + :param model: swmmio.Model object + :param start_node: string of Node Name + :param end_node: string of Node Name + :param include_nodes: list of node name strings + :param include_links: list of link name strings + :return: Network Path Trace Tuple + """ + nodes = model.nodes.dataframe + links = model.links.dataframe + model_digraph = model.network + + include_nodes = [] if include_nodes is None else include_nodes + include_links = [] if include_links is None else include_links + + if str(start_node) not in nodes.index: + raise(error.NodeNotInInputFile(start_node)) + if str(end_node) not in nodes.index: + raise(error.NodeNotInInputFile(end_node)) + for node_id in include_nodes: + if str(node_id) not in nodes.index: + raise(error.NodeNotInInputFile(node_id)) + for link_id in include_links: + if str(link_id) not in links.index: + raise(error.LinkNotInInputFile(link_id)) + + simple_paths = nx.all_simple_edge_paths(model_digraph, start_node, end_node) + path_selection = None + for path_index, path_info in enumerate(simple_paths): + included_nodes = {name: False for name in include_nodes} + included_links = {name: False for name in include_links} + for selection in path_info: + us, ds, link = selection + if us in included_nodes.keys(): + included_nodes[us] = True + if ds in included_nodes.keys(): + included_nodes[ds] = True + if link in included_links.keys(): + included_links[link] = True + + if False not in [included_nodes[key] for key in included_nodes.keys()]: + if False not in [included_links[key] for key in included_links.keys()]: + path_selection = path_info + break + + if path_selection is None: + raise error.NoTraceFound + + return path_selection diff --git a/swmmio/wrapper/pyswmm_wrapper.py b/swmmio/wrapper/pyswmm_wrapper.py new file mode 100644 index 0000000..55d735c --- /dev/null +++ b/swmmio/wrapper/pyswmm_wrapper.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright (c) 2022 Bryant E. McDonnell +# +# Licensed under the terms of the BSD2 License +# See LICENSE.txt for details +# ----------------------------------------------------------------------------- +""" +Function developed to execute a PySWMM Simulation on the command line. To run, +execute the following on the command line: + +python --help # Produces a list of options + +python /pyswmm_wrapper.py /*.inp /*.rpt /*.out # optional args +""" + +import argparse +import pathlib +import pyswmm + + +def run_model(): + """Run Model.""" + # Argument Resolution + parser = argparse.ArgumentParser() + parser.add_argument('inp_file', type=pathlib.Path, + help='Input File Path') + parser.add_argument('rpt_file', type=pathlib.Path, nargs='?', + help='Report File Path (Optional).') + parser.add_argument('out_file', type=pathlib.Path, nargs='?', + help='Output File Path (Optional).') + + report_prog_help = "--report-progress can be useful for longer model runs. The drawback "\ + +"is that it slows the simulation down. Use an integer to specify how "\ + +"frequent to interrup the simulation. This depends of the number of time "\ + +"steps" + parser.add_argument('--report_progress', default=False, type=int, + help=report_prog_help) + args = parser.parse_args() + + # File Naming -> Str Paths + inp_file = str(args.inp_file) + if args.rpt_file: + rpt_file = str(args.rpt_file) + else: + rpt_file = args.rpt_file + out_file = str(args.out_file) + if args.out_file: + out_file = str(args.out_file) + else: + out_file = args.out_file + + # Running the simulation without and with progress reporting. + if args.report_progress == False: + sim = pyswmm.Simulation(inp_file, rpt_file, out_file) + sim.execute() + else: + with pyswmm.Simulation(inp_file, rpt_file, out_file) as sim: + for ind, step in enumerate(sim): + if ind % args.report_progress == 0: + print(round(sim.percent_complete*1000)/10.0) + + return 0 + +if __name__ in "__main__": + run_model()