From 21a8d1484cfa87f1144bada6ba6dbbd2973bcfe0 Mon Sep 17 00:00:00 2001 From: dbhart Date: Sat, 16 Sep 2023 11:55:48 -0600 Subject: [PATCH 01/15] Update epanet exceptions --- wntr/epanet/exceptions.py | 189 ++++++++++++++++++++++++++ wntr/epanet/toolkit.py | 278 +++++++++++++++++++++++++------------- 2 files changed, 376 insertions(+), 91 deletions(-) create mode 100644 wntr/epanet/exceptions.py diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py new file mode 100644 index 000000000..5a2bdf353 --- /dev/null +++ b/wntr/epanet/exceptions.py @@ -0,0 +1,189 @@ +from enum import IntEnum +from typing import List + +EN_ERROR_CODES = { + # Runtime errors + 1: "At %s, system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials", + 2: "At %s, system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed", + 3: "At %s, system disconnected - one or more nodes with positive demands were disconnected for all supply sources", + 4: "At %s, pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow", + 5: "At %s, vavles cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open", + 6: "At %s, system has negative pressures - negative pressures occurred at one or more junctions with positive demand", + 101: "insufficient memory available", + 102: "no network data available", + 103: "hydraulics not initialized", + 104: "no hydraulics for water quality analysis", + 105: "water quality not initialized", + 106: "no results saved to report on", + 107: "hydraulics supplied from external file", + 108: "cannot use external file while hydraulics solver is active", + 109: "cannot change time parameter when solver is active", + 110: "cannot solve network hydraulic equations", + 120: "cannot solve water quality transport equations", + # Apply only to an input file + 200: "one or more errors in input file", + 201: "syntax error", + # Apply to both IO file and API functions + 202: "illegal numeric value", + 203: "undefined node %s", + 204: "undefined link %s", + 205: "undefined time pattern %s", + 206: "undefined curve %s", + 207: "attempt to control a CV/GPV link", + 208: "illegal PDA pressure limits", + 209: "illegal node property value", + 211: "illegal link property value", + 212: "undefined trace node", + 213: "invalid option value", + 214: "too many characters in input line", + 215: "duplicate ID label", + 216: "reference to undefined pump", + 217: "invalid pump energy data", + 219: "illegal valve connection to tank node", + 220: "illegal valve connection to another valve", + 221: "misplaced rule clause in rule-based control", + 222: "link assigned same start and end nodes", + # Network consistency + 223: "not enough nodes in network", + 224: "no tanks or reservoirs in network", + 225: "invalid lower/upper levels for tank", + 226: "no head curve or power rating for pump", + 227: "invalid head curve for pump", + 230: "nonincreasing x-values for curve", + 233: "network has unconnected node", + 234: "network has an unconnected node with ID %s", + # API functions only + 240: "nonexistent water quality source", + 241: "nonexistent control", + 250: "invalid format (e.g. too long an ID name)", + 251: "invalid parameter code", + 252: "invalid ID name", + 253: "nonexistent demand category", + 254: "node with no coordinates", + 255: "invalid link vertex", + 257: "nonexistent rule", + 258: "nonexistent rule clause", + 259: "attempt to delete a node that still has links connected to it", + 260: "attempt to delete node assigned as a Trace Node", + 261: "attempt to delete a node or link contained in a control", + 262: "attempt to modify network structure while a solver is open", + 263: "node is not a tank", + # File errors + 301: "identical file names used for different types of files", + 302: "cannot open input file %s", + 303: "cannot open report file %s", + 304: "cannot open binary output file %s", + 305: "cannot open hydraulics file %s", + 306: "hydraulics file does not match network data", + 307: "cannot read hydraulics file %s", + 308: "cannot save results to binary file %s", + 309: "cannot save results to report file %s", +} + +class EpanetErrors(IntEnum): + insufficient_memory = 101 + no_network = 102 + no_init_hyd = 103 + no_hydraulics = 104 + no_init_qual = 105 + no_results = 106 + hyd_file = 107 + hyd_init_and_hyd_file = 108 + modify_time_during_solve = 109 + solve_hyd_fail = 110 + solve_qual_fail = 120 + input_file_error = 200 + syntax_error = 201 + illegal_numeric_value = 202 + undefined_node = 203 + undefined_link = 204 + undefined_pattern = 205 + undefined_curve = 206 + control_on_cv_gpv = 207 + illegal_pda_limits = 208 + illegal_node_property = 209 + illegal_link_property = 211 + undefined_trace_node = 212 + invalid_option_value = 213 + too_many_chars_inp = 214 + duplicate_id = 215 + undefined_pump = 216 + invalid_energy_value = 217 + illegal_valve_tank = 219 + illegal_tank_valve = 219 + illegal_valve_valve = 220 + misplaced_rule = 221 + link_to_self = 222 + not_enough_nodes = 223 + no_tanks_or_res = 224 + invalid_tank_levels = 225 + missing_pump_data = 226 + invalid_head_curve = 227 + nonincreasing_x_curve = 230 + unconnected_node = 233 + unconnected_node_id = 234 + no_such_source_node = 240 + no_such_control = 241 + invalid_name_format = 250 + invalid_parameter_code = 251 + invalid_id_name = 252 + no_such_demand_category = 253 + missing_coords = 254 + invalid_vertex = 255 + no_such_rule = 257 + no_such_rule_clause = 258 + delete_node_still_linked = 259 + delete_node_is_trace = 260 + delete_node_in_control = 261 + modify_network_during_solve = 262 + node_not_a_tank = 263 + same_file_names = 301 + open_inp_fail = 302 + open_rpt_fail = 303 + open_bin_fail = 304 + open_hyd_fail = 305 + hyd_file_different_network = 306 + read_hyd_fail = 307 + save_bin_fail = 308 + save_rpt_fail = 309 + + +class EpanetException(Exception): + + def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> None: + if isinstance(code, EpanetErrors): + code = int(code) + elif isinstance(code, str): + try: + code = code.strip().replace('-','_').replace(' ','_') + code = int(EpanetErrors[code]) + except KeyError: + return super().__init__('unknown error code: {}'.format(repr(code)), *args) + elif not isinstance(code, int): + return super().__init__('unknown error code: {}'.format(repr(code)), *args) + msg = EN_ERROR_CODES.get(code, 'unknown error') + if args is not None: + args = [*args] + if r'%' in msg and len(args) > 0: + msg = msg % repr(args.pop(0)) + if len(args) > 0: + msg = msg + ' ' + repr(args) + if line_num: + msg = msg + ", at line {}".format(line_num) + if line: + msg = msg + ':\n ' + str(line) + msg = '(Error {}) '.format(code) + msg + super().__init__(msg) + +class ENSyntaxError(EpanetException, SyntaxError): + def __init__(self, code, *args) -> None: + super().__init__(code, *args) + +class ENNameError(EpanetException, NameError): + def __init__(self, code, name, *args) -> None: + super().__init__(code, name, *args) + +class ENValueError(EpanetException, ValueError): + def __init__(self, code, value, *args) -> None: + super().__init__(code, value, *args) + diff --git a/wntr/epanet/toolkit.py b/wntr/epanet/toolkit.py index 8afbb7578..21ffb67a5 100644 --- a/wntr/epanet/toolkit.py +++ b/wntr/epanet/toolkit.py @@ -37,9 +37,7 @@ # import warnings - -class EpanetException(Exception): - pass +from .exceptions import EpanetException, EN_ERROR_CODES def ENgetwarning(code, sec=-1): @@ -48,41 +46,15 @@ def ENgetwarning(code, sec=-1): sec -= hours * 3600 mm = int(sec / 60.0) sec -= mm * 60 - header = "At %3d:%.2d:%.2d, " % (hours, mm, sec) + header = "%3d:%.2d:%.2d" % (hours, mm, sec) else: - header = "" - if code == 1: - return ( - header - + "System hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials" - ) - elif code == 2: - return ( - header - + "System may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed" - ) - elif code == 3: - return ( - header - + "System disconnected - one or more nodes with positive demands were disconnected for all supply sources" - ) - elif code == 4: - return ( - header - + "Pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow" - ) - elif code == 5: - return ( - header - + "Vavles cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open" - ) - elif code == 6: - return ( - header - + "System has negative pressures - negative pressures occurred at one or more junctions with positive demand" - ) + header = "{}".format(code) + if code < 100: + msg = EN_ERROR_CODES.get(code, 'Unknown warning %s') else: - return header + "Unknown warning: %d" % code + raise EpanetException(code) + + return msg % header def runepanet(inpfile, rptfile=None, binfile=None): """Run an EPANET command-line simulation @@ -187,19 +159,22 @@ def isOpen(self): """Checks to see if the file is open""" return self.fileLoaded - def _error(self): + def _error(self, *args): """Print the error text the corresponds to the error code returned""" if not self.errcode: return # errtxt = self.ENlib.ENgeterror(self.errcode) - logger.error("EPANET error: %d", self.errcode) + errtext = EN_ERROR_CODES.get(self.errcode, 'unknown error') + if '%' in errtext and len(args) == 1: + errtext % args if self.errcode >= 100: self.Errflag = True - self.errcodelist.append(self.errcode) - raise EpanetException("EPANET Error {}".format(self.errcode)) + logger.error("EPANET error {} - {}".format(self.errcode, errtext)) + raise EpanetException(self.errcode) else: self.Warnflag = True # warnings.warn(ENgetwarning(self.errcode)) + logger.warning('EPANET warning {} - {}'.format(self.errcode, ENgetwarning(self.errcode, self.cur_time))) self.errcodelist.append(ENgetwarning(self.errcode, self.cur_time)) return @@ -538,21 +513,6 @@ def ENgetflowunits(self): self._error() return iCode.value - def ENgetnodeid(self, iIndex): - """ - desc: Gets the ID name of a node given its index. - - :param a node's index (starting from 1). - :return the node's ID name. - """ - fValue = ctypes.create_string_buffer(SizeLimits.EN_MAX_ID.value) - if self._project is not None: - self.errcode = self.ENlib.EN_getnodeid(self._project, iIndex, byref(fValue)) - else: - self.errcode = self.ENlib.ENgetnodeid(iIndex, byref(fValue)) - self._error() - return str(fValue.value, 'UTF-8') - def ENgetnodeindex(self, sId): """Retrieves index of a node with specific ID @@ -574,22 +534,6 @@ def ENgetnodeindex(self, sId): self._error() return iIndex.value - def ENgetnodetype(self, iIndex): - """ - desc: Retrieves a node's type given its index. - - :param iIndex: idx - :param nodeType: the node's type (see EN_NodeType). - :return int node type - """ - fValue = ctypes.c_int() - if self._project is not None: - self.errcode = self.ENlib.EN_getnodetype(self._project, iIndex, byref(fValue)) - else: - self.errcode = self.ENlib.ENgetnodetype(iIndex, byref(fValue)) - self._error() - return fValue.value - def ENgetnodevalue(self, iIndex, iCode): """ Retrieves parameter value for a node @@ -636,21 +580,6 @@ def ENgetlinkindex(self, sId): self._error() return iIndex.value - def ENgetlinktype(self, iIndex): - """ - Retrieves a link's type. - - :param iIndex: index - :return:linkType - """ - fValue = ctypes.c_int() - if self._project is not None: - self.errcode = self.ENlib.EN_getlinktype(self._project, iIndex, byref(fValue)) - else: - self.errcode = self.ENlib.EN_getlinktype(iIndex, byref(fValue)) - self._error() - return fValue.value - def ENgetlinkvalue(self, iIndex, iCode): """Retrieves parameter value for a link @@ -690,8 +619,8 @@ def ENsetlinkvalue(self, iIndex, iCode, fValue): """ if self._project is not None: self.errcode = self.ENlib.EN_setlinkvalue(self._project, - ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) - ) + ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) + ) else: self.errcode = self.ENlib.ENsetlinkvalue( ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_float(fValue) @@ -713,8 +642,8 @@ def ENsetnodevalue(self, iIndex, iCode, fValue): """ if self._project is not None: self.errcode = self.ENlib.EN_setnodevalue(self._project, - ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) - ) + ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) + ) else: self.errcode = self.ENlib.ENsetnodevalue( ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_float(fValue) @@ -768,6 +697,173 @@ def ENgettimeparam(self, eParam): self._error() return lValue.value + def ENaddcontrol(self, iType: int, iLinkIndex: int, dSetting: float, iNodeIndex: int, dLevel: float) -> int: + """ + Add a new simple control + + Parameters + ---------- + iType : int + _description_ + iLinkIndex : int + _description_ + dSetting : float + _description_ + iNodeIndex : int + Set to 0 for time of day or timer + dLevel : float + _description_ + + Returns + ------- + int + _description_ + """ + lValue = ctypes.c_int() + if self._project is not None: + self.errcode = self.ENlib.EN_addcontrol( + self._project, + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), + ctypes.c_double(dLevel), + byref(lValue) + ) + else: + self.errcode = self.ENlib.ENaddcontrol( + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), + ctypes.c_double(dLevel), + byref(lValue) + ) + self._error() + return lValue.value + + def ENgetcontrol(self, iIndex: int): + """ + Add a new simple control + + Parameters + ---------- + iIndex : int + _description_ + """ + iType = ctypes.c_int() + iLinkIndex = ctypes.c_int() + dSetting = ctypes.c_double() + iNodeIndex = ctypes.c_int() + dLevel = ctypes.c_double() + if self._project is not None: + self.errcode = self.ENlib.EN_getcontrol( + self._project, + ctypes.c_int(iIndex), + byref(iType), + byref(iLinkIndex), + byref(dSetting), + byref(iNodeIndex), + byref(dLevel) + ) + else: + self.errcode = self.ENlib.ENgetcontrol( + ctypes.c_int(iIndex), + byref(iType), + byref(iLinkIndex), + byref(dSetting), + byref(iNodeIndex), + byref(dLevel) + ) + self._error() + return dict(index=iIndex, type=iType.value, linkindex=iLinkIndex.value, setting=dSetting.value, nodeindex=iNodeIndex.value, level=dLevel.value) + + def ENsetcontrol(self, iIndex: int, iType: int, iLinkIndex: int, dSetting: float, iNodeIndex: int, dLevel: float): + """ + Add a new simple control + + Parameters + ---------- + iIndex : int + _description_ + iType : int + _description_ + iLinkIndex : int + _description_ + dSetting : float + _description_ + iNodeIndex : int + Set to 0 for time of day or timer + dLevel : float + _description_ + + Warning + ------- + There is an error in EPANET 2.2 that sets the :param:`dLevel` to 0.0 on Macs + regardless of the value the user passes in. This means that to use this toolkit + functionality on a Mac, the user must delete and create a new control to change + the level. + + """ + if self._project is not None: + try: + self.errcode = self.ENlib.EN_setcontrol( + self._project, + ctypes.c_int(iIndex), + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), + ctypes.c_double(dLevel) + ) + except: + self.errcode = self.ENlib.EN_setcontrol( + self._project, + ctypes.c_int(iIndex), + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), + ctypes.c_float(dLevel) + ) + else: + self.errcode = self.ENlib.ENsetcontrol( + ctypes.c_int(iIndex), + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), + ctypes.c_double(dLevel) + ) + self._error() + + def ENdeletecontrol(self, iControlIndex): + """ + Get a time parameter value + + Parameters + ---------- + iControlIndex : int + the time parameter to get + + Returns + ------- + int + the index of the new control + """ + lValue = ctypes.c_long() + if self._project is not None: + self.errcode = self.ENlib.EN_deletecontrol( + self._project, + ctypes.c_int(iControlIndex) + ) + else: + self.errcode = self.ENlib.ENdeletecontrol( + ctypes.c_int(iControlIndex) + ) + self._error() + return lValue.value + def ENsaveinpfile(self, inpfile): """Saves EPANET input file @@ -785,4 +881,4 @@ def ENsaveinpfile(self, inpfile): self.errcode = self.ENlib.ENsaveinpfile(inpfile) self._error() - return + return \ No newline at end of file From 8fa41a3bb9259e3337df9540097e1d762aee8e60 Mon Sep 17 00:00:00 2001 From: dbhart Date: Sat, 16 Sep 2023 12:46:45 -0600 Subject: [PATCH 02/15] Update to have cleaner exceptions during IO --- wntr/epanet/exceptions.py | 33 ++++---- wntr/epanet/io.py | 169 +++++++++++++++++++------------------- 2 files changed, 101 insertions(+), 101 deletions(-) diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index 5a2bdf353..6672cc95e 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -21,24 +21,24 @@ 110: "cannot solve network hydraulic equations", 120: "cannot solve water quality transport equations", # Apply only to an input file - 200: "one or more errors in input file", - 201: "syntax error", + 200: "one or more errors in input file %s", + 201: "syntax error (%s)", # Apply to both IO file and API functions - 202: "illegal numeric value", - 203: "undefined node %s", - 204: "undefined link %s", - 205: "undefined time pattern %s", - 206: "undefined curve %s", + 202: "illegal numeric value, %s", + 203: "undefined node, %s", + 204: "undefined link, %s", + 205: "undefined time pattern, %s", + 206: "undefined curve, %s", 207: "attempt to control a CV/GPV link", 208: "illegal PDA pressure limits", 209: "illegal node property value", 211: "illegal link property value", 212: "undefined trace node", - 213: "invalid option value", + 213: "invalid option value %s", 214: "too many characters in input line", 215: "duplicate ID label", 216: "reference to undefined pump", - 217: "invalid pump energy data", + 217: "pump has no head curve or power defined", 219: "illegal valve connection to tank node", 220: "illegal valve connection to another valve", 221: "misplaced rule clause in rule-based control", @@ -176,14 +176,13 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> super().__init__(msg) class ENSyntaxError(EpanetException, SyntaxError): - def __init__(self, code, *args) -> None: - super().__init__(code, *args) + def __init__(self, code, *args, line_num=None, line=None) -> None: + super().__init__(code, *args, line_num=line_num, line=line) -class ENNameError(EpanetException, NameError): - def __init__(self, code, name, *args) -> None: - super().__init__(code, name, *args) +class ENKeyError(EpanetException, KeyError): + def __init__(self, code, name, *args, line_num=None, line=None) -> None: + super().__init__(code, name, *args, line_num=line_num, line=line) class ENValueError(EpanetException, ValueError): - def __init__(self, code, value, *args) -> None: - super().__init__(code, value, *args) - + def __init__(self, code, value, *args, line_num=None, line=None) -> None: + super().__init__(code, value, *args, line_num=line_num, line=line) diff --git a/wntr/epanet/io.py b/wntr/epanet/io.py index 27936ba3c..8372faf76 100644 --- a/wntr/epanet/io.py +++ b/wntr/epanet/io.py @@ -25,6 +25,7 @@ import pandas as pd import six import wntr +from wntr.epanet.exceptions import ENKeyError, ENSyntaxError, ENValueError, EpanetException import wntr.network from wntr.network.base import Link from wntr.network.controls import (AndCondition, Comparison, Control, @@ -147,8 +148,7 @@ def _str_time_to_sec(s): if bool(time_tuple): return int(time_tuple.groups()[0])*60*60 else: - raise RuntimeError("Time format in " - "INP file not recognized. ") + raise ENValueError('invalid-option-value', s) def _clock_time_to_sec(s, am_pm): @@ -176,7 +176,7 @@ def _clock_time_to_sec(s, am_pm): elif am_pm.upper() == 'PM': am = False else: - raise RuntimeError('am_pm option not recognized; options are AM or PM') + raise ENValueError('invalid-option-value', s, 'Ambiguous time of day') pattern1 = re.compile(r'^(\d+):(\d+):(\d+)$') time_tuple = pattern1.search(s) @@ -188,7 +188,7 @@ def _clock_time_to_sec(s, am_pm): time_sec -= 3600*12 if not am: if time_sec >= 3600*12: - raise RuntimeError('Cannot specify am/pm for times greater than 12:00:00') + raise ENValueError('invalid-option-value', s, 'Cannot specify am/pm for times greater than 12:00:00') time_sec += 3600*12 return time_sec else: @@ -201,7 +201,7 @@ def _clock_time_to_sec(s, am_pm): time_sec -= 3600*12 if not am: if time_sec >= 3600 * 12: - raise RuntimeError('Cannot specify am/pm for times greater than 12:00:00') + raise ENValueError('invalid-option-value', s, 'Cannot specify am/pm for times greater than 12:00:00') time_sec += 3600*12 return time_sec else: @@ -213,12 +213,11 @@ def _clock_time_to_sec(s, am_pm): time_sec -= 3600*12 if not am: if time_sec >= 3600 * 12: - raise RuntimeError('Cannot specify am/pm for times greater than 12:00:00') + raise ENValueError('invalid-option-value', s, 'Cannot specify am/pm for times greater than 12:00:00') time_sec += 3600*12 return time_sec else: - raise RuntimeError("Time format in " - "INP file not recognized. ") + raise ENValueError('invalid-option-value', s, 'Cannot parse time') def _sec_to_string(sec): @@ -313,99 +312,102 @@ def read(self, inp_files, wn=None): section = None break else: - raise RuntimeError('%(fname)s:%(lnum)d: Invalid section "%(sec)s"' % edata) + raise ENSyntaxError(201, line_num=lnum, line = line) elif section is None and line.startswith(';'): self.top_comments.append(line[1:]) continue elif section is None: logger.debug('Found confusing line: %s', repr(line)) - raise RuntimeError('%(fname)s:%(lnum)d: Non-comment outside of valid section!' % edata) + raise ENSyntaxError(201, line_num=lnum, line=line) # We have text, and we are in a section self.sections[section].append((lnum, line)) # Parse each of the sections # The order of operations is important as certain things require prior knowledge + try: - ### OPTIONS - self._read_options() + ### OPTIONS + self._read_options() - ### TIMES - self._read_times() + ### TIMES + self._read_times() - ### CURVES - self._read_curves() + ### CURVES + self._read_curves() - ### PATTERNS - self._read_patterns() + ### PATTERNS + self._read_patterns() - ### JUNCTIONS - self._read_junctions() + ### JUNCTIONS + self._read_junctions() - ### RESERVOIRS - self._read_reservoirs() + ### RESERVOIRS + self._read_reservoirs() - ### TANKS - self._read_tanks() + ### TANKS + self._read_tanks() - ### PIPES - self._read_pipes() + ### PIPES + self._read_pipes() - ### PUMPS - self._read_pumps() + ### PUMPS + self._read_pumps() - ### VALVES - self._read_valves() + ### VALVES + self._read_valves() - ### COORDINATES - self._read_coordinates() + ### COORDINATES + self._read_coordinates() - ### SOURCES - self._read_sources() + ### SOURCES + self._read_sources() - ### STATUS - self._read_status() + ### STATUS + self._read_status() - ### CONTROLS - self._read_controls() + ### CONTROLS + self._read_controls() - ### RULES - self._read_rules() + ### RULES + self._read_rules() - ### REACTIONS - self._read_reactions() + ### REACTIONS + self._read_reactions() - ### TITLE - self._read_title() + ### TITLE + self._read_title() - ### ENERGY - self._read_energy() + ### ENERGY + self._read_energy() - ### DEMANDS - self._read_demands() + ### DEMANDS + self._read_demands() - ### EMITTERS - self._read_emitters() - - ### QUALITY - self._read_quality() + ### EMITTERS + self._read_emitters() + + ### QUALITY + self._read_quality() - self._read_mixing() - self._read_report() - self._read_vertices() - self._read_labels() + self._read_mixing() + self._read_report() + self._read_vertices() + self._read_labels() - ### Parse Backdrop - self._read_backdrop() + ### Parse Backdrop + self._read_backdrop() - ### TAGS - self._read_tags() + ### TAGS + self._read_tags() + + # Set the _inpfile io data inside the water network, so it is saved somewhere + wn._inpfile = self + + ### Finish tags + self._read_end() + except EpanetException as e: + raise EpanetException(200, filename) from e - # Set the _inpfile io data inside the water network, so it is saved somewhere - wn._inpfile = self - - ### Finish tags - self._read_end() - return self.wn def write(self, filename, wn, units=None, version=2.2, force_coordinates=False): @@ -635,7 +637,7 @@ def _read_tanks(self): overflow = False volume = 0.0 else: - raise RuntimeError('Tank entry format not recognized.') + raise ENSyntaxError(201, 'Tank entry format not recognized.', line_num=lnum, line=line) self.wn.add_tank(current[0], to_si(self.flow_units, float(current[1]), HydParam.Elevation), to_si(self.flow_units, float(current[2]), HydParam.Length), @@ -772,13 +774,12 @@ def create_curve(curve_name): # assert pattern is None, 'In [PUMPS] entry, PATTERN may only be specified once.' pattern = self.wn.get_pattern(current[i+1]).name else: - raise RuntimeError('Pump keyword in inp file not recognized.') - + raise ENSyntaxError(201, 'Pump keyword not recognized: {}'.format(current[i].upper()), line_num=lnum, line=line) if speed is None: speed = 1.0 if pump_type is None: - raise RuntimeError('Either head curve id or pump power must be specified for all pumps.') + raise ENSyntaxError(217, line_num=lnum, line=line) self.wn.add_pump(current[0], current[1], current[2], pump_type, value, speed, pattern) def _write_pumps(self, f, wn): @@ -826,7 +827,7 @@ def _read_valves(self): current.append(0.0) else: if len(current) != 7: - raise RuntimeError('The [VALVES] section of an INP file must have 6 or 7 entries.') + raise ENSyntaxError(201, 'valve definitions must have 6 or 7 values', line_num=lnum, line=line) valve_type = current[4].upper() if valve_type in ['PRV', 'PSV', 'PBV']: valve_set = to_si(self.flow_units, float(current[5]), HydParam.Pressure) @@ -844,7 +845,7 @@ def _read_valves(self): self.wn.add_curve(curve_name, 'HEADLOSS', curve_points) valve_set = curve_name else: - raise RuntimeError('VALVE type "%s" unrecognized' % valve_type) + raise ENSyntaxError(213, 'valve type unrecognized', line_num=lnum, line=line) self.wn.add_valve(current[0], current[1], current[2], @@ -994,7 +995,7 @@ def _read_patterns(self): # If default is '1' but it does not exist, then it is constant # Any other default that does not exist is an error if self.wn.options.hydraulic.pattern is not None and self.wn.options.hydraulic.pattern != '1': - raise KeyError('Default pattern {} is undefined'.format(self.wn.options.hydraulic.pattern)) + raise ENKeyError(205, self.wn.options.hydraulic.pattern) self.wn.options.hydraulic.pattern = None def _write_patterns(self, f, wn): @@ -1402,7 +1403,7 @@ def _read_reactions(self): elif key1 == 'ROUGHNESS': self.wn.options.reaction.roughness_correl = float(current[2]) else: - raise RuntimeError('Reaction option not recognized: %s'%key1) + raise ENValueError(213, key1, line_num=lnum, line=line) def _write_reactions(self, f, wn): f.write( '[REACTIONS]\n'.encode(sys_default_enc)) @@ -1512,7 +1513,7 @@ def _read_mixing(self): tank.mixing_model = MixType.Mix2 tank.mixing_fraction = float(current[2]) elif key == '2COMP' and len(current) < 3: - raise RuntimeError('Mixing model 2COMP requires fraction on tank %s'%tank_name) + raise ENSyntaxError(201, 'missing fraction for mixing', line_num=lnum, line=line) elif key == 'FIFO': tank.mixing_model = MixType.FIFO elif key == 'LIFO': @@ -1555,7 +1556,7 @@ def _read_options(self): if words is not None and len(words) > 0: if len(words) < 2: edata['key'] = words[0] - raise RuntimeError('%(lnum)-6d %(sec)13s no value provided for %(key)s' % edata) + raise ENValueError(213, 'NULL', line_num=lnum, line=line) key = words[0].upper() if key == 'UNITS': self.flow_units = FlowUnits[words[1].upper()] @@ -1583,7 +1584,7 @@ def _read_options(self): self.mass_units = MassUnits.ug opts.quality.inpfile_units = words[2] else: - raise ValueError('Invalid chemical units in OPTIONS section') + raise ENValueError(213, 'for chemical units', line_num=lnum, line=line) else: self.mass_units = MassUnits.mg opts.quality.inpfile_units = 'mg/L' @@ -1617,7 +1618,7 @@ def _read_options(self): opts.hydraulic.pressure_exponent = float(words[2]) else: edata['key'] = ' '.join(words) - raise RuntimeError('%(lnum)-6d %(sec)13s unknown option %(key)s' % edata) + raise ENSyntaxError(201, 'unknown option', line_num=lnum, line=line) else: opts.hydraulic.inpfile_pressure_units = words[1] elif key == 'PATTERN': @@ -1630,16 +1631,16 @@ def _read_options(self): opts.hydraulic.demand_model = words[2] else: edata['key'] = ' '.join(words) - raise RuntimeError('%(lnum)-6d %(sec)13s unknown option %(key)s' % edata) + raise ENSyntaxError(201, 'unknown option', line_num=lnum, line=line) else: edata['key'] = ' '.join(words) - raise RuntimeError('%(lnum)-6d %(sec)13s no value provided for %(key)s' % edata) + raise ENSyntaxError(201, 'unknown option', line_num=lnum, line=line) elif key == 'EMITTER': if len(words) > 2: opts.hydraulic.emitter_exponent = float(words[2]) else: edata['key'] = 'EMITTER EXPONENT' - raise RuntimeError('%(lnum)-6d %(sec)13s no value provided for %(key)s' % edata) + raise ENSyntaxError(201, 'unknown option', line_num=lnum, line=line) elif key == 'TOLERANCE': opts.quality.tolerance = float(words[1]) elif key == 'CHECKFREQ': @@ -1661,9 +1662,9 @@ def _read_options(self): logger.warn('%(lnum)-6d %(sec)13s option "%(key)s" is undocumented; adding, but please verify syntax', edata) if isinstance(opts.time.report_timestep, (float, int)): if opts.time.report_timestep < opts.time.hydraulic_timestep: - raise RuntimeError('opts.report_timestep must be greater than or equal to opts.hydraulic_timestep.') + raise ENValueError(202, 'report timestep less than hydraulic timestep') if opts.time.report_timestep % opts.time.hydraulic_timestep != 0: - raise RuntimeError('opts.report_timestep must be a multiple of opts.hydraulic_timestep') + raise ENValueError(202, 'report timestep must be integer multiple of hydraulic timestep') def _write_options(self, f, wn, version=2.2): f.write('[OPTIONS]\n'.encode(sys_default_enc)) From ae27699a2d4cf579668b2c04bcd4d40f4c190df7 Mon Sep 17 00:00:00 2001 From: dbhart Date: Sat, 16 Sep 2023 13:03:06 -0600 Subject: [PATCH 03/15] Update to exception documentation --- .../apidoc/wntr.epanet.exceptions.rst | 7 ++ documentation/apidoc/wntr.epanet.rst | 1 + wntr/epanet/__init__.py | 2 +- wntr/epanet/exceptions.py | 66 +++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 documentation/apidoc/wntr.epanet.exceptions.rst diff --git a/documentation/apidoc/wntr.epanet.exceptions.rst b/documentation/apidoc/wntr.epanet.exceptions.rst new file mode 100644 index 000000000..6cb9d9029 --- /dev/null +++ b/documentation/apidoc/wntr.epanet.exceptions.rst @@ -0,0 +1,7 @@ +wntr.epanet.exceptions module +============================= + +.. automodule:: wntr.epanet.exceptions + :members: + :no-undoc-members: + :show-inheritance: diff --git a/documentation/apidoc/wntr.epanet.rst b/documentation/apidoc/wntr.epanet.rst index 613df4e41..1af96c98b 100644 --- a/documentation/apidoc/wntr.epanet.rst +++ b/documentation/apidoc/wntr.epanet.rst @@ -12,6 +12,7 @@ Submodules .. toctree:: + wntr.epanet.exceptions wntr.epanet.io wntr.epanet.toolkit wntr.epanet.util diff --git a/wntr/epanet/__init__.py b/wntr/epanet/__init__.py index 272572d1c..de5a4f410 100644 --- a/wntr/epanet/__init__.py +++ b/wntr/epanet/__init__.py @@ -4,4 +4,4 @@ from .io import InpFile #, BinFile, HydFile, RptFile from .util import FlowUnits, MassUnits, HydParam, QualParam, EN import wntr.epanet.toolkit - +import wntr.epanet.exceptions diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index 6672cc95e..c230af2dd 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -1,3 +1,6 @@ +# coding: utf-8 +"""Exceptions for EPANET toolkit and IO operations.""" + from enum import IntEnum from typing import List @@ -79,8 +82,10 @@ 308: "cannot save results to binary file %s", 309: "cannot save results to report file %s", } +"""A list of the error codes and their meanings from the EPANET toolkit.""" class EpanetErrors(IntEnum): + """A list of short phrases that can be used in place of the error code numbers.""" insufficient_memory = 101 no_network = 102 no_init_hyd = 103 @@ -151,6 +156,20 @@ class EpanetErrors(IntEnum): class EpanetException(Exception): def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> None: + """An Exception class for EPANET Toolkit and IO exceptions. + + Parameters + ---------- + code : int or str or EpanetErrors + The EPANET error code (int) or a string mapping to the EpanetErrors enum members + args : additional non-keyword arguments, optional + If there is a string-format within the error code's text, these will be used to + replace the format, otherwise they will be output at the end of the Exception message. + line_num : int, optional + The line number, if reading an INP file, by default None + line : str, optional + The contents of the line, by default None + """ if isinstance(code, EpanetErrors): code = int(code) elif isinstance(code, str): @@ -177,12 +196,59 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> class ENSyntaxError(EpanetException, SyntaxError): def __init__(self, code, *args, line_num=None, line=None) -> None: + """An EPANET exception class that also subclasses SyntaxError + + Parameters + ---------- + code : int or str or EpanetErrors + The EPANET error code (int) or a string mapping to the EpanetErrors enum members + args : additional non-keyword arguments, optional + If there is a string-format within the error code's text, these will be used to + replace the format, otherwise they will be output at the end of the Exception message. + line_num : int, optional + The line number, if reading an INP file, by default None + line : str, optional + The contents of the line, by default None + """ super().__init__(code, *args, line_num=line_num, line=line) class ENKeyError(EpanetException, KeyError): def __init__(self, code, name, *args, line_num=None, line=None) -> None: + """An EPANET exception class that also subclasses KeyError. + + Parameters + ---------- + code : int or str or EpanetErrors + The EPANET error code (int) or a string mapping to the EpanetErrors enum members + name : str + The key/name/id that is missing + args : additional non-keyword arguments, optional + If there is a string-format within the error code's text, these will be used to + replace the format, otherwise they will be output at the end of the Exception message. + line_num : int, optional + The line number, if reading an INP file, by default None + line : str, optional + The contents of the line, by default None + """ + super().__init__(code, name, *args, line_num=line_num, line=line) class ENValueError(EpanetException, ValueError): def __init__(self, code, value, *args, line_num=None, line=None) -> None: + """An EPANET exception class that also subclasses ValueError + + Parameters + ---------- + code : int or str or EpanetErrors + The EPANET error code (int) or a string mapping to the EpanetErrors enum members + value : Any + The value that is invalid + args : additional non-keyword arguments, optional + If there is a string-format within the error code's text, these will be used to + replace the format, otherwise they will be output at the end of the Exception message. + line_num : int, optional + The line number, if reading an INP file, by default None + line : str, optional + The contents of the line, by default None + """ super().__init__(code, value, *args, line_num=line_num, line=line) From 209021509b9a51aa9beced10dbe913f351332f37 Mon Sep 17 00:00:00 2001 From: dbhart Date: Sat, 16 Sep 2023 13:16:44 -0600 Subject: [PATCH 04/15] Update documentation for EN errors --- wntr/epanet/exceptions.py | 89 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index c230af2dd..0cc6dbc01 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -82,75 +82,156 @@ 308: "cannot save results to binary file %s", 309: "cannot save results to report file %s", } -"""A list of the error codes and their meanings from the EPANET toolkit.""" +"""A dictionary of the error codes and their meanings from the EPANET toolkit. +See :class:`EpanetErrors` for the documentation of each code number. -class EpanetErrors(IntEnum): +:meta hide-value: +""" + +class EpanetErrorEnum(IntEnum): """A list of short phrases that can be used in place of the error code numbers.""" + warn_unbalanced = 1 + """system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials""" + warn_unstable = 2 + """system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed""" + warn_disconnected = 3 + """system disconnected - one or more nodes with positive demands were disconnected for all supply sources""" + warn_pumps = 4 + """pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow""" + warn_valves = 5 + """vavles cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open""" + warn_pressures = 6 + """system has negative pressures - negative pressures occurred at one or more junctions with positive demand""" insufficient_memory = 101 + """insufficient memory available""" no_network = 102 + """no network data available""" no_init_hyd = 103 + """hydraulics not initialized""" no_hydraulics = 104 + """no hydraulics for water quality analysis""" no_init_qual = 105 + """water quality not initialized""" no_results = 106 + """no results saved to report on""" hyd_file = 107 + """hydraulics supplied from external file""" hyd_init_and_hyd_file = 108 + """cannot use external file while hydraulics solver is active""" modify_time_during_solve = 109 + """cannot change time parameter when solver is active""" solve_hyd_fail = 110 + """cannot solve network hydraulic equations""" solve_qual_fail = 120 + """cannot solve water quality transport equations""" input_file_error = 200 + """one or more errors in input file""" syntax_error = 201 + """syntax error""" illegal_numeric_value = 202 + """illegal numeric value""" undefined_node = 203 + """undefined node""" undefined_link = 204 + """undefined link""" undefined_pattern = 205 + """undefined time pattern""" undefined_curve = 206 + """undefined curve""" control_on_cv_gpv = 207 + """attempt to control a CV/GPV link""" illegal_pda_limits = 208 + """illegal PDA pressure limits""" illegal_node_property = 209 + """illegal node property value""" illegal_link_property = 211 + """illegal link property value""" undefined_trace_node = 212 + """undefined trace node""" invalid_option_value = 213 + """invalid option value""" too_many_chars_inp = 214 + """too many characters in input line""" duplicate_id = 215 + """duplicate ID label""" undefined_pump = 216 + """reference to undefined pump""" invalid_energy_value = 217 + """pump has no head curve or power defined""" illegal_valve_tank = 219 + """illegal valve connection to tank node""" illegal_tank_valve = 219 + """illegal valve connection to tank node""" illegal_valve_valve = 220 + """illegal valve connection to another valve""" misplaced_rule = 221 + """misplaced rule clause in rule-based control""" link_to_self = 222 + """link assigned same start and end nodes""" not_enough_nodes = 223 + """not enough nodes in network""" no_tanks_or_res = 224 + """no tanks or reservoirs in network""" invalid_tank_levels = 225 + """invalid lower/upper levels for tank""" missing_pump_data = 226 + """no head curve or power rating for pump""" invalid_head_curve = 227 + """invalid head curve for pump""" nonincreasing_x_curve = 230 + """nonincreasing x-values for curve""" unconnected_node = 233 + """network has unconnected node""" unconnected_node_id = 234 + """network has an unconnected node with ID""" no_such_source_node = 240 + """nonexistent water quality source""" no_such_control = 241 + """nonexistent control""" invalid_name_format = 250 + """invalid format (e.g. too long an ID name)""" invalid_parameter_code = 251 + """invalid parameter code""" invalid_id_name = 252 + """invalid ID name""" no_such_demand_category = 253 + """nonexistent demand category""" missing_coords = 254 + """node with no coordinates""" invalid_vertex = 255 + """invalid link vertex""" no_such_rule = 257 + """nonexistent rule""" no_such_rule_clause = 258 + """nonexistent rule clause""" delete_node_still_linked = 259 + """attempt to delete a node that still has links connected to it""" delete_node_is_trace = 260 + """attempt to delete node assigned as a Trace Node""" delete_node_in_control = 261 + """attempt to delete a node or link contained in a control""" modify_network_during_solve = 262 + """attempt to modify network structure while a solver is open""" node_not_a_tank = 263 + """node is not a tank""" same_file_names = 301 + """identical file names used for different types of files""" open_inp_fail = 302 + """cannot open input file""" open_rpt_fail = 303 + """cannot open report file""" open_bin_fail = 304 + """cannot open binary output file""" open_hyd_fail = 305 + """cannot open hydraulics file""" hyd_file_different_network = 306 + """hydraulics file does not match network data""" read_hyd_fail = 307 + """cannot read hydraulics file""" save_bin_fail = 308 + """cannot save results to binary file""" save_rpt_fail = 309 + """cannot save results to report file""" class EpanetException(Exception): @@ -170,12 +251,12 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> line : str, optional The contents of the line, by default None """ - if isinstance(code, EpanetErrors): + if isinstance(code, EpanetErrorEnum): code = int(code) elif isinstance(code, str): try: code = code.strip().replace('-','_').replace(' ','_') - code = int(EpanetErrors[code]) + code = int(EpanetErrorEnum[code]) except KeyError: return super().__init__('unknown error code: {}'.format(repr(code)), *args) elif not isinstance(code, int): From f2235867f658816118407fc333465d344629d258 Mon Sep 17 00:00:00 2001 From: dbhart Date: Mon, 23 Oct 2023 11:34:06 -0600 Subject: [PATCH 05/15] Update to correct the exceptions listings --- wntr/epanet/exceptions.py | 276 +++++++++++++++++--------------------- wntr/epanet/io.py | 12 +- 2 files changed, 127 insertions(+), 161 deletions(-) diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index 0cc6dbc01..fd4b5aa18 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -83,156 +83,130 @@ 309: "cannot save results to report file %s", } """A dictionary of the error codes and their meanings from the EPANET toolkit. -See :class:`EpanetErrors` for the documentation of each code number. + +.. rubric:: Runtime warnings +==== ============================================================================================================================================================================== +Err# Description +---- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ +1 At `time`, system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials +2 At `time`, system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed +3 At `time`, system disconnected - one or more nodes with positive demands were disconnected for all supply sources +4 At `time`, pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow +5 At `time`, valves cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open +6 At `time`, system has negative pressures - negative pressures occurred at one or more junctions with positive demand +==== ============================================================================================================================================================================== + +.. rubric:: Runtime errors + +==== ================================================================= +Err# Description +---- ----------------------------------------------------------------- +101 insufficient memory available +102 no network data available +103 hydraulics not initialized +104 no hydraulics for water quality analysis +105 water quality not initialized +106 no results saved to report on +107 hydraulics supplied from external file +108 cannot use external file while hydraulics solver is active +109 cannot change time parameter when solver is active +110 cannot solve network hydraulic equations +120 cannot solve water quality transport equations +==== ================================================================= + +.. rubric:: Input file errors, exclusively + +==== ================================================================= +Err# Description +---- ----------------------------------------------------------------- +200 one or more errors in input file +201 syntax error +==== ================================================================= + +.. rubric:: Input file and/or toolkit errors + +==== ================================================================= +Err# Description +---- ----------------------------------------------------------------- +202 illegal numeric value +203 undefined node +204 undefined link +205 undefined time pattern +206 undefined curve +207 attempt to control a CV/GPV link +208 illegal PDA pressure limits +209 illegal node property value +211 illegal link property value +212 undefined trace node +213 invalid option value +214 too many characters in input line +215 duplicate ID label +216 reference to undefined pump +217 pump has no head curve or power defined +218 `note: error number 218 is undefined in the EPANET 2.2 toolkit` +219 illegal valve connection to tank node +220 illegal valve connection to another valve +221 misplaced rule clause in rule-based control +222 link assigned same start and end nodes +==== ================================================================= + +.. rubric:: Network consistency errors (INP-file and/or toolkit) + +==== ================================================================= +Err# Description +---- ----------------------------------------------------------------- +223 not enough nodes in network +224 no tanks or reservoirs in network +225 invalid lower/upper levels for tank +226 no head curve or power rating for pump +227 invalid head curve for pump +230 nonincreasing x-values for curve +233 network has unconnected node +234 network has an unconnected node with ID `id` +==== ================================================================= + +.. rubric:: Toolkit-only errors + +==== ================================================================= +Err# Description +---- ----------------------------------------------------------------- +240 nonexistent water quality source +241 nonexistent control +250 invalid format (e.g. too long an ID name) +251 invalid parameter code +252 invalid ID name +253 nonexistent demand category +254 node with no coordinates +255 invalid link vertex +257 nonexistent rule +258 nonexistent rule clause +259 attempt to delete a node that still has links connected to it +260 attempt to delete node assigned as a Trace Node +261 attempt to delete a node or link contained in a control +262 attempt to modify network structure while a solver is open +263 node is not a tank +==== ================================================================= + +.. rubric:: File I/O errors + +==== ========================================================= +Err# Description +---- --------------------------------------------------------- +301 identical file names used for different types of files +302 cannot open input file +303 cannot open report file +304 cannot open binary output file +305 cannot open hydraulics file +306 hydraulics file does not match network data +307 cannot read hydraulics file +308 cannot save results to binary file +309 cannot save results to report file +==== ========================================================= + :meta hide-value: """ -class EpanetErrorEnum(IntEnum): - """A list of short phrases that can be used in place of the error code numbers.""" - warn_unbalanced = 1 - """system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials""" - warn_unstable = 2 - """system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed""" - warn_disconnected = 3 - """system disconnected - one or more nodes with positive demands were disconnected for all supply sources""" - warn_pumps = 4 - """pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow""" - warn_valves = 5 - """vavles cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open""" - warn_pressures = 6 - """system has negative pressures - negative pressures occurred at one or more junctions with positive demand""" - insufficient_memory = 101 - """insufficient memory available""" - no_network = 102 - """no network data available""" - no_init_hyd = 103 - """hydraulics not initialized""" - no_hydraulics = 104 - """no hydraulics for water quality analysis""" - no_init_qual = 105 - """water quality not initialized""" - no_results = 106 - """no results saved to report on""" - hyd_file = 107 - """hydraulics supplied from external file""" - hyd_init_and_hyd_file = 108 - """cannot use external file while hydraulics solver is active""" - modify_time_during_solve = 109 - """cannot change time parameter when solver is active""" - solve_hyd_fail = 110 - """cannot solve network hydraulic equations""" - solve_qual_fail = 120 - """cannot solve water quality transport equations""" - input_file_error = 200 - """one or more errors in input file""" - syntax_error = 201 - """syntax error""" - illegal_numeric_value = 202 - """illegal numeric value""" - undefined_node = 203 - """undefined node""" - undefined_link = 204 - """undefined link""" - undefined_pattern = 205 - """undefined time pattern""" - undefined_curve = 206 - """undefined curve""" - control_on_cv_gpv = 207 - """attempt to control a CV/GPV link""" - illegal_pda_limits = 208 - """illegal PDA pressure limits""" - illegal_node_property = 209 - """illegal node property value""" - illegal_link_property = 211 - """illegal link property value""" - undefined_trace_node = 212 - """undefined trace node""" - invalid_option_value = 213 - """invalid option value""" - too_many_chars_inp = 214 - """too many characters in input line""" - duplicate_id = 215 - """duplicate ID label""" - undefined_pump = 216 - """reference to undefined pump""" - invalid_energy_value = 217 - """pump has no head curve or power defined""" - illegal_valve_tank = 219 - """illegal valve connection to tank node""" - illegal_tank_valve = 219 - """illegal valve connection to tank node""" - illegal_valve_valve = 220 - """illegal valve connection to another valve""" - misplaced_rule = 221 - """misplaced rule clause in rule-based control""" - link_to_self = 222 - """link assigned same start and end nodes""" - not_enough_nodes = 223 - """not enough nodes in network""" - no_tanks_or_res = 224 - """no tanks or reservoirs in network""" - invalid_tank_levels = 225 - """invalid lower/upper levels for tank""" - missing_pump_data = 226 - """no head curve or power rating for pump""" - invalid_head_curve = 227 - """invalid head curve for pump""" - nonincreasing_x_curve = 230 - """nonincreasing x-values for curve""" - unconnected_node = 233 - """network has unconnected node""" - unconnected_node_id = 234 - """network has an unconnected node with ID""" - no_such_source_node = 240 - """nonexistent water quality source""" - no_such_control = 241 - """nonexistent control""" - invalid_name_format = 250 - """invalid format (e.g. too long an ID name)""" - invalid_parameter_code = 251 - """invalid parameter code""" - invalid_id_name = 252 - """invalid ID name""" - no_such_demand_category = 253 - """nonexistent demand category""" - missing_coords = 254 - """node with no coordinates""" - invalid_vertex = 255 - """invalid link vertex""" - no_such_rule = 257 - """nonexistent rule""" - no_such_rule_clause = 258 - """nonexistent rule clause""" - delete_node_still_linked = 259 - """attempt to delete a node that still has links connected to it""" - delete_node_is_trace = 260 - """attempt to delete node assigned as a Trace Node""" - delete_node_in_control = 261 - """attempt to delete a node or link contained in a control""" - modify_network_during_solve = 262 - """attempt to modify network structure while a solver is open""" - node_not_a_tank = 263 - """node is not a tank""" - same_file_names = 301 - """identical file names used for different types of files""" - open_inp_fail = 302 - """cannot open input file""" - open_rpt_fail = 303 - """cannot open report file""" - open_bin_fail = 304 - """cannot open binary output file""" - open_hyd_fail = 305 - """cannot open hydraulics file""" - hyd_file_different_network = 306 - """hydraulics file does not match network data""" - read_hyd_fail = 307 - """cannot read hydraulics file""" - save_bin_fail = 308 - """cannot save results to binary file""" - save_rpt_fail = 309 - """cannot save results to report file""" - class EpanetException(Exception): @@ -251,15 +225,7 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> line : str, optional The contents of the line, by default None """ - if isinstance(code, EpanetErrorEnum): - code = int(code) - elif isinstance(code, str): - try: - code = code.strip().replace('-','_').replace(' ','_') - code = int(EpanetErrorEnum[code]) - except KeyError: - return super().__init__('unknown error code: {}'.format(repr(code)), *args) - elif not isinstance(code, int): + if not isinstance(code, int): return super().__init__('unknown error code: {}'.format(repr(code)), *args) msg = EN_ERROR_CODES.get(code, 'unknown error') if args is not None: diff --git a/wntr/epanet/io.py b/wntr/epanet/io.py index 8372faf76..1769191de 100644 --- a/wntr/epanet/io.py +++ b/wntr/epanet/io.py @@ -148,7 +148,7 @@ def _str_time_to_sec(s): if bool(time_tuple): return int(time_tuple.groups()[0])*60*60 else: - raise ENValueError('invalid-option-value', s) + raise ENValueError(213, s) def _clock_time_to_sec(s, am_pm): @@ -176,7 +176,7 @@ def _clock_time_to_sec(s, am_pm): elif am_pm.upper() == 'PM': am = False else: - raise ENValueError('invalid-option-value', s, 'Ambiguous time of day') + raise ENValueError(213, s, 'Ambiguous time of day') pattern1 = re.compile(r'^(\d+):(\d+):(\d+)$') time_tuple = pattern1.search(s) @@ -188,7 +188,7 @@ def _clock_time_to_sec(s, am_pm): time_sec -= 3600*12 if not am: if time_sec >= 3600*12: - raise ENValueError('invalid-option-value', s, 'Cannot specify am/pm for times greater than 12:00:00') + raise ENValueError(213, s, 'Cannot specify am/pm for times greater than 12:00:00') time_sec += 3600*12 return time_sec else: @@ -201,7 +201,7 @@ def _clock_time_to_sec(s, am_pm): time_sec -= 3600*12 if not am: if time_sec >= 3600 * 12: - raise ENValueError('invalid-option-value', s, 'Cannot specify am/pm for times greater than 12:00:00') + raise ENValueError(213, s, 'Cannot specify am/pm for times greater than 12:00:00') time_sec += 3600*12 return time_sec else: @@ -213,11 +213,11 @@ def _clock_time_to_sec(s, am_pm): time_sec -= 3600*12 if not am: if time_sec >= 3600 * 12: - raise ENValueError('invalid-option-value', s, 'Cannot specify am/pm for times greater than 12:00:00') + raise ENValueError(213, s, 'Cannot specify am/pm for times greater than 12:00:00') time_sec += 3600*12 return time_sec else: - raise ENValueError('invalid-option-value', s, 'Cannot parse time') + raise ENValueError(213, s, 'Cannot parse time') def _sec_to_string(sec): From 4171489656c5fba07e76edcbea963e8ef0343605 Mon Sep 17 00:00:00 2001 From: dbhart Date: Mon, 23 Oct 2023 11:42:28 -0600 Subject: [PATCH 06/15] Fix the imports in wntr-epanet-init --- wntr/epanet/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wntr/epanet/__init__.py b/wntr/epanet/__init__.py index de5a4f410..d7f099faa 100644 --- a/wntr/epanet/__init__.py +++ b/wntr/epanet/__init__.py @@ -3,5 +3,5 @@ """ from .io import InpFile #, BinFile, HydFile, RptFile from .util import FlowUnits, MassUnits, HydParam, QualParam, EN -import wntr.epanet.toolkit -import wntr.epanet.exceptions +import wntr.epanet.toolkit as toolkit +import wntr.epanet.exceptions as exceptions From 2fb49db660b8eca2162b5982912d6d894d0d4f5a Mon Sep 17 00:00:00 2001 From: dbhart Date: Mon, 23 Oct 2023 12:13:19 -0600 Subject: [PATCH 07/15] Documentation on EPANET exceptions --- wntr/epanet/exceptions.py | 233 +++++++++++++++++++------------------- 1 file changed, 116 insertions(+), 117 deletions(-) diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index fd4b5aa18..4240d3701 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -10,7 +10,7 @@ 2: "At %s, system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed", 3: "At %s, system disconnected - one or more nodes with positive demands were disconnected for all supply sources", 4: "At %s, pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow", - 5: "At %s, vavles cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open", + 5: "At %s, valves cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open", 6: "At %s, system has negative pressures - negative pressures occurred at one or more junctions with positive demand", 101: "insufficient memory available", 102: "no network data available", @@ -84,124 +84,125 @@ } """A dictionary of the error codes and their meanings from the EPANET toolkit. -.. rubric:: Runtime warnings -==== ============================================================================================================================================================================== -Err# Description ----- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ -1 At `time`, system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials -2 At `time`, system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed -3 At `time`, system disconnected - one or more nodes with positive demands were disconnected for all supply sources -4 At `time`, pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow -5 At `time`, valves cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open -6 At `time`, system has negative pressures - negative pressures occurred at one or more junctions with positive demand -==== ============================================================================================================================================================================== +.. table:: EPANET warnings -.. rubric:: Runtime errors + =========== ============================================================================================================================================================================== + **Err No.** **Description** + ----------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + 1 At `time`, system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials + 2 At `time`, system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed + 3 At `time`, system disconnected - one or more nodes with positive demands were disconnected for all supply sources + 4 At `time`, pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow + 5 At `time`, valves cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open + 6 At `time`, system has negative pressures - negative pressures occurred at one or more junctions with positive demand + =========== ============================================================================================================================================================================== -==== ================================================================= -Err# Description ----- ----------------------------------------------------------------- -101 insufficient memory available -102 no network data available -103 hydraulics not initialized -104 no hydraulics for water quality analysis -105 water quality not initialized -106 no results saved to report on -107 hydraulics supplied from external file -108 cannot use external file while hydraulics solver is active -109 cannot change time parameter when solver is active -110 cannot solve network hydraulic equations -120 cannot solve water quality transport equations -==== ================================================================= +.. table:: EPANET runtime errors -.. rubric:: Input file errors, exclusively + =========== ================================================================= + *Err No.* *Description* + ----------- ----------------------------------------------------------------- + **101-120** **Runtime and simulation errors** + ----------- ----------------------------------------------------------------- + 101 insufficient memory available + 102 no network data available + 103 hydraulics not initialized + 104 no hydraulics for water quality analysis + 105 water quality not initialized + 106 no results saved to report on + 107 hydraulics supplied from external file + 108 cannot use external file while hydraulics solver is active + 109 cannot change time parameter when solver is active + 110 cannot solve network hydraulic equations + 120 cannot solve water quality transport equations + =========== ================================================================= -==== ================================================================= -Err# Description ----- ----------------------------------------------------------------- -200 one or more errors in input file -201 syntax error -==== ================================================================= +.. table:: EPANET network errors -.. rubric:: Input file and/or toolkit errors + =========== ================================================================= + *Err No.* *Description* + ----------- ----------------------------------------------------------------- + **200-201** **Input file errors (exclusively for input files)** + ----------- ----------------------------------------------------------------- + 200 one or more errors in input file + 201 syntax error + ----------- ----------------------------------------------------------------- + **202-222** **Input file and toolkit errors** + ----------- ----------------------------------------------------------------- + 202 illegal numeric value + 203 undefined node + 204 undefined link + 205 undefined time pattern + 206 undefined curve + 207 attempt to control a CV/GPV link + 208 illegal PDA pressure limits + 209 illegal node property value + 211 illegal link property value + 212 undefined trace node + 213 invalid option value + 214 too many characters in input line + 215 duplicate ID label + 216 reference to undefined pump + 217 pump has no head curve or power defined + 218 `note: error number 218 is undefined in the EPANET 2.2 toolkit` + 219 illegal valve connection to tank node + 220 illegal valve connection to another valve + 221 misplaced rule clause in rule-based control + 222 link assigned same start and end nodes + ----------- ----------------------------------------------------------------- + **223-234** **Network consistency errors (INP-file and/or toolkit)** + ----------- ----------------------------------------------------------------- + 223 not enough nodes in network + 224 no tanks or reservoirs in network + 225 invalid lower/upper levels for tank + 226 no head curve or power rating for pump + 227 invalid head curve for pump + 230 nonincreasing x-values for curve + 233 network has unconnected node + 234 network has an unconnected node with ID `id` + ----------- ----------------------------------------------------------------- + **240-263** **Toolkit-only errors** + ----------- ----------------------------------------------------------------- + 240 nonexistent water quality source + 241 nonexistent control + 250 invalid format (e.g. too long an ID name) + 251 invalid parameter code + 252 invalid ID name + 253 nonexistent demand category + 254 node with no coordinates + 255 invalid link vertex + 257 nonexistent rule + 258 nonexistent rule clause + 259 attempt to delete a node that still has links connected to it + 260 attempt to delete node assigned as a Trace Node + 261 attempt to delete a node or link contained in a control + 262 attempt to modify network structure while a solver is open + 263 node is not a tank + =========== ================================================================= -==== ================================================================= -Err# Description ----- ----------------------------------------------------------------- -202 illegal numeric value -203 undefined node -204 undefined link -205 undefined time pattern -206 undefined curve -207 attempt to control a CV/GPV link -208 illegal PDA pressure limits -209 illegal node property value -211 illegal link property value -212 undefined trace node -213 invalid option value -214 too many characters in input line -215 duplicate ID label -216 reference to undefined pump -217 pump has no head curve or power defined -218 `note: error number 218 is undefined in the EPANET 2.2 toolkit` -219 illegal valve connection to tank node -220 illegal valve connection to another valve -221 misplaced rule clause in rule-based control -222 link assigned same start and end nodes -==== ================================================================= +.. table:: EPANET file/system errors -.. rubric:: Network consistency errors (INP-file and/or toolkit) - -==== ================================================================= -Err# Description ----- ----------------------------------------------------------------- -223 not enough nodes in network -224 no tanks or reservoirs in network -225 invalid lower/upper levels for tank -226 no head curve or power rating for pump -227 invalid head curve for pump -230 nonincreasing x-values for curve -233 network has unconnected node -234 network has an unconnected node with ID `id` -==== ================================================================= - -.. rubric:: Toolkit-only errors - -==== ================================================================= -Err# Description ----- ----------------------------------------------------------------- -240 nonexistent water quality source -241 nonexistent control -250 invalid format (e.g. too long an ID name) -251 invalid parameter code -252 invalid ID name -253 nonexistent demand category -254 node with no coordinates -255 invalid link vertex -257 nonexistent rule -258 nonexistent rule clause -259 attempt to delete a node that still has links connected to it -260 attempt to delete node assigned as a Trace Node -261 attempt to delete a node or link contained in a control -262 attempt to modify network structure while a solver is open -263 node is not a tank -==== ================================================================= - -.. rubric:: File I/O errors - -==== ========================================================= -Err# Description ----- --------------------------------------------------------- -301 identical file names used for different types of files -302 cannot open input file -303 cannot open report file -304 cannot open binary output file -305 cannot open hydraulics file -306 hydraulics file does not match network data -307 cannot read hydraulics file -308 cannot save results to binary file -309 cannot save results to report file -==== ========================================================= + =========== ================================================================= + *Err No.* *Description* + ----------- ----------------------------------------------------------------- + **301-305** **Filename errors** + ----------- ----------------------------------------------------------------- + 301 identical file names used for different types of files + 302 cannot open input file + 303 cannot open report file + 304 cannot open binary output file + 305 cannot open hydraulics file + ----------- ----------------------------------------------------------------- + **306-307** **File structure errors** + ----------- ----------------------------------------------------------------- + 306 hydraulics file does not match network data + 307 cannot read hydraulics file + ----------- ----------------------------------------------------------------- + **308-309** **Filesystem errors** + ----------- ----------------------------------------------------------------- + 308 cannot save results to binary file + 309 cannot save results to report file + =========== ================================================================= :meta hide-value: @@ -215,7 +216,7 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> Parameters ---------- - code : int or str or EpanetErrors + code : int The EPANET error code (int) or a string mapping to the EpanetErrors enum members args : additional non-keyword arguments, optional If there is a string-format within the error code's text, these will be used to @@ -225,8 +226,6 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> line : str, optional The contents of the line, by default None """ - if not isinstance(code, int): - return super().__init__('unknown error code: {}'.format(repr(code)), *args) msg = EN_ERROR_CODES.get(code, 'unknown error') if args is not None: args = [*args] @@ -247,7 +246,7 @@ def __init__(self, code, *args, line_num=None, line=None) -> None: Parameters ---------- - code : int or str or EpanetErrors + code : int The EPANET error code (int) or a string mapping to the EpanetErrors enum members args : additional non-keyword arguments, optional If there is a string-format within the error code's text, these will be used to @@ -265,7 +264,7 @@ def __init__(self, code, name, *args, line_num=None, line=None) -> None: Parameters ---------- - code : int or str or EpanetErrors + code : int The EPANET error code (int) or a string mapping to the EpanetErrors enum members name : str The key/name/id that is missing From cc4587e5e5813d736fde9196a1ad45bc94582363 Mon Sep 17 00:00:00 2001 From: dbhart Date: Mon, 23 Oct 2023 12:18:30 -0600 Subject: [PATCH 08/15] Spelling errors --- wntr/epanet/exceptions.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index 4240d3701..ede847c16 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -1,7 +1,6 @@ # coding: utf-8 """Exceptions for EPANET toolkit and IO operations.""" -from enum import IntEnum from typing import List EN_ERROR_CODES = { @@ -87,7 +86,9 @@ .. table:: EPANET warnings =========== ============================================================================================================================================================================== - **Err No.** **Description** + *Err No.* *Description* + ----------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + **1-6** **Simulation warnings** ----------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 1 At `time`, system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials 2 At `time`, system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed @@ -217,7 +218,7 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> Parameters ---------- code : int - The EPANET error code (int) or a string mapping to the EpanetErrors enum members + The EPANET error code args : additional non-keyword arguments, optional If there is a string-format within the error code's text, these will be used to replace the format, otherwise they will be output at the end of the Exception message. @@ -247,7 +248,7 @@ def __init__(self, code, *args, line_num=None, line=None) -> None: Parameters ---------- code : int - The EPANET error code (int) or a string mapping to the EpanetErrors enum members + The EPANET error code args : additional non-keyword arguments, optional If there is a string-format within the error code's text, these will be used to replace the format, otherwise they will be output at the end of the Exception message. @@ -265,7 +266,7 @@ def __init__(self, code, name, *args, line_num=None, line=None) -> None: Parameters ---------- code : int - The EPANET error code (int) or a string mapping to the EpanetErrors enum members + The EPANET error code name : str The key/name/id that is missing args : additional non-keyword arguments, optional @@ -285,8 +286,8 @@ def __init__(self, code, value, *args, line_num=None, line=None) -> None: Parameters ---------- - code : int or str or EpanetErrors - The EPANET error code (int) or a string mapping to the EpanetErrors enum members + code : int + The EPANET error code value : Any The value that is invalid args : additional non-keyword arguments, optional From a3594a3c539833b0bba5ccb942da1fb3d993ae8e Mon Sep 17 00:00:00 2001 From: dbhart Date: Mon, 30 Oct 2023 10:12:27 -0600 Subject: [PATCH 09/15] Updates to address comments in PR. Tests forthcoming --- documentation/errors.rst | 166 +++++++++++++++++++ documentation/index.rst | 1 + wntr/epanet/exceptions.py | 124 +------------- wntr/epanet/toolkit.py | 338 ++++++++++++++++++++------------------ 4 files changed, 343 insertions(+), 286 deletions(-) create mode 100644 documentation/errors.rst diff --git a/documentation/errors.rst b/documentation/errors.rst new file mode 100644 index 000000000..5206caaba --- /dev/null +++ b/documentation/errors.rst @@ -0,0 +1,166 @@ +.. raw:: latex + + \clearpage + + +Errors and debugging +==================== + +WNTR extends several of the standard python exceptions, :class:`KeyError`, :class:`SyntaxError`, +and :class:`ValueError` with EPANET toolkit specific versions, +:class:`~wntr.epanet.exceptions.ENKeyError`, +:class:`~wntr.epanet.exceptions.ENSyntaxError`, +and :class:`~wntr.epanet.exceptions.ENValueError`, +and a base :class:`~wntr.epanet.exceptions.EpanetException`. +These exceptions are raised when errors occur during INP-file reading/writing, +when using the EPANET toolkit functions, and when running the +:class:`~wntr.sim.epanet.EpanetSimulator`. + +In addition to the normal information that a similar python exception would provide, +these exceptions return the EPANET error code number and the error description +from the EPANET source code. WNTR also tries to intuit the specific variable, +line number (of an input file), and timestamp to give the user the most information +possible. Tables :numref:`table-epanet-warnings` through :numref:`table-epanet-errors-filesystem` +provide the description of the various warnings and error codes defined in [Ross00]_. + + +.. _table-epanet-warnings: +.. table:: EPANET warnings + + =========== ============================================================================================================================================================================== + *Err No.* *Description* + ----------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + **1-6** **Simulation warnings** + ----------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + 1 At `{time}`, system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials + 2 At `{time}`, system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed + 3 At `{time}`, system disconnected - one or more nodes with positive demands were disconnected for all supply sources + 4 At `{time}`, pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow + 5 At `{time}`, valves cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open + 6 At `{time}`, system has negative pressures - negative pressures occurred at one or more junctions with positive demand + =========== ============================================================================================================================================================================== + +.. _table-epanet-errors-runtime: +.. table:: EPANET runtime errors + + =========== ================================================================= + *Err No.* *Description* + ----------- ----------------------------------------------------------------- + **101-120** **Runtime and simulation errors** + ----------- ----------------------------------------------------------------- + 101 insufficient memory available + 102 no network data available + 103 hydraulics not initialized + 104 no hydraulics for water quality analysis + 105 water quality not initialized + 106 no results saved to report on + 107 hydraulics supplied from external file + 108 cannot use external file while hydraulics solver is active + 109 cannot change time parameter when solver is active + 110 cannot solve network hydraulic equations + 120 cannot solve water quality transport equations + =========== ================================================================= + + +.. _table-epanet-errors-network: +.. table:: EPANET network errors + + =========== ================================================================= + *Err No.* *Description* + ----------- ----------------------------------------------------------------- + **200-201** **Input file errors (exclusively for input files)** + ----------- ----------------------------------------------------------------- + 200 one or more errors in input file + 201 syntax error + ----------- ----------------------------------------------------------------- + **202-222** **Input file and toolkit errors** + ----------- ----------------------------------------------------------------- + 202 illegal numeric value + 203 undefined node + 204 undefined link + 205 undefined time pattern + 206 undefined curve + 207 attempt to control a CV/GPV link + 208 illegal PDA pressure limits + 209 illegal node property value + 211 illegal link property value + 212 undefined trace node + 213 invalid option value + 214 too many characters in input line + 215 duplicate ID label + 216 reference to undefined pump + 217 pump has no head curve or power defined + 218 `note: error number 218 is undefined in EPANET 2.2` + 219 illegal valve connection to tank node + 220 illegal valve connection to another valve + 221 misplaced rule clause in rule-based control + 222 link assigned same start and end nodes + ----------- ----------------------------------------------------------------- + **223-234** **Network consistency errors (INP-file and/or toolkit)** + ----------- ----------------------------------------------------------------- + 223 not enough nodes in network + 224 no tanks or reservoirs in network + 225 invalid lower/upper levels for tank + 226 no head curve or power rating for pump + 227 invalid head curve for pump + 230 nonincreasing x-values for curve + 233 network has unconnected node + 234 network has an unconnected node with ID `id` + ----------- ----------------------------------------------------------------- + **240-263** **Toolkit-only errors** + ----------- ----------------------------------------------------------------- + 240 nonexistent water quality source + 241 nonexistent control + 250 invalid format (e.g. too long an ID name) + 251 invalid parameter code + 252 invalid ID name + 253 nonexistent demand category + 254 node with no coordinates + 255 invalid link vertex + 257 nonexistent rule + 258 nonexistent rule clause + 259 attempt to delete a node that still has links connected to it + 260 attempt to delete node assigned as a Trace Node + 261 attempt to delete a node or link contained in a control + 262 attempt to modify network structure while a solver is open + 263 node is not a tank + =========== ================================================================= + + +.. _table-epanet-errors-filesystem: +.. table:: EPANET file/system errors + + =========== ================================================================= + *Err No.* *Description* + ----------- ----------------------------------------------------------------- + **301-305** **Filename errors** + ----------- ----------------------------------------------------------------- + 301 identical file names used for different types of files + 302 cannot open input file + 303 cannot open report file + 304 cannot open binary output file + 305 cannot open hydraulics file + ----------- ----------------------------------------------------------------- + **306-307** **File structure errors** + ----------- ----------------------------------------------------------------- + 306 hydraulics file does not match network data + 307 cannot read hydraulics file + ----------- ----------------------------------------------------------------- + **308-309** **Filesystem errors** + ----------- ----------------------------------------------------------------- + 308 cannot save results to binary file + 309 cannot save results to report file + =========== ================================================================= + + +For developers +-------------- + +The custom exceptions for EPANET that are included in the :class:`wntr.epanet.exceptions` +module subclass both the :class:`~wntr.epanet.exceptions.EpanetException` +and the standard python exception they are named after. This means that when handling +exceptions, a try-catch block that is looking for a :class:`KeyError`, for example, +will still catch an :class:`~wntr.epanet.exceptions.ENKeyError`. The newest versions +of Python, e.g., 3.11, have a new style of multiple inheritence for Exceptions, called +exception groups, but this has not yet been used in WNTR because older versions of +Python are still supported at this time. diff --git a/documentation/index.rst b/documentation/index.rst index 5ef2fdd51..610f9d509 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -30,6 +30,7 @@ designed to simulate and analyze resilience of water distribution networks. graphics gis advancedsim + errors license whatsnew developers diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index ede847c16..82ce50f61 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -82,129 +82,7 @@ 309: "cannot save results to report file %s", } """A dictionary of the error codes and their meanings from the EPANET toolkit. - -.. table:: EPANET warnings - - =========== ============================================================================================================================================================================== - *Err No.* *Description* - ----------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - **1-6** **Simulation warnings** - ----------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - 1 At `time`, system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials - 2 At `time`, system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed - 3 At `time`, system disconnected - one or more nodes with positive demands were disconnected for all supply sources - 4 At `time`, pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow - 5 At `time`, valves cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open - 6 At `time`, system has negative pressures - negative pressures occurred at one or more junctions with positive demand - =========== ============================================================================================================================================================================== - -.. table:: EPANET runtime errors - - =========== ================================================================= - *Err No.* *Description* - ----------- ----------------------------------------------------------------- - **101-120** **Runtime and simulation errors** - ----------- ----------------------------------------------------------------- - 101 insufficient memory available - 102 no network data available - 103 hydraulics not initialized - 104 no hydraulics for water quality analysis - 105 water quality not initialized - 106 no results saved to report on - 107 hydraulics supplied from external file - 108 cannot use external file while hydraulics solver is active - 109 cannot change time parameter when solver is active - 110 cannot solve network hydraulic equations - 120 cannot solve water quality transport equations - =========== ================================================================= - -.. table:: EPANET network errors - - =========== ================================================================= - *Err No.* *Description* - ----------- ----------------------------------------------------------------- - **200-201** **Input file errors (exclusively for input files)** - ----------- ----------------------------------------------------------------- - 200 one or more errors in input file - 201 syntax error - ----------- ----------------------------------------------------------------- - **202-222** **Input file and toolkit errors** - ----------- ----------------------------------------------------------------- - 202 illegal numeric value - 203 undefined node - 204 undefined link - 205 undefined time pattern - 206 undefined curve - 207 attempt to control a CV/GPV link - 208 illegal PDA pressure limits - 209 illegal node property value - 211 illegal link property value - 212 undefined trace node - 213 invalid option value - 214 too many characters in input line - 215 duplicate ID label - 216 reference to undefined pump - 217 pump has no head curve or power defined - 218 `note: error number 218 is undefined in the EPANET 2.2 toolkit` - 219 illegal valve connection to tank node - 220 illegal valve connection to another valve - 221 misplaced rule clause in rule-based control - 222 link assigned same start and end nodes - ----------- ----------------------------------------------------------------- - **223-234** **Network consistency errors (INP-file and/or toolkit)** - ----------- ----------------------------------------------------------------- - 223 not enough nodes in network - 224 no tanks or reservoirs in network - 225 invalid lower/upper levels for tank - 226 no head curve or power rating for pump - 227 invalid head curve for pump - 230 nonincreasing x-values for curve - 233 network has unconnected node - 234 network has an unconnected node with ID `id` - ----------- ----------------------------------------------------------------- - **240-263** **Toolkit-only errors** - ----------- ----------------------------------------------------------------- - 240 nonexistent water quality source - 241 nonexistent control - 250 invalid format (e.g. too long an ID name) - 251 invalid parameter code - 252 invalid ID name - 253 nonexistent demand category - 254 node with no coordinates - 255 invalid link vertex - 257 nonexistent rule - 258 nonexistent rule clause - 259 attempt to delete a node that still has links connected to it - 260 attempt to delete node assigned as a Trace Node - 261 attempt to delete a node or link contained in a control - 262 attempt to modify network structure while a solver is open - 263 node is not a tank - =========== ================================================================= - -.. table:: EPANET file/system errors - - =========== ================================================================= - *Err No.* *Description* - ----------- ----------------------------------------------------------------- - **301-305** **Filename errors** - ----------- ----------------------------------------------------------------- - 301 identical file names used for different types of files - 302 cannot open input file - 303 cannot open report file - 304 cannot open binary output file - 305 cannot open hydraulics file - ----------- ----------------------------------------------------------------- - **306-307** **File structure errors** - ----------- ----------------------------------------------------------------- - 306 hydraulics file does not match network data - 307 cannot read hydraulics file - ----------- ----------------------------------------------------------------- - **308-309** **Filesystem errors** - ----------- ----------------------------------------------------------------- - 308 cannot save results to binary file - 309 cannot save results to report file - =========== ================================================================= - +Please see :doc:`/errors` for a tables of these values. :meta hide-value: """ diff --git a/wntr/epanet/toolkit.py b/wntr/epanet/toolkit.py index 21ffb67a5..c368e2a28 100644 --- a/wntr/epanet/toolkit.py +++ b/wntr/epanet/toolkit.py @@ -13,14 +13,20 @@ """ import ctypes +import logging import os import os.path import platform import sys from ctypes import byref -from .util import SizeLimits + from pkg_resources import resource_filename +from .exceptions import EN_ERROR_CODES, EpanetException +from .util import SizeLimits + +logger = logging.getLogger(__name__) + epanet_toolkit = "wntr.epanet.toolkit" if os.name in ["nt", "dos"]: @@ -30,15 +36,6 @@ else: libepanet = resource_filename(__name__, "Linux/libepanet2.so") -import logging - -logger = logging.getLogger(__name__) - - -# import warnings - -from .exceptions import EpanetException, EN_ERROR_CODES - def ENgetwarning(code, sec=-1): if sec >= 0: @@ -50,20 +47,20 @@ def ENgetwarning(code, sec=-1): else: header = "{}".format(code) if code < 100: - msg = EN_ERROR_CODES.get(code, 'Unknown warning %s') + msg = EN_ERROR_CODES.get(code, "Unknown warning %s") else: raise EpanetException(code) - + return msg % header + def runepanet(inpfile, rptfile=None, binfile=None): """Run an EPANET command-line simulation - + Parameters ---------- inpfile : str The input file name - """ file_prefix, file_ext = os.path.splitext(inpfile) if rptfile is None: @@ -98,11 +95,9 @@ class ENepanet: Results file to generate version : float EPANET version to use (either 2.0 or 2.2) - """ def __init__(self, inpfile="", rptfile="", binfile="", version=2.2): - self.ENlib = None self.errcode = 0 self.errcodelist = [] @@ -127,19 +122,13 @@ def __init__(self, inpfile="", rptfile="", binfile="", version=2.2): for lib in libnames: try: if os.name in ["nt", "dos"]: - libepanet = resource_filename( - epanet_toolkit, "Windows/%s.dll" % lib - ) + libepanet = resource_filename(epanet_toolkit, "Windows/%s.dll" % lib) self.ENlib = ctypes.windll.LoadLibrary(libepanet) elif sys.platform in ["darwin"]: - libepanet = resource_filename( - epanet_toolkit, "Darwin/lib%s.dylib" % lib - ) + libepanet = resource_filename(epanet_toolkit, "Darwin/lib%s.dylib" % lib) self.ENlib = ctypes.cdll.LoadLibrary(libepanet) else: - libepanet = resource_filename( - epanet_toolkit, "Linux/lib%s.so" % lib - ) + libepanet = resource_filename(epanet_toolkit, "Linux/lib%s.so" % lib) self.ENlib = ctypes.cdll.LoadLibrary(libepanet) return except Exception as E1: @@ -147,7 +136,7 @@ def __init__(self, inpfile="", rptfile="", binfile="", version=2.2): raise E1 pass finally: - if version >= 2.2 and '32' not in lib: + if version >= 2.2 and "32" not in lib: self._project = ctypes.c_uint64() elif version >= 2.2: self._project = ctypes.c_uint32() @@ -164,8 +153,8 @@ def _error(self, *args): if not self.errcode: return # errtxt = self.ENlib.ENgeterror(self.errcode) - errtext = EN_ERROR_CODES.get(self.errcode, 'unknown error') - if '%' in errtext and len(args) == 1: + errtext = EN_ERROR_CODES.get(self.errcode, "unknown error") + if "%" in errtext and len(args) == 1: errtext % args if self.errcode >= 100: self.Errflag = True @@ -174,7 +163,7 @@ def _error(self, *args): else: self.Warnflag = True # warnings.warn(ENgetwarning(self.errcode)) - logger.warning('EPANET warning {} - {}'.format(self.errcode, ENgetwarning(self.errcode, self.cur_time))) + logger.warning("EPANET warning {} - {}".format(self.errcode, ENgetwarning(self.errcode, self.cur_time))) self.errcodelist.append(ENgetwarning(self.errcode, self.cur_time)) return @@ -190,7 +179,6 @@ def ENopen(self, inpfile=None, rptfile=None, binfile=None): Output file to create (default to constructor value) binfile : str Binary output file to create (default to constructor value) - """ if self._project is not None: if self.fileLoaded: @@ -260,7 +248,6 @@ def ENsaveH(self): Must be called before ENreport() if no water quality simulation made. Should not be called if ENsolveQ() will be used. - """ if self._project is not None: self.errcode = self.ENlib.EN_saveH(self._project) @@ -288,7 +275,6 @@ def ENinitH(self, iFlag): if link flows should be re-initialized (1) or not (0) and 2nd digit indicates if hydraulic results should be saved to file (1) or not (0) - """ if self._project is not None: self.errcode = self.ENlib.EN_initH(self._project, iFlag) @@ -299,16 +285,15 @@ def ENinitH(self, iFlag): def ENrunH(self): """Solves hydraulics for conditions at time t - + This function is used in a loop with ENnextH() to run an extended period hydraulic simulation. See ENsolveH() for an example. - + Returns -------- int Current simulation time (seconds) - """ lT = ctypes.c_long() if self._project is not None: @@ -321,16 +306,15 @@ def ENrunH(self): def ENnextH(self): """Determines time until next hydraulic event - + This function is used in a loop with ENrunH() to run an extended period hydraulic simulation. See ENsolveH() for an example. - + Returns --------- int Time (seconds) until next hydraulic event (0 marks end of simulation period) - """ lTstep = ctypes.c_long() if self._project is not None: @@ -356,7 +340,6 @@ def ENsavehydfile(self, filename): ------------- filename : str Name of hydraulics file to output - """ if self._project is not None: self.errcode = self.ENlib.EN_savehydfile(self._project, filename.encode("latin-1")) @@ -372,7 +355,6 @@ def ENusehydfile(self, filename): ------------- filename : str Name of hydraulics file to use - """ if self._project is not None: self.errcode = self.ENlib.EN_usehydfile(self._project, filename.encode("latin-1")) @@ -406,7 +388,6 @@ def ENinitQ(self, iSaveflag): ------------- iSaveflag : int EN_SAVE (1) if results saved to file, EN_NOSAVE (0) if not - """ if self._project is not None: self.errcode = self.ENlib.EN_initQ(self._project, iSaveflag) @@ -417,16 +398,15 @@ def ENinitQ(self, iSaveflag): def ENrunQ(self): """Retrieves hydraulic and water quality results at time t - + This function is used in a loop with ENnextQ() to run an extended period water quality simulation. See ENsolveQ() for an example. - + Returns ------- int Current simulation time (seconds) - """ lT = ctypes.c_long() if self._project is not None: @@ -442,12 +422,11 @@ def ENnextQ(self): This function is used in a loop with ENrunQ() to run an extended period water quality simulation. See ENsolveQ() for an example. - + Returns -------- int Time (seconds) until next hydraulic event (0 marks end of simulation period) - """ lTstep = ctypes.c_long() if self._project is not None: @@ -487,7 +466,7 @@ def ENgetcount(self, iCode): --------- int Number of components in network - + """ iCount = ctypes.c_int() if self._project is not None: @@ -503,7 +482,7 @@ def ENgetflowunits(self): Returns ----------- Code of flow units in use (see toolkit.optFlowUnits) - + """ iCode = ctypes.c_int() if self._project is not None: @@ -513,6 +492,27 @@ def ENgetflowunits(self): self._error() return iCode.value + def ENgetnodeid(self, iIndex): + """Gets the ID name of a node given its index. + + Parameters + ---------- + iIndex : int + a node's index (starting from 1). + + Returns + ------- + str + the node name + """ + fValue = ctypes.create_string_buffer(SizeLimits.EN_MAX_ID.value) + if self._project is not None: + self.errcode = self.ENlib.EN_getnodeid(self._project, iIndex, byref(fValue)) + else: + self.errcode = self.ENlib.ENgetnodeid(iIndex, byref(fValue)) + self._error() + return str(fValue.value, "UTF-8") + def ENgetnodeindex(self, sId): """Retrieves index of a node with specific ID @@ -524,7 +524,7 @@ def ENgetnodeindex(self, sId): Returns --------- Index of node in list of nodes - + """ iIndex = ctypes.c_int() if self._project is not None: @@ -534,9 +534,29 @@ def ENgetnodeindex(self, sId): self._error() return iIndex.value - def ENgetnodevalue(self, iIndex, iCode): + def ENgetnodetype(self, iIndex): + """Retrieves a node's type given its index. + + Parameters + ---------- + iIndex : int + The index of the node + + Returns + ------- + int + the node type as an integer """ - Retrieves parameter value for a node + fValue = ctypes.c_int() + if self._project is not None: + self.errcode = self.ENlib.EN_getnodetype(self._project, iIndex, byref(fValue)) + else: + self.errcode = self.ENlib.ENgetnodetype(iIndex, byref(fValue)) + self._error() + return fValue.value + + def ENgetnodevalue(self, iIndex, iCode): + """Retrieves parameter value for a node Parameters ------------- @@ -559,6 +579,27 @@ def ENgetnodevalue(self, iIndex, iCode): self._error() return fValue.value + def ENgetlinktype(self, iIndex): + """Retrieves a link's type given its index. + + Parameters + ---------- + iIndex : int + The index of the link + + Returns + ------- + int + the link type as an integer + """ + fValue = ctypes.c_int() + if self._project is not None: + self.errcode = self.ENlib.EN_getlinktype(self._project, iIndex, byref(fValue)) + else: + self.errcode = self.ENlib.EN_getlinktype(iIndex, byref(fValue)) + self._error() + return fValue.value + def ENgetlinkindex(self, sId): """Retrieves index of a link with specific ID @@ -605,8 +646,7 @@ def ENgetlinkvalue(self, iIndex, iCode): return fValue.value def ENsetlinkvalue(self, iIndex, iCode, fValue): - """ - Set the value on a link + """Set the value on a link Parameters ---------- @@ -618,13 +658,11 @@ def ENsetlinkvalue(self, iIndex, iCode, fValue): the value to set on the link """ if self._project is not None: - self.errcode = self.ENlib.EN_setlinkvalue(self._project, - ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) + self.errcode = self.ENlib.EN_setlinkvalue( + self._project, ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) ) else: - self.errcode = self.ENlib.ENsetlinkvalue( - ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_float(fValue) - ) + self.errcode = self.ENlib.ENsetlinkvalue(ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_float(fValue)) self._error() def ENsetnodevalue(self, iIndex, iCode, fValue): @@ -641,18 +679,15 @@ def ENsetnodevalue(self, iIndex, iCode, fValue): the value to set on the node """ if self._project is not None: - self.errcode = self.ENlib.EN_setnodevalue(self._project, - ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) + self.errcode = self.ENlib.EN_setnodevalue( + self._project, ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_double(fValue) ) else: - self.errcode = self.ENlib.ENsetnodevalue( - ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_float(fValue) - ) + self.errcode = self.ENlib.ENsetnodevalue(ctypes.c_int(iIndex), ctypes.c_int(iCode), ctypes.c_float(fValue)) self._error() def ENsettimeparam(self, eParam, lValue): - """ - Set a time parameter value + """Set a time parameter value Parameters ---------- @@ -662,18 +697,13 @@ def ENsettimeparam(self, eParam, lValue): the value to set, in seconds """ if self._project is not None: - self.errcode = self.ENlib.EN_settimeparam( - self._project, ctypes.c_int(eParam), ctypes.c_long(lValue) - ) + self.errcode = self.ENlib.EN_settimeparam(self._project, ctypes.c_int(eParam), ctypes.c_long(lValue)) else: - self.errcode = self.ENlib.ENsettimeparam( - ctypes.c_int(eParam), ctypes.c_long(lValue) - ) + self.errcode = self.ENlib.ENsettimeparam(ctypes.c_int(eParam), ctypes.c_long(lValue)) self._error() def ENgettimeparam(self, eParam): - """ - Get a time parameter value + """Get a time parameter value Parameters ---------- @@ -687,69 +717,63 @@ def ENgettimeparam(self, eParam): """ lValue = ctypes.c_long() if self._project is not None: - self.errcode = self.ENlib.EN_gettimeparam( - self._project, ctypes.c_int(eParam), byref(lValue) - ) + self.errcode = self.ENlib.EN_gettimeparam(self._project, ctypes.c_int(eParam), byref(lValue)) else: - self.errcode = self.ENlib.ENgettimeparam( - ctypes.c_int(eParam), byref(lValue) - ) + self.errcode = self.ENlib.ENgettimeparam(ctypes.c_int(eParam), byref(lValue)) self._error() return lValue.value def ENaddcontrol(self, iType: int, iLinkIndex: int, dSetting: float, iNodeIndex: int, dLevel: float) -> int: - """ - Add a new simple control + """Add a new simple control Parameters ---------- iType : int - _description_ + the type of control iLinkIndex : int - _description_ + the index of the link dSetting : float - _description_ + the new link setting value iNodeIndex : int Set to 0 for time of day or timer dLevel : float - _description_ + the level to compare against Returns ------- int - _description_ + the new control number """ lValue = ctypes.c_int() if self._project is not None: self.errcode = self.ENlib.EN_addcontrol( - self._project, - ctypes.c_int(iType), - ctypes.c_int(iLinkIndex), - ctypes.c_double(dSetting), - ctypes.c_int(iNodeIndex), + self._project, + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), ctypes.c_double(dLevel), - byref(lValue) + byref(lValue), ) else: self.errcode = self.ENlib.ENaddcontrol( - ctypes.c_int(iType), - ctypes.c_int(iLinkIndex), - ctypes.c_double(dSetting), - ctypes.c_int(iNodeIndex), + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), ctypes.c_double(dLevel), - byref(lValue) + byref(lValue), ) self._error() return lValue.value def ENgetcontrol(self, iIndex: int): - """ - Add a new simple control + """Get values defined by a control. Parameters ---------- iIndex : int - _description_ + the control number """ iType = ctypes.c_int() iLinkIndex = ctypes.c_int() @@ -758,111 +782,99 @@ def ENgetcontrol(self, iIndex: int): dLevel = ctypes.c_double() if self._project is not None: self.errcode = self.ENlib.EN_getcontrol( - self._project, - ctypes.c_int(iIndex), + self._project, + ctypes.c_int(iIndex), byref(iType), - byref(iLinkIndex), - byref(dSetting), - byref(iNodeIndex), - byref(dLevel) + byref(iLinkIndex), + byref(dSetting), + byref(iNodeIndex), + byref(dLevel), ) else: self.errcode = self.ENlib.ENgetcontrol( - ctypes.c_int(iIndex), - byref(iType), - byref(iLinkIndex), - byref(dSetting), - byref(iNodeIndex), - byref(dLevel) + ctypes.c_int(iIndex), byref(iType), byref(iLinkIndex), byref(dSetting), byref(iNodeIndex), byref(dLevel) ) self._error() - return dict(index=iIndex, type=iType.value, linkindex=iLinkIndex.value, setting=dSetting.value, nodeindex=iNodeIndex.value, level=dLevel.value) + return dict( + index=iIndex, + type=iType.value, + linkindex=iLinkIndex.value, + setting=dSetting.value, + nodeindex=iNodeIndex.value, + level=dLevel.value, + ) def ENsetcontrol(self, iIndex: int, iType: int, iLinkIndex: int, dSetting: float, iNodeIndex: int, dLevel: float): - """ - Add a new simple control + """Change values on a simple control Parameters ---------- iIndex : int - _description_ + the control index iType : int - _description_ + the type of control comparison iLinkIndex : int - _description_ + the link being changed dSetting : float - _description_ + the setting to change to iNodeIndex : int - Set to 0 for time of day or timer + the node being compared against, Set to 0 for time of day or timer dLevel : float - _description_ + the level being checked Warning - ------- - There is an error in EPANET 2.2 that sets the :param:`dLevel` to 0.0 on Macs + ------- + There is an error in EPANET 2.2 that sets the `dLevel` parameter to 0.0 on Macs regardless of the value the user passes in. This means that to use this toolkit functionality on a Mac, the user must delete and create a new control to change the level. - + """ if self._project is not None: try: self.errcode = self.ENlib.EN_setcontrol( - self._project, - ctypes.c_int(iIndex), - ctypes.c_int(iType), - ctypes.c_int(iLinkIndex), - ctypes.c_double(dSetting), - ctypes.c_int(iNodeIndex), - ctypes.c_double(dLevel) + self._project, + ctypes.c_int(iIndex), + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), + ctypes.c_double(dLevel), ) except: self.errcode = self.ENlib.EN_setcontrol( - self._project, - ctypes.c_int(iIndex), - ctypes.c_int(iType), - ctypes.c_int(iLinkIndex), - ctypes.c_double(dSetting), - ctypes.c_int(iNodeIndex), - ctypes.c_float(dLevel) + self._project, + ctypes.c_int(iIndex), + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), + ctypes.c_float(dLevel), ) else: self.errcode = self.ENlib.ENsetcontrol( - ctypes.c_int(iIndex), - ctypes.c_int(iType), - ctypes.c_int(iLinkIndex), - ctypes.c_double(dSetting), - ctypes.c_int(iNodeIndex), - ctypes.c_double(dLevel) + ctypes.c_int(iIndex), + ctypes.c_int(iType), + ctypes.c_int(iLinkIndex), + ctypes.c_double(dSetting), + ctypes.c_int(iNodeIndex), + ctypes.c_double(dLevel), ) self._error() def ENdeletecontrol(self, iControlIndex): - """ - Get a time parameter value + """Delete a control. Parameters ---------- iControlIndex : int - the time parameter to get - - Returns - ------- - int - the index of the new control + the simple control to delete """ - lValue = ctypes.c_long() if self._project is not None: - self.errcode = self.ENlib.EN_deletecontrol( - self._project, - ctypes.c_int(iControlIndex) - ) + self.errcode = self.ENlib.EN_deletecontrol(self._project, ctypes.c_int(iControlIndex)) else: - self.errcode = self.ENlib.ENdeletecontrol( - ctypes.c_int(iControlIndex) - ) + self.errcode = self.ENlib.ENdeletecontrol(ctypes.c_int(iControlIndex)) self._error() - return lValue.value def ENsaveinpfile(self, inpfile): """Saves EPANET input file @@ -870,7 +882,7 @@ def ENsaveinpfile(self, inpfile): Parameters ------------- inpfile : str - EPANET INP output file + EPANET INP output file """ @@ -881,4 +893,4 @@ def ENsaveinpfile(self, inpfile): self.errcode = self.ENlib.ENsaveinpfile(inpfile) self._error() - return \ No newline at end of file + return From 900561a5ddd3b9fb3ac9534936104487d8392a38 Mon Sep 17 00:00:00 2001 From: dbhart Date: Fri, 17 Nov 2023 09:37:07 -0700 Subject: [PATCH 10/15] Added the tests for exceptions --- .../apidoc/wntr.epanet.exceptions.rst | 7 --- wntr/tests/test_epanet_exceptions.py | 47 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) delete mode 100644 documentation/apidoc/wntr.epanet.exceptions.rst create mode 100644 wntr/tests/test_epanet_exceptions.py diff --git a/documentation/apidoc/wntr.epanet.exceptions.rst b/documentation/apidoc/wntr.epanet.exceptions.rst deleted file mode 100644 index 6cb9d9029..000000000 --- a/documentation/apidoc/wntr.epanet.exceptions.rst +++ /dev/null @@ -1,7 +0,0 @@ -wntr.epanet.exceptions module -============================= - -.. automodule:: wntr.epanet.exceptions - :members: - :no-undoc-members: - :show-inheritance: diff --git a/wntr/tests/test_epanet_exceptions.py b/wntr/tests/test_epanet_exceptions.py new file mode 100644 index 000000000..5dd3b20c3 --- /dev/null +++ b/wntr/tests/test_epanet_exceptions.py @@ -0,0 +1,47 @@ +import unittest +from os.path import abspath, dirname, join, exists + +import wntr.epanet.exceptions + +testdir = dirname(abspath(__file__)) +datadir = join(testdir, "..", "..", "examples", "networks") + + +class TestEpanetExceptions(unittest.TestCase): + + def test_epanet_exception(self): + try: + raise wntr.epanet.exceptions.EpanetException(213, '13:00:00 pm', 'Cannot specify am/pm for times greater than 12:00:00') + except Exception as e: + self.assertTupleEqual(e.args, ("(Error 213) invalid option value '13:00:00 pm' ['Cannot specify am/pm for times greater than 12:00:00']",)) + try: + raise wntr.epanet.exceptions.EpanetException(999) + except Exception as e: + self.assertTupleEqual(e.args, ('(Error 999) unknown error',)) + try: + raise wntr.epanet.exceptions.EpanetException(108) + except Exception as e: + self.assertTupleEqual(e.args, ('(Error 108) cannot use external file while hydraulics solver is active',)) + + def test_epanet_syntax_error(self): + try: + raise wntr.epanet.exceptions.ENSyntaxError(223, line_num=38, line='I AM A SYNTAX ERROR') + except SyntaxError as e: + self.assertTupleEqual(e.args, ('(Error 223) not enough nodes in network, at line 38:\n I AM A SYNTAX ERROR',)) + + def test_epanet_key_error(self): + try: + raise wntr.epanet.exceptions.ENKeyError(206, 'NotACurve') + except KeyError as e: + self.assertTupleEqual(e.args, ("(Error 206) undefined curve, 'NotACurve'",)) + + def test_epanet_value_error(self): + try: + raise wntr.epanet.exceptions.ENValueError(213, 423.0e28) + except ValueError as e: + self.assertTupleEqual(e.args, ('(Error 213) invalid option value 4.23e+30',)) + + + +if __name__ == "__main__": + unittest.main() From 1474f425f77a212d5673b1618f10a58c8eaaffef Mon Sep 17 00:00:00 2001 From: dbhart Date: Fri, 17 Nov 2023 09:51:44 -0700 Subject: [PATCH 11/15] Documentation updates for exceptions. --- documentation/errors.rst | 1 + documentation/userguide.rst | 1 + wntr/epanet/exceptions.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/documentation/errors.rst b/documentation/errors.rst index 5206caaba..bd945d6f7 100644 --- a/documentation/errors.rst +++ b/documentation/errors.rst @@ -2,6 +2,7 @@ \clearpage +.. _epanet-errors: Errors and debugging ==================== diff --git a/documentation/userguide.rst b/documentation/userguide.rst index 6e31a2128..fa4c7d302 100644 --- a/documentation/userguide.rst +++ b/documentation/userguide.rst @@ -70,6 +70,7 @@ U.S. Department of Energy's National Nuclear Security Administration under contr graphics gis advancedsim + errors .. toctree:: :maxdepth: 1 diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index 82ce50f61..6442a0706 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -82,7 +82,7 @@ 309: "cannot save results to report file %s", } """A dictionary of the error codes and their meanings from the EPANET toolkit. -Please see :doc:`/errors` for a tables of these values. +Please see :doc:`/errors` for descriptions of these values. :meta hide-value: """ From bd5aca1125327430bb3b108c7ac5eab3e53ca56e Mon Sep 17 00:00:00 2001 From: dbhart Date: Fri, 17 Nov 2023 12:33:39 -0700 Subject: [PATCH 12/15] Fix to a documentation error that slipped in --- wntr/epanet/util.py | 55 ++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/wntr/epanet/util.py b/wntr/epanet/util.py index 5a1939bda..c06528573 100644 --- a/wntr/epanet/util.py +++ b/wntr/epanet/util.py @@ -70,17 +70,17 @@ class FlowUnits(enum.Enum): .. rubric:: Enum Members ============== ==================================== ======================== - :attr:`~CFS` :math:`ft^3\,/\,s` :attr:`is_traditional` - :attr:`~GPM` :math:`gal\,/\,min` :attr:`is_traditional` - :attr:`~MGD` :math:`10^6\,gal\,/\,day` :attr:`is_traditional` - :attr:`~IMGD` :math:`10^6\,Imp.\,gal\,/\,day` :attr:`is_traditional` - :attr:`~AFD` :math:`acre\cdot\,ft\,/\,day` :attr:`is_traditional` - :attr:`~LPS` :math:`L\,/\,s` :attr:`is_metric` - :attr:`~LPM` :math:`L\,/\,min` :attr:`is_metric` - :attr:`~MLD` :math:`ML\,/\,day` :attr:`is_metric` - :attr:`~CMH` :math:`m^3\,\,hr` :attr:`is_metric` - :attr:`~CMD` :math:`m^3\,/\,day` :attr:`is_metric` - :attr:`~SI` :math:`m^3\,/\,s` + :attr:`~CFS` :math:`\rm ft^3\,/\,s` :attr:`is_traditional` + :attr:`~GPM` :math:`\rm gal\,/\,min` :attr:`is_traditional` + :attr:`~MGD` :math:`\rm 10^6\,gal\,/\,day` :attr:`is_traditional` + :attr:`~IMGD` :math:`\rm 10^6\,Imp.\,gal\,/\,day` :attr:`is_traditional` + :attr:`~AFD` :math:`\rm acre\cdot\,ft\,/\,day` :attr:`is_traditional` + :attr:`~LPS` :math:`\rm L\,/\,s` :attr:`is_metric` + :attr:`~LPM` :math:`\rm L\,/\,min` :attr:`is_metric` + :attr:`~MLD` :math:`\rm ML\,/\,day` :attr:`is_metric` + :attr:`~CMH` :math:`\rm m^3\,\,hr` :attr:`is_metric` + :attr:`~CMD` :math:`\rm m^3\,/\,day` :attr:`is_metric` + :attr:`~SI` :math:`\rm m^3\,/\,s` ============== ==================================== ======================== .. rubric:: Enum Member Attributes @@ -459,21 +459,21 @@ class HydParam(enum.Enum): .. rubric:: Enum Members ========================== =================================================================== - :attr:`Elevation` Nodal elevation - :attr:`Demand` Nodal demand - :attr:`HydraulicHead` Nodal head - :attr:`Pressure` Nodal pressure - :attr:`EmitterCoeff` Emitter coefficient - :attr:`TankDiameter` Tank diameter - :attr:`Volume` Tank volume - :attr:`Length` Link length - :attr:`PipeDiameter` Pipe diameter - :attr:`Flow` Link flow - :attr:`Velocity` Link velocity - :attr:`HeadLoss` Link headloss (from start node to end node) - :attr:`RoughnessCoeff` Link roughness (requires `darcy_weisbach` setting for conversion) - :attr:`Energy` Pump energy - :attr:`Power` Pump power + :attr:`Elevation` Nodal elevation + :attr:`Demand` Nodal demand + :attr:`HydraulicHead` Nodal head + :attr:`Pressure` Nodal pressure + :attr:`EmitterCoeff` Emitter coefficient + :attr:`TankDiameter` Tank diameter + :attr:`Volume` Tank volume + :attr:`Length` Link length + :attr:`PipeDiameter` Pipe diameter + :attr:`Flow` Link flow + :attr:`Velocity` Link velocity + :attr:`HeadLoss` Link headloss (from start node to end node) + :attr:`RoughnessCoeff` Link roughness (requires `darcy_weisbach` setting for conversion) + :attr:`Energy` Pump energy + :attr:`Power` Pump power ========================== =================================================================== @@ -852,14 +852,17 @@ class FormulaType(enum.Enum): 0, "H-W", ) + """Hazen-Williams headloss formula.""" DW = ( 1, "D-W", ) + """Darcy-Weisbach formula, requires untis conversion.""" CM = ( 2, "C-M", ) + """Chezy-Manning formula.""" def __init__(self, eid, inpcode): v2mm = getattr(self, "_value2member_map_") From f3588ff74c28de5de763a6bcab96d33a32dc382a Mon Sep 17 00:00:00 2001 From: David Hart Date: Sun, 10 Mar 2024 18:25:17 -0600 Subject: [PATCH 13/15] Updates to make exceptions tested in io.py --- wntr/epanet/io.py | 8 +- .../tests/networks_for_testing/bad_syntax.inp | 462 ++++++++++++++++++ wntr/tests/networks_for_testing/bad_times.inp | 461 +++++++++++++++++ .../tests/networks_for_testing/bad_values.inp | 460 +++++++++++++++++ wntr/tests/test_epanet_exceptions.py | 30 +- 5 files changed, 1418 insertions(+), 3 deletions(-) create mode 100644 wntr/tests/networks_for_testing/bad_syntax.inp create mode 100644 wntr/tests/networks_for_testing/bad_times.inp create mode 100644 wntr/tests/networks_for_testing/bad_values.inp diff --git a/wntr/epanet/io.py b/wntr/epanet/io.py index e09ae6b12..c2f615fcd 100644 --- a/wntr/epanet/io.py +++ b/wntr/epanet/io.py @@ -693,8 +693,8 @@ def _read_pipes(self): minor_loss = 0. link_status = LinkStatus.Open check_valve = False - - self.wn.add_pipe(current[0], + try: + self.wn.add_pipe(current[0], current[1], current[2], to_si(self.flow_units, float(current[3]), HydParam.Length), @@ -703,6 +703,10 @@ def _read_pipes(self): minor_loss, link_status, check_valve) + except KeyError as e: + raise ENKeyError(203, str(e.args[0]), line_num=lnum) from e + except ValueError as e: + raise ENValueError(211, str(e.args[0]), line_num=lnum) from e def _write_pipes(self, f, wn): f.write('[PIPES]\n'.encode(sys_default_enc)) diff --git a/wntr/tests/networks_for_testing/bad_syntax.inp b/wntr/tests/networks_for_testing/bad_syntax.inp new file mode 100644 index 000000000..144b31b29 --- /dev/null +++ b/wntr/tests/networks_for_testing/bad_syntax.inp @@ -0,0 +1,462 @@ +[TITLE] + Small Test Network 1 +A very simple network with a reservoir and two nodes all at the same elevation + + + + + + +[JUNCTIONS] +;Format (One line for each junction): +; JunctionID Elevation BaseDemandFlow DemandPatternID +;If no demand pattern is specified, then the junction demand follows the default pattern specified in the OPTIONS section or Pattern 1 if no default pattern is specified. +;ID Elev Demand Pattern + junction1 0 3600 pattern1 ; + junction2 0 3600 pattern2 ; + + + + + + +[RESERVOIRS] +;Format: +; ReservoirID Head HeadPatternID +; head is the hydraulic head (elevation+pressure) +;ID Head Pattern + reservoir1 1000 pattern1 ; + + + + + + +[TANKS] +;Format: +; TankID BottomElevation InitalWaterLevel MinimumWaterLevel MaximumWaterLevel NominalDiameter MinimumVolume VolumeCurveID +;where +; VolumeCurveID if for volume vs. water depth curves for non-cylindrical tanks +; If a volume curve is specified, the diameter can be any non-zero number +; MinimumVolume is the tank volume at minimum water level. It can be zero for a cylindrical tank or if a volume curve is supplied +;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve + + + + + + +[PIPES] +;Format (one line for each pipe): +; PipeID StartNodeID EndNodeID Length Diameter RoughnessCoefficient MinorLossCoefficient Status +;Status can be OPEN, CLOSED, or CV (pipe contains check valve) +;If minor loss coefficient is 0 and the pipe is open, then the last two columns can be left out. +;ID Node1 Node2 Length Diameter Roughness MinorLoss Status + pipe1 reservoir1 junction1 1000 457.2 100 0 Open ; + pipe2 junction1 junction2 1000 457.2 100 0 Open ; + + + + + + +[PUMPS] +;Format (one line for each pump): +; PumpID StartNodeID EndNodeID KeywordAndValue +;Keywords: +; POWER - power for constant energy pump +; HEAD - ID of curve that describes head vs flow for the pump +; SPEED - relative speed setting (normal speed is 1.0, 0 means pump is off) +; PATTERN - ID of time pattern that describes how speed setting varies with time +;Keyword/Value pairs can be repeated +;Either POWER or HEAD must be supplied for each pump. Other keywords are optional + + + + + + +[VALVES] +;Format (one line for each valve): +; ValveID StartNodeID EndNodeID Diameter ValveType ValveSetting MinorLossCoefficient +;Valve Types and Settings: +; Type Setting +; PRV (pressure reducing valve) Pressure +; PSV (pressure sustaining valve) Pressure +; PBV (pressure breaker valve) Pressure +; FCV (flow control valve) Flow +; TCV (throttle control valve) Loss Coefficient +; GPV (general purpose valve) ID of head loss curve +;Note: Shutoff valves and check valves are considered to be a part of a pipe, not a separate control valve component + + + + + + +[TAGS] +;Not used in hydraulic or water quality simulation +;Associates category labels with specific nodes and links +;Format (one line for each node and link with a tag): +; NODE/LINK NodeID/LinkID TagLabelTextNoSpaces + + + + + + +[DEMANDS] +;Supplement to JUNCTIONS section for defining multiple water demands at junction nodes +;Format (one line for each category of demand at a junction): +; JunctionID BaseDemand DemandPatternID ;DemandCategoryName + + + + + + +[STATUS] +;Defines the initial status of selected links at the start of a simulation +;Format (one line per link being controlled): +; LinkID Status/Setting +;Links not listed default to OPEN or ACTIVE + + + + + + +[PATTERNS] +;Format: +; PatternID Multiplier1 Multiplier2 ... +;All patterns share the same time period interval, but they can have different numbers of time periods. +;Patterns wrap around back to first multiplier +;Multiple lines can be used for a single pattern +;ID Multipliers +;Demand Pattern + pattern1 1.0 1.0 1.0 1.0 + pattern2 0.5 1.0 1.5 1.0 + + + + + + +[CURVES] +;One line needed for each x,y point on each curve +;Points must be entered in order of increasing x-values +;Format: +; CurveID x-value y-value +;Place a comment with the curve type followed by a colon and then a description on line directly above first point for that curve: +; PUMP: Head vs flow curve for pump xxx +;Curve types are: PUMP, EFFICIENCY, VOLUME, AND HEADLOSS + + + + + + +[CONTROLS] +;Possible formats (Note: all caps words are keywords that should not be changed): +; LINK linkID status IF NODE nodeID ABOVE/BELOW value +; LINK linkID status AT TIME time +; LINK linkID status AT CLOCKTIME clocktime AM/PM +;where: +; linkID = a link ID label +; status = OPEN or CLOSED, a pump speed setting, or a control valve setting +; nodeID = a node ID label +; value = a pressure for a junction or a water level for a tank +; time = a time since the start of the simulation in decimal hours or in hours:minutes format +; clocktime = a 24-hour clock time (hours:minutes) + + + + + + +[RULES] +;Format (each rule is a series of statements): +; RULE RuleID +; IF condition_1 +; AND condition_2 +; OR condition_3 +; AND condition_4 +; etc. +; THEN action_1 +; AND action_2 +; etc. +; ELSE action_3 +; AND action_4 +; etc. +; PRIORITY value +; See epanet manual for more details + + + + + + +[ENERGY] +;Defines parameters used to compute pumping energy and cost +;Possible formats (Note: all caps words are keywords that should not be changed): +; GLOBAL PRICE/PATTERN/EFFIC value +; PUMP PumpID PRICE/PATTERN/EFFIC value +; DEMAND CHARGE value +;PUMP overrides GLOBAL for specified pump +;Price: average cost per kWh +;Pattern: pattern ID describing how energy price varies with time +;Efficiency: single percent efficiency for GLOBAL or efficiency curve ID for PUMP +;DEMAND CHARGE: ? +;Default global pump efficiency = 75 +;Default global energy price = 0 + + + + + + +[EMITTERS] +;Defines junctions modeled as emitters (sprinklers or orifices) +;Format: +; JunctionID FlowCoefficient + + + + + + +[QUALITY] +;Define inital water quality at nodes +;Format (one line per node): +; NodeID InitialQuality +;Quality is 0 for nodes not listed +;Quality represents concentration for chemicals, hours for water age, or percent for source tracing + + + + + + +[SOURCES] +;Defines locations of water quality sources +;Format (one line for each source): +; NodeID SourceType BaselineSourceStrength PatternID +;where +; SourceType can be CONCEN, MASS, FLOWPACED, OR SETPOINT +; BaselineSourceStrength is the water quality (e.g., concentration of the source or flow rate of the source) +; MASS type sources measure strength in mass flow per minute. All other source types measure strength in concentration. +; CONCEN type sources represent the concentration of any external source inflow to the node +; A MASS, FLOWPACED, OR SETPOINT type source represents a booster source, where the substance is injected directly into the network regardless of what the demand at the node is. See epanet manual for more details +;Node Type Quality Pattern + + + + + + +[REACTIONS] +;Format: +; ORDER BULK/WALL/TANK value +; GLOBAL BULK/WALL value +; BULK/WALL/TANK PipeID value +; LIMITING POTENTIAL value +; ROUGHNESS CORRELATION value +;where: +; Order is the order of the reaction. Values for wall reactions must be either 0 or 1. Default is 1.0. +; Global is used to set a global value for all bulk reaction coefficients (pipes and tanks) or for all pipe wall coefficients. Default value is 0. +; Bulk, wall, and tank are used to override the global reaction coefficients for specific pipes and tanks +; Limiting potential specifies that reaction rates are proportional to the difference between the current concentration and some limiting potential value. +; Roughness correlation will make all default pipe wall reaction coefficients be related to pipe roughness (see epanet manual page 159 for details). +;Remember: positive coefficients for products and negative coefficients for reactants + + + + + + +[MIXING] +;Identify the model that governs mixing in storage tanks +;Format (one line per tank): +; TankID MixingModel CompartmentVolumeFraction +;Possible mixing models: +; MIXED: completely mixed +; 2COMP: two-compartment mixing +; FIFO: plug flow +; LIFO: stacked plug flow +;CompartmentVolumeFraction only applies to 2COMP and represents the fraction of the total tank volume devoted to the inlet/outlet compartment +;Default is MIXED + + + + + + +[TIMES] +;Format: +; Duration value +; Hydraulic Timestep value +; Quality Timestep value +; Rule Timestep value +; Pattern Timestep value +; Pattern Start value +; Report Timestep value +; Report Start value +; Start ClockTime value +; Statistic value +;where: +; Duration is the simulation duration +; Hydraulic Timestep determines how often a new hydraulic state of the network is computed. Default is 1 hour +; Quality Timestep is the time step used to track changes in water quality. The default is 1/10 of the hydraulic time step +; Rule Timestep is the time step used to check for changes in system status due to activation of rule-based controls between hydraulic time steps. The default is 1/10 of the hydraulic time step. +; Pattern Timestep is the interval between time periods in all time patterns. The default is 1 hour. +; Pattern Start is the time offset at which all patterns will start. E.g., a value of 6 hours would start the simulation with each pattern in the time period that corresponds to hour 6. the default is 0. +; Report Timestep sets the time interval between which output results are reported. The default is 1 hour. +; Report Start is the length of time into the simulation at which output results begin to be reported. The default is 0. +; Start Clocktime is the time of day at which the simulation begins. The default is 12:00 AM. +; Statistic determines what kind of statistical post-processing should be done on the time series of simulation results. Options include AVERAGED, MINIMUM, MAXIMUM, RANGE, and NONE. NONE reports the full time +; series for all quantities for all nodes and links and is the default. +;Units can be SEC, MIN, HOURS, or DAYS. The default is hours. If units are not supplied, then time values can be entered as decimal hours or in hours:minuts format. + Duration 27:05 + Hydraulic Timestep 1:05 + Quality Timestep 1:05 + Rule Timestep 1:05 + Pattern Timestep 2:10 + Pattern Start 0:00 + Report Timestep 1:05 + Report Start 0:00 + Start ClockTime 12 am + Statistic NONE + +[FOO] + This should raise an error + + + + +[REPORT] +;Format: +; PAGESIZE value +; FILE filename +; STATUS YES/NO/FULL +; SUMMARY YES/NO +; ENERGY YES/NO +; NODES NONE/ALL/node1 node2 ... +; LINKS NONE/ALL/link1 link2 ... +; parameter YES/NO +; parameter BELOW/ABOVE/PRECISION value +;where: +; Pagesize sets the number of lines written per page of the output report. The default is 0, meaning that no line limit per page is in effect. +; File supplies the name of a file to which the output report will be written +; Status determines whether a hydraulic status report should be generated. Full will also include information from each trial. +; Summary determines wheter a summary table is generated +; Energy determines if a table reporting average energy usage and cost for each pump is provided. +; Nodes identifies which nodes will be reported on +; Links identifies which links will be reported on +; The parameter option is used to identify which quantities are reported on, how many decimal places are displayed, and what kind of filtering should be used to limit output. +; Node parameters that can be reported: elevation, demand, head, pressure, quality. +; Link parameters that can be reported: Length, diameter, flow, velocity, headloss, position, setting, reaction, f-factor + Status Yes + Summary No + Energy No + + + + + +[OPTIONS] +;UNITS CFS/GPM/MGD/IMGD/AFD/LPS/LPM/MLD/CMH/CMD +;HEADLOSS H-W/D-W/C-M +;HYDRAULICS USE/SAVE filename +;QUALITY NONE/CHEMICAL/AGE/TRACE id +;VISCOSITY value +;DIFFUSIVITY value +;SPECIFIC GRAVITY value +;TRIALS value +;ACCURACY value +;UNBALANCED STOP/CONTINUE/CONTINUE n +;PATTERN id +;DEMAND MULTIPLIER value +;EMITTER EXPONENT value +;TOLERANCE value +;MAP filename +; +;UNITS: units of flow rate. For CFS, GPM, MGD, IMGD, and AFD, other input quantities must be in US Customary Units. If flow units are in liters or cubic meters, then Metric Units must be used for all other input quantities. +;HEADLOSS: Method for calculating head loss for flow through a pipe +;HYDRAULICS: either saves the current hydraulics solution or uses a previously saved hydraulics solution. +;QUALITY: Type of water quality analysis to perform. In place of CHEMICAL, the actual name of the chemical can be used followed by it's concentration units. If TRACE is used, it must be followed by the ID of the node being traced. +;VISCOSITY: The kinematic viscosity of the fluid being modeled relative to that of water at 20 ded C. +;DIFFUSIVITY: Molecular diffusivity of the chemical being analyzed relative to that of chlorine in water. It is only used when mass transfer limitations are considered in pipe wall reactions. A value of 0 will cause +; EPANET to ignore mass transfer limitations +;SPECIFIC GRAVITY: The ratio of the density of the fluid being modeled to that of water at 4 deg C +;TRIALS: The maximum number of trials used to solve the network hydraulics at each hydraulic time step of a simulation +;Accuracy: Convergence criteria for hydraulic solution. The sum of all flow changes from the previous solution divided by the total flow in all links +;UNBALANCED: Specifies what to do if a hydraulic solution cannot be reached in the specified number of trials. +;PATTERN: Default demand pattern for all junctions where no demand pattern was specified. +;DEMAND MULTIPLIER: Used to adjust the values of baseline demands for all junctions and all demand categories. +;EMITTER EXPONENT: Specifies the power to which the pressure at a junction is raised when computing the flow issuing from an emitter. +;MAP: Used to supply the name of a file containing coordinates of the network's nodes so that a map of the network can be drawn. +;TOLERANCE: The difference in water quality level below which one can say that one parcel of water is essentially the same as another. + Units CMH + Headloss H-W + Quality None + Specific Gravity 1.0 + Viscosity 1.0 + Trials 50 + Accuracy 0.0000001 + Unbalanced Stop + Pattern pattern1 + Demand Multiplier 1.0 + Tolerance 0.01 + + + + + + +[COORDINATES] +;Not used in simulation (only for producing a map) +;Coordinates are distances from arbitrary origin at lower left of map +;Possible formats: +; NodeID X-Coordinate Y-Coordinate +;Node X-Coord Y-Coord + reservoir1 0.00 0.00 + junction1 25.00 0.00 + junction2 50.00 0.00 + + + + + + +[VERTICES] +;Not used in simulation +;Assigns interior vertex points to network links - allows links to be drawn as polylines instead of simple straight-lines between their end nodes +;Format: +; LinkID x-coordinate y-coordinate + + + + + + +[LABELS] +;Not used in simulation +;Assigns coordinates to map labels +;Format (One line for each label): +; x-coordinate y-coordinate "LabelText" AnchorNodeIDLabel +;Coordinates refer to upper left corner of label +;Optional anchor node anchors the label to the node when the map is re-scaled during zooming + + + + + + +[BACKDROP] + + + + + + +[END] diff --git a/wntr/tests/networks_for_testing/bad_times.inp b/wntr/tests/networks_for_testing/bad_times.inp new file mode 100644 index 000000000..58bcee519 --- /dev/null +++ b/wntr/tests/networks_for_testing/bad_times.inp @@ -0,0 +1,461 @@ +[TITLE] + Small Test Network 1 +A very simple network with a reservoir and two nodes all at the same elevation + + + + + + +[JUNCTIONS] +;Format (One line for each junction): +; JunctionID Elevation BaseDemandFlow DemandPatternID +;If no demand pattern is specified, then the junction demand follows the default pattern specified in the OPTIONS section or Pattern 1 if no default pattern is specified. +;ID Elev Demand Pattern + junction1 0 3600 pattern1 ; + junction2 0 3600 pattern2 ; + + + + + + +[RESERVOIRS] +;Format: +; ReservoirID Head HeadPatternID +; head is the hydraulic head (elevation+pressure) +;ID Head Pattern + reservoir1 1000 pattern1 ; + + + + + + +[TANKS] +;Format: +; TankID BottomElevation InitalWaterLevel MinimumWaterLevel MaximumWaterLevel NominalDiameter MinimumVolume VolumeCurveID +;where +; VolumeCurveID if for volume vs. water depth curves for non-cylindrical tanks +; If a volume curve is specified, the diameter can be any non-zero number +; MinimumVolume is the tank volume at minimum water level. It can be zero for a cylindrical tank or if a volume curve is supplied +;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve + + + + + + +[PIPES] +;Format (one line for each pipe): +; PipeID StartNodeID EndNodeID Length Diameter RoughnessCoefficient MinorLossCoefficient Status +;Status can be OPEN, CLOSED, or CV (pipe contains check valve) +;If minor loss coefficient is 0 and the pipe is open, then the last two columns can be left out. +;ID Node1 Node2 Length Diameter Roughness MinorLoss Status + pipe1 reservoir1 junction1 1000 457.2 100 0 Open ; + pipe2 junction1 junction2 1000 457.2 100 0 Open ; + + + + + + +[PUMPS] +;Format (one line for each pump): +; PumpID StartNodeID EndNodeID KeywordAndValue +;Keywords: +; POWER - power for constant energy pump +; HEAD - ID of curve that describes head vs flow for the pump +; SPEED - relative speed setting (normal speed is 1.0, 0 means pump is off) +; PATTERN - ID of time pattern that describes how speed setting varies with time +;Keyword/Value pairs can be repeated +;Either POWER or HEAD must be supplied for each pump. Other keywords are optional + + + + + + +[VALVES] +;Format (one line for each valve): +; ValveID StartNodeID EndNodeID Diameter ValveType ValveSetting MinorLossCoefficient +;Valve Types and Settings: +; Type Setting +; PRV (pressure reducing valve) Pressure +; PSV (pressure sustaining valve) Pressure +; PBV (pressure breaker valve) Pressure +; FCV (flow control valve) Flow +; TCV (throttle control valve) Loss Coefficient +; GPV (general purpose valve) ID of head loss curve +;Note: Shutoff valves and check valves are considered to be a part of a pipe, not a separate control valve component + + + + + + +[TAGS] +;Not used in hydraulic or water quality simulation +;Associates category labels with specific nodes and links +;Format (one line for each node and link with a tag): +; NODE/LINK NodeID/LinkID TagLabelTextNoSpaces + + + + + + +[DEMANDS] +;Supplement to JUNCTIONS section for defining multiple water demands at junction nodes +;Format (one line for each category of demand at a junction): +; JunctionID BaseDemand DemandPatternID ;DemandCategoryName + + + + + + +[STATUS] +;Defines the initial status of selected links at the start of a simulation +;Format (one line per link being controlled): +; LinkID Status/Setting +;Links not listed default to OPEN or ACTIVE + + + + + + +[PATTERNS] +;Format: +; PatternID Multiplier1 Multiplier2 ... +;All patterns share the same time period interval, but they can have different numbers of time periods. +;Patterns wrap around back to first multiplier +;Multiple lines can be used for a single pattern +;ID Multipliers +;Demand Pattern + pattern1 1.0 1.0 1.0 1.0 + pattern2 0.5 1.0 1.5 1.0 + + + + + + +[CURVES] +;One line needed for each x,y point on each curve +;Points must be entered in order of increasing x-values +;Format: +; CurveID x-value y-value +;Place a comment with the curve type followed by a colon and then a description on line directly above first point for that curve: +; PUMP: Head vs flow curve for pump xxx +;Curve types are: PUMP, EFFICIENCY, VOLUME, AND HEADLOSS + + + + + + +[CONTROLS] +;Possible formats (Note: all caps words are keywords that should not be changed): +; LINK linkID status IF NODE nodeID ABOVE/BELOW value +; LINK linkID status AT TIME time +; LINK linkID status AT CLOCKTIME clocktime AM/PM +;where: +; linkID = a link ID label +; status = OPEN or CLOSED, a pump speed setting, or a control valve setting +; nodeID = a node ID label +; value = a pressure for a junction or a water level for a tank +; time = a time since the start of the simulation in decimal hours or in hours:minutes format +; clocktime = a 24-hour clock time (hours:minutes) + + + + + + +[RULES] +;Format (each rule is a series of statements): +; RULE RuleID +; IF condition_1 +; AND condition_2 +; OR condition_3 +; AND condition_4 +; etc. +; THEN action_1 +; AND action_2 +; etc. +; ELSE action_3 +; AND action_4 +; etc. +; PRIORITY value +; See epanet manual for more details + + + + + + +[ENERGY] +;Defines parameters used to compute pumping energy and cost +;Possible formats (Note: all caps words are keywords that should not be changed): +; GLOBAL PRICE/PATTERN/EFFIC value +; PUMP PumpID PRICE/PATTERN/EFFIC value +; DEMAND CHARGE value +;PUMP overrides GLOBAL for specified pump +;Price: average cost per kWh +;Pattern: pattern ID describing how energy price varies with time +;Efficiency: single percent efficiency for GLOBAL or efficiency curve ID for PUMP +;DEMAND CHARGE: ? +;Default global pump efficiency = 75 +;Default global energy price = 0 + + + + + + +[EMITTERS] +;Defines junctions modeled as emitters (sprinklers or orifices) +;Format: +; JunctionID FlowCoefficient + + + + + + +[QUALITY] +;Define inital water quality at nodes +;Format (one line per node): +; NodeID InitialQuality +;Quality is 0 for nodes not listed +;Quality represents concentration for chemicals, hours for water age, or percent for source tracing + + + + + + +[SOURCES] +;Defines locations of water quality sources +;Format (one line for each source): +; NodeID SourceType BaselineSourceStrength PatternID +;where +; SourceType can be CONCEN, MASS, FLOWPACED, OR SETPOINT +; BaselineSourceStrength is the water quality (e.g., concentration of the source or flow rate of the source) +; MASS type sources measure strength in mass flow per minute. All other source types measure strength in concentration. +; CONCEN type sources represent the concentration of any external source inflow to the node +; A MASS, FLOWPACED, OR SETPOINT type source represents a booster source, where the substance is injected directly into the network regardless of what the demand at the node is. See epanet manual for more details +;Node Type Quality Pattern + + + + + + +[REACTIONS] +;Format: +; ORDER BULK/WALL/TANK value +; GLOBAL BULK/WALL value +; BULK/WALL/TANK PipeID value +; LIMITING POTENTIAL value +; ROUGHNESS CORRELATION value +;where: +; Order is the order of the reaction. Values for wall reactions must be either 0 or 1. Default is 1.0. +; Global is used to set a global value for all bulk reaction coefficients (pipes and tanks) or for all pipe wall coefficients. Default value is 0. +; Bulk, wall, and tank are used to override the global reaction coefficients for specific pipes and tanks +; Limiting potential specifies that reaction rates are proportional to the difference between the current concentration and some limiting potential value. +; Roughness correlation will make all default pipe wall reaction coefficients be related to pipe roughness (see epanet manual page 159 for details). +;Remember: positive coefficients for products and negative coefficients for reactants + + + + + + +[MIXING] +;Identify the model that governs mixing in storage tanks +;Format (one line per tank): +; TankID MixingModel CompartmentVolumeFraction +;Possible mixing models: +; MIXED: completely mixed +; 2COMP: two-compartment mixing +; FIFO: plug flow +; LIFO: stacked plug flow +;CompartmentVolumeFraction only applies to 2COMP and represents the fraction of the total tank volume devoted to the inlet/outlet compartment +;Default is MIXED + + + + + + +[TIMES] +;Format: +; Duration value +; Hydraulic Timestep value +; Quality Timestep value +; Rule Timestep value +; Pattern Timestep value +; Pattern Start value +; Report Timestep value +; Report Start value +; Start ClockTime value +; Statistic value +;where: +; Duration is the simulation duration +; Hydraulic Timestep determines how often a new hydraulic state of the network is computed. Default is 1 hour +; Quality Timestep is the time step used to track changes in water quality. The default is 1/10 of the hydraulic time step +; Rule Timestep is the time step used to check for changes in system status due to activation of rule-based controls between hydraulic time steps. The default is 1/10 of the hydraulic time step. +; Pattern Timestep is the interval between time periods in all time patterns. The default is 1 hour. +; Pattern Start is the time offset at which all patterns will start. E.g., a value of 6 hours would start the simulation with each pattern in the time period that corresponds to hour 6. the default is 0. +; Report Timestep sets the time interval between which output results are reported. The default is 1 hour. +; Report Start is the length of time into the simulation at which output results begin to be reported. The default is 0. +; Start Clocktime is the time of day at which the simulation begins. The default is 12:00 AM. +; Statistic determines what kind of statistical post-processing should be done on the time series of simulation results. Options include AVERAGED, MINIMUM, MAXIMUM, RANGE, and NONE. NONE reports the full time +; series for all quantities for all nodes and links and is the default. +;Units can be SEC, MIN, HOURS, or DAYS. The default is hours. If units are not supplied, then time values can be entered as decimal hours or in hours:minuts format. + Duration 27:05 + Hydraulic Timestep 1:05 + Quality Timestep 1:05 + Rule Timestep 1:05 + Pattern Timestep 2:10 + Pattern Start 0:00:00:00 + Report Timestep 1:05 + Report Start 0:00 + Start ClockTime 12 am + Statistic NONE + + + + + + +[REPORT] +;Format: +; PAGESIZE value +; FILE filename +; STATUS YES/NO/FULL +; SUMMARY YES/NO +; ENERGY YES/NO +; NODES NONE/ALL/node1 node2 ... +; LINKS NONE/ALL/link1 link2 ... +; parameter YES/NO +; parameter BELOW/ABOVE/PRECISION value +;where: +; Pagesize sets the number of lines written per page of the output report. The default is 0, meaning that no line limit per page is in effect. +; File supplies the name of a file to which the output report will be written +; Status determines whether a hydraulic status report should be generated. Full will also include information from each trial. +; Summary determines wheter a summary table is generated +; Energy determines if a table reporting average energy usage and cost for each pump is provided. +; Nodes identifies which nodes will be reported on +; Links identifies which links will be reported on +; The parameter option is used to identify which quantities are reported on, how many decimal places are displayed, and what kind of filtering should be used to limit output. +; Node parameters that can be reported: elevation, demand, head, pressure, quality. +; Link parameters that can be reported: Length, diameter, flow, velocity, headloss, position, setting, reaction, f-factor + Status Yes + Summary No + Energy No + + + + + +[OPTIONS] +;UNITS CFS/GPM/MGD/IMGD/AFD/LPS/LPM/MLD/CMH/CMD +;HEADLOSS H-W/D-W/C-M +;HYDRAULICS USE/SAVE filename +;QUALITY NONE/CHEMICAL/AGE/TRACE id +;VISCOSITY value +;DIFFUSIVITY value +;SPECIFIC GRAVITY value +;TRIALS value +;ACCURACY value +;UNBALANCED STOP/CONTINUE/CONTINUE n +;PATTERN id +;DEMAND MULTIPLIER value +;EMITTER EXPONENT value +;TOLERANCE value +;MAP filename +; +;UNITS: units of flow rate. For CFS, GPM, MGD, IMGD, and AFD, other input quantities must be in US Customary Units. If flow units are in liters or cubic meters, then Metric Units must be used for all other input quantities. +;HEADLOSS: Method for calculating head loss for flow through a pipe +;HYDRAULICS: either saves the current hydraulics solution or uses a previously saved hydraulics solution. +;QUALITY: Type of water quality analysis to perform. In place of CHEMICAL, the actual name of the chemical can be used followed by it's concentration units. If TRACE is used, it must be followed by the ID of the node being traced. +;VISCOSITY: The kinematic viscosity of the fluid being modeled relative to that of water at 20 ded C. +;DIFFUSIVITY: Molecular diffusivity of the chemical being analyzed relative to that of chlorine in water. It is only used when mass transfer limitations are considered in pipe wall reactions. A value of 0 will cause +; EPANET to ignore mass transfer limitations +;SPECIFIC GRAVITY: The ratio of the density of the fluid being modeled to that of water at 4 deg C +;TRIALS: The maximum number of trials used to solve the network hydraulics at each hydraulic time step of a simulation +;Accuracy: Convergence criteria for hydraulic solution. The sum of all flow changes from the previous solution divided by the total flow in all links +;UNBALANCED: Specifies what to do if a hydraulic solution cannot be reached in the specified number of trials. +;PATTERN: Default demand pattern for all junctions where no demand pattern was specified. +;DEMAND MULTIPLIER: Used to adjust the values of baseline demands for all junctions and all demand categories. +;EMITTER EXPONENT: Specifies the power to which the pressure at a junction is raised when computing the flow issuing from an emitter. +;MAP: Used to supply the name of a file containing coordinates of the network's nodes so that a map of the network can be drawn. +;TOLERANCE: The difference in water quality level below which one can say that one parcel of water is essentially the same as another. + Units CMH + Headloss H-W + Quality None + Specific Gravity 1.0 + Viscosity 1.0 + Trials 50 + Accuracy 0.0000001 + Unbalanced Stop + Pattern pattern1 + Demand Multiplier 1.0 + Tolerance 0.01 + + + + + + +[COORDINATES] +;Not used in simulation (only for producing a map) +;Coordinates are distances from arbitrary origin at lower left of map +;Possible formats: +; NodeID X-Coordinate Y-Coordinate +;Node X-Coord Y-Coord + reservoir1 0.00 0.00 + junction1 25.00 0.00 + junction2 50.00 0.00 + + + + + + +[VERTICES] +;Not used in simulation +;Assigns interior vertex points to network links - allows links to be drawn as polylines instead of simple straight-lines between their end nodes +;Format: +; LinkID x-coordinate y-coordinate + + + + + + +[LABELS] +;Not used in simulation +;Assigns coordinates to map labels +;Format (One line for each label): +; x-coordinate y-coordinate "LabelText" AnchorNodeIDLabel +;Coordinates refer to upper left corner of label +;Optional anchor node anchors the label to the node when the map is re-scaled during zooming + + + + + + +[BACKDROP] + + + + + + +[END] diff --git a/wntr/tests/networks_for_testing/bad_values.inp b/wntr/tests/networks_for_testing/bad_values.inp new file mode 100644 index 000000000..3fb685045 --- /dev/null +++ b/wntr/tests/networks_for_testing/bad_values.inp @@ -0,0 +1,460 @@ +[TITLE] + Small Test Network 1 +A very simple network with a reservoir and two nodes all at the same elevation + + + + + + +[JUNCTIONS] +;Format (One line for each junction): +; JunctionID Elevation BaseDemandFlow DemandPatternID +;If no demand pattern is specified, then the junction demand follows the default pattern specified in the OPTIONS section or Pattern 1 if no default pattern is specified. +;ID Elev Demand Pattern + junction1 0 3600 pattern1 ; + junction2 0 3600 pattern2 ; + + + + + + +[RESERVOIRS] +;Format: +; ReservoirID Head HeadPatternID +; head is the hydraulic head (elevation+pressure) +;ID Head Pattern + reservoir1 1000 pattern1 ; + + + + + + +[TANKS] +;Format: +; TankID BottomElevation InitalWaterLevel MinimumWaterLevel MaximumWaterLevel NominalDiameter MinimumVolume VolumeCurveID +;where +; VolumeCurveID if for volume vs. water depth curves for non-cylindrical tanks +; If a volume curve is specified, the diameter can be any non-zero number +; MinimumVolume is the tank volume at minimum water level. It can be zero for a cylindrical tank or if a volume curve is supplied +;ID Elevation InitLevel MinLevel MaxLevel Diameter MinVol VolCurve + + + + + + +[PIPES] +;Format (one line for each pipe): +; PipeID StartNodeID EndNodeID Length Diameter RoughnessCoefficient MinorLossCoefficient Status +;Status can be OPEN, CLOSED, or CV (pipe contains check valve) +;If minor loss coefficient is 0 and the pipe is open, then the last two columns can be left out. +;ID Node1 Node2 Length Diameter Roughness MinorLoss Status + pipe1 reservoir1 junction1 1000 457.2 100 0 Open ; + pipe2 node1 junction2 1000 457.2 100 0 Open ; + + + + + + +[PUMPS] +;Format (one line for each pump): +; PumpID StartNodeID EndNodeID KeywordAndValue +;Keywords: +; POWER - power for constant energy pump +; HEAD - ID of curve that describes head vs flow for the pump +; SPEED - relative speed setting (normal speed is 1.0, 0 means pump is off) +; PATTERN - ID of time pattern that describes how speed setting varies with time +;Keyword/Value pairs can be repeated +;Either POWER or HEAD must be supplied for each pump. Other keywords are optional + + + + + + +[VALVES] +;Format (one line for each valve): +; ValveID StartNodeID EndNodeID Diameter ValveType ValveSetting MinorLossCoefficient +;Valve Types and Settings: +; Type Setting +; PRV (pressure reducing valve) Pressure +; PSV (pressure sustaining valve) Pressure +; PBV (pressure breaker valve) Pressure +; FCV (flow control valve) Flow +; TCV (throttle control valve) Loss Coefficient +; GPV (general purpose valve) ID of head loss curve +;Note: Shutoff valves and check valves are considered to be a part of a pipe, not a separate control valve component + + + + + + +[TAGS] +;Not used in hydraulic or water quality simulation +;Associates category labels with specific nodes and links +;Format (one line for each node and link with a tag): +; NODE/LINK NodeID/LinkID TagLabelTextNoSpaces + + + + + + +[DEMANDS] +;Supplement to JUNCTIONS section for defining multiple water demands at junction nodes +;Format (one line for each category of demand at a junction): +; JunctionID BaseDemand DemandPatternID ;DemandCategoryName + + + + + + +[STATUS] +;Defines the initial status of selected links at the start of a simulation +;Format (one line per link being controlled): +; LinkID Status/Setting +;Links not listed default to OPEN or ACTIVE + + + + + + +[PATTERNS] +;Format: +; PatternID Multiplier1 Multiplier2 ... +;All patterns share the same time period interval, but they can have different numbers of time periods. +;Patterns wrap around back to first multiplier +;Multiple lines can be used for a single pattern +;ID Multipliers +;Demand Pattern + pattern1 1.0 1.0 1.0 1.0 + pattern2 0.5 1.0 1.5 1.0 + + + + + + +[CURVES] +;One line needed for each x,y point on each curve +;Points must be entered in order of increasing x-values +;Format: +; CurveID x-value y-value +;Place a comment with the curve type followed by a colon and then a description on line directly above first point for that curve: +; PUMP: Head vs flow curve for pump xxx +;Curve types are: PUMP, EFFICIENCY, VOLUME, AND HEADLOSS + + + + + + +[CONTROLS] +;Possible formats (Note: all caps words are keywords that should not be changed): +; LINK linkID status IF NODE nodeID ABOVE/BELOW value +; LINK linkID status AT TIME time +; LINK linkID status AT CLOCKTIME clocktime AM/PM +;where: +; linkID = a link ID label +; status = OPEN or CLOSED, a pump speed setting, or a control valve setting +; nodeID = a node ID label +; value = a pressure for a junction or a water level for a tank +; time = a time since the start of the simulation in decimal hours or in hours:minutes format +; clocktime = a 24-hour clock time (hours:minutes) + + + + + + +[RULES] +;Format (each rule is a series of statements): +; RULE RuleID +; IF condition_1 +; AND condition_2 +; OR condition_3 +; AND condition_4 +; etc. +; THEN action_1 +; AND action_2 +; etc. +; ELSE action_3 +; AND action_4 +; etc. +; PRIORITY value +; See epanet manual for more details + + + + + + +[ENERGY] +;Defines parameters used to compute pumping energy and cost +;Possible formats (Note: all caps words are keywords that should not be changed): +; GLOBAL PRICE/PATTERN/EFFIC value +; PUMP PumpID PRICE/PATTERN/EFFIC value +; DEMAND CHARGE value +;PUMP overrides GLOBAL for specified pump +;Price: average cost per kWh +;Pattern: pattern ID describing how energy price varies with time +;Efficiency: single percent efficiency for GLOBAL or efficiency curve ID for PUMP +;DEMAND CHARGE: ? +;Default global pump efficiency = 75 +;Default global energy price = 0 + + + + + + +[EMITTERS] +;Defines junctions modeled as emitters (sprinklers or orifices) +;Format: +; JunctionID FlowCoefficient + + + + + + +[QUALITY] +;Define inital water quality at nodes +;Format (one line per node): +; NodeID InitialQuality +;Quality is 0 for nodes not listed +;Quality represents concentration for chemicals, hours for water age, or percent for source tracing + + + + + + +[SOURCES] +;Defines locations of water quality sources +;Format (one line for each source): +; NodeID SourceType BaselineSourceStrength PatternID +;where +; SourceType can be CONCEN, MASS, FLOWPACED, OR SETPOINT +; BaselineSourceStrength is the water quality (e.g., concentration of the source or flow rate of the source) +; MASS type sources measure strength in mass flow per minute. All other source types measure strength in concentration. +; CONCEN type sources represent the concentration of any external source inflow to the node +; A MASS, FLOWPACED, OR SETPOINT type source represents a booster source, where the substance is injected directly into the network regardless of what the demand at the node is. See epanet manual for more details +;Node Type Quality Pattern + + + + + + +[REACTIONS] +;Format: +; ORDER BULK/WALL/TANK value +; GLOBAL BULK/WALL value +; BULK/WALL/TANK PipeID value +; LIMITING POTENTIAL value +; ROUGHNESS CORRELATION value +;where: +; Order is the order of the reaction. Values for wall reactions must be either 0 or 1. Default is 1.0. +; Global is used to set a global value for all bulk reaction coefficients (pipes and tanks) or for all pipe wall coefficients. Default value is 0. +; Bulk, wall, and tank are used to override the global reaction coefficients for specific pipes and tanks +; Limiting potential specifies that reaction rates are proportional to the difference between the current concentration and some limiting potential value. +; Roughness correlation will make all default pipe wall reaction coefficients be related to pipe roughness (see epanet manual page 159 for details). +;Remember: positive coefficients for products and negative coefficients for reactants + + + + + + +[MIXING] +;Identify the model that governs mixing in storage tanks +;Format (one line per tank): +; TankID MixingModel CompartmentVolumeFraction +;Possible mixing models: +; MIXED: completely mixed +; 2COMP: two-compartment mixing +; FIFO: plug flow +; LIFO: stacked plug flow +;CompartmentVolumeFraction only applies to 2COMP and represents the fraction of the total tank volume devoted to the inlet/outlet compartment +;Default is MIXED + + + + + + +[TIMES] +;Format: +; Duration value +; Hydraulic Timestep value +; Quality Timestep value +; Rule Timestep value +; Pattern Timestep value +; Pattern Start value +; Report Timestep value +; Report Start value +; Start ClockTime value +; Statistic value +;where: +; Duration is the simulation duration +; Hydraulic Timestep determines how often a new hydraulic state of the network is computed. Default is 1 hour +; Quality Timestep is the time step used to track changes in water quality. The default is 1/10 of the hydraulic time step +; Rule Timestep is the time step used to check for changes in system status due to activation of rule-based controls between hydraulic time steps. The default is 1/10 of the hydraulic time step. +; Pattern Timestep is the interval between time periods in all time patterns. The default is 1 hour. +; Pattern Start is the time offset at which all patterns will start. E.g., a value of 6 hours would start the simulation with each pattern in the time period that corresponds to hour 6. the default is 0. +; Report Timestep sets the time interval between which output results are reported. The default is 1 hour. +; Report Start is the length of time into the simulation at which output results begin to be reported. The default is 0. +; Start Clocktime is the time of day at which the simulation begins. The default is 12:00 AM. +; Statistic determines what kind of statistical post-processing should be done on the time series of simulation results. Options include AVERAGED, MINIMUM, MAXIMUM, RANGE, and NONE. NONE reports the full time +; series for all quantities for all nodes and links and is the default. +;Units can be SEC, MIN, HOURS, or DAYS. The default is hours. If units are not supplied, then time values can be entered as decimal hours or in hours:minuts format. + Duration 27:05 + Hydraulic Timestep 1:05 + Quality Timestep 1:05 + Rule Timestep 1:05 + Pattern Timestep 2:10 + Pattern Start 0:00 + Report Timestep 1:05 + Report Start 0:00 + Start ClockTime 12 am + Statistic NONE + + + + + +[REPORT] +;Format: +; PAGESIZE value +; FILE filename +; STATUS YES/NO/FULL +; SUMMARY YES/NO +; ENERGY YES/NO +; NODES NONE/ALL/node1 node2 ... +; LINKS NONE/ALL/link1 link2 ... +; parameter YES/NO +; parameter BELOW/ABOVE/PRECISION value +;where: +; Pagesize sets the number of lines written per page of the output report. The default is 0, meaning that no line limit per page is in effect. +; File supplies the name of a file to which the output report will be written +; Status determines whether a hydraulic status report should be generated. Full will also include information from each trial. +; Summary determines wheter a summary table is generated +; Energy determines if a table reporting average energy usage and cost for each pump is provided. +; Nodes identifies which nodes will be reported on +; Links identifies which links will be reported on +; The parameter option is used to identify which quantities are reported on, how many decimal places are displayed, and what kind of filtering should be used to limit output. +; Node parameters that can be reported: elevation, demand, head, pressure, quality. +; Link parameters that can be reported: Length, diameter, flow, velocity, headloss, position, setting, reaction, f-factor + Status Yes + Summary No + Energy No + + + + + +[OPTIONS] +;UNITS CFS/GPM/MGD/IMGD/AFD/LPS/LPM/MLD/CMH/CMD +;HEADLOSS H-W/D-W/C-M +;HYDRAULICS USE/SAVE filename +;QUALITY NONE/CHEMICAL/AGE/TRACE id +;VISCOSITY value +;DIFFUSIVITY value +;SPECIFIC GRAVITY value +;TRIALS value +;ACCURACY value +;UNBALANCED STOP/CONTINUE/CONTINUE n +;PATTERN id +;DEMAND MULTIPLIER value +;EMITTER EXPONENT value +;TOLERANCE value +;MAP filename +; +;UNITS: units of flow rate. For CFS, GPM, MGD, IMGD, and AFD, other input quantities must be in US Customary Units. If flow units are in liters or cubic meters, then Metric Units must be used for all other input quantities. +;HEADLOSS: Method for calculating head loss for flow through a pipe +;HYDRAULICS: either saves the current hydraulics solution or uses a previously saved hydraulics solution. +;QUALITY: Type of water quality analysis to perform. In place of CHEMICAL, the actual name of the chemical can be used followed by it's concentration units. If TRACE is used, it must be followed by the ID of the node being traced. +;VISCOSITY: The kinematic viscosity of the fluid being modeled relative to that of water at 20 ded C. +;DIFFUSIVITY: Molecular diffusivity of the chemical being analyzed relative to that of chlorine in water. It is only used when mass transfer limitations are considered in pipe wall reactions. A value of 0 will cause +; EPANET to ignore mass transfer limitations +;SPECIFIC GRAVITY: The ratio of the density of the fluid being modeled to that of water at 4 deg C +;TRIALS: The maximum number of trials used to solve the network hydraulics at each hydraulic time step of a simulation +;Accuracy: Convergence criteria for hydraulic solution. The sum of all flow changes from the previous solution divided by the total flow in all links +;UNBALANCED: Specifies what to do if a hydraulic solution cannot be reached in the specified number of trials. +;PATTERN: Default demand pattern for all junctions where no demand pattern was specified. +;DEMAND MULTIPLIER: Used to adjust the values of baseline demands for all junctions and all demand categories. +;EMITTER EXPONENT: Specifies the power to which the pressure at a junction is raised when computing the flow issuing from an emitter. +;MAP: Used to supply the name of a file containing coordinates of the network's nodes so that a map of the network can be drawn. +;TOLERANCE: The difference in water quality level below which one can say that one parcel of water is essentially the same as another. + Units CMH + Headloss H-W + Quality None + Specific Gravity 1.0 + Viscosity 1.0 + Trials 50 + Accuracy 0.0000001 + Unbalanced Stop + Pattern pattern1 + Demand Multiplier 1.0 + Tolerance 0.01 + + + + + + +[COORDINATES] +;Not used in simulation (only for producing a map) +;Coordinates are distances from arbitrary origin at lower left of map +;Possible formats: +; NodeID X-Coordinate Y-Coordinate +;Node X-Coord Y-Coord + reservoir1 0.00 0.00 + junction1 25.00 0.00 + junction2 50.00 0.00 + + + + + + +[VERTICES] +;Not used in simulation +;Assigns interior vertex points to network links - allows links to be drawn as polylines instead of simple straight-lines between their end nodes +;Format: +; LinkID x-coordinate y-coordinate + + + + + + +[LABELS] +;Not used in simulation +;Assigns coordinates to map labels +;Format (One line for each label): +; x-coordinate y-coordinate "LabelText" AnchorNodeIDLabel +;Coordinates refer to upper left corner of label +;Optional anchor node anchors the label to the node when the map is re-scaled during zooming + + + + + + +[BACKDROP] + + + + + + +[END] diff --git a/wntr/tests/test_epanet_exceptions.py b/wntr/tests/test_epanet_exceptions.py index 5dd3b20c3..7e6b27d1d 100644 --- a/wntr/tests/test_epanet_exceptions.py +++ b/wntr/tests/test_epanet_exceptions.py @@ -4,7 +4,7 @@ import wntr.epanet.exceptions testdir = dirname(abspath(__file__)) -datadir = join(testdir, "..", "..", "examples", "networks") +datadir = join(testdir, "networks_for_testing") class TestEpanetExceptions(unittest.TestCase): @@ -41,7 +41,35 @@ def test_epanet_value_error(self): except ValueError as e: self.assertTupleEqual(e.args, ('(Error 213) invalid option value 4.23e+30',)) + def test_broken_time_string(self): + f = wntr.epanet.io.InpFile() + inp_file = join(datadir, "bad_times.inp") + try: + f.read(inp_file) + except Exception as e: + self.assertIsInstance(e.__cause__, wntr.epanet.exceptions.ENValueError) + else: + self.fail('Failed to catch expected errors in bad_times.inp') + def test_broken_syntax(self): + f = wntr.epanet.io.InpFile() + inp_file = join(datadir, "bad_syntax.inp") + try: + f.read(inp_file) + except Exception as e: + self.assertIsInstance(e, wntr.epanet.exceptions.ENSyntaxError) + else: + self.fail('Failed to catch expected errors in bad_syntax.inp') + + def test_bad_values(self): + f = wntr.epanet.io.InpFile() + inp_file = join(datadir, "bad_values.inp") + try: + f.read(inp_file) + except Exception as e: + self.assertIsInstance(e.__cause__, wntr.epanet.exceptions.ENKeyError) + else: + self.fail('Failed to catch expected errors in bad_values.inp') if __name__ == "__main__": unittest.main() From db2a2d64009d37fdf070773835a793692c44e4a9 Mon Sep 17 00:00:00 2001 From: David Hart Date: Mon, 11 Mar 2024 10:20:14 -0600 Subject: [PATCH 14/15] Get rid of spurrious documentation. Ready for PR --- documentation/errors.rst | 167 -------------------------------------- wntr/epanet/exceptions.py | 22 ++--- 2 files changed, 12 insertions(+), 177 deletions(-) delete mode 100644 documentation/errors.rst diff --git a/documentation/errors.rst b/documentation/errors.rst deleted file mode 100644 index bd945d6f7..000000000 --- a/documentation/errors.rst +++ /dev/null @@ -1,167 +0,0 @@ -.. raw:: latex - - \clearpage - -.. _epanet-errors: - -Errors and debugging -==================== - -WNTR extends several of the standard python exceptions, :class:`KeyError`, :class:`SyntaxError`, -and :class:`ValueError` with EPANET toolkit specific versions, -:class:`~wntr.epanet.exceptions.ENKeyError`, -:class:`~wntr.epanet.exceptions.ENSyntaxError`, -and :class:`~wntr.epanet.exceptions.ENValueError`, -and a base :class:`~wntr.epanet.exceptions.EpanetException`. -These exceptions are raised when errors occur during INP-file reading/writing, -when using the EPANET toolkit functions, and when running the -:class:`~wntr.sim.epanet.EpanetSimulator`. - -In addition to the normal information that a similar python exception would provide, -these exceptions return the EPANET error code number and the error description -from the EPANET source code. WNTR also tries to intuit the specific variable, -line number (of an input file), and timestamp to give the user the most information -possible. Tables :numref:`table-epanet-warnings` through :numref:`table-epanet-errors-filesystem` -provide the description of the various warnings and error codes defined in [Ross00]_. - - -.. _table-epanet-warnings: -.. table:: EPANET warnings - - =========== ============================================================================================================================================================================== - *Err No.* *Description* - ----------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - **1-6** **Simulation warnings** - ----------- ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - 1 At `{time}`, system hydraulically unbalanced - convergence to a hydraulic solution was not achieved in the allowed number of trials - 2 At `{time}`, system may be hydraulically unstable - hydraulic convergence was only achieved after the status of all links was held fixed - 3 At `{time}`, system disconnected - one or more nodes with positive demands were disconnected for all supply sources - 4 At `{time}`, pumps cannot deliver enough flow or head - one or more pumps were forced to either shut down (due to insufficient head) or operate beyond the maximum rated flow - 5 At `{time}`, valves cannot deliver enough flow - one or more flow control valves could not deliver the required flow even when fully open - 6 At `{time}`, system has negative pressures - negative pressures occurred at one or more junctions with positive demand - =========== ============================================================================================================================================================================== - -.. _table-epanet-errors-runtime: -.. table:: EPANET runtime errors - - =========== ================================================================= - *Err No.* *Description* - ----------- ----------------------------------------------------------------- - **101-120** **Runtime and simulation errors** - ----------- ----------------------------------------------------------------- - 101 insufficient memory available - 102 no network data available - 103 hydraulics not initialized - 104 no hydraulics for water quality analysis - 105 water quality not initialized - 106 no results saved to report on - 107 hydraulics supplied from external file - 108 cannot use external file while hydraulics solver is active - 109 cannot change time parameter when solver is active - 110 cannot solve network hydraulic equations - 120 cannot solve water quality transport equations - =========== ================================================================= - - -.. _table-epanet-errors-network: -.. table:: EPANET network errors - - =========== ================================================================= - *Err No.* *Description* - ----------- ----------------------------------------------------------------- - **200-201** **Input file errors (exclusively for input files)** - ----------- ----------------------------------------------------------------- - 200 one or more errors in input file - 201 syntax error - ----------- ----------------------------------------------------------------- - **202-222** **Input file and toolkit errors** - ----------- ----------------------------------------------------------------- - 202 illegal numeric value - 203 undefined node - 204 undefined link - 205 undefined time pattern - 206 undefined curve - 207 attempt to control a CV/GPV link - 208 illegal PDA pressure limits - 209 illegal node property value - 211 illegal link property value - 212 undefined trace node - 213 invalid option value - 214 too many characters in input line - 215 duplicate ID label - 216 reference to undefined pump - 217 pump has no head curve or power defined - 218 `note: error number 218 is undefined in EPANET 2.2` - 219 illegal valve connection to tank node - 220 illegal valve connection to another valve - 221 misplaced rule clause in rule-based control - 222 link assigned same start and end nodes - ----------- ----------------------------------------------------------------- - **223-234** **Network consistency errors (INP-file and/or toolkit)** - ----------- ----------------------------------------------------------------- - 223 not enough nodes in network - 224 no tanks or reservoirs in network - 225 invalid lower/upper levels for tank - 226 no head curve or power rating for pump - 227 invalid head curve for pump - 230 nonincreasing x-values for curve - 233 network has unconnected node - 234 network has an unconnected node with ID `id` - ----------- ----------------------------------------------------------------- - **240-263** **Toolkit-only errors** - ----------- ----------------------------------------------------------------- - 240 nonexistent water quality source - 241 nonexistent control - 250 invalid format (e.g. too long an ID name) - 251 invalid parameter code - 252 invalid ID name - 253 nonexistent demand category - 254 node with no coordinates - 255 invalid link vertex - 257 nonexistent rule - 258 nonexistent rule clause - 259 attempt to delete a node that still has links connected to it - 260 attempt to delete node assigned as a Trace Node - 261 attempt to delete a node or link contained in a control - 262 attempt to modify network structure while a solver is open - 263 node is not a tank - =========== ================================================================= - - -.. _table-epanet-errors-filesystem: -.. table:: EPANET file/system errors - - =========== ================================================================= - *Err No.* *Description* - ----------- ----------------------------------------------------------------- - **301-305** **Filename errors** - ----------- ----------------------------------------------------------------- - 301 identical file names used for different types of files - 302 cannot open input file - 303 cannot open report file - 304 cannot open binary output file - 305 cannot open hydraulics file - ----------- ----------------------------------------------------------------- - **306-307** **File structure errors** - ----------- ----------------------------------------------------------------- - 306 hydraulics file does not match network data - 307 cannot read hydraulics file - ----------- ----------------------------------------------------------------- - **308-309** **Filesystem errors** - ----------- ----------------------------------------------------------------- - 308 cannot save results to binary file - 309 cannot save results to report file - =========== ================================================================= - - -For developers --------------- - -The custom exceptions for EPANET that are included in the :class:`wntr.epanet.exceptions` -module subclass both the :class:`~wntr.epanet.exceptions.EpanetException` -and the standard python exception they are named after. This means that when handling -exceptions, a try-catch block that is looking for a :class:`KeyError`, for example, -will still catch an :class:`~wntr.epanet.exceptions.ENKeyError`. The newest versions -of Python, e.g., 3.11, have a new style of multiple inheritence for Exceptions, called -exception groups, but this has not yet been used in WNTR because older versions of -Python are still supported at this time. diff --git a/wntr/epanet/exceptions.py b/wntr/epanet/exceptions.py index 6442a0706..1f742def9 100644 --- a/wntr/epanet/exceptions.py +++ b/wntr/epanet/exceptions.py @@ -82,7 +82,6 @@ 309: "cannot save results to report file %s", } """A dictionary of the error codes and their meanings from the EPANET toolkit. -Please see :doc:`/errors` for descriptions of these values. :meta hide-value: """ @@ -98,27 +97,28 @@ def __init__(self, code: int, *args: List[object], line_num=None, line=None) -> code : int The EPANET error code args : additional non-keyword arguments, optional - If there is a string-format within the error code's text, these will be used to + If there is a string-format within the error code's text, these will be used to replace the format, otherwise they will be output at the end of the Exception message. line_num : int, optional The line number, if reading an INP file, by default None line : str, optional The contents of the line, by default None """ - msg = EN_ERROR_CODES.get(code, 'unknown error') + msg = EN_ERROR_CODES.get(code, "unknown error") if args is not None: args = [*args] - if r'%' in msg and len(args) > 0: + if r"%" in msg and len(args) > 0: msg = msg % repr(args.pop(0)) if len(args) > 0: - msg = msg + ' ' + repr(args) + msg = msg + " " + repr(args) if line_num: msg = msg + ", at line {}".format(line_num) if line: - msg = msg + ':\n ' + str(line) - msg = '(Error {}) '.format(code) + msg + msg = msg + ":\n " + str(line) + msg = "(Error {}) ".format(code) + msg super().__init__(msg) + class ENSyntaxError(EpanetException, SyntaxError): def __init__(self, code, *args, line_num=None, line=None) -> None: """An EPANET exception class that also subclasses SyntaxError @@ -128,7 +128,7 @@ def __init__(self, code, *args, line_num=None, line=None) -> None: code : int The EPANET error code args : additional non-keyword arguments, optional - If there is a string-format within the error code's text, these will be used to + If there is a string-format within the error code's text, these will be used to replace the format, otherwise they will be output at the end of the Exception message. line_num : int, optional The line number, if reading an INP file, by default None @@ -137,6 +137,7 @@ def __init__(self, code, *args, line_num=None, line=None) -> None: """ super().__init__(code, *args, line_num=line_num, line=line) + class ENKeyError(EpanetException, KeyError): def __init__(self, code, name, *args, line_num=None, line=None) -> None: """An EPANET exception class that also subclasses KeyError. @@ -148,7 +149,7 @@ def __init__(self, code, name, *args, line_num=None, line=None) -> None: name : str The key/name/id that is missing args : additional non-keyword arguments, optional - If there is a string-format within the error code's text, these will be used to + If there is a string-format within the error code's text, these will be used to replace the format, otherwise they will be output at the end of the Exception message. line_num : int, optional The line number, if reading an INP file, by default None @@ -158,6 +159,7 @@ def __init__(self, code, name, *args, line_num=None, line=None) -> None: super().__init__(code, name, *args, line_num=line_num, line=line) + class ENValueError(EpanetException, ValueError): def __init__(self, code, value, *args, line_num=None, line=None) -> None: """An EPANET exception class that also subclasses ValueError @@ -169,7 +171,7 @@ def __init__(self, code, value, *args, line_num=None, line=None) -> None: value : Any The value that is invalid args : additional non-keyword arguments, optional - If there is a string-format within the error code's text, these will be used to + If there is a string-format within the error code's text, these will be used to replace the format, otherwise they will be output at the end of the Exception message. line_num : int, optional The line number, if reading an INP file, by default None From 806b04e89c0e119bd1e1707c65d449df1a484053 Mon Sep 17 00:00:00 2001 From: David Hart Date: Mon, 11 Mar 2024 10:21:44 -0600 Subject: [PATCH 15/15] Update userguide --- documentation/userguide.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/documentation/userguide.rst b/documentation/userguide.rst index fa4c7d302..6e31a2128 100644 --- a/documentation/userguide.rst +++ b/documentation/userguide.rst @@ -70,7 +70,6 @@ U.S. Department of Energy's National Nuclear Security Administration under contr graphics gis advancedsim - errors .. toctree:: :maxdepth: 1