diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 2160f1e..212a8d9 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -44,7 +44,7 @@ jobs: path: './docs/build' deploy: - if: startsWith(github.event.ref, 'refs/tags/v') + # if: startsWith(github.event.ref, 'refs/tags/v') environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} diff --git a/.gitignore b/.gitignore index 2e38097..08a3a0a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,8 @@ _build/ *.thm # env -venv/ \ No newline at end of file +venv/ + +# docs artifacts +docs/_build/ +docs/site/ \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index e48e09a..f9c82a9 100644 --- a/AUTHORS +++ b/AUTHORS @@ -6,6 +6,7 @@ Assela Pathirana Bruce Rindahl Bryant E. McDonnell BuczynskiRafal +David Irwin Jackie Fortin-Flefil Jenn Wu Stijn Van Hoey diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b0528..13d2f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## Version 0.7.2 (2024/11/27) + +### What's Changed +* Handle several Pandas FutureWarnings, provided by @dirwin5 [PR224](https://github.com/pyswmm/swmmio/pull/224) +* Fix docs build process and update docs layout and theme [PR220](https://github.com/pyswmm/swmmio/pull/220) +* Fixed typos in the README [226](https://github.com/pyswmm/swmmio/pull/226) + + ## Version 0.7.1 (2024/08/19) ### What's Changed diff --git a/README.md b/README.md index 5b14174..0d82aa4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # swmmio -*v0.7.1 (2024/08/19)* +*v0.7.2 (2024/11/27)* [![Build status](https://ci.appveyor.com/api/projects/status/qywujm5w2wm0y2tv/branch/master?svg=true)](https://ci.appveyor.com/project/aerispaha/swmmio/branch/master) ![example workflow](https://github.com/aerispaha/swmmio/actions/workflows/python-app.yml/badge.svg) diff --git a/docs/img/default_draw.png b/docs/_static/img/default_draw.png similarity index 100% rename from docs/img/default_draw.png rename to docs/_static/img/default_draw.png diff --git a/docs/img/flooded_anno_example.png b/docs/_static/img/flooded_anno_example.png similarity index 100% rename from docs/img/flooded_anno_example.png rename to docs/_static/img/flooded_anno_example.png diff --git a/docs/img/impact_of_option.png b/docs/_static/img/impact_of_option.png similarity index 100% rename from docs/img/impact_of_option.png rename to docs/_static/img/impact_of_option.png diff --git a/docs/conf.py b/docs/conf.py index e98e6bd..778b388 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,8 +12,9 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -from recommonmark.transform import AutoStructify -from m2r import MdInclude +# from recommonmark.transform import AutoStructify +# from m2r import MdInclude +from datetime import datetime import os import sys import swmmio @@ -23,11 +24,11 @@ # -- Project information ----------------------------------------------------- project = 'swmmio' -copyright = '2022, Adam Erispaha' +copyright = f'{datetime.now().year}, Adam Erispaha' author = 'Adam Erispaha' # The short X.Y version -version = '' +version = swmmio.__version__ # The full version, including alpha/beta/rc tags release = swmmio.__version__ @@ -46,7 +47,8 @@ 'sphinx.ext.coverage', 'sphinx.ext.mathjax', 'sphinx.ext.viewcode', - 'recommonmark', + 'sphinx.ext.autosummary', + 'myst_parser', ] # Add any paths that contain templates here, relative to this directory. @@ -55,8 +57,10 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = ['.rst', '.md'] -# source_suffix = '.rst' +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', +} # The master toctree document. master_doc = 'index' @@ -66,12 +70,12 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' @@ -83,7 +87,7 @@ # a list of builtin themes. # # html_theme = 'alabaster' -html_theme = "sphinx_rtd_theme" +html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -94,7 +98,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['.'] +html_static_path = ['_static'] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -181,27 +185,3 @@ # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] - -# -- Extension configuration ------------------------------------------------- -def setup(app): - config = { - # 'url_resolver': lambda url: github_doc_root + url, - 'auto_toc_tree_section': 'Contents', - 'enable_eval_rst': True, - } - app.add_config_value('recommonmark_config', config, True) - app.add_transform(AutoStructify) - - # from m2r to make `mdinclude` work - app.add_config_value('no_underscore_emphasis', False, 'env') - app.add_config_value('m2r_parse_relative_links', False, 'env') - app.add_config_value('m2r_anonymous_references', False, 'env') - app.add_config_value('m2r_disable_inline_math', False, 'env') - app.add_directive('mdinclude', MdInclude) - - -# custom logic -from shutil import copytree -import os -os.makedirs('_build/html/docs', exist_ok=True) -copytree('img', '_build/html/docs/img') diff --git a/docs/index.rst b/docs/index.rst index 5518444..c5f07f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,20 @@ -.. mdinclude:: ../README.md +swmmio +======================== + +.. image:: _static/img/impact_of_option.png + :alt: Impact of Option + + +`swmmio` provides a Pythonic interface to the EPA Stormwater Management Model (SWMM) +allowing engineers to programmatically create, modify, and +analyze SWMM models. + +With `swmmio`, model parameters encoded in the `.inp` file can be +retrieved conveniently as Pandas dataframes making it possible to make +automate your hydraulics and hydologic modeling workflow. After simulations are +completed, results in the `.rpt` file can also be retrieved as Pandas dataframes, +making post-processing automation trivial. + .. swmmio documentation master file, created by @@ -6,15 +22,17 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. + +Contents +================== + .. toctree:: :maxdepth: 2 - :caption: Contents: - swmmio - elements - version_control - utils - graphics + reference/index + installing + usage + Indices and tables ================== diff --git a/docs/installing.md b/docs/installing.md new file mode 100644 index 0000000..e5202dc --- /dev/null +++ b/docs/installing.md @@ -0,0 +1,8 @@ +### Installation +Before installation, it's recommended to first activate a [virtualenv](https://github.com/pypa/virtualenv) to +not crowd your system's package library. If you don't use any of the dependencies listed above, +this step is less important. `swmmio` can be installed via pip in your command line: + +```console +pip install swmmio +``` diff --git a/docs/swmmio.rst b/docs/reference/core.rst similarity index 83% rename from docs/swmmio.rst rename to docs/reference/core.rst index a257436..37ef94e 100644 --- a/docs/swmmio.rst +++ b/docs/reference/core.rst @@ -1,4 +1,4 @@ -`swmmio` Core Objects +`swmmio` core objects ======================== .. automodule:: swmmio.core diff --git a/docs/elements.rst b/docs/reference/elements.rst similarity index 100% rename from docs/elements.rst rename to docs/reference/elements.rst diff --git a/docs/graphics.rst b/docs/reference/graphics.rst similarity index 100% rename from docs/graphics.rst rename to docs/reference/graphics.rst diff --git a/docs/reference/index.rst b/docs/reference/index.rst new file mode 100644 index 0000000..eeb7c94 --- /dev/null +++ b/docs/reference/index.rst @@ -0,0 +1,20 @@ +.. -*- coding: utf-8 -*- + +Reference +********* +.. only:: html + + :Release: |release| + :Date: |today| + + +.. toctree:: + :maxdepth: 2 + + core + elements + utils + graphics + version_control + +.. only:: html \ No newline at end of file diff --git a/docs/utils.rst b/docs/reference/utils.rst similarity index 100% rename from docs/utils.rst rename to docs/reference/utils.rst diff --git a/docs/version_control.rst b/docs/reference/version_control.rst similarity index 100% rename from docs/version_control.rst rename to docs/reference/version_control.rst diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..78eecb7 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,153 @@ +# Usage +The `swmmio.Model()` class provides the basic endpoint for interfacing with SWMM models. To get started, save a SWMM5 +model (.inp) in a directory with its report file (.rpt). A few examples: + +```python +import swmmio + +# instantiate a swmmio model object +mymodel = swmmio.Model('/path/to/directory with swmm files') + +# dataframe with useful data related to model nodes, conduits, and subcatchments +nodes = mymodel.nodes.dataframe +links = mymodel.links.dataframe +subs = mymodel.subcatchments.dataframe + +# enjoy all the Pandas functions +nodes.head() + +# write to a csv +nodes.to_csv('/path/mynodes.csv') + +# calculate average and weighted average impervious +avg_imperviousness = subs.PercImperv.mean() +weighted_avg_imp = (subs.Area * subs.PercImperv).sum() / len(subs) +``` + +# Nodes and Links Objects +Specific sections of data from the inp and rpt can be extracted with `Nodes` and `Links` objects. +Although these are the same object-type of the `swmmio.Model.nodes` and `swmmio.Model.links`, +accessing them directly allows for custom control over what sections of data are retrieved. + +```python +from swmmio import Model, Nodes +m = Model("coolest-model.inp") + +# pass custom init arguments into the Nodes object instead of using default settings referenced by m.nodes() +nodes = Nodes( + model=m, + inp_sections=['junctions', 'storage', 'outfalls'], + rpt_sections=['Node Depth Summary', 'Node Inflow Summary'], + columns=[ 'InvertElev', 'MaxDepth', 'InitDepth', 'SurchargeDepth', 'MaxTotalInflow', 'coords'] +) + +# access data +nodes.dataframe +``` + +# Generating Graphics +Create an image (.png) visualization of the model. By default, pipe stress and node flood duration is +visualized if your model includes output data (a .rpt file should accompany the .inp). + +```python +swmmio.draw_model(mymodel) +``` + +![Default Draw Output](_static/img/default_draw.png "Sewer Stress, Node Flooding") + +Use pandas to calculate some interesting stats, and generate a image to highlight +what's interesting or important for your project: + +```python +#isolate nodes that have flooded for more than 30 minutes +flooded_series = nodes.loc[nodes.HoursFlooded>0.5, 'TotalFloodVol'] +flood_vol = sum(flooded_series) #total flood volume (million gallons) +flooded_count = len(flooded_series) #count of flooded nodes + +#highlight these nodes in a graphic +nodes['draw_color'] = '#787882' #grey, default node color +nodes.loc[nodes.HoursFlooded>0.5, 'draw_color'] = '#751167' #purple, flooded nodes + +#set the radius of flooded nodes as a function of HoursFlooded +nodes.loc[nodes.HoursFlooded>1, 'draw_size'] = nodes.loc[nodes.HoursFlooded>1, 'HoursFlooded'] * 12 + +#make the conduits grey, sized as function of their geometry +conds['draw_color'] = '#787882' +conds['draw_size'] = conds.Geom1 + +#add an informative annotation, and draw: +annotation = 'Flooded Volume: {}MG\nFlooded Nodes:{}'.format(round(flood_vol), flooded_count) +swmmio.draw_model(mymodel, annotation=annotation, file_path='flooded_anno_example.png') +``` +![Flooded highlight](_static/img/flooded_anno_example.png "Node Flooding with annotation") + +# Building Variations of Models +Starting with a base SWMM model, other models can be created by inserting altered data into a new inp file. Useful for sensitivity analysis or varying boundary conditions, models can be created using a fairly simple loop, leveraging the `modify_model` package. + +For example, climate change impacts can be investigated by creating a set of models with varying outfall Fixed Stage elevations: + +```python +import os +import swmmio + +# initialize a baseline model object +baseline = swmmio.Model(r'path\to\baseline.inp') +rise = 0.0 #set the starting sea level rise condition + +# create models up to 5ft of sea level rise. +while rise <= 5: + + # create a dataframe of the model's outfalls + outfalls = baseline.inp.outfalls + + # create the Pandas logic to access the StageOrTimeseries column of FIXED outfalls + slice_condition = outfalls.OutfallType == 'FIXED', 'StageOrTimeseries' + + # add the current rise to the outfalls' stage elevation + outfalls.loc[slice_condition] = pd.to_numeric(outfalls.loc[slice_condition]) + rise + baseline.inp.outfalls = outfalls + + # copy the base model into a new directory + newdir = os.path.join(baseline.inp.dir, str(rise)) + os.mkdir(newdir) + newfilepath = os.path.join(newdir, baseline.inp.name + "_" + str(rise) + '_SLR.inp') + + # Overwrite the OUTFALLS section of the new model with the adjusted data + baseline.inp.save(newfilepath) + + # increase sea level rise for the next loop + rise += 0.25 + +``` + +# Access Model Network +The `swmmio.Model` class returns a Networkx MultiDiGraph representation of the model via that `network` parameter: +```python + +# access the model as a Networkx MutliDiGraph +G = model.network + +# iterate through links +for u, v, key, data in model.network.edges(data=True, keys=True): + + print (key, data['Geom1']) + # do stuff with the network +``` + +# Running Models +Using the command line tool, individual SWMM5 models can be run by invoking the swmmio module in your shell as such: +```shell +python -m swmmio --run path/to/mymodel.inp +``` +If you have many models to run and would like to take advantage of your machine's cores, you can start a pool of simulations with the `--start_pool` (or `-sp`) command. After pointing `-sp` to one or more directories, swmmio will search for SWMM .inp files and add all them to a multiprocessing pool. By default, `-sp` leaves 4 of your machine's cores unused. This can be changed via the `-cores_left` argument. +```shell +# run all models in models in directories Model_Dir1 Model_Dir2 +python -m swmmio -sp Model_Dir1 Model_Dir2 + +# leave 1 core unused +python -m swmmio -sp Model_Dir1 Model_Dir2 -cores_left=1 +``` +
+

Warning

+

Using all cores for simultaneous model runs can put your machine's CPU usage at 100% for extended periods of time. This probably puts stress on your hardware. Use at your own risk.

+
diff --git a/swmmio/__init__.py b/swmmio/__init__.py index 2f668d0..ab2f912 100644 --- a/swmmio/__init__.py +++ b/swmmio/__init__.py @@ -11,7 +11,7 @@ '''Python SWMM Input/Output Tools''' -VERSION_INFO = (0, 7, 2, 'dev0') +VERSION_INFO = (0, 7, 2) __version__ = '.'.join(map(str, VERSION_INFO)) __author__ = 'Adam Erispaha' __copyright__ = 'Copyright (c) 2024' diff --git a/swmmio/graphics/profiler.py b/swmmio/graphics/profiler.py index fd9552b..269efe5 100644 --- a/swmmio/graphics/profiler.py +++ b/swmmio/graphics/profiler.py @@ -32,7 +32,7 @@ def build_profile_plot(ax, model, path_selection): us_node, ds_node, link_id = link_set # Plot first Node if ind == 0: - invert_el = float(nodes.loc[[us_node]].InvertElev) + 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}) @@ -43,18 +43,18 @@ def build_profile_plot(ax, model, path_selection): # 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": + if links.loc[link_id].Type == "CONDUIT": + rolling_x_pos += float(links.loc[link_id].Length) + elif links.loc[link_id].Type == "WEIR": rolling_x_pos += DEFAULT_WEIR_LENGTH - elif links.loc[[link_id]].Type[0] == "ORIFICE": + elif links.loc[link_id].Type == "ORIFICE": rolling_x_pos += DEFAULT_ORIFICE_LENGTH - elif links.loc[[link_id]].Type[0] == "PUMP": + elif links.loc[link_id].Type == "PUMP": rolling_x_pos += DEFAULT_PUMP_LENGTH - elif links.loc[[link_id]].Type[0] == "OUTLET": + elif links.loc[link_id].Type == "OUTLET": rolling_x_pos += DEFAULT_OUTLET_LENGTH # Plot DS node - invert_el = float(nodes.loc[[ds_node]].InvertElev) + 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}) @@ -87,17 +87,17 @@ def _add_node_plot(ax, x, model, node_name, link_set, surcharge_depth=0, width=M nodes = model.nodes.dataframe links = model.links.dataframe - invert_el = float(nodes.loc[[node_name]].InvertElev) + 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) + 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) + 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) + depth = float(nodes.loc[node_name].MaxD) # Plotting Configuration ll_x, ll_y = x - width, invert_el @@ -136,17 +136,17 @@ def _add_link_plot(ax, us_x_position, ds_x_position, model, link_set, width=0, g if model.inp.options.loc['LINK_OFFSETS','Value'] == "ELEVATION": us_node_el, ds_node_el = 0.0, 0.0 else: - us_node_el = float(nodes.loc[[us_node]].InvertElev) - ds_node_el = float(nodes.loc[[ds_node]].InvertElev) + 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] + link_type = links.loc[link_id].Type 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) + 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 @@ -158,10 +158,10 @@ def _add_link_plot(ax, us_x_position, ds_x_position, model, link_set, width=0, g 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) + 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 @@ -186,8 +186,8 @@ def _add_link_plot(ax, us_x_position, ds_x_position, model, link_set, width=0, g 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) + 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 @@ -274,17 +274,17 @@ def add_node_labels_plot(ax, model, profile_config, font_size=8, label_y_max = 0 for val in profile_config['nodes']: name = val['id_name'] - invert_el = float(nodes.loc[[name]].InvertElev) + 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) + 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) + depth = float(nodes.loc[name].MaxD) calc = invert_el + depth if calc > label_y_max: @@ -298,17 +298,17 @@ def add_node_labels_plot(ax, model, profile_config, font_size=8, stagger_value = 4 name = val['id_name'] x_offset = val['rolling_x_pos'] - invert_el = float(nodes.loc[[name]].InvertElev) + 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) + 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) + 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), diff --git a/swmmio/tests/test_graphics.py b/swmmio/tests/test_graphics.py index 83ff7e8..734a941 100644 --- a/swmmio/tests/test_graphics.py +++ b/swmmio/tests/test_graphics.py @@ -83,7 +83,7 @@ def test_change_crs(): J4-001.1 -70.959423 43.730452 J2-095.1 -70.951378 43.767796 """ - v2_test = pd.read_csv(StringIO(s), index_col=0, delim_whitespace=True, skiprows=[0]) + v2_test = pd.read_csv(StringIO(s), index_col=0, sep=r'\s+', 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) diff --git a/swmmio/utils/dataframes.py b/swmmio/utils/dataframes.py index 95507fb..b5ad1a3 100644 --- a/swmmio/utils/dataframes.py +++ b/swmmio/utils/dataframes.py @@ -104,7 +104,7 @@ def dataframe_from_rpt(rpt_path, section, element_id=None): # extract the string and read into a dataframe s = extract_section_of_file(rpt_path, start_strings, end_strings) - df = pd.read_csv(StringIO(s), header=None, sep='\s+', skiprows=[0], + df = pd.read_csv(StringIO(s), header=None, sep=r'\s+', skiprows=[0], index_col=0, names=cols) # confirm index name is string @@ -164,7 +164,7 @@ def dataframe_from_inp(inp_path, section, additional_cols=None, quote_replace=' return pd.read_csv(StringIO(s), delim_whitespace=False) else: try: - df = pd.read_csv(StringIO(s), header=None, sep='\s+', + df = pd.read_csv(StringIO(s), header=None, sep=r'\s+', skiprows=[0], index_col=0, names=cols) except: raise IndexError(f'failed to parse {section} with cols: {cols}. head:\n{s[:500]}') @@ -225,7 +225,7 @@ def get_inp_options_df(inp_path): ops_tag = '[OPTIONS]' ops_cols = INP_OBJECTS['OPTIONS']['columns'] ops_string = extract_section_of_file(inp_path, ops_tag, INP_SECTION_TAGS, comment=';') - ops_df = pd.read_csv(StringIO(ops_string), header=None, delim_whitespace=True, skiprows=[0], + ops_df = pd.read_csv(StringIO(ops_string), header=None, sep=r'\s+', skiprows=[0], index_col=0, names=ops_cols) return ops_df